@bongos/core 1.19.305 → 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 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.305",
6
- "core_contract": "1.19.305",
7
- "source_commit": "6dc5a84be60b26f11d572eeccf10b8092c7352ae",
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-23T19:20:49.275Z",
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": 1414,
14
+ "functional_verbatim": 1418,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1758,
20
- "tree_sha256": "3a7cd2627a967fe3dbe7edf3dc916b4d728170b826669b64ae457d5e92ac10d1",
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": "bb9075e768b4cbc6142e2b4b24a57f92e9a4ae75d76f6849566082e0c5495d13"
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": "2ccc2a59ce44ba21c18db999c86768252b2fa6c8be6fa81f2b00a71eb727e664"
145
+ "sha256": "59b29fcdf54fe51e85191d1a66be237ef35432b76b26a10ec67e46bf08b4d1d3"
146
146
  },
147
147
  {
148
148
  "path": ".claude/skills/goal-create/SKILL.md",
@@ -212,12 +212,12 @@
212
212
  {
213
213
  "path": ".claude/skills/otb-tile-generate/SKILL.md",
214
214
  "mode": "0000644",
215
- "sha256": "cab6aee8bce317982447625d021d862e79c8363a1259d60d109de13331afa1bd"
215
+ "sha256": "53bfcf6c531c0aeefc1696b470ba6a746f4490501ab092766f42c501f06d453d"
216
216
  },
217
217
  {
218
218
  "path": ".claude/skills/paint/SKILL.md",
219
219
  "mode": "0000644",
220
- "sha256": "a0ef068b967e8560ead467c790543f707353bee740ce0a7fa0f01c67445119dd"
220
+ "sha256": "54af43e9417eaa369d9f166e0bbc8567c4540e064504e67ef32186fe31b2f766"
221
221
  },
222
222
  {
223
223
  "path": ".claude/skills/planning-session/SKILL.md",
@@ -1662,7 +1662,7 @@
1662
1662
  {
1663
1663
  "path": "docs/recipes/claude-design-loop.md",
1664
1664
  "mode": "0000644",
1665
- "sha256": "4f2e7cc445ae05968ba65fcb0c995cb358f144344dcb498047ec753a5a229a39"
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": "0c2c462d59634b9ec0e777d713cbc0bdc5af06408aa8b3717d2afac09c0070f0"
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": "4678a5bfd751ffe8ac5c3283eb25af2e011ccb14d32032bf373c7c77bcb61a3d"
4580
+ "sha256": "4815e8ce78e80ac2fff7dad9f77bef508e2f0090459c67e59722b65af3e27294"
4581
4581
  },
4582
4582
  {
4583
4583
  "path": "package.json",
4584
4584
  "mode": "0000644",
4585
- "sha256": "5ff86f97c1df0bf8d224512adc339aac2859db0a65e40e7783032b0128f7cbbd"
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": "95574e210b1fd215b10c7664da109b0698b18aa7724daa736bade6d752318f63"
5085
+ "sha256": "5aa658b099b2acda06cdb3aecfe905da7de79f2b12c844faca091ff061454017"
5086
5086
  },
5087
5087
  {
5088
5088
  "path": "scripts/gds/code.js",
@@ -5189,6 +5189,11 @@
5189
5189
  "mode": "0000644",
5190
5190
  "sha256": "7e279cd4cb8fb0458b8b3be7b0d694e7afcc96eec926abf68b8ccd6afb65429f"
5191
5191
  },
5192
+ {
5193
+ "path": "scripts/gds/doc-cli-guard.js",
5194
+ "mode": "0000644",
5195
+ "sha256": "e7feb497a7b568273d1a2c16c2a65036c60017d163d792d05d3c43addcfd7d8f"
5196
+ },
5192
5197
  {
5193
5198
  "path": "scripts/gds/docs-entropy.js",
5194
5199
  "mode": "0000644",
@@ -5227,7 +5232,7 @@
5227
5232
  {
5228
5233
  "path": "scripts/gds/fitness.js",
5229
5234
  "mode": "0000644",
5230
- "sha256": "e4399cd9d5283ff23b8c5baf9e7b22a4663cab3f6e0e477b138f279cda50bd72"
5235
+ "sha256": "71f7b70fb6b7b2251c5a335b90faf0b63b51c614829a247679b6f25209549264"
5231
5236
  },
5232
5237
  {
5233
5238
  "path": "scripts/gds/gate-review.js",
@@ -5779,10 +5784,15 @@
5779
5784
  "mode": "0000644",
5780
5785
  "sha256": "087def1396283b19cb8a6deea4cc7f362d7a2f164d496a587220f0a48568aae6"
5781
5786
  },
5787
+ {
5788
+ "path": "scripts/gds/ship-honesty.js",
5789
+ "mode": "0000644",
5790
+ "sha256": "32827178a46cc01c36c422e1adef433be99492e8444a8affbe51e5e8c1e8ce9f"
5791
+ },
5782
5792
  {
5783
5793
  "path": "scripts/gds/ship.js",
5784
5794
  "mode": "0000644",
5785
- "sha256": "73c2e91340171d6a9112e869552be17e0f82aeb80a5c680205ac452d34c3ddc4"
5795
+ "sha256": "f3005175abc0ba20e958381df6aee02402267e9956da466386656dd4e86ad8cf"
5786
5796
  },
5787
5797
  {
5788
5798
  "path": "scripts/gds/smoke-batch-claim.sh",
@@ -6237,7 +6247,7 @@
6237
6247
  {
6238
6248
  "path": "src/module-api.js",
6239
6249
  "mode": "0000644",
6240
- "sha256": "a59a54dc09613fc6d95a2efa1262b6d8099fabfb1ef90289462b81accd9c7fea"
6250
+ "sha256": "90436d6db6e32424c48549f61ad48a3a77f5c7cac1ae16d5559b7db76fee7b4a"
6241
6251
  },
6242
6252
  {
6243
6253
  "path": "src/module-loader/catalog.js",
@@ -7019,6 +7029,11 @@
7019
7029
  "mode": "0000644",
7020
7030
  "sha256": "d9fd04cc3f2fcb3b4a9945c1e743c44445a216a10648fa619f74716b381468f3"
7021
7031
  },
7032
+ {
7033
+ "path": "tests/doc_cli_guard.mjs",
7034
+ "mode": "0000644",
7035
+ "sha256": "4e68c67489daa51167b8d738a36f7c0187016b342e8a57731d14241563262878"
7036
+ },
7022
7037
  {
7023
7038
  "path": "tests/docs_entropy.mjs",
7024
7039
  "mode": "0000644",
@@ -8272,7 +8287,7 @@
8272
8287
  {
8273
8288
  "path": "tests/route_rank_check.mjs",
8274
8289
  "mode": "0000644",
8275
- "sha256": "1cb0af04d3f0f837ab19c5c821e91ca7a183f2744608d7ee3b79b4886fbbd4ed"
8290
+ "sha256": "bdc58224050392ece492ae8e8f12f62a8934d86e3993e0df5b8cb0aac80602e8"
8276
8291
  },
8277
8292
  {
8278
8293
  "path": "tests/route_validation.mjs",
@@ -8494,6 +8509,11 @@
8494
8509
  "mode": "0000644",
8495
8510
  "sha256": "afdb09a3ee072fea82390a187c12c42d8249fce0ce84644a4b8dff6a643dd009"
8496
8511
  },
8512
+ {
8513
+ "path": "tests/ship_cannot_lie.mjs",
8514
+ "mode": "0000644",
8515
+ "sha256": "82f6f951cc50c34860545be0f309e18a796ebb5baa712ccb341e1fd0dd72a233"
8516
+ },
8497
8517
  {
8498
8518
  "path": "tests/ship_card.mjs",
8499
8519
  "mode": "0000644",
@@ -8542,7 +8562,7 @@
8542
8562
  {
8543
8563
  "path": "tests/ship_smoke_gate.mjs",
8544
8564
  "mode": "0000644",
8545
- "sha256": "43e2f109ce85e99e71daa4573a6ec4fc591b167fe81ee02f2f73ea1ce84d465c"
8565
+ "sha256": "6ce7bd5d2a8910e201a9869c8a3f22ef210fd4400fbd14777d8d15d13252a97e"
8546
8566
  },
8547
8567
  {
8548
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 "..." --summary "..."`.
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
- node scripts/gds/ship.js <task-id> \
58
- --notes "$(cat <<'EOF'
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 "..." --summary "..."
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
 
@@ -30,7 +30,7 @@ Runs `modules/art-pipeline/pipeline/orchestrator.py` for the requested tiles. Th
30
30
 
31
31
  **View it on your sandbox without placing it in-game** (#1262): every tile you've generated — native + zoomed, with its score and critique — is served at **`https://sandbox-<login>.example.com/art`** (locally: `http://localhost:<port>/art`). This is the generated-art *gallery* (`modules/art-pipeline/generated/preview.html`), so you can show the builder what they made even when the tile isn't placed in the world yet. The atlas-rebuild in-game step below is only needed once you actually want it *in the playable world*.
32
32
 
33
- 6. **See it in-game on your sandbox** (#1067): run `node scripts/gds/art-stage.js` — it rebuilds the atlas the client loads, surfaces it on your sandbox live preview (`sandbox-<login>.example.com`, ADR 0044), and prints the URL + affected tiles. With the auto-reload (task 1065) an already-open tab refreshes itself. (The family/refresh runs also accept `--stage` to do this automatically: `family_orchestrator.py <id> --publish --stage`, `refresh_all.py --stage`.) Only a genuinely NEW base-tile *type* still needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the atlas rebuild above.
33
+ 6. **See it in-game on your sandbox** (#1067): run `node scripts/gds/art-stage.js` (**ships with the `art-pipeline` host module, not the portable core** — absent from a core-only checkout, where this step doesn't apply) — it rebuilds the atlas the client loads, surfaces it on your sandbox live preview (`sandbox-<login>.example.com`, ADR 0044), and prints the URL + affected tiles. With the auto-reload (task 1065) an already-open tab refreshes itself. (The family/refresh runs also accept `--stage` to do this automatically: `family_orchestrator.py <id> --publish --stage`, `refresh_all.py --stage`.) Only a genuinely NEW base-tile *type* still needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the atlas rebuild above.
34
34
 
35
35
  ## Constraints
36
36
 
@@ -40,7 +40,7 @@ Ground every asset in the locked theme so nothing drifts:
40
40
  1. **Establish the visual target first, not in prose.** Confirm the subject (an existing vocab id, or a new in-world prop/plant/animal/accessory) and, if helpful, show the reference cluster it should sit beside. One or two lines, then move to making.
41
41
  2. **Generate via the pipeline — never freelance.** Drive `/otb-tile-generate` (which runs `modules/art-pipeline/pipeline/orchestrator.py`: generate → post-process to 32×32 → score against rubric → retry up to 5 tiers → save to `modules/art-pipeline/generated/tiles/<id>.png`, logging cost). Check the budget first (`python3 modules/art-pipeline/pipeline/cost_ledger.py status`; hard stop at $80) — but report it as one line.
42
42
  3. **SHOW the result.** Display the final accepted PNG and its rubric score. The gallery serves every tile (native + zoomed, with score + critique) at `sandbox-<login>.example.com/art` — point them there and/or render the image inline. **Default visibility: show the final accepted asset and any failures. Show the intermediate retry attempts only if the artist asks ("show me the attempts").**
43
- 4. **See it in the world.** When they want it placed, run `node scripts/gds/art-stage.js` — it rebuilds the atlas and surfaces the asset live in the game on their sandbox (auto-reloads an open tab). Screenshot/point at the in-world view. (A genuinely new base-tile *type* also needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the rebuild.)
43
+ 4. **See it in the world.** When they want it placed, run `node scripts/gds/art-stage.js` (**ships with the `art-pipeline` host module, not the portable core** — on a core-only checkout the file is absent and this step doesn't apply) — it rebuilds the atlas and surfaces the asset live in the game on their sandbox (auto-reloads an open tab). Screenshot/point at the in-world view. (A genuinely new base-tile *type* also needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the rebuild.)
44
44
  5. **Feedback is visual → a durable rule.** When the artist reacts ("too saturated", "trees too modern", "weather the marble"), don't just tweak once — run `/otb-feedback-capture` to write the note into `modules/art-pipeline/template/style_guide.md` as a dated rule, then regenerate and **show the new result**. The loop is see → react → see again, and the style guide gets smarter each pass.
45
45
  6. **Use `/otb-design-review`** when you want a rubric-grounded second opinion on whether an asset truly hits the bar — surface its numbers, not a paragraph.
46
46
 
@@ -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 "..." --summary "..."
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 "..." --summary "..."
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.305",
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.305",
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.305",
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",
@@ -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,245 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/doc-cli-guard.js — a doc that NAMES a CLI invocation must match the
3
+ // real CLI (task 1003165 / G01, goal 1000077; criterion guards-doc-names-a-command).
4
+ //
5
+ // WHY. Task 1002931 shipped a skill instructing `capture.js --full --fields-file`
6
+ // while those flags lived on an unlanded branch. Its test passed, because the test
7
+ // pinned the skill's PROSE — it asserted the file said the words, never that the
8
+ // words worked. So the skill confidently told agents to run a command that
9
+ // silently ignored both flags and filed the wrong thing. That is the whole class:
10
+ // AN UNRECOGNISED FLAG IS NOT AN ERROR. `arg('--x')`/`hasFlag('--x')` return null
11
+ // and the command proceeds, so a doc can name a flag that does nothing and every
12
+ // test stays green (learning 1000198). It happened TWICE in one session: the
13
+ // second time /workspace ran an older capture.js and the same documented flags
14
+ // were dropped on the floor again.
15
+ //
16
+ // WHAT IT CHECKS, deliberately narrow: for every `node scripts/gds/<x>.js` in a
17
+ // tracked .md, (a) the script exists, and (b) each `--flag` written next to it
18
+ // appears in that script's source. Nothing else — this is not a prose linter.
19
+ //
20
+ // STATIC ONLY, NEVER EXECUTED. The reason matters: running every documented CLI
21
+ // would be slow, would need a session, and a command that blocks on a prompt
22
+ // would hang CI forever — the exact failure R151's TTY gate exists to prevent.
23
+ //
24
+ // It lives in its OWN FILE rather than in fitness.js because fitness.js is already
25
+ // over the size ratchet (config/fitness-baselines.json) — the control-manifest.js
26
+ // precedent. fitness.js requires it in CHECKS.
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { execFileSync } = require('node:child_process');
33
+
34
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Check 22 — a doc that NAMES a CLI invocation must match the real CLI
38
+ // (task 1003165 / G01, goal 1000077).
39
+ //
40
+ // WHY. Task 1002931 shipped a skill instructing `capture.js --full --fields-file`
41
+ // while those flags lived on an unlanded branch. Its test passed, because the test
42
+ // pinned the skill's PROSE — it asserted the file said the words, never that the
43
+ // words worked. So the skill confidently told agents to run a command that
44
+ // silently ignored both flags and filed the wrong thing. That is the whole class:
45
+ // AN UNRECOGNISED FLAG IS NOT AN ERROR. `arg('--x')`/`hasFlag('--x')` just return
46
+ // null and the command proceeds, so a doc can name a flag that does nothing and
47
+ // every test stays green (learning 1000198).
48
+ //
49
+ // It happened TWICE in one session: the second time, /workspace ran an older
50
+ // capture.js and the same documented flags were dropped on the floor again.
51
+ //
52
+ // WHAT IT CHECKS, deliberately narrow: for every `node scripts/gds/<x>.js` in a
53
+ // tracked .md, (a) the script exists, and (b) each `--flag` written next to it is
54
+ // parsed by that script. Nothing else — this is not a linter for prose.
55
+ //
56
+ // STATIC ONLY, NEVER EXECUTED. The task says so and the reason matters: running
57
+ // every documented CLI from a fitness check would be slow, would need a session,
58
+ // and a command that blocks on a prompt would hang CI forever — the exact failure
59
+ // R151's TTY gate exists to prevent.
60
+ // Key is `<doc path>::<token>`, where token is either the script path (the whole
61
+ // invocation is tolerated) or `--flag` (just that flag). Every entry carries its
62
+ // reason on the same line: an unexplained entry is a silent hole, and the point of
63
+ // this check is that a doc's claims are checkable.
64
+ const DOC_CLI_ALLOW = new Map([
65
+ ['.claude/skills/paint/SKILL.md::scripts/gds/art-stage.js',
66
+ 'art-stage.js ships with the art-pipeline HOST module, which publish-manifest.js excludes from core — absent from a core-only checkout by design; the skill says so inline.'],
67
+ ['.claude/skills/otb-tile-generate/SKILL.md::scripts/gds/art-stage.js',
68
+ 'same as paint/SKILL.md — the art staging CLI travels with the excluded art-pipeline module, not the portable core.'],
69
+ ]);
70
+
71
+ // `node scripts/gds/foo.js` … up to the end of the line/segment, so the flags that
72
+ // follow travel with the script that owns them.
73
+ const DOC_CLI_RE = /node\s+(scripts\/gds\/[A-Za-z0-9._-]+\.js)([^\n`]*)/g;
74
+ // A long flag only. Short flags and bare words are ignored: too many false
75
+ // positives from prose, and the failure this guards is long-flag drift.
76
+ const DOC_FLAG_RE = /(?<![\w-])--([A-Za-z][A-Za-z0-9-]*)/g;
77
+
78
+ // Flags every CLI inherits from shared arg plumbing rather than naming itself.
79
+ const UNIVERSAL_FLAGS = new Set(['quiet', 'help', 'dry-run', 'json']);
80
+
81
+ // Does `script` know about `--flag` AT ALL?
82
+ //
83
+ // This deliberately asks the WEAK question — does the literal appear anywhere in
84
+ // the source — and not the strong one ("is it wired into arg parsing"). The first
85
+ // draft of this check asked the strong question, matching `arg('--x')` and
86
+ // `hasFlag('--x')`, and produced FIVE false positives out of nine hits on its
87
+ // first real run: triage.js parses via `argv.indexOf('--development')`,
88
+ // provision.js via `hasFlag(args, '--apply')`, oauth-secret.js via
89
+ // `argVal(args, '--client-id')`, status.js via `a === '--widget'`, and
90
+ // validate-design.js via `ARGS.has('--strict')`. Every one of those is correct
91
+ // code that the strong check called broken.
92
+ //
93
+ // The set of ways to read an argv in JavaScript is open-ended, so a check that
94
+ // enumerates them will keep growing false positives forever — and a fitness check
95
+ // that cries wolf gets muted, which costs more than the drift it was catching.
96
+ // The weak question is ONE-DIRECTIONAL and therefore sound: a script whose source
97
+ // never contains the string `--full` cannot possibly honour `--full`, whatever
98
+ // idiom it uses. That is exactly the failure this exists for (learning 1000198:
99
+ // /workspace ran a capture.js from before those flags existed, and both were
100
+ // silently dropped). A stale usage comment naming a removed flag slips through —
101
+ // accepted, deliberately: guessing which mentions are "real" is how the strong
102
+ // version went wrong.
103
+ function scriptMentionsFlag(src, flag) {
104
+ const f = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
105
+ // Trailing guard so `--client` does not match `--client-id`.
106
+ return new RegExp(`--${f}(?![\\w-])`).test(src);
107
+ }
108
+
109
+ // The scan CORE. IO is injected — `readDoc(path)` and `readScript(path)` both
110
+ // return source or null — so a test can exercise a DRIFTED doc without writing
111
+ // a broken file into the tree (the control-manifest.js `evaluate` precedent).
112
+ // A check that cannot be shown to fail is not evidence of anything, which is
113
+ // precisely how task 1002931's prose-pinning test let the bad skill through.
114
+ function scanDocs(docs, { readDoc, readScript, allow = DOC_CLI_ALLOW } = {}) {
115
+ const violations = [];
116
+ const warnings = [];
117
+ let invocations = 0;
118
+ const srcCache = new Map();
119
+ const script = (rel) => {
120
+ if (!srcCache.has(rel)) srcCache.set(rel, readScript(rel));
121
+ return srcCache.get(rel);
122
+ };
123
+
124
+ for (const doc of docs) {
125
+ const text = readDoc(doc);
126
+ if (text === null) continue;
127
+ DOC_CLI_RE.lastIndex = 0;
128
+ let m;
129
+ while ((m = DOC_CLI_RE.exec(text))) {
130
+ const scriptRel = m[1];
131
+ const tail = m[2] || '';
132
+ invocations += 1;
133
+ const src = script(scriptRel);
134
+ if (src === null) {
135
+ if (!allow.has(`${doc}::${scriptRel}`)) {
136
+ violations.push(
137
+ `${doc} → \`node ${scriptRel}\` names a script that does not exist. `
138
+ + 'An agent following this doc gets MODULE_NOT_FOUND. Fix the path, or allowlist it in DOC_CLI_ALLOW with a reason.'
139
+ );
140
+ }
141
+ continue;
142
+ }
143
+ // A whole-invocation allowlist covers its flags too — the script isn't here to check them against.
144
+ if (allow.has(`${doc}::${scriptRel}`)) continue;
145
+ DOC_FLAG_RE.lastIndex = 0;
146
+ let f;
147
+ while ((f = DOC_FLAG_RE.exec(tail))) {
148
+ const flag = f[1];
149
+ if (UNIVERSAL_FLAGS.has(flag)) continue;
150
+ if (allow.has(`${doc}::--${flag}`)) continue;
151
+ if (scriptMentionsFlag(src, flag)) {
152
+ // The criterion asks that the flag be PARSED, not merely present. The
153
+ // hard check can only prove presence (see scriptMentionsFlag), so the
154
+ // residual gap — a flag surviving in a usage comment after the code that
155
+ // read it was deleted — is reported as a WARNING, not a failure. Warning
156
+ // rather than failing is the deliberate choice: a usage banner is the
157
+ // ONLY place some scripts name a flag they genuinely handle via a helper,
158
+ // so failing here would resurrect the false positives that sank draft 1.
159
+ if (!scriptMentionsFlag(stripLineComments(src), flag)) {
160
+ warnings.push(
161
+ `${doc} → \`node ${scriptRel} --${flag}\`: ${scriptRel} names \`--${flag}\` only in comments/usage text, `
162
+ + 'never in code — it may be a flag whose implementation was deleted. Verify it is still handled.'
163
+ );
164
+ }
165
+ } else {
166
+ violations.push(
167
+ `${doc} → \`node ${scriptRel} --${flag}\`: ${scriptRel} never mentions \`--${flag}\` anywhere in its source, so it cannot honour it. `
168
+ + 'An unrecognised flag is silently IGNORED, not an error — so the documented command does the wrong thing while every test stays green. '
169
+ + 'Add the flag, fix the doc, or allowlist it in DOC_CLI_ALLOW with a reason.'
170
+ );
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return { violations, warnings, invocations };
176
+ }
177
+
178
+ // Drop `//` line comments and `/* */` blocks — enough to tell "named in code" from
179
+ // "named only in a usage banner". Not a JS parser, and it does not need to be: a
180
+ // false split here downgrades a warning, never a failure.
181
+ function stripLineComments(src) {
182
+ return String(src)
183
+ .replace(/\/\*[\s\S]*?\*\//g, '')
184
+ .split('\n')
185
+ .map((l) => l.replace(/(^|\s)\/\/.*$/, '$1'))
186
+ .join('\n');
187
+ }
188
+
189
+ // Tracked markdown, via an argv ARRAY and never a shell string: `git ls-files
190
+ // '*.md'` as a string goes through cmd.exe on Windows, where the quotes are
191
+ // literal, git matches nothing, and the scan silently reports on zero files
192
+ // (task 1002526).
193
+ function trackedMarkdown() {
194
+ const out = execFileSync('git', ['ls-files', '*.md'], {
195
+ cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
196
+ });
197
+ return String(out || '').split('\n').map((f) => f.trim()).filter(Boolean);
198
+ }
199
+
200
+ const readFileOrNull = (rel) => {
201
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
202
+ };
203
+
204
+ const NAME = 'a doc that names a CLI matches the real CLI (G01)';
205
+
206
+ function checkDocNamesRealCli({ files = null } = {}) {
207
+ const docs = files || trackedMarkdown();
208
+ // A lint that reports on nothing is worse than one that fails (the
209
+ // docs-entropy precedent) — this repo tracks hundreds of .md files, so an
210
+ // empty enumeration is a broken scan, not a clean result.
211
+ if (!docs.length) {
212
+ return {
213
+ name: NAME, ok: false, hardFail: true,
214
+ violations: ['scan defect — enumerated 0 tracked markdown files; a broken enumeration, not a clean result.'],
215
+ warnings: [], note: 'static scan of documented `node scripts/gds/*.js` invocations.',
216
+ };
217
+ }
218
+
219
+ const { violations, warnings, invocations } = scanDocs(docs, {
220
+ readDoc: readFileOrNull, readScript: readFileOrNull,
221
+ });
222
+
223
+ return {
224
+ name: NAME,
225
+ ok: violations.length === 0,
226
+ hardFail: violations.length > 0,
227
+ violations,
228
+ warnings,
229
+ note: `${invocations} documented \`node scripts/gds/*.js\` invocation(s) across ${docs.length} tracked .md file(s): the script exists and every long flag it is written with appears in that script's source. Static — never executes a CLI (a documented command that blocks on a prompt would hang CI). ${DOC_CLI_ALLOW.size} allowlisted.`,
230
+ };
231
+ }
232
+
233
+ function main() {
234
+ const r = checkDocNamesRealCli();
235
+ for (const v of r.violations) console.error(`\u2717 ${v}`);
236
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
237
+ process.exit(r.hardFail ? 1 : 0);
238
+ }
239
+
240
+ if (require.main === module) main();
241
+
242
+ module.exports = {
243
+ checkDocNamesRealCli, scanDocs, trackedMarkdown, scriptMentionsFlag, stripLineComments,
244
+ DOC_CLI_ALLOW, DOC_CLI_RE, DOC_FLAG_RE, UNIVERSAL_FLAGS,
245
+ };