@bongos/core 1.19.435 → 1.19.436

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.435",
6
- "core_contract": "1.19.435",
7
- "source_commit": "93f1acd4ec60f7f6d7c3241e716a7a2c25315f02",
5
+ "core_version": "1.19.436",
6
+ "core_contract": "1.19.436",
7
+ "source_commit": "a1241e98d95626ad0f0e4fb92e0bf5570ebd1d19",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-30T01:56:28.344Z",
9
+ "built_at": "2026-08-30T02:36:42.560Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 367,
12
+ "docs_redacted": 369,
13
13
  "agent_docs_stubbed": 23,
14
- "functional_verbatim": 1740,
14
+ "functional_verbatim": 1743,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2130,
20
- "tree_sha256": "f47b962f793eb603e2233c230c31e64d83e1c9d0059a8e68f40c27badabb6f72",
19
+ "file_count": 2135,
20
+ "tree_sha256": "fb1407f644a9e1ce28d901f454bb38108b7de3165d3aca0ae74533bf44192727",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -259,6 +259,16 @@
259
259
  "mode": "0000644",
260
260
  "sha256": "0f5cb8e895c5bcb5e4901d8adce8c1394f6bbba2615a78837f5cc9393c841b09"
261
261
  },
262
+ {
263
+ "path": ".claude/skills/strand-fix/SKILL.md",
264
+ "mode": "0000644",
265
+ "sha256": "9a0b4f5bebdf780c4bfadde487146cb8ab5d08b3a748ee8623f336d7cbec5291"
266
+ },
267
+ {
268
+ "path": ".claude/skills/worktree-clean/SKILL.md",
269
+ "mode": "0000644",
270
+ "sha256": "18ac872b38786a748b597351304ccead88a3f8f795b18ac7a258b558997921f6"
271
+ },
262
272
  {
263
273
  "path": ".devcontainer/Dockerfile",
264
274
  "mode": "0000644",
@@ -1977,7 +1987,7 @@
1977
1987
  {
1978
1988
  "path": "docs/file-map.md",
1979
1989
  "mode": "0000644",
1980
- "sha256": "b0b9355c79b2f1caa965baf8afd645140d341f30a56483a3724697611e20322a"
1990
+ "sha256": "6029a720937dd848e9fb4c39f7adc8b4d096b077a211899a8ec3a78654b1e425"
1981
1991
  },
1982
1992
  {
1983
1993
  "path": "docs/handoff-template.md",
@@ -1987,7 +1997,7 @@
1987
1997
  {
1988
1998
  "path": "docs/module-api-changelog.md",
1989
1999
  "mode": "0000644",
1990
- "sha256": "a143667876a68f55e726b5342461ebb87ef424c43b73de5d234f13df0ab9e333"
2000
+ "sha256": "8261227744e98ae0628e0f819b9df602e3f7417ccc23b1753f6314a74ac3b9ce"
1991
2001
  },
1992
2002
  {
1993
2003
  "path": "docs/modules-contract.md",
@@ -5597,12 +5607,12 @@
5597
5607
  {
5598
5608
  "path": "package-lock.json",
5599
5609
  "mode": "0000644",
5600
- "sha256": "bbd8414f9a02067c7742dfbfdf0c5ec5a386330994822a47867d2d04b6b2b29e"
5610
+ "sha256": "6a868844044c8ad9bb4914949f7787e746f9e88fc03c2517a10a3aa3c31f0962"
5601
5611
  },
5602
5612
  {
5603
5613
  "path": "package.json",
5604
5614
  "mode": "0000644",
5605
- "sha256": "5cbdd7b7313ace6d7481d61088e1c29805ccd2df5ba21368f28bdfe4fcf7fd8e"
5615
+ "sha256": "185654794f0cbca9357dd5804fb45bf89fed4d8c358b2e6b0ffab14f1c2806f7"
5606
5616
  },
5607
5617
  {
5608
5618
  "path": "public-docs/index.html",
@@ -7269,6 +7279,11 @@
7269
7279
  "mode": "0000755",
7270
7280
  "sha256": "dfd5aa72caa26eb2c8cf3063e1b4f51e1f4bcd7fddcdd93d243279c130850628"
7271
7281
  },
7282
+ {
7283
+ "path": "scripts/gds/stale-worktrees.js",
7284
+ "mode": "0000644",
7285
+ "sha256": "40c5ef66ef5efc3754da367e64604af765d2e595986fc7af1af10f793d1a57e0"
7286
+ },
7272
7287
  {
7273
7288
  "path": "scripts/gds/start.js",
7274
7289
  "mode": "0000644",
@@ -7732,7 +7747,7 @@
7732
7747
  {
7733
7748
  "path": "src/module-api.js",
7734
7749
  "mode": "0000644",
7735
- "sha256": "1ff4e0ce398b437fc6cfadfaede99b6b977e108e8cf820ef3b954d49163be38f"
7750
+ "sha256": "61dda9cd35bcb286dd5573c96baded0e1c651945c7249113131f9b50e7f767f0"
7736
7751
  },
7737
7752
  {
7738
7753
  "path": "src/module-loader/catalog.js",
@@ -8689,6 +8704,11 @@
8689
8704
  "mode": "0000644",
8690
8705
  "sha256": "da4904efa11dbce4d4a61e0d0936d58d5566f89337dd855f4cdfab9e649626c2"
8691
8706
  },
8707
+ {
8708
+ "path": "tests/git_guard.mjs",
8709
+ "mode": "0000644",
8710
+ "sha256": "1b0b13310fb1afa544784d18fe0fa3ac2abc0f5ecc01e3ec0cb71c227df296ae"
8711
+ },
8692
8712
  {
8693
8713
  "path": "tests/git_merge_regen.mjs",
8694
8714
  "mode": "0000644",
@@ -10414,6 +10434,11 @@
10414
10434
  "mode": "0000644",
10415
10435
  "sha256": "27bd2a87e001eec2bc58e13cfa58e53f88c73ebe7504b944c718182247d98ca7"
10416
10436
  },
10437
+ {
10438
+ "path": "tests/stale_worktrees.mjs",
10439
+ "mode": "0000644",
10440
+ "sha256": "e003d6391d14212198c0e55494190e702adc0a27c2820362afe3d676c67a4963"
10441
+ },
10417
10442
  {
10418
10443
  "path": "tests/start_env_guard.mjs",
10419
10444
  "mode": "0000644",
@@ -0,0 +1,46 @@
1
+ ---
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`.
4
+ requires: [gh]
5
+ ---
6
+
7
+ You are landing one stranded task. The reconciler is not coming back for it — a strand produces no signal of its own, so nothing happens until someone walks it.
8
+
9
+ ## Preflight (first, always)
10
+
11
+ ```bash
12
+ node scripts/gds/skill-preflight.js strand-fix # exits 1 naming what is MISSING (gh)
13
+ ```
14
+
15
+ ## Which shape is this? (read first)
16
+
17
+ `node scripts/gds/strand-watch.js --json` — find the task. Three shapes, three owners:
18
+
19
+ | shape | status | reason | this skill? |
20
+ |---|---|---|---|
21
+ | `unprovable` | confirmed | `strand:*` | **yes** — below |
22
+ | `unlanded` | confirmed | none | no → `/merge-mode` |
23
+ | `ungraded` | completed | none | no → `/grade-recover` |
24
+
25
+ ## The walk (unprovable only)
26
+
27
+ 1. **Find the work.** In the main checkout:
28
+ ```
29
+ git fetch -q origin
30
+ git branch -a --list "*N*"
31
+ git log origin/main --oneline --grep="N" | head
32
+ gh pr list --search "N" --state all --json number,state,mergeable,url
33
+ ```
34
+ 2. **Pick the ONE matching case.**
35
+ - **A. A main commit carries `(task N` and the post-merge `unit` + `main-audit` on main are green** (`gh run list --branch main --limit 4`): the land happened; only the proof failed (the ship deleted the branch before the reconciler probed — task 1002724). Flip it: `node scripts/gds/api.js POST /api/gds/tasks/N/ship --body '{}'`. Credits land here (ADR 0120).
36
+ - **B. A local branch exists with commits ahead of `origin/main` and no PR**: the ship never pushed. From that worktree: `git push -u origin <branch>` then `gh pr create --base main --head <branch> --fill`. Green `unit` → server auto-merge → the reconciler flips it within ~5 min. If it merges but does not flip, do **A**.
37
+ - **C. A PR exists but is `CONFLICTING`**: the branch owner rebases it (`git merge origin/main`, resolve, push) — not this skill, and never `--force`. Report the PR URL and stop.
38
+ - **D. Nothing anywhere** — no branch, no PR, no commit: a codeless confirm. Ask what the task delivered. Work done outside this repo (on the box's instance checkout, a DB-only change captured in the ship note) is legitimately artifact-free → **A** with the reason in the ship body: `--body '{"note":"codeless: <where the work lives>"}'`. If the work was never done, release it instead so it returns to `ready`: `node scripts/gds/release.js N --reason "stranded codeless confirm; nothing delivered"`.
39
+ 3. **Verify**: `node scripts/gds/api.js GET /api/gds/tasks/N` → `"status": "shipped"` (or `ready` after a release). Re-run `strand-watch.js --line` — the task must be gone from it. If the claim gate was the symptom, `claim.js` now succeeds.
40
+
41
+ ## Rules
42
+
43
+ - One task per invocation; the strand list is short and every case differs.
44
+ - Never `git push origin main`, never force-push, never re-`confirm` around the grader — the fix is always land, flip, or release.
45
+ - Wrong shape → hand off (`/merge-mode`, `/grade-recover`) instead of improvising.
46
+ - No claim is needed to walk someone else's strand — landing confirmed work is the reconciler's job, and this is its manual fallback (Metic+; the ship route is rank-gated server-side).
@@ -0,0 +1,26 @@
1
+ ---
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".
4
+ ---
5
+
6
+ **Script skill (authoritative).** Two scripts do the work; this skill only sequences them and shows the list before anything is removed.
7
+
8
+ ## What this skill does
9
+
10
+ 1. `node scripts/gds/stale-worktrees.js` — the list. A worktree is **stale** when its branch is an ancestor of `origin/main` (the land happened) or the branch is gone (the ship deleted it). Detached worktrees and unmerged branches are **live** and never touched. Run `git fetch origin` first so "merged into main" reflects today's main, not the last fetch.
11
+ 2. Show the stale list to the builder (names + why). The worktree holding the **current claim** is never stale by construction (its branch is unmerged), but say so if the builder asks.
12
+ 3. For each stale name: `node scripts/gds/worktree.js remove <name>`. That helper unlinks any node_modules junction BEFORE `git worktree remove`, which is the whole reason raw git is off-limits here (learning in `scripts/gds/CLAUDE.md` → "Gotchas / invariants"; the `git-guard.js` PreToolUse hook blocks the raw form).
13
+ - A worktree with untracked leftovers (`.playwright-mcp/`, a stray screenshot) refuses without `--force`. Show the `git -C <path> status --porcelain` output; if it is only untracked junk, re-run with `--force`. If it shows **modified tracked files**, skip it and report — someone's uncommitted work is not this skill's to delete.
14
+ 4. `git worktree prune` — clears any registrations whose directory is already gone.
15
+ 5. Print the tally: removed / skipped (with why) / still live. Stale branches stay — a merged branch is harmless and `git branch -d` is the ship's business, not a cleanup chore.
16
+
17
+ ## Safety
18
+
19
+ - Read-only until step 3; the list is shown first.
20
+ - Never `--force` over modified tracked files.
21
+ - Never deletes branches, never touches the primary checkout, never calls `git worktree remove` directly.
22
+ - No claim is needed: worktrees are local machine state, not project state — nothing here is committed.
23
+
24
+ ## Calls
25
+
26
+ `node scripts/gds/stale-worktrees.js [--json]` · `node scripts/gds/worktree.js remove <name> [--force]` · `git worktree prune`.
package/docs/file-map.md CHANGED
@@ -183,6 +183,7 @@
183
183
  │ ├── recall.js ← `/recall` CLI: two-call progressive-disclosure search (summary → `--expand <id>`) over Bongos retrieval layer (ADR 0060); `node scripts/gds/recall.js <query>`
184
184
  │ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills. Task 1002927 / R151 taught it the GRADES: `--full --fields-file f.json` files a Full Idea, `--spark` is capture-then-mark (two requests — the route refuses a pre-marked capture), and after a plain quick capture it OFFERS to develop further. That offer is TTY-gated (`--no-prompt` too): capture.js is run by agents with no terminal and reads its body from stdin, so an unguarded prompt would hang every non-interactive caller
185
185
  │ ├── spark.js ← (tasks 1002920 / R144 + 1002921 / R145 + 1002922 / R146) the whole spark handoff from the terminal: mark one of your OWN quick ideas as a Big Idea Spark (`--undo` withdraws it), `--list` is the SPARK QUEUE, `--template` prints the five Full Idea fields, `--develop <id> --fields-file <json>` is the PICKUP (any builder; author + developer both recorded). Author-only server-side for the mark (it is the consent); its own command because sparking acts on an EXISTING idea, which is why capture.js refuses grade='spark'
186
+ │ ├── stale-worktrees.js ← (task 1003365) the per-claim worktrees whose branch already landed on origin/main or is gone — pure git facts, no API; `--line` is the SessionStart probe, `/worktree-clean` walks the list through worktree.js remove
186
187
  │ ├── worktree-claim-guard.js ← (task 1002579) the PURE decision behind worktree-per-claim, enforced at claim time: claiming in the MAIN CHECKOUT is refused (escape hatch `--here` → loud warning), a linked worktree passes, an unclassifiable tree fails open. Stops the ship-time "the main checkout is on <branch>, not main" strand at its cause
187
188
  │ ├── worktree.js ← (task 1002809) junction-safe worktree add/remove/list — outside-repo worktrees get a node_modules junction via node (no shell quoting), removed BEFORE `git worktree remove`
188
189
  │ ├── learning-capture.js ← file a `learnings` row (DB-backed short-form rule capture, replacing hand-edited recipe files for a single gotcha)
@@ -413,7 +414,9 @@ tests/
413
414
  ├── recall/SKILL.md ← /recall — one-call project-knowledge search across repo docs + DB prose (tasks, learnings, session logs)
414
415
  ├── 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)
415
416
  ├── 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
416
- └── status/SKILL.md ← /status — what's the status of X / what's left for criterion Cn (read-only rollup)
417
+ ├── status/SKILL.md ← /status — what's the status of X / what's left for criterion Cn (read-only rollup)
418
+ ├── 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)
419
+ └── 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)
417
420
  ```
418
421
  <!-- END GENERATED FILE-MAP .claude/skills/ -->
419
422
 
@@ -422,7 +425,25 @@ tests/
422
425
  <!-- BEGIN GENERATED FILE-MAP .claude/scheduled-tasks/ (scripts/gds/gen-file-map.js — do not hand-edit; notes live in docs/file-map.notes.json) -->
423
426
  ```
424
427
  .claude/scheduled-tasks/
425
- └── redteam-weekly/SKILL.md ← weekly deterministic red-team patrol (task 1003128): full-history gitleaks scan + dependency drift review (audit delta, cooldown, young pkgs, stale overrides) + action-pin check → files security reports (advisory, ADR 0162). FIRED BY .github/workflows/redteam-patrol.yml, Mondays 06:17 UTC — `cadence: weekly` here and that cron must agree or fitness Check 25 (scripts/gds/routine-schedule.js) fails the gate (task 1003209, ADR 0188)
428
+ ├── bfg/SKILL.md ← nightly BFG pass (MAS Phase 6, ADR 0026) — doc/memory hygiene + DB counter-vs-ledger drift (Part A), per-builder interaction scoring into transferable-practice suggestions (Part B), and the audited cross-builder memory write (Part C, OFF unless BFG_WRITE_ENABLED) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
429
+ ├── bfg-session-eval/SKILL.md ← nightly session-inefficiency evaluator (MAS Phase 6D, ADR 0027) — mines the session corpus for wasteful sessions and files the fixes as idea_inbox rows plus a docs/audits report (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
430
+ ├── box-dormant-reclaim/SKILL.md ← daily control-plane sweep that fully de-provisions dev boxes parked longer than BOX_DORMANT_DAYS, destroying the leftover snapshot and pinging the builder (ADR 0031 §5(a)); a no-op on an instance with no managed boxes (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
431
+ ├── box-idle-suspend/SKILL.md ← hourly control-plane sweep that DESTROYS idle dev boxes — droplet and snapshot, not a reversible park — once BOX_IDLE_MINUTES elapses with no heartbeat (ADR 0031 §5(a), task 804) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
432
+ ├── core-update-subscription/SKILL.md ← daily patch-only auto-upgrade of subscribed consumer instances (ADR 0136) — deterministic `subscribe.js` step, health-gated with auto-rollback; opt-in and autonomy-gated, so a no-op until an operator arms it (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
433
+ ├── dep-audit-nightly/SKILL.md ← nightly dependency-vulnerability scan (`scripts/security/dep-audit.js`) across npm and the droplet's system packages; findings become idea_inbox rows plus one vulnerability_report per high/critical (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
434
+ ├── diagram-drift/SKILL.md ← daily deterministic backstop that re-derives the onboarding diagrams from migrations plus the live version API and files an idea when the committed ones have drifted (`check.js --file-idea`) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
435
+ ├── idea-triage-nightly/SKILL.md ← nightly autonomous idea_inbox walk — promotes high-confidence ideas to tasks, discards obvious noise, defers anything ambiguous; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
436
+ ├── main-audit/SKILL.md ← post-merge trust-boundary backstop (ADR 0043 / SEC 863) — audits commits landed on main for sub-Metic edits to permission-sensitive surfaces (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
437
+ ├── newcomer-floor/SKILL.md ← daily check that the three evergreen newcomer chores (engineer / artist / ideator) stay stocked; the server restocks them via modules/onboarding/newcomer-restock.js (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
438
+ ├── newcomer-generator/SKILL.md ← daily deterministic generator (`generate.js`) that tops the newcomer_friendly task queue back up to its floor (criterion C1 — newcomer queue always populated) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
439
+ ├── overnight-builder/SKILL.md ← autonomous overnight session-runner — claims the highest-priority safe task, works it end-to-end and ships it; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
440
+ ├── overnight-code-review/SKILL.md ← nightly autonomous code-quality review; each finding lands as its own idea_inbox row so it survives triage; autonomy-gated and OFF by default (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
441
+ ├── peer-vote-tally/SKILL.md ← deterministic tally (`tally.js`) folding builder-on-builder peer votes at least 7 days old into karma_log rows (criterion C2) (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
442
+ ├── redteam-weekly/SKILL.md ← weekly deterministic red-team patrol (task 1003128): full-history gitleaks scan + dependency drift review (audit delta, cooldown, young pkgs, stale overrides) + action-pin check → files security reports (advisory, ADR 0162). FIRED BY .github/workflows/redteam-patrol.yml, Mondays 06:17 UTC — `cadence: weekly` here and that cron must agree or fitness Check 25 (scripts/gds/routine-schedule.js) fails the gate (task 1003209, ADR 0188)
443
+ ├── repo-simplify-sweep/SKILL.md ← weekly repo-cleanliness janitor — runs repo-metrics, files dead-code findings to idea_inbox, and auto-fixes only the narrow safe class (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
444
+ ├── security-review-weekly/SKILL.md ← weekly security-state review — stale vulnerability_reports, ageing security-tagged tasks, recent high/critical dependency alerts; one Archon broadcast when anything is stale, silent otherwise (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
445
+ ├── skill-usage-review/SKILL.md ← nightly skill-usage feedback loop — aggregates 24h of skill invocations into friction findings filed as `[skill-friction]` idea_inbox rows (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
446
+ └── stale-claim-release/SKILL.md ← hourly sweep releasing claims whose session_pulses heartbeat has been silent for GDS_STALE_CLAIM_HOURS, returning the task to ready and pinging the builder (restored in task 1003365; unscheduled in core — only redteam-weekly has a firing workflow; config/scheduled-routines.json records the intended cadence)
426
447
  ```
427
448
  <!-- END GENERATED FILE-MAP .claude/scheduled-tasks/ -->
428
449
 
@@ -432,6 +453,11 @@ To install on a new machine, symlink each routine's directory under `~/.claude/s
432
453
 
433
454
  ```
434
455
  .claude/hooks/
456
+ ├── 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)
457
+ ├── 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)
458
+ ├── _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/`
459
+ ├── cascade-route.js ← PreToolUse (Task|Agent): cheapest-sufficient start-model advisory for a subagent spawn (ADR 0024); inert unless OTB_AUTONOMY_ENABLED=1
460
+ ├── land-watch-card.js ← UserPromptSubmit: delivers the SHIPPED banner once an async CI land completes (reads `scripts/gds/land-watch.js`)
435
461
  ├── worktree-guard.js ← PreToolUse (Edit|Write|MultiEdit|NotebookEdit): DENY a write that lands in the wrong git checkout (main/sibling worktree) when the session is in a worktree — hands back the corrected worktree path. Fail-open; opt-out OTB_ALLOW_CROSS_TREE=1 (V4.R13, [#951](https://example.com/builders#/task/951))
436
462
  ├── session-start.js ← SessionStart: <redacted> .env.local into a fresh worktree, then chains onboarding-ship + rank-change greeting + memory pull
437
463
  ├── rank-change-greeting.js ← promotion/demotion greeting (V3.R28 #249); spawned by session-start.js, also runnable standalone
@@ -442,11 +468,10 @@ To install on a new machine, symlink each routine's directory under `~/.claude/s
442
468
  ├── lifecycle-card.js ← PostToolUse: deterministic delivery of MUTATING lifecycle cards (claim/ship/release) — the write-side mirror of session-cards.js (task 1288)
443
469
  ├── pre-compact.js ← PreCompact: thin reader around eviction-policy.js — emits an eviction directive (blocks to drop + one-line recovery stubs) at the compaction seam (task 1349)
444
470
  ├── eviction-policy.js ← pure deterministic in-session compaction classifier (transcript turns → eviction set + replacement stubs); no I/O, unit-tested in tests/eviction_policy.mjs; consumed by pre-compact.js (task 1348)
445
- ├── stop-sound.js ← ⚠ ABSENT from this repo (see the note below). Superseded anyway: the turn-end sound is now `scripts/gds/turn-end-sound.js`, wired as the Stop hook in `.claude/settings.json` and cross-platform rather than macOS-only (task 1003272)
446
471
  └── stop-refcheck.js ← Stop: deterministic backstop that catches a bare `#NNN` Bongos ref in live session text (autolinks to a 404 GitHub issue) and asks for the clickable form (follow-on to task 929)
447
472
  ```
448
473
 
449
- > **⚠ THIS TREE IS STALE: 12 of the 13 files listed above are NOT in this repo** — only `secret-gate.js` exists on disk, and `.claude/settings.json` wires just `SessionStart` (three `scripts/gds/*` probes + the sound-prefs mirror), `PreToolUse` (secret-gate) and `Stop` (`scripts/gds/turn-end-sound.js`). The hook tree went the same way as `.claude/sounds/` — absent from every commit, lost when core was re-seeded to 1.18.0 (task 1003272 found and fixed the sound half of that). Do not assume a hook named here runs; check `.claude/settings.json`. Auditing and either restoring or deleting the other twelve is its own task, not a doc edit.
474
+ > Restored in task 1003365 from the box's pre-re-seed checkout: the tree was lost when core was re-seeded to 1.18.0 (task 1003272 found the sound half; this brought back the rest). `stop-sound.js` was NOT restored — `scripts/gds/turn-end-sound.js` superseded it. Every hook above is wired in `.claude/settings.json`, which is the authority on what actually runs.
450
475
 
451
476
  (Before [#671](https://example.com/builders#/task/671) these ran via `bash` shims — `sync-env-local.sh`, `conductor.sh`, `onboarding-ship-logs.sh`, `rank-change-greeting.sh`, `pull-memory.sh` — which failed on Windows without Git Bash. The Node entries above replaced them.)
452
477
 
@@ -1307,5 +1307,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1307
1307
  landed since 1.19.433 with no explicit bump. run 33281881764. (task 1002620)
1308
1308
  1.19.435 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1309
1309
  landed since 1.19.434 with no explicit bump. run 33286790214. (task 1002620)
1310
+ 1.19.436 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1311
+ landed since 1.19.435 with no explicit bump. run 33288364716. (task 1002620)
1310
1312
  ---------------------------------------------------------------------------
1311
1313
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.435",
3
+ "version": "1.19.436",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.435",
9
+ "version": "1.19.436",
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.435",
3
+ "version": "1.19.436",
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",
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/stale-worktrees.js — the worktrees whose work already landed.
3
+ //
4
+ // Worktree-per-claim (task 1642, enforced at claim time by worktree-claim-guard.js)
5
+ // means every shipped task leaves a `.claude/worktrees/task-N` behind unless someone
6
+ // removes it. Nothing did: the 2026-08-29 audit (task 1003365) found 74 dirs for 70
7
+ // shipped tasks. A stale worktree costs disk, slows `git worktree list`, and — the
8
+ // real hazard — tempts a builder into `git worktree remove`, which follows the
9
+ // node_modules junction into the SHARED node_modules (task 1002809).
10
+ //
11
+ // "Stale" needs no API call: a worktree is stale when its branch is already an
12
+ // ancestor of origin/main (the land happened) or its branch is gone (the ship
13
+ // deleted it). Both are pure git facts, so this runs in well under a second at
14
+ // SessionStart. The primary checkout is never listed.
15
+ //
16
+ // Usage:
17
+ // node scripts/gds/stale-worktrees.js --line # ONE line, or nothing; always exit 0
18
+ // node scripts/gds/stale-worktrees.js --json # { stale: [{name, path, branch, why}], live: [...] }
19
+ // node scripts/gds/stale-worktrees.js # human report; exit 0 clean, 1 stale found
20
+ //
21
+ // Removal is deliberately NOT here — `/worktree-clean` walks this list through
22
+ // `node scripts/gds/worktree.js remove <name>`, the junction-safe path.
23
+
24
+ 'use strict';
25
+
26
+ const { execFileSync } = require('child_process');
27
+ const path = require('path');
28
+
29
+ const MAIN_REF = 'origin/main';
30
+
31
+ function git(args, cwd) {
32
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
33
+ }
34
+
35
+ // Parse `git worktree list --porcelain` into [{path, branch|null, isMain}].
36
+ function parseWorktreeList(porcelain) {
37
+ const out = [];
38
+ let cur = null;
39
+ for (const raw of String(porcelain).split('\n')) {
40
+ const line = raw.trim();
41
+ if (line.startsWith('worktree ')) {
42
+ cur = { path: line.slice('worktree '.length), branch: null, isMain: out.length === 0 };
43
+ out.push(cur);
44
+ } else if (cur && line.startsWith('branch ')) {
45
+ cur.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // Pure classifier so the decision is unit-testable without a repo: `isMerged(branch)`
52
+ // and `branchExists(branch)` are the two git facts it needs.
53
+ function classifyWorktrees(worktrees, { isMerged, branchExists }) {
54
+ const stale = [];
55
+ const live = [];
56
+ for (const wt of worktrees) {
57
+ if (wt.isMain) continue;
58
+ const name = path.basename(wt.path);
59
+ if (!wt.branch) { live.push({ name, path: wt.path, branch: null }); continue; } // detached: not ours to judge
60
+ if (!branchExists(wt.branch)) stale.push({ name, path: wt.path, branch: wt.branch, why: 'branch gone' });
61
+ else if (isMerged(wt.branch)) stale.push({ name, path: wt.path, branch: wt.branch, why: 'merged into main' });
62
+ else live.push({ name, path: wt.path, branch: wt.branch });
63
+ }
64
+ return { stale, live };
65
+ }
66
+
67
+ // Three git calls total, whatever the worktree count: the list, every local branch,
68
+ // every branch already merged into origin/main. Per-worktree subprocesses would be
69
+ // N+1 over exactly the number this probe exists to shrink.
70
+ function refNames(out) {
71
+ return new Set(String(out).split('\n').map((s) => s.trim()).filter(Boolean));
72
+ }
73
+
74
+ function scan(cwd) {
75
+ const worktrees = parseWorktreeList(git(['worktree', 'list', '--porcelain'], cwd));
76
+ const existing = refNames(git(['for-each-ref', '--format=%(refname:short)', 'refs/heads/'], cwd));
77
+ let merged;
78
+ try { merged = refNames(git(['branch', '--merged', MAIN_REF, '--format=%(refname:short)'], cwd)); }
79
+ catch { merged = new Set(); } // no origin/main yet (fresh clone, no fetch) → nothing is provably stale
80
+ return classifyWorktrees(worktrees, {
81
+ branchExists: (b) => existing.has(b),
82
+ isMerged: (b) => merged.has(b),
83
+ });
84
+ }
85
+
86
+ function lineFor(r) {
87
+ if (!r.stale.length) return '';
88
+ const names = r.stale.slice(0, 6).map((s) => s.name).join(', ');
89
+ const more = r.stale.length > 6 ? ` +${r.stale.length - 6} more` : '';
90
+ return `Stale worktrees: ${r.stale.length} whose work already landed (${names}${more}). `
91
+ + 'Run /worktree-clean — never `git worktree remove` (it follows the node_modules junction).';
92
+ }
93
+
94
+ function main() {
95
+ const argv = process.argv.slice(2);
96
+ const cwd = process.cwd();
97
+ let r;
98
+ try { r = scan(cwd); } catch (e) {
99
+ if (argv.includes('--line')) process.exit(0); // fail-open: a probe never blocks a session
100
+ console.error(`stale-worktrees: undetermined — ${e.message || e}`);
101
+ process.exit(2);
102
+ }
103
+ if (argv.includes('--json')) { console.log(JSON.stringify(r, null, 2)); return; }
104
+ if (argv.includes('--line')) { const l = lineFor(r); if (l) console.log(l); return; }
105
+ console.log(` Worktrees: ${r.live.length} live, ${r.stale.length} stale`);
106
+ for (const s of r.stale) console.log(` stale ${s.name} (${s.why})`);
107
+ for (const l of r.live) console.log(` live ${l.name} [${l.branch || 'detached'}]`);
108
+ if (r.stale.length) { console.log(' Remove with: node scripts/gds/worktree.js remove <name> (or /worktree-clean)'); process.exit(1); }
109
+ }
110
+
111
+ if (require.main === module) main();
112
+
113
+ module.exports = { parseWorktreeList, classifyWorktrees, lineFor };
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.435'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.436'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,119 @@
1
+ // tests/git_guard.mjs — the PreToolUse gate that refuses four raw git shapes.
2
+ // The gate's contract: exit 2 (blocked, reason on stderr) on `git stash`,
3
+ // `git worktree remove`, `git push origin main`, and `git branch -D`; exit 0 on
4
+ // everything else — including the read-only `git stash list`, the supported
5
+ // worktree remover, and any payload it does not understand. It is the cooperative
6
+ // layer (ADR 0043 shape), so every unknown input fails OPEN.
7
+ //
8
+ // Spawned rather than called in-process (unlike tests/secret_gate.mjs, which
9
+ // exercises decide() directly) because the EXIT CODE is the whole contract with
10
+ // Claude Code: 2 blocks the tool call, anything else lets it through.
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { spawnSync } from 'node:child_process';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const HOOK = path.join(ROOT, '.claude', 'hooks', 'git-guard.js');
19
+
20
+ // Run the hook exactly as Claude Code does: the PreToolUse payload on stdin.
21
+ function runHook(stdin, env = {}) {
22
+ const r = spawnSync(process.execPath, [HOOK], {
23
+ input: stdin,
24
+ encoding: 'utf8',
25
+ env: { ...process.env, BONGOS_GIT_GUARD_OFF: '', ...env },
26
+ });
27
+ return { status: r.status, stderr: r.stderr || '' };
28
+ }
29
+
30
+ function bash(command, env) {
31
+ return runHook(JSON.stringify({ tool_name: 'Bash', tool_input: { command } }), env);
32
+ }
33
+
34
+ test('blocks every git stash form except the read-only list', () => {
35
+ for (const cmd of [
36
+ 'git stash',
37
+ 'git stash push -m "wip"',
38
+ 'git stash pop',
39
+ 'git stash show -p',
40
+ 'git -C ../wt-1003346 stash',
41
+ 'cd ../wt-1003346 && git stash save "before the merge"',
42
+ ]) {
43
+ const r = bash(cmd);
44
+ assert.equal(r.status, 2, `expected block for: ${cmd}`);
45
+ assert.match(r.stderr, /git-guard: blocked/);
46
+ assert.match(r.stderr, /commit on your own branch/);
47
+ }
48
+ });
49
+
50
+ test('blocks git worktree remove and points at the supported remover', () => {
51
+ for (const cmd of [
52
+ 'git worktree remove ../wt-1003346',
53
+ 'git worktree remove --force ../wt-1003346',
54
+ 'git -C /repo worktree remove wt-1',
55
+ ]) {
56
+ const r = bash(cmd);
57
+ assert.equal(r.status, 2, `expected block for: ${cmd}`);
58
+ assert.match(r.stderr, /scripts\/gds\/worktree\.js remove/);
59
+ }
60
+ });
61
+
62
+ test('blocks a direct push to main in both refspec forms', () => {
63
+ for (const cmd of [
64
+ 'git push origin main',
65
+ 'git push origin HEAD:main',
66
+ 'git push origin feature/x:main',
67
+ 'git push --force origin main',
68
+ 'git push -u origin main',
69
+ 'cd "$CLAUDE_PROJECT_DIR" && git push origin HEAD:main',
70
+ ]) {
71
+ const r = bash(cmd);
72
+ assert.equal(r.status, 2, `expected block for: ${cmd}`);
73
+ assert.match(r.stderr, /builder-ship/);
74
+ }
75
+ });
76
+
77
+ test('blocks git branch -D and points at -d', () => {
78
+ for (const cmd of ['git branch -D task-1003346', 'git branch -D old', 'git -C . branch -r -D stale']) {
79
+ const r = bash(cmd);
80
+ assert.equal(r.status, 2, `expected block for: ${cmd}`);
81
+ assert.match(r.stderr, /use `-d`/);
82
+ }
83
+ });
84
+
85
+ test('allows the read-only, the supported, and the ordinary', () => {
86
+ for (const cmd of [
87
+ 'git stash list',
88
+ 'git status',
89
+ 'git push origin task-1003346',
90
+ 'git branch -d merged-branch',
91
+ 'git worktree list --porcelain',
92
+ 'node scripts/gds/worktree.js remove wt-1003346',
93
+ 'npm test',
94
+ ]) {
95
+ assert.equal(bash(cmd).status, 0, `false positive on: ${cmd}`);
96
+ }
97
+ });
98
+
99
+ test('non-Bash tools and unknown payload shapes fail open', () => {
100
+ assert.equal(
101
+ runHook(JSON.stringify({ tool_name: 'Write', tool_input: { file_path: 'x.js', content: 'git stash' } })).status,
102
+ 0,
103
+ );
104
+ assert.equal(runHook(JSON.stringify({ tool_name: 'Bash' })).status, 0);
105
+ assert.equal(runHook(JSON.stringify({})).status, 0);
106
+ assert.equal(runHook('null').status, 0);
107
+ });
108
+
109
+ test('malformed stdin fails open rather than breaking the turn', () => {
110
+ for (const raw of ['', 'not json at all', '{"tool_name":"Bash",']) {
111
+ assert.equal(runHook(raw).status, 0, `expected fail-open for: ${JSON.stringify(raw)}`);
112
+ }
113
+ });
114
+
115
+ test('kill-switch disables the gate entirely', () => {
116
+ for (const cmd of ['git stash', 'git push origin main', 'git worktree remove x', 'git branch -D y']) {
117
+ assert.equal(bash(cmd, { BONGOS_GIT_GUARD_OFF: '1' }).status, 0, `kill-switch missed: ${cmd}`);
118
+ }
119
+ });
@@ -0,0 +1,66 @@
1
+ // tests/stale_worktrees.mjs — the stale-worktree classifier is a pure function of git
2
+ // facts (task 1003365): no repo, no API, just the porcelain text + two predicates.
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { parseWorktreeList, classifyWorktrees, lineFor } from '../scripts/gds/stale-worktrees.js';
6
+
7
+ const PORCELAIN = [
8
+ 'worktree C:/repo',
9
+ 'HEAD aaaa',
10
+ 'branch refs/heads/main',
11
+ '',
12
+ 'worktree C:/repo/.claude/worktrees/task-1',
13
+ 'HEAD bbbb',
14
+ 'branch refs/heads/task-1',
15
+ '',
16
+ 'worktree C:/repo/.claude/worktrees/task-2',
17
+ 'HEAD cccc',
18
+ 'branch refs/heads/task-2',
19
+ '',
20
+ 'worktree C:/repo/.claude/worktrees/task-3',
21
+ 'HEAD dddd',
22
+ 'branch refs/heads/task-3',
23
+ '',
24
+ 'worktree C:/repo/.claude/worktrees/spike',
25
+ 'HEAD eeee',
26
+ 'detached',
27
+ '',
28
+ ].join('\n');
29
+
30
+ test('parses porcelain into worktrees, first entry is the primary checkout', () => {
31
+ const wts = parseWorktreeList(PORCELAIN);
32
+ assert.equal(wts.length, 5);
33
+ assert.equal(wts[0].isMain, true);
34
+ assert.equal(wts[0].branch, 'main');
35
+ assert.equal(wts[1].branch, 'task-1');
36
+ assert.equal(wts[4].branch, null);
37
+ });
38
+
39
+ test('merged or branch-gone worktrees are stale; unmerged and detached are live; main is skipped', () => {
40
+ const wts = parseWorktreeList(PORCELAIN);
41
+ const r = classifyWorktrees(wts, {
42
+ isMerged: (b) => b === 'task-1',
43
+ branchExists: (b) => b !== 'task-2',
44
+ });
45
+ assert.deepEqual(r.stale.map((s) => [s.name, s.why]), [
46
+ ['task-1', 'merged into main'],
47
+ ['task-2', 'branch gone'],
48
+ ]);
49
+ assert.deepEqual(r.live.map((l) => l.name), ['task-3', 'spike']);
50
+ });
51
+
52
+ test('--line is empty when nothing is stale and names the junction-safe path when something is', () => {
53
+ assert.equal(lineFor({ stale: [], live: [] }), '');
54
+ const line = lineFor({ stale: [{ name: 'task-9', why: 'merged into main' }], live: [] });
55
+ assert.match(line, /Stale worktrees: 1 /);
56
+ assert.match(line, /task-9/);
57
+ assert.match(line, /\/worktree-clean/);
58
+ });
59
+
60
+ test('--line caps the named list at six', () => {
61
+ const stale = Array.from({ length: 8 }, (_, i) => ({ name: `task-${i}`, why: 'merged into main' }));
62
+ const line = lineFor({ stale, live: [] });
63
+ assert.match(line, /Stale worktrees: 8 /);
64
+ assert.match(line, /\+2 more/);
65
+ assert.doesNotMatch(line, /task-7/);
66
+ });