.gitspace/bundle.json version 1 and scripts under .gitspace/lifecycle. There is no second lifecycle manifest. For the human approval flow, see Workspace lifecycle.1234567891011121314151617181920212223242526272829{ "version": 1, "defaultProfile": "base", "profiles": { "base": { "checks": ["bun"], "secrets": [], "values": ["APP_MODE"], "notes": "Local development" }, "preview": { "checks": ["github"], "secrets": ["GH_TOKEN"], "values": ["PREVIEW_REPOSITORY"] } }, "checks": { "bun": { "kind": "built-in", "check": "bun" }, "github": { "kind": "command", "label": "GitHub CLI version", "command": "gh --version" } }, "values": { "APP_MODE": { "default": "development", "description": "Application mode" }, "PREVIEW_REPOSITORY": { "description": "Existing GitHub owner/repository to adopt" } } }
| Field | Rules |
version | Must be 1 |
defaultProfile | Existing profile name; defaults to base |
profiles | Must include base |
Profile checks, secrets, values | Arrays; default to empty. A selected profile adds to base rather than replacing it |
Profile notes | Optional text, up to 2,000 characters |
checks | Named built-in or command checks. Referenced check IDs must exist |
values | Named non-secret values with optional default and description. Referenced value names must exist |
bun, gh, git, postgres, vercel, and node. They run bun --version, gh --version, git --version, pg_isready, vercel whoami, and node --version, respectively. A check name is not an installer. Optional built-in requirement text does not enforce a version constraint by itself. Use a command check when you need an exact assertion.kind: "command", a label, and a command. Checks need execution approval too; read their commands before running them.1234567891011121314151617.gitspace/ bundle.json lifecycle/ cloud/ provision/ 10-adopt.preview.sh destroy/ 10-delete.preview.sh machine/ prepare/ 10-tools.sh workspace/ materialize/ 10-dependencies.sh 20-config.preview.sh dematerialize/ 10-flush.sh
.profile suffix, and .sh. Examples: 10-tools.sh, 20-config.preview.sh..base.sh scripts run for every profile. A script suffixed with another declared profile runs only when that profile is selected. Unknown profiles and malformed filenames reject. Scripts sort by filename, not by numeric value: use equal-width prefixes such as 10, 20, and 30. Selected-profile scripts add to base scripts; they do not override a base file.| Phase | Successful-run identity |
cloud/provision | Durable workspace identity. A prior success skips later requests unless the user explicitly requests a rerun, even if the profile or script hashes changed |
machine/prepare | Account, project, verified machine, profile, and approved script hashes |
workspace/materialize | Workspace checkout generation and phase. Profile or script changes do not repeat a completed phase without an explicit rerun |
workspace/dematerialize | Workspace checkout generation and phase. Profile or script changes do not repeat a completed phase without an explicit rerun |
cloud/destroy | Explicit destruction request for the durable workspace's recorded resources |
checks is a run phase for bundle checks, not a script directory. setup, select, remove, and pre are not executable lifecycle aliases.cloud/provision request, including for a local-only repository with no provisioning script. It enables the workspace's automatic local-preparation policy, then runs approved machine/prepare, checks, cloud/provision, and workspace/materialize in that order. A failure stops later phases. An empty provisioning phase still records successful local-only setup. Creating or selecting a workspace does not enable the policy.machine/prepare, checks, and workspace/materialize, without reprovisioning. Unapproved content waits for the human. A preparation failure leaves the workspace accessible; inspect the local phase results even when provisioning succeeded.cloud/destroy require a human browser action. The workspace agent cannot grant itself content approval or invoke destruction.cloud/destroy. If that runner does not hold the workspace checkout, it restores the latest durable checkpoint into an isolated directory and restores the canonical repository origin. The full checkout supplies repository helpers; the destroy entry script comes from the approved cloud execution record.machine/prepare and checks before destruction. It does not open or claim the workspace's original placement, run materialization, or provision replacement resources. Without a durable checkpoint, it reports the missing prerequisite and executes nothing. If the runner already holds the current checkout, it uses that checkout instead.| Variable | Value |
GITSPACE_PROJECT_ID | Durable project ID |
GITSPACE_WORKSPACE_ID | Durable workspace ID; use this for resource identity rather than a machine path |
GITSPACE_MACHINE_ID | Authorized runner machine ID |
GITSPACE_WORKSPACE_GENERATION | Current checkout generation |
GITSPACE_ENVIRONMENT_PROFILE | Selected profile name |
GITSPACE_MACHINE_TOOLS | Private, stable machine tool directory; install persistent tools here rather than temporary HOME |
GITSPACE_LIFECYCLE_BINDINGS contains a JSON object of durable, non-secret resource bindings. Parse it as JSON; do not source or evaluate it as shell code.GITSPACE_LIFECYCLE_OUTPUT is a file path supplied by the runner. Write JSON in this form:123456{ "bindings": { "previewRepository": "example/preview-workspace", "previewRepositoryId": "123456789" } }
secret:NAME references. They are not a credential store. A secret reference names a secret; it does not turn the binding into the secret's plaintext value. Store it as a project secret or grant an account secret to the project, then declare the required name in the profile.secret:NAME reference. Private keys and URLs with embedded credentials reject. These checks catch common mistakes, not every possible secret.HOME and a runner-controlled PATH, not the machine daemon's ambient credential environment. GITSPACE_MACHINE_TOOLS is a stable, private machine directory; its bin directory leads the lifecycle PATH. Have machine/prepare install persistent tools there, not into temporary HOME. The selected profile supplies the authorized secrets and values. Do not depend on the user's usual dotfiles or home-directory tool configuration..sh files execute their approved bytes with /bin/bash, without login startup files. Command checks use /bin/sh. The working directory is the checkout root; $0 is the script's path in that checkout. Use $0, not BASH_SOURCE, when locating helpers beside an entry script.eval. The space.environment namespace uses the same environment API as the browser:12const current = await space.environment.get(); const schema = await space.describe({ method: 'environment.runPhase' });
get, setProfile, putValue, deleteValue, runChecks, runPhase, and runLog. Workspace targets default to the current workspace where optional. Use the discovered workspaceId field for another workspace in the same project, not a session ID.rerun: true only for an explicitly authorized rerun. The agent surface excludes cloud/destroy. Do not use raw shell to bypass this restriction or a missing content approval.lifecycle, the cloud-owned record of profile, values, approvals, automatic policy, execution definitions and content, bindings, last successful provision, destruction time, and phase runs. Saved script content lets the human inspect what a closed workspace would execute; do not embed secrets in it. Run status is running, succeeded, failed, or abandoned. The run stores its machine, profile, hashes, timestamps, results, and output preview.environment.runLog request accepts spaceId, runId, and nullable offset. It returns output and nextOffset; a null next offset means the log has ended. The agent wrapper uses its discovered workspace-target schema. Read subsequent chunks rather than treating the preview as the full log..gitspace/bundle.json:123456{ "version": 1, "defaultProfile": "base", "profiles": { "base": { "checks": ["bun"] } }, "checks": { "bun": { "kind": "built-in", "check": "bun" } } }
.gitspace/lifecycle/workspace/materialize/10-dependencies.sh:123#!/usr/bin/env bash set -euo pipefail bun install --frozen-lockfile
machine/prepare script based on its existing CI installation method.preview profile that names GH_TOKEN in secrets and PREVIEW_REPOSITORY in values. Add the value definition under top-level values:123456789101112131415{ "version": 1, "defaultProfile": "preview", "profiles": { "base": { "checks": ["bun"] }, "preview": { "secrets": ["GH_TOKEN"], "values": ["PREVIEW_REPOSITORY"] } }, "checks": { "bun": { "kind": "built-in", "check": "bun" } }, "values": { "PREVIEW_REPOSITORY": { "description": "Existing GitHub owner/repository" } } }
PREVIEW_REPOSITORY to the existing owner/repository in Environment. Store a GitHub token with access to read that repository as the project secret GH_TOKEN. Do not paste the token into a value, binding, or script..gitspace/lifecycle/cloud/provision/10-adopt.preview.sh:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647#!/usr/bin/env bash set -euo pipefail bun - <<'JS' import { rename } from 'node:fs/promises'; const output = process.env.GITSPACE_LIFECYCLE_OUTPUT; const token = process.env.GH_TOKEN; if (!token || !output) throw new Error('Run through the approved lifecycle runner'); const previous = await Bun.file(output).json(); const bindings = { ...JSON.parse(process.env.GITSPACE_LIFECYCLE_BINDINGS || '{}'), ...previous.bindings, }; const configured = process.env.PREVIEW_REPOSITORY; const repository = bindings.previewRepository || configured; if (!repository || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) { throw new Error('Set PREVIEW_REPOSITORY to an existing owner/repository'); } if (bindings.previewRepository && configured && configured !== repository) { throw new Error('Configured repository differs from the recorded binding'); } const response = await fetch(`https://api.github.com/repos/${repository}`, { headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'gitspace-workspace-lifecycle', }, }); if (!response.ok) throw new Error(`Repository lookup failed: HTTP ${response.status}`); const resource = await response.json(); if (!Number.isSafeInteger(resource.id) || typeof resource.full_name !== 'string') { throw new Error('GitHub returned an invalid repository identity'); } if (bindings.previewRepositoryId && bindings.previewRepositoryId !== String(resource.id)) { throw new Error('Repository identity changed; inspect before recovery'); } const pendingOutput = `${output}.${crypto.randomUUID()}.tmp`; await Bun.write(pendingOutput, JSON.stringify({ bindings: { ...bindings, previewRepository: resource.full_name, previewRepositoryId: String(resource.id), }, })); await rename(pendingOutput, output); console.log(`Adopted repository ${resource.full_name} (${resource.id})`); JS
preview, approve the final script content, and separately authorize setup. Later materialization reads the saved bindings rather than adopting or creating resources again.cloud/destroy script here. Adoption records a resource reference, not permission to delete a shared resource. For a dedicated workspace-owned resource, write a separately reviewed destroy script that verifies the provider resource ID against the recorded binding and deletes only that resource. Never search by a broad name prefix and delete every match. Retirement requires explicit human authorization even when the destroy script already has content approval.