diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 7db02424a..7869e377d 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -901,6 +901,15 @@ export function renderAutoplanDriver(options: RenderOptions): string { return GENERATED_HEADER.replace('{{SOURCE}}', `${AUTOPLAN_DRIVER_RELPATH}.tmpl`) + resolvePlaceholders(tmpl, ctx, getHostConfig('claude'), `${AUTOPLAN_DRIVER_RELPATH}.tmpl`, options); } +/** ship/DRIVER.md (GSTA-167 PR 5): the compact /ship entry for a parent agent, rendered the same way. */ +export const SHIP_DRIVER_RELPATH = 'ship/DRIVER.md'; +export function renderShipDriver(options: RenderOptions): string { + const tmplPath = path.join(ROOT, `${SHIP_DRIVER_RELPATH}.tmpl`); + const tmpl = fs.readFileSync(tmplPath, 'utf-8'); + const ctx = buildContext(tmpl, tmplPath, 'claude', options, 'ship'); + return GENERATED_HEADER.replace('{{SOURCE}}', `${SHIP_DRIVER_RELPATH}.tmpl`) + resolvePlaceholders(tmpl, ctx, getHostConfig('claude'), `${SHIP_DRIVER_RELPATH}.tmpl`, options); +} + /** * Generate one on-demand section file (`/sections/.md.tmpl` → * `.md`). Sections are BODY FRAGMENTS — no frontmatter, no catalog trim, @@ -1092,6 +1101,9 @@ export async function runGeneration(settings: GenerationOptions = {}): Promise t.tmpl === 'autoplan/SKILL.md.tmpl')) { emit(path.join(options.outputRoot, AUTOPLAN_DRIVER_RELPATH), renderAutoplanDriver(options), 'asset', host); } + if (host === 'claude' && includesSkill(hostConfig, 'ship') && templates.some(t => t.tmpl === 'ship/SKILL.md.tmpl')) { + emit(path.join(options.outputRoot, SHIP_DRIVER_RELPATH), renderShipDriver(options), 'asset', host); + } if (host === 'openclaw') { for (const variant of ['lite', 'full', 'plan'] as const) { diff --git a/scripts/resolvers/index.ts b/scripts/resolvers/index.ts index 56d9747c4..4af305d8d 100644 --- a/scripts/resolvers/index.ts +++ b/scripts/resolvers/index.ts @@ -54,6 +54,7 @@ import { generatePlanRealityRows } from './plan-reality'; import { generateSidewaysSweep, generateFlakeFixNumber } from './sideways'; import { generateCoordinatorContract } from './coordinator-contract'; import { generateAutoplanDriverCodes, generateAutoplanDriverCommands, generateAutoplanDriverFiles, generateAutoplanDriverGate, generateAutoplanDriverPhases, generateAutoplanDriverResultFormat } from './autoplan-driver'; +import { generateShipDriverSegmentCall, generateShipDriverStops } from './ship-driver'; import { generateQAScope, generateQAExploratory, generateQAFunctional, generateQAResource, generateQAReview, generateQAReviewPreflight, generateQAMethodReads } from './qa'; import { generateVoiceTriggerRouting } from './voice-triggers'; import { generateTranscriptDiscovery, generateTranscriptSources } from './transcripts'; @@ -191,6 +192,8 @@ export const RESOLVERS: Record = { AUTOPLAN_DRIVER_GATE: generateAutoplanDriverGate, AUTOPLAN_DRIVER_FILES: generateAutoplanDriverFiles, AUTOPLAN_DRIVER_CODES: generateAutoplanDriverCodes, + SHIP_DRIVER_STOPS: generateShipDriverStops, + SHIP_DRIVER_SEGMENT_CALL: generateShipDriverSegmentCall, SECTION, SECTION_INDEX, TRANSCRIPT_SOURCES: generateTranscriptSources, diff --git a/scripts/resolvers/ship-driver.ts b/scripts/resolvers/ship-driver.ts new file mode 100644 index 000000000..52502e7ed --- /dev/null +++ b/scripts/resolvers/ship-driver.ts @@ -0,0 +1,43 @@ +/** + * ship/DRIVER.md resolvers (GSTA-167 PR 5): the compact entry a parent agent + * reads to run an unattended /ship, instead of ship/SKILL.md plus its sections. + * The phases live in ship/DRIVER.md.tmpl; this file owns the one list a reader + * must trust most: which heading each STOP sends the parent to. test/ship-driver.test.ts + * resolves every anchor against the generated files. + */ +import { DELEGATE_SEGMENT_CALL } from '../../lib/ship-budget'; +import type { ResolverFn } from './types'; + +/** Each STOP (or read-when line) a driver run can meet, and the one heading it reads, relative to /ship/. */ +export const SHIP_DRIVER_STOPS: Array<{ when: string; read: string }> = [ + { when: '`merge conflicts`', read: 'SKILL.md#step-3-merge-the-base-branch-before-tests' }, + { when: '`NEXT: carry route`', read: 'SKILL.md#step-1-pre-flight' }, + { when: '`DISTRIBUTION: candidates=`', read: 'SKILL.md#step-2-distribution-pipeline-check' }, + { when: '`APPLE:` with a store ask', read: 'sections/apple-release.md#apple-app-store-release' }, + { when: '`pregate fail` or `incomplete`', read: 'sections/tests.md#step-5-run-tests-on-merged-code' }, + { when: 'a failed or zero-run lane', read: 'sections/tests.md#test-failure-ownership-triage' }, + { when: 'a red eval or a failed free-suite shard', read: 'sections/measure.md#measure-then-fix-loop-a-red-paid-eval-or-a-failed-free-suite-shard' }, + { when: 'Greptile found', read: 'sections/greptile-early.md#step-65-early-greptile-pr-after-the-free-tests-pass' }, + { when: 'coverage below target, a failed audit', read: 'sections/test-coverage.md#step-7-test-coverage-audit' }, + { when: 'plan items NOT DONE or UNVERIFIABLE', read: 'sections/plan-completion.md#gate-logic' }, + { when: '`REVIEW_COVERAGE_INCOMPLETE` or `NEXT: stop`', read: 'sections/review-army.md#decide-whether-to-repeat-step-9' }, + { when: 'a `REVIEW_SCOPE_*` refusal, the review-log row', read: 'sections/review-army.md#step-94-fix-first-and-persistence' }, + { when: 'a required probe not passed', read: 'sections/review-army.md#step-921-exploratory-qa-before-fix-first' }, + { when: 'native adversarial incomplete, queued fixes', read: 'sections/adversarial.md#finish-the-adversarial-phase' }, + { when: 'Step 11.5 mismatch', read: 'SKILL.md#step-115-bind-the-reviews' }, + { when: 'a `version` STOP', read: 'SKILL.md#step-12-version-bump-auto-decide' }, + { when: '`Documentation: blocked` or a `DOCS_CAP:` over', read: 'sections/documentation.md#blocked-recovery' }, + { when: 'finish: changed since review', read: 'SKILL.md#2-choose-the-change-route' }, + { when: 'finish: documentation inputs changed', read: 'SKILL.md#3-resolve-documentation-freshness' }, + { when: 'finish: build, lane or pregate STOP', read: 'SKILL.md#4-verify-the-frozen-candidate' }, + { when: 'a push STOP', read: 'SKILL.md#step-17-push' }, + { when: 'unattended, no open PR at HEAD', read: 'sections/handoff.md#step-18-h-handoff-without-a-pr-automation-lanes' }, + { when: 'a receipt write or `gstack-post` refusal', read: 'sections/pr-body.md#compose-the-body-then-publish-through-gstack-post' }, + { when: '`verdict=stop` or `DELEGATE:`', read: 'sections/budget-handoff.md#budget-and-context-handoff-read-when-a-cap-is-set-or-verdictdelegate-prints' }, + { when: '`QUEUE_RECEIPT_STALE`', read: '../docs/ship-policy.md#ship-before-ready' }, +]; + +export const generateShipDriverSegmentCall: ResolverFn = () => `${DELEGATE_SEGMENT_CALL / 1000}k`; + +export const generateShipDriverStops: ResolverFn = () => + SHIP_DRIVER_STOPS.map(s => `- ${s.when}: \`${s.read}\``).join('\n'); diff --git a/ship/DRIVER.md b/ship/DRIVER.md new file mode 100644 index 000000000..0147e0edf --- /dev/null +++ b/ship/DRIVER.md @@ -0,0 +1,131 @@ + + +# /ship driver: run /ship from a parent agent + +Read this instead of `ship/SKILL.md` when a parent agent on any host runs /ship unattended +(`SESSION_KIND: unattended`): bins do the mechanics; this file holds the phases, the judgment and the +lines each phase prints. Commands live in `/bin/` (`` holds `ship/`). Run each with the +`GSTACK_LAUNCH` assignments your installer printed (`. `), never +`export $(...)`, `env`, `printenv` or a bare `set` in a tool fence. + +**Rules.** A phase is one program (an execute program on Capy, else a shell script) unless it +says otherwise; print its result lines verbatim. `STOP:` (exit 3) means nothing after it ran; `NEXT:` +names the next act. Read a section only when the list at the end sends you, only that heading, never +up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it +there, come back. Unattended, a question with a recommended option takes it (log it); a gate +disposition and every approval stay pending and end the run. Keep a private invocation record +(Release, Decisions, Reviews, Checks, Documentation, Next steps) with every token, tree and path. +Budget: read the host's token counter in the program that runs +`gstack-ship-budget` (`--reading R --prior-reading `); `check --step --next ` +opens each phase, `check --next "$BUDGET_NEXT"` each reader batch; obey `verdict=`. + +## 1. Preflight and tests: Steps 0, 0.9, 1, 2, 3, 4, 5 + +`gstack-ship-budget start --record [--token-cap ] --delegate measured --reading R` and +`gstack-ship-preflight --base --lane