wicked-garden 12.52.0 → 12.52.1

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.
@@ -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.52.0"}], "version": "12.52.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.52.1"}], "version": "12.52.1"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.52.0",
3
+ "version": "12.52.1",
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.1] — 2026-10-11
6
+
7
+ ### Changed
8
+ - **The memory durability gate refuses only the unambiguous (w9-memfix3, PR #1283).** `store` / `capture-batch` refuse a body under 40 characters, a bare id / SHA / version, and first-person progress narration. Ids, versions, SHAs, PIDs and date words inside a body are returned as `warnings` and the store succeeds. "Keep SQLite >= 3.35.0 because RETURNING …" and "CRC32 polynomial edb88320 … database commit …" are stored. The three-month test in the mem skills is the agent's judgment.
9
+
10
+ ### Fixed
11
+ - **Relative-path worktrees resolve to their main checkout's project scope** (`gitdir: ../../.git/worktrees/run` is resolved against the `.git` file, and `commondir` is followed), not `project:..`.
12
+
5
13
  ## [12.52.0] — 2026-10-11
6
14
 
7
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.52.0",
3
+ "version": "12.52.1",
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": [
@@ -963,6 +963,30 @@ def context(
963
963
  return call("ContextBundle", args, timeout=timeout)
964
964
 
965
965
 
966
+ def _worktree_main(git_file: Path) -> Optional[Path]:
967
+ """Main checkout of a linked worktree whose ``.git`` file is ``git_file``.
968
+
969
+ ``gitdir:`` may be relative to the ``.git`` file's directory
970
+ (``git worktree add --relative-paths``); the worktree's ``commondir``
971
+ names the shared git dir (relative to the gitdir). The main checkout is
972
+ that git dir's parent. None when the file is not a worktree pointer.
973
+ """
974
+ try:
975
+ line = git_file.read_text(encoding="utf-8").strip()
976
+ except OSError:
977
+ return None
978
+ if not line.startswith("gitdir:"):
979
+ return None
980
+ gitdir = (git_file.parent / line[len("gitdir:"):].strip()).resolve()
981
+ try:
982
+ common = (gitdir / (gitdir / "commondir").read_text(encoding="utf-8").strip()).resolve()
983
+ except OSError:
984
+ common = gitdir.parent.parent if gitdir.parent.name == "worktrees" else None
985
+ if common is None or common.name != ".git":
986
+ return None
987
+ return common.parent
988
+
989
+
966
990
  def checkout_root() -> Optional[Path]:
967
991
  """The git checkout the cwd is in, resolved through linked worktrees.
968
992
 
@@ -979,14 +1003,7 @@ def checkout_root() -> Optional[Path]:
979
1003
  if git.is_dir():
980
1004
  return d
981
1005
  if git.is_file():
982
- try:
983
- line = git.read_text(encoding="utf-8").strip()
984
- except OSError:
985
- return d
986
- gitdir = line[len("gitdir:"):].strip() if line.startswith("gitdir:") else ""
987
- marker = f"{os.sep}.git{os.sep}worktrees{os.sep}"
988
- norm = gitdir.replace("/", os.sep)
989
- return Path(norm.split(marker)[0]) if marker in norm else d
1006
+ return _worktree_main(git) or d
990
1007
  return None
991
1008
 
992
1009
 
@@ -322,6 +322,22 @@ def _torn(page, wait: bool = False):
322
322
  return None
323
323
 
324
324
 
325
+ def python_with_playwright() -> str | None:
326
+ """The first python3 / python on PATH that can import playwright. Each is probed, never compared by
327
+ path: a venv's python links to the same base binary yet sees other site-packages (codex)."""
328
+ import shutil, subprocess # noqa: E401, PLC0415
329
+ for name in ("python3", "python"):
330
+ exe = shutil.which(name)
331
+ if exe is None:
332
+ continue
333
+ try:
334
+ if subprocess.run([exe, "-c", "import playwright.sync_api"], capture_output=True, timeout=30).returncode == 0:
335
+ return exe
336
+ except (OSError, subprocess.TimeoutExpired):
337
+ continue
338
+ return None
339
+
340
+
325
341
  def manifest_case(pack: Path) -> dict:
326
342
  """Case 0: garden's own `pack check` (the same gate wicked-crew runs before an install)."""
327
343
  try:
@@ -369,6 +385,13 @@ def main(argv: list[str] | None = None) -> int:
369
385
  try:
370
386
  from playwright.sync_api import sync_playwright # noqa: PLC0415
371
387
  except ImportError:
388
+ # The launcher's snapshot venv carries no browser driver (E2E finding F-EDIT-2): run once more
389
+ # under a python on PATH that has one, with the same arguments.
390
+ other = python_with_playwright() if not os.environ.get("WG_EDITOR_CONFORMANCE_REEXEC") else None
391
+ if other is not None:
392
+ import subprocess # noqa: PLC0415
393
+ env = {**os.environ, "WG_EDITOR_CONFORMANCE_REEXEC": "1"}
394
+ return subprocess.call([other, os.path.abspath(__file__), *(argv if argv is not None else sys.argv[1:])], env=env)
372
395
  return out({"ok": False, "pack": str(args.pack), "error": "no headless browser: the Python playwright package is "
373
396
  "not installed (pip install playwright && playwright install chromium); refusing to report conformance "
374
397
  "without the run"}, 3)
@@ -278,6 +278,17 @@ def do_uninstall(args) -> tuple[int, dict]:
278
278
  return 0, out
279
279
 
280
280
 
281
+ def consent_refusal(args, env) -> str | None:
282
+ """Inside a governed run (``WICKED_RUN_ID`` set) a live install happens only in the consent-gated
283
+ Tool unit, the one unit the engine hands ``WICKED_CONSENT_CHOICE`` (the operator's approved
284
+ choice). Any other unit (a creator in build, a reviewer) may plan with ``--dry-run`` but never
285
+ installs or uninstalls (E2E finding F-EDIT-1: a creator installed before the gate asked)."""
286
+ if not env.get("WICKED_RUN_ID") or env.get("WICKED_CONSENT_CHOICE"):
287
+ return None
288
+ return ("refusing to install inside a governed run outside its consent-gated install step: the operator "
289
+ "approves the install at that gate. Plan it with --dry-run; the install step runs it.")
290
+
291
+
281
292
  def main(argv: list[str] | None = None) -> int:
282
293
  parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
283
294
  where = parser.add_mutually_exclusive_group()
@@ -301,6 +312,10 @@ def main(argv: list[str] | None = None) -> int:
301
312
  except Failure as err:
302
313
  sys.stderr.write(f"editor install --dry-run: {err}\n")
303
314
  return err.code
315
+ refusal = consent_refusal(args, os.environ)
316
+ if refusal is not None:
317
+ print(json.dumps({"ok": False, "error": refusal}))
318
+ return 2
304
319
  try:
305
320
  code, out = do_uninstall(args) if args.uninstall else do_install(args)
306
321
  except Failure as err:
@@ -36,9 +36,10 @@ projects' memories need the explicit opt-in ``scope_prefix: ""``.
36
36
 
37
37
  Durability gate
38
38
  ---------------
39
- `store` / `capture-batch` refuse content that is not a durable learning (see
40
- ``durability_rejects``): too short, or carrying SHAs, versions, PR/run ids,
41
- PIDs, "today", or first-person progress narration.
39
+ `store` / `capture-batch` refuse only the unambiguous (``durability_rejects``):
40
+ an empty or under-40-char body, a bare id / SHA / version, or first-person
41
+ progress narration. Ids, versions, SHAs, PIDs and dates inside a body are
42
+ advisory ``warnings`` (``durability_warnings``); the store still succeeds.
42
43
 
43
44
  Erase guard
44
45
  -----------
@@ -200,65 +201,57 @@ def _parse_budget(args):
200
201
 
201
202
  # ── durability gate ─────────────────────────────────────────────────────────
202
203
  # A memory must help a fresh session on a different task months from now: it
203
- # states a rule, why it holds, and when it applies. The skill text asks the
204
- # agent that question; these checks refuse the mechanical misses outright, so
205
- # narration and status never reach the store even when the judgment slips.
204
+ # states a rule, why it holds, and when it applies. That judgment is the
205
+ # agent's (the skill text asks it). The backend refuses only what is
206
+ # unambiguous, and WARNS on the rest: an id, version or SHA inside a real rule
207
+ # ("keep SQLite >= 3.35.0 because RETURNING", "CRC32 polynomial edb88320") is
208
+ # fine, so those heuristics never block a store.
206
209
  MIN_CONTENT_CHARS = 40
207
- _HEX_RUN = re.compile(r"\b[0-9a-f]{7,40}\b")
208
- # A hex run is a commit id when it is long (>= 12) or sits in commit context; a short one
209
- # alone (a CRC32 polynomial, a colour) is a constant, and constants are durable.
210
- _COMMIT_CONTEXT = re.compile(
211
- r"\b(?:commit|sha|rev(?:ision)?|cherry-?pick(?:ed)?|revert(?:ed)?|fixed in|landed in|merged as"
212
- r"|pin(?:ned)?(?: moved)? (?:to|at))\b|@[0-9a-f]{7}", re.I)
213
- # A version is narration only next to release status ("released 2.4.1", "shipped 0.7.26"); a
214
- # version constraint ("keep SQLite >= 3.35.0 because RETURNING") is a durable rule.
215
- _VERSION = re.compile(r"(?<![\w.])v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?(?![.\d])")
216
- _RELEASE_STATUS = re.compile(
217
- r"\b(?:released|shipped|published|tagged|cut|bumped|landed|merged|deployed|rolled out"
218
- r"|upgraded to|is out|went out|now on)\b", re.I)
219
- _NON_DURABLE = (
210
+ # First-person progress narration: "I'll retry", "I'm going to guess again", "We shipped …".
211
+ _NARRATION = re.compile(
212
+ r"\b(?:i['\u2019]ll|i will|we['\u2019]ll|let me"
213
+ r"|(?:i['\u2019]m|i am|we['\u2019]re|we are) (?:going|about) to)\b"
214
+ r"|^(?:i|we)(?: just| finally)? (?:shipped|released|merged|pushed|deployed|landed|tried|retried"
215
+ r"|restarted|bumped|tagged)\b", re.I)
216
+ # One identifier on its own: a SHA, a version, a uuid, a #123 / repo#123.
217
+ _ID_TOKEN = (r"(?:[0-9a-f]{7,40}|v?\d+\.\d+\.\d+(?:[-+][0-9a-z.]+)?"
218
+ r"|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[a-z][\w.-]*#\d+|#\d+)")
219
+ _BARE_ID = re.compile(r"^[\W_]*" + _ID_TOKEN + r"[\W_]*$", re.I)
220
+ _ADVISORIES = (
221
+ (re.compile(r"\b(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\b", re.I), "a commit SHA?"),
222
+ (re.compile(r"(?<![\w.])v?\d+\.\d+\.\d+(?![.\d])"), "a version?"),
220
223
  (re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I),
221
- "a run/session id (uuid)"),
222
- # x.y.z, but not a dotted quad (127.0.0.1) or a longer dotted run.
223
- # PR #12 / issue #12 / run 38093054271 / core#880 / a bare #880 — not "run 3 retries".
224
+ "a run/session id?"),
224
225
  (re.compile(r"\b(?:pr|pull request|issue|run|job)\s*(?:#\s*\d+|\d{5,})\b"
225
- r"|\b[a-z][\w.-]*#\d+\b|(?<![\w&])#\d+\b", re.I), "a PR/issue/run id"),
226
- # pid 48668 — not "PID 1 is init".
227
- (re.compile(r"\bpids?\s*[:=#]?\s*\d{3,}\b", re.I), "a process id"),
226
+ r"|\b[a-z][\w.-]*#\d+\b|(?<![\w&])#\d+\b", re.I), "a PR/issue/run id?"),
227
+ (re.compile(r"\bpids?\s*[:=#]?\s*\d{3,}\b", re.I), "a process id?"),
228
228
  (re.compile(r"\b(?:today|yesterday|tonight|this (?:morning|afternoon|evening|week))\b", re.I),
229
- "a date-relative word"),
230
- # I'll / we'll / I'm going to — the apostrophe is required ("ill-formed", "works well").
231
- (re.compile(r"\b(?:i['\u2019]ll|i will|we['\u2019]ll|let me"
232
- r"|(?:i['\u2019]m|i am|we['\u2019]re|we are) (?:going|about) to)\b", re.I),
233
- "first-person progress narration"),
229
+ "a date-relative word?"),
234
230
  )
235
231
 
236
232
 
237
- def _is_sha(token):
238
- return any(c.isdigit() for c in token) and any(c.isalpha() for c in token)
239
-
240
-
241
233
  def durability_rejects(content):
242
- """Why ``content`` is not a durable learning ([] = it may be stored).
243
-
244
- Deterministic and offline: empty or too-short bodies, commit SHAs (long, or
245
- in commit context), versions next to release status,
246
- PR/issue/run ids, PIDs, "today"-style dates, and first-person narration
247
- ("I'll retry", "going to guess again") are records of a moment; the repo,
248
- CHANGELOG or tracker already holds those.
234
+ """Hard refusals ([] = it may be stored): an empty or under-40-char body, a
235
+ body that is only an id / SHA / version, or first-person progress narration.
249
236
  """
250
237
  text = (content or "").strip()
251
238
  if len(text) < MIN_CONTENT_CHARS:
252
239
  return [f"too short to state a rule, why it holds and when it applies "
253
240
  f"(< {MIN_CONTENT_CHARS} chars)"]
254
- reasons = []
255
- shas = [m.group(0) for m in _HEX_RUN.finditer(text.lower()) if _is_sha(m.group(0))]
256
- if any(len(h) >= 12 for h in shas) or (shas and _COMMIT_CONTEXT.search(text)):
257
- reasons.append("a commit SHA")
258
- if _VERSION.search(text) and _RELEASE_STATUS.search(text):
259
- reasons.append("release status (a version that was shipped/released)")
260
- reasons += [why for rx, why in _NON_DURABLE if rx.search(text)]
261
- return reasons
241
+ if _BARE_ID.match(text):
242
+ return ["only an identifier (a SHA, version or id), not a learning"]
243
+ if _NARRATION.search(text):
244
+ return ["first-person progress narration (what was done or will be done)"]
245
+ return []
246
+
247
+
248
+ def durability_warnings(content):
249
+ """Advisory only: ids, versions, SHAs, PIDs or dates that MAY mark a moment
250
+ rather than a rule. The store still succeeds; the agent re-checks the
251
+ three-month test and keeps the memory if it holds.
252
+ """
253
+ text = (content or "").strip()
254
+ return [why for rx, why in _ADVISORIES if rx.search(text)]
262
255
 
263
256
 
264
257
  def _capture_one(item):
@@ -274,7 +267,7 @@ def _capture_one(item):
274
267
  rejects = durability_rejects(content)
275
268
  if rejects:
276
269
  return None, ("not a durable learning (" + "; ".join(rejects) + "): state the rule, "
277
- "why it holds and when it applies, without ids, versions or progress")
270
+ "why it holds and when it applies")
278
271
  kind = item.get("kind") or "fact"
279
272
  if kind not in KINDS:
280
273
  return None, f"unknown kind: {kind} (valid: {', '.join(KINDS)})"
@@ -300,7 +293,11 @@ def do_store(args):
300
293
  memory_id, err = _capture_one(args)
301
294
  if err:
302
295
  return _fail_open(err)
303
- _emit({"ok": True, "memory_id": memory_id, "scope": args.get("scope") or _default_scope()})
296
+ out = {"ok": True, "memory_id": memory_id, "scope": args.get("scope") or _default_scope()}
297
+ warnings = durability_warnings(args.get("content"))
298
+ if warnings:
299
+ out["warnings"] = warnings
300
+ _emit(out)
304
301
  return 0
305
302
 
306
303
 
@@ -402,12 +399,17 @@ def do_capture_batch(args):
402
399
  if err:
403
400
  failures.append({"index": i, "reason": err})
404
401
  else:
405
- stored.append({"index": i, "memory_id": memory_id})
402
+ entry = {"index": i, "memory_id": memory_id}
403
+ warnings = durability_warnings(item.get("content"))
404
+ if warnings:
405
+ entry["warnings"] = warnings
406
+ stored.append(entry)
406
407
  _emit({
407
408
  "ok": not failures or bool(stored),
408
409
  "stored": len(stored),
409
410
  "failed": len(failures),
410
411
  "ids": [s["memory_id"] for s in stored],
412
+ "warnings": [s for s in stored if s.get("warnings")],
411
413
  "failures": failures,
412
414
  })
413
415
  return 0
@@ -87,9 +87,11 @@ A chat seat asked by wicked-crew to label the operator's decisions reads
87
87
  when it applies, in 1–3 self-contained sentences. Never store progress or
88
88
  status, release versions ("shipped 2.4.1"), commit SHAs, PR/run ids, PIDs,
89
89
  "today", or what the repo,
90
- CHANGELOG or tracker already records. The backend refuses the mechanical
91
- cases with `not a durable learning`; treat that as "don't store", not as
92
- an error to work around.
90
+ CHANGELOG or tracker already records. That test is your judgment. The
91
+ backend refuses only a body under 40 characters, a bare id / SHA / version
92
+ and first-person narration (`not a durable learning`: don't store, don't
93
+ work around it); it stores bodies containing ids, versions or SHAs and
94
+ returns `warnings` naming them, so check those before you store.
93
95
  Provenance: memory is what we witnessed, were told, or decided. A claim
94
96
  read from code, docs or the web is knowledge: use `write`
95
97
  (`{"content","class":"concept","source"}`) or `ingest` instead; `store`
@@ -54,10 +54,14 @@ Reject, and do not rephrase into something that sneaks past:
54
54
 
55
55
  A version *constraint* ("needs SQLite >= 3.35.0 because …") and a fixed constant
56
56
  (a CRC polynomial, a colour) are durable; keep them.
57
- The backend refuses the mechanical cases (ids, release versions, commit SHAs, PIDs, "today",
58
- first-person narration, bodies under 40 characters) with
59
- `not a durable learning`. A refusal means the candidate failed the test:
60
- drop it, unless it holds a real rule you can state without the moment.
57
+ The test above is YOUR judgment; the backend only backs it up. It refuses
58
+ (`not a durable learning`) a body under 40 characters, a bare id / SHA /
59
+ version, and first-person progress narration ("I'll retry", "We shipped …"):
60
+ drop those. Ids, versions, SHAs, PIDs or dates inside a body are allowed (a
61
+ version constraint or a fixed constant is a real rule); the backend stores
62
+ the memory and returns `warnings` naming them. Check for them yourself
63
+ before the write, since the warning arrives after it, and report any
64
+ warning with the memory it was attached to.
61
65
  If nothing passes, store nothing and say so. An empty capture is a valid
62
66
  result, and the usual one.
63
67