wicked-garden 12.60.0 → 12.62.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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +79 -1
  5. package/README.md +9 -0
  6. package/docs/domains.md +13 -1
  7. package/docs/getting-started.md +2 -0
  8. package/package.json +2 -2
  9. package/pyproject.toml +4 -0
  10. package/scripts/domain/_clients.py +133 -6
  11. package/scripts/domain/_mocks.py +3 -2
  12. package/scripts/domain/_rule_extractor.py +14 -5
  13. package/scripts/domain/apply_rules.py +328 -0
  14. package/scripts/domain/batch_extract.py +9 -1
  15. package/scripts/domain/extract_loop.py +784 -141
  16. package/scripts/domain/generation.py +391 -0
  17. package/scripts/domain/next_page.py +147 -0
  18. package/scripts/domain/prepare_worklist.py +178 -0
  19. package/scripts/domain/rules_file.py +410 -0
  20. package/scripts/domain/validate_domain_model.py +17 -11
  21. package/scripts/editor/install.py +11 -3
  22. package/scripts/mcp/install.py +110 -24
  23. package/skills/domain/SKILL.md +75 -12
  24. package/skills/domain/refs/domain-model-emit.md +9 -3
  25. package/skills/domain/refs/extraction-flow.md +9 -7
  26. package/skills/domain/vendor/README.md +8 -7
  27. package/skills/domain/vendor/VERSION +1 -1
  28. package/skills/domain/vendor/domain-model.schema.json +52 -5
  29. package/skills/domain-coverage/SKILL.md +35 -11
  30. package/skills/domain-extractor/SKILL.md +87 -13
  31. package/skills/domain-modeler/SKILL.md +29 -18
  32. package/skills/governed-worker/SKILL.md +40 -4
  33. package/skills/mcp-scaffold/SKILL.md +2 -2
  34. package/skills/repo-learn/SKILL.md +12 -0
  35. package/skills/repo-learn/refs/style-lens.md +54 -0
  36. package/uv.lock +94 -2
@@ -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, multi-model second opinions (fallback seats labelled). 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.60.0"}], "version": "12.60.0"}
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, multi-model second opinions (fallback seats labelled). 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.62.0"}], "version": "12.62.0"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.60.0",
3
+ "version": "12.62.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), multi-model second opinions (fallback seats labelled) (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.8.0",
6
6
  "wicked_ledger_version": "^0.6.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.60.0",
3
+ "version": "12.62.0",
4
4
  "description": "The skills your coding agent can't build alone: \"done\" re-checked from evidence, graph-aware search and multi-file refactor, 40 QE specialists, review panels from other models, and cross-session memory. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
5
5
  "keywords": [
6
6
  "coding-agent",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,82 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.62.0] — 2026-11-01
6
+
7
+ ### Added
8
+
9
+ - **Every seat proposes a memory for what it witnessed (governed-worker A7).** On the rig, every memory in the review queue came from the pi seat. That was the seat seated first for a capture-learnings run's capture phase, or the seat that distils a finished run. The claude and codex seats that did the work in bug and feature runs proposed none, because the floor every seat follows had no rule asking them to. A7 gives every role a fixed moment to check (before its final message; in power mode, before each `CHECKPOINT:` or `HANDOFF:` line) and asks it to propose what bit for a reason the code does not show: a command that failed because of the toolchain, the environment or the setup and what made it pass, a tool trap, or an operator ruling. A reviewer's finding with such a cause counts too. At most three per unit go through the one-line `scripts/mem/estate_memory.py --readonly propose` path the shim already relays from a container seat. The run, the unit and the seat are stamped as provenance, so the proposal names the seat that saw it. A unit that proposes none says what it checked. Code facts stay knowledge. The `wicked-capture-report` line is now a capture-phase contract only (A4), so an evaluator's VERDICT line stays last.
10
+ - **A domain-extraction run keeps mapping page after page until the target (pairs with wicked-core's bounded loop).** The new host step `extract-next` (`scripts/domain/next_page.py`) runs after each page's `extract-apply`: it measures the mapped share against the repository's target (`wicked-core requirements-target`: the repository's own, else the daemon minimum), prints one progress line ("Mapping wicked-crew · 3% → 12% → 19% · target 80% · page 4 · about 2 h left") and decides: another page, done, or an ask when the budget per grant is spent (`WICKED_DOMAIN_MAX_PAGES` 20, `WICKED_DOMAIN_MAX_MINUTES` 240), nothing is left to map, or two pages mapped nothing new. The ask's choices: map more (a fresh grant), or lower the target for this repository to the share mapped so far and build the graph; Stop ends the run and keeps what was mapped. (As released, the loop counts REAL requirements, see Changed: the stall ask comes after two pages that add no real requirement, "map more" is offered only while units are left to ask, "lower the minimum" only when the real share is at least 1%. With nothing left to ask and nothing to lower to, the run goes on to coverage, except after a low-yield last page, whose ask offers Finish.) The generation's record (`scripts/domain/generation.py`: its start, a point per page, the grant) lives beside the store, never in the seat-writable worktree. A page is 500 behaviours by default (`WICKED_DOMAIN_PAGE_SIZE`).
11
+
12
+ - **`wicked-garden-repo-learn` has a style lens, and writing is a source.** Studio's Learn → Style (program DES-LEARN §3.1) runs `capture-learnings` on a showcase repository to learn how the person or team works, not only how the code works. When the run asks for style, phase 2 also reads naming, structure, test shape, change size, and commit and review tone, each with a stated cap (`git log -n 200 --shortstat`, `-n 100 --format=%s`, 3 to 5 reviews when `gh` is signed in). Phase 3 captures those as style memories (`reach: "operator"`, so the person decides at review whether they apply everywhere). An enforced convention stays a policy and what the code does stays knowledge. The same lens covers writing (a blog, a docs site, a document): a sample of at most 10 pages is read, the patterns are found, and they're captured as memories, because the person's own voice is told, not acquired. `refs/style-lens.md`.
13
+
14
+ ### Changed
15
+
16
+ - **The domain skills and docs describe the loop as it now runs (#1380).** `domain`, `domain-extractor`,
17
+ `domain-coverage`, `domain-modeler` and their refs, `docs/domains.md`, `docs/getting-started.md` and the README say
18
+ real requirements and the configured minimum, the page loop, and the current worker names. They say the harness
19
+ checks a rule's shape and that its source is readable product code, while grounding is judged by the page review
20
+ and the coverage evaluator. The README's paragraph under the bold line says what garden does, and where (#1383).
21
+ - **A domain-extraction run maps until REAL requirements reach the target (pairs with wicked-core#1233).** The target
22
+ counts resolved behaviours only; risk-flagged placeholders are shown, never counted.
23
+ - `extract-prepare` frames the unmapped behaviours, then the placeholders an earlier page left (empty, or under
24
+ the confidence threshold), hub-first. It skips any unit an earlier page of the same run closed: one the seat
25
+ answered and the host wrote or kept, recorded in the generation's record beside the store, so a unit left empty
26
+ again cannot loop the run. A unit the seat never reached (its turn ran out) goes back to the pool for the next
27
+ page. `extract-apply` accepts entries for placeholders; an empty answer never overwrites a statement.
28
+ - `extract-next` measures the real share: "Mapping wicked-crew · real requirements 23% → 25% · target 80% · page
29
+ 2 · 4331 placeholders · last page resolved 93 of 100 units · about 3 h left". A page of 20 or more units that
30
+ resolved under 1 in 20 asks the operator instead of mapping on (wicked-crew run 4b49cbe1 passed a page with 3
31
+ rules for 500 units). The stall ask is now "two pages added no real requirement".
32
+ - The extractor's file-mode instructions and the rule prompt say most product code encodes a rule (a check, a
33
+ refusal, a limit, a default, an ordering, a permission, a state change, a required field) and to leave a
34
+ statement empty for pure plumbing, and (as the Fixed entry below says) for a plain interface or field list that
35
+ encodes no business constraint. On the same 100 wicked-crew units (27 unmapped, 73 placeholders, all
36
+ sourced), codex resolved 85 with the old instructions and 93 with the new.
37
+ - The vendored domain-model schema is bundle 1.3.0 (`metadata.coverage` is `real_pct`, `real`, `placeholders`,
38
+ `unmapped`, `not_product`).
39
+
40
+ - **Domain extraction maps hub-first and stops gating on 100% (operator ruling 2026-10-31; pairs with wicked-core#1215).**
41
+ - *Hub-first, fan-out order.* `scripts/domain/extract_loop.py` takes the unmapped worklist in graph order: the most central symbols first (estate's PageRank via `wicked-estate rank`, `CliEstateClient.rank`; the store's own degree when the command is unavailable, said on stderr), then their neighbours and onward by hop distance over the use graph (calls, references, type relations, a route serving a handler; never contains/imports/data flow). The hub count scales with the graph (about √n/4, 1..25). The order is deterministic, so a re-invoked pass or a later run re-derives it and resumes at the frontier (each batch logs `frontier: <symbol> (hop H, position P of T)`); `--cluster-offset N` is now this worker's index among `--workers` (default offset + 1): with the hub-first order, worker k of N takes only stripe windows w with w % N == k, so workers given the same `--workers` do not pick the same window; a lone worker owns every window, and without the store's order (the flat fallback) there is no striping.
42
+ - *Fail-closed below the configured minimum.* The domain skills (`domain`, `domain-extractor`, `domain-coverage`, `domain-modeler`, refs) say the graph is built once the share reaches the configured minimum (as released, the REAL share: risk-flagged placeholders are mapped but never counted, see Changed) (crew's `requirementsGraph.minCoveragePct`, `WICKED_REQUIREMENTS_MIN_COVERAGE_PCT`, default 80), not 1.0; below it the run says "N% mapped, minimum M%" and keeps the mapping. The coverage evaluator runs `wicked-core coverage … --min-coverage-pct` and its verdict follows that check; holes above the minimum are NOTEs, the next run's frontier.
43
+ - *Schema re-vendored at domain-model 1.1.0 (bundle 1.2.3).* The optional `metadata.coverage` and `requirement.corroboration` core now emits (validated by two or more independent sources, or unconfirmed with why). `validate_domain_model.py` accepts every version the schema's `schema_version` enum lists (1.0.0 and 1.1.0); garden's own emitter still writes 1.0.0.
44
+
45
+ ### Fixed
46
+
47
+ - **The domain loop's line says "minimum", as studio does, and prints counts as "2,174".** The progress line, the
48
+ repeat gate's ask and its choices read "target 80% · 2174 placeholders" while studio's requirements view beside them
49
+ read "minimum 80% · 2,174 placeholders". `extract-next` now says "minimum 80%", "minimum out of reach in this run",
50
+ "Lower the minimum to N%" and "minimum lowered to N%", and every count it prints carries thousands separators.
51
+ - **No time estimate for a target the run cannot reach.** On wicked-crew run 29bbbfb1 the line read "about 3 h 37 min
52
+ left" with about 2,000 more real requirements needed and about 1,900 units left to ask. When the units left to ask
53
+ are fewer than the real requirements still needed, the line now says "target out of reach in this run: N more real
54
+ requirements needed, M units left to ask" in place of an ETA; with none left, no ETA at all.
55
+ - **The extractor no longer states a rule for a plain interface (follow-up to #1375).** The file-mode instructions
56
+ said "an interface or type states what a request or record must hold". On wicked-crew run 29bbbfb1, page 3 (500
57
+ units), codex stated 441 rules, all at confidence 0.9, and most of them restated interface fields. The page review
58
+ failed it: they describe code. A type is now a rule only when it encodes a constraint the business relies on (a
59
+ closed set of values, a limit, a required field whose comment says what it means). A plain field list or any
60
+ restated shape is left empty. A constraint visible in the unit's definition, whether in its body or its type,
61
+ is high confidence; a rule guessed from a name, a comment alone or a cut-off slice is low. Each confidence is
62
+ judged on its own and never stamped across the page. The bounded rule model's prompt says the same.
63
+ - **The seat's reviewed confidence decides whether a rule resolves, never its Louvain community.** On the same run,
64
+ page 3 restated 230 grounded rules at 0.80 to 0.84, and the page review passed them. Apply resolved only 42.
65
+ A unit with no community took a 0.85 penalty (0.84 x 0.85 < 0.75), while a community boost of up to 1.1 raised
66
+ rules the seat was unsure of. Communities still frame a unit on the worklist; `extract-apply` no longer reads
67
+ them.
68
+
69
+ - **Hub-first extraction starts at the product's code, not builtin-name collisions (#1366).** On a TypeScript store, `s.startsWith(...)` or `map.get(k)` resolves to any local symbol of that name, so a one-line helper collected thousands of callers and became the first hub. A JS/TS symbol named like a builtin method, or a symbol in a test or fixture file, never seeds the fan-out; a collision with callers in 8 or more files is not fanned out from; tests and fixtures come after the product's code in the worklist.
70
+ - **Communities are read when estate warns that the index is stale.** estate prints `STALENESS: …` before its JSON, so `clusters` failed to parse and every node framed as a singleton community; the cohesion penalty then pushed most stated rules under the resolve threshold (rig run 7588cff1: 99 resolved, 201 risk-flagged out of 300). The document now starts at the first line that opens one.
71
+ - **`extract-apply` reads each write back with one query** instead of a third estate process per rule (30 rules: 35 s → 27.6 s on a copy of wicked-crew's store).
72
+ - **The domain extraction harness can write against estate 0.30 (#1361).** estate >= 0.30 refuses `semantics --validated` without `--validated-by`, so `extract_loop.py`'s first write (a RESOLVED or a RISK node) exited with a usage error and the pass aborted before mapping anything; a `domain-extraction` run could never reach its coverage step. `CliEstateClient.set_requirement` now names its validator, and the harness's writes name the seat: `extract-loop:<WICKED_RUN_AGENT>` as the semantics validator and the business-rule annotation's `--author`, which is what wicked-core's corroboration counts as an extractor seat. That holds for the in-process loop (`extract_loop.py`). Since #1363 the default file-mode path applies on the host as `extract-loop`: the seat named in the rules file is recorded only as `declared_seat` in the apply report, never as the author, so those writes carry no independent seat attribution for corroboration. Proven against the real estate 0.30.0 and wicked-core main on a copy of tiny-math's store: 8 of 8 mapped hub-first, `coverage --min-coverage-pct` passes, `domain-graph --repo-root` writes the graph (sum/mean/clamp validated by callers, tests and the README; the package.json keys unconfirmed, `config_only`).
73
+
74
+ ## [12.61.0] — 2026-10-26
75
+
76
+ ### Fixed
77
+ - **`install.py --from-run` finds the pack in a real run (editor and MCP) (#1351).** The engine runs the install-plan and install Tool phases in the run's worktree and sets `WICKED_TREE` to that worktree's git tree id (wicked-core#776), not a path. Both install scripts read it as a directory, found nothing, and every editor-plugin run failed at install-plan with "found 0 (none)", so the install consent gate was never reached. `--from-run` now searches the worktree (the cwd) and uses `WICKED_TREE` only when it names a directory.
78
+ - **A credential-free MCP server installs (`scripts/mcp/install.py`) (#1351).** The installer assumed every server reads a `<SERVER>_TOKEN` secret. A local-computation server has no upstream, and its config names no `baseUrl` and no auth scheme (`auth` absent, null or `{"scheme": "none"}`). The installer still sent `auth: env:<SERVER>_TOKEN`, crew answered `secret_missing`, and the server was never registered, so a design that correctly invented no credential could not be installed. Such a server now smokes and registers with no secret variable. A server with an upstream, and `--from-npm` installs, are unchanged.
79
+ - **An MCP server delivered without a `package-lock.json` installs (`scripts/mcp/install.py`) (#1352).** A container seat can't reach the npm registry, so the server a `/workflow-mcp-server` run delivers has no lockfile. `npm ci` refuses such a tree, so the install failed after the operator had consented, with only npm's debug-log path as the reason. The installer now copies the server to a transient `<root>/<key>/resolve/`, resolves the dependencies there once with `npm install --package-lock=true --ignore-scripts`, and builds there. The staged copy installs from that lock, and the delivered tree is never written. An npm that writes no lock (`package-lock=false`) fails before anything is built. The plan and the install record say `"lockfile": "resolved at install"`, because those versions were picked at install time and the run never reviewed them. A delivered lock still goes through `npm ci`, and the output says `"lockfile": "delivered"`. A failing npm step now reports npm's own reason instead of its debug-log path. That is the `npm error code` line and the first line after it that isn't metadata, plus the script's own last line (tsc's error) when `npm run build` fails.
80
+
5
81
  ## [12.60.0] — 2026-10-26
6
82
 
7
83
  ### Fixed
@@ -1212,7 +1288,9 @@ through `v9.2.18`. Major themes across that span:
1212
1288
 
1213
1289
  For each of those releases, see the corresponding tag in git history.
1214
1290
 
1215
- [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.60.0...HEAD
1291
+ [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.62.0...HEAD
1292
+ [12.62.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.61.0...v12.62.0
1293
+ [12.61.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.60.0...v12.61.0
1216
1294
  [12.60.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.59.0...v12.60.0
1217
1295
  [12.59.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.58.0...v12.59.0
1218
1296
  [12.58.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.57.0...v12.58.0
package/README.md CHANGED
@@ -11,6 +11,14 @@
11
11
 
12
12
  **Your coding agent already plans and swarms. wicked-garden is the curated toolkit for what it can't do alone.**
13
13
 
14
+ It works inside Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or inside a wicked-crew job. Its proof gate
15
+ re-checks "done" from evidence, rejecting unsupported claims and failing closed by default when it cannot run. With
16
+ wicked-estate's code graph it sees the event and command links grep never will, and its patch skill renames across files
17
+ as a graph operation. Its 40 QE specialist skills include an acceptance pipeline whose reviewer never runs inline with the
18
+ test writer or the executor (those two also run as separate workers where the host can start them), and a disputed
19
+ decision goes to a review panel of your other agents, with a labelled same-family fallback and no verdict without a quorum. Which entry paths block and which only advise is spelled out in
20
+ [What blocks, what advises](#what-blocks-what-advises).
21
+
14
22
  > 📖 Docs → [wg.wickedagile.com](https://wg.wickedagile.com). Identity & beliefs → [`ETHOS.md`](ETHOS.md). How it works → [`CLAUDE.md`](.claude/CLAUDE.md).
15
23
 
16
24
  ---
@@ -34,6 +42,7 @@ Most plugins try to boss them around — re-implement planning, impose a workflo
34
42
  | re-derives *how to work in this repo* every task — which file owns the bug, the wiring step, the test command | loads the repo's own playbooks (`fix-bug`/`add-feature`/`verify`…), generated from HEAD → wicked-understanding |
35
43
  | asks *itself* for a second opinion | convenes a **review panel** → the jam skill's `council` action: usable external CLIs (Antigravity / Codex / …) take the seats; with fewer than two it fills them with isolated same-family workers and **labels them as weaker diversity**, and below two isolated seats it reports *no quorum* instead of synthesizing |
36
44
  | re-derives WCAG/CWE/SOC2 from memory every time | loads the rubric on demand, ships it to any repo |
45
+ | reads the code, but can't say what it *requires* | states the business rule behind each behaviour onto estate's graph, so wicked-core builds a **requirements graph** that counts only stated rules toward its minimum, never placeholders → the domain skill |
37
46
  | grades its own homework | author ≠ executor ≠ reviewer as separate workers → evidence-gated testing (the `qe` domain) |
38
47
 
39
48
  The throughline: **done is re-derived, not asserted** — wherever the gate actually runs. Which entry paths block and which only advise is spelled out in [What blocks, what advises](#what-blocks-what-advises).
package/docs/domains.md CHANGED
@@ -6,7 +6,7 @@ Work is shaped by the **archetypes** (triage, explore, specify, decide, ship, re
6
6
 
7
7
  The plugin is **skills-only** (v12.25). Each domain is one consolidated, user-invokable skill — `skills/{domain}/SKILL.md`, frontmatter name `wicked-garden-{domain}` — that routes to its **actions** (the former `/wicked-garden:{domain}:{command}` surfaces) and loads rubrics on demand from `skills/{domain}/refs/` and nested knowledge modules. The former dispatch agents are **fork worker skills** — `skills/{domain}-{role}/SKILL.md` with `context: fork`, frontmatter name `wicked-garden-{domain}-{role}` — reached by `Skill()`/`Task()` dispatch, never invoked as entry points. Invoke a domain action as `/wicked-garden-{domain} {action} [args]` (or just describe what you want — the skill descriptions carry the triggers).
8
8
 
9
- There are **8 domains**: engineering, platform, product, data, jam, search, agentic, persona.
9
+ There are **9 domains** here: engineering, platform, product, data, jam, search, agentic, persona, domain. The `qe` (evidence-gated testing) and `mem` (memory over wicked-estate) domains are covered in the [README](../README.md).
10
10
 
11
11
  > **v12 cleanup (ADR 0002).** Most former domain commands were *rubric-wrappers* — a checklist the agent already applies. These were **collapsed**: the rubric moved to an on-demand `skills/{domain}/refs/{name}.md` and the action now loads it and works **inline** (no `Task` dispatch hop). Dispatch is kept only where it earns it — real parallelism (multiple lenses at once), a real external tool, or an independent gate. The dispatch-only agents that nothing reaches anymore were removed; workers still referenced by a surviving action, skill, scenario, or the specialist registry live on as fork skills. Capability is preserved; only the token-burning hop is gone.
12
12
 
@@ -161,6 +161,18 @@ They do not overlap; pick by target:
161
161
 
162
162
  **Worker**: none — the former `wicked-garden-persona-agent` worker is retired into `refs/as.md` (wave-2 B9): the `as` action builds the profile from the registry at runtime and embodies it inline. `define` runs inline from `refs/define.md`; the no-op `persona:submit` stub was deleted.
163
163
 
164
+ ## domain — The Domain Model and the Requirements Graph
165
+
166
+ `wicked-garden-domain` — extracts a codebase's domain (testable business rules with a confidence and a provenance, entities, requirements) onto the estate graph, so wicked-core can build the repository's **requirements graph**. A shared substrate: `modernize` derives from it, and build, migrate, review, specify and explore read the same model.
167
+
168
+ | Worker | What It Does |
169
+ |--------|-------------|
170
+ | `wicked-garden-domain-extractor` | States one business rule per behaviour (hub-first, the most central symbols first) and annotates the estate store |
171
+ | `wicked-garden-domain-modeler` | Groups the rules into domains and invokes `wicked-core domain-graph`, which saves the graph into the estate store |
172
+ | `wicked-garden-domain-coverage` | Checks the share against the minimum and threat-models the extracted model before the build (ungrounded rules, reason-less drops, holes) |
173
+
174
+ Only **real requirements** count toward the minimum (80% by default; a repository can keep its own with `wicked-core requirements-target`): behaviours with a business rule at or above the confidence threshold, or a validated requirement. A placeholder (a risk flag, or a rule under the threshold) is shown and never counted. Test, spec and fixture files are left out by path, and so are data and document files, except an extracted rules-engine rule. The extractor checks each rule's shape and source before writing it; the page's review and the coverage evaluator judge whether it is grounded. In wicked-crew, a `domain-extraction` run maps a repository page by page until real requirements reach the minimum, printing one line per page ("Mapping wicked-crew · real requirements 3% → 12% → 19% · minimum 80% · page 4 · 2,174 placeholders · about 2 h left"), and asks the operator when its budget is spent or the pages stall: map more, lower the minimum for this repository, or stop.
175
+
164
176
  ## How archetypes invoke domains
165
177
 
166
178
  An archetype playbook (`skills/archetype/refs/{archetype}.md`) doesn't hardcode a domain pipeline — it reaches for whatever expertise the work needs:
@@ -103,6 +103,8 @@ Skill(skill="wicked-garden-mem", args="store \"Chose Postgres over Redis for ses
103
103
  Skill(skill="wicked-garden-mem", args="recall \"session storage decisions\"")
104
104
  ```
105
105
 
106
+ Inside a wicked-crew governed run, every seat also proposes what it witnessed and the code doesn't show (a command that failed because of the toolchain and what made it pass, a tool trap, an operator ruling), at most three per unit. Proposals land inert in the review queue until a person accepts them; the run, the unit and the seat are stamped on each (rule A7 of the `wicked-garden-governed-worker` skill).
107
+
106
108
  Search the same knowledge layer instead of grep — the `wicked-garden-search` skill gives structural code intelligence, lineage, and blast radius over the estate graph.
107
109
 
108
110
  ## Common Workflows
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.60.0",
3
+ "version": "12.62.0",
4
4
  "type": "module",
5
- "description": "The skills your coding agent can't build alone: \"done\" re-checked from evidence, graph-aware search and multi-file refactor, 40 QE specialists, review panels from other models, and cross-session memory. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
5
+ "description": "The skills your coding agent can't build alone: a proof gate that re-checks \"done\" from evidence, graph-aware search and cross-file renames (with wicked-estate), 40 QE specialists, and review panels with labelled fallbacks. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
6
6
  "keywords": [
7
7
  "ai",
8
8
  "coding-agent",
package/pyproject.toml CHANGED
@@ -19,6 +19,10 @@ dependencies = [
19
19
  "pathspec>=0.11.0",
20
20
  "pyyaml>=6.0",
21
21
  "flask>=3.0",
22
+ # The code-graph store keeps file content zstd-compressed; domain extraction slices each
23
+ # behaviour's source out of it (wicked-garden#1370). Python 3.14+ decodes it in the stdlib
24
+ # (`compression.zstd`); before that, without this, every source fell back to a checkout.
25
+ "zstandard>=0.22; python_version < '3.14'",
22
26
  ]
23
27
 
24
28
  [project.optional-dependencies]
@@ -135,6 +135,18 @@ def _run_json(argv: list[str]) -> Any:
135
135
  try:
136
136
  return json.loads(out)
137
137
  except json.JSONDecodeError as e:
138
+ # estate prints advisory lines BEFORE its JSON on stdout ("STALENESS: 1 commit(s) since
139
+ # last index — run `wicked-estate index …`"): the document starts at the first line that
140
+ # opens one. Read without that, `clusters` failed and every node framed as a singleton
141
+ # community, which pulled each rule's confidence under the resolve threshold.
142
+ lines = out.splitlines()
143
+ for start in range(1, len(lines)):
144
+ if lines[start].lstrip()[:1] not in ("[", "{"):
145
+ continue
146
+ try:
147
+ return json.loads("\n".join(lines[start:]))
148
+ except json.JSONDecodeError:
149
+ continue # an advisory line that itself opens with a bracket: keep looking
138
150
  raise RuntimeError(f"{argv[0]} output is not JSON: {e}: {out[:200]!r}") from e
139
151
 
140
152
 
@@ -230,6 +242,19 @@ class CliEstateClient:
230
242
  )
231
243
  return data
232
244
 
245
+ def rank(self, limit: int = 200) -> list[str]:
246
+ """The store's most central symbols, most central first — estate's PageRank over
247
+ Calls+Imports (`rank --json`, the CLI twin of the `RankHotspots` MCP tool the search
248
+ domain's hotspots use). The tool clamps `limit` to its own ceiling. Fail-loud on a
249
+ shape break; callers that can do without centrality catch and fall back."""
250
+ data = _run_json(self._argv("rank", "--limit", str(int(limit)), "--json"))
251
+ hot = data.get("hotspots") if isinstance(data, dict) else None
252
+ if not isinstance(hot, list):
253
+ raise RuntimeError(
254
+ "wicked-estate rank returned no `hotspots` array — expected {hotspots:[{symbol,…}]}"
255
+ )
256
+ return [h["symbol"] for h in hot if isinstance(h, dict) and isinstance(h.get("symbol"), str)]
257
+
233
258
  def source(self, name_or_symbol: str) -> str:
234
259
  """The source slice(s) for a symbol (`source <name>`) — the untrusted DATA
235
260
  the model reads to state a rule. Fail-loud (empty ⇒ no slice to reason over)."""
@@ -255,7 +280,7 @@ class CliEstateClient:
255
280
 
256
281
  def annotate(self, symbol_id: str, type: str, key: str, value: str,
257
282
  confidence: float | None = None, provenance: str | None = None,
258
- replace: bool = True) -> None:
283
+ replace: bool = True, author: str | None = None) -> None:
259
284
  if not _looks_like_symbol_id(symbol_id):
260
285
  raise ValueError(
261
286
  f"refusing annotate on non-SymbolId {symbol_id!r} — a bare name fans "
@@ -267,20 +292,28 @@ class CliEstateClient:
267
292
  argv += ["--confidence", repr(float(confidence))]
268
293
  if provenance is not None:
269
294
  argv += ["--provenance", provenance]
295
+ if author:
296
+ # The seat that stated it: wicked-core's corroboration counts extractor SEATS by
297
+ # annotation author (never provenance), so two seats agreeing read as two sources.
298
+ argv += ["--author", author]
270
299
  if replace:
271
300
  argv.append("--replace") # else the CLI APPENDs (stacks duplicates)
272
301
  _run(argv)
273
302
 
274
303
  def set_requirement(self, symbol_id: str, requirement: str,
275
- validated: bool) -> None:
304
+ validated: bool, validated_by: str = "extract-loop") -> None:
276
305
  if not _looks_like_symbol_id(symbol_id):
277
306
  raise ValueError(
278
307
  f"refusing set_requirement on non-SymbolId {symbol_id!r} — a bare "
279
308
  "name is a silent no-op in estate; resolve() first"
280
309
  )
281
- # node_semantics (requirement/validated), a SEPARATE subcommand from annotate.
310
+ # node_semantics (requirement/validated), a SEPARATE subcommand from annotate. estate
311
+ # >= 0.30 refuses `--validated` without `--validated-by` (who made the claim), so every
312
+ # write names its validator — a usage error here used to abort the whole pass on its first
313
+ # node.
282
314
  _run(self._argv("semantics", symbol_id, "--requirement", requirement,
283
- "--validated", "true" if validated else "false"))
315
+ "--validated", "true" if validated else "false",
316
+ "--validated-by", validated_by or "extract-loop"))
284
317
 
285
318
  def read_annotations(self, symbol_id: str) -> list[dict[str, Any]]:
286
319
  if not _looks_like_symbol_id(symbol_id):
@@ -339,6 +372,34 @@ class CliCoreClient:
339
372
  _run([*self._bin, "coverage", "--db", db, "--out", out])
340
373
  return _read_output_json(out, "wicked-core coverage")
341
374
 
375
+ def coverage_against_minimum(self, db: str, out: str) -> tuple[dict[str, Any], bool, str]:
376
+ """Emit the coverage report to ``out`` checked against the CONFIGURED minimum
377
+ (``--min-coverage-pct``: ``WICKED_REQUIREMENTS_MIN_COVERAGE_PCT``, else 80). Returns
378
+ (report, at_or_above_minimum, the CLI's "N% real requirements, minimum M%" line; "N%
379
+ mapped" from an older wicked-core). Below the minimum is an answer (exit 3), not an
380
+ error; any other non-zero exit fails loud."""
381
+ proc = _invoke([*self._bin, "coverage", "--db", db, "--out", out, "--min-coverage-pct"])
382
+ if proc.returncode not in (0, 3):
383
+ raise RuntimeError(f"{self._bin[0]} coverage exited {proc.returncode}: "
384
+ f"{(proc.stderr or proc.stdout).strip()[:300]}")
385
+ lines = [ln.strip() for ln in (proc.stderr or "").splitlines()
386
+ if "real requirements" in ln or "mapped" in ln]
387
+ return _read_output_json(out, "wicked-core coverage"), proc.returncode == 0, \
388
+ (lines[-1] if lines else "")
389
+
390
+ def requirements_target(self, db: str, set_pct: int | None = None) -> tuple[int, str]:
391
+ """The coverage target this repository's graph is held to and where it comes from
392
+ (``"repository"``: its own, kept in its store; ``"daemon"``: the configured minimum).
393
+ ``set_pct`` WRITES the repository's own target first. Fails loud."""
394
+ argv = [*self._bin, "requirements-target", "--db", db, "--json"]
395
+ if set_pct is not None:
396
+ argv += ["--set", str(int(set_pct))]
397
+ doc = _run_json(argv)
398
+ pct = doc.get("min_coverage_pct") if isinstance(doc, dict) else None
399
+ if not isinstance(pct, int) or isinstance(pct, bool) or not 1 <= pct <= 100:
400
+ raise RuntimeError(f"wicked-core requirements-target answered no target: {doc!r}")
401
+ return pct, str(doc.get("source") or "")
402
+
342
403
  def domain_graph(self, db: str, out: str,
343
404
  coverage: str | None = None) -> dict[str, Any]:
344
405
  """Build ``requirements_graph.json`` from the annotated store and return
@@ -373,12 +434,24 @@ def unaccounted_nodes(coverage_doc: dict[str, Any]) -> list[dict[str, Any]]:
373
434
  return nodes
374
435
 
375
436
 
437
+ def placeholder_nodes(coverage_doc: dict[str, Any]) -> list[dict[str, Any]]:
438
+ """The coverage report's risk-flagged placeholders (accounted, not real requirements: an
439
+ empty statement, a rule under the confidence threshold, or one written unconfirmed), each
440
+ {symbol_id,name,kind,file,…}: what a page may revisit to raise the REAL share
441
+ (wicked-core#1233). Optional on the wire (an older wicked-core lists none), so absent is [];
442
+ present and not a list of objects fails loud."""
443
+ nodes = coverage_doc.get("placeholder_nodes", [])
444
+ if not isinstance(nodes, list) or not all(isinstance(n, dict) for n in nodes):
445
+ raise RuntimeError("`placeholder_nodes` is not a list of nodes")
446
+ return nodes
447
+
448
+
376
449
  def total_node_community(clusters: list[dict[str, Any]],
377
450
  all_nodes: list[dict[str, Any]]) -> dict[str, str]:
378
451
  """Port of anti-legacy `_cluster_from_native`: map EVERY node → a community
379
452
  label — Louvain members keep their community, and every node the engine omitted
380
453
  is SINGLETON-assigned its own symbol_id as label, so ``node_community`` is TOTAL.
381
- Feeds the cohesion framing signal ONLY (resolved-vs-RISK quality), NOT the
454
+ Feeds the community label a worklist unit is framed with ONLY (never its verdict), NOT the
382
455
  coverage denominator (which `unaccounted_nodes` owns)."""
383
456
  node_community: dict[str, str] = {}
384
457
  for comm in clusters:
@@ -426,9 +499,63 @@ def estate_client(db: Optional[str] = None,
426
499
  return EstateClient()
427
500
 
428
501
 
502
+ # The engine's own CLI as wicked-crew resolved it (crew exports it to the daemon; the engine's
503
+ # Tool steps inherit it). It matches the running engine, so it wins over config and PATH.
504
+ ENGINE_CORE_ENV = "WICKED_CORE_EXE"
505
+
506
+
507
+ def _resolve_trusted(env_vars: tuple[str, ...], package: str) -> Optional[list[str]]:
508
+ """The HOST steps' resolver (`prepare_worklist.py`, `apply_rules.py`): they run as the daemon
509
+ user in a worktree a seat can write, so a binary may come only from the operator and the
510
+ engine — the env (set-but-empty = the kill-switch), config, or PATH — never from the working
511
+ directory (no `node_modules/.bin` fallback), and every answer is pinned to the real path of
512
+ the file that will run (`rules_file.trusted_executable`)."""
513
+ from domain import rules_file # stdlib-only sibling; imported late to keep this module light
514
+ choice: Optional[str] = None
515
+ for env_var in env_vars:
516
+ if env_var in os.environ:
517
+ choice = os.environ[env_var].strip()
518
+ if not choice:
519
+ return None
520
+ break
521
+ if choice is None:
522
+ choice = _config_preference(package) or package
523
+ pinned = rules_file.trusted_executable(choice)
524
+ return [pinned] if pinned else None
525
+
526
+
527
+ def host_estate_client(db: str) -> "CliEstateClient":
528
+ """The real estate client for a host step, from a trusted binary only (fails loud)."""
529
+ bin_argv = _resolve_trusted((ESTATE_ENV,), "wicked-estate")
530
+ if not bin_argv:
531
+ raise RuntimeError("wicked-estate is not resolvable on this host (WICKED_ESTATE_BIN or "
532
+ "PATH)")
533
+ return CliEstateClient(bin_argv, db)
534
+
535
+
536
+ def host_core_client() -> "CliCoreClient":
537
+ """The real core client for a host step: WICKED_CORE_BIN, else the engine's WICKED_CORE_EXE,
538
+ else config or PATH — never the working directory (fails loud)."""
539
+ if CORE_ENV in os.environ:
540
+ bin_argv = _resolve_trusted((CORE_ENV,), "wicked-core")
541
+ else:
542
+ bin_argv = _resolve_trusted((ENGINE_CORE_ENV,), "wicked-core") \
543
+ if (os.environ.get(ENGINE_CORE_ENV) or "").strip() \
544
+ else _resolve_trusted((), "wicked-core")
545
+ if not bin_argv:
546
+ raise RuntimeError("wicked-core is not resolvable on this host (WICKED_CORE_BIN, "
547
+ "WICKED_CORE_EXE or PATH)")
548
+ return CliCoreClient(bin_argv)
549
+
550
+
429
551
  def core_client(project_dir: Optional[Path] = None) -> Optional[CliCoreClient]:
430
552
  """A CLI-backed :class:`CliCoreClient` when ``wicked-core`` resolves, else
431
553
  ``None`` (the caller falls back to the hermetic ``_mocks.BrainClient`` +
432
- ``emit_domain_model`` doc-assembly lane)."""
554
+ ``emit_domain_model`` doc-assembly lane). ``WICKED_CORE_BIN`` (set-but-empty = the
555
+ kill-switch) wins; then the engine's ``WICKED_CORE_EXE``; then config, PATH, node_modules."""
556
+ if CORE_ENV not in os.environ:
557
+ engine = (os.environ.get(ENGINE_CORE_ENV) or "").strip()
558
+ if engine:
559
+ return CliCoreClient([engine])
433
560
  bin_argv = _resolve_bin(CORE_ENV, "wicked-core", project_dir)
434
561
  return CliCoreClient(bin_argv) if bin_argv else None
@@ -89,7 +89,7 @@ class EstateClient:
89
89
 
90
90
  def annotate(self, symbol_id: str, type: str, key: str, value: str,
91
91
  confidence: float | None = None, provenance: str | None = None,
92
- replace: bool = True) -> None:
92
+ replace: bool = True, author: str | None = None) -> None:
93
93
  if not _looks_like_symbol_id(symbol_id):
94
94
  raise ValueError(
95
95
  f"refusing annotate on non-SymbolId {symbol_id!r} — a bare name "
@@ -98,10 +98,11 @@ class EstateClient:
98
98
  self.annotation_writes.append({
99
99
  "symbol_id": symbol_id, "type": type, "key": key, "value": value,
100
100
  "confidence": confidence, "provenance": provenance, "replace": replace,
101
+ "author": author,
101
102
  })
102
103
 
103
104
  def set_requirement(self, symbol_id: str, requirement: str,
104
- validated: bool) -> None:
105
+ validated: bool, validated_by: str = "extract-loop") -> None:
105
106
  if not _looks_like_symbol_id(symbol_id):
106
107
  raise ValueError(
107
108
  f"refusing set_requirement on non-SymbolId {symbol_id!r} — a bare "
@@ -36,8 +36,15 @@ _PROMPT_HEADER = (
36
36
  'or \\"\\" if you cannot state it safely>", "confidence": <number 0..1>, '
37
37
  '"provenance": {"source": "<repo/module>", "ref": "<the symbol_id>", '
38
38
  '"source_kinds": ["code-body"|"type-def"|"comment"|"doc", ...]}}\n'
39
- "If a unit’s behavior is unclear, set statement to \"\" and confidence low — do "
40
- "NOT guess. Ground trusted rules in code-body/type-def, not comment/doc alone.\n"
39
+ "Most product code encodes a rule: a check, a refusal, a limit, a default, an "
40
+ "ordering, a permission or a state change is a rule even when it is small, so state "
41
+ "it. A type or interface is a rule only when it encodes a constraint the business "
42
+ "relies on (a closed set of values, a limit, a required field whose comment says what "
43
+ "it means); a plain list of fields describes code, so its statement is \"\". Leave a "
44
+ "statement \"\" for pure plumbing too (a re-export, a trivial getter, a pass-through "
45
+ "wrapper, formatting) or when the unit's behavior is unclear, with confidence low — do "
46
+ "NOT guess. Judge each confidence on its own evidence. Ground trusted rules in "
47
+ "code-body/type-def, not comment/doc alone.\n"
41
48
  "State each rule in PRODUCT-FUNCTIONAL terms: what the business requires, not the "
42
49
  "implementation. Name a specific tool, library, or technology ONLY when the source "
43
50
  "makes that tool itself the requirement — otherwise state the capability (e.g. "
@@ -137,21 +144,23 @@ def validate_rule(rule: Any, batch_ids: set[str]) -> tuple[bool, str]:
137
144
  return True, "ok"
138
145
 
139
146
 
140
- def extract_rules(batch: list[dict[str, Any]], model_argv: list[str]) -> list[dict[str, Any]]:
147
+ def extract_rules(batch: list[dict[str, Any]], model_argv: list[str],
148
+ timeout: float | None = None) -> list[dict[str, Any]]:
141
149
  """Shell the bounded model CLI over one batch of framed contexts; return the
142
150
  parsed (UNvalidated) rule objects. Fail-loud on a non-zero exit / timeout /
143
151
  unparseable output — the caller then validates each rule and RISK-floors the
144
152
  residue, so a model failure degrades to RISK, never to a silent gap."""
145
153
  prompt = _build_prompt(batch)
154
+ limit = _MODEL_TIMEOUT if timeout is None else max(1.0, min(float(_MODEL_TIMEOUT), timeout))
146
155
  try:
147
156
  proc = subprocess.run(
148
157
  model_argv, input=prompt, capture_output=True, text=True,
149
- encoding="utf-8", timeout=_MODEL_TIMEOUT,
158
+ encoding="utf-8", timeout=limit,
150
159
  )
151
160
  except FileNotFoundError as e:
152
161
  raise RuntimeError(f"rule model {model_argv[0]} not found: {e}") from e
153
162
  except subprocess.TimeoutExpired as e:
154
- raise RuntimeError(f"rule model exceeded {_MODEL_TIMEOUT}s") from e
163
+ raise RuntimeError(f"rule model exceeded {int(limit)}s") from e
155
164
  if proc.returncode != 0:
156
165
  raise RuntimeError(
157
166
  f"rule model exited {proc.returncode}: {(proc.stderr or proc.stdout).strip()[:300]}"