wicked-garden 12.35.0 → 12.37.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.
- package/.claude-plugin/components.json +4 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/hooks/hooks.json +1 -1
- package/hooks/scripts/pre_tool.py +3 -3
- package/package.json +1 -1
- package/scripts/_agents.py +3 -3
- package/scripts/_estate_client.py +276 -22
- package/scripts/mem/estate_memory.py +31 -3
- package/scripts/platform/observability/health_probe.py +2 -2
- package/scripts/smaht/adapters/delegation_adapter.py +1 -1
- package/skills/archetype/refs/review.md +1 -1
- package/skills/core/SKILL.md +1 -0
- package/skills/crew-implementer/SKILL.md +4 -172
- package/skills/crew-researcher/SKILL.md +4 -103
- package/skills/crew-reviewer/SKILL.md +4 -198
- package/skills/domain-coverage/SKILL.md +5 -0
- package/skills/engineering/patch/SKILL.md +4 -0
- package/skills/governed-worker/SKILL.md +155 -0
- package/skills/integration-discovery/refs/discovery-sources.md +2 -2
- package/skills/integration-discovery/refs/task-patterns.md +7 -7
- package/skills/jam-council/SKILL.md +26 -16
- package/skills/mem/SKILL.md +12 -0
- package/skills/qe/SKILL.md +2 -2
- package/skills/qe/refs/accept.md +18 -12
- package/skills/qe/refs/review.md +16 -10
- package/skills/qe-a11y-test-engineer/SKILL.md +17 -9
- package/skills/qe-ai-feature-test-engineer/SKILL.md +12 -2
- package/skills/qe-code-analyzer/SKILL.md +1 -1
- package/skills/qe-security-test-engineer/SKILL.md +13 -3
- package/skills/qe-snapshot-hygiene-auditor/SKILL.md +12 -4
- package/skills/qe-test-code-quality-auditor/SKILL.md +15 -5
- package/skills/repo-learn/SKILL.md +78 -41
- package/skills/repo-learn/refs/capture-proposals.md +38 -4
- package/skills/search/SKILL.md +74 -75
- package/skills/search/refs/hotspots.md +14 -5
- package/skills/swarm/SKILL.md +4 -5
- package/skills/swarm/refs/fan-out.md +5 -4
- package/skills/swarm/refs/independent-verification.md +1 -1
- package/skills/swarm/refs/recon-synthesis.md +1 -1
- package/skills/swarm/refs/ship-discipline.md +4 -4
- package/skills/wickedizer/refs/patterns.md +1 -1
|
@@ -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":
|
|
6
|
-
"fork_skills":
|
|
5
|
+
"skills": 144,
|
|
6
|
+
"fork_skills": 75,
|
|
7
7
|
"hooks": 13,
|
|
8
8
|
"specialists": 7
|
|
9
9
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"engineering-conformance-reviewer": ["engineering-conformance-reviewer"],
|
|
32
32
|
"engineering-migration-engineer": ["engineering-migration-engineer"],
|
|
33
33
|
"engineering-solution-architect": ["engineering-solution-architect"],
|
|
34
|
+
"governed-worker": ["governed-worker"],
|
|
34
35
|
"ground": ["ground"],
|
|
35
36
|
"integration-discovery": ["integration-discovery"],
|
|
36
37
|
"jam": ["jam"],
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
"workflow": ["workflow"],
|
|
103
104
|
"worktrees": ["worktrees"]
|
|
104
105
|
},
|
|
105
|
-
"fork_skills": ["wicked-garden-agentic-architect", "wicked-garden-agentic-performance-analyst", "wicked-garden-agentic-safety-reviewer", "wicked-garden-archetype", "wicked-garden-classify", "wicked-garden-
|
|
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"],
|
|
106
107
|
"hooks": ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "TaskCompleted", "SubagentStart", "SubagentStop", "PermissionRequest", "Notification", "PreCompact", "Stop", "SessionEnd"],
|
|
107
108
|
"specialists": [
|
|
108
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.
|
|
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.37.0"}], "version": "12.37.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.37.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.37.0] — 2026-09-14
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Evaluator verdicts: one grammar — `VERDICT: PASS` or `VERDICT: FAIL`, one plain-text line, the LAST line of the message (FIX-IT-ALL L6-0; the garden half of D-9, mirrored by wicked-crew's qe review text and wicked-core's evaluator-verdict carrier).** The review surfaces said APPROVE / CONDITIONAL / REJECT in one place (`wicked-garden-governed-worker`), `VERDICT={PASS|CONDITIONAL|FAIL} MODE=produced-test` in another (the `qe` router's `review` action), `{PASS|CONDITIONAL|FAIL|SKIP}` in a third (`qe-security-test-engineer`), TWO verdict lines in one message (`qe-test-code-quality-auditor` §8 + §9) and no verdict line at all in an evaluator skill (`domain-coverage`) — a gate cannot read five spellings. wicked-core's evaluator gate (wave 3, core-ts 0.7.27) reads the LAST `^VERDICT[:=]` line of an evaluator unit's output and passes ONLY on the token `PASS`; any other token — or no line — parks the run at the human gate, never a false pass. Every garden text surface now asks for exactly that: `governed-worker` (evaluator method; output contract item 6 "the contract line — the LAST line of your message"; a self-reviewing seat writes no line and the gate treats that as FAIL), `qe/refs/review.md` (vocabulary + the produced-test dispatch — `MODE: produced-test` on its own line), `qe-test-code-quality-auditor` (the §8 audit line becomes `AUDIT: {p0}/{p1}/{p2} REVIEWER: … RUN_ID: …` with no VERDICT token — a smell audit certifies nothing; §9 carries the ONE contract line with `MODE:` / `REVIEWER:` / `RUN_ID:` on their own lines), `qe-security-test-engineer` (`SKIP` → `VERDICT: FAIL` + `reason: not applicable: <why>` — a review with no security surface parks at the gate instead of passing silently), `qe-a11y-test-engineer` / `qe-ai-feature-test-engineer` / `qe-snapshot-hygiene-auditor` (the footer is the grammar with `REVIEWER:` / `RUN_ID:` on their own lines; CONDITIONAL stays a ledger RECORD value, the output line is FAIL with the conditions listed above it), `domain-coverage` (gains the line: PASS iff `coverage == 1.0`, zero unaccounted and no `BLOCK`), `qe/refs/accept.md` (the acceptance reviewer writes PASS|FAIL; `VERDICT_TO_STATUS` is unchanged — PARTIAL / CONDITIONAL / INCONCLUSIVE are legacy record values readers keep mapping) and `wickedizer/refs/patterns.md` (the comparison template's `VERDICT:` heading is prose again). **Behaviour change (D-9, text half):** CONDITIONAL and SKIP are no longer verdicts — unmet conditions and not-applicable reviews FAIL into the human gate; the wicked-ledger `Verdict` enum and the DomainStore `verdict:` records are untouched (record vocabulary). **Lint:** `tests/test_skill_portability.py` gains the garden-only token `verdict-spelling` — every line-leading `VERDICT[:=]` line under `skills/**/*.md` (fenced blocks included) must be `VERDICT: PASS`, `VERDICT: FAIL` or the template `VERDICT: PASS|FAIL`; nothing that is not line-leading is judged. The legacy `VERDICT=… REVIEWER=… RUN_ID=…` footers of seven `qe-*` specialists already parse as PASS/FAIL and are tolerated ONLY in the files listed under `verdict_spelling.legacy_footer_files` in `tests/portability_rules.garden.json`; a listed file with no footer left fails the build, so the qe batches that rewrite them shrink the list in the same change. **Mixed generations:** a skills generation published from garden ≤ 12.36.0 still writes the old words and gates every evaluator unit under core-ts 0.7.27 — publish garden ≥ 12.37.0 first.
|
|
9
|
+
|
|
10
|
+
## [12.36.0] — 2026-09-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`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`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **`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.
|
|
20
|
+
|
|
5
21
|
## [12.35.0] — 2026-09-13
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -841,3 +857,6 @@ through `v9.2.18`. Major themes across that span:
|
|
|
841
857
|
condition-resolution mechanism, scope-delta HITL trigger.
|
|
842
858
|
|
|
843
859
|
For each of those releases, see the corresponding tag in git history.
|
|
860
|
+
|
|
861
|
+
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.37.0...HEAD
|
|
862
|
+
[12.37.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.36.0...v12.37.0
|
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
|
|
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
|
|
425
|
-
f"
|
|
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"
|
|
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.
|
|
3
|
+
"version": "12.37.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": [
|
package/scripts/_agents.py
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
429
|
-
# close + restart, NOT counted against the
|
|
430
|
-
|
|
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
|
|
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":
|
|
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
|
|