pi-plans 0.3.2 → 0.3.3
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 +5 -3
- package/index.ts +92 -6
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +7 -1
- package/references/plan-artifact-template.md +4 -0
- package/references/state-and-config.md +19 -0
- package/src/autocomplete.ts +2 -1
- package/src/code-graph/commands.ts +21 -6
- package/src/compaction.ts +10 -0
- package/src/config-command.ts +2 -0
- package/src/exec.ts +256 -10
- package/src/guard.ts +7 -1
- package/src/resume-command.ts +450 -0
- package/src/resume.ts +205 -0
- package/src/run-context.ts +97 -0
- package/src/run-ownership.ts +310 -0
- package/src/state.ts +24 -1
- package/src/termination-prompt.ts +8 -0
- package/src/workflow-state.ts +1159 -0
- package/tests/ask-choice.test.ts +113 -0
- package/tests/compaction.test.ts +39 -0
- package/tests/exec.test.ts +250 -0
- package/tests/guard.test.ts +46 -0
- package/tests/plans.test.ts +71 -0
- package/tests/refine-resume.test.ts +324 -0
- package/tests/resume-lifecycle.test.ts +385 -0
- package/tests/resume.test.ts +384 -0
- package/tests/run-context.test.ts +119 -0
- package/tests/run-ownership.test.ts +170 -0
- package/tests/state.test.ts +16 -0
- package/tests/workflow-state.test.ts +432 -0
- package/tools/analyze-refs.ts +2 -1
- package/tools/ask-choice.ts +61 -3
- package/tools/code-graph.ts +5 -1
- package/tools/execute-plan.ts +2 -1
- package/tools/plans.ts +119 -0
- package/tools/refine.ts +91 -7
package/src/exec.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
12
13
|
import type {
|
|
13
14
|
CompactionResult,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
entryCurrentIMarkers,
|
|
26
27
|
formatVccCompactionStats,
|
|
27
28
|
loadVccSettings,
|
|
29
|
+
PLANNING_PREPLAN_COMPACT_HINT,
|
|
28
30
|
scaffoldVccSettings,
|
|
29
31
|
shouldScheduleAutoContinue,
|
|
30
32
|
type CompactionEntryLike,
|
|
@@ -34,13 +36,35 @@ import {
|
|
|
34
36
|
type VccCompactionBuildResult,
|
|
35
37
|
type VccCompactionStats,
|
|
36
38
|
} from "./compaction.ts";
|
|
37
|
-
import { getRun, readActive, resolveStateRootOrNull, setRunStatus, utcNow } from "./state.ts";
|
|
39
|
+
import { getRun, readActive, resolveStateRootOrNull, setRunStatus, StateError, utcNow } from "./state.ts";
|
|
40
|
+
import { bindRun, resolveActiveRun } from "./run-context.ts";
|
|
41
|
+
import { OwnershipError } from "./run-ownership.ts";
|
|
42
|
+
import {
|
|
43
|
+
applyExecutionApproved,
|
|
44
|
+
applyExecutionCompleted,
|
|
45
|
+
applyExecutionHeadChanged,
|
|
46
|
+
applyExecutionProgress,
|
|
47
|
+
applyExecutionStopped,
|
|
48
|
+
applyQuestionAsked,
|
|
49
|
+
applyReviewRoundStarted,
|
|
50
|
+
createCheckpoint,
|
|
51
|
+
loadCheckpoint,
|
|
52
|
+
mutateCheckpoint,
|
|
53
|
+
StaleCheckpointError,
|
|
54
|
+
planIdentityOf,
|
|
55
|
+
resolveHeadAt,
|
|
56
|
+
resolveWorktreeRoot,
|
|
57
|
+
sha256File,
|
|
58
|
+
type ExecutionApproval,
|
|
59
|
+
} from "./workflow-state.ts";
|
|
38
60
|
import { graphBlockForExecutor } from "./code-graph/prompts.ts";
|
|
39
61
|
import { resolveGraphMode } from "./code-graph/mode.ts";
|
|
40
|
-
import { TERMINATION_QUESTION, TERMINATION_OPTIONS, renderTerminationOptions } from "./termination-prompt.ts";
|
|
62
|
+
import { TERMINATION_QUESTION, TERMINATION_OPTIONS, TERMINATION_RECORDING_INSTRUCTIONS, renderTerminationOptions } from "./termination-prompt.ts";
|
|
41
63
|
import {
|
|
42
64
|
extractCoverage,
|
|
43
65
|
latestPlanVersion,
|
|
66
|
+
parseChecklist,
|
|
67
|
+
parseImplItems,
|
|
44
68
|
resolveImplStatuses,
|
|
45
69
|
scanDoneMarkers,
|
|
46
70
|
scanImplMarkers,
|
|
@@ -131,6 +155,105 @@ export function getExecution(): ExecState | null {
|
|
|
131
155
|
return execution;
|
|
132
156
|
}
|
|
133
157
|
|
|
158
|
+
export interface CheckpointExecutionLoad {
|
|
159
|
+
status: "loaded" | "no-execution" | "plan-missing" | "plan-mismatch" | "no-checkpoint" | "corrupt";
|
|
160
|
+
planPath?: string;
|
|
161
|
+
doneVcIds?: string[];
|
|
162
|
+
reverifyAll?: boolean;
|
|
163
|
+
pausedReason?: string;
|
|
164
|
+
error?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Shared restore primitive (I-005/I-006): load the executing state from a run
|
|
169
|
+
* checkpoint into THIS session. Authorization is kept only when the recorded
|
|
170
|
+
* approval matches the current plan digest; a HEAD change keeps the
|
|
171
|
+
* authorization but re-verifies previously verified VCs (D-011/F-001).
|
|
172
|
+
* F-002: the loaded state is persisted to the current session IMMEDIATELY so
|
|
173
|
+
* session_start/session_tree restore paths cannot silently clear it.
|
|
174
|
+
*/
|
|
175
|
+
export function loadExecutionFromCheckpoint(
|
|
176
|
+
pi: ExtensionAPI,
|
|
177
|
+
ctx: ExtensionContext,
|
|
178
|
+
runId: string,
|
|
179
|
+
): CheckpointExecutionLoad {
|
|
180
|
+
const load = loadCheckpoint(ctx.cwd, runId);
|
|
181
|
+
if (load.status === "missing") return { status: "no-checkpoint" };
|
|
182
|
+
if (load.status === "corrupt") return { status: "corrupt", error: load.error };
|
|
183
|
+
const cp = load.checkpoint;
|
|
184
|
+
if (!cp.execution || cp.phase !== "executing") return { status: "no-execution" };
|
|
185
|
+
const planPath = cp.plan?.path;
|
|
186
|
+
if (!planPath || !fs.existsSync(planPath)) {
|
|
187
|
+
return { status: "plan-missing", error: planPath ? `plan file vanished: ${planPath}` : "checkpoint has no plan identity" };
|
|
188
|
+
}
|
|
189
|
+
const planText = fs.readFileSync(planPath, "utf8");
|
|
190
|
+
// F-002 (implementation review): the recorded plan identity is over BYTES —
|
|
191
|
+
// an in-place edit at the same path must not inherit the authorization or
|
|
192
|
+
// the verified VCs. Refuse the load and require a fresh handoff.
|
|
193
|
+
if (sha256File(planPath) !== cp.plan.sha256) {
|
|
194
|
+
return {
|
|
195
|
+
status: "plan-mismatch" as const,
|
|
196
|
+
error: `plan file changed since the approval record (${planPath}); re-approve via /plans-execute before executing`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const items = parseChecklist(planText);
|
|
200
|
+
if (items.length === 0) {
|
|
201
|
+
return { status: "plan-missing", error: `${planPath} has no parsable verifier checklist` };
|
|
202
|
+
}
|
|
203
|
+
const implItems = parseImplItems(planText);
|
|
204
|
+
const doneIds = new Set(cp.execution.doneVcIds);
|
|
205
|
+
// D-011/F-001: an unchanged plan digest keeps the recorded authorization;
|
|
206
|
+
// a changed HEAD under it forces re-verification of previously verified VCs.
|
|
207
|
+
// F-006 (implementation review): an approval without a resolvable HEAD
|
|
208
|
+
// recorded an unverifiable code state — re-verify instead of trusting.
|
|
209
|
+
const headNow = resolveHeadAt(ctx.cwd);
|
|
210
|
+
const headUnverifiable = cp.execution.approval === null || cp.execution.approval.headAtApproval === null;
|
|
211
|
+
const headChanged =
|
|
212
|
+
cp.execution.approval !== null &&
|
|
213
|
+
cp.execution.approval.headAtApproval !== null &&
|
|
214
|
+
cp.execution.approval.headAtApproval !== headNow;
|
|
215
|
+
const reverifyAll = cp.execution.reverifyAll === true || headChanged || headUnverifiable;
|
|
216
|
+
if (!reverifyAll) {
|
|
217
|
+
for (const item of items) {
|
|
218
|
+
if (doneIds.has(item.id)) item.done = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
execution = {
|
|
222
|
+
planPath,
|
|
223
|
+
items,
|
|
224
|
+
startedAt: utcNow(),
|
|
225
|
+
usage: { inToks: cp.execution.usage.inToks, outToks: cp.execution.usage.outToks },
|
|
226
|
+
implItems,
|
|
227
|
+
implStatus: { ...cp.execution.implStatus },
|
|
228
|
+
currentI: cp.execution.currentI,
|
|
229
|
+
goalWait: {
|
|
230
|
+
noProgressRounds: 0,
|
|
231
|
+
waitRounds: 0,
|
|
232
|
+
lastMarkers: null,
|
|
233
|
+
paused: cp.execution.pausedReason !== undefined,
|
|
234
|
+
pausedReason: cp.execution.pausedReason,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
executionRunId = runId;
|
|
238
|
+
bindRun(ctx.sessionManager, ctx.cwd, runId);
|
|
239
|
+
resetGoalWaitRuntime(ctx);
|
|
240
|
+
pendingExecutionFlush = false; // restored state: no inherited flush debt
|
|
241
|
+
resetExecutionCompactionState(ctx);
|
|
242
|
+
if (headChanged) {
|
|
243
|
+
withExecutionCheckpoint(ctx, (current) => applyExecutionHeadChanged(current));
|
|
244
|
+
}
|
|
245
|
+
if (execution.goalWait) execution.goalWait.lastMarkers = goalWaitSnapshot();
|
|
246
|
+
persist(pi); // F-002: immediate session snapshot
|
|
247
|
+
updateStatusWidget(ctx);
|
|
248
|
+
return {
|
|
249
|
+
status: "loaded",
|
|
250
|
+
planPath,
|
|
251
|
+
doneVcIds: [...doneIds],
|
|
252
|
+
reverifyAll,
|
|
253
|
+
pausedReason: cp.execution.pausedReason,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
134
257
|
const EXECUTION_COMPACTION_RESUME_MESSAGE = "Continue execution.";
|
|
135
258
|
|
|
136
259
|
interface ExecutionCompactionState {
|
|
@@ -255,7 +378,7 @@ export function updateStatusWidget(ctx: ExtensionContext): void {
|
|
|
255
378
|
ctx.ui.setStatus("pi-plans", ctx.ui.theme.fg("accent", line));
|
|
256
379
|
return;
|
|
257
380
|
}
|
|
258
|
-
const active =
|
|
381
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
259
382
|
if (active) {
|
|
260
383
|
// Idle indicator depends on the run's lifecycle, not just its existence:
|
|
261
384
|
// done reads as finished, abandoned as closed, stopped/accepted as paused.
|
|
@@ -302,6 +425,24 @@ function persist(pi: ExtensionAPI): void {
|
|
|
302
425
|
});
|
|
303
426
|
}
|
|
304
427
|
|
|
428
|
+
/** Checkpoint bookkeeping for the executing run; best-effort for legacy runs
|
|
429
|
+
* without checkpoints (their cross-session resume degrades to R-008 rules). */
|
|
430
|
+
function withExecutionCheckpoint(ctx: ExtensionContext, mutator: (cp: import("./workflow-state.ts").WorkflowCheckpoint) => import("./workflow-state.ts").WorkflowCheckpoint): void {
|
|
431
|
+
if (!execution) return;
|
|
432
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
433
|
+
if (!active || active.run_id !== executionRunId) return;
|
|
434
|
+
try {
|
|
435
|
+
mutateCheckpoint(ctx.cwd, active.run_id, mutator);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
// F-005 (implementation review): ownership loss and revision staleness
|
|
438
|
+
// must stop the advance, not vanish into the catch block.
|
|
439
|
+
if (error instanceof OwnershipError || error instanceof StaleCheckpointError) throw error;
|
|
440
|
+
/* legacy run or corrupt checkpoint: session snapshot still carries the loop */
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let executionRunId: string | null = null;
|
|
445
|
+
|
|
305
446
|
export async function startExecution(
|
|
306
447
|
pi: ExtensionAPI,
|
|
307
448
|
ctx: ExtensionContext,
|
|
@@ -317,8 +458,40 @@ export async function startExecution(
|
|
|
317
458
|
pendingExecutionFlush = false; // fresh run: no inherited flush debt
|
|
318
459
|
resetExecutionCompactionState(ctx);
|
|
319
460
|
persist(pi);
|
|
320
|
-
const active =
|
|
461
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
462
|
+
executionRunId = active?.run_id ?? null;
|
|
321
463
|
if (active) {
|
|
464
|
+
bindRun(ctx.sessionManager, ctx.cwd, active.run_id);
|
|
465
|
+
// I-005: durable approval evidence — run + plan digest + HEAD at
|
|
466
|
+
// approval (D-003/D-011). Sets phase executing via the state machine.
|
|
467
|
+
try {
|
|
468
|
+
const load = loadCheckpoint(ctx.cwd, active.run_id);
|
|
469
|
+
if (load.status === "missing") {
|
|
470
|
+
createCheckpoint(ctx.cwd, { runId: active.run_id, originWorkdir: ctx.cwd, workdir: ctx.cwd });
|
|
471
|
+
}
|
|
472
|
+
const approval: ExecutionApproval = {
|
|
473
|
+
plan: planIdentityOf(path.resolve(planPath), 1),
|
|
474
|
+
worktree: resolveWorktreeRoot(ctx.cwd) ?? path.resolve(ctx.cwd),
|
|
475
|
+
headAtApproval: resolveHeadAt(ctx.cwd),
|
|
476
|
+
approvedAt: utcNow(),
|
|
477
|
+
};
|
|
478
|
+
mutateCheckpoint(ctx.cwd, active.run_id, (cp) => {
|
|
479
|
+
// Plan refinement may not have recorded the plan identity yet.
|
|
480
|
+
const withPlan = cp.plan === null ? { ...cp, plan: approval.plan } : cp;
|
|
481
|
+
// The checkpoint may not carry accept-execute (legacy flow);
|
|
482
|
+
// approval here came from the explicit handoff confirmation.
|
|
483
|
+
const aligned = withPlan.nextAction === "accept-execute"
|
|
484
|
+
? withPlan
|
|
485
|
+
: { ...withPlan, nextAction: "accept-execute" as const };
|
|
486
|
+
return applyExecutionApproved(aligned, approval);
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
// F-002 (implementation review): a plan-digest mismatch between the
|
|
490
|
+
// recorded checkpoint plan and the approval must fail closed and
|
|
491
|
+
// visibly — never silently execute without durable approval.
|
|
492
|
+
if (error instanceof StateError && /does not match/.test(error.message)) throw error;
|
|
493
|
+
/* legacy/corrupt checkpoint: run status still transitions below */
|
|
494
|
+
}
|
|
322
495
|
try {
|
|
323
496
|
setRunStatus(ctx.cwd, active.run_id, "executing");
|
|
324
497
|
} catch {
|
|
@@ -348,10 +521,30 @@ export function recordExecutionTurn(
|
|
|
348
521
|
execution.usage.inToks += usage.input;
|
|
349
522
|
execution.usage.outToks += usage.output;
|
|
350
523
|
}
|
|
524
|
+
// I-005: mirror progress into the run checkpoint so a different session
|
|
525
|
+
// can resume with the verified VC/I set (R-004).
|
|
526
|
+
withExecutionCheckpoint(_ctx, (cp) =>
|
|
527
|
+
applyExecutionProgress(cp, {
|
|
528
|
+
doneVcIds: execution!.items.filter((item) => item.done).map((item) => item.id),
|
|
529
|
+
implStatus: implStatusSnapshot(),
|
|
530
|
+
currentI: execution!.currentI,
|
|
531
|
+
usage: usage ? { inToks: usage.input, outToks: usage.output } : undefined,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
351
534
|
requestExecutionFlush(pi, _ctx);
|
|
352
535
|
updateStatusWidget(_ctx);
|
|
353
536
|
}
|
|
354
537
|
|
|
538
|
+
function implStatusSnapshot(): Record<string, string> {
|
|
539
|
+
const snapshot: Record<string, string> = {};
|
|
540
|
+
if (!execution?.implItems) return snapshot;
|
|
541
|
+
for (const item of execution.implItems) {
|
|
542
|
+
const state = execution.implStatus?.[item.id];
|
|
543
|
+
if (state) snapshot[item.id] = state;
|
|
544
|
+
}
|
|
545
|
+
return snapshot;
|
|
546
|
+
}
|
|
547
|
+
|
|
355
548
|
export function registerExecutionTurnHandlers(
|
|
356
549
|
pi: ExtensionAPI,
|
|
357
550
|
onTurnEnd?: (ctx: ExtensionContext) => Promise<void> | void,
|
|
@@ -380,6 +573,7 @@ export function registerExecutionTurnHandlers(
|
|
|
380
573
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
381
574
|
drainExecutionFlush(pi, ctx);
|
|
382
575
|
execution = null;
|
|
576
|
+
executionRunId = null;
|
|
383
577
|
goalWaitRuntime = null;
|
|
384
578
|
lastAssistantUsage = null;
|
|
385
579
|
});
|
|
@@ -428,7 +622,7 @@ const EXECUTION_RESUME_CUSTOM_TYPE = "pi-plans-exec-resume";
|
|
|
428
622
|
function activeVccSettings(ctx: ExtensionContext, phase: PiPlansCompactionPhase): { settings: PiPlansVccSettings; runId: string; artifactDir: string } | null {
|
|
429
623
|
const stateRoot = resolveStateRootOrNull(ctx.cwd);
|
|
430
624
|
if (!stateRoot) return null;
|
|
431
|
-
const active =
|
|
625
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
432
626
|
if (!active) return null;
|
|
433
627
|
const run = getRun(ctx.cwd, active.run_id);
|
|
434
628
|
if (!run) return null;
|
|
@@ -623,6 +817,49 @@ export const PLANNING_RUN_START_CUSTOM_TYPE = "pi-plans-run-start";
|
|
|
623
817
|
export const PLANNING_PLAN_WRITTEN_CUSTOM_TYPE = "pi-plans-plan-written";
|
|
624
818
|
const PLANNING_RESUME_CUSTOM_TYPE = "pi-plans-plan-resume";
|
|
625
819
|
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
// Pre-plan compaction: right after `plans start-run` creates a new planning
|
|
822
|
+
// run, the extension triggers one VCC compaction so the new plan starts on a
|
|
823
|
+
// lean context (LLM reasoning degrades with longer context; see PLAN
|
|
824
|
+
// preplan-compact). The pending flag is session-scoped and opportunistic: it
|
|
825
|
+
// is set by the start-run tool case and consumed by the plans tool_result
|
|
826
|
+
// hook in index.ts, which requests the extension-context compact action and
|
|
827
|
+
// resumes planning exactly once regardless of success or failure.
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
export { PLANNING_PREPLAN_COMPACT_HINT };
|
|
831
|
+
export const PLANNING_PREPLAN_RESUME_CUSTOM_TYPE = "pi-plans-preplan-resume";
|
|
832
|
+
|
|
833
|
+
interface PrePlanCompactPending {
|
|
834
|
+
runId: string;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export function markPrePlanCompactPending(ctx: ExtensionContext, runId: string): void {
|
|
838
|
+
const session = ctx.sessionManager as unknown as { __piPlansPrePlanCompact?: PrePlanCompactPending | null };
|
|
839
|
+
session.__piPlansPrePlanCompact = { runId };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function consumePrePlanCompactPending(ctx: ExtensionContext): PrePlanCompactPending | null {
|
|
843
|
+
const session = ctx.sessionManager as unknown as { __piPlansPrePlanCompact?: PrePlanCompactPending | null };
|
|
844
|
+
const pending = session.__piPlansPrePlanCompact ?? null;
|
|
845
|
+
session.__piPlansPrePlanCompact = null;
|
|
846
|
+
return pending;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Hidden resume message after the pre-plan compaction settles (success or
|
|
850
|
+
* failure): Pi's manual compaction never continues the aborted turn, so the
|
|
851
|
+
* planning workflow is continued exactly once from here. */
|
|
852
|
+
export function sendPrePlanCompactResume(pi: ExtensionAPI): void {
|
|
853
|
+
pi.sendMessage?.(
|
|
854
|
+
{
|
|
855
|
+
customType: PLANNING_PREPLAN_RESUME_CUSTOM_TYPE,
|
|
856
|
+
content: "Continue planning.",
|
|
857
|
+
display: false,
|
|
858
|
+
},
|
|
859
|
+
{ triggerTurn: true },
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
626
863
|
interface PlanningCompactionState {
|
|
627
864
|
inFlight: boolean;
|
|
628
865
|
resumeGuard: boolean;
|
|
@@ -752,8 +989,11 @@ export function refreshPlanningCompactionCooldown(_ctx: ExtensionContext): void
|
|
|
752
989
|
}
|
|
753
990
|
|
|
754
991
|
export function requestPlanningCompaction(_ctx: ExtensionContext): void {
|
|
755
|
-
//
|
|
992
|
+
// Generic proactive pi-plans compaction is intentionally disabled. Manual,
|
|
756
993
|
// threshold, and overflow compactions are handled by session_before_compact.
|
|
994
|
+
// The single exception is the pre-plan compaction: index.ts requests the
|
|
995
|
+
// extension-context compact action from the plans tool_result hook right
|
|
996
|
+
// after start-run (see PLANNING_PREPLAN_COMPACT_HINT).
|
|
757
997
|
}
|
|
758
998
|
|
|
759
999
|
function buildPlanningVccResult(event: SessionBeforeCompactEvent, ctx: ExtensionContext): VccCompactionBuildResult | null {
|
|
@@ -887,7 +1127,7 @@ export function handlePlanningCompactFailed(pi: ExtensionAPI, ctx: ExtensionCont
|
|
|
887
1127
|
}
|
|
888
1128
|
|
|
889
1129
|
export function filterPlanningResumeMessages<T extends { customType?: string }>(messages: T[]): T[] {
|
|
890
|
-
return messages.filter((message) => message.customType !== PLANNING_RESUME_CUSTOM_TYPE);
|
|
1130
|
+
return messages.filter((message) => message.customType !== PLANNING_RESUME_CUSTOM_TYPE && message.customType !== PLANNING_PREPLAN_RESUME_CUSTOM_TYPE);
|
|
891
1131
|
}
|
|
892
1132
|
|
|
893
1133
|
export async function stopExecution(pi: ExtensionAPI, ctx: ExtensionContext, reason: string): Promise<void> {
|
|
@@ -896,7 +1136,10 @@ export async function stopExecution(pi: ExtensionAPI, ctx: ExtensionContext, rea
|
|
|
896
1136
|
// Final synchronous write: drain any deferred flush and land the last snapshot.
|
|
897
1137
|
pendingExecutionFlush = false;
|
|
898
1138
|
persist(pi);
|
|
1139
|
+
// Checkpoint first: withExecutionCheckpoint guards on the live execution.
|
|
1140
|
+
withExecutionCheckpoint(ctx, (cp) => applyExecutionStopped(cp, reason));
|
|
899
1141
|
execution = null;
|
|
1142
|
+
executionRunId = null;
|
|
900
1143
|
goalWaitRuntime = null;
|
|
901
1144
|
pi.appendEntry("pi-plans-exec-cleared", { reason });
|
|
902
1145
|
pi.sendMessage(
|
|
@@ -907,7 +1150,7 @@ export async function stopExecution(pi: ExtensionAPI, ctx: ExtensionContext, rea
|
|
|
907
1150
|
},
|
|
908
1151
|
{ triggerTurn: false },
|
|
909
1152
|
);
|
|
910
|
-
const active =
|
|
1153
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
911
1154
|
if (active) {
|
|
912
1155
|
try {
|
|
913
1156
|
setRunStatus(ctx.cwd, active.run_id, "stopped");
|
|
@@ -1105,7 +1348,10 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1105
1348
|
|
|
1106
1349
|
const summary = execution.items.map((item) => `- ✅ \`${item.id}\` ${item.text.split(";")[0]}`).join("\n");
|
|
1107
1350
|
const planPath = execution.planPath;
|
|
1351
|
+
// Checkpoint first (live-execution guard), then clear the session state.
|
|
1352
|
+
withExecutionCheckpoint(ctx, (cp) => applyExecutionCompleted(cp));
|
|
1108
1353
|
execution = null;
|
|
1354
|
+
executionRunId = null;
|
|
1109
1355
|
goalWaitRuntime = null;
|
|
1110
1356
|
pi.appendEntry("pi-plans-exec-cleared", { reason: "complete" });
|
|
1111
1357
|
// Post-execution goal-running continuation: in interactive sessions, attach
|
|
@@ -1128,7 +1374,7 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1128
1374
|
if (interactive) {
|
|
1129
1375
|
pi.appendEntry("pi-plans-ameliorate", { planPath, phase: "goal-started", rounds: null, currentRound: 0 });
|
|
1130
1376
|
}
|
|
1131
|
-
const active =
|
|
1377
|
+
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
1132
1378
|
if (active) {
|
|
1133
1379
|
try {
|
|
1134
1380
|
setRunStatus(ctx.cwd, active.run_id, "done");
|
|
@@ -1144,7 +1390,7 @@ export async function completeExecution(pi: ExtensionAPI, ctx: ExtensionContext)
|
|
|
1144
1390
|
* implementation-review loop. Termination options are single-sourced from
|
|
1145
1391
|
* src/termination-prompt.ts (shared with the ask_choice trailing branch). */
|
|
1146
1392
|
export const AMELIORATION_PROMPT_TEXT = `---
|
|
1147
|
-
Goal-running continuation: immediately ask the user now via ask_choice (autoComplete: false, in the session language) the termination question: "${TERMINATION_QUESTION}" Options (recommended first): ${renderTerminationOptions()}. Then keep running the implementation-review loop without asking whether to continue; the goal-wait option keeps the loop running until no unpassed VCs remain.`;
|
|
1393
|
+
Goal-running continuation: immediately ask the user now via ask_choice (autoComplete: false, in the session language) the termination question: "${TERMINATION_QUESTION}" Options (recommended first): ${renderTerminationOptions()}. ${TERMINATION_RECORDING_INSTRUCTIONS} Then keep running the implementation-review loop without asking whether to continue; the goal-wait option keeps the loop running until no unpassed VCs remain.`;
|
|
1148
1394
|
|
|
1149
1395
|
/** Injection text for before_agent_start while executing. */
|
|
1150
1396
|
export function executionContextMessage(ctx: ExtensionContext): string | null {
|
package/src/guard.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { getRun, loadConfig, readActive, resolveStateRootOrNull } from "./state.ts";
|
|
11
|
+
import { activeInfoById } from "./run-context.ts";
|
|
11
12
|
|
|
12
13
|
const GUARDED_TOOLS = new Set(["write", "edit"]);
|
|
13
14
|
const GUARDED_STATUSES = new Set(["planning", "accepted"]);
|
|
@@ -16,12 +17,17 @@ export interface GuardInput {
|
|
|
16
17
|
workdir: string;
|
|
17
18
|
toolName: string;
|
|
18
19
|
rawPath: string;
|
|
20
|
+
/** Session-bound run id (I-002); when provided it overrides the shared active pointer. null = no binding. */
|
|
21
|
+
activeRunId?: string | null;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/** Returns a block reason when the write must be blocked, or null when allowed. */
|
|
22
25
|
export function planningWriteBlockReason(input: GuardInput): string | null {
|
|
23
26
|
if (!GUARDED_TOOLS.has(input.toolName)) return null;
|
|
24
|
-
const active =
|
|
27
|
+
const active =
|
|
28
|
+
input.activeRunId !== undefined && input.activeRunId !== null
|
|
29
|
+
? activeInfoById(input.workdir, input.activeRunId)
|
|
30
|
+
: readActive(input.workdir);
|
|
25
31
|
if (!active) return null;
|
|
26
32
|
const run = getRun(input.workdir, active.run_id);
|
|
27
33
|
if (!run || !GUARDED_STATUSES.has(run.status)) return null;
|