wicked-garden 12.51.1 → 12.52.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/scripts/learn/index.py +69 -21
|
@@ -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.52.0"}], "version": "12.52.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.52.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,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.52.0] — 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Learn indexing reads what merged and replaces a topic by its source (PR #1280).**
|
|
9
|
+
- `scripts/learn/index.py --ref <ref>` reads `.wicked/knowledge/` at a git ref, never the working copy. wicked-crew's learn schedule passes `origin/<default>`.
|
|
10
|
+
- When estate advertises `knowledge.forget` (estate K1+K2), each topic becomes one `knowledge.ingest {replace_source: true}` carrying `observed_at` and `source_rev`, and a topic with nothing left is forgotten by its source.
|
|
11
|
+
- An older estate keeps the per-revision scope fallback.
|
|
12
|
+
|
|
5
13
|
## [12.51.1] — 2026-10-11
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.52.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/learn/index.py
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
wicked-garden run scripts/learn/index.py --repo <checkout> [--name <repo name>] [--slug <slug>] [--dry-run]
|
|
5
5
|
|
|
6
6
|
Reads ``.wicked/knowledge/<slug>/`` from the checkout (run it on the default branch: index what was
|
|
7
|
-
MERGED, never an open pull request) and, per topic, REPLACES
|
|
8
|
-
``knowledge.
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
MERGED, never an open pull request) and, per topic, REPLACES its knowledge in estate: one
|
|
8
|
+
``knowledge.ingest {replace_source: true}`` (source ``repo:<repo>/.wicked/knowledge/<slug>``, scope
|
|
9
|
+
``learn:<repo>/topic:<slug>``, ``observed_at`` = the topic's refreshed_at, ``source_rev`` = its tree hash)
|
|
10
|
+
whose chunks are the topic's ACTIVE claims (each chunk ends with its sources and observed dates) and the
|
|
11
|
+
README's sections. A retired, superseded or contradicted claim therefore leaves recall at the next
|
|
12
|
+
index; a topic with nothing left is forgotten by its source (``knowledge.forget``).
|
|
11
13
|
|
|
12
|
-
When
|
|
13
|
-
scope ``learn:<repo>/topic:<slug>/rev:<tree hash>`` instead and the output says
|
|
14
|
-
(an older revision stays recallable until estate can
|
|
14
|
+
When estate does not advertise ``knowledge.forget`` (before estate K1+K2), the topic is ingested under a
|
|
15
|
+
per-revision scope ``learn:<repo>/topic:<slug>/rev:<tree hash>`` instead and the output says
|
|
16
|
+
``"replace": "unavailable"`` (an older revision stays recallable until estate can replace it).
|
|
15
17
|
|
|
16
18
|
Never from inside a governed run: a run's estate is read-only, and learned knowledge reaches the
|
|
17
19
|
store only after its pull request merged. Prints one JSON object ``{ok, repo, topics: [{slug, scope,
|
|
@@ -77,40 +79,72 @@ def topic_payload(repo: Path, name: str, d: Path) -> dict:
|
|
|
77
79
|
"source": f"repo:{name}/.wicked/knowledge/{t.slug}",
|
|
78
80
|
"chunks": chunks,
|
|
79
81
|
"tree_hash": tree_hash(d),
|
|
82
|
+
# when the topic was last researched (topic.json refreshed_at, else created_at)
|
|
83
|
+
"observed_at": (t.meta.get("refreshed_at") or t.meta.get("created_at")) if isinstance(t.meta, dict) else None,
|
|
80
84
|
"read_errors": t.errors,
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
|
|
84
|
-
def index_topic(client, p: dict) -> dict:
|
|
88
|
+
def index_topic(client, p: dict, tools: list | None = None) -> dict:
|
|
89
|
+
"""Replace one topic in estate. With ``knowledge.forget`` advertised (estate K1+K2), the topic's
|
|
90
|
+
source is replaced in place (``knowledge.ingest {replace_source: true}``, carrying ``observed_at``
|
|
91
|
+
and ``source_rev``), and a topic with nothing left to index is forgotten by its source. Without
|
|
92
|
+
it, the topic is ingested under a per-revision scope and the output says so."""
|
|
85
93
|
out = {"slug": p["slug"], "scope": p["scope"], "chunks": len(p["chunks"]), "tree_hash": p["tree_hash"]}
|
|
86
94
|
if p["read_errors"]:
|
|
87
95
|
out["read_errors"] = p["read_errors"]
|
|
88
|
-
if p["read_errors"]:
|
|
89
96
|
out["ingested"] = False
|
|
90
|
-
out["reason"] = "the topic could not be fully read; its indexed
|
|
97
|
+
out["reason"] = "the topic could not be fully read; its indexed knowledge is left as it was"
|
|
91
98
|
return out
|
|
92
|
-
|
|
93
|
-
if erased is None:
|
|
94
|
-
out["replace"] = "unavailable"
|
|
95
|
-
out["scope"] = f"{p['scope']}/rev:{p['tree_hash']}"
|
|
96
|
-
else:
|
|
97
|
-
out["replace"] = "erased"
|
|
98
|
-
out["erased"] = erased
|
|
99
|
+
replace = "knowledge.forget" in (tools if tools is not None else client.list_tools())
|
|
99
100
|
if not p["chunks"]:
|
|
100
101
|
out["ingested"] = False
|
|
101
102
|
out["reason"] = "no active claims and no README to index"
|
|
103
|
+
if replace:
|
|
104
|
+
forgotten = client.call("knowledge.forget", {"source": p["source"]}, timeout=30.0)
|
|
105
|
+
out["replace"] = "forgotten" if forgotten is not None else "forget-failed"
|
|
106
|
+
out["forgotten"] = forgotten
|
|
107
|
+
out["ok"] = forgotten is not None
|
|
102
108
|
return out
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
args = {"title": p["title"], "chunks": p["chunks"], "source": p["source"]}
|
|
110
|
+
if replace:
|
|
111
|
+
out["replace"] = "replaced"
|
|
112
|
+
args.update(scope=p["scope"], replace_source=True, source_rev=p["tree_hash"])
|
|
113
|
+
if p.get("observed_at"):
|
|
114
|
+
args["observed_at"] = p["observed_at"]
|
|
115
|
+
else:
|
|
116
|
+
out["replace"] = "unavailable"
|
|
117
|
+
out["scope"] = f"{p['scope']}/rev:{p['tree_hash']}"
|
|
118
|
+
args["scope"] = out["scope"]
|
|
119
|
+
res = client.call("knowledge.ingest", args, timeout=60.0)
|
|
105
120
|
out["ingested"] = res is not None
|
|
106
121
|
if res is None:
|
|
107
122
|
out["reason"] = "estate knowledge.ingest failed or estate is unreachable"
|
|
108
123
|
return out
|
|
109
124
|
|
|
110
125
|
|
|
126
|
+
def materialize(repo: Path, ref: str, dest: Path) -> None:
|
|
127
|
+
"""Copy ``.wicked/knowledge/`` as it is at git ``ref`` (e.g. ``origin/main``: what MERGED) into
|
|
128
|
+
``dest``, so the index reads the merged tree, never the checkout's working copy."""
|
|
129
|
+
listing = subprocess.run(["git", "-C", str(repo), "ls-tree", "-r", "-z", "--name-only", ref, "--", ".wicked/knowledge"],
|
|
130
|
+
capture_output=True, timeout=30)
|
|
131
|
+
if listing.returncode != 0:
|
|
132
|
+
raise RuntimeError(f"git ls-tree {ref} failed: {listing.stderr.decode('utf-8', 'replace').strip()[:200]}")
|
|
133
|
+
for rel in filter(None, listing.stdout.decode("utf-8", "replace").split("\0")):
|
|
134
|
+
blob = subprocess.run(["git", "-C", str(repo), "show", f"{ref}:{rel}"], capture_output=True, timeout=30)
|
|
135
|
+
if blob.returncode != 0:
|
|
136
|
+
raise RuntimeError(f"git show {ref}:{rel} failed")
|
|
137
|
+
target = dest / rel
|
|
138
|
+
if not target.resolve().is_relative_to(dest.resolve()):
|
|
139
|
+
raise RuntimeError(f"refusing a path outside the index root: {rel}")
|
|
140
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
target.write_bytes(blob.stdout)
|
|
142
|
+
|
|
143
|
+
|
|
111
144
|
def main(argv: list[str] | None = None) -> int:
|
|
112
145
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
113
146
|
parser.add_argument("--repo", type=Path, default=Path("."))
|
|
147
|
+
parser.add_argument("--ref", help="read the knowledge at this git ref (e.g. origin/main) instead of the working tree")
|
|
114
148
|
parser.add_argument("--name", help="the repository name in the scope (default: origin's name, else the directory)")
|
|
115
149
|
parser.add_argument("--slug")
|
|
116
150
|
parser.add_argument("--dry-run", action="store_true", help="print what would be indexed; touch no store")
|
|
@@ -123,13 +157,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
123
157
|
"learned knowledge is indexed from the merged default branch (DES-learn-workflow §4.2)"}))
|
|
124
158
|
return 2
|
|
125
159
|
name = args.name or repo_name(repo)
|
|
160
|
+
if args.ref:
|
|
161
|
+
import tempfile
|
|
162
|
+
with tempfile.TemporaryDirectory(prefix="learn-index-") as tmp:
|
|
163
|
+
try:
|
|
164
|
+
materialize(repo, args.ref, Path(tmp))
|
|
165
|
+
except (RuntimeError, OSError, subprocess.SubprocessError) as e:
|
|
166
|
+
print(json.dumps({"ok": False, "repo": name, "reason": str(e)}))
|
|
167
|
+
return 1
|
|
168
|
+
return _run(client, Path(tmp), name, args)
|
|
169
|
+
return _run(client, repo, name, args)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _run(client, repo: Path, name: str, args) -> int:
|
|
126
173
|
payloads = [topic_payload(repo, name, d) for d in topic_dirs(repo, args.slug)]
|
|
127
174
|
if args.dry_run:
|
|
128
175
|
print(json.dumps({"ok": True, "repo": name, "dry_run": True,
|
|
129
176
|
"topics": [{k: v for k, v in p.items() if k != "chunks"} | {"chunks": len(p["chunks"])} for p in payloads]}, indent=2))
|
|
130
177
|
return 0
|
|
131
|
-
|
|
132
|
-
|
|
178
|
+
tools = client.list_tools()
|
|
179
|
+
results = [index_topic(client, p, tools) for p in payloads]
|
|
180
|
+
ok = all(r.get("ingested") or (not r["chunks"] and "read_errors" not in r and r.get("ok", True)) for r in results)
|
|
133
181
|
print(json.dumps({"ok": ok, "repo": name, "topics": results}, indent=2))
|
|
134
182
|
return 0 if ok else 1
|
|
135
183
|
|