wicked-garden 12.34.0 → 12.36.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 (44) hide show
  1. package/.claude-plugin/components.json +5 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +16 -0
  5. package/hooks/hooks.json +1 -1
  6. package/hooks/scripts/pre_tool.py +3 -3
  7. package/package.json +1 -1
  8. package/scripts/_agents.py +3 -3
  9. package/scripts/_estate_client.py +276 -22
  10. package/scripts/draft/__init__.py +13 -0
  11. package/scripts/draft/_dom.py +188 -0
  12. package/scripts/draft/claims_scan.py +326 -0
  13. package/scripts/draft/contrast_check.py +1077 -0
  14. package/scripts/draft/page_count.py +321 -0
  15. package/scripts/draft/self_check.py +133 -0
  16. package/scripts/mem/estate_memory.py +31 -3
  17. package/scripts/platform/observability/health_probe.py +2 -2
  18. package/scripts/smaht/adapters/delegation_adapter.py +1 -1
  19. package/skills/archetype/refs/review.md +1 -1
  20. package/skills/core/SKILL.md +2 -0
  21. package/skills/crew-implementer/SKILL.md +4 -172
  22. package/skills/crew-researcher/SKILL.md +4 -103
  23. package/skills/crew-reviewer/SKILL.md +4 -198
  24. package/skills/draft/SKILL.md +160 -0
  25. package/skills/draft/refs/claims.md +67 -0
  26. package/skills/draft/refs/design-floor.md +74 -0
  27. package/skills/draft/refs/self-check.md +100 -0
  28. package/skills/engineering/patch/SKILL.md +4 -0
  29. package/skills/governed-worker/SKILL.md +148 -0
  30. package/skills/integration-discovery/refs/discovery-sources.md +2 -2
  31. package/skills/integration-discovery/refs/task-patterns.md +7 -7
  32. package/skills/jam-council/SKILL.md +26 -16
  33. package/skills/mem/SKILL.md +12 -0
  34. package/skills/qe/SKILL.md +2 -2
  35. package/skills/qe-code-analyzer/SKILL.md +1 -1
  36. package/skills/repo-learn/SKILL.md +78 -41
  37. package/skills/repo-learn/refs/capture-proposals.md +38 -4
  38. package/skills/search/SKILL.md +74 -75
  39. package/skills/search/refs/hotspots.md +14 -5
  40. package/skills/swarm/SKILL.md +4 -5
  41. package/skills/swarm/refs/fan-out.md +5 -4
  42. package/skills/swarm/refs/independent-verification.md +1 -1
  43. package/skills/swarm/refs/recon-synthesis.md +1 -1
  44. package/skills/swarm/refs/ship-discipline.md +4 -4
@@ -2,8 +2,8 @@
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
3
  "domains": ["agentic", "crew", "data", "engineering", "jam", "mem", "persona", "platform", "product", "qe", "search", "smaht"],
4
4
  "summary": {
5
- "skills": 142,
6
- "fork_skills": 78,
5
+ "skills": 144,
6
+ "fork_skills": 75,
7
7
  "hooks": 13,
8
8
  "specialists": 7
9
9
  },
@@ -25,11 +25,13 @@
25
25
  "domain-coverage": ["domain-coverage"],
26
26
  "domain-extractor": ["domain-extractor"],
27
27
  "domain-modeler": ["domain-modeler"],
28
+ "draft": ["draft"],
28
29
  "engineering": ["architecture", "backend", "debugging", "docs", "engineering", "frontend", "integration", "large-scale-migration", "patch", "system-design", "unit-test-quality"],
29
30
  "engineering-api-documentarian": ["engineering-api-documentarian"],
30
31
  "engineering-conformance-reviewer": ["engineering-conformance-reviewer"],
31
32
  "engineering-migration-engineer": ["engineering-migration-engineer"],
32
33
  "engineering-solution-architect": ["engineering-solution-architect"],
34
+ "governed-worker": ["governed-worker"],
33
35
  "ground": ["ground"],
34
36
  "integration-discovery": ["integration-discovery"],
35
37
  "jam": ["jam"],
@@ -101,7 +103,7 @@
101
103
  "workflow": ["workflow"],
102
104
  "worktrees": ["worktrees"]
103
105
  },
104
- "fork_skills": ["wicked-garden-agentic-architect", "wicked-garden-agentic-performance-analyst", "wicked-garden-agentic-safety-reviewer", "wicked-garden-archetype", "wicked-garden-classify", "wicked-garden-crew-implementer", "wicked-garden-crew-researcher", "wicked-garden-crew-reviewer", "wicked-garden-data-engineer", "wicked-garden-domain-coverage", "wicked-garden-domain-extractor", "wicked-garden-domain-modeler", "wicked-garden-engineering-api-documentarian", "wicked-garden-engineering-conformance-reviewer", "wicked-garden-engineering-debugging", "wicked-garden-engineering-migration-engineer", "wicked-garden-engineering-solution-architect", "wicked-garden-ground", "wicked-garden-integration-discovery", "wicked-garden-jam-brainstorm-facilitator", "wicked-garden-jam-council", "wicked-garden-mem-capture", "wicked-garden-mem-ingest", "wicked-garden-persona-agent", "wicked-garden-platform-compliance-officer", "wicked-garden-platform-privacy-expert", "wicked-garden-platform-security-engineer", "wicked-garden-product-a11y-expert", "wicked-garden-product-requirements-analyst", "wicked-garden-product-ui-reviewer", "wicked-garden-product-ux-designer", "wicked-garden-product-value-strategist", "wicked-garden-qe-a11y-test-engineer", "wicked-garden-qe-acceptance-test-executor", "wicked-garden-qe-acceptance-test-reviewer", "wicked-garden-qe-acceptance-test-writer", "wicked-garden-qe-ai-feature-test-engineer", "wicked-garden-qe-chaos-test-engineer", "wicked-garden-qe-code-analyzer", "wicked-garden-qe-compliance-test-engineer", "wicked-garden-qe-contract-testing-engineer", "wicked-garden-qe-coverage-archaeologist", "wicked-garden-qe-data-quality-tester", "wicked-garden-qe-e2e-orchestrator", "wicked-garden-qe-exploratory-tester", "wicked-garden-qe-flaky-test-hunter", "wicked-garden-qe-fuzz-property-engineer", "wicked-garden-qe-iac-test-engineer", "wicked-garden-qe-incident-to-scenario-synthesizer", "wicked-garden-qe-integration-test-engineer", "wicked-garden-qe-load-performance-engineer", "wicked-garden-qe-localization-test-engineer", "wicked-garden-qe-mutation-test-engineer", "wicked-garden-qe-observability-test-engineer", "wicked-garden-qe-production-quality-engineer", "wicked-garden-qe-release-readiness-engineer", "wicked-garden-qe-requirements-quality-analyst", "wicked-garden-qe-risk-assessor", "wicked-garden-qe-scenario-executor", "wicked-garden-qe-security-test-engineer", "wicked-garden-qe-semantic-reviewer", "wicked-garden-qe-snapshot-hygiene-auditor", "wicked-garden-qe-test-automation-engineer", "wicked-garden-qe-test-code-quality-auditor", "wicked-garden-qe-test-data-manager", "wicked-garden-qe-test-designer", "wicked-garden-qe-test-impact-analyzer", "wicked-garden-qe-test-oracle", "wicked-garden-qe-test-strategist", "wicked-garden-qe-testability-reviewer", "wicked-garden-qe-ui-component-test-engineer", "wicked-garden-qe-visual-regression-engineer", "wicked-garden-runtime-exec", "wicked-garden-smaht", "wicked-garden-swarm", "wicked-garden-wickedizer", "wicked-garden-workflow", "wicked-garden-worktrees"],
106
+ "fork_skills": ["wicked-garden-agentic-architect", "wicked-garden-agentic-performance-analyst", "wicked-garden-agentic-safety-reviewer", "wicked-garden-archetype", "wicked-garden-classify", "wicked-garden-data-engineer", "wicked-garden-domain-coverage", "wicked-garden-domain-extractor", "wicked-garden-domain-modeler", "wicked-garden-engineering-api-documentarian", "wicked-garden-engineering-conformance-reviewer", "wicked-garden-engineering-debugging", "wicked-garden-engineering-migration-engineer", "wicked-garden-engineering-solution-architect", "wicked-garden-ground", "wicked-garden-integration-discovery", "wicked-garden-jam-brainstorm-facilitator", "wicked-garden-jam-council", "wicked-garden-mem-capture", "wicked-garden-mem-ingest", "wicked-garden-persona-agent", "wicked-garden-platform-compliance-officer", "wicked-garden-platform-privacy-expert", "wicked-garden-platform-security-engineer", "wicked-garden-product-a11y-expert", "wicked-garden-product-requirements-analyst", "wicked-garden-product-ui-reviewer", "wicked-garden-product-ux-designer", "wicked-garden-product-value-strategist", "wicked-garden-qe-a11y-test-engineer", "wicked-garden-qe-acceptance-test-executor", "wicked-garden-qe-acceptance-test-reviewer", "wicked-garden-qe-acceptance-test-writer", "wicked-garden-qe-ai-feature-test-engineer", "wicked-garden-qe-chaos-test-engineer", "wicked-garden-qe-code-analyzer", "wicked-garden-qe-compliance-test-engineer", "wicked-garden-qe-contract-testing-engineer", "wicked-garden-qe-coverage-archaeologist", "wicked-garden-qe-data-quality-tester", "wicked-garden-qe-e2e-orchestrator", "wicked-garden-qe-exploratory-tester", "wicked-garden-qe-flaky-test-hunter", "wicked-garden-qe-fuzz-property-engineer", "wicked-garden-qe-iac-test-engineer", "wicked-garden-qe-incident-to-scenario-synthesizer", "wicked-garden-qe-integration-test-engineer", "wicked-garden-qe-load-performance-engineer", "wicked-garden-qe-localization-test-engineer", "wicked-garden-qe-mutation-test-engineer", "wicked-garden-qe-observability-test-engineer", "wicked-garden-qe-production-quality-engineer", "wicked-garden-qe-release-readiness-engineer", "wicked-garden-qe-requirements-quality-analyst", "wicked-garden-qe-risk-assessor", "wicked-garden-qe-scenario-executor", "wicked-garden-qe-security-test-engineer", "wicked-garden-qe-semantic-reviewer", "wicked-garden-qe-snapshot-hygiene-auditor", "wicked-garden-qe-test-automation-engineer", "wicked-garden-qe-test-code-quality-auditor", "wicked-garden-qe-test-data-manager", "wicked-garden-qe-test-designer", "wicked-garden-qe-test-impact-analyzer", "wicked-garden-qe-test-oracle", "wicked-garden-qe-test-strategist", "wicked-garden-qe-testability-reviewer", "wicked-garden-qe-ui-component-test-engineer", "wicked-garden-qe-visual-regression-engineer", "wicked-garden-runtime-exec", "wicked-garden-smaht", "wicked-garden-swarm", "wicked-garden-wickedizer", "wicked-garden-workflow", "wicked-garden-worktrees"],
105
107
  "hooks": ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "TaskCompleted", "SubagentStart", "SubagentStop", "PermissionRequest", "Notification", "PreCompact", "Stop", "SessionEnd"],
106
108
  "specialists": [
107
109
  {
@@ -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.34.0"}], "version": "12.34.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, 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.36.0"}], "version": "12.36.0"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.34.0",
3
+ "version": "12.36.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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.36.0] — 2026-09-13
6
+
7
+ ### Fixed
8
+ - **Governed grounding goes through the estate stdio shim in `--readonly` with a pinned store — never the raw write CLI (garden#1130; the garden half of wicked-core#463/#471, ledger F-RC1-045/046/048).** Governed workers ground through `scripts/_estate_client.py` (the DEC-R shim the `mem` domain already uses) because an organization-managed Claude Code `allowedMcpServers` allowlist silently drops a handed `wicked-estate` MCP server, while a script the skill runs is unaffected and works on every seat CLI. The shim gains a governed / read-only mode: with `WICKED_RUN_ID` set (or `--readonly` on the backend argv, or `WICKED_ESTATE_READONLY=1`) every spawn is `wicked-estate-mcp --readonly [--db <store>]`; in a run the store MUST be pinned (`--db <path>`, or `WICKED_ESTATE_DB` / `WICKED_HOME` / `WICKED_MEMORY_DB` in the worker environment) and an unpinned store is refused without spawning (`{"ok": false, "reason": …}`), the same shape the gate hook denies as `no-pin`. `parse_cli_flags` accepts `--readonly` / `--db <path>` anywhere on a backend argv and rejects any other `--flag` as a usage error (a misspelt `--read-only` would otherwise fall through and spawn a WRITE-capable MCP — the remedy the fence coaches must work); `scripts/mem/estate_memory.py` forwards the flags instead of dropping them (before: `argv[2:]` was ignored, so the fence recorded ALLOW on a write-capable MCP). New shim actions: `propose` (`proposal.submit` — the safe write a read-only MCP still permits; `provenance` is never forwarded, the server stamps it from `WICKED_RUN_*`; returns `{ok,id}` or `{ok:false,reason,code}` so a `-32602` shape error is told apart from an unreachable estate) and `mode` (a no-spawn probe of governed / readonly / store_pinned / refusal). The persistent broker restarts when the read-only mode changes, not only when the db does. **Ladders**: `wicked-garden-search` and `wicked-garden-repo-learn` (and `refs/hotspots.md`, `refs/capture-proposals.md`, the `mem` skill) now open with a governed-run rung — the shim in `--readonly` with the store pinned from the env — then read-only CLI subcommands (`query|blast-radius|rank|stats|source|semantic|cross-graph`, `clusters` without `--annotate`), then grep flagged as missing injected edges; `wicked-estate index|scip|tfstate|import-telemetry|compact|watch` and `clusters --annotate` are never a rung in a governed run; every degrade names the rung that answered; a denied call is final (governed-worker A5). **Capture contract**: the repo-learn capture phase submits each proposal through the shim AND writes the exact `{kind_type, payload, facets}` objects as one JSON array to the deliverable file the run names — an absolute path in the unit's declared write root, never inside the repository (governed-worker E1) — so wicked-crew (crew#553) can land them when the shim path is unavailable on a seat; counts are reported derived / submitted / written-to-file / failed. **Lint**: `tests/test_skill_governed_grounding.py` fails the build when a write-CLI invocation appears in a governed-mode block of any skill text (a block that names the write CLI only to forbid it passes), and asserts the three ladders' governed blocks name the shim with `--readonly`. Release ordering: this must ship before any wicked-core-ts release that contains core#471 — that fence's deny remedy coaches `--readonly`, which shims older than this release ignore.
9
+
10
+ ### Added
11
+ - **`wicked-garden-governed-worker` — the discipline every governed unit follows, by role (creator / evaluator / neutral), in the cross-CLI skill format (garden#1131; the pilot for garden#1132).** Ten distinct governed runs were lost on 2026-09-13 to one failure class: creators shipped without running the repo's checks and called red results "pre-existing" (F-RC1-019, F-RC1-070), hand-edited generated artifacts (F-RC1-019), evaluators wrote notes or fixes into the guarded worktree and were killed by the guard (F-RC1-060/062, F-RC1-071, F-RC2-037), triage units implemented because a steer addressed to the fix phase reached them (F-RC2-038), units asked "Shall I proceed?" into a headless run (F-RC2-038), skills shelled a denied CLI and retried variants (F-RC1-046), and results were reported "done" with nothing submitted (F-RC1-048). New top-level skill `skills/governed-worker/` carries the fourteen rules (A1–A6 for every unit, C1–C5 creator, E1–E2 evaluator, N1 neutral), the role-inference table, an output contract (what you did · commands + exit codes · derived/submitted/failed counts · verdict or plan · open questions as statements) and a Provenance table citing the ledger id behind each rule. Frontmatter is the cross-CLI closed set only — `name`, `description`, `metadata.role: worker`, `metadata.phases: "*"`, `metadata.roles` — no `context: fork`, `subagent_type`, `model`, `effort`, `max-turns`, `color` or `allowed-tools`; the body names tools generically ("your shell", "your file-edit tool", "the handed estate shim in read-only mode"), so codex / pi / opencode / copilot seats can honour it. wicked-core will prepend a role-keyed base-skill directive to every unit prompt (core#468) and wicked-crew will default it (crew#554); until then the skill is handed like any `skill_ref`.
12
+
13
+ ### Changed
14
+ - **`wicked-garden-crew-implementer` / `-crew-reviewer` / `-crew-researcher` retired into `wicked-garden-governed-worker` (Creator / Evaluator / Neutral).** The trio were Claude Code fork-subagent definitions (`context: fork`, `subagent_type`, `model`, `allowed-tools`, `Task(`/`TaskUpdate(` plumbing) that no governed worker was ever handed and no other CLI could honour; their method content (work from the approved design and scenarios in small verified steps; validate against design and evidence with `file:line` verdicts; map structure, name files and risks, hand the plan on) moved into the role sections and each `skills/crew-*/SKILL.md` is now a 7-line redirect stub (`metadata.role: module`). Every in-repo reference follows the fold — swarm (SKILL + `refs/{fan-out,recon-synthesis,ship-discipline,independent-verification}.md`), archetype `refs/review.md`, qe / qe-code-analyzer disambiguation notes, jam-council seat dispatch (now `wicked-garden-governed-worker` with `Role: evaluator`), integration-discovery refs, the smaht delegation hint, the two pre_tool hook messages, the hooks.json #344 note, the site's orchestration group, `.claude/CLAUDE.md`, `.claude/commands/wg-test.md`, and the three tests that pinned worker identity on the trio (repointed to `wicked-garden-engineering-solution-architect`). `components.json` regenerated with `sync_components.py` (fork_skills 78 → 75; skills 143 → 144). No version bump.
15
+
16
+ ## [12.35.0] — 2026-09-13
17
+
18
+ ### Added
19
+ - **`wicked-garden-draft` — the quality floor for document deliverables: contrast ≥ 4.5:1, an honoured page budget, cited claims and no placeholders (F-RECON-007/008/009).** The 2026-09 recon ran a governed brochure draft end to end through the studio and the deliverable that landed was unreadable, over-long and partly invented: the hero fact strip, footer requirements and KPI label rendered `rgba(255,255,255,0.14)`-class text on a `#1a1a26` card (≈1.5:1 — invisible in the PNG and the PDF; F-RECON-007), a "two pages" brief printed as **four** A4 pages because two `.page` wrappers each overflowed their sheet (F-RECON-008), and the copy carried an invented "live instance" URL claim, a command-center mock whose figures were called illustrative only inside an HTML comment, and a visible `[PLACEHOLDER: Customer validation]` box (F-RECON-009) — while every gate auto-allowed and the run's deliverable floor proved only that the file existed. Neither garden nor crew carried a document-drafting skill (crew's `interactive-draft` phases have `skill_ref: null`; the contract was inlined prose adapted from wicked-interactive's assist skill), so nothing re-derived any of it. **New top-level skill `skills/draft/`** (user-invocable; `refs/design-floor.md`, `refs/claims.md`, `refs/self-check.md`): design guidance for the three floors — READABLE (every text/background pair ≥ 4.5:1, 3:1 for large text, judged on the COMPOSITED colours; print text ≥ 7pt — thin 6–7pt type never reaches full pixel coverage, which is why the 4.9:1 fact strip still read as grey), SIZED TO THE BRIEF (the RENDERED page count meets the budget the brief names, default = exactly as asked, any format cap disclosed, wrappers are not pages), GROUNDED (every number/URL cites a repo path via `data-source="path:line"` plus a visible Sources strip, mocks carry `data-illustrative` AND a visible label, no placeholder ever ships — gaps go to the notes) — **and one mandatory self-check** the author runs on the saved file before "done", with a `PASS` / `FAIL` / `UNVERIFIED` verdict that is pasted into the reply, never paraphrased. **Runtime (`scripts/draft/`, stdlib-only, reachable as `wicked-garden run scripts/draft/…`)**: `contrast_check.py` cascades the document's own CSS (specificity, source order, inline, `!important`, `var()` with inheritance — including `:root` tokens when the instrumented deliverable has no `<html>` element — `@media print|screen`, alpha compositing over the real background, `opacity`, every gradient stop) and judges every text-bearing element; `page_count.py` reads the rendered count from an exported PDF (page-tree `/Count`, object streams inflated) or a headless-Chrome print (`--render`; `$WICKED_CHROME`), and otherwise reports the structural estimate as **UNVERIFIED (exit 3)** — never a pass; `claims_scan.py` scans the VISIBLE text for placeholders, uncited numbers, uncited/unsourced URLs (bare domains included), hidden or missing mock labels and, with `--repo`, dangling `data-source` paths and URLs no source mentions; `self_check.py` runs all three and exits 0/1/3. Against the recon's landed v1 (kept as `tests/draft/fixtures/recon-brochure-v1.html`) the checks reproduce the findings exactly — footer `.cta-reqs` and `.mockup-kpi-label` at 1.53:1, the 6.8pt fact strip under the size floor, the accent violet at 3.7:1 on cards, 4 PDF pages vs budget 2, the placeholder, the `3 active · 1 needs you` hidden-label mock, the uncited 2 s / 3 s latencies — and pass the same document once the tokens are lifted and the claims cited (`tests/draft/`, 60+ tests: colour maths, cascade, media modes, PDF page trees incl. object streams, a fake-Chrome render path, the citation forms, CLI exit codes). Cross-CLI per the 12.33 convention (skill-relative refs, the exact `## Runtime` block, no Claude-only mechanics; `tests/portability_rules.garden.json` gains quiet/trip corpus entries for the new launcher lines). The core help overview, `components.json` and the site's honest counts move to 143 skills (`draft` folded into the product & UX group). **Hardened after the independent review (same wave):** shrink-to-fit is judged, not rewarded — `zoom`, `transform: scale()`/`matrix()` and `scale:` on any ancestor multiply into the EFFECTIVE text size the 7pt floor is applied to (the reviewer's `body{zoom:.62}` on a corrected brochure went from a full `PASS` at ≈4.3pt to `FAIL`), an unevaluable transform makes the size UNVERIFIED, and the skill text forbids scaling a page to meet a budget; unsupported colour functions (`oklch()`, `lab()`, `color-mix()`, `light-dark()`…) and image-only backgrounds are reported as `[unknown]` UNVERIFIED pairs (exit 3) instead of silently inheriting or being judged "on white"; `@media not print|screen` is evaluated correctly (it was inverted); inline `!important` sits at the top of the cascade; `--mode both` reports a pair once (`print+screen`); colour-declaring rules with selectors the matcher skips (`:nth-child`, `:is`…) leave a note instead of vanishing; the CLIs never crash on a cp1252 console (Windows piped stdout — `errors="replace"`); `page_count.py` fails (exit 1) before it reports unverified when even the estimate exceeds the budget, ignores a forced break on the last element, refuses a `--pdf` path that does not exist (no silent render), and prints offline (`--host-resolver-rules=MAP * ~NOTFOUND`); `claims_scan.py` no longer flags prose dates, numbered headings, `Step N`/`Page N of M` and lone badge numerals; the skill spells out the two "done" states (`PASS`, or `UNVERIFIED` with a disclosure naming which check, why on this seat, and what a reviewer verifies — the check prints the `DISCLOSE` lines) and that `FAIL` is never done; `refs/design-floor.md` ratios recomputed with the module (`#94a3b8` on white is 2.6:1, not 3.2). **Follow-ups (other repos):** wicked-crew's `interactive-draft` / `-edit` / `-chat` phases should set `skill_ref: 'wicked-garden-draft'` and pass the budget/snapshot to the self-check, and the deliverable floor could run `self_check.py` as a deterministic tool phase; wicked-interactive's assist skill Step 5 can point at the same floor.
20
+
5
21
  ## [12.34.0] — 2026-09-11
6
22
 
7
23
  ### Changed
package/hooks/hooks.json CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  "_TODO_341_http_hooks": "Issue #341: HTTP hook type (type: 'http') is not supported. When supported, add HTTP hooks for external service integration: POST crew phase transitions to a webhook URL, send gate results to a quality dashboard, notify Slack/Teams on crew project completion. Currently all hooks are type: 'command' (subprocess).",
17
17
 
18
- "_TODO_344_isolation_worktree": "Issue #344: Skill frontmatter field 'isolation: worktree' is not supported. When supported, add isolation: worktree to the crew fork-worker skills (skills/crew-implementer, skills/crew-researcher, skills/crew-reviewer) so they operate in separate git worktrees. This would prevent destructive operations (branch switching, file modifications) in one worker from affecting others running in parallel.",
18
+ "_TODO_344_isolation_worktree": "Issue #344: Skill frontmatter field 'isolation: worktree' is not supported. When supported, add isolation: worktree to the context:fork worker skills that modify files so they operate in separate git worktrees. This would prevent destructive operations (branch switching, file modifications) in one worker from affecting others running in parallel. (The crew-implementer / crew-researcher / crew-reviewer trio this note used to name was retired 2026-09 into the cross-CLI wicked-garden-governed-worker skill, which carries no fork frontmatter.)",
19
19
 
20
20
  "_TODO_345_once_flag": "Issue #345: The 'once' flag is not documented as a supported hook property. When supported, add \"once\": true to the SessionStart bootstrap hook so it only fires on the first session start, not on reconnects. Currently bootstrap.py handles this by checking session state for staleness.",
21
21
 
@@ -421,8 +421,8 @@ def _check_challenge_gate(file_path: str) -> str:
421
421
  f"'{project_name}' (complexity={complexity}). {reason} "
422
422
  f"Invoke a contrarian reviewer to produce and clear "
423
423
  f"phases/design/challenge-artifacts.md before continuing build. "
424
- f"Dispatch the reviewer fork skill: Skill("
425
- f"skill='wicked-garden-crew-reviewer') with a contrarian challenge brief. "
424
+ f"Dispatch a reviewer that follows the evaluator section of the "
425
+ f"wicked-garden-governed-worker skill with a contrarian challenge brief. "
426
426
  f"To bypass temporarily, set WG_CHALLENGE_GATE=off."
427
427
  )
428
428
  except Exception:
@@ -1064,7 +1064,7 @@ def _warn_reviewer_report(phase_dir: Path, phase_name: str, complexity: int) ->
1064
1064
  f"[wicked-garden] Warning: phases/{phase_name}/reviewer-report.md is missing "
1065
1065
  f"for a complexity-{complexity} project. The async reviewer (Tier 2) should "
1066
1066
  f"have written this. Proceeding with approve, but consider dispatching "
1067
- f"Skill(skill='wicked-garden-crew-reviewer') to generate a reviewer report."
1067
+ f"a reviewer that follows the wicked-garden-governed-worker skill's evaluator section to generate a reviewer report."
1068
1068
  )
1069
1069
  return ""
1070
1070
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.34.0",
3
+ "version": "12.36.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": [
@@ -13,7 +13,7 @@ Usage:
13
13
 
14
14
  loader = AgentLoader()
15
15
  loader.load_fork_skills(skills_dir) # skills/ root
16
- profile = loader.get("wicked-garden-crew-implementer")
16
+ profile = loader.get("wicked-garden-engineering-solution-architect")
17
17
  """
18
18
 
19
19
  import json
@@ -69,7 +69,7 @@ class AgentLoader:
69
69
  Lifecycle (called once in bootstrap.py):
70
70
  loader = AgentLoader()
71
71
  loader.load_fork_skills(skills_dir)
72
- profile = loader.get("wicked-garden-crew-implementer")
72
+ profile = loader.get("wicked-garden-engineering-solution-architect")
73
73
  """
74
74
 
75
75
  def __init__(self) -> None:
@@ -174,7 +174,7 @@ def _parse_agent_md(path: Path, fork_only: bool = False) -> AgentProfile | None:
174
174
 
175
175
  Format:
176
176
  ---
177
- name: wicked-garden-crew-implementer
177
+ name: wicked-garden-engineering-solution-architect
178
178
  description: |
179
179
  Optional description
180
180
  context: fork # required when fork_only=True
@@ -32,12 +32,46 @@ The spawn-vs-broker choice is an implementation detail behind a single seam:
32
32
  and swappable via `set_dispatch()`). Every public function routes through it, so
33
33
  **no caller changes** when the transport does.
34
34
 
35
+ Governed / read-only mode (wicked-garden #1130, DES-GROUNDING-001 §7)
36
+ ------------------------------------------------------------------
37
+ A governed worker (a unit of a wicked-crew run, `WICKED_RUN_ID` set) grounds
38
+ through THIS shim, never through a Claude-Code-registered MCP server (an
39
+ organization-managed `allowedMcpServers` allowlist drops that silently) and
40
+ never through the raw write CLI. In that mode every spawn is
41
+ ``wicked-estate-mcp --readonly [--db <store>]``:
42
+
43
+ * ``--readonly`` is spelled literally — the MCP detects read-only by the exact
44
+ token and refuses every write tool except the safe ``proposal.submit``.
45
+ Read-only is on when a governed-unit marker is present (`GOVERNED_MARKERS`:
46
+ `WICKED_RUN_ID`, or the fence env the wrapped carrier sets on the worker —
47
+ `WICKED_GATE_SCOPE` / `WICKED_WRITE_ROOTS`), when the caller passed
48
+ ``--governed`` / ``--readonly`` on the backend argv (`parse_cli_flags` /
49
+ `set_governed` / `set_readonly`), or when ``WICKED_GOVERNED`` /
50
+ ``WICKED_ESTATE_READONLY`` is truthy. The markers are a CONFIRMATION, not
51
+ the definition: the skill text defines governed mode by the handed context
52
+ (a phase directive / the governed-worker skill), because no carrier stamps
53
+ `WICKED_RUN_ID` on the worker's own environment today and the ACP carrier
54
+ hands the child none of the fence env.
55
+ * The store must be PINNED: ``--db <path>`` on the backend argv, or
56
+ ``WICKED_ESTATE_DB`` / ``WICKED_HOME`` / ``WICKED_MEMORY_DB`` in the worker
57
+ environment. In a governed run an unpinned store is REFUSED (no spawn,
58
+ `governed_refusal()` names the fix) — an unpinned MCP would resolve the
59
+ operator's defaults, which is exactly the leak the fence denies.
60
+ * Backends forward the flags instead of dropping them: `parse_cli_flags(argv)`
61
+ strips ``--readonly`` / ``--db`` anywhere on argv and REJECTS any other
62
+ ``--flag`` (a misspelling such as ``--read-only`` must never fall through —
63
+ the MCP would run write-capable). The wicked-core gate hook allows a shim
64
+ invocation only with ``--readonly`` AND a pinned store (core #471); this
65
+ module produces exactly that argv shape for the spawned MCP.
66
+
35
67
  Fail-open contract
36
68
  ------------------
37
69
  Every public function is fail-open: on a missing binary, spawn failure,
38
70
  timeout, non-zero exit, malformed JSON, or a tool `isError`, it returns a safe
39
71
  empty value (`False` / `None` / `[]`) and never raises. Hooks degrade; they do
40
- not crash.
72
+ not crash. The governed refusal above is the one deliberate "do not even try":
73
+ it still returns the empty value — the CLI surfaces the reason as
74
+ ``{"ok": false, "reason": …}``.
41
75
 
42
76
  Cross-platform
43
77
  --------------
@@ -108,11 +142,14 @@ def resolve_estate_bin() -> Optional[str]:
108
142
  def resolve_db() -> Optional[str]:
109
143
  """Resolve the graph DB path, mirroring the binary's own resolution.
110
144
 
111
- Order: WICKED_ESTATE_DB env > <cwd>/.wicked-estate/graph.db (if present) >
112
- None. When None, callers omit `--db` and let the binary apply its default,
113
- so we never fight the binary's resolution — we only *pin* it when we have a
114
- concrete answer. `:memory:` is honoured (passed through) for tests.
145
+ Order: an explicit `--db` (`set_db` / `parse_cli_flags`) > WICKED_ESTATE_DB
146
+ env > <cwd>/.wicked-estate/graph.db (if present) > None. When None, callers
147
+ omit `--db` and let the binary apply its default, so we never fight the
148
+ binary's resolution — we only *pin* it when we have a concrete answer.
149
+ `:memory:` is honoured (passed through) for tests.
115
150
  """
151
+ if _db_override:
152
+ return _db_override
116
153
  env_db = os.environ.get("WICKED_ESTATE_DB")
117
154
  if env_db:
118
155
  return env_db
@@ -125,6 +162,153 @@ def resolve_db() -> Optional[str]:
125
162
  return None
126
163
 
127
164
 
165
+ # ─────────────────────────────────────────────────────────────────────────────
166
+ # Governed / read-only mode — the argv contract the gate hook allows (#1130)
167
+ # ─────────────────────────────────────────────────────────────────────────────
168
+
169
+ # Governed-unit markers — any of these non-empty in the environment means "you are a
170
+ # unit of a wicked-crew run". `WICKED_RUN_ID` is what wicked-core stamps on the estate
171
+ # MCP SERVER's launch env (`estate_provenance_env`), not on the worker's own Command env,
172
+ # so on its own it is an unreliable cue inside a unit (garden #1134 HIGH-1). The fence
173
+ # env the WRAPPED carrier does set on the worker Command is the reliable marker there —
174
+ # wicked-core main `ad9a0c1`, `src/execute_wrapped.rs:853` (`WRITE_ROOTS_ENV` =
175
+ # "WICKED_WRITE_ROOTS", `src/gate_hook.rs:114`) and `:863` (`GATE_SCOPE_ENV` =
176
+ # "WICKED_GATE_SCOPE", `src/gate_hook.rs:56`). The ACP carrier hands its child none of
177
+ # these today (`gate_hook.rs` `estate_store_pinned_for_child`), which is why the skill
178
+ # text defines governed mode by the HANDED CONTEXT and `--governed` / WICKED_GOVERNED=1
179
+ # let a seat (or a launcher) say so explicitly.
180
+ GOVERNED_RUN_ENV = "WICKED_RUN_ID"
181
+ GOVERNED_MARKERS = (GOVERNED_RUN_ENV, "WICKED_GATE_SCOPE", "WICKED_WRITE_ROOTS")
182
+ GOVERNED_ENV = "WICKED_GOVERNED" # explicit override (any truthy value)
183
+ GOVERNED_FLAG = "--governed" # explicit override on the backend argv
184
+ READONLY_ENV = "WICKED_ESTATE_READONLY" # explicit read-only opt-in outside a run
185
+ # Any of these, non-empty in the worker environment, pins the store the spawned
186
+ # MCP resolves (the same set wicked-core's shim allow rule reads: core #471).
187
+ STORE_PIN_ENV = ("WICKED_ESTATE_DB", "WICKED_HOME", "WICKED_MEMORY_DB")
188
+ READONLY_FLAG = "--readonly" # exact token — the MCP matches it literally
189
+ DB_FLAG = "--db"
190
+
191
+ _readonly_flag: bool = False # set by --readonly / set_readonly()
192
+ _governed_flag: bool = False # set by --governed / set_governed()
193
+ _db_override: Optional[str] = None # set by --db <path> / set_db()
194
+
195
+
196
+ def set_governed(flag: bool = True) -> None:
197
+ """Declare this process a governed unit (`--governed`): read-only + pinned store required."""
198
+ global _governed_flag
199
+ _governed_flag = bool(flag)
200
+
201
+
202
+ def set_readonly(flag: bool = True) -> None:
203
+ """Force read-only spawns (`--readonly` on the MCP argv) for this process."""
204
+ global _readonly_flag
205
+ _readonly_flag = bool(flag)
206
+
207
+
208
+ def set_db(path: Optional[str]) -> None:
209
+ """Pin the store the spawned MCP opens (`--db <path>`); None clears the pin."""
210
+ global _db_override
211
+ _db_override = path or None
212
+
213
+
214
+ def _env_truthy(name: str) -> bool:
215
+ return os.environ.get(name, "").strip().lower() not in ("", "0", "false", "no")
216
+
217
+
218
+ def governed_marker() -> Optional[str]:
219
+ """Which cue says "governed unit": the `--governed` flag, `WICKED_GOVERNED`, or the
220
+ first `GOVERNED_MARKERS` variable set in the environment. None = no marker seen
221
+ (which does NOT prove a human session — see the module docstring)."""
222
+ if _governed_flag:
223
+ return GOVERNED_FLAG
224
+ if _env_truthy(GOVERNED_ENV):
225
+ return GOVERNED_ENV
226
+ for name in GOVERNED_MARKERS:
227
+ if os.environ.get(name, "").strip():
228
+ return name
229
+ return None
230
+
231
+
232
+ def is_governed() -> bool:
233
+ """True when any governed-unit marker is present (`governed_marker`)."""
234
+ return governed_marker() is not None
235
+
236
+
237
+ def read_only() -> bool:
238
+ """Spawn the MCP with `--readonly`? Governed run, explicit flag, or env opt-in."""
239
+ return _readonly_flag or is_governed() or _env_truthy(READONLY_ENV)
240
+
241
+
242
+ def store_pinned() -> bool:
243
+ """Is the store named — `--db` on argv or a pin variable in the environment?"""
244
+ return bool(_db_override) or any(os.environ.get(k, "").strip() for k in STORE_PIN_ENV)
245
+
246
+
247
+ def governed_refusal() -> Optional[str]:
248
+ """The reason NOT to spawn: a governed run with no pinned store. None = clear to go.
249
+
250
+ Belt-and-braces with the gate hook (which denies the same shape as `no-pin`):
251
+ the shim never resolves the operator's default stores from inside a run.
252
+ """
253
+ marker = governed_marker()
254
+ if marker is None or store_pinned():
255
+ return None
256
+ return (
257
+ f"governed unit ({marker} is set) but no estate store is pinned: pass "
258
+ f"{DB_FLAG} <path> or set {' / '.join(STORE_PIN_ENV)} in the worker environment; "
259
+ "refusing to spawn wicked-estate-mcp against an unpinned store (it would resolve "
260
+ "the operator's defaults)"
261
+ )
262
+
263
+
264
+ def mcp_argv(exe: str, db: Optional[str]) -> list:
265
+ """The exact argv every transport spawns: `<exe> [--readonly] [--db <db>]`."""
266
+ argv = [exe]
267
+ if read_only():
268
+ argv.append(READONLY_FLAG)
269
+ if db:
270
+ argv += [DB_FLAG, db]
271
+ return argv
272
+
273
+
274
+ def parse_cli_flags(argv: list) -> list:
275
+ """Strip the shim's own flags from a backend argv, apply them, return the rest.
276
+
277
+ `--readonly` → `set_readonly(True)`; `--governed` → `set_governed(True)`;
278
+ `--db <path>` / `--db=<path>` → `set_db`.
279
+ Flags are accepted ANYWHERE on argv (the gate hook's remedy coaches appending
280
+ `--readonly` to a denied command). Any other `--flag` raises ValueError — a
281
+ misspelling (`--read-only`, `--readonly=false`) must never pass through as a
282
+ positional, because the spawned MCP would then run write-capable. `-`
283
+ (read JSON from stdin) and JSON objects are positionals and pass through.
284
+ """
285
+ rest: list = []
286
+ it = iter(argv)
287
+ for tok in it:
288
+ if tok == READONLY_FLAG:
289
+ set_readonly(True)
290
+ elif tok == GOVERNED_FLAG:
291
+ set_governed(True)
292
+ elif tok == DB_FLAG:
293
+ value = next(it, None)
294
+ if not value or value.startswith("-"):
295
+ raise ValueError(f"{DB_FLAG} requires a store path")
296
+ set_db(value)
297
+ elif tok.startswith(DB_FLAG + "="):
298
+ value = tok[len(DB_FLAG) + 1:]
299
+ if not value:
300
+ raise ValueError(f"{DB_FLAG}= requires a store path")
301
+ set_db(value)
302
+ elif tok.startswith("--") and len(tok) > 2:
303
+ raise ValueError(
304
+ f"unknown flag {tok!r}: the estate shim accepts {READONLY_FLAG}, "
305
+ f"{GOVERNED_FLAG} and {DB_FLAG} <path> only (spell {READONLY_FLAG} exactly)"
306
+ )
307
+ else:
308
+ rest.append(tok)
309
+ return rest
310
+
311
+
128
312
  # ─────────────────────────────────────────────────────────────────────────────
129
313
  # Transport seam — spawn-per-call today; swappable for a broker later
130
314
  # ─────────────────────────────────────────────────────────────────────────────
@@ -139,14 +323,15 @@ def _dispatch(requests: list, *, db: Optional[str], timeout: float) -> dict:
139
323
  its `id`. Notifications (no `id`) produce no output and are simply absent.
140
324
 
141
325
  Fail-open: any failure returns {} — the caller treats a missing id as a
142
- dead call and degrades.
326
+ dead call and degrades. A governed run with no pinned store never spawns
327
+ (`governed_refusal`).
143
328
  """
329
+ if governed_refusal():
330
+ return {}
144
331
  exe = resolve_mcp_bin()
145
332
  if not exe:
146
333
  return {}
147
- argv = [exe]
148
- if db:
149
- argv += ["--db", db]
334
+ argv = mcp_argv(exe, db)
150
335
  payload = "".join(json.dumps(r, separators=(",", ":")) + "\n" for r in requests)
151
336
  try:
152
337
  proc = subprocess.Popen(
@@ -240,6 +425,7 @@ class _PersistentBroker:
240
425
  self._proc: Optional[subprocess.Popen] = None # type: ignore[type-arg]
241
426
  self._init_resp: Optional[dict] = None # cached after handshake
242
427
  self._db: Optional[str] = None # db the process was started with
428
+ self._readonly: bool = False # mode the process was started in
243
429
  self._q: queue.Queue = queue.Queue() # response lines from reader thread
244
430
  self._failed: bool = False # permanent-degrade flag
245
431
  self._reconnect_used: bool = False # one reconnect per lifetime
@@ -252,9 +438,8 @@ class _PersistentBroker:
252
438
  exe = resolve_mcp_bin()
253
439
  if not exe:
254
440
  return False
255
- argv = [exe]
256
- if db:
257
- argv += ["--db", db]
441
+ readonly = read_only()
442
+ argv = mcp_argv(exe, db)
258
443
 
259
444
  # Fresh queue per session: a previous reader thread may enqueue an EOF
260
445
  # sentinel *after* any drain loop runs, corrupting the new session.
@@ -312,6 +497,7 @@ class _PersistentBroker:
312
497
  self._proc = proc
313
498
  self._init_resp = resp
314
499
  self._db = db
500
+ self._readonly = readonly
315
501
  return True
316
502
 
317
503
  @staticmethod
@@ -418,16 +604,17 @@ class _PersistentBroker:
418
604
 
419
605
  def _locked(self, requests: list, *, db: Optional[str], timeout: float) -> dict:
420
606
  """Called under self._lock. Returns {id: response}, {} on any failure."""
421
- if self._failed:
607
+ if self._failed or governed_refusal():
422
608
  return {}
423
609
 
424
610
  # Ensure the process is running. Three cases:
425
611
  # (a) First start: no process, no init_resp yet — start freely.
426
612
  # (b) Unexpected death between calls: process gone, init_resp present —
427
613
  # consumes the one reconnect budget; degrade if budget exhausted.
428
- # (c) Planned restart: DB changed while process is still alive —
429
- # close + restart, NOT counted against the reconnect budget.
430
- if not self._is_alive() or self._db != db:
614
+ # (c) Planned restart: DB or read-only mode changed while the process is
615
+ # still alive — close + restart, NOT counted against the budget.
616
+ mode_changed = self._db != db or self._readonly != read_only()
617
+ if not self._is_alive() or mode_changed:
431
618
  previously_started = self._init_resp is not None
432
619
  if not self._is_alive() and previously_started:
433
620
  # Case (b): death between calls.
@@ -435,8 +622,8 @@ class _PersistentBroker:
435
622
  self._failed = True
436
623
  return {}
437
624
  self._reconnect_used = True
438
- elif self._is_alive() and self._db != db:
439
- # Case (c): DB changed — close old process before restarting.
625
+ elif self._is_alive() and mode_changed:
626
+ # Case (c): DB / mode changed — close old process before restarting.
440
627
  self._close()
441
628
  if not self._start(db):
442
629
  # Only mark permanently failed when this is a reconnect path
@@ -692,6 +879,42 @@ def knowledge_recall(query: str, token_budget: int = 2000, timeout: float = 8.0)
692
879
  return call("knowledge.recall", {"query": query, "token_budget": token_budget}, timeout=timeout)
693
880
 
694
881
 
882
+ def propose(
883
+ kind_type: str,
884
+ payload: dict,
885
+ facets: Optional[dict] = None,
886
+ timeout: float = 8.0,
887
+ ) -> dict:
888
+ """Submit one inert proposal → estate `proposal.submit` (the safe write a
889
+ `--readonly` MCP still permits; it lands `pending` until an operator approves).
890
+
891
+ Returns ``{"ok": True, "id": <proposal-id>}`` or ``{"ok": False, "reason": …,
892
+ "code": <json-rpc code | None>}`` so a caller can tell a shape error (-32602 —
893
+ fix the args and resubmit) from an unreachable estate (code None — keep the
894
+ objects in the deliverable file / report instead). `provenance` is NEVER sent:
895
+ the server stamps it from its own `WICKED_RUN_*` environment.
896
+ """
897
+ arguments: dict = {"kind_type": kind_type, "payload": payload}
898
+ if facets:
899
+ arguments["facets"] = facets
900
+ envelope = call_raw("proposal.submit", arguments, timeout=timeout)
901
+ if envelope is None:
902
+ return {"ok": False, "reason": "estate unreachable (proposal.submit did not round-trip)",
903
+ "code": None}
904
+ error = envelope.get("error")
905
+ if isinstance(error, dict):
906
+ return {"ok": False, "reason": error.get("message") or "proposal.submit failed",
907
+ "code": error.get("code")}
908
+ result = _unwrap(envelope)
909
+ if isinstance(result, dict) and result.get("id"):
910
+ return {"ok": True, "id": result["id"]}
911
+ text = None
912
+ content = (envelope.get("result") or {}).get("content") if isinstance(envelope.get("result"), dict) else None
913
+ if isinstance(content, list) and content and isinstance(content[0], dict):
914
+ text = content[0].get("text")
915
+ return {"ok": False, "reason": text or "proposal.submit returned no id", "code": None}
916
+
917
+
695
918
  def stats(timeout: float = 8.0) -> Optional[dict]:
696
919
  """Index summary → the `wicked-estate stats` CLI, parsed to a dict.
697
920
 
@@ -735,22 +958,44 @@ def stats(timeout: float = 8.0) -> Optional[dict]:
735
958
 
736
959
  # ─────────────────────────────────────────────────────────────────────────────
737
960
  # CLI — the `wicked-estate-call` entry for non-Python / shell / markdown callers
738
- # python3 scripts/_estate_client.py <action> [json-args]
961
+ # python3 scripts/_estate_client.py [--readonly] [--governed] [--db <path>] <action> [json-args]
739
962
  # Prints JSON to stdout; exit 0 always on a clean (even empty) result, 1 only on
740
963
  # a usage error. Never crashes a caller.
741
964
  # ─────────────────────────────────────────────────────────────────────────────
742
965
 
966
+ _USAGE = (
967
+ "usage: _estate_client.py [--readonly] [--governed] [--db <path>] "
968
+ "<health|mode|search|context|recall|knowledge-recall|stats|list-tools|call|propose> "
969
+ "[json-args]"
970
+ )
971
+
972
+
743
973
  def _emit(obj: Any) -> None:
744
974
  sys.stdout.write(json.dumps(obj))
745
975
  sys.stdout.write("\n")
746
976
 
747
977
 
748
978
  def main(argv: list) -> int:
979
+ try:
980
+ argv = parse_cli_flags(argv)
981
+ except ValueError as exc:
982
+ _emit({"error": f"{exc}; {_USAGE}"})
983
+ return 1
749
984
  if not argv:
750
- _emit({"error": "usage: _estate_client.py <health|search|context|recall|"
751
- "knowledge-recall|stats|list-tools|call> [json-args]"})
985
+ _emit({"error": _USAGE})
752
986
  return 1
753
987
  action = argv[0]
988
+ if action == "mode":
989
+ # A pure local probe — reports the mode without spawning, refusal included.
990
+ _emit({"governed": is_governed(), "marker": governed_marker(),
991
+ "readonly": read_only(), "store_pinned": store_pinned(),
992
+ "db": resolve_db(), "refusal": governed_refusal()})
993
+ return 0
994
+ refusal = governed_refusal()
995
+ if refusal:
996
+ # Fail-open shape (exit 0): a degrade the worker reports, not a crash.
997
+ _emit({"ok": False, "reason": refusal, "governed": True})
998
+ return 0
754
999
  raw = argv[1] if len(argv) > 1 else "{}"
755
1000
  try:
756
1001
  args = json.loads(raw) if raw else {}
@@ -784,8 +1029,17 @@ def main(argv: list) -> int:
784
1029
  _emit({"error": "call requires {\"tool\": \"<ToolName>\", \"arguments\": {...}}"})
785
1030
  return 1
786
1031
  _emit({"result": call(tool, args.get("arguments", {}))})
1032
+ elif action == "propose":
1033
+ kind_type = args.get("kind_type")
1034
+ payload = args.get("payload")
1035
+ if not isinstance(kind_type, str) or not kind_type or not isinstance(payload, dict):
1036
+ _emit({"error": 'propose requires {"kind_type": "memory"|"policy:<type>", '
1037
+ '"payload": {...}, "facets": {...}?} — never "provenance"'})
1038
+ return 1
1039
+ # `provenance` is deliberately not forwarded (server-stamped from WICKED_RUN_*).
1040
+ _emit(propose(kind_type, payload, args.get("facets")))
787
1041
  else:
788
- _emit({"error": f"unknown action: {action}"})
1042
+ _emit({"error": f"unknown action: {action}; {_USAGE}"})
789
1043
  return 1
790
1044
  return 0
791
1045
 
@@ -0,0 +1,13 @@
1
+ """wicked-garden `draft` runtime — the self-checks behind the `wicked-garden-draft` skill.
2
+
3
+ Stdlib-only. Every module is both importable (pure functions the tests drive) and a CLI the
4
+ skill invokes through the launcher (`wicked-garden run scripts/draft/<x>.py …`):
5
+
6
+ * ``contrast_check`` — WCAG contrast (and a minimum print size) for every text-bearing element,
7
+ resolved from the document's own CSS (custom properties, alpha, media).
8
+ * ``page_count`` — rendered page count of a PDF (or a Chrome print of the HTML) against the
9
+ brief's page budget; a structural estimate when nothing can render.
10
+ * ``claims_scan`` — placeholders, uncited numbers, hidden mock labels, dangling
11
+ ``data-source`` paths and unsourced URLs in the visible text.
12
+ * ``self_check`` — the one command the worker runs before "done": all three, one verdict.
13
+ """