wicked-garden 12.55.1 → 12.57.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.
@@ -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.55.1"}], "version": "12.55.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.57.0"}], "version": "12.57.0"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.55.1",
3
+ "version": "12.57.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.8.0",
6
6
  "wicked_ledger_version": "^0.6.0",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.57.0] — 2026-10-18
6
+
7
+ ### Added
8
+ - **The estate shim relays through wicked-crew's broker on a container seat (wicked-core#1018).** A seat on the container floor has no estate of its own (no store is mounted, no `wicked-estate-mcp` is installed, no store pin reaches it), so every blast-radius, hotspot and recall lookup and every capture proposal was refused there. When a governed unit holds the broker channel (`WICKED_CREW_URL` + `WICKED_MCP_TOKEN`) and cannot reach estate locally (wicked-core's `WICKED_ESTATE_BROKER` marker, no pinned store, or no local binary), `scripts/_estate_client.py` sends each call to `POST /api/v1/mcp/call` as `mcp:estate/<tool>`; crew answers it on the host, scoped to the run. It is one more transport behind the same seam, so the skills that call the shim are unchanged: `--readonly` keeps its meaning (the read-only lookups plus `proposal.submit`), a `-32602` from estate keeps its code, and `health` asks the broker without spending a call. With neither a pinned store nor the channel, a governed unit still fails closed, and the reason now names both fixes. A relayed call the broker refused returns the usual empty value plus `broker_refusal` with its reason; `mode` reports the `transport`. Needs wicked-crew with the estate relay.
9
+
10
+ ## [12.56.0] — 2026-10-17
11
+
12
+ ### Added
13
+ - **Operator memories and an explicit reach (needs wicked-estate with the operator scope).**
14
+ - How the user works (style, preferences, the extra steps they always take) is an operator memory. `mem-capture` stores it at `operator:local`, which every project's recall includes, so it is never pinned to one project.
15
+ - Every policy proposal now carries `payload.reach`: `operator` (every project), or `project` (that project or repo). A rule the user states defaults to `operator` unless they named a project. A rule repo-learn learns from a repo is `project`.
16
+ - `mem/refs/scopes.md` documents the operator scope and the reach vocabulary.
17
+
18
+ ### Fixed
19
+ - **Session capture files standing rules for review instead of storing them as memory.** The
20
+ `mem-capture` worker swept "always use…", "never do…" and team conventions into the memory
21
+ batch, so an enforceable rule became a memory nobody approved. A standing rule the user stated
22
+ now goes to the review queue as a `policy:<type>` proposal (Step 3b); a preference that is not a
23
+ checkable rule stays a memory. The `mem` skill and `refs/scopes.md` say the same.
24
+ - **repo-learn no longer files code-wiring facts as memory.** Its 2×2 map and "derive both"
25
+ guidance told the worker to record "how X wires up" as a memory, contradicting its own rule that
26
+ what is read from code is knowledge. Those findings are now `knowledge` proposals; a memory is
27
+ only what the worker witnessed or was told. The skill's summary and memory guidance say the
28
+ same, and a convention the team must follow is a policy. Its capture reference no longer
29
+ documents piping long JSON to `propose -` (the shim never read stdin).
30
+ - **Onboarding records the detected languages, frameworks and tools as knowledge.** `core` setup
31
+ stored them as a `skill` memory; they are read from the repository, so they go through the mem
32
+ `write` action now.
33
+ - **Answer synthesis takes "what should hold" from approved steering rules, not memory.** On a
34
+ conflict, `search` answer cited an operator memory as the ruling; an approved rule
35
+ (`rules.recall`) now governs, and a memory gives what was decided and why.
36
+
37
+ ### Changed
38
+ - **The site's skill count is counted at build time, and the council is described as what it is.** `site/astro.config.mjs` counts `SKILL.md` under `skills/**` (`__WICKED_GARDEN_SKILLS__`), so the hero, the toolbox and the page description can no longer disagree (they said 143 and 142; the tree holds 138). The site no longer says "the multi-model council IS the router": a panel of other models settles a disputed decision. The hero drops "capability plane" for plain words, the README drops the retired Zed/ACP mention, and the npm description names every host it installs into (takes effect on the next publish).
39
+
5
40
  ## [12.55.1] — 2026-10-15
6
41
 
7
42
  ### Fixed
@@ -1135,7 +1170,9 @@ through `v9.2.18`. Major themes across that span:
1135
1170
 
1136
1171
  For each of those releases, see the corresponding tag in git history.
1137
1172
 
1138
- [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.55.1...HEAD
1173
+ [Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.57.0...HEAD
1174
+ [12.57.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.56.0...v12.57.0
1175
+ [12.56.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.55.1...v12.56.0
1139
1176
  [12.55.1]: https://github.com/mikeparcewski/wicked-garden/compare/v12.55.0...v12.55.1
1140
1177
  [12.55.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.54.0...v12.55.0
1141
1178
  [12.54.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.53.3...v12.54.0
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## The premise
19
19
 
20
- Coding agents grew up. Claude Code, Codex, Cursor, Antigravity, Aider, OpenCode, Zed/ACP — they're not autocomplete anymore. They plan. They parallelize. And each has *strong opinions* about how it likes to work.
20
+ Coding agents grew up. Claude Code, Codex, Cursor, Antigravity, Aider, OpenCode — they're not autocomplete anymore. They plan. They parallelize. And each has *strong opinions* about how it likes to work.
21
21
 
22
22
  Most plugins try to boss them around — re-implement planning, impose a workflow, make the agent dance. You end up fighting your own tools.
23
23
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wicked-garden",
3
- "version": "12.55.1",
3
+ "version": "12.57.0",
4
4
  "type": "module",
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.",
5
+ "description": "The skills your coding agent can't build alone: \"done\" re-checked from evidence, graph-aware search and multi-file refactor, 40 QE specialists, review panels from other models, and cross-session memory. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
6
6
  "keywords": [
7
7
  "ai",
8
8
  "coding-agent",
@@ -64,6 +64,24 @@ never through the raw write CLI. In that mode every spawn is
64
64
  invocation only with ``--readonly`` AND a pinned store (core #471); this
65
65
  module produces exactly that argv shape for the spawned MCP.
66
66
 
67
+ Container seats: the broker relay (wicked-core#1018)
68
+ ----------------------------------------------------
69
+ A seat on wicked-crew's container floor has no estate of its own: the host's
70
+ stores are never mounted into it (SQLite WAL across the VM file share is a
71
+ corruption risk), the seat image ships no ``wicked-estate-mcp``, and no store
72
+ pin reaches it. Its one way out is wicked-crew's broker, which every governed
73
+ unit can reach with ``WICKED_CREW_URL`` + ``WICKED_MCP_TOKEN`` (a token bound
74
+ to that unit). So in a governed unit that cannot reach estate locally (the
75
+ container marker ``WICKED_ESTATE_BROKER`` is set, no store is pinned, or no
76
+ ``wicked-estate-mcp`` resolves) and that holds the broker channel, every call
77
+ is RELAYED: ``POST <WICKED_CREW_URL>/api/v1/mcp/call {token, subject:
78
+ "mcp:estate/<tool>", args}``. Crew answers it on the host, scoped to the run
79
+ the token belongs to: the read-only lookups plus ``proposal.submit``, exactly
80
+ what a ``--readonly`` MCP allows. No caller changes: the relay is one more
81
+ transport behind the same seam (`_transport`). With neither a pinned local
82
+ store nor the broker channel, a governed unit still fails closed with a reason
83
+ (`governed_refusal`).
84
+
67
85
  Fail-open contract
68
86
  ------------------
69
87
  Every public function is fail-open: on a missing binary, spawn failure,
@@ -88,6 +106,8 @@ import shutil
88
106
  import subprocess
89
107
  import sys
90
108
  import threading
109
+ import urllib.error
110
+ import urllib.request
91
111
  from pathlib import Path
92
112
  from typing import Any, Callable, Optional
93
113
 
@@ -192,6 +212,22 @@ DB_FLAG = "--db"
192
212
  # capture proposals (`facets.project`), overriding whatever the worker self-derived;
193
213
  # absent/blank (older cores, or a non-run invocation) it changes nothing.
194
214
  RUN_PROJECT_ENV = "WICKED_RUN_PROJECT"
215
+ # The broker relay (wicked-core#1018). wicked-core sets the marker on a container seat whose
216
+ # gateway reaches crew's broker; the URL and the unit-bound token are on every governed unit.
217
+ BROKER_ENV = "WICKED_ESTATE_BROKER"
218
+ CREW_URL_ENV = "WICKED_CREW_URL"
219
+ MCP_TOKEN_ENV = "WICKED_MCP_TOKEN"
220
+ BROKER_SUBJECT_PREFIX = "mcp:estate/"
221
+ # The broker's own per-call deadline is 30 s; the HTTP wait outlasts it so the broker, not the
222
+ # client, decides a slow call (a proposal that timed out client-side may still have landed).
223
+ BROKER_MIN_TIMEOUT_S = 35.0
224
+ # The tools crew relays (`wicked-crew` `src/mcp/estate-relay.ts`): what `list-tools` reports in
225
+ # relay mode. A tool off this list is refused by the broker with its reason.
226
+ BROKER_TOOLS = (
227
+ "SearchEntity", "RetrieveEntity", "TraverseGraph", "Path", "BlastRadius", "Lineage",
228
+ "RankHotspots", "Communities", "RulesInventory", "rules.recall", "memory.recall",
229
+ "knowledge.recall", "proposal.submit",
230
+ )
195
231
 
196
232
  _readonly_flag: bool = False # set by --readonly / set_readonly()
197
233
  _governed_flag: bool = False # set by --governed / set_governed()
@@ -249,20 +285,48 @@ def store_pinned() -> bool:
249
285
  return bool(_db_override) or any(os.environ.get(k, "").strip() for k in STORE_PIN_ENV)
250
286
 
251
287
 
288
+ def broker_channel() -> Optional[tuple]:
289
+ """``(url, token)`` for crew's broker when this unit holds the channel, else None."""
290
+ url = os.environ.get(CREW_URL_ENV, "").strip().rstrip("/")
291
+ token = os.environ.get(MCP_TOKEN_ENV, "").strip()
292
+ if not url or not token or not (url.startswith("http://") or url.startswith("https://")):
293
+ return None
294
+ return url, token
295
+
296
+
297
+ def use_broker() -> bool:
298
+ """Relay through crew's broker? A governed unit that holds the broker channel and cannot
299
+ reach estate locally: the container marker is set, no store is pinned, or no
300
+ `wicked-estate-mcp` resolves (module docstring, wicked-core#1018)."""
301
+ if not is_governed() or broker_channel() is None:
302
+ return False
303
+ return _env_truthy(BROKER_ENV) or not store_pinned() or resolve_mcp_bin() is None
304
+
305
+
306
+ def transport_name() -> Optional[str]:
307
+ """``broker`` (relayed), ``local`` (spawn a pinned `wicked-estate-mcp`), or None (refused)."""
308
+ if use_broker():
309
+ return "broker"
310
+ return None if governed_refusal() else "local"
311
+
312
+
252
313
  def governed_refusal() -> Optional[str]:
253
- """The reason NOT to spawn: a governed run with no pinned store. None = clear to go.
314
+ """The reason NOT to call estate: a governed run with no pinned store and no broker
315
+ channel. None = clear to go.
254
316
 
255
317
  Belt-and-braces with the gate hook (which denies the same shape as `no-pin`):
256
- the shim never resolves the operator's default stores from inside a run.
318
+ the shim never resolves the operator's default stores from inside a run. A unit
319
+ that holds the broker channel relays instead (`use_broker`): crew picks the store.
257
320
  """
258
321
  marker = governed_marker()
259
- if marker is None or store_pinned():
322
+ if marker is None or store_pinned() or use_broker():
260
323
  return None
261
324
  return (
262
- f"governed unit ({marker} is set) but no estate store is pinned: pass "
263
- f"{DB_FLAG} <path> or set {' / '.join(STORE_PIN_ENV)} in the worker environment; "
264
- "refusing to spawn wicked-estate-mcp against an unpinned store (it would resolve "
265
- "the operator's defaults)"
325
+ f"governed unit ({marker} is set) but no estate store is pinned and no broker "
326
+ f"channel is present: pass {DB_FLAG} <path> or set {' / '.join(STORE_PIN_ENV)} in "
327
+ f"the worker environment, or run where wicked-crew hands the unit {CREW_URL_ENV} "
328
+ f"and {MCP_TOKEN_ENV} (its estate relay); refusing to spawn wicked-estate-mcp "
329
+ "against an unpinned store (it would resolve the operator's defaults)"
266
330
  )
267
331
 
268
332
 
@@ -684,6 +748,120 @@ def _maybe_install_broker() -> None:
684
748
  _maybe_install_broker()
685
749
 
686
750
 
751
+ # ─────────────────────────────────────────────────────────────────────────────
752
+ # Broker relay transport (wicked-core#1018) — same contract as `_dispatch`
753
+ # ─────────────────────────────────────────────────────────────────────────────
754
+
755
+ #: The last reason the broker gave for not answering (a refusal, an unreachable broker), so
756
+ #: the CLI can say WHY a relayed call came back empty instead of a bare empty result.
757
+ _last_broker_refusal: Optional[str] = None
758
+
759
+
760
+ def _broker_post(url: str, body: dict, timeout: float) -> "tuple[Optional[int], Optional[dict], Optional[str]]":
761
+ """POST JSON to the broker: ``(status, body, None)``, or ``(None, None, why)`` unreachable."""
762
+ req = urllib.request.Request(url, data=json.dumps(body).encode("utf-8"), method="POST",
763
+ headers={"Content-Type": "application/json"})
764
+ try:
765
+ with urllib.request.urlopen(req, timeout=timeout) as res:
766
+ status, raw = res.status, res.read()
767
+ except urllib.error.HTTPError as err:
768
+ status, raw = err.code, err.read()
769
+ except (urllib.error.URLError, OSError, ValueError) as err:
770
+ return None, None, f"the broker at {url} did not answer: {getattr(err, 'reason', err)}"
771
+ try:
772
+ parsed = json.loads(raw.decode("utf-8")) if raw else {}
773
+ except (UnicodeDecodeError, json.JSONDecodeError):
774
+ return status, None, f"the broker answered {status} with a body that is not JSON"
775
+ return (status, parsed, None) if isinstance(parsed, dict) else (status, None, "not a JSON object")
776
+
777
+
778
+ def _broker_init(rid: Any) -> dict:
779
+ return {"jsonrpc": "2.0", "id": rid, "result": {
780
+ "protocolVersion": "2024-11-05", "capabilities": {},
781
+ "serverInfo": {"name": "wicked-estate", "version": "relay"}}}
782
+
783
+
784
+ def _broker_dispatch(requests: list, *, db: Optional[str], timeout: float) -> dict:
785
+ """Relay transport: answer a JSON-RPC batch through crew's broker. Return {id: response}.
786
+
787
+ * ``initialize`` is answered once the broker has answered anything in the batch; alone
788
+ (a `health()` probe) it asks the broker for this unit's tool list, which spends no
789
+ call budget, so a dead broker or a revoked token is unhealthy.
790
+ * ``tools/list`` lists the relayed tools (`BROKER_TOOLS`).
791
+ * ``tools/call`` is ``POST /api/v1/mcp/call`` with subject ``mcp:estate/<name>``. An
792
+ ``ok`` answer hands back the envelope crew relayed; a JSON-RPC error estate raised
793
+ (``result.rpcError``) becomes the same ``error`` a local server would send, code kept
794
+ (``-32602`` = fix the arguments). Any refusal becomes an ``error`` with code None
795
+ and its reason and remedy, also kept in `_last_broker_refusal`.
796
+
797
+ ``db`` is ignored: crew picks the store (the run's own). Fail-open: an unreachable
798
+ broker leaves the ids out, as a dead local server does.
799
+ """
800
+ global _last_broker_refusal
801
+ _last_broker_refusal = None
802
+ channel = broker_channel()
803
+ if channel is None:
804
+ return {}
805
+ url, token = channel
806
+ calls = [r for r in requests if r.get("id") is not None and r.get("method") == "tools/call"]
807
+ responses: dict = {}
808
+ if not calls:
809
+ # `health()` / `list_tools()`: ask the broker for this unit's tool list — it checks the
810
+ # token and spends no call budget. A dead broker or a revoked token answers nothing.
811
+ status, body, why = _broker_post(f"{url}/api/v1/mcp/tools", {"token": token}, max(timeout, 5.0))
812
+ if status != 200 or body is None:
813
+ _last_broker_refusal = why or str((body or {}).get("error") or f"the broker answered {status}")
814
+ return {}
815
+ for req in requests:
816
+ rid = req.get("id")
817
+ if rid is None:
818
+ continue
819
+ if req.get("method") == "initialize":
820
+ responses[rid] = _broker_init(rid)
821
+ elif req.get("method") == "tools/list":
822
+ responses[rid] = {"jsonrpc": "2.0", "id": rid,
823
+ "result": {"tools": [{"name": t} for t in BROKER_TOOLS]}}
824
+ return responses
825
+ reached = False
826
+ for req in calls:
827
+ rid = req["id"]
828
+ params = req.get("params") or {}
829
+ tool = str(params.get("name") or "")
830
+ status, body, why = _broker_post(
831
+ f"{url}/api/v1/mcp/call",
832
+ {"token": token, "subject": BROKER_SUBJECT_PREFIX + tool, "args": params.get("arguments") or {}},
833
+ max(timeout, BROKER_MIN_TIMEOUT_S))
834
+ if status is None or body is None:
835
+ _last_broker_refusal = why
836
+ continue
837
+ reached = True
838
+ result = body.get("result")
839
+ if body.get("outcome") == "ok" and isinstance(result, dict):
840
+ rpc = result.get("rpcError")
841
+ if isinstance(rpc, dict):
842
+ responses[rid] = {"jsonrpc": "2.0", "id": rid, "error": {
843
+ "code": rpc.get("code"), "message": rpc.get("message") or "estate error"}}
844
+ else:
845
+ responses[rid] = {"jsonrpc": "2.0", "id": rid, "result": result}
846
+ continue
847
+ outcome = body.get("outcome") or body.get("code") or f"http {status}"
848
+ reason = body.get("reason") or body.get("error") or "refused"
849
+ remedy = body.get("remedy")
850
+ message = f"broker {outcome}: {reason}" + (f" ({remedy})" if remedy else "")
851
+ _last_broker_refusal = message
852
+ responses[rid] = {"jsonrpc": "2.0", "id": rid, "error": {"code": None, "message": message}}
853
+ if reached:
854
+ for req in requests:
855
+ if req.get("method") == "initialize" and req.get("id") is not None:
856
+ responses[req["id"]] = _broker_init(req["id"])
857
+ return responses
858
+
859
+
860
+ def _transport() -> Callable[..., dict]:
861
+ """The transport this call uses: the broker relay when `use_broker`, else the local seam."""
862
+ return _broker_dispatch if use_broker() else _active_dispatch
863
+
864
+
687
865
  # ─────────────────────────────────────────────────────────────────────────────
688
866
  # JSON-RPC — handshake + one tool call, all fail-open
689
867
  # ─────────────────────────────────────────────────────────────────────────────
@@ -721,7 +899,7 @@ def _rpc(method: str, params: dict, *, timeout: float) -> Optional[dict]:
721
899
  {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
722
900
  {"jsonrpc": "2.0", "id": 2, "method": method, "params": params},
723
901
  ]
724
- responses = _active_dispatch(requests, db=resolve_db(), timeout=timeout)
902
+ responses = _transport()(requests, db=resolve_db(), timeout=timeout)
725
903
  if 1 not in responses: # handshake never landed → estate unreachable
726
904
  return None
727
905
  return responses.get(2)
@@ -787,7 +965,7 @@ def health(timeout: float = 5.0) -> bool:
787
965
  """
788
966
  try:
789
967
  requests = [_initialize_request()]
790
- responses = _active_dispatch(requests, db=resolve_db(), timeout=timeout)
968
+ responses = _transport()(requests, db=resolve_db(), timeout=timeout)
791
969
  resp = responses.get(1)
792
970
  return bool(
793
971
  isinstance(resp, dict)
@@ -1189,6 +1367,10 @@ _USAGE = (
1189
1367
 
1190
1368
 
1191
1369
  def _emit(obj: Any) -> None:
1370
+ # A relayed call that came back empty says why (the broker's reason and remedy), additively:
1371
+ # the fail-open empty value is unchanged.
1372
+ if isinstance(obj, dict) and _last_broker_refusal and "broker_refusal" not in obj:
1373
+ obj = {**obj, "broker_refusal": _last_broker_refusal}
1192
1374
  sys.stdout.write(json.dumps(obj))
1193
1375
  sys.stdout.write("\n")
1194
1376
 
@@ -1207,7 +1389,8 @@ def main(argv: list) -> int:
1207
1389
  # A pure local probe — reports the mode without spawning, refusal included.
1208
1390
  _emit({"governed": is_governed(), "marker": governed_marker(),
1209
1391
  "readonly": read_only(), "store_pinned": store_pinned(),
1210
- "db": resolve_db(), "refusal": governed_refusal()})
1392
+ "db": resolve_db(), "transport": transport_name(),
1393
+ "broker": broker_channel() is not None, "refusal": governed_refusal()})
1211
1394
  return 0
1212
1395
  refusal = governed_refusal()
1213
1396
  if refusal:
@@ -233,16 +233,16 @@ First, ask which directories to onboard. **INTERACTIVE mode**: native prompt wit
233
233
 
234
234
  **Hand-off** — open the `wicked-garden-mem` skill and run its `ingest` action with `{selected_directory}` as the argument; on Claude Code this is the Skill tool, on any other seat open the named skill from your catalog and carry it out inline, then continue here.
235
235
 
236
- **Step D — Store onboarding memory**. `{DETECTED_TOOLS_SUMMARY}` lists only available tools (e.g. "gh, docker"):
237
- **Hand-off** — open the `wicked-garden-mem` skill and run its `store` action with the memory text `Onboarding: {project} fully onboarded on {date}. Languages: {DETECTED_LANGS}. Frameworks: {DETECTED_FWS}. Tools available: {DETECTED_TOOLS_SUMMARY}.` and `kind=skill, about=[onboarding, project-context, {project}]`; on Claude Code this is the Skill tool, on any other seat open the named skill from your catalog and carry it out inline, then continue here.
236
+ **Step D — Record the onboarding scan as knowledge**. `{DETECTED_TOOLS_SUMMARY}` lists only available tools (e.g. "gh, docker"). Languages, frameworks and tools were read from the repository, so they are knowledge, not memory:
237
+ **Hand-off** — open the `wicked-garden-mem` skill and run its `write` action with content `{project} project context: languages {DETECTED_LANGS}; frameworks {DETECTED_FWS}; tools available {DETECTED_TOOLS_SUMMARY}.`, `class=concept` and `source={selected_directory}`; on Claude Code this is the Skill tool, on any other seat open the named skill from your catalog and carry it out inline, then continue here.
238
238
 
239
239
  Show: "Onboarding complete — knowledge store has {N} chunks (mem review reports totals)."
240
240
 
241
241
  #### 5.2 Quick Scout
242
242
 
243
- Ask which directories to scout (same question mode pattern as 5.1 — native prompt or plain text depending on mode). Then Glob/Read the key entry points and store an enriched onboarding memory with detected context from Step 5.0:
243
+ Ask which directories to scout (same question mode pattern as 5.1 — native prompt or plain text depending on mode). Then Glob/Read the key entry points and record the detected context from Step 5.0 as knowledge (it was read from the repository):
244
244
 
245
- **Hand-off** — open the `wicked-garden-mem` skill and run its `store` action with the memory text `Onboarding: {project} quick-scouted on {date}. Languages: {DETECTED_LANGS}. Frameworks: {DETECTED_FWS}. Tools: {DETECTED_TOOLS_SUMMARY}. Full onboarding not yet run.` and `kind=skill, about=[onboarding, project-context, {project}]`; on Claude Code this is the Skill tool, on any other seat open the named skill from your catalog and carry it out inline, then continue here.
245
+ **Hand-off** — open the `wicked-garden-mem` skill and run its `write` action with content `{project} project context (quick scout): languages {DETECTED_LANGS}; frameworks {DETECTED_FWS}; tools {DETECTED_TOOLS_SUMMARY}.`, `class=concept` and `source={project}`; on Claude Code this is the Skill tool, on any other seat open the named skill from your catalog and carry it out inline, then continue here.
246
246
 
247
247
  #### 5.3 Skip
248
248
 
@@ -97,7 +97,9 @@ A chat seat asked by wicked-crew to label the operator's decisions reads
97
97
  (`{"content","class":"concept","source"}`) or `ingest` instead; `store`
98
98
  refuses content that carries a `source`.
99
99
  2. Pick `kind` (+ tier defaults): `fact` → semantic (decisions, gotchas,
100
- stable facts) · `skill` → procedural (patterns, conventions, how-tos) ·
100
+ stable facts) · `skill` → procedural (patterns, how-tos; a convention the
101
+ team must follow is a rule: propose it as `policy:<type>`, see the
102
+ mem-capture worker's Step 3b) ·
101
103
  `entity` → semantic (a person/system profile). Add `about` tags for
102
104
  recall hooks.
103
105
  3. Run the backend:
@@ -117,7 +119,8 @@ A chat seat asked by wicked-crew to label the operator's decisions reads
117
119
 
118
120
  By default recall searches the current project (`project:<cwd-basename>`)
119
121
  and its ancestors, so root-scoped memories stay visible and other projects'
120
- memories do not leak in. Pass `"scope"` to recall another project the same
122
+ memories do not leak in. Operator memories (`operator:<id>`, how the user
123
+ works) are recalled in every project. Pass `"scope"` to recall another project the same
121
124
  way. Searching everything, brain-migrated leaves
122
125
  (`brain:<project>/doc:<id>`) included, is an explicit opt-in:
123
126
  `"scope_prefix": ""` (or `"scope_prefix": "project:<name>"` for one subtree). Items return content, scope, tier,
@@ -175,7 +178,12 @@ Dispatch the **wicked-garden-mem-capture** fork worker at session end ("
175
178
  capture what we learned", before /clear or exit). It sweeps the conversation
176
179
  for decisions, patterns, gotchas, discoveries, and preferences, classifies
177
180
  each onto the estate kind/tier vocabulary, and batch-writes them via
178
- `capture-batch`. Report what it stored.
181
+ `capture-batch`. A standing rule the user stated ("always…", "never…") is
182
+ filed as a `policy:<type>` proposal for review (reach `operator`, every
183
+ project, unless they named a project), not stored as a memory. How the user
184
+ works (style, preferences, standing extra steps) is an operator memory at
185
+ `operator:local`, recalled in every project.
186
+ Report what it stored and proposed.
179
187
 
180
188
  No hook extracts memories from the transcript. The Stop hook used to
181
189
  pattern-match "decided to…" / "going to…" and filled the store with
@@ -14,8 +14,18 @@ project:wicked-garden
14
14
  org:acme/agent:claude
15
15
  brain:wicked-garden/doc:4f2a91
16
16
  project:wicked-estate/session:2026-08-11
17
+ operator:local
17
18
  ```
18
19
 
20
+ **Operator scope.** `operator:<id>` (`operator:local` by default) holds
21
+ memories about how the operator works: style, preferences, the extra steps
22
+ they always take. Every scoped recall includes it, whatever the project, so
23
+ these follow the operator everywhere. A memory about a project's work stays
24
+ at `project:<name>`. A rule the operator wants enforced is neither: it is a
25
+ `policy:<type>` proposal. A proposal says which with `payload.reach`:
26
+ `project`, `pattern`, or `operator`. For a policy it is `project` or
27
+ `operator`.
28
+
19
29
  Rules of thumb:
20
30
 
21
31
  - **Store** under the narrowest scope that still makes the memory findable.
@@ -24,8 +34,9 @@ Rules of thumb:
24
34
  - **Recall** uses one of two filters (mutually exclusive on the wire):
25
35
  - `scope` — estate's ancestor-visible inheritance: memories at this scope
26
36
  plus those inherited from ancestor scopes. **The default**, at
27
- `project:<cwd-basename>`, so a session sees its own project's memories
28
- and root-scoped ones, never another project's.
37
+ `project:<cwd-basename>`, so a session sees its own project's memories,
38
+ root-scoped ones and operator memories (`operator:<id>`), never another
39
+ project's.
29
40
  - `scope_prefix` — subtree match: every memory whose scope equals the
30
41
  prefix or descends from it. `scope_prefix: ""` = the root subtree =
31
42
  **everything**. Opt-in only: pass it when the user asks to search
@@ -47,7 +58,7 @@ for review or cleanup).
47
58
  | Kind | Default tier | Store when… |
48
59
  |------|--------------|-------------|
49
60
  | `fact` | `semantic` | a decision, gotcha, or stable distilled fact ("we chose X because Y") |
50
- | `skill` | `procedural` | a pattern, convention, or how-to that gets reinforced by use |
61
+ | `skill` | `procedural` | a pattern or how-to that gets reinforced by use (a convention the team must follow is a rule: a `policy:<type>` proposal, not a memory) |
51
62
  | `episode` | `episodic` | raw experience estate distills from; not a capture target (store the rule an event taught as a `fact`) |
52
63
  | `entity` | `semantic` | a profile of a person/system/component |
53
64
  | `working` | `working` | session-local scratch written by hooks; estate expires it after 24 h |
@@ -68,7 +79,8 @@ Brain's memory types map onto estate kinds as follows (used by
68
79
  |------------|-------------|------|
69
80
  | decision | `fact` | `semantic` |
70
81
  | pattern / procedural | `skill` | `procedural` |
71
- | preference | `fact` | `semantic` |
82
+ | preference, style, standing extra step (how the operator works) | `fact` at `operator:local` | `semantic` |
83
+ | standing rule ("always…", "never…") | none: a `policy:<type>` proposal | n/a |
72
84
  | gotcha | `fact` | `semantic` |
73
85
  | discovery | `fact` (the rule it taught) | `semantic` |
74
86
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wicked-garden-mem-capture
3
- description: "Session-teardown memory capture: sweep the conversation for durable learnings (decisions with a reason, patterns, gotchas, preferences), keep only what passes the durability test, classify each onto the estate kind/tier vocabulary, and batch-write them to wicked-estate memory. Use when: dispatched by the wicked-garden-mem skill's capture action, at session end, before /clear or exit, or when the user says 'capture what we learned' / 'remember this session'."
3
+ description: "Session-teardown memory capture: sweep the conversation for durable learnings (decisions with a reason, patterns, gotchas, preferences), keep only what passes the durability test, classify each onto the estate kind/tier vocabulary, and batch-write them to wicked-estate memory; standing rules the user stated go to the review queue as policy proposals instead. Use when: dispatched by the wicked-garden-mem skill's capture action, at session end, before /clear or exit, or when the user says 'capture what we learned' / 'remember this session'."
4
4
  metadata:
5
5
  role: worker
6
6
  ---
@@ -26,9 +26,17 @@ Relative paths in this skill are relative to the directory that contains this SK
26
26
  Look for things the session *learned*, not things it *did*:
27
27
 
28
28
  - **Decisions with a reason**: "chose X over Y because…"
29
- - **Patterns / conventions**: "this always…", "the convention is…"
29
+ - **Patterns**: "this always…" (how something behaves, witnessed)
30
30
  - **Gotchas**: "this broke because…", "don't X because…"
31
- - **Preferences** the user stated: "always use…", "never do…"
31
+ - **Standing rules** the user wants ENFORCED on the work: "always use…",
32
+ "never do…", "the convention is…", something a change can be checked
33
+ against ("UI work is not done without a screenshot"). These are steering, not memory: Step 3b files them as policy
34
+ proposals for review.
35
+ - **How the user works**: their style, preferences and the extra steps they
36
+ always take ("prefers tables in replies", "likes short commit subjects",
37
+ "usually opens the rendered page before reviewing CSS"). When the user
38
+ said it must be enforced, it is a standing rule instead. These are
39
+ **operator memories**: they apply in every project (Step 3).
32
40
 
33
41
  Then apply the **provenance test**: memory is what we *witnessed*, were
34
42
  *told*, or *decided*. What was read out of code, docs or the web is
@@ -74,14 +82,57 @@ applies), then map it onto the estate vocabulary
74
82
  | Finding | `kind` | tier (default) |
75
83
  |---------|--------|----------------|
76
84
  | decision, preference, gotcha, stable fact | `fact` | `semantic` |
77
- | pattern, convention, how-to | `skill` | `procedural` |
85
+ | pattern, how-to | `skill` | `procedural` |
86
+
87
+ A standing rule is not in this table: it goes to Step 3b, never to the
88
+ memory batch.
78
89
 
79
90
  Events ("X happened when Y") are not memories. Keep only the rule the event
80
91
  taught, as a `fact`.
81
92
 
82
93
  Add 2–5 `about` tags per memory (systems, components, topics mentioned) so
83
- recall has hooks. Scope: leave the default (`project:<cwd-basename>`)
84
- unless the learning clearly belongs to another scope.
94
+ recall has hooks.
95
+
96
+ Scope follows what the memory is about:
97
+
98
+ - **About this project or its code** (a decision, gotcha or pattern here):
99
+ leave the default (`project:<cwd-basename>`).
100
+ - **About how the user works** (style, preferences, standing extra steps):
101
+ pass `"scope": "operator:local"`. Every project's recall includes the
102
+ operator scope, so the memory follows the user everywhere. Never pin an
103
+ operator memory to one project.
104
+
105
+ ## Step 3b — standing rules go to review as policy proposals
106
+
107
+ A rule the user stated for the work ("always use X", "never do Y", "the
108
+ convention is Z") is enforceable: a reviewer can judge a change against it.
109
+ That makes it steering, and steering is approved by a person before it
110
+ applies. Do not write it as a memory. File one proposal per rule through the
111
+ estate shim's `propose` action (the review queue; nothing applies until the
112
+ user approves it):
113
+
114
+ ```bash
115
+ wicked-garden run scripts/_estate_client.py --readonly propose '{"kind_type":"policy:development","payload":{"rule":"<imperative: what must hold>","severity":"warn","capture":"rule","reach":"operator"},"facets":{"project":"<project>"}}'
116
+ ```
117
+
118
+ - `kind_type` is `policy:<type>`, with `<type>` exactly one of
119
+ `architecture`, `development`, `security`, `testing`, `operations`,
120
+ `compliance` or `design-ux`.
121
+ - `severity` is exactly one of `info`, `warn`, `error` or `critical`.
122
+ - `reach` says where the rule applies. A rule the user states is theirs
123
+ for **every** project: `"reach": "operator"`. Use `"reach": "project"`
124
+ only when they named this project or one of its repos (add `repo` to the
125
+ facets for a repo). The reviewer sees the reach and can change it; never
126
+ leave it out.
127
+ - `facets.project` is required (the shim refuses a proposal without it). It
128
+ records where the rule was said; with `"reach": "operator"` it does not
129
+ narrow the rule.
130
+ - Never pass `provenance`.
131
+
132
+ Store a pointer memory only when the *reason* for the rule is worth
133
+ recalling on its own ("we ban X because incident Y"). The rule itself lives
134
+ in the proposal. If the shim answers `ok: false`, report the rule text and
135
+ the degrade; never fall back to storing it as a memory.
85
136
 
86
137
  ## Step 4 — batch write
87
138
 
@@ -90,7 +141,8 @@ One backend call, JSON on stdin (never argv — summaries contain quotes):
90
141
  ```bash
91
142
  printf '%s' '{"memories":[
92
143
  {"content":"<summary 1>","kind":"fact","about":["tag1","tag2"]},
93
- {"content":"<summary 2>","kind":"skill","about":["tag3"]}
144
+ {"content":"<summary 2>","kind":"skill","about":["tag3"]},
145
+ {"content":"<how the user works>","kind":"fact","about":["style"],"scope":"operator:local"}
94
146
  ]}' | wicked-garden run \
95
147
  scripts/mem/estate_memory.py capture-batch -
96
148
  ```
@@ -112,6 +164,7 @@ wicked-garden run \
112
164
  ## Step 6 — report
113
165
 
114
166
  - `{N} memories stored` (+ `{K} failed` if any, with each refusal reason), with kinds
167
+ - `{P} rules proposed for review` (Step 3b), with each proposal id
115
168
  - one-line topic list
116
169
  - if estate was unreachable (`ok: false`): report the degrade explicitly so
117
170
  the learnings can be re-captured later — never claim success on a
@@ -4,7 +4,7 @@ description: |
4
4
  Learn an unfamiliar repo the way a senior engineer does: follow where the code
5
5
  actually churns, find what that churn touches that the rest of the system leans
6
6
  on, read the load-bearing code for real, then write down BOTH what you learned
7
- (memories) and what should hold (policies) — as inert estate proposals a human
7
+ (knowledge) and what should hold (policies) — as inert estate proposals a human
8
8
  reviews, never as asserted fact.
9
9
 
10
10
  Three BOUNDED phases, one wicked-crew unit each: (1) CHURN — sampled git churn →
@@ -28,8 +28,8 @@ metadata:
28
28
  A repo teaches you where to look if you follow two independent signals at once:
29
29
  **temporal** (where the code churns) and **structural** (what the rest of the system
30
30
  depends on). This skill walks the intersection, reads it for real, and **captures the
31
- learning back** as estate proposals — memories (what is true) and policies (what should
32
- hold). Garden ORCHESTRATES; estate GROUNDS (graph + read) and RECEIVES (the proposal
31
+ learning back** as estate proposals — knowledge (what is true, read from the code) and
32
+ policies (what should hold); a memory only for what you witnessed or were told. Garden ORCHESTRATES; estate GROUNDS (graph + read) and RECEIVES (the proposal
33
33
  queue); a human APPROVES — every capture is an inert proposal the studio Memories /
34
34
  Policies surfaces review, never an asserted fact.
35
35
 
@@ -97,7 +97,7 @@ Churn and centrality answer different questions; the intersection ranks targets.
97
97
 
98
98
  | | **High churn** | **Low churn** |
99
99
  |-----------------------|----------------|---------------|
100
- | **High centrality** | load-bearing + volatile → **read deeply**; likely yields BOTH a memory (how it works / why it moves) AND a policy (what must hold as it changes) | stable core → **read for invariants**; yields policies (severity `error`/`critical`) more than memories |
100
+ | **High centrality** | load-bearing + volatile → **read deeply**; likely yields BOTH knowledge (how it works / why it moves, read from the code) AND a policy (what must hold as it changes) | stable core → **read for invariants**; yields policies (severity `error`/`critical`) more than knowledge |
101
101
  | **Low centrality** | churny periphery (config, fixtures, generated) → **mostly noise**; skip unless it's a hot integration seam | quiet leaf → **skip** |
102
102
 
103
103
  The prime targets are the top-left cell: paths that appear in BOTH the phase-1
@@ -148,18 +148,21 @@ when the proposal is approved, so the learning is lost):
148
148
  knowledge. `kind_type: "memory"`; payload
149
149
  `{"content": "<1–3 sentences>", "tier": "<value>"}` where `tier` is EXACTLY one
150
150
  of `working` / `episodic` / `semantic` / `procedural` / `archival` — for a repo
151
- learning use `semantic` (a fact/decision) or `procedural` (a how-to/convention);
152
- NEVER invent a tier like `durable`. Facets `{"repo": "<name>", "project": "<name>"}`.
151
+ learning use `semantic` (a decision or gotcha) or `procedural` (a how-to you were
152
+ taught); NEVER invent a tier like `durable`. A convention or rule the team must
153
+ follow ("our convention is always X") is a **policy**, never a memory. Facets `{"repo": "<name>", "project": "<name>"}`.
153
154
  - **Policy** — what *should hold*. `kind_type: "policy:<type>"` where `<type>` ∈
154
155
  architecture · development · security · testing · operations · compliance ·
155
- design-ux; payload `{"rule": "<imperative>", "severity": "<value>"}` where
156
+ design-ux; payload `{"rule": "<imperative>", "severity": "<value>", "reach": "project"}`
157
+ (a rule learned from this repo applies to its project) where
156
158
  `severity` is EXACTLY one of `info` / `warn` / `error` / `critical` — the middle
157
159
  band is `warn`, NOT `warning`. Facets `{"repo": "<name>", "project": "<name>", "language": "<lang>"}`.
158
160
  - **Never pass `provenance`** — the server stamps it from `WICKED_RUN_*`. Passing
159
161
  it is at best ignored; treat provenance as not yours to set.
160
- - Derive **both** per target where the 2×2 warrants it: the memory records the
161
- *is*, the policy records the *ought*. A stable high-centrality invariant is a
162
- policy; a "this is how X actually wires up" discovery is a memory.
162
+ - Derive **both** per target where the 2×2 warrants it: the knowledge record
163
+ holds the *is*, the policy holds the *ought*. A stable high-centrality
164
+ invariant is a policy; a "this is how X actually wires up" discovery was read
165
+ from the code, so it is knowledge, never a memory.
163
166
 
164
167
  ## In a governed run
165
168
 
@@ -187,7 +190,7 @@ default; the shim refuses an unpinned store — report that, never guess one.
187
190
  call, no variants, no wrappers (governed-worker A2/A5). The read-only CLI is still
188
191
  allowed by the fence but is not a documented rung.
189
192
  2. **Capture** (phase 3) submits each proposal through the shim —
190
- `wicked-garden run scripts/_estate_client.py --readonly propose '{"kind_type":"memory","payload":{…},"facets":{…}}'`
193
+ `wicked-garden run scripts/_estate_client.py --readonly propose '{"kind_type":"knowledge","payload":{…},"facets":{…}}'`
191
194
  (`proposal.submit`; provenance is server-stamped from the `WICKED_RUN_*` markers on your
192
195
  environment, never passed). **There is no deliverable file and no file fallback**: write
193
196
  nothing into the repository (governed-worker E1) and nothing elsewhere either. If the shim
@@ -1,4 +1,4 @@
1
- # Capture — memories AND policies as estate proposals
1
+ # Capture — knowledge, memories AND policies as estate proposals
2
2
 
3
3
  The learning phase ends by writing back records through the estate
4
4
  **`proposal.submit`** tool (reached through the estate shim): **knowledge** (how the
@@ -17,7 +17,7 @@ the record; a human promotes it.
17
17
 
18
18
  | Field | Type | Rule |
19
19
  |-------|------|------|
20
- | `kind_type` | string | `"memory"` OR `"policy:<type>"`; must match `^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)?$` |
20
+ | `kind_type` | string | `"knowledge"`, `"memory"` OR `"policy:<type>"`; must match `^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)?$` |
21
21
  | `payload` | JSON **object** | required; a present non-object is rejected `-32602` (never silently defaulted) |
22
22
  | `facets` | JSON object of `axis:value` strings | each axis is a lowercase token `^[a-z][a-z0-9_-]*$`, each value non-empty; `project` is **required** (the shim refuses a proposal without it) |
23
23
 
@@ -36,17 +36,19 @@ The estate shim reaches `proposal.submit` without an MCP server being registered
36
36
  with your harness, on every seat CLI:
37
37
 
38
38
  ```bash
39
- wicked-garden run scripts/_estate_client.py --readonly propose '{"kind_type":"memory","payload":{"content":"…","tier":"semantic"},"facets":{"repo":"<name>","project":"<name>"}}'
39
+ wicked-garden run scripts/_estate_client.py --readonly propose '{"kind_type":"knowledge","payload":{"content":"…","class":"concept","source":"<repo>:<path>","source_rev":"<sha>"},"facets":{"repo":"<name>","project":"<name>"}}'
40
40
  ```
41
41
 
42
42
  `--readonly` is literal (the shim spawns `wicked-estate-mcp --readonly`; `proposal.submit`
43
43
  is the safe write it still permits). The store is pinned from the worker environment
44
- (`WICKED_ESTATE_DB` / `WICKED_HOME` / `WICKED_MEMORY_DB`) or `--db <path>`; in a run an
45
- unpinned store is refused with `{"ok": false, "reason": …}` — report it, do not guess a
46
- store. The shim answers `{"ok": true, "id": "<proposal-id>"}` or `{"ok": false,
44
+ (`WICKED_ESTATE_DB` / `WICKED_HOME` / `WICKED_MEMORY_DB`) or `--db <path>`; a container seat
45
+ relays the same `propose` through wicked-crew's broker (`WICKED_CREW_URL` + `WICKED_MCP_TOKEN`),
46
+ which files it under the run's project. With neither, a run refuses it with `{"ok": false,
47
+ "reason": …}` — report it, do not guess a store. The shim answers `{"ok": true, "id": "<proposal-id>"}` or `{"ok": false,
47
48
  "reason": …, "code": <json-rpc code or null>}`: `-32602` is a *your-args* bug (fix the
48
- shape), `null` means the estate was unreachable (keep the objects — next section). Long
49
- JSON: pass `-` as the json-args and pipe it on stdin. A `provenance` key is dropped.
49
+ shape), `null` means the estate was unreachable (keep the objects — next section). Pass
50
+ the JSON as ONE quoted argument, even when it is long: `propose` does not read stdin. A
51
+ `provenance` key is dropped.
50
52
 
51
53
  ### When `propose` fails in a governed run — no file fallback
52
54
 
@@ -113,10 +115,11 @@ implementation fails closed" is knowledge.
113
115
  - `content` — distilled, self-contained: the *why* and *what* a future session
114
116
  needs, not a paragraph of *how*. One to three sentences. If it needs the current
115
117
  file open to make sense, it isn't distilled yet.
116
- - `tier` — `"semantic"` for stable facts/decisions/gotchas ("X is wired via the
117
- bus, not a direct call"); `"procedural"` for a repeatable how-to/convention
118
- ("to add a language, drop a row in languages.toml + a .scm file — no core
119
- change").
118
+ - `tier` — `"semantic"` for a decision or gotcha you witnessed or were told
119
+ ("the bus path was chosen over a direct call because direct calls dropped
120
+ events in incident X"); `"procedural"` for a how-to you were taught. How the
121
+ code is wired ("X is wired via the bus") was read from the code: that is a
122
+ `knowledge` proposal, not a memory.
120
123
  - `facets` — `repo:` and `project:` locate the memory so faceted recall admits it
121
124
  for the right work. Use the repo's directory/remote name and the umbrella
122
125
  project (here, `wicked`). **`project:` is required** on every proposal,
@@ -133,7 +136,7 @@ implementation fails closed" is knowledge.
133
136
  ```json
134
137
  {
135
138
  "kind_type": "policy:architecture",
136
- "payload": { "rule": "<imperative statement of what must hold>", "severity": "error" },
139
+ "payload": { "rule": "<imperative statement of what must hold>", "severity": "error", "reach": "project" },
137
140
  "facets": { "repo": "<repo-name>", "project": "<project-name>", "language": "<lang>" }
138
141
  }
139
142
  ```
@@ -142,6 +145,9 @@ implementation fails closed" is knowledge.
142
145
  a diff against it ("Every emitted Edge carries {confidence, provenance,
143
146
  resolved_by}"; "No content-hash or line-number node ids"). Not a description —
144
147
  a *ought*.
148
+ - `reach` — `"project"`: a rule learned from this repo applies to this
149
+ repo's project. (`"operator"` = every project is for rules the operator
150
+ states, not for what a repo teaches.)
145
151
  - `severity` — `info` (advisory) · `warn` (should) · `error` (must; a violation
146
152
  blocks) · `critical` (must; a violation is a security/data-loss class). Map from
147
153
  the 2×2 below and from what the code's own guardrails signal.
@@ -156,16 +162,17 @@ proposal a read target warrants:
156
162
 
157
163
  | Cell | Signal | Propose |
158
164
  |------|--------|---------|
159
- | **high churn + high centrality** | load-bearing and moving | **BOTH**: a memory (how it works / why it churns / what it wires) + a policy (the invariant that must survive the churn — often `error`) |
160
- | **low churn + high centrality** | stable core / invariant | mostly **policy** (`error`/`critical`) capturing the invariant; add a memory only if the *why* is non-obvious |
161
- | **high churn + low centrality** | churny periphery | usually **neither**; a memory only if it's a hot integration seam worth flagging (e.g. "config X is edited every release — treat as an interface") |
165
+ | **high churn + high centrality** | load-bearing and moving | **BOTH**: knowledge (how it works / what it wires, read from the code) + a policy (the invariant that must survive the churn — often `error`) |
166
+ | **low churn + high centrality** | stable core / invariant | mostly **policy** (`error`/`critical`) capturing the invariant; add knowledge only if the *why* is non-obvious from the code, and a memory only if the operator told you the why |
167
+ | **high churn + low centrality** | churny periphery | usually **neither**; knowledge only if it's a hot integration seam worth flagging (e.g. "config X is edited every release — treat as an interface") |
162
168
  | **low churn + low centrality** | quiet leaf | **skip** |
163
169
 
164
- The discipline: a memory records the **is** (this is how the system is), a policy
165
- records the **ought** (this is what a change must preserve). A single load-bearing
166
- target frequently yields one of each — the memory so the next reader understands
167
- it, the policy so the next editor doesn't break it. Do not force a proposal from a
168
- cell that says skip; a thin capture pollutes the queue the operator must triage.
170
+ The discipline: knowledge records the **is** (this is how the system is, read
171
+ from the code), a policy records the **ought** (this is what a change must
172
+ preserve). A single load-bearing target frequently yields one of each — the
173
+ knowledge so the next reader understands it, the policy so the next editor
174
+ doesn't break it. A memory is only for what you witnessed or were told.
175
+ Do not force a proposal from a cell that says skip; a thin capture pollutes the queue the operator must triage.
169
176
 
170
177
  ## Worked examples
171
178
 
@@ -191,7 +198,8 @@ Policy (architecture, from the same subsystem's invariant):
191
198
  "kind_type": "policy:architecture",
192
199
  "payload": {
193
200
  "rule": "Every emitted Edge must carry {confidence, provenance, resolved_by}; never present a heuristic edge as a fact.",
194
- "severity": "error"
201
+ "severity": "error",
202
+ "reach": "project"
195
203
  },
196
204
  "facets": { "repo": "wicked-estate", "project": "wicked", "language": "rust" }
197
205
  }
@@ -204,7 +212,8 @@ Policy (development, language-scoped, from a stable core with a clear guardrail)
204
212
  "kind_type": "policy:development",
205
213
  "payload": {
206
214
  "rule": "Rust: no bare panic!/unwrap in library crates; surface a typed error.",
207
- "severity": "warn"
215
+ "severity": "warn",
216
+ "reach": "project"
208
217
  },
209
218
  "facets": { "repo": "wicked-estate", "project": "wicked", "language": "rust" }
210
219
  }
@@ -87,10 +87,13 @@ wicked-garden run scripts/_estate_client.py --readonly call '{"tool":"BlastRadiu
87
87
  ```
88
88
  `--readonly` is literal (the spawned estate binary reads only; `WICKED_ESTATE_READONLY=1` rides
89
89
  every worker so it spawns read-only by default). The store rides `WICKED_ESTATE_DB` /
90
- `WICKED_HOME` / `WICKED_MEMORY_DB` from the environment, or `--db <path>`; the shim refuses an
91
- unpinned store (`{"ok": false, "reason": …}`) — report that, never guess a store. There is no
92
- other transport: no estate tool is registered on any seat, so there is nothing to "connect",
93
- nothing to fall back to, and no second rung — a grep is not the graph.
90
+ `WICKED_HOME` / `WICKED_MEMORY_DB` from the environment, or `--db <path>`. A container seat has
91
+ no local store: the same command relays through wicked-crew's broker (the run hands the seat
92
+ its address and a unit token), which answers from the run's own graph. With neither, the shim refuses
93
+ (`{"ok": false, "reason": …}`); a relayed call the broker refused names why in
94
+ `broker_refusal` — report either, never guess a store. There is no other transport: no estate
95
+ tool is registered on any seat, so there is nothing to "connect", nothing to fall back to, and
96
+ no second rung — a grep is not the graph.
94
97
 
95
98
  **Resolve the symbol.** Estate tools take symbol **names** directly (a file node's name is its
96
99
  repo-relative path, e.g. `scripts/_bus.py`); when a name is ambiguous or you need the node id,
@@ -41,9 +41,10 @@ every project, on explicit request); `--budget <tokens>`
41
41
  `[knowledge · src docs/adr/0005.md]` or
42
42
  `[memory · fact · 2026-10-10 · project:wicked-garden]`, so the reader
43
43
  sees which side it came from and how old it is
44
- - on conflict, an operator ruling (memory) governs what *should* hold and
45
- the source (knowledge) governs what *does*: show the disagreement, never
46
- pick silently by date
44
+ - on conflict, an approved steering rule (estate `rules.recall`) governs
45
+ what *should* hold and the source (knowledge) governs what *does*; a
46
+ memory says what was decided or told and why, not what is enforced.
47
+ Show the disagreement, never pick silently by date
47
48
 
48
49
  End with a `Sources:` list — every distinct `source` / scope used, so
49
50
  the reader can verify each claim.