Move the tools only Capy crews use into a crew/ folder (GST-1833) (#4145)

* Load the Capy crew skills only on Capy machines (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Add changelog entry for Capy-only crew skills (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Move the Capy-only crew tooling into crew/ and run its tests only when needed (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Test that the Capy launch file puts crew/bin on PATH

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Count any ~/.capy as a Capy machine, so a Mac running Capy keeps /captain

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Fix the tests and fixtures that broke when crew tooling moved into crew/

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Plan every crew test when a pull request's merge-base can't be read

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Add a test that checks each crew-only module left the root tree for crew/

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Run crew tests on pull requests that change bin/ commands or workflows they drive

Review-Finding: p1/specialist-batched#1 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep running every crew test on every pull request; the skip comes in a later change

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep copied installs and older release bisects working after the crew/ move

Setup now copies crew/lib into every host's runtime root beside bin and lib, so Windows installs (which copy instead of linking) can still start gstack-ship-receipt, gstack-pregate and the other commands that import it. The release bisect runs the train-gate script from wherever the checked-out commit has it, so a bisect over commits from before the move still gets a verdict.

Review-Finding: p1/outside-voice#1 reader=outside-voice
Review-Finding: p1/outside-voice#2 reader=outside-voice
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Say in the changelog that Windows installs keep working after the crew/ move

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Point the harness guide's merge-queue rows at crew/lib/ where those modules moved

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Point the plain-title test at the wave module's new home in crew/ (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-08 14:26:14 +00:00
1 parent 6042f6ad48
commit 6417abf7a6
210 files changed
+1963 -1784

No files matched your search

+2 -2
View File
@@ -87,10 +87,10 @@ jobs:
GH_TOKEN: ${{ github.token }}
GSTACK_STATE_ROOT: ${{ runner.temp }}/gstack-state
run: |
bun --no-env-file --config=/dev/null --no-install run scripts/train-backstop.ts run --repo "$GITHUB_REPOSITORY" --branch "$GITHUB_REF_NAME" --sha "$GITHUB_SHA" \
bun --no-env-file --config=/dev/null --no-install run crew/scripts/train-backstop.ts run --repo "$GITHUB_REPOSITORY" --branch "$GITHUB_REF_NAME" --sha "$GITHUB_SHA" \
--summary "$GITHUB_STEP_SUMMARY" --outputs "$GITHUB_OUTPUT"
# The per-commit verdict the stable release reads (scripts/release-cut.ts).
# The per-commit verdict the stable release reads (crew/scripts/release-cut.ts).
# Its own job with checks: write and no repository code (the same split as
# evals.yml's train-check): only the action word the gate step recorded.
backstop-check:
+8 -8
View File
@@ -15,7 +15,7 @@ name: Periodic Evals
# diff-billed, so without it the full gate census might never execute
# anywhere); the hollow-shard guard (exit 0 + zero executed tests under
# EVALS_ALL fails) makes both lanes census-health checks, not just test runs.
# The gate census is a matrix of arms (scripts/census-arms.ts): each arm is a
# The gate census is a matrix of arms (crew/scripts/census-arms.ts): each arm is a
# host and model with its own plan. Monday runs claude-opus (the capture
# model) and codex-sol (gpt-6.1-sol on the Codex host, adapter cases only);
# the monthly cron runs only the census, with claude-fable and codex-astra.
@@ -41,7 +41,7 @@ on:
type: boolean
default: false
census_arms:
description: 'Run only the gate census, with these arms (comma-separated: claude-opus, codex-sol, claude-fable, codex-astra; scripts/census-arms.ts). Empty keeps the periodic lane.'
description: 'Run only the gate census, with these arms (comma-separated: claude-opus, codex-sol, claude-fable, codex-astra; crew/scripts/census-arms.ts). Empty keeps the periodic lane.'
type: string
default: ''
expected_sha:
@@ -159,7 +159,7 @@ jobs:
with:
bun-version: 1.4.2
# Census arms (scripts/census-arms.ts): which host/model arms run the
# Census arms (crew/scripts/census-arms.ts): which host/model arms run the
# gate census, and whether the periodic lane runs beside them.
- name: Select the census arms
id: census
@@ -167,7 +167,7 @@ jobs:
SCHEDULE: ${{ github.event.schedule }}
CENSUS_ARMS: ${{ inputs.census_arms }}
INCLUDE_GATE_CENSUS: ${{ github.ref == 'refs/heads/main' || inputs.include_gate_census }}
run: bun --no-install run scripts/census-arms.ts --select >> "$GITHUB_OUTPUT"
run: bun --no-install run crew/scripts/census-arms.ts --select >> "$GITHUB_OUTPUT"
- name: Emit run manifest (ALL periodic tests minus reasoned excludes)
if: steps.census.outputs.periodic_lane == 'true'
@@ -204,7 +204,7 @@ jobs:
run: |
set -euo pipefail
for arm in $(jq -r '.[]' <<<"$ARMS"); do
mapfile -t arm_env < <(bun --no-install run scripts/census-arms.ts --env "$arm")
mapfile -t arm_env < <(bun --no-install run crew/scripts/census-arms.ts --env "$arm")
env "${arm_env[@]}" EVALS_TIER=gate bun run scripts/test-paid-shards.ts --tier gate --emit-plan "/tmp/gate-census-plan/$arm/manifest.json" --slice-budget 420 --jobs 2 --skip-judges --max-parallel 20
done
@@ -213,7 +213,7 @@ jobs:
id: gate-matrix
env:
ARMS: ${{ steps.census.outputs.arms }}
run: bun --no-install run scripts/census-arms.ts --matrix /tmp/gate-census-plan "$ARMS" >> "$GITHUB_OUTPUT"
run: bun --no-install run crew/scripts/census-arms.ts --matrix /tmp/gate-census-plan "$ARMS" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
if: steps.census.outputs.arms != '[]'
@@ -553,7 +553,7 @@ jobs:
name: gate-census-${{ matrix.arm }} (${{ matrix.slice }})
runs-on: ubicloud-standard-8
needs: [build-image, plan-slices]
# The planner picks the arms (scripts/census-arms.ts). A branch dispatch
# The planner picks the arms (crew/scripts/census-arms.ts). A branch dispatch
# validates the periodic lane only unless it sets include_gate_census or
# census_arms; gate cases on a branch also go through an evals.yml
# dispatch with evals_all. Main (schedule or dispatch) always runs it.
@@ -634,7 +634,7 @@ jobs:
- name: Select the census arm's host and model
env:
ARM: ${{ matrix.arm }}
run: bun --no-install run scripts/census-arms.ts --env "$ARM" >> "$GITHUB_ENV"
run: bun --no-install run crew/scripts/census-arms.ts --env "$ARM" >> "$GITHUB_ENV"
- name: Run gate census slice ${{ matrix.slice }} (${{ matrix.arm }})
env:
+23 -20
View File
@@ -34,7 +34,7 @@ on:
default: ''
# Merge trains (docs/ship-policy.md#run-a-merge-train): only under a train
# block in the default branch's .gstack/ship-policy.json. The route job
# validates them before any paid work (scripts/train-gate.ts route).
# validates them before any paid work (crew/scripts/train-gate.ts route).
cases:
description: 'Train bisect: comma-separated case ids from the full gate plan (gstack-ship-queue train bisect sets it)'
type: string
@@ -194,7 +194,7 @@ jobs:
# changes. Under a train block a PR push is `pr-summary` (screen-summary
# prints the screening selection and its estimated cost; no paid slice
# starts), and gstack-ship-queue's dispatches are `screen`, `train` or
# `bisect` once scripts/train-gate.ts has validated their inputs. A refused
# `bisect` once crew/scripts/train-gate.ts has validated their inputs. A refused
# dispatch fails here, before any paid work.
route:
runs-on: ubuntu-24.04
@@ -290,10 +290,10 @@ jobs:
if [ "$TRAIN_POLICY" = true ]; then mode=pr-summary; fi
echo "mode=${mode}" >> "$GITHUB_OUTPUT"
echo "- Run mode: ${mode}" >> "$GITHUB_STEP_SUMMARY"
elif [ -f scripts/train-gate.ts ]; then
bun --no-env-file --config=/dev/null --no-install run scripts/train-gate.ts route --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
elif [ -f crew/scripts/train-gate.ts ]; then
bun --no-env-file --config=/dev/null --no-install run crew/scripts/train-gate.ts route --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
elif [ -n "${CASES}${PREFIX_SHAS}${SCREEN_PR}${SCREEN_MERGE_SHA}${SCREEN_POLICY}" ]; then
echo "::error::${GITHUB_REF_NAME} has no scripts/train-gate.ts to validate the train or screen inputs; dispatch from a ref that has it"
echo "::error::${GITHUB_REF_NAME} has no crew/scripts/train-gate.ts to validate the train or screen inputs; dispatch from a ref that has it"
exit 1
else
echo "mode=dispatch" >> "$GITHUB_OUTPUT"
@@ -323,7 +323,7 @@ jobs:
PREFIXES: ${{ steps.route.outputs.prefixes }}
NONCE: ${{ inputs.nonce }}
run: |
bun --no-env-file --config=/dev/null --no-install run scripts/train-gate.ts bisect-plan --cases "$CASES" --prefixes "$PREFIXES" --nonce "$NONCE" \
bun --no-env-file --config=/dev/null --no-install run crew/scripts/train-gate.ts bisect-plan --cases "$CASES" --prefixes "$PREFIXES" --nonce "$NONCE" \
--out /tmp/train-bisect/bisect.json --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
- name: Hand the bisect plan to its executors
@@ -460,11 +460,11 @@ jobs:
RECOVERY_MODE: ${{ github.event_name == 'workflow_dispatch' && 'dispatch' || 'pr' }}
run: |
mkdir -p /tmp/gstack-eval-input-cache
if [ ! -f scripts/recover-receipts.ts ]; then
echo "- Receipt recovery unavailable: the base ref has no scripts/recover-receipts.ts; reuse stays off this run" >> "$GITHUB_STEP_SUMMARY"
if [ ! -f crew/scripts/recover-receipts.ts ]; then
echo "- Receipt recovery unavailable: the base ref has no crew/scripts/recover-receipts.ts; reuse stays off this run" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
bun --no-install run scripts/recover-receipts.ts collect --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" \
bun --no-install run crew/scripts/recover-receipts.ts collect --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" \
--head-repo "$HEAD_REPO" --branch "$HEAD_BRANCH" --run-id "$GITHUB_RUN_ID" \
--store /tmp/gstack-eval-input-cache --store-key "$STORE_KEY" --mode "$RECOVERY_MODE" --budget-seconds 60 >> "$GITHUB_STEP_SUMMARY" \
|| echo "- Receipt recovery crashed; reuse stays off this run" >> "$GITHUB_STEP_SUMMARY"
@@ -493,11 +493,11 @@ jobs:
TESTED_SHA: ${{ inputs.screen_merge_sha || github.sha }}
SCREEN_POLICY: ${{ inputs.screen_policy }}
run: |
if [ ! -f scripts/gate-reuse.ts ]; then
echo "- Gate verdict reuse unavailable: the base ref has no scripts/gate-reuse.ts; the full gate runs" >> "$GITHUB_STEP_SUMMARY"
if [ ! -f crew/scripts/gate-reuse.ts ]; then
echo "- Gate verdict reuse unavailable: the base ref has no crew/scripts/gate-reuse.ts; the full gate runs" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
bun --no-install run scripts/gate-reuse.ts find --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" --head-repo "$HEAD_REPO" \
bun --no-install run crew/scripts/gate-reuse.ts find --repo "$GITHUB_REPOSITORY" --pr "$PR_NUMBER" --head-repo "$HEAD_REPO" \
--branch "$HEAD_BRANCH" --run-id "$GITHUB_RUN_ID" --head-sha "$HEAD_SHA" --tested-sha "$TESTED_SHA" \
${SCREEN_POLICY:+--screen-policy "$SCREEN_POLICY"} --out /tmp/gate-reuse --budget-seconds 90 >> "$GITHUB_STEP_SUMMARY" \
|| echo "- Gate verdict reuse crashed; the full gate runs" >> "$GITHUB_STEP_SUMMARY"
@@ -548,7 +548,7 @@ jobs:
EVALS_SCREEN_POLICY: narrow
run: |
EVALS_TIER=gate bun --no-install run scripts/test-paid-shards.ts --tier gate --emit-plan /tmp/paid-plan/manifest.json --slices 1
bun --no-env-file --config=/dev/null --no-install run scripts/train-gate.ts screen-summary /tmp/paid-plan/manifest.json --pr "$PR_NUMBER" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run crew/scripts/train-gate.ts screen-summary /tmp/paid-plan/manifest.json --pr "$PR_NUMBER" >> "$GITHUB_STEP_SUMMARY"
bun --no-install run scripts/test-pr-profile.ts summary /tmp/paid-plan/manifest.json >> "$GITHUB_STEP_SUMMARY"
# ── Sliced lane (the ONLY paid lane; legacy 17-row matrix deleted) ──────────
@@ -633,7 +633,7 @@ jobs:
if: github.event_name == 'pull_request' || inputs.pr_receipts != ''
run: |
mkdir -p /tmp/gstack-eval-input-cache
bun --no-install run scripts/recover-receipts.ts decide /tmp/gstack-eval-input-cache --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
bun --no-install run crew/scripts/recover-receipts.ts decide /tmp/gstack-eval-input-cache --github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
- name: Emit run manifest
env:
@@ -661,7 +661,7 @@ jobs:
if: github.event_name == 'pull_request' || needs.route.outputs.mode == 'screen'
run: |
mkdir -p /tmp/gate-reuse
bun --no-install run scripts/gate-reuse.ts confirm /tmp/gate-reuse /tmp/paid-plan/manifest.json --out /tmp/paid-plan \
bun --no-install run crew/scripts/gate-reuse.ts confirm /tmp/gate-reuse /tmp/paid-plan/manifest.json --out /tmp/paid-plan \
--github-output "$GITHUB_OUTPUT" >> "$GITHUB_STEP_SUMMARY"
# CEO-13: the mode, case counts and, on fallback, every file that
@@ -870,7 +870,7 @@ jobs:
# The rerun runs fresh (no receipts, no reuse) and never changes a trial
# count, kind or threshold; slices-report decides FLAKY (green, annotated,
# flake record) or red. Train runs use the same path; bisects rerun inside
# scripts/train-gate.ts.
# crew/scripts/train-gate.ts.
rerun-plan:
runs-on: ubuntu-24.04
needs: [plan-slices, eval-slices]
@@ -1167,7 +1167,10 @@ jobs:
EVALS_JOBS: "1"
EVALS_CONCURRENCY: "1"
SLICE: ${{ matrix.slice }}
run: EVALS_TIER=gate bun --no-env-file --config=/dev/null run scripts/train-gate.ts bisect-prefix --plan /tmp/train-bisect/bisect.json --index "$SLICE" --out /tmp/train-bisect/out
run: |
# A prefix from before the crew/ move (GST-1833) has the executor at scripts/train-gate.ts.
gate=crew/scripts/train-gate.ts; [ -f "$gate" ] || gate=scripts/train-gate.ts
EVALS_TIER=gate bun --no-env-file --config=/dev/null run "$gate" bisect-prefix --plan /tmp/train-bisect/bisect.json --index "$SLICE" --out /tmp/train-bisect/out
- name: Upload the prefix record
if: always() && steps.prefix.outputs.short != ''
@@ -1305,7 +1308,7 @@ jobs:
if: needs.plan-slices.outputs.gate_reuse == 'true'
run: |
set +e
bun --no-install run scripts/gate-reuse.ts report /tmp/paid-report/gate-reuse.json --repo "$GITHUB_REPOSITORY" > /tmp/report.txt
bun --no-install run crew/scripts/gate-reuse.ts report /tmp/paid-report/gate-reuse.json --repo "$GITHUB_REPOSITORY" > /tmp/report.txt
code=$?
cat /tmp/report.txt
echo "exit=$code" >> "$GITHUB_OUTPUT"
@@ -1378,7 +1381,7 @@ jobs:
SCREEN_POLICY: ${{ inputs.screen_policy }}
RECONCILE_EXIT: ${{ steps.reconcile.outputs.exit }}
run: |
bun --no-install run scripts/gate-reuse.ts bind /tmp/paid-report --out /tmp/gate-binding --repo "$GITHUB_REPOSITORY" \
bun --no-install run crew/scripts/gate-reuse.ts bind /tmp/paid-report --out /tmp/gate-binding --repo "$GITHUB_REPOSITORY" \
--pr "$PR_NUMBER" --run-id "$GITHUB_RUN_ID" --run-attempt "$GITHUB_RUN_ATTEMPT" --head-sha "$HEAD_SHA" \
--tested-sha "$TESTED_SHA" --profile "$EVALS_PROFILE" --reconcile-exit "$RECONCILE_EXIT" \
${SCREEN_POLICY:+--purpose screen --screen-policy "$SCREEN_POLICY"} >> "$GITHUB_STEP_SUMMARY"
@@ -1413,7 +1416,7 @@ jobs:
RECORD: ${{ needs.plan-slices.outputs.record_artifact }}
RECONCILE_EXIT: ${{ steps.reconcile.outputs.exit }}
run: |
bun --no-env-file --config=/dev/null --no-install run scripts/train-gate.ts train-record /tmp/paid-report --nonce "$NONCE" --head "$HEAD_SHA" \
bun --no-env-file --config=/dev/null --no-install run crew/scripts/train-gate.ts train-record /tmp/paid-report --nonce "$NONCE" --head "$HEAD_SHA" \
--reconcile-exit "$RECONCILE_EXIT" --run-id "$GITHUB_RUN_ID" --run-attempt "$GITHUB_RUN_ATTEMPT" --repo "$GITHUB_REPOSITORY" \
--out "/tmp/train-record/${RECORD}.json" >> "$GITHUB_STEP_SUMMARY"
+1 -1
View File
@@ -35,5 +35,5 @@ jobs:
run: bun install --frozen-lockfile
- name: Measure one guarded read (3 runs per journal)
run: |
bun scripts/measure-journal-read.ts --runs 3 | tee "$RUNNER_TEMP/measure.md"
bun crew/scripts/measure-journal-read.ts --runs 3 | tee "$RUNNER_TEMP/measure.md"
cat "$RUNNER_TEMP/measure.md" >> "$GITHUB_STEP_SUMMARY"
+6 -6
View File
@@ -1,9 +1,9 @@
name: Release gate
# The release gate (docs/ship-policy.md#release-gate; bin/gstack-release-gate,
# lib/release-gate.ts): the full paid gate on main's head every 8 hours, only
# The release gate (docs/ship-policy.md#release-gate; crew/bin/gstack-release-gate,
# crew/lib/release-gate.ts): the full paid gate on main's head every 8 hours, only
# when main moved since the last gate. It records the verdict as a release
# note in refs/notes/gstack-trains, publishes the main-backstop check run on
# the gated commit (what scripts/release-cut.ts reads as gated), and bisects
# the gated commit (what crew/scripts/release-cut.ts reads as gated), and bisects
# a red gate's cases. It never writes the stable branch: release-cut is its
# only writer, and refuses every cut while the newest gate is red. Inert (no
# paid work, nothing written) unless main's .gstack/ship-policy.json declares
@@ -12,7 +12,7 @@ name: Release gate
on:
schedule:
# Every 8 hours (RELEASE_GATE_SCHEDULE_HOURS in lib/release-gate-policy.ts;
# test/release-gate-workflow.test.ts pins the two together), off the hour.
# crew/test/release-gate-workflow.test.ts pins the two together), off the hour.
- cron: '17 */8 * * *'
workflow_dispatch:
inputs:
@@ -90,12 +90,12 @@ jobs:
# branch) publishes no check run, so it fails the job, as does a run that recorded no action (a crash).
run: |
rc=0
./bin/gstack-release-gate run --repo "$GITHUB_REPOSITORY" --budget-minutes 330 \
./crew/bin/gstack-release-gate run --repo "$GITHUB_REPOSITORY" --budget-minutes 330 \
--summary "$GITHUB_STEP_SUMMARY" --outputs "$GITHUB_OUTPUT" ${DRY_RUN:+--dry-run} ${IGNORE_CADENCE:+--ignore-cadence} || rc=$?
if [ "$rc" != 0 ] && ! { grep -qE '^action=(held|failed)$' "$GITHUB_OUTPUT" && grep -qE '^sha=[0-9a-f]{40}$' "$GITHUB_OUTPUT"; }; then exit "$rc"; fi
# The gated commit's check run (release_gate.check_name, main-backstop), the
# evidence scripts/release-cut.ts reads. Its own job with checks: write and
# evidence crew/scripts/release-cut.ts reads. Its own job with checks: write and
# no repository code, as for backstop-check: only the recorded action word.
release-check:
runs-on: ubuntu-24.04
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
# reads below fail loudly when a fetch fails (.github/scripts/history-read.sh).
filter: blob:none
ref: ${{ github.event.pull_request.head.sha }}
# The head's release_tool runs inside the gate (lib/version-gate-train.ts);
# The head's release_tool runs inside the gate (crew/lib/version-gate-train.ts);
# no token may sit in the checkout it stages. The base fetch below is public.
persist-credentials: false
+6 -6
View File
@@ -2,7 +2,7 @@ name: Stable release
# Tags the newest green commit on main as v<VERSION>, publishes the GitHub
# Release, and fast-forwards refs/heads/stable to it (docs/releases.md). Team
# installs follow stable, so this is the only thing that moves them. Runs
# every day at 14:00 UTC as the daily check (scripts/release-cadence.ts, R74):
# every day at 14:00 UTC as the daily check (crew/scripts/release-cadence.ts, R74):
# it cuts when a gated change stable lacks carries an important: changelog
# fragment and stable is 24 hours old, when stable is 7 days old and main has
# gated changes, or on Monday and Thursday past the 72-hour floor; otherwise it
@@ -54,8 +54,8 @@ on:
pull_request:
paths:
- '.github/workflows/weekly-release.yml'
- 'scripts/release-cut.ts'
- 'scripts/release-cadence.ts'
- 'crew/scripts/release-cut.ts'
- 'crew/scripts/release-cadence.ts'
- 'lib/changelog-fragments.ts'
# One cut at a time; a second dispatch waits for the first. A pull request's
@@ -133,7 +133,7 @@ jobs:
[ -n "$SECURITY_REASON" ] && args+=(--security "$SECURITY_REASON")
[ -n "$REGRESSION_REASON" ] && args+=(--regression "$REGRESSION_REASON")
echo "## Stable release ($([ "$DRY_RUN" = true ] && echo 'dry run' || echo 'cut')$([ "$DAILY" = true ] && echo ', daily check'), $GITHUB_EVENT_NAME)" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts "${args[@]}"
bun --no-env-file --config=/dev/null --no-install run crew/scripts/release-cut.ts "${args[@]}"
dry-run:
name: Dry run against main
@@ -161,6 +161,6 @@ jobs:
git fetch --quiet origin '+refs/heads/stable:refs/remotes/origin/stable' 2>/dev/null || echo "origin has no stable yet"
git fetch --quiet --tags origin
echo "## Stable release (dry run from PR #${{ github.event.pull_request.number }})" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run
bun --no-env-file --config=/dev/null --no-install run crew/scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run
echo "## Daily check (dry run from PR #${{ github.event.pull_request.number }})" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run --daily
bun --no-env-file --config=/dev/null --no-install run crew/scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run --daily
+2 -2
View File
@@ -213,7 +213,7 @@ the new defaults.
| See a case's pass rate across recent weekly runs | `bun run eval:pass-rates --case <case-id>` | Free | `gh` with repo read access |
| Measure one red case alone against the ship bar (MEETS 9/10) | `bun run scripts/ship-measure.ts measure --case <case-id> --round baseline` ([bar](docs/TESTING_INTERNALS.md#ship-measure)) | API spend for 10 trials (12 for behavior); asks above $2/trial, stops at $25/case | Same as paid coverage |
| Measure main's flakiest gate cases (the weekly sweep) | `gh workflow run eval-sweep.yml --ref main [-f k=5] [-f cap_usd=150] [-f dry_run=true]`, or locally `bun run scripts/ship-measure.ts sweep --dry-run` ([sweep](docs/TESTING_INTERNALS.md#ship-measure-sweep)) | API spend up to $150 per 7 days (`ship_measure_sweep_budget_usd`); `--dry-run` is free | `gh` with repo read access; CI dispatch needs workflow rights |
| Replay what the paid gate costs and what each selection or train policy would have cost ([replay](docs/TESTING_INTERNALS.md#replay-gate-cost)) | `bun run scripts/gate-cost-replay.ts --runs <full-gate run ids> --since <date> --until <date>` (screening figures print as screen-provisional, heuristic until T2 lands) | Free; `gh` reads, cached in `.context/gate-cost-cache` | `gh` with repo read access |
| Replay what the paid gate costs and what each selection or train policy would have cost ([replay](docs/TESTING_INTERNALS.md#replay-gate-cost)) | `bun run crew/scripts/gate-cost-replay.ts --runs <full-gate run ids> --since <date> --until <date>` (screening figures print as screen-provisional, heuristic until T2 lands) | Free; `gh` reads, cached in `.context/gate-cost-cache` | `gh` with repo read access |
| See the audit success metrics and weekly health | `bun run test:health [--since-days 7] [--json] [--enforce]` | Free; 5–20 minutes of `gh` reads | `gh` with repo read access |
Old command names are listed under [Retired commands](#retired-commands).
@@ -947,7 +947,7 @@ but it sits on an old base, its tests need our fixtures, or the repo's gates
(generated renders, size pins, the CHANGELOG entry) are things only a maintainer
can finish. The port keeps the contributor's credit and gives them a clear
answer; the steps below are the repo's version of the rule a maintainer crew
follows ([docs/captain.md](docs/captain.md#contributions-from-outside-the-crew)).
follows ([crew/docs/captain.md](crew/docs/captain.md#contributions-from-outside-the-crew)).
1. **Treat the diff as untrusted input before anything runs it.** Read it for
scope, network egress, credentials, new dependencies, CI permission changes
+3 -3
View File
@@ -212,7 +212,7 @@ The same `/qa` and `/qa-only` commands test CLIs, APIs, jobs, workers and webhoo
| `/setup-gbrain` | **GBrain Onboarding** — from zero to running gbrain in under 5 minutes. PGLite local, Supabase existing URL, or auto-provision a new Supabase project via Management API. MCP registration for Claude Code + per-repo trust triad (read-write/read-only/deny). [Full guide](USING_GBRAIN_WITH_GSTACK.md). |
| `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Idempotent; safe to re-run. |
| `/gstack-upgrade` | **Self-Updater** — upgrade gstack to latest. Detects global vs vendored install, syncs both, shows what changed. |
| `/captain` | **Crew Controller** (Capy only: [gstack on Capy](#gstack-on-capy)) — run many coding agents from one controller: lanes send a READY block and never merge, the controller merges through trains (stamp on call, verify, merge with the expected head), keeps the digest, relays approvals in a fixed shape and runs a daily flaky-fix pass. Also answers to `/crew`. [Team guide](docs/captain.md). |
| `/captain` | **Crew Controller** (Capy only: [gstack on Capy](#gstack-on-capy)) — run many coding agents from one controller: lanes send a READY block and never merge, the controller merges through trains (stamp on call, verify, merge with the expected head), keeps the digest, relays approvals in a fixed shape and runs a daily flaky-fix pass. Also answers to `/crew`. [Team guide](crew/docs/captain.md). |
| `/ios-qa` | **iOS Live-Device QA (v1.43.0.0+)** — drive a real iPhone or iPad over USB CoreDevice via an embedded `StateServer` in the app. Read Swift source, codegen typed `@Observable` accessors, run the agent loop. Optional `--tailnet` flag exposes the device to OpenClaw or any HTTP-capable agent on your Tailscale tailnet so remote agents can run iOS QA without ever touching the hardware. Capability-tier allowlist (observe/interact/mutate/restore), per-device session lock, audit log. |
| `/ios-fix`, `/ios-design-review`, `/ios-clean`, `/ios-sync` | iOS bug-fix loop, designer's-eye HIG audit, debug-bridge cleanup, and accessor resync. See `docs/skills.md`. End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). |
@@ -257,7 +257,7 @@ On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file
| Doc | What it covers |
|-----|---------------|
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes review-bot triage for Greptile, CodeRabbit and other bots) |
| [The recommended harness](docs/recommended-harness.md) | gstack + Capy from zero to the first merged train: install, the Captain and its Drive, trains, the daily flaky pass, the twice-weekly release, the factory scorecard, what is Capy-specific, known Capy limits |
| [The recommended harness](crew/docs/recommended-harness.md) | gstack + Capy from zero to the first merged train: install, the Captain and its Drive, trains, the daily flaky pass, the twice-weekly release, the factory scorecard, what is Capy-specific, known Capy limits |
| [gstack + Capy: the software factory](capy/README.md) | What we built with Capy Captains and gstack, how the pieces fit, the measured before/after, tips, and how Garry uses it |
| [Why gstack](docs/why-gstack.md) | The story, the productivity numbers and their caveats, Karpathy's failure modes, parallel sprints |
| [Install details](docs/install.md) | Setup timing, requirements in detail, team mode, the hooks setup registers, agent harness registration |
@@ -278,7 +278,7 @@ On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file
| [Contributing](CONTRIBUTING.md) | Dev setup, testing, contributor mode, and dev mode |
| [Memorable recall bridge](docs/memorable-workflow-memory.md) | Opt-in third-party workflow memory through gstack: two consents, what gstack hands over and can attest, removal, troubleshooting |
| [Troubleshooting](docs/troubleshooting.md) | Every `not run` / `unavailable` message, what it means, and the fix |
| [Running a Captain crew](docs/captain.md) | One controller, many lanes on Capy: the drive layout, CREW.md, the READY contract, merge trains, approvals, the digest and the daily flaky pass, with templates |
| [Running a Captain crew](crew/docs/captain.md) | One controller, many lanes on Capy: the drive layout, CREW.md, the READY contract, merge trains, approvals, the digest and the daily flaky pass, with templates |
| [Changelog](CHANGELOG.md) | What's new in every version |
## Privacy & Telemetry
+4 -4
View File
@@ -13,12 +13,12 @@
### P3: clarity-judge removal follow-ups (filed 2026-11-04, GST-1371)
- **Delete the four orphaned judge-calibration corpora.** `test/fixtures/judge-calibration/{qa-workflow,qa-health-rubric,workflow-default,cookie-workflow}/` serve no configuration since the skill-prose clarity judges were removed; deleting them in the removal PR put its diff over the /ship review ceiling. Delete them alone once main is unfrozen (or after `review_exclude_paths` lands), and drop their rows from the README's results tables if the record is no longer wanted. **Effort:** S. **Priority:** P3.
- **Refresh the judge counts the removal left stale.** `docs/TESTING_INTERNALS.md`'s all-green probability table counts 24 PR-gate judges and `scripts/gate-cost-replay.ts`'s header says complete full-gate runs hold 24; there are none now (GST-1403 removed the last three). Recompute the table from current rates rather than editing the count alone. **Effort:** S. **Priority:** P3.
- **Refresh the judge counts the removal left stale.** `docs/TESTING_INTERNALS.md`'s all-green probability table counts 24 PR-gate judges and `crew/scripts/gate-cost-replay.ts`'s header says complete full-gate runs hold 24; there are none now (GST-1403 removed the last three). Recompute the table from current rates rather than editing the count alone. **Effort:** S. **Priority:** P3.
- **Retire the empty standalone-judge lane (GST-1403).** `LLM_JUDGE_TOUCHFILES` is empty and `test/skill-llm-eval.test.ts` is gone, but the paid planner, report, rerun, train gate, ship-measure and gate-cost code still carry a judge branch keyed on that file (`JUDGE_FILE`, `selection.judges`, `--skip-judges` in `evals-periodic.yml`, `afterRepairCommand`'s judge form). Remove the branch and the `selection.judges` manifest field together, keeping old manifests parseable, or keep the lane documented as the home for a future standalone judge. **Effort:** M. **Priority:** P3.
### P3: screen verdict reuse lookback (filed 2026-11-02, GST-1027)
- **Screen reuse sees only the newest 50 evals.yml dispatches.** `listGateRuns` (`scripts/recover-receipts.ts`) lists dispatched screens as one unfiltered page (`event=workflow_dispatch`, `per_page=50`), shared with every PR's screens and every train. At week-B traffic (220 dispatches from 2026-10-25 to 11-02, about 27 a day) that is under two days, and nothing says when a PR's earlier screen fell off the page: `gate-reuse find` prints "no earlier executed gate on this PR bound a verdict" and the full screen runs. No miss yet: the longest gap among the week-B re-screens was 44 dispatches (#3607). Page the listing until runs are older than the PR's oldest candidate head (or filter by `created>=`), and print a truncation line when the last page is full. A missed run only costs a screen, never a wrong verdict. **Effort:** S. **Priority:** P3.
- **Screen reuse sees only the newest 50 evals.yml dispatches.** `listGateRuns` (`crew/scripts/recover-receipts.ts`) lists dispatched screens as one unfiltered page (`event=workflow_dispatch`, `per_page=50`), shared with every PR's screens and every train. At week-B traffic (220 dispatches from 2026-10-25 to 11-02, about 27 a day) that is under two days, and nothing says when a PR's earlier screen fell off the page: `gate-reuse find` prints "no earlier executed gate on this PR bound a verdict" and the full screen runs. No miss yet: the longest gap among the week-B re-screens was 44 dispatches (#3607). Page the listing until runs are older than the PR's oldest candidate head (or filter by `created>=`), and print a truncation line when the last page is full. A missed run only costs a screen, never a wrong verdict. **Effort:** S. **Priority:** P3.
### P3: gstack-clean follow-ups (filed 2026-10-31, GSTA-226 E2, #2627)
@@ -3602,7 +3602,7 @@ Linux cookie import shipped in v0.11.11.0 (Wave 3). Supports Chrome, Chromium, B
### Crew template rule 22: say when a lane picks `--screen` (GSTA-165 1a follow-up)
**What:** Update the rule 22 text in `captain/templates/CREW.md` (and its `docs/captain.md` home if it repeats the rule) so a lane picks `--screen` only when `gstack-ship-queue ready` recommends it: the paid selector's mode is `full-fallback` or it cannot select.
**What:** Update the rule 22 text in `captain/templates/CREW.md` (and its `crew/docs/captain.md` home if it repeats the rule) so a lane picks `--screen` only when `gstack-ship-queue ready` recommends it: the paid selector's mode is `full-fallback` or it cannot select.
**Why:** `gstack-ship-queue ready` now recommends `--no-screen` for every diff the selector can narrow, and unscreened PRs fill train seats. A crew instantiated from the template still reads the older screen guidance, so lanes can keep paying for screens the recommendation no longer asks for.
@@ -3620,7 +3620,7 @@ Deferred from plan: /home/user/.capy/drive/captain-gstack-minder/plans/GSTA-165.
**Why:** The trigger for reverting GSTA-165 D1 (screen only full-fallback diffs) is "PR-attributed red trains exceed the forecast (about 10% at size 3, 16% at size 5) by 2x over 20 trains". Without an attribution field nobody can measure it, so the default cannot be revisited on evidence.
**Context:** Restated in handoffs/GSTA-165/v2-additions.md item 6 and unassigned to a PR. Start from the train record in `lib/ship-queue-train-record.ts` and what `train bisect` writes back to it.
**Context:** Restated in handoffs/GSTA-165/v2-additions.md item 6 and unassigned to a PR. Start from the train record in `crew/lib/ship-queue-train-record.ts` and what `train bisect` writes back to it.
Deferred from plan: /home/user/.capy/drive/captain-gstack-minder/plans/GSTA-165.md
+1 -1
View File
@@ -17,7 +17,7 @@ import { classifyBaseRed, classifyLines, parseEvidence, readPause, waitBaseGreen
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { resultLineWithDocs } from '../lib/result-codes';
import { isUnavailable, loadPolicyFromBase, policyUnavailableLine, resolveBaseBranch } from '../lib/ship-policy';
import { git } from '../lib/ship-queue-gate';
import { git } from '../crew/lib/ship-queue-gate';
const HELP = `gstack-base-red — prove a Step 5 failure is the base branch's own, and pause /ship on it
+2
View File
@@ -709,6 +709,7 @@ esac
# so no caller is left with only the line and the re-typing path.
LAUNCH_FILE="${VERDICT%.json}.env"
BIN_DIR=$(cd "$ROOT/bin" 2>/dev/null && pwd -P)
CREW_BIN_DIR=$(cd "$ROOT/crew/bin" 2>/dev/null && pwd -P) || CREW_BIN_DIR=""
write_launch_file() {
local tmp
mkdir -p "$(dirname "$LAUNCH_FILE")" || return 1
@@ -719,6 +720,7 @@ write_launch_file() {
echo "# Never print it or the environment: no \`export \$(...)\`, \`env\`, \`printenv\` or bare \`set\` in a tool fence."
[ -n "$LAUNCH_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$LAUNCH_DIR"):*) ;; *) PATH=$(shq "$LAUNCH_DIR"):\"\$PATH\" ;; esac; export PATH"
[ -n "$BIN_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$BIN_DIR"):*) ;; *) PATH=\"\${PATH:+\$PATH:}\"$(shq "$BIN_DIR") ;; esac; export PATH"
[ -n "$CREW_BIN_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$CREW_BIN_DIR"):*) ;; *) PATH=\"\${PATH:+\$PATH:}\"$(shq "$CREW_BIN_DIR") ;; esac; export PATH"
echo "export GSTACK_SESSION_KIND=unattended"
echo "export GSTACK_STATE_ROOT=$(shq "$GSTACK_STATE_ROOT")"
echo "export GSTACK_EPHEMERAL=$(shq "${GSTACK_EPHEMERAL:-1}")"
+1 -1
View File
@@ -4,7 +4,7 @@
* read from GitHub through gh: merged PRs (crew vs outside), lead time (created
* to merged, READY to merged), gate runs with known cost, red-train rate, flaky
* events, issues opened vs closed, releases cut. Markdown table on stdout plus
* a CSV; /retro Step 7.5 runs it; docs/recommended-harness.md explains each
* a CSV; /retro Step 7.5 runs it; crew/docs/recommended-harness.md explains each
* line and where gstack's own numbers stand. Read-only: no write, no paid work.
*
* Usage:
+6 -4
View File
@@ -354,10 +354,12 @@ GSTACK_RETIRED_SKILLS="plan-eng-review investigate"
# CREW_SKILLS in scripts/discover-skills.ts (test/crew-skills-on-capy.test.ts).
GSTACK_CREW_SKILLS="captain"
# gstack_on_capy — true on a Capy machine: the test gstack-capy-install's bun
# shim and gstack-update-channel.sh already use (CAPY_MACHINE_CONFIG set, or
# ~/.capy/system present).
gstack_on_capy() { [ -n "${CAPY_MACHINE_CONFIG:-}" ] || [ -d "${HOME:-/nonexistent}/.capy/system" ]; }
# gstack_on_capy — true wherever Capy has run: CAPY_MACHINE_CONFIG set (cloud
# machines), or any ~/.capy entry. A paired device (a Mac running Capy) has no
# CAPY_MACHINE_CONFIG but has ~/.capy/system and ~/.capy/device. Any ~/.capy
# counts, so an unclear case installs the crew skills rather than dropping
# /captain for a Capy user.
gstack_on_capy() { [ -n "${CAPY_MACHINE_CONFIG:-}" ] || [ -e "${HOME:-/nonexistent}/.capy" ]; }
# GSTACK_UNREGISTERED_SKILLS — what setup and gstack-relink keep off every host
# on this machine: the retired skills, plus the crew skills off Capy.
+2 -2
View File
@@ -8,12 +8,12 @@
* base branch and prints one `LANE_CONFLICT:` line per planned file it already
* changes (PR number, branch, hunk count), then `LANE_CHECK: clear | conflicts | unresolved`.
* Exit 0 clear, 1 conflicts, 3 when a lane could not be resolved and nothing conflicts.
* Logic: lib/lane-ownership.ts. Codes: LANE_CONFLICT, LANE_REF_UNAVAILABLE (lib/result-codes.ts).
* Logic: crew/lib/lane-ownership.ts. Codes: LANE_CONFLICT, LANE_REF_UNAVAILABLE (lib/result-codes.ts).
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { laneCheck, renderLaneCheck } from '../lib/lane-ownership';
import { laneCheck, renderLaneCheck } from '../crew/lib/lane-ownership';
import { resultLine } from '../lib/result-codes';
const HELP = `gstack-lane-check — one owner per file across in-flight lanes
+1 -1
View File
@@ -13,7 +13,7 @@
* for the full review; --write also saves brief.md in the run directory.
* `render --captain` prints the plan ask in the shape Capy Captains relay (lib/captain-brief.ts): header with
* the "do it" reply, Before → after, Why it matters, What caused this work, Big decisions, What we need from
* you (the lettered ask lines of docs/captain.md § The ask format, headed by --captain-name), and one
* you (the lettered ask lines of crew/docs/captain.md § The ask format, headed by --captain-name), and one
* trailing line for the relaying agent; a plan missing a section exits 1
* (CAPTAIN_BRIEF_INCOMPLETE) unless --allow-incomplete. --write saves captain-brief.md.
* `close` rewrites every `draft direction stands until the owner decides` line in
+7 -7
View File
@@ -27,7 +27,7 @@
* under changelog_fragments (its READY receipt note), and under a
* ship policy with a `train` block: merge trains push train/<nonce> branches
* and the refs/notes/gstack-trains records, and dispatch through eval-bg.
* Logic: lib/ship-queue.ts, lib/ship-queue-gate.ts, lib/ship-queue-train*.ts
* Logic: lib/ship-queue.ts, crew/lib/ship-queue-gate.ts, crew/lib/ship-queue-train*.ts
* (loaded only on the train paths).
*/
import { spawnSync } from 'node:child_process';
@@ -38,7 +38,7 @@ import { versionAtRef } from '../lib/restamp';
import { RESULT_CODES } from '../lib/result-codes';
import { resolveBaseBranch } from '../lib/ship-policy';
import { loadBasePolicy, ready, stamp, verify, type QueueContext, type StepOutput } from '../lib/ship-queue';
import { attemptFile, listAttempts, prepareGateAhead, recordGateAhead, type QueueFailure } from '../lib/ship-queue-gate';
import { attemptFile, listAttempts, prepareGateAhead, recordGateAhead, type QueueFailure } from '../crew/lib/ship-queue-gate';
import { logShipCall } from '../lib/ship-call-log';
logShipCall('gstack-ship-queue');
@@ -172,14 +172,14 @@ async function main(argv: string[]): Promise<number> {
const pr = flag(args, '--pr');
if (cmd === 'gate-ahead') return gateAhead(ctx, args);
if (cmd === 'train') {
const train = await import('../lib/ship-queue-train-cli');
const train = await import('../crew/lib/ship-queue-train-cli');
if (!args[0] || args[0].startsWith('-')) { process.stdout.write(train.TRAIN_HELP + '\n'); return args.includes('--help') || args.includes('-h') ? EXIT.ok : EXIT.usage; }
const loaded = args[0] === 'build' || args[0] === 'land' ? loadBasePolicy(ctx) : null;
if (loaded && !('message' in loaded) && loaded.policy.merge === 'direct') return finish(`train ${args[0]}`, { ok: false, lines: [], run: 'none', failure: { code: 'QUEUE_NOT_READY', message: 'the base policy sets merge: direct, so trains are off', fix: 'land each READY PR with gstack-ship-queue land --pr <n> --pin <sha> (docs/ship-policy.md#land-direct)' } });
return train.trainMain(ctx, args);
}
if (cmd === 'wave') return (await import('../lib/ship-queue-wave')).waveMain(ctx, args);
if (cmd === 'verify' && flag(args, '--post-merge') !== undefined) return (await import('../lib/ship-queue-train-cli')).postMergeMain(ctx, args);
if (cmd === 'wave') return (await import('../crew/lib/ship-queue-wave')).waveMain(ctx, args);
if (cmd === 'verify' && flag(args, '--post-merge') !== undefined) return (await import('../crew/lib/ship-queue-train-cli')).postMergeMain(ctx, args);
if (cmd === 'land') {
const l = await import('../lib/ship-queue-land');
if (args.includes('--census')) return finish('land', l.census(ctx));
@@ -198,7 +198,7 @@ async function main(argv: string[]): Promise<number> {
const gate = { gateExit: gateExit === undefined ? undefined : Number(gateExit), gateLog: flag(args, '--gate-log') };
if (cmd === 'ready') {
const loaded = loadBasePolicy(ctx);
if (!('message' in loaded) && loaded.policy.train) return (await import('../lib/ship-queue-train-cli')).readyMain(ctx, args, { pr, pregate: flag(args, '--pregate'), ...gate });
if (!('message' in loaded) && loaded.policy.train) return (await import('../crew/lib/ship-queue-train-cli')).readyMain(ctx, args, { pr, pregate: flag(args, '--pregate'), ...gate });
if (['--screen', '--no-screen', '--broad'].some(f => args.includes(f))) process.stderr.write('gstack-ship-queue ready: --screen/--no-screen/--broad ignored: the base policy declares no train block\n');
return finish('ready', ready(ctx, { pr, pregate: flag(args, '--pregate'), ...gate }));
}
@@ -214,7 +214,7 @@ async function main(argv: string[]): Promise<number> {
}));
}
if (cmd === 'verify') {
if (flag(args, '--train') || args.includes('--screen')) return (await import('../lib/ship-queue-train-cli')).verifyMain(ctx, args, pr);
if (flag(args, '--train') || args.includes('--screen')) return (await import('../crew/lib/ship-queue-train-cli')).verifyMain(ctx, args, pr);
const record = flag(args, '--record');
return finish('verify', verify(ctx, {
pr, gated: flag(args, '--gated'), record: record ? path.resolve(record) : undefined, receipt: flag(args, '--receipt'),
+2 -2
View File
@@ -51,7 +51,7 @@ import {
} from '../lib/ship-receipt';
import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs';
import { comparePath, contextPacket, PACKET_SECTION_CAP, readScopedReview, touchedPaths } from '../lib/ship-patch';
import { planRecheck, readRecheck, recheckPacket, recheckRecordTemplate } from '../lib/ship-recheck';
import { planRecheck, readRecheck, recheckPacket, recheckRecordTemplate } from '../crew/lib/ship-recheck';
import { carryExcludeBinds, deeper, depthBelow, exceptionHead, exceptionLoopProblem, exceptionProblems, parseReviewLoopException, parseReviewTargeted, plainDocsSince, platformReceiptDepth, receiptExclude, receiptReviewAtHead, receiptTargetedAtHead, reviewLoopExceptionLine, targetedClassesSince, writeExcludeBinds } from '../lib/ship-receipt-depth';
import { RISK_CLASSES, type RiskClass } from '../lib/review-risk';
import { checkReceiptProse, proseLine } from '../lib/ship-receipt-prose';
@@ -845,7 +845,7 @@ function cmdCarry(rest: string[], json: boolean): number {
// Evidence for the record, never the decision: a slow range-diff is reported unavailable rather than waited for.
const rangeRaw = spawnSync('git', ['range-diff', '--no-color', `${rangeBase}..${from}`, `${headBase}..${head}`], { cwd: repoRoot, encoding: 'utf8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
const rangeDiff = rangeRaw.status === 0 ? (rangeRaw.stdout.trim().split('\n').slice(0, 20).join('\n') || '(empty: identical ranges)') : `unavailable: ${(rangeRaw.stderr ?? '').trim().split('\n')[0] || (rangeRaw.signal ? `range-diff killed by ${rangeRaw.signal} after 15 s` : 'range-diff failed')}`;
// J1: a bounded product change since the reviewed head gets one reader on the range-diff instead of a full /ship (lib/ship-recheck.ts).
// J1: a bounded product change since the reviewed head gets one reader on the range-diff instead of a full /ship (crew/lib/ship-recheck.ts).
if (changedProduct.length) {
const named = changedProduct.filter(p => !p.startsWith('(') && !p.includes(' ('));
const inter = named.length ? spawnSync('git', ['diff', '--no-color', '--no-ext-diff', from, head, '--', ...named.map(p => `:(literal)${p}`)], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024 }) : null;
+4 -4
View File
@@ -110,7 +110,7 @@ through it to one person (the owner). The operating model below is what keeps do
lanes merging into one base branch every day without a red base: lanes never merge, a
READY block is the only merge trigger, every merge runs through a train, every decision
the owner makes is recorded in their own words, and flakes are fixed daily instead of
retried away. The team guide, drive layout included, is `docs/captain.md`; the copy-paste
retried away. The team guide, drive layout included, is `crew/docs/captain.md`; the copy-paste
files are in `captain/templates/` next to this skill.
Usage: `/captain [setup | ready <pr> | train | digest | approvals | flaky-pass]`; `/crew` is
@@ -154,7 +154,7 @@ rules file read the same names. A repo without a `train` block runs the per-PR R
flow: the same block, no slots.
The rules file is the lanes' contract, and it is pointers plus parameters: each rule is
one line that names its home in gstack (`docs/captain.md`, the ship-policy, pregate and
one line that names its home in gstack (`crew/docs/captain.md`, the ship-policy, pregate and
testing docs, the /ship and /review skills, the repository's own AGENTS.md), so no crew
carries its own copy of how to do things. The rules: session-start install check,
validation discipline, untrusted external diffs, file ownership, pre-approved spend, the
@@ -364,7 +364,7 @@ recommendation is option 1; "do it" takes every recommendation in the last list,
option ("A2") takes that option instead. When the owner has to do something, the line
carries the exact link to the page (never a menu path; a Capy app page gets its exact path
and field), numbered steps in the simplest words, and the one word to reply when done.
Home: `docs/captain.md`, The ask format.
Home: `crew/docs/captain.md`, The ask format.
**Plan before build.** A lane that builds something runs a sized `/autoplan` on its brief as
its first act after the session-start check and sends the controller the plan ask above; the controller relays it, and the
@@ -375,7 +375,7 @@ or upgrade, red main, a security bug) and fixes from an already-diagnosed, audit
list are exempt. Other bug fixes skip plan review (`--kind fix`, R65) but carry a repro
on current main or a failing test written first; features get the lean run (`--kind feature`). The brief a
controller hands a lane, with its `Plan: required | approved | exempt` line, is
`captain/templates/brief.md`; the rule is `docs/captain.md` (Plan before build).
`captain/templates/brief.md`; the rule is `crew/docs/captain.md` (Plan before build).
**C. The merge ask.** Every READY the controller brings to the owner, and every PR body:
before/after images (uploaded with the host's PR asset tool, each captioned with what
+4 -4
View File
@@ -38,7 +38,7 @@ through it to one person (the owner). The operating model below is what keeps do
lanes merging into one base branch every day without a red base: lanes never merge, a
READY block is the only merge trigger, every merge runs through a train, every decision
the owner makes is recorded in their own words, and flakes are fixed daily instead of
retried away. The team guide, drive layout included, is `docs/captain.md`; the copy-paste
retried away. The team guide, drive layout included, is `crew/docs/captain.md`; the copy-paste
files are in `captain/templates/` next to this skill.
Usage: `/captain [setup | ready <pr> | train | digest | approvals | flaky-pass]`; `/crew` is
@@ -82,7 +82,7 @@ rules file read the same names. A repo without a `train` block runs the per-PR R
flow: the same block, no slots.
The rules file is the lanes' contract, and it is pointers plus parameters: each rule is
one line that names its home in gstack (`docs/captain.md`, the ship-policy, pregate and
one line that names its home in gstack (`crew/docs/captain.md`, the ship-policy, pregate and
testing docs, the /ship and /review skills, the repository's own AGENTS.md), so no crew
carries its own copy of how to do things. The rules: session-start install check,
validation discipline, untrusted external diffs, file ownership, pre-approved spend, the
@@ -292,7 +292,7 @@ recommendation is option 1; "do it" takes every recommendation in the last list,
option ("A2") takes that option instead. When the owner has to do something, the line
carries the exact link to the page (never a menu path; a Capy app page gets its exact path
and field), numbered steps in the simplest words, and the one word to reply when done.
Home: `docs/captain.md`, The ask format.
Home: `crew/docs/captain.md`, The ask format.
**Plan before build.** A lane that builds something runs a sized `/autoplan` on its brief as
its first act after the session-start check and sends the controller the plan ask above; the controller relays it, and the
@@ -303,7 +303,7 @@ or upgrade, red main, a security bug) and fixes from an already-diagnosed, audit
list are exempt. Other bug fixes skip plan review (`--kind fix`, R65) but carry a repro
on current main or a failing test written first; features get the lean run (`--kind feature`). The brief a
controller hands a lane, with its `Plan: required | approved | exempt` line, is
`captain/templates/brief.md`; the rule is `docs/captain.md` (Plan before build).
`captain/templates/brief.md`; the rule is `crew/docs/captain.md` (Plan before build).
**C. The merge ask.** Every READY the controller brings to the owner, and every PR body:
before/after images (uploaded with the host's PR asset tool, each captioned with what
+1 -1
View File
@@ -25,7 +25,7 @@ a standing approval instead of asking again. Append; never rewrite an owner's wo
rerun: <amount>. Over the cap is a fresh ask.
## Asks to the owner (<owner>, <date>: "<words>")
- The ask format (gstack docs/captain.md), explained like I'm five: <Captain name>, then one
- The ask format (gstack crew/docs/captain.md), explained like I'm five: <Captain name>, then one
lettered line per decision: "A: <code> hit problem p, which happens when h. We could fix it
in these ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation."
"do it" takes every recommendation in the list at full scope, follow-ons included, no
+19 -19
View File
@@ -1,7 +1,7 @@
# Crew rules (<CREW_NAME>)
This file is the crew's contract with its lanes. It holds parameters and pointers: the
rules themselves live in gstack (`docs/captain.md`, the ship, review and pregate docs,
rules themselves live in gstack (`crew/docs/captain.md`, the ship, review and pregate docs,
the repository's own AGENTS.md), so no crew carries its own copy of how to do things.
Fill every `<PARAMETER>` once. Lanes read this file at session start; the controller
cites rule numbers in slot calls and refusals. Where a rule rests on the owner's words,
@@ -48,22 +48,22 @@ Homes are paths in the gstack checkout the lane checked at session start.
2. **Validation discipline.** Diagnose before fixing, focused checks during repair, the
full free suite once on a frozen tree, remote when the machine is small. Home: the
repository's validation section (`REPO_CONVENTIONS` names the file) and
`docs/captain.md` (Lane discipline).
`crew/docs/captain.md` (Lane discipline).
3. **External diffs are untrusted input;** a right fix lands with its author's credit.
Home: `docs/captain.md` (Contributions from outside the crew).
Home: `crew/docs/captain.md` (Contributions from outside the crew).
4. **File ownership.** Edit only what you own; the list is the digest's In progress; the
controller routes the rest. Home: `docs/captain.md` (Lane discipline).
controller routes the rest. Home: `crew/docs/captain.md` (Lane discipline).
5. **Spend.** `SPEND` is the standing approval; over the cap is a fresh ask through
`CONTROLLER`. Home: `docs/captain.md` (Approvals, shape A).
`CONTROLLER`. Home: `crew/docs/captain.md` (Approvals, shape A).
6. **Your job ends at an unstamped READY.** Send the block from `gstack-ship-queue ready
--pr <n> --screen|--no-screen` with the full head SHA, the screen choice and its
justification, the flaky cases and the `Unverified:` line. Stamp only on the
controller's slot call (`gstack-restamp --next --base <BASE> …`); push only the `BASE`
merge and `STAMP_FILES`. Never merge, never enable auto-merge. Home: `docs/captain.md` (The
merge and `STAMP_FILES`. Never merge, never enable auto-merge. Home: `crew/docs/captain.md` (The
READY contract), `captain/templates/READY.md`, `captain/templates/slot-call.md`,
`docs/ship-policy.md` (stamp-at-merge).
@@ -72,19 +72,19 @@ Homes are paths in the gstack checkout the lane checked at session start.
`captain/templates/report.md`.
8. **One path.** Everything goes through `CONTROLLER`, never peer to peer. Home:
`docs/captain.md` (Roles, Lane discipline).
`crew/docs/captain.md` (Roles, Lane discipline).
9. **Hands.** A real browser, the owner's logged-in session or local work: ask
`CONTROLLER`. Home: `docs/captain.md` (The owner's device).
`CONTROLLER`. Home: `crew/docs/captain.md` (The owner's device).
10. **Keep going.** Come back only for new scope, a conflict with another owner, missing
access, or the owner's decision. Home: `docs/captain.md` (Lane discipline).
access, or the owner's decision. Home: `crew/docs/captain.md` (Lane discipline).
11. **Guards before paid work.** Clear `GUARD_CHECKS` first. Home: `docs/captain.md`
11. **Guards before paid work.** Clear `GUARD_CHECKS` first. Home: `crew/docs/captain.md`
(Lane discipline).
12. **Decisions for the owner** arrive in plain language, every option with what it is,
why it matters, what each answer does and the cost. Home: `docs/captain.md`
why it matters, what each answer does and the cost. Home: `crew/docs/captain.md`
(Approvals, shape B), `docs/unattended.md` (Owner brief).
13. **Pregate on small machines.** Run `REMOTE_PREGATE` to clear the requires-remote
@@ -93,7 +93,7 @@ Homes are paths in the gstack checkout the lane checked at session start.
14. **Don't merge `BASE` into a READY PR.** Only on the controller's word after a real-code
train conflict (merge, never rebase, free suite, READY again) or as part of the stamp
commit. A merge whose product diff is unchanged carries the /ship receipt; a rebase
onto a new tree costs a full /ship. Home: `docs/captain.md` (Merge trains),
onto a new tree costs a full /ship. Home: `crew/docs/captain.md` (Merge trains),
`docs/ship-policy.md` (Run a merge train, Carrying a receipt).
15. **CHANGELOG entry under `## [Unreleased]` before READY;** version files stay at
@@ -103,27 +103,27 @@ Homes are paths in the gstack checkout the lane checked at session start.
16. **Flaky reruns.** Once, same tree, logged in `handoffs/flaky-ledger.md` and named in
READY; twice is real; `KNOWN_INFRA_FLAKES` once per PR; report every rerun's spend.
Home: `docs/captain.md` (The daily flaky pass), `docs/TESTING_INTERNALS.md` (Flaky
Home: `crew/docs/captain.md` (The daily flaky pass), `docs/TESTING_INTERNALS.md` (Flaky
reruns).
17. **New files follow `REPO_CONVENTIONS`.** Home: the repository's AGENTS.md.
18. **Porting a contributor's PR.** Review and test the port head first; keep their
commits, yours on top. Home: `docs/captain.md` (Contributions from outside the crew).
commits, yours on top. Home: `crew/docs/captain.md` (Contributions from outside the crew).
19. **CHANGELOG-only conflicts** are the train's, not yours (folded into 14). Home:
`docs/ship-policy.md` (/ship before READY: the stamp keeps the pin's receipt).
20. **READYs from before `CONTRACT_DATE`** need a fresh `ready` receipt. Home:
`docs/captain.md` (The READY contract).
`crew/docs/captain.md` (The READY contract).
21. **Approvals.** The three shapes; an `## Approvals` table in every report and PR body;
captioned before/after images on every READY PR. Home: `docs/captain.md` (Approvals),
captioned before/after images on every READY PR. Home: `crew/docs/captain.md` (Approvals),
`capy/skill/gstack/captain.md`.
22. **/ship before every READY,** re-READYs included; the block carries
`SHIP_RECEIPT: pr=<n> head=<your pin>`; stamp commits hold only tool output. Home:
`docs/ship-policy.md` (/ship before READY), `docs/captain.md` (The lane's /ship
`docs/ship-policy.md` (/ship before READY), `crew/docs/captain.md` (The lane's /ship
sequence).
23. **Review loops converge.** gstack's review convergence policy in the /ship and /review
@@ -139,7 +139,7 @@ Homes are paths in the gstack checkout the lane checked at session start.
re-pins under a written grant, S1 emergencies (broken install or upgrade, red main,
a security bug) and fixes from an already-diagnosed, audited problem list. Any other
bug fix is exempt too (`gstack-autoplan next --kind fix`, R65) and carries a repro on
current main or a failing test written first in the PR; a feature gets the lean run (`--kind feature`). Home: `docs/captain.md` (Plan before build),
current main or a failing test written first in the PR; a feature gets the lean run (`--kind feature`). Home: `crew/docs/captain.md` (Plan before build),
`captain/templates/brief.md`.
@@ -147,5 +147,5 @@ Homes are paths in the gstack checkout the lane checked at session start.
Add rules here, numbered from 25, each with the date, the owner's words where there are
any, and the thread that produced it. A crew whose own rules already took 24 before rule
24 shipped keeps its numbering and cites `docs/captain.md` (Plan before build) by section
24 shipped keeps its numbering and cites `crew/docs/captain.md` (Plan before build) by section
name. A rule that stays here for long is a gap in gstack: file it.
+1 -1
View File
@@ -12,7 +12,7 @@ every row with its thread and an age. Codes are the thread codes the host assign
- B: …
"do it" takes every recommendation above; a named option ("A2") takes that option instead. The
ask format is gstack docs/captain.md, The ask format.
ask format is gstack crew/docs/captain.md, The ask format.
## New
+3 -3
View File
@@ -3,7 +3,7 @@
Sent by the controller when a lane starts, and again when a lane takes on scope its brief
did not name. The lane reads the rules file first, then this. Everything in angle brackets
is filled by the controller; `Plan:` says whether the lane plans first
(`docs/captain.md`, Plan before build).
(`crew/docs/captain.md`, Plan before build).
```text
Lane: <thread code> · Controller: <CONTROLLER> · Rules: <drive>/CREW.md · Base: <BASE>
@@ -18,7 +18,7 @@ Context: after the install check, read the gstack skill's lane-card.md (the chec
send long output to files under ~/.capy/work. A wait under about 30 minutes checks in every
240 s (wait on the operation with duration_seconds 240, then a silent re-wait, for about 30
minutes at most); longer waits sleep on their operations, and so does the lane while /ship runs. Past twice the median lane spend, write handoffs/<code>/next.md and
tell me in one line (docs/captain.md, Lane context).
tell me in one line (crew/docs/captain.md, Lane context).
1. Plan (when Plan: required). After the session-start install check, install for autoplan
and follow autoplan/DRIVER.md "On Capy" (launch file sourced, GSTACK_AUTOPLAN_CHECKPOINT_DIR
@@ -46,7 +46,7 @@ Context: after the install check, read the gstack skill's lane-card.md (the chec
its checks and comments once, stop the /ship subagent, run gstack-ship-queue ready
--pr <n> --pregate <pregate.json> --screen|--no-screen at the pin (its notes on origin),
audit your decision trail, write Attention and send READY; after READY, handle PR events
as usual (docs/captain.md, the lane's /ship sequence).
as usual (crew/docs/captain.md, the lane's /ship sequence).
Done when: the READY block is sent at a frozen head, reports/<thread code>.md is written,
and its Approvals table carries the plan approval row (the owner's words, the date) or the
+1 -1
View File
@@ -25,7 +25,7 @@ flaky: <cases, both runs, ledger date | none> · pregate: pass tree=<tree12>
The controller's merge report: the PR link, the key image inline, the version it will
take, the slot, and, if a decision is attached, its lettered line in the ask format
(gstack docs/captain.md): "A: … Recommendation: x because why. A1 to take recommendation."
(gstack crew/docs/captain.md): "A: … Recommendation: x because why. A1 to take recommendation."
If the merge needs the owner to act (approve a deployment, grant access), the line carries
the exact link to that page and numbered steps in the simplest words, ending with the one
word to reply when done (a Capy app page: its exact path and field instead of a link).
+1 -1
View File
@@ -18,7 +18,7 @@ READY block sent: <yes, date> · SHIP_RECEIPT: pr=<n> head=<sha12 of the pin, as
## Needs
<"nothing", or the owner's decisions in the ask format (gstack docs/captain.md): one lettered line each, "A1 to take recommendation">
<"nothing", or the owner's decisions in the ask format (gstack crew/docs/captain.md): one lettered line each, "A1 to take recommendation">
## Cost
+8 -8
View File
@@ -9,8 +9,8 @@ lands on main green, reviewed, and in the order he chose.
This page is what we built, how the pieces fit, why we think it is the best way
to run a software factory today, what we learned the hard way, and how Garry
actually uses it. The step-by-step setup is in
[docs/recommended-harness.md](../docs/recommended-harness.md); the controller's
reference is [docs/captain.md](../docs/captain.md).
[crew/docs/recommended-harness.md](../crew/docs/recommended-harness.md); the controller's
reference is [crew/docs/captain.md](../crew/docs/captain.md).
## The numbers
@@ -47,19 +47,19 @@ Most of what made it work did not exist on day one. It came out of Garry's
replies, one at a time, each turned into a rule and then into gstack itself:
- "yes merge when green" became the Captain's standing merge approval, and then
[merge trains](../docs/captain.md#merge-trains): one paid gate covers up to
[merge trains](../crew/docs/captain.md#merge-trains): one paid gate covers up to
five READY PRs, and the Captain merges them slot by slot at the exact head.
- "you can just run the failed test again and note that it is flaky and we
should have a flaky test fix pass every day" became the one-rerun rule and a
[daily flaky pass](../docs/captain.md#the-daily-flaky-pass) automation.
[daily flaky pass](../crew/docs/captain.md#the-daily-flaky-pass) automation.
- "for each plan approval i want a before/after, why it matters, what caused us
to work on this, and big decisions you had to make" became
`gstack-owner-brief` and [the three approval shapes](../docs/captain.md#approvals-three-shapes).
`gstack-owner-brief` and [the three approval shapes](../crew/docs/captain.md#approvals-three-shapes).
- "also we should really be using the gstack ship skill before merging PRs"
became the `SHIP_RECEIPT` every READY must carry at its head.
- "When you make a new thread or take on new functionality can you remember to
use gstack autoplan to make a plan for it and then eli5 for approval from me?"
became [Plan before build](../docs/captain.md#plan-before-build).
became [Plan before build](../crew/docs/captain.md#plan-before-build).
- "Gstack should be the actual holder of how to do things not one off Capy
files" is why all of the above lives in this repository, not in anyone's Drive.
@@ -209,8 +209,8 @@ did it go on by default.
## Where to go next
- [docs/recommended-harness.md](../docs/recommended-harness.md): set it up, day 0 to the first merged train.
- [docs/captain.md](../docs/captain.md): the controller's reference (READY contract, trains, approvals, digest, flaky pass).
- [crew/docs/recommended-harness.md](../crew/docs/recommended-harness.md): set it up, day 0 to the first merged train.
- [crew/docs/captain.md](../crew/docs/captain.md): the controller's reference (READY contract, trains, approvals, digest, flaky pass).
- [capy/skill/gstack/](skill/gstack/): the Capy skill every thread loads, its [factory.md](skill/gstack/factory.md) one-page summary of how crews work, and its [captain.md](skill/gstack/captain.md) for lanes.
- [docs/why-gstack.md](../docs/why-gstack.md): the longer story behind gstack itself.
+3 -3
View File
@@ -50,12 +50,12 @@ Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.
## Plan, approve, ship
Every Capy thread using gstack follows these for new work, Captain or not. The rule in full is `docs/captain.md`, "Plan before build"; the lane side is section 3 of captain.md in this folder.
Every Capy thread using gstack follows these for new work, Captain or not. The rule in full is `crew/docs/captain.md`, "Plan before build"; the lane side is section 3 of captain.md in this folder.
- **Plan first.** Run `/autoplan` on new functionality right after the install check. The lean run (the Capy default) is one reviewer over plan and draft code; `--full` adds every phase.
- **ELI5, then the word.** Send the plan and gate to the person, or the Captain, in plain language: `gstack-owner-brief render --captain --out <run dir> --captain-name "<Captain name>"`, its choices as lettered lines in the ask format (`docs/captain.md`) and "do it" taking every recommendation at full scope. Build only after their word is answered into the run and it reports `APPROVED`; then `gstack-approvals import --autoplan <run dir> --by "<owner>"`.
- **ELI5, then the word.** Send the plan and gate to the person, or the Captain, in plain language: `gstack-owner-brief render --captain --out <run dir> --captain-name "<Captain name>"`, its choices as lettered lines in the ask format (`crew/docs/captain.md`) and "do it" taking every recommendation at full scope. Build only after their word is answered into the run and it reports `APPROVED`; then `gstack-approvals import --autoplan <run dir> --by "<owner>"`.
- **/ship before ready.** Run /ship at the final pushed head before a PR is called ready; `SHIP_RECEIPT: pr=<n> head=<head>` must name that head, and a later push needs a new /ship. The review inside /ship, like the autoplan, is sized to the change.
- **Sleep while /ship runs, then stop it.** After dispatching /ship the parent ends its turn with no PR watch until /ship completes (a re-READY drops a held watch first). On that wake it subscribes to the PR (renewed on expiry, cancelled at merge or close), reads its checks and comments once, stops the subagent, runs `gstack-ship-queue ready` and owns the PR thereafter: slot stamp, re-READY, receipt carry (`docs/captain.md`, the lane's /ship sequence). A stopped /ship subagent woken by a check, review or merge notice, not its parent, reports what woke it, its pin and the PR head (`gh pr view <n> --json headRefOid --jq .headRefOid`) in one line and ends the turn without acting, even if the head moved. A message from its parent is an instruction it carries out.
- **Sleep while /ship runs, then stop it.** After dispatching /ship the parent ends its turn with no PR watch until /ship completes (a re-READY drops a held watch first). On that wake it subscribes to the PR (renewed on expiry, cancelled at merge or close), reads its checks and comments once, stops the subagent, runs `gstack-ship-queue ready` and owns the PR thereafter: slot stamp, re-READY, receipt carry (`crew/docs/captain.md`, the lane's /ship sequence). A stopped /ship subagent woken by a check, review or merge notice, not its parent, reports what woke it, its pin and the PR head (`gh pr view <n> --json headRefOid --jq .headRefOid`) in one line and ends the turn without acting, even if the head moved. A message from its parent is an instruction it carries out.
- **No merge without the owner's word.** A thread never merges on its own; under a Captain the controller merges on a recorded standing approval, and a thread with no Captain asks the person.
Exempt from the plan step: read-only work, the daily flaky pass's repairs, slot stamps, CHANGELOG-only resolves, cap re-pins under a written grant, S1 emergencies (a broken install or upgrade, a red main, a security bug) and fixes from an already-diagnosed, audited problem list. Other bug fixes skip plan review (`--kind fix`, R65) but carry a repro on current main or a failing test written first; features get the lean run (`--kind feature`).
+3 -3
View File
@@ -6,7 +6,7 @@ the thread reports to a Captain (a crew thread) or straight to a person. `gstack
short form as the `capy-asks` block whenever `GSTACK_CAPY=1` (the Capy launch line sets it).
`GSTACK_CAPY_ASKS=0` drops that block for one session. This is the lane's side. The controller's side (the
READY contract, merge trains, the digest, the daily flaky pass, and the templates for the crew rules file
and the Captain's drive) is the `/captain` skill and `docs/captain.md` in the gstack checkout.
and the Captain's drive) is the `/captain` skill and `crew/docs/captain.md` in the gstack checkout.
## 1. How approvals work, per thread
@@ -77,7 +77,7 @@ Under a Captain, a thread that builds something plans before any product code. A
brief, sized to the work, is its first act, the gate goes up in the shape below, and the build starts only
when the answer is in the run and it reports `APPROVED`. The short form every Capy thread follows, with the
exempt work (read-only threads, flaky-pass repairs, slot stamps, CHANGELOG resolves, cap re-pins under a
grant), is the "Plan, approve, ship" section of SKILL.md in this folder; the rule in full is `docs/captain.md`,
grant), is the "Plan, approve, ship" section of SKILL.md in this folder; the rule in full is `crew/docs/captain.md`,
Plan before build.
Every plan or gate you bring for approval has these parts, in this order, written so someone outside the
@@ -89,7 +89,7 @@ project can follow it:
3. **What caused this work.** The trigger: an issue or PR link, a failed run, a user report, or the
person's request, quoted.
4. **Big decisions.** For each: the options considered, the pick and why, and what would flip it.
5. **What we need from you.** The ask format (`docs/captain.md`, The ask format): the Captain's name, then one
5. **What we need from you.** The ask format (`crew/docs/captain.md`, The ask format): the Captain's name, then one
lettered line per decision, letters unique across the list: "A: <code> hit problem p, which happens when h.
We could fix it in these ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation."
The recommendation is option 1. Never offer "the plan as first written" as the default.
+4 -4
View File
@@ -3,15 +3,15 @@
What seven Captain crews agreed on. Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person; never merge without their word. Story: capy/README.md; examples: org Drive software-factory/README.md.
1. **Plan, then a yes.** New functionality starts with /autoplan sized to the work; the build waits for "do it". [SKILL.md: Plan, approve, ship]
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output verbatim, and stop. The merge owner merges on green and assigns versions. [docs/captain.md: The READY contract]
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output verbatim, and stop. The merge owner merges on green and assigns versions. [crew/docs/captain.md: The READY contract]
3. **Block only on the floor.** A finding blocks if it would fail a test, break production, or lose data or secrets; the rest goes in the PR body or next wave. [docs/review-policy.md]
4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your rules allow. Fold deferred findings and test follow-ups into one PR. [docs/triage.md]
5. **Severe first, same gates.** A severe user-facing bug jumps the queue with a short plan; receipt, checks and merge owner stay.
6. **Trade findings, not small talk.** Another crew's finding goes to its Captain, through yours, with evidence.
7. **Measure before you act.** Run the real system on the merged tree you ship; a real run outranks a reviewed plan. Each number names its run, commit or uncapped query; say what you did not verify.
8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) first. Controls: no-skill and placebo arms, clean decoys. Report use and pass apart; quote spend from the meter. A new part needs proof it helps or a dated test plan [ETHOS.md: 9. Earn Its Place].
9. **Flakes: one rerun, a ledger, a daily fix.** Only the policy's rerun, never for a harness or infra red; a second red is real; never loosen a test. [docs/captain.md: The daily flaky pass]
9. **Flakes: one rerun, a ledger, a daily fix.** Only the policy's rerun, never for a harness or infra red; a second red is real; never loosen a test. [crew/docs/captain.md: The daily flaky pass]
10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast needing no reply goes in the Captain's notes; never reply just to acknowledge.
11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: <code> hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered steps, the reply word. Never re-ask a standing rule; check what you can yourself. [docs/captain.md: The ask format]
12. **Fresh threads, closed out.** One thread per unit of work; past 500M tokens, rotate [/runaway-stopper]. Crew threads never create threads: ask your Captain (Captain to Captain across projects), or report one you made by code for crew.add. Its Captain archives only its crew's threads; threads outside every crew are the owner's, even its automation's. Size reviews to the diff; read the meter. [docs/ship-budget.md; docs/captain.md: New threads join the crew]
11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: <code> hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered steps, the reply word. Never re-ask a standing rule; check what you can yourself. [crew/docs/captain.md: The ask format]
12. **Fresh threads, closed out.** One thread per unit of work; past 500M tokens, rotate [/runaway-stopper]. Crew threads never create threads: ask your Captain (Captain to Captain across projects), or report one you made by code for crew.add. Its Captain archives only its crew's threads; threads outside every crew are the owner's, even its automation's. Size reviews to the diff; read the meter. [docs/ship-budget.md; crew/docs/captain.md: New threads join the crew]
13. **The owner's model preference.** `gstack-models resolve --role agent` picks agent models, except cross-family voices and eval subjects. [docs/model-policy.md: Your model preference]
@@ -0,0 +1 @@
- **gstack's Capy-only tooling now lives under `crew/`.** The merge-train and wave code, the release gate, the release cut and its daily check, version-gate trains, lane ownership and the paid-eval CI steps only Capy crews run moved from `lib/`, `scripts/` and `bin/` into `crew/lib/`, `crew/scripts/` and `crew/bin/` (`gstack-release-gate`, `gstack-coauthor-trailers`), with their tests in `crew/test/`. The Captain manuals moved to `crew/docs/captain.md` and `crew/docs/recommended-harness.md`, with short pointers left at the old paths. Workflows, imports and docs point at the new paths; behavior is unchanged. Setup copies `crew/lib/` into each host's install folder beside `bin/` and `lib/`, so Windows installs, which copy files instead of linking them, still start `gstack-ship-receipt`, `gstack-pregate` and the other commands that use it. On Capy the session launch file puts `crew/bin/` on PATH, so the commands keep their names; nothing else does, so other installs never see them. `gstack-flaky` and the factory scorecard stay where they are because outside users run them. A Mac or other paired device running Capy now always counts as a Capy machine for `/captain`: any `~/.capy` entry counts, not only `~/.capy/system`, so an unclear case installs the skill instead of dropping it.
+24 -5
View File
@@ -10,7 +10,7 @@ gstack works exactly as it does without it.
- **Captains.** `/captain` runs many agent threads from one controller: lanes
send a READY block and never merge, the controller keeps the digest, relays
approvals in a fixed shape and runs a daily flaky-test pass
([docs/captain.md](../docs/captain.md)).
([docs/captain.md](docs/captain.md)).
- **A merge queue.** `gstack-ship-queue` lands verified pull requests alone or
in trains, checks the exact head before it merges, and stamps the version
only at release time.
@@ -25,13 +25,32 @@ How the whole factory fits together: [capy/README.md](../capy/README.md).
## How it is loaded
A Capy machine is one where `CAPY_MACHINE_CONFIG` is set or `~/.capy/system`
exists, the same test the Capy installer (`bin/gstack-capy-install`) already
uses. `gstack_on_capy` in `bin/gstack-install-registry.sh` is that test, and
A Capy machine is one where `CAPY_MACHINE_CONFIG` is set (cloud machines) or
`~/.capy` exists (cloud machines and paired devices, such as a Mac running
Capy). Any `~/.capy` counts, so when it is unclear the crew skills install
rather than a Capy user losing `/captain`. `gstack_on_capy` in
`bin/gstack-install-registry.sh` is that test, and
`GSTACK_CREW_SKILLS` there lists the crew skills. On a Capy machine setup and
`gstack-relink` register them like any other skill; elsewhere they get no entry
on any host, and an entry an earlier install made is removed.
On Capy the session launch file (`bin/gstack-capy-install`) also puts
`crew/bin/` on PATH, so the crew commands keep their bare names there. Public
CI runs `crew/test/` with the rest of the free suite.
## What is here
| Path | What it holds |
|---|---|
| `bin/` | `gstack-release-gate` (the paid gate on main every 8 hours) and `gstack-coauthor-trailers` (credit for ported contributor work) |
| `lib/` | merge trains and waves (`ship-queue-train*`, `ship-queue-wave`, the land lock, screens, rechecks), the release gate, version-gate trains and lane ownership |
| `scripts/` | the release cut and its daily check (`release-cut`, `release-cadence`), and the paid-eval CI steps only our crews run (train gate and backstop, gate cost, gate reuse, receipt recovery, census arms, judge calibration configs) |
| `test/` | their tests |
| `docs/` | the manuals: [running a Captain crew](docs/captain.md) and [the recommended harness](docs/recommended-harness.md) |
The crew tooling moves here in stages. Code that no public skill calls comes
first; the steps `/ship`, `/review` and `/autoplan` take only for crews move
after they call it through one switch.
after they call it through one switch. Some crew tools stay outside `crew/` for
now because public code still calls them: `/captain`'s own folder (the skill
generator reads top-level folders), `gstack-ship-queue`, receipts, the
pre-gate, spend caps and approvals.
@@ -17,8 +17,8 @@
*/
import * as fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import { EXIT } from '../lib/headless-artifacts';
import { resolveBaseBranch } from '../lib/ship-policy';
import { EXIT } from '../../lib/headless-artifacts';
import { resolveBaseBranch } from '../../lib/ship-policy';
const HELP = `gstack-coauthor-trailers — Co-authored-by trailers for the authors of a ported range
@@ -2,8 +2,8 @@
/**
* gstack-release-gate — the full paid gate on main every cadence_hours, only
* when main moved since the last gate (docs/ship-policy.md#release-gate;
* logic: lib/release-gate.ts). The gate never moves the stable branch:
* scripts/release-cut.ts is its only writer and holds every cut while the
* logic: crew/lib/release-gate.ts). The gate never moves the stable branch:
* crew/scripts/release-cut.ts is its only writer and holds every cut while the
* newest gate is red.
*
* gstack-release-gate run [--dry-run] [--sha <main sha>] [--ignore-cadence]
+657
View File
@@ -0,0 +1,657 @@
# Running a Captain crew on Capy with gstack
This page is for a team that wants one coordinating agent (a Capy Captain, or any
controller agent) to run many worker threads against one repository, with gstack as the
engineering harness. It covers the drive layout, the crew rules file, the READY contract,
merge trains, approvals, the digest and the daily flaky pass. The skill that teaches the
controller the same model at run time is [`/captain`](../../docs/skills.md#captain) (it also answers
to `/crew`); the lane-side rules for a thread working under a Captain are in
[`capy/skill/gstack/captain.md`](../../capy/skill/gstack/captain.md) and
[capy-install.md](../../docs/capy-install.md#working-under-a-captain). Every file named below has a
copy-paste template under [`captain/templates/`](../../captain/templates/). For the story of how
one owner's crews run this day to day, with measured numbers, read
[capy/README.md](../../capy/README.md). Every Capy session also reads
[`capy/skill/gstack/factory.md`](../../capy/skill/gstack/factory.md), a one-page summary of
these practices with pointers to the rules.
## The model in one paragraph
Lanes build on their own branches and machines. A lane's job ends at an **unstamped
READY**: a block printed by `gstack-ship-queue ready` plus the full head SHA, the screen
choice and the flaky cases. The controller validates the block, batches READY pins into a
**merge train**, reads one full gate on the train head, then merges slot by slot: it calls
the slot, the lane stamps the version on call, the controller verifies the tree, merges
with the expected head, verifies again. A red train is bisected on its red cases and
rebuilt at most once. Every decision the owner makes travels through the controller in a
fixed shape and is recorded in the owner's words. Flakes are logged in a ledger and fixed
by a daily pass; nothing is retried into green.
## Roles
- **The owner** decides what only the owner can decide: product and behaviour, spend over
a cap, anything published outside the repository, thresholds and budgets, closing a
contribution, anything destructive. Every answer is recorded in the owner's words.
- **The controller** (the Captain) runs the crew: starts lanes on disjoint files, keeps the
rules file, the digest and the notes, validates READYs, builds and merges trains, calls
slots, relays asks to the owner in the three shapes, runs the flaky pass, and decides
everything that needs no access, credentials, spend over cap or a cited approval. It
archives only its own crew threads, each when its plan for that thread says the work is
done; it knows each thread's next step, so nobody else archives them. A thread outside
every crew belongs to the owner and stays as it is: no Captain archives it, flags it or
lists it for archiving unless the owner asks, even when the Captain's own automation
created it.
- **A lane** (a crew thread) owns named files, builds on its own branch and machine,
runs /ship, sends one READY block, stamps on call (merging the base into its pin only
as part of that stamp commit, or when the controller names a real-code conflict),
writes one report, and never merges a PR.
- **The controller never commits to a lane's branch**, and a lane never pushes to another
lane's. One committer per shared tree.
## Drive layout
The Captain's drive is mounted read-write on every crew thread, so it is the handoff
surface. Keep it small and predictable:
| Path | Who writes | What it holds |
|---|---|---|
| `AGENTS.md` | the controller | standing rules and the owner's decisions, each with date and words ([template](../../captain/templates/AGENTS.md)) |
| `CREW.md` | the controller | the lanes' contract ([template](../../captain/templates/CREW.md)) |
| `DIGEST.md` | the controller | Needs you / New / In progress, rewritten every turn ([template](../../captain/templates/DIGEST.md)) |
| `NOTES.md` | the controller | the append-only timestamped log the digest is derived from ([template](../../captain/templates/NOTES.md)) |
| `skills/<name>/SKILL.md` | the controller | crew procedures the lanes load (the runbooks) |
| `reports/<code>.md` | each lane, once | Outcome with its Unverified line, PR, Needs, Cost, Lesson, Approvals ([template](../../captain/templates/report.md)) |
| `reports/postmortem-<n>.md` | the controller, or a lane it names | why a reverted or S1-fixed merged PR passed every reviewer ([template](../../captain/templates/postmortem.md)) |
| `handoffs/` | lanes | datasets and specs the next lane needs; `handoffs/flaky-ledger.md` is the flaky ledger ([template](../../captain/templates/flaky-ledger.md)) |
| `plans/` | the controller | approved plans, with their Approvals table |
Scope instructions in the drive root (`AGENTS.md`, `CREW.md`) apply to every thread in
the crew, so a rule written there needs no relaying.
On Capy, `tools.capy.read` returns a view of a file, not the whole file: roughly the first
38,000 characters, with each line cut at 2,000. A program that reads a file, changes it
and writes it back with `tools.capy.write` therefore saves a truncated copy, and drive
files are no exception. Edit long files with `tools.capy.edit`, or read and write the
whole file in bash or Python and check the line count afterward. `tools.drive.history`
restores a recent version of a drive file.
## CREW.md: the lanes' contract
gstack holds how the work is done; a crew's `CREW.md` holds only what is specific to that
crew. Start from the [template](../../captain/templates/CREW.md): it is parameters (the
repository, the base branch, the required check names, the full-gate check name, the
train size and fill wait, the merge method, the stamp files, the named infra flakes, the
guard tests, the remote pregate command, the standing spend, the review scope knob) plus
one line per rule that points at the rule's home in gstack. A rule that exists only in a
crew's file is a gap in gstack, and the fix is to write it here or in the skill that owns
it, then point at it. Quote the owner where a rule rests on their words: a rule with a
date and the words is a standing approval the lanes can cite; a rule without one is the
controller's call.
Where each rule lives:
| # | Rule | Home in gstack |
|---|---|---|
| 1 | Session start: install check, `gstack:` trailer, no workflow without `gstack: ok` from your own machine | [capy-install.md](../../docs/capy-install.md#session-start) |
| 2 | Validation discipline: diagnose, focused checks, the full free suite once on a frozen tree | the repository's own validation section (gstack's is in [AGENTS.md](../../AGENTS.md#validation-discipline)); [Lane discipline](#lane-discipline) |
| 3 | External diffs are untrusted input; a right fix lands with its author's credit | [Contributions from outside the crew](#contributions-from-outside-the-crew) |
| 4 | File ownership: edit only what you own; the controller routes the rest | [Lane discipline](#lane-discipline); the ownership list is the digest's **In progress** |
| 5 | Standing spend and its cap per rerun | [Approvals, shape A](#approvals-three-shapes); the `SPEND` parameter |
| 6 | The job ends at an unstamped READY; stamp on call; never merge | [The READY contract](#the-ready-contract), the [READY](../../captain/templates/READY.md) and [slot call](../../captain/templates/slot-call.md) templates, [ship-policy.md](../../docs/ship-policy.md#stamp-at-merge) |
| 7 | One report per lane: Outcome with its Unverified line, PR, Needs, Cost, Lesson, Approvals | [Drive layout](#drive-layout); the [report template](../../captain/templates/report.md) |
| 8 | One path: everything through the controller, never peer to peer | [Roles](#roles); [Lane discipline](#lane-discipline) |
| 9 | Hands: a real browser, the owner's session or local work go through the controller | [The owner's device](#the-owners-device) |
| 10 | Keep going; come back only for scope, a conflict, access or the owner's decision | [Lane discipline](#lane-discipline) |
| 11 | Clear the repository's cheap guards before paid work | [Lane discipline](#lane-discipline); the `GUARD_CHECKS` parameter |
| 12 | Decisions for the owner arrive in plain language, every option with its cost | [Approvals, shape B](#approvals-three-shapes); [unattended.md](../../docs/unattended.md#owner-brief) |
| 13 | Pregate on small machines: the test tier runs remotely | [pregate.md](../../docs/pregate.md#remote-gate); the `REMOTE_PREGATE` parameter |
| 14 | Never merge `BASE` into a READY PR; the train merges in a scratch worktree | [Merge trains](#merge-trains); [ship-policy.md](../../docs/ship-policy.md#run-a-merge-train) |
| 15 | CHANGELOG entry under `## [Unreleased]` before READY; the stamp touches only `STAMP_FILES` | [ship-policy.md](../../docs/ship-policy.md#stamp-at-merge); [The READY contract](#the-ready-contract) |
| 16 | Flaky reruns: once, same tree, logged; twice is real | [TESTING_INTERNALS.md](../../docs/TESTING_INTERNALS.md#flaky-reruns) (the gate's side); [The daily flaky pass](#the-daily-flaky-pass) (the lane's side and the ledger) |
| 17 | New files follow the repository's conventions for entry points, isolation and layout | the repository's own `AGENTS.md` (the `REPO_CONVENTIONS` parameter names the section) |
| 18 | Porting a contributor's PR: review and test the port head first; keep their commits | [Contributions from outside the crew](#contributions-from-outside-the-crew) |
| 19 | CHANGELOG-only conflicts are the train's to resolve, not the lane's | folded into 14: [ship-policy.md](../../docs/ship-policy.md#ship-before-ready) ("the stamp keeps the pin's receipt") |
| 20 | A READY from before a contract change needs a fresh receipt | [The READY contract](#the-ready-contract) |
| 21 | Approvals, plan asks and merge asks in the three shapes | [Approvals](#approvals-three-shapes); [capy/skill/gstack/captain.md](../../capy/skill/gstack/captain.md) |
| 22 | /ship before every READY; the block carries `SHIP_RECEIPT`; stamp commits hold only tool output | [ship-policy.md](../../docs/ship-policy.md#ship-before-ready); [the lane's /ship sequence](#the-lanes-ship-sequence-before-ready) |
| 23 | Review loops converge: scope frozen after pass 1, later passes on the delta, a pass cap, a blocking floor | the `/ship` and `/review` skills' own convergence policy ([skills.md](../../docs/skills.md#ship)); a crew names it in one line and sets only the scope knob (`REVIEW_SCOPE`) |
| 24 | Plan before build: a sized `/autoplan` is a building lane's first act; the plan ask goes up, the build starts after the owner's word is in the run and it reports `APPROVED` | [Plan before build](#plan-before-build); the [brief template](../../captain/templates/brief.md) |
## Lane discipline
A lane follows the repository's own validation discipline (gstack's is the
[Validation discipline](../../AGENTS.md#validation-discipline) section of its AGENTS.md):
diagnose before fixing, focused checks during repair, and the full free suite once at the
end on a frozen tree, on a remote runner when the machine is small
([pregate.md](../../docs/pregate.md#remote-gate)). Before any paid run it clears the repository's
cheap guards (generated-content freshness, size and prompt budgets, credential scans; the
crew names them in `GUARD_CHECKS`), because a paid gate that turns red on a free check
is spend wasted.
A lane writes the public record in plain words: its branch name, PR title, commit subjects
and changelog fragment say what changed for someone using the project, with an issue code
only at the end (`capy/github-rate-limit-gst-1713`, "Stop gstack from hitting GitHub's rate
limit while it waits on PRs (GST-1713)"), and its PR body keeps codes, receipts and lane
state in the closing Agent notes section ([ship-policy.md](../../docs/ship-policy.md#plain-titles)).
A lane edits only the files it was given. The controller keeps the ownership list in the
digest's **In progress**; a lane that needs a file another live lane owns tells the
controller, who routes it, and never edits it itself. Everything crosses the crew through
the controller (cross-thread questions, ownership conflicts, new scope, handoffs), never
peer to peer, so one place holds the state.
A lane never stops to ask whether to continue. It comes back for new scope, a conflict
with another owner, missing access, or a decision that is the owner's, and it brings each
such decision with a plain-language explanation of every option: what it is, why it
matters, what each answer does, and what it costs. A gate code or a tool's result line
alone is not an ask.
## Lane context
Every step a lane takes re-reads its whole conversation. A step within about five minutes
of the previous one reads it from the prompt cache; after five idle minutes the cache is
gone and the next step costs about eight times as much. Fixed instruction text and idle
restarts, not checks, are now most of a lane's own spend, so a lane keeps its context
cheap:
- **Warm waits.** When a lane expects a wait under about 30 minutes (a CI run, a test
tier, a background build), it checks in every 240 seconds: on Capy, `wait` on the
operation with `duration_seconds: 240`, and on a timer wake that brings nothing new it
re-issues the same wait silently, with no message. Once a warm wait has run about 30
minutes, or past the end the lane expected, the lane stops re-issuing the timer and
sleeps on the operation or pull request watch alone. `gstack-runaway scan` leaves these
check-ins out of its empty-wake rate: a turn whose only call re-waits on the same
operation, or a 240-second timer on it or on nothing, within 30 minutes of the first wait.
A status read in between counts and does not restart the 30 minutes. A wait expected to run
longer sleeps on its operations alone. For short lane waits this overrides "waits go on
operations, never on polling wakes" ([unattended.md](../../docs/unattended.md#report-length)). The
controller never warm-waits, and a lane still sleeps while /ship runs
([the lane's /ship sequence](#the-lanes-ship-sequence-before-ready)).
- **Batched reads.** Independent reads (files, greps, pull request and thread state) go in
one parallel program, not one call each. Each call returns 4k characters or less; long
output (logs, test output, diffs) goes to a file under `~/.capy/work`, and the lane reads
the part it needs with `grep` or a paged read.
- **Start card.** Once the install check passes, a lane reads
[lane-card.md](../../capy/skill/gstack/lane-card.md) instead of the rest of the gstack
skill's SKILL.md. It still reads the SKILL.md sections on /ship before it ships.
- **Tail check.** When a lane's spend passes twice the crew's median lane spend per merged
PR (about $50 at today's prices), it writes `handoffs/<code>/next.md` in the controller's
drive (what is done, what is left and what finishing costs) and tells the controller in
one line. The controller decides whether the lane continues, rotates to a fresh lane
that starts from that file, or parks. This sits below the crew's hard spend stop (a
per-PR figure in the controller's rules), where the lane stops and asks whether the PR
is still worth it. Spend is the
thread's credit meter, never a token count.
## New threads join the crew
A crew thread never creates a thread itself. It asks its Captain, and the Captain creates
the thread, which puts it in the crew from the start: the Captain briefs it, reads its
reports and archives it when its work is done. When a crew thread has already made a
thread, it reports the new thread's code to its Captain in that same turn, and the Captain
adds it to the crew (`crew.add` on Capy).
A thread in another project goes Captain to Captain, because `crew.add` refuses a thread
from another project and a Captain creates threads only in its own project. The crew
thread asks its Captain, who asks that project's Captain to create and own the thread, and
the results come back the same way. For the receiving Captain, that ask is work: it
creates the thread in its own crew, briefs it and returns the result. Only when the other
project has no Captain does the crew thread create the thread itself, and then it records
the new thread's code on the work's issue so the thread can still be found. A thread the
crew thread already created in another project goes the same way: it reports the code to
its Captain, who asks that project's Captain to adopt the thread, and the code goes on
the work's issue.
Why: a crew thread once created a thread in another project directly. That thread ran
outside every crew, so no Captain briefed it, tracked it or could archive it.
## Keep issues tidy
Issues show what the crew is really doing, so keep them true:
- Before archiving a thread, close or move to Backlog every issue assigned to it.
- On each wake, leave every issue you touched accurate: its status, assignee and links.
- An unassigned Todo that nobody will start goes to Backlog.
- A daily sweep closes, re-assigns or backlogs any open issue outside Backlog that nobody
has touched for 2 days and whose assignee is archived, or idle with no open PR.
- The sweep skips an issue assigned to a person (an ask waiting on the owner, say) and a
Waiting issue that names what it waits on, by a blocked-by link or a dated comment, while that is still open.
The target: the Overview's In progress count is about the number of working threads plus
the PRs waiting to merge.
## Plan before build
A lane that builds something plans first. Its first act after the session-start install
check, before any product code, is a `/autoplan` run on its brief, sized to the work. The
full run's phases run in order, CEO, then design, then DX, then eng, with design and DX only when the
scope has them. A bug fix gets no plan review and a feature gets the lean run (both below). On Capy the
unattended loop is `autoplan/DRIVER.md` in the gstack checkout. New functionality a lane
takes on part-way, scope its brief did not name, gets its own plan and its own ask before
the lane builds it. In the rules file this is rule 24 of the
[template](../../captain/templates/CREW.md); a crew whose own rules already took 24 before
this rule shipped cites this section by name.
**What the lane sends.** The plan and its gate in plain language a person outside the
project can follow: what changes, why now, what the change does, what it costs, the lane's
pick, and the choices as lettered ask lines ([the ask format](#the-ask-format)). `gstack-owner-brief render --captain --out <run dir>`
prints that from the plan's `## ELI5`, `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections (also saved as `eli5` in
`gate.json`, so `gstack-gate render` opens with them), with the cost lines
(`GATE_SPEND:`, `GATE_REC_COST:`) and a last line for the controller; a plan missing a
section exits `CAPTAIN_BRIEF_INCOMPLETE`, so the lane adds the section and renders again
([capy/skill/gstack/captain.md](../../capy/skill/gstack/captain.md) § 3 lists them). The lane
sends it whole, as a needs-you report, and builds nothing yet.
**What the controller relays.** One ask to the owner in shape B below, its choices in
[the ask format](#the-ask-format): "do it" (every recommendation, plan changes included,
at full scope, follow-ons included) is the default, and a named option such as "A2" takes
that option instead. The plan as first written is never the default.
**The build gate.** The lane builds only after the owner's word has come back through the
controller and been answered into the run
(`gstack-autoplan answer --out <run dir> --gate-rev <n> --reply <tokens> --by agent:<thread> --owner-reply "<their words>"`),
every accepted plan change has been incorporated (`revise`), every reopened phase has
finished, and the run reports `APPROVED` (`status=complete` after `export`). An answered
gate that still prints `PLAN_AMEND_PENDING` or `REOPENED:` is not a completed plan, and an
imported approval row alone is not completion. A later gate revision that asks nothing new
is answered with the owner's recorded words and says `CARRIED:`; that is the owner's word,
not a new ask ([carry-forward](../../docs/unattended.md#carry-forward)).
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` (with
`--stack <b1,b2,...>` for a stacked PR set) writes the approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
what it covers; a gate answered with `--words-by agent:<id>` is listed under that agent as its own decision, not the owner's words); `gstack-approvals render` puts it in the report and the PR body. Before
training the PR the controller compares that row's words and `gate_rev` with the ask it
relayed and the reply it carried back; a row that does not match is not READY (shape A).
The brief's done line names the row. The READY block carries the resolved plan as its
`PLAN:` line, which the [READY template](../../captain/templates/READY.md) refuses when it is
missing, says `warn`, or names another plan than the brief's `Plan:` line.
**Exemptions.** Read-only lanes (triage, research, measurement, a review voice), the daily
flaky pass's repairs, slot stamps, CHANGELOG-only conflict resolves and a size or budget
cap re-pinned under the controller's written grant need no plan. Two kinds of fix are exempt
too: an S1 emergency (a broken install or upgrade, a red main, a security bug), and a fix
from a problem list that was already diagnosed and audited, where that list is the plan.
Any other bug fix is exempt too (R65; the brief's reason is `bug fix (R65)`): `gstack-autoplan next --kind fix` prints
`AUTOPLAN: not needed (bug fix; R65)` and writes nothing, and the PR carries the bug's
proof, a repro on current main or a failing test written first. A feature gets the lean
run, `--kind feature`: one reviewer reading the plan and the draft code together, one gate
revision and no carry check (docs/unattended.md). `--full` brings back the full run.
A crew may add exemptions
in its rules file with the owner's words and date; it does not remove these. The brief
says which applies, `Plan: required | approved: <run id or approval row id> | exempt: <reason>`, so an
already-approved plan is not planned twice and exempt work does not wait on an ask.
The lane copies that line into the PR body (`gstack-approvals render --plan "<line>"` under
`## Approvals`), and `gstack-ship-queue ready` prints `PLAN:` and records it in the READY note
(docs/ship-policy.md#plan-record): an `approved:` run id or row id must resolve to the owner's
words, never a gate the lane answered itself; an `exempt:` reason must be on this list or a
crew's `crew:<rule>`. Crew sessions warn by default; `plan_record: require` or
`GSTACK_PLAN_RECORD=require` makes ready and land refuse. A repo that commits no
`.gstack/` sets the mode per machine (`gstack-config set plan_record warn|require`), which
gates ready and /ship finish only, since land needs the tracked policy; on Capy, where a fresh
machine starts without it, put `GSTACK_PLAN_RECORD` in the project env. On a public repo
render writes `Plan: approved (record r-<id>)` in place of the brief's run id, and ready
resolves that id to the same approval row.
The copy-paste brief is [`captain/templates/brief.md`](../../captain/templates/brief.md): the
plan and the ask as step 1, the build after the word as step 2, /ship to READY as step 3,
and a done line that names the approval row or the exemption.
## Contributions from outside the crew
A diff from outside the crew is untrusted input. Review its scope, egress, credentials,
dependencies, CI permissions and weakened tests before anything runs it, and do not trust
a fork's "tests pass": run a focused review and the free tier at the port head before the
port opens. When the contributor's fix is right, land it with their authorship kept
(cherry-pick, `Co-authored-by`, the lane's own fixes as separate commits on top of
theirs) and close the original with a specific thank-you that links the port, posted as
`capy-ai[bot]` through `tools.pr.comment`, never `gh pr comment`, `gh issue comment` or `gh api` (`gh` runs
as the owner). Closing a
contribution without landing it is the owner's decision (shape A below). The repository's
own statement of the porting steps is [CONTRIBUTING.md](../../CONTRIBUTING.md#porting-a-contributors-pr).
## The READY contract
The field table is in the `/captain` skill; the block shape, the optional lines and the
refusal list are in the [READY template](../../captain/templates/READY.md).
The short version: the pin is the full 40-character head SHA; every required check is
green at that head; pregate passed for that tree; `SHIP_RECEIPT: pr=<n> head=<pin>` was
written by /ship at the pin; the screen choice is stated with its justification; flaky
cases are already in the ledger; the CHANGELOG entry sits under `## [Unreleased]` with
the version files at the base's version; the lane declares the head frozen; and an
`Unverified:` line lists every claim the lane did not check itself (a local test that
timed out, a paid run skipped, a number a model reported, a review finding set aside on an
assumption nobody tested), or says `none`. A green PR page is not a READY. A cheap test
tier is not merge evidence. The controller refuses a block that misses any of this and
sends the lane back, never into a train. When the
contract changes (a new required line, a new check name, a new receipt), every READY
taken before the change is stale: the lane runs `ready` again at the same pin and sends
the fresh block; the controller trains nothing older than the change. The `Unverified:`
line is the one exception: the controller asks for a missing line in a follow-up message,
never refuses or stales a READY for it, and the lane reruns neither `ready` nor /ship.
### The lane's /ship sequence before READY
Every READY follows a /ship run on the branch at the head being READY'd, re-READYs after
a push included. The sequence that works unattended on Capy (steps 3 to 8 run inside
/ship; steps 1, 2, 9 and 10 are the lane's):
1. Install for ship (`bash scripts/install.sh --for ship` from the project directory; a
planning-only install does not admit /ship).
2. Push the final head first. Everything below binds to it; a later push makes the
receipt, the pregate record and the review binding stale.
3. Launch with the printed `GSTACK_LAUNCH` assignments (unattended session kind, state
root, Capy flag) and follow ship/SKILL.md with the Capy adaptations: take every
recommended option, leave consents and gates pending, never push or merge from the run.
4. Pregate on the still tree: preflight, then the test tier on a remote runner when the
machine is small; clear the macOS and Windows lanes with this head's own green CI run
URLs. Never run a bare pregate afterwards (it resets the regen check).
5. Reviews: mint tokens, run both passes read-only, finish them so the review log binds
to the current tree.
6. Docs audit: snapshot the documentation candidates read-only and record the vetted
section.
7. Body and receipt: compose the PR body (What changed, Why, Blast radius, Approvals first on Capy),
write the ship receipt into it, post body and title.
8. Terminal line: `gstack-ship-receipt result --pr <n>` prints
`GSTACK_RESULT: skill=ship status=complete`. `SHIP_PAUSED: base-red …` with
`status=incomplete` means Step 5 proved its failures are the base's own
([base_red](../../docs/ship-policy.md#base-red)): the lane is waiting, not failed. It
registers `gstack-base-red wait --pause <file>` as a background operation,
tells the controller which cases hold it, and on `BASE_GREEN` runs
`/ship --resume <record>`; no READY goes out on the conditional receipt,
except on a train with `gates` ([Merge trains for other repos](#merge-trains-for-other-repos)).
9. READY: `gstack-ship-queue ready --pr <n> --pregate <pregate.json> --screen|--no-screen`;
send the printed block. The lane runs this on /ship's completion wake, below.
10. After the slot call, the stamp commit keeps the pin's receipt; re-READY with the same
receipt, no second /ship.
When /ship runs in a subagent, the lane sleeps while it runs. After dispatching /ship, the
lane ends its turn and wakes only on /ship's completion: one wake to relay READY, because the
controller hears READY from the lane, never from its subagent. So the lane holds no pull request watch of its own
while /ship runs: /ship opens the pull request, and on a re-READY the lane cancels the watch
it already holds before it dispatches. Every READY duty has one owner, so `train build`
never refuses the pin with `QUEUE_NOT_READY` for a step nobody ran:
| READY duty | Owner |
|---|---|
| The ship receipt (`SHIP_RECEIPT:`) and the pregate record behind the `PREGATE:` line (steps 4, 7 and 8) | /ship |
| `gstack-ship-queue ready --pr <n> --pregate <pregate.json> --screen\|--no-screen` at the pin, with its notes pushed to origin `refs/notes/gstack-trains` (step 9; /ship runs on the lane's machine, or the lane copies its `pregate.json` over first) | the lane |
| The decision-trail audit and the READY's `Attention` section, when the crew keeps a trail ([unattended.md](../../docs/unattended.md#the-decision-trail)) | the lane |
On the completion wake the lane subscribes to the pull request's check and review notices,
reads its checks and comments once (a notice that landed while /ship ran went to /ship, or to nobody on a re-READY),
stops the /ship subagent, runs `ready`, audits its trail, writes `Attention` and sends the
READY. A completion that ends `SHIP_PAUSED: base-red` follows step 8 instead: the lane
registers the base-red wait, tells the controller, and sends no READY. From then on the lane handles the pull request's events as before: red checks, human
review, the slot stamp and re-READY of step 10, and any receipt carry. It does that work
itself or sends it to the subagent as an explicit instruction. The tradeoff: a red check
that lands while /ship runs waits for /ship to finish instead of being fixed in parallel.
In exchange, the lane stops re-reading its whole context on every notice during /ship. On
Capy the mechanics are in [the gstack skill](../../capy/skill/gstack/SKILL.md#plan-approve-ship).
When the base moves under a reviewed branch, merge it in; never rebase. A merge that
leaves the product diff unchanged carries the receipt with `gstack-ship-receipt carry`
(pregate at HEAD, no review rerun), and a merge whose resolution changed product code
needs a fresh /ship. A rebase carries only onto a tree the receipt already covers with
the same product diff; any other rebase is a full /ship
([ship-policy.md](../../docs/ship-policy.md#receipt-carry)). A controller never asks a lane to
rebase a reviewed branch.
## Direct land (`merge: direct`)
When the base policy sets `"merge": "direct"`, there are no trains, slot calls or stamp
commits. Each PR merges at its own green head, and versions are set at release
([ship-policy.md](../../docs/ship-policy.md#land-direct)):
1. Lane: /ship, then `gstack-ship-queue ready --pr <n> --no-screen` at the pinned head,
then READY to the Captain with that pin. The PR carries a `changelog.d` fragment and
leaves VERSION, the `package.json` version and CHANGELOG.md alone.
2. Captain: `gstack-ship-queue land --pr <n> --pin <sha>` (add `--allow-repo-commands` when
the base policy lists `land.merged_checks`, so a stale base can run them). It merges with
`--match-head-commit`, checks main's tree and writes a land note.
3. Release: before each regular stable cut (Monday and Thursday, 14:00 UTC), and in the same
turn it lands a PR whose fragment carries `important:` (the READY says `rides the daily
cut`; [releases.md](../../docs/releases.md#the-daily-check)), the Captain runs
`gstack-release-stamp open`, waits for the release PR's checks, then lands it with
`gstack-ship-queue land --pr <n> --pin <head>` (no /ship receipt or READY note needed for a
`release/<version>` head). The cut then tags the new version
([ship-policy.md](../../docs/ship-policy.md#release-stamp)).
What to do with each refusal (nothing was merged unless the code says so):
| Code | Who acts | Action |
|---|---|---|
| `QUEUE_LAND_STAMPED` | lane | run the printed `git checkout $(git merge-base origin/main HEAD) -- …`, keep the fragment, commit, /ship and `ready` again |
| `QUEUE_LAND_PIN_MOVED` | lane | /ship and `ready` at the new head, new READY |
| `QUEUE_RECEIPT_MISSING` / `QUEUE_RECEIPT_STALE` | lane | /ship at the pin |
| `QUEUE_NOT_READY` | lane, or Captain | no READY note: `ready --pr <n> --no-screen` at the pin; `merge: train`: use the train; closed or retargeted PR: as named |
| `QUEUE_LAND_CHECKS` | Captain, then lane | pending: `gstack-pr-wait <n> --until green` and rerun; red: back to the lane (rule 16 rerun for a flaky case); missing: dispatch the workflow on the branch |
| `QUEUE_LAND_MERGED_CHECKS` | lane | a `land.merged_checks` command failed on the pin merged onto main: the lane merges main (rule 19), fixes it, /ship, `ready`, new READY |
| `QUEUE_LAND_NOT_CLEAN` | lane, or Captain | `CONFLICTING`, or the pin conflicts with main's tip on a stale base: the lane merges main (rule 19), /ship, `ready`; `UNKNOWN`: rerun in a minute |
| `QUEUE_TRAIN_FROZEN` / `QUEUE_LAND_LOCKED` | Captain | finish the repair or the holder first |
| `QUEUE_TRAIN_UNTRUSTED` | Captain | a fork head; rewrite on a crew branch |
| `QUEUE_LAND_UNVERIFIED` | Captain | merged, but main isn't the pin on its parent, or main moved after land's check and `land.merged_checks` failed or is absent on the landed tree: revert the stray change or fix forward, report it |
| `RELEASE_STAMP_INVALID` | Captain | the `release/<version>` head is more than its stamp: close it, `gstack-release-stamp open` again |
| `RELEASE_STAMP_EMPTY` | Captain | no fragment landed since the last release: nothing to release |
## Merge trains
gstack's queue tool drives trains when the repository's `.gstack/ship-policy.json` carries
a `train` block ([ship-policy.md](../../docs/ship-policy.md#run-a-merge-train)). The controller's
loop, per train:
1. `train build --prs <pins> --dry-run`, then without `--dry-run`: the record, the train
branch, free tests, one full gate.
2. `train verdict`: GREEN (with `FLAKY N` when an in-run rerun turned a case green), RED
(cases that failed twice), or INCOMPLETE (never green).
3. Per slot, in order: the [slot call](../../captain/templates/slot-call.md) to the lane; the
lane stamps and pushes only the base merge and the stamp files; `verify --train`;
`gh pr merge --match-head-commit`; `verify --train` again.
4. `train prune` when every slot has merged.
Under `changelog_fragments` ([ship-policy.md](../../docs/ship-policy.md#train-land)) lanes READY with a
fragment (`changelog.d/<slug>.md`) instead of an `[Unreleased]` section, there is no slot
call and no lane stamp, and step 3 is one command per slot: `train land --train <nonce> --pr
<n>` commits the stamped prefix S_i on [pin, main], pushes it, waits for checks, merges and
confirms main is S_i exactly. Outside a train, `gstack-ship-queue land --pr <n>` is the same
step for one PR ([ship-policy.md](../../docs/ship-policy.md#land)).
A red train: `train bisect` on the red cases over every prefix. An attributed PR leaves
the train and goes back to its lane; red on the base branch itself is fixed forward
through an urgent one-PR train (or `train build --repair` while the queue is frozen, which
refuses `--urgent`); all prefixes green earns exactly one unchanged rebuild;
anything else goes to a repair lane. An infra-only census earns one re-dispatch. A
backstop gate runs on every push to the base branch and freezes the queue when it finds
a tree no green train covered; one repair PR at its exact head clears the freeze. A
policy with a `release_gate` block swaps that per-push backstop for one full gate on main
every 8 hours, whose red verdict holds the stable release instead of freezing the queue
([ship-policy.md#release-gate](../../docs/ship-policy.md#release-gate)).
**Wave PRs when the queue is long.** Every slot pays its own
stamp, its own round of required checks and its own version, one after another. When more
than 6 PRs are READY, `gstack-ship-queue wave plan` prints `WAVE_ADVISED` and suggested
area groups of 3 to 6 (`train build` prints the same line when handed more than 6 pins). The
controller asks the owner of each group's largest PR to integrate: `wave build` makes the
wave branch with every constituent merged unchanged, the integrator runs /ship once and
READYs the wave PR, which then takes one slot and one version. Constituent heads are frozen
once in; fixes go through the integrator, and a constituent that must change is dropped
(`wave build --wave <nonce> --drop <pr>`) and goes back to its owner. After the wave lands,
`wave settle` closes each constituent with a link. While the queue stays long, start new
work as area waves. The full flow is in [ship-policy.md](../../docs/ship-policy.md#wave-prs).
**Slot 2 and later.** The base branch already carries the earlier slots' stamps, so a
stamp on the bare pin leaves the PR conflicting on the CHANGELOG. The lane merges the base
into its pin first (expect a CHANGELOG-only conflict; resolve it with `mergeChangelogs`
from `lib/changelog-check.ts`, the `bun -e` one-liner in
[ship-policy.md](../../docs/ship-policy.md#run-a-merge-train), never by hand, and grep for conflict
markers before stamping), then restamps (the restamp stages the stamp files it wrote,
so a plain `git commit` after a `--no-commit` merge keeps them), then commits only the
merge and the stamp files. In practice slot 1 needs the same merge whenever the base moved after the pin was
taken. A stamp commit with one extra byte freezes the queue for every
lane behind it.
The slot stamp is the base merge, `mergeChangelogs`, and the stamp files the slot call
names (`STAMP_FILES` in CREW.md; gstack's are `CHANGELOG.md`, `VERSION`, `package.json` and
`agents-digest/gstack-AGENTS.md`, plus the file carrying a `*_KEY_VERSION` the PR
introduced, which the restamp moves on the PR's own line, when the call names it), never a
fifth: a file the restamp lists that the call did not name is not a stamp, so the lane
leaves it out and says so in its READY. The restamp itself never
moves a line the base added since the pin's fork point, base merge committed or staged. A
pin stacked on a PR that squash-merged earlier conflicts beyond the CHANGELOG where the
parent's squash touched the pin's lines, so the lane takes its pin's bytes for those files
(`verify` checks the tree against T<i>, which already carries the parent), and
`mergeChangelogs` carries the pin's edits to the parent's now-released entry as Gate
integrity corrections in the pin's own entry. The post-merge `verify` judges the CHANGELOG
by the same rule the pre-merge one did, so an accepted head never freezes the queue after
its merge ([ship-policy.md](../../docs/ship-policy.md#run-a-merge-train)).
Options the controller writes into CREW.md rather than inheriting: squash versus merge
commits; lane stamps versus controller stamps; gate-ahead (slot i+1 gates on slot i's
head while i merges); the severe race (several severe lanes told at once that the next
urgent train is theirs, first complete READY takes it; never a race for a slot inside a
built train); whether a READY also carries a gate run id and tree hash.
### Merge trains for other repos
A repository without gstack's eval workflow declares `gates` in its train block
([ship-policy.md](../../docs/ship-policy.md#train-gates)): its own CI workflows, a test
command on a Ubicloud VM, or both. Every prefix is gated, so a red train names
its culprit with no bisect and no rebuild. To turn one on, run `train init` in
a checkout, commit the printed block (and the workflow snippet) on the base
branch through a PR, then run `train doctor` until it reports no findings.
The controller's loop shortens to `train build --prs <pins>`, then
`train watch --train <nonce> --land`, which waits for every prefix run, reads
the verdict and lands each landable slot in order. Lanes READY with
`ready --pr <n> --no-screen`; there is no screen and no slot call. A lane whose
/ship paused on a red base READYs with `--base-red --fixer <pr>`, its fixer with
`--fixes-base-red <cases>`, and the train seats the fixer at slot 1. The `update` commit `train land` pushes to a PR
branch (the gated tree on [pin, main]) is the one exception to CREW rule 14's "never merge
the base into a PR": it is tool-made, recorded before the push, and pushed under the pin's
lease, like a stamped train's stamp commit. A peer crew
adopting this starts from
[captain/templates/merge-train-peer.md](../../captain/templates/merge-train-peer.md).
## Approvals: three shapes
**A. The record.** Every lane keeps an `## Approvals` table in its report and PR body,
rendered by `gstack-approvals render`: date, what was asked, the owner's exact words, who
relayed them, what the answer covers, what is pending. Standing approvals are cited with
their rule; a fresh ask is always needed for a product or behaviour decision, spend over
a cap, anything published outside the repository, a threshold, budget, trial-count or
case-kind change, closing a contribution without landing it, and anything destructive.
Standing spend is the commonest standing approval: `SPEND` in CREW.md names what the
owner pre-approved (train gates, screens, bisects, the backstop, focused paid cases) with
the owner's date and words and the cap per rerun; anything over the cap is a fresh ask.
**B. The plan ask** ([template](../../captain/templates/plan-ask.md)): before → after, why it
matters, what caused this work, the big decisions, and what we need from you in the ask
format below, closed by "do it", which takes every recommendation at full scope.
`gstack-owner-brief render --captain --captain-name "<Captain name>"` prints it from an
autoplan run.
**C. The merge ask** ([template](../../captain/templates/merge-ask.md)): captioned
before/after images, why, blast radius, the Approvals table, and the evidence. /ship writes the PR
body in this shape on Capy; the controller's merge report shows the key image inline.
## The ask format
Every ask to the owner, from a plan gate to a merge question to the digest, is written so a
five-year-old could follow it: the Captain's name, then one lettered line per decision, with
letters unique across the list.
```text
<Captain name>
- A: <code> hit problem <p>, which happens when <h>. We could fix it in these ways: 1) <x>; 2) <y>; 3) <z>. Recommendation: <x> because <why>. A1 to take recommendation.
- B: …
```
The recommendation is always option 1. "do it" takes every recommendation in the last
list; a named option ("A2") takes that option instead. `<code>` is the issue, PR or thread
the decision is about. An ask that cannot be put this way is not ready to send.
**When the owner has to do something** (create a token, approve a deployment, accept an
invite, change a setting), the line carries the exact link to the page where it happens, a
full URL and never a menu path like "Settings → Tokens", then numbered steps in the
simplest words: what to click, what to type, what to name it, and the one word to reply
when it is done. A page inside the Capy app is the one exception, because it has no stable
link: name the exact path and field instead ("Settings → Integrations → Slack, the Channel
field").
```text
<Captain name>
- A: <code> needs you to <action>, because <why>. Open <exact URL>, then:
1. Click <button>.
2. Type <value> in <field> and name it <name>.
3. Reply **done**.
```
## The digest
`DIGEST.md` is rewritten every controller turn: **Needs you** (decisions only the owner
can make, in [the ask format](#the-ask-format); empty is the goal), **New** (what landed or
changed, newest first), **In progress** (every lane, what it owns, its state, the age of
its last report). Ages turn stalls into the controller's next action. When the controller
archives a thread, it drops that thread from **In progress** in the same turn.
## The daily flaky pass
A scheduled automation starts one lane a day with the
[flaky-pass prompt](../../captain/templates/automation-flaky-pass.md). It reads the gate
runs' flake-records artifacts and the free suites' retried cases, writes one ledger line
per event, groups by case, and fixes the worst repeat first with a PR that shows the
failure red before green. It never reruns a gate and never touches a threshold, trial
count or case kind. Where a repository runs `gstack-flaky`, the lane also reads
`gstack-flaky quarantine report --ledger <downloaded ledgers>` and fixes expiring quarantine
entries before anything else ([flaky.md](../../docs/flaky.md#the-daily-flaky-pass)). On Capy, create it with the automation tool, scheduled in the owner's
timezone, running as a thread in the crew so its report lands in `reports/`.
The lane's side of the same rule: in CI the gate reruns a case that failed on its own
verdict once, in the same run ([TESTING_INTERNALS.md](../../docs/TESTING_INTERNALS.md#flaky-reruns));
a pass is green with `FLAKY N`, and a case that failed twice is a real red that nobody
reruns by hand. In a lane's own focused paid runs it reruns only the failed case, once, at
the same tree: a pass goes into the ledger (case, PR, head, both runs, a one-line symptom)
and is named in the READY; a second failure is real. A harness, infra or incomplete red is
never a flake. One named failure from `KNOWN_INFRA_FLAKES` may be rerun once per PR and
is stated in the READY. A red train is the controller's (bisect, rebuild once), and every
rerun reports its spend.
## The owner's device
Some actions exist only in the owner's logged-in browser or on the owner's machine:
approving a deployment review, switching the GitHub account a run used, a local-only
build. A lane that needs one asks the controller; the controller does it through the
owner's device (on Capy, a paired device and the owner's browser) and records what it
did. Before approving a deployment, check the run's pending deployments list; approve
only the run the lane named.
## The lessons loop
Every report ends with one Lesson. The controller reads lessons before chat, and each
one becomes a rule in CREW.md, a line in AGENTS.md, or a tool fix filed as an issue;
nothing stays a lesson for long. Rules carry the thread that produced them, so a new lane
can read the incident behind the rule.
A revert or an S1 fix of a merged crew PR gets a postmortem as well as a lesson. The
controller starts it when the fix's brief goes out, from the
[postmortem template](../../captain/templates/postmortem.md), saves it as
`reports/postmortem-<n>.md` and links it from the fixing PR. Its root cause section asks
why the lane and every reviewer believed the change was right, and it is answered from the
review outputs, receipts, decision trail and approvals, not the diff. Each rule change it
names ships as a gstack PR, because gstack holds the how; a rule kept only in a crew file
is a gap.
When the owner states a rule meant for every Captain, forward it in the same turn to the
thread that keeps the account's shared rules, so every crew gets it.
## Setting it up on Capy
1. Create the Captain and give it the drive layout above; paste the filled `CREW.md`
and an empty `DIGEST.md`.
2. Add the `train` block to the repository's `.gstack/ship-policy.json` and name the
same check names in `CREW.md`.
3. Install the gstack Capy skill in the owner's user scope so every thread runs the
session-start check ([capy-install.md](../../docs/capy-install.md)).
4. Create the daily flaky-pass automation.
5. Start lanes with the [brief](../../captain/templates/brief.md): the files they own, the
rules file, whether a plan is required, and the READY contract. Read their reports
before their chat.
+449
View File
@@ -0,0 +1,449 @@
# The recommended harness: gstack + Capy, from zero to the first merged train
This is the operating model a team adopts when it runs gstack under a Capy
Captain: one person, one Captain thread that directs a crew, crew threads that
each own a branch and stop at an unstamped READY, one controller that merges
through trains, a daily flaky pass, a twice-weekly release, and a scorecard that says
whether the factory is getting better. It was written from two crews that ran
it for a week on their own repositories (about 100 merges a week each) and from
the recurring problems four Captains reported while doing so. Every path and
check name below is configurable; nothing here depends on gstack's own
repository.
Two layers, kept apart on purpose:
- **Harness-generic** (works in Claude Code, Codex and any host that reads the
SKILL.md files): `/ship`, `gstack-ship-queue`, `gstack-restamp`,
`gstack-pregate`, the ship receipt, the train protocol, the flaky-rerun
rule, `release-cut`, `gstack-factory-scorecard`, `/retro`.
- **Capy-specific**: the user-scope skill folder and its install check, the
Captain and its Drive, crew threads, automations, `tools.pr.create`, the
approvals relay. Each one is marked **Capy** below; the section at the end
lists what is only Capy.
```mermaid
flowchart LR
P[Person] -->|plain-language asks and one-word answers| C[Captain thread]
C -->|briefs, slot calls| L1[Crew thread: branch A]
C --> L2[Crew thread: branch B]
C --> L3[Crew thread: branch C]
L1 -->|/ship, READY block with SHIP_RECEIPT| C
L2 --> C
L3 --> C
C -->|train build: one gate for up to 5 PRs| G[(Gate workflow)]
G -->|GREEN or RED with flake records| C
C -->|slot: stamp, verify, merge, verify| M[(main)]
M -->|Mon + Thu, 72 h floor| R[Stable release: tag, notes, stable]
G -.->|flake-records artifacts| F[Daily flaky pass]
M -.->|free-suite retries| F
M -.->|/retro Step 7.5| S[Factory scorecard]
```
## Day 0: install
### 1. The user-scope gstack skill (Capy)
Copy `capy/skill/gstack/` from a gstack checkout into the person's user-scope
Drive as `skills/gstack/` (SKILL.md, `captain.md`, `scripts/install.sh`).
Add to the user-scope `AGENTS.md`:
> At the start of each session on a Capy cloud machine, load the user-scoped
> `gstack` skill and perform its installation check before starting work. If
> the check fails, follow the skill's session-start steps. Treat a session with
> no `gstack: ok` line from this machine as having no gstack: run no gstack
> workflow, claim none works, and report any blocker. Repeat this check on each
> separate task machine.
What that buys you: every thread runs `bash scripts/install.sh --check
[--for <skill>]` first and reports exactly one trailer, `gstack: ok <revision>
for=<skill>` or `gstack: fail <ids>`. The install is about 30 seconds cold
and prints the `GSTACK_LAUNCH:` assignments every later command needs
([docs/capy-install.md](../../docs/capy-install.md)). A thread that runs `/ship` installs
with `--for ship`; a planning-only verdict (`SKIP pins (planning-only)`) does
not admit `/ship`, `/review` or `/qa`.
Two things the first week taught every crew:
- **Most threads never install.** The rule installs right before the first
workflow, so a thread that does not plan one never gets `/review` or
`/qa` either. If most threads in a project run a workflow, bake
gstack into the project's Capy Setup snapshot instead
([capy-install.md#bake-into-a-capy-snapshot](../../docs/capy-install.md#bake-into-a-capy-snapshot)):
each session then pays only the check.
- **One revision per thread.** Subagents on fresh machines install
separately and can land one patch apart. Put the minimum revision your crew
needs in the crew rules and have every first report quote the trailer, so a
lane that is behind says so.
### 2. The repository
The harness reads four files from the repository. A team can adopt them one at
a time; each is independent.
| File | Why | Without it |
|---|---|---|
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](../../docs/pregate.md)) | `/ship` warns on every touched file with no test lane (`PREGATE_NO_POLICY`) and prints a starter policy; the selected tests still run. A policy that sets only `timeout_s` or other keys without `dependencies`, `empty_lanes`, `selection` or `local` behaves the same. Write it once, from one owner, before the crew starts. |
| `.gstack/ship-policy.json` | `queue_mode`, the `train` block (gate and free workflow names, the full-gate check, the train size and fill wait), the stamp files ([docs/ship-policy.md](../../docs/ship-policy.md)) | Per-PR queue; `train …` refuses with `QUEUE_TRAIN_OFF`. |
| `CHANGELOG.md` + `VERSION` | A stamp per merge and a twice-weekly release with notes ([docs/releases.md](../../docs/releases.md)) | Skip the version and changelog steps; the train still works on the tree receipt. |
| `AGENTS.md` validation discipline | Diagnose before fixing, focused checks during repair, one full suite at the end on a frozen tree | Threads retry blindly and report passes from older revisions. |
A `.github/workflows` gate that the train can dispatch is the one hard
requirement for trains: a workflow that accepts `workflow_dispatch` with the
train nonce and publishes a check (`full_gate_check`) and a record artifact on
the train head. gstack's own `evals.yml` is the reference; a repository with a
plain test workflow sets `gate_workflow` to it and gets a train without paid
cases.
### 3. The Captain and its Drive (Capy)
Create a Captain from the Capy app (New Captain) or with
`tools.captain.create`. Its Drive is mounted read-write on every crew thread;
this layout is what the crews converged on:
```
captain-<name>/
AGENTS.md the Captain's standing rules: mandate, severity bar, standing approvals, every decision quoted verbatim with its date
CREW.md the crew rules every thread reads at start (install check, validation discipline, READY shape, stamp-on-call, flaky rule, approvals)
DIGEST.md the state of the crew, "Needs you" first; rewritten, never appended
NOTES.md the Captain's running log
skills/ procedures the crew runs: merge-train/, captain-approvals/, and any repo-specific runbook
reports/<code>.md one per thread: Outcome, PR (with head SHAs), Needs, Cost, Lesson, Approvals
handoffs/ datasets and files the next thread needs (the flaky ledger lives here)
plans/ plans brought for approval
```
The phase-1 templates for these files (`AGENTS.md`, `CREW.md`, `DIGEST.md`, a
report, a READY block, an approvals table, a slot call) ship under
`captain/templates/` with the `/captain` skill, and `crew/docs/captain.md`
explains how the Captain runs a day. Drop the templates in, replace the
placeholders (repository, crew login list, gate workflow name, minimum
revision, who may approve what), and the crew has its rules before its first
thread starts. The approvals shape itself (plan asks with before → after, why,
cause, big decisions, and the choices as lettered ask lines closed by "do it"; a `## Approvals` table in every
report and PR body) is the harness-generic part and is documented in
[capy/skill/gstack/captain.md](../../capy/skill/gstack/captain.md).
## The first train
A lane (one crew thread, one branch) does this, in order. Every command is
harness-generic; only the thread and the PR tool are Capy.
1. **Branch from the default branch, build, test with focused checks.** Write
the CHANGELOG entry under `## [Unreleased]` (under `changelog_fragments`, a
fragment in the policy's fragment directory instead); leave VERSION at
main's value. Never pre-pick a version.
2. **Push the final head.** Everything below binds to the pushed head; a later
push invalidates the receipt, the pregate record and the review binding.
3. **Open the PR** with the host's PR tool (Capy: `tools.pr.create`, never
`gh pr create`, so CI and review callbacks reach the thread), base the
default branch.
4. **Run `/ship` unattended** with the `GSTACK_LAUNCH` assignments
exported: it runs the pregate (on a machine under 8 cores the test tier
clears from a verified CI run at the pushed tree when the base declares
`remote_gate_runs` ([CI carry](../../docs/pregate.md#ci-carry)); otherwise it needs
`gstack-pregate --stage tests --remote`, about $0.06 on a Ubicloud VM
or free locally for small selections), both reviews, the gate-integrity
list, the docs audit, and writes the ship receipt into the PR body
(`gstack-ship-receipt write --pr <n>`). With `queue_mode:
stamp-at-merge`, `/ship` does not bump the version. `/ship` never merges
and never merges main into the branch.
5. **READY.** `gstack-ship-queue ready --pr <n> --no-screen` (after running the
skill's own paid cases focused) or `--screen` (one paid screen on the merge
SHA). Send the printed block as is: `READY #<n> <head>
STAMP_VERSION=unstamped`, CI at that head, `PREGATE: pass`, the screen line
and `SHIP_RECEIPT: pr=<n> head=<head>`. A block with `SHIP_RECEIPT: none` is
not READY. From now on the head is frozen: a push drops the PR from any
train (`QUEUE_TRAIN_MOVED`) and the lane must READY again.
6. **Post the READY block as a PR comment too.** The Captain reads it in the
thread; the scorecard (below) measures READY → merge from the first PR
comment that carries it. A crew that only reports in-thread has no
READY-to-merge number.
The controller (the Captain, or a thread it designates) does this; the
Captain's `skills/merge-train/` runbook holds the crew's copy and
[ship-policy.md#run-a-merge-train](../../docs/ship-policy.md#run-a-merge-train) is the
reference:
1. `gstack-ship-queue train build --prs 41@<sha>,42@<sha> --dry-run`, then
without `--dry-run`. Up to 5 READY pins, screened first; with fewer than 5
it waits until the oldest READY is `fill_hours` old, `--urgent` for one PR.
Refusals happen before any spend: a moved head, a missing receipt, a fork
head, a real conflict (the line names the PR and files; rebuild without it
and send it back to its lane to merge main).
2. `train verdict`: GREEN, possibly with `FLAKY N` (a case failed once and
passed its one in-run rerun; that is green, and the flake record goes to the
daily pass), RED (cases that failed twice: bisect on them, drop the attributed
PR, or rebuild once after an all-green bisect, never more), or INCOMPLETE
(one re-dispatch for an INFRA-only census).
3. **Per slot, in order:** call the lane ("#41 slot <version>, base <sha>,
order <…>"). The lane merges main if needed (CHANGELOG by section), runs
`gstack-restamp --next --base main --pr 41 --expect-base <sha> --expect-order
<…> --allow-repo-commands`, commits **only** the stamp files, pushes, waits
for required CI, and reports `READY #41 <stamped sha> STAMP_VERSION=<X>`.
The controller runs `gstack-ship-queue verify --pr 41 --train <nonce>`,
merges with the exact head (`gh pr merge 41 --squash --match-head-commit
<sha>`, or `--merge` where the repository keeps merge commits), and
verifies again. A stamp commit that holds anything beyond the stamp files
fails `verify --train`, freezes the queue and costs a backstop gate.
Under `changelog_fragments` there is no slot call and no lane stamp: the
controller runs `gstack-ship-queue train land --train <nonce> --pr 41` per
slot, which pushes the stamped prefix the gate covered, waits for checks,
merges and confirms main
([ship-policy.md#train-land](../../docs/ship-policy.md#train-land)).
4. `train prune` when every slot has merged.
**Who merges.** One controller, never the lane. Both crews that ran this
learned it the same way: a PR merged off a green list state whose gate never
passed, and a stamp commit with one extra line that froze the queue.
## The daily flaky pass
The rule (set once by the owner and quoted in the crew rules): a case that
fails on its own verdict is rerun once at the same tree, inside the same run.
A pass is green with a flake record; a second failure is a real red; harness,
INFRA and INCOMPLETE reds are never flakes. Nothing is retried twice, and no
threshold, budget or trial count moves to manufacture a pass.
The pass reads two sources, the same two the scorecard counts:
- **Paid:** the gate workflow's `flake-records-a<attempt>` artifact
(`flake-records.jsonl`, one record per flaky case: case, kind, PR, head,
run, both attempts, symptom, rerun cost).
- **Free:** the free workflow's `flake-ledger-*` artifacts (one `flaky-pass`
line per file that failed and passed on the serial retry).
It appends each event to a ledger the crew can read (the crews keep it at
`handoffs/flaky-ledger.md` in the Captain's Drive: date, case, tier, PR and
head, both runs, a one-line symptom), ranks repeats first, and opens one fix
thread per day with the ledger's Open section as its brief. The fix thread
moves entries to Fixed (with the PR) or Won't fix (with the reason), never
deletes them. `bun run test:health` is the free weekly check on the free lane
(a file that flakes in more than 5% of main runs over at least 20 runs fails
it).
**As a Capy automation** (Capy): one automation, one schedule trigger a day in
the owner's timezone, prompt along the lines of "read the newest gate runs'
`flake-records-a*` artifacts and the free runs' `flake-ledger-*` artifacts
since the last pass, append new events to `handoffs/flaky-ledger.md`, rank
repeats first, start one fix thread with the Open section as its brief, and
report the count to the Captain". Pass `thread: "this"` to keep the history in
one place, and run it once by hand before enabling it.
## The stable release
`.github/workflows/weekly-release.yml` ("Stable release") runs
`crew/scripts/release-cut.ts --daily` every day at 14:00 UTC: a regular cut on Monday
and Thursday, an important change's cut after 24 hours and a forced cut when
`stable` is a week old ([the daily check](../../docs/releases.md#the-daily-check)). It never
moves `stable` more than once in 72 hours otherwise, except for a verified
security or regression fix (`--security` or `--regression`, with `--ref`): it takes the newest green commit on main's first-parent history
(its required check succeeded, the backstop check succeeded, every check run
completed and none failed, no active freeze), tags it `v<VERSION>`, publishes a
GitHub Release whose notes are one line per CHANGELOG version since the
previous release, and fast-forwards `stable`. New installs and team-mode installs
follow `stable`; `gstack-config set update_channel main` follows every merge. A
hotfix is `--ref <sha on main>`, held by the same floor. Details, the floor's
refusal codes and the "nothing green in the window" failure mode:
[docs/releases.md](../../docs/releases.md). A repository adopting this
copies the workflow, sets its required check and backstop check names
(`REQUIRED_CHECK`, `BACKSTOP_CHECK`), and keeps the CHANGELOG headline
convention ([docs/CHANGELOG_STYLE.md](../../docs/CHANGELOG_STYLE.md)).
Watch for release starvation: when trains merge faster than the backstop
settles, most main commits are superseded before they are gated and the week
can end with no candidate. The fix is in tooling (accept a commit a green train
record covers), not in holding slots by hand.
## The factory scorecard
`bin/gstack-factory-scorecard` reads one window (default 7 days) from GitHub
through `gh` and prints a Markdown table plus a CSV; `/retro` runs it as
Step 7.5 when `gh` is authenticated. Read-only, no paid work; it downloads the
small verdict and flake artifacts, so a busy repository takes a few minutes
(`--max-artifact-runs` caps that). Every line prints its counting rule, and a
line whose input could not be read says `unavailable (<reason>)` rather than 0.
```bash
gstack-factory-scorecard --repo owner/name --since-days 7 \
[--crew login,login] [--gate-workflow evals.yml] [--free-workflow free-tests.yml] \
[--train-prefix train/] [--ready-marker 'READY #\d+|SHIP_RECEIPT:'] [--pass-marker '"passes": (\d+)|passes=(\d+)'] \
[--pass-cap 2] [--out-dir .context/factory-scorecard] [--now <iso>]
```
`--repo` defaults to the origin remote's owner/name on github.com (or
`GH_HOST`) and is required without one. Each run writes this window's `scorecard.md` and `scorecard.csv`
to `--out-dir` and appends the same rows to `trend.csv` there, so a weekly
retro accumulates its own trend without renaming files.
The window is `[now - since-days, now)`: `--now` (default: the current time)
sets its end, and every line counts only items inside it, so a past week
measured later does not count anything that happened after that week.
| Line | How it is counted | What one of the best software factories would show |
|---|---|---|
| Merged PRs | `merged_at` in the window, base = default branch; crew by `--crew` logins, else author association OWNER/MEMBER/COLLABORATOR | several merges a day, every day; outside contributions land in the same week |
| Lead time, created → merged | PR `created_at` to `merged_at`, median and p90 | median under 24h, p90 under 3 days (DORA "elite" change lead time is under a day) |
| Lead time, READY → merged | first PR comment matching `--ready-marker` at or before `merged_at`; PRs with no such comment are left out and counted | under 2h; every merged PR carries a READY comment |
| Review passes per PR | the largest count `--pass-marker` captures in the PR body (the ship receipt's `"passes": N` once /ship writes it) or a PR comment (`passes=N`, `REVIEW_PASSES: N`), median and p90, plus how many PRs reached `--pass-cap` (2, the [review policy](../../docs/review-policy.md)'s cap of two rounds, a fix-check included: pass 1 reviews the whole diff, pass 2 only the range since, and a pass that finds nothing blocking is the exit) | median 1 pass; few at the cap |
| Gate runs green / red / cancelled | completed runs of the gate workflow (`workflow_dispatch` by default; `--gate-event any`); cost from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment | green on 95%+ of runs, cancelled near 0, cost known for every run |
| Known gate cost per merged PR | known cost / merged PRs; unknown-cost runs make it a floor | known, stable or falling week over week |
| Red-train rate | runs whose head branch starts with `--train-prefix`, cancelled excluded; red = conclusion other than success; red backstops and screens shown beside it | under 5% (each red train is a bisect plus a rebuild, about two gates) |
| Flaky events | `flake-records-a*` records of the newest gate runs (count, known rerun spend) and `flake-ledger-*` entries of free runs on the default branch; a run whose artifacts cannot be read is counted and named, never scored 0, and a main push that reused its pull request's run (no artifacts, `reuse` job green) is left out and named | 0 repeat cases; under 1 flaky case per 10 gates; free retries under 1% of runs |
| Issues opened / closed | GitHub search, created and closed in the window | closed ≥ opened; nothing severe older than a week |
| Releases cut | releases published in the window, drafts excluded | at least one tagged release a week, on schedule |
The third column is a bar, not a boast: it is what DORA's elite band and the
best-run open source projects show, and it is in the table so a reader sees
the gap next to the number.
### Where gstack's own numbers stand
Measured on gstack's repository with this tool for the week of 2026-10-19
through 2026-10-25, UTC (`--now 2026-10-26T00:00:00Z --since-days 7`; the first
full week of trains and `/ship`-before-READY; 83 merged PRs, 54 gate runs,
artifacts read for all 54):
| Line | gstack, that week | Against the bar |
|---|---|---|
| Merged PRs | 83 (crew 80, outside 3) | volume is there; the 3 outside PRs are bot PRs (2 from github-actions, 1 from dependabot), and outside contributions are ported by the crew rather than merged from forks |
| Lead time, created to merged | median 6.5h, p90 20.0h | inside the bar on both |
| Lead time, READY to merged | no number: 0 of 83 PRs had a READY comment | the crew reports READY in-thread; step 6 above (post it on the PR) is what makes this measurable |
| Review passes per PR | no number: 0 of 83 PRs carried a pass count | the receipt's `passes` field lands with /ship's convergence rule; until then crews record the count in the READY block, and the marker reads it from a PR comment |
| Gate runs | 52 green / 2 red / 0 cancelled; known cost $1,361.94 on 52 of 54 runs | green rate 96%; cost unknown only on the 2 red runs (the red train and the red screen) |
| Known gate cost per merged PR | $16.41 (a floor) | trains averaged $32.48 each for up to 5 PRs (21 trains with a known cost), screens $21.18 each (29 with a known cost) |
| Red-train rate | 5% (1 red of 22 trains; 0 red backstop, 1 red screen) | the tool rounds 1 of 22 (4.5%) to 5%, so this is just under the 5% bar; the 1 red screen is counted separately from trains |
| Flaky events | 17 paid flake records across 11 cases, each passing its one rerun (rerun spend $7.20); 17 free-suite retries in 17 of 83 runs | above the bar on all three: 0.31 flake records per gate against 0.1, 4 cases that flaked more than once against 0, and 20% of free runs retried against 1%. One case (`ship-docsync-current`) did it four times in three days |
| Issues opened / closed | 28 / 51 | inside the bar |
| Releases cut | 0 | none inside the week: the first weekly release, v1.91.242.0, was published at 12:01 UTC on 2026-10-26, just after the window closed (see the starvation caveat above) |
Read the gap honestly: lead time and throughput are where they should be, the
gate is green and its cost is mostly known, and flakiness is the line that is
not. The daily pass exists for exactly that line; the scorecard's job is to
show next week whether it moved. The inventory lane's per-PR baseline CSV in
the Captain's Drive holds the longer history behind these numbers; from here on
`trend.csv` in `--out-dir` carries the weeks.
## What is Capy-specific and what is not
| Concern | Harness-generic (any host) | Capy-specific |
|---|---|---|
| Install and health | `./setup`, `gstack-doctor`, `gstack-upgrade` | the user-scope skill folder, `scripts/install.sh --check` and its trailer, the project Setup snapshot |
| Running a workflow | the SKILL.md files, `GSTACK_SESSION_KIND=unattended`, `GSTACK_RESULT:` lines | the `GSTACK_LAUNCH` assignments, `GSTACK_CAPY=1` and the `capy-asks` block, no enforced hooks (safety skills are advisory) |
| Approvals | `gstack-approvals`, `gstack-owner-brief render --captain`, the `## Approvals` table | the Captain relay (`needs_you`), the Captain drive `approvals/` mirror |
| Pull requests | `/ship`, the ship receipt, `gstack-pr-shots` | `tools.pr.create` (callbacks), `tools.pr.uploadAsset` (before/after images) |
| Merging | `gstack-ship-queue` (ready, train build, verdict, verify; train land and land under `changelog_fragments`), `gstack-restamp`, `gh pr merge --match-head-commit` | the controller is a Captain thread; slot calls are thread messages |
| Flaky pass | the rerun rule, `flake-records-a*` and `flake-ledger-*` artifacts, `test:health` | the daily automation and the ledger in the Captain's Drive |
| Release | `release-cut`, `weekly-release.yml`, the `stable` channel | nothing |
| Scorecard | `gstack-factory-scorecard`, `/retro` Step 7.5 | nothing (the CSV can be kept in the Captain's Drive) |
| Memory | `reports/<code>.md`, `DIGEST.md`, the flaky ledger | the Drive scopes they live in |
## Options where crews differ
Two crews ran this a week on different repositories and kept different habits
on a few points. Ship the one that fits the repository; both are supported.
| Point | Option A | Option B |
|---|---|---|
| Merge method | squash with `--match-head-commit` (one commit per PR; CHANGELOG merges by section) | merge commit with `--match-head-commit` (keeps the lane's commits; `history: merge` in the policy) |
| Paid evidence per PR | one train gate covers up to 5 PRs; lanes choose `--screen` or focused own cases | a full gate per PR at handoff, plus "gate-ahead": PR N+1 gates on (its branch + N's head) so N's merge needs no second gate |
| Reusing a gate | tree rule: a head whose diff from the gated tree touches only stamp files reuses the gate (`gstack-tree-receipt`) | the same rule, checked by the controller with `git diff <gated> <head>` before every merge |
| Severe fixes racing for a slot | the next train, `--urgent` for one PR | call the slot for all; the first complete READY wins and the rest restamp to the next patch |
| Cheap CI on PR pushes | free tests on every push; paid work only in trains and screens | a cheap tier on pushes, a `full-ci` label that dispatches the full suites on the exact head; the cheap tier is never merge evidence |
| Infra flakes | never rerun a harness/INFRA red; one re-dispatch for an INFRA-only census | one named infra flake (for example a known runner wedge) may be rerun once and must be stated in the READY; a second wedge escalates |
| Small fixes | one PR each, batched by the train | batched into wave PRs, since serial slots cost about 30 minutes each |
| After the merge | the main backstop gates any push a train did not cover, and freezes the queue on red | the controller watches main's test, e2e and release runs until the release publishes; a red main freezes the queue until root-caused |
| Severe triage | the Captain watches issues and PRs and starts a thread per unowned severe item | a scheduled severe sweep (every 4 to 8 hours) with a fixed severity bar, dedupe against owned lanes, one hotfix thread per batch |
| Contributor PRs | ported onto a crew branch with the author's commits kept and `Co-authored-by`; the fork PR is closed with a thank-you that links the port, posted as `capy-ai[bot]` through `tools.pr.comment`, never `gh` | landed through a wave PR with "Contributed by @handle", `Co-Authored-By` and `Supersedes` lines |
| Privacy | the pregate's sealed-output and secret scans before every push | plus a per-user denylist grep (names, hostnames, identifying counts from private sources) before any public artifact |
## Known Capy limits
Collected from the issue files five Captains wrote on 2026-10-26 after a week
of running this harness on five repositories. Each is a limit of the platform or of the
harness on it as of that date, with the workaround the crews use.
**Install and versions**
- Every machine, subagents included, needs its own install and trailer; a
fresh machine spends 30 seconds to a few minutes on it, and crews with nine
shard subagents per run pay it nine times. Workaround: bake gstack into the
project Setup snapshot.
- Threads that plan no workflow never install, so `/review` and
`/qa` go unused. Workaround: a crew rule that names the workflow
each thread will run, or the snapshot.
- gstack versions drift between machines in one thread (`behind=1` on one,
current on another); every minimum bump was a crew-wide broadcast (eight in
ten days for one crew). Workaround: the minimum revision in the crew rules,
quoted in every first report.
- A reviewer installed `--for review-voice` is refused by every workflow and
must reinstall `--for <skill>` when later asked to run one.
**Machines and long work**
- Machine sleep and restarts kill detached servers, local drivers and long
verify loops; `/tmp` does not survive a wake. Workaround: watched background
operations (they keep the machine awake), run directories under
`~/.capy/work`, long drivers on a remote VM, one worktree per counted run.
- Commits on a shared tree sweep up another subagent's uncommitted hunks.
Workaround: one committer per tree.
- Capy's git wrapper forces the linked identity, so tests that set
`GIT_AUTHOR_NAME` and assert on it fail on every Capy machine and pass in CI
(gstack's own `test/gstack-retro-metrics.test.ts` is one). Workaround: read
those failures as environment, and run the free suite on a remote VM for
acceptance.
- A broken shell line can print the environment, keys included, into a tool
log. Workaround: never run `env`, `printenv`, `set` or `export` bare;
check a secret as set or unset only (gstack's launch-env ratchet enforces
this in its own files).
**Pull requests and review**
- `tools.pr.create` without `index` or `head` publishes the primary
checkout's branch, not a worktree's, and targets the configured base, which
on a project that works off a non-default branch must be passed every time.
- Capy's per-push automatic review plus `/ship`'s two review passes double the
review time, and a review loop can run five or more cycles on a P0 as each
push surfaces one new finding. Workaround: run the review voices before the
first push; on a P0 fix only findings in the P0 path and defer the rest to a
follow-up.
- A clean review is not carried forward to a tree-identical merge head, which
can sit "running" for ten minutes or more.
- PR watches and CI callbacks fire only for repositories in a Capy project;
a thread waiting on a repository outside one sleeps until a timer wakes it.
- No per-subagent cost is visible, so `/ship` and plan reports cannot state
what the host subagents spent ("six host subagents, spend unknown").
- A merge rule keyed on "ship receipt on the head" has no machine-checkable
form on the platform side: the controller greps the PR body. `gstack-ship-queue
ready` and `train build` check it on the harness side.
**Approvals**
- An autoplan gate inside a crew thread costs a Captain round trip even when
the owner's standing answer is "take every recommendation"; record the
standing answer once with `gstack-approvals` and cite it. Rows mirror to the
Captain drive's `approvals/` directory when the
Captain created one, so other crew threads can read them; without it they
stay in the private project-user Drive and the Captain carries the text into
PR bodies.
**Harness gaps the same files name (fixed or owned in gstack, listed so the
reader knows the date):** the pregate failing every touched file in a
repository without `.gstack/pregate.json`; `/ship` printing no
`GSTACK_RESULT: skill=ship` line in unattended mode; the receipt going stale
on a late push (now refused by `ready`); the receipt-write step truncating a
PR body; `/ship`'s docs audit blocking on a risk the owner must accept by
hand; the pregate's secret scan refusing benign per-trial dummy tokens with no
allowlist; no cost ceiling surfaced at autoplan gates.
## Where things live
| What | Path |
|---|---|
| The user-scope skill and the Captain guide | `capy/skill/gstack/SKILL.md`, `capy/skill/gstack/captain.md`, `capy/skill/gstack/scripts/install.sh` |
| Captain templates and the Captain's day | `captain/templates/`, `crew/docs/captain.md` (ship with the `/captain` skill) |
| Install on Capy, snapshots, the launch line | `docs/capy-install.md` |
| Ship policy, queue and trains | `docs/ship-policy.md`, `lib/ship-queue*.ts`, `crew/lib/ship-queue-*.ts` (trains and waves), `bin/gstack-ship-queue`, `bin/gstack-restamp` |
| Pregate and lanes | `docs/pregate.md`, `bin/gstack-pregate` |
| Flaky reruns and records | `docs/TESTING_INTERNALS.md#flaky-reruns`, `scripts/lib/paid-rerun.ts`, `scripts/lib/ci-history.ts` |
| Stable release | `docs/releases.md`, `crew/scripts/release-cut.ts`, `.github/workflows/weekly-release.yml` |
| Scorecard | `bin/gstack-factory-scorecard`, `lib/factory-scorecard.ts`, `retro/SKILL.md.tmpl` Step 7.5, `test/factory-scorecard.test.ts` |
@@ -1,7 +1,7 @@
/**
* gate-cost-history — the two history replays of GSTA-49 plan lane T0 (P-i,
* P-k, and the READY-to-merge guardrail in its Risks). Pure logic: the CLI
* (`scripts/gate-cost-replay.ts --history`) passes run lists, report texts,
* (`crew/scripts/gate-cost-replay.ts --history`) passes run lists, report texts,
* head signatures and selections in; nothing here runs git or calls GitHub.
*
* 1. Red attribution: each red PR-gate run's failed cases split into
+2 -2
View File
@@ -2,7 +2,7 @@
* gate-cost — measurement-as-code for the paid PR gate's bill (GSTA-49 plan
* lane T0; numbers in its MEASUREMENT.md). Pure logic: callers pass artifact
* contents, changed-file lists and selector functions in; nothing here reads
* files, runs git or calls GitHub. scripts/gate-cost-replay.ts is the CLI.
* files, runs git or calls GitHub. crew/scripts/gate-cost-replay.ts is the CLI.
*
* Four parts:
* 1. Paid report artifacts -> per-case records, a gate's recorded cost by
@@ -18,7 +18,7 @@
* unattributed red). It ports the measurement's sim.py and uses CPython's
* Mersenne Twister, so the same seed gives the same figures.
*/
import { pricingRow } from './pricing';
import { pricingRow } from '../../lib/pricing';
export const SCREEN_LABEL = 'screen-provisional';
/** Anthropic bills 5-minute prompt-cache writes at 1.25x the model's input price (lib/pricing.ts excludes cache writes). */
@@ -9,7 +9,7 @@
* paths may be globs (`lib/**`); `*` and `**` are the only wildcards.
*/
import { spawnSync } from 'node:child_process';
import { globToRegExp } from './ship-policy';
import { globToRegExp } from '../../lib/ship-policy';
export interface LaneRef { branch: string; pr: number | null; ref: string }
export interface LaneFile { path: string; hunks: number; added: number; deleted: number }
@@ -17,7 +17,7 @@
* a green one; spend-capped and resumable.
* status stable, the last gate, pending commits and the hold.
*
* The gate never writes the stable branch. scripts/release-cut.ts is its only
* The gate never writes the stable branch. crew/scripts/release-cut.ts is its only
* writer: it reads a green record (or the check_name check run this gate's
* workflow publishes) as gated, and refuses every cut while a red record is not
* followed by a green gate on a later commit (the hold, on_red hold-release).
@@ -25,19 +25,19 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { DISPATCH_INPUTS, makeNonce } from '../scripts/eval-bg';
import { pollDelayMs } from './gh-poll';
import { DISPATCH_INPUTS, makeNonce } from '../../scripts/eval-bg';
import { pollDelayMs } from '../../lib/gh-poll';
import { ghFailureReason, type GhRunner } from '../scripts/recover-receipts';
import { MAX_BISECT_CASES, MAX_PREFIXES, RELEASE_BISECT_ARTIFACT, RELEASE_BISECT_REF, type Git } from '../scripts/train-gate';
import type { ReleaseGatePolicy } from './release-gate-policy';
import { loadPolicyFromBase, mergePolicy, validatePolicy } from './ship-policy';
import type { ReleaseGatePolicy } from '../../lib/release-gate-policy';
import { loadPolicyFromBase, mergePolicy, validatePolicy } from '../../lib/ship-policy';
import {
NOTES_REF, fetchNotes, nowIso, persist, repoIdentity,
type PrefixResult, type ReleaseBisect, type ReleaseBisectRound, type ReleaseRecord,
} from './ship-queue-train-record';
export const RELEASE_TRIAL_RESERVATION_USD = 1.5;
/** A bisect case runs one trial and, on a VERDICT failure, its one rule-16 rerun (scripts/train-gate.ts runBisectPrefix). */
/** A bisect case runs one trial and, on a VERDICT failure, its one rule-16 rerun (crew/scripts/train-gate.ts runBisectPrefix). */
export const RELEASE_TRIALS_PER_CASE = 2;
/** The gate plus the one re-dispatch EVAL_POLICY.infraRedispatch allows an incomplete census. */
export const MAX_RELEASE_ATTEMPTS = 2;
@@ -290,7 +290,7 @@ export function runGate(env: GateEnv, o: RunOptions): GateOut {
if (rec.flaky.length) lines.push(`RELEASE: flaky (failed once, passed its one rule-16 rerun in run ${run.id}): ${rec.flaky.join(', ')}`);
if (rec.verdict === 'GREEN') {
lines.push(`RELEASE: green sha=${sha} stable=${c.stable ?? 'none'} run=${run.id} cost=${usd(rec.cost_usd)}`,
`RELEASE: ${gate.check_name} success goes on ${short(sha)}; scripts/release-cut.ts moves ${gate.stable_ref} on its own schedule and floor`);
`RELEASE: ${gate.check_name} success goes on ${short(sha)}; crew/scripts/release-cut.ts moves ${gate.stable_ref} on its own schedule and floor`);
return out('gated', sha);
}
if (rec.verdict === 'RED') return finishRed(env, c, rec, lines, out);
@@ -301,7 +301,7 @@ export function runGate(env: GateEnv, o: RunOptions): GateOut {
function finishRed(env: GateEnv, c: Context, rec: ReleaseRecord, lines: string[], out: (a: GateAction, s: string | null) => GateOut): GateOut {
bisectRecord(env, c, rec, { dryRun: false }, lines);
lines.push(`RELEASE: red sha=${rec.sha} first_bad=${rec.bisect?.first_bad ?? 'unknown'} run=${rec.run?.id ?? '-'}`,
`RELEASE: hold: scripts/release-cut.ts refuses to move ${c.gate.stable_ref} until a gate on a later commit is green (red: ${rec.red_cases.join(', ')}); land the revert or the fix-forward`);
`RELEASE: hold: crew/scripts/release-cut.ts refuses to move ${c.gate.stable_ref} until a gate on a later commit is green (red: ${rec.red_cases.join(', ')}); land the revert or the fix-forward`);
return out('held', rec.sha);
}
@@ -413,7 +413,7 @@ export function gateStatus(env: GateEnv, o: { gate: ReleaseGatePolicy | null; la
const pending = base && SHA.test(head) ? (base === head ? 0 : firstParents(env, base, head)?.length ?? null) : null;
const hold = activeHold(c.records, c.isAncestor);
lines.push(
`STABLE: ${o.gate.stable_ref} ${c.stable ?? 'absent'} (scripts/release-cut.ts moves it; the gate never does)`,
`STABLE: ${o.gate.stable_ref} ${c.stable ?? 'absent'} (crew/scripts/release-cut.ts moves it; the gate never does)`,
`LAST_GATE: ${last ? `${last.verdict} sha=${last.sha} run=${last.run?.id ?? '-'} at=${last.created_at} cost=${usd(last.cost_usd)}${last.flaky.length ? ` flaky=${last.flaky.join(',')}` : ''}` : 'none'}`,
`PENDING: ${pending ?? 'unknown'} commit(s) on ${o.gate.branch} ${short(head)} since ${short(base)}`,
`HOLD: ${hold ? `red on ${hold.sha} since ${hold.created_at} (red: ${hold.red_cases.join(', ')}; first_bad=${hold.bisect?.first_bad ?? 'unknown'}, bisect ${hold.bisect?.state ?? 'not run'}); release-cut refuses until a gate on a later commit is green` : 'none'}`,
File renamed without changes.
@@ -16,10 +16,10 @@
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { pollDelayMs } from './gh-poll';
import type { PregateFile } from './headless-artifacts';
import { PREGATE_OUT_REL, clearObligation, readPregateFile, writePregateFile } from './pregate/index';
import type { QueueContext } from './ship-queue';
import { pollDelayMs } from '../../lib/gh-poll';
import type { PregateFile } from '../../lib/headless-artifacts';
import { PREGATE_OUT_REL, clearObligation, readPregateFile, writePregateFile } from '../../lib/pregate/index';
import type { QueueContext } from '../../lib/ship-queue';
import type { QueueFailure } from './ship-queue-gate';
import { git } from './ship-queue-gate';
import { prInfo, repoSlug, runView } from './ship-queue-train-github';
@@ -20,10 +20,10 @@ import { createHash, randomBytes } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { mergeChangelogs } from './changelog-check';
import { ensureTmpExcluded, restamp, versionFiles } from './restamp';
import type { ResultCodeName } from './result-codes';
import type { ShipPolicy } from './ship-policy';
import { mergeChangelogs } from '../../lib/changelog-check';
import { ensureTmpExcluded, restamp, versionFiles } from '../../lib/restamp';
import type { ResultCodeName } from '../../lib/result-codes';
import type { ShipPolicy } from '../../lib/ship-policy';
export const QUEUE_DIR_REL = '.gstack/tmp/ship-queue';
export const GATE_AHEAD_DIR_REL = `${QUEUE_DIR_REL}/gate-ahead`;
File renamed without changes.
@@ -12,8 +12,8 @@
* commit pending, a failed one makes it red (named as a commit status when no
* check run failed). Rerun it after an interruption and it continues.
*/
import { pollDelayMs } from './gh-poll';
import { ciAtHead, type QueueContext } from './ship-queue';
import { pollDelayMs } from '../../lib/gh-poll';
import { ciAtHead, type QueueContext } from '../../lib/ship-queue';
import { outFailed, outOk, outPending, type TrainOut } from './ship-queue-train';
const Q = 'gstack-ship-queue';
@@ -1,7 +1,7 @@
/**
* A dispatched screen's verdict, read from the run itself:
* never from a lane's prose. evals.yml's report job publishes
* `gate-binding-a<attempt>` (scripts/gate-reuse.ts: verdict, reconcile exit,
* `gate-binding-a<attempt>` (crew/scripts/gate-reuse.ts: verdict, reconcile exit,
* flaky cases) and `report-verdict-a<attempt>` (collector-outcomes.json: the
* panels and the rule-16 flaky lines) on every executed screen; a superseded
* screen plans nothing and uploads neither (hollowScreen): it concluded
@@ -13,7 +13,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { REUSE_FILE, parseBinding, parseReuse } from '../scripts/gate-reuse';
import type { QueueContext } from './ship-queue';
import type { QueueContext } from '../../lib/ship-queue';
import { downloadJson, hollowScreen, runView } from './ship-queue-train-github';
import { nowIso, type ScreenChoice, type ScreenVerdictRecord } from './ship-queue-train-record';
@@ -15,7 +15,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { bunIsolationArgs } from './bun-isolation';
import { bunIsolationArgs } from '../../lib/bun-isolation';
import { git } from './ship-queue-gate';
import type { TrainPrefix, TrainSlot } from './ship-queue-train-record';
@@ -6,9 +6,9 @@
* and the canonical `GSTACK_RESULT: skill=ship-queue status=<s> run=<record>`.
* Loaded by the bin only when one of those paths runs.
*/
import { EXIT, formatResult } from './headless-artifacts';
import { RESULT_CODES } from './result-codes';
import type { QueueContext } from './ship-queue';
import { EXIT, formatResult } from '../../lib/headless-artifacts';
import { RESULT_CODES } from '../../lib/result-codes';
import type { QueueContext } from '../../lib/ship-queue';
import { buildTrain, pruneTrains, trainStatus, type TrainOut } from './ship-queue-train';
import { trainReady, verifyScreen } from './ship-queue-train-ready';
import { trainBisect, trainRebuild, trainVerdict } from './ship-queue-train-verdict';
@@ -10,7 +10,7 @@
* the output says so.
*/
import * as fs from 'node:fs';
import type { QueueContext } from './ship-queue';
import type { QueueContext } from '../../lib/ship-queue';
import { outOk, outRefused, trainEnv, type TrainOut } from './ship-queue-train';
import { attemptsOf, forgetNotes, loadBisect, loadReady, loadTrain, noteName, readyPath, rebuildFromNotes, trainDir, type AnyRecord, type TrainState } from './ship-queue-train-record';
@@ -23,12 +23,12 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { awaitNonceRun, listDispatchRuns, makeNonce, runForNonce } from '../scripts/eval-bg';
import { scriptInvocation } from './script-invocation';
import type { ShipReceipt } from './headless-artifacts';
import { extractReceipt, placeReceipt } from './ship-receipt';
import { bindShippedReceipt } from './ship-receipt-depth';
import type { CommandGate, TrainGate, WorkflowsGate } from './ship-policy';
import { awaitNonceRun, listDispatchRuns, makeNonce, runForNonce } from '../../scripts/eval-bg';
import { scriptInvocation } from '../../lib/script-invocation';
import type { ShipReceipt } from '../../lib/headless-artifacts';
import { extractReceipt, placeReceipt } from '../../lib/ship-receipt';
import { bindShippedReceipt } from '../../lib/ship-receipt-depth';
import type { CommandGate, TrainGate, WorkflowsGate } from '../../lib/ship-policy';
import { git, type QueueFailure } from './ship-queue-gate';
import { execFor, repoSlug } from './ship-queue-train-github';
import {
@@ -38,7 +38,7 @@ import {
import { effect, outFailed, outOk, outPending, outRefused, prsArgs, save, type TrainEnv, type TrainOut } from './ship-queue-train';
const Q = 'gstack-ship-queue';
const UBI_RUNNER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'ubicloud', 'ubi-runner.sh');
const UBI_RUNNER = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'ubicloud', 'ubi-runner.sh');
/** Ubicloud list price per minute of the size command gates run on (scripts/ubicloud/pregate-remote.sh). */
export const COMMAND_SIZE = 'standard-16';
export const COMMAND_USD_PER_MIN = 0.00831;
@@ -7,8 +7,8 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import type { Exec, RunSummary } from '../scripts/eval-bg';
import type { QueueContext } from './ship-queue';
import type { Exec, RunSummary } from '../../scripts/eval-bg';
import type { QueueContext } from '../../lib/ship-queue';
export interface PrInfo { headRefOid: string; isCrossRepository: boolean; state: string; headRefName: string; body: string; baseRefName?: string }
export interface RunInfo { databaseId: number; status: string; conclusion: string | null; url: string; headSha: string; event: string; displayTitle: string }
@@ -9,10 +9,10 @@
* that matches train-prefix/ refs, a workflow never green on the base, and a merge method GitHub or history refuses.
* Neither writes anything.
*/
import { loadPolicyFromBase, isUnavailable, TRAIN_GATES_KEY_VERSION, globToRegExp, type ShipPolicy } from './ship-policy';
import type { QueueContext } from './ship-queue';
import { loadPolicyFromBase, isUnavailable, TRAIN_GATES_KEY_VERSION, globToRegExp, type ShipPolicy } from '../../lib/ship-policy';
import type { QueueContext } from '../../lib/ship-queue';
import { git } from './ship-queue-gate';
import { versionAtRef } from './restamp';
import { versionAtRef } from '../../lib/restamp';
import { commandReservation, concurrencyKeyed } from './ship-queue-train-gate';
import { repoSlug } from './ship-queue-train-github';
import { UNPROTECTED } from './ship-queue-train-land';
@@ -18,7 +18,7 @@
* (QUEUE_TRAIN_TREE_MISMATCH), as for verify. Fork heads are refused. Journaled beside the train record; a rerun
* resumes: open at the pin, open at the update commit, or merged (confirmed through main's tree).
*
* Train land takes the shared land lock (lib/ship-queue-land-lock.ts, #3599) as `train <nonce> slot <i> (#pr)`
* Train land takes the shared land lock (crew/lib/ship-queue-land-lock.ts, #3599) as `train <nonce> slot <i> (#pr)`
* before its first main read, so no standalone land publishes beside a slot. After a slot merges with landable
* slots left, the lock stays in a train-owned `between` phase that the same train's next slot takes over; a stop
* that freezes the queue leaves it in a `freeze` phase only the admitted repair train takes over. The release is
@@ -27,14 +27,14 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ciAtHead, type QueueContext } from './ship-queue';
import { ciAtHead, type QueueContext } from '../../lib/ship-queue';
import { git, writeJsonAtomic, type QueueFailure } from './ship-queue-gate';
import { acquireLandLock, lockLabel, type HeldLandLock, type LandLock, type LandLockPhase } from './ship-queue-land-lock';
import { waitChecks } from './ship-queue-land';
import { waitChecks } from '../../lib/ship-queue-land';
import { verifyPostMerge } from './ship-queue-post-merge';
import { bindTrainReceipt } from './ship-queue-train-gate';
import { checkRun, prInfo, type PrInfo } from './ship-queue-train-github';
import { landPlan } from './plan-record';
import { landPlan } from '../../lib/plan-record';
import { TERMINAL_STATES, activeFreeze, attemptsOf, freezeLockCleared, loadReady, nowIso, trainFile, type LandingRecord, type TrainRecord } from './ship-queue-train-record';
import { fetchBase, fetchTrainBranch, findTrain, frozenLines, nextFor, outFailed, outPending, outRefused, save, trainEnv, type TrainEnv, type TrainOut } from './ship-queue-train';
import { confirmMerged, stopped } from './ship-queue-train-verify';
@@ -8,9 +8,9 @@
* eval-bg's nonce with its purpose and policy in the key, and records the
* choice as a READY receipt (local state and refs/notes/gstack-trains) that
* `train build` requires. The screen's dollar price is left to the T0 cost
* replay (lib/gate-cost.ts) and printed as pending until it lands.
* replay (crew/lib/gate-cost.ts) and printed as pending until it lands.
*
* The screen's verdict comes from its run (lib/ship-queue-screen.ts): a head
* The screen's verdict comes from its run (crew/lib/ship-queue-screen.ts): a head
* whose recorded screen is RED is refused (QUEUE_SCREEN_RED) under either
* choice, and a red run is never re-dispatched at its merge SHA. The screen
* is planned on GitHub's refs/pull/<n>/merge when that merges the head, else
@@ -19,20 +19,20 @@
* conflict is QUEUE_SCREEN_STALE_REF.
*/
import { randomBytes } from 'node:crypto';
import { DISPATCH_INPUTS, awaitNonceRun, dedupeKey, dispatchWorkflow, findDuplicateRun, listDispatchRuns, makeNonce, type WorkflowDispatch } from '../scripts/eval-bg';
import { EXIT } from './headless-artifacts';
import { ready, type QueueContext } from './ship-queue';
import { DISPATCH_INPUTS, awaitNonceRun, dedupeKey, dispatchWorkflow, findDuplicateRun, listDispatchRuns, makeNonce, type WorkflowDispatch } from '../../scripts/eval-bg';
import { EXIT } from '../../lib/headless-artifacts';
import { ready, type QueueContext } from '../../lib/ship-queue';
import { classifyTrainHead } from './version-gate-train';
import { git } from './ship-queue-gate';
import { dispatchCi } from './ship-queue-dispatch-ci';
import { recordVerdict, screenVerdict, verdictLines, type ScreenOutcome } from './ship-queue-screen';
import { execFor, prInfo, repoSlug } from './ship-queue-train-github';
import { bindTrainReceipt } from './ship-queue-train-gate';
import { extractReceipt } from './ship-receipt';
import { extractReceipt } from '../../lib/ship-receipt';
import { loadReady, nowIso, persist, readyPath, rebuildFromNotes, type ReadyReceipt, type ScreenChoice } from './ship-queue-train-record';
import { baseOverlap, fetchBase, mergeCommit, outOk, outPending, outRefused, trainEnv, type TrainEnv, type TrainOut } from './ship-queue-train';
import { checkWavePin } from './ship-queue-wave';
import { readyPlan } from './plan-record';
import { readyPlan } from '../../lib/plan-record';
const Q = 'gstack-ship-queue';
@@ -64,8 +64,8 @@ export function skillOwnCases(selected: readonly string[], touchfiles: Record<st
* unscreened, where the full gate census runs every case on the tree that merges (GSTA-165).
*/
export async function gstackPaidSelection(repoRoot: string, baseSha: string, changed: string[], hasTemplate: (dir: string) => boolean): Promise<ScreenSelection> {
const { computePaidCaseSelection } = await import('../scripts/lib/paid-select');
const { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES } = await import('../test/helpers/touchfiles');
const { computePaidCaseSelection } = await import('../../scripts/lib/paid-select');
const { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES } = await import('../../test/helpers/touchfiles');
const skills = touchedSkills(changed, hasTemplate);
let e2e: string[]; let judges: number; let note = "the narrow screen's selection; --broad adds the dependents of files whose references reach every gate case";
let why: string;
@@ -97,7 +97,7 @@ export async function gstackPaidSelection(repoRoot: string, baseSha: string, cha
}
async function screenSelection(env: TrainEnv, head: string, baseSha: string): Promise<ScreenSelection> {
const price = 'SCREEN: price pending the T0 cost replay (lib/gate-cost.ts): cases without cost data are unknown; a red train holds its PRs about an hour';
const price = 'SCREEN: price pending the T0 cost replay (crew/lib/gate-cost.ts): cases without cost data are unknown; a red train holds its PRs about an hour';
if (env.train.gates || env.train.screen_selection === 'none') return { recommend: null, lines: ['SCREEN: selection not computed (train.screen_selection is none); no recommendation, the lane decides', price] };
const changed = git(env.ctx.repoRoot, ['diff', '--name-only', `${baseSha}...${head}`]).out.split('\n').filter(Boolean);
const hasTemplate = (dir: string) => git(env.ctx.repoRoot, ['cat-file', '-e', `${head}:${dir}/SKILL.md.tmpl`]).ok;
@@ -10,10 +10,10 @@
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { redactFindingSpans } from './redact-engine';
import { redactFindingSpans } from '../../lib/redact-engine';
import { git, sha256, writeJsonAtomic } from './ship-queue-gate';
import { resolveStateRoot } from './state-root';
import type { PlanRecord } from './plan-record';
import { resolveStateRoot } from '../../lib/state-root';
import type { PlanRecord } from '../../lib/plan-record';
export const TRAIN_SCHEMA = 1;
export const NOTES_REF = 'refs/notes/gstack-trains';
@@ -25,7 +25,7 @@ export type TrainState =
/** States after which a train never dispatches again; prune deletes their branches (`stale`: every PR merged or closed outside the train, `train prune --stale`). */
export const TERMINAL_STATES: readonly TrainState[] = ['free-red', 'attributed', 'red-on-main', 'merged', 'superseded', 'stale'];
/** What the screen run itself said (lib/ship-queue-screen.ts), recorded when it was read; absent while the run is pending or unread. */
/** What the screen run itself said (crew/lib/ship-queue-screen.ts), recorded when it was read; absent while the run is pending or unread. */
export interface ScreenVerdictRecord { outcome: 'GREEN' | 'RED' | 'NOT_RUN' | 'INCOMPLETE'; run: number; red: string[]; flaky: string[]; detail: string; read_at: string }
export interface ScreenChoice {
choice: 'screen' | 'no-screen'; policy: 'narrow' | 'broad' | null; nonce: string | null; run: number | null; merge_sha: string | null;
@@ -41,17 +41,17 @@ export interface TrainSlot {
base_red?: BaseRedCondition;
/** The fixer's READY (`ready --fixes-base-red`): the gate cases T0 may be red on, and only those. */
fixes_base_red?: string[];
/** The slot is a wave PR (lib/ship-queue-wave.ts) carrying these constituents at their pins. */
/** The slot is a wave PR (crew/lib/ship-queue-wave.ts) carrying these constituents at their pins. */
wave?: { nonce: string; constituents: string[] };
}
/** One prefix a generic train gates (lib/ship-queue-train-gate.ts), on its own train-prefix/<nonce>/t<i> (or s<i>) ref. */
/** One prefix a generic train gates (crew/lib/ship-queue-train-gate.ts), on its own train-prefix/<nonce>/t<i> (or s<i>) ref. */
export interface GatedPrefix { index: number; ref: string; commit: string; tree: string; outcome: GateOutcome | null; red: string[]; detail: string | null }
/** A generic train's verdict over its prefixes: slots 1..landable may land; culprit is the held slot's PR. */
export interface GenericVerdict { kind: 'green' | 'attributed' | 'red-on-main' | 'unattributed' | 'incomplete'; landable: number; culprit: string | null; at: string }
/** Written to the record and the notes before train land's first remote effect for a slot. */
export interface LandingRecord { slot: number; mode: 'direct' | 'update'; method: 'squash' | 'merge' | 'rebase'; pin: string; update: string | null; base: string; expected_tree: string; at: string }
export interface TrainPrefix { index: number; commit: string; tree: string; pr: string | null }
/** A stamped prefix S_i (changelog_fragments trains, lib/ship-queue-train-stamp.ts): what slot i publishes, exactly. */
/** A stamped prefix S_i (changelog_fragments trains, crew/lib/ship-queue-train-stamp.ts): what slot i publishes, exactly. */
export interface StampedPrefix { index: number; commit: string; tree: string; pr: string; version: string; date: string }
export interface FreeRun { run: number | null; url: string | null; head_sha: string | null; conclusion: string | null; verdict: 'green' | 'red' | 'missing' | 'stale' | 'cancelled' | 'pending' }
export interface TrainRecord {
@@ -99,7 +99,7 @@ export interface FreezeRecord {
main: { sha: string; tree: string }; reason: string; backstop: { nonce: string; run: number | null; state: string } | null;
repair: { pr: string; head: string; train: string | null } | null; created_at: string; cleared_at: string | null; cleared_by: string | null; updated_at: string;
}
/** A constituent of a wave PR (lib/ship-queue-wave.ts): the PR, its pinned head and the wave-branch merge commit whose second parent is that head. */
/** A constituent of a wave PR (crew/lib/ship-queue-wave.ts): the PR, its pinned head and the wave-branch merge commit whose second parent is that head. */
export interface WaveConstituent {
pr: string; head: string; merge_commit: string | null;
/** Files the integrator resolved by hand in this constituent's merge (wave build --continue); absent for a clean merge. */
@@ -120,14 +120,14 @@ export interface ReadyReceipt {
/** The PR body's Plan line as ready resolved it (lib/plan-record.ts); absent before plan_record or when it did not resolve. */
plan?: PlanRecord;
}
/** One release-gate bisect round (lib/release-gate.ts): the prefixes it dispatched on release-gate/<id> and what each case did there. */
/** One release-gate bisect round (crew/lib/release-gate.ts): the prefixes it dispatched on release-gate/<id> and what each case did there. */
export interface ReleaseBisectRound { nonce: string; ref: string; run: { id: number; url: string } | null; prefixes: Array<{ sha: string; results: Record<string, PrefixResult> }> }
/** A red release gate's bisect over the first-parent commits after the last green gate: first_bad once good and bad are adjacent. */
export interface ReleaseBisect {
cases: string[]; good: string | null; bad: string; first_bad: string | null; spend_cap: number; spent_usd: number;
state: 'running' | 'found' | 'unknown' | 'capped'; detail: string; rounds: ReleaseBisectRound[];
}
/** The release gate's verdict on one commit of the gated branch (lib/release-gate.ts), the evidence scripts/release-cut.ts reads. */
/** The release gate's verdict on one commit of the gated branch (crew/lib/release-gate.ts), the evidence crew/scripts/release-cut.ts reads. */
export interface ReleaseRecord {
schema: 1; kind: 'release'; repo_key: string; sha: string; tree: string; branch: string; stable: string | null; since: string | null;
nonce: string; attempts: number; run: { id: number; url: string } | null; verdict: 'PENDING' | 'GREEN' | 'RED' | 'INCOMPLETE';
@@ -12,14 +12,14 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { parseChangelog } from './changelog-check';
import { restamp, versionAtRef, versionFiles } from './restamp';
import { matchesAny, type ShipPolicy } from './ship-policy';
import { parseChangelog } from '../../lib/changelog-check';
import { restamp, versionAtRef, versionFiles } from '../../lib/restamp';
import { matchesAny, type ShipPolicy } from '../../lib/ship-policy';
import { git, type QueueFailure } from './ship-queue-gate';
import { linkedReleaseFile } from './ship-queue-land';
import { linkedReleaseFile } from '../../lib/ship-queue-land';
import { mergeCommit } from './ship-queue-train';
import type { StampedPrefix, TrainPrefix, TrainSlot } from './ship-queue-train-record';
import { buildReceipt } from './tree-receipt';
import { buildReceipt } from '../../lib/tree-receipt';
export type { StampedPrefix };
export interface StampChainOptions {
@@ -12,8 +12,8 @@
*/
import * as path from 'node:path';
import { randomBytes } from 'node:crypto';
import { DISPATCH_INPUTS, listDispatchRuns, makeNonce, runForNonce } from '../scripts/eval-bg';
import type { QueueContext } from './ship-queue';
import { DISPATCH_INPUTS, listDispatchRuns, makeNonce, runForNonce } from '../../scripts/eval-bg';
import type { QueueContext } from '../../lib/ship-queue';
import { git } from './ship-queue-gate';
import {
checkRun, downloadJson, execFor, asRun, remoteBranchSha, repoSlug, runView, type CheckInfo, type RunInfo,
@@ -15,19 +15,19 @@
* freeze; verify clears the freeze when main after that merge carries it.
*/
import { randomBytes } from 'node:crypto';
import { DISPATCH_INPUTS, awaitNonceRun, dispatchWorkflow, makeNonce } from '../scripts/eval-bg';
import type { QueueContext } from './ship-queue';
import { DISPATCH_INPUTS, awaitNonceRun, dispatchWorkflow, makeNonce } from '../../scripts/eval-bg';
import type { QueueContext } from '../../lib/ship-queue';
import { git } from './ship-queue-gate';
import { bindReceipt } from './ship-receipt';
import { bindReceipt } from '../../lib/ship-receipt';
import { checkRun, execFor, prInfo, repoSlug } from './ship-queue-train-github';
import { activeFreeze, attemptsOf, listTrains, nowIso, persist, rebuildFromNotes, trainFile, type FreezeRecord, type TrainRecord } from './ship-queue-train-record';
import {
effect, fetchBase, fetchTrainBranch, findTrain, frozenLines, nextFor, outFailed, outOk, outRefused, pinList, prState, prsArgs, save, trainEnv, type TrainEnv, type TrainOut,
} from './ship-queue-train';
import { mergeChangelogs, ownEntryAboveMain, parseChangelog } from './changelog-check';
import { buildReceipt } from './tree-receipt';
import { versionAtRef } from './restamp';
import { cmpVersion, parseVersion } from './version-source';
import { mergeChangelogs, ownEntryAboveMain, parseChangelog } from '../../lib/changelog-check';
import { buildReceipt } from '../../lib/tree-receipt';
import { versionAtRef } from '../../lib/restamp';
import { cmpVersion, parseVersion } from '../../lib/version-source';
import { readLock, writeLock } from './ship-queue-land-lock';
import { genericBackstop } from './ship-queue-train-gate';
@@ -13,8 +13,8 @@
* --land lands each landable slot in order through train land, stopping at the first refusal with its code and NEXT.
* It never merges without --land and never merges main into a PR.
*/
import { pollDelayMs } from './gh-poll';
import type { QueueContext } from './ship-queue';
import { pollDelayMs } from '../../lib/gh-poll';
import type { QueueContext } from '../../lib/ship-queue';
import { buildTrain, findTrain, nextFor, outOk, outPending, outRefused, trainEnv, trainStatus, type TrainOut } from './ship-queue-train';
import { repoSlug, runView } from './ship-queue-train-github';
import { attemptsOf, trainFile, type TrainRecord } from './ship-queue-train-record';
@@ -10,20 +10,20 @@
* gate-ahead merge rules: version files take the earlier prefix, the
* CHANGELOG merges by section), so each prefix T0..Tk is a commit with a
* fixed identity and date and the same pins always give the same train head.
* Records (lib/ship-queue-train-record.ts) are written before every remote
* Records (crew/lib/ship-queue-train-record.ts) are written before every remote
* effect: the branch push, the free-tests dispatch, the paid dispatch through
* scripts/eval-bg.ts's nonce.
*/
import { randomBytes } from 'node:crypto';
import { DISPATCH_INPUTS, awaitNonceRun, dispatchWorkflow, listDispatchRuns, makeNonce, runForNonce, type WorkflowDispatch } from '../scripts/eval-bg';
import { carryReleasedParent, changelogDamage, mergeChangelogs, parseChangelog, type ChangelogSection } from './changelog-check';
import type { ResultStatus } from './headless-artifacts';
import { pollDelayMs } from './gh-poll';
import { baseOverlapLine } from './pr-wait';
import { versionAtRef, versionFiles } from './restamp';
import type { LoadedPolicy, ShipPolicy, TrainPolicy } from './ship-policy';
import { loadBasePolicy, type QueueContext } from './ship-queue';
import { mergeManifest } from './ship-queue-land';
import { DISPATCH_INPUTS, awaitNonceRun, dispatchWorkflow, listDispatchRuns, makeNonce, runForNonce, type WorkflowDispatch } from '../../scripts/eval-bg';
import { carryReleasedParent, changelogDamage, mergeChangelogs, parseChangelog, type ChangelogSection } from '../../lib/changelog-check';
import type { ResultStatus } from '../../lib/headless-artifacts';
import { pollDelayMs } from '../../lib/gh-poll';
import { baseOverlapLine } from '../../lib/pr-wait';
import { versionAtRef, versionFiles } from '../../lib/restamp';
import type { LoadedPolicy, ShipPolicy, TrainPolicy } from '../../lib/ship-policy';
import { loadBasePolicy, type QueueContext } from '../../lib/ship-queue';
import { mergeManifest } from '../../lib/ship-queue-land';
import { git, sha256, withIndex, type QueueFailure } from './ship-queue-gate';
import { recordVerdict, screenVerdict, verdictLines } from './ship-queue-screen';
import { stackTestLine, stackTests } from './ship-queue-stack-tests';
@@ -332,7 +332,7 @@ export function gateDispatch(env: TrainEnv, train: TrainRecord, nonce: string):
export interface BuildOptions {
pins?: string; order?: string; resume?: string; dryRun: boolean; urgent: boolean; repair: boolean; freeTimeoutSec: number; pollMs: number;
/** `--dry-run --stack-tests`: run each PR's new test files on its integrated prefix (lib/ship-queue-stack-tests.ts); the command's words when `--stack-tests-command` names one. */
/** `--dry-run --stack-tests`: run each PR's new test files on its integrated prefix (crew/lib/ship-queue-stack-tests.ts); the command's words when `--stack-tests-command` names one. */
stackTests?: string[] | true;
/** Stamping S_i runs the base policy's restamp_command or release_tool (changelog_fragments trains). */
allowRepoCommands?: boolean;
@@ -568,7 +568,7 @@ export async function buildTrain(ctx: QueueContext, o: BuildOptions): Promise<Tr
if (!('kind' in t)) return t;
if (o.dryRun) {
const dry = [...lines, ...describe(t), env.train.gates ? `ESTIMATE: ${env.train.gates.length} gate(s) on each of ${t.prefixes.length} prefixes (T0..T${t.prefixes.length - 1}, every one gated)`
: `ESTIMATE: one ${env.train.gate_workflow} full gate (evals_all) on T${t.prefixes.length - 1} plus ${env.train.free_workflow}; dollar figure pending the T0 cost replay (lib/gate-cost.ts)`];
: `ESTIMATE: one ${env.train.gate_workflow} full gate (evals_all) on T${t.prefixes.length - 1} plus ${env.train.free_workflow}; dollar figure pending the T0 cost replay (crew/lib/gate-cost.ts)`];
if (o.stackTests) {
const results = stackTests(env.ctx.repoRoot, t.prefixes[0]!.commit, t.slots, t.prefixes, o.stackTests === true ? undefined : o.stackTests);
dry.push(...results.map(stackTestLine));
@@ -13,7 +13,7 @@
* wave settle after the wave PR merged: close each constituent whose head is still its pin with a
* link to the wave and version; a moved constituent stays open with a comment
*
* The wave record (kind 'wave', lib/ship-queue-train-record.ts) is written locally and to
* The wave record (kind 'wave', crew/lib/ship-queue-train-record.ts) is written locally and to
* refs/notes/gstack-trains before the branch push, so ready and train build on any machine
* check the same pins (checkWavePin). Constituent heads freeze once in: a moved head refuses
* the wave (QUEUE_WAVE_MOVED) and fixes go on the wave branch as integrator commits.
@@ -21,11 +21,11 @@
import { randomBytes } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, formatResult } from './headless-artifacts';
import { RESULT_CODES } from './result-codes';
import { versionAtRef } from './restamp';
import { globToRegExp, type ShipPolicy } from './ship-policy';
import { ciAtHead, type QueueContext } from './ship-queue';
import { EXIT, formatResult } from '../../lib/headless-artifacts';
import { RESULT_CODES } from '../../lib/result-codes';
import { versionAtRef } from '../../lib/restamp';
import { globToRegExp, type ShipPolicy } from '../../lib/ship-policy';
import { ciAtHead, type QueueContext } from '../../lib/ship-queue';
import { git, sha256, type QueueFailure } from './ship-queue-gate';
import { fetchBase, mergeCommit, mergeStep, outFailed, outOk, outRefused, parsePins, trainEnv, type TrainEnv, type TrainOut } from './ship-queue-train';
import { prInfo, type PrInfo } from './ship-queue-train-github';
@@ -51,11 +51,12 @@ export function isReleasePath(file: string, policy: ShipPolicy): boolean {
/**
* A file's functional area: the skill directory for files in a skill, the subdirectory for
* lib/<dir>/…, the first word of the module name for lib, bin, test and scripts files
* (lib/ship-queue-train.ts and test/ship-receipt.test.ts are both "ship"), docs for docs/**.
* (crew/lib/ship-queue-train.ts and test/ship-receipt.test.ts are both "ship"), docs for docs/**.
*/
export function areaKey(file: string, isSkillDir: (dir: string) => boolean): string {
const parts = file.split('/');
const top = parts[0]!;
if (top === 'crew' && parts.length > 2) return areaKey(parts.slice(1).join('/'), isSkillDir);
if (parts.length > 1 && isSkillDir(top)) return top;
if (top === 'docs') return 'docs';
if (['lib', 'bin', 'test', 'scripts'].includes(top) && parts.length > 1) {
File renamed without changes.
@@ -11,11 +11,11 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { parseChangelog } from './changelog-check';
import { effectiveBump, readFragments } from './changelog-fragments';
import { HistoryReadError, readHistoryFile } from './history-read';
import { classifyRelease } from './release-stamp';
import { isUnavailable, loadPolicyFromBase, matchesAny } from './ship-policy';
import { parseChangelog } from '../../lib/changelog-check';
import { effectiveBump, readFragments } from '../../lib/changelog-fragments';
import { HistoryReadError, readHistoryFile } from '../../lib/history-read';
import { classifyRelease } from '../../lib/release-stamp';
import { isUnavailable, loadPolicyFromBase, matchesAny } from '../../lib/ship-policy';
export type TrainHead =
| { kind: 'off' }
@@ -17,7 +17,7 @@
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { EvalHost } from '../test/helpers/e2e-hosts';
import type { EvalHost } from '../../test/helpers/e2e-hosts';
export interface CensusArm { host: EvalHost; model?: string }
@@ -18,7 +18,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { isBuiltin } from 'node:module';
import { createHash } from 'node:crypto';
import { atomicWriteSync } from '../lib/fs-atomic';
import { atomicWriteSync } from '../../lib/fs-atomic';
/**
* Follow literal module imports from `entries` (repo-relative) without
@@ -4,16 +4,16 @@
* policy would have cost on recent merges (GSTA-49 plan lane T0). Free and
* read-only: it downloads recorded report artifacts with `gh` into a cache,
* replays origin/main's first-parent squash merges through the selectors in
* this checkout, and models merge trains. Logic lives in lib/gate-cost.ts.
* this checkout, and models merge trains. Logic lives in crew/lib/gate-cost.ts.
*
* Usage:
* bun run scripts/gate-cost-replay.ts --runs <id,id,...> [--since YYYY-MM-DD --until YYYY-MM-DD | --prs 3201,3202]
* bun run crew/scripts/gate-cost-replay.ts --runs <id,id,...> [--since YYYY-MM-DD --until YYYY-MM-DD | --prs 3201,3202]
* [--ref origin/main] [--repo owner/name] [--cache .context/gate-cost-cache] [--window-days N]
* [--usd-per-second R]
* [--train-size 5] [--reps 400] [--seed 1] [--cases] [--history] [--json]
*
* --history (needs --since/--until) adds the red-head replay and the
* READY-arrival train replay (lib/gate-cost-history.ts): it lists the
* READY-arrival train replay (crew/lib/gate-cost-history.ts): it lists the
* window's E2E Evals and Free Tests runs and PRs, downloads report-verdict
* and trial-outcomes artifacts of red runs, and fetches PR heads by SHA.
*
@@ -37,12 +37,12 @@ import {
stampFreeSignature, summarizeRedHeads, SCREEN_CANARY_CASES,
type ArrivalTrainResult, type CheckRun, type GatedRun, type HeadDiff, type LatencySummary, type ReadyRow, type RedHeadRow, type RedHeadSummary, type RedRun,
} from '../lib/gate-cost-history';
import { GH_CLIENT, GH_HISTORY, downloadRunArtifacts, repoSlug } from './lib/ci-history';
import { computePaidCaseSelection } from './lib/paid-select';
import { packageChangeOnlyVersion } from './test-pr-profile';
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES, matchGlob } from '../test/helpers/touchfiles';
import { GH_CLIENT, GH_HISTORY, downloadRunArtifacts, repoSlug } from '../../scripts/lib/ci-history';
import { computePaidCaseSelection } from '../../scripts/lib/paid-select';
import { packageChangeOnlyVersion } from '../../scripts/test-pr-profile';
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES, LLM_JUDGE_TOUCHFILES, matchGlob } from '../../test/helpers/touchfiles';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
const DAY_MS = 86_400_000;
const ARTIFACT = /^(paid-slice-\d+|trial-outcomes[A-Za-z0-9_-]*?)-a(\d+)$/;
export const MODEL_POLICIES: ScreenPolicy[] = [
@@ -2,7 +2,7 @@
* gate-diff calibration — free, unshipped-to-skills tool inventory for the
* gate-edit detector (W1 of docs/designs/HONEST_WORK_GATE_INTEGRITY.md).
*
* bun scripts/gate-diff-calibrate.ts [--ref origin/main] [--count 50] [--repo <path>] [--markdown]
* bun crew/scripts/gate-diff-calibrate.ts [--ref origin/main] [--count 50] [--repo <path>] [--markdown]
*
* Runs the pure core (lib/gate-diff) over the first-parent delta of the last
* N commits on <ref>'s first-parent line (one per landed PR: a merge commit
@@ -17,9 +17,9 @@
* No artifact is written: calibration never touches the state root.
*/
import * as path from 'node:path';
import { EMPTY_PROJECT_CONFIG, parseGateIntegrityBlock, scanCandidate, specDirectoryHoldsSpecs, type ScanResult } from '../lib/gate-diff/index';
import { commitInfo, diffCommit, firstParent, readClaudeMd, repoRoot, runGit, treePaths } from '../lib/gate-diff/git';
import { COVERAGE_TAGS } from '../lib/gate-diff/tags';
import { EMPTY_PROJECT_CONFIG, parseGateIntegrityBlock, scanCandidate, specDirectoryHoldsSpecs, type ScanResult } from '../../lib/gate-diff/index';
import { commitInfo, diffCommit, firstParent, readClaudeMd, repoRoot, runGit, treePaths } from '../../lib/gate-diff/git';
import { COVERAGE_TAGS } from '../../lib/gate-diff/tags';
interface Args {
ref: string;
@@ -37,7 +37,7 @@ function parseArgs(argv: string[]): Args {
else if (x === '--repo') a.repo = argv[++i] ?? a.repo;
else if (x === '--markdown') a.markdown = true;
else if (x === '--help' || x === '-h') {
console.log('usage: bun scripts/gate-diff-calibrate.ts [--ref origin/main] [--count 50] [--repo <path>] [--markdown]');
console.log('usage: bun crew/scripts/gate-diff-calibrate.ts [--ref origin/main] [--count 50] [--repo <path>] [--markdown]');
process.exit(0);
} else throw new Error(`unknown argument ${x}`);
}
@@ -54,12 +54,12 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { receiptDigest } from '../lib/ship-queue';
import { isUnavailable, loadPolicyFromBase, matchesAny, type ShipPolicy } from '../lib/ship-policy';
import { rewriteStamps } from '../lib/restamp';
import { buildReceipt } from '../lib/tree-receipt';
import { receiptDigest } from '../../lib/ship-queue';
import { isUnavailable, loadPolicyFromBase, matchesAny, type ShipPolicy } from '../../lib/ship-policy';
import { rewriteStamps } from '../../lib/restamp';
import { buildReceipt } from '../../lib/tree-receipt';
import { ghFailureReason, listGateRuns, runGh, type GhRunner, type WorkflowRun } from './recover-receipts';
import type { PaidRunManifest } from './lib/paid-plan';
import type { PaidRunManifest } from '../../scripts/lib/paid-plan';
export const BINDING_FILE = 'gate-binding.json';
export const CANDIDATE_FILE = 'candidate.json';
@@ -505,14 +505,14 @@ export function reuseReport(reuse: GateReuse, repo: string): string {
}
const USAGE = `usage:
bun run scripts/gate-reuse.ts bind <report dir> --out <dir> --repo <owner/name> --pr <n> --run-id <id> --run-attempt <n>
bun run crew/scripts/gate-reuse.ts bind <report dir> --out <dir> --repo <owner/name> --pr <n> --run-id <id> --run-attempt <n>
--head-sha <sha> --tested-sha <merge sha> --profile <pr|full> --reconcile-exit <n> [--purpose pr|screen|train|bisect|dispatch]
[--screen-policy narrow|broad]
bun run scripts/gate-reuse.ts find --repo <owner/name> --pr <n> --head-repo <owner/name> --branch <head branch> --run-id <id>
bun run crew/scripts/gate-reuse.ts find --repo <owner/name> --pr <n> --head-repo <owner/name> --branch <head branch> --run-id <id>
--head-sha <sha> --tested-sha <merge sha> --out <dir> [--screen-policy narrow|broad] [--budget-seconds 90]
--screen-policy marks a dispatched screen (its tested sha is the pinned refs/pull/<n>/merge commit).
bun run scripts/gate-reuse.ts confirm <candidate dir> <manifest.json> --out <paid-plan dir> [--github-output <file>]
bun run scripts/gate-reuse.ts report <gate-reuse.json> --repo <owner/name>
bun run crew/scripts/gate-reuse.ts confirm <candidate dir> <manifest.json> --out <paid-plan dir> [--github-output <file>]
bun run crew/scripts/gate-reuse.ts report <gate-reuse.json> --repo <owner/name>
bind and find never fail the run; no candidate means the full gate runs.`;
if (import.meta.main) {
@@ -4,8 +4,8 @@
* Each config builds prompts through the same builder the eval sends; "old" is
* today's request and "new" adds only the JSON schema (comparison 1).
*/
import { ARM_JUDGE_MODEL, ARM_JUDGE_SCHEMA, buildArmJudgePrompt, parseArmJudgeResponse } from '../test/helpers/llm-judge';
import type { CalibrationConfig } from './judge-calibration';
import { ARM_JUDGE_MODEL, ARM_JUDGE_SCHEMA, buildArmJudgePrompt, parseArmJudgeResponse } from '../../test/helpers/llm-judge';
import type { CalibrationConfig } from '../../scripts/judge-calibration';
export const CALIBRATION_CONFIGS: CalibrationConfig[] = [
{
@@ -5,7 +5,7 @@
* appending to it while the guard runs, and measure one hook call (read plus
* evaluation) in a fresh process: wall milliseconds and peak RSS.
*
* bun scripts/measure-journal-read.ts [--runs N] [--json] [--strict]
* bun crew/scripts/measure-journal-read.ts [--runs N] [--json] [--strict]
*
* Two journals, 120 MiB each:
* - `mixed`: many records of mixed size (assistant text, Bash and Read pairs,
@@ -68,7 +68,7 @@ const BENCHMARKS: Benchmark[] = [
/** Build one journal: a published Phase 1 and the Phase 2 entry Read as the current, journaled call. */
async function build(bench: Benchmark) {
const { guardFixture, section } = await import('../test/helpers/autoplan-guard-fixture');
const { guardFixture, section } = await import('../../test/helpers/autoplan-guard-fixture');
const f = guardFixture('ceo');
f.publish();
f.use('next', 'Read', { file_path: section('design-phase.md') });
@@ -99,8 +99,8 @@ async function build(bench: Benchmark) {
/** Child: one hook call in a fresh process; peak RSS is this process's own. */
async function measure(meta: string) {
const { input, cwd, stateRoot } = JSON.parse(fs.readFileSync(meta, 'utf8'));
const { runPublicationHook } = await import('../autoplan/bin/phase-publication-hook.ts');
const root = fs.realpathSync(path.join(import.meta.dir, '..'));
const { runPublicationHook } = await import('../../autoplan/bin/phase-publication-hook.ts');
const root = fs.realpathSync(path.join(import.meta.dir, '..', '..'));
process.env.CLAUDE_PROJECT_DIR = cwd; process.env.GSTACK_STATE_ROOT = stateRoot; process.env.GSTACK_HOME = stateRoot;
const started = performance.now();
const output: any = await runPublicationHook(input, root);
@@ -27,8 +27,8 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { mergeReceiptDirs, writeNegativeReceipt } from './e2e-shard-reuse';
import { loadSliceArtifacts, panelReports, parseRunManifest, shardFile } from './test-paid-shards';
import { mergeReceiptDirs, writeNegativeReceipt } from '../../scripts/e2e-shard-reuse';
import { loadSliceArtifacts, panelReports, parseRunManifest, shardFile } from '../../scripts/test-paid-shards';
export const RECOVERY_FILE = 'recovery.json';
export const MAX_RECOVERED_RUNS = 5;
@@ -302,9 +302,9 @@ export function recoveryDecision(store: string): { reuse: boolean; reason: strin
}
const USAGE = `usage:
bun run scripts/recover-receipts.ts collect --repo <owner/name> --pr <n> --head-repo <owner/name> --branch <head branch>
bun run crew/scripts/recover-receipts.ts collect --repo <owner/name> --pr <n> --head-repo <owner/name> --branch <head branch>
--run-id <this run> --store <dir> [--store-key <restored cache key>] [--mode pr|dispatch] [--budget-seconds 60]
bun run scripts/recover-receipts.ts decide <store> [--github-output <file>]
bun run crew/scripts/recover-receipts.ts decide <store> [--github-output <file>]
Recovery never fails: on any gh error it records why and the run executes unreduced.`;
if (import.meta.main) {
@@ -3,7 +3,7 @@
* Garry, 2026-11-06 (R74): "make sure anything important makes it to stable as frequently as once a day
* and as infrequently as once a week".
*
* .github/workflows/weekly-release.yml runs scripts/release-cut.ts --daily every day at 14:00 UTC. A
* .github/workflows/weekly-release.yml runs crew/scripts/release-cut.ts --daily every day at 14:00 UTC. A
* day's run cuts when one trigger holds, each over its own floor since stable last moved:
* important a changelog.d fragment with `important: security|regression|data-loss|blocker` was added
* in stable..candidate (the newest green, gated commit): IMPORTANT_FLOOR_HOURS (24);
@@ -16,7 +16,7 @@
* moved with nothing newer green and gated is STABLE_STALE. Both are blocked: the run goes red. Fragments are read from the
* commits that added them, so the release stamp deleting the files does not hide them.
*/
import { parseFragment, type Importance } from '../lib/changelog-fragments';
import { parseFragment, type Importance } from '../../lib/changelog-fragments';
import type { Git } from './release-cut';
export const FLOOR_HOURS = 72;
@@ -4,12 +4,12 @@
* GitHub Release whose notes are that span's CHANGELOG headlines, and
* fast-forward refs/heads/stable to it (docs/releases.md). Run by
* .github/workflows/weekly-release.yml ("Stable release") every day at 14:00
* UTC with --daily (the daily check, scripts/release-cadence.ts: an important
* UTC with --daily (the daily check, crew/scripts/release-cadence.ts: an important
* change after 24 h, a forced cut at 7 days, the regular cut on Monday and
* Thursday) and by hand (--ref <sha on main> for a hotfix). Dry runs print the
* plan and change nothing.
*
* bun scripts/release-cut.ts --repo owner/name [--ref <sha>] [--security <reason> | --regression <reason>] [--daily [--fragments-dir <dir>]] [--dry-run]
* bun crew/scripts/release-cut.ts --repo owner/name [--ref <sha>] [--security <reason> | --regression <reason>] [--daily [--fragments-dir <dir>]] [--dry-run]
* [--summary <file>] [--required-check <name>] [--backstop-check <name>] [--notes-ref <ref>]
*
* The checkout needs origin's full history (fetch-depth 0). Green, for a
@@ -30,7 +30,7 @@
* the main commits `gstack-ship-queue verify --train` confirmed, so a neutral
* or absent backstop run on such a commit is not held against it, and the
* CANDIDATE line says "gated by train <nonce>". Or a GREEN release record
* (kind release, lib/release-gate.ts) names the commit: "gated by release gate
* (kind release, crew/lib/release-gate.ts) names the commit: "gated by release gate
* run <id>". A red backstop run stays red either way. A commit with none of
* these is not green. A RED release record with no GREEN one on a later
* commit that contains it holds every cut, like a freeze.
@@ -105,7 +105,7 @@ export function compareVersions(a: string, b: string): number {
export type Verdict = { green: true; via: string } | { green: false; why: string; pending: boolean };
interface CheckRun { name: string; status: string; conclusion: string | null }
/** Main commits a green, merging or merged train record lists in merged[], by sha and by tree, each naming the train; `release` maps a commit a green release gate passed to its run (lib/release-gate.ts). */
/** Main commits a green, merging or merged train record lists in merged[], by sha and by tree, each naming the train; `release` maps a commit a green release gate passed to its run (crew/lib/release-gate.ts). */
export interface TrainEvidence { bySha: Map<string, string>; byTree: Map<string, string>; release?: Map<string, string> }
export interface GateOptions { requiredCheck?: string; backstopCheck?: string; evidence?: TrainEvidence; treeOf?: (sha: string) => string | null }
@@ -148,7 +148,7 @@ interface NoteRecord {
* ship-queue-train-record.ts TrainRecord: kind train, state green, merging or
* merged, merged[].sha and .tree). A fetch failure other than a missing ref is
* an error; a missing ref holds nothing. Release-gate records (kind release,
* lib/release-gate.ts) add evidence when GREEN, and a RED one with no GREEN
* crew/lib/release-gate.ts) add evidence when GREEN, and a RED one with no GREEN
* record on a later commit that contains it is the hold: it refuses the cut
* like a freeze (a train freeze, when both exist, is the one named). A backstop
* freeze clears for the cut once a GREEN release record sits on a commit that
@@ -426,7 +426,7 @@ export interface PlanOptions {
security?: string;
/** --regression <reason>: bypass a dated floor for a verified regression fix; needs ref. */
regression?: string;
/** The floor in hours (default FLOOR_HOURS); the daily check plans at IMPORTANT_FLOOR_HOURS and applies each trigger's own floor (scripts/release-cadence.ts). */
/** The floor in hours (default FLOOR_HOURS); the daily check plans at IMPORTANT_FLOOR_HOURS and applies each trigger's own floor (crew/scripts/release-cadence.ts). */
floorHours?: number;
}
@@ -601,7 +601,7 @@ export function describePlan(p: Plan, dryRun: boolean): string[] {
}
/**
* The daily check (--daily, scripts/release-cadence.ts): plan at the 24-hour important floor, then let
* The daily check (--daily, crew/scripts/release-cadence.ts): plan at the 24-hour important floor, then let
* decideDaily pick the trigger, if any; NO_RELEASE_PR becomes a blocked decision only when a trigger
* is due. decision is null when the plan's refusal stands on its own (a freeze, nothing green,
* FLOOR_UNKNOWN, a non-fast-forward, a version stamped on an unrelated commit); lines then say which
@@ -13,7 +13,7 @@
* manifest, its trial-outcomes artifact, and the three-dot compare of the
* merge-base with the run's head SHA. Downloads are cached under --cache.
*
* bun run scripts/replay-pr-selection.ts --runs <runs.json> [--cache <dir>] [--repo garrytan/gstack] [--json]
* bun run crew/scripts/replay-pr-selection.ts --runs <runs.json> [--cache <dir>] [--repo garrytan/gstack] [--json]
*
* <runs.json> is `gh run list --workflow evals.yml --json databaseId,headSha,event,createdAt,conclusion -L 200` output.
*/
@@ -21,12 +21,12 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { computePaidCaseSelection } from './test-paid-shards';
import { packageChangeOnlyVersion } from './test-pr-profile';
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { matchGlob } from '../test/helpers/test-selection';
import { derivedDependencies } from './pr-dependencies';
import { knownNonBehaviorFile, PR_PROFILE_MAPS } from './test-pr-profile';
import { computePaidCaseSelection } from '../../scripts/test-paid-shards';
import { packageChangeOnlyVersion } from '../../scripts/test-pr-profile';
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from '../../test/helpers/touchfiles-data';
import { matchGlob } from '../../test/helpers/test-selection';
import { derivedDependencies } from '../../scripts/pr-dependencies';
import { knownNonBehaviorFile, PR_PROFILE_MAPS } from '../../scripts/test-pr-profile';
export interface ReplayInput {
runId: number;
@@ -65,7 +65,7 @@ export interface ReplayRow {
*/
export function caseTouchesDiff(id: string, changedFiles: readonly string[]): boolean {
const patterns = [...(E2E_TOUCHFILES[id] ?? []), ...GLOBAL_TOUCHFILES];
const derived = derivedDependencies(PR_PROFILE_MAPS, path.resolve(import.meta.dir, '..'));
const derived = derivedDependencies(PR_PROFILE_MAPS, path.resolve(import.meta.dir, '..', '..'));
// A path absent from today's tree reaches a case through the live files that still reference it.
const via = (file: string) => derived.tracked.has(file) ? [file] : derived.referencers(file);
return changedFiles.some(file => patterns.some(pattern => matchGlob(file, pattern))
@@ -73,7 +73,7 @@ export function caseTouchesDiff(id: string, changedFiles: readonly string[]): bo
}
export function replayRun(input: ReplayInput, oldSelection: readonly string[]): ReplayRow {
const tracked = derivedDependencies(PR_PROFILE_MAPS, path.resolve(import.meta.dir, '..')).tracked;
const tracked = derivedDependencies(PR_PROFILE_MAPS, path.resolve(import.meta.dir, '..', '..')).tracked;
const base = { runId: input.runId, oldMode: input.oldMode, oldCases: input.oldCases, truncated: input.truncated,
changed: input.changedFiles.length, vanished: input.changedFiles.filter(file => !tracked.has(file)).length };
const retiredFailures = input.failedCases.filter(id => !Object.hasOwn(E2E_TIERS, id));
@@ -173,7 +173,7 @@ if (import.meta.main) {
const flag = (name: string) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
const runsFile = flag('runs');
if (args.includes('--help') || !runsFile) {
console.log('usage: bun run scripts/replay-pr-selection.ts --runs <runs.json> [--cache <dir>] [--repo garrytan/gstack] [--json]\n'
console.log('usage: bun run crew/scripts/replay-pr-selection.ts --runs <runs.json> [--cache <dir>] [--repo garrytan/gstack] [--json]\n'
+ 'runs.json: gh run list --workflow evals.yml --json databaseId,headSha,event,createdAt,conclusion -L 200');
process.exit(runsFile || args.includes('--help') ? 0 : 2);
}
@@ -4,8 +4,8 @@
* legacy policy and under docs/review-policy.md#review-depth, on fixed fixture diffs, with an input
* plus output token estimate per run. The proof-of-saving table in the GSTA-131 PR comes from here.
*
* bun scripts/review-dispatch-compare.ts [--fixture <file>]... [--tokens <measured.json>] [--json]
* bun scripts/review-dispatch-compare.ts --capture <sha> --name <name> --out <file>
* bun crew/scripts/review-dispatch-compare.ts [--fixture <file>]... [--tokens <measured.json>] [--json]
* bun crew/scripts/review-dispatch-compare.ts --capture <sha> --name <name> --out <file>
*
* --capture writes a fixture from one commit (its diff against its first parent) through
* gstack-review-plan --json. --tokens takes the live per-dispatch output-token medians by role and
@@ -16,10 +16,10 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runScriptSync } from '../lib/script-invocation';
import { planReview, specialistsFor, type DiffClass, type History, type NumbersResult, type PathLines } from '../lib/review-depth';
import { runScriptSync } from '../../lib/script-invocation';
import { planReview, specialistsFor, type DiffClass, type History, type NumbersResult, type PathLines } from '../../lib/review-depth';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
const BYTES_PER_TOKEN = 4;
const FIXED_AUDITS = ['coverage-audit', 'plan-completion', 'docs-audit'];
const RANGE_FRACTION = 0.15;
@@ -7,7 +7,7 @@
* 2. Debounce: wait until no train is merging (refs/notes/gstack-trains
* holds no train in state `merging`), up to the settle window; a newer
* push to main supersedes this run.
* 3. Compute main's tree modulo stamps (scripts/train-gate.ts stampFreeTree)
* 3. Compute main's tree modulo stamps (crew/scripts/train-gate.ts stampFreeTree)
* and look for a green-train record that lists it among its prefix trees
* T1..Tk: a successful train dispatch of the gate workflow whose record
* artifact is GREEN with a complete census and whose `full_gate_check`
@@ -21,10 +21,10 @@ import { randomBytes } from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { loadPolicyFromHead, type TrainPolicy } from '../lib/ship-policy';
import { loadPolicyFromHead, type TrainPolicy } from '../../lib/ship-policy';
import { NOTES_REF, NOTES_REMOTE_REF, fetchNotes, nowIso, persist, repoIdentity, type FreezeRecord } from '../lib/ship-queue-train-record';
import { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { DISPATCH_INPUTS, makeNonce, nonceScope } from './eval-bg';
import { E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES } from '../../test/helpers/touchfiles-data';
import { DISPATCH_INPUTS, makeNonce, nonceScope } from '../../scripts/eval-bg';
import { ghFailureReason, runGh, type GhRunner } from './recover-receipts';
import { gitIn, stampFreeTree, type Git, type TrainRecord } from './train-gate';
@@ -274,7 +274,7 @@ function freeze(o: BackstopOptions, c: { gh: GhRunner; git: Git; repoKey: string
return { action: pushed.pushed ? 'frozen' : 'failed', lines };
}
const USAGE = `usage: bun run scripts/train-backstop.ts run --repo <owner/name> --branch <main> --sha <sha>
const USAGE = `usage: bun run crew/scripts/train-backstop.ts run --repo <owner/name> --branch <main> --sha <sha>
[--settle-minutes 60] [--poll-seconds 60] [--gate-deadline-minutes 270] [--summary <file>] [--outputs <file>]
Exit 0 when inert, superseded, covered, deferred to a freeze's own backstop, or gated green; 1 when it froze the queue or could not finish.
--outputs appends action=<action> (the workflow reads it as a step output and publishes the main-backstop check run from it).`;
@@ -1,6 +1,6 @@
/**
* The merge-train side of evals.yml (plan GSTA-49 lane T1b;
* docs/ship-policy.md#run-a-merge-train). gstack-ship-queue (lib/ship-queue-train*.ts)
* docs/ship-policy.md#run-a-merge-train). gstack-ship-queue (crew/lib/ship-queue-train*.ts)
* dispatches the runs and reads what they publish; this file is the workflow's half.
*
* route (route job, dispatches) names the run's mode and validates
@@ -24,21 +24,21 @@ import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { normalizeChangelog } from '../lib/changelog-check';
import { normalizeLockfile } from '../lib/evidence-bundle';
import { stampMentions, rewriteStamps, versionAtRef } from '../lib/restamp';
import { loadPolicyFromHead, matchesAny, type ShipPolicy } from '../lib/ship-policy';
import { normalizeChangelog } from '../../lib/changelog-check';
import { normalizeLockfile } from '../../lib/evidence-bundle';
import { stampMentions, rewriteStamps, versionAtRef } from '../../lib/restamp';
import { loadPolicyFromHead, matchesAny, type ShipPolicy } from '../../lib/ship-policy';
import type { BisectPrefixRecord, TrainGateRecord } from '../lib/ship-queue-train-verdict';
import type { CasePanel } from '../lib/ship-queue-train-record';
import { E2E_TIERS, LLM_JUDGE_TOUCHFILES } from '../test/helpers/touchfiles-data';
import { panelVerdict } from '../test/helpers/eval-store';
import { UNRECORDED_SHARD_MS, nonceScope, plannedSerialMs } from './eval-bg';
import { caseTrialPlan, fileCaseRegistration, shardCaseId, shardFile } from './lib/paid-cases';
import { buildRunManifest, loadPaidTestDurations, parseRunManifest, recordedShardMs, type PaidRunManifest } from './lib/paid-plan';
import { loadSliceArtifacts } from './lib/paid-report';
import type { FlakeRecord } from './lib/paid-rerun';
import { paidSelectionEnv } from './lib/paid-select';
import { ROOT } from './lib/paid-types';
import { E2E_TIERS, LLM_JUDGE_TOUCHFILES } from '../../test/helpers/touchfiles-data';
import { panelVerdict } from '../../test/helpers/eval-store';
import { UNRECORDED_SHARD_MS, nonceScope, plannedSerialMs } from '../../scripts/eval-bg';
import { caseTrialPlan, fileCaseRegistration, shardCaseId, shardFile } from '../../scripts/lib/paid-cases';
import { buildRunManifest, loadPaidTestDurations, parseRunManifest, recordedShardMs, type PaidRunManifest } from '../../scripts/lib/paid-plan';
import { loadSliceArtifacts } from '../../scripts/lib/paid-report';
import type { FlakeRecord } from '../../scripts/lib/paid-rerun';
import { paidSelectionEnv } from '../../scripts/lib/paid-select';
import { ROOT } from '../../scripts/lib/paid-types';
const SHA = /^[0-9a-f]{40}$/;
const CASE_ID = /^[A-Za-z0-9 _.:/-]{1,128}$/;
@@ -46,9 +46,9 @@ export const JUDGE_FILE = 'test/skill-llm-eval.test.ts';
/** T1 caps a train at 6 PRs, so a bisect has at most 7 prefixes (T0..T6). */
export const MAX_PREFIXES = 7;
export const MAX_BISECT_CASES = 40;
/** A full gate's recorded cost (MEASUREMENT.md: $90.93 to $94.70; lib/gate-cost.ts uses the same scale). */
/** A full gate's recorded cost (MEASUREMENT.md: $90.93 to $94.70; crew/lib/gate-cost.ts uses the same scale). */
export const FULL_GATE_USD = 93.14;
/** T1's prefix commits: `train T<i>: merge #<pr> at <sha12>` (lib/ship-queue-train.ts buildChain). */
/** T1's prefix commits: `train T<i>: merge #<pr> at <sha12>` (crew/lib/ship-queue-train.ts buildChain). */
const PREFIX_SUBJECT = /^train (?:T(\d+): merge #(\d+) at [0-9a-f]{12}|S(\d+): merge #(\d+) at [0-9a-f]{12} v[0-9.]+)$/;
export type Git = (args: string[]) => { ok: boolean; out: string };
@@ -66,7 +66,7 @@ export interface RouteInput {
}
/** artifact: the prefix of a release bisect's per-prefix records (a train bisect uses the train block's record_artifact). */
export interface Route { mode: RunMode; cases: string[]; prefixes: string[]; artifact?: string }
/** The release gate's bisect runs on its own ref and names its own prefix records (lib/release-gate.ts RELEASE_REF_PREFIX, RELEASE_BISECT_ARTIFACT). */
/** The release gate's bisect runs on its own ref and names its own prefix records (crew/lib/release-gate.ts RELEASE_REF_PREFIX, RELEASE_BISECT_ARTIFACT). */
export const RELEASE_BISECT_REF = 'release-gate/';
export const RELEASE_BISECT_ARTIFACT = 'gstack-release-record';
@@ -320,9 +320,9 @@ export async function runBisectPrefix(plan: BisectPlan, index: number, o: { root
/** One trial of one case exactly as CI runs it: E2E cases through the panel runner, a judge through its file with only it selected. */
async function runOneTrial(id: string, kind: string, evalDir: string, rootDir: string): Promise<{ status: string; redClass: string | null }> {
const { runPaidShards } = await import('./test-paid-shards');
const { runPaidShards } = await import('../../scripts/test-paid-shards');
if (kind !== 'judge') {
const { runCaseDiagnosis } = await import('./lib/paid-report');
const { runCaseDiagnosis } = await import('../../scripts/lib/paid-report');
const v = await runCaseDiagnosis(id, { trials: 1, jobs: 1, rootDir, evalDirBase: evalDir, runShards: runPaidShards });
return { status: v.status, redClass: v.redClass };
}
@@ -351,11 +351,11 @@ export function screenSummary(manifest: PaidRunManifest, pr: string, rootDir = R
}
const USAGE = `usage:
bun run scripts/train-gate.ts route --github-output <file> (inputs from the route job's env)
bun run scripts/train-gate.ts bisect-plan --cases <ids> --prefixes <shas> --nonce <nonce> --out <bisect.json> [--github-output <file>]
bun run scripts/train-gate.ts bisect-prefix --plan <bisect.json> --index <n> --out <dir>
bun run scripts/train-gate.ts train-record <report dir> --nonce <nonce> --head <sha> --reconcile-exit <n> --run-id <id> --run-attempt <n> --repo <owner/name> --out <file>
bun run scripts/train-gate.ts screen-summary <manifest.json> --pr <n>`;
bun run crew/scripts/train-gate.ts route --github-output <file> (inputs from the route job's env)
bun run crew/scripts/train-gate.ts bisect-plan --cases <ids> --prefixes <shas> --nonce <nonce> --out <bisect.json> [--github-output <file>]
bun run crew/scripts/train-gate.ts bisect-prefix --plan <bisect.json> --index <n> --out <dir>
bun run crew/scripts/train-gate.ts train-record <report dir> --nonce <nonce> --head <sha> --reconcile-exit <n> --run-id <id> --run-attempt <n> --repo <owner/name> --out <file>
bun run crew/scripts/train-gate.ts screen-summary <manifest.json> --pr <n>`;
if (import.meta.main) {
const [command, ...rest] = process.argv.slice(2);
File renamed without changes.
File renamed without changes.
@@ -13,9 +13,9 @@ import {
type GateBinding, type GateReuse,
} from '../scripts/gate-reuse';
import type { GhRunner, WorkflowRun } from '../scripts/recover-receipts';
import type { PaidRunManifest } from '../scripts/lib/paid-plan';
import type { PaidRunManifest } from '../../scripts/lib/paid-plan';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
const REPO = 'garrytan/gstack';
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'gate-reuse-'));
afterAll(() => fs.rmSync(scratch, { recursive: true, force: true }));
@@ -439,7 +439,7 @@ describe('bind and report', () => {
test('the CLI binds a missing reconcile exit as -1, never as a pass', () => {
const out = path.join(scratch, 'bind-cli');
const bind = (head: string, testedSha: string, dir: string) => spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/gate-reuse.ts'), 'bind',
const bind = (head: string, testedSha: string, dir: string) => spawnSync(process.execPath, ['run', path.join(ROOT, 'crew/scripts/gate-reuse.ts'), 'bind',
reportDir('bind-cli-report'), '--out', dir, '--repo', REPO, '--pr', '42', '--run-id', '300', '--run-attempt', '1', '--head-sha', head,
'--tested-sha', testedSha, '--profile', 'pr', '--reconcile-exit', ''], { cwd: receipt, encoding: 'utf8', timeout: 30_000 });
const r = bind(sha.gated!, tested(sha.moved!, sha.gated!), out);
@@ -464,7 +464,7 @@ describe('bind and report', () => {
expect(movedText).toContain('OWN DIFF: identical sha256=' + moved.own_diff_sha256);
expect(movedText).toContain('why: main moved');
expect(movedText).toContain('at ' + sha.gated);
const bad = spawnSync(process.execPath, ['run', path.join(ROOT, 'scripts/gate-reuse.ts'), 'report', path.join(scratch, 'missing.json'), '--repo', REPO],
const bad = spawnSync(process.execPath, ['run', path.join(ROOT, 'crew/scripts/gate-reuse.ts'), 'report', path.join(scratch, 'missing.json'), '--repo', REPO],
{ encoding: 'utf8', timeout: 30_000 });
expect(bad.status).toBe(1);
});
@@ -10,8 +10,8 @@ import {
acceptedScreenRun, collectRecovery, ghFailureReason, listGateRuns, readCheckpoint, recoveryDecision, screenRunPr, storeRunId,
type GhRunner, type WorkflowRun,
} from '../scripts/recover-receipts';
import { selectPlanReceipts, writeNegativeReceipt, writePanelReceipt } from '../scripts/e2e-shard-reuse';
import { buildRunManifest, type SliceResult } from '../scripts/test-paid-shards';
import { selectPlanReceipts, writeNegativeReceipt, writePanelReceipt } from '../../scripts/e2e-shard-reuse';
import { buildRunManifest, type SliceResult } from '../../scripts/test-paid-shards';
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'recover-receipts-'));
const REPO = 'garrytan/gstack';
@@ -8,7 +8,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { bunIsolationArgs } from '../../lib/bun-isolation';
import { FLOOR_HOURS, FORCED_AFTER_HOURS, IMPORTANT_FLOOR_HOURS, SCHEDULED_GRACE_HOURS, decideDaily, importantChanges, type DailyInput, type ImportantChange } from '../scripts/release-cadence';
import { gitIn, planDaily, type Gh } from '../scripts/release-cut';
@@ -74,7 +74,7 @@ describe('decideDaily', () => {
// Value: protects=the release-PR dependency: a due important or forced cut whose VERSION is already released reports "important change waiting, no release
// PR" instead of cutting or failing silently; fails_when=the cut proceeds, the report loses its words or the trigger, or a cut that is not due reports it;
// why_new=no daily path existed; seam=pure; no_claim=that the Captain reads it (docs/captain.md says when to stamp)
// why_new=no daily path existed; seam=pure; no_claim=that the Captain reads it (crew/docs/captain.md says when to stamp)
test('a due cut with no release PR is blocked and says which wait it is; an undue one is not', () => {
const blocked = decideDaily(input({ important: [SEC], releasePr: false }));
expect(blocked.action).toBe('blocked');
@@ -248,7 +248,7 @@ describe.skipIf(process.platform === 'win32')('release-cut --daily (the workflow
fs.writeFileSync(path.join(shim, 'gh'), '#!/usr/bin/env bash\ncase "$*" in\n *check-runs*) echo \'' + JSON.stringify([{ check_runs: GREEN }]) + '\' ;;\n "release view v1.0.0 "*) echo \'{}\' ;;\n *) exit 1 ;;\nesac\n', { mode: 0o755 });
const env: Record<string, string | undefined> = { ...process.env, PATH: shim + path.delimiter + process.env.PATH, GITHUB_ACTIONS: 'true' };
for (const k of ['GITHUB_EVENT_NAME', 'GITHUB_STEP_SUMMARY']) delete env[k];
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..'), 'scripts', 'release-cut.ts'), '--repo', 'o/r', ...args],
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..', '..'), 'crew', 'scripts', 'release-cut.ts'), '--repo', 'o/r', ...args],
{ cwd: r.work, encoding: 'utf8', timeout: 60_000, env });
r.commit('docs (#4100)', { 'changelog.d/docs.md': '- docs\n' });
@@ -288,7 +288,7 @@ describe.skipIf(process.platform === 'win32')('release-cut --daily (the workflow
const fresh = repo(1);
fresh.commit('fix the tar path (#4101)', { 'changelog.d/fix.md': '---\nimportant: security\n---\n- tar\n' });
fresh.refresh();
const wait = spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..'), 'scripts', 'release-cut.ts'), '--repo', 'o/r', '--daily'], { cwd: fresh.work, encoding: 'utf8', timeout: 60_000, env });
const wait = spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..', '..'), 'crew', 'scripts', 'release-cut.ts'), '--repo', 'o/r', '--daily'], { cwd: fresh.work, encoding: 'utf8', timeout: 60_000, env });
expect(wait.status, wait.stdout + wait.stderr).toBe(0);
expect(wait.stdout.split('\n')[0]).toStartWith('DAILY: wait: important: security in #4101');
expect(wait.stdout.split('\n')[1]).toStartWith('NEXT_ALLOWED: ');
@@ -10,8 +10,8 @@ import { afterAll, afterEach, describe, expect, test } from 'bun:test';
import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { gitFixtureTemplate, rebindGitDir } from './helpers/git-fixture-template';
import { bunIsolationArgs } from '../../lib/bun-isolation';
import { gitFixtureTemplate, rebindGitDir } from '../../test/helpers/git-fixture-template';
import {
BACKSTOP_CHECK, FLOOR_HOURS, NOTES_CAP, NOTES_REF, REQUIRED_CHECK, SCHEDULED_GRACE_HOURS, changelogEntries, compareVersions, cutRelease, describePlan, floorStatus,
gateVerdict, gitIn, planRelease, readTrainNotes, releaseNotes, sanitizeReason, stableMovedAt, verifyRegression, verifySecurity, type Gh, type Git,
@@ -262,7 +262,7 @@ describe('release-cut: planning', () => {
});
});
describe('release-cut: the release gate holds and gates (lib/release-gate.ts records)', () => {
describe('release-cut: the release gate holds and gates (crew/lib/release-gate.ts records)', () => {
const release = (sha: string, verdict: string, run: number) => ({ schema: 1, kind: 'release', repo_key: 'k', sha, verdict, run: { id: run, url: 'u' }, red_cases: verdict === 'RED' ? ['case-a'] : [], created_at: 'x', updated_at: 'x' });
const note = (f: ReturnType<typeof fixture>, record: unknown, on: string) => {
git(f.seed, 'notes', '--ref=' + NOTES_REF, 'add', '-f', '-m', JSON.stringify(record), on);
@@ -319,7 +319,7 @@ describe('release-cut: train evidence gates a commit whose backstop never settle
// merged[] names its sha or its tree, and the CANDIDATE line says which train gated it; fails_when=the gate still needs a successful backstop run on
// every commit (the weekly cut returns NO_GREEN_COMMIT for a week of train-merged commits), evidence is matched on sha only, or the plan hides what gated
// the commit; why_new=the planning tests know only the backstop check run as evidence; seam=none; no_claim=real refs/notes/gstack-trains content (a
// fixture record shaped like lib/ship-queue-train-record.ts TrainRecord)
// fixture record shaped like crew/lib/ship-queue-train-record.ts TrainRecord)
test('a neutral backstop plus a train record naming the sha, or only the tree, is a candidate gated by that train', () => {
const f = fixture();
const head = f.shas['1.0.3']!;
@@ -421,7 +421,7 @@ describe('release-cut: train evidence gates a commit whose backstop never settle
const shim = path.join(f.base, 'shim');
fs.mkdirSync(shim);
fs.writeFileSync(path.join(shim, 'gh'), '#!/usr/bin/env bash\ncase "$*" in\n *check-runs*) echo \'' + JSON.stringify([{ check_runs: f.state.checks[head] }]) + '\' ;;\n *) exit 1 ;;\nesac\n', { mode: 0o755 });
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..'), 'scripts', 'release-cut.ts'), ...args],
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..', '..'), 'crew', 'scripts', 'release-cut.ts'), ...args],
{ cwd: f.work, encoding: 'utf8', timeout: 60_000, env: cliEnv(shim) });
const cli = run('--repo', f.repo, '--dry-run', '--ref', head, '--required-check', 'ci', '--backstop-check', 'nightly-backstop', '--notes-ref', 'refs/notes/other-trains');
expect(cli.status, cli.stdout + cli.stderr).toBe(0);
@@ -584,7 +584,7 @@ describe.skipIf(process.platform === 'win32')('release-cut: the workflow entry p
const shim = path.join(f.base, 'shim');
fs.mkdirSync(shim);
fs.writeFileSync(path.join(shim, 'gh'), '#!/usr/bin/env bash\necho "gh $*" >> "' + path.join(f.base, 'gh.log') + '"\ncase "$*" in\n *check-runs*) echo \'' + JSON.stringify([{ check_runs: GREEN }]) + '\' ;;\n "release view v1.0.3 "*) echo \'{\"tagName\":\"v1.0.3\"}\' ;;\n *) exit 1 ;;\nesac\n', { mode: 0o755 });
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..'), 'scripts', 'release-cut.ts'), ...args],
const run = (...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..', '..'), 'crew', 'scripts', 'release-cut.ts'), ...args],
{ cwd: f.work, encoding: 'utf8', timeout: 60_000, env: cliEnv(shim) });
const summary = path.join(f.base, 'summary.md');
@@ -1080,7 +1080,7 @@ describe.skipIf(process.platform === 'win32')('release-cut: refusals at the work
const shim = path.join(f.base, 'shim');
fs.mkdirSync(shim);
fs.writeFileSync(path.join(shim, 'gh'), '#!/usr/bin/env bash\ncase "$*" in\n *check-runs*) echo \'' + JSON.stringify([{ check_runs: GREEN }]) + '\' ;;\n "release view v1.0.3 "*) echo \'{"publishedAt":"' + isoZ(at) + '"}\' ;;\n *) echo "release not found" >&2; exit 1 ;;\nesac\n', { mode: 0o755 });
const run = (env: Record<string, string>, ...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..'), 'scripts', 'release-cut.ts'), '--repo', f.repo, ...args],
const run = (env: Record<string, string>, ...args: string[]) => spawnSync(process.execPath, [...bunIsolationArgs(), 'run', path.join(path.resolve(import.meta.dir, '..', '..'), 'crew', 'scripts', 'release-cut.ts'), '--repo', f.repo, ...args],
{ cwd: f.work, encoding: 'utf8', timeout: 60_000, env: cliEnv(shim, env) });
const stable = () => git(f.origin, 'rev-parse', 'refs/heads/stable');
@@ -1,6 +1,6 @@
/**
* The release gate's workflow wiring (docs/ship-policy.md#release-gate):
* release-gate.yml runs bin/gstack-release-gate on its schedule and publishes
* release-gate.yml runs crew/bin/gstack-release-gate on its schedule and publishes
* the gated commit's check run from a code-free job; the per-push backstop
* stands down once main's policy declares a release_gate block; evals.yml routes
* a release bisect only under that block. Policy steps run for real (bash, git,
@@ -11,10 +11,10 @@ import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { RELEASE_GATE_SCHEDULE_HOURS } from '../lib/release-gate-policy';
import { RELEASE_GATE_SCHEDULE_HOURS } from '../../lib/release-gate-policy';
import { RELEASE_BISECT_ARTIFACT } from '../scripts/train-gate';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
type Step = { id?: string; name?: string; uses?: string; if?: string; run?: string; env?: Record<string, string> };
type Job = { if?: string; needs?: string; outputs?: Record<string, string>; permissions?: Record<string, string>; steps: Step[] };
type Workflow = { on: Record<string, any>; concurrency: unknown; permissions?: unknown; jobs: Record<string, Job> };
@@ -81,7 +81,7 @@ describe('release-gate.yml', () => {
expect(runStep(policy, { release_gate: GATE })).toEqual({ release: 'true' });
const gate = job.steps.find(s => s.id === 'gate')!;
expect(gate.if).toBe("steps.policy.outputs.release == 'true'");
expect(gate.run).toContain('./bin/gstack-release-gate run --repo "$GITHUB_REPOSITORY" --budget-minutes');
expect(gate.run).toContain('./crew/bin/gstack-release-gate run --repo "$GITHUB_REPOSITORY" --budget-minutes');
expect(gate.run).not.toContain('--sha');
expect(gate.run).toContain("|| rc=$?");
expect(gate.run).toContain(`if [ "$rc" != 0 ] && ! { grep -qE '^action=(held|failed)$' "$GITHUB_OUTPUT" && grep -qE '^sha=[0-9a-f]{40}$' "$GITHUB_OUTPUT"; }; then exit "$rc"; fi`);
@@ -92,7 +92,7 @@ describe('release-gate.yml', () => {
// Value: protects=the gate step's exit: a held or failed verdict on a gated commit leaves the job green (release-check publishes its check run), but a
// failed verdict with no commit (a refused policy, unreadable notes, an unresolved branch) or no recorded action fails the job, since no check run
// follows it; fails_when=a sha-less failure leaves the job green while the backstop stands down (GST-1263); why_new=reader note on #3917;
// seam=a stub bin in place of bin/gstack-release-gate; no_claim=a live scheduled run
// seam=a stub bin in place of crew/bin/gstack-release-gate; no_claim=a live scheduled run
test('the gate step fails the job when the bin fails without a gated commit', () => {
const gate = release.jobs['release-gate']!.steps.find(s => s.id === 'gate')!;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-gate-step-'));
@@ -103,7 +103,7 @@ describe('release-gate.yml', () => {
fs.writeFileSync(stub, `#!/usr/bin/env bash\nprintf '%s' '${written}' >> "$GITHUB_OUTPUT"\nexit ${code}\n`, { mode: 0o755 });
const out = path.join(dir, 'out');
fs.writeFileSync(out, '');
const r = spawnSync('bash', ['-e', '-c', gate.run!.replace('./bin/gstack-release-gate', stub)], {
const r = spawnSync('bash', ['-e', '-c', gate.run!.replace('./crew/bin/gstack-release-gate', stub)], {
cwd: dir, encoding: 'utf8', timeout: 30_000,
env: { ...process.env, GITHUB_OUTPUT: out, GITHUB_STEP_SUMMARY: path.join(dir, 'summary'), GITHUB_REPOSITORY: 'o/r', DRY_RUN: '', IGNORE_CADENCE: '' },
});
@@ -1,5 +1,5 @@
/**
* gstack-release-gate (lib/release-gate.ts, docs/ship-policy.md#release-gate):
* gstack-release-gate (crew/lib/release-gate.ts, docs/ship-policy.md#release-gate):
* the full paid gate on main's head at most once per cadence and only when main
* moved, a release record in refs/notes/gstack-trains, a bisect of a red gate's
* cases over the commits since the last green gate, and the hold release-cut
@@ -11,17 +11,17 @@ import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { bunIsolationArgs } from '../../lib/bun-isolation';
import {
MAX_RELEASE_ATTEMPTS, OPEN_GATE_CADENCES, RELEASE_TRIAL_RESERVATION_USD, activeHold, bisectGate, classifyRun, gateStatus, loadReleaseGate, narrow, parseReport, planPrefixes, releaseRecords, runGate,
type GateEnv, type Run,
} from '../lib/release-gate';
import { RELEASE_GATE_SCHEDULE_HOURS, releaseGateErrors, type ReleaseGatePolicy } from '../lib/release-gate-policy';
import { RELEASE_GATE_KEY_VERSION, validatePolicy } from '../lib/ship-policy';
import { RELEASE_GATE_SCHEDULE_HOURS, releaseGateErrors, type ReleaseGatePolicy } from '../../lib/release-gate-policy';
import { RELEASE_GATE_KEY_VERSION, validatePolicy } from '../../lib/ship-policy';
import { NOTES_REF, repoIdentity, type PrefixResult, type ReleaseRecord } from '../lib/ship-queue-train-record';
import { gitIn } from '../scripts/train-gate';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
const GATE: ReleaseGatePolicy = { cadence_hours: 8, gate_workflow: 'evals.yml', branch: 'main', stable_ref: 'stable', check_name: 'main-backstop', on_red: 'hold-release', bisect_spend_cap_usd: 40 };
const roots: string[] = [];
const savedStateRoot = process.env.GSTACK_STATE_ROOT;
@@ -319,7 +319,7 @@ describe('release gate: red, bisect and the hold', () => {
});
describe('release gate: the bisect cap with unknown costs', () => {
// Value: protects=a bisect whose prefix records report cost_usd null (what scripts/train-gate.ts writes today) books
// Value: protects=a bisect whose prefix records report cost_usd null (what crew/scripts/train-gate.ts writes today) books
// RELEASE_TRIAL_RESERVATION_USD per case and prefix, so a later round sees the spend and the bisect stops capped within its cap; fails_when=null
// costs count as $0 and every round reserves the whole cap again (about 4x the cap over MAX_BISECT_ROUNDS); why_new=review finding (targeted
// money, l1 fix-check); seam=fake gh; no_claim=real case prices
@@ -344,7 +344,7 @@ describe('release gate: the bisect cap with unknown costs', () => {
expect(fake.dispatches).toHaveLength(2);
expect(fake.dispatches[1]!.inputs.prefix_shas!.split(',')).toHaveLength(1);
});
// Value: protects=a red prefix runs its case twice (one trial plus its one rule-16 rerun in scripts/train-gate.ts runBisectPrefix),
// Value: protects=a red prefix runs its case twice (one trial plus its one rule-16 rerun in crew/scripts/train-gate.ts runBisectPrefix),
// so each prefix reserves both trials and books the trials it ran, and the bisect ends capped at or under its dollar cap;
// fails_when=a round reserves one trial a prefix, dispatches more prefixes than the cap pays for once they rerun, and books
// half of what ran; why_new=GSTA-36 condition before adoption (l1 fix-check open note); seam=fake gh; no_claim=real case prices
@@ -481,8 +481,8 @@ describe('release gate: verdict, range and policy units', () => {
expect((loadReleaseGate(ROOT, { policyFile: bad }) as { error: string }).error).toContain('release_gate introduced in gstack ' + RELEASE_GATE_KEY_VERSION);
});
test('bin/gstack-release-gate: usage exits 2; an inert policy file exits 0 without touching GitHub', () => {
const bin = path.join(ROOT, 'bin/gstack-release-gate');
test('crew/bin/gstack-release-gate: usage exits 2; an inert policy file exits 0 without touching GitHub', () => {
const bin = path.join(ROOT, 'crew/bin/gstack-release-gate');
const usage = spawnSync('bun', [...bunIsolationArgs(), bin, 'frobnicate'], { cwd: ROOT, encoding: 'utf8', timeout: 60_000 });
expect(usage.status).toBe(2);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-gate-bin-'));
File renamed without changes.
@@ -2,10 +2,10 @@ import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runScriptSync } from '../lib/script-invocation';
import { runScriptSync } from '../../lib/script-invocation';
const ROOT = path.join(import.meta.dir, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'review-dispatch-compare.ts');
const ROOT = path.join(import.meta.dir, '..', '..');
const SCRIPT = path.join(ROOT, 'crew', 'scripts', 'review-dispatch-compare.ts');
const FIXTURES = ['test/fixtures/review-depth/docs-only.json', 'test/fixtures/review-depth/code.json'].map(f => path.join(ROOT, f));
const run = (args: string[]) => {
const r = runScriptSync(SCRIPT, [...FIXTURES.flatMap(f => ['--fixture', f]), ...args], { cwd: ROOT, encoding: 'utf8', timeout: 120_000 });
@@ -13,7 +13,7 @@ const run = (args: string[]) => {
return r.stdout as string;
};
describe('scripts/review-dispatch-compare.ts', () => {
describe('crew/scripts/review-dispatch-compare.ts', () => {
test('the new policy never dispatches more reviewers than the legacy one, and a docs diff costs under a third', () => {
const out = JSON.parse(run(['--json']));
expect(out.tokens_source).toBe('DEFAULT');
@@ -1,5 +1,5 @@
/**
* The shared land lock (lib/ship-queue-land-lock.ts, #3599): `land` and `train land` take the same
* The shared land lock (crew/lib/ship-queue-land-lock.ts, #3599): `land` and `train land` take the same
* refs/notes/gstack-land lock, so a standalone land and a train slot can never both publish onto one main.
* Module cases run against a bare origin and two clones (the race, the read states, owner kinds, the
* --break-lock rules); command cases drive bin/gstack-ship-queue against the train fixture and its gh
@@ -9,14 +9,14 @@
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { EXIT } from '../../lib/headless-artifacts';
import { acquireLandLock, LAND_LOCK_REF, readLock, writeLock, type LandLock, type LockOwner } from '../lib/ship-queue-land-lock';
import { BASE_POLICY, cleanup, makeFixtureRepo, type FixtureRepo } from './helpers/restamp-fixture';
import { flakyGit } from './helpers/flaky-git';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import { BASE_POLICY, cleanup, makeFixtureRepo, type FixtureRepo } from '../../test/helpers/restamp-fixture';
import { flakyGit } from '../../test/helpers/flaky-git';
import { coordinatorClone } from '../../test/helpers/ship-queue-fixture';
import {
GREEN_GATE, TRAIN_POLICY, fragmentPr, nonceOf, queue, repoKey, trainFixture, trainJson, wrapLand, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const repos: FixtureRepo[] = [];
const fixtures: TrainFixture[] = [];
@@ -11,11 +11,11 @@ import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { screenVerdict, verdictLines } from '../lib/ship-queue-screen';
import type { QueueContext } from '../lib/ship-queue';
import type { QueueContext } from '../../lib/ship-queue';
const RUN = 41662815506;
const SLUG = 'garrytan/gstack';
const FIXTURE = path.join(import.meta.dir, 'fixtures', 'screen-verdict-41662815506');
const FIXTURE = path.join(import.meta.dir, '..', '..', 'test', 'fixtures', 'screen-verdict-41662815506');
const realBinding = () => JSON.parse(fs.readFileSync(path.join(FIXTURE, 'gate-binding.json'), 'utf8'));
const realOutcomes = () => JSON.parse(fs.readFileSync(path.join(FIXTURE, 'collector-outcomes.json'), 'utf8'));
const TWICE = ['browse-basic', 'browse-snapshot', 'diagram-triplet', 'qa-quick', 'skillmd-no-local-binary', 'skillmd-outside-git', 'skillmd-setup-discovery'];
@@ -1,14 +1,14 @@
// Split from test/ship-queue-train.test.ts (GSTA-151): dispatch recovery, train status, and the verdict, bisect and rebuild cases.
// Split from crew/test/ship-queue-train.test.ts (GSTA-151): dispatch recovery, train status, and the verdict, bisect and rebuild cases.
import { afterEach, describe, expect, test } from 'bun:test';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup } from './helpers/restamp-fixture';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import { twoPrTrain } from './helpers/ship-queue-train-cases';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import { coordinatorClone } from '../../test/helpers/ship-queue-fixture';
import { twoPrTrain } from '../../test/helpers/ship-queue-train-cases';
import {
COMPLETE, FAIL, GREEN_GATE, PASS, RED_GATE, TRAIN_POLICY, dispatches, line, queue, trainFixture, trainJson,
type Rule, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = (policy: Record<string, unknown> = TRAIN_POLICY) => { const f = trainFixture(policy); fixtures.push(f); return f; };
@@ -6,9 +6,9 @@
* named by display label had no case attributed (scripts/lib/paid-report.ts).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup } from './helpers/restamp-fixture';
import { GREEN_GATE, PASS, TRAIN_POLICY, line, nonceOf, openPr, queue, readyReceipt, trainFixture, type Rule, type TrainFixture } from './helpers/ship-queue-train-fixture';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import { GREEN_GATE, PASS, TRAIN_POLICY, line, nonceOf, openPr, queue, readyReceipt, trainFixture, type Rule, type TrainFixture } from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = () => { const f = trainFixture(TRAIN_POLICY); fixtures.push(f); return f; };
@@ -1,15 +1,15 @@
// Split from test/ship-queue-train.test.ts (GSTA-151): verify freezes, GitHub merge-state reads and stale-train refusals.
// Split from crew/test/ship-queue-train.test.ts (GSTA-151): verify freezes, GitHub merge-state reads and stale-train refusals.
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup } from './helpers/restamp-fixture';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import { laneStamp, markMerged, publishMain, twoPrTrain } from './helpers/ship-queue-train-cases';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import { coordinatorClone } from '../../test/helpers/ship-queue-fixture';
import { laneStamp, markMerged, publishMain, twoPrTrain } from '../../test/helpers/ship-queue-train-cases';
import {
TRAIN_POLICY, dispatches, line, nonceOf, openPr, queue, readyReceipt, repoKey, trainFixture, trainJson,
type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = (policy: Record<string, unknown> = TRAIN_POLICY) => { const f = trainFixture(policy); fixtures.push(f); return f; };
@@ -11,14 +11,14 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { TRAIN_GATES_KEY_VERSION, validatePolicy } from '../lib/ship-policy';
import { EXIT } from '../../lib/headless-artifacts';
import { TRAIN_GATES_KEY_VERSION, validatePolicy } from '../../lib/ship-policy';
import { baseRedAdmission, classifyPrefixes, classifyWorkflowRun, commandReservation, concurrencyKeyed, gatedChain, parseTestRecord } from '../lib/ship-queue-train-gate';
import { pushMatchesPrefix, workflowFacts } from '../lib/ship-queue-train-init';
import type { TrainSlot } from '../lib/ship-queue-train-record';
import { BASE_POLICY, cleanup } from './helpers/restamp-fixture';
import { line, nonceOf, openPr, queue, readyReceipt, repoKey, shippedBody, trainFixture, trainJson, TRAIN_POLICY, type TrainFixture } from './helpers/ship-queue-train-fixture';
import { runBin } from './helpers/restamp-fixture';
import { BASE_POLICY, cleanup } from '../../test/helpers/restamp-fixture';
import { line, nonceOf, openPr, queue, readyReceipt, repoKey, shippedBody, trainFixture, trainJson, TRAIN_POLICY, type TrainFixture } from '../../test/helpers/ship-queue-train-fixture';
import { runBin } from '../../test/helpers/restamp-fixture';
const WORKFLOWS_GATE = { kind: 'workflows', workflows: ['ci.yml'], required_jobs: ['ci.yml/test', 'ci.yml/lint'] };
const GENERIC = { gates: [WORKFLOWS_GATE], land: { merge_method: 'squash', required_checks: ['test'] }, max_size: 5, fill_hours: 3 };
@@ -100,7 +100,7 @@ const P = (outcome: string, red: string[] = []) => ({ outcome: outcome as never,
describe('generic train policy', () => {
const errs = (raw: Record<string, unknown>) => validatePolicy(raw, { gstackVersion: TRAIN_GATES_KEY_VERSION }).map(e => e.path + ': ' + e.message);
test('gstack\'s own policy file parses unchanged as the gstack-evals kind', () => {
const own = JSON.parse(fs.readFileSync(path.join(import.meta.dir, '..', '.gstack', 'ship-policy.json'), 'utf8'));
const own = JSON.parse(fs.readFileSync(path.join(import.meta.dir, '..', '..', '.gstack', 'ship-policy.json'), 'utf8'));
expect(validatePolicy(own)).toEqual([]);
expect(own.train.gate_workflow).toBe('evals.yml');
expect(own.train.gates).toBeUndefined();
@@ -7,11 +7,11 @@
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup } from './helpers/restamp-fixture';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import {
GREEN_GATE, RED_GATE, TRAIN_POLICY, fragmentPr, line, nonceOf, openPr, queue, readyReceipt, shippedBody, trainFixture, trainJson, wrapLand as wrap, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const POLICY = { ...TRAIN_POLICY, release_tool: null, release_outputs: [], changelog_fragments: { dir: 'changelog.d' }, min_gstack_version: '1.91.336.0' };
const fixtures: TrainFixture[] = [];
@@ -6,20 +6,20 @@
* key with purpose and screen policy.
*/
import { describe, expect, test } from 'bun:test';
import { DISPATCH_INPUTS, dedupeKey, dispatchPlan, findDuplicateRun, makeNonce, nonceScope, type RunSummary } from '../scripts/eval-bg';
import { DEFAULT_POLICY, TRAIN_KEYS_VERSION, validatePolicy } from '../lib/ship-policy';
import { DISPATCH_INPUTS, dedupeKey, dispatchPlan, findDuplicateRun, makeNonce, nonceScope, type RunSummary } from '../../scripts/eval-bg';
import { DEFAULT_POLICY, TRAIN_KEYS_VERSION, validatePolicy } from '../../lib/ship-policy';
import { fillDue, orderSlots, parseOrder, parsePins, prsArgs } from '../lib/ship-queue-train';
import { gstackPaidSelection, skillOwnCases, touchedSkills } from '../lib/ship-queue-train-ready';
import { normalizeRemoteUrl, repoIdentity, type TrainSlot } from '../lib/ship-queue-train-record';
import { INFRA_REDISPATCH, REBUILD_ONCE, classifyBisect, classifyGate } from '../lib/ship-queue-train-verdict';
import { EVAL_POLICY } from './helpers/periodic-exclude-data';
import { COMPLETE, FAIL, PASS, TRAIN_BLOCK } from './helpers/ship-queue-train-fixture';
import { EVAL_POLICY } from '../../test/helpers/periodic-exclude-data';
import { COMPLETE, FAIL, PASS, TRAIN_BLOCK } from '../../test/helpers/ship-queue-train-fixture';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const ROOT = path.resolve(import.meta.dir, '..', '..');
const SHA = 'a'.repeat(40);
const slot = (pr: string, screened: boolean, at: string): TrainSlot => ({ pr, head: SHA, ready_at: at, screen: { choice: screened ? 'screen' : 'no-screen', policy: null, nonce: null, run: null, merge_sha: null } });
@@ -9,11 +9,11 @@
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup, runBin } from './helpers/restamp-fixture';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup, runBin } from '../../test/helpers/restamp-fixture';
import {
GREEN_GATE, TRAIN_POLICY, dispatches, line, nonceOf, openPr, queue, readyReceipt, repoKey, trainJson, trainFixture, type Rule, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = (policy: Record<string, unknown> = TRAIN_POLICY) => { const f = trainFixture(policy); fixtures.push(f); return f; };
@@ -1,16 +1,16 @@
/**
* Stamped train prefixes (lib/ship-queue-train-stamp.ts): S_i = stamp(merge(S_(i-1), pin_i)) keeps every
* Stamped train prefixes (crew/lib/ship-queue-train-stamp.ts): S_i = stamp(merge(S_(i-1), pin_i)) keeps every
* earlier release, folds only slot i's own fragments (stacked pins included), runs the versions in ORDER
* with each slot's effective bump, and a tampered earlier entry or a product change is refused.
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { mergePolicy } from '../lib/ship-policy';
import { mergePolicy } from '../../lib/ship-policy';
import { buildChain } from '../lib/ship-queue-train';
import { stampChain, stampMismatch, type StampChainOptions } from '../lib/ship-queue-train-stamp';
import type { TrainSlot } from '../lib/ship-queue-train-record';
import { BASE_POLICY, cleanup, makeFixtureRepo, type FixtureRepo } from './helpers/restamp-fixture';
import { BASE_POLICY, cleanup, makeFixtureRepo, type FixtureRepo } from '../../test/helpers/restamp-fixture';
const POLICY = { ...BASE_POLICY, release_tool: null, release_outputs: [], changelog_fragments: { dir: 'changelog.d' }, min_gstack_version: '1.91.336.0' };
const repos: FixtureRepo[] = [];
@@ -139,7 +139,7 @@ describe('stamped train prefixes', () => {
// Value: protects=the train's stamp check bounds a branch-added row the same way the train restamp writes it, so a pin
// with a row left at the fork version still stamps; fails_when=stampMismatch reads that row against the pin's own
// VERSION and refuses the moved row; seam=lib/ship-queue-train-stamp.ts stampMismatch baseRef (GST-963)
// VERSION and refuses the moved row; seam=crew/lib/ship-queue-train-stamp.ts stampMismatch baseRef (GST-963)
test('a pin claiming 1.2.4.0 with a stamp_paths row left at the base 1.2.3.0 stamps, and the row moves (GST-963)', () => {
const r = makeFixtureRepo({ policy: POLICY, prefix: 'train-stamp-' }); repos.push(r);
pin(r, 'f', 'origin/main', '- f change\n');
@@ -1,16 +1,16 @@
// Split from test/ship-queue-train.test.ts (GSTA-151): verify --train acceptance, prefix equality and the CHANGELOG rule before and after merge.
// Split from crew/test/ship-queue-train.test.ts (GSTA-151): verify --train acceptance, prefix equality and the CHANGELOG rule before and after merge.
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { mergeChangelogs, parseChangelog } from '../lib/changelog-check';
import { EXIT } from '../lib/headless-artifacts';
import { cleanup } from './helpers/restamp-fixture';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import { laneStamp, markMerged, publishMain, twoPrTrain } from './helpers/ship-queue-train-cases';
import { mergeChangelogs, parseChangelog } from '../../lib/changelog-check';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import { coordinatorClone } from '../../test/helpers/ship-queue-fixture';
import { laneStamp, markMerged, publishMain, twoPrTrain } from '../../test/helpers/ship-queue-train-cases';
import {
GREEN_GATE, TRAIN_POLICY, dispatches, line, nonceOf, openPr, queue, readyReceipt, repoKey, setPrBody, shippedBody, trainFixture, trainJson,
type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = (policy: Record<string, unknown> = TRAIN_POLICY) => { const f = trainFixture(policy); fixtures.push(f); return f; };
@@ -15,17 +15,17 @@ import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { parseChangelog } from '../lib/changelog-check';
import { EXIT } from '../lib/headless-artifacts';
import { parseChangelog } from '../../lib/changelog-check';
import { EXIT } from '../../lib/headless-artifacts';
import { baseOverlap } from '../lib/ship-queue-train';
import { TRAIN_HELP } from '../lib/ship-queue-train-cli';
import { BASE_POLICY, cleanup, runBin } from './helpers/restamp-fixture';
import { coordinatorClone } from './helpers/ship-queue-fixture';
import { publishMain, twoPrTrain } from './helpers/ship-queue-train-cases';
import { BASE_POLICY, cleanup, runBin } from '../../test/helpers/restamp-fixture';
import { coordinatorClone } from '../../test/helpers/ship-queue-fixture';
import { publishMain, twoPrTrain } from '../../test/helpers/ship-queue-train-cases';
import {
GREEN_GATE, TRAIN_BLOCK, TRAIN_POLICY, dispatches, githubOrigin, line, mergeRefCommit, nonceOf, openPr, queue, readyReceipt, repoKey, setPrBody, shippedBody, trainFixture, trainJson, urlKey,
type Rule, type TrainFixture,
} from './helpers/ship-queue-train-fixture';
} from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = (policy: Record<string, unknown> = TRAIN_POLICY) => { const f = trainFixture(policy); fixtures.push(f); return f; };
@@ -146,7 +146,7 @@ describe('ready under a train policy: the screen choice', () => {
expect(none.status).toBe(EXIT.refused);
expect(none.err).toContain('(READY_SCREEN_CHOICE_REQUIRED)');
expect(none.out).toContain('SCREEN: selection not computed (train.screen_selection is none)');
expect(none.out).toContain('SCREEN: price pending the T0 cost replay (lib/gate-cost.ts)');
expect(none.out).toContain('SCREEN: price pending the T0 cost replay (crew/lib/gate-cost.ts)');
expect(next(none.out)).toBe('NEXT: gstack-ship-queue ready --pr 1 --no-screen');
const ok = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen']);
expect(ok.status).toBe(EXIT.ok);
@@ -1,5 +1,5 @@
/**
* Wave PRs (lib/ship-queue-wave.ts; docs/ship-policy.md#wave-prs; Garry R46, GST-574)
* Wave PRs (crew/lib/ship-queue-wave.ts; docs/ship-policy.md#wave-prs; Garry R46, GST-574)
* through the real bin/gstack-ship-queue against the train fixture (bare origin, gh shim):
* the area and grouping rules, wave build (merge chain, record on the notes ref, frozen
* notices, size and moved refusals), the conflict worktree and --continue, --drop as a new
@@ -11,10 +11,10 @@ import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { EXIT } from '../../lib/headless-artifacts';
import { WAVE_HELP, areaKey, groupWaves, isHubFile, type PlanPr } from '../lib/ship-queue-wave';
import { cleanup, runBin } from './helpers/restamp-fixture';
import { line, openPr, queue, readyReceipt, shippedBody, trainFixture, type TrainFixture } from './helpers/ship-queue-train-fixture';
import { cleanup, runBin } from '../../test/helpers/restamp-fixture';
import { line, openPr, queue, readyReceipt, shippedBody, trainFixture, type TrainFixture } from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = () => { const f = trainFixture(); fixtures.push(f); return f; };
@@ -72,12 +72,12 @@ describe('areas and grouping', () => {
const skill = (dir: string) => dir === 'ship' || dir === 'review';
test('areaKey maps skills, lib modules, tests, bins and docs to one functional area', () => {
expect(areaKey('ship/SKILL.md.tmpl', skill)).toBe('ship');
expect(areaKey('lib/ship-queue-train.ts', skill)).toBe('ship');
expect(areaKey('crew/lib/ship-queue-train.ts', skill)).toBe('ship');
expect(areaKey('test/ship-receipt.test.ts', skill)).toBe('ship');
expect(areaKey('test/helpers/ship-queue-train-fixture.ts', skill)).toBe('ship');
expect(areaKey('bin/gstack-pregate', skill)).toBe('pregate');
expect(areaKey('lib/pregate/checks/guards.ts', skill)).toBe('pregate');
expect(areaKey('docs/captain.md', skill)).toBe('docs');
expect(areaKey('crew/docs/captain.md', skill)).toBe('docs');
expect(areaKey('README.md', skill)).toBe('root');
});
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more