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 +34 -15
- package/docs/assets/pi-plans-logo.svg +2 -2
- package/index.ts +17 -2
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +11 -17
- package/references/plan-artifact-template.md +1 -1
- package/references/state-and-config.md +1 -1
- package/skills/debug-and-plan/SKILL.md +1 -1
- package/skills/plan-big/SKILL.md +1 -1
- package/skills/plan-normal/SKILL.md +1 -1
- package/skills/plan-small/SKILL.md +1 -1
- package/skills/plan-with-refs/SKILL.md +1 -1
- package/src/exec.ts +67 -13
- package/src/execution-panel.ts +10 -8
- package/tests/exec.test.ts +100 -22
- package/tests/execution-panel.test.ts +15 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<h1 align="center"
|
|
1
|
+
<h1 align="center">π-plans</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="docs/assets/pi-plans-logo.svg?v=
|
|
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
|
|
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:
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
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:
|
|
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
|
|
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
|
|
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"
|
|
36
|
-
<rect x="153" y="110" width="
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
148
|
-
- ending a turn after a completed refinement round without asking the next
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/skills/plan-big/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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(
|
|
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
|
-
-
|
|
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++) {
|
package/src/execution-panel.ts
CHANGED
|
@@ -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
|
-
|
|
401
|
-
const
|
|
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) {
|
package/tests/exec.test.ts
CHANGED
|
@@ -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: (
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
assert.
|
|
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
|
-
|
|
122
|
-
assert.match(
|
|
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
|
-
//
|
|
211
|
-
//
|
|
212
|
-
assert.
|
|
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
|
|
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
|
|
241
|
-
//
|
|
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
|
|
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); //
|
|
122
|
-
assert.equal(factories.length,
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
130
|
-
|
|
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,
|
|
142
|
+
assert.equal(factories.length, 5); // #4 was the explicit clear (undefined)
|
|
136
143
|
});
|
|
137
144
|
});
|
|
138
145
|
|