@bongos/core 1.19.318 → 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 +125 -40
- 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/go-live.js +142 -48
- 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/gds/update-channel.js +12 -0
- package/scripts/status-probe.mjs +30 -2
- package/src/module-api.js +5 -1
- package/tests/go_live.mjs +65 -32
- 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
- package/tests/update_channel.mjs +33 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// scripts/gds/ship-grade.js — grader baseline/diff/fingerprint, the subagent-grader runner and the smoke runners.
|
|
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 1763-2049 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 crypto = require('node:crypto');
|
|
12
|
+
const { spawn } = require('node:child_process');
|
|
13
|
+
const { cliClient } = require('./cli-lib');
|
|
14
|
+
const grader = require('../../modules/grading/grader');
|
|
15
|
+
const { resolveBuilderModel } = require('./ship-memory-sync.js');
|
|
16
|
+
const { argvOut, gitArgvOk, gitOk, shellOk } = require('./ship-git.js');
|
|
17
|
+
const { GRADER_DIFF_CHAR_CAP, GRADER_TIMEOUT_MS, REPO_ROOT, noteServerReward } = require('./ship-state.js');
|
|
18
|
+
|
|
19
|
+
// ---------- Step 1+2: local verification ----------
|
|
20
|
+
|
|
21
|
+
// V3.R18 (#209) → MAS Phase 1 #395: inputs for the subagent grader. The grader
|
|
22
|
+
// needs three things from the local working tree: the changed files list, the
|
|
23
|
+
// full unified diff against the baseline, and (for the audit-trail signals)
|
|
24
|
+
// the shortstat summary.
|
|
25
|
+
//
|
|
26
|
+
// task 1002422 (blocker 1000082): a builder-settable pre-task baseline. When a
|
|
27
|
+
// task's artifact reached main BEFORE the ship (a release task: package-core
|
|
28
|
+
// archives committed HEAD, so the version bump must merge first), merge-base(
|
|
29
|
+
// origin/main, HEAD) == HEAD → an EMPTY committed diff → the --allow-empty /
|
|
30
|
+
// skip-grade / Archon-only /confirm dead-end that NO sub-Archon builder can
|
|
31
|
+
// finish. --base <ref> pins the pre-task commit so the grader reads the REAL diff
|
|
32
|
+
// <base>...HEAD and the ship confirms via the rank-open graded path instead. Set
|
|
33
|
+
// once in main() after resolveBaseRef() validates it; null on a normal ship.
|
|
34
|
+
let baseOverrideSha = null;
|
|
35
|
+
|
|
36
|
+
// resolveBaseRef — validate + resolve a --base <ref> to a commit sha. Injection-
|
|
37
|
+
// safe: the ref reaches git as a literal argv element (execFile, no shell), never
|
|
38
|
+
// a string interpolation. Pure over injected git runners so the unit test drives
|
|
39
|
+
// it with fakes (no real repo). Returns { ok:true, sha } or { ok:false, reason }.
|
|
40
|
+
function resolveBaseRef(rawBase, { revParse, isAncestor } = {}) {
|
|
41
|
+
const ref = String(rawBase == null ? '' : rawBase).trim();
|
|
42
|
+
if (!ref) return { ok: false, reason: 'empty ref' };
|
|
43
|
+
// Conservative allowlist (defence-in-depth even though transport is argv): no
|
|
44
|
+
// whitespace, no leading dash (git would read it as a flag), no shell metachar.
|
|
45
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/~^-]*$/.test(ref)) {
|
|
46
|
+
return { ok: false, reason: `invalid ref syntax "${ref}"` };
|
|
47
|
+
}
|
|
48
|
+
const rp = revParse || ((r) => argvOut('git', ['rev-parse', '--verify', '--quiet', `${r}^{commit}`]));
|
|
49
|
+
const sha = rp(ref);
|
|
50
|
+
if (!sha) return { ok: false, reason: `"${ref}" is not a commit this repo knows` };
|
|
51
|
+
const anc = isAncestor || ((s) => gitArgvOk(['merge-base', '--is-ancestor', s, 'HEAD']));
|
|
52
|
+
if (!anc(sha)) {
|
|
53
|
+
return { ok: false, reason: `"${ref}" (${sha.slice(0, 9)}) is not an ancestor of HEAD — --base must be a commit your work built on top of` };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, sha };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// baseRefShipGuard — the SECURITY invariant behind --base. --base decouples the
|
|
59
|
+
// GRADED diff (<base>...HEAD) from what the merge leg lands (the whole branch:
|
|
60
|
+
// autoMerge runs `git merge <branch>`, bringing every commit ahead of main, not
|
|
61
|
+
// just <base>...HEAD). So if HEAD has commits AHEAD of origin/main, honoring
|
|
62
|
+
// --base would let the grader review only a SUBSET while the full branch merges —
|
|
63
|
+
// an unreviewed-code path onto main. --base is therefore ONLY valid for work
|
|
64
|
+
// ALREADY fully on main (its documented premise: "nothing left to merge"). Pure
|
|
65
|
+
// over the ahead-count string (git rev-list --count origin/main..HEAD) so it is
|
|
66
|
+
// unit-tested without a repo. '0' → allowed; anything else (or null) → refuse.
|
|
67
|
+
function baseRefShipGuard(aheadCount) {
|
|
68
|
+
if (aheadCount === '0') return { ok: true };
|
|
69
|
+
const n = aheadCount == null ? 'an unknown number of' : aheadCount;
|
|
70
|
+
return { ok: false, reason: `HEAD is ${n} commit(s) ahead of origin/main — --base is only for work already merged to main` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// applyBaseOverride — the SINGLE source of truth for --base validation, shared by
|
|
74
|
+
// shipMain + regradeMain so the security guard can never diverge between entry
|
|
75
|
+
// points. Resolves the ref, enforces the 0-ahead invariant, and sets the module-
|
|
76
|
+
// level baseOverrideSha on success. Returns { ok:true, sha } | { ok:false, reason }
|
|
77
|
+
// (the caller prints + exits). runGit is injectable for tests.
|
|
78
|
+
function applyBaseOverride(baseRefRaw, { runGit = gitOk } = {}) {
|
|
79
|
+
const r = resolveBaseRef(baseRefRaw);
|
|
80
|
+
if (!r.ok) return { ok: false, reason: `--base ${baseRefRaw} is unusable (${r.reason})` };
|
|
81
|
+
// Freshen origin/main (best-effort; a stale ref only OVER-counts → errs to refuse).
|
|
82
|
+
runGit('git fetch origin main --quiet');
|
|
83
|
+
const guard = baseRefShipGuard(runGit('git rev-list --count origin/main..HEAD'));
|
|
84
|
+
if (!guard.ok) return { ok: false, reason: guard.reason };
|
|
85
|
+
baseOverrideSha = r.sha;
|
|
86
|
+
return { ok: true, sha: r.sha };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// #536: the grader's baseline is computed INDEPENDENTLY of touches[] — always
|
|
90
|
+
// from merge-base, never gated on the declared scope. (Historically the
|
|
91
|
+
// touches-gated drift scanner returned a null baseline for empty touches[],
|
|
92
|
+
// which once blinded the grader to a fully-committed diff and hard-FAILed
|
|
93
|
+
// empty-touches newcomer ships; the scanner is gone as of task 879, but the
|
|
94
|
+
// grader keeping its own merge-base baseline remains the correct invariant.)
|
|
95
|
+
// baseSha (task 1002422): a validated --base override wins over merge-base, so a
|
|
96
|
+
// task whose commits already landed on main still grades its real diff instead of
|
|
97
|
+
// seeing an empty one.
|
|
98
|
+
function computeGraderBaseline(claim, baseSha = baseOverrideSha) {
|
|
99
|
+
if (baseSha) return baseSha;
|
|
100
|
+
return (
|
|
101
|
+
gitOk('git merge-base origin/main HEAD') ||
|
|
102
|
+
gitOk('git merge-base main HEAD') ||
|
|
103
|
+
(claim && claim.head_sha) ||
|
|
104
|
+
null
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function computeChangedFiles(baseline) {
|
|
109
|
+
if (!baseline) return [];
|
|
110
|
+
const out = gitOk(`git diff --name-only ${baseline}...HEAD`);
|
|
111
|
+
return out ? out.split('\n').map((s) => s.trim()).filter(Boolean) : [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function computeDiffText(baseline) {
|
|
115
|
+
if (!baseline) return '';
|
|
116
|
+
// -M to track renames; --no-color so the subagent gets clean diff lines.
|
|
117
|
+
const diff = gitOk(`git diff --no-color -M ${baseline}...HEAD`);
|
|
118
|
+
if (!diff) return '';
|
|
119
|
+
if (diff.length > GRADER_DIFF_CHAR_CAP) {
|
|
120
|
+
const head = diff.slice(0, GRADER_DIFF_CHAR_CAP);
|
|
121
|
+
return `${head}\n\n[diff truncated — original was ${diff.length} chars, capped at ${GRADER_DIFF_CHAR_CAP}. The subagent can Read any file in its post-resolve state for the full picture.]`;
|
|
122
|
+
}
|
|
123
|
+
return diff;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function computeDiffShortstat(baseline) {
|
|
127
|
+
if (!baseline) return null;
|
|
128
|
+
const summary = gitOk(`git diff --shortstat ${baseline}...HEAD`);
|
|
129
|
+
if (!summary) return { files_changed: 0, lines_added: 0, lines_removed: 0 };
|
|
130
|
+
const files = /(\d+)\s+files?\s+changed/.exec(summary);
|
|
131
|
+
const ins = /(\d+)\s+insertions?\(\+\)/.exec(summary);
|
|
132
|
+
const del = /(\d+)\s+deletions?\(-\)/.exec(summary);
|
|
133
|
+
return {
|
|
134
|
+
files_changed: files ? Number(files[1]) : 0,
|
|
135
|
+
lines_added: ins ? Number(ins[1]) : 0,
|
|
136
|
+
lines_removed: del ? Number(del[1]) : 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------- V4.R08 — diff fingerprint + grade reuse ----------
|
|
141
|
+
|
|
142
|
+
// SHA-256 of the raw diff string. Stable identifier for "has the committed
|
|
143
|
+
// diff changed?" — used as the key to skip re-running the adversarial panel
|
|
144
|
+
// when the diff is byte-for-byte identical to the last PASS.
|
|
145
|
+
function diffHash(rawDiff) {
|
|
146
|
+
return crypto.createHash('sha256').update(rawDiff || '').digest('hex');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compute the hash of the full (untruncated) diff against baseline. Returns
|
|
150
|
+
// null when baseline is unavailable so callers can treat null as "unknown".
|
|
151
|
+
function computeRawDiffHash(baseline) {
|
|
152
|
+
if (!baseline) return null;
|
|
153
|
+
const raw = gitOk(`git diff --no-color -M ${baseline}...HEAD`);
|
|
154
|
+
return diffHash(raw || '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Pure predicate: should we reuse the stored grade instead of running the
|
|
158
|
+
// panel again? True when a PASS grade exists for the exact same diff hash.
|
|
159
|
+
// A null storedGrade, a non-PASS grade, or a hash mismatch all return false.
|
|
160
|
+
function shouldReuseGrade(storedGrade, currentDiffHash) {
|
|
161
|
+
if (!storedGrade || !storedGrade.passed) return false;
|
|
162
|
+
if (!storedGrade.diff_hash || !currentDiffHash) return false;
|
|
163
|
+
return storedGrade.diff_hash === currentDiffHash;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// task 1002660: task_grades has no issues column — the panel's deduped
|
|
167
|
+
// issues[] survive only inside signals.workers[].issues. Rebuild them (same
|
|
168
|
+
// (file, summary) dedupe as aggregatePanel) so the reuse re-POST carries the
|
|
169
|
+
// substance the original PASS carried. Before this, the reused body posted
|
|
170
|
+
// issues: undefined, so every reused PASS with findings read as a zero-issue
|
|
171
|
+
// pass server-side and tripped the SR-9 plausibility guard into
|
|
172
|
+
// manual-confirm — 0/106 successful uses, the feature defeated itself.
|
|
173
|
+
function reconstructPanelIssues(signals) {
|
|
174
|
+
const out = [];
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const w of (Array.isArray(signals?.workers) ? signals.workers : [])) {
|
|
177
|
+
for (const it of (Array.isArray(w?.issues) ? w.issues : [])) {
|
|
178
|
+
if (!it || typeof it !== 'object') continue;
|
|
179
|
+
const key = `${it.file || ''}|${it.summary || ''}`;
|
|
180
|
+
if (seen.has(key)) continue;
|
|
181
|
+
seen.add(key);
|
|
182
|
+
out.push(it);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Subagent grader runner ---
|
|
189
|
+
// Wraps grader.gradeSubagent so ship.js callers see a single function with
|
|
190
|
+
// progress logging. MAS Phase 2.0 #410: gradeSubagent now runs the WORKER
|
|
191
|
+
// PANEL (Narc + Quality; Hacker / Efficiency Monger added in 2.1/2.2) in
|
|
192
|
+
// parallel. Each worker is a specialized prompt with its own verdict; the
|
|
193
|
+
// panel gate is unanimous PASS — any 'fail' blocks, any 'concerns' falls
|
|
194
|
+
// through to manual-confirm.
|
|
195
|
+
async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: precomputedChanged, priorRound = null }) {
|
|
196
|
+
// Reuse the caller's changed-files list when provided (the empty-diff guard
|
|
197
|
+
// computes it just before calling us) so we don't run `git diff --name-only`
|
|
198
|
+
// twice per ship (#513).
|
|
199
|
+
const changedFiles = precomputedChanged || computeChangedFiles(baseline);
|
|
200
|
+
const diff = computeDiffText(baseline);
|
|
201
|
+
const shortstat = computeDiffShortstat(baseline);
|
|
202
|
+
// SEC #863: feed the grader the builder's LIVE rank so the deterministic
|
|
203
|
+
// permission-path pre-pass can hard-fail a sub-Metic diff that touches the
|
|
204
|
+
// rank/permission/ship-grade/deploy machinery. Best-effort read; if it fails
|
|
205
|
+
// the pre-pass fails OPEN (the pre-push hook + post-push main audit are the
|
|
206
|
+
// fail-closed layers).
|
|
207
|
+
let builderRank;
|
|
208
|
+
try {
|
|
209
|
+
const meRes = noteServerReward(await (await cliClient()).me.getMe());
|
|
210
|
+
if (meRes.ok) builderRank = meRes.data?.builder?.rank;
|
|
211
|
+
} catch (_) { /* unknown rank → pre-pass fails open; audit + hook still gate */ }
|
|
212
|
+
const runOpts = {
|
|
213
|
+
timeoutMs: GRADER_TIMEOUT_MS,
|
|
214
|
+
cwd: process.cwd(), // grade the FEATURE worktree, not REPO_ROOT
|
|
215
|
+
};
|
|
216
|
+
// task 1002659: the real session model (transcript-derived) drives the
|
|
217
|
+
// cross-model grader pick; null → the grader assumes, as before.
|
|
218
|
+
const builderModel = resolveBuilderModel();
|
|
219
|
+
const t0 = Date.now();
|
|
220
|
+
let result;
|
|
221
|
+
// #513: a small copy/docs-only diff gets a SINGLE generalist grader shot
|
|
222
|
+
// rather than the full adversarial panel — "lighter, not skipped". The 1-shot
|
|
223
|
+
// still returns ff+cq+economy+confidence so the server gate applies; the
|
|
224
|
+
// deterministic scanner + smoke + route-rank checks ran regardless. Fixes the
|
|
225
|
+
// inefficiency of running a 4-worker panel on a +1/-1 copy tweak.
|
|
226
|
+
if (grader.isTrivialDiff({ changedFiles, diffStats: shortstat })) {
|
|
227
|
+
const totalLines = (shortstat.lines_added || 0) + (shortstat.lines_removed || 0);
|
|
228
|
+
console.log(` subagent grader ${grader.VERSION}: TRIVIAL diff (${shortstat.files_changed} file(s), ${totalLines} line(s), copy/docs only) → single generalist shot, model=${grader.selectGraderModel({ builderModel })} — instead of the ${grader.WORKERS.DEFAULT_PANEL.length}-worker panel (#513)`);
|
|
229
|
+
console.log(` gate: the shot must return functional_fidelity>=${grader.THRESHOLD} AND code_quality>=${grader.THRESHOLD} AND confidence>=0.4`);
|
|
230
|
+
result = await grader.gradeSubagentV3MultiShot({
|
|
231
|
+
task,
|
|
232
|
+
valueSummary,
|
|
233
|
+
diff,
|
|
234
|
+
changedFiles,
|
|
235
|
+
diffStats: shortstat,
|
|
236
|
+
shots: 1,
|
|
237
|
+
builderModel, // task 1002659: real session model → cross-model grader pick
|
|
238
|
+
runOpts,
|
|
239
|
+
});
|
|
240
|
+
// task 1002660: the single generalist shot has no Hacker or Efficiency
|
|
241
|
+
// lens — acceptable for genuinely inert copy/docs files, but never
|
|
242
|
+
// silently. The marker makes the dropped lenses queryable by /grade-audit
|
|
243
|
+
// (same pattern as implausible_pass_waived, ADR 0164).
|
|
244
|
+
result.signals = {
|
|
245
|
+
...(result.signals || {}),
|
|
246
|
+
trivial_path: true,
|
|
247
|
+
lenses_dropped: ['hacker', 'efficiency'],
|
|
248
|
+
};
|
|
249
|
+
} else {
|
|
250
|
+
const panel = grader.WORKERS.DEFAULT_PANEL;
|
|
251
|
+
console.log(` subagent grader ${grader.VERSION}: panel of ${panel.length} workers (${panel.join(', ')}), model=${grader.selectGraderModel({ builderModel })} (builder ${builderModel || `${grader.ASSUMED_BUILDER_MODEL} assumed`}), changed_files=${changedFiles.length}, diff_chars=${diff.length}, per-worker timeout=${GRADER_TIMEOUT_MS}ms`);
|
|
252
|
+
console.log(` gate: every worker must return verdict='pass'; 'concerns' → manual-confirm fall-through; 'fail' blocks`);
|
|
253
|
+
if (priorRound) {
|
|
254
|
+
console.log(` delta re-grade (task 1002662): prior round's findings + builder response ride along, fenced; answered findings can only re-veto with NEW evidence`);
|
|
255
|
+
}
|
|
256
|
+
result = await grader.gradeSubagent({
|
|
257
|
+
task,
|
|
258
|
+
valueSummary,
|
|
259
|
+
diff,
|
|
260
|
+
changedFiles,
|
|
261
|
+
diffStats: shortstat, // V3.R92 #353: feed diff size into the economy axis
|
|
262
|
+
builderRank, // SEC #863: drives the permission-path pre-pass
|
|
263
|
+
builderModel, // task 1002659: real session model → cross-model grader pick
|
|
264
|
+
priorRound, // task 1002662: delta-aware re-grade context (panel path only)
|
|
265
|
+
runOpts,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const wallMs = Date.now() - t0;
|
|
269
|
+
// Decorate signals with the local shortstat — useful in audit-trail.
|
|
270
|
+
// graded_head_sha (task 1002662): record WHAT commit this grade judged, so a
|
|
271
|
+
// later --regrade can compute the interim fix diff (prior graded HEAD → now)
|
|
272
|
+
// for the delta-aware prior-round payload.
|
|
273
|
+
result.signals = {
|
|
274
|
+
...(result.signals || {}),
|
|
275
|
+
diff_stats: shortstat,
|
|
276
|
+
wall_ms: wallMs,
|
|
277
|
+
changed_files_count: changedFiles.length,
|
|
278
|
+
graded_head_sha: gitOk('git rev-parse HEAD') || null,
|
|
279
|
+
};
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function runSmoke() {
|
|
284
|
+
// `cwd: REPO_ROOT` + a relative script path is robust to spaces or other
|
|
285
|
+
// shell metacharacters that may appear in the absolute repo path on some
|
|
286
|
+
// machines.
|
|
287
|
+
const okPms = shellOk(`"${process.execPath}" scripts/gds/smoke-gds.js`, { cwd: REPO_ROOT });
|
|
288
|
+
return { ran: true, clean: okPms };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// task 1002660: the async twin of runSmoke for the PRE-RESOLVE gate only. The
|
|
292
|
+
// sync runSmoke blocks the event loop (execSync), which would freeze the
|
|
293
|
+
// overlapped grade-panel promise mid-flight; awaiting this one keeps the loop
|
|
294
|
+
// free so the panel's subprocess callbacks progress while smoke runs. The
|
|
295
|
+
// post-merge smoke call keeps the sync form (nothing overlaps there).
|
|
296
|
+
function runSmokeAsync() {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const child = spawn(process.execPath, ['scripts/gds/smoke-gds.js'], {
|
|
299
|
+
cwd: REPO_ROOT, stdio: 'inherit', windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
child.on('error', () => resolve({ ran: true, clean: false }));
|
|
302
|
+
child.on('close', (code) => resolve({ ran: true, clean: code === 0 }));
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
applyBaseOverride,
|
|
308
|
+
baseRefShipGuard,
|
|
309
|
+
computeChangedFiles,
|
|
310
|
+
computeGraderBaseline,
|
|
311
|
+
computeRawDiffHash,
|
|
312
|
+
diffHash,
|
|
313
|
+
reconstructPanelIssues,
|
|
314
|
+
resolveBaseRef,
|
|
315
|
+
runSmoke,
|
|
316
|
+
runSmokeAsync,
|
|
317
|
+
runSubagentGrader,
|
|
318
|
+
shouldReuseGrade,
|
|
319
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// scripts/gds/ship-io.js — API-error normalization and the small terminal effects (bell, post-land outcome).
|
|
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 2928-3007 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 os = require('node:os');
|
|
12
|
+
const { playSound } = require('./ship-state.js');
|
|
13
|
+
|
|
14
|
+
function playShipBell() {
|
|
15
|
+
let shipWav = 'ship';
|
|
16
|
+
try {
|
|
17
|
+
const prefsPath = require('path').join(os.homedir(), '.config', 'otb', 'sound-prefs.json');
|
|
18
|
+
const raw = require('fs').readFileSync(prefsPath, 'utf8');
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
const ev = parsed && parsed.event_sounds && parsed.event_sounds['task-shipped'];
|
|
21
|
+
if (typeof ev === 'string') shipWav = ev;
|
|
22
|
+
} catch (_) { /* fall through to default */ }
|
|
23
|
+
playSound(shipWav);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// task 1002809: normalize an API error body across BOTH wire shapes — the live
|
|
27
|
+
// ADR 0116 (R01) envelope `{ error: { code, message } }` and the legacy flat
|
|
28
|
+
// `{ error: 'code' }`. Same normalization as claim.js:formatClaimFailure and
|
|
29
|
+
// cli-lib:isBoxScope; pure + exported for tests/ship_post_land_verify.mjs.
|
|
30
|
+
function normalizeApiError(data) {
|
|
31
|
+
const errRaw = data ? data.error : undefined;
|
|
32
|
+
const isEnvelope = errRaw !== null && typeof errRaw === 'object';
|
|
33
|
+
return {
|
|
34
|
+
code: isEnvelope ? errRaw.code : errRaw,
|
|
35
|
+
message: isEnvelope ? errRaw.message : undefined,
|
|
36
|
+
// task 1002789: `details` is where the envelope carries the structured
|
|
37
|
+
// payload the flat shape used to put at the TOP level (claimWorktree,
|
|
38
|
+
// step/reason, floors). A site that reads `data.<field>` directly is
|
|
39
|
+
// reading a field that moved. Fall back to the body so a legacy flat
|
|
40
|
+
// response still resolves.
|
|
41
|
+
details: (isEnvelope && errRaw.details && typeof errRaw.details === 'object')
|
|
42
|
+
? errRaw.details
|
|
43
|
+
: (data && typeof data === 'object' ? data : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// task 1002789: the ONE renderer for a failed API call on the ship path.
|
|
48
|
+
// Every generic bail site used to hand the raw body to console.error (or
|
|
49
|
+
// interpolate `data.error` into a template, which stringifies the ADR 0116
|
|
50
|
+
// envelope object to the literal "[object Object]"). Both hide a fully
|
|
51
|
+
// diagnosable server error: console.error's util.inspect stops at depth 2, so
|
|
52
|
+
// the `details` that names WHICH git step failed prints as "[Object]".
|
|
53
|
+
// Collapse the body to one readable line instead, whichever shape it is.
|
|
54
|
+
// Pure + exported for tests/ship_error_shape.mjs.
|
|
55
|
+
function apiErrorLine(data, status) {
|
|
56
|
+
const { code, message, details } = normalizeApiError(data);
|
|
57
|
+
const head = code
|
|
58
|
+
? (message && message !== code ? `${code}: ${message}` : code)
|
|
59
|
+
: (message || (status ? `HTTP ${status}` : 'unknown error'));
|
|
60
|
+
// Only the small scalar fields — a details blob can carry a whole bundle.
|
|
61
|
+
const extras = Object.entries(details || {})
|
|
62
|
+
.filter(([k, v]) => k !== 'code' && k !== 'message' && k !== 'error'
|
|
63
|
+
&& (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
|
|
64
|
+
.map(([k, v]) => `${k}: ${String(v).replace(/\s+/g, ' ').trim()}`);
|
|
65
|
+
const line = extras.length ? `${head} (${extras.join('; ')})` : head;
|
|
66
|
+
return status ? `${line} [HTTP ${status}]` : line;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// task 1002809: classify a FAILED post-land POST /tasks/<id>/ship. The merge
|
|
70
|
+
// already succeeded, so the server may simply have landed the task first
|
|
71
|
+
// (reconcile-on-read — task 1049 — or the ~5-min reconciler racing this call).
|
|
72
|
+
// The old check compared `data.error === 'task_not_confirmed'` — dead against
|
|
73
|
+
// the envelope shape, so the already-landed SUCCESS rendered as a failure with
|
|
74
|
+
// an "[object Object]" reason (the HANDOFF-apex-ui "400 after a successful
|
|
75
|
+
// land" gotcha). liveStatus is the task's status from a verify GET (null when
|
|
76
|
+
// that GET itself failed): 'shipped' is authoritative success whatever the
|
|
77
|
+
// error said; task_not_confirmed with no live status keeps the task-1049
|
|
78
|
+
// benefit of the doubt; any KNOWN non-shipped status stays a loud failure.
|
|
79
|
+
// Pure + exported for tests/ship_post_land_verify.mjs.
|
|
80
|
+
function postLandShipOutcome({ code, liveStatus }) {
|
|
81
|
+
if (liveStatus === 'shipped') {
|
|
82
|
+
return { alreadyShipped: true, why: 'the server landed it first — the failed ship call lost that race, nothing to fix' };
|
|
83
|
+
}
|
|
84
|
+
if (liveStatus == null && code === 'task_not_confirmed') {
|
|
85
|
+
return { alreadyShipped: true, why: 'the server reconciled it from the merged PR' };
|
|
86
|
+
}
|
|
87
|
+
return { alreadyShipped: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 3 of the lifecycle: confirmed → shipped. Merge the branch into main,
|
|
91
|
+
// deploy, flip the task to shipped, and fire the Discord broadcast (only when
|
|
92
|
+
// the change actually deploys — #94 / ADR 0032; best-effort). Side-effecting;
|
|
93
|
+
// the early returns are control flow only.
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
apiErrorLine,
|
|
97
|
+
normalizeApiError,
|
|
98
|
+
playShipBell,
|
|
99
|
+
postLandShipOutcome,
|
|
100
|
+
};
|