@bongos/core 1.19.319 → 1.19.320
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +121 -36
- package/.claude/skills/merge-mode/SKILL.md +3 -1
- package/.devcontainer/init-firewall.sh +41 -5
- package/clients/bongos-client/examples/hello-world.mjs +11 -2
- package/docs/copy-inventory.md +20 -20
- package/docs/copy-registry.json +22 -22
- package/modules/hall-ui/public/builders.js +0 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-replay.sh +20 -3
- package/scripts/gds/box.js +79 -86
- package/scripts/gds/db-backup-nightly.sh +17 -4
- package/scripts/gds/db-backup-offsite.sh +23 -6
- package/scripts/gds/discipline-modes.json +1 -1
- package/scripts/gds/doc-cli-guard.js +34 -2
- package/scripts/gds/fitness.js +8 -4
- package/scripts/gds/gds-db-backup.service +15 -4
- package/scripts/gds/gds-db-backup.timer +12 -1
- package/scripts/gds/gds-db-offsite.service +16 -5
- package/scripts/gds/gds-db-offsite.timer +12 -1
- package/scripts/gds/instance-apex.js +28 -0
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/ship-deploy-target.js +388 -0
- package/scripts/gds/ship-finish.js +377 -0
- package/scripts/gds/ship-flow.js +814 -0
- package/scripts/gds/ship-git.js +100 -0
- package/scripts/gds/ship-grade-steps.js +376 -0
- package/scripts/gds/ship-grade.js +319 -0
- package/scripts/gds/ship-io.js +100 -0
- package/scripts/gds/ship-land.js +539 -0
- package/scripts/gds/ship-memory-sync.js +192 -0
- package/scripts/gds/ship-merge.js +297 -0
- package/scripts/gds/ship-preflight-steps.js +323 -0
- package/scripts/gds/ship-regen.js +286 -0
- package/scripts/gds/ship-resume.js +151 -0
- package/scripts/gds/ship-session-upload.js +216 -0
- package/scripts/gds/ship-state.js +73 -0
- package/scripts/gds/ship-surprise.js +84 -0
- package/scripts/gds/ship.js +105 -4195
- package/scripts/status-probe.mjs +30 -2
- package/src/module-api.js +3 -1
- package/tests/helpers.mjs +13 -0
- package/tests/provision.mjs +7 -1
- package/tests/regrade_eligibility.mjs +43 -0
- package/tests/ship_assurance.mjs +4 -2
- package/tests/ship_publish_error.mjs +43 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
// scripts/gds/ship-flow.js — the two entry flows — shipMain and regradeMain.
|
|
2
|
+
//
|
|
3
|
+
// Carved out of scripts/gds/ship.js by task 1003205 (goal 1000079, the R03
|
|
4
|
+
// de-monolith plan) — a STRUCTURAL move, not a rewrite: the code below is the
|
|
5
|
+
// original ship.js lines 3478-4259 unchanged. ship.js remains the facade that
|
|
6
|
+
// re-exports every name and owns the CLI entry point, so the ship contract and
|
|
7
|
+
// every flag are untouched, and the other scripts that import from it are too.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { cliClient, requireSession, arg } = require('./cli-lib');
|
|
12
|
+
const { resolveProse, markShipProgress, smokeGateMessages } = require('./ship-honesty.js');
|
|
13
|
+
const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
|
|
14
|
+
const { scopeForModule, isModuleKey } = require('../../src/bongos/module-scope-map');
|
|
15
|
+
const { matchOne } = require('../../src/bongos/path-match');
|
|
16
|
+
const { runShipGate } = require('./sandbox-stage');
|
|
17
|
+
const { captureSurpriseLearning, promptSurprise } = require('./ship-surprise.js');
|
|
18
|
+
const { gitOk } = require('./ship-git.js');
|
|
19
|
+
const { autoLinkifyChangedDocs, mergeMainIntoBranch, preflight } = require('./ship-preflight-steps.js');
|
|
20
|
+
const { applyBaseOverride, computeChangedFiles, computeGraderBaseline, runSmokeAsync } = require('./ship-grade.js');
|
|
21
|
+
const { buildPriorRoundContext, computeGradeResult, gradeAndRecord, printCreditsAndAchievements, printGradeOutcome } = require('./ship-grade-steps.js');
|
|
22
|
+
const { apiErrorLine, normalizeApiError } = require('./ship-io.js');
|
|
23
|
+
const { diffstatFor, finishShip } = require('./ship-finish.js');
|
|
24
|
+
const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge } = require('./ship-resume.js');
|
|
25
|
+
const { noteServerReward } = require('./ship-state.js');
|
|
26
|
+
|
|
27
|
+
// ---------- Main flow ----------
|
|
28
|
+
|
|
29
|
+
async function shipMain() {
|
|
30
|
+
await requireSession();
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const taskId = Number(args[0]);
|
|
33
|
+
if (!Number.isFinite(taskId)) {
|
|
34
|
+
console.error('usage: node scripts/gds/ship.js <task-id> (--notes-file <path|-> | --notes "...") (--summary-file <path|-> | --summary "...") [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
|
|
35
|
+
console.error(' prefer the -file forms for prose: a shell command-substitutes backticks in an inline value before node sees it (learning 1000197).');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
// G04: prose from a FILE or inline, never shell-mangled in silence.
|
|
39
|
+
const prose = resolveProse(args, ['notes', 'summary']);
|
|
40
|
+
const notes = prose.notes;
|
|
41
|
+
const summary = prose.summary;
|
|
42
|
+
const skipMerge = args.includes('--no-merge');
|
|
43
|
+
const force = args.includes('--force');
|
|
44
|
+
// task 391 (idea #37): a deliberate zero-commit / direct-SQL ship. Without it,
|
|
45
|
+
// a branch with no commits ahead of main is REFUSED (not silently shipped).
|
|
46
|
+
// task 1002422: --base <ref> also implies db-only — the work is already on main
|
|
47
|
+
// (that's the whole point of --base), so the merge leg has nothing to merge and
|
|
48
|
+
// advances straight to shipped. Unlike --allow-empty it does NOT skip the grade:
|
|
49
|
+
// the pinned baseline gives the grader a real diff (see resolveBaseRef / main).
|
|
50
|
+
const baseRefRaw = arg('--base', args);
|
|
51
|
+
const dbOnly = args.includes('--db-only') || args.includes('--allow-empty') || Boolean(baseRefRaw);
|
|
52
|
+
// task 1053 (F): explicit attestation that a verification task's verify
|
|
53
|
+
// actually passed. Required to confirm a kind='verify' task (the server
|
|
54
|
+
// enforces it too); a no-op for every other task.
|
|
55
|
+
const verified = args.includes('--verified');
|
|
56
|
+
// #853 (ADR 0041): `let` not `const` — when the server has the temporary
|
|
57
|
+
// global grader bypass ON (surfaced as me.grader_bypass below), we force the
|
|
58
|
+
// skip-grade/confirm path for everyone so no builder runs the subagent grader
|
|
59
|
+
// during a grader outage window.
|
|
60
|
+
let skipGrade = args.includes('--skip-grade');
|
|
61
|
+
// task 1365: explicit acknowledgement that this task legitimately has NO
|
|
62
|
+
// committed artifact (a pure DB-only / no-op task). Required for the
|
|
63
|
+
// empty-diff guard below to ALLOW an empty committed diff; without it, an
|
|
64
|
+
// empty diff is refused (it used to slip through under --skip-grade — the
|
|
65
|
+
// task-1363 phantom ship). Never overrides a dirty working tree.
|
|
66
|
+
const allowEmpty = args.includes('--allow-empty');
|
|
67
|
+
// task 1002605 (G-18): explicit opt-in to continue past a FAILED production
|
|
68
|
+
// smoke run. Without it a smoke FAIL now ABORTS before the claim resolves
|
|
69
|
+
// (claim intact); with it, the resolve proceeds and the task deliberately
|
|
70
|
+
// parks at 'completed' ungraded — and the ship says so instead of "Done."
|
|
71
|
+
const allowSmokeFail = args.includes('--allow-smoke-fail');
|
|
72
|
+
// Surprise flag handling — see captureSurpriseLearning(). Resolved below
|
|
73
|
+
// once we know which claim is being shipped (so touches[] can become tags).
|
|
74
|
+
const surpriseRaw = arg('--surprise', args);
|
|
75
|
+
const surpriseProvided = surpriseRaw !== null;
|
|
76
|
+
const noSurprise = args.includes('--no-surprise');
|
|
77
|
+
|
|
78
|
+
// task 1002422 (blocker 1000082): resolve + validate --base BEFORE any heavy
|
|
79
|
+
// work so a bad ref aborts with the claim STILL ACTIVE. On success it sets the
|
|
80
|
+
// module-level baseOverrideSha that computeGraderBaseline() reads, so the grader
|
|
81
|
+
// reads <base>...HEAD (a real diff) and the ship takes the rank-open graded path
|
|
82
|
+
// instead of the --allow-empty → skip-grade → Archon-only /confirm dead-end.
|
|
83
|
+
if (baseRefRaw) {
|
|
84
|
+
const res = applyBaseOverride(baseRefRaw);
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
console.error(`Ship aborted: ${res.reason}. No claim resolved, no credits awarded.`);
|
|
87
|
+
console.error('Omit --base and ship normally — the grader reviews ALL your commits and the merge lands them. Your claim is still active.');
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
console.log(`--base: grading ${res.sha.slice(0, 9)}...HEAD — the task's real committed diff (work already on main). Graded via the rank-open path; no --allow-empty needed.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const me = noteServerReward(await (await cliClient()).me.getMe());
|
|
94
|
+
if (!me.ok) {
|
|
95
|
+
console.error('failed to load profile:', me.data);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
// #853 (ADR 0041): honor the server-side global grader bypass. When ON, every
|
|
99
|
+
// builder lands work without the subagent grader (the /tasks/:id/confirm gate
|
|
100
|
+
// is relaxed to any-builder server-side to match). Default OFF.
|
|
101
|
+
if (me.data.grader_bypass && !skipGrade) {
|
|
102
|
+
skipGrade = true;
|
|
103
|
+
console.log('Note: GRADER_BYPASS_ENABLED is on server-side — skipping the subagent grader and confirming directly (temporary outage window).');
|
|
104
|
+
}
|
|
105
|
+
const claims = me.data.active_claims || (me.data.active_claim ? [me.data.active_claim] : []);
|
|
106
|
+
const ac = claims.find((c) => Number(c.task_id) === taskId);
|
|
107
|
+
if (!ac) {
|
|
108
|
+
// task 941 (V4.R03): no active claim in THIS session is NOT automatically a
|
|
109
|
+
// dead-end. The claim is consumed at resolve, so a ship that stalled AFTER it
|
|
110
|
+
// (grade fail → `completed`; merge/deploy fail → `confirmed`, the #1027 case)
|
|
111
|
+
// leaves no claim — but a plain re-run should RESUME the stalled leg, not
|
|
112
|
+
// strand the task. Route on the task's current status + ownership.
|
|
113
|
+
const tRes = await (await cliClient()).tasks.getTasksId({ id: taskId, query: { include: 'last_claim' } });
|
|
114
|
+
const task = tRes.ok ? tRes.data.task : null;
|
|
115
|
+
const lastClaim = tRes.ok ? (tRes.data.last_claim || null) : null;
|
|
116
|
+
const action = evaluateResumeAction({ task, lastClaim, builder: me.data.builder });
|
|
117
|
+
if (action.action === 'resume-grade') {
|
|
118
|
+
console.log(`Task #${taskId} is at 'completed' (grade stalled) and ${action.via === 'owner' ? 'you last held the claim' : 'you are an Archon'} — resuming the grade (no re-claim needed).`);
|
|
119
|
+
return regradeMain();
|
|
120
|
+
}
|
|
121
|
+
if (action.action === 'resume-merge') {
|
|
122
|
+
console.log(`Task #${taskId} is at 'confirmed' (merge/deploy stalled) and ${action.via === 'owner' ? 'you last held the claim' : 'you are an Archon'} — resuming the merge + deploy (no re-claim needed).`);
|
|
123
|
+
return resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly });
|
|
124
|
+
}
|
|
125
|
+
if (action.action === 'done') {
|
|
126
|
+
console.log(action.reason);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.error(action.reason || `You don't have an active claim on task #${taskId}.`);
|
|
130
|
+
if (claims.length > 0) {
|
|
131
|
+
console.error(`Your active claims:`);
|
|
132
|
+
for (const c of claims) console.error(` #${c.task_id}: ${c.title}`);
|
|
133
|
+
}
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- Hard cap: refuse to ship over the monthly budget (V3.R84 #302, C3) ----
|
|
138
|
+
// Shipping runs the subagent grader, which spends API $. If this builder is at
|
|
139
|
+
// or over their monthly budget, refuse here — BEFORE the grader runs and BEFORE
|
|
140
|
+
// the claim resolves, so the claim stays active and no cost is incurred. Any
|
|
141
|
+
// Metic+ builder lifts the block by raising the builder's budget
|
|
142
|
+
// (PATCH /builders/:id/budget — builder.budget.set, metic-floored since ADR 0157).
|
|
143
|
+
// Fail-open: a missing/unreadable cost status (e.g. an older server) never blocks a ship.
|
|
144
|
+
const costStatus = me.data.cost;
|
|
145
|
+
if (costStatus && costStatus.level === 'over') {
|
|
146
|
+
console.error('');
|
|
147
|
+
console.error(`Ship refused: monthly budget exceeded — $${costStatus.spend_mtd_usd} of $${costStatus.budget_mtd_usd} spent this month (${costStatus.pct_used}%).`);
|
|
148
|
+
console.error('Shipping runs the quality grader, which spends API budget. No claim resolved, no credits awarded.');
|
|
149
|
+
console.error('Ask any Metic or Archon to raise your monthly budget (PATCH /builders/:id/budget), then re-ship.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- Surprise gate: forced "anything surprise you?" prompt ----
|
|
154
|
+
//
|
|
155
|
+
// Resolve the user's intent into a single value: either a non-empty surprise
|
|
156
|
+
// string (capture a learning) or empty (no surprise, ship normally).
|
|
157
|
+
// --surprise "text" → capture
|
|
158
|
+
// --surprise none → no surprise
|
|
159
|
+
// --surprise "" → no surprise
|
|
160
|
+
// --no-surprise → no surprise
|
|
161
|
+
// neither + TTY → prompt
|
|
162
|
+
// neither + non-TTY → no surprise, with stderr warning
|
|
163
|
+
let surpriseText = '';
|
|
164
|
+
if (surpriseProvided) {
|
|
165
|
+
const v = (surpriseRaw || '').trim();
|
|
166
|
+
surpriseText = (v === '' || v.toLowerCase() === 'none') ? '' : v;
|
|
167
|
+
} else if (noSurprise) {
|
|
168
|
+
surpriseText = '';
|
|
169
|
+
} else if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
170
|
+
surpriseText = await promptSurprise();
|
|
171
|
+
if (surpriseText.toLowerCase() === 'none') surpriseText = '';
|
|
172
|
+
} else {
|
|
173
|
+
console.error('warn: no --surprise or --no-surprise provided in non-interactive context; defaulting to no surprise.');
|
|
174
|
+
surpriseText = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- Step 0: preflight (working tree + branch freshness) ----
|
|
178
|
+
console.log(`Verifying claim #${ac.claim_id} on task #${ac.task_id}: ${ac.title}`);
|
|
179
|
+
console.log('Preflight (working tree + branch freshness)...');
|
|
180
|
+
const pre = preflight(ac, { force });
|
|
181
|
+
if (!pre.ok) {
|
|
182
|
+
console.error('');
|
|
183
|
+
console.error(`Ship aborted: ${pre.reason}. No claim resolved, no credits awarded.`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- Step 0b: pull origin/main into the branch up front (#1068, gap 2) ----
|
|
188
|
+
// So the tree reviewed on the sandbox == the tree we ship — no post-approval
|
|
189
|
+
// re-stage loop. Skipped under --no-merge (a deliberate "don't touch main"
|
|
190
|
+
// opt-out). Aborts only on a genuine conflict, claim left intact.
|
|
191
|
+
if (!skipMerge) {
|
|
192
|
+
const m = mergeMainIntoBranch();
|
|
193
|
+
if (!m.ok) {
|
|
194
|
+
console.error('');
|
|
195
|
+
console.error(`Ship aborted: ${m.reason}.`);
|
|
196
|
+
console.error('No claim resolved, no credits awarded — your claim is still active.');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- Capture surprise learning (if any) ----
|
|
202
|
+
if (surpriseText) {
|
|
203
|
+
console.log('Capturing surprise as a learning...');
|
|
204
|
+
await captureSurpriseLearning(surpriseText, ac, taskId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- Pre-verify: auto-linkify this ship's own changed docs (#747) ----
|
|
208
|
+
// Fix bare "#NNN" task refs in the docs this ship changed BEFORE the merge,
|
|
209
|
+
// so they never red-X the doc-ref-linkify gate (tests/linkify_check.mjs in
|
|
210
|
+
// the `unit` check). Best-effort.
|
|
211
|
+
autoLinkifyChangedDocs(ac);
|
|
212
|
+
|
|
213
|
+
// ---- Empty-diff / phantom-ship guard (must run BEFORE the review gate + resolve) ----
|
|
214
|
+
// #513 added this check, but at step 2 — AFTER the resolve below already
|
|
215
|
+
// landed the task at `completed` and consumed the claim. A builder who ran
|
|
216
|
+
// /builder-ship before committing then burned the claim and couldn't re-ship
|
|
217
|
+
// ("You don't have an active claim" — the task-595-regrade-gap). Observed
|
|
218
|
+
// live on #662 (Windows). Moving it here means an empty committed diff aborts
|
|
219
|
+
// cleanly with the claim STILL ACTIVE, so the builder can commit and re-run.
|
|
220
|
+
// The grader reads committed history (baseline...HEAD), independent of
|
|
221
|
+
// touches[] (see computeGraderBaseline). The computed list is reused by the
|
|
222
|
+
// grader at step 2 so we don't diff twice.
|
|
223
|
+
//
|
|
224
|
+
// task 1365: this guard now runs REGARDLESS of --skip-grade. The old version
|
|
225
|
+
// was gated on `!skipGrade`, so --skip-grade (and the server-side
|
|
226
|
+
// grader_bypass that forces skipGrade) sailed an empty committed diff straight
|
|
227
|
+
// through to resolve → manual-confirm → the publish reconciler then shipped a
|
|
228
|
+
// task with NO artifact on main (task 1363: a new doc left UNTRACKED, never
|
|
229
|
+
// committed, HEAD never moved — the ledger lied, CLAUDE.md §8). classifyEmptyShip
|
|
230
|
+
// splits the three cases: a dirty/untracked working tree means the deliverable
|
|
231
|
+
// was never committed (refuse, even under --allow-empty); a clean tree with an
|
|
232
|
+
// explicit --allow-empty is a genuine no-artifact/DB-only ship (allow); a clean
|
|
233
|
+
// tree without the flag is nothing-to-ship (refuse). Refusing here leaves the
|
|
234
|
+
// claim ACTIVE so the builder can commit + re-run.
|
|
235
|
+
const graderBaseline = computeGraderBaseline(ac);
|
|
236
|
+
const committedFiles = computeChangedFiles(graderBaseline);
|
|
237
|
+
if (committedFiles.length === 0) {
|
|
238
|
+
const verdict = classifyEmptyShip(gitOk('git status --porcelain'), { allowEmpty });
|
|
239
|
+
if (verdict.reason === 'uncommitted-work') {
|
|
240
|
+
console.error('Ship refused: nothing is committed on this branch, but your working tree has uncommitted changes:');
|
|
241
|
+
for (const f of verdict.uncommitted) console.error(` - ${f}`);
|
|
242
|
+
console.error('');
|
|
243
|
+
console.error('Your work was never committed — shipping now would mark the task "shipped" with NO artifact on main (the ledger would lie).');
|
|
244
|
+
console.error('Stage + commit your work (e.g. `git add -A && git commit`), then re-run /builder-ship. Your claim is still active.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (verdict.reason === 'nothing-to-ship') {
|
|
248
|
+
console.error(`Nothing to ship: the committed diff vs ${graderBaseline || 'baseline'} is empty and the working tree is clean.`);
|
|
249
|
+
console.error('The grader reads committed history, not the working tree — commit your work on this branch, then re-run /builder-ship. Your claim is still active.');
|
|
250
|
+
console.error('If this task legitimately has no code/doc artifact (a pure DB-only or no-op task), re-run with --allow-empty.');
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
// verdict.reason === 'allow-empty': a deliberate no-artifact / DB-only ship.
|
|
254
|
+
// There is no committed diff for the subagent grader to read, so confirm
|
|
255
|
+
// directly (this is the DB-only path the old --skip-grade route served).
|
|
256
|
+
if (!skipGrade) skipGrade = true;
|
|
257
|
+
console.log('--allow-empty: no committed diff (no-artifact / DB-only ship) — confirming directly without the subagent grader.');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---- Module drift check (BV1.R53, task 1422) ----
|
|
261
|
+
// If the task carries a module_key, verify the committed diff doesn't stray
|
|
262
|
+
// into a DIFFERENT module's territory. "You said game, you touched dev-box" is
|
|
263
|
+
// the semantic violation this catches. Files outside all module scopes (docs,
|
|
264
|
+
// session logs, general scripts) are allowed — they're cross-cutting. Only
|
|
265
|
+
// contact with a DIFFERENT module's glob set is refused.
|
|
266
|
+
//
|
|
267
|
+
// The check is ADVISORY (warns but does not refuse) on the first cut because
|
|
268
|
+
// the module_key column is new and existing tasks may carry a broader diff scope
|
|
269
|
+
// until all tasks are labelled. Enforced tasks can opt in with --enforce-module.
|
|
270
|
+
// ac.module_key comes from getActiveClaimsForBuilder (t.module_key in the join).
|
|
271
|
+
const enforceModule = args.includes('--enforce-module');
|
|
272
|
+
if (ac.module_key && isModuleKey(ac.module_key) && committedFiles.length > 0) {
|
|
273
|
+
const declaredScope = scopeForModule(ac.module_key);
|
|
274
|
+
const { MODULE_SCOPE_MAP } = require('../../src/bongos/module-scope-map');
|
|
275
|
+
const moduleDriftFiles = committedFiles.filter((f) => {
|
|
276
|
+
if (declaredScope && matchOne(f, declaredScope.globs)) return false;
|
|
277
|
+
for (const [key, entry] of Object.entries(MODULE_SCOPE_MAP)) {
|
|
278
|
+
if (key === ac.module_key) continue;
|
|
279
|
+
if (matchOne(f, entry.globs)) return true;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
});
|
|
283
|
+
if (moduleDriftFiles.length > 0) {
|
|
284
|
+
const msg = [
|
|
285
|
+
`\n⚠ Module drift detected: task is scoped to '${ac.module_key}' but ${moduleDriftFiles.length} file(s) touch a different module:`,
|
|
286
|
+
...moduleDriftFiles.map((f) => ` - ${f}`),
|
|
287
|
+
'',
|
|
288
|
+
enforceModule
|
|
289
|
+
? 'Refused (--enforce-module). Confine your changes to modules/' + ac.module_key + '/ or clear the module_key label.'
|
|
290
|
+
: 'Advisory — ship proceeds, but consider confining your changes to modules/' + ac.module_key + '/.',
|
|
291
|
+
].join('\n');
|
|
292
|
+
console.log(msg);
|
|
293
|
+
if (enforceModule) process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---- Sandbox-first review gate (#927, ADR 0046) ----
|
|
298
|
+
// On a dev box, a diff that touches game surfaces must be STAGED on the
|
|
299
|
+
// builder's own live preview (sandbox-<login>.amazonprimea.com, ADR 0044)
|
|
300
|
+
// and reviewed in a browser before the claim resolves — see it running,
|
|
301
|
+
// get the chance to iterate, THEN decide to ship to the main game. Laptop
|
|
302
|
+
// sessions, non-game diffs, quick-tunnel boxes, and --skip-stage all pass
|
|
303
|
+
// through untouched. Interactive: stage + ask; declining aborts with the
|
|
304
|
+
// claim intact. Non-TTY (agent-driven sessions): stage, then abort with the
|
|
305
|
+
// review URL — the re-run after the human reviews passes on the fresh
|
|
306
|
+
// staged marker. Never hangs on stdin.
|
|
307
|
+
const sandboxGate = await runShipGate({
|
|
308
|
+
taskId,
|
|
309
|
+
files: committedFiles,
|
|
310
|
+
skipStage: args.includes('--skip-stage'),
|
|
311
|
+
// #1048: --approved records the builder's sign-off after they reviewed the
|
|
312
|
+
// staged change on their sandbox. Only honored on a re-run whose game state
|
|
313
|
+
// is already staged-fresh — it can't pre-approve an unstaged change.
|
|
314
|
+
approved: args.includes('--approved'),
|
|
315
|
+
tty: Boolean(process.stdout.isTTY && process.stdin.isTTY),
|
|
316
|
+
});
|
|
317
|
+
if (!sandboxGate.ok) {
|
|
318
|
+
if (sandboxGate.review) {
|
|
319
|
+
// task 1275: the in-review card — the SAME lifecycle card, amber, whose
|
|
320
|
+
// action is "Open in sandbox ↗" (the sandbox URL the owner asked to surface
|
|
321
|
+
// here). Best-effort, never blocks the pause.
|
|
322
|
+
try {
|
|
323
|
+
const shipCard = require('../../modules/lifecycle/ship-card');
|
|
324
|
+
const { added, removed } = diffstatFor(graderBaseline);
|
|
325
|
+
const model = shipCard.buildCardModel({
|
|
326
|
+
stage: 'review',
|
|
327
|
+
taskId,
|
|
328
|
+
title: ac.title,
|
|
329
|
+
valueSummary: summary,
|
|
330
|
+
fileCount: committedFiles.length || null,
|
|
331
|
+
added,
|
|
332
|
+
removed,
|
|
333
|
+
sandboxUrl: sandboxGate.url,
|
|
334
|
+
});
|
|
335
|
+
// task 1279: honour the builder's render-widgets knob (from the /me
|
|
336
|
+
// fetched at the top of shipMain; default ON if absent/older server).
|
|
337
|
+
const widgetsEnabled = me?.data?.render?.widgets_enabled !== false;
|
|
338
|
+
shipCard.emitCard(model, taskId, { widgetsEnabled });
|
|
339
|
+
} catch (_) { /* the review pause stands with or without the card */ }
|
|
340
|
+
// #1061 polish: a sandbox review pause is a DELIBERATE, expected
|
|
341
|
+
// checkpoint — not a failed ship. printReviewBlock already showed the URL
|
|
342
|
+
// + the --approved next step, so don't follow it with red "Ship aborted /
|
|
343
|
+
// no credits awarded" text that makes the review moment read as broken.
|
|
344
|
+
// Calm note on stdout (not stderr); the claim is left fully intact.
|
|
345
|
+
console.log('');
|
|
346
|
+
console.log('⏸ Paused for your sandbox review — this is expected, not an error.');
|
|
347
|
+
console.log(' Your claim is still active; nothing was lost.');
|
|
348
|
+
console.log(' Walk the change at the link above, then approve with: /builder-ship --approved');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
console.error('');
|
|
352
|
+
console.error(`Ship aborted: ${sandboxGate.reason}.`);
|
|
353
|
+
console.error('No claim resolved, no credits awarded — your claim is still active.');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
// #1048: the recorded sandbox self-approval (when the builder approved a
|
|
357
|
+
// staged game change), threaded into the resolve call below so the server
|
|
358
|
+
// persists it on the task. null on laptop ships, non-game diffs, quick-tunnel
|
|
359
|
+
// boxes, --skip-stage, and bare fresh-marker re-runs without --approved.
|
|
360
|
+
const sandboxReview = sandboxGate.sandboxReview || null;
|
|
361
|
+
|
|
362
|
+
// ---- Step 1+2: verify locally (smoke) — runs AFTER the review gate (#1061) ----
|
|
363
|
+
// touches[] cleanup [4/8] (task 879): the predictive drift scanner is gone;
|
|
364
|
+
// local verification is smoke-only. Smoke runs HERE (post-gate) so a review
|
|
365
|
+
// PAUSE doesn't bury the sandbox link under the smoke output — smoke only runs
|
|
366
|
+
// on the actual ship: the --approved re-run, or a laptop / non-game ship the
|
|
367
|
+
// gate waved straight through. (#1061 — the gate is a no-op for those, so this
|
|
368
|
+
// is the same single smoke run as before, just sequenced after it.)
|
|
369
|
+
|
|
370
|
+
// task 1002660: start the grade COMPUTATION now, so the multi-minute panel
|
|
371
|
+
// overlaps the smoke run + claim resolve instead of serializing behind them.
|
|
372
|
+
// computeGradeResult performs no server writes — the grade POST stays in the
|
|
373
|
+
// grade step below, after the resolve lands the task at 'completed'. On a
|
|
374
|
+
// smoke abort the in-flight panel is discarded (its partial spend is lost —
|
|
375
|
+
// rare, and cheaper than serializing every ship behind smoke).
|
|
376
|
+
let gradeCompute = null;
|
|
377
|
+
let fullTaskForGrade = null;
|
|
378
|
+
if (!skipGrade) {
|
|
379
|
+
const tFetch = await (await cliClient()).tasks.getTasksId({ id: taskId });
|
|
380
|
+
fullTaskForGrade = tFetch.ok ? tFetch.data.task : { id: taskId, title: ac.title };
|
|
381
|
+
console.log('Starting the quality grader panel (overlaps the smoke run — task 1002660)...');
|
|
382
|
+
gradeCompute = computeGradeResult({
|
|
383
|
+
taskId, task: fullTaskForGrade, summary,
|
|
384
|
+
baseline: graderBaseline, committedFiles,
|
|
385
|
+
});
|
|
386
|
+
gradeCompute.catch(() => {}); // an abort path must not die on an unhandled rejection
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log('Running smoke tests against production...');
|
|
390
|
+
// task 1002660: the async twin — awaiting keeps the event loop free so the
|
|
391
|
+
// overlapped panel's subprocess callbacks progress while smoke runs.
|
|
392
|
+
const smoke = await runSmokeAsync();
|
|
393
|
+
const passed = smoke.clean;
|
|
394
|
+
console.log('');
|
|
395
|
+
console.log(`Verification: smoke=${smoke.clean ? 'clean' : 'FAIL'}, overall=${passed ? 'PASS' : 'FAIL'}`);
|
|
396
|
+
console.log('');
|
|
397
|
+
|
|
398
|
+
// task 1002605 (G-18): a smoke FAIL used to fall straight through into the
|
|
399
|
+
// claim resolve — claim consumed, grading silently skipped, task parked at
|
|
400
|
+
// 'completed', exit 0, and the builder read "Done." believing they shipped.
|
|
401
|
+
// Abort BEFORE the resolve instead — the same claim-preserving posture as the
|
|
402
|
+
// budget cap and the sandbox-review gate above. --allow-smoke-fail is the
|
|
403
|
+
// deliberate continuation; the parked message where the grader would have run
|
|
404
|
+
// says exactly where that leaves the task.
|
|
405
|
+
if (!passed && !allowSmokeFail) {
|
|
406
|
+
console.error('');
|
|
407
|
+
for (const line of smokeGateMessages(taskId).abort) console.error(line);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---- Step 1: resolve the claim (lands at completed) ----
|
|
412
|
+
// V3.R18 (#209): resolveClaim no longer runs the v1 deterministic grader.
|
|
413
|
+
// It now lands the task at `completed` when verification (smoke) passes, and
|
|
414
|
+
// stops. The v2 subagent grader (step 2 below) is what gates the credits +
|
|
415
|
+
// the completed→confirmed transition.
|
|
416
|
+
const r = await (await cliClient()).claims.postClaimsIdResolve({
|
|
417
|
+
id: ac.claim_id,
|
|
418
|
+
body: {
|
|
419
|
+
outcome: 'shipped',
|
|
420
|
+
notes_md: notes,
|
|
421
|
+
value_summary: summary,
|
|
422
|
+
verification: {
|
|
423
|
+
passed,
|
|
424
|
+
// touches[] cleanup [4/8] (task 879): the drift scanner is gone; with no
|
|
425
|
+
// scanner there is nothing to drift, so this audit-trail signal
|
|
426
|
+
// (grader.scoreScannerSmoke) is vacuously clean.
|
|
427
|
+
scanner_clean: true,
|
|
428
|
+
smoke_clean: smoke.clean,
|
|
429
|
+
},
|
|
430
|
+
// touches[] cleanup [7/8] (task 882): the real committed changed-files, so
|
|
431
|
+
// the server backfills touches[] from reality (the planning prediction is
|
|
432
|
+
// now optional). Same list the grader already used.
|
|
433
|
+
committed_files: committedFiles,
|
|
434
|
+
// #1048: the sandbox self-approval { stamp, url, method } when the builder
|
|
435
|
+
// reviewed + approved a staged game change. The server stamps by/at and
|
|
436
|
+
// persists it to tasks.sandbox_review. null = nothing to record.
|
|
437
|
+
sandbox_review: sandboxReview,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
if (!r.ok) {
|
|
441
|
+
const resolveErr = normalizeApiError(r.data);
|
|
442
|
+
if (resolveErr.code === 'CLAIM_SESSION_MISMATCH') {
|
|
443
|
+
const wt = resolveErr.details.claimWorktree;
|
|
444
|
+
console.error(`This claim was created in a different session${wt ? ` (worktree: ${wt})` : ''}.`);
|
|
445
|
+
console.error('You can only ship from the session that originally claimed this task.');
|
|
446
|
+
console.error(`To force-release from this session: node scripts/gds/release.js ${taskId} --force`);
|
|
447
|
+
} else {
|
|
448
|
+
console.error(`resolve failed: ${apiErrorLine(r.data, r.status)}`);
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const taskStatus = r.data.taskStatus || 'shipped (legacy response)';
|
|
454
|
+
console.log(`Task #${r.data.taskId} is now: ${taskStatus}`);
|
|
455
|
+
// Claim CONSUMED, task at 'completed' — a crash from here needs fatalRecoveryLines.
|
|
456
|
+
markShipProgress('resolved', taskId);
|
|
457
|
+
|
|
458
|
+
// ---- Step 2: subagent quality grader ----
|
|
459
|
+
// Runs when local verification passed AND resolveClaim landed at completed.
|
|
460
|
+
// If verification did not pass we don't bother grading — the task stays at
|
|
461
|
+
// completed (or whatever resolveClaim returned) with no credits.
|
|
462
|
+
let credits = r.data.creditsAwarded ?? 0;
|
|
463
|
+
let postGradeStatus = taskStatus;
|
|
464
|
+
let postGradeJustUnlocked = r.data.just_unlocked || [];
|
|
465
|
+
let gradePassed = false;
|
|
466
|
+
let advanceToMerge = false;
|
|
467
|
+
let gradeObj = null; // task 1270: the grade result, hoisted for the completion card
|
|
468
|
+
// V3.R27 (#247): credit breakdown surfaced from the grade or confirm
|
|
469
|
+
// response. Kept separate from `credits` so we only print the multiplier
|
|
470
|
+
// line when the multiplier actually moved the payout.
|
|
471
|
+
let creditsRaw = null;
|
|
472
|
+
let creditsMultiplier = null;
|
|
473
|
+
let ideaBonusInfo = null;
|
|
474
|
+
|
|
475
|
+
if (passed && taskStatus === 'completed' && skipGrade) {
|
|
476
|
+
// Escape hatch: skip the subagent grader and manual-confirm. The
|
|
477
|
+
// task_grades row gets grader_kind='lars' so the audit trail captures
|
|
478
|
+
// that this ship bypassed the v2 grader.
|
|
479
|
+
console.log('');
|
|
480
|
+
console.log('--skip-grade: skipping subagent grader; manual-confirming via POST /tasks/:id/confirm.');
|
|
481
|
+
// task 1053 (F): pass the verify attestation. The server refuses to confirm a
|
|
482
|
+
// kind='verify' task without it (so a hardware-verify can't land before the
|
|
483
|
+
// verify ran). --verified is a no-op for every other kind.
|
|
484
|
+
const confirm = await (await cliClient()).tasks.postTasksIdConfirm({ id: taskId, body: { verified } });
|
|
485
|
+
if (!confirm.ok) {
|
|
486
|
+
if (confirm.status === 409 && normalizeApiError(confirm.data).code === 'verify_attestation_required') {
|
|
487
|
+
console.error('');
|
|
488
|
+
console.error(`This is a VERIFICATION task — it won't confirm until you attest the verify actually passed.`);
|
|
489
|
+
console.error(`Run the verification, then re-ship with --verified:`);
|
|
490
|
+
console.error(` node scripts/gds/ship.js ${taskId} --verified`);
|
|
491
|
+
console.error(`(Your work is committed and the task is at 'completed' — re-running resumes, task 941.)`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
// task 1002613 (map P-1): the one genuine permission wall on the ship path
|
|
495
|
+
// used to surface as a raw envelope dump — an atom name is not a rank, and
|
|
496
|
+
// no remedy printed at the one station where self-service exists. Interpret
|
|
497
|
+
// it: name the wall, the floor (from the server's floors map — never
|
|
498
|
+
// hardcoded), and both ways forward.
|
|
499
|
+
const errObj = confirm.data && confirm.data.error;
|
|
500
|
+
const errCode = typeof errObj === 'string' ? errObj : (errObj && errObj.code);
|
|
501
|
+
if (confirm.status === 403 && errCode === 'permission_forbidden') {
|
|
502
|
+
const details = (errObj && typeof errObj === 'object' && errObj.details) || {};
|
|
503
|
+
const floor = details.floors && details.floors['task.confirm.grade_bypass'];
|
|
504
|
+
const floorNote = floor ? ` (floor: ${floor})` : '';
|
|
505
|
+
console.error('');
|
|
506
|
+
console.error(`✖ Confirming without a passing grade is permission-gated: task.confirm.grade_bypass${floorNote}.`);
|
|
507
|
+
console.error(' This is an authorization wall, not a verdict on your work.');
|
|
508
|
+
console.error(' Self-service: re-run WITHOUT --skip-grade to use the subagent grader, and if a');
|
|
509
|
+
console.error(' grade fails, fix its findings and re-run (unlimited, no attempt cap):');
|
|
510
|
+
console.error(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
511
|
+
console.error(` If the grade genuinely cannot pass, ask a builder whose role holds`);
|
|
512
|
+
console.error(` task.confirm.grade_bypass${floorNote} to POST /api/gds/tasks/${taskId}/confirm.`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
console.error(`manual-confirm failed: ${apiErrorLine(confirm.data, confirm.status)}`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
postGradeStatus = confirm.data.taskStatus;
|
|
519
|
+
credits = confirm.data.creditsAwarded ?? 0;
|
|
520
|
+
ideaBonusInfo = confirm.data.ideaBonus ?? null;
|
|
521
|
+
advanceToMerge = postGradeStatus === 'confirmed';
|
|
522
|
+
gradePassed = true; // by builder fiat, not by grader
|
|
523
|
+
} else if (passed && taskStatus === 'completed') {
|
|
524
|
+
console.log('');
|
|
525
|
+
console.log('Running subagent quality grader...');
|
|
526
|
+
|
|
527
|
+
// #513 + #536: the grader reads COMMITTED history (baseline...HEAD), not
|
|
528
|
+
// the working tree. The empty-committed-diff guard now runs earlier — BEFORE
|
|
529
|
+
// the claim is resolved (see above) — so by the time we reach here the diff
|
|
530
|
+
// is guaranteed non-empty on the grader path. Reuse the baseline + file list
|
|
531
|
+
// computed there so we don't diff twice. #678: gradeAndRecord POSTs the
|
|
532
|
+
// result and logs the panel cost — the same unit the --regrade path reuses.
|
|
533
|
+
// task 1002660: the panel computation was started BEFORE the smoke gate
|
|
534
|
+
// (gradeCompute above) — hand it in so this step only waits out whatever
|
|
535
|
+
// panel time smoke + resolve didn't already absorb.
|
|
536
|
+
const gradePost = await gradeAndRecord({
|
|
537
|
+
taskId,
|
|
538
|
+
task: fullTaskForGrade || { id: taskId, title: ac.title },
|
|
539
|
+
summary,
|
|
540
|
+
baseline: graderBaseline,
|
|
541
|
+
committedFiles,
|
|
542
|
+
precomputed: gradeCompute,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
if (!gradePost.ok) {
|
|
546
|
+
console.error(`grade POST failed (${gradePost.status}):`, gradePost.data);
|
|
547
|
+
console.error('Task stays at completed. Manual options:');
|
|
548
|
+
console.error(` - Re-run /builder-ship ${taskId} to try grading again, or`);
|
|
549
|
+
console.error(` - node scripts/gds/ship.js ${taskId} --regrade after fixing findings, or`);
|
|
550
|
+
console.error(` - POST /api/gds/tasks/${taskId}/confirm to manual-confirm (last resort —`);
|
|
551
|
+
console.error(' permission-gated on task.confirm.grade_bypass).');
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const g = gradePost.data.grade;
|
|
556
|
+
gradeObj = g;
|
|
557
|
+
gradePassed = g.passed;
|
|
558
|
+
advanceToMerge = gradePost.data.advanceToMerge;
|
|
559
|
+
postGradeStatus = gradePost.data.taskStatus;
|
|
560
|
+
credits = gradePost.data.creditsAwarded;
|
|
561
|
+
postGradeJustUnlocked = gradePost.data.just_unlocked || [];
|
|
562
|
+
// V3.R27 (#247): pull the multiplier breakdown + idea-bonus info forward.
|
|
563
|
+
creditsRaw = gradePost.data.creditsRaw ?? null;
|
|
564
|
+
creditsMultiplier = gradePost.data.creditsMultiplier ?? null;
|
|
565
|
+
ideaBonusInfo = gradePost.data.ideaBonus ?? null;
|
|
566
|
+
|
|
567
|
+
printGradeOutcome(g, taskId);
|
|
568
|
+
} else if (!passed && taskStatus === 'completed') {
|
|
569
|
+
// task 1002605 (G-18): the --allow-smoke-fail continuation. The resolve above
|
|
570
|
+
// consumed the claim with verification.passed=false, so the grader deliberately
|
|
571
|
+
// does not run. This used to be a silent fall-through; say where the task is.
|
|
572
|
+
console.log('');
|
|
573
|
+
for (const line of smokeGateMessages(taskId).parked) console.log(line);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
printCreditsAndAchievements({
|
|
577
|
+
postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo,
|
|
578
|
+
justUnlocked: postGradeJustUnlocked,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ---- Step 3: confirmed→shipped tail (merge + deploy → memory/session sync →
|
|
582
|
+
// Done → onboarding nudge → completion card), shared with regradeMain via
|
|
583
|
+
// finishShip (#1093/C9). ----
|
|
584
|
+
// Graded. If it passed, only the merge remains — a different crash story.
|
|
585
|
+
if (postGradeStatus === 'confirmed' || postGradeStatus === 'shipped') {
|
|
586
|
+
markShipProgress(postGradeStatus === 'shipped' ? 'landed' : 'graded', taskId);
|
|
587
|
+
}
|
|
588
|
+
await finishShip({
|
|
589
|
+
taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly,
|
|
590
|
+
// task 1270: data for the deterministic completion card.
|
|
591
|
+
card: {
|
|
592
|
+
title: ac.title,
|
|
593
|
+
valueSummary: summary,
|
|
594
|
+
notes,
|
|
595
|
+
files: committedFiles,
|
|
596
|
+
baseline: graderBaseline,
|
|
597
|
+
creditsAwarded: credits,
|
|
598
|
+
// task 1275: a real grade ran → pass/fail + issues; otherwise (--skip-grade
|
|
599
|
+
// or server-side GRADER_BYPASS) it was confirmed without a grade → skipped,
|
|
600
|
+
// so the card reads "grade skipped" rather than implying a real pass.
|
|
601
|
+
grade: gradeObj
|
|
602
|
+
? { passed: gradeObj.passed, issues: gradeObj.issues || [], panel: Array.isArray(gradeObj.workers) ? gradeObj.workers.length : null, skipped: false }
|
|
603
|
+
: { passed: gradePassed, skipped: true },
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------- Re-grade flow (#678) ----------
|
|
609
|
+
//
|
|
610
|
+
// `node scripts/gds/ship.js <id> --regrade` closes the task-595-regrade-gap
|
|
611
|
+
// (re-hit on #600): when a ship lands at `completed` but the subagent grader
|
|
612
|
+
// fails, /builder-ship has already resolved+consumed the claim, leaving no CLI
|
|
613
|
+
// path back in (claim refuses "not ready", PATCH refuses, promote refuses, a
|
|
614
|
+
// fresh ship.js says "no active claim"). The only escapes were the Archon
|
|
615
|
+
// /confirm override (bypasses the grader — see feedback_prefer_regrade_over_override)
|
|
616
|
+
// or hand-building a grader harness (learning #43).
|
|
617
|
+
//
|
|
618
|
+
// This re-runs ONLY the grade step against the committed diff, with no claim:
|
|
619
|
+
// - eligibility: task at `completed` AND (you last held the claim OR Archon)
|
|
620
|
+
// - preflight: working tree committed + ship/grade tooling current
|
|
621
|
+
// - empty-diff guard: the grader reads committed history, so refuse if empty
|
|
622
|
+
// - grade: gradeAndRecord → POST /tasks/:id/grade (rank-open; applyGrade
|
|
623
|
+
// promotes completed→confirmed + awards credits on pass, idempotent)
|
|
624
|
+
// - on pass: the normal confirmed→shipped merge + sync tail runs.
|
|
625
|
+
async function regradeMain() {
|
|
626
|
+
await requireSession();
|
|
627
|
+
const args = process.argv.slice(2);
|
|
628
|
+
const taskId = Number(args[0]);
|
|
629
|
+
if (!Number.isFinite(taskId)) {
|
|
630
|
+
console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary-file <path|-> | --summary "..."] [--response-file <path|-> | --response "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
|
|
631
|
+
process.exit(2);
|
|
632
|
+
}
|
|
633
|
+
const regradeProse = resolveProse(args, ['summary', 'response']);
|
|
634
|
+
const summaryArg = regradeProse.summary;
|
|
635
|
+
// task 1002662: the builder's response to the prior round's findings — how
|
|
636
|
+
// each was fixed, tracked (a task ref), or why it's out of scope. Rides to
|
|
637
|
+
// the panel fenced, under the delta re-grade policy.
|
|
638
|
+
const responseArg = regradeProse.response;
|
|
639
|
+
const skipMerge = args.includes('--no-merge');
|
|
640
|
+
const force = args.includes('--force');
|
|
641
|
+
// task 1002422: honor --base on regrade/resume too, so `--regrade --base <ref>`
|
|
642
|
+
// and shipMain's resume-grade dispatch grade the real <base>...HEAD diff and
|
|
643
|
+
// advance already-on-main work, instead of aborting "nothing to grade". Same
|
|
644
|
+
// 0-ahead security guard as shipMain (applyBaseOverride is the shared gate).
|
|
645
|
+
const baseRefRaw = arg('--base', args);
|
|
646
|
+
const dbOnly = args.includes('--db-only') || args.includes('--allow-empty') || Boolean(baseRefRaw); // task 391 — honor --db-only on a regrade too
|
|
647
|
+
const verified = args.includes('--verified'); // task 1053 (F) — see gate below
|
|
648
|
+
|
|
649
|
+
if (baseRefRaw) {
|
|
650
|
+
const res = applyBaseOverride(baseRefRaw);
|
|
651
|
+
if (!res.ok) {
|
|
652
|
+
console.error(`Regrade aborted: ${res.reason}. Omit --base and re-grade normally.`);
|
|
653
|
+
process.exit(2);
|
|
654
|
+
}
|
|
655
|
+
console.log(`--base: re-grading ${res.sha.slice(0, 9)}...HEAD (work already on main).`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ---- Eligibility: completed + (owner | archon) ----
|
|
659
|
+
const me = noteServerReward(await (await cliClient()).me.getMe());
|
|
660
|
+
if (!me.ok) {
|
|
661
|
+
console.error('failed to load profile:', me.data);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const builder = me.data.builder || {};
|
|
665
|
+
// ?include=grade,last_claim — the claim-holder lookup for eligibility (#678)
|
|
666
|
+
// + the stored grade row for the delta-aware prior-round payload (task 1002662).
|
|
667
|
+
const tRes = await (await cliClient()).tasks.getTasksId({ id: taskId, query: { include: 'grade,last_claim' } });
|
|
668
|
+
if (!tRes.ok) {
|
|
669
|
+
if (tRes.status === 404) {
|
|
670
|
+
console.error(`Task #${taskId} not found.`);
|
|
671
|
+
} else {
|
|
672
|
+
console.error(`failed to load task #${taskId} (${tRes.status}):`, tRes.data);
|
|
673
|
+
}
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
const task = tRes.data.task;
|
|
677
|
+
const lastClaim = tRes.data.last_claim || null;
|
|
678
|
+
// pushVia() decides whether /merge-mode is even runnable here (task 1003221).
|
|
679
|
+
const elig = evaluateRegradeEligibility({ task, lastClaim, builder, pushMode: pushVia() });
|
|
680
|
+
if (!elig.ok) {
|
|
681
|
+
console.error(elig.reason);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
// task 1053 (F): the resume/regrade path promotes completed→confirmed via the
|
|
685
|
+
// grade (applyGrade), which the server-side /confirm verify guard does NOT
|
|
686
|
+
// cover. Gate it here so a verification task can't be confirmed by simply
|
|
687
|
+
// re-running without attesting the verify passed (closing the bypass).
|
|
688
|
+
if (task.kind === 'verify' && !verified) {
|
|
689
|
+
console.error('');
|
|
690
|
+
console.error(`Task #${taskId} is a VERIFICATION task — it won't confirm until you attest the verify actually passed.`);
|
|
691
|
+
console.error(`Run the verification, then re-run with --verified:`);
|
|
692
|
+
console.error(` node scripts/gds/ship.js ${taskId} --verified`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
console.log(`Re-grading task #${taskId}: ${task.title}`);
|
|
696
|
+
console.log(` eligibility: ${elig.reason} (${elig.via})`);
|
|
697
|
+
|
|
698
|
+
// A grade-failed ship never reached the merge/broadcast step, so a passing
|
|
699
|
+
// re-grade is the FIRST time this task actually ships. Fall back to the
|
|
700
|
+
// task's stored value_summary so the merge commit + Discord #ship-news post
|
|
701
|
+
// carry meaningful text even when --summary is omitted.
|
|
702
|
+
const summary = summaryArg || task.value_summary || '';
|
|
703
|
+
|
|
704
|
+
// The grader helpers want a claim-like object for touches[] (preflight's
|
|
705
|
+
// working-tree-drift check) and head_sha (baseline fallback). The claim is
|
|
706
|
+
// gone, so synthesize one from the task row + the last claim's head_sha.
|
|
707
|
+
const claimLike = { touches: task.touches || [], head_sha: lastClaim?.head_sha ?? null, task_id: taskId, title: task.title };
|
|
708
|
+
|
|
709
|
+
// ---- Preflight: work committed + ship/grade tooling current ----
|
|
710
|
+
// No scanner/smoke re-run — --regrade re-runs ONLY the grade step. But the
|
|
711
|
+
// grader judges COMMITTED history, so uncommitted fixes on touches[] files
|
|
712
|
+
// wouldn't be graded — refuse that (it would grade a stale diff). Also refuse
|
|
713
|
+
// if the local ship/grade tooling is behind main (a stale grader silently
|
|
714
|
+
// fails the same way — the #662 lesson). --force overrides the freshness leg.
|
|
715
|
+
console.log('Preflight (working tree + branch freshness)...');
|
|
716
|
+
const pre = preflight(claimLike, { force });
|
|
717
|
+
if (!pre.ok) {
|
|
718
|
+
console.error('');
|
|
719
|
+
console.error(`Re-grade aborted: ${pre.reason}. Task stays at completed.`);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ---- Empty-diff guard (the grader reads committed history) ----
|
|
724
|
+
const graderBaseline = computeGraderBaseline(claimLike);
|
|
725
|
+
const committedFiles = computeChangedFiles(graderBaseline);
|
|
726
|
+
if (committedFiles.length === 0) {
|
|
727
|
+
console.error(`Nothing to grade: the committed diff vs ${graderBaseline || 'baseline'} is empty.`);
|
|
728
|
+
console.error('The grader reads committed history, not the working tree — commit your fixes on this branch, then re-run --regrade.');
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ---- Delta-aware prior-round context (task 1002662) ----
|
|
733
|
+
// The prior grade's findings + the builder's response + the interim fix diff
|
|
734
|
+
// ride to the panel so an already-answered finding can't re-veto on
|
|
735
|
+
// re-wording alone. The interim range comes from the prior grade's
|
|
736
|
+
// signals.graded_head_sha (recorded by runSubagentGrader since this task);
|
|
737
|
+
// older grade rows lack it — the findings still ride, just without the diff.
|
|
738
|
+
const storedGradeRow = tRes.data.grade ?? null;
|
|
739
|
+
let interimCommits = '';
|
|
740
|
+
let interimDiffStat = '';
|
|
741
|
+
const priorSha = storedGradeRow?.signals?.graded_head_sha;
|
|
742
|
+
// The sha comes from a DB JSONB blob — validate the shape before shelling out.
|
|
743
|
+
if (typeof priorSha === 'string' && /^[0-9a-f]{7,40}$/i.test(priorSha) && gitOk(`git rev-parse --verify --quiet ${priorSha}`)) {
|
|
744
|
+
interimCommits = gitOk(`git log --oneline ${priorSha}..HEAD`) || '';
|
|
745
|
+
interimDiffStat = gitOk(`git diff --stat ${priorSha}..HEAD`) || '';
|
|
746
|
+
}
|
|
747
|
+
const priorRound = buildPriorRoundContext({
|
|
748
|
+
storedGrade: storedGradeRow,
|
|
749
|
+
responseText: responseArg,
|
|
750
|
+
interimCommits,
|
|
751
|
+
interimDiffStat,
|
|
752
|
+
});
|
|
753
|
+
if (priorRound) {
|
|
754
|
+
console.log(` prior round context: ${priorRound.audit.prior_findings_count} finding(s) from the prior grade${priorRound.audit.response_present ? ' + builder response' : ''}${priorRound.audit.interim_present ? ' + interim fix diff' : ''} → round ${priorRound.audit.round}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ---- Grade step (the only step) ----
|
|
758
|
+
console.log('');
|
|
759
|
+
console.log('Running subagent quality grader...');
|
|
760
|
+
const gradePost = await gradeAndRecord({
|
|
761
|
+
taskId,
|
|
762
|
+
task,
|
|
763
|
+
summary,
|
|
764
|
+
baseline: graderBaseline,
|
|
765
|
+
committedFiles,
|
|
766
|
+
priorRound,
|
|
767
|
+
});
|
|
768
|
+
if (!gradePost.ok) {
|
|
769
|
+
console.error(`grade POST failed (${gradePost.status}):`, gradePost.data);
|
|
770
|
+
console.error('Task stays at completed. Fix the issue and re-run --regrade (unlimited, no attempt cap).');
|
|
771
|
+
console.error('(POST /confirm is a last resort — it is permission-gated on task.confirm.grade_bypass.)');
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const g = gradePost.data.grade;
|
|
776
|
+
const postGradeStatus = gradePost.data.taskStatus;
|
|
777
|
+
printGradeOutcome(g, taskId);
|
|
778
|
+
printCreditsAndAchievements({
|
|
779
|
+
postGradeStatus,
|
|
780
|
+
credits: gradePost.data.creditsAwarded,
|
|
781
|
+
creditsRaw: gradePost.data.creditsRaw ?? null,
|
|
782
|
+
creditsMultiplier: gradePost.data.creditsMultiplier ?? null,
|
|
783
|
+
ideaBonusInfo: gradePost.data.ideaBonus ?? null,
|
|
784
|
+
justUnlocked: gradePost.data.just_unlocked || [],
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// On a pass the task is now confirmed — run the SAME confirmed→shipped tail as a
|
|
788
|
+
// normal ship (finishShip, #1093/C9) so a re-grade lands identically.
|
|
789
|
+
await finishShip({
|
|
790
|
+
taskId, summary, advanceToMerge: gradePost.data.advanceToMerge, skipMerge, dbOnly,
|
|
791
|
+
postGradeStatus,
|
|
792
|
+
doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
|
|
793
|
+
// task 1270: completion card for the re-grade path too.
|
|
794
|
+
card: {
|
|
795
|
+
title: task.title,
|
|
796
|
+
valueSummary: summary,
|
|
797
|
+
notes: '',
|
|
798
|
+
files: committedFiles,
|
|
799
|
+
baseline: graderBaseline,
|
|
800
|
+
creditsAwarded: gradePost.data.creditsAwarded,
|
|
801
|
+
grade: { passed: g.passed, issues: g.issues || [], panel: Array.isArray(g.workers) ? g.workers.length : null },
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Pure helpers are exported for unit tests (tests/session_records.mjs). The
|
|
807
|
+
// orchestration only runs when ship.js is invoked directly — so `require()`ing
|
|
808
|
+
// this module (a test) does NOT kick off a ship. (uploadSessionOnShip is not
|
|
809
|
+
// exported: it does I/O + network and is covered by the live smoke at ship.)
|
|
810
|
+
|
|
811
|
+
module.exports = {
|
|
812
|
+
regradeMain,
|
|
813
|
+
shipMain,
|
|
814
|
+
};
|