* test(#1856): failing-first contract for the orchestrator cwd-drift guard handoff The #48 guard correctly refuses to execute waves from an agent worktree, but the refusal is a dead end: that worktree can hold committed fixes AND uncommitted work, and "re-run from the orchestrator's own worktree" silently means abandoning them. The reporter was left choosing between continuing from a blocked worktree and losing the work. The guard is shell embedded in execute-phase.md, so these tests extract the block by a stable marker and EXECUTE it against real git fixtures — the shipped text is the runtime contract. Covers the stranded-commit and dirty-tree report, the integration commands, both agent- namespaces, commit-count boundaries 0/1/2, and the constraints the guard's own comment records: it must NOT fire on an ordinary branch, on 'agentic-refactor', or on a legitimate feature worktree under .claude/worktrees/, and must degrade cleanly with no resolvable base or a detached HEAD. RED expected: the marker does not exist, so extraction fails and every case errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso * fix(#1856): give the executor-worktree refusal a handoff instead of a dead end The #48 cwd-drift guard correctly refuses to execute waves from an agent worktree — its comment records why ("this is how a wrong-base merge nearly shipped ~1000 files"), and that refusal is untouched here. The defect is that it was a dead end. At the moment it fires, the worktree can hold committed product work, uncommitted product and planning changes, and the live gap-planning context. Telling the user to "re-run from the orchestrator's own worktree" silently means abandoning all of it, because the orchestrator worktree cannot see commits that live only on the agent branch. The reporter was left choosing between continuing from a blocked worktree and losing five commits plus uncommitted work. The refusal now reports what is actually stranded — the commit count and log against the resolved base, and the uncommitted files — followed by the concrete integration sequence (commit here, switch to an orchestrator-safe checkout, merge or cherry-pick, re-run) and a verify command. Nothing is claimed that is not there: a clean worktree with no commits ahead prints the plain refusal with no empty sections. Every added command is diagnostic and `|| true`-guarded, so a failure degrades to the original refusal rather than crashing before the message prints. Verified: an unresolvable base still refuses cleanly. Deliberately NOT done: auto-merging or auto-cherry-picking the agent branch. That is precisely the operation #48 exists to stop the orchestrator performing from a drifted cwd, at the moment it has least confidence about which tree is which. Reporting beats acting here. The guard block carries a `gsd:guard=orchestrator-cwd-drift` marker so the new contract test can extract and EXECUTE the shipped shell against real git fixtures rather than asserting on its characters. Fixes #1856 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso * fix(#1856): report true counts, add the changeset, and document the seam Review findings, all from the isolated adversarial pass: - The dirty-file list was capped at 20 with no indication, so a worktree with 27 uncommitted files reported 20 — under-informing the user about exactly what is stranded, which is the entire point of this change. Both lists now count BEFORE truncating and print "… and N more". Verified with 25 commits / 27 dirty files. - The has-commits condition was written out twice and could drift on a future edit. Collapsed to a single _WT_HAS_COMMITS flag. - The changeset fragment existed but was untracked, so it was in neither commit on this branch and the PR gate would have failed against real history. - CONTEXT.md:122 documents this exact seam ("the orchestrator runs a cwd-drift guard at execute_waves entry…") and was not extended. Now records the handoff report, that the refusal condition and exit code are unchanged, and that every added command is diagnostic and || true-guarded. Verified NOT a defect, correcting the review's premise: the guard block does break when its line endings are CRLF, but .gitattributes:2 is `* text=auto eol=lf`, which OVERRIDES core.autocrlf and forces LF on checkout on every platform including Windows — so the shipped file is LF there too, and the installer copies it through Node without translating endings. The reproduction (mine and the reviewer's) required injecting CRLF by hand. It is also not fixable from inside the script: a \r breaks the shell parse at the block's first line, before any #1856 code runs. Neither introduced nor amplified by this change. Also noted and left as-is by design: the review flagged that #1856's "offer an explicit recovery option" could be read as requiring an interactive/automated integration rather than printed instructions. Deliberate — see the commit that added the block: auto-merging is the exact operation #48 exists to prevent the orchestrator performing from a drifted cwd. Called out in the PR body for a maintainer decision rather than silently chosen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso * chore(#1856): backfill changeset PR number Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/plucky-wolves-dart.md
Normal file
5
.changeset/plucky-wolves-dart.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2727
|
||||
---
|
||||
**Refusing to run a phase from an executor worktree now tells you how to recover your work** — when GSD stopped because the session had drifted into an executor worktree, it only said to re-run from the orchestrator's worktree. If that worktree held commits or uncommitted changes, following that advice silently abandoned them. The refusal now lists the commits and files that exist only there, and gives the exact steps to integrate them before continuing. (#1856)
|
||||
@@ -122,7 +122,7 @@ Leaf module owning the **out-of-band** half of ADR-1411's "corrupt is not absent
|
||||
CJS Module owning worktree lifecycle safety policy for the GSD orchestration layer. Interface: `resolveWorktreeContext(cwd, deps) → WorktreeContext` (linked-worktree root mapping), `parseWorktreePorcelain(output) → WorktreeEntry[]` (porcelain parser, skips detached HEAD), `planWorktreePrune(repoRoot, opts, deps) → PrunePlan` (metadata-prune plan, never destructive by default), `executeWorktreePrunePlan(plan, deps) → PruneResult` (executes prune; degrades gracefully on git timeout), `listLinkedWorktreePaths(repoRoot, deps) → LinkedPathsResult`, `inspectWorktreeHealth(repoRoot, opts, deps) → HealthResult` (orphan + stale detection), `snapshotWorktreeInventory(repoRoot, opts, deps) → InventoryResult`, `planWorktreeWaveCleanup(repoRoot, manifest) → CleanupPlan` (manifest-scoped, fail-closed), `executeWorktreeWaveCleanupPlan(plan, deps) → CleanupResult`, `planWorktreeRecordAgent(manifestRaw, fields) → RecordAgentPlan` (write-strict per-agent manifest append; validates each field at write time via the same `normalizeCleanupManifestEntry` rules the reader enforces; fail-closed on a missing/garbled field or a duplicate `(worktree_path, branch)` the reader would dedup away), `cmdWorktreeRecordAgent(cwd, args, deps) → RecordAgentCmdResult` (thin deps-injectable IO wrapper for the `worktree record-agent` verb), `planWorktreeCreate(fields) → WorktreeCreatePlan` (write-strict `worktree create` planner — same missing-field-hint and `normalizeCleanupManifestEntry` validation as `planWorktreeRecordAgent`, pure/no-git), `executeWorktreeCreatePlan(plan, repoRoot, deps) → WorktreeCreateResult` (bounded `git rev-parse --verify` base check THEN `git worktree add -b <branch> <path> <base>`; fail-closed `base_unresolved`/`git_timeout`/`worktree_add_failed`; returns `cwd` — the working directory an executor spawn would use), `cmdWorktreeCreate(cwd, args, deps) → WorktreeCreateCmdResult` (CLI verb: plans, creates the worktree, then appends the manifest entry so it is immediately manageable by cleanup-wave/reap-orphans; dedupes by `(worktree_path, branch)`). #2584 ADR-1239 Codex-binding amendment, Phase 2: `worktree create` is the git-worktree-creation primitive for `dispatch.isolation: orchestrator-worktree` hosts — declared and testable but UNCONSUMED (no scheduler calls it yet; Phase 3 wires it). Source of truth: `gsd-core/bin/lib/worktree-safety.cjs`. Timeout path: all git subprocess calls are bounded; callers receive `ok:false, reason:'git_timed_out'` rather than a thrown exception. Test anchor: `tests/worktree-safety.test.cjs`. The `core.cjs` re-export spine was retired in epic #1267: this module absorbed the two thin compositional wrappers that squatted in Core — `resolveWorktreeRoot(cwd, deps)` (a projection over `resolveWorktreeContext`) and `pruneOrphanedWorktrees(...)` (sequences `planWorktreePrune` + `executeWorktreePrunePlan` with a timeout warning) — so callers reach this single worktree-lifecycle seam directly. `gitWorktreeInfoInternal` did NOT move here — worktree-info detection belongs to the Git Query Module.
|
||||
|
||||
### Worktree Lifecycle Module
|
||||
Workflow contract seam covering agent worktree lifecycle orchestration rules. The `worktree_branch_check` block lives in one canonical fragment (`gsd-core/references/worktree-branch-check.md`) that `execute-phase.md`, `quick.md`, `diagnose-issues.md`, and `execute-plan.md` embed at dispatch. Key invariants: `worktree_branch_check` is **verify-only and fail-closed** — the orchestrator owns worktree lifecycle and base recovery, so the sub-agent holds no state-correction primitives; HEAD attachment verified via `git symbolic-ref`; positive allow-list `^worktree-agent-*` enforced; `git update-ref` on protected refs is prohibited; on base mismatch the sub-agent halts with `exit 42` and surfaces to the orchestrator (#48); the orchestrator runs a cwd-drift guard at `execute_waves` entry that resolves the worktree root and refuses drift into an agent worktree (#48); cleanup is manifest-scoped (`WAVE_WORKTREE_MANIFEST`) not global-discovery-based; worktree spawning is sequential (one `run_in_background` at a time to avoid `config.lock` contention). Test anchor: `tests/worktree.test.cjs`.
|
||||
Workflow contract seam covering agent worktree lifecycle orchestration rules. The `worktree_branch_check` block lives in one canonical fragment (`gsd-core/references/worktree-branch-check.md`) that `execute-phase.md`, `quick.md`, `diagnose-issues.md`, and `execute-plan.md` embed at dispatch. Key invariants: `worktree_branch_check` is **verify-only and fail-closed** — the orchestrator owns worktree lifecycle and base recovery, so the sub-agent holds no state-correction primitives; HEAD attachment verified via `git symbolic-ref`; positive allow-list `^worktree-agent-*` enforced; `git update-ref` on protected refs is prohibited; on base mismatch the sub-agent halts with `exit 42` and surfaces to the orchestrator (#48); the orchestrator runs a cwd-drift guard at `execute_waves` entry that resolves the worktree root and refuses drift into an agent worktree (#48); #1856: that refusal now also reports what the agent worktree holds — commits ahead of the resolved base and uncommitted files, both with true counts plus a truncation notice — and the commit/switch/merge-or-cherry-pick sequence to integrate them, because `re-run from the orchestrator worktree` alone silently meant abandoning work that lives only on the agent branch. The refusal condition and exit code are unchanged, and every added command is diagnostic and `|| true`-guarded so a failure degrades to the plain refusal; cleanup is manifest-scoped (`WAVE_WORKTREE_MANIFEST`) not global-discovery-based; worktree spawning is sequential (one `run_in_background` at a time to avoid `config.lock` contention). Test anchor: `tests/worktree.test.cjs`.
|
||||
|
||||
### Worktree Root Resolution Adapter Module
|
||||
Adapter Module owning linked-worktree root mapping and metadata-prune policy (`git worktree prune` non-destructive default) for planning/workstream callers.
|
||||
|
||||
@@ -440,11 +440,56 @@ pin to `git worktree list`'s first entry — that is the main worktree, the wron
|
||||
when the orchestrator legitimately runs from a feature worktree.
|
||||
|
||||
```bash
|
||||
# gsd:guard=orchestrator-cwd-drift
|
||||
ORCHESTRATOR_WT=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
||||
echo "FATAL: execute_waves entry is not inside a git worktree (#48)." >&2; exit 1; }
|
||||
ORCH_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
if printf '%s' "$ORCH_BRANCH" | grep -Eq '^(worktree-)?agent-'; then
|
||||
echo "FATAL: orchestrator cwd is inside an agent worktree (branch '$ORCH_BRANCH', root '$ORCHESTRATOR_WT') — refusing to execute waves (#48). A prior isolation=\"worktree\" dispatch drifted the cwd; re-run from the orchestrator's own worktree." >&2
|
||||
# #1856 handoff: the refusal above is correct, but on its own it is a dead end —
|
||||
# this worktree may hold committed fixes AND uncommitted work, and "re-run from
|
||||
# the orchestrator's worktree" silently means abandoning them. Report exactly
|
||||
# what is stranded and how to integrate it. Every command here is DIAGNOSTIC:
|
||||
# each is `|| true`-guarded so a failure degrades to the plain refusal above
|
||||
# rather than crashing before the message prints.
|
||||
_WT_BASE=""
|
||||
for _ref in "$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)" \
|
||||
origin/next origin/main next main; do
|
||||
[ -n "$_ref" ] || continue
|
||||
if git rev-parse --verify --quiet "$_ref" >/dev/null 2>&1; then _WT_BASE="$_ref"; break; fi
|
||||
done
|
||||
_WT_AHEAD=""
|
||||
[ -n "$_WT_BASE" ] && _WT_AHEAD=$(git rev-list --count "$_WT_BASE..HEAD" 2>/dev/null || true)
|
||||
# Count BEFORE truncating, so a long list reports its true size rather than
|
||||
# under-reporting what is stranded — which is the whole point of this report.
|
||||
_WT_DIRTY_ALL=$(git status --porcelain 2>/dev/null || true)
|
||||
_WT_DIRTY_N=0
|
||||
[ -n "$_WT_DIRTY_ALL" ] && _WT_DIRTY_N=$(printf '%s\n' "$_WT_DIRTY_ALL" | wc -l | tr -d ' ')
|
||||
_WT_HAS_COMMITS=0
|
||||
[ -n "$_WT_AHEAD" ] && [ "$_WT_AHEAD" -gt 0 ] 2>/dev/null && _WT_HAS_COMMITS=1
|
||||
|
||||
echo "" >&2
|
||||
echo "── Handoff: what is in this worktree (#1856) ──" >&2
|
||||
if [ "$_WT_HAS_COMMITS" -eq 1 ]; then
|
||||
echo " $_WT_AHEAD commit(s) on '$ORCH_BRANCH' not on '$_WT_BASE':" >&2
|
||||
git log --oneline --no-decorate "$_WT_BASE..HEAD" 2>/dev/null | head -20 | sed 's/^/ /' >&2 || true
|
||||
[ "$_WT_AHEAD" -gt 20 ] 2>/dev/null && echo " … and $((_WT_AHEAD - 20)) more" >&2
|
||||
echo " These live ONLY on this branch. Switching away without integrating loses them." >&2
|
||||
fi
|
||||
if [ -n "$_WT_DIRTY_ALL" ]; then
|
||||
echo " $_WT_DIRTY_N uncommitted change(s) still in this worktree:" >&2
|
||||
printf '%s\n' "$_WT_DIRTY_ALL" | head -20 | sed 's/^/ /' >&2
|
||||
[ "$_WT_DIRTY_N" -gt 20 ] 2>/dev/null && echo " … and $((_WT_DIRTY_N - 20)) more" >&2
|
||||
fi
|
||||
if [ "$_WT_HAS_COMMITS" -eq 1 ] || [ -n "$_WT_DIRTY_ALL" ]; then
|
||||
echo "" >&2
|
||||
echo " To integrate before continuing:" >&2
|
||||
[ -n "$_WT_DIRTY_ALL" ] && echo " 1. git add -A && git commit -m 'wip: recover worktree state' # from THIS worktree" >&2
|
||||
echo " 2. cd <orchestrator worktree> # a checkout whose branch is NOT agent-*/worktree-agent-*" >&2
|
||||
echo " 3. git merge --no-ff $ORCH_BRANCH # or: git cherry-pick <sha>... for selected commits" >&2
|
||||
echo " 4. re-run the phase from there" >&2
|
||||
echo " Verify with: git log --oneline ${_WT_BASE:-HEAD}..$ORCH_BRANCH" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
# Pin to the worktree root; each later orchestrator-side block re-pins the same way
|
||||
|
||||
227
tests/execute-phase-worktree-guard.test.cjs
Normal file
227
tests/execute-phase-worktree-guard.test.cjs
Normal file
@@ -0,0 +1,227 @@
|
||||
// allow-test-rule: runtime-contract-is-the-product see #1856
|
||||
// The orchestrator cwd-drift guard (#48) is shell EMBEDDED in execute-phase.md.
|
||||
// The shipped text IS the runtime contract, so these tests extract the block and
|
||||
// EXECUTE it against real git fixtures rather than asserting on its characters —
|
||||
// the readFileSync here is extraction for execution, not a source-grep assertion.
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const WORKFLOW = path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md');
|
||||
const GUARD_MARKER = 'gsd:guard=orchestrator-cwd-drift';
|
||||
|
||||
/**
|
||||
* Pull the guard's bash block out of the workflow. Anchored on a stable marker
|
||||
* comment rather than a line range so the test does not rot when the file moves.
|
||||
*/
|
||||
function guardScript() {
|
||||
const md = fs.readFileSync(WORKFLOW, 'utf8');
|
||||
const fences = md.split('```');
|
||||
for (let i = 1; i < fences.length; i += 2) {
|
||||
const body = fences[i].replace(/^bash\r?\n/, '');
|
||||
if (body.includes(GUARD_MARKER)) return body;
|
||||
}
|
||||
throw new Error(
|
||||
`no fenced block carrying "${GUARD_MARKER}" in ${WORKFLOW} — the guard must be ` +
|
||||
'marked so this contract test can execute the shipped text.',
|
||||
);
|
||||
}
|
||||
|
||||
const git = (cwd, ...args) =>
|
||||
execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
/** A real repo with a base branch and one commit. */
|
||||
function makeRepo() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1856-'));
|
||||
git(dir, 'init', '--quiet', '--initial-branch', 'next');
|
||||
git(dir, 'config', 'user.email', 'test@example.com');
|
||||
git(dir, 'config', 'user.name', 'Test');
|
||||
fs.writeFileSync(path.join(dir, 'base.txt'), 'base\n');
|
||||
git(dir, 'add', '-A');
|
||||
git(dir, 'commit', '--quiet', '-m', 'base');
|
||||
return dir;
|
||||
}
|
||||
|
||||
function commitFile(dir, name, msg) {
|
||||
fs.writeFileSync(path.join(dir, name), `${name}\n`);
|
||||
git(dir, 'add', '-A');
|
||||
git(dir, 'commit', '--quiet', '-m', msg);
|
||||
}
|
||||
|
||||
/** Run the extracted guard in `dir`. Never throws — returns the observed result. */
|
||||
function runGuard(dir) {
|
||||
const res = spawnSync('bash', ['-c', guardScript()], {
|
||||
cwd: dir,
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
||||
}
|
||||
|
||||
test('#1856: refusal names the stranded commits and the dirty tree', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
git(dir, 'checkout', '--quiet', '-b', 'worktree-agent-36.12-05-090827');
|
||||
for (const m of ['wire roto toggle', 'preserve roto keys', 'simplify controls',
|
||||
'count semantics', 'bound regeneration']) {
|
||||
commitFile(dir, m.replace(/\s/g, '-') + '.txt', `fix(36.12-05): ${m}`);
|
||||
}
|
||||
fs.writeFileSync(path.join(dir, 'uncommitted.txt'), 'work in progress\n');
|
||||
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 1, 'the guard must still REFUSE — #48 is load-bearing');
|
||||
assert.match(r.stderr, /worktree-agent-36\.12-05-090827/, 'names the branch');
|
||||
|
||||
// The whole point of #1856: the refusal must not be a dead end.
|
||||
assert.match(
|
||||
r.stderr,
|
||||
/5\s+commit/i,
|
||||
'must report how many commits are stranded on the agent branch — without this the ' +
|
||||
'user cannot tell that switching away loses work (#1856)',
|
||||
);
|
||||
assert.match(
|
||||
r.stderr,
|
||||
/uncommitted|dirty|unstaged/i,
|
||||
'must report that the worktree still has uncommitted changes (#1856)',
|
||||
);
|
||||
assert.match(
|
||||
r.stderr,
|
||||
/cherry-pick|merge/i,
|
||||
'must give an integration command, not just "re-run from the orchestrator worktree"',
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: the bare agent- namespace is handled identically', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
git(dir, 'checkout', '--quiet', '-b', 'agent-a1b2c3');
|
||||
commitFile(dir, 'one.txt', 'feat: one');
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /agent-a1b2c3/);
|
||||
assert.match(r.stderr, /1\s+commit/i);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: a clean agent worktree still refuses, without a wall of empty sections', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
git(dir, 'checkout', '--quiet', '-b', 'agent-clean');
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 1, 'still refuses');
|
||||
// Nothing is stranded, so nothing must be claimed to be.
|
||||
assert.doesNotMatch(
|
||||
r.stderr,
|
||||
/\b[1-9]\d*\s+commit/i,
|
||||
'a branch with no commits ahead must not report stranded commits',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
r.stderr,
|
||||
/uncommitted changes/i,
|
||||
'a clean tree must not be reported as dirty',
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: boundary — 1 and 2 commits ahead are both reported accurately', () => {
|
||||
for (const n of [1, 2]) {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
git(dir, 'checkout', '--quiet', '-b', 'agent-count');
|
||||
for (let i = 0; i < n; i += 1) commitFile(dir, `c${i}.txt`, `feat: c${i}`);
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, new RegExp(`\\b${n}\\s+commit`, 'i'), `${n} ahead reported`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: does not fire on an ordinary orchestrator branch', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
const r = runGuard(dir); // still on `next`
|
||||
assert.equal(r.status, 0, `guard must not fire on a normal branch: ${r.stderr}`);
|
||||
assert.doesNotMatch(r.stderr, /refusing to execute waves/);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: does not fire on a branch that merely starts with "agent"', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
// The discriminator is the `agent-` NAMESPACE. `agentic-refactor` is an
|
||||
// ordinary feature branch and must run.
|
||||
git(dir, 'checkout', '--quiet', '-b', 'agentic-refactor');
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 0, `guard must not fire on agentic-refactor: ${r.stderr}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: does not fire on a legitimate feature worktree', () => {
|
||||
// Recorded constraint in the guard's own comment: the discriminator is the branch
|
||||
// namespace, NOT the .claude/worktrees/ path — the orchestrator may legitimately
|
||||
// run from a feature worktree there, and a path check would break that.
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
const wt = path.join(dir, '.claude', 'worktrees', 'feature');
|
||||
fs.mkdirSync(path.dirname(wt), { recursive: true });
|
||||
git(dir, 'worktree', 'add', '--quiet', '-b', 'feat/legit', wt);
|
||||
const r = runGuard(wt);
|
||||
assert.equal(r.status, 0, `a feature worktree must run: ${r.stderr}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: reporting degrades to the plain refusal when no base ref resolves', () => {
|
||||
// No origin, no main/next to compare against — the guard must still refuse
|
||||
// cleanly rather than crash or hang before printing.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1856-nobase-'));
|
||||
try {
|
||||
git(dir, 'init', '--quiet', '--initial-branch', 'agent-orphan');
|
||||
git(dir, 'config', 'user.email', 'test@example.com');
|
||||
git(dir, 'config', 'user.name', 'Test');
|
||||
fs.writeFileSync(path.join(dir, 'a.txt'), 'a\n');
|
||||
git(dir, 'add', '-A');
|
||||
git(dir, 'commit', '--quiet', '-m', 'only');
|
||||
const r = runGuard(dir);
|
||||
assert.equal(r.status, 1, 'still refuses with no resolvable base');
|
||||
assert.match(r.stderr, /refusing to execute waves/);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('#1856: detached HEAD does not crash the guard', () => {
|
||||
const dir = makeRepo();
|
||||
try {
|
||||
const sha = git(dir, 'rev-parse', 'HEAD').trim();
|
||||
git(dir, 'checkout', '--quiet', '--detach', sha);
|
||||
const r = runGuard(dir);
|
||||
// `rev-parse --abbrev-ref HEAD` yields "HEAD" when detached — not an agent
|
||||
// branch, so the guard must pass through without erroring.
|
||||
assert.equal(r.status, 0, `detached HEAD must not crash the guard: ${r.stderr}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
@@ -248,7 +248,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "53dd4148dd0f4c90",
|
||||
"gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a",
|
||||
"gsd-core/workflows/eval-review.md": "610775f4968f7807",
|
||||
"gsd-core/workflows/execute-phase.md": "8c0521b091bbc1b5",
|
||||
"gsd-core/workflows/execute-phase.md": "a20deff10f1a9c7b",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1832b97d0923fa67",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "0404bcd32f47715e",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "fcc3181bc5593f8f",
|
||||
"gsd-core/workflows/execute-phase.md": "d9ecf8b35ae4a771",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "dc44aa6aab38535d",
|
||||
"gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e",
|
||||
"gsd-core/workflows/eval-review.md": "c0cf2d0c5dd9df36",
|
||||
"gsd-core/workflows/execute-phase.md": "abebe1ef79aa5786",
|
||||
"gsd-core/workflows/execute-phase.md": "3a1457523d8cf965",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9a1f3deffb4dab14",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ea0ebff5ac00fee6",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -247,7 +247,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "7a0be3529a99b11f",
|
||||
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
|
||||
"gsd-core/workflows/eval-review.md": "02cefe2dbdd0af52",
|
||||
"gsd-core/workflows/execute-phase.md": "f88215925dc01075",
|
||||
"gsd-core/workflows/execute-phase.md": "4bc5300e8b3aa889",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "5f953a0d4c0f3cad",
|
||||
"gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74",
|
||||
"gsd-core/workflows/eval-review.md": "f67caf4dba0586d1",
|
||||
"gsd-core/workflows/execute-phase.md": "769064cfeb1baf4a",
|
||||
"gsd-core/workflows/execute-phase.md": "b8c122c15467a6d8",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1b3558cb8f41b65a",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "e01459552d587863",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "79e703c637e2d7b1",
|
||||
"gsd-core/workflows/execute-phase.md": "40765d7517891c09",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "2b0db5c092ac573d",
|
||||
"gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f",
|
||||
"gsd-core/workflows/eval-review.md": "4d367d1e028629bd",
|
||||
"gsd-core/workflows/execute-phase.md": "7bcc0a33f91336c7",
|
||||
"gsd-core/workflows/execute-phase.md": "4d94ef7b8cf41b80",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "cd8678d082d6e191",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "df1d2f6a653e0bb4",
|
||||
"gsd-core/workflows/edit-phase.md": "8667c28b22b1599f",
|
||||
"gsd-core/workflows/eval-review.md": "6fd7f2b0770b48f1",
|
||||
"gsd-core/workflows/execute-phase.md": "d9ad04be8e897208",
|
||||
"gsd-core/workflows/execute-phase.md": "261fb868e55c9289",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "ca37f91f7bd6f05c",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "e0ec3d9748a0fed9",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "b4bf43f3005f2e45",
|
||||
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
|
||||
"gsd-core/workflows/eval-review.md": "e8a7646e1699a17d",
|
||||
"gsd-core/workflows/execute-phase.md": "e8ddb24f9cdca5f0",
|
||||
"gsd-core/workflows/execute-phase.md": "f33882f452e603d0",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "06b54ecd4015c64c",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "c017abde0209656e",
|
||||
"gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8",
|
||||
"gsd-core/workflows/eval-review.md": "6d1c9c167f91b0f8",
|
||||
"gsd-core/workflows/execute-phase.md": "fcf5f258a1e2aeea",
|
||||
"gsd-core/workflows/execute-phase.md": "e66dfb357797ab51",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9b2193de25506b3b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "4dfefdd56d814d06",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "4bbc9fcc8fea78a0",
|
||||
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
|
||||
"gsd-core/workflows/eval-review.md": "fb3b8244d66972ee",
|
||||
"gsd-core/workflows/execute-phase.md": "e41ab56e1388afc8",
|
||||
"gsd-core/workflows/execute-phase.md": "f0697801da646722",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "b1e6588cd32a6f08",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "6a663b8755ed216e",
|
||||
"gsd-core/workflows/execute-phase.md": "87111d41f6498208",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "df8c032fb91cedbd",
|
||||
"gsd-core/workflows/execute-phase.md": "357e82b83fb4719d",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "55cbaed5020bdd71",
|
||||
"gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39",
|
||||
"gsd-core/workflows/eval-review.md": "4f932c3ea3eb807b",
|
||||
"gsd-core/workflows/execute-phase.md": "78169e590b406fa8",
|
||||
"gsd-core/workflows/execute-phase.md": "983551331e155e7d",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "a15993affd62f4bf",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "f74ce2de21b2d252",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
2
tests/fixtures/golden-install-parity/pi.json
vendored
2
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -215,7 +215,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "4eddcc41c823db8b",
|
||||
"gsd-core/workflows/execute-phase.md": "be00d0535fa8c720",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "03599fe1d110e511",
|
||||
"gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36",
|
||||
"gsd-core/workflows/eval-review.md": "57a5eaade256fee4",
|
||||
"gsd-core/workflows/execute-phase.md": "da1e79f7407090a9",
|
||||
"gsd-core/workflows/execute-phase.md": "5f70bb011fd5ca0d",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "0e0949db56deebeb",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "2ca3c7397d44613f",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "78cd8efff2717c77",
|
||||
"gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad",
|
||||
"gsd-core/workflows/eval-review.md": "263c8971520f632e",
|
||||
"gsd-core/workflows/execute-phase.md": "67f3aaa7eae674e2",
|
||||
"gsd-core/workflows/execute-phase.md": "695cf3c2b07e8920",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "296b812e8d0e9ce8",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "c1d6db937a51d433",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "8fd49fe7a89da620",
|
||||
"gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789",
|
||||
"gsd-core/workflows/eval-review.md": "26051580110e6217",
|
||||
"gsd-core/workflows/execute-phase.md": "043544b7a948cae5",
|
||||
"gsd-core/workflows/execute-phase.md": "383836c8c1d0fa7e",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "3194ecd382690755",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "becb931701890353",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
"gsd-core/workflows/docs-update.md": "fba7329f525c9463",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "c2933259ff7fb6ac",
|
||||
"gsd-core/workflows/execute-phase.md": "1fb281c6e0511e2a",
|
||||
"gsd-core/workflows/execute-phase.md": "3ffcd069e2aeb62b",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
|
||||
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md": "ab70374e44eb85cd",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"docs-update.md": 56876,
|
||||
"edit-phase.md": 12927,
|
||||
"eval-review.md": 10316,
|
||||
"execute-phase.md": 90143,
|
||||
"execute-phase.md": 92874,
|
||||
"execute-plan.md": 35143,
|
||||
"explore.md": 11127,
|
||||
"extract-learnings.md": 13762,
|
||||
|
||||
Reference in New Issue
Block a user