diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 7fdd364f4a..5d37d441c8 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -824,6 +824,7 @@ The default `worktree init` still seeds eagerly. A lean worktree (created withou - `pnpm paperclipai worktree ensure-seeded` performs the deferred seed **exactly once**. It is lock-guarded and idempotent: only a complete `verified` manifest short-circuits it, so it is safe to call repeatedly and from concurrent processes. Managed workspaces derive the source from the control-plane-provided base project workspace when it carries its own `.paperclip/config.json`, and otherwise from the control plane's own registered instance config; either way the workspace's manifest never selects it. Manual worktrees must pass `--from-config`. - `paperclipai run` calls `ensureWorktreeSeeded` automatically before doctor/boot. Managed runs transparently seed a lean worktree from their registered base workspace; an unmanaged lean worktree must first run `worktree ensure-seeded --from-config `. - Managed Paperclip git worktrees default to the repository's `scripts/provision-worktree.sh` when the strategy omits `provisionCommand`, so the isolated config and pending manifest cannot be silently skipped. Runtime startup also runs `scripts/provision-worktree-runtime.sh` automatically when no explicit runtime provision command is configured and the manifest is not verified. Explicitly configured provision commands still take precedence. +- If the built-in provisioner reports an unavailable seed source config, decide whether the task needs a seeded development instance or only an isolated checkout. A seeded instance needs a canonical config from the registered base workspace or control-plane instance; environment-only server configuration does not provide that file. For a checkout-only worktree, explicitly set the `git_worktree` strategy's `provisionCommand` to `"true"`. This skips setup and does not establish runtime or seed readiness. Repair rejected symlinks or non-regular source files instead of treating those validation failures as a missing prerequisite. - The built-in deferred seed is recorded as its own terminal `workspace_seed` operation. A zero exit code is not enough for success: the operation succeeds only when `.paperclip/seed-manifest.json` contains complete verified evidence; failed, missing, or malformed manifests produce a failed operation with the seed phase in metadata. - Worktrees created before lazy seeding shipped may have neither marker. Paperclip adopts them only after their configured database proves a compatible migration journal and the core Paperclip schema; otherwise managed startup creates a pending manifest and performs the normal verified seed. Manual markerless worktrees must provide `--from-config` so the source remains explicit. diff --git a/scripts/__tests__/provision-worktree-self-heal.test.mjs b/scripts/__tests__/provision-worktree-self-heal.test.mjs index 943a15f48b..bff638cdc4 100644 --- a/scripts/__tests__/provision-worktree-self-heal.test.mjs +++ b/scripts/__tests__/provision-worktree-self-heal.test.mjs @@ -102,11 +102,12 @@ process.exit(0); return baseCwd; } -function runProvision(baseCwd, { pathPrefix, setupWorktree, existingWorktree } = {}) { +function runProvision(baseCwd, { pathPrefix, setupWorktree, setupInstance, existingWorktree } = {}) { const worktreeCwd = existingWorktree ?? makeTempDir("paperclip-provision-worktree-"); setupWorktree?.(worktreeCwd); const worktreesHome = makeTempDir("paperclip-provision-home-"); const paperclipHome = makeInstanceHome(); + setupInstance?.(paperclipHome); const result = spawnSync("bash", [script], { cwd: worktreeCwd, encoding: "utf8", @@ -182,15 +183,52 @@ test("uses the base CLI when its import graph boots", () => { ); }); +test("explains unavailable instance source config without creating target state", () => { + const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0 }); + const { result, worktreeCwd } = runProvision(baseCwd, { + setupInstance: (home) => fs.rmSync(path.join(home, "instances", "default", "config.json")), + }); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /config is unavailable \(control-plane instance\)/); + assert.match(result.stderr, /For a seeded development instance, configure a canonical config/); + assert.match(result.stderr, /Only for a checkout-only worktree/); + assert.match(result.stderr, /workspaceStrategy\.provisionCommand to "true"/); + assert.match(result.stderr, /does not prepare a development runtime/); + assert.equal(fs.existsSync(path.join(worktreeCwd, ".paperclip")), false); + assert.deepEqual(readCliInvocations(baseCwd), []); +}); + test("rejects a dangling base workspace config symlink instead of falling back", () => { const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0 }); fs.mkdirSync(path.join(baseCwd, ".paperclip"), { recursive: true }); fs.symlinkSync(path.join(baseCwd, "absent.json"), path.join(baseCwd, ".paperclip", "config.json")); - const { result } = runProvision(baseCwd); + const { result, worktreeCwd } = runProvision(baseCwd); assert.notEqual(result.status, 0); - assert.match(result.stderr, /is missing or is not a canonical file/); + assert.match(result.stderr, /is not a canonical file \(base project workspace\)/); + assert.doesNotMatch(result.stderr, /checkout-only|provisionCommand/); + assert.equal(fs.existsSync(path.join(worktreeCwd, ".paperclip")), false); + assert.deepEqual(readCliInvocations(baseCwd), []); +}); + +test("rejects a non-regular instance config without suggesting a setup bypass", () => { + const baseCwd = makeBaseWorkspace({ helpExit: 0, initExit: 0 }); + const { result, worktreeCwd } = runProvision(baseCwd, { + setupInstance: (home) => { + const configPath = path.join(home, "instances", "default", "config.json"); + fs.rmSync(configPath); + fs.mkdirSync(configPath); + }, + }); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /is not a canonical file \(control-plane instance\)/); + assert.match(result.stderr, /Repair the registered source path/); + assert.doesNotMatch(result.stderr, /checkout-only|provisionCommand/); + assert.equal(fs.existsSync(path.join(worktreeCwd, ".paperclip")), false); + assert.deepEqual(readCliInvocations(baseCwd), []); }); test("rejects a dangling base workspace .paperclip symlink instead of falling back", () => { diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index af7c61b1a8..2418fa3bb0 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -48,14 +48,23 @@ if [[ -L "$canonical_base_cwd/.paperclip" && ! -d "$canonical_base_cwd/.papercli exit 1 fi source_config_path="$canonical_base_cwd/.paperclip/config.json" +source_config_origin="base project workspace" if [[ ! -e "$source_config_path" && ! -L "$source_config_path" ]]; then # A base workspace that is a plain checkout carries no instance config of its own. # Fall back to the control plane's own registered instance config, which is process # state this workspace cannot rewrite. source_config_path="${PAPERCLIP_CONFIG:-$paperclip_home/instances/$paperclip_instance_id/config.json}" + source_config_origin="control-plane instance" fi if [[ ! -f "$source_config_path" || -L "$source_config_path" ]]; then - echo "Registered Paperclip seed source config is missing or is not a canonical file: $source_config_path" >&2 + if [[ ! -e "$source_config_path" && ! -L "$source_config_path" ]]; then + echo "Registered Paperclip seed source config is unavailable ($source_config_origin): $source_config_path" >&2 + echo "For a seeded development instance, configure a canonical config for that registered source before retrying." >&2 + echo 'Only for a checkout-only worktree, explicitly set workspaceStrategy.provisionCommand to "true". This skips setup; it does not prepare a development runtime.' >&2 + else + echo "Registered Paperclip seed source config is not a canonical file ($source_config_origin): $source_config_path" >&2 + echo "Repair the registered source path; symlinks and non-regular files are not accepted." >&2 + fi exit 1 fi canonical_source_dir="$(cd "$(dirname "$source_config_path")" && pwd -P)"