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.
Files changed (49) hide show
  1. package/.claude-plugin/components.json +3 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +3 -3
  4. package/CHANGELOG.md +28 -0
  5. package/README.md +24 -6
  6. package/WICKED_GARDEN_BUS_EVENTS.md +0 -6
  7. package/docs/extending.md +10 -0
  8. package/hooks/scripts/pre_compact.py +33 -54
  9. package/hooks/scripts/stop.py +16 -169
  10. package/package.json +1 -1
  11. package/scripts/_bus.py +2 -11
  12. package/scripts/_context_backend.py +18 -25
  13. package/scripts/_estate_client.py +106 -1
  14. package/scripts/_validate_registry.py +0 -1
  15. package/scripts/crew/_task_reader.py +1 -1
  16. package/scripts/editor/_pack.py +95 -0
  17. package/scripts/editor/conformance.py +416 -0
  18. package/scripts/editor/install.py +315 -0
  19. package/scripts/editor/pin.py +45 -0
  20. package/scripts/editor/scaffold.py +136 -0
  21. package/scripts/learn/_knowledge.py +108 -0
  22. package/scripts/learn/check.py +255 -0
  23. package/scripts/learn/index.py +138 -0
  24. package/scripts/mem/estate_memory.py +136 -19
  25. package/scripts/qe/runner/package.json +1 -1
  26. package/skills/core/SKILL.md +2 -0
  27. package/skills/core/refs/inventory-report.md +26 -0
  28. package/skills/editor-scaffold/SKILL.md +153 -0
  29. package/skills/editor-scaffold/assets/pack/README.md +32 -0
  30. package/skills/editor-scaffold/assets/pack/editors/__EDITOR_ID__/index.html +159 -0
  31. package/skills/learn/SKILL.md +104 -0
  32. package/skills/learn/refs/claims.md +58 -0
  33. package/skills/learn/refs/refresh.md +43 -0
  34. package/skills/mem/SKILL.md +25 -17
  35. package/skills/mem/refs/scopes.md +10 -8
  36. package/skills/mem-capture/SKILL.md +44 -19
  37. package/skills/platform/gh-cli/SKILL.md +7 -0
  38. package/skills/qe/SKILL.md +6 -2
  39. package/skills/qe/refs/accept.md +8 -8
  40. package/skills/qe/refs/evidence.md +1 -1
  41. package/skills/qe/refs/execute.md +1 -1
  42. package/skills/qe/refs/review.md +1 -1
  43. package/skills/qe/refs/setup.md +9 -5
  44. package/skills/repo-learn/SKILL.md +12 -3
  45. package/skills/repo-learn/refs/capture-proposals.md +45 -9
  46. package/skills/search/SKILL.md +9 -2
  47. package/skills/search/refs/answer.md +15 -4
  48. package/scripts/mem/auto_memorize.py +0 -494
  49. 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": 136,
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, 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.48.0"}], "version": "12.48.0"}
1
+ {"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, multi-model second opinions (fallback seats labelled). Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.50.0"}], "version": "12.50.0"}
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.48.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), 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.",
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.4.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 **real multi-model panel** (Antigravity / Codex / …) → the jam skill's `council` action |
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.** Verdicts you can trust on the first read — not green checkmarks you can't.
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 applies the right rigor underneath, quietly
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 that isn't just you
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 are signed by someone who isn't the author.
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 a lightweight WIP memory to the context backend (wicked-estate
9
- memory.capture) using SessionState + native in-progress task subjects
10
- as the input
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(session_state_dict, in_progress):
66
- """Format lightweight WIP state as markdown, capped at _MAX_WIP_CHARS."""
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
- turn_count = session_state_dict.get("turn_count")
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, project):
88
- """Save a lightweight WIP memory to the routed context backend.
76
+ def _save_wip_state(session_id):
77
+ """Save the in-progress task state as a project-scoped working memory.
89
78
 
90
- v6: no HistoryCondenser ticket rail. Input is SessionState + native
91
- in-progress task subjects. The richer v5 snapshot (decisions, file scope,
92
- open questions) is gone. S4: the write routes through
93
- _context_backend.capture_memory (estate primary).
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
- state = SessionState.load()
94
+ active_project = getattr(SessionState.load(), "active_project", "") or ""
98
95
  except Exception:
99
- state = None
96
+ pass
100
97
 
101
- ss_fields = {}
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 f"sess_{_uuid_mod.uuid4().hex[:8]}"
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 structured WIP state (unless dedup guard triggered)
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, project)
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. WIP state has been auto-saved. "
203
- "After compaction, your WIP will be automatically restored on the next prompt. "
204
- f"Store any additional decisions or patterns NOW with {_mem_target}."
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
 
@@ -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, session-end fact emission.
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. Automatic memory promotion from native tasks (session_fact_extractor)
10
- + auto-memorize drain into estate memory
11
- 3. Persist final SessionState
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
- # 2b. Automatic memory promotion (fail open — never blocks session end)
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
- # 5. Event store retention purge
421
+ # 3. Event store retention purge
561
422
  _purge_old_events()
562
423
 
563
- # 6. Heavy cadence — Stop is a reliable co-primary carrier of the
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
- # NOTE (v9.2.15): the [Memory] reflection block was deleted.
604
- # - It had a parser bug: looked for substring "Auto-extracted " but
605
- # _run_memory_promotion emits "Emitted N fact event(s)" — so
606
- # `promoted_count` was ALWAYS 0 and only the fallback prompt-mode
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 + promotion_messages + consolidation_messages + telemetry_messages + guard_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.48.0",
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. wicked.garden.fact.extracted
660
- must ship content to the brain auto-memorize subscriber).
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}