pi-plans 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,12 @@ import { after, before, describe, it } from "node:test";
8
8
  import {
9
9
  applyDoneMarkers,
10
10
  applyImplMarkers,
11
+ applyCurrentIMarker,
11
12
  buildExecutionCompactionResult,
12
13
  buildPlanningCompactionResult,
13
14
  completeExecution,
14
15
  consumePendingExecutionFlush,
15
16
  consumePlanningCompactionResumeGuard,
16
- consumePendingPanelSync,
17
17
  drainExecutionFlush,
18
18
  executionContextMessage,
19
19
  filterExecutionResumeMessages,
@@ -21,6 +21,7 @@ import {
21
21
  getExecution,
22
22
  handleExecutionBeforeCompact,
23
23
  handleExecutionCompact,
24
+ handleExecutionTurnCompaction,
24
25
  handleExecutionCompactFailed,
25
26
  handlePlanningBeforeCompact,
26
27
  handlePlanningCompact,
@@ -32,17 +33,14 @@ import {
32
33
  requestPlanningCompaction,
33
34
  restoreFromSession,
34
35
  shouldTriggerPlanningCompaction,
35
- ensureExecutionModelActive,
36
36
  startExecution,
37
37
  recordExecutionTurn,
38
38
  registerExecutionTurnHandlers,
39
39
  stopExecution,
40
- chooseExecutionModelSelection,
41
- toggleExecutionPanelView,
42
40
  updateStatusWidget,
43
41
  } from "../src/exec.ts";
44
42
  import type { CheckItem } from "../src/plan.ts";
45
- import { initState, setExecutionModel, setRunStatus, showConfig, startRun } from "../src/state.ts";
43
+ import { initState, setRunStatus, startRun } from "../src/state.ts";
46
44
 
47
45
  interface Recorded {
48
46
  entries: { type: string; customType?: string; data?: unknown }[];
@@ -50,8 +48,6 @@ interface Recorded {
50
48
  status: string | undefined;
51
49
  statusCalls: number;
52
50
  colors: string[];
53
- widget?: { key: string; options?: unknown; factory: any };
54
- widgetCalls: number;
55
51
  models: { provider: string; id: string }[];
56
52
  thinkingLevels: (string | null)[];
57
53
  notifies: { message: string; severity: string }[];
@@ -76,7 +72,6 @@ function makeHarness(workdir: string): Harness {
76
72
  status: undefined,
77
73
  statusCalls: 0,
78
74
  colors: [],
79
- widgetCalls: 0,
80
75
  models: [],
81
76
  thinkingLevels: [],
82
77
  notifies: [],
@@ -129,14 +124,6 @@ function makeHarness(workdir: string): Harness {
129
124
  recorded.statusCalls += 1;
130
125
  recorded.status = value;
131
126
  },
132
- setWidget: (key: string, factory: any, options?: unknown) => {
133
- recorded.widgetCalls += 1;
134
- if (factory === undefined) {
135
- recorded.widget = undefined;
136
- return;
137
- }
138
- recorded.widget = { key, options, factory };
139
- },
140
127
  notify: (message: string, severity: string) => {
141
128
  recorded.notifies.push({ message, severity });
142
129
  },
@@ -214,36 +201,20 @@ describe("execution loop", () => {
214
201
  const { pi, ctx, recorded } = makeHarness(workdir);
215
202
  await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001", "VC-002"));
216
203
 
217
- // Collapsed by default: progress lives in the bottom status bar (same
218
- // layer as the ⛔/⌛ indicators); no panel widget is registered yet.
219
- assert.equal(recorded.widget, undefined);
220
204
  assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent \d{2}:\d{2}:\d{2}/);
221
205
  assert.match(recorded.status ?? "", /in-toks/);
222
206
  assert.match(recorded.status ?? "", /out-toks/);
223
207
 
224
- toggleExecutionPanelView(pi, ctx);
225
- assert.ok(recorded.widget);
226
- assert.equal(recorded.widget?.key, "pi-plans-execution");
227
- assert.deepEqual(recorded.widget?.options, { placement: "belowEditor" });
228
- // Expanded: footer ⌛ line is cleared — the panel renders its own header.
229
- assert.equal(recorded.status, undefined);
230
- const expandedWidget = recorded.widget?.factory(
231
- {} as any,
232
- { fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
233
- );
234
- assert.ok(expandedWidget);
235
- const expandedLines = expandedWidget.render(80);
236
- // Header line first, then the legacy item list.
237
- assert.match(expandedLines[0] ?? "", /⌛ plans 0\/2: spent/);
238
- assert.match(expandedLines.join("\n"), /☐/);
239
- // Detail view never repeats the keyboard hint.
240
- assert.doesNotMatch(expandedLines.join("\n"), /alt\+o/);
241
-
242
208
  assert.ok(getExecution());
243
209
  const rules = executionContextMessage()!;
244
210
  assert.match(rules, /PI-PLANS EXECUTION/);
245
211
  assert.match(rules, /VC-001/);
246
- // Seven-principle rule set: representative anchors (PLAN_v2 D-003/D-004).
212
+ assert.match(rules, /subprocess-backed verification/);
213
+ assert.match(rules, /waiting for/);
214
+ assert.match(rules, /5s\s*->\s*10s\s*->\s*20s\s*->\s*40s\s*->\s*80s/);
215
+ assert.match(rules, /keep polling at 80s/);
216
+ assert.match(rules, /restart at 5s for each new subprocess/);
217
+ // Representative anchors for the core execution rules (PLAN_v2 D-003/D-004).
247
218
  assert.match(rules, /for the long term/);
248
219
  assert.match(rules, /Simplest implementation/);
249
220
  assert.match(rules, /grow the change in layers/);
@@ -270,17 +241,6 @@ describe("execution loop", () => {
270
241
  planPath: path.join(workdir, "PLAN_v1.md"),
271
242
  items: items("VC-001", "VC-002"),
272
243
  startedAt: "2026-08-25T00:00:00Z",
273
- panel: {
274
- expanded: true,
275
- baseline: { added: 0, removed: 0, files: 0 },
276
- lastSnapshot: { added: 1, removed: 0, files: 1 },
277
- touchedPaths: ["src/exec.ts"],
278
- itemSummaries: {
279
- "VC-001": {
280
- summary: { added: 1, removed: 0, files: 1, paths: ["src/exec.ts"] },
281
- },
282
- },
283
- },
284
244
  compaction: {
285
245
  inFlight: true,
286
246
  resumeGuard: true,
@@ -302,15 +262,6 @@ describe("execution loop", () => {
302
262
  const execution = getExecution();
303
263
  assert.ok(execution);
304
264
  assert.equal("compaction" in execution!, false, "legacy scheduler state must not reactivate on restore");
305
- assert.ok(recorded.widget);
306
- const widget = recorded.widget?.factory(
307
- {} as any,
308
- { fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~~${text}~~` },
309
- );
310
- assert.ok(widget);
311
- const rendered = widget.render(80);
312
- assert.match(rendered.join("\n"), /☑/);
313
- assert.match(rendered.join("\n"), /\+1/);
314
265
 
315
266
  const clearedEntries = [...entries, { type: "custom", customType: "pi-plans-exec-cleared", data: {} }];
316
267
  await restoreFromSession(pi, ctx, clearedEntries as any);
@@ -343,99 +294,21 @@ describe("execution loop", () => {
343
294
  assert.equal(getExecution(), null);
344
295
  });
345
296
 
346
- it("switches to the execution model and restores the planning model", async () => {
297
+ it("starts execution without switching models", async () => {
347
298
  const workdir = freshWorkdir();
348
299
  const { pi, ctx, recorded } = makeHarness(workdir);
349
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v6.md"), items("VC-001"), {
350
- planningSelector: "p/m:high",
351
- executionSelector: "prov/other:xhigh",
352
- });
300
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v6.md"), items("VC-001"));
353
301
 
354
- assert.deepEqual(recorded.models, [{ provider: "prov", id: "other" }]);
355
- assert.equal(recorded.thinking, "xhigh");
302
+ assert.deepEqual(recorded.models, []);
303
+ assert.equal(recorded.thinking, "high");
356
304
  assert.match(recorded.status ?? "", /⌛ plans 0\/1/);
357
305
 
358
306
  await stopExecution(pi, ctx, "restore-check");
359
- assert.equal(recorded.models.at(-1)?.provider, "p");
360
- assert.equal(recorded.thinking, "high");
361
- });
362
-
363
- it("defers execution model restore until the first agent turn and restores planning model on completion", async () => {
364
- const workdir = freshWorkdir();
365
- const { pi, ctx, recorded } = makeHarness(workdir);
366
- const snapshot = {
367
- planPath: path.join(workdir, "PLAN_v7.md"),
368
- items: items("VC-001"),
369
- startedAt: "2026-08-25T00:00:00Z",
370
- modelState: { planningSelector: "p/m:high", executionSelector: "prov/other:xhigh" },
371
- };
372
- fs.writeFileSync(snapshot.planPath, "# plan");
373
- const entries = [{ type: "custom", customType: "pi-plans-exec", data: snapshot }];
374
-
375
- await restoreFromSession(pi, ctx, entries as any);
376
- assert.ok(getExecution());
377
307
  assert.deepEqual(recorded.models, []);
378
-
379
- await ensureExecutionModelActive(pi, ctx);
380
- assert.deepEqual(recorded.models, [{ provider: "prov", id: "other" }]);
381
- assert.equal(recorded.thinking, "xhigh");
382
-
383
- const doneEntries = [
384
- ...entries,
385
- { type: "message", message: { role: "assistant", content: [{ type: "text", text: "did [DONE:VC-001]" }] } },
386
- ];
387
- await restoreFromSession(pi, ctx, doneEntries as any);
388
- assert.equal(getExecution(), null);
389
- assert.equal(recorded.models.at(-1)?.provider, "p");
390
308
  assert.equal(recorded.thinking, "high");
391
309
  });
392
310
 
393
- it("prompts for the execution model when unset and confirms inherit current session model", async () => {
394
- const workdir = freshWorkdir();
395
- const { ctx, recorded } = makeHarness(workdir);
396
- initState(workdir);
397
- recorded.selectAnswer = "Inherit current session model";
398
- const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", {
399
- model_selector: null,
400
- source: "unset",
401
- updated_at: null,
402
- }, async (selector) => {
403
- setExecutionModel(workdir, { modelSelector: selector, source: "user" });
404
- });
405
-
406
- assert.equal(recorded.selects.length, 1);
407
- assert.equal(recorded.selects[0]?.options[0], "Inherit current session model");
408
- assert.equal(selection?.selector, "p/m:high");
409
- assert.equal(showConfig(workdir).execution.source, "user");
410
- assert.equal(showConfig(workdir).execution.model_selector, null);
411
- });
412
-
413
- it("does not prompt when the execution model is already confirmed as inherit", async () => {
414
- const workdir = freshWorkdir();
415
- const { ctx, recorded } = makeHarness(workdir);
416
- initState(workdir);
417
- setExecutionModel(workdir, { modelSelector: "inherit", source: "user" });
418
- const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", showConfig(workdir).execution);
419
-
420
- assert.equal(recorded.selects.length, 0);
421
- assert.equal(selection?.selector, "p/m:high");
422
- });
423
-
424
- it("continues on the current model with a warning when the selector is unavailable", async () => {
425
- const workdir = freshWorkdir();
426
- const { ctx, recorded } = makeHarness(workdir);
427
- (ctx as any).hasUI = false;
428
- const selection = await chooseExecutionModelSelection(ctx as any, "Execution model", {
429
- model_selector: "prov/missing:xhigh",
430
- source: "user",
431
- updated_at: null,
432
- });
433
-
434
- assert.equal(selection?.provider, "p");
435
- assert.ok(recorded.notifies.some((n) => n.severity === "warning" && n.message.includes("prov/missing")));
436
- });
437
-
438
- it("keeps the status bar count-free while executing and points at the panel", async () => {
311
+ it("keeps the execution status bar current while executing", async () => {
439
312
  const workdir = freshWorkdir();
440
313
  const { pi, ctx, recorded } = makeHarness(workdir);
441
314
  await startExecution(pi, ctx, path.join(workdir, "PLAN_v2.md"), items("VC-001", "VC-002"));
@@ -519,32 +392,6 @@ describe("execution loop", () => {
519
392
  assert.equal(getExecution(), null);
520
393
  });
521
394
 
522
- it("defers persistence and widget churn when toggling mid-turn", async () => {
523
- const workdir = freshWorkdir();
524
- const { pi, ctx, recorded } = makeHarness(workdir);
525
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v3.md"), items("VC-001", "VC-002"));
526
-
527
- const entriesBefore = recorded.entries.length;
528
- const factoriesBefore = recorded.widgetCalls;
529
-
530
- // Mid-turn: flip must be pure memory — no session writes, no re-register.
531
- (ctx as any).isIdle = () => false;
532
- assert.equal(toggleExecutionPanelView(pi, ctx), true);
533
- assert.equal(recorded.entries.length, entriesBefore, "busy toggle appended a session entry");
534
- assert.equal(recorded.widgetCalls, factoriesBefore, "busy toggle re-registered the widget");
535
- assert.equal(consumePendingPanelSync(), true, "expected a pending panel sync marker");
536
- assert.equal(consumePendingPanelSync(), false, "marker should be consumed exactly once");
537
-
538
- // Back to idle: the next toggle persists and syncs without touching the
539
- // widget slot because the panel is collapsed again.
540
- ctx.isIdle = () => true;
541
- assert.equal(toggleExecutionPanelView(pi, ctx), false);
542
- assert.ok(recorded.entries.length > entriesBefore, "idle toggle did not persist");
543
- assert.equal(recorded.widgetCalls, factoriesBefore, "collapsed toggle churned the widget registration");
544
-
545
- await stopExecution(pi, ctx, "test-done");
546
- });
547
-
548
395
  it("lets Pi core own execution compaction scheduling and customizes every reason", async () => {
549
396
  const workdir = freshWorkdir();
550
397
  const { pi, ctx, setUsagePercent } = makeHarness(workdir);
@@ -599,15 +446,73 @@ describe("execution loop", () => {
599
446
  await stopExecution(pi, ctx, "test-done");
600
447
  });
601
448
 
602
- it("does not install an execution threshold trigger in turn_end", () => {
449
+ it("triggers execution compaction at the high watermark, suppresses repeats, and re-arms below the low watermark", async () => {
450
+ const workdir = freshWorkdir();
451
+ const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
452
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v9.md"), items("VC-001", "VC-002"));
453
+
454
+ setUsagePercent(96);
455
+ handleExecutionTurnCompaction(ctx);
456
+ assert.equal(recorded.compacts?.length ?? 0, 1, "first high-watermark turn should request one compact");
457
+
458
+ handleExecutionTurnCompaction(ctx);
459
+ assert.equal(recorded.compacts?.length ?? 0, 1, "in-flight compact must not be requested twice");
460
+
461
+ handleExecutionCompact(pi, ctx, {
462
+ type: "session_compact",
463
+ compactionEntry: { type: "compaction" } as never,
464
+ fromExtension: true,
465
+ reason: "threshold",
466
+ willRetry: false,
467
+ });
468
+ assert.equal(
469
+ recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length,
470
+ 1,
471
+ "non-retry execution compaction should queue one hidden resume",
472
+ );
473
+
474
+ setUsagePercent(96);
475
+ handleExecutionTurnCompaction(ctx);
476
+ assert.equal(recorded.compacts?.length ?? 0, 1, "resume guard should suppress the immediate follow-up turn");
477
+
478
+ setUsagePercent(79);
479
+ handleExecutionTurnCompaction(ctx);
480
+ assert.equal(recorded.compacts?.length ?? 0, 1, "low-watermark turns stay quiet while below re-arm");
481
+
482
+ setUsagePercent(96);
483
+ handleExecutionTurnCompaction(ctx);
484
+ assert.equal(recorded.compacts?.length ?? 0, 2, "high watermark should re-arm after the low-watermark drop");
485
+
486
+ await stopExecution(pi, ctx, "test-done");
487
+ });
488
+
489
+ it("skips proactive current-I compaction when no eligible prefix exists", async () => {
490
+ const workdir = freshWorkdir();
491
+ const { pi, ctx, recorded } = makeHarness(workdir);
492
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v22.md"), items("VC-001"), [
493
+ { id: "I-001", text: "First item." },
494
+ ]);
495
+ (ctx.sessionManager as any).getBranch = () => [
496
+ { id: "only", type: "message", tokens: 30000, message: { role: "assistant", content: [{ type: "text", text: "[I-001:current] one oversized turn" }] } },
497
+ ];
498
+ handleExecutionTurnCompaction(ctx);
499
+ assert.equal(recorded.compacts?.length ?? 0, 0);
500
+ await stopExecution(pi, ctx, "no-prefix");
501
+ });
502
+ it("wires execution compaction without restoring execution model helpers", () => {
603
503
  const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
604
504
  const execSource = fs.readFileSync(path.join(process.cwd(), "src/exec.ts"), "utf8");
605
- const turnEndStart = execSource.indexOf('pi.on("turn_end"');
606
- const nextSection = execSource.indexOf("type CompactBranchEntry", turnEndStart);
607
- assert.match(indexSource, /registerExecutionTurnHandlers\(pi/);
608
- assert.ok(turnEndStart >= 0);
609
- assert.ok(nextSection > turnEndStart);
610
- assert.doesNotMatch(execSource.slice(turnEndStart, nextSection), /requestExecutionCompaction|shouldTriggerExecutionCompaction|ctx\.compact/);
505
+ assert.match(indexSource, /handleExecutionTurnCompaction/);
506
+ assert.match(execSource, /shouldTriggerExecutionCompaction/);
507
+ assert.match(execSource, /requestExecutionCompaction/);
508
+ assert.doesNotMatch(
509
+ indexSource,
510
+ /setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel/,
511
+ );
512
+ assert.doesNotMatch(
513
+ execSource,
514
+ /setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel/,
515
+ );
611
516
  });
612
517
 
613
518
  it("queues one non-retry resume, skips overflow retry, and keeps execution after failure", async () => {
@@ -708,7 +613,48 @@ describe("execution loop", () => {
708
613
  await stopExecution(pi, ctx, "test-done");
709
614
  });
710
615
 
711
- it("filters the hidden resume message out of the LLM context payload", () => {
616
+ it("uses the current model for bounded valid summaries and falls back on invalid output", async () => {
617
+ const workdir = freshWorkdir();
618
+ const { pi, ctx, recorded } = makeHarness(workdir);
619
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v21.md"), items("VC-001"), [
620
+ { id: "I-001", text: "First item." },
621
+ ]);
622
+ const signal = new AbortController().signal;
623
+ const calls: unknown[][] = [];
624
+ let response: any = {
625
+ stopReason: "stop",
626
+ content: [{ type: "text", text: "## Implementation Items\n- I-001\n\n## Current I\n- I-001\n\n## Read Records\n- none\n\n## Compaction Boundary\n- a-2\n\n## Decisions\n- preserved\n\n## Open Questions\n- none\n\n## Next Steps\n- continue" }],
627
+ usage: { input: 12, output: 34 },
628
+ };
629
+ (ctx.modelRegistry as any).complete = async (...args: unknown[]) => {
630
+ calls.push(args);
631
+ return response;
632
+ };
633
+ const event: any = {
634
+ type: "session_before_compact",
635
+ preparation: makePreparation("threshold", null),
636
+ branchEntries: [
637
+ { id: "i-1", type: "message", tokens: 100, message: { role: "assistant", content: [{ type: "text", text: "[I-001:current] work" }] } },
638
+ { id: "a-1", type: "message", tokens: 100, message: { role: "assistant", content: [{ type: "text", text: "details" }] } },
639
+ ],
640
+ reason: "threshold",
641
+ willRetry: false,
642
+ signal,
643
+ };
644
+ const valid = await handleExecutionBeforeCompact(pi, ctx, event);
645
+ assert.ok(valid?.compaction);
646
+ assert.equal(calls[0]?.[0], recorded.current);
647
+ assert.equal((calls[0]?.[2] as any).signal, signal);
648
+ assert.equal((calls[0]?.[2] as any).cacheRetention, "none");
649
+ assert.ok((calls[0]?.[2] as any).maxTokens <= 2048);
650
+ assert.deepEqual(valid?.compaction?.usage, response.usage);
651
+ assert.equal((valid?.compaction?.details as any)?.metrics?.summaryTokens, Math.ceil(response.content[0].text.length / 4));
652
+
653
+ response = { stopReason: "length", content: [{ type: "text", text: "## Implementation Items\npartial" }] };
654
+ const invalid = await handleExecutionBeforeCompact(pi, ctx, { ...event, signal: new AbortController().signal });
655
+ assert.equal(invalid, undefined, "incomplete model output must return control to Pi default compaction");
656
+ await stopExecution(pi, ctx, "model-test");
657
+ }); it("filters the hidden resume message out of the LLM context payload", () => {
712
658
  const messages = [
713
659
  { customType: "user", content: "real prompt" },
714
660
  { customType: "pi-plans-exec-resume", content: "Continue execution." },
@@ -910,63 +856,10 @@ describe("execution loop", () => {
910
856
  assert.equal(lastComplete.items?.[0]?.done, true, "completion snapshot missing final done state");
911
857
  });
912
858
 
913
- it("plans-list groups by implementation item with status tags and strikethrough", async () => {
914
- const workdir = freshWorkdir();
915
- const { pi, ctx, recorded } = makeHarness(workdir);
916
- const implItems = [
917
- { id: "I-001", text: "Add state helpers; evaluate percent." },
918
- { id: "I-002", text: "Wire the turn_end trigger and status bar." },
919
- { id: "I-003", text: "Zero coverage orphan item." },
920
- ];
921
- const vcItems: CheckItem[] = [
922
- { id: "VC-001", text: "`VC-001` covers `I-001`; pass condition: tests", done: false },
923
- { id: "VC-002", text: "`VC-002` covers `I-001` and `I-002`; pass condition: lint", done: false },
924
- ];
925
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v14.md"), vcItems, undefined, implItems);
926
-
927
- // Status bar counts by I, excluding the zero-coverage orphan.
928
- updateStatusWidget(ctx);
929
- assert.match(recorded.status ?? "", /⌛ plans 0\/2: spent/);
930
-
931
- toggleExecutionPanelView(pi, ctx);
932
- assert.ok(recorded.widget);
933
- // Expanded: footer ⌛ line cleared — the panel renders its own header.
934
- assert.equal(recorded.status, undefined);
935
- const widget = recorded.widget!.factory(
936
- {} as any,
937
- { fg: (_c: string, t: string) => t, strikethrough: (t: string) => `~~${t}~~` },
938
- );
939
- const lines = widget.render(120).join("\n");
940
- assert.match(lines, /⌛ plans 0\/2: spent/);
941
- assert.match(lines, /\[Implementing\] I-001: Add state helpers/);
942
- assert.match(lines, /\[Pending\] I-002: Wire the turn_end trigger/);
943
- assert.match(lines, /\[Pending\] I-003: Zero coverage orphan item/);
944
- // Only I status rows — no VC sub-rows in grouped mode.
945
- assert.doesNotMatch(lines, /☐/);
946
- assert.doesNotMatch(lines, /`VC-001` covers/);
947
-
948
- // Markers move the states; fan-out advances both covered I items.
949
- applyImplMarkers("[I-001:implemented]");
950
- applyDoneMarkers("[DONE:VC-001]");
951
- applyDoneMarkers("[DONE:VC-002]");
952
- widget.invalidate();
953
- const rendered = widget.render(120).join("\n");
954
- assert.match(rendered, /~~\[VC passed\] I-001: Add state helpers~~/);
955
- assert.match(rendered, /~~\[VC passed\] I-002: Wire the turn_end trigger and status bar\.~~/);
956
-
957
- // Status bar now counts both I items as passed.
958
- updateStatusWidget(ctx);
959
- assert.equal(recorded.status, undefined, "expanded panel keeps the footer ⌛ line cleared");
960
-
961
- await stopExecution(pi, ctx, "done");
962
- // Panel cleared and execution over: the ⌛ line must not linger.
963
- assert.doesNotMatch(recorded.status ?? "", /⌛ plans/);
964
- });
965
-
966
859
  it("updates the status bar in real time on every turn without session writes", async () => {
967
860
  const workdir = freshWorkdir();
968
861
  const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
969
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v15.md"), items("VC-001", "VC-002"), undefined, [
862
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v15.md"), items("VC-001", "VC-002"), [
970
863
  { id: "I-001", text: "First item." },
971
864
  { id: "I-002", text: "Second item." },
972
865
  ]);
@@ -1003,19 +896,6 @@ describe("execution loop", () => {
1003
896
  });
1004
897
  assert.match(recorded.status ?? "", /⌛ plans 1\/3: spent/);
1005
898
  assert.match(recorded.status ?? "", /12 in-toks/);
1006
- assert.equal(recorded.widgetCalls, 0, "collapsed progress must not touch the widget slot");
1007
-
1008
- // Expanded progress is updated through the same event handler without
1009
- // re-registering the live widget.
1010
- toggleExecutionPanelView(pi, ctx);
1011
- const renderRequests = { count: 0 };
1012
- const widget = recorded.widget?.factory(
1013
- { requestRender: () => { renderRequests.count += 1; } },
1014
- { fg: (_color: string, text: string) => text, strikethrough: (text: string) => text },
1015
- );
1016
- assert.ok(widget);
1017
- assert.match(widget.render(100)[0] ?? "", /⌛ plans 1\/3: spent/);
1018
- const widgetCalls = recorded.widgetCalls;
1019
899
 
1020
900
  await emit("message_end", {
1021
901
  message: { role: "assistant", usage: { input: 8, output: 3 } },
@@ -1023,17 +903,15 @@ describe("execution loop", () => {
1023
903
  await emit("turn_end", {
1024
904
  message: { role: "assistant", content: [{ type: "text", text: "verified [DONE:VC-002]" }] },
1025
905
  });
1026
- assert.equal(recorded.status, undefined, "expanded execution keeps the footer indicator cleared");
1027
- assert.equal(recorded.widgetCalls, widgetCalls, "progress refresh must reuse the live widget");
1028
- assert.equal(renderRequests.count, 1, "expanded progress must request a TUI render");
1029
- assert.match(widget.render(100)[0] ?? "", /⌛ plans 2\/3: spent/);
906
+ assert.match(recorded.status ?? "", /⌛ plans 2\/3: spent/);
907
+ assert.match(recorded.status ?? "", /20 in-toks/);
1030
908
 
1031
909
  await stopExecution(pi, ctx, "event-chain-test");
1032
910
  });
1033
911
  it("applies impl markers with silent unknown ids and later-overwrite semantics", async () => {
1034
912
  const workdir = freshWorkdir();
1035
913
  const { pi, ctx } = makeHarness(workdir);
1036
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v16.md"), items("VC-001"), undefined, [
914
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v16.md"), items("VC-001"), [
1037
915
  { id: "I-001", text: "First item." },
1038
916
  ]);
1039
917
 
@@ -1045,6 +923,27 @@ describe("execution loop", () => {
1045
923
  await stopExecution(pi, ctx, "done");
1046
924
  });
1047
925
 
926
+ it("applies current-I markers to the live progress bar and snapshot", async () => {
927
+ const workdir = freshWorkdir();
928
+ const { pi, ctx, recorded, emit } = makeHarness(workdir);
929
+ registerExecutionTurnHandlers(pi);
930
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v20.md"), items("VC-001", "VC-002", "VC-003"), [
931
+ { id: "I-001", text: "First item." },
932
+ { id: "I-002", text: "Second item." },
933
+ { id: "I-003", text: "Third item." },
934
+ ]);
935
+
936
+ await emit("turn_end", {
937
+ message: { role: "assistant", content: [{ type: "text", text: "starting [I-003:current]" }] },
938
+ });
939
+ assert.match(recorded.status ?? "", /⌛ plans 2\/3: spent/);
940
+ assert.equal(getExecution()?.currentI, "I-003");
941
+ drainExecutionFlush(pi, ctx);
942
+ const snapshot = recorded.entries.filter((entry) => entry.customType === "pi-plans-exec").at(-1)?.data as { currentI?: string };
943
+ assert.equal(snapshot.currentI, "I-003");
944
+ assert.equal(applyCurrentIMarker("[I-999:current]"), false);
945
+ await stopExecution(pi, ctx, "current-I-test");
946
+ });
1048
947
  it("replays impl markers from post-snapshot messages on restore", async () => {
1049
948
  const workdir = freshWorkdir();
1050
949
  const { pi, ctx } = makeHarness(workdir);
@@ -1064,15 +963,21 @@ describe("execution loop", () => {
1064
963
  assert.equal(getExecution()?.implStatus?.["I-001"], "implemented");
1065
964
  });
1066
965
 
966
+
1067
967
  it("keeps the injection rules teaching the impl markers", async () => {
1068
968
  const workdir = freshWorkdir();
1069
969
  const { pi, ctx } = makeHarness(workdir);
1070
- await startExecution(pi, ctx, path.join(workdir, "PLAN_v18.md"), items("VC-001"), undefined, [
970
+ await startExecution(pi, ctx, path.join(workdir, "PLAN_v18.md"), items("VC-001"), [
1071
971
  { id: "I-001", text: "First item." },
1072
972
  ]);
1073
973
  const rules = executionContextMessage()!;
1074
974
  assert.match(rules, /\[I-001:implemented\]/);
1075
975
  assert.match(rules, /\[I-001:validating\]/);
976
+ assert.match(rules, /subprocess-backed verification/);
977
+ assert.match(rules, /waiting for/);
978
+ assert.match(rules, /5s\s*->\s*10s\s*->\s*20s\s*->\s*40s\s*->\s*80s/);
979
+ assert.match(rules, /keep polling at 80s/);
980
+ assert.match(rules, /restart at 5s for each new subprocess/);
1076
981
  await stopExecution(pi, ctx, "done");
1077
982
  });
1078
983
 
@@ -0,0 +1,65 @@
1
+ import * as assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { after, before, describe, it } from "node:test";
6
+
7
+ import { getExecution, startExecution, stopExecution } from "../src/exec.ts";
8
+
9
+ let tmpRoot: string;
10
+
11
+ before(() => {
12
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-execute-test-"));
13
+ });
14
+
15
+ after(() => {
16
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
17
+ });
18
+
19
+ function mkWorkdir(name: string): string {
20
+ const dir = path.join(tmpRoot, name);
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ describe("execute handoff", () => {
26
+ it("starts execution without switching models", async () => {
27
+ const workdir = mkWorkdir("handoff");
28
+ const recorded = {
29
+ messages: [] as string[],
30
+ entries: [] as Array<{ customType: string; data: unknown }>,
31
+ setModelCalls: 0,
32
+ };
33
+ const pi = {
34
+ appendEntry: (customType: string, data: unknown) => {
35
+ recorded.entries.push({ customType, data });
36
+ },
37
+ sendMessage: (message: { customType: string; content: string }) => {
38
+ recorded.messages.push(message.content);
39
+ },
40
+ setModel: async () => {
41
+ recorded.setModelCalls += 1;
42
+ return true;
43
+ },
44
+ };
45
+ const ctx = {
46
+ cwd: workdir,
47
+ ui: {
48
+ setStatus: () => {},
49
+ theme: {
50
+ fg: (_kind: string, text: string) => text,
51
+ bold: (text: string) => text,
52
+ },
53
+ },
54
+ } as any;
55
+
56
+ await startExecution(pi as any, ctx, path.join(workdir, "PLAN_v1.md"), [
57
+ { id: "VC-001", text: "first item", done: false },
58
+ ] as any);
59
+ assert.equal(recorded.setModelCalls, 0);
60
+ assert.ok(getExecution());
61
+
62
+ await stopExecution(pi as any, ctx, "cleanup");
63
+ assert.equal(getExecution(), null);
64
+ });
65
+ });
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { after, before, describe, it } from "node:test";
8
- import { latestPlanVersion, nextPlanVersionPath, parseChecklist, parseImplItems, resolveImplStatuses, scanDoneMarkers, scanImplMarkers, shortImplDescription, extractCoverage } from "../src/plan.ts";
8
+ import { latestPlanVersion, nextPlanVersionPath, parseChecklist, parseImplItems, resolveImplStatuses, scanCurrentIMarkers, scanDoneMarkers, scanImplMarkers, shortImplDescription, extractCoverage, resolveCurrentI, inferCurrentI } from "../src/plan.ts";
9
9
 
10
10
  const PLAN = `# PLAN_v1 - demo
11
11
 
@@ -148,6 +148,16 @@ describe("implementation items", () => {
148
148
  assert.deepEqual(extractCoverage("no coverage clause here"), []);
149
149
  });
150
150
 
151
+ it("parses and resolves current-I markers without changing progress marker states", () => {
152
+ const implItems = [{ id: "I-001", text: "first" }, { id: "I-002", text: "second" }];
153
+ assert.deepEqual(scanCurrentIMarkers("[I-001:current] [I-999:current] [I-002:current]"), [
154
+ { id: "I-001" },
155
+ { id: "I-999" },
156
+ { id: "I-002" },
157
+ ]);
158
+ assert.equal(resolveCurrentI(implItems, scanCurrentIMarkers("[I-999:current] [I-002:current]")), "I-002");
159
+ assert.equal(inferCurrentI(implItems, [], undefined), "I-001");
160
+ });
151
161
  it("scans impl markers", () => {
152
162
  assert.deepEqual(scanImplMarkers("[I-001:implemented] then [I-002:validating] and [I-003:done]"), [
153
163
  { id: "I-001", state: "implemented" },