wicked-garden 12.38.1 → 12.40.0

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.
Files changed (107) hide show
  1. package/.claude-plugin/components.json +5 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.gitattributes +3 -0
  5. package/CHANGELOG.md +31 -1
  6. package/docs/extending.md +20 -2
  7. package/package.json +1 -1
  8. package/schemas/wicked-pack.schema.json +185 -12
  9. package/scripts/_pack_registry.py +25 -5
  10. package/scripts/demo/_playwright.mjs +25 -0
  11. package/scripts/demo/contact_sheet.py +189 -0
  12. package/scripts/demo/package-lock.json +44 -0
  13. package/scripts/demo/package.json +17 -0
  14. package/scripts/demo/postprocess.mjs +79 -0
  15. package/scripts/demo/probe.mjs +59 -0
  16. package/scripts/demo/readonly.mjs +99 -0
  17. package/scripts/demo/record.mjs +355 -0
  18. package/scripts/demo/setup.mjs +27 -0
  19. package/scripts/demo/stage.mjs +316 -0
  20. package/scripts/draft/claims_scan.py +182 -4
  21. package/scripts/mcp/probe.py +207 -0
  22. package/scripts/mcp/scaffold.py +74 -0
  23. package/scripts/mcp/shim.py +205 -0
  24. package/scripts/pack/check.py +314 -6
  25. package/scripts/qe/lib/crew-run.mjs +20 -0
  26. package/scripts/qe/lib/gate.mjs +3 -0
  27. package/scripts/qe/runner/src/evidence.mjs +4 -0
  28. package/skills/core/SKILL.md +3 -0
  29. package/skills/demo/SKILL.md +315 -0
  30. package/skills/demo/assets/example-storyline.mjs +150 -0
  31. package/skills/demo/refs/gotchas.md +139 -0
  32. package/skills/demo/refs/rehearsal.md +57 -0
  33. package/skills/demo/refs/review-checklist.md +91 -0
  34. package/skills/demo/refs/stage-design.md +92 -0
  35. package/skills/demo/refs/storyline-api.md +107 -0
  36. package/skills/demo/refs/storytelling.md +62 -0
  37. package/skills/demo/refs/templates.md +132 -0
  38. package/skills/draft/SKILL.md +5 -1
  39. package/skills/draft/refs/claims.md +23 -0
  40. package/skills/governed-worker/SKILL.md +5 -1
  41. package/skills/mcp/SKILL.md +107 -0
  42. package/skills/mcp-scaffold/SKILL.md +147 -0
  43. package/skills/mcp-scaffold/assets/node/server.mjs +131 -0
  44. package/skills/mcp-scaffold/assets/python/server.py +159 -0
  45. package/skills/mem/SKILL.md +3 -0
  46. package/skills/mem/refs/decision-report.md +33 -0
  47. package/skills/qe/refs/accept.md +5 -0
  48. package/skills/qe/refs/execute.md +3 -0
  49. package/skills/qe-a11y-test-engineer/SKILL.md +2 -0
  50. package/skills/qe-ai-feature-test-engineer/SKILL.md +2 -0
  51. package/skills/qe-chaos-test-engineer/SKILL.md +2 -0
  52. package/skills/qe-compliance-test-engineer/SKILL.md +2 -0
  53. package/skills/qe-flaky-test-hunter/SKILL.md +2 -0
  54. package/skills/qe-iac-test-engineer/SKILL.md +2 -0
  55. package/skills/qe-incident-to-scenario-synthesizer/SKILL.md +2 -0
  56. package/skills/qe-mutation-test-engineer/SKILL.md +2 -0
  57. package/skills/qe-release-readiness-engineer/SKILL.md +2 -0
  58. package/skills/qe-security-test-engineer/SKILL.md +2 -0
  59. package/skills/qe-snapshot-hygiene-auditor/SKILL.md +2 -0
  60. package/skills/qe-test-code-quality-auditor/SKILL.md +2 -0
  61. package/skills/qe-visual-regression-engineer/SKILL.md +2 -0
  62. package/skills/repo-learn/SKILL.md +20 -1
  63. package/skills/repo-learn/refs/capture-proposals.md +8 -1
  64. package/.product/DES-001-technical-design.md +0 -310
  65. package/.product/RAID.md +0 -62
  66. package/.product/REQ-001-application-overview.md +0 -94
  67. package/.product/REQ-002-technology-constraints.md +0 -103
  68. package/.product/REQ-003-domain-model.md +0 -191
  69. package/.product/REQ-004-ways-of-working.md +0 -189
  70. package/.product/REQ-005-dod-criteria.md +0 -132
  71. package/.product/TEST-001-test-strategy.md +0 -165
  72. package/.product/evidence/.gitkeep +0 -0
  73. package/.product/reviews/.gitkeep +0 -0
  74. package/.product/reviews/adversarial-review-v12.28.1.md +0 -211
  75. package/evidence/aw25-golden/README.md +0 -80
  76. package/evidence/aw25-golden/campaign-recon.aw25.json +0 -53
  77. package/evidence/aw25-golden/chain/00-daemon-ready.txt +0 -43
  78. package/evidence/aw25-golden/chain/01-ingest.txt +0 -1
  79. package/evidence/aw25-golden/chain/02-fanout-manifest.json +0 -63
  80. package/evidence/aw25-golden/chain/02-fanout-manifest.json.crew-payload.json +0 -58
  81. package/evidence/aw25-golden/chain/02-fanout.txt +0 -6
  82. package/evidence/aw25-golden/chain/03-enforcement-post.json +0 -54
  83. package/evidence/aw25-golden/chain/04-rules-recall.json +0 -22
  84. package/evidence/aw25-golden/chain/05-run-terminal.json +0 -55
  85. package/evidence/aw25-golden/chain/06-governance-claims.json +0 -1
  86. package/evidence/aw25-golden/chain/07-acceptance.json +0 -1
  87. package/evidence/aw25-golden/ruleset/aw25-golden.md +0 -53
  88. package/evidence/aw25-golden/ruleset/policies/POL-2500.json +0 -11
  89. package/evidence/aw25-golden/run.sh +0 -244
  90. package/evidence/th6-dod/00-daemon-ready-line.txt +0 -1
  91. package/evidence/th6-dod/01-acceptance-deny.jsonl +0 -2
  92. package/evidence/th6-dod/02-qe-run-1.json +0 -15
  93. package/evidence/th6-dod/03-qe-run-2.json +0 -15
  94. package/evidence/th6-dod/04-flake-history.json +0 -27
  95. package/evidence/th6-dod/05-manifest-run2.json +0 -58
  96. package/evidence/th6-dod/06-gate-pass.json +0 -1
  97. package/evidence/th6-dod/07-acceptance-satisfied.jsonl +0 -2
  98. package/evidence/th6-dod/08-host-run-terminal.json +0 -13
  99. package/evidence/th6-dod/09-acceptance-after-terminal.jsonl +0 -2
  100. package/evidence/th6-dod/10-verdicts-rows.json +0 -16
  101. package/evidence/th6-dod/README.md +0 -62
  102. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/console.json +0 -3
  103. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/manifest.json +0 -113
  104. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/result.json +0 -75
  105. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/steps.json +0 -48
  106. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/th6-home-connected.png +0 -0
  107. package/evidence/th6-dod/qe-evidence-bundle-8d49b3fe/wire.json +0 -80
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "_comment": "Issue #328: Component declarations for wicked-garden plugin. Claude Code plugin.json does not yet support inline component declarations beyond path overrides. This manifest documents all components for tooling, validation, and future schema support. When plugin.json gains a 'components' field, merge this data into plugin.json. The plugin is skills-only: former commands/ became actions of consolidated per-domain skills, and former agents/ became standalone context:fork worker skills (enumerated under fork_skills). Regenerate derived blocks with scripts/ci/sync_components.py.",
3
- "domains": ["agentic", "crew", "data", "engineering", "jam", "mem", "persona", "platform", "product", "qe", "search"],
3
+ "domains": ["agentic", "crew", "data", "demo", "engineering", "jam", "mem", "persona", "platform", "product", "qe", "search"],
4
4
  "summary": {
5
- "skills": 133,
5
+ "skills": 136,
6
6
  "fork_skills": 69,
7
7
  "hooks": 13,
8
8
  "specialists": 7
@@ -18,6 +18,7 @@
18
18
  "data": ["data"],
19
19
  "data-engineer": ["data-engineer"],
20
20
  "deliberate": ["deliberate"],
21
+ "demo": ["demo"],
21
22
  "domain": ["domain"],
22
23
  "domain-coverage": ["domain-coverage"],
23
24
  "domain-extractor": ["domain-extractor"],
@@ -32,6 +33,8 @@
32
33
  "jam": ["jam"],
33
34
  "jam-brainstorm-facilitator": ["jam-brainstorm-facilitator"],
34
35
  "jam-council": ["jam-council"],
36
+ "mcp": ["mcp"],
37
+ "mcp-scaffold": ["mcp-scaffold"],
35
38
  "mem": ["mem"],
36
39
  "mem-capture": ["mem-capture"],
37
40
  "mem-ingest": ["mem-ingest"],
@@ -1 +1 @@
1
- {"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.38.1"}], "version": "12.38.1"}
1
+ {"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.40.0"}], "version": "12.40.0"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.38.1",
3
+ "version": "12.40.0",
4
4
  "description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), real multi-model second opinions (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
5
5
  "wicked_vault_version": "^0.5.0",
6
6
  "wicked_ledger_version": "^0.4.0",
package/.gitattributes CHANGED
@@ -1,2 +1,5 @@
1
1
  # Windows command twins must keep CRLF line endings; everything else stays LF.
2
2
  *.cmd text eol=crlf
3
+
4
+ # Pack editor fixtures are pinned by sha256: never rewrite their line endings.
5
+ tests/fixtures/packs/**/editors/** -text
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.40.0] — 2026-10-02
6
+
7
+ ### Added
8
+ - **QE ledger rows name the governed crew run they were written in (WT-G4, DES-walkthrough-proof §4.14).** Inside a wicked-crew run (`WICKED_RUN_ID` set), the qe-runner's `runs` row (`scripts/qe/runner/src/evidence.mjs`), the gate's `verdicts` row (`scripts/qe/lib/gate.mjs`), the accept playbook's run and verdict rows and every QE specialist's verdict write carry a top-level `crew_run_id`, so crew's acceptance reader attributes the QE run `kind: 'stamped'` instead of inferring it from the run's lifetime. Outside a governed run nothing is stamped. One helper, `scripts/qe/lib/crew-run.mjs`.
9
+ - **`mem`: the decision-report codebook for wicked-crew's chat recorder (DC-S5, DES-decision-capture §4.3.3).** `skills/mem/refs/decision-report.md` (under 2 KB, strict frontmatter) tells a chat seat how to end its reply with a fenced `wicked-decisions` JSON block: `quote`, `decision_text`, `type`, `codify`, `ambiguous`, `steering_type`, `approves_proposal`, `same_as`. The unit is the (previous reply, operator message) pair: when the operator approves or picks from the seat's proposal, `decision_text` is written from the approved proposal plus any amendment, never from the operator's short wording. Labels are suggestions; crew decides what is remembered. `tests/mem/test_decision_report_codebook.py` pins the size, the parse, the vocabulary and the pair rule, plus a local-only copy guard against the operator corpus.
10
+ - **Packs: `wicked-pack.json` spec 2 with `editors[]` and `blocks[]`, and `pack check` for them (EP-G1, DES-artifact-editor-plugins §5.2).** A spec-2 manifest keeps every spec-1 field and adds artifact editors (one self-contained HTML entry inside the pack, pinned by sha256, with kinds, sizes, protocol and permissions, each with its reason) and blocks (the preset that produces the pack's kind, with the pack skills it uses). `domains` becomes optional when `editors` is present. Spec-1 packs stay valid. `pack check` gains PK060–PK072, one code and message per refusal: the entry escaping the pack (including by symlink), not one `.html` file, over its limit or the host's 5 MiB cap, a hash mismatch, a script/stylesheet/frame loaded from outside the file, an unknown kind/size/permission or one without its "why", a malformed block, a block naming a skill the pack does not ship. A `wicked`/`wicked-*` editor id is refused in every pack, and since this is a structural check, `pack register --force` refuses it too. The JSON schema matches, and now accepts the `$schema` key its own fixture already carried.
11
+
12
+ ### Changed
13
+ - **Demo recorder: the fixture origin is the one write door, and a failed take keeps its evidence (WT-G1, DES-walkthrough-proof).** `scripts/demo/readonly.mjs` `armGuard(context, { writableOrigin })` replaces `armReadOnly`: with `record.mjs --fixture-origin <origin>` (a loopback http(s) origin you started for the recording; anything else is refused before recording), writes and page WebSocket frames to exactly that origin pass, and every other write is still aborted and listed. With no fixture origin a recording stays fully read-only. The environment switch that lifted the guard is deleted. A take that fails now keeps its timeline, a video built from the frames it has, and `failure.json` (error, blocked requests, `failed_at_sec`) under `segments/<key>/failed-<take>/`; the stitch and `--reencode` refuse a segment whose last take failed (`failed_take`). `guard.json` gains `take`, `failed` and `writableOrigin`; `recording.json` labels a segment `fixture-writable`.
14
+
15
+ ## [12.39.0] — 2026-09-29
16
+
17
+ ### Added
18
+ - **`repo-learn` + `governed-worker`: a capture unit ends with a machine-readable capture report (wicked-core#535, BC-80).** A capture-learnings run whose skill loaded but never ran produced 0 proposals and reported `completed` — nothing told that apart from a repository with nothing worth capturing. The capture unit's output now ends with `wicked-capture-report {"derived": N, "submitted": M, "failed": K}`, **always**, including a degrade and a legitimate 0 (an honest `0/0/0` is a pass). A proposal counts as `submitted` only when the shim answered `{"ok": true, …}`, so `submitted` below `derived`, or any `failed`, is the loss the engine's capture-report floor stops — the derived items are in the unit's fenced `json` block and a human must act on them. Stated in `skills/repo-learn/SKILL.md` ("In a governed run" step 3 and the degrade rules), `skills/repo-learn/refs/capture-proposals.md` and `governed-worker` A4, so every carrier of the honest-counts rule spells it the same way.
19
+ - **`mcp` skill: a governed unit reaches MCP tools through the wicked-crew broker on every seat (DES-MCP-TOOLS-001 S4).** `scripts/mcp/shim.py list` answers the tools the unit's capability token (`WICKED_MCP_TOKEN`) may try, each with its class and whether a call runs (`allow`) or waits for approval (`ask`); `scripts/mcp/shim.py call mcp:<server>/<tool> --args JSON` sends the call to the broker (`WICKED_CREW_URL`, `POST /api/v1/mcp/call`) and prints its answer. It prints one JSON object and exits by outcome: `0` ok, `3` denied or withheld, `4` pending approval, `5` unavailable (budget, breaker, upstream), `2` no MCP channel or bad arguments, `1` anything else. The token is never printed. Standard library only. `skills/mcp/SKILL.md` names the commands, the exit codes and the rules (only through the broker, the token is a credential, a denied call is final); `tests/mcp/test_mcp_shim.py` pins the shim against a broker-shaped HTTP server.
20
+ - **`mcp-scaffold` skill: build an MCP server the wicked-crew broker can register and govern (DES-MCP-TOOLS-001 S5b).** `skills/mcp-scaffold/` ships two zero-dependency stdio templates (`assets/node/server.mjs`, `assets/python/server.py`) that answer `initialize` (with version negotiation), `ping`, `tools/list` and `tools/call`, with one read and one write example tool. `scripts/mcp/scaffold.py` stamps a template into a directory and never overwrites; `scripts/mcp/probe.py` spawns any stdio server with a minimal environment, runs `initialize` + `tools/list` (following `nextCursor`) and derives each tool's class the way the broker does (`readOnlyHint: true` is read, `destructiveHint` absent or true is destructive, otherwise write; no annotations is write). The skill states the rules the broker relies on: honest annotations, secrets only from the injected environment, stdout for protocol frames only, and first-use approval before any call runs. `tests/mcp/test_mcp_scaffold.py` proves both templates answer initialize + tools/list in CI.
21
+
22
+ - **Demo recorder is read-only against the app it films (wicked-studio#373, wicked-crew#565).** `scripts/demo/readonly.mjs` aborts every request that is not GET/HEAD/OPTIONS, and every WebSocket frame the page sends, while `record.mjs` records; the segment then fails `side_effect_blocked`, naming each, even when the blocked write made the storyline's own wait time out. A blocked or failed take is never kept as video: it leaves only its `guard.json` (no `segment.mp4`, no `timeline.json` for `--reencode` to rebuild from), and `--reencode` and `--stitch` refuse to run while any segment's last take was blocked, so it is never rebuilt, stitched, or quietly left out. A read-only recording blocks service workers, whose requests the guard's routes would not see. `recording.json` says `readOnly` only when every stitched segment's guard blocked nothing. `DEMO_ALLOW_WRITES=1` lifts the guard, for disposable targets only. Each segment writes `guard.json`, and the stitch writes `recording.json` (`readOnly` is true only when every stitched segment was recorded read-only). `tests/demo/test_readonly_recorder.py` proves no write reaches a local app whose submit button the storyline presses.
23
+
24
+ ### Fixed
25
+ - **`claims_scan.py` #1184: the citation rule is now written down and implemented once — a `data-source="path:N"` asserts the text of THE ELEMENT THAT CARRIES IT and is verified against that element's own text, never a sibling's and never a wider block.** Every carrier of a line citation is checked against its own `all_text()`: the element, each inline wrapper between it and its block (previously dropped entirely), and the block. The dedup key is `(source, id(carrier))`, so two inline siblings carrying the same source are two checks against two texts — the layer-5 skip, where a wrong citation on one `<span>` was satisfied by its sibling's text, is gone. Verification now needs a CONTENT word in common (a `STOPWORDS`-only overlap verifies nothing and is reported as such), and a range `path:N-M` is scored against the best span of ITS OWN WIDTH instead of against single lines, so a wide range no longer ties its way to a pass; a verified range is counted and disclosed separately (`cited_ranges`, in the summary as "N by line range"). `add()`'s dedup key gained the detail, so two different bad citations on one element are two findings and `stats["cite_off"]` can no longer disagree with the findings list. The rule is stated in the module docstring, beside `_check_cite_off`, and in `skills/draft/refs/claims.md`. Both sides of the span comparison are scored on CONTENT tokens, so a line sharing nothing but "it is" can no longer outscore the cited line that carries the claim's one content word (which reported a correct citation as `cite-off`), and the finding dedup keys on NODE IDENTITY rather than `path()` — which carries no sibling index — so two sibling carriers with the same text and the same bad source are two findings.
26
+ - **`claims_scan.py` cite-off dedup: this file has now produced the same defect class at three layers — `return None` meaning "verified", then the double-report, then this sibling skip (keying the dedup on `path()` instead of node identity) — each fix introducing the next; the next reader should approach this code expecting that pattern.**
27
+ - **`claims_scan.py` #1178: a `data-source="path:N"` whose line number is past end-of-file (or ≤ 0) is now a `cite-off` finding instead of silently clamping to the last line and reporting clean.** Both the single-line spec `N` and the range spec `N-M` fail closed when `N > len(lines)`, `N < 1`, or (for the range) `M > len(lines)`, with a detail naming the real file length (e.g., `"data-source README.md:9999 is past end of file (3 lines)"`).
28
+ - **`claims_scan.py` #1179: a citation whose overlap with the block text is beaten by a strictly higher-scoring line elsewhere in the file is now a `cite-off` finding instead of silently passing on shared stopwords.** A block whose true text is on line 1, cited at line 2, where the two lines share only stopwords (`"is"`, `"the"`, `"for"`), now produces `cite-off`. The cited line must be the best or tied-best match to pass; the hint message names the nearest-match line.
29
+ - **`claims_scan.py` #1181 (three sub-cases): `_check_cite_off` now distinguishes VERIFIED (`""`) from COULD-NOT-VERIFY (`_UNVERIFIABLE` sentinel), which the caller treats as a `cite-off` finding. `None` means path-only citation (no `:N` suffix, nothing to verify); `""` means verified clean.** Could-not-verify cases that previously returned `None` and incremented `cited_lines`: (A) an inline-wrapped citation — `<li data-source="README.md:1"><span>text</span></li>` — is now resolved through the enclosing block's `all_text()` and the block's own direct `data-source` attributes, so the invisible citation is checked against the full block text; (B) a block whose token set is all single characters (e.g. `"7"`) now keeps digit tokens (`len(t) >= 2 or t.isdigit()`), and an empty set after filtering fails closed per the sentinel; (C) a `data-source` that names a directory no longer passes the `is_file()` check, and an `OSError` on one repo now `continue`s to the next rather than aborting the entire search.
30
+ - **`claims_scan.py`: a `data-source="path:N"` on an existing file now verifies that the cited lines carry the block's own text — a mismatch is reported as `cite-off` ("data-source README.md:7 does not carry this text — nearest match: line 4") and fails the floor exactly like `dangling-source` (#1176).** Path-only citations remain valid; inherited (ancestor-level) citations are not checked for line content — only per-element citations are verified. The summary line gains `cited_lines (N cite-off)` counts. `skills/draft/SKILL.md` §3 Grounded now instructs authors to re-open the file at the line before writing the citation, and the verdict table's FAIL row lists `cite-off`. `self_check.py` surfaces `cite-off` automatically (it calls `claims_scan.run`).
31
+ - **`scripts/ci/validate.py` judges the checkout you asked about, not every tree nested inside it (#1180, second half).** Check 8 walked `root.glob("**/*.json")`, which descends into gitignored scratch (`tmp/` held 11,518 files after run `b7084e9d`) and into nested checkouts — governed-run worktrees are created inside the repo — so an operator running the validator in a run worktree got exit 1 with 5 errors that were pytest's deliberately-malformed fixtures and another tree's files: a failure a correct tree could not avoid, whose obvious wrong fix was to edit the fixtures. The file list now comes from git's own view of the tree (`ls-files --cached --others --exclude-standard`), which is exactly "tracked, or new and not ignored" and reports a nested repository as one opaque entry instead of walking into it; outside a git work tree the filesystem walk remains, now with a warning that says scratch and nested checkouts cannot be excluded. `validate.py` also takes `--root <checkout>` so the tree under judgement can be named (the default is unchanged). `tests/test_validate_scope.py` rebuilds the incident tree and pins both halves, including that the pre-fix glob DOES report the two foreign files. The first half of #1180 (`tmp/` unignored) landed in #1182.
32
+
5
33
  ## [12.38.1] — 2026-09-15
6
34
 
7
35
  ### Fixed
@@ -933,7 +961,9 @@ through `v9.2.18`. Major themes across that span:
933
961
 
934
962
  For each of those releases, see the corresponding tag in git history.
935
963
 
936
- [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.38.1...HEAD
964
+ [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.40.0...HEAD
965
+ [12.40.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.39.0...v12.40.0
966
+ [12.39.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.38.1...v12.39.0
937
967
  [12.38.1]: https://github.com/mikeparcewski/wicked-garden/compare/v12.38.0...v12.38.1
938
968
  [12.38.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.37.2...v12.38.0
939
969
  [12.37.2]: https://github.com/mikeparcewski/wicked-garden/compare/v12.37.1...v12.37.2
package/docs/extending.md CHANGED
@@ -80,6 +80,23 @@ A complete working example (used as garden's own e2e fixture):
80
80
  7. **Events** (if you emit them): 4-segment
81
81
  `wicked.<domain>.<noun>.<past-verb>` — bus SPEC.md is the authority.
82
82
 
83
+ ### Spec 2: editors and blocks
84
+
85
+ Manifest `spec: 2` keeps every spec-1 field and adds two arrays. A spec-1 pack stays valid as it is.
86
+
87
+ - `editors[]`: artifact editors the pack ships. Each one is **one self-contained HTML file** inside the
88
+ pack (`entry`), pinned by its `sha256`, with an `id`, a plain-words `title`, its own `version`, the
89
+ `protocol` versions it speaks, the artifact `kinds` it opens, the `sizes` it supports (`inline` is
90
+ required), and the `permissions` it asks for, each with a `why` shown at install. An optional
91
+ `limits.bundleBytes` may only be lower than the host's cap (5 MiB). With `editors` present, `domains`
92
+ is optional: an editor-only pack ships no skills.
93
+ - `blocks[]`: the block that **produces** the pack's kind: an `id`, a `label`, a `preset` JSON file
94
+ inside the pack, the `produces_kind`, and the pack `skills` it uses (approved together at install).
95
+
96
+ `wicked` and `wicked-*` editor and block ids are refused in every pack: first-party editors ship inside
97
+ wicked-studio and first-party blocks are core presets. Example (garden's own spec-2 fixture):
98
+ [`tests/fixtures/packs/acme-terms/`](../tests/fixtures/packs/acme-terms/).
99
+
83
100
  ## Validate: the shipped conformance gate
84
101
 
85
102
  ```bash
@@ -90,8 +107,9 @@ npx wicked-garden pack check ./acme-seo-pack --json # CI
90
107
  Exit 0 = conformant (warnings allowed), 1 = errors. The gate checks the
91
108
  manifest, router/worker shape, naming, disclosure tiers, NOT-THIS-WHEN
92
109
  reciprocity, produces contracts (archetype names must exist in garden's
93
- catalog), and peer-floor syntax — rule codes PK001–PK050, each with an
94
- actionable message. It runs anywhere Python ≥ 3.10 exists; no garden install
110
+ catalog), peer-floor syntax, and for spec 2 every editor (inside the pack, one `.html` file, its
111
+ hash, its size, nothing loaded from outside it, kinds, sizes, permissions) and block (preset file,
112
+ produced kind, pack skills): rule codes PK001–PK072, each with an actionable message. It runs anywhere Python ≥ 3.10 exists; no garden install
95
113
  required (`scripts/pack/check.py` ships in the npm package).
96
114
 
97
115
  ## Install + register
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.38.1",
3
+ "version": "12.40.0",
4
4
  "type": "module",
5
5
  "description": "Curated toolkit for what AI coding agents can't do alone: evidence-gated work, codegraph relationships, deterministic multi-file refactor, multi-model second opinions, and cross-session memory. Installs as a Claude Code plugin.",
6
6
  "keywords": [
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://github.com/mikeparcewski/wicked-garden/blob/main/schemas/wicked-pack.schema.json",
4
- "title": "wicked-pack.json — third-party skill-pack manifest",
5
- "description": "Drop-in registration manifest for a third-party pack (extension contract, SKILL-RATIONALIZATION §5). Placed at the pack root next to its skills/ tree. Garden discovers it at runtime from WICKED_PACK_PATH, the registered-pack file, Claude Code plugin dirs, and <project>/.wicked/packs/ — no garden PR required. Executable validation: `npx wicked-garden pack check <dir>` (scripts/pack/check.py).",
4
+ "title": "wicked-pack.json — third-party pack manifest (skills, editors, blocks)",
5
+ "description": "Drop-in registration manifest for a third-party pack (extension contract, SKILL-RATIONALIZATION §5). Spec 2 adds editors[] and blocks[] (DES-artifact-editor-plugins §5.2); spec-1 packs stay valid. Placed at the pack root next to its skills/ tree. Garden discovers it at runtime from WICKED_PACK_PATH, the registered-pack file, Claude Code plugin dirs, and <project>/.wicked/packs/ — no garden PR required. Executable validation: `npx wicked-garden pack check <dir>` (scripts/pack/check.py).",
6
6
  "type": "object",
7
- "required": ["spec", "name", "vendor", "version", "domains"],
7
+ "required": ["spec", "name", "vendor", "version"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "Optional pointer to this schema, for editors."
13
+ },
10
14
  "spec": {
11
- "const": 1,
12
- "description": "Manifest spec version. Currently 1."
15
+ "enum": [1, 2],
16
+ "description": "Manifest spec version. 1: skills only. 2: adds editors[] and blocks[]; domains becomes optional when editors is present."
13
17
  },
14
18
  "name": {
15
19
  "type": "string",
@@ -40,7 +44,7 @@
40
44
  "domains": {
41
45
  "type": "array",
42
46
  "minItems": 1,
43
- "description": "The domains this pack contributes. Each declared domain must have exactly one router skill named {vendor}-{domain}; its workers are {vendor}-{domain}-{role} fork skills.",
47
+ "description": "The domains this pack contributes. Each declared domain must have exactly one router skill named {vendor}-{domain}; its workers are {vendor}-{domain}-{role} fork skills. Required, except in a spec-2 pack that declares editors.",
44
48
  "items": {
45
49
  "type": "object",
46
50
  "required": ["name"],
@@ -104,20 +108,189 @@
104
108
  "peers": {
105
109
  "type": "object",
106
110
  "description": "Peer version floors (extension-contract gap 6), e.g. {\"wicked-garden\": \">=12.29.0\", \"wicked-vault\": \">=0.5.0\"}. Checked FAIL-OPEN: violations are surfaced at SessionStart and by `pack floors`, never blocked on.",
107
- "additionalProperties": {
108
- "type": "string",
109
- "pattern": "^(>=|\\^)\\s*\\d+\\.\\d+\\.\\d+$"
110
- }
111
+ "additionalProperties": { "type": "string", "pattern": "^(>=|\\^)\\s*\\d+\\.\\d+\\.\\d+$" }
111
112
  },
112
113
  "provenance": {
113
114
  "type": "object",
114
115
  "description": "Declared provenance (extension-contract gap 4 — minimal + honest). Registration additionally records a sha256 of this manifest and a content hash of the skills tree. There is NO signing: hashes detect post-registration drift, they do not prove authorship. Install packs only from sources you trust — skills carry shell.",
115
116
  "additionalProperties": false,
116
117
  "properties": {
117
- "source": { "type": "string", "description": "Canonical origin URL (git repo or npm page)." },
118
+ "source": {
119
+ "type": "string",
120
+ "description": "Canonical origin URL (git repo or npm page)."
121
+ },
118
122
  "publisher": { "type": "string", "description": "Vendor / maintainer identity as published." },
119
123
  "license": { "type": "string" }
120
124
  }
125
+ },
126
+ "editors": {
127
+ "type": "array",
128
+ "minItems": 1,
129
+ "description": "Spec 2. Artifact editors this pack ships (DES-artifact-editor-plugins §5.2). Each is ONE self-contained HTML entry inside the pack, pinned by sha256. `pack check` also verifies the file: inside the pack, the hash, the size, and no script/stylesheet/frame loaded from outside it.",
130
+ "items": {
131
+ "type": "object",
132
+ "additionalProperties": false,
133
+ "required": ["id", "title", "version", "protocol", "kinds", "entry", "sha256", "sizes", "permissions"],
134
+ "properties": {
135
+ "id": {
136
+ "type": "string",
137
+ "pattern": "^(?!wicked($|-))[a-z0-9]+(-[a-z0-9]+)*$",
138
+ "maxLength": 64,
139
+ "description": "Unique per install. 'wicked' and 'wicked-*' are refused in every pack: first-party editors ship inside studio."
140
+ },
141
+ "title": {
142
+ "type": "string",
143
+ "minLength": 1,
144
+ "pattern": "\\S",
145
+ "description": "Plain words, shown in Settings → Editors."
146
+ },
147
+ "version": {
148
+ "type": "string",
149
+ "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$",
150
+ "description": "The editor's own SemVer, anchored."
151
+ },
152
+ "protocol": {
153
+ "type": "array",
154
+ "minItems": 1,
155
+ "items": { "type": "integer", "minimum": 1 },
156
+ "description": "wicked.editor protocol versions it speaks."
157
+ },
158
+ "kinds": {
159
+ "type": "array",
160
+ "minItems": 1,
161
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,40}$" },
162
+ "description": "Artifact kinds it can open."
163
+ },
164
+ "entry": {
165
+ "type": "string",
166
+ "pattern": "\\.html$",
167
+ "description": "Relative path of the single HTML file (scripts inline)."
168
+ },
169
+ "sha256": {
170
+ "type": "string",
171
+ "pattern": "^[0-9a-f]{64}$",
172
+ "description": "sha256 of the entry file; pinned at install."
173
+ },
174
+ "sizes": {
175
+ "type": "array",
176
+ "items": {
177
+ "enum": ["inline", "pane", "full"]
178
+ },
179
+ "contains": { "const": "inline" },
180
+ "description": "'inline' is required; a missing 'pane' morphs inline to full."
181
+ },
182
+ "panels": {
183
+ "type": "array",
184
+ "items": {
185
+ "enum": ["checks"]
186
+ },
187
+ "description": "Optional host-fed panels it renders."
188
+ },
189
+ "permissions": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "required": ["id", "why"],
195
+ "properties": {
196
+ "id": {
197
+ "enum": ["artifact.read", "artifact.write", "selection.chip", "composer.draft", "checks.read", "checks.contribute", "sources.read", "media.read", "artifact.export", "ui.fullscreen", "network.media"]
198
+ },
199
+ "why": {
200
+ "type": "string",
201
+ "pattern": "\\S",
202
+ "description": "The reason shown at install."
203
+ }
204
+ }
205
+ },
206
+ "description": "What it asks for (§6.1), each with its reason."
207
+ },
208
+ "limits": {
209
+ "type": "object",
210
+ "additionalProperties": false,
211
+ "properties": {
212
+ "bundleBytes": {
213
+ "type": "integer",
214
+ "minimum": 1,
215
+ "maximum": 5242880,
216
+ "description": "May only be lower than the host's cap (5 MiB)."
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ },
223
+ "blocks": {
224
+ "type": "array",
225
+ "description": "Spec 2. Blocks that PRODUCE this pack's kind (DES-artifact-editor-plugins §9.2): each names a crew preset file inside the pack and the pack skills it uses, approved together at install.",
226
+ "items": {
227
+ "type": "object",
228
+ "additionalProperties": false,
229
+ "required": ["id", "label", "preset", "produces_kind"],
230
+ "properties": {
231
+ "id": {
232
+ "type": "string",
233
+ "pattern": "^(?!wicked($|-))[a-z0-9]+(-[a-z0-9]+)*$",
234
+ "maxLength": 64,
235
+ "description": "'wicked' and 'wicked-*' are refused: first-party blocks are core presets."
236
+ },
237
+ "label": {
238
+ "type": "string",
239
+ "minLength": 1,
240
+ "pattern": "\\S",
241
+ "description": "Plain words the block is offered with."
242
+ },
243
+ "preset": {
244
+ "type": "string",
245
+ "description": "Relative path of the preset JSON inside the pack."
246
+ },
247
+ "produces_kind": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,40}$" },
248
+ "skills": {
249
+ "type": "array",
250
+ "items": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
251
+ "description": "Skills of this pack the block uses."
252
+ }
253
+ }
254
+ }
255
+ }
256
+ },
257
+ "allOf": [
258
+ {
259
+ "if": {
260
+ "properties": {
261
+ "spec": { "const": 1 }
262
+ }
263
+ },
264
+ "then": {
265
+ "required": ["domains"],
266
+ "not": {
267
+ "anyOf": [
268
+ {
269
+ "required": ["editors"]
270
+ },
271
+ {
272
+ "required": ["blocks"]
273
+ }
274
+ ]
275
+ }
276
+ }
277
+ },
278
+ {
279
+ "if": {
280
+ "properties": {
281
+ "spec": { "const": 2 }
282
+ }
283
+ },
284
+ "then": {
285
+ "anyOf": [
286
+ {
287
+ "required": ["domains"]
288
+ },
289
+ {
290
+ "required": ["editors"]
291
+ }
292
+ ]
293
+ }
121
294
  }
122
- }
295
+ ]
123
296
  }
@@ -52,7 +52,11 @@ from datetime import datetime, timezone
52
52
  from pathlib import Path
53
53
 
54
54
  MANIFEST_NAME = "wicked-pack.json"
55
- SPEC_VERSION = 1
55
+ SPEC_VERSION = 1 # the registered-pack file's own format
56
+ # Manifest specs this garden reads. Spec 2 adds ``editors[]`` (artifact editors) and ``blocks[]``
57
+ # (the preset that produces the pack's kind), keeps every spec-1 field, and makes ``domains``
58
+ # optional when ``editors`` is present (DES-artifact-editor-plugins §5.2). Spec-1 packs stay valid.
59
+ MANIFEST_SPECS = (1, 2)
56
60
 
57
61
  # ``wicked`` is the reserved first-party vendor prefix — third-party packs
58
62
  # must never claim it (ruling naming rule #3).
@@ -126,8 +130,22 @@ def structural_errors(manifest: dict, root: Path) -> list:
126
130
  errors: list = []
127
131
 
128
132
  spec = manifest.get("spec")
129
- if spec != SPEC_VERSION:
130
- errors.append(f"spec must be {SPEC_VERSION} (got {spec!r})")
133
+ if spec not in MANIFEST_SPECS or isinstance(spec, bool):
134
+ errors.append(f"spec must be 1 or 2 (got {spec!r})")
135
+ editors = manifest.get("editors")
136
+ if spec == 1 and ("editors" in manifest or "blocks" in manifest):
137
+ errors.append("editors and blocks need manifest spec 2 (this pack declares spec 1)")
138
+ # An editor id that is exactly "wicked" or starts with "wicked-" is refused in EVERY pack, here (not
139
+ # only in `pack check`) so that even a forced registration cannot carry one: first-party editors ship
140
+ # inside studio, never as a pack. As for vendors, a name that merely starts with the letters
141
+ # ("wickedly-terms") is allowed.
142
+ for editor in editors if isinstance(editors, list) else []:
143
+ eid = editor.get("id") if isinstance(editor, dict) else None
144
+ if isinstance(eid, str) and any(eid == r or eid.startswith(r + "-") for r in RESERVED_VENDOR_PREFIXES):
145
+ errors.append(
146
+ f"editor id {eid!r} uses the reserved prefix 'wicked' (first-party editors ship "
147
+ "inside studio, never in a pack)"
148
+ )
131
149
 
132
150
  name = manifest.get("name")
133
151
  vendor = manifest.get("vendor")
@@ -165,15 +183,17 @@ def structural_errors(manifest: dict, root: Path) -> list:
165
183
  or normalized.startswith("/")
166
184
  or bool(re.match(r"^[A-Za-z]:", skills_rel))
167
185
  )
186
+ # Spec 2: an editor-only pack (editors, no domains) may ship no skills at all.
187
+ editor_only = spec == 2 and isinstance(editors, list) and bool(editors) and "domains" not in manifest
168
188
  if escapes_root:
169
189
  errors.append(f"skills_dir must be a relative path inside the pack (got {skills_rel!r})")
170
- else:
190
+ elif not editor_only:
171
191
  skills_dir = Path(root) / skills_rel
172
192
  if not skills_dir.is_dir():
173
193
  errors.append(f"skills dir not found: {skills_dir}")
174
194
 
175
195
  domains = manifest.get("domains")
176
- if not isinstance(domains, list) or not domains:
196
+ if not editor_only and (not isinstance(domains, list) or not domains):
177
197
  errors.append("domains must be a non-empty array")
178
198
 
179
199
  return errors
@@ -0,0 +1,25 @@
1
+ // Part of the wicked-garden-demo skill. Playwright lives in a per-user cache directory, outside the plugin tree:
2
+ // an install inside the plugin leaves node_modules symlinks the garden installer refuses, and a garden upgrade
3
+ // would drop it. Default: <garden cache dir>/demo-deps — the launcher's per-user cache (~/.cache/wicked-garden,
4
+ // %LOCALAPPDATA%\wicked-garden\cache on Windows, or WICKED_GARDEN_CACHE_DIR). Override with WICKED_DEMO_DEPS.
5
+ // Install it once with `wicked-garden run scripts/demo/setup.mjs`.
6
+ import path from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { pathToFileURL } from "node:url";
9
+ import { cacheDir } from "../wicked-garden.mjs";
10
+
11
+ export const DEPS_DIR = path.resolve(
12
+ process.env.WICKED_DEMO_DEPS || path.join(cacheDir(), "demo-deps"),
13
+ );
14
+
15
+ /** Playwright's chromium, loaded from DEPS_DIR on first use. */
16
+ export async function loadChromium() {
17
+ let entry;
18
+ try {
19
+ entry = createRequire(path.join(DEPS_DIR, "package.json")).resolve("playwright");
20
+ } catch {
21
+ throw new Error(`Playwright is not installed in ${DEPS_DIR}; run: wicked-garden run scripts/demo/setup.mjs`);
22
+ }
23
+ const pw = await import(pathToFileURL(entry).href);
24
+ return pw.chromium ?? pw.default.chromium;
25
+ }