@bongos/core 1.20.7 → 1.20.8

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.20.7",
6
- "core_contract": "1.20.7",
7
- "source_commit": "9a8e336459336ffc6beebeac8b6496960dec3bdd",
5
+ "core_version": "1.20.8",
6
+ "core_contract": "1.20.8",
7
+ "source_commit": "fb1af293583380a02ab4fa8df4a06e31a89c73cc",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-30T04:20:20.175Z",
9
+ "built_at": "2026-09-30T04:32:44.486Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 556,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2621,
14
+ "functional_verbatim": 2623,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3203,
20
- "tree_sha256": "dcbd562f300693a4aac156ab9d3d09aa65f2ec33fe5f0b469846f0eaca1a6005",
19
+ "file_count": 3205,
20
+ "tree_sha256": "4b5867abb42ec4c580d94333e13d9f029d81da62bf68a26c753511d54082f26c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1842,7 +1842,7 @@
1842
1842
  {
1843
1843
  "path": "docs/adr/0274-one-kernel-three-role-packs.md",
1844
1844
  "mode": "0000644",
1845
- "sha256": "d37cbce118162e144969efde564d92778bb6175c3af0d4e1502195aea18aa004"
1845
+ "sha256": "4164f1f1fb3cf0317ee19e168420aef85d7c529a5300d3fc46566010f42d9fbf"
1846
1846
  },
1847
1847
  {
1848
1848
  "path": "docs/adr/0275-one-source-for-a-written-role-responsibility.md",
@@ -2762,7 +2762,7 @@
2762
2762
  {
2763
2763
  "path": "docs/file-map.md",
2764
2764
  "mode": "0000644",
2765
- "sha256": "803f616840f604487828796195cdc74a3f834dc5f5117309ef01725b9d13b7aa"
2765
+ "sha256": "fd6ec16a880ab7b3ef0e31674c51ab9733ffda14855043aa6cc5afaddb20c03e"
2766
2766
  },
2767
2767
  {
2768
2768
  "path": "docs/handoff-template.md",
@@ -2772,7 +2772,7 @@
2772
2772
  {
2773
2773
  "path": "docs/module-api-changelog.md",
2774
2774
  "mode": "0000644",
2775
- "sha256": "20bca63270af7e3ccc11037fc0faf76c5d7ed75d8bde9b115c96b1b6f4e985a2"
2775
+ "sha256": "4bcc9fffeff37ae0b087af39c16e3cf8f381a066b2e70019fe8b1f30f49bce3c"
2776
2776
  },
2777
2777
  {
2778
2778
  "path": "docs/modules-contract.md",
@@ -2837,22 +2837,22 @@
2837
2837
  {
2838
2838
  "path": "docs/onboarding/slash-commands.md",
2839
2839
  "mode": "0000644",
2840
- "sha256": "92ce9c4de763330c4ac877a1ba79d0b11d88797c711b3517adebe8604bdf59ea"
2840
+ "sha256": "cc792eb9391d147e443ba646f991f99f88245fbb96adeda7bf96364fa9bc7a85"
2841
2841
  },
2842
2842
  {
2843
2843
  "path": "docs/packs/artist.md",
2844
2844
  "mode": "0000644",
2845
- "sha256": "9d317443b3676bf68b7818473e75448e26a16c6316cad7298de7a4037d73cf5c"
2845
+ "sha256": "d9a0b37fb3e03ab2e0f874abf3db40fed123cae5c0f6443a0e176adcdfc30b1b"
2846
2846
  },
2847
2847
  {
2848
2848
  "path": "docs/packs/engineer.md",
2849
2849
  "mode": "0000644",
2850
- "sha256": "416aa7f4303a0148d52ab5c0e5720a327fa260a034a9558774df125e8876eee2"
2850
+ "sha256": "30e4d2363f1fea52cb347ba8a642277fe5046249f77858e947b4434150ff5746"
2851
2851
  },
2852
2852
  {
2853
2853
  "path": "docs/packs/ideator.md",
2854
2854
  "mode": "0000644",
2855
- "sha256": "bcb98ba4302f46c2001eb7081684300150966a7b05cc43691199fa1573292f90"
2855
+ "sha256": "e09e80d1bd99951f9edde09a743ef4e597b26810fca26c37045c2d1f9dc8b2f6"
2856
2856
  },
2857
2857
  {
2858
2858
  "path": "docs/page-inventory.json",
@@ -6567,7 +6567,7 @@
6567
6567
  {
6568
6568
  "path": "modules/lifecycle/ship-card.js",
6569
6569
  "mode": "0000644",
6570
- "sha256": "a055854c667a30b46465db35e8d88dd92b0d6438a5609281e1c49e1ae66f3a7f"
6570
+ "sha256": "1935e6f0a3fb9089c2aacd9bbdf1ff1296487065c8c5a25beaf96306d88a9f5b"
6571
6571
  },
6572
6572
  {
6573
6573
  "path": "modules/lifecycle/ship-files.js",
@@ -8852,12 +8852,12 @@
8852
8852
  {
8853
8853
  "path": "package-lock.json",
8854
8854
  "mode": "0000644",
8855
- "sha256": "1822db240575bf86013d014572456d1f007004bfdc29209929938288ba43e3b2"
8855
+ "sha256": "3a10ecd6f93e42f1792e7edb0a406edaefb3377506afb359cde35e3fdd901296"
8856
8856
  },
8857
8857
  {
8858
8858
  "path": "package.json",
8859
8859
  "mode": "0000644",
8860
- "sha256": "1b1b7816c342fed258f9eaa453ce64588f26fe28beb4ab3eb1dfc06ec8f4529e"
8860
+ "sha256": "065257986765387a5b632092aab054e6a9d8299b9c27473a9b0e306dec3d73cc"
8861
8861
  },
8862
8862
  {
8863
8863
  "path": "public-docs/index.html",
@@ -8877,7 +8877,7 @@
8877
8877
  {
8878
8878
  "path": "release-notes.json",
8879
8879
  "mode": "0000644",
8880
- "sha256": "59dcfa061d238c013470c67e6a82249f559fdbc4509c0c1d4ba8c29c03cc68ab"
8880
+ "sha256": "3c69418a8c305f1aa08dd13193ee4489ac818f9723147c218e8f5745dfae1e8b"
8881
8881
  },
8882
8882
  {
8883
8883
  "path": "scripts/bongos-mcp.js",
@@ -9172,7 +9172,7 @@
9172
9172
  {
9173
9173
  "path": "scripts/gds/claim.js",
9174
9174
  "mode": "0000644",
9175
- "sha256": "7c1917a46bad730419a57047cc34ab21ff9fb2268d4c090a18effa0625e9678a"
9175
+ "sha256": "78a7e407218051892c1aa0934474084deec94768fdd28fe2c3fa447fe48905fd"
9176
9176
  },
9177
9177
  {
9178
9178
  "path": "scripts/gds/classify-discipline.js",
@@ -9327,7 +9327,7 @@
9327
9327
  {
9328
9328
  "path": "scripts/gds/discipline-modes.json",
9329
9329
  "mode": "0000644",
9330
- "sha256": "a62a714209198f02ca4f964ecfd1211dd3712b946f049843a7ec921b5907f1d0"
9330
+ "sha256": "467b66fbb778b9f8eeb03989b18fc085e4c4ccb6b397ca2777cdd82360675c42"
9331
9331
  },
9332
9332
  {
9333
9333
  "path": "scripts/gds/discord-channels.js",
@@ -10017,7 +10017,7 @@
10017
10017
  {
10018
10018
  "path": "scripts/gds/role-pack-guard.js",
10019
10019
  "mode": "0000644",
10020
- "sha256": "7ac55428d01852cd169bd5cb319af306a8f7798489fa89780921f4119c2f552f"
10020
+ "sha256": "34edfb2553f3441387b8bca2f0608b95b089f9d3efd7cd370e83fd9bc3652296"
10021
10021
  },
10022
10022
  {
10023
10023
  "path": "scripts/gds/route-shadow-guard.js",
@@ -10699,6 +10699,11 @@
10699
10699
  "mode": "0000644",
10700
10700
  "sha256": "27630af327d56e6eea77eb7e402086ec695f9fd02f419e48af463ed1adbfab8c"
10701
10701
  },
10702
+ {
10703
+ "path": "src/bongos/craft-mode.js",
10704
+ "mode": "0000644",
10705
+ "sha256": "6df8bd98fbd15d3be903465390ab8f300aa029b1847c42cbe0e10c7af2e62a74"
10706
+ },
10702
10707
  {
10703
10708
  "path": "src/bongos/db-kernel.js",
10704
10709
  "mode": "0000644",
@@ -10912,7 +10917,7 @@
10912
10917
  {
10913
10918
  "path": "src/bongos/routes/me.js",
10914
10919
  "mode": "0000644",
10915
- "sha256": "375859169dde72844541a62a1ff0e97b97321faeed058a287ad7b9c31cdc3a82"
10920
+ "sha256": "989cb7aeb70d956dcd6e93f9fea3acf27fca85f17c5fd2f185b6a99541ede0ae"
10916
10921
  },
10917
10922
  {
10918
10923
  "path": "src/bongos/routes/modules.js",
@@ -10987,7 +10992,7 @@
10987
10992
  {
10988
10993
  "path": "src/module-api.js",
10989
10994
  "mode": "0000644",
10990
- "sha256": "37545c18b037b0de0faaba69a716f67b779a48815d0f5e8937a7202ac7bb8f65"
10995
+ "sha256": "fdb2e5eb052bbdc4698fdb5cde2da471036c90c52f7f105cd8b057ae330ee8c1"
10991
10996
  },
10992
10997
  {
10993
10998
  "path": "src/module-loader/catalog.js",
@@ -11807,7 +11812,7 @@
11807
11812
  {
11808
11813
  "path": "tests/conductor_main_e2e.mjs",
11809
11814
  "mode": "0000644",
11810
- "sha256": "c3b01032ebd3d2c4093bfc1101c2a445c93c52ff520341d6f39a5f582a1e2b97"
11815
+ "sha256": "b81e385b1c1cc3a7b6981b7c3ee4cbe169d24384404fd0e9681560e85a6cf9a1"
11811
11816
  },
11812
11817
  {
11813
11818
  "path": "tests/confirm_attribution.mjs",
@@ -11959,6 +11964,11 @@
11959
11964
  "mode": "0000644",
11960
11965
  "sha256": "50f195ef2060df92016b58f541a28e7492dd95ad4da8bd0781bdc07007193338"
11961
11966
  },
11967
+ {
11968
+ "path": "tests/craft_mode.mjs",
11969
+ "mode": "0000644",
11970
+ "sha256": "26e499e01ee9ec71094e5d2b3b0a0cf367513b9f25a54d1bd716b0d8e318f91d"
11971
+ },
11962
11972
  {
11963
11973
  "path": "tests/credential_presence.mjs",
11964
11974
  "mode": "0000644",
@@ -15947,7 +15957,7 @@
15947
15957
  {
15948
15958
  "path": "tests/visual_review.mjs",
15949
15959
  "mode": "0000644",
15950
- "sha256": "102e80ed4c6c61583d9dfeca23db321be9b871cab3d8f47e4594674b7ecd3cf5"
15960
+ "sha256": "c6f6846bfcfd73ec9e7c47062d26a4ff0c38b6bf5ade1eab6293f458011de8c9"
15951
15961
  },
15952
15962
  {
15953
15963
  "path": "tests/wandering_prefs.mjs",
@@ -22,7 +22,7 @@ Meanwhile the craft-specific material that *did* exist lived in three slash-comm
22
22
 
23
23
  3. **`/dev`, `/ideate` and `/paint` are deleted.** The skill listing drops to 25,624 chars and 17 over-length descriptions.
24
24
 
25
- 4. **`scripts/gds/discipline-modes.json` becomes the role registry.** A core craft names a `pack` (a file to *read*); a module-contributed discipline may still name a `skill` (a command to *invoke*), which is how `ui` → `/design` survives unchanged, as ADR 0272 requires. `claim.js` prints whichever the entry carries, with the right verb.
25
+ 4. **`scripts/gds/discipline-modes.json` becomes the role registry.** A core craft names a `pack` (a file to *read*); a module-contributed discipline may still name a `skill` (a command to *invoke*), which is how `ui` → `/design` survives unchanged, as ADR 0272 requires. `claim.js` prints whichever the entry carries, with the right verb. *(Since task 1002992 the full pointer is injected by the Conductor from GET /me and `claim.js` prints only the one role line.)*
26
26
 
27
27
  5. **`scripts/gds/role-pack-guard.js` hard-fails CI** when an entry names a pack that is not in the tree, when an entry names neither pointer, when fewer than three crafts carry a pack, or when a pack exceeds its character budget (16,000 hard / 13,000 target, ratcheted just above the packs as authored).
28
28
 
package/docs/file-map.md CHANGED
@@ -143,7 +143,7 @@
143
143
  │ ├── discord-channels.js ← Archon CLI: plan/apply the Discord channel layout via the API (ADR 0036, #727)
144
144
  │ ├── setup.js ← one-time GitHub Device Flow → ~/.config/otb/gds-session.json
145
145
  │ ├── start.js ← list claimable tasks + active claims (`/builder-start`)
146
- │ ├── claim.js ← atomic POST /api/bongos/claims (`/builder-claim`); runs the worktree-per-claim guard (worktree-claim-guard.js) BEFORE the session check and the POST; prints the claim-time context pack incl. next-free ADR/migration numbers (V4.R26); printDisciplineMode() reads discipline-modes.json to route a claimed task to its playbook skill
146
+ │ ├── claim.js ← atomic POST /api/bongos/claims (`/builder-claim`); runs the worktree-per-claim guard (worktree-claim-guard.js) BEFORE the session check and the POST; prints the claim-time context pack incl. next-free ADR/migration numbers (V4.R26); printRoleLine() prints the claimed craft's one role line from discipline-modes.json and lands a ui claim's module-owned skills (the full pack pointer is injected by the Conductor from GET /me, task 1002992)
147
147
  │ ├── discipline-modes.json ← the ROLE REGISTRY read by claim.js: engineer/artist/ideator → their role pack under docs/packs/, module-contributed ui → the /design skill; absence of an entry = the default build-and-ship loop, fail-open at claim time — but scripts/gds/role-pack-guard.js hard-fails CI if an entry names a pack that is not in the tree ([#1267](https://example.com/builders#/task/1267))
148
148
  │ ├── next-number.js ← deterministic next-free ADR + migration number from origin/main ∪ local working tree; supersedes the drift-prone claim-time DB counter (V4.R26, [#964](https://example.com/builders#/task/964)). `node scripts/gds/next-number.js [--json]`
149
149
  │ ├── gen-repo-map.js ← Path B (ADR 0063, [#1224](https://example.com/builders#/task/1224)): zero-dep, deterministic symbol-skeleton generator → docs/repo-map.md + the generated factual block in each nested CLAUDE.md; ranks exported symbols by reference frequency. `--check` = CI gate; regenerated on deploy by ship.js regenerateRepoMap()
@@ -2671,5 +2671,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2671
2671
  landed since 1.20.5 with no explicit bump. run 36667238912. (task 1002620)
2672
2672
  1.20.7 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2673
2673
  landed since 1.20.6 with no explicit bump. run 36668328505. (task 1002620)
2674
+ 1.20.8 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2675
+ landed since 1.20.7 with no explicit bump. run 36669272909. (task 1002620)
2674
2676
  ---------------------------------------------------------------------------
2675
2677
  ```
@@ -60,7 +60,7 @@ Every one of these talks to the build system. They're grouped by when you'd reac
60
60
 
61
61
  ### Discipline modes — not slash commands any more
62
62
 
63
- The three core crafts no longer have a slash command. `/dev`, `/paint` and `/ideate` were **deleted** when the root instructions split into a role-neutral kernel (`CLAUDE.md`) plus one **role pack** per craft: `docs/packs/engineer.md`, `docs/packs/artist.md`, `docs/packs/ideator.md`. A claimed task's discipline routes you to your pack — `claim.js` prints its path — and you read it. A module-contributed discipline can still ship a skill, which is how `ui` → `/design` below works.
63
+ The three core crafts no longer have a slash command. `/dev`, `/paint` and `/ideate` were **deleted** when the root instructions split into a role-neutral kernel (`CLAUDE.md`) plus one **role pack** per craft: `docs/packs/engineer.md`, `docs/packs/artist.md`, `docs/packs/ideator.md`. A claimed task's discipline routes you to your pack — the Conductor injects its path into your session on the next prompt, and `claim.js` prints a one-line reminder — and you read it. A module-contributed discipline can still ship a skill, which is how `ui` → `/design` below works.
64
64
 
65
65
  ### Design and art pipeline
66
66
 
@@ -1,6 +1,6 @@
1
1
  # Artist pack
2
2
 
3
- > **The role pack for `discipline = artist`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for artists. Read the whole thing before acting — it changes how you *communicate* for the rest of the session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json), which `claim.js` prints on a claim.
3
+ > **The role pack for `discipline = artist`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for artists. Read the whole thing before acting — it changes how you *communicate* for the rest of the session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json); the Conductor injects this pointer into a session holding a claim of this craft (task 1002992).
4
4
 
5
5
  An art session is not the engineer's text-heavy build loop. The artist's subject is how the project **looks and reads** — its visuals, its copy, its voice, the feeling of the thing. A craft is an identity, not a toolchain ([ADR 0272](../adr/0272-a-craft-is-an-identity-not-a-toolchain.md)): an instance has artists whether or not it ships an art pipeline.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Engineer pack
2
2
 
3
- > **The role pack for `discipline = engineer`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for engineers. Read it once at the top of an engineering session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json), which `claim.js` prints on a claim.
3
+ > **The role pack for `discipline = engineer`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for engineers. Read it once at the top of an engineering session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json); the Conductor injects this pointer into a session holding a claim of this craft (task 1002992).
4
4
 
5
5
  Engineering is the project's **default loop**, so this pack introduces no new dynamic — its job is to make the standard explicit and keep you on it. Lean on it hardest when you are new here or running autonomously. (The Ideator pack flips *who drives*; the Artist pack flips *the medium*. This one is the baseline they invert from.)
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Ideator pack
2
2
 
3
- > **The role pack for `discipline = ideator`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for ideators. Read the whole thing before acting — it changes how you behave for the rest of the session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json), which `claim.js` prints on a claim.
3
+ > **The role pack for `discipline = ideator`.** The kernel ([`CLAUDE.md`](../../CLAUDE.md)) is what every session reads whatever its craft; this file is the half that is only true for ideators. Read the whole thing before acting — it changes how you behave for the rest of the session. Registered in [`scripts/gds/discipline-modes.json`](../../scripts/gds/discipline-modes.json); the Conductor injects this pointer into a session holding a claim of this craft (task 1002992).
4
4
 
5
5
  An ideation session is not the build-and-ship loop, and **it needs no claim**. "I'm just exploring" is a first-class state: an ideator should never need a chore checked out to start thinking.
6
6
 
@@ -198,7 +198,7 @@ function fmtMins(n) {
198
198
  return m ? `~${h}h${m}m` : `~${h}h`;
199
199
  }
200
200
 
201
- // Mirrors scripts/gds/discipline-modes.json, which is what claim.js prints. 'ui'
201
+ // Mirrors scripts/gds/discipline-modes.json, which routes a claim to its playbook. 'ui'
202
202
  // was missing here, so a UI task's card sent the builder to /dev while the claim
203
203
  // had just sent them to /design (task 1003487).
204
204
  //
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.7",
3
+ "version": "1.20.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.7",
9
+ "version": "1.20.8",
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.20.7",
3
+ "version": "1.20.8",
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",
@@ -8094,5 +8094,11 @@
8094
8094
  "id": "1002458",
8095
8095
  "text": "The starter task that asks a new builder to add a test now finds its own targets from the current code, so it no longer goes out of date every time someone completes it."
8096
8096
  }
8097
+ ],
8098
+ "1.20.8": [
8099
+ {
8100
+ "id": "1002992",
8101
+ "text": "When you claim a task, your assistant is now told on its own which playbook to work from: engineer, artist, ideator or UI. Before, the instructions only reached it if someone read the claim output. Each workspace gets only its"
8102
+ }
8097
8103
  ]
8098
8104
  }
@@ -8,7 +8,6 @@
8
8
  // builder knows the parallel-safety boundary they're working within.
9
9
 
10
10
  const { execSync } = require('node:child_process');
11
- const fs = require('node:fs');
12
11
  const path = require('node:path');
13
12
  const { cliClient, requireSession, fmtTouches, printOnboardingNudge, rewardPolicy, costPlusOnlyNote, detectWorktreeName, cliExit } = require('./cli-lib');
14
13
  const { nextNumbers, padAdr, padMig } = require('./next-number');
@@ -60,40 +59,30 @@ async function printRelevantLearnings(task) {
60
59
  }
61
60
  }
62
61
 
63
- // Discipline-fork at claim (task 1267; pack-aware since task 1002990). If the
64
- // claimed task's `discipline` has an entry in the role registry
65
- // (discipline-modes.json), print a directive so the session opens that craft's
66
- // playbook instead of working the task the default way. Since the kernel/pack
67
- // split a CORE craft names a `pack` — a markdown role pack under docs/packs/
68
- // carrying what the role-neutral root CLAUDE.md deliberately leaves out — while a
69
- // module-contributed discipline may still name a `skill` slash-command (`ui` ->
70
- // /design). This IS the whole fork — one data lookup on the claim the builder
71
- // already runs; no hook, the Conductor untouched. `discipline` is already a
72
- // column, so there is no classification step here. Fail-open in every branch: a
73
- // missing or invalid map, or a discipline with no entry, prints nothing and never
74
- // breaks a successful claim. (The registry itself is NOT trusted to be intact by
75
- // luck: fitness.js hard-fails CI when an entry names a pack that is not in the
76
- // tree, so this fail-open path cannot silently swallow a broken registry.)
77
- function printDisciplineMode(task) {
62
+ // The claimed task's craft (task 1267; pack-aware since 1002990; INJECTED since
63
+ // 1002992). The directive itself — which role pack or skill this craft works
64
+ // from — no longer prints here: GET /me carries it on the active claim and the
65
+ // Conductor relays it on the session's next prompt (src/bongos/craft-mode.js), so
66
+ // it reaches the session whether or not anyone read the claim output. What stays
67
+ // is ONE role indicator line, and the side effect that must happen where the claim
68
+ // happens: landing the mode's module-owned skills (materializeModeSkills below).
69
+ //
70
+ // Reads the registry beside this CLI (the file fitness.js hard-fails on when a
71
+ // pack is missing), through the same helper the server uses. Fail-open in every
72
+ // branch: a missing or invalid map, or a discipline with no entry, prints nothing
73
+ // and never breaks a successful claim.
74
+ function printRoleLine(task) {
78
75
  let mode = null;
79
76
  try {
80
77
  const discipline = task && task.discipline;
81
78
  if (!discipline) return;
82
- const raw = fs.readFileSync(path.join(__dirname, 'discipline-modes.json'), 'utf8');
83
- const modes = (JSON.parse(raw) || {}).modes || {};
84
- mode = modes[discipline];
79
+ const { readModes, roleLine } = require('../../src/bongos/craft-mode');
80
+ mode = readModes(path.join(__dirname, 'discipline-modes.json'))[discipline] || null;
85
81
  if (!mode || !mode.directive) return;
86
- // The pointer a session actually follows. A pack is a file to READ (there is no
87
- // slash command behind it any more); a skill is one to INVOKE. Naming the wrong
88
- // verb sends the session hunting for a command that was deleted, so the two
89
- // shapes print differently and an entry with neither prints only the label.
90
- const opener = mode.pack ? `read ${mode.pack}` : (mode.skill ? `/${mode.skill}` : null);
91
82
  console.log('');
92
- console.log(`▶ ${mode.label || 'Working mode'}${opener ? ` — ${opener}` : ''}`);
93
- console.log(` ${mode.directive}`);
94
- if (mode.pack) console.log(` Your pack: ${mode.pack} (the kernel is CLAUDE.md; this is the half that is only true for your craft)`);
83
+ console.log(roleLine(mode));
95
84
  } catch (_) {
96
- // never let the mode hint break a successful claim
85
+ // never let the role line break a successful claim
97
86
  return;
98
87
  }
99
88
  materializeModeSkills(mode);
@@ -719,10 +708,10 @@ async function main() {
719
708
  console.log('');
720
709
  console.log(task.description);
721
710
  }
722
- // Discipline-fork: route ideator (and any future discipline with a mapped
723
- // playbook) into its skill. Printed right after the description so it's the
724
- // last thing the builder reads before deciding how to work — task 1267.
725
- printDisciplineMode(task);
711
+ // The craft's role line (and, for a mode that opts in, its module-owned
712
+ // skills). The full directive reaches the session through the Conductor —
713
+ // task 1002992.
714
+ printRoleLine(task);
726
715
  // task 1004202: a task that changes a page is told to use /design and get its
727
716
  // human's OK on the before/after. A nudge, never a gate: gating on that OK would
728
717
  // put a person's approval in the land path, which ADR 0162 retired.
@@ -782,4 +771,4 @@ if (require.main === module) {
782
771
  });
783
772
  }
784
773
 
785
- module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, worktreeDetectionNote };
774
+ module.exports = { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, materializeModeSkills, printRoleLine, worktreeDetectionNote };
@@ -1,5 +1,5 @@
1
1
  {
2
- "_doc": "The ROLE REGISTRY: discipline -> the operating playbook that craft works from (task 1267; reshaped by task 1002990). Since the kernel/pack split, a CORE craft names a `pack` — a markdown role pack under docs/packs/ that carries everything the root CLAUDE.md kernel deliberately does not, because it is true for one craft only. A module-contributed discipline may instead name a `skill` (a slash-command playbook under .claude/skills/), which is how `ui` -> /design still works (ADR 0272 keeps ui-design's contribution unchanged). An entry may carry both; it must carry at least one. claim.js reads this after a successful claim and prints the entry's `directive` plus its pack path, so the session opens the right playbook. There is NO classification step at claim time — `discipline` is already a column on the task. One carrier (the claim the builder already runs), one data lookup, no hooks, the Conductor untouched. A missing or invalid version of this file must never break a claim (claim.js fails open) — but fitness.js DOES hard-fail CI when an entry names a pack that is not in the tree, so the fail-open runtime cannot hide a broken registry from the build. GOVERNOR IS DEFERRED: three packs, not four, until the owner decides (criterion wa6-kernel-and-packs). A mode entry may also set \"materializeModuleSkills\": true to have the claim land that mode's module-owned skills into this checkout's .claude/skills/ (task 1003486) — opt-in per discipline, core-checkout only, and fail-open like the hint itself.",
2
+ "_doc": "The ROLE REGISTRY: discipline -> the operating playbook that craft works from (task 1267; reshaped by task 1002990). Since the kernel/pack split, a CORE craft names a `pack` — a markdown role pack under docs/packs/ that carries everything the root CLAUDE.md kernel deliberately does not, because it is true for one craft only. A module-contributed discipline may instead name a `skill` (a slash-command playbook under .claude/skills/), which is how `ui` -> /design still works (ADR 0272 keeps ui-design's contribution unchanged). An entry may carry both; it must carry at least one. GET /me reads this (src/bongos/craft-mode.js) and puts each active claim's entry — `directive` plus pack path — on the claim as `craft_mode`; the Conductor hook injects the one for the claim bound to the session's worktree, so the session opens the right playbook without anyone reading terminal output (task 1002992). claim.js reads it too, printing only the one role line and running `materializeModuleSkills`. There is NO classification step — `discipline` is already a column on the task. One registry, never a server-side copy. A missing or invalid version of this file must never break a claim (claim.js fails open) — but fitness.js DOES hard-fail CI when an entry names a pack that is not in the tree, so the fail-open runtime cannot hide a broken registry from the build. GOVERNOR IS DEFERRED: three packs, not four, until the owner decides (criterion wa6-kernel-and-packs). A mode entry may also set \"materializeModuleSkills\": true to have the claim land that mode's module-owned skills into this checkout's .claude/skills/ (task 1003486) — opt-in per discipline, core-checkout only, and fail-open like the hint itself.",
3
3
  "modes": {
4
4
  "engineer": {
5
5
  "pack": "docs/packs/engineer.md",
@@ -72,8 +72,8 @@ function checkRolePacks(fsImpl = fs) {
72
72
  violations.push(`role registry entry '${discipline}' is not an object — every entry must carry a directive plus a pack or a skill.`);
73
73
  continue;
74
74
  }
75
- // An entry with neither pointer routes a claim nowhere: claim.js prints the
76
- // label and the session is left to guess its own playbook.
75
+ // An entry with neither pointer routes a claim nowhere: the session is told the
76
+ // label only and is left to guess its own playbook.
77
77
  if (!mode.pack && !mode.skill) {
78
78
  violations.push(`role registry entry '${discipline}' names neither a 'pack' (docs/packs/<craft>.md) nor a 'skill' — a claim of this discipline would be routed to no playbook at all.`);
79
79
  }
@@ -0,0 +1,87 @@
1
+ // src/bongos/craft-mode.js — a claimed task's craft, as the pointer a session
2
+ // follows to its playbook (task 1002992).
3
+ //
4
+ // WHY. The role registry (scripts/gds/discipline-modes.json) says which role
5
+ // pack or skill each craft works from. claim.js used to PRINT that as a directive
6
+ // block, which reaches the session only if someone reads the claim scroll by —
7
+ // and a role pack skipped that way is invisible. This turns an entry into the same
8
+ // { inject, contract } shape the interaction profile and the speciality contract
9
+ // use, so GET /me can carry it on each active claim and the Conductor can relay it
10
+ // on the next prompt, exactly as it relays those.
11
+ //
12
+ // ONE REGISTRY. This reads the file claim.js and fitness.js (role-pack-guard.js)
13
+ // read — never a server-side copy of the map — so CI's hard-fail on a pack that is
14
+ // not in the tree covers what the session is told, too.
15
+ //
16
+ // Fail-open throughout: an unreadable or invalid registry, or a craft with no
17
+ // entry, yields nothing, never an error, so /me and a claim behave as before.
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { resolveCoreRoot } = require('../instance-config');
23
+
24
+ function registryPath() {
25
+ return path.join(resolveCoreRoot(), 'scripts', 'gds', 'discipline-modes.json');
26
+ }
27
+
28
+ // The `modes` map, or {} for a missing / unparsable file.
29
+ //
30
+ // The DEFAULT registry is read once per process and memoised: GET /me calls this
31
+ // on every request, and the file ships with the core, so it changes only on a
32
+ // deploy, which restarts the process. A failed read is NOT memoised, so a
33
+ // transient error does not pin {} for the life of the server. An explicit `file`
34
+ // (claim.js, tests) is always read fresh.
35
+ let memo = null;
36
+ function readModes(file) {
37
+ if (file === undefined) {
38
+ if (!memo) {
39
+ const modes = readModesFile(registryPath());
40
+ if (Object.keys(modes).length) memo = modes;
41
+ return modes;
42
+ }
43
+ return memo;
44
+ }
45
+ return readModesFile(file);
46
+ }
47
+
48
+ function readModesFile(file) {
49
+ try {
50
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
51
+ const modes = parsed && parsed.modes;
52
+ return modes && typeof modes === 'object' ? modes : {};
53
+ } catch (_) {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ // The opener a session acts on. A pack is a file to READ (no slash command sits
59
+ // behind it); a skill is one to INVOKE. The wrong verb sends a session hunting for
60
+ // a command that does not exist, so the two shapes stay distinct.
61
+ function opener(mode) {
62
+ if (!mode) return null;
63
+ if (mode.pack) return `read ${mode.pack}`;
64
+ if (mode.skill) return `/${mode.skill}`;
65
+ return null;
66
+ }
67
+
68
+ // The one-line role indicator claim.js prints, or null.
69
+ function roleLine(mode) {
70
+ if (!mode) return null;
71
+ const o = opener(mode);
72
+ return `▶ ${mode.label || 'Working mode'}${o ? ` — ${o}` : ''}`;
73
+ }
74
+
75
+ // The injected fragment for one craft, or null when the registry has no entry
76
+ // for it. Static registry text only — no task or builder text is interpolated —
77
+ // so this adds no prompt-injection surface to the session.
78
+ function describeCraftMode(discipline, modes) {
79
+ if (!discipline || !modes) return null;
80
+ const mode = modes[discipline];
81
+ if (!mode || typeof mode !== 'object' || !mode.directive) return null;
82
+ const parts = [`${roleLine(mode)} (the craft of the task claimed in this worktree).`, String(mode.directive).trim()];
83
+ if (mode.pack) parts.push(`Your pack: ${mode.pack} (the kernel is CLAUDE.md; this is the half that is only true for your craft).`);
84
+ return { discipline, inject: true, contract: parts.join('\n') };
85
+ }
86
+
87
+ module.exports = { readModes, roleLine, describeCraftMode };
@@ -33,6 +33,7 @@ const db = require('../db');
33
33
  // provider is default-on (present in prod); resolveOptional degrades on a
34
34
  // prefs-module-less instance so the slices simply render their unset/default shape.
35
35
  const { validateOrRespond } = require('./_helpers');
36
+ const { readModes: readCraftModes, describeCraftMode } = require('../craft-mode'); // task 1002992
36
37
  const { branding } = require('../../branding'); // task 2011: instance model-allocation default
37
38
  // The onboarding state machine carved into modules/onboarding/ (BV1.R81 / ADR
38
39
  // 0093 §1): the GET /me aggregator + PATCH /me/disciplines resolve the
@@ -333,10 +334,15 @@ module.exports = function buildMeRouter() {
333
334
  };
334
335
  // For each active claim, compute the streak preview (cheap; pool query).
335
336
  // Lets the CLI render "shipping this would extend streak to N days" hints.
337
+ // task 1002992: each claim also carries its craft's pack pointer
338
+ // (craft_mode: { discipline, inject, contract } or null), read from the one
339
+ // role registry, so the Conductor can tell the session which playbook to open
340
+ // for the claim in ITS worktree. Read once per request; fail-open to null.
341
+ const craftModes = readCraftModes();
336
342
  const activeClaimsWithPreview = await Promise.all(
337
343
  activeClaims.map(async (c) => {
338
344
  const preview = await reward.wouldExtendStreak(req.builder.id, c.task_id);
339
- return { ...c, streak_preview: preview };
345
+ return { ...c, streak_preview: preview, craft_mode: describeCraftMode(c.discipline, craftModes) };
340
346
  })
341
347
  );
342
348
  const payload = {
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.20.7'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.8'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { strict as assert } from 'node:assert';
16
16
  import { createRequire } from 'node:module';
17
- import { spawn } from 'node:child_process';
17
+ import { spawn, execFileSync } from 'node:child_process';
18
18
  import http from 'node:http';
19
19
  import fs from 'node:fs';
20
20
  import os from 'node:os';
@@ -47,7 +47,9 @@ function mePayload({ discipline = 'engineer' } = {}) {
47
47
  }
48
48
 
49
49
  // Run the hook once in a throwaway HOME whose session points at `base`.
50
- async function runHook(me) {
50
+ // `worktree` names a git repo created under the fake home for the hook to stand
51
+ // in — its basename is what binds a claim to "this" tree (task 1002992).
52
+ async function runHook(me, { worktree = null } = {}) {
51
53
  const server = http.createServer((req, res) => {
52
54
  if (req.url.startsWith('/api/bongos/me')) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(me)); return; }
53
55
  res.writeHead(404); res.end('{}');
@@ -62,12 +64,18 @@ async function runHook(me) {
62
64
  fs.writeFileSync(sessionPath, JSON.stringify({ token: 'test-token', api_base: base, builder: { id: 4242 } }));
63
65
  const env = { ...process.env, HOME: home, USERPROFILE: home };
64
66
  for (const k of Object.keys(env)) if (/_API_BASE$/.test(k) || k === 'OTB_CONDUCTOR_OFF' || k === 'OTB_SUBAGENT') delete env[k];
67
+ let cwd = home;
68
+ if (worktree) {
69
+ cwd = path.join(home, worktree);
70
+ fs.mkdirSync(cwd, { recursive: true });
71
+ execFileSync('git', ['init', '-q'], { cwd, stdio: 'ignore' });
72
+ }
65
73
  try {
66
- const child = spawn(process.execPath, [HOOK], { env, cwd: home, stdio: ['pipe', 'pipe', 'pipe'] });
74
+ const child = spawn(process.execPath, [HOOK], { env, cwd, stdio: ['pipe', 'pipe', 'pipe'] });
67
75
  let out = ''; let err = '';
68
76
  child.stdout.on('data', (d) => { out += d; });
69
77
  child.stderr.on('data', (d) => { err += d; });
70
- child.stdin.end(JSON.stringify({ session_id: 'e2e-' + Date.now(), prompt: 'hello', cwd: home }));
78
+ child.stdin.end(JSON.stringify({ session_id: 'e2e-' + Date.now(), prompt: 'hello', cwd }));
71
79
  const code = await new Promise((r) => child.on('close', r));
72
80
  return { out, err, code };
73
81
  } finally {
@@ -94,4 +102,30 @@ await test('with an artist claim, the artist contract is the one injected', asyn
94
102
  assert.ok(!r.out.includes(SPECIALITY));
95
103
  });
96
104
 
105
+ // task 1002992: the craft's pack pointer, built by the SERVER's own helper from
106
+ // the real registry (what GET /me puts on each claim), run through the real hook.
107
+ const craftMode = require('../src/bongos/craft-mode.js');
108
+ const MODES = craftMode.readModes();
109
+ function twoTreePayload() {
110
+ const claim = (id, discipline) => ({ task_id: id, title: 't', touches: [], worktree_name: `task-${id}`, discipline, craft_mode: craftMode.describeCraftMode(discipline, MODES) });
111
+ return { ...mePayload(), specialities: [], active_claims: [claim(77, 'engineer'), claim(88, 'ideator')] };
112
+ }
113
+
114
+ await test('a session in a claimed worktree is told its craft\'s pack on the first prompt, and not the other tree\'s', async () => {
115
+ const r = await runHook(twoTreePayload(), { worktree: 'task-77' });
116
+ assert.equal(r.code, 0);
117
+ assert.ok(r.out.includes('docs/packs/engineer.md'), `the engineer pack pointer reaches the session; stdout was: ${JSON.stringify(r.out.slice(0, 400))}`);
118
+ assert.ok(!r.out.includes('docs/packs/ideator.md'), 'worktree task-88\'s ideator craft does not');
119
+ const other = await runHook(twoTreePayload(), { worktree: 'task-88' });
120
+ assert.ok(other.out.includes('docs/packs/ideator.md'));
121
+ assert.ok(!other.out.includes('docs/packs/engineer.md'));
122
+ });
123
+
124
+ await test('outside any claimed worktree no craft is guessed', async () => {
125
+ const r = await runHook(twoTreePayload());
126
+ assert.equal(r.code, 0);
127
+ assert.ok(!r.out.includes('docs/packs/'), 'no tree, no craft — silent rather than pooled');
128
+ assert.ok(r.out.includes(INTERACTION), 'and the rest of the hook still runs');
129
+ });
130
+
97
131
  summary();
@@ -0,0 +1,141 @@
1
+ // tests/craft_mode.mjs — the claimed craft's pack pointer is INJECTED, not
2
+ // printed (task 1002992).
3
+ //
4
+ // Four altitudes: the server helper over the real registry, the /me wiring, the
5
+ // Conductor's worktree-scoped relay, and claim.js — which now prints one role line
6
+ // but must still land a ui claim's module-owned skills (the regression this task
7
+ // was most likely to ship). The hook itself is run end to end, against the
8
+ // helper's real output, in tests/conductor_main_e2e.mjs.
9
+ //
10
+ // Run: node tests/craft_mode.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import { createRequire } from 'node:module';
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { makeRunner } from './helpers.mjs';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const craft = require('../src/bongos/craft-mode.js');
23
+ const conductor = require('../.claude/hooks/conductor.js');
24
+ const { test, summary } = makeRunner();
25
+
26
+ const REGISTRY = path.join(ROOT, 'scripts', 'gds', 'discipline-modes.json');
27
+ const MODES = craft.readModes();
28
+
29
+ // ---- the helper, over the ONE registry ---------------------------------------
30
+ await test('the server reads the same registry file claim.js and fitness read', () => {
31
+ assert.deepEqual(MODES, JSON.parse(fs.readFileSync(REGISTRY, 'utf8')).modes);
32
+ });
33
+
34
+ await test('every registry craft yields a contract naming its pack or skill, with its directive', () => {
35
+ const names = Object.keys(MODES);
36
+ assert.ok(names.length >= 3, 'the registry was read at all');
37
+ for (const d of names) {
38
+ const m = craft.describeCraftMode(d, MODES);
39
+ assert.ok(m && m.inject, `${d} injects`);
40
+ assert.equal(m.discipline, d);
41
+ const pointer = MODES[d].pack || `/${MODES[d].skill}`;
42
+ assert.ok(m.contract.includes(pointer), `${d} names ${pointer}`);
43
+ assert.ok(m.contract.includes(MODES[d].directive.slice(0, 40)), `${d} carries its directive`);
44
+ }
45
+ assert.match(craft.describeCraftMode('engineer', MODES).contract, /read docs\/packs\/engineer\.md/);
46
+ assert.match(craft.describeCraftMode('ui', MODES).contract, /— \/design/, 'a skill is a command to invoke, not a file to read');
47
+ });
48
+
49
+ await test('fail-open: no craft, an unknown craft, or an unreadable registry yields nothing', () => {
50
+ assert.equal(craft.describeCraftMode(null, MODES), null);
51
+ assert.equal(craft.describeCraftMode('governor', MODES), null);
52
+ assert.equal(craft.describeCraftMode('engineer', null), null);
53
+ assert.deepEqual(craft.readModes(path.join(os.tmpdir(), 'no-such-registry.json')), {});
54
+ const bad = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'craft-')), 'modes.json');
55
+ fs.writeFileSync(bad, '{ not json');
56
+ assert.deepEqual(craft.readModes(bad), {});
57
+ });
58
+
59
+ await test('the default registry is read once per process; an explicit file is read fresh', () => {
60
+ assert.equal(craft.readModes(), craft.readModes(), 'GET /me does not re-read and re-parse per request');
61
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'craft-'));
62
+ const f = path.join(dir, 'modes.json');
63
+ fs.writeFileSync(f, JSON.stringify({ modes: { a: { directive: 'x' } } }));
64
+ assert.deepEqual(Object.keys(craft.readModes(f)), ['a']);
65
+ fs.writeFileSync(f, JSON.stringify({ modes: { b: { directive: 'y' } } }));
66
+ assert.deepEqual(Object.keys(craft.readModes(f)), ['b'], 'claim.js always sees the file as it is now');
67
+ });
68
+
69
+ // ---- /me carries it on each claim ---------------------------------------------
70
+ await test('GET /me puts each claim\'s craft_mode on the claim, from the helper', () => {
71
+ const me = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
72
+ assert.match(me, /require\('\.\.\/craft-mode'\)/);
73
+ assert.match(me, /craft_mode: describeCraftMode\(c\.discipline, craftModes\)/);
74
+ assert.doesNotMatch(me, /"modes"\s*:/, 'no second copy of the registry in the route');
75
+ });
76
+
77
+ // ---- the Conductor relays THIS worktree's craft only ---------------------------
78
+ const claim = (id, discipline) => ({ task_id: String(id), title: 't', touches: [], worktree_name: `task-${id}`, discipline, craft_mode: craft.describeCraftMode(discipline, MODES) });
79
+ const craftIds = (out) => out.filter((a) => String(a.id).startsWith('craft:')).map((a) => a.id);
80
+
81
+ await test('the claim bound to this worktree gets its craft; the other tree\'s does not', () => {
82
+ const claims = [claim(1, 'engineer'), claim(2, 'ideator')];
83
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: claims, worktree: 'task-1' })), ['craft:engineer']);
84
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: claims, worktree: 'task-2' })), ['craft:ideator']);
85
+ });
86
+
87
+ await test('no bound claim, no session, or no registry entry: nothing is injected', () => {
88
+ const claims = [claim(1, 'engineer')];
89
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: claims, worktree: 'task-9' })), [], 'an unrelated tree');
90
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: claims, worktree: null })), [], 'not in a worktree');
91
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: null, worktree: 'task-1' })), [], 'no session / no network');
92
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: [{ ...claim(1, 'governor') }], worktree: 'task-1' })), [], 'a craft with no entry');
93
+ assert.deepEqual(craftIds(conductor.evaluateTriggers({ activeClaims: [{ ...claim(1, 'engineer'), craft_mode: { inject: true, contract: '' } }], worktree: 'task-1' })), [], 'an empty contract');
94
+ });
95
+
96
+ // ---- claim.js: one role line, and the ui skills still land ---------------------
97
+ const claimCli = require('../scripts/gds/claim.js');
98
+ const materialize = require('../scripts/gds/claude-materialize.js');
99
+
100
+ function capture(fn) {
101
+ const lines = [];
102
+ const orig = console.log;
103
+ console.log = (...a) => { lines.push(a.join(' ')); };
104
+ try { fn(); } finally { console.log = orig; }
105
+ return lines.filter((l) => l !== '');
106
+ }
107
+
108
+ await test('claim.js prints exactly one role line and no directive block', () => {
109
+ const lines = capture(() => claimCli.printRoleLine({ discipline: 'engineer' }));
110
+ assert.deepEqual(lines, ['▶ Engineering session — read docs/packs/engineer.md']);
111
+ const src = fs.readFileSync(path.join(ROOT, 'scripts/gds/claim.js'), 'utf8');
112
+ assert.doesNotMatch(src, /mode\.directive\}/, 'the directive is no longer printed');
113
+ assert.doesNotMatch(src, /Your pack:/);
114
+ });
115
+
116
+ await test('claiming a ui task STILL materialises its module-owned skills', () => {
117
+ const saved = { into: materialize.materializeModuleSkillsIntoCore, root: materialize.isCoreCheckoutRoot };
118
+ const calls = [];
119
+ materialize.isCoreCheckoutRoot = () => true;
120
+ materialize.materializeModuleSkillsIntoCore = (opts) => { calls.push(opts); return { materialized: ['impeccable', 'minimalist-ui'] }; };
121
+ try {
122
+ const lines = capture(() => claimCli.printRoleLine({ discipline: 'ui' }));
123
+ assert.equal(calls.length, 1, 'the ui claim landed its skills');
124
+ assert.equal(lines[0], '▶ UI design session — /design');
125
+ assert.match(lines[1], /2 module-owned skill\(s\)/);
126
+ calls.length = 0;
127
+ capture(() => claimCli.printRoleLine({ discipline: 'engineer' }));
128
+ assert.equal(calls.length, 0, 'a craft that did not opt in lands nothing');
129
+ } finally {
130
+ materialize.materializeModuleSkillsIntoCore = saved.into;
131
+ materialize.isCoreCheckoutRoot = saved.root;
132
+ }
133
+ });
134
+
135
+ await test('claim.js fails open: no craft or an unknown craft prints nothing and never throws', () => {
136
+ assert.deepEqual(capture(() => claimCli.printRoleLine({})), []);
137
+ assert.deepEqual(capture(() => claimCli.printRoleLine({ discipline: 'governor' })), []);
138
+ assert.deepEqual(capture(() => claimCli.printRoleLine(null)), []);
139
+ });
140
+
141
+ summary();
@@ -90,10 +90,12 @@ await test('a backend task is not nudged — no noise on work that renders nothi
90
90
  assert.equal(visualReviewNudge(task()), null);
91
91
  });
92
92
 
93
- await test('claim.js prints the nudge after the craft directive, and cannot fail a claim with it', () => {
93
+ await test('claim.js prints the nudge after the craft role line, and cannot fail a claim with it', () => {
94
94
  const src = read('scripts/gds/claim.js');
95
95
  const at = src.indexOf('visualReviewNudge(task)');
96
- assert.ok(at > src.indexOf('printDisciplineMode(task);'), 'right after the craft directive');
96
+ const role = src.indexOf('printRoleLine(task);');
97
+ assert.ok(role > -1, 'the role line call exists (task 1002992 renamed it from printDisciplineMode)');
98
+ assert.ok(at > role, 'right after the craft role line');
97
99
  assert.match(src.slice(at - 200, at + 300), /catch \(_\)/, 'wrapped, so a hint can never break a successful claim');
98
100
  });
99
101