@bongos/core 1.19.526 → 1.19.527
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 +72 -47
- package/.claude/skills/blocker-solve/SKILL.md +2 -1
- package/.claude/skills/builder-box/SKILL.md +2 -1
- package/.claude/skills/builder-connect/SKILL.md +2 -1
- package/.claude/skills/builder-end/SKILL.md +2 -1
- package/.claude/skills/builder-exit/SKILL.md +2 -1
- package/.claude/skills/builder-key/SKILL.md +2 -1
- package/.claude/skills/builder-sequence/SKILL.md +2 -1
- package/.claude/skills/builder-stage/SKILL.md +2 -1
- package/.claude/skills/design/SKILL.md +2 -1
- package/.claude/skills/goal-create/SKILL.md +2 -1
- package/.claude/skills/goal-review/SKILL.md +2 -1
- package/.claude/skills/grade-audit/SKILL.md +2 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/ideate/SKILL.md +2 -1
- package/.claude/skills/new-project/SKILL.md +2 -1
- package/.claude/skills/otb-character-review/SKILL.md +2 -1
- package/.claude/skills/paint/SKILL.md +2 -1
- package/.claude/skills/scan-before-install/SKILL.md +2 -1
- package/.claude/skills/ship-check/SKILL.md +44 -0
- package/.claude/skills/strand-fix/SKILL.md +2 -1
- package/.claude/skills/worktree-clean/SKILL.md +2 -1
- package/.gitattributes +6 -2
- package/docs/file-map.md +4 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/windows-builders.md +46 -0
- package/modules/grading/generated-provenance.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claude-materialize.js +7 -2
- package/scripts/gds/fitness.js +40 -1
- package/scripts/gds/gen-repo-map.js +3 -1
- package/scripts/gds/session-cost.js +10 -7
- package/scripts/gds/session-digest.js +7 -4
- package/scripts/gds/ship-check.js +78 -0
- package/scripts/gds/ship-regen.js +2 -2
- package/scripts/gds/skill-lint.js +171 -0
- package/scripts/gds/strand-watch.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +10 -0
- package/tests/edit_hints.mjs +78 -0
- package/tests/git_merge_regen.mjs +1 -1
- package/tests/hooks_drain.mjs +8 -2
- package/tests/repo_map.mjs +1 -1
- package/tests/skill_lint.mjs +73 -0
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
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,7 @@ 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)
|
|
1492
1494
|
---------------------------------------------------------------------------
|
|
1493
1495
|
```
|
|
@@ -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
|
-
|
|
61
|
+
// scripts/gds/CLAUDE.md left NESTED_DIRS in task 1003548 — it carries no fence any more.
|
|
62
62
|
],
|
|
63
63
|
},
|
|
64
64
|
{
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.527",
|
|
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.527",
|
|
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.527",
|
|
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
|
-
|
|
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 =
|
|
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).
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
@@ -331,13 +331,13 @@ async function main() {
|
|
|
331
331
|
if (!transcriptPath) transcriptPath = newestTranscriptFor(cwd);
|
|
332
332
|
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
333
333
|
// nothing to price — quiet skip
|
|
334
|
-
|
|
334
|
+
return;
|
|
335
335
|
}
|
|
336
336
|
if (!sessionId) sessionId = path.basename(transcriptPath, '.jsonl');
|
|
337
337
|
|
|
338
338
|
// No GDS session → nothing to attribute spend to; skip quietly.
|
|
339
339
|
const session = await loadSession();
|
|
340
|
-
if (!session || !session.token)
|
|
340
|
+
if (!session || !session.token) return;
|
|
341
341
|
|
|
342
342
|
const inter = priceTranscript(transcriptPath);
|
|
343
343
|
|
|
@@ -347,7 +347,7 @@ async function main() {
|
|
|
347
347
|
const sub = priceSubagents(sessionDir);
|
|
348
348
|
|
|
349
349
|
const total = inter.total + sub.subagentTotal;
|
|
350
|
-
if (!(total > 0))
|
|
350
|
+
if (!(total > 0)) return;
|
|
351
351
|
|
|
352
352
|
// task 1344: split into call-site-tagged, tier-carrying rows; cron sessions
|
|
353
353
|
// are tagged via OTB_COST_CRON_ROUTINE.
|
|
@@ -362,7 +362,7 @@ async function main() {
|
|
|
362
362
|
console.log(` ${r.source} (${r.source_ref}) $${r.amount_usd.toFixed(4)} tiers: fresh $${r.tiers.fresh_input_usd} / cache_read $${r.tiers.cache_read_usd} / cache_write $${r.tiers.cache_write_usd} / output $${r.tiers.output_usd}`);
|
|
363
363
|
}
|
|
364
364
|
console.log(` reconcile: prior-single $${built.priorSingle.toFixed(4)} vs rows-sum $${built.rowsTotal.toFixed(4)} (Δ $${Math.abs(built.priorSingle - built.rowsTotal).toFixed(4)})`);
|
|
365
|
-
|
|
365
|
+
return;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
const api = await cliClient({ allowUnauthenticated: true });
|
|
@@ -389,18 +389,21 @@ async function main() {
|
|
|
389
389
|
console.error(`session-cost: skipped (non-fatal): HTTP ${res.status} for ${r.source}`);
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
|
-
|
|
392
|
+
return;
|
|
393
393
|
} catch (err) {
|
|
394
394
|
// Best-effort: never break session exit.
|
|
395
395
|
console.error('session-cost: skipped (non-fatal):', err && err.message ? err.message : err);
|
|
396
|
-
|
|
396
|
+
return;
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
// Run only when invoked directly (the SessionEnd hook / CLI). When required by a
|
|
401
401
|
// test, expose the pure-ish helpers without firing the IIFE / network / exit.
|
|
402
402
|
if (require.main === module) {
|
|
403
|
-
|
|
403
|
+
// Drain instead of process.exit(): an exit while a socket/child handle is still closing aborts node on
|
|
404
|
+
// Windows (UV_HANDLE_CLOSING, src\win\async.c) — 59 SessionStart/UserPromptSubmit runs died that way
|
|
405
|
+
// (task 1003548). The unref'd backstop only fires if a hung request keeps the loop alive.
|
|
406
|
+
main().catch(() => {}).finally(() => { process.exitCode = 0; setTimeout(() => process.exit(0), 1500).unref(); });
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
module.exports = {
|
|
@@ -127,12 +127,12 @@ async function main() {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
if (!transcriptPath) transcriptPath = newestTranscriptFor(cwd || process.cwd());
|
|
130
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
130
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return; // nothing to capture
|
|
131
131
|
if (!sessionId) sessionId = path.basename(transcriptPath, '.jsonl');
|
|
132
132
|
|
|
133
133
|
// No GDS session → nothing to attribute the digest to; skip quietly.
|
|
134
134
|
const session = await loadSession();
|
|
135
|
-
if (!session || !session.token)
|
|
135
|
+
if (!session || !session.token) return;
|
|
136
136
|
|
|
137
137
|
// Secret hygiene: buildSessionDigest (ship.js) is the PRIMARY scrubber — it
|
|
138
138
|
// redacts known secret PATTERNS + a high-entropy catch-all (task 1003).
|
|
@@ -180,10 +180,13 @@ async function main() {
|
|
|
180
180
|
} catch (e) {
|
|
181
181
|
console.error('session-digest: skipped —', e && e.message ? e.message : e);
|
|
182
182
|
} finally {
|
|
183
|
-
|
|
183
|
+
return;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
// Drain instead of process.exit(): an exit while a socket/child handle is still closing aborts node on
|
|
188
|
+
// Windows (UV_HANDLE_CLOSING, src\win\async.c) — 59 SessionStart/UserPromptSubmit runs died that way
|
|
189
|
+
// (task 1003548). The unref'd backstop only fires if a hung request keeps the loop alive.
|
|
190
|
+
if (require.main === module) main().catch(() => {}).finally(() => { process.exitCode = 0; setTimeout(() => process.exit(0), 1500).unref(); });
|
|
188
191
|
|
|
189
192
|
module.exports = { parseHookInput };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/ship-check.js — run every freshness/fitness check ship.js will run, in one command,
|
|
3
|
+
// BEFORE you ship, and print the healing command for each stale thing.
|
|
4
|
+
//
|
|
5
|
+
// Why (task 1003548): a scan of 140 sessions found builders running these generators and checks by
|
|
6
|
+
// hand 223 times in three days (fitness.js ×84, gen-repo-map ×41, copy-inventory ×42, gen-api-docs
|
|
7
|
+
// ×16, linkify-refs ×13, docs-entropy ×12, gen-file-map ×9, gen-session-index ×6), one Bash call at
|
|
8
|
+
// a time, because a ship that fails preflight costs a grader run. The last 30 days of session logs
|
|
9
|
+
// name a stale generated file in roughly a quarter of all sessions. This is that sequence as one
|
|
10
|
+
// call with one table. It CHANGES NOTHING: every step is a --check; the "fix" column is what to run.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// node scripts/gds/ship-check.js checks only (fast; no tests)
|
|
14
|
+
// node scripts/gds/ship-check.js --tests also run the DB-free unit lane (npm test) and list failures
|
|
15
|
+
// node scripts/gds/ship-check.js --json
|
|
16
|
+
// Exit 1 when any check is stale/failing, 0 when everything is fresh.
|
|
17
|
+
'use strict';
|
|
18
|
+
const { spawnSync } = require('node:child_process');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
23
|
+
|
|
24
|
+
// Each step: a label, the argv to run (node scripts run with THIS node), the command that heals it,
|
|
25
|
+
// and how to read a non-zero exit. `optional` steps are skipped when their script is absent.
|
|
26
|
+
const STEPS = [
|
|
27
|
+
{ key: 'fitness', label: 'fitness.js (architecture + budgets + freshness gate)', argv: ['scripts/gds/fitness.js'], fix: 'read the ✗ lines above; each names its fix' },
|
|
28
|
+
{ key: 'repo-map', label: 'nested CLAUDE.md symbol blocks fresh', argv: ['scripts/gds/gen-repo-map.js', '--check-block'], fix: 'node scripts/gds/gen-repo-map.js' },
|
|
29
|
+
{ key: 'session-index', label: 'CLAUDE.md §13 session snippet fresh', argv: ['scripts/gds/gen-session-index.js', '--check-block'], fix: 'node scripts/gds/gen-session-index.js' },
|
|
30
|
+
{ key: 'file-map', label: 'docs/file-map.md skills/routines sections fresh', argv: ['scripts/gds/gen-file-map.js', '--check'], fix: 'node scripts/gds/gen-file-map.js (+ a note in docs/file-map.notes.json for a new skill)' },
|
|
31
|
+
{ key: 'api-docs', label: 'docs/api/openapi.json + api-reference fresh', argv: ['scripts/gds/gen-api-docs.js', '--check'], fix: 'node scripts/gds/gen-api-docs.js' },
|
|
32
|
+
{ key: 'copy-registry', label: 'docs/copy-registry.json fresh', argv: ['scripts/gds/copy-inventory.js', '--check'], fix: 'node scripts/gds/copy-inventory.js', optional: true },
|
|
33
|
+
{ key: 'linkify', label: 'no bare #NNN task refs in committed markdown', argv: ['scripts/gds/linkify-refs.js', '--check'], fix: 'node scripts/gds/linkify-refs.js', optional: true },
|
|
34
|
+
{ key: 'skill-lint', label: 'SKILL.md frontmatter parses', argv: ['scripts/gds/skill-lint.js'], fix: 'fix the ✗ lines (description: >- block scalars)' },
|
|
35
|
+
{ key: 'tests', label: 'DB-free unit lane (npm test)', argv: ['scripts/gds/run-unit-tests.js'], fix: 'read the FAIL lines; compare against origin/main before calling any of them environmental', onlyWith: '--tests', slow: true },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function runStep(step, { cwd = REPO_ROOT } = {}) {
|
|
39
|
+
const script = path.join(cwd, step.argv[0]);
|
|
40
|
+
if (!fs.existsSync(script)) return { ...step, status: step.optional ? 'skipped' : 'missing', ms: 0, tail: `${step.argv[0]} not present` };
|
|
41
|
+
const t0 = Date.now();
|
|
42
|
+
const r = spawnSync(process.execPath, [script, ...step.argv.slice(1)], { cwd, encoding: 'utf8', timeout: step.slow ? 15 * 60 * 1000 : 5 * 60 * 1000, env: { ...process.env, GDS_QUIET: '1' } });
|
|
43
|
+
const out = ((r.stdout || '') + (r.stderr || '')).trim().split(/\r?\n/);
|
|
44
|
+
const failing = out.filter((l) => /^\s*(✗|FAIL|not ok|STALE|✘)/.test(l) || /STALE|is stale|would update|failed/i.test(l));
|
|
45
|
+
return { ...step, status: r.status === 0 ? 'ok' : 'fail', code: r.status, signal: r.signal, ms: Date.now() - t0, tail: (failing.length ? failing : out.slice(-4)).slice(0, 12).join('\n') };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function summarize(results) {
|
|
49
|
+
const stale = results.filter((r) => r.status === 'fail' || r.status === 'missing');
|
|
50
|
+
return { ok: stale.length === 0, stale: stale.map((r) => r.key), results };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function format(summary) {
|
|
54
|
+
const lines = ['ship-check — what ship.js will trip on', ''];
|
|
55
|
+
for (const r of summary.results) {
|
|
56
|
+
const mark = r.status === 'ok' ? '✓' : r.status === 'skipped' ? '·' : '✗';
|
|
57
|
+
lines.push(`${mark} ${r.label} (${r.ms} ms)`);
|
|
58
|
+
if (r.status !== 'ok' && r.status !== 'skipped') {
|
|
59
|
+
for (const t of String(r.tail).split('\n')) lines.push(` ${t}`);
|
|
60
|
+
lines.push(` fix → ${r.fix}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(summary.ok ? 'all fresh — ship.js preflight should pass; the grader and CI are the remaining gates.' : `${summary.stale.length} stale: ${summary.stale.join(', ')} — heal, commit, re-run.`);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
const steps = STEPS.filter((s) => !s.onlyWith || argv.includes(s.onlyWith));
|
|
71
|
+
const results = steps.map((s) => runStep(s));
|
|
72
|
+
const summary = summarize(results);
|
|
73
|
+
console.log(argv.includes('--json') ? JSON.stringify(summary, null, 1) : format(summary));
|
|
74
|
+
process.exitCode = summary.ok ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { STEPS, runStep, summarize, format };
|
|
78
|
+
if (require.main === module) main();
|
|
@@ -87,12 +87,12 @@ function regenerateRepoMap() {
|
|
|
87
87
|
console.log(' repo-map regen failed (non-fatal) — CI --check-block backstops the tracked blocks');
|
|
88
88
|
return 'regen-failed';
|
|
89
89
|
}
|
|
90
|
-
const dirty = gitOk('git status --porcelain src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md
|
|
90
|
+
const dirty = gitOk('git status --porcelain src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md');
|
|
91
91
|
if (!dirty) {
|
|
92
92
|
console.log(' nested-doc symbol blocks already current — nothing to commit (docs/repo-map.md is gitignored, ADR 0110)');
|
|
93
93
|
return 'current';
|
|
94
94
|
}
|
|
95
|
-
shellOk('git add src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md
|
|
95
|
+
shellOk('git add src/bongos/CLAUDE.md src/bongos/routes/CLAUDE.md');
|
|
96
96
|
if (shellOk('git commit -m "chore(repo-map): auto-regenerate nested-doc symbol blocks (task 1224)"')) {
|
|
97
97
|
console.log(' committed auto-regenerated nested-doc symbol blocks');
|
|
98
98
|
return 'committed';
|