wicked-garden 12.49.0 → 12.51.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 +2 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +0 -6
- 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/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 +1 -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/qe/refs/setup.md +12 -4
- 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
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"jam": ["jam"],
|
|
35
35
|
"jam-brainstorm-facilitator": ["jam-brainstorm-facilitator"],
|
|
36
36
|
"jam-council": ["jam-council"],
|
|
37
|
+
"learn": ["learn"],
|
|
37
38
|
"mcp": ["mcp"],
|
|
38
39
|
"mcp-scaffold": ["mcp-scaffold"],
|
|
39
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, 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.
|
|
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.51.0"}], "version": "12.51.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.51.0",
|
|
4
4
|
"description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), multi-model second opinions (fallback seats labelled) (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
|
|
5
5
|
"wicked_vault_version": "^0.5.0",
|
|
6
6
|
"wicked_ledger_version": "^0.6.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.51.0] — 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`wicked-garden-qe` setup scaffolds the ledger root with Python (wicked-core#881, wicked-crew#951, PR #1277).** It no longer uses a shell `mkdir` of `$WICKED_QE_LEDGER_DIR`. A governed run's write fence judges a shell write target as written, and the unexpanded variable read as a path inside the worktree, so a governed QE unit's scaffold was refused (rig run 46fb2c04). With wicked-core#889 (core-ts 0.9.2), the QE unit runs garden's pipeline into its own roots.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [12.50.0] — 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`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.
|
|
15
|
+
- Every claim carries its source URL and `observed_at`.
|
|
16
|
+
- A refresh proposes only changes (confirm, supersede, contradict, retire) and never deletes.
|
|
17
|
+
- `scripts/learn/check.py` is the deterministic provenance and skill lint.
|
|
18
|
+
- `scripts/learn/index.py` replaces a topic's estate knowledge scope from merged content, and refuses inside a governed run.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **The estate client follows estate's paging cursor to the whole answer (estate#279, PR #1272).**
|
|
22
|
+
- **Memory creation keeps learnings, not narration (w9-memfix, PR #1274).**
|
|
23
|
+
- The Stop hook's regex fact extraction and its auto-memorize drain are removed: `scripts/mem/session_fact_extractor.py`, `scripts/mem/auto_memorize.py`, and the `wicked.garden.fact.extracted` event. They filed about 333 sentence fragments as semantic facts.
|
|
24
|
+
- `pre_compact` saves a working memory only when tasks are in progress, always scoped `project:<cwd-basename>` (it wrote one empty, unscoped "WIP snapshot" per compaction).
|
|
25
|
+
- Memory recall (`recall`, `sources`, the hooks' context backend) defaults to the current project and its ancestors; every scope is the explicit `scope_prefix: ""` opt-in. `recall`/`sources` pass the repo intent, so faceted repo-learn memories are reachable, and `sources` labels every item `memory · <kind> · <date> · <scope>` or `knowledge · src <source>`.
|
|
26
|
+
- `store` / `capture-batch` refuse non-durable content (SHAs, versions, PR/run ids, PIDs, "today", first-person narration, under 40 chars) and source-cited claims (knowledge: use `write`). mem-capture and mem store apply the durability and told/witnessed/decided tests; the event → episode row is gone.
|
|
27
|
+
- Shim proposals need `facets.project`; repo-learn proposes code facts as `kind_type: "knowledge"` (`content, class, source, source_rev`) and never graph metrics.
|
|
28
|
+
|
|
5
29
|
## [12.49.0] — 2026-10-11
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -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 |
|
|
@@ -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.51.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}
|
|
@@ -70,35 +70,29 @@ _RRF_K = 60 # standard reciprocal-rank-fusion constant
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def _memory_scope() -> str:
|
|
73
|
-
"""Scope for estate memory capture/recall.
|
|
73
|
+
"""Scope for estate memory capture/recall: the current project.
|
|
74
74
|
|
|
75
|
-
Default
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
Default ``project:<cwd-basename>`` (``_estate_client.project_scope``), so
|
|
76
|
+
no hook ever writes an unscoped memory, and recall sees this project plus
|
|
77
|
+
its ancestors (root-scoped memories included) but not other projects.
|
|
78
|
+
Override via WICKED_ESTATE_MEMORY_SCOPE.
|
|
78
79
|
"""
|
|
79
|
-
|
|
80
|
+
pinned = os.environ.get("WICKED_ESTATE_MEMORY_SCOPE", "").strip()
|
|
81
|
+
if pinned:
|
|
82
|
+
return pinned
|
|
83
|
+
import _estate_client
|
|
84
|
+
|
|
85
|
+
return _estate_client.project_scope()
|
|
80
86
|
|
|
81
87
|
|
|
82
88
|
def _memory_scope_prefix() -> Optional[str]:
|
|
83
89
|
"""Subtree filter for estate memory recall (estate #98 ``scope_prefix``).
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
``scope_prefix`` REPLACES that filter with subtree matching; the default
|
|
89
|
-
``""`` (root subtree) sees ALL memories: root + every migrated subtree.
|
|
90
|
-
|
|
91
|
-
When the operator pinned a custom recall scope via
|
|
92
|
-
WICKED_ESTATE_MEMORY_SCOPE, honor it — return None (param omitted,
|
|
93
|
-
inheritance semantics) so the pin keeps meaning what it meant.
|
|
94
|
-
Explicit override: WICKED_ESTATE_MEMORY_SCOPE_PREFIX.
|
|
91
|
+
Default None: the param is omitted and recall uses ancestor inheritance
|
|
92
|
+
from ``_memory_scope()``. Searching other projects' memories is an
|
|
93
|
+
explicit opt-in: WICKED_ESTATE_MEMORY_SCOPE_PREFIX ("" = every memory).
|
|
95
94
|
"""
|
|
96
|
-
|
|
97
|
-
if prefix is not None:
|
|
98
|
-
return prefix
|
|
99
|
-
if os.environ.get("WICKED_ESTATE_MEMORY_SCOPE"):
|
|
100
|
-
return None # custom scope pinned — keep ancestor-visible semantics
|
|
101
|
-
return ""
|
|
95
|
+
return os.environ.get("WICKED_ESTATE_MEMORY_SCOPE_PREFIX")
|
|
102
96
|
|
|
103
97
|
|
|
104
98
|
def _f(value: Any) -> float:
|
|
@@ -163,8 +157,8 @@ def _estate_search(query: str, limit: int = 10, timeout: float = 5.0) -> Optiona
|
|
|
163
157
|
"""Two-call estate retrieval. None = estate unreachable.
|
|
164
158
|
|
|
165
159
|
knowledge.recall is the primary signal; memory.recall is fused in when it
|
|
166
|
-
has anything to say
|
|
167
|
-
|
|
160
|
+
has anything to say, scoped to the current project and its ancestors
|
|
161
|
+
(``_memory_scope``; other projects only via the explicit prefix opt-in). An
|
|
168
162
|
empty-but-reachable answer is a valid [] — only a dead estate returns
|
|
169
163
|
None. A memory-leg failure (e.g. an older binary rejecting
|
|
170
164
|
``scope_prefix``) degrades the fusion to knowledge-only — knowledge
|
|
@@ -218,8 +212,7 @@ def search(query: str, limit: int = 10, timeout: float = 5.0) -> List[dict]:
|
|
|
218
212
|
def recall_memories(query: str, limit: int = 10, timeout: float = 5.0) -> List[dict]:
|
|
219
213
|
"""Memory-only recall (estate memory.recall).
|
|
220
214
|
|
|
221
|
-
Same
|
|
222
|
-
migrated ``brain:…`` leaf memories are recallable here too.
|
|
215
|
+
Same visibility as the fusion: the current project and its ancestors.
|
|
223
216
|
"""
|
|
224
217
|
if not query or not str(query).strip():
|
|
225
218
|
return []
|