mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 10:18:32 +01:00
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:
1 parent
6042f6ad48
commit
6417abf7a6
210 files changed
+1963
-1784
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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}")"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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,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
|
||||
|
||||
@@ -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).
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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]
|
||||
@@ -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.
|
||||
@@ -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,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[] = [];
|
||||
+2
-2
@@ -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
Reference in new issue
Block a user