From 791eb870c2b7d853ec4fc131bb0a8802dba0114e Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 30 Aug 2026 23:46:44 +0000 Subject: [PATCH] =?UTF-8?q?fix(ship):=20Step=2018=20marks=20the=20document?= =?UTF-8?q?-release=20subagent=20spawned=20=E2=80=94=20env=20prefix=20+=20?= =?UTF-8?q?auto-choose=20prompt=20(#2733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dispatch prompt now (1) frames the run as a SPAWNED subagent whose LAST line is machine-parsed, (2) instructs prefixing the preamble's gstack-skill-start invocation with GSTACK_SESSION_KIND=spawned on the same command line (template bash blocks don't share exports), and (3) resolves every AUQ gate to auto-choosing the recommended option, conservative on no-recommendation, never destructive. The JSON contract gains a required "decisions" array (auto-chosen gates, printed to the ship console — never embedded in the public PR body) and a placement clause so the skill's own doc-health summary stops competing with the LAST-line JSON. Tripwire pins added; codex/factory goldens refreshed. Co-Authored-By: Claude Fable 5 --- ship/sections/pr-body.md | 15 ++++++++++----- ship/sections/pr-body.md.tmpl | 15 ++++++++++----- test/fixtures/golden/codex-ship-SKILL.md | 15 ++++++++++----- test/fixtures/golden/factory-ship-SKILL.md | 15 ++++++++++----- test/ship-document-release-dispatch.test.ts | 14 ++++++++++++++ 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/ship/sections/pr-body.md b/ship/sections/pr-body.md index 742ba0c57..91ca76399 100644 --- a/ship/sections/pr-body.md +++ b/ship/sections/pr-body.md @@ -2,19 +2,23 @@ ## Step 18: Documentation sync (via subagent, before PR creation) -**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. +**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. The dispatch prompt marks the subagent session as spawned (`GSTACK_SESSION_KIND=spawned`) so document-release's interactive gates auto-choose their recommended options instead of prose-stopping — a prose-STOP inside the subagent breaks the parent's LAST-line JSON parse and drops the Documentation section (#2733). **Sequencing:** This step runs AFTER Step 17 (Push) and BEFORE Step 19 (Create PR). The PR is created once from final HEAD with the `## Documentation` section baked into the initial body. No create-then-re-edit dance. **Subagent prompt:** -> You are executing the /document-release workflow after a code push. Read the full skill file `${HOME}/.claude/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. +> You are executing the /document-release workflow after a code push, as a SPAWNED subagent: no human reads your output mid-run, and only the LAST line of your response is machine-parsed by the parent /ship session. Read the full skill file `${HOME}/.claude/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. > -> After completing the workflow, output a single JSON object on the LAST LINE of your response (no other text after it): -> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":""}` +> Session marking: when the skill's Preamble has you run `gstack-skill-start`, prefix that exact command with `GSTACK_SESSION_KIND=spawned ` on the same command line (e.g. `GSTACK_SESSION_KIND=spawned "$_SS" --skill "document-release" ...`) — bash blocks run in separate shells, so an exported variable from an earlier block does NOT persist; the prefix must ride the invocation itself. The preamble will then echo `SESSION_KIND: spawned` and `SPAWNED_SESSION: true`. +> +> Decision gates: at EVERY decision point in the workflow (risky doc updates, CHANGELOG fixes and voice rewrites, narrative contradictions, TODO updates, the VERSION-bump question, doc-review apply decisions), do NOT call AskUserQuestion and do NOT stop to render a prose decision brief — auto-choose the RECOMMENDED option and continue; where the skill says "always use AskUserQuestion", that resolves to auto-choosing the recommendation in this spawned session. If no option is marked recommended, take the most conservative choice (skip/defer). Never auto-choose a destructive or irreversible option — take the conservative non-destructive choice instead. Never end your response waiting for an answer. Record each auto-chosen decision as one line in the `decisions` array of the final JSON. +> +> After completing the workflow, include the skill's doc health summary in your response body, then output a single JSON object on the LAST LINE of your response (no other text after it): +> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":"","decisions":[""]}` > > If no documentation files needed updating, output: -> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null}` +> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null,"decisions":[]}` **Parent processing:** @@ -22,6 +26,7 @@ 2. Store `documentation_section` — Step 19 embeds it in the PR body (or omits the section if null). 3. If `files_updated` is non-empty, print: `Documentation synced: {files_updated.length} files updated, committed as {commit_sha}`. 4. If `files_updated` is empty, print: `Documentation is current — no updates needed.` +5. If `decisions` is non-empty, print `Doc-sync auto-decisions:` followed by each entry on its own line — console transparency for the gates the subagent auto-chose. Treat an ABSENT `decisions` key as an empty array (older installed skills). `decisions` is never embedded in the PR body. **If the subagent fails or returns invalid JSON:** Print a warning and proceed to Step 19 without a `## Documentation` section. Do not block /ship on subagent failure. The user can run `/document-release` manually after the PR lands. diff --git a/ship/sections/pr-body.md.tmpl b/ship/sections/pr-body.md.tmpl index dbc087e5b..5f47d4e56 100644 --- a/ship/sections/pr-body.md.tmpl +++ b/ship/sections/pr-body.md.tmpl @@ -1,18 +1,22 @@ ## Step 18: Documentation sync (via subagent, before PR creation) -**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. +**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. The dispatch prompt marks the subagent session as spawned (`GSTACK_SESSION_KIND=spawned`) so document-release's interactive gates auto-choose their recommended options instead of prose-stopping — a prose-STOP inside the subagent breaks the parent's LAST-line JSON parse and drops the Documentation section (#2733). **Sequencing:** This step runs AFTER Step 17 (Push) and BEFORE Step 19 (Create PR). The PR is created once from final HEAD with the `## Documentation` section baked into the initial body. No create-then-re-edit dance. **Subagent prompt:** -> You are executing the /document-release workflow after a code push. Read the full skill file `${HOME}/.claude/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. +> You are executing the /document-release workflow after a code push, as a SPAWNED subagent: no human reads your output mid-run, and only the LAST line of your response is machine-parsed by the parent /ship session. Read the full skill file `${HOME}/.claude/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. > -> After completing the workflow, output a single JSON object on the LAST LINE of your response (no other text after it): -> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":""}` +> Session marking: when the skill's Preamble has you run `gstack-skill-start`, prefix that exact command with `GSTACK_SESSION_KIND=spawned ` on the same command line (e.g. `GSTACK_SESSION_KIND=spawned "$_SS" --skill "document-release" ...`) — bash blocks run in separate shells, so an exported variable from an earlier block does NOT persist; the prefix must ride the invocation itself. The preamble will then echo `SESSION_KIND: spawned` and `SPAWNED_SESSION: true`. +> +> Decision gates: at EVERY decision point in the workflow (risky doc updates, CHANGELOG fixes and voice rewrites, narrative contradictions, TODO updates, the VERSION-bump question, doc-review apply decisions), do NOT call AskUserQuestion and do NOT stop to render a prose decision brief — auto-choose the RECOMMENDED option and continue; where the skill says "always use AskUserQuestion", that resolves to auto-choosing the recommendation in this spawned session. If no option is marked recommended, take the most conservative choice (skip/defer). Never auto-choose a destructive or irreversible option — take the conservative non-destructive choice instead. Never end your response waiting for an answer. Record each auto-chosen decision as one line in the `decisions` array of the final JSON. +> +> After completing the workflow, include the skill's doc health summary in your response body, then output a single JSON object on the LAST LINE of your response (no other text after it): +> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":"","decisions":[""]}` > > If no documentation files needed updating, output: -> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null}` +> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null,"decisions":[]}` **Parent processing:** @@ -20,6 +24,7 @@ 2. Store `documentation_section` — Step 19 embeds it in the PR body (or omits the section if null). 3. If `files_updated` is non-empty, print: `Documentation synced: {files_updated.length} files updated, committed as {commit_sha}`. 4. If `files_updated` is empty, print: `Documentation is current — no updates needed.` +5. If `decisions` is non-empty, print `Doc-sync auto-decisions:` followed by each entry on its own line — console transparency for the gates the subagent auto-chose. Treat an ABSENT `decisions` key as an empty array (older installed skills). `decisions` is never embedded in the PR body. **If the subagent fails or returns invalid JSON:** Print a warning and proceed to Step 19 without a `## Documentation` section. Do not block /ship on subagent failure. The user can run `/document-release` manually after the PR lands. diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index b4140ddcf..3ec1da130 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -2283,19 +2283,23 @@ git push -u origin ## Step 18: Documentation sync (via subagent, before PR creation) -**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. +**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. The dispatch prompt marks the subagent session as spawned (`GSTACK_SESSION_KIND=spawned`) so document-release's interactive gates auto-choose their recommended options instead of prose-stopping — a prose-STOP inside the subagent breaks the parent's LAST-line JSON parse and drops the Documentation section (#2733). **Sequencing:** This step runs AFTER Step 17 (Push) and BEFORE Step 19 (Create PR). The PR is created once from final HEAD with the `## Documentation` section baked into the initial body. No create-then-re-edit dance. **Subagent prompt:** -> You are executing the /document-release workflow after a code push. Read the full skill file `${HOME}/.agents/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. +> You are executing the /document-release workflow after a code push, as a SPAWNED subagent: no human reads your output mid-run, and only the LAST line of your response is machine-parsed by the parent /ship session. Read the full skill file `${HOME}/.agents/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. > -> After completing the workflow, output a single JSON object on the LAST LINE of your response (no other text after it): -> `{"files_updated":["README.md","AGENTS.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":""}` +> Session marking: when the skill's Preamble has you run `gstack-skill-start`, prefix that exact command with `GSTACK_SESSION_KIND=spawned ` on the same command line (e.g. `GSTACK_SESSION_KIND=spawned "$_SS" --skill "document-release" ...`) — bash blocks run in separate shells, so an exported variable from an earlier block does NOT persist; the prefix must ride the invocation itself. The preamble will then echo `SESSION_KIND: spawned` and `SPAWNED_SESSION: true`. +> +> Decision gates: at EVERY decision point in the workflow (risky doc updates, CHANGELOG fixes and voice rewrites, narrative contradictions, TODO updates, the VERSION-bump question, doc-review apply decisions), do NOT call AskUserQuestion and do NOT stop to render a prose decision brief — auto-choose the RECOMMENDED option and continue; where the skill says "always use AskUserQuestion", that resolves to auto-choosing the recommendation in this spawned session. If no option is marked recommended, take the most conservative choice (skip/defer). Never auto-choose a destructive or irreversible option — take the conservative non-destructive choice instead. Never end your response waiting for an answer. Record each auto-chosen decision as one line in the `decisions` array of the final JSON. +> +> After completing the workflow, include the skill's doc health summary in your response body, then output a single JSON object on the LAST LINE of your response (no other text after it): +> `{"files_updated":["README.md","AGENTS.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":"","decisions":[""]}` > > If no documentation files needed updating, output: -> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null}` +> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null,"decisions":[]}` **Parent processing:** @@ -2303,6 +2307,7 @@ git push -u origin 2. Store `documentation_section` — Step 19 embeds it in the PR body (or omits the section if null). 3. If `files_updated` is non-empty, print: `Documentation synced: {files_updated.length} files updated, committed as {commit_sha}`. 4. If `files_updated` is empty, print: `Documentation is current — no updates needed.` +5. If `decisions` is non-empty, print `Doc-sync auto-decisions:` followed by each entry on its own line — console transparency for the gates the subagent auto-chose. Treat an ABSENT `decisions` key as an empty array (older installed skills). `decisions` is never embedded in the PR body. **If the subagent fails or returns invalid JSON:** Print a warning and proceed to Step 19 without a `## Documentation` section. Do not block /ship on subagent failure. The user can run `/document-release` manually after the PR lands. diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 25d5d7e73..4a965c535 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -2710,19 +2710,23 @@ git push -u origin ## Step 18: Documentation sync (via subagent, before PR creation) -**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. +**Dispatch /document-release as a subagent** using the Agent tool with `subagent_type: "general-purpose"`. The subagent gets a fresh context window — zero rot from the preceding 17 steps. It also runs the **full** `/document-release` workflow (with CHANGELOG clobber protection, doc exclusions, risky-change gates, named staging, race-safe PR body editing) rather than a weaker reimplementation. The dispatch prompt marks the subagent session as spawned (`GSTACK_SESSION_KIND=spawned`) so document-release's interactive gates auto-choose their recommended options instead of prose-stopping — a prose-STOP inside the subagent breaks the parent's LAST-line JSON parse and drops the Documentation section (#2733). **Sequencing:** This step runs AFTER Step 17 (Push) and BEFORE Step 19 (Create PR). The PR is created once from final HEAD with the `## Documentation` section baked into the initial body. No create-then-re-edit dance. **Subagent prompt:** -> You are executing the /document-release workflow after a code push. Read the full skill file `${HOME}/.factory/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. +> You are executing the /document-release workflow after a code push, as a SPAWNED subagent: no human reads your output mid-run, and only the LAST line of your response is machine-parsed by the parent /ship session. Read the full skill file `${HOME}/.factory/skills/gstack/document-release/SKILL.md` and execute its complete workflow end-to-end, including CHANGELOG clobber protection, doc exclusions, risky-change gates, and named staging. Do NOT attempt to edit the PR body — no PR exists yet. Branch: ``, base: ``. > -> After completing the workflow, output a single JSON object on the LAST LINE of your response (no other text after it): -> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":""}` +> Session marking: when the skill's Preamble has you run `gstack-skill-start`, prefix that exact command with `GSTACK_SESSION_KIND=spawned ` on the same command line (e.g. `GSTACK_SESSION_KIND=spawned "$_SS" --skill "document-release" ...`) — bash blocks run in separate shells, so an exported variable from an earlier block does NOT persist; the prefix must ride the invocation itself. The preamble will then echo `SESSION_KIND: spawned` and `SPAWNED_SESSION: true`. +> +> Decision gates: at EVERY decision point in the workflow (risky doc updates, CHANGELOG fixes and voice rewrites, narrative contradictions, TODO updates, the VERSION-bump question, doc-review apply decisions), do NOT call AskUserQuestion and do NOT stop to render a prose decision brief — auto-choose the RECOMMENDED option and continue; where the skill says "always use AskUserQuestion", that resolves to auto-choosing the recommendation in this spawned session. If no option is marked recommended, take the most conservative choice (skip/defer). Never auto-choose a destructive or irreversible option — take the conservative non-destructive choice instead. Never end your response waiting for an answer. Record each auto-chosen decision as one line in the `decisions` array of the final JSON. +> +> After completing the workflow, include the skill's doc health summary in your response body, then output a single JSON object on the LAST LINE of your response (no other text after it): +> `{"files_updated":["README.md","CLAUDE.md",...],"commit_sha":"abc1234","pushed":true,"documentation_section":"","decisions":[""]}` > > If no documentation files needed updating, output: -> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null}` +> `{"files_updated":[],"commit_sha":null,"pushed":false,"documentation_section":null,"decisions":[]}` **Parent processing:** @@ -2730,6 +2734,7 @@ git push -u origin 2. Store `documentation_section` — Step 19 embeds it in the PR body (or omits the section if null). 3. If `files_updated` is non-empty, print: `Documentation synced: {files_updated.length} files updated, committed as {commit_sha}`. 4. If `files_updated` is empty, print: `Documentation is current — no updates needed.` +5. If `decisions` is non-empty, print `Doc-sync auto-decisions:` followed by each entry on its own line — console transparency for the gates the subagent auto-chose. Treat an ABSENT `decisions` key as an empty array (older installed skills). `decisions` is never embedded in the PR body. **If the subagent fails or returns invalid JSON:** Print a warning and proceed to Step 19 without a `## Documentation` section. Do not block /ship on subagent failure. The user can run `/document-release` manually after the PR lands. diff --git a/test/ship-document-release-dispatch.test.ts b/test/ship-document-release-dispatch.test.ts index 638fd0124..bc2da4d3f 100644 --- a/test/ship-document-release-dispatch.test.ts +++ b/test/ship-document-release-dispatch.test.ts @@ -69,6 +69,20 @@ describe('/ship Step 18 dispatches /document-release (carve visibility)', () => expect(content).toContain('.claude/skills/gstack/document-release/SKILL.md'); expect(content).toContain('## Step 19: Create PR/MR'); expect(content).toContain('Parent processing:'); + // #2733: the dispatch marks the subagent spawned so document-release's + // AUQ gates auto-choose instead of prose-stopping (which breaks the + // parent's LAST-line JSON parse). Three layers pinned: the env marker + // prefix, the behavioral instruction, and the framing sentence. + expect(content).toContain('GSTACK_SESSION_KIND=spawned'); + expect(content).toContain('auto-choose the RECOMMENDED option'); + expect(content).toContain('as a SPAWNED subagent'); + // Auto-chosen gate decisions ride the JSON contract (console-printed by + // the parent), never the public PR body. + expect(content).toContain('"decisions"'); + const docHeading = content.indexOf('\n## Documentation\n'); + expect(docHeading, 'PR-body template must carry the ## Documentation heading').toBeGreaterThan(0); + const docSection = content.slice(docHeading, content.indexOf('\n## Test plan\n')); + expect(docSection, 'decisions must never leak into the PR-body Documentation embed').not.toContain('decisions'); } });