@bongos/core 1.19.526 → 1.19.528

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.
Files changed (50) hide show
  1. package/.bongos-core.json +81 -51
  2. package/.claude/skills/blocker-solve/SKILL.md +2 -1
  3. package/.claude/skills/builder-box/SKILL.md +2 -1
  4. package/.claude/skills/builder-connect/SKILL.md +2 -1
  5. package/.claude/skills/builder-end/SKILL.md +2 -1
  6. package/.claude/skills/builder-exit/SKILL.md +2 -1
  7. package/.claude/skills/builder-key/SKILL.md +2 -1
  8. package/.claude/skills/builder-sequence/SKILL.md +2 -1
  9. package/.claude/skills/builder-stage/SKILL.md +2 -1
  10. package/.claude/skills/design/SKILL.md +2 -1
  11. package/.claude/skills/goal-create/SKILL.md +2 -1
  12. package/.claude/skills/goal-review/SKILL.md +2 -1
  13. package/.claude/skills/grade-audit/SKILL.md +2 -1
  14. package/.claude/skills/grade-recover/SKILL.md +2 -1
  15. package/.claude/skills/ideate/SKILL.md +2 -1
  16. package/.claude/skills/new-project/SKILL.md +2 -1
  17. package/.claude/skills/otb-character-review/SKILL.md +2 -1
  18. package/.claude/skills/paint/SKILL.md +2 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +2 -1
  20. package/.claude/skills/ship-check/SKILL.md +44 -0
  21. package/.claude/skills/strand-fix/SKILL.md +2 -1
  22. package/.claude/skills/worktree-clean/SKILL.md +2 -1
  23. package/.gitattributes +6 -2
  24. package/docs/file-map.md +4 -1
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/recipes/windows-builders.md +46 -0
  27. package/modules/grading/generated-provenance.js +1 -1
  28. package/modules/grading/grader-subagent.js +20 -1
  29. package/modules/grading/grader.js +24 -1
  30. package/modules/lifecycle/cascade.js +86 -4
  31. package/modules/lifecycle/tests/cascade.mjs +63 -6
  32. package/package-lock.json +2 -2
  33. package/package.json +1 -1
  34. package/scripts/gds/claude-materialize.js +7 -2
  35. package/scripts/gds/fitness.js +40 -1
  36. package/scripts/gds/gen-repo-map.js +3 -1
  37. package/scripts/gds/session-cost.js +10 -7
  38. package/scripts/gds/session-digest.js +7 -4
  39. package/scripts/gds/ship-check.js +78 -0
  40. package/scripts/gds/ship-regen.js +2 -2
  41. package/scripts/gds/skill-lint.js +171 -0
  42. package/scripts/gds/strand-watch.js +3 -0
  43. package/src/module-api.js +1 -1
  44. package/tests/claude_materialize.mjs +10 -0
  45. package/tests/edit_hints.mjs +78 -0
  46. package/tests/git_merge_regen.mjs +1 -1
  47. package/tests/grader_root_outage.mjs +109 -0
  48. package/tests/hooks_drain.mjs +8 -2
  49. package/tests/repo_map.mjs +1 -1
  50. package/tests/skill_lint.mjs +73 -0
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: otb-character-review
3
- description: Review a painterly character ANIMATION (a character_anim family — many frames of one character in different poses) against the ADR 0088 animation criteria. Triggers when the user says "review this animation", "score the bongo idle", "check the character animation", "is this animation consistent", "rate these frames", or asks how a generated character/mascot animation holds up. Sibling to otb-design-review (which scores a single pixel-art tile). Returns a plain-language verdict per frame and for the sequence: same character throughout, correct anatomy, each frame on its intended pose, clean edges, and smooth in-order motion. Wraps family_orchestrator.run_family + family_review for the painterly path.
3
+ description: >-
4
+ Review a painterly character ANIMATION (many frames of one character) against the ADR 0088 criteria — same character throughout, correct anatomy, each frame on its pose, clean edges, smooth in-order motion — with a plain-language verdict per frame and for the sequence. Triggers: "review this animation", "is this animation consistent", "rate these frames". Sibling of otb-design-review (one tile).
4
5
  ---
5
6
 
6
7
  You are reviewing a painterly **character animation** — a `character_anim` family: many frames of the *same* character in *different* poses (e.g. the Bongo Buddha idle-breathing cycle). This is the animation sibling of `otb-design-review` (which scores one pixel-art tile). Do NOT judge it as pixel art — there is no palette to match, no FireRed reference, no 32×32 grid.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: paint
3
- description: The art session — the operating playbook for artist-discipline work. Triggers when the user says "/paint", "let's make art", "I want to work on a tile/sprite", "start an art session", or when a claimed task's discipline routes here (claim.js prints a directive to invoke /paint for artist tasks). Show-first, low-text: Claude communicates in pictures, not prose, and drives the existing pixel-art pipeline underneath.
3
+ description: >-
4
+ The art session — the operating playbook for artist-discipline work. Triggers when the user says "/paint", "let's make art", "I want to work on a tile/sprite", "start an art session", or when a claimed task's discipline routes here (claim.js prints a directive to invoke /paint for artist tasks). Show-first, low-text: Claude communicates in pictures, not prose, and drives the existing pixel-art pipeline underneath.
4
5
  ---
5
6
 
6
7
  You are running an **art session** for Example. This is not the engineer's text-heavy build loop. Read this whole file before acting — it changes how you *communicate* for the rest of the session.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: scan-before-install
3
- description: Scan a third-party GitHub repo or git-hosted Claude plugin/marketplace entry BEFORE anything from it is installed — hardened quarantine fetch, deterministic zero-LLM floor, tiered subagent panel over a scrubbed mirror, arithmetic verdict, honest-limits report, and three evidence-gated landings (print install command / sanitized copy / rebuild spec). The scanner itself NEVER installs anything. Triggers when the user says "/scan-before-install", "scan this repo before I install it", "is this plugin safe to install", "check this tool before I add it", "vet this GitHub repo", "scan this marketplace plugin", or pastes a repo/plugin URL and asks whether it's safe.
3
+ description: >-
4
+ Scan a third-party GitHub repo or git-hosted Claude plugin BEFORE installing anything: quarantined fetch, deterministic floor, tiered subagent panel over a scrubbed mirror, arithmetic verdict, honest-limits report, three evidence-gated landings. The scanner never installs. Triggers: "/scan-before-install", "is this plugin safe to install", "vet this GitHub repo".
4
5
  ---
5
6
 
6
7
  You are scanning a third-party artifact someone wants to install. The engine lives in `modules/security/artifact-scan/` — this skill is the orchestration wrapper: it runs the pipeline, spawns the panel, and lands one of three offers. Every load-bearing decision below is an owner decision; do not soften any of them.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: ship-check
3
+ description: >-
4
+ Run every freshness and fitness check ship.js will run, in one command, before shipping — and get the healing command for each stale generated file. Triggers: "/ship-check", "am I ready to ship", "what will ship trip on", "check before I ship", "is anything stale".
5
+ ---
6
+
7
+ # /ship-check — the pre-ship table
8
+
9
+ **Script skill (authoritative).** The whole action is one command, run from the worktree you will ship from:
10
+
11
+ ```bash
12
+ node scripts/gds/ship-check.js
13
+ ```
14
+
15
+ Add `--tests` to also run the DB-free unit lane (slow; minutes). Add `--json` for machine output.
16
+
17
+ ## Why this exists
18
+
19
+ Builders were running the same eight generators and checks by hand, one Bash call at a time — 223
20
+ invocations across 140 sessions in three days — because a ship that fails preflight costs a grader
21
+ run, and the last 30 days of session logs name a stale generated file (`docs/copy-registry.json`,
22
+ a nested `CLAUDE.md` symbol block, `docs/file-map.md`, `docs/api/openapi.json`, the §13 session
23
+ snippet) in roughly a quarter of all sessions. This is that sequence as one call with one table.
24
+ It changes nothing: every step is a `--check`; the **fix →** column is what to run.
25
+
26
+ ## What to do with the output
27
+
28
+ 1. Every `✗` row names the generator to run. Run it, look at the diff it produced, commit it on your
29
+ branch. Then re-run `/ship-check` until every row is `✓`.
30
+ 2. `fitness.js` failures name their own fix inline; warnings are advisory (budgets, ratchets).
31
+ 3. With `--tests`: a failing test is only "environmental" if the SAME test fails on an untouched
32
+ checkout of `origin/main`. Compare by test name, never by count — a Windows baseline of ~25
33
+ environment failures has masked real regressions before (see `docs/recipes/windows-builders.md`).
34
+ 4. When the table is all `✓`, ship: `/builder-ship`. The grader and CI are the remaining gates;
35
+ this skill removes the preflight trip, not the review.
36
+
37
+ ## Relationship to other tools
38
+
39
+ - `ship.js` runs these checks itself at ship time and refuses on a stale one; `/ship-check` is the
40
+ same gate run early, when fixing is cheap.
41
+ - The PostToolUse hook `.claude/hooks/edit-hints.js` tells you the moment an edit makes a generated
42
+ file stale; `/ship-check` is the sweep that catches whatever the hints did not.
43
+ - `node scripts/gds/preflight.js` checks the MACHINE (node version, node_modules, `claude` auth);
44
+ this checks the TREE.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: strand-fix
3
- description: Walk ONE stranded ship to landed — a task parked at `confirmed` that the server reconciler has already probed and given up on (`needs_rebase_reason` = `strand:no_branch_no_tip` or `strand:land_not_proven`), the shape the SessionStart line calls "will never self-heal". Finds where the work actually is (a main commit, an unpushed local branch, a conflicting PR, or nowhere), does the one sanctioned move for that case, and verifies the flip to `shipped`. Also the fix for a claim refused `409 REBASE_REQUIRED` — that gate is one of YOUR strands. Triggers when the user says "/strand-fix N", "fix strand N", "task N is stranded", "land the strand", "my claim says REBASE_REQUIRED", or after the SessionStart "Stranded ships:" line names a task. For a `completed` (ungraded) task use `/grade-recover`; for a `confirmed` task with no strand reason use `/merge-mode`.
3
+ description: >-
4
+ Walk ONE stranded ship to landed — a task parked at confirmed that the reconciler gave up on (strand:no_branch_no_tip, strand:land_not_proven). Finds where the work is, makes the one sanctioned move, verifies the flip to shipped. Also the fix for a claim refused 409 REBASE_REQUIRED. Triggers: "/strand-fix N", "task N is stranded", "land the strand", "my claim says REBASE_REQUIRED".
4
5
  requires: [gh]
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: worktree-clean
3
- description: Remove the per-claim worktrees whose work already landed — every shipped task leaves a `.claude/worktrees/task-N` behind, and nothing else prunes them. Lists what is stale (branch merged into origin/main, or branch already deleted by the ship), shows the list, then removes each through the junction-safe `worktree.js remove` — never raw `git worktree remove`, which follows the node_modules junction into the shared node_modules (task 1002809). Triggers when the user says "/worktree-clean", "clean up worktrees", "prune stale worktrees", "remove old worktrees", or when the SessionStart line reports "Stale worktrees: N".
3
+ description: >-
4
+ Remove the per-claim worktrees whose work already landed (branch merged into origin/main, or deleted by the ship): lists them, then removes each through the junction-safe worktree.js remove — never raw git worktree remove, which follows the node_modules junction (task 1002809). Triggers: "/worktree-clean", "prune stale worktrees", or the SessionStart line "Stale worktrees: N".
4
5
  ---
5
6
 
6
7
  **Script skill (authoritative).** Two scripts do the work; this skill only sequences them and shows the list before anything is removed.
package/.gitattributes CHANGED
@@ -58,7 +58,12 @@
58
58
  # that conflicts only on these files lands without a human (GitHub's own auto-merge
59
59
  # can't run a custom driver, hence the server does the merge locally then pushes).
60
60
  # NB: the TOP-LEVEL CLAUDE.md is hand-written and deliberately NOT listed here —
61
- # only the three nested CLAUDE.md docs carry a generated block. docs/file-map.md
61
+ # only the nested CLAUDE.md docs that carry a generated block are. scripts/gds/CLAUDE.md
62
+ # LEFT that set in task 1003548 (its ~40k-char block was 76% of a file loaded on every
63
+ # visit to that directory; docs/repo-map.md still carries the full index), so its entry
64
+ # came out with it — a path listed here whose generator no longer owns it would route a
65
+ # merge through a driver that cannot regenerate it, which tests/git_merge_regen.mjs
66
+ # asserts against. docs/file-map.md
62
67
  # carries two generated section blocks (.claude/skills/ + .claude/scheduled-tasks/,
63
68
  # ADR 0066, gen-file-map.js), so it joins the set; docs/session-log-index.md is
64
69
  # whole-file generated (gen-session-index.js), so it joins too.
@@ -72,7 +77,6 @@ docs/file-map.md merge=otb-regen
72
77
  docs/session-log-index.md merge=otb-regen
73
78
  src/bongos/CLAUDE.md merge=otb-regen
74
79
  src/bongos/routes/CLAUDE.md merge=otb-regen
75
- scripts/gds/CLAUDE.md merge=otb-regen
76
80
 
77
81
  # docs/copy-registry.json + docs/copy-inventory.md (task 1003543): the copy
78
82
  # registry's committed pair (scripts/gds/copy-inventory.js) — fully generated from
package/docs/file-map.md CHANGED
@@ -419,6 +419,7 @@ tests/
419
419
  ├── recall/SKILL.md ← /recall — one-call project-knowledge search across repo docs + DB prose (tasks, learnings, session logs)
420
420
  ├── scan-before-install/SKILL.md ← /scan-before-install — quarantine-fetch a third-party repo/plugin, deterministic floor + tiered subagent panel over the scrubbed mirror, arithmetic verdict + honest-limits report to the builder's own config dir; prints install commands (pinned SHA only), never installs (goal 1000055)
421
421
  ├── session-handoff/SKILL.md ← /session-handoff — compose a clean, paste-ready next-steps prompt to START a fresh session with (keeps context light by curating what carries forward); read-only, distinct from /builder-ship's ledger notes and /builder-end's close-out
422
+ ├── ship-check/SKILL.md ← /ship-check — run every freshness + fitness check ship.js will run (repo-map/session-index/file-map/api-docs/copy-registry --check, linkify, skill-lint, fitness; --tests adds the DB-free lane) in ONE command before shipping, with the healing command per stale artifact (task 1003548; script scripts/gds/ship-check.js)
422
423
  ├── status/SKILL.md ← /status — what's the status of X / what's left for criterion Cn (read-only rollup)
423
424
  ├── strand-fix/SKILL.md ← /strand-fix N — walk ONE stranded confirmed task (strand:no_branch_no_tip / land_not_proven — the shape the reconciler gave up on) to shipped: find the work (main commit / unpushed branch / conflicting PR / nowhere), do the one sanctioned move, verify the flip; also clears a 409 REBASE_REQUIRED claim gate (task 1003365)
424
425
  └── worktree-clean/SKILL.md ← /worktree-clean — list the per-claim worktrees whose branch already landed on main (scripts/gds/stale-worktrees.js) and remove each through the junction-safe worktree.js remove; never raw git worktree remove (task 1003365)
@@ -454,11 +455,13 @@ tests/
454
455
 
455
456
  To install on a new machine, symlink each routine's directory under `~/.claude/scheduled-tasks/`, e.g. `ln -s "<repo>/.claude/scheduled-tasks/overnight-code-review" ~/.claude/scheduled-tasks/overnight-code-review`.
456
457
 
457
- **Claude Code hooks** at `.claude/hooks/` (wired in `.claude/settings.json`; all invoked via `node` so they run on Windows/macOS/Linux alike — [#671](https://example.com/builders#/task/671)):
458
+ **Claude Code hooks** at `.claude/hooks/` (wired in `.claude/settings.json`; all invoked through `run-node.sh`, which finds node even when the hook shell's PATH lacks it, so they run on Windows/macOS/Linux alike (task 1003548) — [#671](https://example.com/builders#/task/671)):
458
459
 
459
460
  ```
460
461
  .claude/hooks/
461
462
  ├── secret-gate.js ← PreToolUse (Edit|Write|MultiEdit|NotebookEdit|Bash): cooperative, fail-OPEN block of secret-shaped content before it reaches the tree or a shell (task 1003125, ADR 0022)
463
+ ├── run-node.sh ← the node resolver every hook command runs through (task 1003548): $OTB_NODE_BIN → PATH → known install dirs → nvm; one actionable line + exit 1 when none is found. 1,055 hook runs had died silently with `node: command not found`
464
+ ├── edit-hints.js ← PostToolUse (Edit|Write|MultiEdit): lints a saved SKILL.md's frontmatter (scripts/gds/skill-lint.js) and names the generator an edited path makes stale (copy-registry, repo-map blocks, file-map, openapi, §13 snippet) with its command (task 1003548)
462
465
  ├── git-guard.js ← PreToolUse (Bash): same contract as secret-gate — blocks `git stash` (repo-global stack, learning 1000215), raw `git worktree remove` (follows the node_modules junction, task 1002809), `git push origin main` (PR-only) and `git branch -D`, each with its remedy; kill-switch BONGOS_GIT_GUARD_OFF=1 (task 1003365)
463
466
  ├── _core-resolve.js ← shared helper, not a hook: locates a core module/script across the monolith vs `node_modules/@cloudbongos/core` layouts; required by most hooks here and by four routine scripts under `.claude/scheduled-tasks/`
464
467
  ├── cascade-route.js ← PreToolUse (Task|Agent): cheapest-sufficient start-model advisory for a subagent spawn (ADR 0024); inert unless OTB_AUTONOMY_ENABLED=1
@@ -1489,5 +1489,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1489
1489
  landed since 1.19.524 with no explicit bump. run 33823218193. (task 1002620)
1490
1490
  1.19.526 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1491
1491
  landed since 1.19.525 with no explicit bump. run 33824700579. (task 1002620)
1492
+ 1.19.527 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1493
+ landed since 1.19.526 with no explicit bump. run 33827382028. (task 1002620)
1494
+ 1.19.528 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1495
+ landed since 1.19.527 with no explicit bump. run 33829339717. (task 1002620)
1492
1496
  ---------------------------------------------------------------------------
1493
1497
  ```
@@ -101,3 +101,49 @@ because the server already landed the task first (reconcile-on-read or the ~5-mi
101
101
  `ship.js` now normalizes both API error shapes and **verifies the live task status with a GET
102
102
  before reporting failure** (task 1002809) — a verified `shipped` prints as the success it is. If
103
103
  you see a ship-call failure from older tooling, check `GET /tasks/<id>` before reacting.
104
+
105
+ ## The hook shell can start without node — hooks were silently off (task 1003548)
106
+
107
+ - **Symptom:** every hook run reports `Failed with non-blocking status code: /usr/bin/bash: line 1: node: command not found`. A transcript scan of 2026-09-01..03 counted **1,055** such runs, which meant `secret-gate`, `git-guard`, `worktree-guard`, `lifecycle-card` and `conductor` were off for whole sessions while Claude Code showed only a muted warning.
108
+ - **Root cause:** Claude Code runs hook commands through Git Bash, and on this machine that shell intermittently starts with a PATH that predates the Node install. The Bash tool inherits the same PATH, which is why sessions also prepended `export PATH="/c/Program Files/nodejs:…"` to 1,404 commands.
109
+ - **What the tooling does now:** every hook command in `.claude/settings.json` goes through `.claude/hooks/run-node.sh`, which finds node on PATH or in the known install dirs (`C:\Program Files\nodejs`, `%LOCALAPPDATA%\Programs\nodejs`, nvm, Homebrew, `/usr/local/bin`) and, when it truly cannot, prints one line naming the fix instead of failing quietly. Override with `OTB_NODE_BIN` in the `env` block of `.claude/settings.local.json` if your node lives somewhere exotic.
110
+ - **For the Bash tool itself:** check once with `command -v node`; prepend PATH only if it is missing. Do not put the prepend on every command — see the next section for why that also collides with worktree isolation.
111
+
112
+ ## Compound `export PATH=…; cd …; …` commands collide with worktree isolation
113
+
114
+ - **Symptom:** `This session is isolated in the worktree … but this command is too complex to verify that it stays inside` (25×), `… changes directory to the shared checkout` (7×), `… runs export PATH= with a value every later program …` (6×), `… runs a command whose name is computed at runtime` (3×). 1,931 of 2,370 Bash calls in the scan were compound.
115
+ - **Root cause:** a session that entered a worktree (EnterWorktree) has Claude Code checking that each Bash command stays inside it. A chain of `export … ; cd … && …` defeats the checker, so it refuses the whole command.
116
+ - **Fix:** one purpose per command; use paths relative to the worktree (the Bash tool's cwd is the worktree); no `export PATH=` prefix once node is on PATH; never `cd` into the main checkout from a worktree session (learning 2026-08-29: `../../../` checks and dirties the MAIN checkout — run the worktree's own `scripts/gds/` copy).
117
+
118
+ ## `git fetch` / `git push` / `ls-remote` fail here BY DESIGN
119
+
120
+ - This machine holds no GitHub push credential (ADR 0031 §6 / ADR 0055). The scan found 22 `git fetch` attempts in three days, all wasted. Landing is server-mediated: `/builder-ship` uploads a bundle and the server pushes, opens the PR and lands it. To see whether work reached GitHub use `GET /api/gds/tasks/:id/publish-status`, never a raw git probe. To base a new worktree on fresh main, `node scripts/gds/worktree.js add <task-id> --base origin/main` (worktree.js fetches through the sanctioned path).
121
+
122
+ ## A worktree without `node_modules` fails 300+ tests — and the count is not the signal
123
+
124
+ - **Symptom:** `Cannot find module 'pg'` / `'express'`; `npm test` reports ~300 FAILED; 9 `npm install`s and 2 dead junctions in three days of transcripts.
125
+ - **What the tooling does now:** SessionStart prints one `[otb] node_modules is missing …` line (from `scripts/gds/preflight.js`'s check) when neither the worktree nor the main checkout has it. Install ONCE in the main checkout (`npm ci`); inside-repo worktrees resolve from it.
126
+ - **Rule:** when comparing a local run against CI, compare failing test NAMES against an untouched checkout of `origin/main`, never counts — a ~25-failure Windows baseline has masked real regressions (session logs 2026-08-14, 2026-08-19). `node scripts/gds/ship-check.js --tests` prints the names.
127
+
128
+ ## `tests/git_merge_regen.mjs` — when it reds, suspect `.gitattributes` first, not Windows
129
+
130
+ This test is the consistency gate between **two** sources that must agree, and it is the one that
131
+ caught a real bug during task 1003548, so read its assertion before blaming the platform:
132
+
133
+ - **Every path declared `merge=otb-regen` in `.gitattributes` must be in `REGENERABLE`**
134
+ (`scripts/gds/git-merge-regen.js`, derived from `gen-repo-map.NESTED_DIRS` + the whole-file set).
135
+ Drop a doc from the generators and its `.gitattributes` line has to come out in the same commit —
136
+ otherwise a merge on that path routes through a driver that cannot regenerate it. The failure reads
137
+ `.gitattributes declares merge=otb-regen for <path> but REGENERABLE … does not know it`.
138
+ - **A `real git merge must auto-resolve via the driver; got 1` failure** with a mangled driver path
139
+ (`Cannot find module 'C:\…\Temp\otb-gitmerge-*\<redacted>…'`) has been seen on this
140
+ machine and then stopped reproducing in all three trees (worktree, baseline worktree, main
141
+ checkout) — so treat it as intermittent, not as a standing Windows defect. Do NOT record it as a
142
+ known baseline failure: doing so is what let a genuine regression hide behind it for a whole
143
+ session. Re-run it, and follow the naming rule above — if it fails on your branch and passes on an
144
+ untouched `origin/main` worktree, it is yours.
145
+
146
+ ## CRLF: a multi-line find silently misses, and a whole-file diff is usually line endings
147
+
148
+ - Several hall files (`people.css`, `people.html`, `style.css`, the privacy spec) and every `.claude/skills/*/SKILL.md` are CRLF; `scripts/gds/*.js` and the tests are LF. An exact-string patch must detect the file's EOL before joining lines (`text.includes('\r\n') ? '\r\n' : '\n'`), or the find matches nothing and the edit is a silent no-op (session log 2026-08-24).
149
+ - A `git diff --stat` that touches every line of a file you barely edited is an EOL flip — check with `git diff --stat --ignore-cr-at-eol`, and restore the original EOL before committing.
@@ -58,7 +58,7 @@ const GENERATORS = [
58
58
  owns: [
59
59
  { file: 'src/bongos/CLAUDE.md', scope: 'fence', marker: 'BEGIN GENERATED REPO-MAP' },
60
60
  { file: 'src/bongos/routes/CLAUDE.md', scope: 'fence', marker: 'BEGIN GENERATED REPO-MAP' },
61
- { file: 'scripts/gds/CLAUDE.md', scope: 'fence', marker: 'BEGIN GENERATED REPO-MAP' },
61
+ // scripts/gds/CLAUDE.md left NESTED_DIRS in task 1003548 — it carries no fence any more.
62
62
  ],
63
63
  },
64
64
  {
@@ -23,6 +23,25 @@ function runSubagent({ prompt, opts = {} }) {
23
23
  // isn't named `claude` on PATH — covers any OS, not just Windows.
24
24
  const bin = opts.claudeBin ?? process.env.OTB_CLAUDE_BIN ?? process.env.CLAUDE_BIN ?? 'claude';
25
25
 
26
+ // THE PERMISSION MODE IS ROOT-DEPENDENT (task 1003574). `bypassPermissions` maps
27
+ // to --dangerously-skip-permissions, and the CLI REFUSES that when euid is 0:
28
+ // "--dangerously-skip-permissions cannot be used with root/sudo privileges"
29
+ // It exits non-zero before doing any work, so under root EVERY worker returns
30
+ // SUBAGENT_NONZERO_EXIT and the whole panel dies. A root container is an ordinary
31
+ // CI/sandbox shape, not an exotic one — this was hit on a real ship (task 1003280).
32
+ //
33
+ // `acceptEdits` is the fallback because it is the closest mode the CLI accepts as
34
+ // root, and it grants this worker NOTHING extra: the tool list is `Read` alone, so
35
+ // there are no edits to accept. Verified as root that a worker under acceptEdits
36
+ // still reads files inside its cwd (which is all a grader worker ever does) while
37
+ // a path outside the project is refused without hanging — in -p there is no prompt
38
+ // to answer, so a denial returns rather than blocking.
39
+ //
40
+ // Non-root keeps bypassPermissions exactly as before: this narrows the mode only
41
+ // where the wider one cannot run at all.
42
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
43
+ const permissionMode = isRoot ? 'acceptEdits' : 'bypassPermissions';
44
+
26
45
  const args = [
27
46
  '-p',
28
47
  '--print',
@@ -30,7 +49,7 @@ function runSubagent({ prompt, opts = {} }) {
30
49
  '--output-format', 'json',
31
50
  '--tools', 'Read',
32
51
  '--disable-slash-commands',
33
- '--permission-mode', 'bypassPermissions',
52
+ '--permission-mode', permissionMode,
34
53
  ];
35
54
 
36
55
  const startedAt = Date.now();
@@ -183,6 +183,11 @@ function aggregatePanel(workerResults, meta = {}) {
183
183
  // If the panel were somehow advisory-only, fall back to gating on everyone so
184
184
  // a lone Narc still counts (defensive — the default panel always has 3+
185
185
  // gating workers).
186
+ // A pseudo-worker produced by a deterministic check rather than an LLM subagent
187
+ // (route-rank, permission-path — both stamp model:'deterministic'). It cannot be
188
+ // taken out by a grader outage, so it is not evidence either way about whether the
189
+ // panel ran. Task 1003574.
190
+ const isDeterministic = (w) => w && w.model === 'deterministic';
186
191
  const nonAdvisory = workerResults.filter((w) => !isAdvisory(w));
187
192
  const gating = nonAdvisory.length ? nonAdvisory : workerResults;
188
193
  const gatingFail = gating.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
@@ -202,7 +207,25 @@ function aggregatePanel(workerResults, meta = {}) {
202
207
  // is an infra/outage condition, NOT a 0/10 quality rejection. Distinguished
203
208
  // from the partial-error case (some gating workers errored, others reported)
204
209
  // so callers (ship.js) can present "grader unavailable" instead of a FAIL.
205
- const gatingErrorAll = gating.length > 0 && gating.every((w) => isMissingSignal(w));
210
+ //
211
+ // TASK 1003574 — WHY THIS COUNTS ONLY THE SUBAGENT WORKERS. When #943 was written
212
+ // every gating worker was an LLM subagent, so "all of them are missing signal" and
213
+ // "the grader could not run" were the same sentence. They stopped being the same
214
+ // the moment the DETERMINISTIC pseudo-workers (route-rank, permission-path) were
215
+ // appended to workerResults: those never spawn a subprocess, so an outage cannot
216
+ // touch them, and a passing route-rank made `gating.every(isMissingSignal)`
217
+ // permanently false. The effect was that a TOTAL subagent outage could no longer
218
+ // reach this branch at all — it fell through to securityLensErrored and was
219
+ // reported as `FAIL · 0/10` with zero issues and every rubric axis null, which is
220
+ // indistinguishable from a real rejection and routes the builder to a re-grade
221
+ // that cannot clear it. Observed end-to-end on task 1003280.
222
+ //
223
+ // Excluding the deterministic checks here does NOT weaken the gate: a
224
+ // deterministic worker that FAILS is a real finding and is caught by `gatingFail`,
225
+ // which is the first branch and takes precedence over this one. All this says is
226
+ // that a deterministic check PASSING is not evidence the grader ran.
227
+ const subagentGating = gating.filter((w) => !isDeterministic(w));
228
+ const gatingErrorAll = subagentGating.length > 0 && subagentGating.every((w) => isMissingSignal(w));
206
229
  // Advisory-worker outcomes — never a hard block. Uses isAdvisory so a Narc
207
230
  // that DID raise a blocker is treated as gating (in `nonAdvisory`), not here.
208
231
  const advisoryFail = workerResults.some((w) => isAdvisory(w) && w.verdict === 'fail' && !isMissingSignal(w));
@@ -77,10 +77,85 @@
77
77
  // a throw is caught and treated as "no match", because a broken
78
78
  // predicate must not stop the other rules.
79
79
  // title(task) / body(task) the generated task's title and description.
80
- // kind optional task kind; defaults to 'chore'.
80
+ // kind optional task kind; must be one the classifier knows
81
+ // (task-classifier.VALID_KINDS). Defaults to 'unclassified' —
82
+ // which is right for generated work anyway, because a request the
83
+ // board has not triaged has, precisely, no classification yet.
81
84
  //
82
- // Production rules live here. It is empty by design — see the header.
83
- const CASCADES = Object.freeze([]);
85
+ // ---------------------------------------------------------------------------
86
+ // The first production rule (R04 of goal 1000074, task 1003115).
87
+ // ---------------------------------------------------------------------------
88
+ //
89
+ // THE ARTIST REVIEW. "Every shipped work with copy or visuals becomes a Review
90
+ // task for an artist, in two steps" (the criterion artist-review-task-on-ship).
91
+ // This is the rule that makes that true, and it is the whole of R04's build: the
92
+ // mechanism (ADR 0242) and its four safety properties already exist and are
93
+ // tested; R04 writes the one declaration.
94
+ //
95
+ // TWO GUARDS THAT KEEP IT FROM MISFIRING, both pinned in the tests:
96
+ // • It excludes `source === CASCADE_SOURCE`. Without that, the artist review
97
+ // task it generates — discipline 'artist', so it matches its own predicate —
98
+ // would on ship generate a review OF the review, and so on. The exclusion
99
+ // makes the generation a single hop, never a chain.
100
+ // • "Copy or visuals" is a NARROW, explicit predicate (hasCopyOrVisualWork),
101
+ // not "every ship". A cascade that fired on everything would bury the artist,
102
+ // and the criterion's "dismissable rather than nagging" would be a lie.
103
+ //
104
+ // It does NOT block anything (ADR 0241 draws the line: an artist may hold a
105
+ // DEPLOY, decided per project, never a builder's ship; that gate is a separate
106
+ // build). The generated task is a `backlog` request that triage may dismiss.
107
+
108
+ // The modules whose shipped work puts user-facing copy or visuals in front of a
109
+ // person. Kept as an explicit set rather than inferred, because "is this
110
+ // copy/visual work" is a judgement the rule should state out loud and a test
111
+ // should be able to pin — an inferred predicate is the kind that quietly widens.
112
+ const COPY_VISUAL_MODULES = Object.freeze([
113
+ 'copy-desk', // the copy registry + proposals (goal 1000074's own spine)
114
+ 'hall-ui', // the builders' hall — the most-seen surface
115
+ 'art-pipeline', // generated visual assets
116
+ 'status-ui', // the public status dashboard
117
+ 'ui-design', // the design-system surfaces (ADR 0081)
118
+ ]);
119
+
120
+ // Does this shipped task's work put copy or visuals in front of a person? Pure
121
+ // and total — the runner treats a throw as "no match", but this must not throw.
122
+ function hasCopyOrVisualWork(task) {
123
+ if (!task) return false;
124
+ if (task.source === 'copy-proposal') return true; // a copy change that landed via the copy desk (ADR 0233)
125
+ if (task.discipline === 'artist') return true; // work an artist did is, by definition, on their surface
126
+ if (task.module_key && COPY_VISUAL_MODULES.includes(task.module_key)) return true;
127
+ return false;
128
+ }
129
+
130
+ // The two-step artist review body. STEP 1 coaches the grader (the artist trains
131
+ // the grading system, not only the artefact); STEP 2 remakes the copy/visuals
132
+ // with both routes explicitly offered — R04's brief, verbatim in intent.
133
+ function artistReviewBody(task) {
134
+ return [
135
+ `This is an ARTIST REVIEW, generated automatically because task ${task.id} put copy or visuals in front of a person. It is a REQUEST, not a gate — it blocks nothing that shipped, and triage may dismiss it.`,
136
+ '',
137
+ `STEP 1 — REVIEW THE GRADER, not only the work. Read how the grader judged task ${task.id}'s copy and visuals, and coach it: where was it right, and where did it miss what an artist would catch — slop, an off-voice line, a claim that does not land, a visual that reads as generated? The artist trains the grading system, not just the artefact, so this step is the one that compounds.`,
138
+ '',
139
+ 'STEP 2 — REMAKE OR TWEAK the copy/visuals if they need it. Your choice of route, both offered on purpose: OUTSIDE the AI chat (edit the surface directly and land it through the copy desk or the normal pipeline), or WITH it (draft in chat, then land). Either way it lands as a claimed, graded, shipped task like everything else — there is no live CMS (goal 1000074\'s hard rule).',
140
+ '',
141
+ 'If the copy and visuals are fine and the grader judged them well, DISMISS this in triage. That is a real, expected outcome — not a failure.',
142
+ ].join('\n');
143
+ }
144
+
145
+ // Production rules. The artist review is the first (R04); add another by
146
+ // appending an entry, never by touching the runner (ADR 0242).
147
+ const CASCADES = Object.freeze([
148
+ Object.freeze({
149
+ key: 'artist-review-on-ship',
150
+ discipline: 'artist',
151
+ kind: 'unclassified',
152
+ // The anti-recursion guard (source !== CASCADE_SOURCE) is FIRST so a
153
+ // cascade-generated task is rejected before any other test runs.
154
+ when: (t) => t.source !== CASCADE_SOURCE && hasCopyOrVisualWork(t),
155
+ title: (t) => `Artist review: the copy and visuals that shipped in task ${t.id}`,
156
+ body: artistReviewBody,
157
+ }),
158
+ ]);
84
159
 
85
160
  // The (source, source_ref) pair the generated task is keyed on. `source` is a
86
161
  // constant so every cascade-generated row is findable as a class; the ref pins
@@ -171,7 +246,11 @@ function cascadeTaskSpec(rule, task) {
171
246
  title: String(rule.title(task) || '').trim(),
172
247
  description: `${preamble}\n\n${body}`,
173
248
  discipline: rule.discipline,
174
- kind: rule.kind || 'chore',
249
+ // 'unclassified', not 'chore' (task 1003115): the cascade calls db.createTask
250
+ // directly, bypassing the route's kind validation, and 'chore' is not in
251
+ // task-classifier.VALID_KINDS — it would have written an invalid kind. A
252
+ // generated request the board has not triaged is genuinely unclassified.
253
+ kind: rule.kind || 'unclassified',
175
254
  status: 'backlog',
176
255
  source: CASCADE_SOURCE,
177
256
  sourceRef: cascadeSourceRef(rule.key, task.id),
@@ -229,6 +308,9 @@ async function runCascadesForShip(shipped, deps = {}) {
229
308
  module.exports = {
230
309
  CASCADES,
231
310
  CASCADE_SOURCE,
311
+ COPY_VISUAL_MODULES,
312
+ hasCopyOrVisualWork,
313
+ artistReviewBody,
232
314
  cascadeSourceRef,
233
315
  assertRuleShape,
234
316
  validateCascades,
@@ -185,12 +185,69 @@ await test('the production table is valid, and a duplicate key is refused', () =
185
185
  assert.ok(shape.some((p) => /body/.test(p)));
186
186
  });
187
187
 
188
- await test('the production table ships EMPTY, deliberately — R04 writes the first rule', () => {
189
- // If this fails because a rule was added, that is fine: replace the assertion
190
- // with one that pins the new rule's key and discipline. It exists so the
191
- // emptiness is a DECISION on the record (see cascade.js's header) rather than
192
- // something nobody noticed.
193
- assert.equal(cascade.CASCADES.length, 0);
188
+ // --- the first production rule: the artist review (R04, task 1003115) --------
189
+
190
+ const shippedCopy = (over = {}) => ({
191
+ id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
192
+ title: 'Reword the hall empty states', discipline: 'engineer',
193
+ kind: 'feature', status: 'shipped', module_key: 'hall-ui', source: 'manual',
194
+ ...over,
195
+ });
196
+
197
+ await test('the artist review is the one production rule, keyed and disciplined', () => {
198
+ assert.equal(cascade.CASCADES.length, 1);
199
+ const r = cascade.CASCADES[0];
200
+ assert.equal(r.key, 'artist-review-on-ship');
201
+ assert.equal(r.discipline, 'artist', 'the review is FOR the artist, whatever role shipped the work');
202
+ assert.equal(r.kind, 'unclassified', 'a valid kind — not the invalid "chore" the spec used to default to');
203
+ assert.deepEqual(cascade.validateCascades(), [], 'and the shipped table is well-formed');
204
+ });
205
+
206
+ await test('a copy/visual ship generates the two-step artist review', () => {
207
+ const { matched } = cascade.cascadesFor(shippedCopy());
208
+ assert.equal(matched.length, 1);
209
+ const spec = matched[0].spec;
210
+ assert.equal(spec.discipline, 'artist');
211
+ assert.equal(spec.kind, 'unclassified');
212
+ assert.equal(spec.status, 'backlog', 'a request that triage may dismiss — not claimable, not nagging');
213
+ assert.match(spec.title, /7001/);
214
+ // Both steps, and both routes of step 2, are in the body.
215
+ assert.match(spec.description, /STEP 1 — REVIEW THE GRADER/);
216
+ assert.match(spec.description, /STEP 2 — REMAKE OR TWEAK/);
217
+ assert.match(spec.description, /OUTSIDE the AI chat/);
218
+ assert.match(spec.description, /WITH it/);
219
+ assert.match(spec.description, /DISMISS this in triage/);
220
+ assert.match(spec.description, /blocks nothing that shipped/, 'it says out loud that it is not a gate');
221
+ });
222
+
223
+ await test('"copy or visuals" is a NARROW predicate — three ways in, and a default of no', () => {
224
+ const H = cascade.hasCopyOrVisualWork;
225
+ assert.equal(H(shippedCopy({ module_key: 'copy-desk' })), true);
226
+ assert.equal(H(shippedCopy({ module_key: 'art-pipeline' })), true);
227
+ assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
228
+ assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
229
+ // The default is NO — a backend/infra ship is not an artist review.
230
+ assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'engineer', source: 'manual' })), false);
231
+ assert.equal(H(shippedCopy({ module_key: null, discipline: 'engineer', source: 'manual' })), false);
232
+ assert.equal(H(null), false);
233
+ });
234
+
235
+ await test('THE ANTI-RECURSION GUARD: an artist review shipping does NOT generate a review of itself', () => {
236
+ // The generated review is discipline:'artist' + source:'cascade'. Its own
237
+ // predicate (discipline artist) would match — so without the source guard it
238
+ // would cascade forever. This is the test that proves it is a single hop.
239
+ const reviewTaskThatShipped = shippedCopy({
240
+ discipline: 'artist', source: cascade.CASCADE_SOURCE, module_key: null, id: '7100',
241
+ });
242
+ const { matched } = cascade.cascadesFor(reviewTaskThatShipped);
243
+ assert.equal(matched.length, 0, 'a cascade-generated task never triggers another cascade');
244
+ });
245
+
246
+ await test('the review inherits the shipped task goal and version, not goal 1000074 by fiat', () => {
247
+ // A copy ship in some other goal gets its review filed next to that work.
248
+ const { matched } = cascade.cascadesFor(shippedCopy({ goal_id: '1000046', version_id: 'BONGOS-V1' }));
249
+ assert.equal(matched[0].spec.goalId, 1000046);
250
+ assert.equal(matched[0].spec.versionId, 'BONGOS-V1');
194
251
  });
195
252
 
196
253
  console.log(`\n${passed} passed, ${failed} failed`);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.526",
3
+ "version": "1.19.528",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.526",
9
+ "version": "1.19.528",
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.526",
3
+ "version": "1.19.528",
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",
@@ -48,9 +48,14 @@ function scriptToVerb(verbs = VERBS) {
48
48
  // skills + settings.json hooks use. The optional $CLAUDE_PROJECT_DIR/ or ./ prefix and surrounding
49
49
  // quote are consumed so the whole thing collapses to the bongos form. `.claude/hooks/<x>.js` refs are
50
50
  // deliberately NOT matched (this targets scripts/gds/) — those stay instance-local after materialize.
51
- const GDS_INVOCATION_RE = /node\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/gds\/([a-zA-Z0-9_-]+)\.js"?/g;
51
+ // The launcher is bare `node`, OR the hook resolver `sh "$CLAUDE_PROJECT_DIR/.claude/hooks/run-node.sh"`
52
+ // that every settings.json hook command goes through since task 1003548 (it finds node when the hook
53
+ // shell's PATH lacks it). Both collapse to the same bongos form; a resolver call whose target is a
54
+ // .claude/hooks/*.js stays untouched exactly like the bare-node form did.
55
+ const LAUNCHER = String.raw`(?:node|sh\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?\.claude\/hooks\/run-node\.sh"?)`;
56
+ const GDS_INVOCATION_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/gds\/([a-zA-Z0-9_-]+)\.js"?`, 'g');
52
57
  // A non-gds core script under scripts/ (rare in skills, but keep the transform total).
53
- const OTHER_SCRIPT_RE = /node\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/((?!gds\/)[a-zA-Z0-9_./-]+\.js)"?/g;
58
+ const OTHER_SCRIPT_RE = new RegExp(LAUNCHER + String.raw`\s+"?(?:\$CLAUDE_PROJECT_DIR\/|\.\/)?scripts\/((?!gds\/)[a-zA-Z0-9_./-]+\.js)"?`, 'g');
54
59
 
55
60
  // Pure: rewrite core-script invocations in a skill/settings command string to the bongos bin.
56
61
  // Idempotent (a string with no matching invocation is returned unchanged).
@@ -182,7 +182,7 @@ function checkGeneratedArtifactsFresh() {
182
182
  try {
183
183
  execFileSync(process.execPath, [path.join(__dirname, 'gen-repo-map.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
184
184
  } catch {
185
- violations.push('The tracked nested-doc symbol blocks (src/bongos/CLAUDE.md, src/bongos/routes/CLAUDE.md, scripts/gds/CLAUDE.md) are STALE — run `node scripts/gds/gen-repo-map.js` and commit (ADR 0063 / ADR 0110). NB docs/repo-map.md itself is gitignored + deploy-regenerated, not gated here.');
185
+ violations.push('The tracked nested-doc symbol blocks (src/bongos/CLAUDE.md, src/bongos/routes/CLAUDE.md) are STALE — run `node scripts/gds/gen-repo-map.js` and commit (ADR 0063 / ADR 0110). NB docs/repo-map.md itself is gitignored + deploy-regenerated, not gated here.');
186
186
  }
187
187
  try {
188
188
  execFileSync(process.execPath, [path.join(__dirname, 'gen-session-index.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
@@ -1295,9 +1295,48 @@ function checkErrorEnvelope() {
1295
1295
  };
1296
1296
  }
1297
1297
 
1298
+ // ============================================================================
1299
+ // Check 2c — every SKILL.md frontmatter parses the way Claude Code's YAML reader reads it (task 1003548)
1300
+ // ============================================================================
1301
+ // Ten of fifty skills shipped with an unquoted description holding ": " or " #": YAML dropped or
1302
+ // truncated EVERY field, the skill matched on its body text, and nothing in CI noticed. A parse
1303
+ // error is a hard fail; description length and the listing budget are advisory (the listing is
1304
+ // resident in every session, ~1% of context, and gets truncated past ~8k chars). The check reads
1305
+ // the instance's .claude/skills/ and is a quiet pass when a checkout carries none (published core).
1306
+ function checkSkillFrontmatter() {
1307
+ const violations = [];
1308
+ const warnings = [];
1309
+ let lint;
1310
+ try { lint = require('./skill-lint'); } catch (e) {
1311
+ return { name: 'skill frontmatter parses (task 1003548)', ok: true, hardFail: false, violations, warnings: [`skill-lint unavailable: ${e.message}`], note: 'linter missing — check skipped.' };
1312
+ }
1313
+ const files = lint.listSkillFiles(INSTANCE_ROOT);
1314
+ const report = lint.lintSkills(files);
1315
+ const rel = (f) => path.relative(INSTANCE_ROOT, f).replace(/\\/g, '/');
1316
+ const long = [];
1317
+ for (const r of report.results) {
1318
+ for (const e of r.errors) violations.push(`${rel(r.file)}: ${e}`);
1319
+ if (r.description.length > lint.DESC_WARN_CHARS) long.push({ file: rel(r.file), n: r.description.length });
1320
+ }
1321
+ if (long.length) {
1322
+ long.sort((a, b) => b.n - a.n);
1323
+ warnings.push(`${long.length} description(s) over ${lint.DESC_WARN_CHARS} chars (longest: ${long.slice(0, 4).map((l) => `${l.file.split('/')[2]} ${l.n}`).join(', ')}) — every char is resident in every session; run node scripts/gds/skill-lint.js.`);
1324
+ }
1325
+ if (report.overBudget) warnings.push(`the skill listing is ${report.listingChars} chars (~${estTokens(report.listingChars)} tokens) against a ~${report.budgetChars}-char budget — Claude Code truncates entries past it and routing degrades.`);
1326
+ return {
1327
+ name: 'skill frontmatter parses (task 1003548)',
1328
+ ok: violations.length === 0,
1329
+ hardFail: violations.length > 0,
1330
+ violations,
1331
+ warnings,
1332
+ note: `${report.results.length} SKILL.md file(s) under .claude/skills/ parse as YAML (name + description present, no ": " / " #" in a plain scalar); length + listing budget advisory.`,
1333
+ };
1334
+ }
1335
+
1298
1336
  const CHECKS = [
1299
1337
  checkCoreHostBoundary,
1300
1338
  checkClaudeMdBudget,
1339
+ checkSkillFrontmatter,
1301
1340
  checkNestedDocsPresent,
1302
1341
  checkRankGuards,
1303
1342
  checkGeneratedArtifactsFresh,
@@ -61,7 +61,9 @@ const SCAN_ROOTS = [...CORE_SCAN_ROOTS, ...INSTANCE_SCAN_ROOTS, MODULE_SCAN_ROOT
61
61
 
62
62
  // Directories whose nested CLAUDE.md gets a generated symbol-skeleton block (must already exist —
63
63
  // they are authored in Path A / ADR 0061; this only refreshes the factual half). All CORE dirs.
64
- const NESTED_DIRS = ['src/bongos', 'src/bongos/routes', 'scripts/gds'];
64
+ // scripts/gds dropped (task 1003548): its block had grown to ~40k chars — 76% of a file every builder
65
+ // session loads when it touches claim/ship. docs/repo-map.md still carries the full index.
66
+ const NESTED_DIRS = ['src/bongos', 'src/bongos/routes'];
65
67
 
66
68
  const EXCLUDE_DIR = new Set(['node_modules', '.git', 'tests', 'test', 'vendor']);
67
69
  const TOP_GLOBAL = 50; // global "most-referenced" table cap