@underactive/pi-topping-moa-fusion 0.1.0 → 0.3.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +54 -12
  3. package/agents/moa-synthesizer.md +9 -6
  4. package/package.json +5 -5
  5. package/src/config/planName.ts +29 -24
  6. package/src/config/settings.ts +38 -1
  7. package/src/debate/debateFanout.ts +16 -6
  8. package/src/debate/runDebate.ts +13 -4
  9. package/src/index.ts +14 -3
  10. package/src/moa/conflictContract.ts +2 -1
  11. package/src/moa/contextContract.ts +1 -1
  12. package/src/moa/fanout.ts +115 -4
  13. package/src/moa/fanoutWiring.ts +13 -3
  14. package/src/moa/implementationRetry.ts +2 -2
  15. package/src/moa/orchestration.ts +3 -0
  16. package/src/moa/panelDependence.ts +87 -0
  17. package/src/moa/reviewLoop.ts +2 -2
  18. package/src/moa/synthesis.ts +12 -8
  19. package/src/moa/verification.ts +117 -20
  20. package/src/moa/verificationCriteria.ts +1 -1
  21. package/src/opinion/opinionFanout.ts +5 -4
  22. package/src/opinion/runOpinion.ts +14 -13
  23. package/src/planning/contextEdits.ts +62 -0
  24. package/src/planning/modeState.ts +17 -0
  25. package/src/planning/planFile.ts +30 -12
  26. package/src/planning/planMode.ts +144 -11
  27. package/src/planning/tools/exitPlanMode.ts +7 -2
  28. package/src/planning/tools/mfPlanSubagent.ts +3 -0
  29. package/src/planning/tools/shared.ts +1 -0
  30. package/src/runtime/activityTracking.ts +28 -12
  31. package/src/runtime/processPool.ts +2 -0
  32. package/src/runtime/results.ts +18 -0
  33. package/src/runtime/runner.ts +45 -9
  34. package/src/runtime/wire.ts +11 -0
  35. package/src/spec/runSpec.ts +343 -0
  36. package/src/spec/specContract.ts +324 -0
  37. package/src/ui/cancelOverlay.ts +1 -1
  38. package/src/ui/moaModelPicker.ts +32 -4
  39. package/src/ui/moaProgressWidget.ts +114 -12
  40. package/src/ui/moaSetupOverlay.ts +51 -7
  41. package/src/ui/planReviewOverlay.ts +16 -8
  42. package/src/ui/rosterEditor.ts +18 -12
  43. package/src/ui/workingOverlay.ts +265 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.3.0]
6
+
7
+ ### Added
8
+
9
+ - Added `/mf-spec`, an in-process requirements-clarification flow that asks up to five focused questions, reviews and saves approved `__spec.md` planning briefs, and prepares a `/mf-plan` handoff without exposing specs to `/mf-plan-implement`.
10
+ - A hidden `mf-plan-verification-pending` session marker now records when an approved implementation is expected to enter independent verification. Resuming before a verifier result is recorded warns that the implementation is still unverified.
11
+
12
+ ### Changed
13
+
14
+ - Updated all four pi development dependencies from 0.86.0 to 0.87.0.
15
+ - The pi 0.87 settle-boundary context-edit and verification-marker integrations are capability-gated and remain inert on older hosts, which retain the existing request-time filtering and verification dispatch behavior.
16
+
17
+ ### Fixed
18
+
19
+ - An assigned slot in the MoA Fusion Pre-flight overview can now be cleared back to `(none)` with Backspace or Delete. Previously a slot could only be reassigned to another model, never emptied.
20
+ - Reusable `[PLAN MODE ACTIVE]` and `[PLAN MODE RE-ENTRY]` instruction entries are now durably omitted with settle-boundary null context edits after plan-mode exit, preventing later compaction from carrying stale plan instructions into implementation.
21
+ - Custom OpenAI-compatible endpoints used by proposers and verifiers now benefit from pi 0.87's upstream strict-schema capability fix: unknown endpoints no longer receive strict tool schemas unless they explicitly advertise support.
22
+ - Saving or deleting an agent roster now takes effect immediately, without requiring a second save from the main settings overlay.
23
+ - A failed agent's error is now readable in full without widening the terminal. Provider failures were cut to 80 characters before reaching the progress table and then truncated again at the ACTIVITY column edge, hiding the part naming the cause. The message is now kept whole and word-wrapped beneath its row — breaking the JSON payload mid-token when it has no spaces to break on — while a short error stays inline as before.
24
+
25
+ ## [0.2.0]
26
+
27
+ ### Added
28
+
29
+ - A `Naming the plan`/`Naming the opinion`/`Naming the debate` working overlay while the cheap/fast model summarizes a submitted prompt into its slug, so the TUI no longer sits unresponsive during that call. Press **Esc** to skip naming and fall back to a name derived from the prompt.
30
+ - Panel dependence note in the synthesizer input: a blinded summary of proposal count, distinct models, and same-model clusters so agreement is weighted by dependence, not vote count alone.
31
+ - Pre-flight and replacement-time diversity warning when proposer slots share a model or provider, reminding that agreement between them is not independent evidence.
32
+ - Evidence-backed vs asserted tags in synthesizer `### Proposer alignment`, plus distinct-model support counts on conflict recommendation Details lines.
33
+ - Configurable `maxConcurrentAgents` setting (1–8, default 1) for MoA, opinion, debate, and plan subagent fan-outs.
34
+ - Configurable `maxVerificationRepairs` setting (0–5, default 2) for verifier-driven implementation repairs. When repairs are exhausted or disabled, a `.pi/mf-plan/<slug>__verification-handoff.md` file is written with every criterion's status, the full frozen checklist, gaps, and guidance to continue verification manually — and its path is named in the exhaustion notification.
35
+ - Cancelling a single proposer from the MoA cancel overlay now prompts, once the fan-out settles, to replace that slot with a different model or continue without it, instead of silently dropping the proposal and proceeding with the survivors. **Cancel ALL** (including during a replacement run) still aborts the run.
36
+
37
+ ### Fixed
38
+
39
+ - Resuming, forking, or `--continue`-ing a session saved mid-plan no longer leaves the implementer with a read-only tool loadout after approval. The pre-plan-mode loadout is now persisted with the plan state.
40
+ - Subagent runs no longer log two spurious `event parse error: invalid session event` lines per run. pi 0.86.0's `system` wire messages are accepted instead of leading every failure report.
41
+ - Typechecking against pi 0.84.3 and newer declarations.
42
+ - Remove trailing ellipses from status labels.
43
+ - Correct progress timing and running counts for agents waiting in the concurrency queue.
44
+ - Show concurrency-limited fan-out agents as queued until they acquire a process-pool slot.
45
+ - Use success colors for positive verification notices instead of warning colors.
46
+
47
+ ### Changed
48
+
49
+ - Child-agent concurrency default drops from the previous hard-coded limit of 5 to **1** via the new `maxConcurrentAgents` setting.
50
+ - Plan naming now uses `ctx.modelRegistry.streamSimple()` instead of the temporary `pi-ai/compat` entry, so proxy and OAuth base-URL overrides are honored.
51
+ - The subagent `CTX` column can now advance mid-turn from `message_update` usage, so one-long-turn provider bridges that report streaming usage are no longer forced to sit at `0.0%` until the run ends. A malformed usage reading is ignored without discarding the subagent's output.
52
+
53
+ ### Removed
54
+
55
+ - The unfiled `docs/upstream-issue-live-usage.md` draft for a change that has shipped upstream.
56
+
3
57
  ## [0.1.0]
4
58
 
5
59
  - Initial commit
package/README.md CHANGED
@@ -16,6 +16,7 @@ A Pi extension built around **Mixture of Agents (MoA)** planning: several models
16
16
  - [The MoA Fusion table](#the-moa-fusion-table)
17
17
  - [Opinions (`/mf-opinion`)](#opinions-mf-opinion)
18
18
  - [Debates (`/mf-debate`)](#debates-mf-debate)
19
+ - [Specs (`/mf-spec`)](#specs-mf-spec)
19
20
  - [Commands](#commands)
20
21
  - [Cancelling running subagents](#cancelling-running-subagents)
21
22
  - [Plan Mode](#plan-mode)
@@ -74,13 +75,13 @@ Fanning out a coding task and merging the resulting token streams does not work
74
75
  ### How a run works
75
76
 
76
77
  1. After you submit your plan prompt, a picker opens on a mode screen (**Mixture of Agents** or **Single model**). Single model — or Esc — runs the 5-phase workflow on the current model.
77
- 2. MoA opens the **MoA Fusion Pre-flight** overview with all eight slots — five proposer slots plus a synthesizer, an implementer, and a verifier — starting empty at `(none)`, plus a **Load Roster** row above **Start fan-out** that applies a saved agent roster to every slot at once (see [Agent rosters](#agent-rosters)). Selecting any slot row opens a two-pane model/thinking picker for that one slot and returns to the overview; cancelling leaves the slot unchanged. **Start fan-out** stays disabled until you assign at least two proposer slots and all three required roles (the minimum roster; the same model may fill several slots, and the extra proposer slots are optional). Thinking levels come from the model's pi registry metadata, which is authoritative for every model the registry lists; the generic `off`/`low`/`medium`/`high` list applies only to models it doesn't. Your saved choices and active model only pre-highlight a slot's picker — they never count as assignments until you confirm them — and confirmed selections are remembered in `~/.pi/agent/mf-plan/settings.json`.
78
+ 2. MoA opens the **MoA Fusion Pre-flight** overview with all eight slots — five proposer slots plus a synthesizer, an implementer, and a verifier — starting empty at `(none)`, plus a **Load Roster** row above **Start fan-out** that applies a saved agent roster to every slot at once (see [Agent rosters](#agent-rosters)). Selecting any slot row opens a two-pane model/thinking picker for that one slot and returns to the overview; cancelling leaves the slot unchanged, and Backspace (or Delete) on a slot row clears it back to `(none)`. **Start fan-out** stays disabled until you assign at least two proposer slots and all three required roles (the minimum roster; the same model may fill several slots, and the extra proposer slots are optional). Thinking levels come from the model's pi registry metadata, which is authoritative for every model the registry lists; the generic `off`/`low`/`medium`/`high` list applies only to models it doesn't. Your saved choices and active model only pre-highlight a slot's picker — they never count as assignments until you confirm them — and confirmed selections are remembered in `~/.pi/agent/mf-plan/settings.json`.
78
79
  3. Proposers run in parallel as isolated `moa-proposer` subprocesses. Each ends with an "Open Questions / Assumptions" section rather than pausing — proposers never block waiting on input.
79
80
  4. `moa-synthesizer` reads every successful proposal (at least 1 must succeed) plus the original request, and writes one merged plan.
80
81
  5. If the proposers substantively disagreed, a conflict-review overlay opens first: one tab per conflict, plain-language options, the synthesized pick tagged **(Recommended)**, and a **Chat about this** box for free-text feedback. Your resolutions are fed back for re-synthesis. Esc cancels the whole run.
81
82
  6. The plan opens in the same review overlay `exit_plan_mode` uses, where you can inspect any proposer's original plan, edit the synthesized one, or send the synthesizer feedback for a revised version (up to five rounds).
82
83
  7. On approval, the synthesizer makes one read-only pass to create a frozen observable checklist at `.pi/mf-plan/<slug>__criteria.md`, then plan mode exits and implementation begins in the current session using the implementer you already picked — there is **no** second picker (the single-model flow picks its implementing model up front when you choose **Single model**, and neither flow shows a model picker after approval — one appears only via `/mf-plan-implement`, or when an MoA run is resumed without a saved roster). If implementation fails (for example, auth expiration or provider error), an automatic recovery dialog prompts to retry, switch to a different model, or continue manually. You can also run `/mf-plan-implement` anytime to resume the approved plan.
83
- 8. Once the implementation turn settles, the **verification phase** runs when a verifier is configured: the project's own `check` / `lint` / `test` scripts run first, then a read-only `moa-verifier` subprocess judges the working tree against the frozen approved plan and scores every frozen criterion pass/fail/cannot-verify. The overall verdict is derived from those scores. If criteria are unavailable, it falls back to judging plan steps directly. If it finds gaps (or a check fails), the repair prompt shows a short Simplified Technical English summary, then the verifier summary, unmet criteria or gaps, and failing project checks, followed in the TUI by three choices in order: *View full findings*, *Send verifier findings to the implementer* (a bounded **repair round**, up to two), and *Accept implementation as-is*. *View full findings* opens the complete verifier report in a read-only, scrollable popup and, when closed, returns to the same three choices, so viewing never consumes a repair round. A repair round sends the findings back to the implementer and re-verifies. The raw verdict is saved next to the plan as `<slug>__verification.md`. A user-cancelled implementation is never verified.
84
+ 8. Once the implementation turn settles, the **verification phase** runs when a verifier is configured: the project's own `check` / `lint` / `test` scripts run first, then a read-only `moa-verifier` subprocess judges the working tree against the frozen approved plan and scores every frozen criterion pass/fail/cannot-verify. The overall verdict is derived from those scores. If criteria are unavailable, it falls back to judging plan steps directly. If it finds gaps (or a check fails), the repair prompt shows a short Simplified Technical English summary, then the verifier summary, unmet criteria or gaps, and failing project checks, followed in the TUI by three choices in order: *View full findings*, *Send verifier findings to the implementer* (a bounded **repair round**, up to the [max verification repairs](#max-verification-repairs) setting — two by default), and *Accept implementation as-is*. *View full findings* opens the complete verifier report in a read-only, scrollable popup and, when closed, returns to the same three choices, so viewing never consumes a repair round. A repair round sends the findings back to the implementer and re-verifies. The raw verdict is saved next to the plan as `<slug>__verification.md`. Once the repair budget is spent, a handoff file is written as `<slug>__verification-handoff.md` and named in the exhaustion notification. A user-cancelled implementation is never verified.
84
85
 
85
86
  Before building the full verification task, MoA Fusion runs a cheap verifier preflight that asks for a one-word response. Auth, quota, model, and liveness problems therefore surface quickly with an actionable child error instead of after the full audit attempt.
86
87
 
@@ -94,8 +95,8 @@ Several safeguards target known LLM judging biases:
94
95
 
95
96
  - **Identity blinding** — proposals arrive headed `### Proposal from Proposer N`; real model names never appear in its input. This counters *self-preference bias*, where a model favors output it recognizes as its own. Enforcement runs both ways: proposers must not hint at their own model name, family, or provider, and the synthesizer must not infer or emit one — including in its own output, which keeps re-synthesis rounds blind too.
96
97
  - **Separate evaluation dimensions** — proposals are judged on correctness, completeness, feasibility, risk, and simplicity as distinct dimensions rather than one overall impression. The prompt states outright that confidence, detail, and length are not evidence of quality, guarding against *verbosity bias*.
97
- - **Agreement is signal, not proof** — where proposers agree, the synthesizer still sanity-checks the shared assumption against the repo, since independent models can share the same error.
98
- - **Auditable reasoning** — the plan opens with a Context section recording dimension reasoning, proposer alignment, and synthesis decisions, so recombination choices are inspectable. Reconciliation commentary is confined there; the rest reads as one coherent plan. The orchestrator verifies all three subsections are present and rejects a first plan that omits them (one corrective retry, then a warning), the same way it enforces `## Proposer Verdicts`.
98
+ - **Agreement is signal, not proof** — where proposers agree, the synthesizer still sanity-checks the shared assumption against the repo, since independent models can share the same error. A blinded `Panel dependence:` note in the synthesizer input names how many distinct models contributed and which slots share one model, and the pre-flight picker warns when assigned slots repeat a model or provider.
99
+ - **Auditable reasoning** — the plan opens with a Context section recording dimension reasoning, proposer alignment, and synthesis decisions, so recombination choices are inspectable. `### Proposer alignment` labels support as evidence-backed or asserted, and conflict recommendation Details lines include distinct-model support counts. Reconciliation commentary is confined there; the rest reads as one coherent plan. The orchestrator verifies all three subsections are present and rejects a first plan that omits them (one corrective retry, then a warning), the same way it enforces `## Proposer Verdicts`.
99
100
  - **Disagreements go to the user** — every substantive disagreement must be emitted as a `## Conflicts` block, even when the synthesizer is confident, so its judgment is a default you can override rather than a decision made for you. Genuine ambiguity about *your intent* instead produces a single `## Open Question`, and the synthesizer is re-run with your answer.
100
101
 
101
102
  **Protocol-internal, always current at runtime.** The proposer, synthesizer, and verifier prompts are tightly coupled to this extension's parsers and UI (e.g. the `## Conflicts` markup, and the verifier's `**Verdict:**`/`### Gaps` markup). A hand-edited copy under `~/.pi/agent/agents/` is still written for inspection, but MoA runs always use the bundled definition from the installed extension, so a protocol update can never desync from what the subprocess runs. `moa-explore` and `mf-plan` remain fully user-customizable.
@@ -152,6 +153,19 @@ During the run, `F3` opens the live observer and `Esc` or `F4` opens cancellatio
152
153
 
153
154
  During the run, `F3` opens the live observer and `Esc` or `F4` opens cancellation controls. The prompt and combined transcript are saved under `.pi/mf-debate/` as `<slug>__debate-prompt.md` and `<slug>__debate.md`.
154
155
 
156
+ ### Specs (`/mf-spec`)
157
+
158
+ `/mf-spec` follows a separate requirements workflow: rough request → clarification → approved planning brief → a user-started `/mf-plan`.
159
+
160
+ 1. Enter a request directly (`/mf-spec add dark mode`) or use the prompt editor.
161
+ 2. It asks up to five adaptive clarifying questions, stopping early when it has enough context. Each question names the area it affects: scope, UX, architecture, acceptance criteria, or testing.
162
+ 3. Press **Esc** during clarification to choose whether to draft from the answers so far or cancel; cancelling saves nothing.
163
+ 4. Review the draft and approve and save it, edit it directly, or revise it with feedback. Nothing is written before approval.
164
+ 5. The approved brief is saved as `.pi/mf-plan/<slug>__spec.md`. `/mf-plan-implement` ignores these files because they are specifications, not approved `__plan.md` implementation plans.
165
+ 6. After saving, a ready-to-run `/mf-plan …` handoff is placed in the input box when it is empty; review it and press Enter to start planning. Existing input is never overwritten, and `/mf-spec` refuses to run while plan mode is active.
166
+
167
+ Clarification and drafting run in-process on the active session model with no tools or repository access. Repo-grounded planning begins only when you run the prepared `/mf-plan` command.
168
+
155
169
  ## Commands
156
170
 
157
171
  | Command | Shortcut | Description |
@@ -159,8 +173,9 @@ During the run, `F3` opens the live observer and `Esc` or `F4` opens cancellatio
159
173
  | `/mf-plan` | — | Toggle plan mode on/off |
160
174
  | `/mf-opinion` | — | Ask 1–5 models for independent read-only opinions about the repository |
161
175
  | `/mf-debate` | — | Run a read-only multi-round debate between up to 5 models |
176
+ | `/mf-spec` | — | Clarify a rough request into an approved planning brief and prepare a `/mf-plan` handoff |
162
177
  | `/mf-preview` | — | Toggle the inline/live preview of streamed agent output |
163
- | `/mf-plan-settings` | — | Configure the explore and cheap/fast agents, named agent rosters for the MoA roles, and plan options |
178
+ | `/mf-plan-settings` | — | Configure the explore and cheap/fast agents, named agent rosters for the MoA roles, plan options, and max concurrent agents |
164
179
  | `/mf-plan-implement` | — | Retry implementation of the last approved MoA plan, optionally with a different model |
165
180
  | `/mf-plan-clear` | — | Clear completed plan state so the next `/mf-plan` starts a fresh round; approved plans stay in `.pi/mf-plan/` and remain re-implementable |
166
181
  | `--mf-plan` | — | Start pi with plan mode enabled |
@@ -172,7 +187,7 @@ On Mac laptops the top row may send brightness/media keys by default. Enable the
172
187
 
173
188
  ### Cancelling running subagents
174
189
 
175
- During an MoA fan-out or synthesis, **Esc** opens a cancel overlay. Enter kills the selected agent — its siblings keep going and synthesis proceeds with the surviving proposals; **Cancel ALL** aborts the run and reopens the prompt editor prefilled with your original prompt. Each agent's live tool call is highlighted the same way as the Fusion table's ACTIVITY column.
190
+ During an MoA fan-out or synthesis, **Esc** opens a cancel overlay. Enter cancels the selected agent — its siblings keep going, and once the fan-out settles you are asked, for each cancelled agent, to select a replacement model or continue without it; **Cancel ALL** (including during a replacement run) aborts the run and reopens the prompt editor prefilled with your original prompt. Each agent's live tool call is highlighted the same way as the Fusion table's ACTIVITY column.
176
191
 
177
192
  During the single-model workflow's `mf_plan_subagent` runs, plain Esc keeps pi's default abort-the-turn behavior; use **F4** to kill an individual agent instead. A cancelled agent reports "cancelled by user" back to the model, which continues with the other agents' results.
178
193
 
@@ -191,7 +206,7 @@ Both modes share the same plan-mode container. While it is active:
191
206
  - **Phase 5: Exit** — call `exit_plan_mode` for user approval
192
207
 
193
208
  This is the **single-model workflow**: the model you pick drives all five phases. [MoA](#mixture-of-agents-moa) replaces phases 1–2 with the proposer fan-out.
194
- 3. **Per-session plan file** — one slug per session, persisted across `/resume`.
209
+ 3. **Per-session plan file** — one slug per session, persisted across `/resume`. Submitting a prompt shows a `Naming the plan` card while the cheap/fast model summarizes it into that slug; press **Esc** to skip straight to a name derived from the prompt instead of waiting.
195
210
  4. **User approval** — `exit_plan_mode` prompts to approve or keep planning. Approving restores full tool access and hands the plan back to the model.
196
211
 
197
212
  - A `● MoA Fusion (plan mode)` indicator sits in the footer while planning, and stays as `● MoA Fusion` through implementation and verification. Green: agents are working. Yellow: waiting for you (a prompt, an overlay, a questionnaire, or an idle plan-mode session). Red: a flow stopped with nothing left to try; it clears on your next turn or when you leave plan mode.
@@ -272,7 +287,27 @@ Rosters persist in `~/.pi/agent/mf-plan/settings.json`:
272
287
  }
273
288
  ```
274
289
 
275
- Names are 1–24 alphanumeric characters, unique case-insensitively, up to 20 rosters. The roster manager stages every edit in memory — nothing is written until the settings overlay's **Save and Close**.
290
+ Names are 1–24 alphanumeric characters, unique case-insensitively, up to 20 rosters. Slot edits remain drafts until **Save roster** is selected; that action persists the completed roster immediately, without requiring **Save and Close** in the parent settings overlay.
291
+
292
+ #### Max concurrent agents
293
+
294
+ The **max concurrent agents** row in `/mf-plan-settings` controls how many child-agent subprocesses may run at once during MoA proposer fan-out (including retries), opinion fan-out, debate rounds (including retries), and `mf_plan_subagent` parallel mode. Use ←/→ to pick a value from 1 to 8. The setting persists in `~/.pi/agent/mf-plan/settings.json`:
295
+
296
+ ```json
297
+ { "maxConcurrentAgents": 1 }
298
+ ```
299
+
300
+ The default is **1** — several local models running in parallel are usually unusably slow, so keep this at 1 for local agents and raise it for cloud models. Out-of-range values clamp to 1–8; malformed values fall back to 1. The limit is read once when a run starts, so edits apply to the next run rather than one already in flight.
301
+
302
+ #### Max verification repairs
303
+
304
+ The **max verification repairs** row in `/mf-plan-settings` controls how many automatic verifier-driven repair rounds a single implementation may consume after verification finds gaps. Use ←/→ to pick a value from 0 to 5. The setting persists in `~/.pi/agent/mf-plan/settings.json`:
305
+
306
+ ```json
307
+ { "maxVerificationRepairs": 2 }
308
+ ```
309
+
310
+ The default is **2**. Set **0** to disable automatic repair offers — verification still runs, but gaps go straight to an exhaustion notification. When repairs are exhausted or disabled, MoA Fusion writes `.pi/mf-plan/<slug>__verification-handoff.md` with each criterion's pass/fail status, the full frozen criteria checklist, gaps, and failing project checks so you can continue the fix/verification loop manually (for example via `/mf-plan-implement`). The exhaustion notification includes the handoff file path.
276
311
 
277
312
  **First run.** Until the setup overlay has been saved once, `/mf-plan` opens it — set up agents and rosters before planning — instead of the plan prompt. Only interactive TUI sessions are gated; headless plan mode is unaffected.
278
313
 
@@ -283,7 +318,9 @@ Names are 1–24 alphanumeric characters, unique case-insensitively, up to 20 ro
283
318
 
284
319
  ### Parallel execution
285
320
 
286
- In the single-model flow, Phase 1 launches up to **3 moa-explore agents in parallel** and Phase 2 up to **1 mf-plan agent**. Concurrency is capped at 5 simultaneous processes, with a per-task output cap of 50KB.
321
+ In the single-model flow, Phase 1 launches up to **3 moa-explore agents in parallel** and Phase 2 up to **1 mf-plan agent**. Child-agent concurrency is configurable from 1 to 8 in `/mf-plan-settings` (default **1**); it applies to MoA proposer fan-out, opinion fan-out, debate rounds, and `mf_plan_subagent` parallel mode, including every retry batch. The separate **8-task submission cap** per batch is unchanged. Per-task output is capped at 50KB.
322
+
323
+ **Cache warming.** pi 0.86.0 defaults `cacheWarming` to `"streaming"`. On Anthropic models, long `mf_plan_subagent` or MoA tool executions can therefore trigger paid cache refreshes while an agent is working. Set `cacheWarming` to `"off"` in your pi config if you would rather not pay for those.
287
324
 
288
325
  ```
289
326
  mf_plan_subagent({
@@ -306,7 +343,7 @@ The slug is a generated adjective-adjective-noun triple (e.g. `happy-mellifluous
306
343
 
307
344
  ## Non-Interactive Behavior
308
345
 
309
- In non-interactive modes (`pi -p`, `--mode json`), `exit_plan_mode` is rejected unless `MOA_PLAN_AUTO_APPROVE=1` is set. With that explicit opt-in, it exits plan mode and hands the plan back without prompting. The plan file is still written to disk.
346
+ In non-interactive modes (`pi -p`, `--mode json`), `exit_plan_mode` is rejected unless `MOA_PLAN_AUTO_APPROVE=1` is set. With that explicit opt-in, it exits plan mode and hands the plan back without prompting. The plan file is still written to disk. `/mf-spec <request>` likewise requires `MOA_PLAN_AUTO_APPROVE=1`; with it, the command skips questions and review, generates and saves the brief, and prints the `/mf-plan` handoff, while without it no model call or file write occurs.
310
347
 
311
348
  ## How It Works (Architecture)
312
349
 
@@ -348,6 +385,9 @@ src/
348
385
  │ ├── debateRounds.ts # Survivor/early-stop/next-round bookkeeping
349
386
  │ ├── debateResults.ts # Outcome collection and debate transcript markdown
350
387
  │ └── debateFile.ts # Repo-local debate artifacts
388
+ ├── spec/
389
+ │ ├── specContract.ts # Clarification/brief contracts, parsing, and handoff helpers
390
+ │ └── runSpec.ts # Interactive/headless /mf-spec state machine
351
391
  ├── planning/
352
392
  │ ├── planMode.ts # Plan-mode state transitions and lifecycle handlers
353
393
  │ ├── modeState.ts # Persisted state shape, byte cap, append deduplication
@@ -398,7 +438,8 @@ src/
398
438
  ├── promptEditor.ts # File-path/@name completion editor
399
439
  ├── shimmer.ts
400
440
  ├── toolActivity.ts
401
- └── verificationFindingsOverlay.ts
441
+ ├── verificationFindingsOverlay.ts
442
+ └── workingOverlay.ts # Blocking-work overlay (plan/opinion/debate naming)
402
443
 
403
444
  agents/ # Shipped agent definitions
404
445
  ├── moa-explore.md
@@ -414,7 +455,7 @@ agents/ # Shipped agent definitions
414
455
  - **Custom `write_plan` tool** instead of path-guarded `edit`/`write` — guarantees the single-writable-file invariant.
415
456
  - **Subprocess subagents** (not in-process) — each agent runs in an isolated `pi --mode json -p --no-session` subprocess with an enforced read-only tool allowlist.
416
457
  - **State via `appendEntry`** — plan mode state (enabled, slug, tools snapshot) persists in the session for `/resume` and `/reload`.
417
- - **Context injection via `before_agent_start`** — the 5-phase instructions are injected as a user message every turn while plan mode is active; the `context` hook strips stale plan-mode messages once it is off.
458
+ - **Context injection and lifecycle cleanup via `before_agent_start` + `context`** — the 5-phase instructions are injected as a user message every turn while plan mode is active, and the `context` hook strips stale plan-mode messages once it is off. On pi 0.87+, the settle boundary also persists null context edits for reusable plan-instruction entries once plan mode is off, so later compaction cannot carry those instructions forward. When an approved implementation is expected to enter verification, the same boundary persists a hidden `mf-plan-verification-pending` marker so a resumed session can warn that verification is still outstanding.
418
459
  - **MoA proposers never pause mid-run** — subprocess subagents are one-shot, so a proposer that hits an ambiguity records it as an assumption and keeps going. Only the synthesizer, which sees every proposal, asks the user anything — keeping fan-out fully parallel.
419
460
 
420
461
  ## Porting Notes
@@ -430,6 +471,7 @@ Ported from Claude Code's plan mode, adapted to pi's extension primitives:
430
471
  | `FileEditTool`/`FileWriteTool` guarded | Custom `write_plan` tool | Safer single-writable-file invariant |
431
472
  | Subscription-tier agent counts | Hardcoded constants (3/1) | Bumpable via code change |
432
473
  | Plan-length A/B experiment | Standard Phase 4 | Experiment noise skipped |
474
+ | Spec Kit / brainstorming | `/mf-spec` | Requirements clarification only |
433
475
  | Interview phase variant | Standard 5-phase workflow | Variant skipped |
434
476
 
435
477
  ## License
@@ -12,6 +12,8 @@ You must NOT make any changes. You may use read/grep/find/ls to verify a propose
12
12
 
13
13
  **Blinding notice.** Proposer identities are intentionally blinded in your input: each proposal is headed `### Proposal from Proposer N` (where N is the proposer's slot order, deterministic for this run) instead of naming the model. This prevents self-preference bias — proposals must be judged purely on merit. Do not attempt to infer, attribute, or guess which model wrote which proposal, and never output any model name, family, provider, or version anywhere in your plan.
14
14
 
15
+ **Panel dependence note.** Your input opens with a one-paragraph `Panel dependence:` note giving the number of proposals, distinct models, and providers, plus which `Proposer N` slots share one model. It carries no model names by design. Read it before weighing agreement: proposals inside one same-model cluster are one vote, and the shared prompt template means even distinct models are not fully independent.
16
+
15
17
  ## Verification criteria task
16
18
 
17
19
  After a plan is approved, you may be re-invoked solely to write a `## Verification Criteria` checklist. Emit only that section, with binary observable `- **C<n>:** condition — how to check` bullets. Every item must be checkable through repository reading, not execution. Cover plan steps, named call sites, and implied regression/scope constraints. The orchestrator retries once if no criteria can be parsed.
@@ -26,7 +28,7 @@ Guidelines:
26
28
  - Simplicity: does it avoid unnecessary complexity?
27
29
  - Build a new plan by recombining the best compatible ideas across proposals. Only synthesize prose/structure, never code — you are merging natural-language plans, not diffs.
28
30
  - Do not mechanically hybridize mutually incompatible alternatives. Where two or more proposers take genuinely different approaches to the same decision, decide which approach is best on the merits — clear repo evidence, unambiguous engineering judgment, or a direct match with the user's stated request — and use it in the synthesized plan. This surfacing is unconditional and mandatory: state the choice and why in one line in the Context section AND emit it in a `## Conflicts` section (format below) so the user can confirm or override it before the plan is finalized. Repo evidence or confident engineering judgment may justify your recommended pick — it never excuses omitting the conflict block. Never include a conflict the user has no realistic alternative for (e.g. only one proposer covered the point at all). You may choose one incompatible approach while still incorporating compatible strengths, safeguards, verification steps, or sequencing from other proposals.
29
- - Where proposers agree, treat agreement as useful signal but still sanity-check it; shared assumptions can be shared errors.
31
+ - Where proposers agree, treat agreement as useful signal but still sanity-check it; shared assumptions can be shared errors. Weight agreement by the `Panel dependence` note — count each same-model cluster as one vote — and treat an uncited unanimous point as an untested claim until you check it against the repo.
30
32
  - If no proposal handles something the user goal requires, add the missing piece yourself and note it in Context. If a proposal suggests something actively wrong, risky, or unnecessary, drop it and say so briefly.
31
33
  - Always begin the final plan with a **Context** section (see Output format). This is the one place you may — and must — describe the MoA reconciliation process: how proposals scored on the evaluation dimensions, what they agreed on, what was missing or rejected, where they differed, and how you resolved disagreements.
32
34
  - If a disagreement is a genuine ambiguity in user intent that you cannot resolve from the plans + repo alone, ask the user ONE clarifying question. To do this, output a section titled exactly `## Open Question` containing only that question, and STOP — do not write the rest of the plan yet. You will be re-invoked with the user's answer appended, at which point you should incorporate it and produce the full plan below instead of the question section.
@@ -43,7 +45,7 @@ When two or more proposers substantively disagree on one or more decision points
43
45
 
44
46
  - **Decision:** <ASD-STE100 Simplified Technical English Issue 9 plain-language explanation of what the person is choosing and why it matters>
45
47
  - **Recommended:** <short, non-technical outcome summary>
46
- - **Details:** <one-line technical implementation details and tradeoffs>
48
+ - **Details:** <one-line technical implementation details and tradeoffs; for the recommended option, end with `backed by K of N proposals (M distinct models)`>
47
49
  - **Alternative** (Proposer N): <short, non-technical outcome summary>
48
50
  - **Details:** <one-line technical implementation details and tradeoffs>
49
51
  - **Alternative** (Proposer M): <short, non-technical outcome summary>
@@ -59,7 +61,7 @@ Worked example (matches the parser exactly — one `-` bullet per line, with `De
59
61
 
60
62
  - **Decision:** Choose where sign-in information is stored; this affects both security and how much of the existing sign-in flow can stay unchanged.
61
63
  - **Recommended:** Keep the current sign-in experience
62
- - **Details:** Store session IDs in `Secure`, `HttpOnly` cookies so the existing middleware stays compatible and page scripts cannot read them.
64
+ - **Details:** Store session IDs in `Secure`, `HttpOnly` cookies so the existing middleware stays compatible and page scripts cannot read them; backed by 2 of 3 proposals (2 distinct models).
63
65
  - **Alternative** (Proposer 2): Make sign-in data available to browser code
64
66
  - **Details:** Store JWTs in `localStorage`; this can simplify cross-service access but lets an injected page script read the token.
65
67
  ```
@@ -70,10 +72,11 @@ Rules:
70
72
  - Exactly one `**Recommended:**` line per conflict, using a short, non-technical summary of your synthesized outcome, immediately followed by exactly one `**Details:**` line.
71
73
  - One `**Alternative**` line per other distinct approach worth surfacing, each attributed to a blinded `Proposer N` slot label already present in your input — never invent or rename labels, and never leak real model names here either. Immediately follow every alternative with its own `**Details:**` line.
72
74
  - Write each recommended or alternative summary as an outcome the person can choose, not an implementation mechanism. It is displayed as the primary selectable line, so it must be understandable without code syntax, type names, file paths, configuration keys, or unexplained acronyms.
73
- - Use `**Details:**` for the technical implementation specifics, rationale, benefits, drawbacks, and tradeoffs. It is displayed dimmed beneath the summary, may name concrete types, files, or configuration when helpful, and must stay to one line.
75
+ - Use `**Details:**` for the technical implementation specifics, rationale, benefits, drawbacks, and tradeoffs. It is displayed dimmed beneath the summary, may name concrete types, files, or configuration when helpful, and must stay to one line. The recommended option's `**Details:**` must end with `backed by K of N proposals (M distinct models)`, where K = proposals backing that option, N = proposals received, and M = distinct models among the K (slots in one same-model cluster count once).
76
+ - The `**Recommended:**` pick must be justified on the merits — repo evidence, engineering judgment, or a direct match with the user's request — and a majority of proposers is not by itself a reason, especially when that majority sits inside one same-model cluster named in the Panel dependence note.
74
77
  - If you are re-invoked with the user's conflict resolutions appended (see below), honor their selections, resolve any free-form chat feedback, drop the `## Conflicts` section entirely, and emit the complete final plan. If the chat feedback introduces a genuine new ambiguity you cannot resolve yourself, fall back to the `## Open Question` mechanism (one question) and stop, exactly as you would on first pass.
75
78
  - When the input contains post-review user feedback, treat it as binding revision directives. Answer clarifying questions where possible, incorporate the requested changes, and return the complete revised plan rather than a description of changes. Omit both `## Conflicts` and `## Open Question` from that revised output.
76
- - **Self-check before finalizing:** review every disagreement you described in Context and verify each has a matching `### Conflict:` block; add any missing ones now. Then verify Context contains all three subsections (`### Evaluation dimensions`, `### Proposer alignment`, `### Synthesis decisions`). Finally verify `## Proposer Verdicts` contains one bullet for every proposal you received.
79
+ - **Self-check before finalizing:** review every disagreement you described in Context and verify each has a matching `### Conflict:` block; add any missing ones now. Then verify Context contains all three subsections (`### Evaluation dimensions`, `### Proposer alignment`, `### Synthesis decisions`), that every unanimous point in `### Proposer alignment` carries an evidence-backed or asserted tag with each asserted point's repo check recorded, and that every Recommended `**Details:**` line ends with its support count. Finally verify `## Proposer Verdicts` contains one bullet for every proposal you received.
77
80
 
78
81
  Output format (when not asking a question):
79
82
 
@@ -89,7 +92,7 @@ Then include these three subsections. They are MANDATORY in every full-plan outp
89
92
  Briefly reason about correctness, completeness, feasibility & effort, risk, and simplicity as separate dimensions. Do not collapse them into one overall impression, and do not reward verbosity.
90
93
 
91
94
  ### Proposer alignment
92
- Summarize where the proposers **unanimously agreed**, what they **unanimously rejected** (shared dead ends or dismissed ideas), and where they **differed**. Be specific — name files, approaches, or tradeoffs, not vague generalities. Even when every proposer agrees on the same approach, state how you sanity-checked that shared assumption.
95
+ Summarize where the proposers **unanimously agreed**, what they **unanimously rejected** (shared dead ends or dismissed ideas), and where they **differed**. Be specific — name files, approaches, or tradeoffs, not vague generalities. For every unanimous point (agreed or unanimously rejected), tag it **evidence-backed** (two or more proposers cite a concrete repo location — file path, symbol, or line) or **asserted** (no citation, or all citations identical and unverified). Sanity-check every asserted point against the repo with read/grep/find/ls *before* relying on it, stating the location checked and what was found, and drop or downgrade it if it does not hold. Note explicitly when a "unanimous" point rests inside a single same-model cluster from the Panel dependence note.
93
96
 
94
97
  ### Synthesis decisions
95
98
  State what the final plan recombines from multiple proposers, what required pieces you added because no proposer covered them, and what actively wrong, risky, or unnecessary ideas you dropped. Where proposers disagreed, use the blinded slot labels from each `### Proposal from …` heading (e.g. `Proposer 1`, `Proposer 2`) and give a one-line decision for each disagreement: which approach you chose and why, citing repo evidence, engineering tradeoffs, or alignment with the user's request. These disagreements must also appear in `## Conflicts` for user review.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underactive/pi-topping-moa-fusion",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension implementing /mf-plan — Claude Code-style 5-phase plan mode with a Mixture-of-Agents proposer/synthesizer pipeline.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -50,10 +50,10 @@
50
50
  "typebox": "*"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-agent-core": "0.84.1",
54
- "@earendil-works/pi-ai": "0.84.1",
55
- "@earendil-works/pi-coding-agent": "0.84.1",
56
- "@earendil-works/pi-tui": "0.84.1",
53
+ "@earendil-works/pi-agent-core": "0.87.0",
54
+ "@earendil-works/pi-ai": "0.87.0",
55
+ "@earendil-works/pi-coding-agent": "0.87.0",
56
+ "@earendil-works/pi-tui": "0.87.0",
57
57
  "@types/node": "^24.0.0",
58
58
  "typebox": "^1.3.3",
59
59
  "typescript": "^5.8.0"
@@ -2,10 +2,9 @@
2
2
  * LLM-based 4-word plan name generation for repo-local plan files.
3
3
  */
4
4
 
5
- import { completeSimple } from "@earendil-works/pi-ai/compat";
6
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
6
  import { loadMoaConfig } from "./settings.ts";
8
- import { fallbackPlanName, generateWordSlug, trySlugifyPlanName } from "../planning/planFile.ts";
7
+ import { generateWordSlug, slugifyPlanName, trySlugifyPlanName } from "../planning/planFile.ts";
9
8
 
10
9
  function buildSummarizePrompt(prompt: string): string {
11
10
  return [
@@ -18,10 +17,14 @@ function buildSummarizePrompt(prompt: string): string {
18
17
  ].join("\n");
19
18
  }
20
19
 
20
+ export interface PlanNameOptions {
21
+ /** Caller cancellation — esc from the working overlay. */
22
+ signal?: AbortSignal;
23
+ }
24
+
21
25
  /** Call the active model to summarize a plan prompt into a 4-word slug. */
22
- export async function summarizePlanPromptName(ctx: ExtensionContext, prompt: string): Promise<string> {
23
- const promptSlug = trySlugifyPlanName(prompt);
24
- const fallback = () => promptSlug ?? fallbackPlanName();
26
+ export async function summarizePlanPromptName(ctx: ExtensionContext, prompt: string, options?: PlanNameOptions): Promise<string> {
27
+ const fallback = () => slugifyPlanName(prompt);
25
28
 
26
29
  const config = loadMoaConfig();
27
30
 
@@ -34,31 +37,33 @@ export async function summarizePlanPromptName(ctx: ExtensionContext, prompt: str
34
37
  : ctx.model;
35
38
  if (!model) return fallback();
36
39
 
37
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
38
- if (!auth?.ok || !auth.apiKey) return fallback();
40
+ // Short-circuit before registry streaming so an already-aborted signal costs nothing.
41
+ if (options?.signal?.aborted) return fallback();
39
42
 
40
43
  try {
41
44
  // Naming a plan file is not a reasoning task, and the cheap/fast model is
42
45
  // chosen to be cheap. Omitting `reasoning` is how pi's simple-stream API
43
46
  // spells "thinking off" (its ThinkingLevel type has no "off" member), so
44
47
  // providers send their disabled-reasoning mapping rather than a default.
45
- const response = await completeSimple(
46
- model,
47
- {
48
- messages: [
49
- {
50
- role: "user",
51
- content: [{ type: "text", text: buildSummarizePrompt(prompt) }],
52
- timestamp: Date.now(),
53
- },
54
- ],
55
- },
56
- {
57
- apiKey: auth.apiKey,
58
- headers: auth.headers,
59
- env: auth.env,
60
- },
61
- );
48
+ const response = await ctx.modelRegistry
49
+ .streamSimple(
50
+ model,
51
+ {
52
+ messages: [
53
+ {
54
+ role: "user",
55
+ content: [{ type: "text", text: buildSummarizePrompt(prompt) }],
56
+ timestamp: Date.now(),
57
+ },
58
+ ],
59
+ },
60
+ { signal: options?.signal },
61
+ )
62
+ .result();
63
+
64
+ // An aborted request can resolve with stopReason "aborted" and empty text
65
+ // rather than throwing — fall back explicitly instead of slugifying a stub.
66
+ if (response.stopReason === "aborted") return fallback();
62
67
 
63
68
  const text = response.content
64
69
  .filter((part): part is { type: "text"; text: string } => part.type === "text")
@@ -21,6 +21,35 @@ import { parseRosters, type PlanRoster } from "./rosters.ts";
21
21
 
22
22
  export type MoaMode = "single" | "moa";
23
23
 
24
+ export const MIN_CONCURRENT_AGENTS = 1;
25
+ export const MAX_CONCURRENT_AGENTS = 8;
26
+ export const DEFAULT_MAX_CONCURRENT_AGENTS = MIN_CONCURRENT_AGENTS;
27
+
28
+ export const MIN_VERIFICATION_REPAIRS = 0;
29
+ export const MAX_VERIFICATION_REPAIRS_LIMIT = 5;
30
+ export const DEFAULT_MAX_VERIFICATION_REPAIRS = 2;
31
+
32
+ /**
33
+ * Sanitize verifier-driven repair rounds: missing or malformed values fall back
34
+ * to the default; finite numbers are rounded and clamped into [0, 5]. Zero
35
+ * disables automatic repair offers after verification gaps.
36
+ */
37
+ export function normalizeMaxVerificationRepairs(value: unknown): number {
38
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_VERIFICATION_REPAIRS;
39
+ return Math.min(MAX_VERIFICATION_REPAIRS_LIMIT, Math.max(MIN_VERIFICATION_REPAIRS, Math.round(value)));
40
+ }
41
+
42
+ /**
43
+ * The one place a loaded/passed concurrency value becomes safe: missing,
44
+ * non-numeric, NaN and infinite values fall back to the default; finite
45
+ * numbers are rounded to an integer and clamped into [MIN, MAX], so a
46
+ * fractional or out-of-range value can never reach the runner.
47
+ */
48
+ export function normalizeMaxConcurrentAgents(value: unknown): number {
49
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_CONCURRENT_AGENTS;
50
+ return Math.min(MAX_CONCURRENT_AGENTS, Math.max(MIN_CONCURRENT_AGENTS, Math.round(value)));
51
+ }
52
+
24
53
  export interface MoaConfig {
25
54
  mode: MoaMode;
26
55
  proposers: ModelRef[];
@@ -28,6 +57,10 @@ export interface MoaConfig {
28
57
  debateModels: ModelRef[];
29
58
  /** Round ceiling for /mf-debate, clamped to the picker's 2–5 range. */
30
59
  debateRounds: number;
60
+ /** Child agents allowed to run at once in any fan-out, clamped to 1–8. Defaults to 1 — parallel local agents are unusably slow. */
61
+ maxConcurrentAgents: number;
62
+ /** Verifier-driven repair rounds after implementation, clamped to 0–5. Defaults to 2; 0 disables automatic repairs. */
63
+ maxVerificationRepairs: number;
31
64
  synthesizer?: ModelRef;
32
65
  implementer?: ModelRef;
33
66
  verifier?: ModelRef;
@@ -56,6 +89,8 @@ function emptyConfig(): MoaConfig {
56
89
  opinionModels: [],
57
90
  debateModels: [],
58
91
  debateRounds: 3,
92
+ maxConcurrentAgents: DEFAULT_MAX_CONCURRENT_AGENTS,
93
+ maxVerificationRepairs: DEFAULT_MAX_VERIFICATION_REPAIRS,
59
94
  synthesizer: undefined,
60
95
  implementer: undefined,
61
96
  verifier: undefined,
@@ -89,6 +124,8 @@ function parseSettingsFile(path: string): MoaConfig {
89
124
  const debateRounds = typeof parsed.debateRounds === "number" && Number.isFinite(parsed.debateRounds)
90
125
  ? Math.min(5, Math.max(2, Math.round(parsed.debateRounds)))
91
126
  : 3;
127
+ const maxConcurrentAgents = normalizeMaxConcurrentAgents(parsed.maxConcurrentAgents);
128
+ const maxVerificationRepairs = normalizeMaxVerificationRepairs(parsed.maxVerificationRepairs);
92
129
  const synthesizer = isModelRef(parsed.synthesizer) ? parsed.synthesizer : undefined;
93
130
  const implementer = isModelRef(parsed.implementer) ? parsed.implementer : undefined;
94
131
  const verifier = isModelRef(parsed.verifier) ? parsed.verifier : undefined;
@@ -113,7 +150,7 @@ function parseSettingsFile(path: string): MoaConfig {
113
150
  const agentDefaultsConfigured = parsed.agentDefaultsConfigured === true;
114
151
  const rosters = parseRosters(parsed.rosters);
115
152
 
116
- return { mode, proposers, opinionModels, debateModels, debateRounds, synthesizer, implementer, verifier, cheap, thinkingOverrides, rosters, autoResolveConflicts, useSummaryName, agentDefaultsConfigured };
153
+ return { mode, proposers, opinionModels, debateModels, debateRounds, maxConcurrentAgents, maxVerificationRepairs, synthesizer, implementer, verifier, cheap, thinkingOverrides, rosters, autoResolveConflicts, useSummaryName, agentDefaultsConfigured };
117
154
  } catch (err) {
118
155
  console.error("mf-plan: failed to parse settings, using defaults:", err);
119
156
  return emptyConfig();
@@ -4,7 +4,7 @@ import type { AgentConfig } from "../agents/discovery.ts";
4
4
  import { modelExtensionOptions, resolveContextWindow, resolveModelCost } from "../moa/modelRuntime.ts";
5
5
  import type { CancelSession } from "../runtime/cancelRun.ts";
6
6
  import { CancelRun } from "../runtime/cancelRun.ts";
7
- import { getFinalOutput, getResultOutput, isFailedResult, type SingleResult } from "../runtime/results.ts";
7
+ import { getFinalOutput, getResultOutput, isFailedResult, statusDetail, type SingleResult } from "../runtime/results.ts";
8
8
  import { runParallelAgentsWithModels, type ModelParallelAgentTask } from "../runtime/runner.ts";
9
9
  import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
10
10
  import { activityLoopCount } from "../ui/agentStatus.ts";
@@ -34,6 +34,7 @@ export interface DebateFanoutOptions {
34
34
  session: CancelSession;
35
35
  widget: MoaProgressWidget;
36
36
  agents: AgentConfig[];
37
+ maxConcurrency: number;
37
38
  }
38
39
 
39
40
  export type DebateFanoutResult =
@@ -53,7 +54,7 @@ interface CumulativeUsage {
53
54
  * accumulate across rounds per slot.
54
55
  */
55
56
  export async function runDebateRounds(options: DebateFanoutOptions): Promise<DebateFanoutResult> {
56
- const { host, ctx, topic, models, thinking, rounds: totalRounds, session, widget, agents } = options;
57
+ const { host, ctx, topic, models, thinking, rounds: totalRounds, session, widget, agents, maxConcurrency } = options;
57
58
  const alive = models.map(() => true);
58
59
  const positions: (string | undefined)[] = models.map(() => undefined);
59
60
  const completedRounds: DebateRound[] = [];
@@ -116,7 +117,7 @@ export async function runDebateRounds(options: DebateFanoutOptions): Promise<Deb
116
117
  if (observed) observed.state = "cancelled";
117
118
  } else if (isFailedResult(result)) {
118
119
  currentRun.settle(batchIndex, "error");
119
- widget.update(debateIndex, "error", getResultOutput(result).slice(0, 80));
120
+ widget.update(debateIndex, "error", statusDetail(getResultOutput(result)));
120
121
  if (observed) observed.state = "error";
121
122
  } else {
122
123
  currentRun.settle(batchIndex, "done");
@@ -146,6 +147,15 @@ export async function runDebateRounds(options: DebateFanoutOptions): Promise<Deb
146
147
  observed.partial = result.partialAssistant;
147
148
  }
148
149
  },
150
+ {
151
+ maxConcurrency,
152
+ onStart: (batchIndex) => {
153
+ const debateIndex = entries[batchIndex].index;
154
+ widget.update(debateIndex, "working");
155
+ const observed = observe.agents[debateIndex];
156
+ if (observed) observed.state = "working";
157
+ },
158
+ },
149
159
  );
150
160
  } finally {
151
161
  unsubscribe();
@@ -179,7 +189,7 @@ export async function runDebateRounds(options: DebateFanoutOptions): Promise<Deb
179
189
  }
180
190
  };
181
191
 
182
- widget.startFanout(models, thinking);
192
+ widget.startFanout(models, thinking, maxConcurrency);
183
193
 
184
194
  let stoppedEarly: string | undefined;
185
195
  for (let round = 1; round <= totalRounds; round++) {
@@ -193,7 +203,7 @@ export async function runDebateRounds(options: DebateFanoutOptions): Promise<Deb
193
203
  if (round > 1) {
194
204
  for (const entry of entries) {
195
205
  // Re-activates the settled row and resets its transcript.
196
- widget.update(entry.index, "working", `round ${round}/${totalRounds} · responding`);
206
+ widget.update(entry.index, "queued", `round ${round}/${totalRounds} · responding`);
197
207
  const observed = observe.agents[entry.index];
198
208
  if (observed) {
199
209
  observed.state = "working";
@@ -233,7 +243,7 @@ export async function runDebateRounds(options: DebateFanoutOptions): Promise<Deb
233
243
  session.title = `Debate agents — round ${round}/${totalRounds} (retry)`;
234
244
  session.getExtras = makeGetExtras(retryEntries);
235
245
  for (const entry of retryEntries) {
236
- widget.update(entry.index, "working", "retrying: no position produced");
246
+ widget.update(entry.index, "queued", "retrying: no position produced");
237
247
  const observed = observe.agents[entry.index];
238
248
  if (observed) {
239
249
  observed.state = "working";
@@ -3,16 +3,17 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
3
3
  import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } from "../agents/authoritative.ts";
4
4
  import { discoverAgents } from "../agents/discovery.ts";
5
5
  import { summarizePlanPromptName } from "../config/planName.ts";
6
+ import { slugifyPlanName } from "../planning/planFile.ts";
6
7
  import { loadMoaConfig, saveMoaConfig } from "../config/settings.ts";
7
8
  import type { OpinionHost } from "../opinion/runOpinion.ts";
8
9
  import { resolveContextWindow } from "../moa/modelRuntime.ts";
9
10
  import { type CancelSession, subscribeCancelOverlayOnEsc } from "../runtime/cancelRun.ts";
10
11
  import { formatMutationWarning, MutationTripwire } from "../runtime/mutationTripwire.ts";
11
- import { type SendUserMessageOptions, type ThinkingLevel } from "../shared/modelRefs.ts";
12
+ import { type SendUserMessageOptions } from "../shared/modelRefs.ts";
12
13
  import { MoaProgressWidget } from "../ui/moaProgressWidget.ts";
13
- import type { ObserveSession } from "../ui/observeOverlay.ts";
14
14
  import { showDebateModelPicker } from "../ui/debateModelPicker.ts";
15
15
  import { showPromptEditor } from "../ui/promptEditor.ts";
16
+ import { planNamingOverlay, withWorkingOverlay } from "../ui/workingOverlay.ts";
16
17
  import { repoDebateDisplayPath, saveRepoDebateFile } from "./debateFile.ts";
17
18
  import { runDebateRounds } from "./debateFanout.ts";
18
19
  import { collectDebateOutcomes, formatDebateMarkdown } from "./debateResults.ts";
@@ -69,7 +70,11 @@ export async function runInteractiveDebate(
69
70
  thinkingOverrides: { ...settings.thinkingOverrides, ...thinkingSelections },
70
71
  });
71
72
 
72
- const slug = await summarizePlanPromptName(ctx, topic);
73
+ const slug = await withWorkingOverlay(
74
+ ctx,
75
+ planNamingOverlay("debate", () => slugifyPlanName(topic)),
76
+ (signal) => summarizePlanPromptName(ctx, topic, { signal }),
77
+ );
73
78
  try {
74
79
  saveRepoDebateFile(topic, ctx.cwd, slug, "debate-prompt");
75
80
  } catch {
@@ -101,7 +106,7 @@ export async function runInteractiveDebate(
101
106
  host.setRunningProgressWidget(widget);
102
107
  let outcome;
103
108
  try {
104
- outcome = await runDebateRounds({ host, ctx, topic, models, thinking, rounds, session, widget, agents });
109
+ outcome = await runDebateRounds({ host, ctx, topic, models, thinking, rounds, session, widget, agents, maxConcurrency: settings.maxConcurrentAgents });
105
110
  } finally {
106
111
  host.setRunningProgressWidget(undefined);
107
112
  widget.stopWidget();
@@ -126,6 +131,10 @@ export async function runInteractiveDebate(
126
131
  } catch {
127
132
  saved = false;
128
133
  }
134
+ // `sendUserMessage` routes to prompt() and ignores triggerTurn (only
135
+ // sendCustomMessage honors it), so this report does start a model turn.
136
+ // Genuinely append-only delivery needs pi.sendMessage({ customType, … },
137
+ // { triggerTurn: false }) plus a message renderer — a UX change, not made here.
129
138
  const appendOnly: SendUserMessageOptions = { triggerTurn: false };
130
139
  await pi.sendUserMessage(markdown, appendOnly);
131
140
  const active = outcomes.filter((item) => item.finalStatus === "active-at-close").length;