@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 +35 -25
- package/docs/adr/0274-one-kernel-three-role-packs.md +1 -1
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/artist.md +1 -1
- package/docs/packs/engineer.md +1 -1
- package/docs/packs/ideator.md +1 -1
- package/modules/lifecycle/ship-card.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/claim.js +22 -33
- package/scripts/gds/discipline-modes.json +1 -1
- package/scripts/gds/role-pack-guard.js +2 -2
- package/src/bongos/craft-mode.js +87 -0
- package/src/bongos/routes/me.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/conductor_main_e2e.mjs +38 -4
- package/tests/craft_mode.mjs +141 -0
- package/tests/visual_review.mjs +4 -2
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.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
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:
|
|
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":
|
|
14
|
+
"functional_verbatim": 2623,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
2840
|
+
"sha256": "cc792eb9391d147e443ba646f991f99f88245fbb96adeda7bf96364fa9bc7a85"
|
|
2841
2841
|
},
|
|
2842
2842
|
{
|
|
2843
2843
|
"path": "docs/packs/artist.md",
|
|
2844
2844
|
"mode": "0000644",
|
|
2845
|
-
"sha256": "
|
|
2845
|
+
"sha256": "d9a0b37fb3e03ab2e0f874abf3db40fed123cae5c0f6443a0e176adcdfc30b1b"
|
|
2846
2846
|
},
|
|
2847
2847
|
{
|
|
2848
2848
|
"path": "docs/packs/engineer.md",
|
|
2849
2849
|
"mode": "0000644",
|
|
2850
|
-
"sha256": "
|
|
2850
|
+
"sha256": "30e4d2363f1fea52cb347ba8a642277fe5046249f77858e947b4434150ff5746"
|
|
2851
2851
|
},
|
|
2852
2852
|
{
|
|
2853
2853
|
"path": "docs/packs/ideator.md",
|
|
2854
2854
|
"mode": "0000644",
|
|
2855
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
8855
|
+
"sha256": "3a10ecd6f93e42f1792e7edb0a406edaefb3377506afb359cde35e3fdd901296"
|
|
8856
8856
|
},
|
|
8857
8857
|
{
|
|
8858
8858
|
"path": "package.json",
|
|
8859
8859
|
"mode": "0000644",
|
|
8860
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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);
|
|
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
|
|
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
|
|
package/docs/packs/artist.md
CHANGED
|
@@ -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)
|
|
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
|
|
package/docs/packs/engineer.md
CHANGED
|
@@ -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)
|
|
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
|
|
package/docs/packs/ideator.md
CHANGED
|
@@ -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)
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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
|
}
|
package/scripts/gds/claim.js
CHANGED
|
@@ -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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// missing or invalid map, or a discipline with no entry, prints nothing
|
|
74
|
-
// breaks a successful claim.
|
|
75
|
-
|
|
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
|
|
83
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
|
|
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.
|
|
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:
|
|
76
|
-
// label and
|
|
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 };
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|
package/tests/visual_review.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|