@underactive/pi-topping-moa-fusion 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +26 -7
- package/package.json +5 -5
- package/src/config/planName.ts +1 -1
- package/src/config/settings.ts +6 -1
- package/src/debate/debateContract.ts +2 -1
- package/src/debate/runDebate.ts +1 -0
- package/src/moa/modelRuntime.ts +28 -2
- package/src/moa/orchestration.ts +1 -0
- package/src/moa/planlessRetry.ts +3 -3
- package/src/moa/verdicts.ts +0 -5
- package/src/moa/verification.ts +13 -8
- package/src/moa/verificationCriteria.ts +2 -1
- package/src/opinion/runOpinion.ts +1 -0
- package/src/planning/planMode.ts +41 -24
- package/src/planning/tools/mfPlanSubagent.ts +3 -2
- package/src/runtime/piCapabilities.ts +72 -0
- package/src/runtime/runner.ts +32 -8
- package/src/ui/conflictOverlay.ts +2 -1
- package/src/ui/menu.ts +11 -5
- package/src/ui/moaProgressWidget.ts +49 -16
- package/src/ui/moaSetupOverlay.ts +14 -0
- package/src/ui/observeOverlay.ts +3 -2
- package/src/ui/verificationFindingsOverlay.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.4]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added the opt-in `minimalFanoutHeader` setting to hide the phase/model band and shimmer the active phase name in all progress tables.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Verified compatibility with pi 1.1.0: no pi export this extension imports was removed or changed, and typecheck and the full test suite pass against 1.1.0.
|
|
14
|
+
- Updated all four pi development dependencies from 1.0.3 to 1.1.0.
|
|
15
|
+
- README and related comments clarify planning, verification, and subagent tool behavior.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fan-out, synthesis, verification, debate, and implementation cost rows no longer overstate cost for tiered-price models (e.g. Claude Haiku 5.5). The displayed cost is now the sum of the per-response costs the child reports, each priced at its own tier. Registry rates over cumulative tokens are used only when the child reports no cost.
|
|
20
|
+
- On pi 1.0.4 and later, pi's built-in MCP `codemode`/`tool_search` tools stay available under a `--tools` allowlist, so a read-only child that loads all extensions (extension-registered providers) could reach write-capable MCP servers. Those children now run with `--no-mcp`. Support is probed on the pi executable the child spawns, not the locally installed pi package. Default and isolated children already exclude built-in MCP through `--no-extensions` and are unchanged. Failed or indeterminate version probes are not cached, so later children can retry the capability check.
|
|
21
|
+
- Cancelling an implementation while a tool is running no longer starts the verifier; the Implement row settles as cancelled. A provider error before any assistant reply still offers the retry. Hosts without an `agent_before_settle` boundary still dispatch verification for normal completed implementations. Known limit: on pi before 1.1, a cancel that arrives just as the implementation turn finishes can still start the verifier.
|
|
22
|
+
- `/mf-plan` proposers, synthesizer, and verifier, `/mf-opinion`, `/mf-debate`, and the `mf_plan_subagent` tool no longer fail to spawn a child for `litellm-auto/*` (or any extension-registered virtual model). Such models are registered via pi's `registerVirtualModel`, which keeps their provider out of `getRegisteredProviderIds()`, so the child was launched with a plain `--no-extensions`, stripped the autorouter extension, and died with `Model "litellm-auto/auto" not found`. Extension-registered virtual models are now detected by pi's virtual-model api id and load extensions in the child, matching native extension providers such as claude-bridge.
|
|
23
|
+
- Control characters cannot reach verifier or debate tasks, the observer transcript, or the verification findings popup. This prevents malformed child output from breaking a subprocess argument or controlling the terminal.
|
|
24
|
+
- Long newline-delimited child stdout events are no longer truncated before parsing.
|
|
25
|
+
- The minimal fan-out header keeps inactive phase headings dim.
|
|
26
|
+
- The selected conflict tab in the plan review overlay now uses a wider label with wrapping. Inactive tabs keep the short width on the same row.
|
|
27
|
+
|
|
5
28
|
## [0.4.3]
|
|
6
29
|
|
|
7
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Fanning out a coding task and merging the resulting token streams does not work
|
|
|
81
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.
|
|
82
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).
|
|
83
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.
|
|
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.
|
|
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. The verifier normally skips a user-cancelled implementation. On pi before 1.1, a cancel arriving as the turn settles can still start verification; on pi before 0.87, a mid-tool cancel may also be verified because the before-settle gate is unavailable.
|
|
85
85
|
|
|
86
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.
|
|
87
87
|
|
|
@@ -122,7 +122,9 @@ Rows are grouped under `── Plan`, `── Synthesize`, `── Implement`, a
|
|
|
122
122
|
- **CTX** — how much of that model's context window its latest turn is using.
|
|
123
123
|
- **MONITOR** — an activity meter of *generated output* rate, independent of CTX: an agent can be deep into its context window and idle, or near empty and generating hard. Uses provider-reported token counts when available, otherwise word counts of the streamed deltas. Each row's meter is tinted with the thinking level that row is running under, using that level's native theme colour (`off` through `max`), so you can read a row's effort at a glance; the hue is tracked per row, so the same model can show different colours in different slots and it follows retries, model swaps, verifier fallback, and resumed runs. A row whose level is unknown falls back to the neutral accent colour, and idle cells and settled traces keep their usual dimming under whichever hue applies.
|
|
124
124
|
- **ACTIVITY** — what the agent is doing, with its current tool call beneath, its tool name and argument highlighted with pi's own bold `toolTitle`/`accent` theme colours.
|
|
125
|
-
- **TURNS** / **TOOLS** / **COST** / **TIME** — assistant turns completed, tool calls started, registry
|
|
125
|
+
- **TURNS** / **TOOLS** / **COST** / **TIME** — assistant turns completed, tool calls started, model cost (the sum of the cost reported for each response; registry rates over the token totals only when no cost is reported), and elapsed time. All freeze when the agent settles.
|
|
126
|
+
|
|
127
|
+
With the [minimal fan-out header](#minimal-fan-out-header) setting on, the band is hidden and the active phase's heading (for example `── Plan`) shimmers instead; per-phase models then appear only in each row's MODEL cell.
|
|
126
128
|
|
|
127
129
|
While an agent is working, its row also shows a dim, guttered preview of the latest four wrapped lines from the same transcript used by Observe. The preview follows the newest output automatically; short terminals reduce or drop preview lines after preserving tool activity, and narrow terminals omit previews entirely.
|
|
128
130
|
|
|
@@ -248,7 +250,12 @@ Seven agent definitions ship with the extension. Two belong to the single-model
|
|
|
248
250
|
|
|
249
251
|
MoA never invokes `moa-explore` or `mf-plan` — each `moa-proposer` is a self-contained fusion of both roles, doing its own exploration so the proposals stay independent. The plan-only tools (`write_plan`, `exit_plan_mode`, and `mf_plan_subagent`) are active only while `/mf-plan` or single-model plan mode is on.
|
|
250
252
|
|
|
251
|
-
Every planning subprocess is forced through a runtime `read,grep,find,ls` allowlist, even when an installed agent definition is stale, customized, or omits its `tools` field.
|
|
253
|
+
Every planning subprocess is forced through a runtime `read,grep,find,ls` allowlist, even when an installed agent definition is stale, customized, or omits its `tools` field. Pi's built-in MCP is also kept out of every child: `--no-extensions` excludes it, and children that must load all extensions (extension-registered providers) run with `--no-mcp` on pi 1.0.4 and later, where MCP tools would otherwise stay available under the allowlist.
|
|
254
|
+
|
|
255
|
+
**Agentic provider bridges.** Some pi providers are not plain chat-completion APIs but bridges to full coding agents with their *own* local edit/shell tools (e.g. pi-cursor-bridge, whose models run Cursor agents in the repo cwd). Pi's tool allowlist cannot restrain those agent-side tools, so plan mode adds two more layers:
|
|
256
|
+
|
|
257
|
+
- **Read-only env handshake** — every planning subprocess is spawned with `PI_CURSOR_FORCE_MODE=plan`, and the parent session sets it while plan mode is active. cursor-bridge maps this to Cursor's native read-only *plan* mode (SDK `mode: "plan"`; CLI `--mode plan` without `--force`).
|
|
258
|
+
- **Mutation tripwire** — `git status --porcelain` is snapshotted before subagents launch and re-checked after every phase. If the working tree changed while planning agents ran, a warning lists the touched files so rogue edits are never silently absorbed.
|
|
252
259
|
|
|
253
260
|
### Choosing their models
|
|
254
261
|
|
|
@@ -266,6 +273,8 @@ thinking: off
|
|
|
266
273
|
|
|
267
274
|
`/mf-plan-settings` writes these two fields for you, but editing the file by hand works equally well — the overlay reads its current values back. An unrecognized `thinking:` value is ignored rather than passed to the subprocess.
|
|
268
275
|
|
|
276
|
+
**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.
|
|
277
|
+
|
|
269
278
|
#### Agent rosters
|
|
270
279
|
|
|
271
280
|
The MoA roles (proposers, synthesizer, implementer, verifier) are no longer assigned one-by-one in `/mf-plan-settings`. Instead the overlay's **agent rosters** row manages named rosters — reusable sets that assign a model + thinking level to every MoA role at once (2–5 proposers plus the three required roles; a roster must be complete to save). Each run, the picker's **Load Roster** row applies a roster to every slot wholesale — a 2-proposer roster also clears stale picks from slots 3–5 — and slots whose model is no longer callable are skipped with a warning while the rest load normally.
|
|
@@ -311,12 +320,17 @@ The **max verification repairs** row in `/mf-plan-settings` controls how many au
|
|
|
311
320
|
|
|
312
321
|
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.
|
|
313
322
|
|
|
314
|
-
|
|
323
|
+
#### Minimal fan-out header
|
|
315
324
|
|
|
316
|
-
**
|
|
325
|
+
The **minimal fan-out header** row in `/mf-plan-settings` hides the phase/model band and makes the active phase name shimmer. Press Space on the row to turn it on or off. The option applies to the plan, resumed-implementation, opinion, and debate tables. On truecolor themes, the sweep covers the phase name alone — the `── ` prefix and the trailing rule stay flat; other themes show a flat highlight on the name.
|
|
317
326
|
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
The setting persists in `~/.pi/agent/mf-plan/settings.json`:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{ "minimalFanoutHeader": false }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The default is **off**. A missing or non-boolean value also means off. Each row still shows its model in the MODEL cell. A resumed implementation table has no Plan or Synthesize rows, so it does not show those model names. The setting is read once when a run starts, so edits apply to the next run, not one already in progress.
|
|
320
334
|
|
|
321
335
|
### Parallel execution
|
|
322
336
|
|
|
@@ -356,6 +370,7 @@ src/
|
|
|
356
370
|
├── activityMeter.ts # Dependency-free output-rate meter
|
|
357
371
|
├── shared/
|
|
358
372
|
│ ├── modelRefs.ts # Model refs, thinking levels, display labels
|
|
373
|
+
│ ├── controlChars.ts # Control-character stripping for argv and display
|
|
359
374
|
│ └── functionKeys.ts # F-key press matching (incl. Kitty encoding)
|
|
360
375
|
├── config/
|
|
361
376
|
│ ├── settings.ts # Persistent MoA settings
|
|
@@ -373,6 +388,7 @@ src/
|
|
|
373
388
|
│ ├── activityTracking.ts # Streaming output/activity assembly
|
|
374
389
|
│ ├── results.ts # Runner result classification/output helpers
|
|
375
390
|
│ ├── cancelRun.ts # Per-agent cancellation bookkeeping
|
|
391
|
+
│ ├── piCapabilities.ts # Flag-support probes for the spawned pi executable
|
|
376
392
|
│ └── mutationTripwire.ts # Working-tree mutation detection
|
|
377
393
|
├── opinion/
|
|
378
394
|
│ ├── runOpinion.ts # Interactive opinion command orchestration
|
|
@@ -396,11 +412,13 @@ src/
|
|
|
396
412
|
│ ├── planFile.ts # Slugs, plan files, proposal staging
|
|
397
413
|
│ ├── instructions.ts # Injected 5-phase plan-mode instructions
|
|
398
414
|
│ ├── askUserQuestion.ts # rpiv questionnaire detection and blocked-state tracking
|
|
415
|
+
│ ├── contextEdits.ts # Plan-mode context omission and verification-pending helpers
|
|
399
416
|
│ └── tools/
|
|
400
417
|
│ ├── shared.ts
|
|
401
418
|
│ ├── enterPlanMode.ts # Interactive entry flow, ESC listener, agent tool
|
|
402
419
|
│ ├── writePlan.ts
|
|
403
420
|
│ ├── exitPlanMode.ts
|
|
421
|
+
│ ├── structuredResults.ts # Structured result schemas for plan-mode tools
|
|
404
422
|
│ └── mfPlanSubagent.ts
|
|
405
423
|
├── moa/
|
|
406
424
|
│ ├── orchestration.ts # Outer phase sequencer and run cleanup
|
|
@@ -418,6 +436,7 @@ src/
|
|
|
418
436
|
│ ├── conflictContract.ts # Conflict-surfacing task-text contract
|
|
419
437
|
│ ├── contextContract.ts # Context-subsection (auditable reasoning) contract
|
|
420
438
|
│ ├── planInfo.ts # Persisted MoA metadata validation
|
|
439
|
+
│ ├── panelDependence.ts # Proposer model-dependence and diversity summaries
|
|
421
440
|
│ ├── verdicts.ts
|
|
422
441
|
│ └── planlessRetry.ts
|
|
423
442
|
└── ui/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping-moa-fusion",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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": "1.0
|
|
54
|
-
"@earendil-works/pi-ai": "1.0
|
|
55
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
56
|
-
"@earendil-works/pi-tui": "1.0
|
|
53
|
+
"@earendil-works/pi-agent-core": "1.1.0",
|
|
54
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
56
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"typebox": "^1.3.3",
|
|
59
59
|
"typescript": "^5.8.0"
|
package/src/config/planName.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface PlanNameOptions {
|
|
|
22
22
|
signal?: AbortSignal;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/** Summarize a plan prompt into a 4-word slug with the cheap/fast model (the active model when none is set), or return a random phrase when summary names are off. */
|
|
26
26
|
export async function summarizePlanPromptName(ctx: ExtensionContext, prompt: string, options?: PlanNameOptions): Promise<string> {
|
|
27
27
|
const fallback = () => slugifyPlanName(prompt);
|
|
28
28
|
|
package/src/config/settings.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface MoaConfig {
|
|
|
73
73
|
autoResolveConflicts: boolean;
|
|
74
74
|
/** Use LLM-summarized plan names (true) or random adjective-adjective-noun phrases (false). */
|
|
75
75
|
useSummaryName: boolean;
|
|
76
|
+
/** Hide the phase/model band above the MoA table and shimmer the active phase name instead. Defaults to false. */
|
|
77
|
+
minimalFanoutHeader: boolean;
|
|
76
78
|
/**
|
|
77
79
|
* Whether the user has been through the setup overlay at least once. Gates the
|
|
78
80
|
* first `/mf-plan` into setup. Kept here rather than inferred from the agent
|
|
@@ -99,6 +101,7 @@ function emptyConfig(): MoaConfig {
|
|
|
99
101
|
rosters: [],
|
|
100
102
|
autoResolveConflicts: false,
|
|
101
103
|
useSummaryName: true,
|
|
104
|
+
minimalFanoutHeader: false,
|
|
102
105
|
agentDefaultsConfigured: false,
|
|
103
106
|
};
|
|
104
107
|
}
|
|
@@ -147,10 +150,12 @@ function parseSettingsFile(path: string): MoaConfig {
|
|
|
147
150
|
? parsed.useSummaryName
|
|
148
151
|
: true;
|
|
149
152
|
|
|
153
|
+
const minimalFanoutHeader = parsed.minimalFanoutHeader === true;
|
|
154
|
+
|
|
150
155
|
const agentDefaultsConfigured = parsed.agentDefaultsConfigured === true;
|
|
151
156
|
const rosters = parseRosters(parsed.rosters);
|
|
152
157
|
|
|
153
|
-
return { mode, proposers, opinionModels, debateModels, debateRounds, maxConcurrentAgents, maxVerificationRepairs, synthesizer, implementer, verifier, cheap, thinkingOverrides, rosters, autoResolveConflicts, useSummaryName, agentDefaultsConfigured };
|
|
158
|
+
return { mode, proposers, opinionModels, debateModels, debateRounds, maxConcurrentAgents, maxVerificationRepairs, synthesizer, implementer, verifier, cheap, thinkingOverrides, rosters, autoResolveConflicts, useSummaryName, minimalFanoutHeader, agentDefaultsConfigured };
|
|
154
159
|
} catch (err) {
|
|
155
160
|
console.error("mf-plan: failed to parse settings, using defaults:", err);
|
|
156
161
|
return emptyConfig();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildRetryCorrection } from "../moa/planlessRetry.ts";
|
|
2
|
+
import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
2
3
|
|
|
3
4
|
export const DEBATE_TASK_PREAMBLE =
|
|
4
5
|
"You are a read-only DEBATER in a multi-round debate between several models. Your output is shown verbatim to the user and reaches your peer debaters — there is no judge, and no agent will merge or verify anything — so argue to convince your peers. You may keep or change your stance in any round. Do not implement anything, edit files, or run commands, builds, or tests; read-only tools are expected and never a blocker. You are running headless: resolve open questions with stated assumptions rather than asking the user or waiting. The required output sections (including the `## Position` argument and the `## Stance` declaration with its `**Stance:**` value) are spelled out in the agent instructions.";
|
|
@@ -38,7 +39,7 @@ export function debaterLabel(index: number): string {
|
|
|
38
39
|
const MAX_PRIOR_CHARS = 16_000;
|
|
39
40
|
|
|
40
41
|
function quotePrior(text: string): string {
|
|
41
|
-
const trimmed = text.trim();
|
|
42
|
+
const trimmed = stripControlCharacters(text).trim();
|
|
42
43
|
const prior = trimmed.length > MAX_PRIOR_CHARS
|
|
43
44
|
? `${trimmed.slice(0, MAX_PRIOR_CHARS)}\n[prior position truncated]`
|
|
44
45
|
: trimmed;
|
package/src/debate/runDebate.ts
CHANGED
package/src/moa/modelRuntime.ts
CHANGED
|
@@ -8,8 +8,27 @@ import { loadMoaConfig, saveMoaConfig } from "../config/settings.ts";
|
|
|
8
8
|
import type { UsageStats } from "../runtime/results.ts";
|
|
9
9
|
import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* pi's internal `api` id for extension-registered virtual models (the autorouter's
|
|
13
|
+
* `litellm-auto/*`). pi does not export the constant, so mirror the value the
|
|
14
|
+
* autorouter extension pins in its own catalog.ts.
|
|
15
|
+
*/
|
|
16
|
+
const VIRTUAL_MODEL_API = "pi-virtual";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether routing this model in a child subprocess requires loading the extension that
|
|
20
|
+
* registered its provider. True for native extension providers (claude-bridge) — surfaced by
|
|
21
|
+
* getRegisteredProviderIds() — and for virtual models registered via registerVirtualModel
|
|
22
|
+
* (litellm-auto/*). A virtual model's provider never reaches getRegisteredProviderIds():
|
|
23
|
+
* registerVirtualModel keeps it in a separate virtual-model map, so a child spawned with a plain
|
|
24
|
+
* --no-extensions cannot resolve `litellm-auto/auto` and dies with "Model not found". External
|
|
25
|
+
* extensions can register virtual models too, so this keys off pi's virtual-model api id rather
|
|
26
|
+
* than a hardcoded provider name.
|
|
27
|
+
*/
|
|
11
28
|
export function modelUsesExtensionRegisteredProvider(ctx: ExtensionContext, ref: ModelRef): boolean {
|
|
12
|
-
|
|
29
|
+
if (ctx.modelRegistry.getRegisteredProviderIds().includes(ref.provider)) return true;
|
|
30
|
+
// Only an extension can register a virtual (api === "pi-virtual") model, so one mandates loadExtensions in the child.
|
|
31
|
+
return ctx.modelRegistry.find(ref.provider, ref.id)?.api === VIRTUAL_MODEL_API;
|
|
13
32
|
}
|
|
14
33
|
|
|
15
34
|
export function cursorBridgeExtensionPath(): string | undefined {
|
|
@@ -44,8 +63,15 @@ export function resolveContextWindow(ctx: ExtensionContext, ref: ModelRef): numb
|
|
|
44
63
|
return ctx.modelRegistry.find(ref.provider, ref.id)?.contextWindow;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Cost for a role's usage. Prefers the per-response costs the child reported
|
|
68
|
+
* (already summed in `usage.cost`), because each response is priced at its own
|
|
69
|
+
* tier. Falls back to the registry rates over cumulative token totals only
|
|
70
|
+
* when the child reported no cost; for tiered models that fallback prices the
|
|
71
|
+
* summed tokens as one request and can overstate the cost.
|
|
72
|
+
*/
|
|
48
73
|
export function resolveModelCost(ctx: ExtensionContext, ref: ModelRef, usage: UsageStats): number | undefined {
|
|
74
|
+
if (typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost > 0) return usage.cost;
|
|
49
75
|
const model = ctx.modelRegistry.find(ref.provider, ref.id);
|
|
50
76
|
if (!model) return undefined;
|
|
51
77
|
const cost = calculateCost(model, {
|
package/src/moa/orchestration.ts
CHANGED
|
@@ -31,6 +31,7 @@ export async function runMoaOrchestration(
|
|
|
31
31
|
session.closeOverlay?.();
|
|
32
32
|
runContext.observeSession?.closeOverlay?.();
|
|
33
33
|
},
|
|
34
|
+
minimalFanoutHeader: loadMoaConfig().minimalFanoutHeader,
|
|
34
35
|
}, host.getPlanRepoSlug());
|
|
35
36
|
host.setRunningProgressWidget(widget);
|
|
36
37
|
widget.setPhaseModels({
|
package/src/moa/planlessRetry.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
|
4
4
|
* Recovery for planning agents that exit cleanly without delivering a plan.
|
|
5
5
|
*
|
|
6
6
|
* Bridged agentic providers can override the moa-proposer/moa-synthesizer
|
|
7
|
-
* prompt with their own interactive harness, producing
|
|
7
|
+
* prompt with their own interactive harness, producing three planless shapes:
|
|
8
8
|
*
|
|
9
9
|
* - Question stops: Cursor's plan mode researches and then stops to ask the
|
|
10
10
|
* user a clarifying question before writing the plan. In a headless
|
|
@@ -18,8 +18,8 @@ import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
|
18
18
|
* prompt never sees the required output sections, explores interactively,
|
|
19
19
|
* and exits with questions or prose instead of a plan.
|
|
20
20
|
*
|
|
21
|
-
* These helpers detect
|
|
22
|
-
* once with an explicit instruction to answer its own questions / emit the
|
|
21
|
+
* These helpers detect all three shapes so the orchestrator can re-run the
|
|
22
|
+
* agent once with an explicit instruction to answer its own questions / emit the
|
|
23
23
|
* plan, and carry the read-only planning contract inside the task text —
|
|
24
24
|
* the only channel guaranteed to survive a bridge's prompt override.
|
|
25
25
|
*/
|
package/src/moa/verdicts.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { parseConflicts } from "./conflicts.ts";
|
|
15
|
-
import { buildRetryCorrection } from "./planlessRetry.ts";
|
|
16
15
|
|
|
17
16
|
const VERDICTS_SECTION_RE = /^##\s*Proposer Verdicts\s*$/m;
|
|
18
17
|
const VERDICT_BULLET_RE = /^[-*]\s+\*\*(Proposer \d+):?\*\*:?\s*(.+)$/gm;
|
|
@@ -75,7 +74,3 @@ export function missingVerdictSlots(verdictsMarkdown: string | null, labels: str
|
|
|
75
74
|
export function buildVerdictRetryHeader(missing: string[]): string {
|
|
76
75
|
return `IMPORTANT: Your previous output (quoted below) is rejected because it does not prove you evaluated every proposal: no verdict was found for ${missing.join(", ")}. Re-read those proposals from your input now and re-emit the COMPLETE plan including a \`## Proposer Verdicts\` section with one \`- **Proposer N:** adopted | partial | rejected — reason\` bullet for every proposal you received. Do not drop or alter the rest of the plan beyond what re-evaluating those proposals requires.`;
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
export function buildVerdictRetryTask(originalTask: string, previousOutput: string, missing: string[]): string {
|
|
80
|
-
return [originalTask, "", "---", "", buildRetryCorrection(buildVerdictRetryHeader(missing), previousOutput)].join("\n");
|
|
81
|
-
}
|
package/src/moa/verification.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { CancelRun, type CancelSession } from "../runtime/cancelRun.ts";
|
|
|
35
35
|
import { formatMutationWarning, MutationTripwire } from "../runtime/mutationTripwire.ts";
|
|
36
36
|
import { getFinalOutput, getResultOutput, isFailedResult, type SingleResult } from "../runtime/results.ts";
|
|
37
37
|
import { runSingleAgent } from "../runtime/runner.ts";
|
|
38
|
+
import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
38
39
|
import { modelRefLabel, TRIGGER_TURN } from "../shared/modelRefs.ts";
|
|
39
40
|
import { activityLoopCount } from "../ui/agentStatus.ts";
|
|
40
41
|
import { showVerificationFindings } from "../ui/verificationFindingsOverlay.ts";
|
|
@@ -170,26 +171,30 @@ export function buildVerifierTask(input: {
|
|
|
170
171
|
criteria?: VerificationCriterion[];
|
|
171
172
|
}): string {
|
|
172
173
|
const { plan, implementerReport, diff, scriptResults, criteria } = input;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
:
|
|
174
|
+
const safePlan = stripControlCharacters(plan);
|
|
175
|
+
const safeCriteria = criteria?.map((criterion) => ({
|
|
176
|
+
id: stripControlCharacters(criterion.id),
|
|
177
|
+
text: stripControlCharacters(criterion.text),
|
|
178
|
+
}));
|
|
179
|
+
const report = implementerReport ? stripControlCharacters(implementerReport).trim() : "";
|
|
180
|
+
const reportSection = report || "(The implementer produced no final summary message.)";
|
|
176
181
|
const diffSection = diff && diff.trim()
|
|
177
182
|
? diff.trim()
|
|
178
183
|
: "(No git diff available — not a git repository, or no changes detected.)";
|
|
179
|
-
const outputContract =
|
|
180
|
-
? `## Required output format\n\nEmit exactly these sections at the end of your reply:\n\n## Criteria Verdicts\n${
|
|
184
|
+
const outputContract = safeCriteria?.length
|
|
185
|
+
? `## Required output format\n\nEmit exactly these sections at the end of your reply:\n\n## Criteria Verdicts\n${safeCriteria.map((criterion) => `- **${criterion.id}:** pass | fail | cannot-verify — <evidence: file:line or what is missing>`).join("\n")}\n(one bullet per supplied criterion; no IDs skipped)\n\n## Deviations\n- <anything implemented differently> (or "None.")\n\n## Verification Result\n**Verdict:** complete | partial | incomplete | cannot-verify\n**Summary:** <one paragraph>\n\n### Gaps\n- <unmet criterion> — <evidence>\n\nUse complete only when every criterion is pass and every provided check passed. Prefer cannot-verify over a guessed pass.`
|
|
181
186
|
: VERIFIER_OUTPUT_CONTRACT;
|
|
182
187
|
const gateSection = scriptResults.length > 0
|
|
183
188
|
? scriptResults
|
|
184
|
-
.map((r) => `- ${r.command} → ${r.status.toUpperCase()} (exit ${r.exitCode})\n\`\`\`\n${r.relevantOutput}\n\`\`\``)
|
|
189
|
+
.map((r) => `- ${r.command} → ${r.status.toUpperCase()} (exit ${r.exitCode})\n\`\`\`\n${stripControlCharacters(r.relevantOutput)}\n\`\`\``)
|
|
185
190
|
.join("\n\n")
|
|
186
191
|
: "(No check/lint/test scripts were discovered in package.json.)";
|
|
187
192
|
|
|
188
193
|
return [
|
|
189
194
|
VERIFIER_TASK_PREAMBLE,
|
|
190
195
|
"---",
|
|
191
|
-
`## Approved plan (source of truth)\nVerify the working tree against every step of this plan:\n\n${
|
|
192
|
-
|
|
196
|
+
`## Approved plan (source of truth)\nVerify the working tree against every step of this plan:\n\n${safePlan}`,
|
|
197
|
+
safeCriteria?.length ? `## Verification criteria (authoritative pass/fail checklist)\n${safeCriteria.map((criterion) => `- **${criterion.id}:** ${criterion.text}`).join("\n")}\n\nWhen criteria are supplied, emit ## Criteria Verdicts with one \`- **C<n>:** pass | fail | cannot-verify — evidence\` bullet for every criterion and no skipped ids, then ## Deviations and the required ## Verification Result. Use complete only when every criterion is pass; use cannot-verify rather than guessing.` : "",
|
|
193
198
|
"---",
|
|
194
199
|
`## Implementer's self-report (UNTRUSTED CLAIM — not evidence)\n${reportSection}`,
|
|
195
200
|
"---",
|
|
@@ -4,6 +4,7 @@ import type { AgentConfig } from "../agents/discovery.ts";
|
|
|
4
4
|
import { CancelRun, type CancelSession } from "../runtime/cancelRun.ts";
|
|
5
5
|
import { getFinalOutput, isFailedResult } from "../runtime/results.ts";
|
|
6
6
|
import { runSingleAgent } from "../runtime/runner.ts";
|
|
7
|
+
import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
7
8
|
import { modelRefLabel, type ModelRef, type ThinkingLevel } from "../shared/modelRefs.ts";
|
|
8
9
|
import { activityLoopCount } from "../ui/agentStatus.ts";
|
|
9
10
|
import type { ObserveSession } from "../ui/observeOverlay.ts";
|
|
@@ -26,7 +27,7 @@ export function buildCriteriaTask(plan: string): string {
|
|
|
26
27
|
return [
|
|
27
28
|
CRITERIA_TASK_PREAMBLE,
|
|
28
29
|
"---",
|
|
29
|
-
`## Approved plan (frozen)\n${plan}`,
|
|
30
|
+
`## Approved plan (frozen)\n${stripControlCharacters(plan)}`,
|
|
30
31
|
"---",
|
|
31
32
|
`Your output must contain exactly one ${VERIFICATION_CRITERIA_HEADING} section and no prose outside it. Emit 3–${MAX_VERIFICATION_CRITERIA} bullets in this exact form:\n- **C<n>:** <observable binary condition> — <how to check: file, symbol, grep, expected state>\n\nEvery criterion must be checkable by reading the repository only; do not require execution. Cover at least one criterion per plan step in plan order, every named file or call site, and implied regression/scope constraints (such as preserving an existing export or avoiding unrelated files). Never reference proposer labels or model names.`,
|
|
32
33
|
].join("\n\n");
|
package/src/planning/planMode.ts
CHANGED
|
@@ -147,6 +147,10 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
147
147
|
let implementationContextTokens: number | undefined;
|
|
148
148
|
let implementationTurns = 0;
|
|
149
149
|
let implementationToolCalls = 0;
|
|
150
|
+
// Older pi hosts have no before-settle boundary; enforce its veto only after
|
|
151
|
+
// this host has exposed the boundary.
|
|
152
|
+
let settleBoundarySeen = false;
|
|
153
|
+
let verificationPermittedAtSettle = false;
|
|
150
154
|
let providerEnvBeforePlanMode: Record<string, string | undefined> | undefined;
|
|
151
155
|
const persistence = new PlanModeStatePersistence(pi);
|
|
152
156
|
// Tracks whether rpiv's ask_user_question (or whichever extension registers
|
|
@@ -181,6 +185,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
181
185
|
implementationContextTokens = undefined;
|
|
182
186
|
implementationTurns = 0;
|
|
183
187
|
implementationToolCalls = 0;
|
|
188
|
+
verificationPermittedAtSettle = false;
|
|
184
189
|
};
|
|
185
190
|
const resetImplementationState = (): void => {
|
|
186
191
|
implementationPending = false;
|
|
@@ -471,7 +476,9 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
471
476
|
// adopted widget, so this only fires when none exists; single-model
|
|
472
477
|
// exit_plan_mode passes no ctx and has no MoA info, so it is skipped.
|
|
473
478
|
if (!activeProgressWidget && ctx && activeRunMoaInfo && implementationHandoff?.model) {
|
|
474
|
-
const widget = new MoaProgressWidget(ctx, (ref) => resolveContextWindow(ctx, ref), {
|
|
479
|
+
const widget = new MoaProgressWidget(ctx, (ref) => resolveContextWindow(ctx, ref), {
|
|
480
|
+
minimalFanoutHeader: loadMoaConfig().minimalFanoutHeader,
|
|
481
|
+
}, planRepoSlug);
|
|
475
482
|
widget.setPhaseModels({
|
|
476
483
|
Plan: activeRunMoaInfo.proposers,
|
|
477
484
|
Synthesize: activeRunMoaInfo.synthesizer,
|
|
@@ -570,8 +577,8 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
570
577
|
// lets the meter reset rather than metering the correction as output.
|
|
571
578
|
const output = implementationOutputTracker?.snapshot();
|
|
572
579
|
if (output && activeProgressWidget) activeProgressWidget.updateRoleOutput("Implement", output.tokens, output.revision);
|
|
580
|
+
implementationUsage ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0, cost: 0, turns: 0, toolCalls: 0 };
|
|
573
581
|
if (msg.usage) {
|
|
574
|
-
implementationUsage ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0, cost: 0, turns: 0, toolCalls: 0 };
|
|
575
582
|
implementationUsage.input += nonnegative(msg.usage.input);
|
|
576
583
|
implementationUsage.output += nonnegative(msg.usage.output);
|
|
577
584
|
implementationUsage.cacheRead += nonnegative(msg.usage.cacheRead);
|
|
@@ -589,7 +596,7 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
589
596
|
implementationContextTokens,
|
|
590
597
|
implementationTurns,
|
|
591
598
|
implementationToolCalls,
|
|
592
|
-
resolveModelCost(ctx, implementationHandoff.model, implementationUsage
|
|
599
|
+
resolveModelCost(ctx, implementationHandoff.model, implementationUsage),
|
|
593
600
|
);
|
|
594
601
|
}
|
|
595
602
|
implementationPartial.clear();
|
|
@@ -601,7 +608,25 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
601
608
|
}
|
|
602
609
|
},
|
|
603
610
|
onAgentBeforeSettle: (event: AgentBeforeSettleEvent, ctx: ExtensionContext) => {
|
|
611
|
+
settleBoundarySeen = true;
|
|
612
|
+
verificationPermittedAtSettle = false;
|
|
604
613
|
try {
|
|
614
|
+
const stopReason = lastImplementationStopReason;
|
|
615
|
+
// KEEP IN LOCKSTEP with onAgentSettled's verification branch below.
|
|
616
|
+
// exit_plan_mode marks implementation pending before the follow-up has
|
|
617
|
+
// run, so implementationTurns distinguishes that planning boundary.
|
|
618
|
+
const verificationWillDispatch = implementationPending
|
|
619
|
+
&& implementationTurns > 0
|
|
620
|
+
&& event.context.pendingMessages.length === 0
|
|
621
|
+
&& event.outcome !== "aborted"
|
|
622
|
+
&& event.outcome !== "error"
|
|
623
|
+
&& !implementationTurnFailed(stopReason)
|
|
624
|
+
&& stopReason !== "aborted"
|
|
625
|
+
&& stopReason !== "toolUse"
|
|
626
|
+
&& implementationHandoff !== undefined
|
|
627
|
+
&& (implementationHandoff.verifier ?? loadMoaConfig().verifier) !== undefined;
|
|
628
|
+
verificationPermittedAtSettle = verificationWillDispatch;
|
|
629
|
+
|
|
605
630
|
const branch = readBranchEntries(ctx);
|
|
606
631
|
const persistedOmissions = new Set(
|
|
607
632
|
branch
|
|
@@ -625,19 +650,6 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
625
650
|
drafts.push(...collectOmissionDrafts(branch, isVerificationPendingEntry, omittedEntryIds));
|
|
626
651
|
}
|
|
627
652
|
|
|
628
|
-
const stopReason = lastImplementationStopReason;
|
|
629
|
-
// KEEP IN LOCKSTEP with onAgentSettled's verification branch below.
|
|
630
|
-
// exit_plan_mode marks implementation pending before the follow-up has
|
|
631
|
-
// run, so implementationTurns distinguishes that planning boundary.
|
|
632
|
-
const verificationWillDispatch = implementationPending
|
|
633
|
-
&& implementationTurns > 0
|
|
634
|
-
&& event.context.pendingMessages.length === 0
|
|
635
|
-
&& event.outcome !== "aborted"
|
|
636
|
-
&& event.outcome !== "error"
|
|
637
|
-
&& !implementationTurnFailed(stopReason)
|
|
638
|
-
&& stopReason !== "aborted"
|
|
639
|
-
&& implementationHandoff !== undefined
|
|
640
|
-
&& (implementationHandoff.verifier ?? loadMoaConfig().verifier) !== undefined;
|
|
641
653
|
if (verificationWillDispatch) {
|
|
642
654
|
drafts.push({
|
|
643
655
|
type: "custom_message",
|
|
@@ -657,10 +669,15 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
657
669
|
return undefined;
|
|
658
670
|
}
|
|
659
671
|
},
|
|
660
|
-
onAgentSettled: (
|
|
672
|
+
onAgentSettled: (event: AgentSettledEvent, ctx: ExtensionContext) => {
|
|
661
673
|
updateStatus(ctx);
|
|
674
|
+
const verificationPermitted = verificationPermittedAtSettle;
|
|
675
|
+
verificationPermittedAtSettle = false;
|
|
662
676
|
if (!implementationPending) return;
|
|
663
677
|
implementationPending = false;
|
|
678
|
+
// pi >= 1.1 reports a cancel that never produced an aborted assistant
|
|
679
|
+
// message (e.g. cancelled mid-tool); older pi omits the field.
|
|
680
|
+
const runAborted = event.aborted === true;
|
|
664
681
|
const stopReason = lastImplementationStopReason;
|
|
665
682
|
const report = lastImplementationReport;
|
|
666
683
|
lastImplementationStopReason = undefined;
|
|
@@ -669,9 +686,9 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
669
686
|
// Settle the Implement row before any retry/verify routing. Aborted
|
|
670
687
|
// runs stop the table outright (no verification follows).
|
|
671
688
|
if (activeProgressWidget) {
|
|
672
|
-
if (implementationTurnFailed(stopReason)) {
|
|
689
|
+
if (implementationTurnFailed(stopReason) && !runAborted) {
|
|
673
690
|
activeProgressWidget.settleRoleRow("Implement", "error");
|
|
674
|
-
} else if (stopReason === "aborted") {
|
|
691
|
+
} else if (stopReason === "aborted" || runAborted) {
|
|
675
692
|
activeProgressWidget.settleRoleRow("Implement", "cancelled");
|
|
676
693
|
stopActiveProgressWidget();
|
|
677
694
|
} else {
|
|
@@ -694,15 +711,15 @@ export function createPlanModeController(pi: ExtensionAPI) {
|
|
|
694
711
|
// ctx is stale (replaced/reloaded); nothing left to notify against.
|
|
695
712
|
}
|
|
696
713
|
};
|
|
697
|
-
if (implementationTurnFailed(stopReason) && implementationHandoff) {
|
|
714
|
+
if (implementationTurnFailed(stopReason) && implementationHandoff && !runAborted) {
|
|
698
715
|
void runImplementationRetryFlow(ctx, moaRunHost, implementationHandoff)
|
|
699
716
|
.catch(reportFlowFailure("Implementation retry"));
|
|
700
717
|
return;
|
|
701
718
|
}
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
if (
|
|
719
|
+
// Older hosts omit the settled aborted flag for mid-tool cancellation, leaving
|
|
720
|
+
// toolUse as the last stop reason.
|
|
721
|
+
if (stopReason === "aborted" || stopReason === "toolUse" || runAborted) return;
|
|
722
|
+
if (settleBoundarySeen && !verificationPermitted) return;
|
|
706
723
|
if (!implementationHandoff) return;
|
|
707
724
|
// KEEP IN LOCKSTEP with verificationWillDispatch in onAgentBeforeSettle.
|
|
708
725
|
const verifier = implementationHandoff.verifier ?? loadMoaConfig().verifier;
|
|
@@ -144,8 +144,9 @@ export function registerMfPlanSubagentTool(pi: ExtensionAPI, host: MfPlanSubagen
|
|
|
144
144
|
const overrideFor = (agentName: string) => (agentName === "mf-plan" ? planOverride : undefined);
|
|
145
145
|
|
|
146
146
|
// An agent whose effective model belongs to an extension-registered
|
|
147
|
-
// provider (claude-bridge, cursor-bridge)
|
|
148
|
-
// spawns with --no-extensions.
|
|
147
|
+
// provider (claude-bridge, cursor-bridge) or is an extension-registered virtual model
|
|
148
|
+
// (litellm-auto/*) cannot resolve in a child that spawns with --no-extensions.
|
|
149
|
+
// Same opt-in the MoA fan-out already does;
|
|
149
150
|
// built-in and bare ids fall through to no extension loading.
|
|
150
151
|
const agentExtensionOptions = (agentName: string): { loadExtensions?: boolean; extensionPath?: string } => {
|
|
151
152
|
const model = overrideFor(agentName)?.model ?? agents.find((agent) => agent.name === agentName)?.model;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability probing for the pi executable a child process will actually run.
|
|
3
|
+
* The local `@earendil-works/pi-coding-agent` VERSION export describes the
|
|
4
|
+
* package this extension was built against, not the host (or `pi` on PATH)
|
|
5
|
+
* that `getPiInvocation()` spawns, so flag support is probed on the executable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export interface PiInvocation {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `--no-mcp` first ships in pi 1.0.4; 1.0.3 rejects it as an unknown option. */
|
|
16
|
+
const NO_MCP_MIN_VERSION = [1, 0, 4] as const;
|
|
17
|
+
const VERSION_PROBE_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
19
|
+
export function parsePiVersion(output: string): [number, number, number] | undefined {
|
|
20
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
|
|
21
|
+
if (!match) return undefined;
|
|
22
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function versionAtLeast(version: readonly number[], minimum: readonly number[]): boolean {
|
|
26
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
27
|
+
const diff = (version[i] ?? 0) - minimum[i];
|
|
28
|
+
if (diff !== 0) return diff > 0;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function versionSupportsNoMcp(output: string): boolean {
|
|
34
|
+
const version = parsePiVersion(output);
|
|
35
|
+
return version !== undefined && versionAtLeast(version, NO_MCP_MIN_VERSION);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const noMcpSupport = new Map<string, Promise<boolean>>();
|
|
39
|
+
|
|
40
|
+
function probeVersion(invocation: PiInvocation): Promise<string> {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
try {
|
|
43
|
+
execFile(
|
|
44
|
+
invocation.command,
|
|
45
|
+
[...invocation.args, "--version"],
|
|
46
|
+
{ timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true, encoding: "utf-8" },
|
|
47
|
+
(error, stdout, stderr) => resolve(error ? "" : `${stdout}\n${stderr}`),
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
resolve("");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the executable behind `invocation` accepts `--no-mcp`. An executable
|
|
57
|
+
* whose version cannot be determined is treated as unsupported, because an
|
|
58
|
+
* unknown flag would make the child exit instead of run. Only determined
|
|
59
|
+
* results are cached per executable for the life of the process.
|
|
60
|
+
*/
|
|
61
|
+
export function piSupportsNoMcp(invocation: PiInvocation): Promise<boolean> {
|
|
62
|
+
const key = JSON.stringify([invocation.command, invocation.args]);
|
|
63
|
+
let cached = noMcpSupport.get(key);
|
|
64
|
+
if (!cached) {
|
|
65
|
+
cached = probeVersion(invocation).then((output) => {
|
|
66
|
+
if (parsePiVersion(output) === undefined) noMcpSupport.delete(key);
|
|
67
|
+
return versionSupportsNoMcp(output);
|
|
68
|
+
});
|
|
69
|
+
noMcpSupport.set(key, cached);
|
|
70
|
+
}
|
|
71
|
+
return cached;
|
|
72
|
+
}
|
package/src/runtime/runner.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { AgentConfig } from "../agents/discovery.ts";
|
|
|
14
14
|
import { normalizeMaxConcurrentAgents } from "../config/settings.ts";
|
|
15
15
|
import type { ThinkingLevel } from "../shared/modelRefs.ts";
|
|
16
16
|
import { OutputActivityTracker, PartialAssistantAssembler, formatToolActivity } from "./activityTracking.ts";
|
|
17
|
+
import { piSupportsNoMcp, type PiInvocation } from "./piCapabilities.ts";
|
|
17
18
|
import { escalateKill, mapWithConcurrencyLimit, trackedProcesses } from "./processPool.ts";
|
|
18
19
|
import { getFinalOutput, type SingleResult } from "./results.ts";
|
|
19
20
|
import {
|
|
@@ -70,7 +71,7 @@ export function getReadOnlyAgentTools(configuredTools?: string[]): string[] {
|
|
|
70
71
|
return configuredTools.filter((tool) => READ_ONLY_AGENT_TOOL_SET.has(tool));
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function getPiInvocation(args: string[]):
|
|
74
|
+
function getPiInvocation(args: string[]): PiInvocation {
|
|
74
75
|
const currentScript = process.argv[1];
|
|
75
76
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
76
77
|
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
@@ -90,7 +91,8 @@ export interface RunSingleAgentOptions {
|
|
|
90
91
|
/**
|
|
91
92
|
* Load all configured pi extensions in the child process. Retained as a
|
|
92
93
|
* fallback for extension-registered providers whose entry point cannot be
|
|
93
|
-
* isolated explicitly.
|
|
94
|
+
* isolated explicitly. Such a child also gets --no-mcp when the spawned pi
|
|
95
|
+
* supports it, because loading all extensions keeps pi's built-in MCP.
|
|
94
96
|
*/
|
|
95
97
|
loadExtensions?: boolean;
|
|
96
98
|
/**
|
|
@@ -118,6 +120,11 @@ export interface RunSingleAgentOptions {
|
|
|
118
120
|
resolveOnAbort?: boolean;
|
|
119
121
|
/** Abort grace period; exposed for focused process-boundary tests. */
|
|
120
122
|
killTimeoutMs?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Replaces the discovered pi executable (the child args are appended).
|
|
125
|
+
* Exposed for focused process-boundary tests.
|
|
126
|
+
*/
|
|
127
|
+
piInvocation?: PiInvocation;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
export async function runSingleAgent(
|
|
@@ -184,8 +191,7 @@ export async function runSingleAgent(
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// argv entries are C strings: a NUL makes spawn throw before execve.
|
|
187
|
-
//
|
|
188
|
-
// argv, which would also remove the E2BIG class of failures.
|
|
194
|
+
// Passing the task via a temp file or stdin instead of argv would remove this failure and the E2BIG class with it.
|
|
189
195
|
if (task.includes("\0")) {
|
|
190
196
|
const message = "Task contains a NUL byte and cannot be passed as a process argument";
|
|
191
197
|
currentResult.exitCode = 1;
|
|
@@ -217,11 +223,24 @@ export async function runSingleAgent(
|
|
|
217
223
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
218
224
|
}
|
|
219
225
|
|
|
226
|
+
// An all-extension child keeps pi's built-in MCP, whose codemode and
|
|
227
|
+
// tool_search tools can reach write-capable MCP servers regardless of the
|
|
228
|
+
// read-only --tools allowlist (pi >= 1.0.4 keeps MCP tools registered under
|
|
229
|
+
// a plain allowlist). Disable it, but only when the executable this child
|
|
230
|
+
// will run knows the flag. The default and isolated paths already pass
|
|
231
|
+
// --no-extensions, which excludes built-in MCP.
|
|
232
|
+
if (options.loadExtensions && !options.extensionPath) {
|
|
233
|
+
const probe = options.piInvocation ?? getPiInvocation([]);
|
|
234
|
+
if (await piSupportsNoMcp(probe)) args.push("--no-mcp");
|
|
235
|
+
}
|
|
236
|
+
|
|
220
237
|
args.push(`Task: ${task}`);
|
|
221
238
|
let wasAborted = false;
|
|
222
239
|
|
|
223
240
|
const exitCode = await new Promise<number>((resolve) => {
|
|
224
|
-
const invocation =
|
|
241
|
+
const invocation = options.piInvocation
|
|
242
|
+
? { command: options.piInvocation.command, args: [...options.piInvocation.args, ...args] }
|
|
243
|
+
: getPiInvocation(args);
|
|
225
244
|
|
|
226
245
|
const appendDiagnostics = (diagnostics: string) => {
|
|
227
246
|
currentResult.stderr = (currentResult.stderr + diagnostics).slice(-MAX_DIAGNOSTIC_CHARS);
|
|
@@ -371,9 +390,14 @@ export async function runSingleAgent(
|
|
|
371
390
|
|
|
372
391
|
proc.stdout.on("data", (data) => {
|
|
373
392
|
resetIdleTimer();
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
393
|
+
const chunk = data.toString();
|
|
394
|
+
const newline = chunk.lastIndexOf("\n");
|
|
395
|
+
if (newline === -1) {
|
|
396
|
+
buffer += chunk;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const lines = (buffer + chunk.slice(0, newline)).split("\n");
|
|
400
|
+
buffer = chunk.slice(newline + 1);
|
|
377
401
|
for (const line of lines) processLine(line);
|
|
378
402
|
});
|
|
379
403
|
|
|
@@ -257,7 +257,8 @@ class ConflictReviewComponent implements Component {
|
|
|
257
257
|
const isAnswered = this.answers.has(conflict.id);
|
|
258
258
|
const box = isAnswered ? "■" : "□";
|
|
259
259
|
const color = isAnswered ? "success" : "muted";
|
|
260
|
-
const
|
|
260
|
+
const maxLen = isActive ? 30 : maxTabLabelLen;
|
|
261
|
+
const lbl = truncateToWidth(conflict.label, maxLen, "…", false);
|
|
261
262
|
const text = ` ${box} ${lbl} `;
|
|
262
263
|
tabParts.push(isActive ? th.bg("selectedBg", th.fg("text", text)) : th.fg(color, text));
|
|
263
264
|
}
|
package/src/ui/menu.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface ToggleMenuItem {
|
|
|
16
16
|
id: string;
|
|
17
17
|
label: string;
|
|
18
18
|
value: boolean;
|
|
19
|
+
/** Dimmed helper text wrapped under the row. */
|
|
20
|
+
description?: string;
|
|
19
21
|
onChange?: (value: boolean) => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -386,21 +388,24 @@ export class MenuComponent implements Component {
|
|
|
386
388
|
}
|
|
387
389
|
let flatIndex = 0;
|
|
388
390
|
const itemLineIndices: number[] = [];
|
|
391
|
+
const itemBlockEndLineIndices: number[] = [];
|
|
389
392
|
for (const section of this.sections) {
|
|
390
393
|
lines.push(renderMenuSectionDivider(theme, innerWidth, section.title));
|
|
391
394
|
const [from, to] = this.itemWindow(section, flatIndex);
|
|
392
395
|
if (from > 0) lines.push(renderMenuContentRow(theme, innerWidth, theme.fg("dim", ` ↑ ${from} more`)));
|
|
393
396
|
for (const [index, item] of section.items.entries()) {
|
|
394
397
|
// The cursor indexes every item, so rows hidden by the window still advance it.
|
|
395
|
-
const
|
|
398
|
+
const itemIndex = flatIndex++;
|
|
399
|
+
const selected = itemIndex === this.cursor;
|
|
396
400
|
if (index < from || index >= to) continue;
|
|
397
|
-
itemLineIndices[
|
|
401
|
+
itemLineIndices[itemIndex] = lines.length;
|
|
398
402
|
lines.push(this.renderItemRow(item, selected, innerWidth));
|
|
399
403
|
const descriptionRows = this.renderDescriptionRows(item, innerWidth);
|
|
400
404
|
lines.push(...descriptionRows);
|
|
401
405
|
if (descriptionRows.length > 0 && index < to - 1) {
|
|
402
406
|
lines.push(renderMenuContentRow(theme, innerWidth, ""));
|
|
403
407
|
}
|
|
408
|
+
itemBlockEndLineIndices[itemIndex] = lines.length - 1;
|
|
404
409
|
}
|
|
405
410
|
const below = section.items.length - to;
|
|
406
411
|
if (below > 0) lines.push(renderMenuContentRow(theme, innerWidth, theme.fg("dim", ` ↓ ${below} more`)));
|
|
@@ -429,8 +434,9 @@ export class MenuComponent implements Component {
|
|
|
429
434
|
const middle = lines.slice(1, footerStart);
|
|
430
435
|
const middleHeight = Math.max(0, this.viewport - top.length - footer.length);
|
|
431
436
|
const selectedMiddleLine = Math.max(0, (itemLineIndices[this.cursor] ?? 1) - 1);
|
|
437
|
+
const selectedBlockEndLine = Math.max(0, (itemBlockEndLineIndices[this.cursor] ?? itemLineIndices[this.cursor] ?? 1) - 1);
|
|
432
438
|
if (selectedMiddleLine < this.scrollOffset) this.scrollOffset = selectedMiddleLine;
|
|
433
|
-
if (
|
|
439
|
+
if (selectedBlockEndLine >= this.scrollOffset + middleHeight) this.scrollOffset = selectedBlockEndLine - middleHeight + 1;
|
|
434
440
|
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, Math.max(0, middle.length - middleHeight)));
|
|
435
441
|
visibleLines = [...top, ...middle.slice(this.scrollOffset, this.scrollOffset + middleHeight), ...footer];
|
|
436
442
|
}
|
|
@@ -448,9 +454,9 @@ export class MenuComponent implements Component {
|
|
|
448
454
|
return [from, from + cap];
|
|
449
455
|
}
|
|
450
456
|
|
|
451
|
-
/** Dimmed helper text wrapped under an action/choice row. */
|
|
457
|
+
/** Dimmed helper text wrapped under an action/choice/toggle row. */
|
|
452
458
|
private renderDescriptionRows(item: MenuItem, innerWidth: number): string[] {
|
|
453
|
-
if (
|
|
459
|
+
if (!item.description) return [];
|
|
454
460
|
const indent = " ";
|
|
455
461
|
return wrapWords(item.description, Math.max(0, innerWidth - indent.length - 2)).map((line) =>
|
|
456
462
|
renderMenuContentRow(this.theme, innerWidth, this.theme.fg("dim", indent + line)),
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* usage and a pi-topping style generated-output activity meter per agent.
|
|
8
8
|
* Rows are grouped under `── Plan / Synthesize / Implement / Verify` headings,
|
|
9
9
|
* with a shimmering four-phase chevron band above the header tracing the
|
|
10
|
-
* active phase.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* active phase. In minimal mode, the band is hidden and only the active phase
|
|
11
|
+
* name shimmers — its rule and prefix stay flat.
|
|
12
|
+
*
|
|
13
|
+
* The table stays mounted across the whole run — orchestration hands it to the
|
|
14
|
+
* plan-mode controller on approval, which keeps it alive through in-session
|
|
15
|
+
* implementation and verification.
|
|
13
16
|
*
|
|
14
17
|
* Each row's MONITOR activity meter is tinted with the thinking level that row
|
|
15
18
|
* runs under, using that level's native theme color (`thinkingOff` through
|
|
@@ -76,7 +79,8 @@ export interface ProposerStatus {
|
|
|
76
79
|
turns?: number;
|
|
77
80
|
/** Tool calls started so far. */
|
|
78
81
|
toolCalls?: number;
|
|
79
|
-
/** Cumulative model cost in USD
|
|
82
|
+
/** Cumulative model cost in USD from resolveModelCost: reported per-response
|
|
83
|
+
* costs, or registry rates when none are reported. */
|
|
80
84
|
costUsd?: number;
|
|
81
85
|
startedAt?: number;
|
|
82
86
|
/** Set once the agent settles, freezing its elapsed reading. */
|
|
@@ -209,6 +213,8 @@ export interface MoaProgressView {
|
|
|
209
213
|
readonly planName: string | undefined;
|
|
210
214
|
/** Whether live transcript previews are currently shown beneath active rows. */
|
|
211
215
|
readonly previewVisible: boolean;
|
|
216
|
+
/** Whether the phase/model band is hidden and the active phase name shimmers. */
|
|
217
|
+
readonly minimalFanoutHeader: boolean;
|
|
212
218
|
/** Display label for a phase without changing its internal identity. */
|
|
213
219
|
phaseLabel(phase: MoaPhase): string;
|
|
214
220
|
/** Model assigned to each phase, for the band above the table header. */
|
|
@@ -229,6 +235,8 @@ export interface MoaProgressCallbacks {
|
|
|
229
235
|
title?: string;
|
|
230
236
|
phaseLabels?: Partial<Record<MoaPhase, string>>;
|
|
231
237
|
fanoutWorkingText?: string;
|
|
238
|
+
/** Hide the phase/model band and shimmer the active phase heading instead. Read once at construction. */
|
|
239
|
+
minimalFanoutHeader?: boolean;
|
|
232
240
|
}
|
|
233
241
|
|
|
234
242
|
interface RowMeter {
|
|
@@ -252,6 +260,7 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
252
260
|
private readonly callbacks: MoaProgressCallbacks;
|
|
253
261
|
readonly title: string;
|
|
254
262
|
readonly planName: string | undefined;
|
|
263
|
+
readonly minimalFanoutHeader: boolean;
|
|
255
264
|
|
|
256
265
|
private statuses: ProposerStatus[] = [];
|
|
257
266
|
private roleRows = new Map<MoaPhase, RoleRow>();
|
|
@@ -278,6 +287,7 @@ export class MoaProgressWidget implements MoaProgressView {
|
|
|
278
287
|
this.callbacks = callbacks;
|
|
279
288
|
this.title = callbacks.title ?? TABLE_TITLE;
|
|
280
289
|
this.planName = planName;
|
|
290
|
+
this.minimalFanoutHeader = callbacks.minimalFanoutHeader ?? false;
|
|
281
291
|
}
|
|
282
292
|
|
|
283
293
|
private get ui(): ExtensionContext["ui"] {
|
|
@@ -783,6 +793,32 @@ export class MoaProgressTableComponent implements Component {
|
|
|
783
793
|
return shortModelName(models);
|
|
784
794
|
}
|
|
785
795
|
|
|
796
|
+
/** Active-phase tone shared by the band and, in minimal mode, the table's phase headings. */
|
|
797
|
+
private paintPhase(lit: boolean, text: string, now: number): string {
|
|
798
|
+
const th = this.theme;
|
|
799
|
+
if (!lit) return th.fg("dim", text);
|
|
800
|
+
return th.getFgAnsi
|
|
801
|
+
? shimmerString(text, now - this.animationStartedAt, th as ShimmerTheme, "ltr", "normal", true)
|
|
802
|
+
: th.fg("text", text);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Minimal-mode heading with the sweep on the phase name alone: the `── `
|
|
807
|
+
* prefix and the trailing rule stay flat, so the shimmer marks the name
|
|
808
|
+
* rather than washing the whole line. The name is cut to the room left by
|
|
809
|
+
* the prefix and rule, so the line still fills the table body exactly.
|
|
810
|
+
*/
|
|
811
|
+
private phaseNameShimmer(label: string, bodyWidth: number, now: number): string {
|
|
812
|
+
const prefix = "── ";
|
|
813
|
+
const suffix = " ";
|
|
814
|
+
const name = fitVisible(label, Math.max(1, bodyWidth - visibleWidth(prefix) - visibleWidth(suffix)), {
|
|
815
|
+
truncationMark: "…",
|
|
816
|
+
padToWidth: false,
|
|
817
|
+
});
|
|
818
|
+
const fill = "─".repeat(Math.max(0, bodyWidth - visibleWidth(prefix) - visibleWidth(name) - visibleWidth(suffix)));
|
|
819
|
+
return `${this.theme.fg("dim", prefix)}${this.paintPhase(true, name, now)}${this.theme.fg("dim", suffix + fill)}`;
|
|
820
|
+
}
|
|
821
|
+
|
|
786
822
|
/**
|
|
787
823
|
* Two centered rows — phase names, then their assigned model ids — shown
|
|
788
824
|
* above the table header, with a two-row powerline chevron standing between
|
|
@@ -798,14 +834,7 @@ export class MoaProgressTableComponent implements Component {
|
|
|
798
834
|
const colWidth = Math.floor((bodyWidth - sepCount) / MOA_PHASES.length);
|
|
799
835
|
if (colWidth < PHASE_BAND_MIN_COL) return [];
|
|
800
836
|
|
|
801
|
-
const th = this.theme;
|
|
802
837
|
const active = this.view.activePhase();
|
|
803
|
-
const paint = (lit: boolean, text: string): string => {
|
|
804
|
-
if (!lit) return th.fg("dim", text);
|
|
805
|
-
return th.getFgAnsi
|
|
806
|
-
? shimmerString(text, now - this.animationStartedAt, th as ShimmerTheme, "ltr", "normal", true)
|
|
807
|
-
: th.fg("text", text);
|
|
808
|
-
};
|
|
809
838
|
// Each boundary carries a two-row powerline chevron: the "\" half on the name
|
|
810
839
|
// row stacks over the "/" half on the model row. Both rows share one column
|
|
811
840
|
// layout, so the halves land in the same terminal column and read as a
|
|
@@ -814,9 +843,9 @@ export class MoaProgressTableComponent implements Component {
|
|
|
814
843
|
const bandRow = (sep: string, textFor: (phase: MoaPhase) => string): string => {
|
|
815
844
|
const parts: string[] = [];
|
|
816
845
|
MOA_PHASES.forEach((phase, i) => {
|
|
817
|
-
parts.push(
|
|
846
|
+
parts.push(this.paintPhase(phase === active, cell(textFor(phase), colWidth, "center"), now));
|
|
818
847
|
const next = MOA_PHASES[i + 1];
|
|
819
|
-
if (next !== undefined) parts.push(
|
|
848
|
+
if (next !== undefined) parts.push(this.paintPhase(phase === active || next === active, sep, now));
|
|
820
849
|
});
|
|
821
850
|
return parts.join("");
|
|
822
851
|
};
|
|
@@ -832,7 +861,7 @@ export class MoaProgressTableComponent implements Component {
|
|
|
832
861
|
const border = (s: string) => th.fg("border", s);
|
|
833
862
|
const row = (s: string) => ` ${fitVisible(s, bodyWidth, { truncationMark: "…", padToWidth: true })} `;
|
|
834
863
|
const now = Date.now();
|
|
835
|
-
const band = this.phaseModelBand(bodyWidth, now);
|
|
864
|
+
const band = this.view.minimalFanoutHeader ? [] : this.phaseModelBand(bodyWidth, now);
|
|
836
865
|
const rows = this.view.progressRows();
|
|
837
866
|
const cols = tableColumns(bodyWidth, rows.map((r) => r.label));
|
|
838
867
|
// Transcript previews stop after ACTIVITY, leaving the right-aligned stats
|
|
@@ -926,14 +955,18 @@ export class MoaProgressTableComponent implements Component {
|
|
|
926
955
|
}
|
|
927
956
|
}
|
|
928
957
|
let renderedPhase = false;
|
|
958
|
+
const activePhase = this.view.activePhase();
|
|
929
959
|
|
|
930
960
|
for (let i = 0; i < rows.length; i++) {
|
|
931
961
|
const r = rows[i];
|
|
932
962
|
const active = isActive(r.state);
|
|
933
963
|
if (r.firstOfPhase) {
|
|
934
964
|
if (renderedPhase) lines.push(row(""));
|
|
935
|
-
const
|
|
936
|
-
|
|
965
|
+
const label = this.view.phaseLabel(r.phase);
|
|
966
|
+
const heading = `── ${label} `;
|
|
967
|
+
const rule = heading + "─".repeat(Math.max(0, bodyWidth - visibleWidth(heading)));
|
|
968
|
+
const lit = this.view.minimalFanoutHeader && r.phase === activePhase;
|
|
969
|
+
lines.push(row(lit ? this.phaseNameShimmer(label, bodyWidth, now) : dim(rule)));
|
|
937
970
|
renderedPhase = true;
|
|
938
971
|
}
|
|
939
972
|
const icon =
|
|
@@ -79,6 +79,7 @@ interface SetupDraft {
|
|
|
79
79
|
useSummaryName: boolean;
|
|
80
80
|
maxConcurrentAgents: number;
|
|
81
81
|
maxVerificationRepairs: number;
|
|
82
|
+
minimalFanoutHeader: boolean;
|
|
82
83
|
rosters: PlanRoster[];
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -94,6 +95,7 @@ class MoaSetupComponent implements Component {
|
|
|
94
95
|
private useSummaryName: boolean;
|
|
95
96
|
private maxConcurrentAgents: number;
|
|
96
97
|
private maxVerificationRepairs: number;
|
|
98
|
+
private minimalFanoutHeader: boolean;
|
|
97
99
|
|
|
98
100
|
constructor(
|
|
99
101
|
private readonly tui: TUI,
|
|
@@ -111,6 +113,7 @@ class MoaSetupComponent implements Component {
|
|
|
111
113
|
this.useSummaryName = draft.useSummaryName;
|
|
112
114
|
this.maxConcurrentAgents = draft.maxConcurrentAgents;
|
|
113
115
|
this.maxVerificationRepairs = draft.maxVerificationRepairs;
|
|
116
|
+
this.minimalFanoutHeader = draft.minimalFanoutHeader;
|
|
114
117
|
const slotItem = (slotIndex: number): MenuItem => ({
|
|
115
118
|
id: `role-${slotIndex}`,
|
|
116
119
|
label: SLOTS[slotIndex]!.menuLabel,
|
|
@@ -158,6 +161,13 @@ class MoaSetupComponent implements Component {
|
|
|
158
161
|
description: "Verifier-driven repair rounds after implementation (0 disables automatic repairs).",
|
|
159
162
|
onChange: (valueIndex: number) => { this.maxVerificationRepairs = MIN_VERIFICATION_REPAIRS + valueIndex; },
|
|
160
163
|
},
|
|
164
|
+
{
|
|
165
|
+
id: "minimal-fanout-header",
|
|
166
|
+
label: "minimal fan-out header",
|
|
167
|
+
value: this.minimalFanoutHeader,
|
|
168
|
+
description: "Hide the phase band and shimmer the active phase heading instead. Applies to the next run.",
|
|
169
|
+
onChange: (value: boolean) => { this.minimalFanoutHeader = value; },
|
|
170
|
+
},
|
|
161
171
|
] },
|
|
162
172
|
],
|
|
163
173
|
buttons: [
|
|
@@ -198,6 +208,7 @@ class MoaSetupComponent implements Component {
|
|
|
198
208
|
useSummaryName: this.useSummaryName,
|
|
199
209
|
maxConcurrentAgents: this.maxConcurrentAgents,
|
|
200
210
|
maxVerificationRepairs: this.maxVerificationRepairs,
|
|
211
|
+
minimalFanoutHeader: this.minimalFanoutHeader,
|
|
201
212
|
rosters: [...this.draft.rosters],
|
|
202
213
|
};
|
|
203
214
|
}
|
|
@@ -331,6 +342,7 @@ export async function showMoaSetup(
|
|
|
331
342
|
useSummaryName: saved.useSummaryName,
|
|
332
343
|
maxConcurrentAgents: saved.maxConcurrentAgents,
|
|
333
344
|
maxVerificationRepairs: saved.maxVerificationRepairs,
|
|
345
|
+
minimalFanoutHeader: saved.minimalFanoutHeader,
|
|
334
346
|
rosters: [...saved.rosters],
|
|
335
347
|
};
|
|
336
348
|
|
|
@@ -361,6 +373,7 @@ export async function showMoaSetup(
|
|
|
361
373
|
draft.useSummaryName = result.draft.useSummaryName;
|
|
362
374
|
draft.maxConcurrentAgents = result.draft.maxConcurrentAgents;
|
|
363
375
|
draft.maxVerificationRepairs = result.draft.maxVerificationRepairs;
|
|
376
|
+
draft.minimalFanoutHeader = result.draft.minimalFanoutHeader;
|
|
364
377
|
draft.rosters = await showRosterManager(ctx, draft.rosters, {
|
|
365
378
|
currentThinking,
|
|
366
379
|
onChange: (rosters) => {
|
|
@@ -404,6 +417,7 @@ export async function showMoaSetup(
|
|
|
404
417
|
useSummaryName: finalDraft.useSummaryName,
|
|
405
418
|
maxConcurrentAgents: finalDraft.maxConcurrentAgents,
|
|
406
419
|
maxVerificationRepairs: finalDraft.maxVerificationRepairs,
|
|
420
|
+
minimalFanoutHeader: finalDraft.minimalFanoutHeader,
|
|
407
421
|
cheap: finalDraft.selections[AGENT_SLOT_COUNT],
|
|
408
422
|
rosters: finalDraft.rosters,
|
|
409
423
|
thinkingOverrides: { ...current.thinkingOverrides, ...finalDraft.thinkingOverrides },
|
package/src/ui/observeOverlay.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
2
|
import { getMarkdownTheme, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Key, Markdown, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Key, Markdown, matchesKey, stripTerminalSequences, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
4
5
|
import { transcriptBody, truncateFromStart } from "./agentTranscript.ts";
|
|
5
6
|
import { ROUNDED_SINGLE_BOX, UI_TICK_MS, createFrame, ratioViewport, safeRenderWidth } from "./chrome.ts";
|
|
6
7
|
|
|
@@ -32,7 +33,7 @@ export function formatAgentMarkdown(agent: ObserveAgentData): string {
|
|
|
32
33
|
const task = truncateToWidth(agent.task.replace(/\s+/g, " ").trim(), MAX_TASK_WIDTH, "…", false);
|
|
33
34
|
const header = [`## ${agent.label}`, `_${agent.model} · ${agent.state}_`, "", `**Task:** ${task}`, ""].join("\n");
|
|
34
35
|
const contentBudget = Math.max(0, MAX_RENDERED_CHARS - header.length - 1);
|
|
35
|
-
const body = transcriptBody(agent.messages, agent.partial, agent.state, contentBudget);
|
|
36
|
+
const body = stripControlCharacters(stripTerminalSequences(transcriptBody(agent.messages, agent.partial, agent.state, contentBudget)));
|
|
36
37
|
const result = `${header}${body}`;
|
|
37
38
|
return result.length <= MAX_RENDERED_CHARS ? result : truncateFromStart(result, MAX_RENDERED_CHARS);
|
|
38
39
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Key, Markdown, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Key, Markdown, matchesKey, stripTerminalSequences, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { stripControlCharacters } from "../shared/controlChars.ts";
|
|
4
5
|
import { ROUNDED_SINGLE_BOX, createFrame, ratioViewport, safeRenderWidth } from "./chrome.ts";
|
|
5
6
|
|
|
6
7
|
const OVERLAY_HEIGHT_RATIO = 0.9;
|
|
@@ -51,7 +52,7 @@ class VerificationFindingsOverlay implements Component {
|
|
|
51
52
|
this.theme = theme;
|
|
52
53
|
this.keybindings = keybindings;
|
|
53
54
|
this.done = done;
|
|
54
|
-
this.markdown = new Markdown(findingsMarkdown, 0, 0, getMarkdownTheme());
|
|
55
|
+
this.markdown = new Markdown(stripControlCharacters(stripTerminalSequences(findingsMarkdown)), 0, 0, getMarkdownTheme());
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
private viewportLines(): number {
|