@bongos/core 1.19.465 → 1.19.466
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 +22 -17
- package/docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/ui-design/skills/README.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claude-materialize.js +86 -3
- package/scripts/gds/gen-file-map.js +12 -2
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +81 -0
- package/tests/file_map.mjs +4 -1
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.466",
|
|
6
|
+
"core_contract": "1.19.466",
|
|
7
|
+
"source_commit": "5b915c14235b08df52728e3d94dc196d548791a3",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-02T02:25:23.295Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 407,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 1822,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2253,
|
|
20
|
+
"tree_sha256": "4bb527a26dc80efbf88ff9d1a853737b1a657a8f300f8fbf01e76b4146834f58",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1624,10 +1624,15 @@
|
|
|
1624
1624
|
"mode": "0000644",
|
|
1625
1625
|
"sha256": "736f27c4a9e86d3169954a2555209e75dfc93f84b4581417188c71a02954e731"
|
|
1626
1626
|
},
|
|
1627
|
+
{
|
|
1628
|
+
"path": "docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md",
|
|
1629
|
+
"mode": "0000644",
|
|
1630
|
+
"sha256": "717bdc174891ccf5bc8756b25f53eca016d4d6dfaeebec8d12bb2a6bfef56976"
|
|
1631
|
+
},
|
|
1627
1632
|
{
|
|
1628
1633
|
"path": "docs/adr/README.md",
|
|
1629
1634
|
"mode": "0000644",
|
|
1630
|
-
"sha256": "
|
|
1635
|
+
"sha256": "ec731cb900dc5b30287a370325042a4ead509759aba5688c32d91f293047c491"
|
|
1631
1636
|
},
|
|
1632
1637
|
{
|
|
1633
1638
|
"path": "docs/api-reference.md",
|
|
@@ -2117,7 +2122,7 @@
|
|
|
2117
2122
|
{
|
|
2118
2123
|
"path": "docs/module-api-changelog.md",
|
|
2119
2124
|
"mode": "0000644",
|
|
2120
|
-
"sha256": "
|
|
2125
|
+
"sha256": "5e800493043ac5941fa8ed5f9efc66359a0622f79968fd7158a61651556c1cf3"
|
|
2121
2126
|
},
|
|
2122
2127
|
{
|
|
2123
2128
|
"path": "docs/modules-contract.md",
|
|
@@ -6387,7 +6392,7 @@
|
|
|
6387
6392
|
{
|
|
6388
6393
|
"path": "modules/ui-design/skills/README.md",
|
|
6389
6394
|
"mode": "0000644",
|
|
6390
|
-
"sha256": "
|
|
6395
|
+
"sha256": "ddc9a2b8788d3e41f1d3ae285d56c75ecb6e7a9d4aa003a0db55f4d38ff24d4a"
|
|
6391
6396
|
},
|
|
6392
6397
|
{
|
|
6393
6398
|
"path": "modules/ui-design/skills/brandkit/SKILL.md",
|
|
@@ -6617,12 +6622,12 @@
|
|
|
6617
6622
|
{
|
|
6618
6623
|
"path": "package-lock.json",
|
|
6619
6624
|
"mode": "0000644",
|
|
6620
|
-
"sha256": "
|
|
6625
|
+
"sha256": "098654f260139d3b7b4d45ef87d7a15c3ca8a0ebb04ce36ba538e212c3dce351"
|
|
6621
6626
|
},
|
|
6622
6627
|
{
|
|
6623
6628
|
"path": "package.json",
|
|
6624
6629
|
"mode": "0000644",
|
|
6625
|
-
"sha256": "
|
|
6630
|
+
"sha256": "9670afc099badbbc922edd16e4cb6a1a99d370267cc15b2232af678b6c65f7b0"
|
|
6626
6631
|
},
|
|
6627
6632
|
{
|
|
6628
6633
|
"path": "public-docs/index.html",
|
|
@@ -6857,7 +6862,7 @@
|
|
|
6857
6862
|
{
|
|
6858
6863
|
"path": "scripts/gds/claude-materialize.js",
|
|
6859
6864
|
"mode": "0000644",
|
|
6860
|
-
"sha256": "
|
|
6865
|
+
"sha256": "450c1a12c5e8c125db6794d74bb2c25251804d8613d83fa50a463e4007a46ddd"
|
|
6861
6866
|
},
|
|
6862
6867
|
{
|
|
6863
6868
|
"path": "scripts/gds/claude-md-template.js",
|
|
@@ -7107,7 +7112,7 @@
|
|
|
7107
7112
|
{
|
|
7108
7113
|
"path": "scripts/gds/gen-file-map.js",
|
|
7109
7114
|
"mode": "0000644",
|
|
7110
|
-
"sha256": "
|
|
7115
|
+
"sha256": "620188a2edcd4aec580193b3d97f0c4e19871db4d31af27cb0b7e0571457a73a"
|
|
7111
7116
|
},
|
|
7112
7117
|
{
|
|
7113
7118
|
"path": "scripts/gds/gen-freshness.js",
|
|
@@ -8272,7 +8277,7 @@
|
|
|
8272
8277
|
{
|
|
8273
8278
|
"path": "src/module-api.js",
|
|
8274
8279
|
"mode": "0000644",
|
|
8275
|
-
"sha256": "
|
|
8280
|
+
"sha256": "400ed351948806a44d6558bbc10d35158237d80c3101415acebb6d075747c3ae"
|
|
8276
8281
|
},
|
|
8277
8282
|
{
|
|
8278
8283
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8787,7 +8792,7 @@
|
|
|
8787
8792
|
{
|
|
8788
8793
|
"path": "tests/claude_materialize.mjs",
|
|
8789
8794
|
"mode": "0000644",
|
|
8790
|
-
"sha256": "
|
|
8795
|
+
"sha256": "6599b89948d3289d869f94fd87072f26a37d10a1e67727faf6c05b5052a035c8"
|
|
8791
8796
|
},
|
|
8792
8797
|
{
|
|
8793
8798
|
"path": "tests/clawback_confirmed_backlog.mjs",
|
|
@@ -9177,7 +9182,7 @@
|
|
|
9177
9182
|
{
|
|
9178
9183
|
"path": "tests/file_map.mjs",
|
|
9179
9184
|
"mode": "0000644",
|
|
9180
|
-
"sha256": "
|
|
9185
|
+
"sha256": "b44f040f21d1dbb978add1a96e392a0f3c1b5a46302c10616b2304505ca66097"
|
|
9181
9186
|
},
|
|
9182
9187
|
{
|
|
9183
9188
|
"path": "tests/filing_abuse_matrix.mjs",
|
package/docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ADR 0224 — Module-owned skills reach the core checkout as untracked, self-ignored copies with a manifest: the self-copy guard stays, the dev path is a flag, and the file map skips what the module documents
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted (2026-09-02)
|
|
4
|
+
- **Task:** [task 1003330](https://cloudbongos.com/builders#/task/1003330) (goal 1000083 — the ui-design module as a platform capability; from idea 1000864, filed by task 1003321)
|
|
5
|
+
- **Context:** [ADR 0198](<redacted>.md) landed module-owned skills for INSTANCES (`claude-materialize.js` step 1b, on `bongos init` / `upgrade`); [ADR 0220](<redacted>.md) and its successors noted each time that "the core checkout itself still does not see module-owned skills".
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`materializeClaude`'s self-copy guard is a deliberate no-op when the instance dir is the core root: a core copying itself onto itself would be a no-op at best and corrupting at worst. Claude Code discovers only `.claude/skills/` at the project root. So a builder working in the core checkout, where all nine `ui-design` skills were written, cannot invoke a single one of them; the only copies on this machine were the upstream ones under the builder's home directory, by the same names, which is the collision ADR 0220 called "by design". Two constraints shaped the fix: nothing under `.claude/skills/` may be committed twice (the module's `skills/<name>/` is the source and the file-map generator lists every dir there and demands a note for each), and `bongos init` from a checkout that carries the copies must still land them through the module path.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
1. **The dev path is a flag on the library, not a change to the guard.** `node scripts/gds/claude-materialize.js --module-skills-only [--dry-run] [--clean]` calls `materializeModuleSkillsIntoCore`, which walks the same roster and passes the same `vendoredSkillGate` and `SKILL_NAME_RE` as step 1b, copies every ENABLED module's skill tree **verbatim** into `.claude/skills/<name>/` (no `bongosify`: the checkout runs `node scripts/gds/…`), leaves a name the core's own tracked skills carry to the core, and excludes a disabled module's. The self-copy guard in `materializeClaude` is untouched; init and upgrade are unchanged.
|
|
14
|
+
2. **Untracked, self-ignored, manifest-named.** The run writes `.claude/skills/.module-skills.json` (each materialised name with its module, source dir, vendored flag and file count, sorted) and `.claude/skills/.gitignore` (ignoring itself, the manifest and every materialised dir), so `git status` shows nothing new and nothing can be committed by accident. Every run first removes what the last manifest named, then lands the current set, so a skill a module stopped declaring does not linger and a second run is byte-identical; `--clean` stops after the removal.
|
|
15
|
+
3. **The file map skips what the module documents.** `gen-file-map.js` reads the manifest beside `.claude/skills/` and excludes those names from its listing and from its notes contract (a module skill is documented by its module's README, not by `docs/file-map.notes.json`), and `tests/file_map.mjs` filters its own exhaustiveness walk the same way through the exported `materializedNames`. `--check` stays green with the copies present and without them.
|
|
16
|
+
4. **Init from a checkout that carries the copies still lands them through the module path.** `materializeClaude` reads the same manifest: step 1 skips the materialised names and the two dotfiles, and step 1b's "also exists under the core" note ignores a materialised copy, so an instance receives the module skills transformed (`bongos …`) as before and never the checkout's dotfiles.
|
|
17
|
+
5. **The pins** (`tests/claude_materialize.mjs`): an enabled module's skills land verbatim with the manifest and the `.gitignore` lines, a disabled module's are excluded, a core-owned name is kept; a second run is byte-identical, a dropped declaration is removed, `--clean` removes exactly what was made and nothing of the core's; an instance materialised from such a core gets the skill through step 1b, transformed, and never the dotfiles; the bare CLI refuses with usage; `gen-file-map`'s `materializedNames` reads the manifest.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
|
|
21
|
+
- **A builder in the core checkout sees all nine `ui-design` skills** after one command; the roster ADRs' standing consequence is closed. The command is not run automatically: a checkout is a workbench, and a builder who wants the module's skills in front of them runs it (or `--clean` to take them away).
|
|
22
|
+
- **The upstream copies under the home directory are now shadowed by the project-level ones**, which is the collision ADR 0220 described, now resolved in the module's favour on this machine too.
|
|
23
|
+
- **`docs/file-map.md` does not list materialised skills.** It never should have: they are the module's, and the module's README is their roster.
|
|
24
|
+
|
|
25
|
+
## Rejected
|
|
26
|
+
|
|
27
|
+
- **Dropping the self-copy guard** so `materializeClaude` writes into the core: it would `bongosify` the checkout's own core skills onto themselves and copy hooks and settings onto their sources.
|
|
28
|
+
- **Committing the module skills under `.claude/skills/` as well:** two sources of truth for every skill, and the file-map notes contract doubled.
|
|
29
|
+
- **A symlink or junction per skill:** Windows junctions into a tracked tree are the trap `worktree.js` exists to avoid, and a symlink is a tracked file the moment it lands in the index.
|
|
30
|
+
- **Adding every materialised name to `docs/file-map.notes.json`:** a note for a dir that is not in the repository is a ghost by the file map's own definition.
|
package/docs/adr/README.md
CHANGED
|
@@ -310,5 +310,6 @@ This keeps the decision history honest and traceable.
|
|
|
310
310
|
| 0221 | [**The impeccable method is one skill with sub-command playbooks: one preamble, one playbook per request, the three homes are the instance's own files, and the origin's mechanisms are dropped rather than disabled** ([task 1003326](https://cloudbongos.com/builders#/task/1003326) · goal 1000083). `pbakaus/impeccable` scanned `dangerous` (ADR 0198: hooks written into five harness configs, a child agent with permissions bypassed, an instruction override, a live-server credential in page source, an unverified remote bundle, default-on telemetry), so its method is rebuilt from the spec. ADR 0220 fixed where a rebuild lives; it had not said how a family of sub-commands lands, what the origin's three persisted homes become, or what to do with the six spec areas whose substance is the condemned mechanism. Decisions: **D1** one skill at `skills/impeccable/`: `SKILL.md` (invocation, the menu from live signals, session setup, the three homes, the routing table, the craft floor, the bounded finish, what is not here) plus `reference/<sub-command>.md`, eighteen playbooks, exactly one loaded per request (the spec's own entry rule); the preamble pinned once in `SKILL.md` and governing every playbook; the materialiser's tree copy ships `reference/` unchanged. **D2** the three homes are the instance's files (product truth = `docs/project-context.template.md` + the pack's identity; the visual system = the root `DESIGN.md` and a look's; the brief = the task's body + a fixed brief block in the ship notes); the skill creates none, `shape` proposes diffs, `document` drafts on the claim's branch with the refresh / overwrite / merge choice. **D3** dropped, not disabled, each with a destination: hooks → none (the ship gate is the sweep, the pre-flight is the kit); the bypass agent → an ordinary read-only subagent via the Agent tool; telemetry, the remote bundle, the credential → none; the live loop → the playwright plugin driving the kit's stub, variants as states, accept = commit, discard = checkout, nothing injected; the detector → task 1003329; the asset producer → the hero-plates rule and task 1003328; native → declines to `/design`; `init`/`doctor`/`pin`/installer → the materialiser is the install. **D4** the finish is the kit in bounded rounds (build, one batched inspection, one batch of fixes, at most one confirming round, stop, one budget) and the reviewer answers with one of `clean` / `recapture` / `defects` / `contradicted` after an evidence check and its own mock inventory. **D5** the craft floor is mechanics loaded late and never recited, translated for a world that may be hairlines by rule. **D6** the pins: sixteen routed playbooks, no orphan, no preamble copy in a playbook, and nothing in the tree directing a bypass, a hook write, a settings edit, telemetry, a remote download, a credential or an override. Rejected: eighteen skills; one inline file; a hooks-lite opt-in; an elevated reviewer; a `doctor`; the native playbooks.](<redacted>.md) | ui-design / skills |
|
|
311
311
|
| 0222 | [**The image family under the hero-only rule: plates and hero objects, never screens; the mark is drawn; the board is a contact sheet; the pair and the sidecar are the output contract** ([task 1003328](https://cloudbongos.com/builders#/task/1003328) · goal 1000083). The four image skills of `Leonxlnx/taste-skill` (scanned `dangerous`, ADR 0198) are built around pictures of interfaces: one reference image per section, one app screen per flow step in a phone mockup, an identity board with the brand name rendered in, and a build loop whose truth is the generated image; the panel flagged the eagerness mandate, the image-count rule and the must-invoke-any-tool directive by name, and the owner's rule (2026-08-26/27) forbids the category. Decisions: **D1** the subject is the composition's plate slots and the count is the slot count; variety survives as anchors (field, beside-bleeding, masthead, orb, backdrop; text-left image-right refused), the palette lock is the pack, cross-frame consistency is one family (material, light, lens, ground). **D2** a screen is the surface itself: the mobile skill makes portrait plates for 390 / 320, the flow is the states file rendered by the kit, the bible is `DESIGN.md`, no phone mockup, native declines to `/design`. **D3** the source of truth for UI is the owner's references and the world; `image-to-code` reads them (analysis, extraction spec, faithful translation, drift as the failure mode, the clarity checklist) and the only image it may cause is the hero plate. **D4** the mark is drawn as inline SVG, the colour system is the pack, the type specimen is the faces on a real page, the applications are the kit's renders, the image direction is a hero-object family, the nine-panel board is a contact sheet of real parts. **D5** the output contract is DESIGN.md's pair and sidecar (JPEG dark plate, WebP alpha cutout, `<file>.<ext>.json` beside each, `--plate-*` tokens on `body`) through the instance's art pipeline key, the spend logged, a white ground through the remover, alpha verified with ffmpeg, never a crop. **D6** the worked example is the grove specimen's hero object, not the front door. **D7** the pins: the four declared, each naming the sidecar, "text-free" and the art key; no eagerness / count / per-section / must-invoke directive. Rejected: per-section images for reference only; phone mockups around real shots; a generated logo as a start; the plate on the front door; a shared base skill with wrappers.](<redacted>.md) | ui-design / skills / imagery |
|
|
312
312
|
| 0223 | [**The anti-pattern detector is two tiers inside the kit: mechanical floors fail, craft tells warn, a false positive is waived in the file with its reason, and nothing new is installed** ([task 1003329](https://cloudbongos.com/builders#/task/1003329) · goal 1000083). The [detector] group of impeccable's rebuild spec (about sixty rules, a CLI plus a browser bundle wired to hooks, per-project ignores, in-file waivers, "a clean scan is a floor and never proof") lands as RULES in the kit that already exists, per the task body: no second tool, no bundle, no hook. Decisions: **D1** `kit/tells.js` carries the static rules and `check-mock.js` runs them after the platform rules and taste bans; `lib.js`'s `factsInPage` collects the computed-value twins as `tells` and `render.js` prints them as `~ tell:` lines per render, recorded in `report.json`, never counted as problems. Two tiers: floors FAIL (a bounce easing; a card nested in a card) and tells WARN (the icon-heading-text grid, hero metrics, section numbering, a kicker above the h1, a side stripe, a hard offset shadow, a coloured halo, decorative blur, a default typeface, a wide measure, a long headline, the same entrance everywhere, a geometric mask on a plate, glyphs as icons, a rainbow gradient, the browser surfaces nobody themed, proportional numerals, tiny type; rendered: measure, tiny text, numerals in cells, headline lines, an overshooting curve). **D2** the world decides which tells are floors: a kicker above the h1 is a FAIL where the instance's `DESIGN.md` bans the eyebrow (a new `eyebrow` taste key), a WARN elsewhere. **D3** a false positive is waived in the file with its reason (`kit-ignore: <rule prefix> (reason)` → a WAIVE row), `--raw` runs with none, `--no-tells` runs the platform rules only, `--json` prints rows with `fails` / `warns` counts; no per-project ignore config. **D4** not ported: the purple-to-blue tell (a literal colour already fails; hue is unresolvable under the fifteen), an injected overlay, the screenshot sampler (`contrast.js --url` is that pass), a cascade resolver (the browser is the resolver), native code, every hook. **D5** the pins: a clean fixture passes every rule, every rule class trips at its tier with the case count equal to the rule count, the promotion, the waiver / raw / json paths through the real CLI, `craftTells` as warnings; the hub mock and the three specimens still pass, `/projects` and the grove specimen still render ALL CLEAN. Rejected: a second CLI; every tell as a floor (the kit as the taste and the instance as the exception); a project-wide ignore config; an edit-time hook.](0223-the-detector-is-two-tiers-in-the-kit.md) | ui-design / kit |
|
|
313
|
+
| 0224 | [**Module-owned skills reach the core checkout as untracked, self-ignored copies with a manifest: the self-copy guard stays, the dev path is a flag, and the file map skips what the module documents** ([task 1003330](https://cloudbongos.com/builders#/task/1003330) · goal 1000083). `materializeClaude`'s self-copy guard is right (a core copying itself is a no-op at best), but Claude Code discovers only `.claude/skills/` at the project root, so a builder IN the core checkout could not invoke any of the nine ui-design skills written there. Decisions: **D1** `node scripts/gds/claude-materialize.js --module-skills-only [--dry-run] [--clean]` calls `materializeModuleSkillsIntoCore`: the same roster, gate and slug rule as step 1b, verbatim copies (no bongosify: the checkout runs `node scripts/gds/…`), a core-owned name kept, a disabled module's excluded; the guard untouched. **D2** untracked and self-ignored: a generated `.claude/skills/.module-skills.json` manifest and a `.gitignore` that ignores itself, the manifest and every materialised dir; each run removes what the last manifest named then lands the current set (byte-identical on repeat; a dropped declaration disappears); `--clean` stops after the removal. **D3** `gen-file-map.js` and `tests/file_map.mjs` skip materialised names by the manifest (a module skill is documented by its module's README, not by the notes sidecar). **D4** init from a checkout that carries the copies still lands them through step 1b, transformed, and never the dotfiles. **D5** pinned end to end in `tests/claude_materialize.mjs`. Rejected: dropping the guard; committing the copies; symlinks or junctions; notes for dirs not in the repository.](<redacted>.md) | materialiser / skills |
|
|
313
314
|
|
|
314
315
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
|
@@ -1367,5 +1367,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1367
1367
|
landed since 1.19.463 with no explicit bump. run 33578746178. (task 1002620)
|
|
1368
1368
|
1.19.465 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1369
1369
|
landed since 1.19.464 with no explicit bump. run 33581328823. (task 1002620)
|
|
1370
|
+
1.19.466 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1371
|
+
landed since 1.19.465 with no explicit bump. run 33583122775. (task 1002620)
|
|
1370
1372
|
---------------------------------------------------------------------------
|
|
1371
1373
|
```
|
|
@@ -43,6 +43,8 @@ A rebuild spec becomes a **first-party skill** at `<name>/` — no `vendor/` seg
|
|
|
43
43
|
|
|
44
44
|
What every rebuild does with the upstream's opinions, in the same words: the palette is the pack and a variant is a look from [`../styles/`](../styles/README.md); the design system is the instance's `DESIGN.md`; images are hero plates and hero objects by token and never a step; every motion rule lives inside the Kill Switch (springs, magnetic hover, pointer-tracked borders are named *not available*); the upstream's bans are defaults an instance's `DESIGN.md` overrides; the kit's renders are the pre-flight. The image family (task 1003328, [ADR 0222](../../../docs/adr/<redacted>.md)) carries the one rewrite the upstream never had: images are for text-free hero plates and hero objects only, so a screen is the surface itself, the mark is drawn, the board is a contact sheet, and what ships is the pair (a black-ground JPEG for dark, an alpha WebP cutout for light) with a provenance sidecar beside every file, through the instance's art pipeline key. Still specs, not skills: the detector (task 1003329), the four looks (1003327).
|
|
45
45
|
|
|
46
|
+
**In the core checkout itself**, where these skills are written, Claude Code reads only `.claude/skills/` at the repo root and the materialiser's self-copy guard leaves it empty of module skills. `node scripts/gds/claude-materialize.js --module-skills-only` lands every enabled module's skills there as untracked, self-ignored copies (a generated `.claude/skills/.module-skills.json` manifest and a `.gitignore` beside them; `--clean` removes them; the file map skips them by the manifest) so a builder can invoke them here too ([task 1003330](https://cloudbongos.com/builders#/task/1003330), [ADR 0224](../../../docs/adr/<redacted>.md)). Edit the module's `skills/<name>/`, never the copy, and re-run to refresh.
|
|
47
|
+
|
|
46
48
|
**The playwright plugin** (`claude-plugins-official`, an MCP plugin) is the LIVE browser-driving loop, where the kit is the headless one. It is a prerequisite, documented — `claude plugin install playwright@claude-plugins-official` — never vendored.
|
|
47
49
|
|
|
48
50
|
## How to land one (when a scan says it may)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.466",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.466",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.466",
|
|
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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const fs = require('node:fs');
|
|
27
27
|
const path = require('node:path');
|
|
28
|
+
const { spawnSync } = require('node:child_process');
|
|
28
29
|
const { VERBS } = require('../../bin/bongos.js'); // side-effect-free require (guarded main)
|
|
29
30
|
const ic = require('../../src/instance-config'); // neutral configDir/envPrefix fallbacks (never a founder literal)
|
|
30
31
|
const { renderInstanceClaudeMd } = require('./claude-md-template'); // task 2104 — the starter CLAUDE.md
|
|
@@ -340,7 +341,11 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
340
341
|
const roster = moduleSkillRoster({ coreRoot, instanceDir });
|
|
341
342
|
const skipSkills = new Set();
|
|
342
343
|
for (const m of roster) if (!m.enabled) for (const s of m.skills) skipSkills.add(s);
|
|
343
|
-
|
|
344
|
+
// A core checkout that materialised module skills into its own .claude/skills/ (task 1003330)
|
|
345
|
+
// carries them as untracked copies plus two dotfiles; step 1 must not ship those as core
|
|
346
|
+
// skills — step 1b lands them from the module, transformed, like everywhere else.
|
|
347
|
+
const materialisedInCore = readModuleSkillsManifest(path.join(coreRoot, '.claude', 'skills')).skills;
|
|
348
|
+
const skillSkip = (rel) => { const top = String(rel).split(/[\\/]/)[0]; return skipSkills.has(top) || top === MODULE_SKILLS_MANIFEST || top === '.gitignore' || Object.prototype.hasOwnProperty.call(materialisedInCore, top); };
|
|
344
349
|
|
|
345
350
|
// 1. skills — ship in the core package; copy, rewrite invocations→bongos, rebrand
|
|
346
351
|
// identity, and exclude any skill owned by a disabled module.
|
|
@@ -369,7 +374,7 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
369
374
|
}
|
|
370
375
|
for (const s of sources) {
|
|
371
376
|
if (!s.enabled) { if (!summary.excludedSkills.includes(s.name)) summary.excludedSkills.push(s.name); continue; }
|
|
372
|
-
if (fs.existsSync(path.join(skillsSrc, s.name))) {
|
|
377
|
+
if (fs.existsSync(path.join(skillsSrc, s.name)) && !Object.prototype.hasOwnProperty.call(materialisedInCore, s.name)) {
|
|
373
378
|
summary.notes.push(`module skill "${s.name}" (${s.module}) also exists under the core's .claude/skills/ — the core copy was materialized, the module copy ignored.`);
|
|
374
379
|
continue;
|
|
375
380
|
}
|
|
@@ -452,4 +457,82 @@ function materializeClaude({ coreRoot, instanceDir, dryRun = false, overwrite =
|
|
|
452
457
|
return summary;
|
|
453
458
|
}
|
|
454
459
|
|
|
455
|
-
|
|
460
|
+
// ---- the dev checkout: module-owned skills materialised INTO the core (task 1003330, ADR 0224) ----
|
|
461
|
+
// materializeClaude's self-copy guard is right for init/upgrade (instance == core is a no-op),
|
|
462
|
+
// but Claude Code discovers only .claude/skills/ at the project root, so a builder working IN
|
|
463
|
+
// the core checkout never sees modules/<key>/skills/. This lands every ENABLED module's skills
|
|
464
|
+
// there as UNTRACKED, self-ignored dirs: verbatim copies (no bongosify — the checkout runs
|
|
465
|
+
// `node scripts/gds/…`), a generated .claude/skills/.module-skills.json manifest naming what
|
|
466
|
+
// was materialised and from where, and a generated .claude/skills/.gitignore covering the
|
|
467
|
+
// manifest, itself and every materialised dir, so `git status` shows nothing new and nothing
|
|
468
|
+
// can be committed by accident. The same vendoredSkillGate + SKILL_NAME_RE apply; a name the
|
|
469
|
+
// core's own tracked .claude/skills/ carries is left to the core. Idempotent: every run first
|
|
470
|
+
// removes what the last manifest named (so a skill a module stopped declaring does not
|
|
471
|
+
// linger), then lands the current set; `clean` stops after the removal.
|
|
472
|
+
const MODULE_SKILLS_MANIFEST = '.module-skills.json';
|
|
473
|
+
function readModuleSkillsManifest(skillsDir) {
|
|
474
|
+
const m = readJsonSafe(path.join(skillsDir, MODULE_SKILLS_MANIFEST));
|
|
475
|
+
return m && m.skills && typeof m.skills === 'object' ? m : { skills: {} };
|
|
476
|
+
}
|
|
477
|
+
function materializeModuleSkillsIntoCore({ coreRoot, dryRun = false, clean = false, log = () => {} }) {
|
|
478
|
+
const skillsDir = path.join(coreRoot, '.claude', 'skills');
|
|
479
|
+
const summary = { materialized: [], removed: [], refused: [], excluded: [], keptCore: [], files: 0, notes: [] };
|
|
480
|
+
const previous = readModuleSkillsManifest(skillsDir);
|
|
481
|
+
// A dir git tracks is the core's, whatever an older manifest said (a core skill can take
|
|
482
|
+
// over a name a module once owned); it is never removed or overwritten here.
|
|
483
|
+
const tracked = (name) => { try { return spawnSync('git', ['-C', coreRoot, 'ls-files', '--error-unmatch', '--', `.claude/skills/${name}`], { encoding: 'utf8' }).status === 0; } catch { return false; } };
|
|
484
|
+
for (const name of Object.keys(previous.skills)) {
|
|
485
|
+
const dir = path.join(skillsDir, name);
|
|
486
|
+
if (!SKILL_NAME_RE.test(name) || !fs.existsSync(dir)) continue;
|
|
487
|
+
if (tracked(name)) { summary.keptCore.push(name); summary.notes.push(`"${name}" is tracked by git now — the last manifest's copy was not removed; the core's dir is kept.`); continue; }
|
|
488
|
+
if (!dryRun) fs.rmSync(dir, { recursive: true, force: true });
|
|
489
|
+
summary.removed.push(name);
|
|
490
|
+
}
|
|
491
|
+
if (clean) {
|
|
492
|
+
for (const f of [MODULE_SKILLS_MANIFEST, '.gitignore']) { const p = path.join(skillsDir, f); if (fs.existsSync(p) && !dryRun) fs.rmSync(p); }
|
|
493
|
+
log(` ${dryRun ? '[dry-run] would remove' : '✓ removed'} ${summary.removed.length} materialised module skill dir(s), the manifest and .claude/skills/.gitignore`);
|
|
494
|
+
return summary;
|
|
495
|
+
}
|
|
496
|
+
const roster = moduleSkillRoster({ coreRoot, instanceDir: coreRoot });
|
|
497
|
+
const sources = moduleSkillSources({ roster });
|
|
498
|
+
for (const r of sources.refused) { summary.refused.push(`${r.name} (${r.module}): ${r.reason}`); log(` ✗ refused module skill "${r.name}" (${r.module}): ${r.reason}`); }
|
|
499
|
+
const manifest = { _doc: 'GENERATED by `node scripts/gds/claude-materialize.js --module-skills-only` (task 1003330, ADR 0224): the module-owned skills materialised into this core checkout as UNTRACKED copies so Claude Code sees them here. Every dir named below is ignored by the sibling .gitignore and removed by --clean; edit the module\'s skills/<name>/ source, never these copies, and re-run to refresh.', skills: {} };
|
|
500
|
+
for (const s of sources) {
|
|
501
|
+
if (!s.enabled) { summary.excluded.push(s.name); continue; }
|
|
502
|
+
const dest = path.join(skillsDir, s.name);
|
|
503
|
+
if (fs.existsSync(dest) && (!Object.prototype.hasOwnProperty.call(previous.skills, s.name) || tracked(s.name))) {
|
|
504
|
+
summary.keptCore.push(s.name);
|
|
505
|
+
summary.notes.push(`"${s.name}" (${s.module}) already exists under .claude/skills/ and is not a materialised copy — the core's own dir is kept.`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const r = copyTreeTransformed(s.dir, dest, { dryRun, overwrite: true, transform: (t) => t });
|
|
509
|
+
summary.files += r.files;
|
|
510
|
+
summary.materialized.push(s.name);
|
|
511
|
+
manifest.skills[s.name] = { module: s.module, source: path.relative(coreRoot, s.dir).split(path.sep).join('/'), vendored: s.vendored, files: r.files };
|
|
512
|
+
}
|
|
513
|
+
const names = Object.keys(manifest.skills).sort();
|
|
514
|
+
manifest.skills = Object.fromEntries(names.map((n) => [n, manifest.skills[n]]));
|
|
515
|
+
const ignore = ['# GENERATED by scripts/gds/claude-materialize.js --module-skills-only (task 1003330): the module-owned skills', '# materialised into this checkout are untracked copies; this file and the manifest ignore themselves. Never commit these.', '/.gitignore', `/${MODULE_SKILLS_MANIFEST}`, ...names.map((n) => `/${n}/`)].join('\n') + '\n';
|
|
516
|
+
if (!dryRun) {
|
|
517
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
518
|
+
fs.writeFileSync(path.join(skillsDir, MODULE_SKILLS_MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
519
|
+
fs.writeFileSync(path.join(skillsDir, '.gitignore'), ignore);
|
|
520
|
+
}
|
|
521
|
+
log(` ${dryRun ? '[dry-run] would materialize' : '✓ materialized'} ${summary.materialized.length} module-owned skill(s) (${summary.files} file(s)) → .claude/skills/ as untracked copies: ${names.join(', ') || 'none'}`);
|
|
522
|
+
return summary;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// The CLI entry is the dev-checkout path only; init/upgrade call materializeClaude directly.
|
|
526
|
+
if (require.main === module) {
|
|
527
|
+
const argv = process.argv.slice(2);
|
|
528
|
+
if (!argv.includes('--module-skills-only') || argv.includes('--help')) {
|
|
529
|
+
process.stderr.write('usage: node scripts/gds/claude-materialize.js --module-skills-only [--dry-run] [--clean]\n Lands every enabled module\'s skills (modules/<key>/skills/<name>/) into THIS checkout\'s .claude/skills/ as untracked,\n self-ignored copies so Claude Code sees them here (task 1003330). --clean removes them. The library behind bongos init / upgrade otherwise.\n');
|
|
530
|
+
process.exit(argv.includes('--help') ? 0 : 2);
|
|
531
|
+
}
|
|
532
|
+
const s = materializeModuleSkillsIntoCore({ coreRoot: path.resolve(__dirname, '..', '..'), dryRun: argv.includes('--dry-run'), clean: argv.includes('--clean'), log: (l) => process.stdout.write(l + '\n') });
|
|
533
|
+
for (const n of s.notes) process.stdout.write(` • ${n}\n`);
|
|
534
|
+
process.stdout.write(`module skills → .claude/skills/: ${s.materialized.length} materialised (${s.files} file(s)), ${s.removed.length} refreshed or removed, ${s.excluded.length} excluded (module off), ${s.keptCore.length} kept as the core's own, ${s.refused.length} refused\n`);
|
|
535
|
+
process.exit(s.refused.length ? 1 : 0);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
module.exports = { scriptToVerb, bongosify, transformSettings, copyTreeTransformed, materializeClaude, materializeModuleSkillsIntoCore, readModuleSkillsManifest, MODULE_SKILLS_MANIFEST, GDS_INVOCATION_RE, buildRebrandRules, rebrandText, disabledModuleSkills, moduleSkillRoster, moduleSkillSources, vendoredSkillGate, SKILL_NAME_RE, VENDOR_DEFAULT_POLICY };
|
|
@@ -63,12 +63,21 @@ function loadNotes() {
|
|
|
63
63
|
try { return JSON.parse(fs.readFileSync(NOTES_PATH, 'utf8')); } catch { return {}; }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// Module-owned skills materialised INTO this checkout (scripts/gds/claude-materialize.js
|
|
67
|
+
// --module-skills-only, task 1003330) are untracked copies the module documents itself; they
|
|
68
|
+
// are not repo skills needing a note here. One reader of the manifest exists — the
|
|
69
|
+
// materialiser's — so this asks it rather than parsing the file a second way.
|
|
70
|
+
const { readModuleSkillsManifest } = require('./claude-materialize.js');
|
|
71
|
+
function materializedNames(absDir) { return new Set(Object.keys(readModuleSkillsManifest(absDir).skills)); }
|
|
72
|
+
|
|
73
|
+
// Immediate subdirectories of an absolute dir, sorted by name (deterministic); materialised
|
|
74
|
+
// module copies excluded.
|
|
67
75
|
function listSubdirs(absDir) {
|
|
68
76
|
let ents = [];
|
|
69
77
|
try { ents = fs.readdirSync(absDir, { withFileTypes: true }); } catch { return []; }
|
|
78
|
+
const materialised = materializedNames(absDir);
|
|
70
79
|
return ents
|
|
71
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !isConflictCopy(e.name))
|
|
80
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !isConflictCopy(e.name) && !materialised.has(e.name))
|
|
72
81
|
.map((e) => e.name)
|
|
73
82
|
.sort((a, b) => a.localeCompare(b));
|
|
74
83
|
}
|
|
@@ -186,6 +195,7 @@ function main() {
|
|
|
186
195
|
if (require.main === module) main();
|
|
187
196
|
|
|
188
197
|
module.exports = {
|
|
198
|
+
materializedNames,
|
|
189
199
|
SECTIONS,
|
|
190
200
|
begin,
|
|
191
201
|
end,
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.466'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -403,5 +403,86 @@ t('materializeClaude: NEVER clobbers an existing CLAUDE.md (init / adopt / upgra
|
|
|
403
403
|
rmSync(dir, { recursive: true, force: true });
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
+
// ---- the dev checkout: module skills materialised INTO the core (task 1003330, ADR 0224) ----
|
|
407
|
+
function fakeCore() {
|
|
408
|
+
const core = mkdtempSync(join(tmpdir(), 'cm-core-'));
|
|
409
|
+
const mod = (key, dflt, skills) => { mkdirSync(join(core, 'modules', key, 'skills'), { recursive: true }); writeFileSync(join(core, 'modules', key, 'module.json'), JSON.stringify({ key, default: dflt, contributes: { skills } })); };
|
|
410
|
+
const skill = (key, name, body = 'run `node scripts/gds/ship.js 1`') => { mkdirSync(join(core, 'modules', key, 'skills', name, 'reference'), { recursive: true }); writeFileSync(join(core, 'modules', key, 'skills', name, 'SKILL.md'), `---\nname: ${name}\n---\n${body}\n`); writeFileSync(join(core, 'modules', key, 'skills', name, 'reference', 'x.md'), 'a playbook\n'); };
|
|
411
|
+
mod('m1', true, ['alpha', 'shared']); skill('m1', 'alpha'); skill('m1', 'shared');
|
|
412
|
+
mod('m2', false, ['beta']); skill('m2', 'beta');
|
|
413
|
+
mkdirSync(join(core, '.claude', 'skills', 'shared'), { recursive: true }); writeFileSync(join(core, '.claude', 'skills', 'shared', 'SKILL.md'), 'the core\'s own shared\n');
|
|
414
|
+
mkdirSync(join(core, '.claude', 'skills', 'core-one'), { recursive: true }); writeFileSync(join(core, '.claude', 'skills', 'core-one', 'SKILL.md'), 'core one\n');
|
|
415
|
+
return core;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
t('--module-skills-only: an enabled module\'s skills land in the core\'s own .claude/skills/ verbatim, with a manifest and a self-ignoring .gitignore; a disabled module\'s are excluded; a core-owned name is kept', () => {
|
|
419
|
+
const core = fakeCore();
|
|
420
|
+
const res = m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
421
|
+
assert.deepEqual(res.materialized, ['alpha']); assert.deepEqual(res.excluded, ['beta']); assert.deepEqual(res.keptCore, ['shared']);
|
|
422
|
+
assert.equal(readFileSync(join(core, '.claude', 'skills', 'alpha', 'SKILL.md'), 'utf8'), '---\nname: alpha\n---\nrun `node scripts/gds/ship.js 1`\n', 'verbatim: no bongosify in the checkout');
|
|
423
|
+
assert.ok(existsSync(join(core, '.claude', 'skills', 'alpha', 'reference', 'x.md')), 'the tree is copied');
|
|
424
|
+
assert.equal(readFileSync(join(core, '.claude', 'skills', 'shared', 'SKILL.md'), 'utf8'), 'the core\'s own shared\n', 'the core copy is untouched');
|
|
425
|
+
const manifest = JSON.parse(readFileSync(join(core, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8'));
|
|
426
|
+
assert.deepEqual(Object.keys(manifest.skills), ['alpha']); assert.equal(manifest.skills.alpha.module, 'm1'); assert.equal(manifest.skills.alpha.source, 'modules/m1/skills/alpha');
|
|
427
|
+
const ignore = readFileSync(join(core, '.claude', 'skills', '.gitignore'), 'utf8');
|
|
428
|
+
for (const line of ['/.gitignore', '/.module-skills.json', '/alpha/']) assert.ok(ignore.split('\n').includes(line), `.gitignore carries ${line}`);
|
|
429
|
+
assert.ok(!ignore.includes('/shared/') && !ignore.includes('/core-one/'), 'only materialised dirs are ignored');
|
|
430
|
+
rmSync(core, { recursive: true, force: true });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
t('--module-skills-only: a second run is idempotent (byte-identical manifest + .gitignore), a skill the module stopped declaring is removed, and --clean removes everything it made', () => {
|
|
434
|
+
const core = fakeCore();
|
|
435
|
+
m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
436
|
+
const before = [m.MODULE_SKILLS_MANIFEST, '.gitignore'].map((f) => readFileSync(join(core, '.claude', 'skills', f), 'utf8'));
|
|
437
|
+
const again = m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
438
|
+
assert.deepEqual(again.removed, ['alpha']); assert.deepEqual(again.materialized, ['alpha']);
|
|
439
|
+
assert.deepEqual([m.MODULE_SKILLS_MANIFEST, '.gitignore'].map((f) => readFileSync(join(core, '.claude', 'skills', f), 'utf8')), before, 'byte-identical on the second run');
|
|
440
|
+
writeFileSync(join(core, 'modules', 'm1', 'module.json'), JSON.stringify({ key: 'm1', default: true, contributes: { skills: ['shared'] } }));
|
|
441
|
+
const dropped = m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
442
|
+
assert.ok(!existsSync(join(core, '.claude', 'skills', 'alpha')), 'a skill no longer declared is removed, not left to linger');
|
|
443
|
+
assert.deepEqual(dropped.materialized, []);
|
|
444
|
+
writeFileSync(join(core, 'modules', 'm1', 'module.json'), JSON.stringify({ key: 'm1', default: true, contributes: { skills: ['alpha', 'shared'] } }));
|
|
445
|
+
m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
446
|
+
const cleaned = m.materializeModuleSkillsIntoCore({ coreRoot: core, clean: true });
|
|
447
|
+
assert.deepEqual(cleaned.removed, ['alpha']);
|
|
448
|
+
for (const p of ['alpha', m.MODULE_SKILLS_MANIFEST, '.gitignore']) assert.ok(!existsSync(join(core, '.claude', 'skills', p)), `--clean removed ${p}`);
|
|
449
|
+
assert.ok(existsSync(join(core, '.claude', 'skills', 'core-one', 'SKILL.md')), 'the core\'s own skills survive --clean');
|
|
450
|
+
rmSync(core, { recursive: true, force: true });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
t('--module-skills-only: an instance materialised FROM a core that carries materialised copies gets them through the module path (transformed), never as core skills, and never the two dotfiles', () => {
|
|
454
|
+
const core = fakeCore();
|
|
455
|
+
m.materializeModuleSkillsIntoCore({ coreRoot: core });
|
|
456
|
+
const inst = mkdtempSync(join(tmpdir(), 'cm-inst-'));
|
|
457
|
+
const res = m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
458
|
+
assert.ok(!res.notes.some((n) => /"alpha".*also exists under the core/.test(n)), 'the materialised copy is not mistaken for a core skill');
|
|
459
|
+
assert.ok(res.moduleSkills >= 2, 'alpha landed through step 1b');
|
|
460
|
+
assert.match(readFileSync(join(inst, '.claude', 'skills', 'alpha', 'SKILL.md'), 'utf8'), /bongos ship 1/, 'transformed on the way into the instance');
|
|
461
|
+
for (const f of [m.MODULE_SKILLS_MANIFEST, '.gitignore']) assert.ok(!existsSync(join(inst, '.claude', 'skills', f)), `${f} is not shipped to an instance`);
|
|
462
|
+
assert.ok(existsSync(join(inst, '.claude', 'skills', 'core-one', 'SKILL.md')) && existsSync(join(inst, '.claude', 'skills', 'shared', 'SKILL.md')), 'the core skills still land');
|
|
463
|
+
rmSync(core, { recursive: true, force: true }); rmSync(inst, { recursive: true, force: true });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
t('--module-skills-only: the CLI entry refuses without the flag (usage, exit 2) and gen-file-map skips materialised names by the manifest', () => {
|
|
467
|
+
const { spawnSync } = require('node:child_process');
|
|
468
|
+
const bare = spawnSync(process.execPath, [join(ROOT, 'scripts', 'gds', 'claude-materialize.js')], { encoding: 'utf8' });
|
|
469
|
+
assert.equal(bare.status, 2); assert.match(bare.stderr, /--module-skills-only/);
|
|
470
|
+
// end to end through the real entry point, on the real checkout, writing nothing: --dry-run
|
|
471
|
+
// reports what it would land, --clean --dry-run what it would remove, both exit 0
|
|
472
|
+
const cli = join(ROOT, 'scripts', 'gds', 'claude-materialize.js');
|
|
473
|
+
const dry = spawnSync(process.execPath, [cli, '--module-skills-only', '--dry-run'], { encoding: 'utf8' });
|
|
474
|
+
assert.equal(dry.status, 0, dry.stderr); assert.match(dry.stdout, /\[dry-run\] would materialize \d+ module-owned skill/); assert.match(dry.stdout, /module skills → \.claude\/skills\//);
|
|
475
|
+
const dryClean = spawnSync(process.execPath, [cli, '--module-skills-only', '--clean', '--dry-run'], { encoding: 'utf8' });
|
|
476
|
+
assert.equal(dryClean.status, 0, dryClean.stderr); assert.match(dryClean.stdout, /\[dry-run\] would remove \d+ materialised module skill dir/);
|
|
477
|
+
assert.ok(!existsSync(join(ROOT, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST)) || readFileSync(join(ROOT, '.claude', 'skills', m.MODULE_SKILLS_MANIFEST), 'utf8').length > 0, 'a dry run never wrote or emptied the real manifest');
|
|
478
|
+
const gen = require('../scripts/gds/gen-file-map.js');
|
|
479
|
+
const dir = mkdtempSync(join(tmpdir(), 'cm-fm-'));
|
|
480
|
+
for (const n of ['real', 'copy']) { mkdirSync(join(dir, n)); writeFileSync(join(dir, n, 'SKILL.md'), 'x'); }
|
|
481
|
+
writeFileSync(join(dir, m.MODULE_SKILLS_MANIFEST), JSON.stringify({ skills: { copy: { module: 'm1' } } }));
|
|
482
|
+
assert.deepEqual([...gen.materializedNames(dir)], ['copy']);
|
|
483
|
+
assert.deepEqual([...gen.materializedNames(join(dir, 'real'))], [], 'no manifest, nothing skipped');
|
|
484
|
+
rmSync(dir, { recursive: true, force: true });
|
|
485
|
+
});
|
|
486
|
+
|
|
406
487
|
console.log(`\nclaude_materialize: ${passed} passed, ${failed} failed`);
|
|
407
488
|
process.exit(failed ? 1 : 0);
|
package/tests/file_map.mjs
CHANGED
|
@@ -37,8 +37,11 @@ test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no s
|
|
|
37
37
|
// core checkout carries neither (task 1002470). Check the sections that exist.
|
|
38
38
|
if (!existsSync(path.join(ROOT, section.dir))) continue;
|
|
39
39
|
checked += 1;
|
|
40
|
+
// A module skill materialised into this checkout (task 1003330) is an untracked copy the
|
|
41
|
+
// module documents; the generator skips it by the manifest beside it, and so does this.
|
|
42
|
+
const materialised = gen.materializedNames(path.join(ROOT, section.dir));
|
|
40
43
|
const onDisk = readdirSync(path.join(ROOT, section.dir), { withFileTypes: true })
|
|
41
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
44
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !materialised.has(e.name))
|
|
42
45
|
.map((e) => e.name)
|
|
43
46
|
.sort();
|
|
44
47
|
const listed = section.entries.map((e) => e.name).sort();
|