pi-plans 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
- <h1 align="center">pi-plans</h1>
1
+ <h1 align="center">π-plans</h1>
2
2
 
3
3
  <p align="center">
4
- <img src="docs/assets/pi-plans-logo.svg?v=3" alt="pi-plans: Plan. Review. Execute." width="640" />
4
+ <img src="docs/assets/pi-plans-logo.svg?v=4" alt="pi-plans: Plan. Review. Execute." width="640" />
5
5
  </p>
6
6
 
7
7
  <h2 align="center"><b>Plan. Review. Execute.</b></h2>
@@ -58,8 +58,9 @@ A rough change request becomes a versioned Markdown plan instead of a surprise d
58
58
  =============================================== write guard OFF
59
59
  |
60
60
  tracked execution loop
61
+ fused AGENTS.md × Ponytail executor rules
61
62
  checklist injected each turn, [DONE:VC-xxx]
62
- markers tracked with a footer widget
63
+ markers tracked via bottom status bar + detail panel
63
64
  |
64
65
  v
65
66
  run status: done
@@ -93,17 +94,13 @@ Pi: Wrote ./docs/pi-plans/2026-08-26-split-execution-loop/PLAN_v1.md
93
94
  - [ ] `VC-001` covers `I-001`; pass condition: `npm test` passes;
94
95
  evidence: test output; metric: zero failing tests.
95
96
 
96
- Pi: Next step for refining the plan?
97
- 1. Reviewer round (recommended)
98
- 2. Criticizer round
99
- 3. Accept plan for tracked execution
97
+ Pi: Accept the plan and execute it now?
98
+ 1. Accept plan and execute now (recommended)
99
+ 2. Accept plan, don't execute yet
100
+ 3. Run another round: Reviewer
100
101
  ...
101
102
 
102
- You: Accept plan for tracked execution.
103
-
104
- Pi: Execute this plan now?
105
- 1. Execute this plan now (recommended)
106
- 2. Stop after planning
103
+ You: 1 accept and execute.
107
104
  ```
108
105
 
109
106
  Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (public, committed). Prefer `.git/pi_plans/plans` if you want them private to the repository.
@@ -116,7 +113,8 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
116
113
  | Choice prompts | `ask_choice`: recommended option first, answers auto-recorded per run |
117
114
  | Refinement rounds | Read-only reviewer/criticizer Pi subagents consolidate findings into the next plan version |
118
115
  | Workspace state | Config, runs, decisions, refs, and subagent ledgers in `.git/pi_plans/` (git common dir) |
119
- | Tracked execution | Checklist injected each turn; `[DONE:VC-xxx]` markers drive progress |
116
+ | Tracked execution | Checklist injected each turn; `[DONE:VC-xxx]` markers drive progress; live `x/y · spent · in/out-toks` in the bottom status bar |
117
+ | Efficient executor prompt | Each turn, the executor is steered by a fused rule set — Marcos Hernanz's AGENTS.md principles × Ponytail minimalism: layered growth, simplest implementation, long-term architecture (no stopgaps), library discipline — so plans finish in fewer tokens and fewer detours |
120
118
  | Write guard | `edit`/`write` blocked outside planning artifacts while a run is active |
121
119
 
122
120
  ## Interface overview
@@ -124,7 +122,7 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
124
122
  | Tool / Command | Purpose |
125
123
  |---|---|
126
124
  | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
127
- | `ask_choice` | Numbered choice prompt; `autoComplete: false` for execution handoff / external-state questions |
125
+ | `ask_choice` | Numbered choice prompt; `autoComplete: false` for the merged accept/execute question and external-state questions |
128
126
  | `refine` | Reviewer/criticizer round via read-only subagents (`--tools read,grep,find,ls`); `reviewers: 3` for big plans; enforces role/model confirmation gates |
129
127
  | `execute_plan` | Execution handoff: re-confirms with the user, enters extension-managed execution mode |
130
128
  | `/plans` | Show config, active run, execution progress |
@@ -132,6 +130,23 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
132
130
  | `/update-plan [plan.md] [reason…]` | Interrupt-and-refine: stops execution (if any), returns the run to planning, and directs the agent to revise the plan into `PLAN_vN+1.md` while preserving verified work |
133
131
  | `/plans-stop` | Stop execution mode |
134
132
  | `/plans-abandon` | Abandon the active run (lifts the write guard; artifacts stay) |
133
+ | Status bar (lifecycle) | 💬 Q&A → 📝 draft written (planning sub-phases) → ⌛ executing `x/y · spent · in/out-toks` → ⛔ stopped / 🎯 done / 🚫 abandoned |
134
+
135
+ ## The execution rules
136
+
137
+ Once you approve the handoff, every turn injects a compact rule set that fuses Marcos Hernanz's AGENTS.md seven principles with Ponytail minimalism — so the executor finishes plans in fewer tokens and fewer detours:
138
+
139
+ <details>
140
+ <summary>The four fused rules (click to expand)</summary>
141
+
142
+ 1. **Grow in layers** — smallest end-to-end slice first, then stack each new capability on top of what already works.
143
+ 2. **Simplest implementation** — no speculative abstractions, configuration, or indirection; modular components with clearly separated concerns.
144
+ 3. **Long-term architecture, no stopgaps** — no backward-compatibility layers, fallbacks, or migrations; remove the obsolete paths a change obsoletes.
145
+ 4. **Library discipline** — prefer established, well-maintained libraries; check the project's existing dependencies (docs and types) before writing your own or adding a package.
146
+
147
+ </details>
148
+
149
+ The rules cost four lines per turn and buy back far more: fewer wrong turns, shorter implementation paths, plans that finish in fewer tokens.
135
150
 
136
151
  ## Skills
137
152
 
@@ -198,7 +213,7 @@ Both run on Node ≥ 22.6 via `--experimental-strip-types`; no npm dependencies.
198
213
 
199
214
  **Why do I have to approve before any code changes?**
200
215
 
201
- The plan is the contract. Refinement converges on scope while nothing is writable yet; the execution handoff is a separate explicit approval that also lifts the write guard. You always see — and can veto — what will happen before it happens.
216
+ The plan is the contract. Refinement converges on scope while nothing is writable yet; the merged accept/execute question is an explicit, never-auto-completed approval that also lifts the write guard. You always see — and can veto — what will happen before it happens.
202
217
 
203
218
  **What can Auto-complete decide on my behalf?**
204
219
 
@@ -212,6 +227,10 @@ Preferences and run ledgers in `.git/pi_plans/` inside your workspace's git dire
212
227
 
213
228
  Prompts produce one-shot diffs with no recorded reasoning. pi-plans produces versioned artifacts — decisions, references, reviewer findings, dispositions, a verifier checklist — that are auditable, resumable across sessions, and enforced by tooling rather than goodwill.
214
229
 
230
+ **Doesn't injecting execution rules every turn cost extra tokens?**
231
+
232
+ The injected rule set is four compressed lines. It buys back more than it costs: the executor stops re-deriving discipline (no speculative abstractions, no compatibility detours, no reinvented helpers), so finished items converge in fewer turns and fewer tokens overall.
233
+
215
234
  ## License
216
235
 
217
236
  MIT.
@@ -32,8 +32,8 @@
32
32
  </g>
33
33
 
34
34
  <!-- wordmark -->
35
- <text x="150" y="96" font-family="-apple-system,'Segoe UI',Helvetica,Arial,sans-serif" font-size="60" font-weight="700" fill="#ffffff">pi-plans</text>
36
- <rect x="153" y="110" width="236" height="6" rx="3" fill="url(#bar)"/>
35
+ <text x="150" y="96" font-family="-apple-system,'Segoe UI',Helvetica,Arial,sans-serif" font-size="60" font-weight="700" fill="#ffffff">π-plans</text>
36
+ <rect x="153" y="110" width="210" height="6" rx="3" fill="url(#bar)"/>
37
37
  <text x="151" y="147" font-family="'SF Mono',Menlo,Consolas,monospace" font-size="16" letter-spacing="4" fill="#ddd6fe">PLAN·REVIEW·EXECUTE</text>
38
38
 
39
39
  <!-- pipeline: plan -> review -> execute -->
package/index.ts CHANGED
@@ -139,6 +139,16 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
139
139
  };
140
140
  });
141
141
 
142
+ // The turn_end projection does not carry usage; message_end delivers the
143
+ // full assistant message, so we cache it there and consume it per turn.
144
+ let lastAssistantUsage: { input: number; output: number } | null = null;
145
+ pi.on("message_end", async (event) => {
146
+ const message = event.message as { role?: string; usage?: { input?: number; output?: number } };
147
+ if (message?.role === "assistant" && message.usage) {
148
+ lastAssistantUsage = { input: message.usage.input ?? 0, output: message.usage.output ?? 0 };
149
+ }
150
+ });
151
+
142
152
  pi.on("turn_end", async (event, ctx) => {
143
153
  const message = event.message as { role?: string; content?: Array<{ type: string; text?: string }> };
144
154
  if (!message || message.role !== "assistant") {
@@ -151,7 +161,12 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
151
161
  .join("\n");
152
162
  const changedIds = applyDoneMarkers(text);
153
163
  if (changedIds.length > 0) {
154
- recordExecutionCompletion(pi, ctx, changedIds);
164
+ // Attribute this turn's token usage to the finished items.
165
+ const projection = (event.message as { usage?: { input?: number; output?: number } }).usage;
166
+ const raw = projection ?? lastAssistantUsage;
167
+ lastAssistantUsage = null; // consumed: never re-attribute a stale turn
168
+ const usage = raw ? { input: raw.input ?? 0, output: raw.output ?? 0 } : undefined;
169
+ recordExecutionCompletion(pi, ctx, changedIds, usage);
155
170
  }
156
171
  if (getExecution() && isExecutionComplete()) {
157
172
  completeExecution(pi, ctx);
@@ -322,7 +337,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
322
337
  if (focus) lines.push(`User-reported problems / refocus: ${focus}`);
323
338
  lines.push(
324
339
  "",
325
- "Follow the original planning-skill contract for revisions: collect needed clarifications via ask_choice (one question at a time, recorded), apply evidence-based revisions only, then ask the next refinement-mode question, and finish with the execution handoff (ask_choice with autoComplete: false execute_plan pointing at the new version).",
340
+ "Follow the original planning-skill contract for revisions: collect needed clarifications via ask_choice (one question at a time, recorded), apply evidence-based revisions only, then ask the next merged accept/execute question (autoComplete: false Accept & execute now / Accept, don't execute yet / another round) and call execute_plan pointing at the new version on accept.",
326
341
  );
327
342
 
328
343
  await pi.sendUserMessage(lines.join("\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plans",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Human-in-the-loop planning extension for the Pi coding agent: researched, refined Markdown plans before any code changes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -59,7 +59,7 @@ Every user-facing planning or refinement question goes through the `ask_choice`
59
59
 
60
60
  - `options`: ordered options, recommended option first with `recommended: true` (exactly one), each with the tradeoff that matters in `description`;
61
61
  - do not add `Other` or `Auto-complete` yourself — the tool appends `Other…` second-last and `Auto-complete` last;
62
- - pass `autoComplete: false` for the execution handoff and for any install waiver, publishing, deployment, merge, push, credential, or external-state question. Auto-complete may choose the recommended planning or refinement option only.
62
+ - pass `autoComplete: false` for the merged accept/execute question — it contains the execution approval, so Auto-complete never appears there — and for any install waiver, publishing, deployment, merge, push, credential, or external-state question. Auto-complete may choose the recommended planning or refinement option only.
63
63
 
64
64
  Answers are recorded automatically in the active run's `decisions.jsonl`. You must still maintain `DECISIONS.md` in the artifact directory (summary table of questions, options, answers, answer sources, open assumptions).
65
65
 
@@ -100,13 +100,13 @@ The execution loop parses `- [ ] \`VC-###\`` items and tracks `[DONE:VC-###]` ma
100
100
 
101
101
  ## Refinement
102
102
 
103
- After each plan version, ask one refinement-mode question via `ask_choice`. The recommended (first) option follows the skill level's default sequence; the user may always pick a different mode or keep refining:
103
+ After each plan version, ask one merged accept/execute question via `ask_choice` with `autoComplete: false` it contains the execution approval, so Auto-complete never appears. Options:
104
104
 
105
- - `plan-small`: recommend `Criticizer` for the first (and only default) round; afterwards recommend `Accept plan for tracked execution`.
106
- - `plan-normal`: recommend `Reviewer` first, then `Criticizer`; afterwards recommend `Accept plan for tracked execution`.
107
- - `plan-big`: recommend `Reviewer` first as three concurrent independent reviewers (`refine` with `reviewers: 3`), then `Criticizer`; afterwards recommend `Accept plan for tracked execution`.
105
+ 1. `✓ Accept PLAN_vN and execute it now` mark the plan accepted (`plans set-status accepted`), then call the `execute_plan` tool.
106
+ 2. `Accept PLAN_vN, don't execute yet` mark accepted; resume later via `/plans-execute`.
107
+ 3. `Run another round: <the level's default next refine mode>` only while the level's default sequence is unfinished.
108
108
 
109
- Option list for the question: the recommended next step; the other refinement modes with their tradeoffs; `Accept plan for tracked execution`; `Other`; `Auto-complete`.
109
+ The recommended option follows the skill level's default sequence: while the default rounds are unfinished it is option 3's default next mode (`plan-small`: `Criticizer`; `plan-normal`: `Reviewer` then `Criticizer`; `plan-big`: three concurrent reviewers (`refine` with `reviewers: 3`) then `Criticizer`); once the default sequence is complete it is option 1.
110
110
 
111
111
  If the user selects `Reviewer` or `Criticizer`, run the `refine` tool with the plan path and any focus. Reviewer output consolidates into `PLAN_vN_reviewer_comments.md` with findings IDs, severity, affected plan IDs, evidence, impact, recommended fix, and disposition. Revise the next plan only for findings accepted on evidence.
112
112
 
@@ -120,19 +120,13 @@ Present each criticizer question with `ask_choice` (one call per question, in th
120
120
 
121
121
  ### Round Lifecycle
122
122
 
123
- A refinement round is complete when all reviewer outputs have returned or all criticizer questions have answers. In the same turn: consolidate, accept or reject each finding on evidence (the user may override any disposition), revise to `PLAN_v(N+1).md` when accepted items require it (copy, edit only the new version, update the revision ledger and verifier checklist), then immediately ask the next refinement-mode question. Never end a turn merely because a round completed.
123
+ A refinement round is complete when all reviewer outputs have returned or all criticizer questions have answers. In the same turn: consolidate, accept or reject each finding on evidence (the user may override any disposition), revise to `PLAN_v(N+1).md` when accepted items require it (copy, edit only the new version, update the revision ledger and verifier checklist), then immediately ask the next merged accept/execute question. Never end a turn merely because a round completed.
124
124
 
125
125
  ## Execution Handoff
126
126
 
127
- After the plan is accepted or refinement converges, ask the explicit execution-handoff question via `ask_choice` with `autoComplete: false`:
127
+ When the user picks `✓ Accept PLAN_vN and execute it now` in the merged question, mark the plan accepted and call the `execute_plan` tool (or the user runs `/plans-execute`). It re-confirms with the user, then the extension enters execution mode:
128
128
 
129
- 1. `Execute this plan now` enter the tracked execution loop (recommended once refinement converged).
130
- 2. `Stop after planning` — keep the plan artifact only.
131
- 3. `Other`.
132
-
133
- On approval, call the `execute_plan` tool (or the user runs `/plans-execute`). It re-confirms with the user, then the extension enters execution mode:
134
-
135
- - every agent turn is injected with the remaining verifier checklist and execution rules (ponytail discipline, minimum tests);
129
+ - every agent turn is injected with the remaining verifier checklist and execution rules (layered simplest implementation, no stopgaps, dependency and library discipline, minimum tests);
136
130
  - the read-only guard lifts: full write access returns;
137
131
  - the run status moves to `executing`, then `done` when the last `[DONE:VC-xxx]` marker lands;
138
132
  - `/plans-stop` stops execution; `/plans` shows progress.
@@ -144,8 +138,8 @@ If the user declines, stay in planning (or stop, per their choice). Never start
144
138
  Stop and return to the workflow if any of these happen:
145
139
 
146
140
  - implementing before the approved execution handoff;
147
- - running `refine` without first asking the refinement-mode question, or before the role gates pass;
148
- - ending a turn after a completed refinement round without asking the next refinement-mode question;
141
+ - running `refine` without first asking the merged accept/execute question, or before the role gates pass;
142
+ - ending a turn after a completed refinement round without asking the next merged accept/execute question;
149
143
  - storing planning settings outside the target workspace's `.git/pi_plans/` state directory;
150
144
  - asking multiple planning questions in one message, or asking them outside `ask_choice`;
151
145
  - writing `PLAN_v1.md` before final scope confirmation;
@@ -70,7 +70,7 @@ Summarize the user's request in one paragraph.
70
70
 
71
71
  ## Execution Handoff Notes
72
72
 
73
- State anything the executor should know, including order of work, files to avoid, and verification commands. The execution handoff still requires explicit user approval (ask_choice with `autoComplete: false`, then the `execute_plan` tool) and must never be auto-completed. Once approved, the extension-managed execution loop injects the remaining checklist every turn and completes when every `[DONE:VC-xxx]` marker has landed — keep this section concise enough to serve as the executor's brief.
73
+ State anything the executor should know, including order of work, files to avoid, and verification commands. The merged accept/execute question still requires explicit user approval (ask_choice with `autoComplete: false`, then the `execute_plan` tool) and must never be auto-completed. Once approved, the extension-managed execution loop injects the remaining checklist every turn and completes when every `[DONE:VC-xxx]` marker has landed — keep this section concise enough to serve as the executor's brief.
74
74
 
75
75
  ## Revision Ledger
76
76
 
@@ -126,7 +126,7 @@ When `mode` is `delegated-subagent`, the `refine` tool spawns a read-only `pi` s
126
126
  - receives the full plan text and a review/criticism brief;
127
127
  - returns its findings as the tool result (recorded in `subagents.jsonl` with name and model).
128
128
 
129
- The main agent consolidates the results, records dispositions, revises the plan, and asks the next refinement-mode question — all in the same turn.
129
+ The main agent consolidates the results, records dispositions, revises the plan, and asks the next merged accept/execute question — all in the same turn.
130
130
 
131
131
  ## Run State
132
132
 
@@ -20,7 +20,7 @@ Read `../../references/pi-planning-workflow.md` and `../../references/state-and-
20
20
  2. `Stop after RCA` — keep the diagnosis only.
21
21
  3. `Other` / 4. `Auto-complete` are added by the tool.
22
22
  4. On opt-out, stop after the summary; do not write `PROBLEM_ANALYSIS.md`.
23
- 5. On opt-in (or `Auto-complete` choosing the recommendation), select the smallest fitting planning skill and follow that skill exactly: first-turn planning question, final scope confirmation, `PLAN_v1.md`, refinement, and the execution handoff (ask_choice with `autoComplete: false`, then the `execute_plan` tool).
23
+ 5. On opt-in (or `Auto-complete` choosing the recommendation), select the smallest fitting planning skill and follow that skill exactly: first-turn planning question, final scope confirmation, `PLAN_v1.md`, refinement, and the merged accept/execute question (ask_choice with `autoComplete: false`) before calling the `execute_plan` tool.
24
24
 
25
25
  ## Level Selection
26
26
 
@@ -17,7 +17,7 @@ Read `../../references/pi-planning-workflow.md` and `../../references/state-and-
17
17
  - Ask at least 10 planning questions, one at a time; no maximum — stop only when the decision tree is genuinely resolved. Each via `ask_choice` (recommended option first; the tool adds `Other` second-last and `Auto-complete` last).
18
18
  - Use web research during both brainstorming and refinement when outside facts, patterns, or ecosystem constraints matter, and cite sources in the plan.
19
19
  - Ask the final scope confirmation, then write `PLAN_v1.md` per `../../references/plan-artifact-template.md`.
20
- - After each plan version, ask the refinement-mode question via `ask_choice` — never run `refine` unless the user or `Auto-complete` selected it at that question. Default sequence: one `Reviewer` round as three concurrent independent reviewers (`refine` with `reviewers: 3`, consolidated by the main agent per the shared workflow), then one `Criticizer` round; afterwards the recommended option is `Accept plan for tracked execution`. Beyond the default sequence, refine until convergence on high-priority findings, unresolved questions, or evidence gaps; surface at most five per round. Then the execution handoff (ask_choice with `autoComplete: false`, then the `execute_plan` tool).
20
+ - After each plan version, ask the merged accept/execute question via `ask_choice` with `autoComplete: false` — never run `refine` unless the user picked another round at that question. Default sequence: one `Reviewer` round as three concurrent independent reviewers (`refine` with `reviewers: 3`, consolidated by the main agent per the shared workflow), then one `Criticizer` round; afterwards the recommended option is `✓ Accept & execute now` in the merged accept/execute question. Beyond the default sequence, refine until convergence on high-priority findings, unresolved questions, or evidence gaps; surface at most five per round. Then the merged accept/execute question (ask_choice with `autoComplete: false`: Accept & execute now / Accept, don't execute yet / another round) and the `execute_plan` tool.
21
21
 
22
22
  ## Fit
23
23
 
@@ -17,7 +17,7 @@ Read `../../references/pi-planning-workflow.md` and `../../references/state-and-
17
17
  - Ask 5 to 10 planning questions, one at a time, each via `ask_choice` (recommended option first; the tool adds `Other` second-last and `Auto-complete` last).
18
18
  - Use web research whenever outside library behavior, ecosystem precedent, UX convention, protocol semantics, or compatibility affects the recommendation (websearch skill when installed; otherwise `curl`/`gh` via bash), and cite sources in the plan.
19
19
  - Ask the final scope confirmation, then write `PLAN_v1.md` per `../../references/plan-artifact-template.md`.
20
- - After each plan version, ask the refinement-mode question via `ask_choice` — never run `refine` unless the user or `Auto-complete` selected it at that question. Default sequence: one `Reviewer` round, then one `Criticizer` round; afterwards the recommended option is `Accept plan for tracked execution`. Up to five rounds total, continuing only for high-priority findings or unresolved criticizer questions; surface at most five per round. Then the execution handoff (ask_choice with `autoComplete: false`, then the `execute_plan` tool).
20
+ - After each plan version, ask the merged accept/execute question via `ask_choice` with `autoComplete: false` — never run `refine` unless the user picked another round at that question. Default sequence: one `Reviewer` round, then one `Criticizer` round; afterwards the recommended option is `✓ Accept & execute now` in the merged accept/execute question. Up to five rounds total, continuing only for high-priority findings or unresolved criticizer questions; surface at most five per round. Then the merged accept/execute question (ask_choice with `autoComplete: false`: Accept & execute now / Accept, don't execute yet / another round) and the `execute_plan` tool.
21
21
 
22
22
  ## Fit
23
23
 
@@ -16,7 +16,7 @@ Read `../../references/pi-planning-workflow.md` and `../../references/state-and-
16
16
  - Inspect the target Git repo read-only before the first product question.
17
17
  - Ask 1 to 3 planning questions, one at a time, each via `ask_choice` (recommended option first; the tool adds `Other` second-last and `Auto-complete` last).
18
18
  - Ask the final scope confirmation, then write `PLAN_v1.md` under the artifact root (normally the configured workspace root, default `./docs/pi-plans/YYYY-MM-DD-topic/`) per `../../references/plan-artifact-template.md`.
19
- - After each plan version, ask the refinement-mode question via `ask_choice` — never run `refine` unless the user or `Auto-complete` selected it at that question. Default: exactly one round, recommended mode `Criticizer`; afterwards the recommended option is `Accept plan for tracked execution`, then the execution handoff (ask_choice with `autoComplete: false`, then the `execute_plan` tool).
19
+ - After each plan version, ask the merged accept/execute question via `ask_choice` with `autoComplete: false` — never run `refine` unless the user picked another round at that question. Default: exactly one round, recommended mode `Criticizer`; afterwards the recommended option is `✓ Accept & execute now` in the merged accept/execute question (ask_choice with `autoComplete: false`: ✓ Accept & execute now / Accept, don't execute yet / another round), then the `execute_plan` tool.
20
20
 
21
21
  ## Fit
22
22
 
@@ -21,7 +21,7 @@ Read `../../references/pi-planning-workflow.md` and `../../references/state-and-
21
21
  6. For every reference, produce a structured analysis artifact (manual structured read recorded in `REF_ANALYSIS.md`) before asking adoption questions.
22
22
  7. For every reference after analysis, ask at least 3 ref-specific adoption questions via `ask_choice` before using its ideas in `PLAN_v1.md`; each based on downloaded content, recommended option first, `Other` second-last, `Auto-complete` last (the tool appends both).
23
23
  8. Block rather than pad if fewer than 3 credible references exist, unless the user explicitly narrows the topic or waives the minimum. `Auto-complete` cannot grant this waiver.
24
- 9. Continue with big-plan depth: at least 10 planning questions, required web research during brainstorming and refinement (`refine` `reviewers: 3` reviewer round, then a criticizer round), no refinement limit, at most five high-priority comments or questions per refinement round. Then the execution handoff (ask_choice with `autoComplete: false`, then the `execute_plan` tool).
24
+ 9. Continue with big-plan depth: at least 10 planning questions, required web research during brainstorming and refinement (`refine` `reviewers: 3` reviewer round, then a criticizer round), no refinement limit, at most five high-priority comments or questions per refinement round. Then the merged accept/execute question (ask_choice with `autoComplete: false`: Accept & execute now / Accept, don't execute yet / another round) and the `execute_plan` tool.
25
25
 
26
26
  ## REF_ANALYSIS.md
27
27
 
package/src/exec.ts CHANGED
@@ -22,12 +22,13 @@ import {
22
22
  type ExecutionPanelState,
23
23
  type ItemDiffSummary,
24
24
  } from "./execution-panel.ts";
25
- import { readActive, setRunStatus, utcNow } from "./state.ts";
26
- import { scanDoneMarkers, type CheckItem } from "./plan.ts";
25
+ import { getRun, readActive, setRunStatus, utcNow } from "./state.ts";
26
+ import { latestPlanVersion, scanDoneMarkers, type CheckItem } from "./plan.ts";
27
27
 
28
28
  export interface ExecState extends ExecutionPanelExecutionLike {
29
29
  startedAt: string;
30
30
  panel?: ExecutionPanelState;
31
+ usage: { inToks: number; outToks: number };
31
32
  }
32
33
 
33
34
  let execution: ExecState | null = null;
@@ -54,18 +55,58 @@ export function executionProgress(): { done: number; total: number } | null {
54
55
  };
55
56
  }
56
57
 
58
+ function formatElapsed(startedAt: string): string {
59
+ const total = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1000));
60
+ const h = String(Math.floor(total / 3600)).padStart(2, "0");
61
+ const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
62
+ const sec = String(total % 60).padStart(2, "0");
63
+ return `${h}:${m}:${sec}`;
64
+ }
65
+
66
+ function formatToks(tokens: number): string {
67
+ const n = Math.max(0, Math.round(tokens));
68
+ return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
69
+ }
70
+
57
71
  export function updateStatusWidget(ctx: ExtensionContext): void {
58
72
  const progress = executionProgress();
59
- if (progress) {
60
- // The below-editor panel owns the in-execution progress display; keep the
61
- // status bar free of a duplicate count (and clear stale ones from before).
62
- ctx.ui.setStatus("pi-plans", undefined);
73
+ if (progress && execution) {
74
+ // Progress lives in the bottom status bar — the same layer as the ⛔/⌛
75
+ // paused indicator so both execution states read from one place.
76
+ const tail = execution.panel?.expanded ? "/plans-list hide" : "/plans-list details";
77
+ const line = `⌛ plans ${progress.done}/${progress.total}: spent ${formatElapsed(execution.startedAt)} · ${formatToks(execution.usage.inToks)} in-toks · ${formatToks(execution.usage.outToks)} out-toks · ${tail}`;
78
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("accent", line));
63
79
  return;
64
80
  }
65
81
  const active = readActive(ctx.cwd);
66
82
  if (active) {
67
- ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("warning", `⏸ plans: ${active.run_id}`));
68
- return;
83
+ // Idle indicator depends on the run's lifecycle, not just its existence:
84
+ // done reads as finished, abandoned as closed, stopped/accepted as paused.
85
+ const status = getRun(ctx.cwd, active.run_id)?.status;
86
+ if (status === "done") {
87
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("success", `🎯 plans: ${active.run_id} (done)`));
88
+ return;
89
+ }
90
+ if (status === "abandoned") {
91
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("error", `🚫 plans: ${active.run_id}`));
92
+ return;
93
+ }
94
+ if (status === "stopped") {
95
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("warning", `⛔ plans: ${active.run_id}`));
96
+ return;
97
+ }
98
+ if (status === "accepted") {
99
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("warning", `⌛ plans: ${active.run_id}`));
100
+ return;
101
+ }
102
+ if (status === "planning") {
103
+ // Planning phase: 💬 while still in Q&A, 📝 once a PLAN draft exists
104
+ // — kept until execution starts (then ⌛ takes over).
105
+ const emoji = latestPlanVersion(active.artifact_dir) ? "📝" : "💬";
106
+ ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("muted", `${emoji} plans: ${active.run_id}`));
107
+ return;
108
+ }
109
+ // unknown status: no indicator.
69
110
  }
70
111
  ctx.ui.setStatus("pi-plans", undefined);
71
112
  }
@@ -76,6 +117,7 @@ function persist(pi: ExtensionAPI): void {
76
117
  planPath: execution.planPath,
77
118
  items: execution.items,
78
119
  startedAt: execution.startedAt,
120
+ usage: execution.usage,
79
121
  panel: snapshotPanelState(execution),
80
122
  });
81
123
  }
@@ -94,7 +136,7 @@ export function startExecution(
94
136
  planPath: string,
95
137
  items: CheckItem[],
96
138
  ): void {
97
- execution = { planPath, items, startedAt: utcNow(), panel: createExecutionPanelState() };
139
+ execution = { planPath, items, startedAt: utcNow(), panel: createExecutionPanelState(), usage: { inToks: 0, outToks: 0 } };
98
140
  attachPanelBaseline(execution, ctx.cwd);
99
141
  consumePendingPanelSync(); // fresh run: drop any stale deferral from a previous one
100
142
  persist(pi);
@@ -109,7 +151,7 @@ export function startExecution(
109
151
  pi.sendMessage(
110
152
  {
111
153
  customType: "pi-plans-exec-start",
112
- content: `**pi-plans: executing** \`${planPath}\` — ${items.length} verifier item(s). Progress appears below the editor; mark verified items with \`[DONE:VC-xxx]\`.`,
154
+ content: `**pi-plans: executing** \`${planPath}\` — ${items.length} verifier item(s). Progress appears in the bottom status bar; mark verified items with \`[DONE:VC-xxx]\`.`,
113
155
  display: true,
114
156
  },
115
157
  { triggerTurn: false },
@@ -148,8 +190,17 @@ export function recordTouchedPaths(_workdir: string, paths: string[]): void {
148
190
  panel.touchedPaths = [...merged];
149
191
  }
150
192
 
151
- export function recordExecutionCompletion(pi: ExtensionAPI, ctx: ExtensionContext, completedIds: string[]): ItemDiffSummary | null {
193
+ export function recordExecutionCompletion(
194
+ pi: ExtensionAPI,
195
+ ctx: ExtensionContext,
196
+ completedIds: string[],
197
+ usage?: { input: number; output: number },
198
+ ): ItemDiffSummary | null {
152
199
  if (!execution) return null;
200
+ if (usage) {
201
+ execution.usage.inToks += usage.input;
202
+ execution.usage.outToks += usage.output;
203
+ }
153
204
  const summary = completeCompletedItems(execution, ctx.cwd, completedIds);
154
205
  persist(pi);
155
206
  syncExecutionPanel(ctx);
@@ -237,8 +288,10 @@ Remaining verifier items:
237
288
  ${list}
238
289
 
239
290
  Execution rules:
240
- - Implement implementation items in dependency order.
241
- - Ponytail discipline: for each item, take the laziest rung that holds (does it need to exist; already in this codebase; stdlib; native platform feature; already-installed dependency; one line). Mark deliberate simplifications with \`# ponytail: <ceiling>, <upgrade path>\`.
291
+ - Implement implementation items in dependency order; grow the change in layers — smallest end-to-end slice first, then stack each new capability on top of what already works.
292
+ - Simplest implementation that fully meets the item: no speculative abstractions, configuration, or indirection; keep components modular with clearly separated concerns.
293
+ - Architectural decisions are for the long term: no stopgaps. Do not add backward-compatibility layers, fallbacks, or migrations — remove the obsolete paths this change obsoletes.
294
+ - Prefer established, well-maintained libraries when they reduce complexity or improve reliability; before writing your own implementation or adding a package, check the project's existing dependencies (docs and types) — never reimplement common functionality without a clear reason.
242
295
  - MINIMUM tests: trivial one-liners get no test; non-trivial logic gets exactly one minimal check; reuse the repo's test runner when one exists; when unsure, skip and emit \`[test skipped: <name>, add when <trigger>]\`.
243
296
  - After verifying an item's pass condition with its stated evidence, include \`[DONE:VC-xxx]\` in your reply.
244
297
  - When every item is done, report a completion summary.`;
@@ -290,6 +343,7 @@ export function restoreFromSession(pi: ExtensionAPI, ctx: ExtensionContext, entr
290
343
  execution = {
291
344
  ...snapshot,
292
345
  items: snapshot.items.map((item) => ({ ...item })),
346
+ usage: snapshot.usage ?? { inToks: 0, outToks: 0 },
293
347
  panel: executionPanelFromEntryData(snapshot.panel) ?? createExecutionPanelState(),
294
348
  };
295
349
  for (let i = snapshotIndex + 1; i < entries.length; i++) {
@@ -396,19 +396,16 @@ function renderItemLines(item: CheckItem, summary: ItemDiffSummary | undefined,
396
396
  }
397
397
 
398
398
  function renderPanelLines(execution: ExecutionPanelExecutionLike, theme: ThemeLike, width: number): string[] {
399
+ // Expanded detail view only: the count/hint live in the bottom status bar,
400
+ // so the panel below the editor never repeats them.
399
401
  const panel = ensurePanelState(execution);
400
- const done = execution.items.filter((item) => item.done).length;
401
- const hint = panel.expanded ? "alt+o /plans-list hide" : "alt+o /plans-list details";
402
- const header = truncateAnsi(theme.fg("accent", `📋 plans ${done}/${execution.items.length} · ${hint}`), width);
403
- if (!panel.expanded) {
404
- return [header];
405
- }
406
- const lines = [header];
402
+ if (!panel.expanded) return [""];
403
+ const lines: string[] = [];
407
404
  for (const item of execution.items) {
408
405
  const summary = panel.itemSummaries[item.id]?.summary;
409
406
  lines.push(...renderItemLines(item, summary, theme, width));
410
407
  }
411
- return lines;
408
+ return lines.length ? lines : [""];
412
409
  }
413
410
 
414
411
  interface WidgetThemeSource {
@@ -457,6 +454,11 @@ export function refreshExecutionPanel(ctx: ExtensionContext, execution: Executio
457
454
  clearExecutionPanel(ctx);
458
455
  return;
459
456
  }
457
+ if (!execution.panel?.expanded) {
458
+ // Collapsed: the bottom status bar carries the count; no panel widget.
459
+ clearExecutionPanel(ctx);
460
+ return;
461
+ }
460
462
  panelRef.current = execution;
461
463
  invalidateRenderCache(); // next render always reflects the latest state
462
464
  if (registeredUi === ctx.ui) {
@@ -14,15 +14,19 @@ import {
14
14
  isExecutionComplete,
15
15
  restoreFromSession,
16
16
  startExecution,
17
+ recordExecutionCompletion,
17
18
  stopExecution,
18
19
  toggleExecutionPanelView,
20
+ updateStatusWidget,
19
21
  } from "../src/exec.ts";
20
22
  import type { CheckItem } from "../src/plan.ts";
23
+ import { initState, setRunStatus, startRun } from "../src/state.ts";
21
24
 
22
25
  interface Recorded {
23
26
  entries: { type: string; customType?: string; data?: unknown }[];
24
27
  messages: { customType: string; content: string }[];
25
28
  status: string | undefined;
29
+ colors: string[];
26
30
  widget?: { key: string; options?: unknown; factory: any };
27
31
  widgetCalls: number;
28
32
  }
@@ -34,7 +38,7 @@ interface Harness {
34
38
  }
35
39
 
36
40
  function makeHarness(workdir: string): Harness {
37
- const recorded: Recorded = { entries: [], messages: [], status: undefined, widgetCalls: 0 };
41
+ const recorded: Recorded = { entries: [], messages: [], status: undefined, colors: [], widgetCalls: 0 };
38
42
  const pi = {
39
43
  appendEntry: (customType: string, data: unknown) => {
40
44
  recorded.entries.push({ type: "custom", customType, data });
@@ -56,7 +60,10 @@ function makeHarness(workdir: string): Harness {
56
60
  recorded.widget = { key, options, factory };
57
61
  },
58
62
  theme: {
59
- fg: (_color: string, text: string) => text,
63
+ fg: (color: string, text: string) => {
64
+ recorded.colors.push(color);
65
+ return text;
66
+ },
60
67
  strikethrough: (text: string) => `~~${text}~~`,
61
68
  },
62
69
  };
@@ -95,20 +102,19 @@ describe("execution loop", () => {
95
102
  const workdir = freshWorkdir();
96
103
  const { pi, ctx, recorded } = makeHarness(workdir);
97
104
  startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001", "VC-002"));
98
- assert.ok(recorded.widget);
99
- assert.equal(recorded.widget?.key, "pi-plans-execution");
100
- assert.deepEqual(recorded.widget?.options, { placement: "belowEditor" });
101
- const widget = recorded.widget?.factory(
102
- {} as any,
103
- { fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
104
- );
105
- assert.ok(widget);
106
- const rendered = widget.render(80);
107
- assert.match(rendered[0] ?? "", /alt\+o/);
108
- assert.match(rendered[0] ?? "", /📋 plans 0\/2/);
105
+
106
+ // Collapsed by default: progress lives in the bottom status bar (same
107
+ // layer as the ⛔/⌛ indicators); no panel widget is registered yet.
108
+ assert.equal(recorded.widget, undefined);
109
+ assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent \d{2}:\d{2}:\d{2}/);
110
+ assert.match(recorded.status ?? "", /in-toks/);
111
+ assert.match(recorded.status ?? "", /out-toks/);
112
+ assert.match(recorded.status ?? "", /\/plans-list details/);
109
113
 
110
114
  toggleExecutionPanelView(pi, ctx);
111
115
  assert.ok(recorded.widget);
116
+ assert.equal(recorded.widget?.key, "pi-plans-execution");
117
+ assert.deepEqual(recorded.widget?.options, { placement: "belowEditor" });
112
118
  const expandedWidget = recorded.widget?.factory(
113
119
  {} as any,
114
120
  { fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
@@ -116,10 +122,23 @@ describe("execution loop", () => {
116
122
  assert.ok(expandedWidget);
117
123
  const expandedLines = expandedWidget.render(80);
118
124
  assert.match(expandedLines.join("\n"), /☐/);
125
+ // Detail view never repeats the count or the keyboard hint.
126
+ assert.doesNotMatch(expandedLines.join("\n"), /📋 plans/);
127
+ assert.doesNotMatch(expandedLines.join("\n"), /alt\+o/);
119
128
 
120
129
  assert.ok(getExecution());
121
- assert.match(executionContextMessage()!, /PI-PLANS EXECUTION/);
122
- assert.match(executionContextMessage()!, /VC-001/);
130
+ const rules = executionContextMessage()!;
131
+ assert.match(rules, /PI-PLANS EXECUTION/);
132
+ assert.match(rules, /VC-001/);
133
+ // Seven-principle rule set: representative anchors (PLAN_v2 D-003/D-004).
134
+ assert.match(rules, /for the long term/);
135
+ assert.match(rules, /Simplest implementation/);
136
+ assert.match(rules, /grow the change in layers/);
137
+ assert.match(rules, /existing dependencies \(docs and types\)/);
138
+ assert.match(rules, /well-maintained libraries/);
139
+ assert.match(rules, /clearly separated concerns/);
140
+ assert.match(rules, /no stopgaps/);
141
+ assert.doesNotMatch(rules, /ponytail/i);
123
142
 
124
143
  assert.deepEqual(applyDoneMarkers("progress… [DONE:VC-001] done"), ["VC-001"]);
125
144
  assert.equal(isExecutionComplete(), false);
@@ -207,18 +226,77 @@ describe("execution loop", () => {
207
226
  const { pi, ctx, recorded } = makeHarness(workdir);
208
227
  startExecution(pi, ctx, path.join(workdir, "PLAN_v2.md"), items("VC-001", "VC-002"));
209
228
 
210
- // The below-editor panel owns progress; the status item must be cleared,
211
- // never carrying a duplicate “📋 plans x/y” count (regression: double display).
212
- assert.equal(recorded.status, undefined);
229
+ // Bottom status bar carries the count — the same layer as ⛔/⌛ — so both
230
+ // execution states read from one consistent place.
231
+ assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent \d{2}:\d{2}:\d{2}/);
232
+ assert.match(recorded.status ?? "", /in-toks/);
233
+ assert.match(recorded.status ?? "", /\/plans-list details/);
213
234
 
214
235
  const start = recorded.messages.find((message) => message.customType === "pi-plans-exec-start");
215
236
  assert.ok(start);
216
- assert.match(start.content, /Progress appears below the editor/);
237
+ assert.match(start.content, /Progress appears in the bottom status bar/);
217
238
  assert.doesNotMatch(start.content, /footer/);
218
239
 
219
240
  applyDoneMarkers("[DONE:VC-001]");
220
241
  completeExecution(pi, ctx);
221
242
  assert.equal(getExecution(), null);
243
+
244
+ });
245
+
246
+ it("renders the idle indicator by run status", () => {
247
+ const workdir = freshWorkdir();
248
+ const { ctx, recorded } = makeHarness(workdir);
249
+ initState(workdir);
250
+ const { run } = startRun(workdir, { topic: "demo", skill: "plan-small", requestText: "x" });
251
+
252
+ // planning before any PLAN draft exists: 💬 (Q&A phase).
253
+ updateStatusWidget(ctx);
254
+ assert.match(recorded.status ?? "", /💬 plans: /);
255
+ assert.equal(recorded.colors.at(-1), "muted");
256
+
257
+ // Once a draft lands: 📝, kept until execution starts.
258
+ fs.writeFileSync(path.join(run.artifact_dir, "PLAN_v1.md"), "# plan");
259
+ updateStatusWidget(ctx);
260
+ assert.match(recorded.status ?? "", /📝 plans: /);
261
+ assert.equal(recorded.colors.at(-1), "muted");
262
+
263
+ setRunStatus(workdir, run.run_id, "accepted");
264
+ updateStatusWidget(ctx);
265
+ assert.match(recorded.status ?? "", /⌛ plans: /);
266
+ assert.equal(recorded.colors.at(-1), "warning");
267
+
268
+ setRunStatus(workdir, run.run_id, "stopped");
269
+ updateStatusWidget(ctx);
270
+ assert.match(recorded.status ?? "", /⛔ plans: /);
271
+ assert.equal(recorded.colors.at(-1), "warning");
272
+
273
+ setRunStatus(workdir, run.run_id, "done");
274
+ updateStatusWidget(ctx);
275
+ assert.match(recorded.status ?? "", /🎯 plans: .*\(done\)/);
276
+ assert.equal(recorded.colors.at(-1), "success");
277
+
278
+ setRunStatus(workdir, run.run_id, "abandoned");
279
+ updateStatusWidget(ctx);
280
+ assert.match(recorded.status ?? "", /🚫 plans: /);
281
+ assert.equal(recorded.colors.at(-1), "error");
282
+ });
283
+
284
+ it("accumulates token usage only for turns that finish items", () => {
285
+ const workdir = freshWorkdir();
286
+ const { pi, ctx, recorded } = makeHarness(workdir);
287
+ startExecution(pi, ctx, path.join(workdir, "PLAN_v5.md"), items("VC-001", "VC-002"));
288
+
289
+ recordExecutionCompletion(pi, ctx, ["VC-001"], { input: 100, output: 40 });
290
+ assert.equal(getExecution()?.usage.inToks, 100);
291
+ assert.equal(getExecution()?.usage.outToks, 40);
292
+
293
+ // A completion without usage data must keep prior totals intact.
294
+ recordExecutionCompletion(pi, ctx, ["VC-002"]);
295
+ assert.equal(getExecution()?.usage.inToks, 100);
296
+ assert.equal(getExecution()?.usage.outToks, 40);
297
+
298
+ stopExecution(pi, ctx, "test-done");
299
+ assert.equal(getExecution(), null);
222
300
  });
223
301
 
224
302
  it("defers persistence and widget churn when toggling mid-turn", () => {
@@ -237,12 +315,12 @@ describe("execution loop", () => {
237
315
  assert.equal(consumePendingPanelSync(), true, "expected a pending panel sync marker");
238
316
  assert.equal(consumePendingPanelSync(), false, "marker should be consumed exactly once");
239
317
 
240
- // Back to idle: the next toggle persists and syncs, but still reuses the
241
- // already-registered factory (cache-drop instead of teardown).
318
+ // Back to idle: the next toggle persists and syncs. It flips the panel to
319
+ // expanded, which registers the detail widget exactly once (no teardown).
242
320
  ctx.isIdle = () => true;
243
321
  assert.equal(toggleExecutionPanelView(pi, ctx), false);
244
322
  assert.ok(recorded.entries.length > entriesBefore, "idle toggle did not persist");
245
- assert.equal(recorded.widgetCalls, factoriesBefore, "idle toggle replaced the widget factory");
323
+ assert.equal(recorded.widgetCalls, factoriesBefore + 1, "idle toggle churned the widget registration");
246
324
 
247
325
  stopExecution(pi, ctx, "test-done");
248
326
  });
@@ -117,22 +117,29 @@ describe("execution panel helpers", () => {
117
117
  const theme = { fg: (_color: string, text: string) => `\u001b[38;5;2m${text}\u001b[39m`, strikethrough: (text: string) => text };
118
118
  const ctx = { ui: { setWidget: (_key: string, factory: any) => void factories.push(factory) } } as any;
119
119
 
120
+ // Collapsed execution renders no widget at all — the bottom status bar
121
+ // owns the count (each clear call pushes `undefined`).
120
122
  refreshExecutionPanel(ctx, execution);
121
- refreshExecutionPanel(ctx, execution); // second pass must only invalidate
122
- assert.equal(factories.length, 1, "factory re-registered on refresh");
123
-
124
- const widget = (factories[0] as any)({}, theme);
125
- assert.equal(widget.render(80).length, 1, "collapsed render should be a single line");
123
+ refreshExecutionPanel(ctx, execution); // stays cleared while collapsed
124
+ assert.equal(factories.length, 2);
125
+ assert.equal(factories[0], undefined);
126
+ assert.equal(factories[1], undefined);
126
127
 
128
+ // Expanding registers the detail widget exactly once.
127
129
  toggleExpanded(execution);
128
130
  refreshExecutionPanel(ctx, execution);
129
- assert.equal(factories.length, 1);
130
- assert.ok(widget.render(80).length > 1, "invalidate did not pick up expanded state");
131
+ assert.equal(factories.length, 3);
132
+ const widget = (factories[2] as any)({}, theme);
133
+ assert.ok(widget.render(80).length >= 1, "expanded render should list items");
134
+
135
+ refreshExecutionPanel(ctx, execution); // same host: reuse, no re-register
136
+ assert.equal(factories.length, 3);
137
+ assert.ok(widget.render(80).length >= 1, "invalidate did not pick up latest state");
131
138
 
132
139
  // Clearing releases the slot so a future run registers afresh.
133
140
  refreshExecutionPanel(ctx, null);
134
141
  refreshExecutionPanel(ctx, execution);
135
- assert.equal(factories.length, 3); // #2 was the explicit clear (undefined)
142
+ assert.equal(factories.length, 5); // #4 was the explicit clear (undefined)
136
143
  });
137
144
  });
138
145