From 15dac66e3377abf733c94d893769f906d655d364 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 4 Nov 2026 15:50:18 -0800 Subject: [PATCH] fix(ship): make the five judge-flagged workflow spots match the code (GST-1346) (#4004) * fix(ship): make the five judge-flagged workflow spots match the code (GST-1346) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship): carry runs the pregate before its push (review fix) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ship): carry pushes early, waits for CI, then pregates; restore stage 1 wording (fix-check) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: diff-plus-blocking stops at the third fix cycle; carry order matches ship Step 1 (Step 14.5) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: changelog and TODOS Documentation: updated Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Revert "docs: diff-plus-blocking stops at the third fix cycle; carry order matches ship Step 1 (Step 14.5)" This reverts commit 5151144c0444dbe460e29815c24c9a3bb9ae9116. --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .../capy-gst-1346-ship-judge-repair-2.md | 1 + lib/ship-parts.ts | 4 +- scripts/resolvers/constants.ts | 6 +- scripts/resolvers/outside-voice-steps.ts | 24 ++--- scripts/resolvers/outside-voice.ts | 10 +- ship/SKILL.md | 100 +++++++++--------- ship/SKILL.md.tmpl | 100 +++++++++--------- ship/sections/adversarial.md | 50 ++++----- ship/sections/adversarial.md.tmpl | 12 +-- ship/sections/greptile-early.md | 16 +-- ship/sections/greptile-early.md.tmpl | 16 +-- ship/sections/tests.md | 4 +- ship/sections/tests.md.tmpl | 4 +- test/fixtures/golden/agy-ship-SKILL.md | 100 +++++++++--------- test/fixtures/golden/claude-ship-SKILL.md | 100 +++++++++--------- test/fixtures/golden/codex-ship-SKILL.md | 100 +++++++++--------- test/fixtures/golden/factory-ship-SKILL.md | 100 +++++++++--------- .../files/ship/sections/adversarial.md.json | 2 +- .../review-pins/replays/GST-1346.json | 1 + test/outside-voice-preflight.test.ts | 4 +- .../capy-gst-1346-ship-judge-repair-2.json | 4 + 21 files changed, 370 insertions(+), 388 deletions(-) create mode 100644 changelog.d/capy-gst-1346-ship-judge-repair-2.md create mode 100644 test/fixtures/review-pins/replays/GST-1346.json create mode 100644 test/size-ledger/ship/capy-gst-1346-ship-judge-repair-2.json diff --git a/changelog.d/capy-gst-1346-ship-judge-repair-2.md b/changelog.d/capy-gst-1346-ship-judge-repair-2.md new file mode 100644 index 000000000..1d2071d5f --- /dev/null +++ b/changelog.d/capy-gst-1346-ship-judge-repair-2.md @@ -0,0 +1 @@ +- **/ship's workflow now matches what its tools do, so main's backstop judge can pass.** The `ship/SKILL.md workflow` judge still failed on main at 622e8b1f (run 41884510834) because the prose and the code disagreed in five places. Step 11's Codex passes and every `CODEX_MODE` branch now follow the review plan's `DISPATCH:` lines, as /ship's driver does; they no longer say the native pass always runs. The review policy now gives one fix-loop cap. It is two rounds under `review_scope: frozen` (the review plan's stop) and Step 9.4's third fix cycle under the other scopes. Step 16 says `finish` runs stages 1–4, diffs the reviewed tree and checks each lane's receipt itself, what to pass when no reviewed tree was saved, and that a stage 5 commit of regenerated outputs is content-preserving. A Step 1 carry now says to push with the secrets scan and wait for CI before the pregate, and that it ends the run with its own result line. The ship, review and unattended docs now say `diff-plus-blocking` stops at Step 9.4's third fix cycle, as the workflow runs it, and give the same carry order. The Step 5 CI-carry push asks no consent, and Step 6.5's Greptile question says it cannot undo that push. No behavior changes. Reviewer briefs are byte-identical; the adversarial section's pin is updated under GST-1346's brief-identity check. (GST-1346) diff --git a/lib/ship-parts.ts b/lib/ship-parts.ts index 18b706f04..cb7299e65 100644 --- a/lib/ship-parts.ts +++ b/lib/ship-parts.ts @@ -39,8 +39,8 @@ export const PARTS: PartSpec[] = [ { id: 'review-9.4', file: RA, from: '## Step 9.4: Fix-First and persistence', phase: 4 }, { id: 'adversarial-11', file: ADV, from: null, phase: 4 }, { id: 'adversarial-native', file: ADV, from: '### Claude adversarial subagent (on `DISPATCH: adversarial-native`)', phase: 4 }, - { id: 'adversarial-codex', file: ADV, from: '### Codex adversarial challenge (runs whenever `CODEX_MODE` is `ready` or `unverified`)', phase: 4 }, - { id: 'adversarial-structured', file: ADV, from: '### Codex structured review (large diffs only, 200+ lines)', phase: 4 }, + { id: 'adversarial-codex', file: ADV, from: '### Codex adversarial challenge (on `DISPATCH: outside-voice`, when `CODEX_MODE` is `ready` or `unverified`)', phase: 4 }, + { id: 'adversarial-structured', file: ADV, from: '### Codex structured review (on `DISPATCH: outside-voice`, 200+ lines)', phase: 4 }, { id: 'adversarial-finish', file: ADV, from: '### Persist the review result', phase: 4 }, { id: 'adversarial-learnings', file: ADV, from: '## Capture Learnings', phase: 4 }, { id: 'documentation', file: DOCS, from: null, phase: 6 }, diff --git a/scripts/resolvers/constants.ts b/scripts/resolvers/constants.ts index 5f865534d..fe6eba36b 100644 --- a/scripts/resolvers/constants.ts +++ b/scripts/resolvers/constants.ts @@ -155,13 +155,13 @@ On any error: continue — ${feature} is informational, not a gate.`; * - `codex-only` (diff adversarial): disabled gates only the Codex passes; the * free Claude adversarial subagent still runs. */ -export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only'; nativeReview?: boolean; role?: boolean }): string { +export function codexPreflight(opts: { modeVar?: string; disabledBehavior: 'skip-all' | 'codex-only'; nativeReview?: boolean; role?: boolean; nativeDispatched?: boolean }): string { const m = opts.modeVar ?? '_CODEX_MODE'; const disabledLine = opts.disabledBehavior === 'codex-only' - ? 'Skip the Codex passes only; the Claude adversarial subagent below STILL runs (it is free and fast). Print: "Codex passes skipped (codex_reviews disabled) — running Claude adversarial only."' + ? (opts.nativeDispatched ? 'Skip the Codex passes only; the Claude adversarial subagent below still follows its DISPATCH line. Print: "Codex passes skipped (codex_reviews disabled)."' : 'Skip the Codex passes only; the Claude adversarial subagent below STILL runs (it is free and fast). Print: "Codex passes skipped (codex_reviews disabled) — running Claude adversarial only."') : 'Skip this section entirely; do NOT fall back to a Claude subagent — disabled means no extra review step. Print: "Codex review skipped (codex_reviews disabled). Re-enable: `gstack-config set codex_reviews enabled`."'; const nativeRoute = opts.disabledBehavior === 'codex-only' - ? 'Keep the required Claude adversarial pass; do not dispatch a duplicate.' + ? (opts.nativeDispatched ? 'The Claude adversarial pass follows its own DISPATCH line.' : 'Keep the required Claude adversarial pass; do not dispatch a duplicate.') : 'Fall back to the Claude subagent path.'; return `\`\`\`bash # Codex preflight: the probe runs as a command, so any shell works. diff --git a/scripts/resolvers/outside-voice-steps.ts b/scripts/resolvers/outside-voice-steps.ts index 356eb2729..6f2a250bd 100644 --- a/scripts/resolvers/outside-voice-steps.ts +++ b/scripts/resolvers/outside-voice-steps.ts @@ -144,15 +144,15 @@ If the subagent fails or times out, record native coverage as incomplete. Contin } function adversarialOutsideChallenge(ctx: TemplateContext, isShip: boolean): string { - return `### ${outsideVoiceFor(ctx).label} adversarial challenge (runs whenever \`CODEX_MODE\` is \`ready\` or \`unverified\`) + return `### ${outsideVoiceFor(ctx).label} adversarial challenge (${isShip ? 'on \`DISPATCH: outside-voice\`, when' : 'runs whenever'} \`CODEX_MODE\` is \`ready\` or \`unverified\`) -If \`CODEX_MODE\` is \`ready\` or \`unverified\`: +${isShip ? 'On \`DISPATCH: outside-voice\`, if' : 'If'} \`CODEX_MODE\` is \`ready\` or \`unverified\`: Outside prompt (supply repository context from the parent): "${CODEX_BOUNDARY}Review the changes on this branch against the base branch. Use the supplied branch diff. If it was not supplied and you have repository tools, run DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE". Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems. A finding blocks only if it ${BLOCKING_FLOOR}; end each finding with \`Severity: Critical|High|Medium|Low\` and \`Blocks: yes|no\`. End your output with ONE line in the canonical format \`Recommendation: because \`. Generic reasons like 'because it's safer' do not qualify; the reason must point to a specific finding or no-fix rationale." -${outsideVoiceInvocation(ctx, { timeoutMs: 540000, nativeAlreadyRequired: true, diffCommand: 'DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE"' })} +${outsideVoiceInvocation(ctx, { timeoutMs: 540000, nativeAlreadyRequired: isShip ? 'dispatched' : true, diffCommand: 'DIFF_BASE=$(git merge-base origin/ HEAD) && git diff "$DIFF_BASE"' })} Present the full output verbatim. A Critical, High or \`Blocks: yes\` finding is CRITICAL; \`Blocks: no\` never lowers one, and only a named policy ground drops it. ${isShip ? 'An unavailable outside challenge does not block shipping by itself; supported findings still enter Step 11, and the structured P0/P1 and non-convergence gates still apply.' : 'This outside challenge is informational; supported findings still enter Step 5 Fix-First, whose approval and convergence gates apply.'} @@ -161,19 +161,19 @@ ${outsideVoiceFailurePolicy(ctx, { timeoutMinutes: 9, onTimeout: 'missing-covera -For other modes, retain the native pass above; do not dispatch it again. +${isShip ? 'Other modes skip it; the native pass follows its own DISPATCH line.' : 'For other modes, retain the native pass above; do not dispatch it again.'} ---`; } function adversarialStructuredReview(ctx: TemplateContext, isShip: boolean): string { - return `### ${outsideVoiceFor(ctx).label} structured review (large diffs only, 200+ lines) + return `### ${outsideVoiceFor(ctx).label} structured review (${isShip ? 'on \`DISPATCH: outside-voice\`, 200+ lines' : 'large diffs only, 200+ lines'}) -If \`CODEX_MODE\` is \`ready\` or \`unverified\` and either \`DIFF_TOTAL >= 200\` or the user requested the override above: +${isShip ? 'On \`DISPATCH: outside-voice\`, if' : 'If'} \`CODEX_MODE\` is \`ready\` or \`unverified\` and either \`DIFF_TOTAL >= 200\` or the user requested the override above: Prepare a structured review prompt requesting severity-tagged findings ([P0]-[P3]) or an explicit NO_FINDINGS conclusion. Preserve the base-branch scope including committed changes and working-tree changes. -${outsideVoiceInvocation(ctx, { timeoutMs: 540000, nativeAlreadyRequired: true, structuredBase: '', gate: 'structured', diffCommand: 'DIFF_BASE=$(git merge-base HEAD) && git diff "$DIFF_BASE"' })} +${outsideVoiceInvocation(ctx, { timeoutMs: 540000, nativeAlreadyRequired: isShip ? 'dispatched' : true, structuredBase: '', gate: 'structured', diffCommand: 'DIFF_BASE=$(git merge-base HEAD) && git diff "$DIFF_BASE"' })} ${outsideVoiceFor(ctx).id === 'codex' ? 'The Codex backend uses `codex review --base` without a positional prompt: those arguments are mutually exclusive. Never drop --base to resolve an argv error; prompt-only review changes the diff scope.' : 'The Claude Code backend receives the parent-captured base diff, including committed and working-tree changes, because review mode cannot execute git.'} @@ -195,7 +195,7 @@ Read stderr for errors (same error handling as ${outsideVoiceFor(ctx).label} adv -If \`DIFF_TOTAL < 200\` without that override, skip structured review; the adversarial passes still run. +If \`DIFF_TOTAL < 200\` without that override, skip structured review; the ${isShip ? 'dispatched ' : ''}adversarial passes still run. ---`; } @@ -250,13 +250,13 @@ echo "DIFF_SIZE: $DIFF_TOTAL" **Detect the ${outsideVoiceFor(ctx).label} master switch + tool availability:** -${outsideVoicePreflight(ctx, { disabledBehavior: 'codex-only', nativeReview: true })} +${outsideVoicePreflight(ctx, { disabledBehavior: 'codex-only', nativeReview: true, nativeDispatched: isShip })} \`CODEX_MODE: disabled\` means skip the ${outsideVoiceFor(ctx).label} passes ONLY. -\`ready\` and \`unverified\` run them; every other mode skips them with the printed reason.${isShip ? '' : ` +\`ready\` and \`unverified\` run them${isShip ? ' on \`DISPATCH: outside-voice\`' : ''}; every other mode skips them with the printed reason.${isShip ? '' : ` The ${outsideVoiceFor(ctx).nativeLabel} adversarial subagent always runs.`} -**User override:** If the user explicitly requested "full review", "structured review", or "P1 gate", also run the ${outsideVoiceFor(ctx).label} structured review regardless of diff size (still requires \`CODEX_MODE: ready\` (or \`unverified\`)). +**User override:** If the user explicitly requested "full review", "structured review", or "P1 gate", also run the ${outsideVoiceFor(ctx).label} structured review regardless of diff size (still requires ${isShip ? '\`DISPATCH: outside-voice\` and ' : ''}\`CODEX_MODE: ready\` (or \`unverified\`)). --- @@ -309,7 +309,7 @@ in order before leaving Step 11: Otherwise insert Steps 9, 10 and 11 before the pending Step 11.5 in the work list. Step 9 completes full review before fixes; any further repair inserts its checks ahead of the remaining items. These fresh reviews after code edits are not recovery retries. - Returning here never resets Step 9's three-cycle fix limit. + Returning here never resets Step 9's three-cycle fix limit (under \`frozen\`, the plan's cap). 3. **Native complete with no queued fixes, or not dispatched:** Finish the memory updates below, then continue to Step 11.5. Never jump directly to release preparation.` : 'The native pass is required for Step 5.8 completion. Optional outside failures remain separately recorded, not completed by native coverage. Return all findings and structured-review decisions to Step 5; the parent owns fixes and the full rerun.'} diff --git a/scripts/resolvers/outside-voice.ts b/scripts/resolvers/outside-voice.ts index 28675ff94..e459fd922 100644 --- a/scripts/resolvers/outside-voice.ts +++ b/scripts/resolvers/outside-voice.ts @@ -47,10 +47,10 @@ fi`; /** Caller-owned workflow role. Only enumerated plan-review generators pass it; everything else stays no-role. */ export type OutsideRole = 'plan-review'; -export function outsideVoicePreflight(ctx: TemplateContext, opts: { disabledBehavior: 'skip-all' | 'codex-only' | 'opt-in'; nativeReview?: boolean; role?: OutsideRole }): string { +export function outsideVoicePreflight(ctx: TemplateContext, opts: { disabledBehavior: 'skip-all' | 'codex-only' | 'opt-in'; nativeReview?: boolean; role?: OutsideRole; nativeDispatched?: boolean }): string { const v = outsideVoiceFor(ctx); if (v.id === 'codex' && (opts.disabledBehavior === 'codex-only' || (opts.disabledBehavior === 'skip-all' && !opts.role))) { - let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview, role: !!opts.role })) + let preflight = outsideVoiceLabels(ctx, codexPreflight({ disabledBehavior: opts.disabledBehavior, nativeReview: opts.nativeReview, role: !!opts.role, nativeDispatched: opts.nativeDispatched })) .replace('```bash\n', `\`\`\`bash\n${runtimeRootPrelude(ctx)}\n`); if (['plan-eng-review', 'plan-ceo-review'].includes(ctx.skillName)) { preflight = preflight.replace("follow the workflow's native-review instructions below", @@ -82,7 +82,7 @@ ${readiness} ${ctx.skillName === 'ship' && opts.disabledBehavior === 'opt-in' ? `Ship attempts this optional design check when frontend review applies and \`codex_reviews\` allows it. Unset means the default, enabled; the first send prints a one-time notice. -\`CODEX_MODE\` names the provider here, **${v.label}**.` : `The historical \`CODEX_MODE\` variable describes **${v.label}** availability here.`}${opts.disabledBehavior === 'opt-in' ? " `CODEX_MODE: disabled` or a send's `consent_missing` line: `codex_reviews` is off or unreadable; do not ask; record `outside_status: disabled`." : ''} ${opts.role ? 'The invocation checks auth and the [policy](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md) plan-review model.' : 'Authentication and configured model validity are checked by the actual invocation, without overriding either.'} Missing/broken CLI: install or repair ${v.label}; authentication failure: run \`${v.id === 'codex' ? 'codex login' : 'claude auth login'}\`. ${opts.disabledBehavior === 'skip-all' ? "Disabled ends this entire extra review step (so does a send's `consent_missing` line), including the native fallback; record outside_status: disabled and continue after the section. Disabled is not an unavailable provider and never triggers a replacement reviewer." : opts.disabledBehavior === 'codex-only' ? "Disabled skips only the outside CLI (so does a send's `consent_missing` line); retain the native pass." : ctx.skillName === 'ship' ? '' : 'Otherwise honor this caller’s opt-in/skip choice.'} ${opts.disabledBehavior === 'skip-all' ? 'Provider failure is missing outside coverage; follow the caller’s existing fallback only when reviews are enabled.' : opts.disabledBehavior === 'codex-only' ? 'Non-ready means missing outside coverage. Keep the required native pass without duplicating it.' : 'Any non-ready outcome is missing outside coverage; follow the caller’s existing fallback.'} Never substitute another external provider.`; +\`CODEX_MODE\` names the provider here, **${v.label}**.` : `The historical \`CODEX_MODE\` variable describes **${v.label}** availability here.`}${opts.disabledBehavior === 'opt-in' ? " `CODEX_MODE: disabled` or a send's `consent_missing` line: `codex_reviews` is off or unreadable; do not ask; record `outside_status: disabled`." : ''} ${opts.role ? 'The invocation checks auth and the [policy](https://github.com/garrytan/gstack/blob/main/docs/model-policy.md) plan-review model.' : 'Authentication and configured model validity are checked by the actual invocation, without overriding either.'} Missing/broken CLI: install or repair ${v.label}; authentication failure: run \`${v.id === 'codex' ? 'codex login' : 'claude auth login'}\`. ${opts.disabledBehavior === 'skip-all' ? "Disabled ends this entire extra review step (so does a send's `consent_missing` line), including the native fallback; record outside_status: disabled and continue after the section. Disabled is not an unavailable provider and never triggers a replacement reviewer." : opts.disabledBehavior === 'codex-only' ? `Disabled skips only the outside CLI (so does a send's \`consent_missing\` line)${opts.nativeDispatched ? '.' : '; retain the native pass.'}` : ctx.skillName === 'ship' ? '' : 'Otherwise honor this caller’s opt-in/skip choice.'} ${opts.disabledBehavior === 'skip-all' ? 'Provider failure is missing outside coverage; follow the caller’s existing fallback only when reviews are enabled.' : opts.disabledBehavior === 'codex-only' ? `Non-ready means missing outside coverage. ${opts.nativeDispatched ? 'The native pass follows its own DISPATCH line.' : 'Keep the required native pass without duplicating it.'}` : 'Any non-ready outcome is missing outside coverage; follow the caller’s existing fallback.'} Never substitute another external provider.`; } export interface OutsideCommandOptions { @@ -97,7 +97,7 @@ export interface OutsideCommandOptions { reasoningEffort?: 'high' | 'medium'; /** Creative proposals retain the recommendation gate with task-specific wording. */ purpose?: 'design-direction'; - nativeAlreadyRequired?: boolean; + nativeAlreadyRequired?: boolean | 'dispatched'; /** Resolve the plan-review policy once in the invocation shell and bind it through probe and dispatch. */ role?: OutsideRole; } @@ -188,7 +188,7 @@ export function outsideVoiceInvocation(ctx: TemplateContext, requested: OutsideC ${outsideVoiceCommand(ctx, opts)} \`\`\` -Use Bash \`timeout: ${gateMs}\`; show the full response in a \`tool-output\` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing ${planRecommendation ? 'Recommendation: because ' : opts.purpose === 'design-direction' ? 'Recommendation' : 'score/severity/completion'} markers, timeout or CLI failure means \`outside_status: unavailable\`. P0/P1 findings block like native ones; \`OUTSIDE_STATUS: unverified\` is missing coverage. ${opts.purpose === 'design-direction' ? 'Continue completed proposals; native completion does not count as outside coverage.' : opts.nativeAlreadyRequired ? 'Retain the required native pass without duplicating it; it cannot complete outside coverage.' : "Use the caller's fallback; missing coverage is never clean/PASS."} ${nativeStructured ? 'Scratch cleanup is automatic.' : 'After either outcome, delete only your private prompt; scratch cleanup is automatic.'}`; +Use Bash \`timeout: ${gateMs}\`; show the full response in a \`tool-output\` fence. Require successful execution and valid markers. Refusal, empty/malformed output, missing ${planRecommendation ? 'Recommendation: because ' : opts.purpose === 'design-direction' ? 'Recommendation' : 'score/severity/completion'} markers, timeout or CLI failure means \`outside_status: unavailable\`. P0/P1 findings block like native ones; \`OUTSIDE_STATUS: unverified\` is missing coverage. ${opts.purpose === 'design-direction' ? 'Continue completed proposals; native completion does not count as outside coverage.' : opts.nativeAlreadyRequired === 'dispatched' ? 'A dispatched native pass runs once either way; outside output cannot complete it.' : opts.nativeAlreadyRequired ? 'Retain the required native pass without duplicating it; it cannot complete outside coverage.' : "Use the caller's fallback; missing coverage is never clean/PASS."} ${nativeStructured ? 'Scratch cleanup is automatic.' : 'After either outcome, delete only your private prompt; scratch cleanup is automatic.'}`; } /** diff --git a/ship/SKILL.md b/ship/SKILL.md index d0e3cdcee..e71db2284 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -204,14 +204,14 @@ A review's **start token** is the opaque value returned by `gstack-review-log -- before it reads the diff. Keep `REVIEW_START` for Step 9, a separate `PASS_START` for each Step 11 attempt, and `DESIGN_START` for design. Finish each pass with its original token; `--finish` stamps the binding fields automatically. Never borrow or replace a token. -`gstack-wtree` prints a Git tree hash covering tracked and non-ignored untracked files, -not a commit ID. Use `git diff ` to compare these snapshots. +`gstack-wtree` prints a Git tree hash of tracked and non-ignored untracked files (not a commit); +compare two with `git diff `. **Review policy** (docs/review-policy.md): only a finding that would fail a test, break production, or lose data or secrets blocks a pass; the rest is a PR-body note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only -raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds, -fix-checks included. `NO FINDINGS` is complete. `ship: review_scope` (Step 3) +raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds under `frozen` +(fix-checks included), else Step 9.4's third fix cycle. `NO FINDINGS` is complete. `ship: review_scope` (Step 3) is the knob; the receipt's `review_loop` records the outcome. ### Ship control flow @@ -306,7 +306,7 @@ branch name wherever the instructions say "the base branch" or ``. --- `` means the detected branch name for fetch/helper arguments; -`origin/` is its remote-tracking ref for comparisons. Step 1 fetches it. +`origin/` is its remote-tracking ref for comparisons. @@ -333,11 +333,12 @@ act on each `STOP:` and `NEXT:` line. restore access. `DIFF:` is the stat and commit count. 4. **Carry check.** An open PR prints `PR: #` and its `gstack-ship-receipt carry --pr --base --dry-run` line. - `CARRY: would carry` (base merge committed, same product patch-id): skip review, fix - what it broke (CHANGELOG, size caps), run `gstack-pregate --allow-repo-commands`, then carry - without `--dry-run` plus `--pregate .gstack/tmp/pregate.json`. `CARRY: carried` binds HEAD and - ends the run. A failed check, HEAD moving after pregate, `current`, or `refused` with no - `NEXT: carry route` re-runs Steps 2–16. + `CARRY: would carry` (base merge committed, same product patch-id): skip review and Step 14.5, + fix what it broke (CHANGELOG, size caps), commit, `gstack-ship-release push --branch + --early`, `gstack-ci-carry wait --base `, run `gstack-pregate --allow-repo-commands`, then carry without + `--dry-run` plus `--pregate .gstack/tmp/pregate.json`. `CARRY: carried` binds HEAD and ends + the run with its `GSTACK_RESULT:` line. A failed check, HEAD moving after pregate, `current`, or `refused` with no + `NEXT: carry route` re-runs Steps 2–16. Steps 17–21 then publish. 5. Display historical readiness using the dashboard below, then finish Step 1. Prior CLEAR reviews or dashboard skips never replace Step 9's gates. @@ -521,7 +522,8 @@ runs items 1 and 3–5; item commands serve only a `STOP:`, which names its reru (`--bump`, `--accept-incomplete`, `--version `, `--keep-current`, `--answer`). `VERSION: skipped` or `VERSION: kept (…; no write)` ends Step 12 with -`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch. +`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch +(`kept`: VERSION stays ``). 1. **Classify state** — pure reader, never writes: ```bash @@ -628,7 +630,7 @@ write failures are non-blocking. ## Step 14.5: Documentation audit (every ship) **Doc-sync invariant:** Every ship dispatches the /document-release subagent before final -commit/verification/publication, including reruns, already-pushed branches, existing PRs and docs-only changes. +commit/verification/publication, including reruns, already-pushed branches, existing PRs and docs-only changes, but not a Step 1 carry. No edits means an executed audit, not a skip; report the section's verified outcome. > **STOP.** Before auditing docs before final commit/verification (Step 14.5), on every ship, Read `~/.claude/skills/gstack/ship/sections/documentation.md` and execute it @@ -653,17 +655,17 @@ Make bisectable commits; if already committed, continue to Step 16. Never create **IRON LAW: NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE.** -Stages run in order; 1–4's other commands are `STOP:` recoveries. Content -changes during or after verification restart at stage 1 and complete all five -stages before Step 17. Content-preserving commits keep evidence valid. +`finish` runs stages 1–4 in order; act on each `STOP:` by its recovery below, then rerun +it. Content changes during or after verification restart at +stage 1 and complete all five stages before Step 17. Content-preserving commits keep evidence valid. **One call for stages 1–4:** `~/.claude/skills/gstack/bin/gstack-ship-release finish --base --reviewed-tree --reviewed-base --docs-post -post.json --lane