wicked-garden 12.59.0 → 12.61.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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -1
- package/docs/README.md +1 -1
- package/docs/outgov-per-turn.md +26 -18
- package/hooks/scripts/pre_tool.py +10 -8
- package/hooks/scripts/prompt_submit.py +181 -49
- package/hooks/scripts/stop.py +96 -20
- package/package.json +1 -1
- package/scripts/_context_backend.py +16 -4
- package/scripts/_estate_client.py +44 -6
- package/scripts/_session.py +5 -0
- package/scripts/editor/install.py +11 -3
- package/scripts/mcp/install.py +110 -24
- package/scripts/mem/estate_memory.py +75 -4
- package/skills/classify/SKILL.md +8 -5
- package/skills/mcp-scaffold/SKILL.md +2 -2
- package/skills/mem/SKILL.md +20 -2
- package/skills/repo-learn/refs/capture-proposals.md +3 -1
|
@@ -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.61.0"}], "version": "12.61.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.61.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.61.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coding-agent",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.61.0] — 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`install.py --from-run` finds the pack in a real run (editor and MCP) (#1351).** The engine runs the install-plan and install Tool phases in the run's worktree and sets `WICKED_TREE` to that worktree's git tree id (wicked-core#776), not a path. Both install scripts read it as a directory, found nothing, and every editor-plugin run failed at install-plan with "found 0 (none)", so the install consent gate was never reached. `--from-run` now searches the worktree (the cwd) and uses `WICKED_TREE` only when it names a directory.
|
|
9
|
+
- **A credential-free MCP server installs (`scripts/mcp/install.py`) (#1351).** The installer assumed every server reads a `<SERVER>_TOKEN` secret. A local-computation server has no upstream, and its config names no `baseUrl` and no auth scheme (`auth` absent, null or `{"scheme": "none"}`). The installer still sent `auth: env:<SERVER>_TOKEN`, crew answered `secret_missing`, and the server was never registered, so a design that correctly invented no credential could not be installed. Such a server now smokes and registers with no secret variable. A server with an upstream, and `--from-npm` installs, are unchanged.
|
|
10
|
+
- **An MCP server delivered without a `package-lock.json` installs (`scripts/mcp/install.py`) (#1352).** A container seat can't reach the npm registry, so the server a `/workflow-mcp-server` run delivers has no lockfile. `npm ci` refuses such a tree, so the install failed after the operator had consented, with only npm's debug-log path as the reason. The installer now copies the server to a transient `<root>/<key>/resolve/`, resolves the dependencies there once with `npm install --package-lock=true --ignore-scripts`, and builds there. The staged copy installs from that lock, and the delivered tree is never written. An npm that writes no lock (`package-lock=false`) fails before anything is built. The plan and the install record say `"lockfile": "resolved at install"`, because those versions were picked at install time and the run never reviewed them. A delivered lock still goes through `npm ci`, and the output says `"lockfile": "delivered"`. A failing npm step now reports npm's own reason instead of its debug-log path. That is the `npm error code` line and the first line after it that isn't metadata, plus the script's own last line (tsc's error) when `npm run build` fails.
|
|
11
|
+
|
|
12
|
+
## [12.60.0] — 2026-10-26
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Per-turn hooks stop injecting on prompts they don't fit (#1333).** The prompt and Stop hooks run on every turn, on every seat that loads garden. An injection that misfires costs context and teaches agents to skim past reminders.
|
|
16
|
+
- **Context Assembly is decided per prompt.** The session intent is sticky, so once a session's first prompt read as research, every later prompt got the directive, including "agree", "still waiting" and "how is it going?". It now fires only when the prompt itself names code, files, symbols, repos or past decisions AND has more than 12 words or asks a question. Acknowledgements (including short ones such as "yes, sounds good to me"), status checks and one- or two-word replies get nothing, even when they name a file or symbol ("how is repo_checks.rs going?"). The directive no longer names the estate MCP `SearchEntity` tool (no estate MCP is registered on a seat). It points at the mem and search skills and the read-only shim instead.
|
|
17
|
+
- **Output Governance runs once per session and does the work itself.** The per-turn line ("If the estate MCP is connected: call `rules.recall`…") could never run. The first Stop of a session now calls `rules.recall {"rule_type":"policy"}` through the read-only shim. When the store holds policy rules, the hook names the count and the top three once, with the command that lists them all. The once-per-session claim is an atomic marker file, so a prompt hook saving a stale session state can't re-arm it. Zero rules, an unreachable estate or an error: silent. `WG_OUTGOV=off` still opts out.
|
|
18
|
+
- **No `classify-due` nag.** When the regex router falls through, the hook emits nothing. A medium-confidence regex match (one keyword, such as "outage" in a status question) emits nothing either. Only a high-confidence match (≥ 0.7) tags the turn.
|
|
19
|
+
- **`prove:` lines ride only a confident archetype** (score ≥ 0.7, from the regex or from a persisted model classification), never a fall-through. A persisted classification (the model's own, from `wicked-garden-classify`) is echoed back once per classification, not on every turn, and only its confident archetypes.
|
|
20
|
+
- **The agent memory path is one line (#1333).**
|
|
21
|
+
- `_estate_client.py <action> -` reads its JSON from stdin. It used to parse `-` as a literal string, so `propose -` failed with a generic shape error.
|
|
22
|
+
- `propose` errors name the exact field: `missing kind_type`, `payload must be an object, got str`, `json-args is not valid JSON`; a missing `facets.project` is still reported by name.
|
|
23
|
+
- New `wicked-garden-mem` action `propose`: `wicked-garden run scripts/mem/estate_memory.py --readonly propose '<text>'` (or `propose -` with text or JSON on stdin) builds the envelope itself: `kind_type: memory`, `payload {content, tier}`, `facets {repo, project}` from the cwd's checkout (outside a checkout, `project` = the cwd name; default `recall` now sends that same intent, so the approved memory is recalled from where it was proposed). It applies the same durability refusals as `store`. It is the write a read-only seat still has, and the memory lands pending until an operator approves it.
|
|
24
|
+
- The hook that blocks writes to `MEMORY.md` and the native memory directory now prints that exact command.
|
|
25
|
+
|
|
26
|
+
|
|
5
27
|
## [12.59.0] — 2026-10-25
|
|
6
28
|
|
|
7
29
|
### Added
|
|
@@ -1197,7 +1219,9 @@ through `v9.2.18`. Major themes across that span:
|
|
|
1197
1219
|
|
|
1198
1220
|
For each of those releases, see the corresponding tag in git history.
|
|
1199
1221
|
|
|
1200
|
-
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.
|
|
1222
|
+
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.61.0...HEAD
|
|
1223
|
+
[12.61.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.60.0...v12.61.0
|
|
1224
|
+
[12.60.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.59.0...v12.60.0
|
|
1201
1225
|
[12.59.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.58.0...v12.59.0
|
|
1202
1226
|
[12.58.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.57.0...v12.58.0
|
|
1203
1227
|
[12.57.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.56.0...v12.57.0
|
package/docs/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Work in wicked-garden is organized around **10 work-shape archetypes** — not a
|
|
|
14
14
|
| [Required Peers](required-peers.md) | The one required peer (plus the opt-in layers) and the install/runtime stance |
|
|
15
15
|
| [The Compiler](compiler.md) | `/wicked-garden-prove compile` — emit a self-contained vault-backed gate into any repo |
|
|
16
16
|
| [Extending the Catalog](extending.md) | Ship a third-party pack — `{vendor}-{domain}-{role}` skills, the `wicked-pack.json` manifest, the shipped conformance gate, install + crew routing |
|
|
17
|
-
| [Output Governance (
|
|
17
|
+
| [Output Governance (hook-side)](outgov-per-turn.md) | The advisory hook lane — `WG_OUTGOV` modes (default `warn`), a once-per-session `rules.recall` audit through the read-only shim, content-hash provenance for the graph-derived rules dir |
|
|
18
18
|
| [Brain Chunk Format](brain-chunk-format.md) | Historical: how content was chunked for the retired wicked-brain index (the migrated chunks in wicked-estate keep this shape) | <!-- historical -->
|
|
19
19
|
|
|
20
20
|
## Quick Links
|
package/docs/outgov-per-turn.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Output Governance — the
|
|
1
|
+
# Output Governance — the Hook-Side Advisory Lane
|
|
2
2
|
|
|
3
3
|
How the interactive (hook-side) half of output governance works after AW-16
|
|
4
4
|
(arch-R14): **one source of per-turn truth, default-on advisory, fail-open by
|
|
@@ -10,7 +10,7 @@ Enforcement in the wicked platform is two-tier (TARGET-ARCHITECTURE contract 3):
|
|
|
10
10
|
|
|
11
11
|
| Tier | Where | Behavior |
|
|
12
12
|
|------|-------|----------|
|
|
13
|
-
| **Advisory** | garden hooks (`stop.py`
|
|
13
|
+
| **Advisory** | garden hooks (`stop.py` session audit, `guard_pipeline` Check 6) | fail-open — surfaces findings, never blocks |
|
|
14
14
|
| **Enforcing** | crew/core gates (`output-gate-hook`, acceptance gate) | fail-closed — deny dominates |
|
|
15
15
|
|
|
16
16
|
There is **no third tier**. `WG_OUTGOV=strict` only strengthens the advisory's
|
|
@@ -23,11 +23,16 @@ enforced belongs in the crew/core gates, not here.
|
|
|
23
23
|
Both hook-side consumers read the **estate graph** — never an independent
|
|
24
24
|
rulebook:
|
|
25
25
|
|
|
26
|
-
1. **
|
|
27
|
-
the Stop
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
1. **Session audit** (`hooks/scripts/stop.py::_check_outgov_compliance`):
|
|
27
|
+
on the FIRST Stop of a session the hook itself runs **`rules.recall`**
|
|
28
|
+
(`{"rule_type": "policy"}`, severity-ordered, graph-backed, the wire twin
|
|
29
|
+
of wicked-governance's `recall_rules`) through the read-only shim
|
|
30
|
+
(`scripts/_estate_client.py --readonly call`). When the store holds policy
|
|
31
|
+
rules it names the count and the top three once, with the command that
|
|
32
|
+
lists them all. Zero rules, an unreachable estate or any error: silent.
|
|
33
|
+
It used to be a per-turn line telling the model to call the tool "if the
|
|
34
|
+
estate MCP is connected", which no seat ever is (by design), so it could
|
|
35
|
+
never run (garden#1333).
|
|
31
36
|
|
|
32
37
|
2. **Session-close pattern check**
|
|
33
38
|
(`scripts/platform/guard_pipeline.py::check_outgov_pattern`, Check 6):
|
|
@@ -39,13 +44,15 @@ rulebook:
|
|
|
39
44
|
|
|
40
45
|
## `WG_OUTGOV` modes and the default
|
|
41
46
|
|
|
42
|
-
| Value |
|
|
43
|
-
|
|
44
|
-
| *(unset)* → **`warn`** |
|
|
45
|
-
| `warn` |
|
|
46
|
-
| `strict` |
|
|
47
|
+
| Value | Session audit (stop.py) | Check 6 (guard_pipeline) |
|
|
48
|
+
|-------|-------------------------|---------------------------|
|
|
49
|
+
| *(unset)* → **`warn`** | runs once per session (default-on advisory) | runs |
|
|
50
|
+
| `warn` | runs once per session | runs |
|
|
51
|
+
| `strict` | runs once, stronger wording — still advisory | runs |
|
|
47
52
|
| `off` (or any other value) | silent | skips |
|
|
48
53
|
|
|
54
|
+
The audit is also silent when `WICKED_CONTEXT_BACKEND=off`.
|
|
55
|
+
|
|
49
56
|
**The default flipped from `off` to `warn` in AW-16**, after the seed corpus
|
|
50
57
|
(AW-13) gave the advisory real rules to recall. Every mode is fail-open: a
|
|
51
58
|
missing estate, a missing rules dir, or a broken bundle can only *reduce*
|
|
@@ -63,7 +70,8 @@ The flip is **default-on in code, per-repo in practice**:
|
|
|
63
70
|
- **Noise budget owner**: whoever keeps a repo opted in owns its noise budget
|
|
64
71
|
(the repo maintainer by default). The advisory costs at most one
|
|
65
72
|
systemMessage per Stop; Check 6 findings are capped by the guard-pipeline
|
|
66
|
-
budget and profile.
|
|
73
|
+
budget and profile. The audit costs at most one systemMessage per session,
|
|
74
|
+
and none when the store has no policy rules.
|
|
67
75
|
- If recall noise is systemic (bad rules, not bad wiring), fix the corpus —
|
|
68
76
|
retire or re-scope the offending rules in the graph — rather than muting the
|
|
69
77
|
lane ecosystem-wide.
|
|
@@ -127,18 +135,18 @@ Hermetic tests — no live estate needed, fixture rules dirs only:
|
|
|
127
135
|
**missing rules dir ⇒ skip with a note, zero findings, no block**; stale
|
|
128
136
|
provenance ⇒ warn-severity finding only, rules still surfaced; hash
|
|
129
137
|
determinism + generator/consumer recipe equality.
|
|
130
|
-
- `tests/hooks/test_stop_outgov.py` — the
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
- `tests/hooks/test_stop_outgov.py` — the audit runs `rules.recall` once per
|
|
139
|
+
session; silent on zero rules, an unreachable estate or an error; names no
|
|
140
|
+
MCP; default-on; `off` opts out; `strict` stays advisory.
|
|
133
141
|
|
|
134
142
|
## Pointers
|
|
135
143
|
|
|
136
|
-
- `hooks/scripts/stop.py` — `_check_outgov_compliance` (per-
|
|
144
|
+
- `hooks/scripts/stop.py` — `_check_outgov_compliance` (once-per-session audit)
|
|
137
145
|
- `scripts/platform/guard_pipeline.py` — Check 6 (`check_outgov_pattern`),
|
|
138
146
|
`compute_rules_content_hash`, `write_rules_provenance`, the `hash` CLI
|
|
139
147
|
- `skills/engineering/refs/conformance.md` — the agent-half rubric that
|
|
140
148
|
evaluates Check 6's surfaced rules semantically
|
|
141
149
|
- wicked-core `crates/wicked-governance` — the graph side: `rules ingest`,
|
|
142
150
|
`rules fanout` (the manifest + store split), `rules.recall`'s Rust twin
|
|
143
|
-
- History: garden#983 (pattern check), garden#984 (per-turn guardrail, closed),
|
|
151
|
+
- History: garden#983 (pattern check), garden#984 (per-turn guardrail, closed), garden#1333 (per-turn line → session audit),
|
|
144
152
|
AW-16 / arch-R14 (this document's change)
|
|
@@ -280,17 +280,19 @@ def _handle_write_guard(tool_input: dict) -> str:
|
|
|
280
280
|
is_auto_memory = _AUTO_MEMORY_MARKER in file_path and "/memory/" in file_path
|
|
281
281
|
|
|
282
282
|
if is_memory_md or is_auto_memory:
|
|
283
|
-
#
|
|
284
|
-
#
|
|
283
|
+
# garden#1333: hand over the exact one-line command, not a skill name
|
|
284
|
+
# the agent then has to route through (and that refuses `store` on a
|
|
285
|
+
# read-only seat). Fail-open to the same literal command.
|
|
285
286
|
try:
|
|
286
|
-
from _context_backend import
|
|
287
|
-
_mem_target = memory_directive_target()
|
|
287
|
+
from _context_backend import MEMORY_PROPOSE_COMMAND as _mem_cmd
|
|
288
288
|
except Exception:
|
|
289
|
-
|
|
289
|
+
_mem_cmd = ("wicked-garden run scripts/mem/estate_memory.py --readonly propose "
|
|
290
|
+
"'<the rule, why it holds, when it applies>'")
|
|
290
291
|
return _deny(
|
|
291
|
-
"Do not write to MEMORY.md or the auto memory directory
|
|
292
|
-
"
|
|
293
|
-
|
|
292
|
+
"Do not write to MEMORY.md or the auto memory directory; this project keeps "
|
|
293
|
+
"memory in wicked-estate. Save the learning with one command instead "
|
|
294
|
+
"(it files a memory proposal for this project; pipe longer text with `propose -`):\n"
|
|
295
|
+
f"{_mem_cmd}"
|
|
294
296
|
)
|
|
295
297
|
|
|
296
298
|
# Issue #442: challenge-artifacts gate for build-phase writes
|
|
@@ -185,51 +185,73 @@ def _with_prove_hints(directive: str, archetypes: list[str]) -> str:
|
|
|
185
185
|
return directive
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
# garden#1333: a `prove:` line is steering the agent acts on at the gate
|
|
189
|
+
# phase, so it rides only a confident classification. Mirrors
|
|
190
|
+
# archetypes_v11.HIGH_CONFIDENCE (kept literal so the hook needs no import
|
|
191
|
+
# to decide; tests lock the two together).
|
|
192
|
+
_PROVE_MIN_SCORE = 0.7
|
|
193
|
+
|
|
194
|
+
|
|
188
195
|
def _build_archetype_directive(prompt: str, intent: str, state=None) -> str | None:
|
|
189
196
|
"""v11 — emit a slim work-shape archetype steering directive.
|
|
190
197
|
|
|
191
198
|
Two-tier classification:
|
|
192
199
|
|
|
193
|
-
1. **Persisted (LLM)**: when SessionState carries
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
2. **Fallback (regex)**:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
1. **Persisted (LLM)**: when SessionState carries `archetypes_v11` (the
|
|
201
|
+
model classified through the wicked-garden-classify skill), emit its
|
|
202
|
+
tag / shape card.
|
|
203
|
+
2. **Fallback (regex)**: archetypes_v11.detect_archetypes() on the
|
|
204
|
+
prompt. Emits only a HIGH-confidence match; a fall-through (only
|
|
205
|
+
triage) or a medium keyword hit emits nothing (garden#1333: the
|
|
206
|
+
fall-through used to ask for a model classification on every
|
|
207
|
+
ordinary question, and medium hits misrouted status questions).
|
|
208
|
+
|
|
209
|
+
Both tiers emit only archetypes scored >= _PROVE_MIN_SCORE, so a
|
|
210
|
+
`prove:` line never rides a low-confidence route; the persisted tier
|
|
211
|
+
is echoed once per classification.
|
|
212
|
+
Silent on simple-edit sessions and on chatter (acknowledgements, status
|
|
213
|
+
checks, short replies). Fail-open on any error.
|
|
205
214
|
"""
|
|
206
215
|
if not prompt or not prompt.strip():
|
|
207
216
|
return None
|
|
208
217
|
if intent == "simple-edit":
|
|
209
218
|
return None
|
|
219
|
+
try:
|
|
220
|
+
if _is_chatter(prompt):
|
|
221
|
+
return None
|
|
222
|
+
except Exception:
|
|
223
|
+
return None
|
|
210
224
|
|
|
211
225
|
# Tier 1: persisted classification from the classify skill
|
|
212
226
|
persisted = (getattr(state, "archetypes_v11", None) or []) if state else []
|
|
213
227
|
if persisted:
|
|
214
|
-
#
|
|
215
|
-
|
|
228
|
+
# The model classified this itself (wicked-garden-classify). Echo it
|
|
229
|
+
# back once per classification, and only its confident archetypes:
|
|
230
|
+
# repeating it on every later turn added nothing (garden#1333).
|
|
231
|
+
real = [a for a in persisted
|
|
232
|
+
if a.get("name") != "triage"
|
|
233
|
+
and float(a.get("score", 0)) >= _PROVE_MIN_SCORE][:3]
|
|
216
234
|
if not real:
|
|
217
235
|
return None
|
|
218
|
-
|
|
236
|
+
key = f"{getattr(state, 'classified_at', None)}|" + json.dumps(real, sort_keys=True)
|
|
237
|
+
if getattr(state, "archetype_echoed", None) == key:
|
|
238
|
+
return None
|
|
239
|
+
try:
|
|
240
|
+
state.update(archetype_echoed=key)
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
if len(real) == 1:
|
|
219
244
|
n = real[0]["name"]
|
|
220
245
|
s = float(real[0]["score"])
|
|
221
246
|
base = f"<wg archetype=\"{n}\" score=\"{s:.2f}\" classified=\"llm\" />"
|
|
222
247
|
return _with_prove_hints(base, [n])
|
|
223
|
-
shape_lines = [
|
|
224
|
-
f" - {a['name']} ({float(a.get('score', 0)):.2f})"
|
|
225
|
-
for a in real[:3]
|
|
226
|
-
]
|
|
248
|
+
shape_lines = [f" - {a['name']} ({float(a['score']):.2f})" for a in real]
|
|
227
249
|
base = (
|
|
228
250
|
"<wg archetypes classified=\"llm\">\n"
|
|
229
251
|
+ "\n".join(shape_lines)
|
|
230
252
|
+ "\n</wg>"
|
|
231
253
|
)
|
|
232
|
-
return _with_prove_hints(base, [a["name"] for a in real
|
|
254
|
+
return _with_prove_hints(base, [a["name"] for a in real])
|
|
233
255
|
|
|
234
256
|
# Tier 2: regex fallback
|
|
235
257
|
try:
|
|
@@ -240,7 +262,7 @@ def _build_archetype_directive(prompt: str, intent: str, state=None) -> str | No
|
|
|
240
262
|
for p in (_scripts, _crew):
|
|
241
263
|
if str(p) not in _sys.path:
|
|
242
264
|
_sys.path.append(str(p))
|
|
243
|
-
from archetypes_v11 import detect_archetypes
|
|
265
|
+
from archetypes_v11 import detect_archetypes
|
|
244
266
|
except Exception:
|
|
245
267
|
return None
|
|
246
268
|
|
|
@@ -249,33 +271,25 @@ def _build_archetype_directive(prompt: str, intent: str, state=None) -> str | No
|
|
|
249
271
|
except Exception:
|
|
250
272
|
return None
|
|
251
273
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if not
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"Regex classifier could not route this prompt to a v11 archetype. "
|
|
262
|
-
"Invoke the `wicked-garden-classify` skill to classify with model "
|
|
263
|
-
"reasoning, then run the chosen archetype's playbook."
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
high = [(n, s, e) for (n, s, e) in real if s >= HIGH_CONFIDENCE]
|
|
267
|
-
if len(high) == 1 and len(real) == 1:
|
|
268
|
-
n, s, _ = high[0]
|
|
274
|
+
high = [(n, s) for (n, s, _e) in matches
|
|
275
|
+
if n != "triage" and s >= _PROVE_MIN_SCORE]
|
|
276
|
+
if not high:
|
|
277
|
+
# Fall-through or medium-only: the regex has no confident route, so
|
|
278
|
+
# it says nothing (garden#1333). The classify skill stays available
|
|
279
|
+
# for the agent to invoke when the work shape matters.
|
|
280
|
+
return None
|
|
281
|
+
if len(high) == 1:
|
|
282
|
+
n, s = high[0]
|
|
269
283
|
base = f"<wg archetype=\"{n}\" score=\"{s:.2f}\" classified=\"regex\" />"
|
|
270
284
|
return _with_prove_hints(base, [n])
|
|
271
285
|
|
|
272
|
-
shape_lines = [f" - {n} ({s:.2f})" for (n, s
|
|
286
|
+
shape_lines = [f" - {n} ({s:.2f})" for (n, s) in high[:3]]
|
|
273
287
|
base = (
|
|
274
288
|
"<wg archetypes classified=\"regex\">\n"
|
|
275
289
|
+ "\n".join(shape_lines)
|
|
276
290
|
+ "\n</wg>"
|
|
277
291
|
)
|
|
278
|
-
return _with_prove_hints(base, [n for (n, _s
|
|
292
|
+
return _with_prove_hints(base, [n for (n, _s) in high[:3]])
|
|
279
293
|
|
|
280
294
|
|
|
281
295
|
def _suggest_jam(prompt: str, state) -> str | None:
|
|
@@ -444,6 +458,122 @@ def _has_imperative_technical_verb(prompt_lower: str) -> bool:
|
|
|
444
458
|
return any(verb in prompt_lower for verb in _IMPERATIVE_TECHNICAL_VERBS)
|
|
445
459
|
|
|
446
460
|
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
# garden#1333 — per-prompt grounding gate
|
|
463
|
+
#
|
|
464
|
+
# The session intent above is sticky: once a session's first prompt read as
|
|
465
|
+
# feature/research, every later prompt ("agree", "still waiting", "how is it
|
|
466
|
+
# going?") got the Context Assembly directive. That trained agents to skim
|
|
467
|
+
# past it. The directive is now decided per prompt, from what the prompt
|
|
468
|
+
# itself names: it fires only when the prompt names code, files, symbols,
|
|
469
|
+
# repos or past decisions AND is long enough to need grounding (more than
|
|
470
|
+
# _GROUNDING_MIN_WORDS words) or asks a question about them.
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
_GROUNDING_MIN_WORDS = 12 # "more than ~12 words" (garden#1333 item 1)
|
|
474
|
+
|
|
475
|
+
# Strong code signals: the prompt points at a concrete thing in the codebase.
|
|
476
|
+
_IDENTIFIER_PATTERN = re.compile(
|
|
477
|
+
r"\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b" # snake_case / SCREAMING_CASE
|
|
478
|
+
r"|\b[a-z]+[A-Z][A-Za-z0-9]*\b" # camelCase
|
|
479
|
+
r"|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b" # PascalCase (two humps)
|
|
480
|
+
r"|\b\w+::\w+" # Rust/C++ paths
|
|
481
|
+
r"|\b\w+\(\)" # call()
|
|
482
|
+
r"|\bwicked-[a-z][a-z0-9-]*\b" # a wicked-* repo
|
|
483
|
+
)
|
|
484
|
+
_DECISION_PATTERN = re.compile(
|
|
485
|
+
r"\b(?:why did we|what did we decide|did we decide|we decided|decision|"
|
|
486
|
+
r"adr|rationale|why do we)\b",
|
|
487
|
+
re.IGNORECASE,
|
|
488
|
+
)
|
|
489
|
+
# Weak code signals: code nouns. They ground only a question or a long prompt.
|
|
490
|
+
_CODE_NOUN_PATTERN = re.compile(
|
|
491
|
+
r"\b(?:function|method|class|module|crate|package|codebase|repo|repository|"
|
|
492
|
+
r"hook|endpoint|schema|symbol|api|script|test|tests|struct|trait|interface|"
|
|
493
|
+
r"config|migration|query|branch|commit)s?\b",
|
|
494
|
+
re.IGNORECASE,
|
|
495
|
+
)
|
|
496
|
+
# Status checks and acknowledgements never need grounding.
|
|
497
|
+
_STATUS_PATTERN = re.compile(
|
|
498
|
+
r"\b(?:how(?:'s| is| are)\b.*\bgoing|status|any (?:update|news|progress)|"
|
|
499
|
+
r"still (?:waiting|running|working|going)|where are we|are (?:you|we) done|"
|
|
500
|
+
r"is it done|done yet|how far|eta|progress|update me|what'?s left)\b",
|
|
501
|
+
re.IGNORECASE,
|
|
502
|
+
)
|
|
503
|
+
_ACKNOWLEDGEMENTS = frozenset({
|
|
504
|
+
"agree", "agreed", "yes", "ok", "okay", "sure", "yep", "yup", "go",
|
|
505
|
+
"go ahead", "do it", "continue", "proceed", "thanks", "thank you", "ty",
|
|
506
|
+
"lgtm", "looks good", "sounds good", "great", "perfect", "nice", "cool",
|
|
507
|
+
"approved", "approve", "no", "nope", "stop", "wait", "next", "done",
|
|
508
|
+
"keep going", "carry on", "makes sense", "got it", "right", "correct",
|
|
509
|
+
})
|
|
510
|
+
# `do` opens a question only with a subject ("do we…"), never an imperative
|
|
511
|
+
# ("do not touch the config").
|
|
512
|
+
_QUESTION_START = re.compile(
|
|
513
|
+
r"^\s*(?:why|how|what|where|which|who|when|does|is|are|can|could|should|"
|
|
514
|
+
r"would|will|do\s+(?:we|you|i|they|these|those|it)\b)\b",
|
|
515
|
+
re.IGNORECASE,
|
|
516
|
+
)
|
|
517
|
+
# A reply that opens with an acknowledgement and stays short ("yes, sounds
|
|
518
|
+
# good to me", "thanks for doing that") is still an acknowledgement.
|
|
519
|
+
_ACK_PREFIX = re.compile(
|
|
520
|
+
r"^\s*(?:yes|yep|yup|ok|okay|sure|thanks|thank you|ty|agreed?|i agree|"
|
|
521
|
+
r"sounds good|looks good|lgtm|great|perfect|nice|cool|go ahead|do it|"
|
|
522
|
+
r"makes sense|got it)\b",
|
|
523
|
+
re.IGNORECASE,
|
|
524
|
+
)
|
|
525
|
+
_ACK_PREFIX_MAX_WORDS = 8
|
|
526
|
+
_SHORT_REPLY_MAX_WORDS = 2 # "go", "still waiting", "repo_checks.rs?"
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _normalize(prompt: str) -> str:
|
|
530
|
+
return re.sub(r"[^\w\s'-]", "", prompt.strip().lower()).strip()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _has_strong_code_signal(prompt: str) -> bool:
|
|
534
|
+
return (
|
|
535
|
+
_has_file_path(prompt)
|
|
536
|
+
or _has_code_markers(prompt)
|
|
537
|
+
or bool(_IDENTIFIER_PATTERN.search(prompt))
|
|
538
|
+
or bool(_DECISION_PATTERN.search(prompt))
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _is_chatter(prompt: str) -> bool:
|
|
543
|
+
"""True for acknowledgements, status checks and one- or two-word replies,
|
|
544
|
+
whatever they name. No per-turn directive belongs on these."""
|
|
545
|
+
if not prompt or not prompt.strip():
|
|
546
|
+
return True
|
|
547
|
+
if _normalize(prompt) in _ACKNOWLEDGEMENTS:
|
|
548
|
+
return True
|
|
549
|
+
if _STATUS_PATTERN.search(prompt):
|
|
550
|
+
return True
|
|
551
|
+
words = len(prompt.split())
|
|
552
|
+
if words <= _SHORT_REPLY_MAX_WORDS:
|
|
553
|
+
return True
|
|
554
|
+
return (
|
|
555
|
+
words <= _ACK_PREFIX_MAX_WORDS
|
|
556
|
+
and bool(_ACK_PREFIX.match(prompt))
|
|
557
|
+
and not _has_strong_code_signal(prompt)
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _prompt_needs_grounding(prompt: str) -> bool:
|
|
562
|
+
"""garden#1333: the Context Assembly gate, decided per prompt.
|
|
563
|
+
|
|
564
|
+
True only when the prompt names code, files, symbols, repos or past
|
|
565
|
+
decisions AND either has more than _GROUNDING_MIN_WORDS words or asks a
|
|
566
|
+
question. Never on acknowledgements, status checks or one-word replies.
|
|
567
|
+
"""
|
|
568
|
+
if _is_chatter(prompt):
|
|
569
|
+
return False
|
|
570
|
+
strong = _has_strong_code_signal(prompt)
|
|
571
|
+
if not strong and not _CODE_NOUN_PATTERN.search(prompt):
|
|
572
|
+
return False
|
|
573
|
+
question = "?" in prompt or bool(_QUESTION_START.match(prompt))
|
|
574
|
+
return question or len(prompt.split()) > _GROUNDING_MIN_WORDS
|
|
575
|
+
|
|
576
|
+
|
|
447
577
|
# ---------------------------------------------------------------------------
|
|
448
578
|
# Setup gate
|
|
449
579
|
# ---------------------------------------------------------------------------
|
|
@@ -723,16 +853,15 @@ def _build_intent_directive(intent: str, turn_count: int, state=None) -> str:
|
|
|
723
853
|
grounding = grounding_directive_lines()
|
|
724
854
|
except Exception:
|
|
725
855
|
grounding = [
|
|
726
|
-
"1.
|
|
727
|
-
"(recall/answer
|
|
728
|
-
"
|
|
729
|
-
"MCP SearchEntity tool for symbol lookup.",
|
|
856
|
+
"1. Recall what is already known: the wicked-garden-mem skill "
|
|
857
|
+
"(`recall` / `answer`) for past decisions, the wicked-garden-search "
|
|
858
|
+
"skill for symbols and blast radius.",
|
|
730
859
|
"2. Open the cited sources with Read before answering; do not answer "
|
|
731
860
|
"from recall snippets alone.",
|
|
732
861
|
]
|
|
733
862
|
directive_lines = [
|
|
734
|
-
f"[Context Assembly — intent={intent}]
|
|
735
|
-
"
|
|
863
|
+
f"[Context Assembly — intent={intent}] This prompt names code or past "
|
|
864
|
+
"decisions; ground it before answering:",
|
|
736
865
|
*grounding,
|
|
737
866
|
]
|
|
738
867
|
if intent == "rigor":
|
|
@@ -942,9 +1071,12 @@ def main():
|
|
|
942
1071
|
# turns produce an empty directive; feature/research/rigor produce
|
|
943
1072
|
# the synthesis directive.
|
|
944
1073
|
_intent = getattr(state, "intent", None) if state else None
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1074
|
+
# garden#1333: the sticky session intent only labels the directive;
|
|
1075
|
+
# whether it fires is decided by THIS prompt (_prompt_needs_grounding).
|
|
1076
|
+
intent_directive = ""
|
|
1077
|
+
if _prompt_needs_grounding(prompt):
|
|
1078
|
+
label = _intent if _intent in ("feature", "research", "rigor") else "research"
|
|
1079
|
+
intent_directive = _build_intent_directive(label, turn_count, state)
|
|
948
1080
|
|
|
949
1081
|
# WIP recovery: surface native in-progress tasks (v5 ticket rail gone)
|
|
950
1082
|
wip_block = _build_wip_recovery_block(session_id, project)
|