Clarify worktree seed source setup failures (#14795)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Managed worktrees can prepare an isolated Paperclip development
instance.
> - The built-in provisioner requires a canonical registered seed source
config.
> - A missing source currently has the same message as a rejected
symlink or non-regular file.
> - This pull request separates those messages and names the supported
setup choices.
> - Operators can choose the intended setup without changing the
source-validation guards.

## Linked Issues or Issue Description

**What happened?**

A plain repository checkout can select the control-plane instance as its
seed source. If that instance runs with environment-only configuration,
the source config file can be unavailable. The provisioner stops with a
message that also covers noncanonical files and gives no repair
guidance.

**Expected behavior**

The error should identify the selected source and distinguish an
unavailable prerequisite from a rejected file. It should explain that a
seeded development instance needs a canonical registered source. It
should describe the explicit no-op only for a checkout-only worktree.

**Steps to reproduce**

1. Use a plain base checkout with no repository-local config.
2. Leave the control-plane instance config file absent.
3. Run the built-in worktree provisioner against an isolated checkout.

**Paperclip version or commit**

Base commit `c8f874311c`.

**Deployment mode**

Managed local worktrees, including servers configured only through
environment variables.

Related: #11733 adds deeper source-readiness checks. #11735 changes
runtime and seed lifecycle handling. This change only improves the
existing shell guard's diagnostics.

## What Changed

- Distinguish unavailable source configs from symlinks and non-regular
files.
- Identify whether the selected source belongs to the base workspace or
control-plane instance.
- Explain seeded-instance prerequisites and the explicit checkout-only
setup choice.
- Verify failure still precedes target-state creation and CLI
invocation.
- Document the setup choice and its runtime-readiness limit.

## Verification

- `node --test scripts/__tests__/provision-worktree-self-heal.test.mjs`:
21 passed; one platform-gated test skipped because macOS lacks `flock`.
- `bash -n scripts/provision-worktree.sh` and `git diff --check`:
passed.
- `pnpm -r typecheck`: passed.
- `pnpm exec vitest run server/src/__tests__/ai-connections.test.ts`: 50
passed after running the installed PostgreSQL package's own symlink
hydration script in this worktree.
- `pnpm test:run`: attempted, then stopped after unrelated database
suites failed at startup. The offline install had omitted PostgreSQL
native library symlinks. The focused database rerun above verifies the
local repair; the complete suite is delegated to CI.
- `pnpm build`: passed.

- [Required PR
CI](https://github.com/paperclipai/paperclip/actions/runs/36797650741)
passed on `0a3ba63e12`: 50 successful checks and two intentional
Storybook skips. Greptile scored that exact commit 5/5; there are zero
unresolved review threads and no merge conflicts.

## Risks

- Diagnostics only. This does not supply a source config or repair an
existing blocked task.
- The failure predicates and exit status stay unchanged. Symlink and
non-regular-file errors do not recommend skipping setup.
- The checkout-only no-op requires an explicit policy choice. It does
not grant runtime or seed readiness.
- No schema, migration, tenant policy, deployment, or Sentry reporting
change.

## Model Used

OpenAI GPT-6-based Codex, with reasoning, shell tools, and code
execution. The exact serving model ID and context-window size are not
exposed by this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-09-30 18:35:33 -07:00
1 parent 4b9a6000f7
commit 0dc8d80eea
3 files changed
+52 -4

No files matched your search

@@ -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", () => {
+10 -1
View File
@@ -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)"