wicked-garden 12.48.0 → 12.50.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 +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -3
- package/CHANGELOG.md +28 -0
- package/README.md +24 -6
- package/WICKED_GARDEN_BUS_EVENTS.md +0 -6
- package/docs/extending.md +10 -0
- package/hooks/scripts/pre_compact.py +33 -54
- package/hooks/scripts/stop.py +16 -169
- package/package.json +1 -1
- package/scripts/_bus.py +2 -11
- package/scripts/_context_backend.py +18 -25
- package/scripts/_estate_client.py +106 -1
- package/scripts/_validate_registry.py +0 -1
- package/scripts/crew/_task_reader.py +1 -1
- package/scripts/editor/_pack.py +95 -0
- package/scripts/editor/conformance.py +416 -0
- package/scripts/editor/install.py +315 -0
- package/scripts/editor/pin.py +45 -0
- package/scripts/editor/scaffold.py +136 -0
- package/scripts/learn/_knowledge.py +108 -0
- package/scripts/learn/check.py +255 -0
- package/scripts/learn/index.py +138 -0
- package/scripts/mem/estate_memory.py +136 -19
- package/scripts/qe/runner/package.json +1 -1
- package/skills/core/SKILL.md +2 -0
- package/skills/core/refs/inventory-report.md +26 -0
- package/skills/editor-scaffold/SKILL.md +153 -0
- package/skills/editor-scaffold/assets/pack/README.md +32 -0
- package/skills/editor-scaffold/assets/pack/editors/__EDITOR_ID__/index.html +159 -0
- package/skills/learn/SKILL.md +104 -0
- package/skills/learn/refs/claims.md +58 -0
- package/skills/learn/refs/refresh.md +43 -0
- package/skills/mem/SKILL.md +25 -17
- package/skills/mem/refs/scopes.md +10 -8
- package/skills/mem-capture/SKILL.md +44 -19
- package/skills/platform/gh-cli/SKILL.md +7 -0
- package/skills/qe/SKILL.md +6 -2
- package/skills/qe/refs/accept.md +8 -8
- package/skills/qe/refs/evidence.md +1 -1
- package/skills/qe/refs/execute.md +1 -1
- package/skills/qe/refs/review.md +1 -1
- package/skills/qe/refs/setup.md +9 -5
- package/skills/repo-learn/SKILL.md +12 -3
- package/skills/repo-learn/refs/capture-proposals.md +45 -9
- package/skills/search/SKILL.md +9 -2
- package/skills/search/refs/answer.md +15 -4
- package/scripts/mem/auto_memorize.py +0 -494
- package/scripts/mem/session_fact_extractor.py +0 -498
|
@@ -2,7 +2,7 @@
|
|
|
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", "demo", "engineering", "jam", "mem", "persona", "platform", "product", "qe", "search"],
|
|
4
4
|
"summary": {
|
|
5
|
-
"skills":
|
|
5
|
+
"skills": 138,
|
|
6
6
|
"fork_skills": 69,
|
|
7
7
|
"hooks": 13,
|
|
8
8
|
"specialists": 7
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"domain-extractor": ["domain-extractor"],
|
|
25
25
|
"domain-modeler": ["domain-modeler"],
|
|
26
26
|
"draft": ["draft"],
|
|
27
|
+
"editor-scaffold": ["editor-scaffold"],
|
|
27
28
|
"engineering": ["architecture", "backend", "debugging", "docs", "engineering", "frontend", "integration", "large-scale-migration", "patch", "system-design", "unit-test-quality"],
|
|
28
29
|
"engineering-api-documentarian": ["engineering-api-documentarian"],
|
|
29
30
|
"engineering-migration-engineer": ["engineering-migration-engineer"],
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"jam": ["jam"],
|
|
34
35
|
"jam-brainstorm-facilitator": ["jam-brainstorm-facilitator"],
|
|
35
36
|
"jam-council": ["jam-council"],
|
|
37
|
+
"learn": ["learn"],
|
|
36
38
|
"mcp": ["mcp"],
|
|
37
39
|
"mcp-scaffold": ["mcp-scaffold"],
|
|
38
40
|
"mem": ["mem"],
|
|
@@ -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,
|
|
1
|
+
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, multi-model second opinions (fallback seats labelled). Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.50.0"}], "version": "12.50.0"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
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),
|
|
3
|
+
"version": "12.50.0",
|
|
4
|
+
"description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), multi-model second opinions (fallback seats labelled) (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
|
|
5
5
|
"wicked_vault_version": "^0.5.0",
|
|
6
|
-
"wicked_ledger_version": "^0.
|
|
6
|
+
"wicked_ledger_version": "^0.6.0",
|
|
7
7
|
"wicked_bus_version": "^2.0.0",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Mike Parcewski",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.50.0] — 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`wicked-garden-learn`: learn a topic or an event into the repository as cited knowledge (PR #1273).** It is the skill behind the `learn` workflow. The run scopes, researches with sources, synthesizes a claims ledger, walks the operator through it in chat, and authors `.wicked/knowledge/<slug>/` plus text-only skills.
|
|
9
|
+
- Every claim carries its source URL and `observed_at`.
|
|
10
|
+
- A refresh proposes only changes (confirm, supersede, contradict, retire) and never deletes.
|
|
11
|
+
- `scripts/learn/check.py` is the deterministic provenance and skill lint.
|
|
12
|
+
- `scripts/learn/index.py` replaces a topic's estate knowledge scope from merged content, and refuses inside a governed run.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **The estate client follows estate's paging cursor to the whole answer (estate#279, PR #1272).**
|
|
16
|
+
|
|
17
|
+
## [12.49.0] — 2026-10-11
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`wicked-garden-editor-scaffold`: build, check and install an artifact editor plugin (X3, PR #1269).** A new skill plus `scripts/editor/`.
|
|
21
|
+
- **Inventory-report codebook (crew#721, PR #1266).** An enumerating step says whether its list is complete.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **QE acceptance inside a governed crew run records into the run's ledger, not the worktree (wicked-crew#951, PR #1270).** When `WICKED_QE_LEDGER_DIR` is set, it is the ledger root. A governed run hands its QE unit the run's own ledger, outside the worktree, this way.
|
|
25
|
+
- The preflight config check, setup's scaffold and `config.json`, accept's evidence dir, the executor and reviewer briefs, and the vault-record argument all use that root.
|
|
26
|
+
- accept resolves it through wicked-ledger's `resolveLedgerRoot`.
|
|
27
|
+
- Before, a governed `qe accept` wrote `<worktree>/.wicked-qe`; the run's guard discarded it, the PASS with it, and delivery was refused.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **`wicked_ledger_version` is `^0.6.0`** (PR #1270): `resolveLedgerRoot` honours `WICKED_QE_LEDGER_DIR`. The QE runner requires wicked-ledger `^0.5.0` (PR #1267).
|
|
31
|
+
- Docs: guidance and the site say what ships, and what blocks vs what advises (PR #1265). Python 3.15 (PR #1268).
|
|
32
|
+
|
|
5
33
|
## [12.48.0] — 2026-10-09
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -32,11 +32,11 @@ Most plugins try to boss them around — re-implement planning, impose a workflo
|
|
|
32
32
|
| refactors on a hope and a prayer | renames across files as a **graph operation**, not find-replace roulette → wicked-patch |
|
|
33
33
|
| forgets everything at `exit` | remembers what session 1 decided when you're in session 47 → the mem domain over wicked-estate |
|
|
34
34
|
| re-derives *how to work in this repo* every task — which file owns the bug, the wiring step, the test command | loads the repo's own playbooks (`fix-bug`/`add-feature`/`verify`…), generated from HEAD → wicked-understanding |
|
|
35
|
-
| asks *itself* for a second opinion | convenes a **
|
|
35
|
+
| asks *itself* for a second opinion | convenes a **review panel** → the jam skill's `council` action: usable external CLIs (Antigravity / Codex / …) take the seats; with fewer than two it fills them with isolated same-family workers and **labels them as weaker diversity**, and below two isolated seats it reports *no quorum* instead of synthesizing |
|
|
36
36
|
| re-derives WCAG/CWE/SOC2 from memory every time | loads the rubric on demand, ships it to any repo |
|
|
37
|
-
| grades its own homework | author ≠ executor ≠ reviewer → evidence-gated testing |
|
|
37
|
+
| grades its own homework | author ≠ executor ≠ reviewer as separate workers → evidence-gated testing (the `qe` domain) |
|
|
38
38
|
|
|
39
|
-
The throughline: **done is re-derived, not asserted
|
|
39
|
+
The throughline: **done is re-derived, not asserted** — wherever the gate actually runs. Which entry paths block and which only advise is spelled out in [What blocks, what advises](#what-blocks-what-advises).
|
|
40
40
|
|
|
41
41
|
## What it's *not*
|
|
42
42
|
|
|
@@ -106,14 +106,14 @@ Optional, lights up the code graph: **wicked-estate** (single binary; `wicked-es
|
|
|
106
106
|
## Try it
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
# Just work — the hook
|
|
109
|
+
# Just work — in Claude Code the plugin's hook reads the work's shape and suggests the rigor (advisory)
|
|
110
110
|
"implement caching for the dashboard"
|
|
111
111
|
|
|
112
112
|
# Or reach for a gap-filler on purpose — everything is a skill now
|
|
113
113
|
/wicked-garden-prove # re-derive "done" from evidence (fail-closed)
|
|
114
114
|
/wicked-garden-search blast-radius emit_event # impact, incl. edges grep can't see
|
|
115
115
|
/wicked-garden-engineering-patch rename oldField newField # deterministic, graph-driven
|
|
116
|
-
/wicked-garden-jam council "redis or memcached?" # a panel
|
|
116
|
+
/wicked-garden-jam council "redis or memcached?" # a panel; it says which seats were external models
|
|
117
117
|
|
|
118
118
|
# Stamp the evidence gate into ANY repo (runs with no wicked-garden installed)
|
|
119
119
|
/wicked-garden-prove compile ~/path/to/repo --trigger ci
|
|
@@ -121,6 +121,24 @@ Optional, lights up the code graph: **wicked-estate** (single binary; `wicked-es
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## What blocks, what advises
|
|
125
|
+
|
|
126
|
+
"Done is re-derived" is a property of the **gate**, not of every surface the toolkit touches. What a
|
|
127
|
+
false "done" runs into depends on the host and the entry path:
|
|
128
|
+
|
|
129
|
+
| Host · entry path | Blocks | Only advises |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| **Any host — the gate, invoked** (`wicked-garden-prove`, an archetype's produces-gate → `scripts/qe/vault_gate.py`) | Re-hashes the recorded evidence and re-runs its verifier through `wicked-vault`: a false claim is **rejected**, an unresolvable backend **fails closed** (`gate: "unavailable"`). Hard gates also need an attestation under an explicit `--actor` that differs from the author's — a local label, not an authenticated identity. The verdict stops whatever acts on it. | With `--no-require`, a backend that cannot run falls back to the claim-only tracker and says so (`claim-only`, not re-derived); a working backend still re-derives. |
|
|
132
|
+
| **Claude Code — plugin hooks** (plain conversation, "just work") | `PreToolUse` denies a few specific tool calls: native `EnterPlanMode` (always — planning goes through the archetype playbooks), writes to `MEMORY.md` / the auto-memory dir, build-phase writes on a high-complexity crew project with no challenge artifacts, a `phase_manager.py approve` whose gate preflight fails, a worktree cwd leak, and — only in `strict` mode (default `warn`) — invalid task metadata (`WG_TASK_METADATA`) and orphan state writes (`WG_BUS_EMIT_LINT`). Prompts are held until `/wicked-garden-core setup` completes. | Everything else. The archetype detector **steers** (it does not gate), the Stop-time claim sentinel is a fail-open **nudge** when a "done/passing" claim has no verdict for HEAD, `TaskCompleted` never blocks, and output governance is advisory. A completion claim is **not** blocked unless a gate is run. |
|
|
133
|
+
| **Codex · OpenCode · Pi · Antigravity — skills only** (`npx wicked-installer install-<cli> wicked-garden`) | Nothing by itself: no plugin hooks are installed on those hosts. | The skill text tells the agent to run the gate; it blocks only when the agent does run it (and the launcher + `wicked-vault` are present). |
|
|
134
|
+
| **Compiled repo gate** (`/wicked-garden-prove compile <repo> --trigger hook,ci`) | `.wicked/gate.py` fails the **git pre-push hook / CI job** it is wired into, with no wicked-garden installed. | Without a trigger it is a script you run by hand. |
|
|
135
|
+
| **wicked-crew governed run** | crew's engine decides each gated phase deny-dominates, on the layers that actually ran — deterministic floors, repo checks, policies, a judge. Review moves to a distinct seat when the roster has an eligible one; team runs refuse without one. | An ordinary run on a too-small roster keeps review on the creator seat and labels it "evaluator ≠ creator not held"; a pinned floor with no distinct judge is labelled "floor only"; a phase nothing gated is "approved by default, not verified". |
|
|
136
|
+
|
|
137
|
+
Garden's own daemon `/council` endpoint is a single-model synthesis (v0.1); the multi-model panel is
|
|
138
|
+
the jam skill's `council` action above.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
124
142
|
## Build on it
|
|
125
143
|
|
|
126
144
|
The catalog is open: ship your own domain pack — a `wicked-pack.json`
|
|
@@ -141,7 +159,7 @@ Full author guide: [`docs/extending.md`](docs/extending.md).
|
|
|
141
159
|
## Principles
|
|
142
160
|
|
|
143
161
|
- **Don't fight the harness.** Fill the gaps; never re-implement what it already does well.
|
|
144
|
-
- **Done is re-derived, not asserted.** Every gate recomputes the evidence; the gates that matter
|
|
162
|
+
- **Done is re-derived, not asserted.** Every gate recomputes the evidence; the gates that matter need an attestation recorded under an actor other than the author (a declared label, not an authenticated identity).
|
|
145
163
|
- **Steering, not blocking.** Rigor follows the shape of the work, applied only where it earns its keep.
|
|
146
164
|
- **Enforcement that travels.** The gate compiles into any repo and runs without wicked-garden present.
|
|
147
165
|
- **Borrow the harness's primitives.** Extend `TaskCreate`/`Task()`/skills/hooks — don't rebuild them.
|
|
@@ -85,12 +85,6 @@ These fields are **stripped automatically** by `_bus.py` before emission:
|
|
|
85
85
|
|------------|-----------|-------------|
|
|
86
86
|
| `wicked.garden.quality.drift_detected` | `delivery.telemetry` | Cross-session quality metric drifted past baseline threshold (special-cause or >=15% drop) |
|
|
87
87
|
|
|
88
|
-
### Facts
|
|
89
|
-
|
|
90
|
-
| Event Type | Subdomain | Description |
|
|
91
|
-
|------------|-----------|-------------|
|
|
92
|
-
| `wicked.garden.fact.extracted` | `facts` | Structured fact extracted from conversation (consumed by the garden-run auto-memorize drain -> estate memory) |
|
|
93
|
-
|
|
94
88
|
### Gate
|
|
95
89
|
|
|
96
90
|
| Event Type | Subdomain | Description |
|
package/docs/extending.md
CHANGED
|
@@ -97,6 +97,16 @@ Manifest `spec: 2` keeps every spec-1 field and adds two arrays. A spec-1 pack s
|
|
|
97
97
|
wicked-studio and first-party blocks are core presets. Example (garden's own spec-2 fixture):
|
|
98
98
|
[`tests/fixtures/packs/acme-terms/`](../tests/fixtures/packs/acme-terms/).
|
|
99
99
|
|
|
100
|
+
**Building an editor-only pack.** The `wicked-garden-editor-scaffold` skill writes one
|
|
101
|
+
(`scripts/editor/scaffold.py`), from a template that already speaks `wicked.editor/1`; `scripts/editor/pin.py`
|
|
102
|
+
re-pins the entry hash after each edit; `scripts/editor/conformance.py` runs the plugin's conformance
|
|
103
|
+
cases in headless Chromium inside studio's own conformance host page; and `scripts/editor/install.py`
|
|
104
|
+
stages the pack under `~/.wicked/editors/<name>/current/` and installs it into a wicked-crew daemon,
|
|
105
|
+
which re-runs `pack check` and the conformance cases itself before it records the editor (fail closed:
|
|
106
|
+
no headless browser is a refusal). A pack that also ships skills or blocks is not installable yet: their
|
|
107
|
+
own approval step is still to come. The governed route is the `editor-plugin` workflow (studio
|
|
108
|
+
Settings → Developer → Create an editor plugin).
|
|
109
|
+
|
|
100
110
|
## Validate: the shipped conformance gate
|
|
101
111
|
|
|
102
112
|
```bash
|
|
@@ -5,10 +5,9 @@ PreCompact hook — wicked-garden WIP snapshot before context compression.
|
|
|
5
5
|
v6: the v5 ticket-rail preservation path (HistoryCondenser + PressureTracker)
|
|
6
6
|
was removed with smaht/v2 in #428. The remaining jobs are:
|
|
7
7
|
1. Stamp SessionState.last_compact_ts (dedup guard)
|
|
8
|
-
2. Save
|
|
9
|
-
memory.capture)
|
|
10
|
-
|
|
11
|
-
3. Prompt Claude to store any additional memories before context is lost
|
|
8
|
+
2. Save the native in-progress task subjects as a project-scoped working
|
|
9
|
+
memory (wicked-estate memory.capture), only when there are any
|
|
10
|
+
3. Prompt Claude to store any durable learning before context is lost
|
|
12
11
|
|
|
13
12
|
Always fails open — any unhandled exception returns {"continue": true}.
|
|
14
13
|
"""
|
|
@@ -17,7 +16,6 @@ import json
|
|
|
17
16
|
import os
|
|
18
17
|
import sys
|
|
19
18
|
import time
|
|
20
|
-
import uuid as _uuid_mod
|
|
21
19
|
from datetime import datetime, timezone
|
|
22
20
|
from pathlib import Path
|
|
23
21
|
|
|
@@ -62,65 +60,44 @@ def _read_in_progress_tasks(session_id, limit=10):
|
|
|
62
60
|
return []
|
|
63
61
|
|
|
64
62
|
|
|
65
|
-
def _build_wip_markdown(
|
|
66
|
-
"""Format
|
|
63
|
+
def _build_wip_markdown(active_project, in_progress):
|
|
64
|
+
"""Format the in-progress task state as markdown, capped at _MAX_WIP_CHARS."""
|
|
67
65
|
sections = ["## WIP State (Pre-Compaction)"]
|
|
68
|
-
|
|
69
|
-
active_project = session_state_dict.get("active_project") or ""
|
|
70
66
|
if active_project:
|
|
71
67
|
sections.append(f"### Active Project\n{active_project}")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if turn_count:
|
|
75
|
-
sections.append(f"### Turn Count\n{turn_count}")
|
|
76
|
-
|
|
77
|
-
if in_progress:
|
|
78
|
-
items = "\n".join(f"- {s}" for s in in_progress)
|
|
79
|
-
sections.append(f"### In-Progress Tasks\n{items}")
|
|
80
|
-
|
|
68
|
+
items = "\n".join(f"- {s}" for s in in_progress)
|
|
69
|
+
sections.append(f"### In-Progress Tasks\n{items}")
|
|
81
70
|
content = "\n\n".join(sections)
|
|
82
71
|
if len(content) > _MAX_WIP_CHARS:
|
|
83
72
|
content = content[:_MAX_WIP_CHARS - 3] + "..."
|
|
84
73
|
return content
|
|
85
74
|
|
|
86
75
|
|
|
87
|
-
def _save_wip_state(session_id
|
|
88
|
-
"""Save
|
|
76
|
+
def _save_wip_state(session_id):
|
|
77
|
+
"""Save the in-progress task state as a project-scoped working memory.
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
_context_backend.capture_memory
|
|
79
|
+
Writes nothing unless the session has real in-progress tasks: a turn count
|
|
80
|
+
or an active-project name alone is not state worth restoring (that wrote
|
|
81
|
+
one empty "WIP snapshot" per compaction). The write goes through
|
|
82
|
+
_context_backend.capture_memory, which always scopes it to the current
|
|
83
|
+
project; estate expires working memories after 24 h.
|
|
84
|
+
Returns the memory id, or None when nothing was written.
|
|
94
85
|
"""
|
|
86
|
+
in_progress = [t for t in _read_in_progress_tasks(session_id, limit=10) if str(t).strip()]
|
|
87
|
+
if not in_progress:
|
|
88
|
+
_log("context", "debug", "pre_compact.no_wip")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
active_project = ""
|
|
95
92
|
try:
|
|
96
93
|
from _session import SessionState
|
|
97
|
-
|
|
94
|
+
active_project = getattr(SessionState.load(), "active_project", "") or ""
|
|
98
95
|
except Exception:
|
|
99
|
-
|
|
96
|
+
pass
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
if state:
|
|
103
|
-
ss_fields = {
|
|
104
|
-
"active_project": getattr(state, "active_project", ""),
|
|
105
|
-
"active_project_id": getattr(state, "active_project_id", ""),
|
|
106
|
-
"turn_count": getattr(state, "turn_count", 0),
|
|
107
|
-
"failure_counts": getattr(state, "failure_counts", None),
|
|
108
|
-
"bash_count": getattr(state, "bash_count", 0),
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
in_progress = _read_in_progress_tasks(session_id, limit=10)
|
|
112
|
-
|
|
113
|
-
# Nothing to save if we have no signal at all
|
|
114
|
-
if not any(ss_fields.values()) and not in_progress:
|
|
115
|
-
_log("context", "debug", "pre_compact.empty_wip")
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
content = _build_wip_markdown(ss_fields, in_progress)
|
|
119
|
-
active_project = ss_fields.get("active_project") or ""
|
|
98
|
+
content = _build_wip_markdown(active_project, in_progress)
|
|
120
99
|
title = f"WIP: {active_project or 'Session work'} — pre-compaction snapshot"
|
|
121
|
-
|
|
122
100
|
try:
|
|
123
|
-
# Estate memory.capture via the context backend.
|
|
124
101
|
from _context_backend import capture_memory
|
|
125
102
|
chunk_id = capture_memory(
|
|
126
103
|
title=title,
|
|
@@ -131,8 +108,10 @@ def _save_wip_state(session_id, project):
|
|
|
131
108
|
if chunk_id:
|
|
132
109
|
_log("context", "verbose", "pre_compact.wip_saved", ok=True,
|
|
133
110
|
detail={"title": title[:60], "chars": len(content)})
|
|
111
|
+
return chunk_id
|
|
134
112
|
except Exception as e:
|
|
135
113
|
print(f"[wicked-garden] pre_compact WIP save error: {e}", file=sys.stderr)
|
|
114
|
+
return None
|
|
136
115
|
|
|
137
116
|
|
|
138
117
|
def main():
|
|
@@ -148,8 +127,7 @@ def main():
|
|
|
148
127
|
# Normal-level log fires regardless of log level (AC-07)
|
|
149
128
|
_log("context", "normal", "pre_compact")
|
|
150
129
|
|
|
151
|
-
session_id = os.environ.get("CLAUDE_SESSION_ID") or
|
|
152
|
-
project = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
|
|
130
|
+
session_id = input_data.get("session_id") or os.environ.get("CLAUDE_SESSION_ID") or ""
|
|
153
131
|
|
|
154
132
|
# Dedup guard: skip WIP save if compaction happened <60s ago
|
|
155
133
|
skip_wip = False
|
|
@@ -169,9 +147,10 @@ def main():
|
|
|
169
147
|
except Exception:
|
|
170
148
|
state = None
|
|
171
149
|
|
|
172
|
-
# Save
|
|
150
|
+
# Save the in-progress task state (unless dedup guard triggered)
|
|
151
|
+
saved = None
|
|
173
152
|
if not skip_wip:
|
|
174
|
-
_save_wip_state(session_id
|
|
153
|
+
saved = _save_wip_state(session_id)
|
|
175
154
|
|
|
176
155
|
# v6: PressureTracker was deleted with smaht/v2 in #428. There is no
|
|
177
156
|
# cumulative-byte pressure model to reset — the pull-model architecture
|
|
@@ -199,9 +178,9 @@ def main():
|
|
|
199
178
|
print(json.dumps({
|
|
200
179
|
"continue": True,
|
|
201
180
|
"systemMessage": (
|
|
202
|
-
"[Memory] Context compression imminent.
|
|
203
|
-
"
|
|
204
|
-
f"Store any
|
|
181
|
+
"[Memory] Context compression imminent. "
|
|
182
|
+
+ ("In-progress task state was saved as a working memory. " if saved else "")
|
|
183
|
+
+ f"Store any durable decision or gotcha NOW with {_mem_target}."
|
|
205
184
|
),
|
|
206
185
|
}))
|
|
207
186
|
|
package/hooks/scripts/stop.py
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Stop hook — wicked-garden unified session teardown (async, 30s timeout).
|
|
4
4
|
|
|
5
|
-
Consolidates: crew stop, mem stop
|
|
5
|
+
Consolidates: crew stop, mem stop.
|
|
6
|
+
|
|
7
|
+
The hook writes no memories: what a session learned is captured on purpose
|
|
8
|
+
(the wicked-garden-mem skill's capture/store actions), never pattern-matched
|
|
9
|
+
out of the transcript.
|
|
6
10
|
|
|
7
11
|
Flow:
|
|
8
12
|
1. Session outcome check (mismatch report from auto_issue_reporter state)
|
|
9
|
-
2.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
4. Event store retention purge
|
|
13
|
-
5. Heavy cadence teardown (telemetry + guard pipeline, once per session)
|
|
13
|
+
2. Persist final SessionState
|
|
14
|
+
3. Event store retention purge
|
|
15
|
+
4. Heavy cadence teardown (telemetry + guard pipeline, once per session)
|
|
14
16
|
|
|
15
17
|
Always fails open — any unhandled exception returns {"systemMessage": ...}.
|
|
16
18
|
Runs async so it does NOT block the user on exit.
|
|
@@ -18,7 +20,6 @@ Runs async so it does NOT block the user on exit.
|
|
|
18
20
|
|
|
19
21
|
import json
|
|
20
22
|
import os
|
|
21
|
-
import subprocess
|
|
22
23
|
import sys
|
|
23
24
|
import time
|
|
24
25
|
from pathlib import Path
|
|
@@ -248,129 +249,6 @@ def _check_session_outcome() -> list:
|
|
|
248
249
|
|
|
249
250
|
|
|
250
251
|
|
|
251
|
-
# ---------------------------------------------------------------------------
|
|
252
|
-
# Step 2b: Session-end fact emission → auto-memorize drain (estate memory)
|
|
253
|
-
# ---------------------------------------------------------------------------
|
|
254
|
-
#
|
|
255
|
-
# v6: Session facts are extracted from native Claude tasks
|
|
256
|
-
# (${CLAUDE_CONFIG_DIR:-~/.claude}/tasks/{session_id}/*.json) by
|
|
257
|
-
# scripts/mem/session_fact_extractor.py. This replaces the v5 smaht
|
|
258
|
-
# FactExtractor/HistoryCondenser pipeline deleted in Gate 4 Phase 2 (#428).
|
|
259
|
-
# Emission shape: wicked.garden.fact.extracted events on wicked-bus, drained
|
|
260
|
-
# by the garden-run auto-memorize consumer (scripts/mem/auto_memorize.py),
|
|
261
|
-
# which re-applies its own type/length/dedup policy.
|
|
262
|
-
|
|
263
|
-
# Fact types eligible for emission — mirrors the auto-memorize policy.
|
|
264
|
-
# The drain will re-check these; this pre-filter keeps bus traffic sensible.
|
|
265
|
-
_EMITTABLE_FACT_TYPES = frozenset({"decision", "discovery"})
|
|
266
|
-
_MIN_FACT_CONTENT_LENGTH = 15
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def _run_memory_promotion(session_id: str, transcript_path: Optional[str] = None) -> list:
|
|
270
|
-
"""Emit high-value session facts as wicked.garden.fact.extracted events on wicked-bus.
|
|
271
|
-
|
|
272
|
-
Reads native task records (TaskCreate/TaskUpdate output) plus the session
|
|
273
|
-
transcript tail via scripts/mem/session_fact_extractor.py,
|
|
274
|
-
filters to decisions + discoveries over the length threshold, skips facts
|
|
275
|
-
this session already emitted (per-session ledger — Stop fires every turn),
|
|
276
|
-
and emits one event per fact. The garden-run auto-memorize consumer
|
|
277
|
-
(scripts/mem/auto_memorize.py) persists them to wicked-estate memory.
|
|
278
|
-
|
|
279
|
-
Returns a list of message strings (empty when nothing was emitted or the
|
|
280
|
-
bus is unavailable). Always fails open — exceptions are caught and logged
|
|
281
|
-
to stderr.
|
|
282
|
-
"""
|
|
283
|
-
try:
|
|
284
|
-
# scripts/ is on sys.path via the hook bootstrap; mem is a package
|
|
285
|
-
# under scripts/ so the qualified import resolves without any
|
|
286
|
-
# additional sys.path manipulation.
|
|
287
|
-
from mem.session_fact_extractor import (
|
|
288
|
-
extract_session_facts,
|
|
289
|
-
filter_unemitted,
|
|
290
|
-
mark_emitted,
|
|
291
|
-
)
|
|
292
|
-
from _bus import emit_event
|
|
293
|
-
|
|
294
|
-
facts = extract_session_facts(session_id, limit=20, transcript_path=transcript_path)
|
|
295
|
-
facts = filter_unemitted(facts, session_id)
|
|
296
|
-
if not facts:
|
|
297
|
-
if os.environ.get("WICKED_DEBUG"):
|
|
298
|
-
print(
|
|
299
|
-
f"[wicked-garden] fact emit: no promotable facts in session {session_id}",
|
|
300
|
-
file=sys.stderr,
|
|
301
|
-
)
|
|
302
|
-
return []
|
|
303
|
-
|
|
304
|
-
emitted = 0
|
|
305
|
-
emitted_facts = []
|
|
306
|
-
for fact in facts:
|
|
307
|
-
if fact.type not in _EMITTABLE_FACT_TYPES:
|
|
308
|
-
continue
|
|
309
|
-
if len(fact.content) < _MIN_FACT_CONTENT_LENGTH:
|
|
310
|
-
continue
|
|
311
|
-
emit_event(
|
|
312
|
-
"wicked.garden.fact.extracted",
|
|
313
|
-
{
|
|
314
|
-
"type": fact.type,
|
|
315
|
-
"content": fact.content,
|
|
316
|
-
"entities": list(fact.entities)[:5] if fact.entities else [],
|
|
317
|
-
"source": fact.source or "task",
|
|
318
|
-
"session_id": session_id,
|
|
319
|
-
},
|
|
320
|
-
)
|
|
321
|
-
emitted += 1
|
|
322
|
-
emitted_facts.append(fact)
|
|
323
|
-
|
|
324
|
-
if emitted == 0:
|
|
325
|
-
return []
|
|
326
|
-
mark_emitted(emitted_facts, session_id)
|
|
327
|
-
return [f"[Memory] Emitted {emitted} fact event(s); the auto-memorize drain persists eligible ones to estate."]
|
|
328
|
-
|
|
329
|
-
except Exception as e:
|
|
330
|
-
print(f"[wicked-garden] fact emit error: {e}", file=sys.stderr)
|
|
331
|
-
return []
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def _run_auto_memorize_drain() -> list:
|
|
335
|
-
"""Drain pending fact events into estate memory (garden-run consumer).
|
|
336
|
-
|
|
337
|
-
Replaces wicked-brain's server-side auto-memorize subscriber (S7): spawns
|
|
338
|
-
scripts/mem/auto_memorize.py, which polls the durable wicked-garden-mem
|
|
339
|
-
cursor, promotes + dedups each fact, and writes via estate memory.capture.
|
|
340
|
-
A failed estate write leaves the cursor put (at-least-once — next Stop
|
|
341
|
-
retries; three failed drains dead-letter the event natively). Always fails
|
|
342
|
-
open and never blocks session end.
|
|
343
|
-
"""
|
|
344
|
-
try:
|
|
345
|
-
from _bus import _check_available
|
|
346
|
-
if not _check_available():
|
|
347
|
-
return []
|
|
348
|
-
except Exception:
|
|
349
|
-
return []
|
|
350
|
-
try:
|
|
351
|
-
script = _PLUGIN_ROOT / "scripts" / "mem" / "auto_memorize.py"
|
|
352
|
-
if not script.is_file():
|
|
353
|
-
return []
|
|
354
|
-
proc = subprocess.run(
|
|
355
|
-
[sys.executable, str(script), "drain", "{}"],
|
|
356
|
-
capture_output=True, text=True, timeout=25,
|
|
357
|
-
)
|
|
358
|
-
if proc.returncode != 0 or not proc.stdout.strip():
|
|
359
|
-
return []
|
|
360
|
-
result = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
361
|
-
stored = result.get("stored", 0)
|
|
362
|
-
dead = result.get("dead_lettered", 0)
|
|
363
|
-
if stored or dead:
|
|
364
|
-
msg = f"[Memory] Auto-memorized {stored} fact(s) into estate"
|
|
365
|
-
if dead:
|
|
366
|
-
msg += f"; {dead} dead-lettered (see `wicked-bus dlq list`)"
|
|
367
|
-
return [msg + "."]
|
|
368
|
-
return []
|
|
369
|
-
except Exception as e:
|
|
370
|
-
print(f"[wicked-garden] auto-memorize drain error: {e}", file=sys.stderr)
|
|
371
|
-
return []
|
|
372
|
-
|
|
373
|
-
|
|
374
252
|
def _get_turn_count() -> int:
|
|
375
253
|
try:
|
|
376
254
|
from _session import SessionState
|
|
@@ -537,30 +415,13 @@ def main():
|
|
|
537
415
|
# 1. Session outcome check
|
|
538
416
|
outcome_messages = _check_session_outcome()
|
|
539
417
|
|
|
540
|
-
#
|
|
541
|
-
# KEPT in stop.py per-turn — lightweight (single bus emit per fact),
|
|
542
|
-
# self-reports cleanly, and the next session's bootstrap relies on
|
|
543
|
-
# memories being current. Heavy cadence work (decay/consolidation/
|
|
544
|
-
# telemetry/guard) was extracted in v9.2.15 — see _heavy_cadence.py.
|
|
545
|
-
promotion_messages = _run_memory_promotion(
|
|
546
|
-
session_id, input_data.get("transcript_path")
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
# 2c. Auto-memorize drain — the garden-run consumer of the fact events
|
|
550
|
-
# (it replaced the retired wicked-brain server subscriber at S7).
|
|
551
|
-
# Durable cursor: events
|
|
552
|
-
# emitted here (or left over from earlier sessions) land in estate
|
|
553
|
-
# memory; failures redeliver next Stop.
|
|
554
|
-
drain_messages = _run_auto_memorize_drain()
|
|
555
|
-
promotion_messages = promotion_messages + drain_messages
|
|
556
|
-
|
|
557
|
-
# 4. Persist session state
|
|
418
|
+
# 2. Persist session state
|
|
558
419
|
_persist_session_state()
|
|
559
420
|
|
|
560
|
-
#
|
|
421
|
+
# 3. Event store retention purge
|
|
561
422
|
_purge_old_events()
|
|
562
423
|
|
|
563
|
-
#
|
|
424
|
+
# 4. Heavy cadence — Stop is a reliable co-primary carrier of the
|
|
564
425
|
# teardown (v9.2.16, #842). SessionEnd fires on <40% of exits (~1% for
|
|
565
426
|
# agents), so it can't be the sole primary. Stop fires every turn and IS
|
|
566
427
|
# reliable, so it now runs the SAME heavy work, guarded by
|
|
@@ -600,24 +461,10 @@ def main():
|
|
|
600
461
|
except Exception:
|
|
601
462
|
pass
|
|
602
463
|
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
# branch ever fired. The "smart" branching had never executed in
|
|
608
|
-
# production.
|
|
609
|
-
# - _run_memory_promotion already self-reports when facts are
|
|
610
|
-
# auto-extracted ("[Memory] Emitted N fact event(s)..."), so the
|
|
611
|
-
# reflection's "store a memory" guidance was duplicate noise.
|
|
612
|
-
# - The brainstorm in v9.2.15 chose Option A (delete) over Option B
|
|
613
|
-
# (fix the parser + re-gate) because the smart branch is dead
|
|
614
|
-
# code defending behavior no user has seen. CLAUDE.md's "Memory
|
|
615
|
-
# Management" section already tells Claude to store decisions
|
|
616
|
-
# (now via the wicked-garden-mem skill); an end-of-turn reminder
|
|
617
|
-
# adds no signal.
|
|
618
|
-
# `tasks_completed_this_session` is no longer read; the variable
|
|
619
|
-
# remains in scope above for any future use without breaking the
|
|
620
|
-
# `session_state` load.
|
|
464
|
+
# The v9.2.15 [Memory] reflection block is gone (an end-of-turn
|
|
465
|
+
# "store a memory" reminder added no signal), and since w9-memfix the
|
|
466
|
+
# hook writes no memories at all. `tasks_completed_this_session` feeds
|
|
467
|
+
# only the session.end log line below.
|
|
621
468
|
reflection = ""
|
|
622
469
|
# for this session (post-first-Stop) — only join with separator if both
|
|
623
470
|
# halves have content.
|
|
@@ -626,7 +473,7 @@ def main():
|
|
|
626
473
|
# of the turn (avoids clobbering session_ended).
|
|
627
474
|
sentinel_messages = _check_claim_sentinel(input_data)
|
|
628
475
|
outgov_messages = _check_outgov_compliance(input_data)
|
|
629
|
-
prepend_messages = sentinel_messages + outgov_messages + outcome_messages +
|
|
476
|
+
prepend_messages = sentinel_messages + outgov_messages + outcome_messages + consolidation_messages + telemetry_messages + guard_messages
|
|
630
477
|
if prepend_messages and reflection:
|
|
631
478
|
final_message = "\n".join(prepend_messages) + "\n\n" + reflection
|
|
632
479
|
elif prepend_messages:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.50.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/_bus.py
CHANGED
|
@@ -290,12 +290,6 @@ BUS_EVENT_MAP: Dict[str, Dict[str, str]] = {
|
|
|
290
290
|
"subdomain": "crew.yolo",
|
|
291
291
|
"description": "Yolo auto-approval revoked due to scope-increase mutation (audit + observability)",
|
|
292
292
|
},
|
|
293
|
-
# Garden fact stream — scripts/mem/session_fact_extractor.py → auto-memorize subscriber
|
|
294
|
-
"wicked.garden.fact.extracted": {
|
|
295
|
-
"domain": "wicked-garden",
|
|
296
|
-
"subdomain": "facts",
|
|
297
|
-
"description": "Structured fact extracted from conversation (consumed by the garden-run auto-memorize drain -> estate memory)",
|
|
298
|
-
},
|
|
299
293
|
# qe integration — test-lifecycle verdict event (#549, AC-25).
|
|
300
294
|
# Renamed to the wicked-bus SPEC form wicked.test.<noun>.<past-verb>
|
|
301
295
|
# (was wicked.verdict.recorded). Domain stamp rebranded wicked-testing → qe
|
|
@@ -429,8 +423,6 @@ _PAYLOAD_DENY_LIST = frozenset({
|
|
|
429
423
|
# named fields passed through the deny-list unchanged. Keep this list short and
|
|
430
424
|
# justified: every entry is a deliberate carve-out auditable at review time.
|
|
431
425
|
#
|
|
432
|
-
# wicked.garden.fact.extracted — content is the whole point of the event. The brain
|
|
433
|
-
# auto-memorize subscriber requires payload.content to produce a memory.
|
|
434
426
|
# wicked.garden.dispatch.log_entry_appended — `raw_payload` is the canonical JSONL
|
|
435
427
|
# bytes the projector replays into the `dispatch_log_entries` table under
|
|
436
428
|
# Site 1 of the bus-cutover (#746). Without it the projector cannot
|
|
@@ -438,7 +430,6 @@ _PAYLOAD_DENY_LIST = frozenset({
|
|
|
438
430
|
# only ships an already-on-disk dispatch record (HMAC-signed), so payload
|
|
439
431
|
# inspection here is bounded by what the orphan check already trusts.
|
|
440
432
|
_PAYLOAD_ALLOW_OVERRIDES: Dict[str, frozenset] = {
|
|
441
|
-
"wicked.garden.fact.extracted": frozenset({"content"}),
|
|
442
433
|
"wicked.garden.dispatch.log_entry_appended": frozenset({"raw_payload"}),
|
|
443
434
|
# Site 2 of bus-cutover (#746): the consensus report and evidence emits
|
|
444
435
|
# carry the canonical on-disk JSON bytes via `raw_payload` so the
|
|
@@ -656,8 +647,8 @@ def _sanitize_payload(payload: Dict[str, Any], event_type: str = "") -> Dict[str
|
|
|
656
647
|
|
|
657
648
|
Per-event allow overrides (see _PAYLOAD_ALLOW_OVERRIDES) let specific event
|
|
658
649
|
types pass named denied fields through — these are explicit, audited carve-outs
|
|
659
|
-
for events whose contract requires a denied key (e.g.
|
|
660
|
-
|
|
650
|
+
for events whose contract requires a denied key (e.g. the dispatch log's
|
|
651
|
+
raw_payload, which the projector replays byte-for-byte).
|
|
661
652
|
"""
|
|
662
653
|
allow = _PAYLOAD_ALLOW_OVERRIDES.get(event_type, frozenset())
|
|
663
654
|
return {k: v for k, v in payload.items() if k not in _PAYLOAD_DENY_LIST or k in allow}
|