@amsterdamdatalabs/enact-extensions 0.1.37 → 0.1.42

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.
Files changed (26) hide show
  1. package/dist/internal/agents.d.ts +1 -1
  2. package/dist/internal/agents.d.ts.map +1 -1
  3. package/extensions/enact-factory/agents/architect.toml +1 -1
  4. package/extensions/enact-factory/agents/code-reviewer.toml +1 -1
  5. package/extensions/enact-factory/agents/critic.toml +1 -1
  6. package/extensions/enact-factory/agents/executor.toml +1 -1
  7. package/extensions/enact-factory/agents/explore.toml +1 -1
  8. package/extensions/enact-factory/agents/planner.toml +2 -2
  9. package/extensions/enact-factory/agents/verifier.toml +1 -1
  10. package/extensions/enact-factory/skills/advisor/SKILL.md +2 -2
  11. package/extensions/enact-factory/skills/autonomous-runner/SKILL.md +16 -16
  12. package/extensions/enact-factory/skills/committee/SKILL.md +4 -4
  13. package/extensions/enact-factory/skills/deslop-handover/SKILL.md +97 -0
  14. package/extensions/enact-factory/skills/deslop-handover/deslop.py +181 -0
  15. package/extensions/enact-factory/skills/drive-loop/SKILL.md +11 -11
  16. package/extensions/enact-factory/skills/paseo-dispatch/SKILL.md +141 -0
  17. package/extensions/enact-loop/scripts/validate.mjs +29 -0
  18. package/extensions/enact-loop/skills/enact-loop/SKILL.md +11 -11
  19. package/extensions/enact-loop/skills/loop-budget/SKILL.md +40 -0
  20. package/extensions/enact-loop/skills/loop-intake/SKILL.md +47 -0
  21. package/package.json +1 -1
  22. package/scripts/enact-extensions.mjs +10 -5
  23. package/scripts/hooks/session-start-drift-check.mjs +4 -3
  24. package/scripts/lib/auto-update.mjs +16 -3
  25. package/scripts/lib/run-install.mjs +7 -0
  26. package/scripts/lib/run-update.mjs +26 -13
@@ -12,7 +12,7 @@ export interface AgentDef {
12
12
  name: string;
13
13
  description: string;
14
14
  body: string;
15
- /** Codex/enact canonical model id (e.g. `gpt-5.4`). */
15
+ /** Codex/enact canonical model id (e.g. `gpt-5.6-luna`). */
16
16
  model?: string;
17
17
  /** Claude model alias (codex `model` is a separate namespace). */
18
18
  claudeModel?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/internal/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAYD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAcxD;AAgBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAczF;AAgCD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAA;CAAE,CA0BlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAWpF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAmB/F"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/internal/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAYD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAcxD;AAgBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAczF;AAgCD,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAA;CAAE,CA0BlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAWpF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAmB/F"}
@@ -1,6 +1,6 @@
1
1
  name = "architect"
2
2
  description = "Architectural review and GO/NO-GO verdict with evidence pointers — independent grader"
3
- model = "gpt-5.5"
3
+ model = "gpt-5.6-sol"
4
4
  claude_model = "opus"
5
5
  cursor_model = "default"
6
6
  model_reasoning_effort = "high"
@@ -1,6 +1,6 @@
1
1
  name = "code-reviewer"
2
2
  description = "Binding code review pass — independent grader on a separate model"
3
- model = "gpt-5.3-codex-spark"
3
+ model = "gpt-5.6-sol"
4
4
  claude_model = "haiku"
5
5
  cursor_model = "default"
6
6
  model_reasoning_effort = "high"
@@ -1,6 +1,6 @@
1
1
  name = "critic"
2
2
  description = "Adversarial debate participant for high-risk decision review — independent grader"
3
- model = "gpt-5.3-codex-spark"
3
+ model = "gpt-5.6-sol"
4
4
  claude_model = "haiku"
5
5
  cursor_model = "default"
6
6
  model_reasoning_effort = "high"
@@ -1,6 +1,6 @@
1
1
  name = "executor"
2
2
  description = "Focused implementation worker for small, reviewable diffs with verification"
3
- model = "gpt-5.4"
3
+ model = "gpt-5.6-luna"
4
4
  claude_model = "sonnet"
5
5
  cursor_model = "composer-2.5"
6
6
  model_reasoning_effort = "medium"
@@ -1,6 +1,6 @@
1
1
  name = "explore"
2
2
  description = "Codebase sweep agent for absolute-path findings and quote-friendly evidence"
3
- model = "gpt-5.3-codex-spark"
3
+ model = "gpt-5.6-luna"
4
4
  claude_model = "haiku"
5
5
  cursor_model = "default"
6
6
  model_reasoning_effort = "medium"
@@ -1,9 +1,9 @@
1
1
  name = "planner"
2
2
  description = "Structured implementation planner that never writes production code"
3
- model = "gpt-5.5"
3
+ model = "gpt-5.6-terra"
4
4
  claude_model = "opus"
5
5
  cursor_model = "default"
6
- model_reasoning_effort = "medium"
6
+ model_reasoning_effort = "high"
7
7
  sandbox_mode = "danger-full-access"
8
8
  developer_instructions = """
9
9
  Agent definition: structured implementation planner that never writes production code.
@@ -1,6 +1,6 @@
1
1
  name = "verifier"
2
2
  description = "Evidence-first verifier for real-surface QA and completion claims — independent grader"
3
- model = "gpt-5.3-codex-spark"
3
+ model = "gpt-5.6-sol"
4
4
  claude_model = "haiku"
5
5
  cursor_model = "default"
6
6
  model_reasoning_effort = "high"
@@ -36,7 +36,7 @@ passed AND that cheap graders would likely have false-GO'd.
36
36
  ## Model selection
37
37
 
38
38
  Advisor runs on a **strong cross-vendor** model:
39
- - **gpt-5.5** — preferred for Frontend / UI a11y / visual judgment (distinct from sonnet executor)
39
+ - **gpt-5.6-sol** with high thinking — preferred for Frontend / UI a11y / visual judgment (distinct from sonnet executor)
40
40
  - **opus 4.8** — preferred when executor is codex / gpt lineage (cross-vendor from OpenAI)
41
41
 
42
42
  If the strong escalation model is unavailable, say so and STOP — do not substitute a cheap or
@@ -60,7 +60,7 @@ same-lineage model and call it independent.
60
60
  ```
61
61
  Agent({
62
62
  subagent_type: "critic" | "architect" | "verifier", // fit the question
63
- model: "gpt-5.5" | "claude-opus-4-8", // strong cross-vendor; ≠ executor model
63
+ model: "gpt-5.6-sol" | "claude-opus-4-8", // strong cross-vendor; ≠ executor model
64
64
  prompt: "<WorkItem context + the exact decision/artifact +
65
65
  acceptance criteria + 'Return GO / CONCERNS / NO-GO with
66
66
  rationale and the single most important risk.'>"
@@ -153,10 +153,10 @@ logic** and add a judgment grader. (Full tree:
153
153
 
154
154
  | Type | Executor (surface) | Grader — default cheap | Escalate to strong grader when |
155
155
  |---|---|---|---|
156
- | Frontend / UI | `claude` / sonnet — UX, visual, copy | **minimax M3** / **kimi K2.7** | `advisor` (gpt-5.5) for a11y / visual / UX judgment |
157
- | Backend / logic | `codex` / **gpt-5.4** — mechanical workhorse | **minimax M3** / **kimi K2.7** | `committee` (opus + gpt-5.5) if architecture unclear |
158
- | Data-heavy | `codex` / **gpt-5.4** high-effort + data-safety stages | **minimax M3** / **kimi K2.7** | `committee` for risky or irreversible migrations |
159
- | Infra / CI | `codex` / **gpt-5.4** + pipeline validation | **minimax M3** / **kimi K2.7** | `committee` before branch policy; `opus 4.8` for security/public-API changes |
156
+ | Frontend / UI | `claude` / sonnet — UX, visual, copy | **minimax M3** / **kimi K2.7** | `advisor` (GPT-5.6 Sol High) for a11y / visual / UX judgment |
157
+ | Backend / logic | `codex` / **GPT-5.6 Terra** — heavyweight implementation | **minimax M3** / **kimi K2.7** | `committee` (opus + GPT-5.6 Terra) if architecture unclear |
158
+ | Data-heavy | `codex` / **GPT-5.6 Terra** high-effort + data-safety stages | **minimax M3** / **kimi K2.7** | `committee` for risky or irreversible migrations |
159
+ | Infra / CI | `codex` / **GPT-5.6 Terra** + pipeline validation | **minimax M3** / **kimi K2.7** | `committee` before branch policy; `opus 4.8` for security/public-API changes |
160
160
  | Docs | `claude` / sonnet (writing) | **minimax M3** / **kimi K2.7** | rarely |
161
161
 
162
162
  The **executor ≠ grader** rule is a hard gate everywhere (see contract above).
@@ -169,16 +169,16 @@ each role:
169
169
 
170
170
  | Function tier | Models | Used for |
171
171
  |---|---|---|
172
- | Workers (execute) | **sonnet** (in-window) · **codex/gpt-5.4** | implementation, diffs, refactors, bugfixes |
173
- | Planning / architecture | **opus 4.8** · **gpt-5.5** (reasoning: xhigh) | plans, interface/architecture design, hard root-cause, committee |
174
- | Verify / review / grade (DEFAULT cheap) | **minimax M3** (opencode) · **kimi K2.7** | first-pass judgment on routine code-review / verify / docs (~80% of grading) |
172
+ | Workers (execute) | **sonnet** (in-window) · **codex/GPT-5.6 Luna** for focused fast execution · **GPT-5.6 Terra** for heavyweight work | implementation, diffs, refactors, bugfixes |
173
+ | Planning / architecture | **opus 4.8** · **GPT-5.6 Terra** (reasoning: high+) | plans, interface/architecture design, hard root-cause, committee |
174
+ | Verify / review / grade (DEFAULT cheap) | **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) · **kimi K2.7** | first-pass judgment on routine code-review / verify / docs (~80% of grading) |
175
175
  | Explorers (map, read-only) | **haiku** · **cursor composer 2.5** | codebase sweeps, naming maps, brownfield discovery — never grade |
176
176
 
177
- **Grading = cheap default + risk-based escalation.** minimax M3 / kimi K2.7 grade the routine
178
- ~80%. Escalate to a strong cross-vendor grader for:
179
- - Frontend / UI a11y or visual / UX judgment → **advisor** = **gpt-5.5** (or opus if executor was codex)
177
+ **Grading = cheap default + risk-based escalation.** MiniMax M3 (`minimax-coding-plan/MiniMax-M3`, always
178
+ `--thinking on`) / kimi K2.7 grade the routine ~80%. Escalate to a strong cross-vendor grader for:
179
+ - Frontend / UI a11y or visual / UX judgment → **advisor** = **GPT-5.6 Sol High** (or opus if executor was codex)
180
180
  - Security, public API / barrel / contract changes → **opus 4.8**
181
- - Architecture decisions / risky or irreversible migrations → **committee** (opus + gpt-5.5)
181
+ - Architecture decisions / risky or irreversible migrations → **committee** (opus + GPT-5.6 Terra)
182
182
 
183
183
  #### Contract shape per tier
184
184
 
@@ -201,11 +201,11 @@ When the executor is **`claude-sonnet-4-6`** (or any sonnet-tier model), the
201
201
  `cursor` grader (`cursor/claude-sonnet-4-6`) is **NOT model-distinct** — it runs
202
202
  the same underlying model and the independence rule is violated.
203
203
 
204
- **Default cheap graders for sonnet work:** use **minimax M3** (opencode) or **kimi K2.7** — both
205
- are vendor-distinct from sonnet and satisfy the independence rule cleanly. Use:
204
+ **Default cheap graders for sonnet work:** use **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always
205
+ `--thinking on`) or **kimi K2.7** — both are vendor-distinct from sonnet and satisfy the independence rule cleanly. Use:
206
206
 
207
- - **minimax M3** / **kimi K2.7** — default graders for sonnet-executor work (cheap, independent)
208
- - **opus 4.8** / **gpt-5.5** — escalation graders for high-risk judgment (see escalation table above)
207
+ - **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) / **kimi K2.7** — default graders for sonnet-executor work (cheap, independent)
208
+ - **opus 4.8** / **GPT-5.6 Sol High** — escalation graders for high-risk judgment (see escalation table above)
209
209
  - `cursor` grader ONLY when the executor is opus-tier (cursor runs claude-sonnet, so it is distinct from opus)
210
210
 
211
211
  #### Paseo permission-mode table
@@ -218,7 +218,7 @@ or return an empty session:
218
218
  | `codex` | `--mode auto-review` | Routes the grader's eligible `on-request` approvals through codex's auto-reviewer subagent instead of BLOCKING — without it a grader can stall on an approval (e.g. a cargo/network fetch during build verification). `--mode bypass` is rejected by codex |
219
219
  | `cursor` | default / agent | `--mode bypass` is a claude mode, NOT cursor's — previously documented wrong; cursor uses default or agent mode |
220
220
  | `kimi` | `--mode auto` | Same as codex |
221
- | `opencode` (minimax M3) | default | No special mode flag needed |
221
+ | `opencode` (MiniMax M3) | `--mode plan --thinking on` | Required for every MiniMax M3 dispatch |
222
222
 
223
223
  Codex mode enum (authoritative): `read-only` · `auto` · `auto-review` · `full-access`. The grader
224
224
  stays read-only BY PROMPT; `auto-review` only governs approval handling and composes with
@@ -34,14 +34,14 @@ two independent strong perspectives before proceeding.
34
34
 
35
35
  ## Model selection
36
36
 
37
- Committee always uses **opus 4.8 + gpt-5.5** — the two strongest available models from different
37
+ Committee always uses **opus 4.8 + gpt-5.6-terra** — the two strongest available models from different
38
38
  vendors. This pairing provides genuine diversity: one OpenAI-lineage, one Anthropic-lineage.
39
39
 
40
40
  ## Boundaries (hard)
41
41
 
42
42
  - **Diagnose + plan only.** No code changes, no commits, no board mutation. The
43
43
  output is a plan; implementation happens later via `drive-loop`.
44
- - **Two distinct vendors: opus 4.8 + gpt-5.5.** Both members run on models that are different
44
+ - **Two distinct vendors: opus 4.8 + gpt-5.6-terra.** Both members run on models that are different
45
45
  from each other and from the executor (genuine diversity). If two distinct models are
46
46
  unavailable, run what you can and STATE the reduced diversity loudly — do not
47
47
  pretend two same-model passes are a committee.
@@ -52,11 +52,11 @@ vendors. This pairing provides genuine diversity: one OpenAI-lineage, one Anthro
52
52
 
53
53
  1. Assemble the dossier: `factory_workitem_get` (goal, acceptance, current
54
54
  state, `blockedBy`), the loop status / stall reason, recent failing output.
55
- 2. Spawn TWO agents on **opus 4.8 + gpt-5.5**, with contrasting lenses:
55
+ 2. Spawn TWO agents on **opus 4.8 + gpt-5.6-terra**, with contrasting lenses:
56
56
 
57
57
  ```
58
58
  Agent({ subagent_type: "critic", model: "claude-opus-4-8", prompt: "<dossier> — root-cause why this is stuck; what is the real blocker?" })
59
- Agent({ subagent_type: "architect", model: "gpt-5.5", prompt: "<dossier> — is the current approach right? propose the smallest viable path to closure." })
59
+ Agent({ subagent_type: "architect", model: "gpt-5.6-terra", prompt: "<dossier> — is the current approach right? propose the smallest viable path to closure." })
60
60
  ```
61
61
 
62
62
  3. Reconcile their findings into ONE plan: the root cause, the chosen approach
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: deslop-handover
3
+ description: "Strip AI-authoring tells (emoji, marketing tone, over-bolding, edit scars) from handover docs for a no-AI customer, while preserving every number, reference, code block, and finding verbatim."
4
+ ---
5
+
6
+ # Deslop Handover
7
+
8
+ Make a documentation handover read as plain, human-authored engineering notes — for a
9
+ customer that enforces a **strict no-AI policy** — without losing any of the hard-won
10
+ technical substance.
11
+
12
+ This is sharper than `ai-slop-cleaner` (which targets TODO/placeholder code residue) because
13
+ it adds the two things that matter for prose handovers:
14
+
15
+ 1. **Preserve-rules** — numbers, percentages, file:line refs, code blocks, table data,
16
+ citations/DOIs stay byte-for-byte. The value is the substance; only the *voice* changes.
17
+ 2. **Sanctioned-vs-violation distinction** — the delivered product running on the customer's
18
+ OWN infrastructure (Azure OpenAI, LLM parser, embeddings, BM25, vector search, ontology,
19
+ taxonomy) is **architecture, not a violation** — KEEP it. Only AI-*authoring* tells go.
20
+
21
+ ## Use When
22
+
23
+ - Preparing any markdown/docs bundle for handover to a no-AI / compliance-sensitive customer.
24
+ - After an AI-assisted writing or redaction pass that left tone slop or edit scars.
25
+ - When docs must look human-authored but every finding/number must survive intact.
26
+
27
+ ## What counts as slop (REMOVE / NEUTRALIZE)
28
+
29
+ - **Emoji & decorative symbols** (✅ ✔ ✓ ❌ ⚠️ 🎯 📊 🚀 ✨ ⭐). Badge labels like `✅ Good` →
30
+ the plain word (`Good`); decorative ones → delete. (Arrows `→`, dashes `—`, math `≤ ≥ ≠`
31
+ are notation — keep.)
32
+ - **Marketing / filler**: comprehensive, seamless, robust (only when decorative — keep
33
+ "robust to X" technical usage), powerful, leverage, elegant, best practices, closes the
34
+ gap, dive in, it's worth noting, it's important to note, "Note that" openers, "Overall,",
35
+ "In summary", "In conclusion", game-changer, unlock(s), elevate, supercharge, effortless,
36
+ cutting-edge.
37
+ - **Rule-of-three flourishes** and hype cadence ("X, Y, and Z — all real") → plain statement.
38
+ - **Over-bolding** — bold used for sentence emphasis; keep bold only on genuine key terms/values.
39
+ - **Self-referential tells** — "generated by an AI", "LLM-authored", "I analyzed", "this
40
+ session", "as an AI"; template title prefixes like `Learning:` / `Fix:`.
41
+ - **Edit scars** from prior passes — blank-date holes (`as of )`, `On , `, `( run, …)`),
42
+ empty parens `( )`, leading-comma parens `(, …)`, space-before-colon `Foo :`, dangling
43
+ heading dashes `# Title —`, duplicated words.
44
+
45
+ ## What to PRESERVE (do not touch)
46
+
47
+ Every number, percentage, row count, threshold, column name, file path, file:line reference,
48
+ SQL/code block (verbatim), table data cell, citation, DOI, author, journal, year. Every
49
+ finding, root cause, decision, and deferred/open/superseded note. Section structure. The
50
+ sanctioned product-architecture terms listed above.
51
+
52
+ ## Workflow
53
+
54
+ **Step 1 — mechanical pass (the ~80%).** Run the bundled script in report mode first, then fix:
55
+
56
+ ```bash
57
+ python skills/deslop-handover/deslop.py --report <root-or-globs...> # see what's there
58
+ python skills/deslop-handover/deslop.py --fix <root-or-globs...> # apply mechanical fixes
59
+ ```
60
+
61
+ The script is **code-fence-aware** (never touches fenced ``` blocks, inline `code`, or digits)
62
+ and only does safe structural edits: emoji removal/badge-strip, edit-scar repair, trailing
63
+ whitespace. It does NOT auto-rewrite prose — it **reports** slop phrases, over-bold lines, and
64
+ blank-date holes for the judgment pass.
65
+
66
+ **Step 2 — judgment pass (the ~20%), fanned out.** Tone, over-bolding ("key term vs emphasis"),
67
+ rule-of-three, and ambiguous scars need a reader. Parallelize:
68
+
69
+ 1. Write a shared rubric file (this SKILL's "What counts as slop" + "PRESERVE" sections).
70
+ 2. Group the dirty files into batches of ~6.
71
+ 3. Dispatch one sonnet executor per batch: *"Read the rubric at <path>. Deslop these files in
72
+ place: <list>. Preserve all numbers/refs/code verbatim. Report per file."*
73
+ 4. Each executor reports per-file changes and confirms zero numbers/refs/code changed.
74
+
75
+ **Step 3 — verification gate.** Re-grep the whole bundle; the count must be zero:
76
+
77
+ ```bash
78
+ rg -lP '[\x{1F300}-\x{1FAFF}\x{2600}-\x{26FF}\x{2705}\x{274C}\x{2714}\x{2713}\x{2716}]' <root> # emoji
79
+ rg -ni -e seamless -e '\bleverage' -e '\bunlock' -e 'best practices' -e 'dive in' <root> # phrases
80
+ rg -n -e 'as of \)' -e '\( *\)' -e '\(, ' -e ' —$' <root> # scars
81
+ ```
82
+
83
+ A grep hit inside a fenced code block (e.g. `.collect()` matching empty-parens) is a
84
+ false positive — confirm by line before acting.
85
+
86
+ ## State Contract
87
+
88
+ - Reads: the target docs (and a temp rubric file for the judgment pass).
89
+ - Writes: the same docs, in place. No new files in the handover bundle, no frontmatter added.
90
+
91
+ ## Final Check
92
+
93
+ - Zero emoji, zero slop phrases, zero edit scars outside fenced code.
94
+ - Every number, file:line ref, code block, table cell, and citation is unchanged (diff to confirm).
95
+ - Product-architecture terms (Azure OpenAI / LLM / embeddings / BM25 / ontology) intact.
96
+ - Protected docs (e.g. a `STRATEGY.md`) excluded unless the user explicitly opts them in.
97
+ - Ambiguous scars (missing words, duplicated phrases, lost dates) surfaced to the user, not guessed.
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """deslop-handover — mechanical AI-tone cleanup for markdown handover docs.
3
+
4
+ Code-fence-aware. Does the safe, deterministic ~80% of a deslop pass:
5
+ - strips emoji / badge symbols (keeps arrows, dashes, math notation)
6
+ - repairs edit scars (empty parens, leading-comma parens, space-before-colon,
7
+ space-before-comma, dangling heading dashes, trailing whitespace)
8
+ - REPORTS (never auto-rewrites) slop phrases, over-bold lines, and blank-date holes
9
+ that need the judgment/agent pass.
10
+
11
+ NEVER touches: fenced ``` code blocks, inline `code` spans, digits, or table data values.
12
+
13
+ Usage:
14
+ python deslop.py --report PATH [PATH ...] # default: show findings, change nothing
15
+ python deslop.py --fix PATH [PATH ...] # apply the mechanical fixes in place
16
+
17
+ PATH may be a file, a directory (recursed for *.md), or a glob. Vendored dirs
18
+ (.venv/.git/node_modules/dist-info/.pytest_cache) are skipped.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import glob
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ _SKIP_DIRS = {".venv", ".git", "node_modules", ".pytest_cache", "__pycache__"}
29
+
30
+ # Emoji / decorative symbols. Deliberately EXCLUDES arrows (U+2190-21FF, →),
31
+ # dashes (—, –) and math (≤ ≥ ≠) which are technical notation.
32
+ EMOJI = re.compile(
33
+ "[\U0001F300-\U0001FAFF\U00002600-\U000027BF\U00002B00-\U00002BFF"
34
+ "✅❌✔✓✖✗✨️]"
35
+ )
36
+
37
+ SLOP_PHRASES = [
38
+ "comprehensive", "seamless", "powerful", "leverage", "elegant", "best practices",
39
+ "closes the gap", "dive in", "worth noting", "important to note", "game-changer",
40
+ "game changer", "unlock", "elevate", "supercharge", "effortless", "cutting-edge",
41
+ "in conclusion", "in summary",
42
+ ]
43
+ # matches that are technical, not slop — do not flag these
44
+ SLOP_ALLOW = re.compile(r"robust to|robustness|leverage point|highest leverage", re.I)
45
+
46
+ # Ambiguous scars that imply a lost word/date — REPORT, do not auto-fix.
47
+ HOLE = re.compile(r"as of \)|\( *run\b|On , |Effective ,|\bpre-\s*$")
48
+
49
+ _INLINE_CODE = re.compile(r"`[^`]*`")
50
+ _FENCE = re.compile(r"^\s*(```|~~~)")
51
+
52
+
53
+ def _split_protecting_code(line: str):
54
+ """Yield (text, is_code) chunks so transforms skip inline `code` spans."""
55
+ pos = 0
56
+ for m in _INLINE_CODE.finditer(line):
57
+ if m.start() > pos:
58
+ yield line[pos:m.start()], False
59
+ yield m.group(0), True
60
+ pos = m.end()
61
+ if pos < len(line):
62
+ yield line[pos:], False
63
+
64
+
65
+ def _fix_text(text: str, is_heading: bool) -> str:
66
+ """Apply mechanical fixes to a non-code text chunk."""
67
+ text = EMOJI.sub("", text)
68
+ text = text.replace("️", "")
69
+ text = re.sub(r"\(\s*\)", "", text) # empty parens
70
+ text = re.sub(r"\(,\s*", "(", text) # leading-comma paren
71
+ text = re.sub(r"\(\s+", "(", text) # leading space in paren
72
+ text = re.sub(r"\s+\)", ")", text) # space before close paren
73
+ text = re.sub(r"\s+,", ",", text) # space before comma
74
+ text = re.sub(r"\s+:\*\*", ":**", text) # space before bold-closing colon
75
+ text = re.sub(r"(\*\*[^*\n]+?) :", r"\1:", text) # "label :" inside bold
76
+ text = re.sub(r"\s+:\s*$", ":", text) # space before trailing label colon
77
+ if is_heading:
78
+ text = re.sub(r"\s*[—–-]\s*$", "", text) # dangling heading dash
79
+ text = re.sub(r"(?<=\S) +", " ", text) # collapse runs of inner spaces
80
+ return text
81
+
82
+
83
+ def process(path: Path, fix: bool):
84
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=False)
85
+ in_fence = False
86
+ out, report = [], {"emoji": 0, "scars": 0, "phrases": [], "bold": [], "holes": []}
87
+ for n, line in enumerate(lines, 1):
88
+ if _FENCE.match(line):
89
+ in_fence = not in_fence
90
+ out.append(line)
91
+ continue
92
+ if in_fence:
93
+ out.append(line)
94
+ continue
95
+
96
+ # ---- report-only signals (computed on the original line) ----
97
+ for p in SLOP_PHRASES:
98
+ if p in line.lower() and not SLOP_ALLOW.search(line):
99
+ report["phrases"].append((n, p))
100
+ if HOLE.search(line):
101
+ report["holes"].append((n, line.strip()[:80]))
102
+ for b in re.findall(r"\*\*([^*\n]+)\*\*", line):
103
+ if "." in b or len(b.split()) > 6: # bold wrapping a sentence/clause
104
+ report["bold"].append((n, b[:60]))
105
+ report["emoji"] += len(EMOJI.findall(line))
106
+
107
+ # ---- mechanical fix ----
108
+ lead = re.match(r"^\s*", line).group(0)
109
+ rest = line[len(lead):]
110
+ is_heading = rest.startswith("#") or rest.startswith(">")
111
+ rebuilt = "".join(
112
+ chunk if is_code else _fix_text(chunk, is_heading)
113
+ for chunk, is_code in _split_protecting_code(rest)
114
+ )
115
+ rebuilt = (lead + rebuilt).rstrip()
116
+ if rebuilt != line:
117
+ report["scars"] += 1
118
+ out.append(rebuilt)
119
+
120
+ changed = out != lines
121
+ if fix and changed:
122
+ path.write_text("\n".join(out) + "\n", encoding="utf-8")
123
+ return changed, report
124
+
125
+
126
+ def collect(paths):
127
+ files = []
128
+ for p in paths:
129
+ for hit in glob.glob(p, recursive=True) if any(c in p for c in "*?[") else [p]:
130
+ hp = Path(hit)
131
+ if hp.is_dir():
132
+ files += [f for f in hp.rglob("*.md")
133
+ if not any(part in _SKIP_DIRS for part in f.parts)]
134
+ elif hp.suffix == ".md":
135
+ files.append(hp)
136
+ return sorted(set(files))
137
+
138
+
139
+ def main():
140
+ ap = argparse.ArgumentParser(description="Mechanical deslop for handover markdown.")
141
+ ap.add_argument("--fix", action="store_true", help="apply fixes (default: report only)")
142
+ ap.add_argument("--report", action="store_true", help="report only (default; no-op flag for clarity)")
143
+ ap.add_argument("paths", nargs="+", help="files, dirs, or globs")
144
+ args = ap.parse_args()
145
+
146
+ files = collect(args.paths)
147
+ if not files:
148
+ print("no markdown files found", file=sys.stderr)
149
+ return 1
150
+
151
+ total_changed = 0
152
+ for f in files:
153
+ changed, r = process(f, args.fix)
154
+ signal = r["emoji"] or r["scars"] or r["phrases"] or r["bold"] or r["holes"]
155
+ if not signal:
156
+ continue
157
+ total_changed += int(bool(changed))
158
+ tag = "FIXED " if (args.fix and changed) else "FOUND "
159
+ print(f"\n{tag}{f}")
160
+ if r["emoji"]:
161
+ print(f" emoji/badges: {r['emoji']}")
162
+ if r["scars"] and not args.fix:
163
+ print(f" mechanical scars: {r['scars']} line(s) (run --fix)")
164
+ if r["phrases"]:
165
+ print(" slop phrases (judgment): " +
166
+ ", ".join(f"L{n}:{p}" for n, p in r["phrases"][:8]))
167
+ if r["bold"]:
168
+ print(f" over-bold lines (judgment): {len(r['bold'])} "
169
+ f"(e.g. L{r['bold'][0][0]})")
170
+ if r["holes"]:
171
+ print(" blank-date / lost-word HOLES (needs human): " +
172
+ "; ".join(f"L{n}" for n, _ in r["holes"]))
173
+
174
+ verb = "fixed" if args.fix else "with findings"
175
+ print(f"\n{total_changed if args.fix else len(files)} file(s) {verb}. "
176
+ f"Run the verification grep + judgment pass next (see SKILL.md).")
177
+ return 0
178
+
179
+
180
+ if __name__ == "__main__":
181
+ sys.exit(main())
@@ -161,7 +161,7 @@ For each judgment stage:
161
161
  > the factory MCP tool `factory_paseo_grader_run` runs the grader with
162
162
  > `paseo run --output-schema --json --wait-timeout` so **stdout is the validated `{verdict,
163
163
  > evidence}` object** — no `paseo logs | grep`. It bakes in the per-provider `--mode`
164
- > (codex `auto-review` so on-request approvals don't block, kimi `auto`, cursor/opencode default) and recovers the actual resolved model via
164
+ > (codex `auto-review` so on-request approvals don't block, kimi `auto`, cursor default, opencode MiniMax M3 `--mode plan --thinking on`) and recovers the actual resolved model via
165
165
  > `paseo ls --label` / `inspect`, which you relay as `graderModelActual` on `loop_grader_verdict`.
166
166
  > Full design: `enact-loop/docs/design/2026-06-22-paseo-verdict-capture.md`.
167
167
 
@@ -235,16 +235,16 @@ acceptance criteria in a real surface.
235
235
 
236
236
  #### Grader selection — cheap default + risk-based escalation
237
237
 
238
- **minimax M3** (opencode) and **kimi K2.7** are the default cheap graders for routine judgment
238
+ **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) and **kimi K2.7** are the default cheap graders for routine judgment
239
239
  (~80% of grading). Escalate to a strong cross-vendor grader when:
240
- - Frontend / UI a11y or visual / UX judgment → `advisor` = **gpt-5.5** (or opus 4.8 if executor was codex)
240
+ - Frontend / UI a11y or visual / UX judgment → `advisor` = **GPT-5.6 Sol High** (or opus 4.8 if executor was codex)
241
241
  - Security, public API / barrel / contract changes → **opus 4.8**
242
- - Architecture decisions / risky or irreversible migrations → `committee` (opus 4.8 + gpt-5.5)
242
+ - Architecture decisions / risky or irreversible migrations → `committee` (opus 4.8 + GPT-5.6 Terra)
243
243
 
244
- **Independence (hard gate — model-distinct):** grader model ≠ executor model. minimax M3 / kimi K2.7
245
- are vendor-distinct from both sonnet and codex workers, so default cheap graders are always
246
- independent. On escalation keep cross-vendor: grade codex work with **opus** (not gpt-5.5 — same
247
- OpenAI lineage); grade sonnet work with gpt-5.5 / opus / minimax / kimi.
244
+ **Independence (hard gate — model-distinct):** grader model ≠ executor model. MiniMax M3
245
+ (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) / kimi K2.7 are vendor-distinct from both sonnet and codex workers, so default cheap graders are always
246
+ independent. On escalation keep cross-vendor: grade codex work with **opus** (not GPT-5.6 Sol — same
247
+ OpenAI lineage); grade sonnet work with GPT-5.6 Sol High / opus / minimax / kimi.
248
248
 
249
249
  #### Permission modes (REQUIRED — non-default modes prevent stall/empty sessions)
250
250
 
@@ -253,7 +253,7 @@ OpenAI lineage); grade sonnet work with gpt-5.5 / opus / minimax / kimi.
253
253
  | `codex` | `--mode auto-review` | Routes the grader's eligible `on-request` approvals through codex's auto-reviewer subagent instead of BLOCKING — without it a grader can stall on an approval (e.g. a cargo/network fetch during build verification). `--mode bypass` is rejected by codex |
254
254
  | `cursor` | default / agent | `--mode bypass` is a claude mode, NOT cursor's — previously documented wrong |
255
255
  | `kimi` | `--mode auto` | Same as codex |
256
- | `opencode` (minimax M3) | default | No special mode flag needed |
256
+ | `opencode` (MiniMax M3) | `--mode plan --thinking on` | Required for every MiniMax M3 dispatch |
257
257
 
258
258
  Codex mode enum (authoritative): `read-only` · `auto` · `auto-review` · `full-access`. The grader
259
259
  stays read-only BY PROMPT; `auto-review` only governs approval handling and composes with
@@ -262,8 +262,8 @@ stays read-only BY PROMPT; `auto-review` only governs approval handling and comp
262
262
  #### Sonnet-executor caveat
263
263
 
264
264
  `cursor` (`cursor/claude-sonnet-4-6`) is NOT model-distinct from a sonnet executor.
265
- Use **minimax M3** or **kimi K2.7** as the clean default cheap graders for sonnet-executor work;
266
- escalate to opus 4.8 / gpt-5.5 for high-risk judgment. Reserve `cursor` for opus-tier executors.
265
+ Use **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) or **kimi K2.7** as the clean default cheap graders for sonnet-executor work;
266
+ escalate to opus 4.8 / GPT-5.6 Sol High for high-risk judgment. Reserve `cursor` for opus-tier executors.
267
267
 
268
268
  #### Verdict capture — structured `--output-schema`, not log-scraped
269
269
 
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: paseo-dispatch
3
+ description: >-
4
+ Canonical Enact Factory reference for launching Paseo agents, reviewers,
5
+ graders, advisors, committees, and handoffs with correct provider/model/mode
6
+ selection across Codex, Claude Code, Kimi, OpenCode/MiniMax, and Cursor. Use
7
+ when a skill invokes Paseo, chooses a grader/advisor/committee model, runs
8
+ factory_paseo_grader_run, needs auto-review/nonblocking review modes, or must
9
+ prove model-distinct review provenance.
10
+ metadata:
11
+ author: Amsterdam Data Labs
12
+ version: 1.0.0
13
+ ---
14
+
15
+ # paseo-dispatch
16
+
17
+ Paseo dispatch is fragile because provider names, model ids, and permission modes
18
+ are not interchangeable. Use this skill whenever factory work launches agents
19
+ through Paseo or records an independent review verdict.
20
+
21
+ ## Discovery First
22
+
23
+ Before choosing a provider or mode, inspect the live surface:
24
+
25
+ ```bash
26
+ paseo provider ls --json
27
+ paseo provider models <provider> --json
28
+ ```
29
+
30
+ If `~/.paseo/orchestration-preferences.json` exists, read it and treat it as the
31
+ user's preferred role map unless the task explicitly requires a different model.
32
+ Do not assume a model id from old docs when live `paseo provider models` disagrees.
33
+
34
+ Prefer explicit provider and model flags:
35
+
36
+ ```bash
37
+ paseo run --provider <provider> --model <model-id> --mode <mode> --cwd <repo> "<prompt>"
38
+ ```
39
+
40
+ This keeps provider, model, and permission mode separable. Record the actual
41
+ model from `paseo inspect <id> --json` when a verdict or committee depends on
42
+ independence.
43
+
44
+ ## Review Modes
45
+
46
+ Use the provider's nonblocking review mode for graders, advisors, and committees.
47
+ The prompt still makes the reviewer read-only; the mode only controls whether
48
+ tool approvals stall the agent.
49
+
50
+ | Provider | Review mode | Common model ids | Notes |
51
+ |---|---|---|---|
52
+ | `codex` | `--mode auto-review` | `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | Use Sol with `--thinking high` for reviewers/graders; use Terra for heavyweight work and Luna for lightweight fast execution. Never use `bypass`. |
53
+ | `claude` | `--mode auto` | `claude-opus-4-8`, `claude-sonnet-4-6`, `claude-fable-5` | Claude Code's nonblocking review path is Auto mode. Use `--mode plan` only for pure planning where no tool approvals are expected. |
54
+ | `kimi` | `--mode auto` | `kimi-code/kimi-for-coding` | Kimi K2.7 Code supports `--thinking on`; use it for review unless speed matters more. |
55
+ | `opencode` | `--mode plan` for review, `--mode build` for implementation | `minimax-coding-plan/MiniMax-M3`, `opencode-go/kimi-k2.7-code` | MiniMax M3 is a MiniMax model, not an OpenCode model. Every OpenCode/MiniMax M3 dispatch MUST pass `--thinking on`. |
56
+ | `cursor` | `--mode ask` or `--mode plan` for review, `--mode agent` for implementation | provider defaults vary | Do not pass Claude `bypass` to Cursor. Verify modes live before use. |
57
+
58
+ ## Standard Review Commands
59
+
60
+ Codex Sol High grader:
61
+
62
+ ```bash
63
+ paseo run --provider codex --model gpt-5.6-sol --mode auto-review \
64
+ --thinking high --cwd "$ROOT" --wait-timeout 10m \
65
+ --label role=reviewer "<review prompt>"
66
+ ```
67
+
68
+ Claude strong reviewer:
69
+
70
+ ```bash
71
+ paseo run --provider claude --model claude-opus-4-8 --mode auto \
72
+ --thinking high --cwd "$ROOT" --wait-timeout 10m \
73
+ --label role=reviewer "<review prompt>"
74
+ ```
75
+
76
+ Kimi K2.7 reviewer:
77
+
78
+ ```bash
79
+ paseo run --provider kimi --model kimi-code/kimi-for-coding --mode auto \
80
+ --thinking on --cwd "$ROOT" --wait-timeout 10m \
81
+ --label role=reviewer "<review prompt>"
82
+ ```
83
+
84
+ MiniMax M3 reviewer:
85
+
86
+ ```bash
87
+ paseo run --provider opencode --model minimax-coding-plan/MiniMax-M3 --mode plan \
88
+ --thinking on --cwd "$ROOT" --wait-timeout 10m \
89
+ --label role=reviewer "<review prompt>"
90
+ ```
91
+
92
+ ## Structured Verdict Capture
93
+
94
+ For loop judgment stages, prefer `factory_paseo_grader_run`; it wraps
95
+ `paseo run --output-schema --json --wait-timeout`, labels the run with the
96
+ continuation id, and recovers the actual model for `loop_grader_verdict`.
97
+
98
+ When direct CLI dispatch is required:
99
+
100
+ ```bash
101
+ paseo run --provider <provider> --model <model-id> --mode <review-mode> \
102
+ --output-schema <verdict-schema.json> --json \
103
+ --label continuation=<continuationId> --cwd "$ROOT" "<grader prompt>"
104
+ ```
105
+
106
+ Then recover provenance:
107
+
108
+ ```bash
109
+ paseo ls -a --label continuation=<continuationId> --json
110
+ paseo inspect <agent-id> --json
111
+ ```
112
+
113
+ Relay `graderModelActual` exactly as resolved. A claimed GO without the actual
114
+ model is weak evidence.
115
+
116
+ ## Model Choice
117
+
118
+ Use model-distinct reviewers. For risky decisions, prefer vendor-distinct too.
119
+
120
+ - Routine judgment: MiniMax M3 (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) or Kimi K2.7.
121
+ - Strong single advisor or grader: Claude Opus 4.8 when the executor is Codex;
122
+ GPT-5.6 Sol with `--thinking high` when the executor is Claude/Sonnet;
123
+ MiniMax/Kimi when cost or breadth matters.
124
+ - Heavyweight implementation and planning: GPT-5.6 Terra. Lightweight focused
125
+ execution and exploration: GPT-5.6 Luna.
126
+ - Committee: launch two contrasting models. Default strong pair is Claude Opus
127
+ 4.8 plus GPT-5.6 Terra. For a cost-sensitive or extra-breadth committee, use Kimi
128
+ K2.7 plus MiniMax M3, or add them as extra reviewers.
129
+ - Never grade with the exact executor model/session. If distinct reviewers are
130
+ unavailable, stop or state reduced diversity loudly.
131
+
132
+ ## Operating Rules
133
+
134
+ - Use `--label` for traceability: `role=...`, `workItem=...`, `continuation=...`.
135
+ - Use `--wait-timeout` for synchronous review requests; otherwise detach and let
136
+ Paseo notify on finish.
137
+ - Do not poll running agents. Use `paseo attach <id>` only when live output is
138
+ needed.
139
+ - Do not restart the Paseo daemon to unstick a run; it kills active agents.
140
+ - Do not confuse implementation modes with review modes. A read-only reviewer in
141
+ `auto-review` or `auto` is still read-only because of its prompt and role.
@@ -113,6 +113,35 @@ if (!existsSync(join(root, skillPath))) {
113
113
  }
114
114
  }
115
115
 
116
+ const intakeSkillPath = "skills/loop-intake/SKILL.md";
117
+ if (!existsSync(join(root, intakeSkillPath))) {
118
+ fail(`${intakeSkillPath} is missing`);
119
+ } else {
120
+ const intake = readFileSync(join(root, intakeSkillPath), "utf8");
121
+ if (!/^name:\s*loop-intake\s*$/m.test(intake)) {
122
+ fail(`${intakeSkillPath} frontmatter must declare name: loop-intake`);
123
+ }
124
+ if (!/approvalRequired/i.test(intake) || !/MUST NOT call `loop_start`/.test(intake)) {
125
+ fail(`${intakeSkillPath} must require approval and prohibit loop_start`);
126
+ }
127
+ if (!/loop_operations_snapshot/.test(intake)) {
128
+ fail(`${intakeSkillPath} must use the read-only operations projection`);
129
+ }
130
+ }
131
+
132
+ const budgetSkillPath = "skills/loop-budget/SKILL.md";
133
+ if (!existsSync(join(root, budgetSkillPath))) {
134
+ fail(`${budgetSkillPath} is missing`);
135
+ } else {
136
+ const budget = readFileSync(join(root, budgetSkillPath), "utf8");
137
+ if (!/^name:\s*loop-budget\s*$/m.test(budget)) {
138
+ fail(`${budgetSkillPath} frontmatter must declare name: loop-budget`);
139
+ }
140
+ if (!/loop_operations_snapshot/.test(budget) || !/unavailable/i.test(budget)) {
141
+ fail(`${budgetSkillPath} must use the operational projection and preserve unavailable host-token truth`);
142
+ }
143
+ }
144
+
116
145
  if (failures.length > 0) {
117
146
  for (const failure of failures) {
118
147
  console.error(`[enact-loop validate] ${failure}`);
@@ -38,7 +38,7 @@ Stage {
38
38
  name: string
39
39
  type: 'mechanical' | 'judgment'
40
40
  required: boolean
41
- requires?: string[] # stage ids that must pass before this stage runs
41
+ requires?: string[] # stage ids that must pass before this stage may run or dispatch
42
42
  command?: string # mechanical stages only
43
43
  grader?: GraderSpec # judgment stages only
44
44
  passCriteria?: string
@@ -111,13 +111,12 @@ idle → running → verifying → completed (terminal)
111
111
  1. Read current state: `loop_status`
112
112
  2. If no active loop, start one:
113
113
  ```
114
- loop_start goal="<goal>" preset=<preset>|contract=<stages JSON>
114
+ loop_start goal="<goal>" contract=<stages JSON>
115
115
  ```
116
- Built-in presets: `frontend-ui` (5 mechanical: lint/typecheck/test/build/visual,
117
- then **two** judgment stages `code-review` **and** `verify` — the shape that
118
- reliably catches both correctness and acceptance defects), `docs` (mechanical +
119
- one lighter `review`), plus `docs-workflow` / `custom-pipeline`. Use a preset to
120
- get the right judgment shape by default instead of hand-authoring stages.
116
+ If you want a named delivery shape such as `frontend-ui` or `docs-workflow`,
117
+ build an explicit contract from the factory-owned profile catalog first.
118
+ Legacy runtime preset compatibility may still exist for migration, but profile
119
+ ownership belongs to `enact-factory`, not `enact-loop`.
121
120
  3. Implement the work while in phase `running`.
122
121
  4. Advance to verification:
123
122
  ```
@@ -192,9 +191,9 @@ The independence rule is checked by model identity, not by vendor or surface nam
192
191
  runs the same underlying model. The engine will emit `grader-independence-missing` and
193
192
  block closure. Ensure the resolved `grader.model` differs from the executor's model string.
194
193
 
195
- **Default cheap graders:** **minimax M3** (opencode) and **kimi K2.7** are vendor-distinct from
194
+ **Default cheap graders:** **MiniMax M3** (`minimax-coding-plan/MiniMax-M3`, always `--thinking on`) and **kimi K2.7** are vendor-distinct from
196
195
  both sonnet and codex, making them always-independent defaults (~80% of grading). Escalate to
197
- **opus 4.8** or **gpt-5.5** for high-risk judgment (a11y / visual / security / architecture).
196
+ **opus 4.8** or **GPT-5.6 Sol High** for high-risk judgment (a11y / visual / security / architecture).
198
197
  `cursor` is only distinct when the executor is opus-tier (cursor runs claude-sonnet).
199
198
 
200
199
  ### fallbackModels rotation on spawn failure (529 / overload)
@@ -270,7 +269,8 @@ verdict fingerprints.
270
269
 
271
270
  Each verdict is stamped with a worktree fingerprint. When the workspace changes (e.g. after
272
271
  retarget or filesystem drift), stale `passed` verdicts are invalidated (`BLOCKER_FILESYSTEM_STATE_CHANGED`).
273
- `loop_retarget` preserves verdicts only when `id + type + worktreeFingerprint` match unchanged stages.
272
+ `loop_retarget` preserves verdicts only when `id + type + stage definition + worktreeFingerprint`
273
+ still match unchanged stages.
274
274
 
275
275
  ## Workspace root resolution
276
276
 
@@ -353,7 +353,7 @@ last message) to decide the next action after the loop completes.
353
353
 
354
354
  | Tool | Purpose |
355
355
  |------|---------|
356
- | `loop_start` | Start loop with goal + contract/preset; stamps executorIdentity |
356
+ | `loop_start` | Start loop with goal + explicit contract; stamps executorIdentity |
357
357
  | `loop_status` | Read current loop state |
358
358
  | `loop_advance` | Phase transition |
359
359
  | `loop_grade` | Run a mechanical stage command in loop root; args: `stageId`, optional `timeoutMs` |
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: loop-budget
3
+ description: This skill should be used when inspecting enact-loop budget status, execution limits, host token availability, 80 percent warnings, exhausted budgets, or why a loop will not run another command or grader.
4
+ ---
5
+
6
+ # Enact Loop Budget
7
+
8
+ Use this skill to inspect and explain an existing loop's execution budget. It is an operational-read path, not a loop driver.
9
+
10
+ ## Safety boundary
11
+
12
+ - Read state only through `loop_operations_snapshot` or `enact-loop operations --json`.
13
+ - NEVER edit `~/.enact/` files directly.
14
+ - MUST NOT call `loop_start`, `loop_grade`, `loop_grader_dispatch`, `loop_advance`, `loop_retarget`, `loop_complete`, `loop_block`, `loop_abort`, `loop_pause`, or `loop_resume`.
15
+ - Do not invent host-reported token use. If `hostReportedTokens.availability` is `unavailable`, report exactly that a trusted host usage adapter is not configured.
16
+
17
+ ## Read and report
18
+
19
+ 1. Call `loop_operations_snapshot` from the loop workspace. If MCP is unavailable, run `enact-loop operations --json` from that workspace.
20
+ 2. Report the loop id, phase, goal, ledger entry count, and latest event kind.
21
+ 3. For each budget counter, preserve its status exactly:
22
+ - `unbounded`: no configured cap.
23
+ - `normal`: below 80% of a configured cap.
24
+ - `warning`: at least 80% used but not exhausted.
25
+ - `exhausted`: the cap is reached; the next matching mechanical command or grader dispatch is refused.
26
+ 4. Separate engine-observed counters from host-owned data. Mechanical attempts and grader dispatches are durable runtime facts. Host tokens remain unavailable unless a future trusted adapter attests them.
27
+ 5. Recommend an explicit, human-approved next action; do not execute it in this skill.
28
+
29
+ ## Contract fields
30
+
31
+ An execution contract may declare:
32
+
33
+ ```yaml
34
+ executionBudget:
35
+ maxMechanicalAttempts: 3 # per mechanical stage; 0 is an explicit no-op
36
+ maxTotalMechanicalAttempts: 8 # total engine-run mechanical commands; 0 is an explicit no-op
37
+ maxTotalGraderDispatches: 2 # total host grader launches; 0 is an explicit no-op
38
+ ```
39
+
40
+ At 80% the projection warns. At 100% the runtime refuses the next matching action. A zero cap is a deliberate no-op, never an inferred lack of data.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: loop-intake
3
+ description: This skill should be used when triaging ambiguous work, an issue, CI failure, or PR into a proposed enact-loop contract and readiness report before any execution loop is approved.
4
+ ---
5
+
6
+ # Enact Loop Intake
7
+
8
+ Turn incoming work into a report-only readiness assessment and a proposed contract. This skill plans; it does not start, mutate, or drive a loop.
9
+
10
+ ## Hard boundary
11
+
12
+ - MUST NOT call `loop_start`, `loop_grade`, `loop_grader_dispatch`, `loop_advance`, `loop_retarget`, `loop_complete`, `loop_block`, `loop_abort`, `loop_pause`, or `loop_resume`.
13
+ - MUST NOT edit `~/.enact/`, create a state file, change a factory board, modify a worktree, or run a mutating command.
14
+ - MAY call `loop_operations_snapshot`, `loop_status`, `enact-loop operations --json`, and other read-only inspection commands.
15
+ - Every result ends with `approvalRequired: true`. A proposed contract is not approval to execute it.
16
+
17
+ ## Intake procedure
18
+
19
+ 1. Capture the request verbatim: desired outcome, affected workspace, constraints, deadline/risk, and available evidence (issue, CI log, PR, or reproduction).
20
+ 2. Inspect only the named workspace and any existing loop through `loop_operations_snapshot` or `loop_status`. If an active loop exists, report its id/phase and do not replace or retarget it.
21
+ 3. Identify unknowns that could change scope, acceptance, risk, or ownership. Ask concise questions instead of assuming those facts.
22
+ 4. Produce a proposed contract only when the goal and acceptance evidence are sufficient. Keep mechanical stages executable and evidence-based; use judgment stages only where an independent assessment is genuinely required.
23
+ 5. State dependencies, blockers, expected evidence, and the suggested budget. Set every budget cap deliberately; use `0` only for an explicit no-op.
24
+
25
+ ## Required report shape
26
+
27
+ ```yaml
28
+ intake:
29
+ outcome: ready | needs-clarification | blocked | active-loop-present
30
+ scope: <one sentence>
31
+ evidence: [<facts inspected>]
32
+ openQuestions: [<questions>]
33
+ risks: [<risks>]
34
+ proposedContract: <contract object or null>
35
+ approvalRequired: true
36
+ proposedNextAction: <human approval or clarification only>
37
+ ```
38
+
39
+ When `outcome` is not `ready`, set `proposedContract: null`. When it is ready, return the contract in the report only; do not write it to disk or invoke any loop lifecycle tool.
40
+
41
+ ## Contract checklist
42
+
43
+ - Unique stage ids and an acyclic `requires` graph.
44
+ - Required mechanical stages have a resolvable command and a concrete pass criterion.
45
+ - Judgment stages name a model distinct from the executor and specify the needed number of rounds.
46
+ - `executionBudget` includes only deliberate limits: per-stage mechanical attempts, total mechanical attempts, and total grader dispatches.
47
+ - Host token data is not part of the proposal unless a trusted host adapter is explicitly available; otherwise mark it unavailable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amsterdamdatalabs/enact-extensions",
3
- "version": "0.1.37",
3
+ "version": "0.1.42",
4
4
  "description": "Create and validate Enact multi-platform plugin manifests",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -5,7 +5,7 @@ import { listBundles } from "./lib/list-bundles.mjs";
5
5
  import { buildIndex } from "./lib/build-index.mjs";
6
6
  import { runValidate } from "./lib/run-validate.mjs";
7
7
  import { runSync } from "./lib/run-sync.mjs";
8
- import { runInstall } from "./lib/run-install.mjs";
8
+ import { INSTALL_DRY_RUN_UNSUPPORTED_MESSAGE, runInstall } from "./lib/run-install.mjs";
9
9
  import { runUpdate } from "./lib/run-update.mjs";
10
10
  import { runPrune } from "./lib/run-prune.mjs";
11
11
  import { runUninstall } from "./lib/run-uninstall.mjs";
@@ -66,6 +66,7 @@ Usage:
66
66
  enact-extensions install [path|name] --platform shared Install skills to host-neutral .agents/skills/
67
67
  enact-extensions install [path|name] --platform all Install to every plugin platform (codex, claude, cursor, enact)
68
68
  enact-extensions install [path|name] --platform claude,cursor Install to a subset of platforms
69
+ enact-extensions install [path|name] --dry-run Unsupported; exits non-zero without writing anything
69
70
  enact-extensions install [path|name] --global Install into the default agent home (default)
70
71
  enact-extensions install [path|name] --local Install into a project home under cwd (./.codex, ...)
71
72
  enact-extensions uninstall [path|name] Uninstall plugin from Codex-compatible homes (default: enact)
@@ -932,8 +933,8 @@ if (command === "serve") {
932
933
  //
933
934
  // Currently supported sub-commands:
934
935
  // enact-extensions hook session-start
935
- // Runs the advisory drift-check and prints a single line only when plugin
936
- // surfaces are outdated.
936
+ // Runs the advisory drift-check and prints valid hook JSON only when plugin
937
+ // surfaces are outdated or auto-updated.
937
938
  // enact-extensions hook user-prompt-submit
938
939
  // Reads a UserPromptSubmit hook payload from stdin, extracts the prompt,
939
940
  // builds the skill catalog from the bundled extensions dir, routes the
@@ -962,12 +963,12 @@ if (command === "hook") {
962
963
  (async () => {
963
964
  if (subCommand === "session-start") {
964
965
  try {
965
- const { handleSessionStartDrift } = await import("./lib/auto-update.mjs");
966
+ const { handleSessionStartDrift, sessionStartHookJson } = await import("./lib/auto-update.mjs");
966
967
  // Opt-in is the USER's: gate auto-apply on the env var they set.
967
968
  const autoApply = !!process.env.ENACT_EXTENSIONS_AUTO_UPDATE;
968
969
  const result = handleSessionStartDrift({ autoApply });
969
970
  if (result.line) {
970
- process.stdout.write(`${result.line}\n`);
971
+ process.stdout.write(`${sessionStartHookJson(result.line)}\n`);
971
972
  }
972
973
  } catch {
973
974
  // Fail-silent for session-start hooks.
@@ -1085,6 +1086,10 @@ try {
1085
1086
  }
1086
1087
 
1087
1088
  if (command === "install") {
1089
+ if (options.dryRun) {
1090
+ console.error(INSTALL_DRY_RUN_UNSUPPORTED_MESSAGE);
1091
+ process.exit(1);
1092
+ }
1088
1093
  // Single global ledger: every install (local or global scope) records to
1089
1094
  // <homedir>/.enact/extensions/ledger.jsonl so `log` always finds it,
1090
1095
  // regardless of which per-platform home the bundle was installed to. The
@@ -12,7 +12,8 @@
12
12
  * - FAIL-SILENT: wrap everything in try/catch; never throw; always exit 0.
13
13
  * - FAST: imports the helper directly (no subprocess) for speed.
14
14
  * - READ-ONLY by default: only mutates state when the user opts into auto-apply.
15
- * - NO OUTPUT on success (or error): only print when there are outdated entries.
15
+ * - NO OUTPUT on success (or error): only print valid hook JSON when there
16
+ * are outdated entries.
16
17
  * - DEPENDENCY-FREE: only node builtins + enact-extensions own modules.
17
18
  */
18
19
 
@@ -25,7 +26,7 @@ const REPO_ROOT = resolve(__dirname, "..", "..");
25
26
 
26
27
  try {
27
28
  // Dynamically import so any module-load failure is caught by the outer try/catch.
28
- const { handleSessionStartDrift } = await import(join(REPO_ROOT, "scripts", "lib", "auto-update.mjs"));
29
+ const { handleSessionStartDrift, sessionStartHookJson } = await import(join(REPO_ROOT, "scripts", "lib", "auto-update.mjs"));
29
30
 
30
31
  // Opt-in is the USER's: gate auto-apply on the env var they set.
31
32
  const autoApply = !!process.env.ENACT_EXTENSIONS_AUTO_UPDATE;
@@ -39,7 +40,7 @@ try {
39
40
  }
40
41
 
41
42
  if (result.line) {
42
- process.stdout.write(`${result.line}\n`);
43
+ process.stdout.write(`${sessionStartHookJson(result.line)}\n`);
43
44
  }
44
45
  } catch {
45
46
  // Any top-level error (import failure, parse error, etc.) — silent exit 0.
@@ -3,13 +3,14 @@
3
3
  * auto-apply when opted in via ENACT_EXTENSIONS_AUTO_UPDATE.
4
4
  *
5
5
  * Single export:
6
- * handleSessionStartDrift({ autoApply, home, cwd }) { outdated, applied, updated, line }
6
+ * handleSessionStartDrift({ autoApply, home, cwd }) -> { outdated, applied, updated, line }
7
+ * sessionStartHookResponse(line) -> valid hook JSON response object
7
8
  *
8
9
  * Behaviour:
9
10
  * - Compute the outdated (drifted) surfaces from the ledger vs canonical source.
10
11
  * - No outdated surfaces → { outdated: [], applied: false, line: null }.
11
12
  * - Outdated + autoApply=false → advisory line (default; mutates nothing).
12
- * - Outdated + autoApply=true → runUpdate({ all: true }); summary line naming
13
+ * - Outdated + autoApply=true → runUpdate({ all: true, silent: true }); summary line naming
13
14
  * what was refreshed. If the refresh updated
14
15
  * nothing actionable (e.g. all orphaned), fall
15
16
  * back to the advisory line.
@@ -43,6 +44,18 @@ export function autoUpdateSummaryLine(updated) {
43
44
  );
44
45
  }
45
46
 
47
+ /** Convert a drift advisory/update line into valid SessionStart hook JSON. */
48
+ export function sessionStartHookResponse(line) {
49
+ const response = { continue: true };
50
+ if (line) response.systemMessage = line;
51
+ return response;
52
+ }
53
+
54
+ /** JSON-string form for command hooks that write their response to stdout. */
55
+ export function sessionStartHookJson(line) {
56
+ return JSON.stringify(sessionStartHookResponse(line));
57
+ }
58
+
46
59
  /**
47
60
  * Resolve SessionStart drift: advise by default, or auto-apply when opted in.
48
61
  *
@@ -62,7 +75,7 @@ export function handleSessionStartDrift({ autoApply, home, cwd } = {}) {
62
75
  }
63
76
 
64
77
  if (autoApply) {
65
- const summary = runUpdate({ all: true, home, cwd });
78
+ const summary = runUpdate({ all: true, silent: true, home, cwd });
66
79
  const updated = summary?.updated ?? [];
67
80
  if (updated.length === 0) {
68
81
  // Nothing actionable was refreshed (e.g. orphaned source) — advise instead.
@@ -17,6 +17,9 @@ import { provisionMcp, summarizeProvision } from "./provision-mcp.mjs";
17
17
  import { registerPluginHooks, removeHook, removePluginHooks } from "./hooks.mjs";
18
18
  import { assertSupportedHookEvents } from "../../dist/index.js";
19
19
 
20
+ export const INSTALL_DRY_RUN_UNSUPPORTED_MESSAGE =
21
+ "install --dry-run is not supported. No changes were made.";
22
+
20
23
  // scope: "global" (default) -> the agent's default home (~/.codex, ~/.enact/agent, ~/.claude, ~/.cursor, ~/.agents)
21
24
  // "local" -> a project-scoped home under the current dir (./.codex, ./.enact/agent, ...)
22
25
  // An explicit --<platform>-home always wins over the scope default.
@@ -439,6 +442,10 @@ function runProvision(pluginRoot, options) {
439
442
  }
440
443
 
441
444
  export function runInstall(pluginRoot, options = {}) {
445
+ if (options.dryRun) {
446
+ throw new Error(INSTALL_DRY_RUN_UNSUPPORTED_MESSAGE);
447
+ }
448
+
442
449
  // Refuse to install a bundle that declares hook events outside the
443
450
  // cross-surface intersection (claude, codex, cursor, enact).
444
451
  assertSupportedHookEvents(pluginRoot);
@@ -2,7 +2,7 @@
2
2
  * run-update.mjs — refresh installed plugins that have drifted from their
3
3
  * canonical source bundle.
4
4
  *
5
- * runUpdate({ name?, all?, dryRun?, home?, cwd? }) → summary
5
+ * runUpdate({ name?, all?, dryRun?, home?, cwd?, silent? }) → summary
6
6
  *
7
7
  * The update is driven ENTIRELY by the ledger, per (plugin, surface, home):
8
8
  *
@@ -80,11 +80,12 @@ function safeResolveBundlePath(name, cwd) {
80
80
  * @param {string} [opts.name] — restrict to a single plugin name.
81
81
  * @param {boolean} [opts.all] — update every outdated surface.
82
82
  * @param {boolean} [opts.dryRun]— report only; make no changes.
83
+ * @param {boolean} [opts.silent]— suppress install stdout during refresh.
83
84
  * @param {string} [opts.home] — ledger home override (defaults to os.homedir()).
84
85
  * @param {string} [opts.cwd] — working dir for canonical bundle resolution.
85
86
  * @returns {{ planned: object[], updated: object[], skipped: object[], failed: {entry: object, error: Error}[], dryRun: boolean }}
86
87
  */
87
- export function runUpdate({ name, all, dryRun, home, cwd } = {}) {
88
+ export function runUpdate({ name, all, dryRun, silent, home, cwd } = {}) {
88
89
  const resolvedHome = home ?? homedir();
89
90
  const resolvedCwd = cwd ?? processCwd();
90
91
 
@@ -131,17 +132,29 @@ export function runUpdate({ name, all, dryRun, home, cwd } = {}) {
131
132
 
132
133
  const homeOption = PLATFORM_HOME_OPTION[entry.platform] ?? "codexHome";
133
134
  try {
134
- runInstall(canonicalPath, {
135
- platform: entry.platform,
136
- scope: entry.scope,
137
- [homeOption]: entry.home,
138
- ledgerHome: resolvedHome,
139
- // Refresh the already-installed surface only; do not regenerate source
140
- // manifests, and skip MCP provisioning (a refresh re-installs identical
141
- // package deps provisioning is best-effort and not needed here).
142
- sync: false,
143
- noProvision: true,
144
- });
135
+ const install = () =>
136
+ runInstall(canonicalPath, {
137
+ platform: entry.platform,
138
+ scope: entry.scope,
139
+ [homeOption]: entry.home,
140
+ ledgerHome: resolvedHome,
141
+ // Refresh the already-installed surface only; do not regenerate source
142
+ // manifests, and skip MCP provisioning (a refresh re-installs identical
143
+ // package deps — provisioning is best-effort and not needed here).
144
+ sync: false,
145
+ noProvision: true,
146
+ });
147
+ if (silent) {
148
+ const originalLog = console.log;
149
+ try {
150
+ console.log = () => {};
151
+ install();
152
+ } finally {
153
+ console.log = originalLog;
154
+ }
155
+ } else {
156
+ install();
157
+ }
145
158
  updated.push(entry);
146
159
  } catch (err) {
147
160
  failed.push({ entry, error: err instanceof Error ? err : new Error(String(err)) });