Skip to content

fix(#5182): watch a GSD-owned return receipt instead of the host transcript in the plan-phase stall watch - #5192

Open
davesienkowski wants to merge 18 commits into
open-gsd:nextfrom
davesienkowski:fix/5182-stall-watch-return-receipt
Open

davesienkowski wants to merge 18 commits into
open-gsd:nextfrom
davesienkowski:fix/5182-stall-watch-return-receipt

Conversation

@davesienkowski

@davesienkowski davesienkowski commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Fix PR


Linked Issue

Fixes #5182

The linked issue carries confirmed-bug. This PR follows @trek-e's diagnosis and Agent Brief on the issue (issuecomment-5963253983); its six acceptance criteria are mapped below. Out of scope, per the brief: #2699 (general subagent watchdog) and #3923 (threshold default).


What was broken

With the default planner.stall_detection_enabled: true, /gsd:plan-phase decides that a background planner or plan-checker has finished by grepping the host's {outputFile} for the return markers. On Claude Code that file is the subagent transcript. The transcript already holds the agent's prompt and its definition snapshot, and both quote the markers, so the wait reported marker_received at its first check whether or not the agent had returned. The revision watch passed no markers, so every revision round could only end in the "Accept as revised / Retry / Stop" stall menu. A host that returns no output file could only ever reach stalled.

What this fix does

Each watched spawn now gets a GSD-owned, per-dispatch return receipt, and the watch reads only that file:

  • gsd_receipt_path "${PHASE_DIR}" <spawn> prints a fresh absolute path, <phase>/.gsd-returns/<spawn>.<epoch>.XXXXXXXX. It creates the directory with a catch-all .gitignore but not the file. It fails closed (prints nothing, returns 1) unless the phase directory's nearest existing ancestor resolves, with pwd -P, under the orchestrator's pwd -P, and no walked component, .gsd-returns or its .gitignore is a symlink. It also fails closed on a phase path holding a quote, $, a backtick, <, > or any control character. Only static, checked-in links are in scope; the check-then-act race is not. The orchestrator binds the printed path as {receipt} in the spawn prompt (<return_receipt>{receipt}</return_receipt>) and in the watch call.
  • The agent's last action is to write the marker line it returns to that path. Both use the Write tool. gsd-plan-checker gains Write for this one file (see "Checker posture" below).
  • gsd_return_marker FILE MARKER... is the single owner of "which marker did the agent return". It does a literal line-start match within the first 64 lines and 4096 bytes. The marker must be followed by the end of the line, whitespace, CR or a colon. It ignores a receipt that is a symlink, and it tolerates a BOM and a missing final newline. gsd_stall_watch uses it for marker_found, and step 11 routes the checker on it, so the watch and the router cannot disagree. Prompt, definition and JSONL transcript text never match it.
  • The revision watch passes ## REVISION COMPLETE and ## REVISION_CONFLICT, so it has a success exit. ## REVISION COMPLETE was already emitted (planner-revision.md Step 7). It is now declared in gsd-planner's Return Markers and in the agent-contracts.md registry, and check-contract-drift stays at 0 violations.
  • The helpers doc now says that a real completion result arriving between cycles ends the wait, and which signal routes when both exist. A receipt-routed ## ISSUES FOUND with no issue list fails closed: it is never counted as 0 issues and goes to 11a.
  • A $TS lost between tool calls falls back to the epoch stamped into the receipt name, so the threshold stays reachable.

All five watch sites get the receipt, and each rule removes it with rm -f "{receipt}" after routing any result, stalled and Retry included: the three in plan-phase.md that the brief names (steps 8, 10, 12) and the chunked outline and per-plan sites in steps/chunked-planning-mode.md. The two chunked prompts also say Return: ## ... COMPLETE, so they had the same first-check match, and tests/plan-phase-stall-detection.test.cjs pins the helper contract at exactly five sites.

Root cause

gsd_stall_watch (stall-detection-helpers.md:156-159 on next) ran grep -qF over the whole host output file. On Claude Code that file contains the agent's inputs as well as its output. The revision call site (plan-phase.md:1189) passed no markers, so the marker_received exit in gsd_stall_should_recover (:124-126) was unreachable for it. Introduced by 067a4d1 (#3015).

gsd_stall_should_recover itself is unchanged on purpose: its order (marker, then fresh artifact, then threshold) was correct. The defect was the input it was given, a host file and an empty marker list at the revision site.

Checker posture (maintainer decision, issuecomment-6036744404)

gsd-plan-checker moves from #767 Group A to Group B, the report-writers (gsd-verifier, gsd-doc-verifier, gsd-eval-auditor, gsd-ui-auditor). Its tools: gains Write, and its READONLY_AGENT_DISALLOWED_TOOLS entry (src/runtime-artifact-conversion.cts) becomes Edit, MultiEdit. Codex then derives sandbox_mode = "workspace-write" from its tools: (src/codex-agent-toml.cts; no hold applies). The receipt rule says the receipt is the checker's only write. <anti_patterns> repeats that limit unconditionally, because the quick, quick-batch and import spawns send no receipt. As a Write-capable agent the checker also carries the commented # hooks: block and the anti-heredoc line (tests/agent-frontmatter.test.cjs). So AC5 holds for the checker as it does for the planner, on Claude Code and on Codex.

One host-level limit applies to the planner and the checker alike. references/runtime-aware-dispatch.md maps -planner and -checker roles on kimi-code to built-ins that do not write files, so neither receipt is written there. On that host the watch behaves as it does on next for a host with no output file. Changing that mapping is outside this fix.

Security posture, stated plainly: on Claude Code the checker is no longer denied Write (it is still denied Edit and MultiEdit). On Codex its sandbox can write anywhere in the workspace, not only the receipt. That is the posture the four existing Group B agents already have, and the "only write" limit is prose, as it is for them. On OpenCode and Kilo the grant is wider still: Write, Edit and MultiEdit collapse to one edit permission there (src/runtime-artifact-conversion.cts), so the checker's Write makes full edit allowed, the same as the other Group B agents. The maintainer's 10-10 security acceptance covered Codex only and predates #5338.

Testing

How I verified the fix

New rows in tests/plan-phase-stall-detection.test.cjs (the module's own test file). They run the shipped bash with sleep stubbed, and they read each call site's markers from plan-phase.md rather than copying them:

Acceptance criterion Row
Markers present only in a prompt, agent definition or transcript yield waiting AC1: markers present only in a prompt or definition snapshot yield waiting (+ wiring row: 5 sites bind {receipt}, none binds {outputFile})
A checker returning issues routes to the revision step AC2: a checker receipt of ## ISSUES FOUND routes to the revision step (+ fail-closed row for a missing issue list)
A finished revision yields success within one poll interval AC3: a finished revision yields marker_received within one poll interval
A spawn that never signals completion yields stalled AC4 (preservation row) + lost-$TS epoch row
Identical results with and without a host output file AC5: identical results on a host with a transcript output file and on a host with none
Regression tests fail on current next first see below

Plus edge rows for gsd_return_marker (missing, empty, CRLF, BOM, no final newline, trailing text, word boundary, indented, mid-line, JSONL, directory) and gsd_receipt_path (absolute, distinct within one second, not pre-created, label sanitized, unsafe phase dir refused, .gitignore).

Red first: with only the test file changed, on next @ af46465, node --test tests/plan-phase-stall-detection.test.cjs gave tests 57, pass 47, fail 10. AC1 and AC5 failed with marker_received where waiting was expected, which is the issue's mechanism. AC4 was green on next by design.
Green (head 18695364a, empty HOME/GSD_HOME): plan-phase-stall-detection + the workflow and agent size budgets: 293/293. The emitted ack/attribution, agent-frontmatter and docs-parity files: 2299/2299. First round: Run against the untouched next source, the final test file gives tests 60, pass 40, fail 20: all 14 new regression rows fail except the AC4 preservation row, and the 6 rows retargeted from {outputFile} to {receipt} fail too. npm run lint:ci exits 0 (check-contract-drift: 41 markers, 0 violations; workflow shellcheck: 0 new findings).

Regression test added?

  • Yes — added a test that would have caught this bug

Platforms tested

  • macOS
  • Windows (including backslash path handling)
  • Linux
  • N/A (not platform-specific)

Linux (WSL2) under bash 5.x. The new helpers were also run directly under bash:3.2.57 (the macOS system bash) and bash:5.2.37 containers with the same results: BOM+CRLF receipt, word boundary, non-ASCII marker with no final newline, revision marker_received, lost-$TS stalled, unsafe phase dir refused. Windows: gsd_receipt_path emits a mixed C:/... path through cygpath -m when it is present. That is not exercised locally; CI's Windows matrix runs the rows.

Runtimes tested

  • Claude Code
  • Antigravity
  • OpenCode
  • Other: runtime-neutral bash helper exercised directly; no live runtime session was run for this PR
  • N/A (not runtime-specific)

Checklist

  • Issue linked above with Fixes #5182 — PR will be auto-closed if missing
  • Linked issue has the confirmed-bug label
  • Fix is scoped to the reported bug — no unrelated changes included
  • Regression test added (or explained why not)
  • All existing tests pass (npm test). Locally the full run exited 1. I re-ran each failing file alone with an empty HOME/GSD_HOME. Five went green (machine-local config noise). host-integration.test.cjs fails the same 5 rows on untouched next @ af46465. Two were mine and are fixed in this commit: a doc comment the contract-drift scanner read as a marker, and the compact-content benchmark baseline refreshed for plan-phase.md's growth. CI is the authority here.
  • .changeset/ fragment added if this is a user-facing fix (pr: filled after open)
  • No unnecessary dependencies added

Breaking changes

No API or output-format change: gsd_stall_watch still prints one of marker_received | active | waiting | stalled. Two behavior changes, both disclosed in the issue:

  • the planner and checker waits no longer return at their first check, and
  • the "Accept as revised" menu no longer follows every successful revision.

The second argument of gsd_stall_watch is now the receipt, not the host output file. Its only callers are the five plan-phase sites.

ADRs consulted: ADR-1239 (per-host behavior "explicit and testable rather than scattered runtime === '…' checks"; per-host values "documentation-sourced ... never inferred": the fix reads no host file and has no runtime branch), ADR-1016 (runtime capability descriptor; no new per-runtime conditional), ADR-5057 (Proposed; one owner per workflow verdict: gsd_return_marker is the single marker matcher for the watch and the checker routing), ADR-857 Phase 6 (plan-phase.md stays under its 98,300-byte freeze: 98,280 LF bytes after this change), ADR-1610 (agent size caps: gsd-plan-checker and gsd-planner stay under their tier caps), ADR-1235 and ADR-3574 (the Claude disallowedTools injection step reads READONLY_AGENT_DISALLOWED_TOOLS; this PR changes one entry, not the mechanism), #767 (the Group A/B split).

Generated by AI during contribution; reviewed and posted by a human maintainer (@davesienkowski).

@davesienkowski davesienkowski added the area: workflow Phase execution, ROADMAP, STATE.md label Oct 3, 2026
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 3, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
@davesienkowski davesienkowski self-assigned this Oct 3, 2026

@trek-e trek-e left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — fix(#5182): watch a GSD-owned return receipt instead of the host transcript in the plan-phase stall watch

Head reviewed: 47b8993 (fresh gh pr view this turn: mergeable=CONFLICTING, mergeStateStatus=DIRTY, reviewDecision=REVIEW_REQUIRED, no reviews/comments).

Summary

Replaces the host {outputFile} grep in gsd_stall_watch with a per-dispatch GSD-owned receipt (gsd_receipt_path, gsd_return_marker), wires all five watch sites, passes revision markers, and adds agent receipt rules. Substance is sound; the branch conflicts with origin/next and the new parser helper has no property test.

Classification & gate compliance

  • Issue #5182: OPEN, labels bug, area: workflow, confirmed-bug -> Bug-Fix track. Body has Fixes #5182; not draft; branch fix/5182-stall-watch-return-receipt; title fix(#5182):.
  • Changeset .changeset/lively-sloths-forage.md: type Fixed, pr: 5192, body format OK. No docs/ requirement (Fixed).
  • PR label is only area: workflow (issue carries the gate label; fine).
  • No competing open PRs for #5182.
  • Acceptance criteria (verbatim from trek-e brief): AC1 markers only in prompt/definition/transcript -> waiting (test row AC1); AC2 checker issues -> revision (row AC2); AC3 finished revision -> success within one poll (row AC3, revision site now passes ## REVISION COMPLETE/## REVISION_CONFLICT, plan-phase.md ~L1193); AC4 never-signals -> stalled (row AC4); AC5 identical on Claude Code vs no-output host (row AC5; receipt is host-independent, but see Finding 3 for the read-only checker host); AC6 fail-first (PR body reports 20 failing rows on untouched next; not executed here per brief).

Functional checklist / root-cause verdict

Root cause (one sentence): gsd_stall_watch grepped the whole host transcript, which already contains the prompt/definition quoting the markers, and the revision site passed no markers. Fix addresses the input to the watch (single owner gsd_return_marker used by watch and step 11), not the symptom. gsd_stall_should_recover unchanged. Verified in stall-detection-helpers.md (helpers ~L152-L195) and plan-phase.md steps 8/10/11/12.

Blast radius + risk tier

Shipped-prose + bash-in-markdown change; consumers are the five plan-phase watch sites, two agent definitions, agent-contracts registry. Tier: medium (runtime-loaded workflow/agent text, size-capped files). compact-content-benchmark-baseline.json refreshed for plan-phase growth.

Co-change completeness

No known co-change gap beyond the conflicting baseline fixture. agent-contracts.md updated for ## REVISION COMPLETE. CONTEXT.md has no gsd_stall_watch entry (grep: 0 hits), so no glossary gap is implied.

Memtrace Evidence

Memtrace is over quota (quota_exceeded) per orchestrator brief; get_impact, get_symbol_context, recall_decision, find_code_review_issues were NOT run (no result obtained; this is not a clean result). Evidence came from git diff/git merge-tree and direct reads of the PR head in the worktree. Co-change (get_cochange_context) not run. Changed symbols are bash helpers inside markdown (not graph-indexed), so graph impact would be limited regardless.

Findings

Blocker

  1. [judgment] Merge conflict with origin/next (behind by 13): git merge-tree --write-tree --name-only exit 1, conflicting path tests/fixtures/compact-content-benchmark-baseline.json (plan-phase offTokens/onTokens/aggregate at lines ~28-45). next moved plan-phase.md after the PR's measurement (e.g. 84b4dfe #5171, 7586af3 #5044). Fix: git fetch origin && git rebase origin/next, regenerate the baseline against the rebased plan-phase.md, re-verify.

Major
2. [judgment] tests/plan-phase-stall-detection.test.cjs (new describe at ~L940-1200): gsd_return_marker is a new parser (line-start literal, word boundary, CR/BOM tolerance) and gsd_receipt_path a new sanitizer; the test file imports fast-check (L49) but the new rows use only example cases. RULESET.TESTS requires at least one discriminating fast-check property for parsers/bijective contracts. Add a property: for arbitrary lines/prefixes, a marker matches iff the line starts with it and the next char is not [A-Za-z0-9_]; and gsd_receipt_path output never escapes .gsd-returns/ for arbitrary spawn labels.
3. [judgment] AC5 / acceptance criterion partially unmet for the checker on read-only hosts: gsd-plan-checker writes its receipt via Bash printf; on a host that runs it read-only (Codex derives read-only from tools:) the write is refused and the checker watch ends stalled (agents/gsd-plan-checker.md ~L957-962; stall-detection-helpers.md "Plan-checker on a read-only host"). The PR discloses this and defers the Group B decision to the maintainer; it is a maintainer decision (must be raised in chat), but until decided the criterion "identical on every host" is not met for the checker.

Minor
4. [judgment] Boundary coverage: AC4 row tests elapsed 9 min and 11 min against a 10 min threshold but not the limit itself (10 min) — add limit case (limit-1/limit/limit+1). Rows derive times from Date.now() (nowSec() helper) rather than a clock seam; offsets are coarse so not flaky, but it is wall-clock-derived input.

Nit
5. [judgment] Several new rows assert prose via assert.match on workflow/agent markdown (e.g. "never counted as 0 issues", rm -f "{receipt}"); these are prose-contract checks, acceptable but brittle to rewording.

Verdict

Changes requested (blocker: merge conflict; major: no property test; maintainer decision pending on checker read-only host). CI: all 36 check-runs for the head are success/skipped, pending approval runs=0 — scoped to the CI result only; mergeStateStatus is DIRTY.

davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 3, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 3, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 3, 2026
…s, AC4 boundary rows and token-level prose asserts

Review follow-up on open-gsd#5192.

- Property: gsd_return_marker matches iff the line starts with the marker
  and the next character is not [A-Za-z0-9_], with a leading BOM, a
  trailing CR and a missing final newline tolerated. Property:
  gsd_receipt_path never leaves <phase>/.gsd-returns/ for any spawn label,
  and an unsafe phase dir (quote, $ or backtick) fails closed. Each runs
  one bash invocation per generated batch, with values passed through
  files, never argv.
- AC4 rows now cover limit-1 / limit / limit+1 (599 / 600 / 601 s) against
  an injected clock (`date +%s` stubbed); no open-gsd#5182 row reads the wall clock.
- Prose-contract asserts are reduced to command tokens (`rm -f
  "{receipt}"`, `gsd_return_marker "{receipt}"`, `<return_receipt>`,
  printf) plus one keyword, so rewording the surrounding prose does not
  break the suite.
@davesienkowski
davesienkowski force-pushed the fix/5182-stall-watch-return-receipt branch from 47b8993 to 345d13e Compare October 3, 2026 19:34
@davesienkowski

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Addressed at 57aab6e31:

1. Merge conflict (blocker). Rebased onto origin/next @ 47b22b8c1. The only conflict was tests/fixtures/compact-content-benchmark-baseline.json. I did not resolve its hunks by hand: I took next's copy and regenerated it with node scripts/benchmark-compact-content.cjs --write against the rebased plan-phase.md, so the plan-phase rows reflect both #5171/#5044 and this change. git merge-tree --write-tree origin/next HEAD is clean.

The rebase also put plan-phase.md over its Phase 6 cap: #5171 had grown it to 98,051 LF bytes, which left 249. In 41fdec384 the receipt binding is stated once at step 7.99 ({receipt} = gsd_receipt_path "${PHASE_DIR}" <spawn>), and each watch site carries only its spawn label. Step 11 keeps its original marker_received bullets. The rule that the routing marker comes from gsd_return_marker "{receipt}" moved into the helpers doc that 7.99 loads. plan-phase.md is now 98,273 LF bytes.

2. Property tests (major). Added two fast-check properties to tests/plan-phase-stall-detection.test.cjs (345d13e46):

  • gsd_return_marker: for generated lines (marker-first, marker plus one following character, marker mid-line, truncated marker, unrelated text; with or without a leading BOM, a trailing CR and a final newline), the helper returns the marker iff the line starts with it and the next character is not [A-Za-z0-9_]. An independent JS oracle states the rule.
  • gsd_receipt_path: for generated spawn labels (including /, .., \, quotes, $, newline and non-ASCII), the path's parent is always <phase>/.gsd-returns, and the label part is in [A-Za-z0-9_-]. A generated phase dir holding a quote, $ or backtick fails closed; any other is accepted.

Each property run is one bash invocation over a whole generated batch. Since 57aab6e31, the #5182 rows run only the shipped helper functions, without the fence's runtime-locator preamble and its two Node config-get spawns per call (the #2650 rows still run the full fence). The first CI run of these rows spent 169.5 s on that preamble on Windows shard 1/3, and a concurrent file timed out (spawnSync bash ETIMEDOUT). The block now runs in about 2 s locally, and the properties still fail against all four helper breaks. Values reach bash through files, not argv: Windows MSYS re-unescapes argv, which is what the earlier Windows shard 1/3 failure was.

Red proof against deliberately broken helpers (each break reverted, file verified byte-identical afterwards):

  • removing the word-boundary check: counterexample ["## ⚠ Source Audit","## ⚠ Source Audita"];
  • removing the BOM strip: caught;
  • leaving the label unsanitized: counterexample "?";
  • accepting an unsafe phase dir: counterexample p".

A fifth break, removing the trailing-CR strip, survived. That was correct: a CR after the marker is already a non-word character, so the strip never changed a result. I removed it as dead code in 41fdec384.

The same commit spells out the ASCII word characters in both helpers instead of using bracket ranges, so no locale can widen them (bash 3.2 has no globasciiranges). I re-ran the helpers under bash 3.2.57 and 5.2.37 with identical results.

3. Checker on a read-only host (your call). Two ways to close it:

I'd prefer the follow-up. Moving the checker to Group B widens its security posture: on Codex, workspace-write covers the whole workspace, not just the receipt file. That deserves its own review rather than riding on a bug fix. Without it, this PR still meets AC5 for the planner, revision and chunked spawns on every host, and for the checker on any host that lets it write one file. On a host that runs the checker read-only, the watch ends stalled at the threshold, the same as next today, so it is not a regression. The one difference from next is that the runtime's own completion result, if it arrives first, now also ends the wait. If you'd rather have Group B here, I'll add it to this PR. Which do you prefer?

4. Threshold boundary (minor). The AC4 rows now cover limit - 1, limit and limit + 1 (599, 600 and 601 s against the 10-minute threshold) for the planner, checker and revision call sites. The #5182 rows no longer read the wall clock: date +%s is stubbed to a fixed NOW inside the script, so every boundary is exact.

5. Prose-contract asserts (nit). Agreed. I removed the asserts that were pure prose ("last action", "only write", "never counted as 0 issues", "nothing persists across fences", the completion-result wording). The rest now anchor on command tokens the orchestrator or agent actually executes: rm -f "{receipt}", gsd_return_marker "{receipt}", <return_receipt>{receipt}</return_receipt>, the checker's printf '%s\n', its tools: line, and step 11's 11a route. That leaves one keyword, read-only, for the disclosed degradation.

CI on 57aab6e31: all check-runs success or skipped, including Required tests, the three Ubuntu shards, macOS, and all three Windows shards. On Windows shard 1/3 the #5182 block took 11.9 s, down from 169.5 s. Local: npm run lint:ci exit 0; the affected suites (stall detection, background-wait, contract drift, compact-content benchmark/partition, Phase 6 conformance, agent and workflow size budgets) 448/448.

Generated by AI during contribution; reviewed and posted by a human maintainer (@davesienkowski).

@davesienkowski
davesienkowski requested a review from trek-e October 3, 2026 20:38
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 5, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 5, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
@davesienkowski
davesienkowski force-pushed the fix/5182-stall-watch-return-receipt branch from 57aab6e to 08ddb32 Compare October 5, 2026 21:59
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 5, 2026
…s, AC4 boundary rows and token-level prose asserts

Review follow-up on open-gsd#5192.

- Property: gsd_return_marker matches iff the line starts with the marker
  and the next character is not [A-Za-z0-9_], with a leading BOM, a
  trailing CR and a missing final newline tolerated. Property:
  gsd_receipt_path never leaves <phase>/.gsd-returns/ for any spawn label,
  and an unsafe phase dir (quote, $ or backtick) fails closed. Each runs
  one bash invocation per generated batch, with values passed through
  files, never argv.
- AC4 rows now cover limit-1 / limit / limit+1 (599 / 600 / 601 s) against
  an injected clock (`date +%s` stubbed); no open-gsd#5182 row reads the wall clock.
- Prose-contract asserts are reduced to command tokens (`rm -f
  "{receipt}"`, `gsd_return_marker "{receipt}"`, `<return_receipt>`,
  printf) plus one keyword, so rewording the surrounding prose does not
  break the suite.
@davesienkowski

Copy link
Copy Markdown
Collaborator Author

Rebased onto next @ 13d37238b to clear a new conflict. As last time, it was only tests/fixtures/compact-content-benchmark-baseline.json. I took next's copy and regenerated it with scripts/benchmark-compact-content.cjs --write at each commit that touches it. Outside that fixture, git range-diff shows every commit unchanged. #5169 shrank the launcher preamble, and plan-phase.md is now 98,079 LF bytes and needed no further trim. Head is now 08ddb32c2.

Local on the new head: build:lib 0, lint:ci 0, the same affected suites as before 448/448. CI is running.

Finding 3 (move gsd-plan-checker to Group B here, or a follow-up issue) still needs your call; see my 10-03 reply.

Generated by AI during review response; reviewed and posted by a human maintainer (@davesienkowski).

@trek-e trek-e left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — fix(#5182): watch a GSD-owned return receipt instead of the host transcript in the plan-phase stall watch

Head reviewed: 08ddb32 (mergeable=MERGEABLE, mergeStateStatus=BEHIND by 1, reviewDecision=CHANGES_REQUESTED).

Summary

Replaces the host {outputFile} grep in gsd_stall_watch with a per-dispatch GSD-owned receipt (gsd_receipt_path, gsd_return_marker), wires all five watch sites, passes revision markers, and adds agent receipt rules. Prior findings 1, 2, 4 and 5 are fixed at this head. Finding 3 (checker on a read-only host) remains open pending a maintainer decision, and one new Minor test finding is added.

Classification & gate compliance

  • #5182: OPEN, labels bug, area: workflow, confirmed-bug -> bug-fix track. Fixes #5182 present, not draft, conventional title, branch fix/5182-stall-watch-return-receipt.
  • Changeset .changeset/lively-sloths-forage.md: type Fixed, pr 5192, format OK. Docs exempt (Fixed). CHANGELOG untouched. No new references/ or workflows/ files.
  • Emitted-Drift-Ack-Growth trailers present for plan-phase.md, gsd-planner.md, gsd-plan-checker.md.
  • No competing open PR for #5182.

Prior-finding resolution (trek-e, 2026-10-03)

# Finding Status
1 Blocker baseline-fixture merge conflict Resolved; merge of origin/next and head is clean
2 Major no fast-check property Resolved; two oracle-backed properties
3 Major checker on read-only host / AC5 OPEN (maintainer decision)
4 Minor boundary triple, wall clock Resolved; 599/600/601 s with stubbed date +%s
5 Nit prose asserts Resolved

Functional checklist / root-cause verdict

Root cause: gsd_stall_watch grepped the whole host transcript, which already quotes the markers via prompt/definition, and the revision site passed no markers. The fix changes the watch's input to a GSD-owned file and uses one marker matcher shared with step 11 routing; gsd_stall_should_recover is untouched. Root-cause fix, not symptom handling.
AC1 pass, AC2 pass, AC3 pass, AC4 pass (limit-1/limit/limit+1), AC5 partial (Finding 1), AC6 per PR body (20 failing rows on next; not executed here).

Blast radius

Shipped prose and bash-in-markdown: five plan-phase watch sites, gsd-planner, gsd-plan-checker, agent-contracts registry. Argument 2 of gsd_stall_watch changes meaning (receipt, not host output file); only the five plan-phase sites call it. Risk: medium (runtime-loaded text, size-capped; growth acked by trailers).

Co-change completeness

agent-contracts.md updated for ## REVISION COMPLETE; benchmark baseline regenerated. No missing INVENTORY, CONTEXT or docs requirement identified.

CodeGraph Evidence

  • codegraph_impact target gsd_stall_watch (base index): 2 affected symbols, both in tests/plan-phase-stall-detection.test.cjs (extractStallHelpersBash). Shell helpers inside markdown are not graph-indexed, so this is a lower bound.
  • codegraph_callers target gsd_return_marker: symbol not found (new in this PR, absent from base index).
  • codegraph_node target gsd_stall_should_recover: only the test wrapper runShouldRecover is indexed; the function body is unchanged by this PR.

Memtrace unavailable: monthly query quota exhausted (20972/20933, resets 2026-10-20)

Findings

Major

  1. [judgment, carried from prior review #3] agents/gsd-plan-checker.md:962-966, gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md ("Plan-checker on a read-only host"): on a host that runs the checker read-only (Codex derives read-only from tools:) the receipt printf is refused and the checker watch ends stalled, so "identical on every host" (AC5) is not met for the checker. The contributor asked twice for a decision (Group B move here vs follow-up issue); it needs a maintainer answer in chat before approval.

Minor
2. [judgment] tests/plan-phase-stall-detection.test.cjs, AC5 row (bind()): once the call site binds {receipt}, both "hosts" receive f.receipt, so the comparison is between identical inputs and cannot fail. Fix: supply a transcript file and a missing file as argument 2 and assert neither is consulted, or remove the row.

Nit
3. [judgment] fast-check numRuns of 10/8/6 is small; acceptable given per-run batching.
4. [judgment] stall-detection-helpers.md ("The runtime's completion result ends the wait"): only checker ## ISSUES FOUND has an explicit fail-closed route when the receipt arrives without a body; planner receipt-only ## CHECKPOINT REACHED / ## PHASE SPLIT RECOMMENDED have no stated step-9 behavior.

Verdict

Request changes (Finding 1 open, Finding 2 Minor). CI at head: 36 check-runs, 32 success / 4 skipped, 0 red, 0 pending-approval runs. Rebase is optional (behind by 1, docs-only chore, clean): git fetch origin next && git rebase origin/next.

@trek-e trek-e added the review: changes requested PR reviewed — changes required before merge label Oct 7, 2026
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 7, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 7, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
@davesienkowski
davesienkowski force-pushed the fix/5182-stall-watch-return-receipt branch from 08ddb32 to 48ee141 Compare October 7, 2026 02:55
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 7, 2026
…s, AC4 boundary rows and token-level prose asserts

Review follow-up on open-gsd#5192.

- Property: gsd_return_marker matches iff the line starts with the marker
  and the next character is not [A-Za-z0-9_], with a leading BOM, a
  trailing CR and a missing final newline tolerated. Property:
  gsd_receipt_path never leaves <phase>/.gsd-returns/ for any spawn label,
  and an unsafe phase dir (quote, $ or backtick) fails closed. Each runs
  one bash invocation per generated batch, with values passed through
  files, never argv.
- AC4 rows now cover limit-1 / limit / limit+1 (599 / 600 / 601 s) against
  an injected clock (`date +%s` stubbed); no open-gsd#5182 row reads the wall clock.
- Prose-contract asserts are reduced to command tokens (`rm -f
  "{receipt}"`, `gsd_return_marker "{receipt}"`, `<return_receipt>`,
  printf) plus one keyword, so rewording the surrounding prose does not
  break the suite.
@davesienkowski

Copy link
Copy Markdown
Collaborator Author

@trek-e thanks. I rebased onto next @ 663acba1f (three commits, no conflict). The new head is 48ee141b9.

  1. Finding 1. I'm proposing the follow-up route: enh(plan-checker): let gsd-plan-checker write its stall-watch return receipt on hosts that run it read-only (#767 Group B) #5245 moves gsd-plan-checker to enh(agents): add disallowedTools to read-only verifier/auditor agents (Claude Code) #767 Group B, so the security posture change gets its own review. This PR keeps the checker in Group A. Could you confirm that route, or say if you'd rather have the move here?
  2. Fixed. The AC5 row now checks behavior. It passes each host's output as argument 2: a transcript that contains ## VERIFICATION PASSED, then a missing file. It expects waiting before the threshold and stalled after, and marker_received only from the receipt. The "AC1 wiring" row still pins all five call sites to {receipt}. Two mutants that the old row passed now fail it: a checker site bound to {outputFile}, and a matcher that accepts the marker anywhere in a line. A watch that ignores argument 2 is now caught by this row alone.
  3. Kept, per your note that the counts are acceptable given the batching.
  4. Fixed. The helpers doc routes a planner ## CHECKPOINT REACHED, ## PHASE SPLIT RECOMMENDED or ## ⚠ Source Audit whose body has not arrived by the next cycle to 9a. It names the marker received and offers Retry planner before Accept. It never routes to an empty checkpoint, 9b or 9c. Step 9a's own trigger line in plan-phase.md still says "no recognized marker". I left it alone because that file is near its byte cap, and the helpers doc that step 7.99 loads states the exception.

Re-requested your review.

Verification (local, head 48ee141)
  • tests/plan-phase-stall-detection.test.cjs 62/62. Each AC5 mutant above fails the row (1 of 1).
  • emitted-attribution 140/140, emitted-ack-trailer 44/44, workflow-size-budget 133/133, benchmark-compact-content 27/27, compact-content-partition-guard 25/25.
  • npm run build:lib 0; npm run lint:ci 0.

Generated by AI during review response; reviewed and posted by a human maintainer (@davesienkowski).

@davesienkowski
davesienkowski requested a review from trek-e October 7, 2026 02:56
@trek-e

trek-e commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Maintainer decision on the open question (review finding 3, AC5): the plan-checker return-receipt write on read-only hosts must be fixed in this PR. AC5 is a must-have acceptance criterion, so it is not deferred to a follow-up and the closing keyword stays. The Major finding remains open until the checker watch works on a read-only host; the Minor and Nits from the latest review also still stand.

davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 9, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
@davesienkowski
davesienkowski force-pushed the fix/5182-stall-watch-return-receipt branch from 1b8d051 to 9441faf Compare October 16, 2026 14:13
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 16, 2026
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 16, 2026
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
davesienkowski added a commit to davesienkowski/gsd-core that referenced this pull request Oct 16, 2026
…s, AC4 boundary rows and token-level prose asserts

Review follow-up on open-gsd#5192.

- Property: gsd_return_marker matches iff the line starts with the marker
  and the next character is not [A-Za-z0-9_], with a leading BOM, a
  trailing CR and a missing final newline tolerated. Property:
  gsd_receipt_path never leaves <phase>/.gsd-returns/ for any spawn label,
  and an unsafe phase dir (quote, $ or backtick) fails closed. Each runs
  one bash invocation per generated batch, with values passed through
  files, never argv.
- AC4 rows now cover limit-1 / limit / limit+1 (599 / 600 / 601 s) against
  an injected clock (`date +%s` stubbed); no open-gsd#5182 row reads the wall clock.
- Prose-contract asserts are reduced to command tokens (`rm -f
  "{receipt}"`, `gsd_return_marker "{receipt}"`, `<return_receipt>`,
  printf) plus one keyword, so rewording the surrounding prose does not
  break the suite.
@davesienkowski

Copy link
Copy Markdown
Collaborator Author

@trek-e I pushed 9441faf05, rebased onto next @ 46a119f06.
The only conflict was tests/fixtures/compact-content-benchmark-baseline.json, in the
plan-phase row and the totals, at the three commits that touch it. I did not merge its
hunks by hand: at each stop I took next's copy and regenerated it with
node scripts/benchmark-compact-content.cjs --write against that commit's tree, so the
rows reflect both next and this change, and --check reports it up to date at the
head. git range-diff shows the other 12 commits unchanged and the three conflicted
ones differing only in that file; commit messages and the Emitted-Drift-Ack-Growth
trailers are unchanged, and git merge-tree --write-tree origin/next HEAD exits 0 with
no conflict. plan-phase.md is still 98,280 bytes. Local, empty HOME/GSD_HOME:
build:lib 0; stall-detection, compact-content, size-budget, codex-config and install
files 1169/1169; emitted-attribution and emitted-ack-trailer with
GSD_EMITTED_BASE=origin/next 184/184; agent-frontmatter and the AGENTS.md parity files
2669 pass, 0 fail, 51 skipped (env-gated opencode smoke); lint:ci 0. No other change
in this push.

Generated by AI; reviewed and posted by a human maintainer (@davesienkowski).

@davesienkowski

Copy link
Copy Markdown
Collaborator Author

@trek-e the branch is conflict-free: 9441faf05 was rebased onto next @ 46a119f06 on 10-16, and git merge-tree against today's next (19f2d5e8e, two commits ahead) is clean. Since my 10-11 and 10-12 replies cite pre-rebase SHAs, the map below uses the current ones.

I pushed two commits on top; the PR head is now efb6dcb7e. 46138e494 adds a row for an in-project link at the phase dir and on the walked path. Before it, deleting the walk's [ -L ] test still passed the file, because every link row pointed outside the project. efb6dcb7e scopes the changeset's symlink sentence. A parent link that stays inside the project is accepted, as the code comment already says.

Still open: Stryker does not run on this PR, and the properties do not generate .. or links. Example rows cover both.

Re-requested your review.

Review 5477738387: finding to current commit
Finding Commit Where
Major 1 cc8308232, 9441faf05, 46138e494, efb6dcb7e stall-detection-helpers.md:199-224
Minor 1, 2, 4, 6; Nit 1, 3 cc8308232 :199, :245, :299-310, :257, :193, :253
Minor 3 eb490d50b, 2f3bbc192 plan-phase.md:961,1101,1199, chunked-planning-mode.md:78,174
Minor 5 all of the above tests/plan-phase-stall-detection.test.cjs:1458-1708
Nit 4 eb490d50b, 9441faf05 docs/{ja-JP,ko-KR}/AGENTS.md:180,224
Nit 6 rebase, 2f3bbc192 changeset backlink

Local, empty HOME/GSD_HOME: stall-detection 78/78, emitted gates 184/184, lint:ci 0.

Minor 5 is only partly addressed. The new in-project link row goes through symlinkOrSkip in tests/plan-phase-stall-detection.test.cjs, which can skip on unprivileged Windows. No Stryker run was done on this PR. The fast-check properties never generate /, .. or links; example rows cover those.

Generated by AI during triage; reviewed and posted by a human maintainer (@davesienkowski).

@trek-e trek-e left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflict with next — blocks review and merge.

git merge-tree --write-tree origin/next <PR head> against next @ f7efbf110a reports conflicts in:

  • tests/fixtures/compact-content-benchmark-baseline.json

Please bring the branch current with next and resolve the conflict(s). The substantive review resumes once the PR is mergeable. This notice is about the conflict only; it does not retire any open finding from earlier reviews.

@trek-e trek-e added the needs-review: merge-conflict Has a merge conflict that must be resolved label Oct 17, 2026
…ost transcript in the plan-phase stall watch

gsd_stall_watch grepped the whole host {outputFile} for the return markers.
On Claude Code that file is the subagent transcript, which already holds the
agent's prompt and definition snapshot (both quote the markers), so the
planner and checker waits reported marker_received at their first check. The
revision watch passed no markers, so it could only end stalled, and a host
with no output file could only ever reach stalled.

Each watched spawn (planner, checker, revision, chunked outline, chunked
per-plan) now gets a per-dispatch receipt from gsd_receipt_path, named in its
prompt as <return_receipt>; the agent writes its marker line there as its last
action. gsd_return_marker, a literal line-start match, is the one matcher used
by the watch and by step 11's checker routing. The revision watch passes
Return Markers and the agent-contracts registry). The checker writes its
receipt with one Bash printf and gains no tool; a refused write is non-fatal.

Emitted-Drift-Ack-Growth: plan-phase.md — the five-site receipt binding, the <return_receipt> prompt lines, the revision markers and receipt-based checker routing (open-gsd#5182); the bytes are the fix itself.
Emitted-Drift-Ack-Growth: gsd-planner.md — the return-receipt rule and the ## REVISION COMPLETE marker declaration (open-gsd#5182); the bytes are the fix itself.
Emitted-Drift-Ack-Growth: gsd-plan-checker.md — the return-receipt rule, written with the Write tool as its only write (open-gsd#5182); the bytes are the fix itself.
… not argv, so Windows MSYS cannot strip the quote

Windows conformance shard 1/3 on PR open-gsd#5192 failed the gsd_receipt_path
unsafe-phase-dir row: the "ph'ase" argument reached bash as "phase"
because Git Bash's MSYS layer re-splits and unescapes argv. The helper
was right to accept "phase". Read the value from a file inside the
script instead, the same transport hazard extractStallHelpersBash()
documents.
…and make the receipt helpers locale-proof

Review follow-up on open-gsd#5192.

- Rebased onto origin/next 47b22b8. next grew plan-phase.md to 98,051 LF
  bytes (open-gsd#5171), so the receipt binding is now stated once at step 7.99
  (`{receipt}` = gsd_receipt_path "${PHASE_DIR}" <spawn>) and each watch
  site carries only its spawn label. Step 11 keeps its original
  marker_received bullets; the rule that the routing marker comes from
  gsd_return_marker over {receipt} moves into the helpers doc that 7.99
  loads. plan-phase.md is 98,273 LF bytes (cap < 98,300).
- gsd_receipt_path and gsd_return_marker spell out their ASCII word
  characters instead of bracket ranges, so no locale (bash 3.2 has no
  globasciiranges) can widen them.
- gsd_return_marker drops its trailing-CR strip: a CR after the marker is
  already a non-word character, so the strip changed no result (a mutation
  of it survived the new property test).
- The compact-content benchmark baseline is regenerated against the rebased
  plan-phase.md (`benchmark-compact-content.cjs --write`), not hand-merged.
…s, AC4 boundary rows and token-level prose asserts

Review follow-up on open-gsd#5192.

- Property: gsd_return_marker matches iff the line starts with the marker
  and the next character is not [A-Za-z0-9_], with a leading BOM, a
  trailing CR and a missing final newline tolerated. Property:
  gsd_receipt_path never leaves <phase>/.gsd-returns/ for any spawn label,
  and an unsafe phase dir (quote, $ or backtick) fails closed. Each runs
  one bash invocation per generated batch, with values passed through
  files, never argv.
- AC4 rows now cover limit-1 / limit / limit+1 (599 / 600 / 601 s) against
  an injected clock (`date +%s` stubbed); no open-gsd#5182 row reads the wall clock.
- Prose-contract asserts are reduced to command tokens (`rm -f
  "{receipt}"`, `gsd_return_marker "{receipt}"`, `<return_receipt>`,
  printf) plus one keyword, so rewording the surrounding prose does not
  break the suite.
… functions only, without the per-call Node preamble

Every open-gsd#5182 row ran the whole helpers fence per bash call, including the
runtime-locator preamble and two `gsd_run query config-get` Node spawns.
On Windows CI shard 1/3 (run 37148401740) that put the open-gsd#5182 block at
169.5 s inside a concurrent chunk, and a sibling file
(execute-phase-decimal-arithmetic) then hit `spawnSync bash ETIMEDOUT`.

The open-gsd#5182 rows now slice the fence from gsd_stall_should_recover() on
(extractStallFunctionsBash), so each call is one bare bash process
running the same shipped functions. Config resolution stays covered by
the open-gsd#2650 rows, which still run the full fence. Locally the block drops
from about 16 s to 2.1 s. Both properties still fail against the four
deliberate helper breaks.
… runs with identical inputs

Once every call site bound {receipt}, the row's bind() gave both "hosts"
the same receipt, so its comparison could not fail (review 5436473886,
Minor 2). The row now gives argument 2 each host's output, a transcript
that contains the checker marker and a missing file, and expects waiting
before the threshold, stalled past it, and marker_received only from the
receipt. The call-site binding stays with the "AC1 wiring" row. A watch
that ignores argument 2 is now caught by this row alone.
… body never arrives fails closed to 9a

The receipt carries only the marker. A receipt-routed CHECKPOINT
REACHED, PHASE SPLIT RECOMMENDED or Source Audit whose body has not
arrived by the next cycle now goes to 9a, naming the marker received and
offering Retry planner before Accept, instead of an empty checkpoint, 9b
or 9c (review 5436473886, Nit 4). This matches the checker's ISSUES
FOUND rule.
…its return receipt is written on read-only hosts

The checker wrote its stall-watch receipt with a Bash printf because it had
no Write tool. Codex derives a read-only sandbox from that tools: line and
refused the write, so a passing checker's watch ended stalled at the
threshold and AC5 ("identical on every host") did not hold for it.

The checker now declares Write, like the other report-writers (gsd-verifier,
gsd-doc-verifier, gsd-eval-auditor, gsd-ui-auditor): Claude Code denies it
only Edit and MultiEdit, Codex derives workspace-write, and its definition
states the receipt is its only write. Maintainer decision on review
5436473886 Finding 3 (issuecomment-6036744404): fixed in this PR.

The write limit is also stated unconditionally in <anti_patterns>, because
the quick, quick-batch and import spawns send no receipt. As a Write-capable
agent the checker now carries the commented hooks block and the
anti-heredoc instruction (tests/agent-frontmatter.test.cjs). The growth is
covered by this PR's existing Emitted-Drift-Ack-Growth key for
gsd-plan-checker.md (the receipt rule).
…e receipt read

gsd_receipt_path ran mkdir -p on <phase>/.gsd-returns and wrote its
.gitignore without checking for symlinks, so a checkout that commits
either one as a link sent the directory creation and the '*' write
outside the phase. It now fails closed (prints nothing, returns 1) when
.gsd-returns or .gsd-returns/.gitignore is a symlink, checked before
each write. New rows cover a linked directory, a linked .gitignore and
a dangling .gitignore link; the link targets stay untouched.

gsd_return_marker read the agent-written receipt with no limit. It now
reads at most the first 64 lines through an in-loop counter (no extra
process per call), keeping the BOM, missing-final-newline and
first-match behavior. A new row shows a marker on line 1 or 64 is found
and one only on line 65 is not.

Answers review Minor 1 (symlinked .gsd-returns) and Nit 5 (unbounded
receipt read).
…atch helpers

kimi-code dispatches -planner to its plan built-in and -checker to
explore, and neither can write files (runtime-aware-dispatch.md), so no
return receipt is written there. The helpers doc now says so beside the
plan-checker receipt rule: the wait ends on the runtime's completion
result, or as stalled at the threshold.

Answers review Finding 3 (kimi-code known limit).
…h and harden the receipt read

gsd_receipt_path checked for a symlink only at .gsd-returns and its
.gitignore, so a checkout that commits the phase dir, .planning or
.planning/phases as a link sent mkdir -p and the '*' .gitignore write
outside the project, and the printed receipt path told the planner and
checker to Write there. It now walks from PHASE_DIR up to its nearest
existing ancestor, refuses any walked component that is a link (tested
before -e, so a dangling one counts), resolves that ancestor with
cd -P/pwd -P and requires it to sit under the orchestrator's pwd -P, then
re-verifies the created .gsd-returns physically after mkdir. Both sides go
through pwd -P, so the macOS /var -> /private/var temp link and Git Bash's
C:/ -> /c/ mapping compare equal. A refused receipt prints nothing and
returns 1; the watch then ends on the runtime's completion result or as
stalled. Only static, checked-in links are in scope (no check-then-act
race guard). It also refuses a PHASE_DIR holding any control character
(newline, tab, CR, ESC, DEL) or < or >, which would land in the
<return_receipt> prompt line.

gsd_return_marker now ignores a receipt that is a symlink, reads at most
the first 4096 bytes (head -c) as well as the first 64 lines, so one huge
line no longer costs tens of seconds per watch cycle (1,000,000 bytes
took 35 s on bash 5.2 and 73-100 s on bash 3.2 before; 0 s now), and
counts a marker only when it is followed by the end of the line, a space,
tab, CR, VT, FF or a colon, so "## PLAN COMPLETE-x" is not
"## PLAN COMPLETE". The colon is kept on purpose: the planner returns
its Source Audit heading with ": Unplanned Items Found" after the
marker the watch passes.

gsd_stall_watch passed its artifact glob to find unquoted, so a space in
PHASE_DIR split it and a fresh *-PLAN.md read as stalled. The directory
part is now quoted and only the basename is globbed, under nullglob
(restored after) with IFS empty; no match gives an empty list and
artifact_fresh false. The call contract is unchanged.

Tests: rows for a symlinked phase dir (relative, absolute, a child of
it), a phase dir under a .planning linked outside the project, dangling
.gsd-returns and phase-dir links, a symlinked receipt, the 4096-byte cap
and a 1,000,000-byte line under the 15 s probe bound, the marker
boundary (-x, ., a letter, space, tab, CR, colon) and a spaced phase dir
with a fresh plan. The unsafe-dir row and property now read values with
read -d '' and cover newline, tab, CR, ESC, DEL, < and >. The receipt-path
property no longer exempts every non-ASCII label from the stem check
(!ascii ||): ASCII labels must match exactly and non-ASCII labels match
with runs of _ collapsed, since bash replaces per character in a UTF-8
locale and per byte in C. Looping rows assert their executed count.

Answers review 5477738387 Major 1, Minor 1, Minor 2, Minor 4, Minor 6,
Nit 1 and Nit 3.
…the no-receipt outcome

The helper doc said the orchestrator removes the receipt "after routing",
but no watch site carried the command, so stalled and Retry paths left
orphan .gsd-returns files. Each of the five watch rules (planner, checker,
revision, chunked outline, chunked per-plan) now ends with
`rm -f "{receipt}"` after routing any result, stalled and Retry included.

The helper doc now states the other half of Minor 3: a checker with no
receipt touches no *-PLAN.md, and neither does a planner before its first
PLAN.md, so that watch can only end on the completion result or as
stalled at the threshold. There is no heartbeat protocol.

Tests: a row per watch site asserting the cleanup clause follows its
gsd_stall_watch call (5 sites, count asserted), a doc row for the
no-receipt outcome, and a *-PLAN.md freshness row just inside and just
outside the interval. The two wiring rows fail on the previous docs.

Also: Write added to the plan-checker row of docs/ja-JP and docs/ko-KR
AGENTS.md (matching docs/AGENTS.md), the changeset states that no receipt
is created for a symlinked or control-character phase path, and the
compact-content baseline is refreshed.

Answers review 5477738387 Minor 3, Minor 5 (remaining rows), Nit 4 and
Nit 6.

The plan-phase.md growth is covered by this PR's existing
Emitted-Drift-Ack-Growth key for plan-phase.md (a second key with a
different reason would be an ambiguous duplicate).
…t is refused

gsd_receipt_path now fails closed on an unsafe or symlinked phase dir,
but the helpers doc did not say what the orchestrator does then. It now
says: omit the <return_receipt> line and pass "" as the watch's second
argument, so the wait ends on the runtime's completion result or as
stalled. A test pins the doc line and that an empty receipt reads
waiting before the threshold and stalled after it, never a match.

The changeset's issue backlink moves to the end of the entry (review
5477738387 Nit 6).
…nd bind the receipt under either toggle

A `..` after a phase-dir component that does not exist yet passed the
nearest-existing-ancestor check (the project itself), so mkdir -p resolved
it and created `.gsd-returns` outside the project before the post-mkdir
check refused the path. gsd_receipt_path now refuses any `..` component
with the other unsafe-input checks, before touching the filesystem; a row
covers the relative, absolute and existing-ancestor forms and asserts
nothing is created outside.

The helpers doc bound `{receipt}` only "before each watched Agent() call"
while also saying the toggle-false prompt still carries the receipt line;
it now binds before every call that carries the line, under either toggle.
The ja-JP and ko-KR plan-checker tools rows gain Write, as the English row
did.
…ect link rows

Every existing symlink row links outside the project, so the physical
containment check alone refuses it; deleting the walk's `[ -L "$a" ]`
test left the file at 77/77. A link that stays inside the project is
refused only by that test. The new row covers a phase dir that is itself
an in-project link and a walked link back to the project followed by a
missing child, and asserts nothing is created through either. It also
accepts an existing phase dir reached through an in-project parent link,
which pins the scope of the claim. RED on the mutant, GREEN on the head.
…eceipt helper checks

The changeset said no receipt is created when the phase directory or one
of its parents is a symlink. A parent link that stays inside the project
is accepted: the helper refuses a linked phase dir, a link on the walked
missing components, and any path that resolves outside the project. The
sentence now says that, and names the `..` refusal.
@davesienkowski
davesienkowski force-pushed the fix/5182-stall-watch-return-receipt branch from efb6dcb to 35bf07e Compare October 18, 2026 01:34
@davesienkowski
davesienkowski requested a review from trek-e October 18, 2026 01:35
@davesienkowski

Copy link
Copy Markdown
Collaborator Author

@trek-e rebased onto next @ 755bf26f0. The rebase stopped only on tests/fixtures/compact-content-benchmark-baseline.json; I regenerated it with --write at each of the three commits that change it (deltas unchanged). git merge-tree reports no conflicts, and outside that file the PR's added and removed lines are unchanged.

Line refs in my 10-16 finding map moved: plan-phase.md 961/1101/1199 -> 977/1117/1215; chunked-planning-mode.md 174 -> 178.

One disclosure I missed: on OpenCode and Kilo, Write/Edit/MultiEdit collapse to one edit permission, so the plan-checker's Write grant is full edit there, as for the other Group B agents. Your 10-10 security acceptance covered Codex only and predates #5338. Disclosed in the PR body; docs/AGENTS.md and stall-detection-helpers.md now say so (35bf07e). Re-requested your review.

Generated by AI; reviewed and posted by a human maintainer (@davesienkowski).

@trek-e trek-e left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — fix(#5182): watch a GSD-owned return receipt instead of the host transcript in the plan-phase stall watch

Head reviewed: 35bf07e3754e3235df17064c2af0f82bc31e5ed6 (gh pr view 5192 --repo open-gsd/gsd-core --json headRefOid,mergeable,mergeStateStatus,reviewDecision this turn: MERGEABLE, BEHIND, CHANGES_REQUESTED). Reviewer is isolated (did not author). Nothing was executed with node --test; CI is the executor.

Summary

The substance is largely sound and the earlier trek-e findings are resolved at this head: the symlinked-ancestor Major (containment via cd -P/pwd -P, walk-up link test, .. refusal), control characters and </> in PHASE_DIR, symlinked receipt, rm -f "{receipt}" at all five sites, quoted glob expansion, head -c 4096, word-boundary rule, ja/ko tools rows. The head is BEHIND next (clean per git merge-tree), CI is fully green. What remains is one security-posture decision that was only accepted for Codex, several test-quality items the author already disclosed as open, a silent-degradation path in the containment check, one unhandled body-less route, and nits. Zero tolerance applies: changes requested. Blocker 0, Major 1, Minor 7, Nit 4.

Classification & gate compliance

Gate Result Evidence
Track / label PASS #5182 labels bug, area: workflow, confirmed-bug -> Bug-Fix track
Scope vs approved issue PASS Agent Brief names planner, checker, revision; the two chunked sites are disclosed in the brief thread and pinned by the existing five-site test. #2699 and #3923 untouched
Closing keyword PASS Fixes #5182 in body; closingIssuesReferences = #5182
Not a draft; title; branch PASS isDraft false; fix(#5182): ...; branch fix/5182-stall-watch-return-receipt
Commits PASS 18 conventional fix/test/docs(#5182) commits
PR template PARTIAL "Fix PR" template used; "All existing tests pass" is unchecked with a stale local-failure narrative and stale head sha 18695364a (Nit 2)
Changeset PASS .changeset/lively-sloths-forage.md: type: Fixed, pr: 5192, bold lead, ends with (#5182)
Docs gate PASS (Fixed type is exempt) docs/AGENTS.md updated anyway; ja-JP/ko-KR partially (Nit 1)
Inventory N/A No file added under references/ or workflows/
ADR gate PASS (judgment) Body names ADR-1239, 1016, 5057, 857 P6, 1610, 1235, 3574 and #767; no conflicting ADR found. recall_decision unavailable (see Memtrace Evidence)
CONTEXT.md NIT No entry for the new return-receipt concept or the #767 Group B move of plan-checker (Nit 3)
Labels NOTE PR still carries needs-review: merge-conflict although mergeable is MERGEABLE; bot-owned, stale

Must-Have Acceptance Checklist (issue #5182, verbatim)

# Criterion Result
1 "Markers present only in a prompt, agent definition or transcript yield waiting." PASS. gsd_stall_watch (stall-detection-helpers.md) reads only the receipt through gsd_return_marker; rows AC1, AC5 pass a transcript holding the marker text and expect waiting. Verified by reading and by probing gsd_return_marker directly (## ISSUES FOUND-x -> empty)
2 "A checker returning issues routes to the revision step, never to the pass step." PASS when the receipt or result arrives. Routing is orchestrator prose (plan-phase.md step 11); a receipt-only ISSUES FOUND with no body fails closed to 11a
3 "A finished revision yields a success result within one poll interval." PASS. Revision watch now passes ## REVISION COMPLETE and ## REVISION_CONFLICT. Body-less REVISION_CONFLICT is not covered (Minor 2)
4 "A spawn that never signals completion yields stalled after the threshold." PASS. Boundary rows at 599/600/601 s with a stubbed clock
5 "Results are identical on Claude Code and on a host with no output file." PARTIAL, accepted limits: kimi-code cannot write a receipt (documented, stall-detection-helpers.md "Known limit, kimi-code"); a refused receipt (symlink, .., outside cwd) degrades to completion-result-or-stalled (Minor 1). Not exercised live (Minor 7)
6 "Regression tests fail on current next first." PASS by construction (helpers and {receipt} binding do not exist on base; AC1/AC5 rows would see marker_received from the transcript on base). I did not execute them; PR body reports tests 60 / pass 40 / fail 20 against untouched next

Root-cause / functional verdict

Root cause (one sentence): gsd_stall_watch decided completion by grep -qF over the host output file, which on Claude Code is a transcript that already contains the agent's prompt and definition quoting the markers, and the revision site passed no markers at all, so the success exit was unreachable.

Verdict: root-cause fix. The input to the decision is replaced with a GSD-owned per-dispatch file, and gsd_return_marker is the single owner of "which marker was returned" for both the watch and step 11 routing (one owner, no parallel parser). gsd_stall_should_recover is unchanged, correctly. I ran the shipped gsd_receipt_path and gsd_return_marker from the head under bash in a scratch directory outside the repo: relative phase dir accepted with sanitized label (a b -> a_b), .gitignore written, marker/word-boundary behavior as specified.

Blast radius

  • Runtime-visible: READONLY_AGENT_DISALLOWED_TOOLS['gsd-plan-checker'] (src/runtime-artifact-conversion.cts:4067): CodeGraph impact = 3 symbols, sole consumer applyAgentFrontmatterExtensions (:4099) via injectDisallowedToolsFrontmatter (Claude-only). Codex sandbox derivation moves the checker from read-only to workspace-write. The checker is spawned from quick, quick-batch and import as well, and now holds Write in all of them (the limit is prose in <anti_patterns>).
  • Tool-grant widening by host (see Major 1): Claude (Edit/MultiEdit still denied), Codex (workspace-write), OpenCode and Kilo (Write -> edit, src/runtime-artifact-conversion.cts:321; buildOpencodeAgentPermissionBlock emits denials only, and disallowedTools injection is Claude-only, so nothing re-denies), Copilot (Write: 'edit', bin/install.js:556).
  • Workflow surface: five watch sites in plan-phase.md and chunked-planning-mode.md; gsd_stall_watch second argument changes meaning (host file -> receipt). No callers outside those five (the existing five-site test pins it).
  • Shell-in-markdown is not graph-indexed; risk rated medium on reading.

Co-change completeness

  • Present: agents (planner, checker), agent-contracts.md, docs/AGENTS.md, ja-JP/ko-KR tools rows, install/Codex/descriptor-parity tests, compact-content baseline fixture, changeset.
  • agents/*.compact.md for planner/checker: not tracked in the tree (only gsd-eval-planner.compact.md exists), no gap.
  • Absent: ja-JP/ko-KR "Principle of Least Privilege" line still says checkers are read-only (Nit 1); CONTEXT.md (Nit 3).

Test quality

  • Fail-first: AC1 (tests/plan-phase-stall-detection.test.cjs:1032) and AC5 (:1116) are the discriminating rows; on base they get marker_received from the transcript where waiting is expected.
  • Persisted artifact: receipt rows assert the file system (treeOf(outside) unchanged, .gitignore content, victim file byte-identical). Good.
  • Controls: symlinkOrSkip rows carry a control (gsd_return_marker row asserts the target itself carries the marker). Good.
  • Parsers/budgets: two fast-check properties exist (:1209, :1235) with an independent oracle; numRuns 8-12 each over batches; generators never produce /, .. or links (Minor 3).
  • Boundaries: 64-line cap has 64/65; 600 s threshold has 599/600/601; the 4096-byte cap does not (Minor 4).
  • Wall clock: :1693-1709 uses Date.now() (Minor 5).
  • Prose asserts remain (Nit 4).

Memtrace Evidence

  • get_impact: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT).
  • get_symbol_context: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT).
  • recall_decision: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT). The ADR/decision check was not evaluated against recorded decisions; the ADR list above is the PR body's own claim, not verified.
  • find_code_review_issues: UNAVAILABLE — Memtrace MCP failed to connect (ENOENT). The deterministic review pass was not run and is not reported clean.
  • _graph_state: no Memtrace graph state available.

CodeGraph / manual evidence used instead: codegraph impact READONLY_AGENT_DISALLOWED_TOOLS (3 symbols, one consumer); codegraph callers injectDisallowedToolsFrontmatter (1 caller, Claude-only path); codegraph node spans of src/runtime-artifact-conversion.cts:135-364 (Kimi/Kilo/OpenCode permission maps and buildOpencodeAgentPermissionBlock) and of the full test file from 900 to 1711; git diff origin/next...refs/prrev/5192 on every changed non-test file; direct execution of the head's gsd_receipt_path and gsd_return_marker in a scratch directory outside the repo; worktree at the head for index freshness. No test was executed.

Findings

Source tags: [fact] = verified on disk or by running the shipped helper; [judgment] = reviewer assessment.

Major

  1. Major 1 — [judgment, built on verified facts] The plan-checker's Write grant widens to full edit on OpenCode, Kilo and Copilot, and the maintainer's security acceptance covered Codex only. agents/gsd-plan-checker.md:4 adds Write; src/runtime-artifact-conversion.cts:321 maps Write and Edit and MultiEdit to the same OpenCode edit permission, :343-364 emits denials only, and disallowedTools is injected for Claude only (:4047), so nothing re-denies Edit there; Kilo (:241) and Copilot (bin/install.js:556) map Write to edit the same way. The checker reads contributor-controlled PLAN.md and source and holds Bash. The PR body states "The maintainer's 10-10 security acceptance covered Codex only and predates #5338", and the PR discloses OpenCode and Kilo but not Copilot or the other tools:-deriving hosts (Antigravity and others were not audited here). The "receipt is the only write" limit is prose on all of them. Fix: obtain explicit maintainer acceptance for each widened host in the PR thread, or scope the grant (for example keep the checker's receipt via the host's completion result on hosts where Write cannot be separated from edit), and extend the disclosure in docs/AGENTS.md:885 and the changeset to every host whose conversion maps Write to a broader permission.

Minor

  1. Minor 1 — [fact] Containment is anchored to the orchestrator's cwd and a refusal is silent. stall-detection-helpers.md gsd_receipt_path (the root=$(pwd -P) and case "$phys/" in "$root"/* lines). Probed: with cwd p/proj, an absolute phase dir p/other/ph returns rc=1 and prints nothing. A legitimate absolute PHASE_DIR outside the cwd (multi-project GSD_PROJECT workspaces per docs/CONFIGURATION.md:2279, or an orchestrator whose cwd is a subdirectory) loses its receipt with no diagnostic and reverts to the original stalled-only behavior. Fix: anchor on the project root the workflow already resolves, and emit a one-line stderr reason on refusal so the orchestrator can name it; add a row for an absolute phase dir under the project root but outside the cwd.
  2. Minor 2 — [fact on code path, judgment on effect] A receipt-only ## REVISION_CONFLICT is not in the fail-closed list. stall-detection-helpers.md lists ISSUES FOUND, CHECKPOINT REACHED, PHASE SPLIT RECOMMENDED and Source Audit as body-needing routes; plan-phase.md (revision conflict block, after the step 12 watch rule) exports CONFLICT_DIMENSION/_PLAN/_PROPERTY/_CONSTRAINT/_ALTERNATIVES from the return body, which the receipt does not carry. With the receipt arriving first, the conflict record written to REVIEWS.md would have empty fields. Fix: add REVISION_CONFLICT to the list (route to the existing revision fallback and offer Retry planner) and add a row beside the ISSUES FOUND fail-closed row.
  3. Minor 3 — [fact, author-disclosed] Test gaps from the prior Minor 5 remain open. tests/plan-phase-stall-detection.test.cjs:1209-1284 fast-check properties never generate /, .. or links (example rows cover them); :1333-1344 symlinkOrSkip skips on EPERM/EACCES/ENOTSUP, so every symlink row (:1346-1381, :1458-1572) passes with zero assertions on an unprivileged Windows runner; Stryker does not run on this PR so the 80% kill threshold is unmeasured. Fix: add path-shaped generators (segments from a, .., ., link) with a oracle for the refusal set, and make the Windows skip fail loudly where CI is known to have the privilege, or add a runner-side privilege check.
  4. Minor 4 — [fact] The 4096-byte cap has no limit-1/limit/limit+1 rows. :1584-1587 uses 4000 and 5000 bytes only. Fix: place the marker so it ends at byte 4095, 4096 and 4097 and assert found / found / not found (a marker straddling the cut must not match).
  5. Minor 5 — [fact] Wall-clock use in the freshness test. :1699-1708 sets mtimes from Date.now() and relies on find's real clock, and :1580 asserts the read "must finish within PROBE_TIMEOUT_MS". CLAUDE.md: "Do not assert on wall-clock time." Fix: put a find shim ahead of PATH that reports a controlled age, and assert the cap by output (the marker is not read) rather than by elapsed time.
  6. Minor 6 — [fact] gsd_return_marker strips a BOM on every line, not only the first. stall-detection-helpers.md line="${line#$'\xef\xbb\xbf'}" runs inside the loop, so a BOM at the start of line 2..64 is also tolerated; the property oracle (:1203-1207) matches that, so the property cannot catch it. Low impact (a BOM mid-file does not occur in a Write-tool receipt) but it contradicts the stated "leading BOM" contract. Fix: strip only when n -eq 1, and add a row with a BOM on line 2.
  7. Minor 7 — [judgment] No live-runtime evidence. PR body: "Runtimes tested: Other: runtime-neutral bash helper exercised directly; no live runtime session was run". The mechanism the fix depends on (a background planner or checker writing its receipt with Write as its last action, and the orchestrator binding {receipt} across tool calls) is exercised only through shell-stubbed unit rows; the issue reproduced on live Claude Code. Fix: one recorded live Claude Code plan-phase run (planner, checker returning ISSUES FOUND, revision) showing the three receipts and the routing, attached to the PR.

Nit

  1. Nit 1 — [fact] docs/ja-JP/AGENTS.md:425 and docs/ko-KR/AGENTS.md:425 still read "checkers are read-only (no Write/Edit)" while the same files' tools row (edited here) now lists Write. English docs/AGENTS.md:885 was updated. Update the line or revert the partial edit.
  2. Nit 2 — [fact] PR body is stale: cites head 18695364a, leaves "All existing tests pass (npm test)" unchecked with a local-failure narrative, and lists Linux only; CI at 35bf07e37 is green. Refresh the body to the current head.
  3. Nit 3 — [judgment] CONTEXT.md carries no entry for the return-receipt concept, gsd_receipt_path / gsd_return_marker as the single owner of "which marker did the agent return", or plan-checker's #767 Group B move. Gate: "CONTEXT.md updated when vocabulary or modules change". CONTEXT.md has no stall-detection entry at all, so precedent is weak.
  4. Nit 4 — [fact] Prose-contract asserts remain after the stated reduction ("Prose-contract rows are deliberately reduced to command tokens and one keyword", :1416): /only write/, /the Write tool/, /Group B report-writer/, doesNotMatch(/refuses that write/), doesNotMatch(/After routing, the orchestrator runs/) (:1405-1423, :1671), /no heartbeat/i (:1670). They read workflow and agent markdown by regex (file carries the source-text-is-the-product exemption), but they pin wording, not behavior.
  5. Nit 5 — [fact] mergeStateStatus is BEHIND: origin/next is 274f8cae13d03ad80e18959b135b0e51b33329e8; git merge-tree --write-tree origin/next refs/prrev/5192 exits clean (tree 999a96f39f03dac67ec2b5679ead27fdf9226f89). The branch has needed a rebase on tests/fixtures/compact-content-benchmark-baseline.json at each of its last five pushes; regenerate that fixture again with node scripts/benchmark-compact-content.cjs --write after the next rebase.

CI (fresh this turn, head 35bf07e3754e3235df17064c2af0f82bc31e5ed6)

  • 50 check runs, collapsed to latest-per-name: 43 SUCCESS, 7 SKIPPED, 0 failing, 0 queued, 0 pending.
  • Non-success non-skipped rows by the prescribed jq/awk collapse: none.
  • action_required workflow runs for this head sha: none.
  • reviewDecision: CHANGES_REQUESTED (trek-e, 2026-10-10 on 8ce426bde; the 2026-10-17 COMMENTED review on efb6dcb7e was a merge-conflict notice that retires nothing). mergeStateStatus BEHIND.
  • No red to attribute.

Verdict

Changes requested. Blocker 0, Major 1, Minor 7, Nit 5. CI is green and the branch is mergeable; the open items are Major 1 (maintainer decision on the Write grant for OpenCode, Kilo and Copilot), the silent containment degradation (Minor 1), the body-less REVISION_CONFLICT route (Minor 2), the disclosed and still-open test gaps (Minor 3 to Minor 5), and the remaining minors and nits above.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: workflow Phase execution, ROADMAP, STATE.md needs-review: merge-conflict Has a merge conflict that must be resolved review: changes requested PR reviewed — changes required before merge

Projects

None yet

2 participants