@bongos/core 1.19.306 → 1.19.307
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 +28 -18
- package/.claude/skills/builder-ship/SKILL.md +21 -9
- package/.claude/skills/fix-task/SKILL.md +1 -1
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/figma-design-loop.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/cli-lib.js +53 -0
- package/scripts/gds/ship-honesty.js +150 -0
- package/scripts/gds/ship.js +23 -32
- package/src/module-api.js +3 -1
- package/tests/route_rank_check.mjs +117 -9
- package/tests/ship_cannot_lie.mjs +168 -0
- package/tests/ship_smoke_gate.mjs +9 -3
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.307",
|
|
6
|
+
"core_contract": "1.19.307",
|
|
7
|
+
"source_commit": "b8d2ed227fd6cb0b948128e23f0daf0070b50bcc",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-23T20:16:34.186Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 322,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1418,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1762,
|
|
20
|
+
"tree_sha256": "2ce3db5cb1a893df39826469894f19d4acdee021e5729859eaa8d429933be04d",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
{
|
|
98
98
|
"path": ".claude/skills/builder-ship/SKILL.md",
|
|
99
99
|
"mode": "0000644",
|
|
100
|
-
"sha256": "
|
|
100
|
+
"sha256": "78fcb68ce3ff4b1028904acb9d4d0a37dadd8377108fefef248feef010e4c8b1"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"path": ".claude/skills/builder-stage/SKILL.md",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
{
|
|
143
143
|
"path": ".claude/skills/fix-task/SKILL.md",
|
|
144
144
|
"mode": "0000644",
|
|
145
|
-
"sha256": "
|
|
145
|
+
"sha256": "59b29fcdf54fe51e85191d1a66be237ef35432b76b26a10ec67e46bf08b4d1d3"
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"path": ".claude/skills/goal-create/SKILL.md",
|
|
@@ -1662,7 +1662,7 @@
|
|
|
1662
1662
|
{
|
|
1663
1663
|
"path": "docs/recipes/claude-design-loop.md",
|
|
1664
1664
|
"mode": "0000644",
|
|
1665
|
-
"sha256": "
|
|
1665
|
+
"sha256": "389dbbbfb7c941315731d533e300b5d43f40a8aac866c5a1c74c32d5be212a22"
|
|
1666
1666
|
},
|
|
1667
1667
|
{
|
|
1668
1668
|
"path": "docs/recipes/core-release-pipeline.md",
|
|
@@ -1687,7 +1687,7 @@
|
|
|
1687
1687
|
{
|
|
1688
1688
|
"path": "docs/recipes/figma-design-loop.md",
|
|
1689
1689
|
"mode": "0000644",
|
|
1690
|
-
"sha256": "
|
|
1690
|
+
"sha256": "6bfee82bddc07a5743316e310dda1e6cb7266fa7805fc1eed67bc8cc17980b46"
|
|
1691
1691
|
},
|
|
1692
1692
|
{
|
|
1693
1693
|
"path": "docs/recipes/gds-db-backup.md",
|
|
@@ -4577,12 +4577,12 @@
|
|
|
4577
4577
|
{
|
|
4578
4578
|
"path": "package-lock.json",
|
|
4579
4579
|
"mode": "0000644",
|
|
4580
|
-
"sha256": "
|
|
4580
|
+
"sha256": "4815e8ce78e80ac2fff7dad9f77bef508e2f0090459c67e59722b65af3e27294"
|
|
4581
4581
|
},
|
|
4582
4582
|
{
|
|
4583
4583
|
"path": "package.json",
|
|
4584
4584
|
"mode": "0000644",
|
|
4585
|
-
"sha256": "
|
|
4585
|
+
"sha256": "bdeee91142d9d7f3930c99134be76d03f28329222adcf293deaa8f8354ab2cd8"
|
|
4586
4586
|
},
|
|
4587
4587
|
{
|
|
4588
4588
|
"path": "public-docs/index.html",
|
|
@@ -5082,7 +5082,7 @@
|
|
|
5082
5082
|
{
|
|
5083
5083
|
"path": "scripts/gds/cli-lib.js",
|
|
5084
5084
|
"mode": "0000644",
|
|
5085
|
-
"sha256": "
|
|
5085
|
+
"sha256": "5aa658b099b2acda06cdb3aecfe905da7de79f2b12c844faca091ff061454017"
|
|
5086
5086
|
},
|
|
5087
5087
|
{
|
|
5088
5088
|
"path": "scripts/gds/code.js",
|
|
@@ -5784,10 +5784,15 @@
|
|
|
5784
5784
|
"mode": "0000644",
|
|
5785
5785
|
"sha256": "087def1396283b19cb8a6deea4cc7f362d7a2f164d496a587220f0a48568aae6"
|
|
5786
5786
|
},
|
|
5787
|
+
{
|
|
5788
|
+
"path": "scripts/gds/ship-honesty.js",
|
|
5789
|
+
"mode": "0000644",
|
|
5790
|
+
"sha256": "32827178a46cc01c36c422e1adef433be99492e8444a8affbe51e5e8c1e8ce9f"
|
|
5791
|
+
},
|
|
5787
5792
|
{
|
|
5788
5793
|
"path": "scripts/gds/ship.js",
|
|
5789
5794
|
"mode": "0000644",
|
|
5790
|
-
"sha256": "
|
|
5795
|
+
"sha256": "f3005175abc0ba20e958381df6aee02402267e9956da466386656dd4e86ad8cf"
|
|
5791
5796
|
},
|
|
5792
5797
|
{
|
|
5793
5798
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -6242,7 +6247,7 @@
|
|
|
6242
6247
|
{
|
|
6243
6248
|
"path": "src/module-api.js",
|
|
6244
6249
|
"mode": "0000644",
|
|
6245
|
-
"sha256": "
|
|
6250
|
+
"sha256": "90436d6db6e32424c48549f61ad48a3a77f5c7cac1ae16d5559b7db76fee7b4a"
|
|
6246
6251
|
},
|
|
6247
6252
|
{
|
|
6248
6253
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8282,7 +8287,7 @@
|
|
|
8282
8287
|
{
|
|
8283
8288
|
"path": "tests/route_rank_check.mjs",
|
|
8284
8289
|
"mode": "0000644",
|
|
8285
|
-
"sha256": "
|
|
8290
|
+
"sha256": "bdc58224050392ece492ae8e8f12f62a8934d86e3993e0df5b8cb0aac80602e8"
|
|
8286
8291
|
},
|
|
8287
8292
|
{
|
|
8288
8293
|
"path": "tests/route_validation.mjs",
|
|
@@ -8504,6 +8509,11 @@
|
|
|
8504
8509
|
"mode": "0000644",
|
|
8505
8510
|
"sha256": "afdb09a3ee072fea82390a187c12c42d8249fce0ce84644a4b8dff6a643dd009"
|
|
8506
8511
|
},
|
|
8512
|
+
{
|
|
8513
|
+
"path": "tests/ship_cannot_lie.mjs",
|
|
8514
|
+
"mode": "0000644",
|
|
8515
|
+
"sha256": "82f6f951cc50c34860545be0f309e18a796ebb5baa712ccb341e1fd0dd72a233"
|
|
8516
|
+
},
|
|
8507
8517
|
{
|
|
8508
8518
|
"path": "tests/ship_card.mjs",
|
|
8509
8519
|
"mode": "0000644",
|
|
@@ -8552,7 +8562,7 @@
|
|
|
8552
8562
|
{
|
|
8553
8563
|
"path": "tests/ship_smoke_gate.mjs",
|
|
8554
8564
|
"mode": "0000644",
|
|
8555
|
-
"sha256": "
|
|
8565
|
+
"sha256": "6ce7bd5d2a8910e201a9869c8a3f22ef210fd4400fbd14777d8d15d13252a97e"
|
|
8556
8566
|
},
|
|
8557
8567
|
{
|
|
8558
8568
|
"path": "tests/ship_upload_skip.mjs",
|
|
@@ -7,7 +7,7 @@ You are shipping a task on behalf of the current builder. Shipping is the moment
|
|
|
7
7
|
|
|
8
8
|
## What this skill does
|
|
9
9
|
|
|
10
|
-
Runs `node scripts/gds/ship.js <task-id> --notes
|
|
10
|
+
Runs `node scripts/gds/ship.js <task-id> --notes-file notes.md --summary-file summary.txt`.
|
|
11
11
|
|
|
12
12
|
The CLI drives a **three-state lifecycle** with a subagent grader between states (Phase 5, 2026-05-10):
|
|
13
13
|
|
|
@@ -34,7 +34,7 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
|
|
|
34
34
|
|
|
35
35
|
**When you see that block, position the sandbox link prominently for the builder.** `ship.js` also emits the deterministic lifecycle card for the `review` stage (an `[otb-card-html] <path>` marker) — if an in-feed visualization tool is available, render that card (its primary action is **Open in sandbox ↗**, the URL); it's the cleanest way to make the link impossible to miss. Either way, surface the URL as its own standalone, clickable line in chat — a clear callout, NOT buried inside a paragraph of command output — and say in one line what to look at. This is the moment the change is ready to review; the link must be impossible to miss. Let the builder open it, review, and suggest changes; iterate if needed. **Only once the builder explicitly approves, re-run with `node scripts/gds/ship.js <id> --approved`** — that records their sign-off as a durable `sandbox_review` fact on the task (who, when, and the content stamp of exactly what was reviewed). Never pass `--approved` yourself without a real human go; the pause + sign-off IS the feature. `--skip-stage` is the escape hatch (use sparingly — the gate exists so unreviewed game changes don't ride straight to prod). Laptop sessions and non-game diffs are unaffected.
|
|
36
36
|
|
|
37
|
-
2. **Compose the handoff notes (`--notes`)** following the structure in `docs/handoff-template.md`:
|
|
37
|
+
2. **Compose the handoff notes (`--notes-file`)** following the structure in `docs/handoff-template.md`:
|
|
38
38
|
- What shipped (with confidence tags: verified-prod | verified-smoke | implemented-not-verified)
|
|
39
39
|
- What's broken or unverified
|
|
40
40
|
- Manual checks the next session should run first
|
|
@@ -52,16 +52,28 @@ The CLI drives a **three-state lifecycle** with a subagent grader between states
|
|
|
52
52
|
- "Sessions can claim work atomically; merge conflicts can't happen across worktrees."
|
|
53
53
|
No jargon, no file paths, no commit hashes.
|
|
54
54
|
|
|
55
|
-
4. **Run
|
|
55
|
+
4. **Run** — write the prose to files and pass the file (task 1003168):
|
|
56
56
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...handoff markdown...
|
|
60
|
-
EOF
|
|
61
|
-
)" \
|
|
62
|
-
--summary "the one-liner"
|
|
57
|
+
# write notes.md and summary.txt with your editor/Write tool first
|
|
58
|
+
node scripts/gds/ship.js <task-id> --notes-file notes.md --summary-file summary.txt
|
|
63
59
|
```
|
|
64
60
|
|
|
61
|
+
**Use the `-file` forms.** Handoff notes are technical prose, so they contain
|
|
62
|
+
backticks — and a backtick in an inline `--notes "..."` is command-substituted
|
|
63
|
+
by the shell *before* node runs: the span executes, its output replaces it, and
|
|
64
|
+
the rest of the argument list is truncated. That is not hypothetical; it shipped
|
|
65
|
+
a task with **empty notes and no summary**, so the ledger held nothing about a
|
|
66
|
+
session that had happened (learning 1000197). A file argument is the one form
|
|
67
|
+
the shell cannot rewrite. `-` reads stdin if you'd rather pipe.
|
|
68
|
+
|
|
69
|
+
Inline `--notes "..."` / `--summary "..."` still work for short prose with no
|
|
70
|
+
backticks, `$`, or newlines. The older documented form —
|
|
71
|
+
`--notes "$(cat <<'EOF' … EOF)"` — is *also* shell-safe, because the quoted
|
|
72
|
+
heredoc delimiter suppresses expansion; it is simply easy to get subtly wrong,
|
|
73
|
+
and when you do the failure is silent. Passing both `--notes` and
|
|
74
|
+
`--notes-file` is refused rather than resolved by precedence, so nothing you
|
|
75
|
+
wrote is dropped without a word.
|
|
76
|
+
|
|
65
77
|
**Never merge a task branch to main before running `ship.js`.** `ship.js` diffs the claim branch against `main`; an already-merged branch yields an empty diff, and the only exits are `--allow-empty` (dead-ends the task at `completed`, requiring an Archon-only confirm) or reverting the merge. Ship from the branch, then let `ship.js` land it. (This trap was hit on tasks 1002417 and 1002419, producing blockers 1000081/1000082 and an ~8.5-hour stall.)
|
|
66
78
|
|
|
67
79
|
5. **Render the completion card — it IS the summary; do NOT write a prose recap (task 1270).**
|
|
@@ -39,7 +39,7 @@ Runs the recurring "claim, fix, and ship task NNNN" macro end to end. Its own va
|
|
|
39
39
|
|
|
40
40
|
6. **Ship from the claim branch** — see `/builder-ship` for handoff notes, the value summary, and the completion-card rendering contract. In short:
|
|
41
41
|
```
|
|
42
|
-
node scripts/gds/ship.js <id> --notes
|
|
42
|
+
node scripts/gds/ship.js <id> --notes-file notes.md --summary-file summary.txt
|
|
43
43
|
```
|
|
44
44
|
**Never merge the claim branch into `main` before running `ship.js`.** `ship.js` diffs the branch against `main`; a pre-merged branch yields an empty diff, and the only way out is `--allow-empty` (dead-ends the task at `completed`, needing an Archon-only confirm) or reverting the merge. Ship from the branch and let `ship.js` land it — the same trap `/builder-ship` calls out.
|
|
45
45
|
|
|
@@ -113,7 +113,7 @@ The adapter writes files to `src/ui/<surfaceId>/` and updates `config/design-sou
|
|
|
113
113
|
```bash
|
|
114
114
|
git add src/ui/hall-ui/ config/design-sources.json
|
|
115
115
|
git commit -m "ui(hall-ui): Claude Design export (task 1234)"
|
|
116
|
-
node scripts/gds/ship.js 1234 --notes
|
|
116
|
+
node scripts/gds/ship.js 1234 --notes-file notes.md --summary-file summary.txt
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
---
|
|
@@ -98,7 +98,7 @@ The adapter translates frames/text/solid-fill rectangles into `.js` + `.css` fil
|
|
|
98
98
|
```bash
|
|
99
99
|
git add src/ui/hall-ui/ config/design-sources.json
|
|
100
100
|
git commit -m "ui(hall-ui): Figma export (task 1234)"
|
|
101
|
-
node scripts/gds/ship.js 1234 --notes
|
|
101
|
+
node scripts/gds/ship.js 1234 --notes-file notes.md --summary-file summary.txt
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
---
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.307",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.307",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.307",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -266,6 +266,58 @@ function stripBom(s) {
|
|
|
266
266
|
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// argText(name, args, io) — resolve prose passed EITHER inline (`--notes "..."`)
|
|
270
|
+
// or from a file (`--notes-file path`, or `-` for stdin). Returns
|
|
271
|
+
// { value, source, error }: `value` is a string ('' when neither flag is
|
|
272
|
+
// present), `source` is 'inline' | 'file' | 'stdin' | 'none', and `error` is a
|
|
273
|
+
// caller-printable string when the request was refused or unreadable.
|
|
274
|
+
//
|
|
275
|
+
// WHY THIS EXISTS (learning 1000197). Prose handed to a CLI inline goes through
|
|
276
|
+
// the SHELL first. A handoff note containing backticks — which technical prose
|
|
277
|
+
// constantly does — is command-substituted by bash before node ever runs: the
|
|
278
|
+
// backticked span executes, its output replaces it, and the rest of the argument
|
|
279
|
+
// list is truncated. That is not a theoretical hazard; it silently shipped a task
|
|
280
|
+
// with EMPTY notes and no summary, so the ledger recorded nothing about a session
|
|
281
|
+
// that had happened. A file argument is the only form the shell cannot rewrite.
|
|
282
|
+
//
|
|
283
|
+
// BOTH-GIVEN IS AN ERROR, never a silent precedence. Picking one would mean the
|
|
284
|
+
// caller's other input vanished without a word — the same class of quiet loss
|
|
285
|
+
// this helper exists to end (the capture.js --spark/--full refusal precedent).
|
|
286
|
+
function argText(name, args = process.argv.slice(2), io = {}) {
|
|
287
|
+
const readFile = io.readFile || ((f) => require('fs').readFileSync(f, 'utf8'));
|
|
288
|
+
const readStdin = io.readStdin || (() => require('fs').readFileSync(0, 'utf8'));
|
|
289
|
+
const fileFlag = `${name}-file`;
|
|
290
|
+
const inline = arg(name, args);
|
|
291
|
+
// arg() returns undefined (not null) for a flag present as the LAST token, so
|
|
292
|
+
// presence must be tested separately — otherwise `--notes-file` with no path
|
|
293
|
+
// slips past the null check and resolves to an empty string, which is the very
|
|
294
|
+
// silent-empty outcome this helper exists to prevent.
|
|
295
|
+
const filePresent = args.includes(fileFlag) || args.some((a) => a.startsWith(`${fileFlag}=`));
|
|
296
|
+
const file = filePresent ? arg(fileFlag, args) : null;
|
|
297
|
+
|
|
298
|
+
if (inline != null && filePresent) {
|
|
299
|
+
return {
|
|
300
|
+
value: '', source: 'none',
|
|
301
|
+
error: `${name} and ${fileFlag} were both given. Pass one — ${fileFlag} is the safe form for prose `
|
|
302
|
+
+ '(a shell rewrites backticks in an inline value before node sees it). Refusing rather than '
|
|
303
|
+
+ 'silently picking one, so nothing you wrote is dropped without a word.',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (filePresent) {
|
|
307
|
+
if (file == null || file === '' || file.startsWith('--')) {
|
|
308
|
+
return { value: '', source: 'none', error: `${fileFlag} needs a path (or - for stdin).` };
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const raw = file === '-' ? readStdin() : readFile(file);
|
|
312
|
+
return { value: stripBom(String(raw)), source: file === '-' ? 'stdin' : 'file', error: null };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { value: '', source: 'none', error: `could not read ${fileFlag} ${file}: ${err.message}` };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (inline != null) return { value: inline, source: 'inline', error: null };
|
|
318
|
+
return { value: '', source: 'none', error: null };
|
|
319
|
+
}
|
|
320
|
+
|
|
269
321
|
// resolveQuiet(args, env) — true when api.js's stderr status banner should be
|
|
270
322
|
// suppressed: the --quiet flag or the GDS_QUIET env var. Both default to the
|
|
271
323
|
// live process, matching hasFlag()'s own default-args contract. Default OFF —
|
|
@@ -853,6 +905,7 @@ module.exports = {
|
|
|
853
905
|
loadSessionSync,
|
|
854
906
|
saveSession,
|
|
855
907
|
arg,
|
|
908
|
+
argText,
|
|
856
909
|
hasFlag,
|
|
857
910
|
stripBom,
|
|
858
911
|
repairMsysMangledPath,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/ship-honesty.js — a ship must not lose the builder's prose, and
|
|
3
|
+
// must not lie about how far it got (task 1003168 / G04, goal 1000077;
|
|
4
|
+
// criterion guards-ship-cannot-lie).
|
|
5
|
+
//
|
|
6
|
+
// TWO fixes live here because they are one incident. A ship was invoked with
|
|
7
|
+
// handoff notes containing backticks; bash command-substituted them before node
|
|
8
|
+
// ran, truncating the argument list. The task resolved with EMPTY notes, landed
|
|
9
|
+
// at 'completed', and the process died before grading with a bare
|
|
10
|
+
// `fatal: <stack>`. Two failures compounded: the prose was lost (nothing in the
|
|
11
|
+
// ledger about a session that happened), and the status could not say which leg
|
|
12
|
+
// died — 'completed' is ALSO what a grade-failed task looks like.
|
|
13
|
+
//
|
|
14
|
+
// Split out of ship.js, which is ~4600 lines and far over the size ratchet, so
|
|
15
|
+
// adding this there would have regressed excess_lines_total.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { argText } = require('./cli-lib.js');
|
|
20
|
+
|
|
21
|
+
// resolveProse(args, specs) — resolve every prose argument for one invocation
|
|
22
|
+
// through cli-lib.argText, refusing the combinations that would silently lose
|
|
23
|
+
// text. Returns a { [bareName]: string } map, or exits 2 having explained why.
|
|
24
|
+
//
|
|
25
|
+
// The extra rule argText cannot enforce alone: stdin can only be consumed ONCE,
|
|
26
|
+
// so two `-file -` arguments in the same command would hand the first reader
|
|
27
|
+
// everything and the second an empty string. Refuse instead — a ship whose notes
|
|
28
|
+
// silently came back empty is the exact incident this whole task is about
|
|
29
|
+
// (learning 1000197).
|
|
30
|
+
function resolveProse(args, specs) {
|
|
31
|
+
const out = {};
|
|
32
|
+
const stdinUsers = [];
|
|
33
|
+
for (const name of specs) {
|
|
34
|
+
const r = argText(`--${name}`, args);
|
|
35
|
+
if (r.error) {
|
|
36
|
+
console.error(r.error);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
if (r.source === 'stdin') stdinUsers.push(`--${name}-file`);
|
|
40
|
+
out[name] = r.value;
|
|
41
|
+
}
|
|
42
|
+
if (stdinUsers.length > 1) {
|
|
43
|
+
console.error(`${stdinUsers.join(' and ')} both read stdin, which can only be consumed once — `
|
|
44
|
+
+ 'the second would silently come back empty. Put at least one in a real file.');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------- The ship must not lie about how far it got (G04, task 1003168) ----
|
|
51
|
+
//
|
|
52
|
+
// THE INCIDENT. A ship died after the claim resolved and before the grade
|
|
53
|
+
// recorded. resolveClaim had already landed the task at 'completed', so the
|
|
54
|
+
// process printed `fatal: <stack>` and exited — and 'completed' is exactly what
|
|
55
|
+
// a task ALSO looks like when its grade ran and failed. The status could not
|
|
56
|
+
// distinguish "the pipeline stopped here" from "the pipeline got further and the
|
|
57
|
+
// grade said no", and the work went unrecovered because nobody could tell.
|
|
58
|
+
//
|
|
59
|
+
// The cure is not more status values: it is that the process which knows where it
|
|
60
|
+
// died must SAY so on the way out. shipProgress records the last leg that
|
|
61
|
+
// completed; fatalRecoveryLines turns it into an instruction.
|
|
62
|
+
let shipProgress = { stage: 'preflight', taskId: null };
|
|
63
|
+
|
|
64
|
+
// The one writer. Called by ship.js as each leg completes.
|
|
65
|
+
function markShipProgress(stage, taskId) {
|
|
66
|
+
shipProgress = { stage, taskId };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Pure so the wording is pinned by a DB-free test (the smokeGateMessages
|
|
70
|
+
// precedent) — a recovery message that drifts out of date is worse than none.
|
|
71
|
+
//
|
|
72
|
+
// WHY EVERY STAGE POINTS AT A PLAIN RE-RUN, never --regrade: shipMain on a task
|
|
73
|
+
// with no active claim routes by the task's CURRENT status (evaluateResumeAction)
|
|
74
|
+
// — 'completed' resumes the grade, 'confirmed' resumes the merge. --regrade only
|
|
75
|
+
// accepts 'completed' and REFUSES a 'confirmed' task with "its grade passed;
|
|
76
|
+
// nothing to re-grade". A builder reading a crash does not know which leg died,
|
|
77
|
+
// which is the entire problem, so the advice must be the command that works from
|
|
78
|
+
// either — and a plain re-run of a 'completed' task calls regradeMain() anyway.
|
|
79
|
+
function fatalRecoveryLines(progress = shipProgress) {
|
|
80
|
+
const id = progress.taskId;
|
|
81
|
+
const rerun = id ? `node scripts/gds/ship.js ${id}` : 'node scripts/gds/ship.js <task-id>';
|
|
82
|
+
const head = 'The ship did NOT finish. Where it stopped, and what recovers it:';
|
|
83
|
+
switch (progress.stage) {
|
|
84
|
+
case 'resolved':
|
|
85
|
+
return [
|
|
86
|
+
head,
|
|
87
|
+
` Task #${id} is at 'completed' — the claim resolved, the grade did NOT record.`,
|
|
88
|
+
' No credits awarded. Your commits are safe; nothing was lost but the grade.',
|
|
89
|
+
` Resume: ${rerun}`,
|
|
90
|
+
" (A plain re-run resumes the stalled leg. Do NOT reach for --regrade: it only",
|
|
91
|
+
" accepts 'completed', so it refuses outright if the ship actually got further.)",
|
|
92
|
+
];
|
|
93
|
+
case 'graded':
|
|
94
|
+
return [
|
|
95
|
+
head,
|
|
96
|
+
` Task #${id} is at 'confirmed' — it graded and PASSED; the merge/deploy did not finish.`,
|
|
97
|
+
' Credits land on the merge, so they have not been awarded yet.',
|
|
98
|
+
` Resume: ${rerun}`,
|
|
99
|
+
' (--regrade would refuse this one — its grade already passed.)',
|
|
100
|
+
];
|
|
101
|
+
case 'landed':
|
|
102
|
+
return [
|
|
103
|
+
'The ship LANDED; the failure above is in the after-work:',
|
|
104
|
+
` Task #${id} reached 'shipped' — the merge landed and credits were awarded.`,
|
|
105
|
+
' Broadcast / card / cleanup did not complete, which does not undo the ship.',
|
|
106
|
+
` Safe to re-run if you want the after-work retried: ${rerun}`,
|
|
107
|
+
];
|
|
108
|
+
case 'preflight':
|
|
109
|
+
default:
|
|
110
|
+
return [
|
|
111
|
+
head,
|
|
112
|
+
' The claim was NOT resolved — it is still active and the task is unchanged.',
|
|
113
|
+
` Fix the error above and re-run: ${rerun}`,
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
|
|
119
|
+
// wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
|
|
120
|
+
// FAIL used to fall silently through into the claim resolve — claim consumed,
|
|
121
|
+
// grading skipped with no message, task parked at 'completed', exit 0 — the
|
|
122
|
+
// choke-point map's likeliest "my task vanished" shape. `abort` is the default
|
|
123
|
+
// (printed BEFORE the resolve; the claim stays active); `parked` is the
|
|
124
|
+
// --allow-smoke-fail continuation (printed where the grader would have run, so
|
|
125
|
+
// the builder knows exactly where the task stopped and how to resume).
|
|
126
|
+
function smokeGateMessages(taskId) {
|
|
127
|
+
return {
|
|
128
|
+
abort: [
|
|
129
|
+
'Ship ABORTED: the production smoke test FAILED — see the FAIL line(s) above.',
|
|
130
|
+
'No claim resolved, no credits awarded — your claim is still active and nothing changed.',
|
|
131
|
+
'The smoke suite hits LIVE endpoints (your change is not deployed yet), so this usually means production is briefly degraded — not that your change broke it.',
|
|
132
|
+
` - Re-run once it recovers: node scripts/gds/ship.js ${taskId}`,
|
|
133
|
+
` - Ship anyway (deliberate): node scripts/gds/ship.js ${taskId} --allow-smoke-fail`,
|
|
134
|
+
` (parks the task at 'completed' ungraded; a plain re-run resumes it)`,
|
|
135
|
+
],
|
|
136
|
+
parked: [
|
|
137
|
+
'--allow-smoke-fail: smoke FAILED and the ship deliberately stops here.',
|
|
138
|
+
`Task #${taskId} is parked at 'completed' — ungraded, unconfirmed, no credits awarded.`,
|
|
139
|
+
`Resume when production is healthy: node scripts/gds/ship.js ${taskId}`,
|
|
140
|
+
' (A plain re-run routes to whichever leg stalled. --regrade only accepts a task at',
|
|
141
|
+
" 'completed', so it refuses if the ship got further — task 1003168.)",
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages,
|
|
148
|
+
_resetForTest: () => { shipProgress = { stage: 'preflight', taskId: null }; },
|
|
149
|
+
_peekForTest: () => shipProgress,
|
|
150
|
+
};
|
package/scripts/gds/ship.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// the task stays at 'confirmed' for /merge-mode to land later.
|
|
13
13
|
//
|
|
14
14
|
// Usage:
|
|
15
|
+
// node scripts/gds/ship.js <task-id> --notes-file notes.md --summary-file summary.txt
|
|
15
16
|
// node scripts/gds/ship.js <task-id> --notes "handoff markdown" --summary "value-summary line"
|
|
16
17
|
// node scripts/gds/ship.js <task-id> --no-merge # stop after confirm; skip auto-merge
|
|
17
18
|
// node scripts/gds/ship.js <task-id> --db-only # deliberate zero-commit / direct-SQL ship (task 391):
|
|
@@ -62,6 +63,8 @@ const os = require('node:os');
|
|
|
62
63
|
const path = require('node:path');
|
|
63
64
|
const readline = require('node:readline');
|
|
64
65
|
const { cliClient, requireSession, loadSession, loadSessionSync, printOnboardingNudge, arg, rewardPolicy } = require('./cli-lib');
|
|
66
|
+
// task 1003168 (G04): how a ship reports prose + an unfinished run — see that file.
|
|
67
|
+
const { resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages } = require('./ship-honesty.js');
|
|
65
68
|
|
|
66
69
|
// The instance's reward policy, as reported by the server on GET /me (ADR 0146 /
|
|
67
70
|
// task 1002469). Cached module-level because the credits print + the completion
|
|
@@ -2235,32 +2238,6 @@ function runSmokeAsync() {
|
|
|
2235
2238
|
});
|
|
2236
2239
|
}
|
|
2237
2240
|
|
|
2238
|
-
// task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
|
|
2239
|
-
// wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
|
|
2240
|
-
// FAIL used to fall silently through into the claim resolve — claim consumed,
|
|
2241
|
-
// grading skipped with no message, task parked at 'completed', exit 0 — the
|
|
2242
|
-
// choke-point map's likeliest "my task vanished" shape. `abort` is the default
|
|
2243
|
-
// (printed BEFORE the resolve; the claim stays active); `parked` is the
|
|
2244
|
-
// --allow-smoke-fail continuation (printed where the grader would have run, so
|
|
2245
|
-
// the builder knows exactly where the task stopped and how to resume).
|
|
2246
|
-
function smokeGateMessages(taskId) {
|
|
2247
|
-
return {
|
|
2248
|
-
abort: [
|
|
2249
|
-
'Ship ABORTED: the production smoke test FAILED — see the FAIL line(s) above.',
|
|
2250
|
-
'No claim resolved, no credits awarded — your claim is still active and nothing changed.',
|
|
2251
|
-
'The smoke suite hits LIVE endpoints (your change is not deployed yet), so this usually means production is briefly degraded — not that your change broke it.',
|
|
2252
|
-
` - Re-run once it recovers: node scripts/gds/ship.js ${taskId}`,
|
|
2253
|
-
` - Ship anyway (deliberate): node scripts/gds/ship.js ${taskId} --allow-smoke-fail`,
|
|
2254
|
-
` (parks the task at 'completed' ungraded; resume later with --regrade)`,
|
|
2255
|
-
],
|
|
2256
|
-
parked: [
|
|
2257
|
-
'--allow-smoke-fail: smoke FAILED and the ship deliberately stops here.',
|
|
2258
|
-
`Task #${taskId} is parked at 'completed' — ungraded, unconfirmed, no credits awarded.`,
|
|
2259
|
-
`Resume when production is healthy: node scripts/gds/ship.js ${taskId} --regrade`,
|
|
2260
|
-
],
|
|
2261
|
-
};
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
2241
|
// ---------- Step 3: merge + deploy chain ----------
|
|
2265
2242
|
|
|
2266
2243
|
function currentBranch() {
|
|
@@ -3696,11 +3673,14 @@ async function shipMain() {
|
|
|
3696
3673
|
const args = process.argv.slice(2);
|
|
3697
3674
|
const taskId = Number(args[0]);
|
|
3698
3675
|
if (!Number.isFinite(taskId)) {
|
|
3699
|
-
console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
|
|
3676
|
+
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>]');
|
|
3677
|
+
console.error(' prefer the -file forms for prose: a shell command-substitutes backticks in an inline value before node sees it (learning 1000197).');
|
|
3700
3678
|
process.exit(2);
|
|
3701
3679
|
}
|
|
3702
|
-
|
|
3703
|
-
const
|
|
3680
|
+
// G04: prose from a FILE or inline, never shell-mangled in silence.
|
|
3681
|
+
const prose = resolveProse(args, ['notes', 'summary']);
|
|
3682
|
+
const notes = prose.notes;
|
|
3683
|
+
const summary = prose.summary;
|
|
3704
3684
|
const skipMerge = args.includes('--no-merge');
|
|
3705
3685
|
const force = args.includes('--force');
|
|
3706
3686
|
// task 391 (idea #37): a deliberate zero-commit / direct-SQL ship. Without it,
|
|
@@ -4114,6 +4094,8 @@ async function shipMain() {
|
|
|
4114
4094
|
|
|
4115
4095
|
const taskStatus = r.data.taskStatus || 'shipped (legacy response)';
|
|
4116
4096
|
console.log(`Task #${r.data.taskId} is now: ${taskStatus}`);
|
|
4097
|
+
// Claim CONSUMED, task at 'completed' — a crash from here needs fatalRecoveryLines.
|
|
4098
|
+
markShipProgress('resolved', taskId);
|
|
4117
4099
|
|
|
4118
4100
|
// ---- Step 2: subagent quality grader ----
|
|
4119
4101
|
// Runs when local verification passed AND resolveClaim landed at completed.
|
|
@@ -4241,6 +4223,10 @@ async function shipMain() {
|
|
|
4241
4223
|
// ---- Step 3: confirmed→shipped tail (merge + deploy → memory/session sync →
|
|
4242
4224
|
// Done → onboarding nudge → completion card), shared with regradeMain via
|
|
4243
4225
|
// finishShip (#1093/C9). ----
|
|
4226
|
+
// Graded. If it passed, only the merge remains — a different crash story.
|
|
4227
|
+
if (postGradeStatus === 'confirmed' || postGradeStatus === 'shipped') {
|
|
4228
|
+
markShipProgress(postGradeStatus === 'shipped' ? 'landed' : 'graded', taskId);
|
|
4229
|
+
}
|
|
4244
4230
|
await finishShip({
|
|
4245
4231
|
taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly,
|
|
4246
4232
|
// task 1270: data for the deterministic completion card.
|
|
@@ -4283,14 +4269,15 @@ async function regradeMain() {
|
|
|
4283
4269
|
const args = process.argv.slice(2);
|
|
4284
4270
|
const taskId = Number(args[0]);
|
|
4285
4271
|
if (!Number.isFinite(taskId)) {
|
|
4286
|
-
console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary "..."] [--response "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
|
|
4272
|
+
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>]');
|
|
4287
4273
|
process.exit(2);
|
|
4288
4274
|
}
|
|
4289
|
-
const
|
|
4275
|
+
const regradeProse = resolveProse(args, ['summary', 'response']);
|
|
4276
|
+
const summaryArg = regradeProse.summary;
|
|
4290
4277
|
// task 1002662: the builder's response to the prior round's findings — how
|
|
4291
4278
|
// each was fixed, tracked (a task ref), or why it's out of scope. Rides to
|
|
4292
4279
|
// the panel fenced, under the delta re-grade policy.
|
|
4293
|
-
const responseArg =
|
|
4280
|
+
const responseArg = regradeProse.response;
|
|
4294
4281
|
const skipMerge = args.includes('--no-merge');
|
|
4295
4282
|
const force = args.includes('--force');
|
|
4296
4283
|
// task 1002422: honor --base on regrade/resume too, so `--regrade --base <ref>`
|
|
@@ -4609,6 +4596,7 @@ module.exports = {
|
|
|
4609
4596
|
// / non-ci → false) is unit-tested with no gh/network (env-driven via
|
|
4610
4597
|
// OTB_DEPLOY_MODE + OTB_PUSH_VIA_SERVER).
|
|
4611
4598
|
broadcastHandledByServer,
|
|
4599
|
+
fatalRecoveryLines, // G04 re-export (definition in ship-honesty.js)
|
|
4612
4600
|
};
|
|
4613
4601
|
|
|
4614
4602
|
if (require.main === module) {
|
|
@@ -4617,6 +4605,9 @@ if (require.main === module) {
|
|
|
4617
4605
|
const main = process.argv.slice(2).includes('--regrade') ? regradeMain : shipMain;
|
|
4618
4606
|
main().catch((err) => {
|
|
4619
4607
|
console.error('fatal:', err);
|
|
4608
|
+
// G04: never exit on a stack trace alone — say which leg died.
|
|
4609
|
+
console.error('');
|
|
4610
|
+
for (const line of fatalRecoveryLines()) console.error(line);
|
|
4620
4611
|
process.exit(1);
|
|
4621
4612
|
});
|
|
4622
4613
|
}
|
package/src/module-api.js
CHANGED
|
@@ -1039,8 +1039,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1039
1039
|
// landed since 1.19.304 with no explicit bump. run 32660902666. (task 1002620)
|
|
1040
1040
|
// 1.19.306 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1041
1041
|
// landed since 1.19.305 with no explicit bump. run 32662000590. (task 1002620)
|
|
1042
|
+
// 1.19.307 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1043
|
+
// landed since 1.19.306 with no explicit bump. run 32663876767. (task 1002620)
|
|
1042
1044
|
// ---------------------------------------------------------------------------
|
|
1043
|
-
const CORE_VERSION = '1.19.
|
|
1045
|
+
const CORE_VERSION = '1.19.307'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1044
1046
|
|
|
1045
1047
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1046
1048
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -427,19 +427,127 @@ test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {
|
|
|
427
427
|
assert.equal(res.hardFail, false);
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
-
// The census invariant (BV1.R109b). Asserting that the 10
|
|
431
|
-
// would prove nothing about the ELEVENTH — the next route
|
|
432
|
-
// this scanner doesn't handle. So count instead: every
|
|
433
|
-
// discovered route file must yield exactly one
|
|
430
|
+
// The census invariant (BV1.R109b, re-derived in task 1002671). Asserting that the 10
|
|
431
|
+
// known multi-line paths parse would prove nothing about the ELEVENTH — the next route
|
|
432
|
+
// someone declares in a form this scanner doesn't handle. So count instead: every
|
|
433
|
+
// `router.<method>(` call in every discovered route file must yield exactly one
|
|
434
|
+
// classified route.
|
|
434
435
|
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
436
|
+
// ORACLE INDEPENDENCE (the 2026-07-30 review's HIGH finding, confirmed): the first cut
|
|
437
|
+
// of this census was a verbatim copy of the scanner's own ROUTE_DECL_RE — hand-written,
|
|
438
|
+
// but shape-identical, so any widening of the scanner would be pasted into the census
|
|
439
|
+
// and both sides would move together. The census below is derived on a structurally
|
|
440
|
+
// different axis instead:
|
|
441
|
+
// • token-level, not line-level — comments and string/template/regex literals are
|
|
442
|
+
// stripped first by a tiny tokenizer, then calls are counted in the whole text;
|
|
443
|
+
// • not line-anchored — an inline/chained/mid-line declaration counts here even
|
|
444
|
+
// though the scanner's `^\s*` form cannot see it;
|
|
445
|
+
// • whitespace-tolerant across the member access (`router . get (` counts).
|
|
446
|
+
// A unilateral change to the scanner's recognizer now shows up as a census mismatch
|
|
447
|
+
// and forces a DELIBERATE census-side decision, instead of going green by paste.
|
|
448
|
+
//
|
|
449
|
+
// The tokenizer is deliberately small and fail-closed: if it ever misreads a future
|
|
450
|
+
// route file, the equality below goes red and a human looks — that is the correct
|
|
451
|
+
// failure mode for an audit oracle (never silently green).
|
|
452
|
+
function stripNonCode(src) {
|
|
453
|
+
let out = '';
|
|
454
|
+
const n = src.length;
|
|
455
|
+
const tplBraceDepth = []; // one entry per open template-literal interpolation
|
|
456
|
+
let i = 0;
|
|
457
|
+
const KEYWORDS_BEFORE_REGEX = new Set(['return', 'typeof', 'case', 'in', 'of', 'instanceof', 'new', 'delete', 'void', 'do', 'else', 'yield', 'await']);
|
|
458
|
+
const regexCanFollow = () => {
|
|
459
|
+
const t = out.trimEnd();
|
|
460
|
+
if (t === '') return true;
|
|
461
|
+
const last = t[t.length - 1];
|
|
462
|
+
if ('(,=:[!&|?{};+-*%~^<>'.includes(last)) return true;
|
|
463
|
+
const word = /[A-Za-z_$][\w$]*$/.exec(t);
|
|
464
|
+
return !!(word && KEYWORDS_BEFORE_REGEX.has(word[0]));
|
|
465
|
+
};
|
|
466
|
+
// Consume string/template BODY starting at src[i] (opening quote already emitted
|
|
467
|
+
// and consumed). For templates, stops early at `${` and pushes an interpolation
|
|
468
|
+
// frame — the main loop then lexes the interpolation as code until its `}`.
|
|
469
|
+
const skipStringBody = (quote) => {
|
|
470
|
+
while (i < n && src[i] !== quote) {
|
|
471
|
+
if (src[i] === '\\') { out += '..'; i += 2; continue; }
|
|
472
|
+
if (quote === '`' && src[i] === '$' && src[i + 1] === '{') { out += '${'; i += 2; tplBraceDepth.push(0); return 'interp'; }
|
|
473
|
+
out += (src[i] === '\n' ? '\n' : '.'); i++;
|
|
474
|
+
}
|
|
475
|
+
out += quote; i++; // closing quote (or EOF)
|
|
476
|
+
return 'closed';
|
|
477
|
+
};
|
|
478
|
+
const skipString = (quote) => {
|
|
479
|
+
out += quote; i++;
|
|
480
|
+
return skipStringBody(quote);
|
|
481
|
+
};
|
|
482
|
+
while (i < n) {
|
|
483
|
+
const c = src[i], d = src[i + 1];
|
|
484
|
+
if (c === '/' && d === '/') { while (i < n && src[i] !== '\n') i++; continue; }
|
|
485
|
+
if (c === '/' && d === '*') { i += 2; while (i < n && !(src[i] === '*' && src[i + 1] === '/')) { if (src[i] === '\n') out += '\n'; i++; } i += 2; out += ' '; continue; }
|
|
486
|
+
if (c === '/' && regexCanFollow()) { // regex literal (handles quotes inside, e.g. /[&<>"']/)
|
|
487
|
+
out += '/'; i++;
|
|
488
|
+
let inClass = false;
|
|
489
|
+
while (i < n && (inClass || src[i] !== '/')) {
|
|
490
|
+
if (src[i] === '\\') { out += '..'; i += 2; continue; }
|
|
491
|
+
if (src[i] === '[') inClass = true;
|
|
492
|
+
else if (src[i] === ']') inClass = false;
|
|
493
|
+
out += '.'; i++;
|
|
494
|
+
}
|
|
495
|
+
out += '/'; i++;
|
|
496
|
+
while (i < n && /[a-z]/i.test(src[i])) { out += src[i]; i++; } // flags
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (c === '\'' || c === '"' || c === '`') { skipString(c); continue; }
|
|
500
|
+
if (tplBraceDepth.length) { // inside a template interpolation: brace-track back to the template
|
|
501
|
+
if (c === '{') tplBraceDepth[tplBraceDepth.length - 1]++;
|
|
502
|
+
if (c === '}') {
|
|
503
|
+
if (tplBraceDepth[tplBraceDepth.length - 1] === 0) {
|
|
504
|
+
tplBraceDepth.pop(); out += '}'; i++;
|
|
505
|
+
skipStringBody('`'); // resume the enclosing template's body (may re-enter an interpolation)
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
tplBraceDepth[tplBraceDepth.length - 1]--;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
out += c; i++;
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// HAND-WRITTEN on purpose and NOT imported from the module (the oracle trap
|
|
517
|
+
// tests/route_permission_check.mjs documents) — and, unlike the pre-1002671 census,
|
|
518
|
+
// deliberately NOT shape-identical to the scanner's ROUTE_DECL_RE either.
|
|
519
|
+
const CENSUS_CALL_RE = /\brouter\s*\.\s*(?:get|post|patch|put|delete)\s*\(/g;
|
|
520
|
+
// A computed member declaration (`router[method](…)`) is invisible to every static
|
|
521
|
+
// recognizer on both sides. Fail loud instead of silently under-counting.
|
|
522
|
+
const CENSUS_DYNAMIC_RE = /\brouter\s*\[/;
|
|
439
523
|
// A floor, so a change that silently guts BOTH sides (e.g. discoverRouteFiles stops
|
|
440
524
|
// finding module routes) reds instead of trivially "matching" at a smaller number.
|
|
441
525
|
const ROUTE_CENSUS_FLOOR = 258;
|
|
442
526
|
|
|
527
|
+
function censusCount(text) {
|
|
528
|
+
const code = stripNonCode(String(text));
|
|
529
|
+
assert.ok(!CENSUS_DYNAMIC_RE.test(code),
|
|
530
|
+
'route file uses a computed router[method](…) declaration — invisible to the rank auditor; declare it statically');
|
|
531
|
+
return (code.match(CENSUS_CALL_RE) || []).length;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
test('census tokenizer: strips comments, strings, and quote-bearing regex literals', () => {
|
|
535
|
+
assert.equal(censusCount(`// router.get('/a', h)\nrouter.post('/b', h);`), 1);
|
|
536
|
+
assert.equal(censusCount(`const s = "router.get('/a'"; router.get('/b', h);`), 1);
|
|
537
|
+
assert.equal(censusCount(`s.replace(/[&<>"']/g, esc); router.get('/x', h);`), 1);
|
|
538
|
+
assert.equal(censusCount('const t = `x ${router.get(\'/live\', h)} y`;'), 1);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('census independence: counts declarations the line-anchored scanner cannot see', () => {
|
|
542
|
+
// Inline (mid-line) declaration: census sees it, scanDeclarations does not. If the
|
|
543
|
+
// scanner ever learns this form, this fixture forces the census comparison to be
|
|
544
|
+
// revisited deliberately rather than drifting in lockstep.
|
|
545
|
+
const text = `if (flag) router.get('/inline', h);`;
|
|
546
|
+
assert.equal(censusCount(text), 1);
|
|
547
|
+
assert.equal(rrc.classifyText(text).length, 0,
|
|
548
|
+
'fixture stale: the scanner now classifies inline declarations — update the census comment');
|
|
549
|
+
});
|
|
550
|
+
|
|
443
551
|
test('LIVE census: every router.<method>( declaration classifies to exactly one route [BV1.R109b]', () => {
|
|
444
552
|
const fs = require('node:fs');
|
|
445
553
|
let declared = 0;
|
|
@@ -447,7 +555,7 @@ test('LIVE census: every router.<method>( declaration classifies to exactly one
|
|
|
447
555
|
const shortfall = [];
|
|
448
556
|
for (const { abs, rel } of rrc.discoverRouteFiles()) {
|
|
449
557
|
const text = fs.readFileSync(abs, 'utf8');
|
|
450
|
-
const d = text
|
|
558
|
+
const d = censusCount(text);
|
|
451
559
|
const c = rrc.classifyText(text).length;
|
|
452
560
|
declared += d;
|
|
453
561
|
classified += c;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// tests/ship_cannot_lie.mjs — G04, task 1003168, goal 1000077, criterion
|
|
2
|
+
// guards-ship-cannot-lie.
|
|
3
|
+
//
|
|
4
|
+
// TWO fixes, one incident, so one test file.
|
|
5
|
+
//
|
|
6
|
+
// (1) Prose reaches ship.js through a FILE, never only through the shell.
|
|
7
|
+
// Backticks in handoff notes are command-substituted by bash BEFORE node
|
|
8
|
+
// runs: the span executes, its output replaces it, and the remaining
|
|
9
|
+
// arguments are truncated. That shipped a task with empty notes and no
|
|
10
|
+
// summary — the ledger recorded nothing about a session that happened
|
|
11
|
+
// (learning 1000197).
|
|
12
|
+
//
|
|
13
|
+
// (2) A ship that dies mid-pipeline must say where it stopped. resolveClaim
|
|
14
|
+
// lands the task at 'completed' before the grade runs, and 'completed' is
|
|
15
|
+
// ALSO what a grade-failed task looks like — so a bare `fatal: <stack>`
|
|
16
|
+
// left nobody able to tell how far it got.
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { argText } = require('../scripts/gds/cli-lib.js');
|
|
23
|
+
const { fatalRecoveryLines, markShipProgress, _peekForTest } = require('../scripts/gds/ship-honesty.js');
|
|
24
|
+
|
|
25
|
+
// Injected IO so no case touches the filesystem.
|
|
26
|
+
const io = {
|
|
27
|
+
readFile: (f) => {
|
|
28
|
+
if (f === 'notes.md') return 'real notes with `backticks` in them';
|
|
29
|
+
if (f === 'bom.md') return 'after a BOM';
|
|
30
|
+
throw new Error('ENOENT: no such file');
|
|
31
|
+
},
|
|
32
|
+
readStdin: () => 'from stdin',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---- (1) prose from a file -------------------------------------------------
|
|
36
|
+
test('THE INCIDENT: a file argument carries backticks through untouched', () => {
|
|
37
|
+
// The shell never sees this content, so the backticks survive verbatim —
|
|
38
|
+
// that is the entire point of the flag.
|
|
39
|
+
const r = argText('--notes', ['--notes-file', 'notes.md'], io);
|
|
40
|
+
assert.equal(r.source, 'file');
|
|
41
|
+
assert.match(r.value, /`backticks`/);
|
|
42
|
+
assert.equal(r.error, null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('inline still works, for a short summary with no awkward characters', () => {
|
|
46
|
+
const r = argText('--summary', ['--summary', 'one line'], io);
|
|
47
|
+
assert.deepEqual([r.value, r.source, r.error], ['one line', 'inline', null]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('BOTH given is REFUSED, never silently resolved', () => {
|
|
51
|
+
// Precedence would mean one of the caller's two inputs vanished with no word
|
|
52
|
+
// said — the same quiet loss the flag exists to end.
|
|
53
|
+
const r = argText('--notes', ['--notes', 'a', '--notes-file', 'notes.md'], io);
|
|
54
|
+
assert.ok(r.error, 'both-given must be an error');
|
|
55
|
+
assert.match(r.error, /both/i);
|
|
56
|
+
assert.equal(r.value, '', 'a refusal must not hand back a half-resolved value');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a dash reads stdin', () => {
|
|
60
|
+
const r = argText('--notes', ['--notes-file', '-'], io);
|
|
61
|
+
assert.deepEqual([r.value, r.source], ['from stdin', 'stdin']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an unreadable file is an error, not silently empty', () => {
|
|
65
|
+
// Silently-empty is precisely how the original incident hid itself.
|
|
66
|
+
const r = argText('--notes', ['--notes-file', 'gone.md'], io);
|
|
67
|
+
assert.ok(r.error);
|
|
68
|
+
assert.match(r.error, /could not read/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a missing value for the file flag is caught, including a following flag', () => {
|
|
72
|
+
for (const args of [['--notes-file'], ['--notes-file', '--summary', 'x']]) {
|
|
73
|
+
const r = argText('--notes', args, io);
|
|
74
|
+
assert.ok(r.error, `${JSON.stringify(args)} must error`);
|
|
75
|
+
assert.match(r.error, /needs a path/);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a PowerShell BOM is stripped', () => {
|
|
80
|
+
// PS 5.1's `>` redirection writes a UTF-8 BOM; without stripping it the first
|
|
81
|
+
// character of every note would be U+FEFF.
|
|
82
|
+
const r = argText('--notes', ['--notes-file', 'bom.md'], io);
|
|
83
|
+
assert.equal(r.value, 'after a BOM');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('neither flag yields empty string, not null — callers concatenate it', () => {
|
|
87
|
+
const r = argText('--notes', [], io);
|
|
88
|
+
assert.deepEqual([r.value, r.source, r.error], ['', 'none', null]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ---- (2) the ship cannot lie about how far it got --------------------------
|
|
92
|
+
test('every stage names the task, its real status, and a recovery command', () => {
|
|
93
|
+
for (const stage of ['preflight', 'resolved', 'graded', 'landed']) {
|
|
94
|
+
const lines = fatalRecoveryLines({ stage, taskId: 4242 });
|
|
95
|
+
assert.ok(Array.isArray(lines) && lines.length >= 3, `${stage} must say something`);
|
|
96
|
+
for (const l of lines) assert.equal(typeof l, 'string', `${stage} lines are printable as-is`);
|
|
97
|
+
assert.ok(lines.some((l) => l.includes('4242')), `${stage} must name the task`);
|
|
98
|
+
assert.ok(
|
|
99
|
+
lines.some((l) => l.includes('node scripts/gds/ship.js 4242')),
|
|
100
|
+
`${stage} must name the command that recovers it`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('pre-resolve says the claim is INTACT — the opposite of the post-resolve case', () => {
|
|
106
|
+
const text = fatalRecoveryLines({ stage: 'preflight', taskId: 7 }).join('\n');
|
|
107
|
+
assert.match(text, /NOT resolved/);
|
|
108
|
+
assert.match(text, /still active/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("post-resolve names 'completed' AND that the grade did not record", () => {
|
|
112
|
+
// The distinction the status itself cannot make.
|
|
113
|
+
const text = fatalRecoveryLines({ stage: 'resolved', taskId: 7 }).join('\n');
|
|
114
|
+
assert.match(text, /'completed'/);
|
|
115
|
+
assert.match(text, /grade did NOT record/);
|
|
116
|
+
assert.match(text, /No credits/i);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("post-grade names 'confirmed' and that the grade PASSED", () => {
|
|
120
|
+
const text = fatalRecoveryLines({ stage: 'graded', taskId: 7 }).join('\n');
|
|
121
|
+
assert.match(text, /'confirmed'/);
|
|
122
|
+
assert.match(text, /PASSED/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('the landed stage does NOT claim the ship failed', () => {
|
|
126
|
+
// It reached 'shipped'; only the after-work broke. Saying "the ship did not
|
|
127
|
+
// finish" there would be the same lying-status problem in reverse.
|
|
128
|
+
const lines = fatalRecoveryLines({ stage: 'landed', taskId: 7 });
|
|
129
|
+
assert.match(lines[0], /LANDED/);
|
|
130
|
+
assert.ok(!/did NOT finish/.test(lines.join('\n')));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('NO stage recommends --regrade as the recovery', () => {
|
|
134
|
+
// A plain re-run routes by the task's CURRENT status, so it resumes whichever
|
|
135
|
+
// leg stalled. --regrade accepts only 'completed' and refuses a task that got
|
|
136
|
+
// to 'confirmed' — and the builder reading a crash cannot know which it is.
|
|
137
|
+
for (const stage of ['preflight', 'resolved', 'graded', 'landed']) {
|
|
138
|
+
const text = fatalRecoveryLines({ stage, taskId: 7 });
|
|
139
|
+
for (const l of text) {
|
|
140
|
+
assert.ok(
|
|
141
|
+
!/^\s*Resume:?\s+node .*--regrade/.test(l) && !/re-run:\s+node .*--regrade/.test(l),
|
|
142
|
+
`${stage} must not prescribe --regrade: ${l}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('an unknown stage degrades to the safe pre-resolve message', () => {
|
|
149
|
+
// A future leg added without a case here must not produce a confident wrong
|
|
150
|
+
// claim about credits or status.
|
|
151
|
+
const text = fatalRecoveryLines({ stage: 'something-new', taskId: 7 }).join('\n');
|
|
152
|
+
assert.match(text, /NOT resolved/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('a missing taskId still yields a usable command', () => {
|
|
156
|
+
const text = fatalRecoveryLines({ stage: 'resolved', taskId: null }).join('\n');
|
|
157
|
+
assert.match(text, /ship\.js <task-id>/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('markShipProgress is the single writer, and ship.js re-exports the reader', () => {
|
|
161
|
+
// Wiring proof: the extraction must not have left ship.js reading a stale copy.
|
|
162
|
+
markShipProgress('graded', 99);
|
|
163
|
+
assert.deepEqual(_peekForTest(), { stage: 'graded', taskId: 99 });
|
|
164
|
+
assert.match(fatalRecoveryLines().join('\n'), /#99/, 'the default arg reads live progress');
|
|
165
|
+
const shipCli = require('../scripts/gds/ship.js');
|
|
166
|
+
assert.equal(typeof shipCli.fatalRecoveryLines, 'function', 'ship.js still exposes it');
|
|
167
|
+
assert.equal(shipCli.fatalRecoveryLines, fatalRecoveryLines, 'same function, not a copy');
|
|
168
|
+
});
|
|
@@ -14,7 +14,7 @@ import { strict as assert } from 'node:assert';
|
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const { smokeGateMessages } = require('../scripts/gds/ship.js');
|
|
17
|
+
const { smokeGateMessages } = require('../scripts/gds/ship-honesty.js');
|
|
18
18
|
|
|
19
19
|
let passed = 0;
|
|
20
20
|
let failed = 0;
|
|
@@ -61,8 +61,14 @@ t('parked: names the deliberate stop, the exact status, and that nothing was pai
|
|
|
61
61
|
assert.match(parkedText, /no credits/i);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
t('parked: names the resume path (--regrade)', () => {
|
|
65
|
-
|
|
64
|
+
t('parked: names the resume path (a plain re-run, not --regrade)', () => {
|
|
65
|
+
// task 1003168 (G04): this used to say --regrade. A plain re-run routes by the
|
|
66
|
+
// task's CURRENT status, so it resumes whichever leg stalled; --regrade accepts
|
|
67
|
+
// only 'completed' and refuses a task that got as far as 'confirmed'. Since the
|
|
68
|
+
// builder reading this cannot know which leg died, the advice must be the
|
|
69
|
+
// command that works from either.
|
|
70
|
+
assert.match(parkedText, /ship\.js 1234$|ship\.js 1234\s/m);
|
|
71
|
+
assert.ok(!/ship\.js 1234 --regrade/.test(parkedText), 'must not recommend --regrade');
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
t('both message sets are non-empty arrays of strings (printable as-is)', () => {
|