@linimin/pi-letscook 0.1.45 → 0.1.47

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.
@@ -3,12 +3,81 @@ import * as fs from "node:fs";
3
3
  import { promises as fsp } from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
- import * as roleReporting from "./role-reporting.js";
7
6
  import { StringEnum } from "@mariozechner/pi-ai";
8
7
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
- import { DynamicBorder, parseFrontmatter } from "@mariozechner/pi-coding-agent";
10
- import { Container, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
8
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
9
+ import { Container, matchesKey, SelectList, Text } from "@mariozechner/pi-tui";
11
10
  import { Type } from "typebox";
11
+ import {
12
+ autoContinueWorkflowIfNeeded,
13
+ completionContinuationFingerprint,
14
+ markQueuedDriverPromptInFlight,
15
+ registerCookCommand,
16
+ } from "./driver";
17
+ import {
18
+ assessMissionAnchor,
19
+ collectRecentDiscussionEntries,
20
+ deriveCookContextProposalFromRecentDiscussion,
21
+ finalizeContextProposalAnalysis,
22
+ isWeakMissionAnchor,
23
+ missionAnchorsLikelyEquivalent,
24
+ missionAnchorsStrictlyEquivalent,
25
+ normalizeMissionAnchorText,
26
+ resolveContextProposalConfirmationAction,
27
+ shouldTreatBareActiveWorkflowProposalAsClearRefocus,
28
+ stripCodeBlocks,
29
+ } from "./proposal";
30
+ import type {
31
+ ContextProposal,
32
+ ContextProposalAnalysis,
33
+ ContextProposalConfirmAction,
34
+ ContextProposalConfirmOptions,
35
+ ContextProposalConfirmationLayout,
36
+ ContextProposalDecision,
37
+ } from "./proposal";
38
+ import {
39
+ buildContextProposalConfirmationLayout as buildExtractedContextProposalConfirmationLayout,
40
+ buildContextProposalConfirmationSelectItems,
41
+ buildContextProposalContinuationReason as buildExtractedContextProposalContinuationReason,
42
+ buildEvaluationRoleContextLines as buildExtractedEvaluationRoleContextLines,
43
+ buildEvaluationRoleReminderText as buildExtractedEvaluationRoleReminderText,
44
+ buildResumeCapsule as buildExtractedResumeCapsule,
45
+ buildSystemReminder as buildExtractedSystemReminder,
46
+ maybeWriteContextProposalConfirmationSnapshot,
47
+ maybeWriteContextProposalSnapshot,
48
+ } from "./prompt-surfaces";
49
+ import { toolCallBlockReason } from "./policy-guards";
50
+ import { analyzeContextProposalWithAgent, runCompletionRole } from "./role-runner";
51
+ import {
52
+ applyLiveRoleEvent,
53
+ buildInlineRunningLines,
54
+ cloneLiveRoleActivity,
55
+ createLiveRoleActivity,
56
+ formatElapsed,
57
+ formatInlineRunningText,
58
+ nowMs,
59
+ refreshCompletionStatus,
60
+ truncateInline,
61
+ } from "./status-surface";
62
+ import {
63
+ asNumber,
64
+ asString,
65
+ asStringArray,
66
+ completionRootKey,
67
+ currentEvaluationProfile,
68
+ currentTaskType,
69
+ findCompletionRoot,
70
+ findRepoRoot,
71
+ isRecord,
72
+ loadCompletionDataForReminder,
73
+ loadCompletionSnapshot,
74
+ pathExists,
75
+ readText,
76
+ scaffoldCompletionFiles as scaffoldCompletionFilesOnDisk,
77
+ } from "./state-store";
78
+ import { parseFirstNumber, parseYesNo } from "./transcription";
79
+ import type { TranscriptionResult } from "./transcription";
80
+ import type { CompletionStateSnapshot, CompletionRole, JsonRecord, LiveRoleActivity } from "./types";
12
81
 
13
82
  const PROTOCOL_ID = "completion";
14
83
  const ROLE_NAMES = [
@@ -28,7 +97,6 @@ const PACKAGE_SKILL_PATH = PACKAGE_ROOT ? path.join(PACKAGE_ROOT, "skills", "com
28
97
  const PACKAGE_REFERENCE_PATH = PACKAGE_ROOT
29
98
  ? path.join(PACKAGE_ROOT, "skills", "completion-protocol", "references", "completion.md")
30
99
  : undefined;
31
- const PACKAGE_AGENTS_DIR = PACKAGE_ROOT ? path.join(PACKAGE_ROOT, "agents") : undefined;
32
100
  const SKILL_PATH = PACKAGE_SKILL_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "SKILL.md");
33
101
  const REFERENCE_PATH = PACKAGE_REFERENCE_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "references", "completion.md");
34
102
  const DEFAULT_TASK_TYPE = "completion-workflow";
@@ -37,429 +105,23 @@ const RUBRIC_EVALUATION_ROLES = ["completion-reviewer", "completion-auditor", "c
37
105
 
38
106
  type RubricEvaluationRole = (typeof RUBRIC_EVALUATION_ROLES)[number];
39
107
 
40
- type CompletionRole = (typeof ROLE_NAMES)[number];
41
- type JsonRecord = Record<string, unknown>;
42
-
43
- type CompletionFiles = {
44
- root: string;
45
- agentDir: string;
46
- tmpDir: string;
47
- profilePath: string;
48
- statePath: string;
49
- planPath: string;
50
- activePath: string;
51
- sliceHistoryPath: string;
52
- stopHistoryPath: string;
53
- verificationEvidencePath: string;
54
- compactionMarkerPath: string;
55
- };
56
-
57
- type CompletionStateSnapshot = {
58
- files: CompletionFiles;
59
- profile?: JsonRecord;
60
- state?: JsonRecord;
61
- plan?: JsonRecord;
62
- active?: JsonRecord;
63
- verificationEvidence?: JsonRecord;
64
- activeSlice?: JsonRecord;
65
- };
66
-
67
- type AgentDefinition = {
68
- name: string;
69
- description?: string;
70
- tools?: string[];
71
- model?: string;
72
- systemPrompt: string;
73
- filePath: string;
74
- };
75
-
76
- type LiveRoleActivity = {
77
- role: string;
78
- status: "running" | "ok" | "error";
79
- currentAction?: string;
80
- toolActivity?: string;
81
- toolRecentActivity: string[];
82
- recentActivity: string[];
83
- assistantSummary?: string;
84
- lastAssistantText?: string;
85
- progress?: string;
86
- rationale?: string;
87
- nextStep?: string;
88
- verifying?: string;
89
- stateDeltas: string[];
90
- startedAt: number;
91
- updatedAt: number;
92
- };
93
-
94
- type CompletionStatusSurface = {
95
- snapshotPresent: boolean;
96
- statusText?: string;
97
- widgetLines: string[];
98
- currentPhase?: string;
99
- sliceId?: string;
100
- nextMandatoryRole?: string;
101
- remainingContractCount?: number;
102
- releaseBlockerCount?: number;
103
- highValueGapCount?: number;
104
- remainingStopJudgeCount?: number;
105
- activeRole?: string;
106
- livePreview?: string;
107
- liveState?: "active" | "waiting" | "stalled";
108
- liveIdleMs?: number;
109
- liveToolActivity?: string;
110
- liveAssistantSummary?: string;
111
- liveProgress?: string;
112
- liveRationale?: string;
113
- liveNextStep?: string;
114
- liveVerifying?: string;
115
- liveStateDeltas?: string[];
116
- liveDetailsLines?: string[];
117
- };
118
-
119
- type ContextProposalAnalysis = {
120
- taskType?: string;
121
- evaluationProfile?: string;
122
- critique: string[];
123
- risks: string[];
124
- possibleNoise: string[];
125
- };
126
-
127
- type ContextProposal = {
128
- mission: string;
129
- scope: string[];
130
- constraints: string[];
131
- acceptance: string[];
132
- analysis: ContextProposalAnalysis;
133
- goalText: string;
134
- basisPreview: string;
135
- source: "session" | "analyst";
136
- };
137
-
138
- type ContextProposalSection = "mission" | "scope" | "constraints" | "acceptance" | "critique" | "risks";
139
-
140
- type RecentDiscussionEntry = {
141
- role: "user" | "assistant" | "custom" | "summary";
142
- text: string;
143
- };
144
-
145
- type ContextProposalDecision = {
146
- missionAnchor: string;
147
- goalText: string;
148
- analysis: ContextProposalAnalysis;
149
- };
150
-
151
- type ContextProposalConfirmAction = "start" | "cancel";
152
-
153
- type ContextProposalConfirmationActionItem = {
154
- id: ContextProposalConfirmAction;
155
- label: string;
156
- description: string;
157
- };
158
-
159
- type ContextProposalConfirmationLayout = {
160
- title: string;
161
- intro: string;
162
- proposalHeading: string;
163
- proposalBody: string;
164
- critiqueHeading?: string;
165
- critiqueBody?: string;
166
- routingHeading?: string;
167
- routingBody?: string;
168
- actionsHeading: string;
169
- actions: ContextProposalConfirmationActionItem[];
170
- footer: string;
171
- };
172
-
173
- type ContextProposalConfirmOptions = {
174
- title: string;
175
- nonInteractiveBehavior?: "accept" | "cancel";
176
- };
177
-
178
- class StartupAnalystOverlay extends Container {
179
- private readonly border: DynamicBorder;
180
- private readonly title: Text;
181
- private readonly body: Text;
182
- private readonly footer: Text;
183
- private lines: string[] = [];
184
- onAbort?: () => void;
185
-
186
- constructor(private readonly theme: any) {
187
- super();
188
- this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
189
- this.title = new Text("", 1, 0);
190
- this.body = new Text("", 1, 1);
191
- this.footer = new Text("", 1, 0);
192
- this.addChild(this.border);
193
- this.addChild(this.title);
194
- this.addChild(this.body);
195
- this.addChild(this.footer);
196
- this.updateDisplay();
197
- }
198
-
199
- setLines(lines: string[]): void {
200
- this.lines = [...lines];
201
- this.updateDisplay();
202
- this.invalidate();
203
- }
204
-
205
- private updateDisplay(): void {
206
- this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
207
- this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
208
- this.footer.setText(this.theme.fg("muted", "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state"));
209
- }
210
-
211
- override handleInput(data: string): void {
212
- if (data === "\u001b" || data === "\u0003") {
213
- this.onAbort?.();
214
- return;
215
- }
216
- // Container does not implement handleInput; ignore all other keys.
217
- }
218
-
219
- override invalidate(): void {
220
- super.invalidate();
221
- this.updateDisplay();
222
- }
223
- }
224
-
225
108
  const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
226
109
  const activatedCompletionRoutingRoots = new Set<string>();
227
- const LIVE_ROLE_WAITING_MS = 15_000;
228
- const LIVE_ROLE_STALLED_MS = 45_000;
229
110
  const LIVE_ROLE_HEARTBEAT_MS = 5_000;
230
- const DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS = 2;
231
-
232
- type DriverContinuationTracker = {
233
- fingerprint: string;
234
- attempts: number;
235
- inFlight: boolean;
236
- warned: boolean;
237
- };
238
-
239
- const driverContinuationByRoot = new Map<string, DriverContinuationTracker>();
240
-
241
- function isRecord(value: unknown): value is JsonRecord {
242
- return typeof value === "object" && value !== null && !Array.isArray(value);
243
- }
244
-
245
- function asString(value: unknown): string | undefined {
246
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
247
- }
248
111
 
249
112
  function asBoolean(value: unknown): boolean | undefined {
250
113
  return typeof value === "boolean" ? value : undefined;
251
114
  }
252
115
 
253
- function asNumber(value: unknown): number | undefined {
254
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
255
- }
256
-
257
- function asStringArray(value: unknown): string[] {
258
- return Array.isArray(value)
259
- ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
260
- : [];
261
- }
262
-
263
116
  function roleFromEnv(): string | undefined {
264
117
  return asString(process.env.PI_COMPLETION_ROLE);
265
118
  }
266
119
 
267
- function resolveFiles(root: string): CompletionFiles {
268
- const agentDir = path.join(root, ".agent");
269
- const tmpDir = path.join(agentDir, "tmp");
270
- return {
271
- root,
272
- agentDir,
273
- tmpDir,
274
- profilePath: path.join(agentDir, "profile.json"),
275
- statePath: path.join(agentDir, "state.json"),
276
- planPath: path.join(agentDir, "plan.json"),
277
- activePath: path.join(agentDir, "active-slice.json"),
278
- sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
279
- stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
280
- verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
281
- compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
282
- };
283
- }
284
-
285
- function walkUpForDir(startCwd: string, segments: string[]): string | undefined {
286
- let current = path.resolve(startCwd);
287
- while (true) {
288
- const candidate = path.join(current, ...segments);
289
- if (fs.existsSync(candidate)) return candidate;
290
- const parent = path.dirname(current);
291
- if (parent === current) return undefined;
292
- current = parent;
293
- }
294
- }
295
-
296
- function completionSearchRoots(startCwd: string): string[] {
297
- return [...new Set([path.resolve(startCwd), path.resolve(process.cwd())])];
298
- }
299
-
300
- function findCompletionRoot(startCwd: string): string | undefined {
301
- for (const candidateRoot of completionSearchRoots(startCwd)) {
302
- const profilePath = walkUpForDir(candidateRoot, [".agent", "profile.json"]);
303
- if (profilePath) return path.dirname(path.dirname(profilePath));
304
- }
305
- return undefined;
306
- }
307
-
308
- function findRepoRoot(startCwd: string): string | undefined {
309
- for (const candidateRoot of completionSearchRoots(startCwd)) {
310
- const gitPath = walkUpForDir(candidateRoot, [".git"]);
311
- if (gitPath) return path.dirname(gitPath);
312
- }
313
- return undefined;
314
- }
315
-
316
- async function readJson(filePath: string): Promise<JsonRecord | undefined> {
317
- try {
318
- const raw = await fsp.readFile(filePath, "utf8");
319
- const parsed = JSON.parse(raw);
320
- return isRecord(parsed) ? parsed : undefined;
321
- } catch {
322
- return undefined;
323
- }
324
- }
325
-
326
- async function readJsonl(filePath: string): Promise<JsonRecord[]> {
327
- try {
328
- const raw = await fsp.readFile(filePath, "utf8");
329
- return raw
330
- .split("\n")
331
- .map((line) => line.trim())
332
- .filter(Boolean)
333
- .flatMap((line) => {
334
- try {
335
- const parsed = JSON.parse(line);
336
- return isRecord(parsed) ? [parsed] : [];
337
- } catch {
338
- return [];
339
- }
340
- });
341
- } catch {
342
- return [];
343
- }
344
- }
345
-
346
- async function writeJsonFile(filePath: string, value: JsonRecord): Promise<void> {
347
- await fsp.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
348
- }
349
-
350
120
  function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
351
121
  const slices = plan?.candidate_slices;
352
122
  return Array.isArray(slices) ? slices.filter(isRecord) : [];
353
123
  }
354
124
 
355
- function findActiveSlice(plan: JsonRecord | undefined, active: JsonRecord | undefined): JsonRecord | undefined {
356
- const sliceId = asString(active?.slice_id);
357
- if (!sliceId) return undefined;
358
- return candidateSlices(plan).find((slice) => asString(slice.slice_id) === sliceId);
359
- }
360
-
361
- async function loadCompletionSnapshot(startCwd: string): Promise<CompletionStateSnapshot | undefined> {
362
- const root = findCompletionRoot(startCwd);
363
- if (!root) return undefined;
364
- const files = resolveFiles(root);
365
- const profile = await readJson(files.profilePath);
366
- if (asString(profile?.protocol_id) !== PROTOCOL_ID) return undefined;
367
- const state = await readJson(files.statePath);
368
- const plan = await readJson(files.planPath);
369
- const active = await readJson(files.activePath);
370
- const verificationEvidence = await readJson(files.verificationEvidencePath);
371
- return {
372
- files,
373
- profile,
374
- state,
375
- plan,
376
- active,
377
- verificationEvidence,
378
- activeSlice: findActiveSlice(plan, active),
379
- };
380
- }
381
-
382
- async function loadCompletionDataForReminder(startCwd: string) {
383
- const snapshot = await loadCompletionSnapshot(startCwd);
384
- if (!snapshot) return undefined;
385
- const sliceHistory = await readJsonl(snapshot.files.sliceHistoryPath);
386
- const stopHistory = await readJsonl(snapshot.files.stopHistoryPath);
387
- return { snapshot, sliceHistory, stopHistory };
388
- }
389
-
390
- async function pathExists(targetPath: string): Promise<boolean> {
391
- try {
392
- await fsp.access(targetPath);
393
- return true;
394
- } catch {
395
- return false;
396
- }
397
- }
398
-
399
- async function readText(filePath: string): Promise<string | undefined> {
400
- try {
401
- return await fsp.readFile(filePath, "utf8");
402
- } catch {
403
- return undefined;
404
- }
405
- }
406
-
407
- async function detectDocsSurfaces(root: string): Promise<string[]> {
408
- const candidates = ["README.md", "docs/", "docs", "CHANGELOG.md"];
409
- const found: string[] = [];
410
- for (const candidate of candidates) {
411
- if (await pathExists(path.join(root, candidate))) found.push(candidate.endsWith("/") ? candidate : candidate.replace(/\/$/, ""));
412
- }
413
- return found.length > 0 ? found : ["README.md"];
414
- }
415
-
416
- async function detectVerifierCommand(root: string): Promise<string | undefined> {
417
- const packageJsonPath = path.join(root, "package.json");
418
- const packageJson = await readJson(packageJsonPath);
419
- if (packageJson) {
420
- const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : undefined;
421
- const packageManager = asString((packageJson as JsonRecord).packageManager) ?? "";
422
- const runner = packageManager.startsWith("pnpm") ? "pnpm" : packageManager.startsWith("yarn") ? "yarn" : packageManager.startsWith("bun") ? "bun" : "npm";
423
- if (scripts && asString(scripts.test)) return runner === "npm" ? "npm test" : `${runner} test`;
424
- if (scripts && asString(scripts.check)) return runner === "npm" ? "npm run check" : `${runner} check`;
425
- if (scripts && asString(scripts.lint)) return runner === "npm" ? "npm run lint" : `${runner} lint`;
426
- }
427
- if (await pathExists(path.join(root, "pnpm-lock.yaml"))) return "pnpm test";
428
- if (await pathExists(path.join(root, "bun.lockb")) || await pathExists(path.join(root, "bun.lock"))) return "bun test";
429
- if (await pathExists(path.join(root, "yarn.lock"))) return "yarn test";
430
- if (await pathExists(path.join(root, "Cargo.toml"))) return "cargo test";
431
- if (await pathExists(path.join(root, "pyproject.toml")) || await pathExists(path.join(root, "pytest.ini"))) return "pytest";
432
- if (await pathExists(path.join(root, "go.mod"))) return "go test ./...";
433
- if (await pathExists(path.join(root, "Makefile"))) return "make test";
434
- return undefined;
435
- }
436
-
437
- function normalizeMissionAnchorText(value: string): string {
438
- return value
439
- .replace(/^\/(?:cook|complete)\s+/i, "")
440
- .replace(/^["'“”‘’]+|["'“”‘’]+$/g, "")
441
- .replace(/^\s*(please|pls|can you|could you|help me|i want to|we need to|let'?s|continue to|continue|resume)\s+/i, "")
442
- .replace(/\s+/g, " ")
443
- .replace(/[。!?.!?]+$/u, "")
444
- .trim();
445
- }
446
-
447
- function isWeakMissionAnchor(value: string): boolean {
448
- const normalized = value.trim().toLowerCase();
449
- if (normalized.length < 8) return true;
450
- if (["continue", "resume", "fix", "fix it", "work on this", "help", "do it", "try again"].includes(normalized)) return true;
451
- if (/^(continue|resume|fix|help|work on)(\s+.*)?$/i.test(normalized) && normalized.split(/\s+/).length <= 3) return true;
452
- return false;
453
- }
454
-
455
- type MissionAnchorAssessment = {
456
- derived: string;
457
- };
458
-
459
- function assessMissionAnchor(rawGoal: string, projectName: string): MissionAnchorAssessment {
460
- return { derived: deriveMissionAnchor(rawGoal, projectName) };
461
- }
462
-
463
125
  type ExistingWorkflowDecision =
464
126
  | { action: "continue"; currentMissionAnchor: string }
465
127
  | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
@@ -483,6 +145,10 @@ function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cance
483
145
  return raw === "continue" || raw === "refocus" || raw === "cancel" ? raw : undefined;
484
146
  }
485
147
 
148
+ function completionTestWorkflowMissionOverride(): string | undefined {
149
+ return asString(process.env.PI_COMPLETION_EXISTING_WORKFLOW_MISSION);
150
+ }
151
+
486
152
  function shouldSkipDriverKickoffForTests(): boolean {
487
153
  return process.env.PI_COMPLETION_SKIP_DRIVER_KICKOFF === "1";
488
154
  }
@@ -540,8 +206,10 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
540
206
  }
541
207
 
542
208
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
209
+ const COOK_BARE_ONLY_GUIDANCE =
210
+ "/cook only supports the bare /cook entrypoint. Move mission text into the main chat, then rerun /cook.";
543
211
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
544
- "/cook failed closed because recent discussion and any optional inline /cook hint did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
212
+ "/cook failed closed because recent discussion did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
545
213
 
546
214
  function buildCookCancellationMessage(prefix: string): string {
547
215
  return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
@@ -593,419 +261,6 @@ function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): s
593
261
  ].join(" ");
594
262
  }
595
263
 
596
- function extractTextFromMessageContent(content: unknown): string {
597
- if (typeof content === "string") return content.trim();
598
- if (!Array.isArray(content)) return "";
599
- return content
600
- .map((item) => {
601
- if (!isRecord(item)) return "";
602
- if (item.type !== "text") return "";
603
- return asString(item.text) ?? "";
604
- })
605
- .filter((item) => item.length > 0)
606
- .join("\n")
607
- .trim();
608
- }
609
-
610
- function stripCodeBlocks(text: string): string {
611
- return text.replace(/```[\s\S]*?```/g, " ");
612
- }
613
-
614
- function normalizeProposalLine(line: string): string {
615
- return line
616
- .replace(/^[-*+]\s+/, "")
617
- .replace(/^\d+[.)]\s+/, "")
618
- .replace(/^\[.?\]\s+/, "")
619
- .replace(/^>\s*/, "")
620
- .replace(/^[`*_~]+|[`*_~]+$/g, "")
621
- .replace(/^\*\*(.+)\*\*$/u, "$1")
622
- .replace(/^__([^_]+)__$/u, "$1")
623
- .trim();
624
- }
625
-
626
- function detectProposalSection(line: string): ContextProposalSection | undefined {
627
- const normalized = normalizeProposalLine(line)
628
- .toLowerCase()
629
- .replace(/[::]$/, "")
630
- .trim();
631
- if (!normalized) return undefined;
632
- if (["mission", "goal", "objective", "summary", "目標", "任務", "計劃", "计划", "方案"].includes(normalized)) return "mission";
633
- if (["scope", "plan", "steps", "implementation", "範圍", "范围", "實作", "实现", "步驟", "步骤"].includes(normalized)) return "scope";
634
- if (["constraints", "constraint", "guardrails", "non-goals", "限制", "約束", "约束", "非目標", "非目标"].includes(normalized)) return "constraints";
635
- if (["acceptance", "acceptance criteria", "deliverables", "verification", "驗收", "验收", "交付", "驗證", "验证"].includes(normalized)) return "acceptance";
636
- if (["critique", "critic", "concerns", "concern", "warnings", "warning", "notes", "note", "評論", "评论", "提醒"].includes(normalized)) return "critique";
637
- if (["risk", "risks", "hazards", "hazard", "failure modes", "failure mode", "風險", "风险"].includes(normalized)) return "risks";
638
- return undefined;
639
- }
640
-
641
- function matchInlineProposalSection(line: string): { section: ContextProposalSection; content: string } | undefined {
642
- const normalized = normalizeProposalLine(line);
643
- const match = normalized.match(/^([^::]+)[::]\s*(.+)$/u);
644
- if (!match) return undefined;
645
- const [, rawLabel, rawContent] = match;
646
- const section = detectProposalSection(rawLabel);
647
- const content = rawContent.trim();
648
- if (!section || !content) return undefined;
649
- return { section, content };
650
- }
651
-
652
- function bulletText(line: string): string | undefined {
653
- if (!/^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(line)) return undefined;
654
- const normalized = normalizeProposalLine(line);
655
- return normalized.length > 0 ? normalized : undefined;
656
- }
657
-
658
- function looksLikeConstraint(text: string): boolean {
659
- return /(do not|don't|must not|avoid|without|keep\b|preserve|retain|remain|不要|不可|不能|不應|不应|保持|保留|避免)/i.test(text);
660
- }
661
-
662
- function looksLikeAcceptance(text: string): boolean {
663
- return /(test|tests|testing|verify|verification|validated|README|docs?|documentation|regression|observability|驗證|验证|測試|测试|文件|文檔|文档|回歸|回归)/i.test(text);
664
- }
665
-
666
- function uniqueProposalItems(items: string[]): string[] {
667
- const seen = new Set<string>();
668
- const result: string[] = [];
669
- for (const item of items) {
670
- const normalized = normalizeProposalLine(item).replace(/\s+/g, " ").trim();
671
- if (!normalized) continue;
672
- const key = normalized.toLowerCase();
673
- if (seen.has(key)) continue;
674
- seen.add(key);
675
- result.push(normalized);
676
- }
677
- return result;
678
- }
679
-
680
- function normalizeContextProposalHint(value: unknown): string | undefined {
681
- const normalized = asString(value)?.replace(/\s+/g, " ").trim();
682
- return normalized || undefined;
683
- }
684
-
685
- function normalizeContextProposalTaskTypeHint(value: unknown): string | undefined {
686
- const normalized = normalizeContextProposalHint(value);
687
- if (!normalized) return undefined;
688
- const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
689
- return canonical === DEFAULT_TASK_TYPE ? DEFAULT_TASK_TYPE : normalized;
690
- }
691
-
692
- function normalizeContextProposalEvaluationProfileHint(value: unknown): string | undefined {
693
- const normalized = normalizeContextProposalHint(value);
694
- if (!normalized) return undefined;
695
- const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
696
- return canonical === DEFAULT_EVALUATION_PROFILE ? DEFAULT_EVALUATION_PROFILE : normalized;
697
- }
698
-
699
- function inferContextProposalTaskType(texts: string[]): string | undefined {
700
- const corpus = texts
701
- .map((text) => normalizeProposalLine(text).toLowerCase())
702
- .filter(Boolean)
703
- .join("\n");
704
- if (!corpus) return undefined;
705
- return /(completion|\/cook|\/complete|\.agent|slice|reground|reviewer|auditor|stop judge|stop-judge|workflow)/i.test(corpus)
706
- ? DEFAULT_TASK_TYPE
707
- : undefined;
708
- }
709
-
710
- function inferContextProposalEvaluationProfile(texts: string[], taskType?: string): string | undefined {
711
- const corpus = texts
712
- .map((text) => normalizeProposalLine(text).toLowerCase())
713
- .filter(Boolean)
714
- .join("\n");
715
- if (!corpus) return undefined;
716
- if (
717
- /(rubric|evaluation[_\s-]*profile|pass\|concern\|fail|contract coverage|correctness risk|verification evidence|docs\/state parity|reviewer|auditor|stop judge|stop-judge)/i.test(
718
- corpus,
719
- )
720
- ) {
721
- return DEFAULT_EVALUATION_PROFILE;
722
- }
723
- return taskType === DEFAULT_TASK_TYPE && /(completion|\/cook|\/complete|slice|workflow|review|audit)/i.test(corpus)
724
- ? DEFAULT_EVALUATION_PROFILE
725
- : undefined;
726
- }
727
-
728
- function buildContextProposalAnalysis(args: {
729
- taskType?: unknown;
730
- evaluationProfile?: unknown;
731
- critique?: string[];
732
- risks?: string[];
733
- possibleNoise?: string[];
734
- hintTexts?: string[];
735
- }): ContextProposalAnalysis {
736
- const critique = uniqueProposalItems(args.critique ?? []);
737
- const risks = uniqueProposalItems(args.risks ?? []);
738
- const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
739
- const hintTexts = [...(args.hintTexts ?? []), ...critique, ...risks, ...possibleNoise];
740
- const taskType = normalizeContextProposalTaskTypeHint(args.taskType) ?? inferContextProposalTaskType(hintTexts);
741
- const evaluationProfile =
742
- normalizeContextProposalEvaluationProfileHint(args.evaluationProfile) ??
743
- inferContextProposalEvaluationProfile(hintTexts, taskType);
744
- return {
745
- taskType,
746
- evaluationProfile,
747
- critique,
748
- risks,
749
- possibleNoise,
750
- };
751
- }
752
-
753
- function mergeContextProposalAnalysis(
754
- sources: Array<ContextProposalAnalysis | undefined>,
755
- hintTexts: string[] = [],
756
- ): ContextProposalAnalysis {
757
- const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
758
- const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
759
- const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
760
- const taskType =
761
- sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
762
- inferContextProposalTaskType([...hintTexts, ...critique, ...risks, ...possibleNoise]);
763
- const evaluationProfile =
764
- sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
765
- inferContextProposalEvaluationProfile([...hintTexts, ...critique, ...risks, ...possibleNoise], taskType);
766
- return {
767
- taskType,
768
- evaluationProfile,
769
- critique,
770
- risks,
771
- possibleNoise,
772
- };
773
- }
774
-
775
- function matchContextProposalRoutingHint(
776
- line: string,
777
- ): { field: "taskType" | "evaluationProfile"; value: string } | undefined {
778
- const normalized = normalizeProposalLine(line);
779
- const match = normalized.match(/^(task[\s_-]*type|evaluation[\s_-]*profile)[::]\s*(.+)$/iu);
780
- if (!match) return undefined;
781
- const label = match[1].toLowerCase().replace(/[\s_-]+/g, "");
782
- const value = match[2].trim();
783
- if (!value) return undefined;
784
- return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
785
- }
786
-
787
- const CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX =
788
- /(?:\b(?:start(?:ing)?|begin|continue|continu(?:e|ing)|resume|implement(?:ing)?|execute|execut(?:e|ing)|carry out|work on|ship|build(?:ing)?)\b.*\b(?:this|that|the|current|latest)\s+(?:plan|proposal|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan)\b|(?:開始|著手|繼續|继续|恢復|恢复)?(?:實作|实现|執行|执行|落地|完成)(?:這個|这个|此|該|该)?(?:方案|計畫|计划|提案|規劃|规划|設計|设计))/iu;
789
- const CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX =
790
- /(?:\b(?:write|draft|prepare|create|produce|share|deliver|document|review)\b.*\b(?:plan|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan|proposal)\b|(?:撰寫|撰写|編寫|编写|起草|準備|准备|產出|产出|整理|分享|交付|審查|审查).*(?:計畫|计划|規格|规格|設計文件|设计文档|提案|方案))/iu;
791
- const CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX = /(?:\b(?:docs? only|documentation only)\b|(?:只改文件|僅文件|仅文件))/iu;
792
- const CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX =
793
- /(?:\b(?:no code(?: changes?)?|without code(?: changes?)?|do not implement|don't implement|planning only|proposal only|spec only|design[- ]doc only|no runtime changes?)\b|(?:不改(?:動)?代碼|不改代码|不要實作|不要实现|只規劃|只规划|僅規劃|仅规划|不改(?:動)?執行|不改运行))/iu;
794
- const CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX =
795
- /(?:\b(?:normalize|fix|update|add|remove|restore|refactor|ship|support|wire|route|rewrite|replace|preserve|filter|separate|refresh|reroute|suppress|align|convert|reconcile|repair|correct|implement|build|land|block|allow|keep|edit(?:ing)?|document(?:ing)?|writ(?:e|ing))\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|正規化|规范化|規範化|过滤|過濾|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
796
-
797
- function contextProposalBodyTexts(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string[] {
798
- return [...proposal.scope, ...proposal.constraints, ...proposal.acceptance];
799
- }
800
-
801
- function isGenericPlanningMissionAnchor(text: string): boolean {
802
- const normalized = normalizeMissionAnchorText(text);
803
- if (!normalized) return false;
804
- return CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX.test(normalized);
805
- }
806
-
807
- function hasExplicitPlanningOnlyDeliverable(texts: string[]): boolean {
808
- return texts.some((text) => CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX.test(normalizeProposalLine(text)));
809
- }
810
-
811
- function normalizeImplementationMissionSourceText(text: string): string {
812
- const normalized = normalizeProposalLine(text);
813
- if (!normalized) return "";
814
- return normalized
815
- .replace(new RegExp(`${CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX.source}[\\s::;,/\\-]*`, "giu"), " ")
816
- .replace(/\s+([,.;:!?])/g, "$1")
817
- .replace(/\s+/g, " ")
818
- .trim();
819
- }
820
-
821
- function hasClearNoImplementationSignal(texts: string[]): boolean {
822
- return texts.some((text) => CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX.test(normalizeProposalLine(text)));
823
- }
824
-
825
- function implementationMissionSourceCandidateText(text: string): string | undefined {
826
- const normalized = normalizeImplementationMissionSourceText(text);
827
- if (!normalized) return undefined;
828
- if (hasExplicitPlanningOnlyDeliverable([normalized])) return undefined;
829
- if (hasClearNoImplementationSignal([normalized])) return undefined;
830
- if (!CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalized)) return undefined;
831
- return normalized;
832
- }
833
-
834
- function pickImplementationMissionSource(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string | undefined {
835
- for (const item of proposal.scope) {
836
- const candidate = implementationMissionSourceCandidateText(item);
837
- if (candidate) return candidate;
838
- }
839
- for (const item of proposal.acceptance) {
840
- const candidate = implementationMissionSourceCandidateText(item);
841
- if (candidate) return candidate;
842
- }
843
- return undefined;
844
- }
845
-
846
- function hasPlanningArtifactOnlyContext(
847
- proposal: Pick<ContextProposal, "mission" | "scope" | "constraints" | "acceptance">,
848
- ): boolean {
849
- const texts = [proposal.mission, ...contextProposalBodyTexts(proposal)];
850
- if (!hasExplicitPlanningOnlyDeliverable(texts)) return false;
851
- return !pickImplementationMissionSource(proposal);
852
- }
853
-
854
- function finalizeContextProposal(proposal: ContextProposal, projectName: string): ContextProposal | undefined {
855
- if (hasPlanningArtifactOnlyContext(proposal)) return undefined;
856
- if (!isGenericPlanningMissionAnchor(proposal.mission)) return proposal;
857
- const missionSource = pickImplementationMissionSource(proposal);
858
- if (!missionSource) return undefined;
859
- const nextMission = assessMissionAnchor(missionSource, projectName).derived;
860
- const normalizedNextMission = normalizeMissionAnchorText(nextMission);
861
- if (!normalizedNextMission || isWeakMissionAnchor(normalizedNextMission)) return undefined;
862
- if (missionAnchorsStrictlyEquivalent(nextMission, proposal.mission)) return proposal;
863
- return {
864
- ...proposal,
865
- mission: nextMission,
866
- goalText: buildContextProposalGoalText({
867
- mission: nextMission,
868
- scope: proposal.scope,
869
- constraints: proposal.constraints,
870
- acceptance: proposal.acceptance,
871
- }),
872
- };
873
- }
874
-
875
- const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
876
- "a",
877
- "an",
878
- "and",
879
- "are",
880
- "as",
881
- "at",
882
- "be",
883
- "by",
884
- "for",
885
- "from",
886
- "goal",
887
- "goals",
888
- "in",
889
- "into",
890
- "is",
891
- "it",
892
- "its",
893
- "mission",
894
- "of",
895
- "on",
896
- "or",
897
- "scope",
898
- "that",
899
- "the",
900
- "their",
901
- "this",
902
- "to",
903
- "using",
904
- "with",
905
- "workflow",
906
- ]);
907
-
908
- function missionScopeFilterTokens(text: string): string[] {
909
- const normalized = normalizeProposalLine(text).toLowerCase();
910
- const tokens = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
911
- return tokens.filter((token) => {
912
- if (/^[\p{Script=Han}]+$/u.test(token)) return token.length >= 2;
913
- if (token.length < 2) return false;
914
- return !MISSION_SCOPE_FILTER_STOPWORDS.has(token);
915
- });
916
- }
917
-
918
- function isSessionScopeItemMissionRelevant(item: string, mission: string): boolean {
919
- const normalizedItem = normalizeProposalLine(item).toLowerCase();
920
- const normalizedMission = normalizeMissionAnchorText(mission).toLowerCase();
921
- if (!normalizedItem || !normalizedMission) return true;
922
- if (normalizedItem.includes(normalizedMission) || normalizedMission.includes(normalizedItem)) return true;
923
- const itemTokens = [...new Set(missionScopeFilterTokens(normalizedItem))];
924
- const missionTokens = new Set(missionScopeFilterTokens(normalizedMission));
925
- if (itemTokens.length === 0 || missionTokens.size === 0) return true;
926
- const overlap = itemTokens.filter((token) => missionTokens.has(token));
927
- if (overlap.length >= 2) return true;
928
- return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
929
- }
930
-
931
- function missionAnchorSemanticTokens(text: string): string[] {
932
- return [...new Set(missionScopeFilterTokens(normalizeMissionAnchorText(text).toLowerCase()))];
933
- }
934
-
935
- function missionAnchorOrderedTokenOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
936
- if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
937
- const dp = new Array(rightTokens.length + 1).fill(0);
938
- for (const leftToken of leftTokens) {
939
- let previous = 0;
940
- for (let index = 0; index < rightTokens.length; index += 1) {
941
- const nextPrevious = dp[index + 1];
942
- if (leftToken === rightTokens[index]) {
943
- dp[index + 1] = previous + 1;
944
- } else {
945
- dp[index + 1] = Math.max(dp[index + 1], dp[index]);
946
- }
947
- previous = nextPrevious;
948
- }
949
- }
950
- return dp[rightTokens.length] / Math.max(leftTokens.length, rightTokens.length);
951
- }
952
-
953
- function missionAnchorBigramOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
954
- if (leftTokens.length < 2 || rightTokens.length < 2) return 0;
955
- const leftBigrams = new Set(leftTokens.slice(0, -1).map((token, index) => `${token} ${leftTokens[index + 1]}`));
956
- const rightBigrams = new Set(rightTokens.slice(0, -1).map((token, index) => `${token} ${rightTokens[index + 1]}`));
957
- if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
958
- let overlap = 0;
959
- for (const bigram of leftBigrams) {
960
- if (rightBigrams.has(bigram)) overlap += 1;
961
- }
962
- return overlap / Math.max(leftBigrams.size, rightBigrams.size);
963
- }
964
-
965
- function missionAnchorsStrictlyEquivalent(left: string, right: string): boolean {
966
- return normalizeMissionAnchorText(left).toLowerCase() === normalizeMissionAnchorText(right).toLowerCase();
967
- }
968
-
969
- const MISSION_NEGATION_CUE_REGEX = /(?:^|[^\p{L}\p{N}_])(?:no|not|without|never|cannot|don['’]?t)(?=$|[^\p{L}\p{N}_])/u;
970
-
971
- function missionAnchorHasNegationCue(text: string): boolean {
972
- return MISSION_NEGATION_CUE_REGEX.test(text);
973
- }
974
-
975
- function missionAnchorsLikelyEquivalent(left: string, right: string): boolean {
976
- const normalizedLeft = normalizeMissionAnchorText(left).toLowerCase();
977
- const normalizedRight = normalizeMissionAnchorText(right).toLowerCase();
978
- if (!normalizedLeft || !normalizedRight) return false;
979
- const leftHasNegationCue = missionAnchorHasNegationCue(normalizedLeft);
980
- const rightHasNegationCue = missionAnchorHasNegationCue(normalizedRight);
981
- if (leftHasNegationCue !== rightHasNegationCue) return false;
982
- if (normalizedLeft === normalizedRight) return true;
983
- if (!leftHasNegationCue && (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft))) return true;
984
- const leftTokens = missionAnchorSemanticTokens(normalizedLeft);
985
- const rightTokens = missionAnchorSemanticTokens(normalizedRight);
986
- if (leftTokens.length === 0 || rightTokens.length === 0) return false;
987
- const rightSet = new Set(rightTokens);
988
- const overlap = leftTokens.filter((token) => rightSet.has(token));
989
- if (overlap.length < 3) return false;
990
- const maxLen = Math.max(leftTokens.length, rightTokens.length);
991
- if (overlap.length / maxLen < 0.75) return false;
992
- if (missionAnchorOrderedTokenOverlapRatio(leftTokens, rightTokens) < 0.75) return false;
993
- if (Math.min(leftTokens.length, rightTokens.length) < 4) return true;
994
- return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
995
- }
996
-
997
- function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
998
- if (proposal.source === "session") {
999
- return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
1000
- }
1001
- return (
1002
- proposal.scope.length > 0 &&
1003
- proposal.constraints.length > 0 &&
1004
- proposal.acceptance.length > 0 &&
1005
- proposal.analysis.possibleNoise.length === 0
1006
- );
1007
- }
1008
-
1009
264
  function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowProposalAssessment): void {
1010
265
  const snapshotPath = completionTestActiveWorkflowRoutingSnapshotPath();
1011
266
  if (!snapshotPath) return;
@@ -1020,6 +275,9 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
1020
275
  proposedMissionAnchor: assessment.proposal?.mission ?? null,
1021
276
  proposalSource: assessment.proposal?.source ?? null,
1022
277
  possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
278
+ alternateMissions: assessment.proposal?.analysis.alternateMissions ?? [],
279
+ suppressedCompletedTopics: assessment.proposal?.analysis.suppressedCompletedTopics ?? [],
280
+ suppressedNegatedTopics: assessment.proposal?.analysis.suppressedNegatedTopics ?? [],
1023
281
  scope: assessment.proposal?.scope ?? [],
1024
282
  constraints: assessment.proposal?.constraints ?? [],
1025
283
  acceptance: assessment.proposal?.acceptance ?? [],
@@ -1030,459 +288,26 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
1030
288
  );
1031
289
  }
1032
290
 
1033
- const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
1034
- "You analyze recent /cook startup discussion and return a strict JSON object.",
1035
- "Do not emit markdown, code fences, or commentary.",
1036
- "Return exactly one JSON object with keys: mission, scope, constraints, acceptance, critique, risks, task_type, evaluation_profile, confidence, possible_noise.",
1037
- "mission must be a concise implementation mission anchor sentence.",
1038
- "scope must contain only work items that directly support the mission.",
1039
- "constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
1040
- "acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
1041
- "critique must contain operator-facing cautions, concerns, or reminders that should be shown separately from mission and scope later.",
1042
- "risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
1043
- "task_type and evaluation_profile should be candidate routing hints only; reuse the existing completion vocabulary when it clearly fits instead of inventing new schema names.",
1044
- "possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
1045
- "When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
1046
- ].join(" ");
1047
-
1048
- function collectRecentDiscussionEntries(ctx: { sessionManager: any }, limit = 8): RecentDiscussionEntry[] {
1049
- let branch: any[] = [];
1050
- try {
1051
- branch = ctx.sessionManager?.getBranch?.() ?? [];
1052
- } catch (error) {
1053
- if (isStaleContextError(error)) return [];
1054
- throw error;
1055
- }
1056
- const entries: RecentDiscussionEntry[] = [];
1057
- for (let index = branch.length - 1; index >= 0; index -= 1) {
1058
- const entry = branch[index];
1059
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
1060
- const message = entry.message as JsonRecord;
1061
- let text = "";
1062
- let role: RecentDiscussionEntry["role"] | undefined;
1063
- const messageRole = asString(message.role);
1064
- if (messageRole === "user" || messageRole === "custom") {
1065
- text = extractTextFromMessageContent(message.content);
1066
- role = messageRole;
1067
- }
1068
- if (!text || !role) continue;
1069
- const trimmed = text.trim();
1070
- if (!trimmed || /^\/(?:cook|complete)\b/i.test(trimmed)) continue;
1071
- entries.push({ role, text: trimmed });
1072
- if (entries.length >= limit) break;
1073
- }
1074
- return entries;
1075
- }
1076
-
1077
- function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[]): string {
1078
- return entries
1079
- .slice()
1080
- .reverse()
1081
- .map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
1082
- .join("\n\n");
1083
- }
1084
-
1085
- function extractJsonObjectFromText(text: string): string | undefined {
1086
- const trimmed = text.trim();
1087
- if (!trimmed) return undefined;
1088
- const unfenced = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
1089
- if (unfenced.startsWith("{") && unfenced.endsWith("}")) return unfenced;
1090
- const start = unfenced.indexOf("{");
1091
- const end = unfenced.lastIndexOf("}");
1092
- if (start < 0 || end <= start) return undefined;
1093
- return unfenced.slice(start, end + 1);
1094
- }
1095
-
1096
- function parseContextProposalAnalystOutput(raw: string, projectName: string): ContextProposal | undefined {
1097
- const jsonText = extractJsonObjectFromText(raw);
1098
- if (!jsonText) return undefined;
1099
- let parsed: unknown;
1100
- try {
1101
- parsed = JSON.parse(jsonText);
1102
- } catch {
1103
- return undefined;
1104
- }
1105
- if (!isRecord(parsed)) return undefined;
1106
- const missionSource = asString(parsed.mission);
1107
- if (!missionSource) return undefined;
1108
- const assessment = assessMissionAnchor(missionSource, projectName);
1109
- const normalizedMission = normalizeMissionAnchorText(missionSource);
1110
- if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
1111
- const mission = assessment.derived;
1112
- const scope = uniqueProposalItems(asStringArray(parsed.scope));
1113
- const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
1114
- const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
1115
- const analysis = buildContextProposalAnalysis({
1116
- taskType: parsed.task_type ?? parsed.taskType,
1117
- evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
1118
- critique: asStringArray(parsed.critique),
1119
- risks: asStringArray(parsed.risks ?? parsed.risk),
1120
- possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
1121
- hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
291
+ function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
292
+ return buildExtractedContextProposalContinuationReason(prefix, goalText, analysis, {
293
+ defaultTaskType: DEFAULT_TASK_TYPE,
294
+ defaultEvaluationProfile: DEFAULT_EVALUATION_PROFILE,
295
+ truncateInline,
1122
296
  });
1123
- const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
1124
- return finalizeContextProposal(
1125
- {
1126
- mission,
1127
- scope,
1128
- constraints,
1129
- acceptance,
1130
- analysis,
1131
- goalText,
1132
- basisPreview: raw.replace(/\s+/g, " ").trim(),
1133
- source: "analyst",
1134
- },
1135
- projectName,
1136
- );
1137
- }
1138
-
1139
- function contextProposalAnalystModelArg(model: unknown): string | undefined {
1140
- if (!isRecord(model)) return undefined;
1141
- const provider = asString(model.provider);
1142
- const id = asString(model.id);
1143
- return provider && id ? `${provider}/${id}` : undefined;
1144
- }
1145
-
1146
- function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], hintText?: string): string {
1147
- const discussion = serializeRecentDiscussionEntries(recentEntries);
1148
- const lines = [
1149
- `Project: ${projectName}`,
1150
- "Infer the current mission from the discussion.",
1151
- "If an inline /cook hint is present, treat it as a high-priority user hint that may focus the mission, but do not ignore conflicting discussion or skip missing details.",
1152
- ];
1153
- if (hintText) {
1154
- lines.push("", "Inline /cook hint:", hintText);
1155
- }
1156
- lines.push("", "Recent discussion:", discussion || "(none)");
1157
- return lines.join("\n");
1158
297
  }
1159
298
 
1160
- function contextProposalAnalystProgressLines(activity: LiveRoleActivity): string[] {
1161
- return [
1162
- ...buildInlineRunningLines({
1163
- role: activity.role,
1164
- startedAt: activity.startedAt,
1165
- updatedAt: activity.updatedAt,
1166
- currentAction: activity.currentAction,
1167
- toolActivity: activity.toolActivity,
1168
- toolRecentActivity: activity.toolRecentActivity,
1169
- recentActivity: activity.recentActivity,
1170
- assistantSummary: activity.assistantSummary,
1171
- progress: activity.progress,
1172
- rationale: activity.rationale,
1173
- nextStep: activity.nextStep,
1174
- verifying: activity.verifying,
1175
- stateDeltas: activity.stateDeltas,
1176
- }),
1177
- "",
1178
- "This step only prepares a proposal for confirmation.",
1179
- ];
1180
- }
1181
-
1182
- async function runContextProposalAnalystSubprocess(
1183
- ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
1184
- projectName: string,
1185
- recentEntries: RecentDiscussionEntry[],
1186
- hintText?: string,
1187
- ): Promise<string | undefined> {
1188
- const modelArg = contextProposalAnalystModelArg(ctx.model);
1189
- if (!modelArg) return undefined;
1190
- const cwd = getCtxCwd(ctx);
1191
- const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
1192
- const rootKey = completionRootKey(undefined, cwd);
1193
- const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries, hintText);
1194
- const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
1195
- const analystRole = "cook-proposal-analyst";
1196
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
1197
- const invocation = getPiInvocation(args);
1198
- const liveActivity = createLiveRoleActivity(analystRole);
1199
- liveActivity.progress = hintText ? "Analyzing recent discussion and inline hint" : "Analyzing recent discussion";
1200
- liveActivity.currentAction = hintText
1201
- ? "Reading recent discussion plus the inline /cook hint and preparing a startup proposal"
1202
- : "Reading recent discussion and preparing a startup proposal";
1203
- liveActivity.assistantSummary = liveActivity.progress;
1204
- liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
1205
- const messages: RoleMessage[] = [];
1206
- let stderr = "";
1207
- let overlay: StartupAnalystOverlay | undefined;
1208
- let finishOverlay: ((value: string | undefined) => void) | undefined;
1209
- let overlaySettled = false;
1210
- const settleOverlay = (value: string | undefined) => {
1211
- if (overlaySettled) return;
1212
- overlaySettled = true;
1213
- finishOverlay?.(value);
1214
- };
1215
- const updateActivity = (fresh = false) => {
1216
- if (fresh) liveActivity.updatedAt = nowMs();
1217
- liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
1218
- void refreshStatus(ctx);
1219
- overlay?.setLines(contextProposalAnalystProgressLines(liveActivity));
1220
- };
1221
- const heartbeat = setInterval(() => updateActivity(false), LIVE_ROLE_HEARTBEAT_MS);
1222
- const run = async (): Promise<string | undefined> => {
1223
- try {
1224
- updateActivity(true);
1225
- const output = await new Promise<string | undefined>((resolve) => {
1226
- const proc = spawn(invocation.command, invocation.args, {
1227
- cwd: runCwd,
1228
- env: process.env,
1229
- stdio: ["ignore", "pipe", "pipe"],
1230
- shell: false,
1231
- });
1232
- let settled = false;
1233
- const resolveOnce = (value: string | undefined) => {
1234
- if (settled) return;
1235
- settled = true;
1236
- resolve(value);
1237
- };
1238
- const abort = () => {
1239
- proc.kill("SIGTERM");
1240
- resolveOnce(undefined);
1241
- };
1242
- const handleSigint = () => abort();
1243
- let buffer = "";
1244
- const processLine = (line: string) => {
1245
- if (!line.trim()) return;
1246
- try {
1247
- const event = JSON.parse(line) as JsonRecord;
1248
- if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
1249
- } catch {
1250
- // ignore malformed lines
1251
- }
1252
- };
1253
- proc.stdout.on("data", (chunk) => {
1254
- buffer += chunk.toString();
1255
- const lines = buffer.split("\n");
1256
- buffer = lines.pop() ?? "";
1257
- for (const line of lines) processLine(line);
1258
- });
1259
- proc.stderr.on("data", (chunk) => {
1260
- stderr += chunk.toString();
1261
- });
1262
- proc.on("close", (code) => {
1263
- process.off("SIGINT", handleSigint);
1264
- if (buffer.trim()) processLine(buffer);
1265
- resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
1266
- });
1267
- proc.on("error", () => {
1268
- process.off("SIGINT", handleSigint);
1269
- resolveOnce(undefined);
1270
- });
1271
- process.once("SIGINT", handleSigint);
1272
- if (overlay) {
1273
- overlay.onAbort = () => {
1274
- process.off("SIGINT", handleSigint);
1275
- abort();
1276
- };
1277
- }
1278
- });
1279
- liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
1280
- await refreshStatus(ctx);
1281
- return output;
1282
- } finally {
1283
- clearInterval(heartbeat);
1284
- setTimeout(() => {
1285
- const current = liveRoleActivityByRoot.get(rootKey);
1286
- if (current && current.role === analystRole && current.status !== "running") {
1287
- liveRoleActivityByRoot.delete(rootKey);
1288
- void refreshStatus(ctx);
1289
- }
1290
- }, 10_000);
1291
- await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
1292
- }
1293
- };
1294
- if (getCtxHasUI(ctx)) {
1295
- const ui = getCtxUi(ctx);
1296
- if (ui) {
1297
- return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
1298
- finishOverlay = done;
1299
- overlay = new StartupAnalystOverlay(theme);
1300
- overlay.setLines(contextProposalAnalystProgressLines(liveActivity));
1301
- run().then(settleOverlay).catch(() => settleOverlay(undefined));
1302
- return overlay;
1303
- });
1304
- }
1305
- }
1306
- return await run();
1307
- }
1308
-
1309
- async function analyzeContextProposalWithAgent(
1310
- ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
1311
- projectName: string,
1312
- recentEntries: RecentDiscussionEntry[],
1313
- hintText?: string,
1314
- ): Promise<ContextProposal | undefined> {
1315
- if (shouldDisableContextProposalAnalyst()) return undefined;
1316
- const testOutput = completionTestContextProposalAnalystOutput();
1317
- if (testOutput) {
1318
- return parseContextProposalAnalystOutput(testOutput, projectName);
1319
- }
1320
- if (recentEntries.length === 0 && !hintText?.trim()) return undefined;
1321
- try {
1322
- const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries, hintText);
1323
- if (!raw) return undefined;
1324
- return parseContextProposalAnalystOutput(raw, projectName);
1325
- } catch (error) {
1326
- console.warn("[completion] context proposal analyst failed", error);
1327
- return undefined;
1328
- }
1329
- }
1330
-
1331
- function buildContextProposalGoalText(proposal: {
1332
- mission: string;
1333
- scope: string[];
1334
- constraints: string[];
1335
- acceptance: string[];
1336
- }): string {
1337
- const lines = [`Mission: ${proposal.mission}`];
1338
- if (proposal.scope.length > 0) {
1339
- lines.push("", "Scope:");
1340
- for (const item of proposal.scope) lines.push(`- ${item}`);
1341
- }
1342
- if (proposal.constraints.length > 0) {
1343
- lines.push("", "Constraints:");
1344
- for (const item of proposal.constraints) lines.push(`- ${item}`);
1345
- }
1346
- if (proposal.acceptance.length > 0) {
1347
- lines.push("", "Acceptance:");
1348
- for (const item of proposal.acceptance) lines.push(`- ${item}`);
1349
- }
1350
- return lines.join("\n");
1351
- }
1352
-
1353
- function buildContextProposalDisplayText(proposal: ContextProposal): string {
1354
- const lines = ["Mission", proposal.mission];
1355
- if (proposal.scope.length > 0) {
1356
- lines.push("", "Scope");
1357
- for (const item of proposal.scope) lines.push(`- ${item}`);
1358
- }
1359
- if (proposal.constraints.length > 0) {
1360
- lines.push("", "Constraints");
1361
- for (const item of proposal.constraints) lines.push(`- ${item}`);
1362
- }
1363
- if (proposal.acceptance.length > 0) {
1364
- lines.push("", "Acceptance");
1365
- for (const item of proposal.acceptance) lines.push(`- ${item}`);
1366
- }
1367
- return lines.join("\n");
1368
- }
1369
-
1370
- function finalizeContextProposalAnalysis(analysis: ContextProposalAnalysis | undefined, hintTexts: string[] = []): ContextProposalAnalysis {
1371
- const merged = mergeContextProposalAnalysis(analysis ? [analysis] : [], hintTexts);
1372
- return {
1373
- taskType: merged.taskType ?? DEFAULT_TASK_TYPE,
1374
- evaluationProfile: merged.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
1375
- critique: merged.critique,
1376
- risks: merged.risks,
1377
- possibleNoise: merged.possibleNoise,
1378
- };
1379
- }
1380
-
1381
- function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
1382
- const lines: string[] = [];
1383
- if (analysis.critique.length > 0) {
1384
- lines.push("Critique");
1385
- for (const item of analysis.critique) lines.push(`- ${item}`);
1386
- }
1387
- if (analysis.risks.length > 0) {
1388
- if (lines.length > 0) lines.push("");
1389
- lines.push("Risks");
1390
- for (const item of analysis.risks) lines.push(`- ${item}`);
1391
- }
1392
- if (analysis.possibleNoise.length > 0) {
1393
- if (lines.length > 0) lines.push("");
1394
- lines.push("Possible noise");
1395
- for (const item of analysis.possibleNoise) lines.push(`- ${item}`);
1396
- }
1397
- if (lines.length === 0) {
1398
- return "No critique, risk, or possible-noise notes were derived for this startup proposal.";
1399
- }
1400
- return lines.join("\n");
1401
- }
1402
-
1403
- function buildContextProposalRoutingText(analysis: ContextProposalAnalysis): string {
1404
- return [`- task_type: ${analysis.taskType ?? DEFAULT_TASK_TYPE}`, `- evaluation_profile: ${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}`].join(
1405
- "\n",
1406
- );
1407
- }
1408
-
1409
- function summarizeContextProposalAnalysisItems(label: string, items: string[]): string | undefined {
1410
- if (items.length === 0) return undefined;
1411
- return `${label}=${truncateInline(items.join(" | "), 160)}`;
1412
- }
1413
-
1414
- function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
1415
- const critiqueParts = [
1416
- analysis.critique.length > 0 ? `accepted critique=${truncateInline(analysis.critique.join(" | "), 160)}` : "accepted critique=none",
1417
- summarizeContextProposalAnalysisItems("risks", analysis.risks),
1418
- summarizeContextProposalAnalysisItems("possible_noise", analysis.possibleNoise),
1419
- ].filter((part): part is string => Boolean(part));
1420
- return `${prefix} ${truncateInline(goalText, 220)} | startup routing: task_type=${analysis.taskType ?? DEFAULT_TASK_TYPE}; evaluation_profile=${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}; critique outcome=${critiqueParts.join("; ")}`;
1421
- }
1422
-
1423
- function buildContextProposalConfirmationActions(): ContextProposalConfirmationActionItem[] {
1424
- return [
1425
- {
1426
- id: "start",
1427
- label: "Start",
1428
- description: "Accept this proposal and let /cook write or refocus canonical workflow state.",
1429
- },
1430
- {
1431
- id: "cancel",
1432
- label: "Cancel",
1433
- description: `Stop here without changing canonical workflow state. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`,
1434
- },
1435
- ];
1436
- }
1437
-
1438
- function buildContextProposalConfirmationLayout(
1439
- title: string,
1440
- proposal: ContextProposal,
1441
- ): ContextProposalConfirmationLayout {
1442
- const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
1443
- return {
1444
- title,
1445
- intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
1446
- proposalHeading: "Proposed workflow",
1447
- proposalBody: buildContextProposalDisplayText(proposal),
1448
- critiqueHeading: "Critique and risks",
1449
- critiqueBody: buildContextProposalCritiqueText(analysis),
1450
- routingHeading: "Routing recommendations",
1451
- routingBody: buildContextProposalRoutingText(analysis),
1452
- actionsHeading: "Actions",
1453
- actions: buildContextProposalConfirmationActions(),
1454
- footer: "↑↓ navigate • enter select • esc cancel",
1455
- };
1456
- }
1457
-
1458
- function maybeWriteContextProposalConfirmationSnapshot(layout: ContextProposalConfirmationLayout): void {
1459
- const snapshotPath = completionTestContextProposalUiSnapshotPath();
1460
- if (!snapshotPath) return;
1461
- try {
1462
- fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
1463
- fs.writeFileSync(snapshotPath, `${JSON.stringify(layout, null, 2)}\n`, "utf8");
1464
- } catch {
1465
- // ignore malformed or unwritable test snapshot paths
1466
- }
1467
- }
1468
-
1469
- function maybeWriteContextProposalSnapshot(proposal: ContextProposal): void {
1470
- const snapshotPath = completionTestContextProposalSnapshotPath();
1471
- if (!snapshotPath) return;
1472
- try {
1473
- fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
1474
- fs.writeFileSync(snapshotPath, `${JSON.stringify(proposal, null, 2)}\n`, "utf8");
1475
- } catch {
1476
- // ignore malformed or unwritable test snapshot paths
1477
- }
1478
- }
1479
-
1480
- function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout): SelectItem[] {
1481
- return layout.actions.map((action) => ({
1482
- value: action.id,
1483
- label: action.label,
1484
- description: action.description,
1485
- }));
299
+ function buildContextProposalConfirmationLayout(
300
+ title: string,
301
+ proposal: ContextProposal,
302
+ ): ContextProposalConfirmationLayout {
303
+ return buildExtractedContextProposalConfirmationLayout({
304
+ title,
305
+ proposal,
306
+ analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
307
+ mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
308
+ defaultTaskType: DEFAULT_TASK_TYPE,
309
+ defaultEvaluationProfile: DEFAULT_EVALUATION_PROFILE,
310
+ });
1486
311
  }
1487
312
 
1488
313
  async function promptContextProposalConfirmationAction(
@@ -1538,258 +363,59 @@ async function promptContextProposalConfirmationAction(
1538
363
  });
1539
364
  }
1540
365
 
1541
- async function resolveContextProposalConfirmationAction(
1542
- proposal: ContextProposal,
1543
- action: ContextProposalConfirmAction,
1544
- ): Promise<ContextProposalDecision | undefined> {
1545
- if (action === "cancel") return undefined;
1546
- return {
1547
- missionAnchor: proposal.mission,
1548
- goalText: proposal.goalText,
1549
- analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
1550
- };
1551
- }
1552
-
1553
- function parseContextProposal(text: string, projectName: string): ContextProposal | undefined {
1554
- const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1555
- if (!cleaned) return undefined;
1556
- const lines = cleaned
1557
- .split("\n")
1558
- .map((line) => line.trim())
1559
- .filter((line) => line.length > 0);
1560
- if (lines.length === 0) return undefined;
1561
-
1562
- let section: ContextProposalSection | undefined;
1563
- let missionLine: string | undefined;
1564
- let taskTypeHint: string | undefined;
1565
- let evaluationProfileHint: string | undefined;
1566
- const scope: string[] = [];
1567
- const constraints: string[] = [];
1568
- const acceptance: string[] = [];
1569
- const critique: string[] = [];
1570
- const risks: string[] = [];
1571
- let structuredSignalCount = 0;
1572
-
1573
- for (const rawLine of lines) {
1574
- const routingHint = matchContextProposalRoutingHint(rawLine);
1575
- if (routingHint) {
1576
- structuredSignalCount += 1;
1577
- if (routingHint.field === "taskType") taskTypeHint = routingHint.value;
1578
- else evaluationProfileHint = routingHint.value;
1579
- continue;
1580
- }
1581
- const inlineSection = matchInlineProposalSection(rawLine);
1582
- if (inlineSection) {
1583
- section = inlineSection.section;
1584
- structuredSignalCount += 1;
1585
- if (inlineSection.section === "mission" && !missionLine) {
1586
- missionLine = inlineSection.content;
1587
- } else if (inlineSection.section === "constraints") {
1588
- constraints.push(inlineSection.content);
1589
- } else if (inlineSection.section === "acceptance") {
1590
- acceptance.push(inlineSection.content);
1591
- } else if (inlineSection.section === "scope") {
1592
- scope.push(inlineSection.content);
1593
- } else if (inlineSection.section === "critique") {
1594
- critique.push(inlineSection.content);
1595
- } else if (inlineSection.section === "risks") {
1596
- risks.push(inlineSection.content);
1597
- }
1598
- continue;
1599
- }
1600
- const headerSection = detectProposalSection(rawLine);
1601
- if (headerSection) {
1602
- section = headerSection;
1603
- structuredSignalCount += 1;
1604
- continue;
1605
- }
1606
- const bullet = bulletText(rawLine);
1607
- if (bullet) {
1608
- structuredSignalCount += 1;
1609
- if (section === "mission" && !missionLine) {
1610
- missionLine = bullet;
1611
- continue;
1612
- }
1613
- if (section === "constraints") {
1614
- constraints.push(bullet);
1615
- continue;
1616
- }
1617
- if (section === "acceptance") {
1618
- acceptance.push(bullet);
1619
- continue;
1620
- }
1621
- if (section === "scope") {
1622
- scope.push(bullet);
1623
- continue;
1624
- }
1625
- if (section === "critique") {
1626
- critique.push(bullet);
1627
- continue;
1628
- }
1629
- if (section === "risks") {
1630
- risks.push(bullet);
1631
- continue;
1632
- }
1633
- if (!missionLine) {
1634
- missionLine = bullet;
1635
- continue;
1636
- }
1637
- if (looksLikeAcceptance(bullet)) acceptance.push(bullet);
1638
- else if (looksLikeConstraint(bullet)) constraints.push(bullet);
1639
- else scope.push(bullet);
1640
- continue;
1641
- }
1642
- const normalized = normalizeProposalLine(rawLine);
1643
- if (!normalized) continue;
1644
- if (!missionLine) {
1645
- missionLine = normalized;
1646
- continue;
1647
- }
1648
- if (section === "critique") {
1649
- critique.push(normalized);
1650
- continue;
1651
- }
1652
- if (section === "risks") {
1653
- risks.push(normalized);
1654
- continue;
1655
- }
1656
- if (section === "constraints" || looksLikeConstraint(normalized)) {
1657
- constraints.push(normalized);
1658
- continue;
1659
- }
1660
- if (section === "acceptance" || looksLikeAcceptance(normalized)) {
1661
- acceptance.push(normalized);
1662
- continue;
1663
- }
1664
- if (section === "scope") {
1665
- scope.push(normalized);
1666
- }
1667
- }
1668
-
1669
- const basisPreview = cleaned.replace(/\s+/g, " ").trim();
1670
- const missionSource = missionLine ?? scope[0] ?? acceptance[0] ?? constraints[0] ?? basisPreview;
1671
- const assessment = assessMissionAnchor(missionSource, projectName);
1672
- const normalizedMission = normalizeMissionAnchorText(missionSource);
1673
- const itemCount = scope.length + constraints.length + acceptance.length + critique.length + risks.length;
1674
- const hasStrongStructure = structuredSignalCount >= 2 || itemCount >= 2;
1675
- if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
1676
- if (!hasStrongStructure && basisPreview.length < 140) return undefined;
1677
- const mission = assessment.derived;
1678
- const analysis = buildContextProposalAnalysis({
1679
- taskType: taskTypeHint,
1680
- evaluationProfile: evaluationProfileHint,
1681
- critique,
1682
- risks,
1683
- hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
1684
- });
1685
- const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
1686
- return finalizeContextProposal(
1687
- {
1688
- mission,
1689
- scope,
1690
- constraints,
1691
- acceptance,
1692
- analysis,
1693
- goalText,
1694
- basisPreview,
1695
- source: "session",
1696
- },
1697
- projectName,
1698
- );
1699
- }
1700
-
1701
- function hasStructuredContextProposalSignal(text: string): boolean {
1702
- const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1703
- if (!cleaned) return false;
1704
- return /(^|\n)\s*(mission|goal|objective|summary|scope|plan|steps|implementation|constraints?|guardrails|non-goals|acceptance|acceptance criteria|deliverables|verification|critique|concerns?|warnings?|notes?|risks?|hazards?|task[\s_-]*type|evaluation[\s_-]*profile)\s*(?:[::]\s*|$)/imu.test(
1705
- cleaned,
1706
- );
1707
- }
1708
-
1709
- function parseStrictStructuredSessionProposal(text: string, projectName: string): ContextProposal | undefined {
1710
- const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
1711
- if (!cleaned) return undefined;
1712
- const lines = cleaned
1713
- .split("\n")
1714
- .map((line) => line.trim())
1715
- .filter((line) => line.length > 0);
1716
- if (lines.length === 0) return undefined;
1717
-
1718
- let section: ContextProposalSection | undefined;
1719
- const sectionsPresent = new Set<ContextProposalSection>();
1720
- const missionCandidates: string[] = [];
1721
-
1722
- for (const rawLine of lines) {
1723
- const inlineSection = matchInlineProposalSection(rawLine);
1724
- if (inlineSection) {
1725
- section = inlineSection.section;
1726
- sectionsPresent.add(section);
1727
- if (section === "mission") missionCandidates.push(inlineSection.content);
1728
- continue;
1729
- }
1730
- const headerSection = detectProposalSection(rawLine);
1731
- if (headerSection) {
1732
- section = headerSection;
1733
- sectionsPresent.add(section);
1734
- continue;
1735
- }
1736
- const normalized = bulletText(rawLine) ?? normalizeProposalLine(rawLine);
1737
- if (normalized && section === "mission") missionCandidates.push(normalized);
1738
- }
1739
-
1740
- const requiredSections: ContextProposalSection[] = ["mission", "scope", "constraints", "acceptance"];
1741
- if (requiredSections.some((candidate) => !sectionsPresent.has(candidate))) return undefined;
1742
-
1743
- const distinctMissionAnchors = Array.from(
1744
- new Set(
1745
- missionCandidates
1746
- .map((candidate) => normalizeMissionAnchorText(assessMissionAnchor(candidate, projectName).derived))
1747
- .filter((candidate): candidate is string => Boolean(candidate)),
1748
- ),
1749
- );
1750
- if (distinctMissionAnchors.length !== 1) return undefined;
1751
-
1752
- const proposal = parseContextProposal(cleaned, projectName);
1753
- if (!proposal) return undefined;
1754
- if (
1755
- normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0] &&
1756
- !isGenericPlanningMissionAnchor(distinctMissionAnchors[0])
1757
- ) {
1758
- return undefined;
1759
- }
1760
- if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
1761
- return { ...proposal, source: "session" };
1762
- }
1763
-
1764
- function extractContextProposalFromStructuredSession(
1765
- recentEntries: RecentDiscussionEntry[],
1766
- projectName: string,
1767
- ): ContextProposal | undefined {
1768
- const structuredTexts = recentEntries
1769
- .slice()
1770
- .reverse()
1771
- .map((entry) => entry.text.trim())
1772
- .filter((text) => hasStructuredContextProposalSignal(text));
1773
- if (structuredTexts.length === 0) return undefined;
1774
- return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName);
1775
- }
1776
-
1777
- async function extractContextProposalFromSession(
1778
- ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1779
- projectName: string,
1780
- hintText?: string,
1781
- ): Promise<ContextProposal | undefined> {
1782
- const recentEntries = collectRecentDiscussionEntries(ctx);
1783
- return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, hintText)) ??
1784
- extractContextProposalFromStructuredSession(recentEntries, projectName);
1785
- }
1786
-
1787
366
  async function deriveCookContextProposal(
1788
367
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1789
368
  projectName: string,
1790
- hintText?: string,
1791
369
  ): Promise<ContextProposal | undefined> {
1792
- return await extractContextProposalFromSession(ctx, projectName, hintText);
370
+ const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
371
+ const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
372
+ const workflowContextLines = snapshot
373
+ ? [
374
+ `current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
375
+ `continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
376
+ `latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
377
+ `latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
378
+ `active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
379
+ `active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
380
+ `verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
381
+ `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
382
+ ]
383
+ : [];
384
+ return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
385
+ asString,
386
+ asStringArray,
387
+ workflowContext: snapshot
388
+ ? {
389
+ currentMissionAnchor:
390
+ asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
391
+ latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
392
+ latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
393
+ activeSliceGoal: asString(snapshot.active?.goal),
394
+ activeSliceWhyNow: asString(snapshot.active?.why_now),
395
+ verificationGoal: asString(snapshot.verificationEvidence?.goal),
396
+ verificationSummary: asString(snapshot.verificationEvidence?.summary),
397
+ continuationPolicy: asString(snapshot.state?.continuation_policy),
398
+ }
399
+ : undefined,
400
+ analyzeContextProposal: async (entries) =>
401
+ await analyzeContextProposalWithAgent({
402
+ ctx,
403
+ projectName,
404
+ recentEntries: entries,
405
+ workflowContextLines,
406
+ liveRoleActivityByRoot,
407
+ completionStatusKey: COMPLETION_STATUS_KEY,
408
+ safeUiCall,
409
+ getCtxCwd,
410
+ getCtxHasUI,
411
+ getCtxUi,
412
+ }),
413
+ assessMissionAnchor,
414
+ isWeakMissionAnchor,
415
+ missionAnchorsStrictlyEquivalent,
416
+ normalizeMissionAnchorText,
417
+ stripCodeBlocks,
418
+ });
1793
419
  }
1794
420
 
1795
421
  async function confirmContextProposal(
@@ -1797,917 +423,42 @@ async function confirmContextProposal(
1797
423
  proposal: ContextProposal,
1798
424
  options: ContextProposalConfirmOptions,
1799
425
  ): Promise<ContextProposalDecision | undefined> {
1800
- maybeWriteContextProposalSnapshot(proposal);
426
+ maybeWriteContextProposalSnapshot(proposal, completionTestContextProposalSnapshotPath());
1801
427
  const actionOverride = completionTestContextProposalActionOverride();
1802
428
  if (actionOverride === "cancel") return undefined;
1803
429
  if (actionOverride === "accept") {
1804
- return await resolveContextProposalConfirmationAction(proposal, "start");
430
+ return resolveContextProposalConfirmationAction(proposal, "start");
1805
431
  }
1806
432
  const layout = buildContextProposalConfirmationLayout(options.title, proposal);
1807
- maybeWriteContextProposalConfirmationSnapshot(layout);
433
+ maybeWriteContextProposalConfirmationSnapshot(layout, completionTestContextProposalUiSnapshotPath());
1808
434
  const uiActionOverride = completionTestContextProposalUiActionOverride();
1809
435
  if (uiActionOverride) {
1810
- return await resolveContextProposalConfirmationAction(proposal, uiActionOverride);
436
+ return resolveContextProposalConfirmationAction(proposal, uiActionOverride);
1811
437
  }
1812
438
  if (!getCtxHasUI(ctx)) {
1813
- return options.nonInteractiveBehavior === "accept"
1814
- ? await resolveContextProposalConfirmationAction(proposal, "start")
1815
- : undefined;
439
+ return options.nonInteractiveBehavior === "accept" ? resolveContextProposalConfirmationAction(proposal, "start") : undefined;
1816
440
  }
1817
441
  const ui = getCtxUi(ctx);
1818
442
  if (!ui) {
1819
- return options.nonInteractiveBehavior === "accept"
1820
- ? await resolveContextProposalConfirmationAction(proposal, "start")
1821
- : undefined;
443
+ return options.nonInteractiveBehavior === "accept" ? resolveContextProposalConfirmationAction(proposal, "start") : undefined;
1822
444
  }
1823
445
  const choice = await promptContextProposalConfirmationAction(ui, layout);
1824
446
  if (!choice) return undefined;
1825
- return await resolveContextProposalConfirmationAction(proposal, choice);
1826
- }
1827
-
1828
- function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
1829
- return (
1830
- asString(snapshot.state?.mission_anchor) ??
1831
- asString(snapshot.plan?.mission_anchor) ??
1832
- asString(snapshot.active?.mission_anchor) ??
1833
- path.basename(snapshot.files.root)
1834
- );
1835
- }
1836
-
1837
- function currentTaskType(snapshot: CompletionStateSnapshot): string | undefined {
1838
- return (
1839
- asString(snapshot.active?.task_type) ??
1840
- asString(snapshot.state?.task_type) ??
1841
- asString(snapshot.plan?.task_type) ??
1842
- asString(snapshot.profile?.task_type)
1843
- );
1844
- }
1845
-
1846
- function currentEvaluationProfile(snapshot: CompletionStateSnapshot): string | undefined {
1847
- return (
1848
- asString(snapshot.active?.evaluation_profile) ??
1849
- asString(snapshot.state?.evaluation_profile) ??
1850
- asString(snapshot.plan?.evaluation_profile) ??
1851
- asString(snapshot.profile?.evaluation_profile)
1852
- );
1853
- }
1854
-
1855
- function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
1856
- if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
1857
- const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
1858
- if (!nextMandatoryRole) return undefined;
1859
- return JSON.stringify({
1860
- mission_anchor: asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? null,
1861
- task_type: currentTaskType(snapshot) ?? null,
1862
- evaluation_profile: currentEvaluationProfile(snapshot) ?? null,
1863
- current_phase: asString(snapshot.state?.current_phase) ?? null,
1864
- next_mandatory_role: nextMandatoryRole,
1865
- next_mandatory_action: asString(snapshot.state?.next_mandatory_action) ?? null,
1866
- active_status: asString(snapshot.active?.status) ?? null,
1867
- active_slice_id: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? null,
1868
- latest_completed_slice: asString(snapshot.state?.latest_completed_slice) ?? null,
1869
- latest_verified_slice: asString(snapshot.state?.latest_verified_slice) ?? null,
1870
- });
1871
- }
1872
-
1873
- function noteQueuedDriverPrompt(rootKey: string, fingerprint: string): void {
1874
- const tracker = driverContinuationByRoot.get(rootKey);
1875
- if (tracker && tracker.fingerprint === fingerprint) {
1876
- tracker.attempts += 1;
1877
- tracker.inFlight = false;
1878
- tracker.warned = false;
1879
- return;
1880
- }
1881
- driverContinuationByRoot.set(rootKey, {
1882
- fingerprint,
1883
- attempts: 1,
1884
- inFlight: false,
1885
- warned: false,
1886
- });
1887
- }
1888
-
1889
- function markQueuedDriverPromptInFlight(rootKey: string, fingerprint: string): void {
1890
- const tracker = driverContinuationByRoot.get(rootKey);
1891
- if (!tracker || tracker.fingerprint !== fingerprint) return;
1892
- tracker.inFlight = true;
1893
- }
1894
-
1895
- function clearDriverContinuationTracker(rootKey: string): void {
1896
- driverContinuationByRoot.delete(rootKey);
1897
- }
1898
-
1899
- function hasRunningCompletionRole(rootKey: string): boolean {
1900
- return liveRoleActivityByRoot.get(rootKey)?.status === "running";
1901
- }
1902
-
1903
- function isWorkflowDriverActive(snapshot: CompletionStateSnapshot | undefined): boolean {
1904
- return Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) === "continue";
1905
- }
1906
-
1907
- function isDriverContinuationStateParked(rootKey: string, fingerprint: string): boolean {
1908
- const tracker = driverContinuationByRoot.get(rootKey);
1909
- if (!tracker || tracker.fingerprint !== fingerprint) return false;
1910
- return tracker.warned;
1911
- }
1912
-
1913
- function rememberParkedDriverContinuation(rootKey: string, fingerprint: string): void {
1914
- const tracker = driverContinuationByRoot.get(rootKey);
1915
- if (!tracker || tracker.fingerprint !== fingerprint) return;
1916
- tracker.warned = true;
1917
- tracker.inFlight = false;
1918
- }
1919
-
1920
- async function queueCompletionDriverPrompt(
1921
- pi: ExtensionAPI,
1922
- ctx: { cwd: string; hasUI: boolean; ui: any },
1923
- rootKey: string,
1924
- fingerprint: string,
1925
- prompt: string,
1926
- kind: "kickoff" | "resume" | "auto-resume",
1927
- ): Promise<boolean> {
1928
- const snapshotPath = kind === "auto-resume" ? completionTestAutoContinuePromptPath() : completionTestDriverPromptPath();
1929
- maybeWriteTestSnapshot(snapshotPath, `${prompt}\n`);
1930
- noteQueuedDriverPrompt(rootKey, fingerprint);
1931
- if (shouldSkipDriverKickoffForTests()) {
1932
- emitCommandText(ctx, `Skipped completion workflow ${kind} prompt (test mode)`, "info");
1933
- return false;
1934
- }
1935
- pi.sendUserMessage(prompt);
1936
- emitCommandText(ctx, `Queued completion workflow ${kind}`, "info");
1937
- return true;
1938
- }
1939
-
1940
- async function autoContinueWorkflowIfNeeded(pi: ExtensionAPI, ctx: { cwd: string; hasUI: boolean; ui: any }): Promise<void> {
1941
- if (roleFromEnv()) return;
1942
- const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
1943
- const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
1944
- if (!snapshot) {
1945
- clearDriverContinuationTracker(rootKey);
1946
- return;
1947
- }
1948
- if (!hasCompletionRoutingActivation(snapshot)) return;
1949
- const fingerprint = completionContinuationFingerprint(snapshot);
1950
- if (!fingerprint) {
1951
- clearDriverContinuationTracker(rootKey);
1952
- return;
1953
- }
1954
- if (!isWorkflowDriverActive(snapshot) || hasRunningCompletionRole(rootKey)) return;
1955
- const tracker = driverContinuationByRoot.get(rootKey);
1956
- if (tracker && tracker.fingerprint === fingerprint) {
1957
- if (tracker.inFlight) {
1958
- tracker.inFlight = false;
1959
- if (tracker.attempts >= DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS) {
1960
- if (!isDriverContinuationStateParked(rootKey, fingerprint)) {
1961
- rememberParkedDriverContinuation(rootKey, fingerprint);
1962
- emitCommandText(
1963
- ctx,
1964
- `Completion workflow is parked before mandatory role dispatch: ${asString(snapshot.state?.next_mandatory_role) ?? "(unknown)"}. Rerun /cook to continue from canonical state.`,
1965
- "warning",
1966
- );
1967
- }
1968
- return;
1969
- }
1970
- } else {
1971
- return;
1972
- }
1973
- }
1974
- const resumePrompt = completionResumePrompt(currentTaskType(snapshot) ?? "(missing)", currentEvaluationProfile(snapshot) ?? "(missing)");
1975
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, "auto-resume");
1976
- }
1977
-
1978
- function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
1979
- return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
1980
- }
1981
-
1982
- function activeSliceContext(snapshot: CompletionStateSnapshot) {
1983
- const active = snapshot.active;
1984
- const activeSlice = snapshot.activeSlice;
1985
- return {
1986
- sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
1987
- status: asString(active?.status) ?? asString(activeSlice?.status),
1988
- goal: asString(active?.goal) ?? asString(activeSlice?.goal),
1989
- contractIds:
1990
- asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
1991
- acceptance:
1992
- asStringArray(active?.acceptance_criteria).length > 0
1993
- ? asStringArray(active?.acceptance_criteria)
1994
- : asStringArray(activeSlice?.acceptance_criteria),
1995
- implementationSurfaces: asStringArray(active?.implementation_surfaces),
1996
- verificationCommands: asStringArray(active?.verification_commands),
1997
- lockedNotes: asStringArray(active?.locked_notes),
1998
- mustFixFindings: asStringArray(active?.must_fix_findings),
1999
- remainingBefore: asStringArray(active?.remaining_contract_ids_before),
2000
- basisCommit: asString(active?.basis_commit),
2001
- releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
2002
- highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
2003
- };
2004
- }
2005
-
2006
- function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
2007
- const evidence = snapshot.verificationEvidence;
2008
- return {
2009
- path: path.relative(snapshot.files.root, snapshot.files.verificationEvidencePath) || ".agent/verification-evidence.json",
2010
- status: evidence ? "present" : "missing",
2011
- subjectType: asString(evidence?.subject_type),
2012
- sliceId: asString(evidence?.slice_id),
2013
- goal: asString(evidence?.goal),
2014
- contractIds: asStringArray(evidence?.contract_ids),
2015
- basisCommit: asString(evidence?.basis_commit),
2016
- headSha: asString(evidence?.head_sha),
2017
- verificationCommands: asStringArray(evidence?.verification_commands),
2018
- outcome: asString(evidence?.outcome),
2019
- recordedAt: asString(evidence?.recorded_at),
2020
- summary:
2021
- asString(evidence?.summary) ??
2022
- (evidence ? "Canonical verification evidence is present but its summary is missing." : "Canonical verification evidence is missing."),
2023
- };
2024
- }
2025
-
2026
- function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
2027
- const context = activeSliceContext(snapshot);
2028
- const evidence = verificationEvidenceContext(snapshot);
2029
- const lines = [
2030
- `Canonical evaluation handoff for ${role}:`,
2031
- `- task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
2032
- `- evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
2033
- `- latest_completed_slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
2034
- `- active_slice_id: ${context.sliceId ?? "(none)"}`,
2035
- `- active_slice_status: ${context.status ?? "(unknown)"}`,
2036
- `- active_slice_goal: ${context.goal ?? "(unknown)"}`,
2037
- `- contract_ids: ${context.contractIds.length > 0 ? context.contractIds.join(", ") : "(none)"}`,
2038
- `- acceptance_criteria: ${context.acceptance.length > 0 ? context.acceptance.join(" | ") : "(none)"}`,
2039
- `- implementation_surfaces: ${context.implementationSurfaces.length > 0 ? context.implementationSurfaces.join(" | ") : "(none)"}`,
2040
- `- verification_commands: ${context.verificationCommands.length > 0 ? context.verificationCommands.join(" | ") : "(none)"}`,
2041
- `- locked_notes: ${context.lockedNotes.length > 0 ? context.lockedNotes.join(" | ") : "(none)"}`,
2042
- `- must_fix_findings: ${context.mustFixFindings.length > 0 ? context.mustFixFindings.join(" | ") : "(none)"}`,
2043
- `- basis_commit: ${context.basisCommit ?? "(none)"}`,
2044
- `- remaining_contract_ids_before: ${context.remainingBefore.length > 0 ? context.remainingBefore.join(", ") : "(none)"}`,
2045
- `- release_blocker_count_before: ${context.releaseBlockerCountBefore ?? "(unknown)"}`,
2046
- `- high_value_gap_count_before: ${context.highValueGapCountBefore ?? "(unknown)"}`,
2047
- `- verification_evidence_path: ${evidence.path}`,
2048
- `- verification_evidence_status: ${evidence.status}`,
2049
- `- verification_evidence_subject_type: ${evidence.subjectType ?? "(missing)"}`,
2050
- `- verification_evidence_slice_id: ${evidence.sliceId ?? "(none)"}`,
2051
- `- verification_evidence_contract_ids: ${evidence.contractIds.length > 0 ? evidence.contractIds.join(", ") : "(none)"}`,
2052
- `- verification_evidence_outcome: ${evidence.outcome ?? "(missing)"}`,
2053
- `- verification_evidence_recorded_at: ${evidence.recordedAt ?? "(missing)"}`,
2054
- `- verification_evidence_head_sha: ${evidence.headSha ?? "(missing)"}`,
2055
- `- verification_evidence_basis_commit: ${evidence.basisCommit ?? "(missing)"}`,
2056
- `- verification_evidence_commands: ${evidence.verificationCommands.length > 0 ? evidence.verificationCommands.join(" | ") : "(none)"}`,
2057
- `- verification_evidence_summary: ${evidence.summary}`,
2058
- ];
2059
- return lines;
2060
- }
2061
-
2062
- function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
2063
- return buildEvaluationRoleContextLines(snapshot, role).join(" ");
2064
- }
2065
-
2066
- async function assessActiveWorkflowProposalRouting(
2067
- ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
2068
- snapshot: CompletionStateSnapshot,
2069
- hintText?: string,
2070
- ): Promise<ActiveWorkflowProposalAssessment> {
2071
- const currentMission = currentMissionAnchor(snapshot);
2072
- const projectName = path.basename(snapshot.files.root);
2073
- const proposal = await deriveCookContextProposal(ctx, projectName, hintText);
2074
- if (!proposal) {
2075
- const assessment: ActiveWorkflowProposalAssessment = {
2076
- action: "unclear",
2077
- currentMissionAnchor: currentMission,
2078
- reason: "missing_proposal",
2079
- };
2080
- maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2081
- return assessment;
2082
- }
2083
- if (missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
2084
- const assessment: ActiveWorkflowProposalAssessment = {
2085
- action: "continue",
2086
- currentMissionAnchor: currentMission,
2087
- proposal,
2088
- reason: "matching_mission",
2089
- };
2090
- maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2091
- return assessment;
2092
- }
2093
- if (shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
2094
- const assessment: ActiveWorkflowProposalAssessment = {
2095
- action: "refocus",
2096
- currentMissionAnchor: currentMission,
2097
- proposal,
2098
- reason: "clear_refocus",
2099
- };
2100
- maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2101
- return assessment;
2102
- }
2103
- const assessment: ActiveWorkflowProposalAssessment = {
2104
- action: "unclear",
2105
- currentMissionAnchor: currentMission,
2106
- proposal,
2107
- reason: "ambiguous_discussion",
2108
- };
2109
- maybeWriteActiveWorkflowRoutingSnapshot(assessment);
2110
- return assessment;
2111
- }
2112
-
2113
- async function resumeActiveWorkflowFromCanonicalState(
2114
- pi: any,
2115
- ctx: { cwd: string; hasUI: boolean; ui: any },
2116
- snapshot: CompletionStateSnapshot,
2117
- ): Promise<void> {
2118
- activateCompletionRoutingForRoot(snapshot.files.root);
2119
- const mission = currentMissionAnchor(snapshot);
2120
- pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
2121
- const resumePrompt = completionResumePrompt(
2122
- currentTaskType(snapshot) ?? "(missing)",
2123
- currentEvaluationProfile(snapshot) ?? "(missing)",
2124
- );
2125
- const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
2126
- const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
2127
- kind: "resume",
2128
- mission_anchor: mission,
2129
- current_phase: asString(snapshot.state?.current_phase) ?? null,
2130
- next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
2131
- });
2132
- const resumeKind = shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
2133
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
2134
- }
2135
-
2136
- async function confirmExistingWorkflowProposal(
2137
- ctx: { hasUI: boolean; ui: any },
2138
- snapshot: CompletionStateSnapshot,
2139
- proposal: ContextProposal,
2140
- options: ExistingWorkflowChooserOptions = {},
2141
- ): Promise<ExistingWorkflowDecision | undefined> {
2142
- const currentMission = currentMissionAnchor(snapshot);
2143
- const comparison = options.comparison ?? "semantic";
2144
- const missionsMatch =
2145
- comparison === "strict"
2146
- ? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
2147
- : missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
2148
- if (missionsMatch) {
2149
- return { action: "continue", currentMissionAnchor: currentMission };
2150
- }
2151
- const title = [
2152
- "Existing completion workflow found",
2153
- "",
2154
- options.intro ?? "A workflow is already in progress. Choose how /cook should proceed:",
2155
- "",
2156
- "Current mission",
2157
- currentMission,
2158
- "",
2159
- options.proposedMissionLabel ?? "New proposed mission",
2160
- proposal.mission,
2161
- ].join("\n");
2162
- const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
2163
- const refocusChoice =
2164
- options.refocusChoiceLabel ??
2165
- "Abandon current workflow and start this new one\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.";
2166
- const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
2167
- maybeWriteTestSnapshot(
2168
- completionTestExistingWorkflowChooserSnapshotPath(),
2169
- `${JSON.stringify({ title, choices: [continueChoice, refocusChoice, cancelChoice] }, null, 2)}\n`,
2170
- );
2171
- const actionOverride = completionTestWorkflowActionOverride();
2172
- if (actionOverride === "continue") {
2173
- return { action: "continue", currentMissionAnchor: currentMission };
2174
- }
2175
- if (actionOverride === "refocus") {
2176
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
2177
- }
2178
- if (actionOverride === "cancel") return undefined;
2179
- if (!getCtxHasUI(ctx)) {
2180
- return { action: "continue", currentMissionAnchor: currentMission };
2181
- }
2182
- const ui = getCtxUi(ctx);
2183
- if (!ui) {
2184
- return { action: "continue", currentMissionAnchor: currentMission };
2185
- }
2186
- const choice = await ui.select(title, [continueChoice, refocusChoice, cancelChoice]);
2187
- if (!choice || choice === cancelChoice) return undefined;
2188
- if (choice === refocusChoice) {
2189
- return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
2190
- }
2191
- return { action: "continue", currentMissionAnchor: currentMission };
2192
- }
2193
-
2194
- async function refocusCompletionMission(
2195
- snapshot: CompletionStateSnapshot,
2196
- missionAnchor: string,
2197
- rawGoal: string,
2198
- analysis?: ContextProposalAnalysis,
2199
- ): Promise<void> {
2200
- const requiredStopJudges = asNumber(snapshot.profile?.required_stop_judges) ?? 3;
2201
- const root = snapshot.files.root;
2202
- const routing = finalizeContextProposalAnalysis(analysis, [rawGoal, missionAnchor]);
2203
- const docsSurfaces = asStringArray(snapshot.profile?.docs_surfaces);
2204
- const nextProfile = buildProfileRecord({
2205
- projectName: asString(snapshot.profile?.project_name) ?? path.basename(root),
2206
- requiredStopJudges,
2207
- priorityPolicyId: asString(snapshot.profile?.priority_policy_id) ?? "completion-default",
2208
- docsSurfaces: docsSurfaces.length > 0 ? docsSurfaces : await detectDocsSurfaces(root),
2209
- taskType: routing.taskType,
2210
- evaluationProfile: routing.evaluationProfile,
2211
- });
2212
- const nextState = {
2213
- ...defaultState(missionAnchor, {
2214
- taskType: routing.taskType,
2215
- evaluationProfile: routing.evaluationProfile,
2216
- continuationReason: buildContextProposalContinuationReason("User refocused workflow via /cook:", rawGoal, routing),
2217
- }),
2218
- remaining_stop_judges: requiredStopJudges,
2219
- next_mandatory_action: "Reconcile canonical state from current repo truth for the refocused mission",
2220
- };
2221
- const nextPlan = {
2222
- ...defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }),
2223
- plan_basis: "user_refocus",
2224
- };
2225
- const nextActive = defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile });
2226
- await Promise.all([
2227
- fsp.writeFile(path.join(snapshot.files.agentDir, "mission.md"), buildMission(path.basename(root), missionAnchor), "utf8"),
2228
- writeJsonFile(snapshot.files.profilePath, nextProfile),
2229
- writeJsonFile(snapshot.files.statePath, nextState),
2230
- writeJsonFile(snapshot.files.planPath, nextPlan),
2231
- writeJsonFile(snapshot.files.activePath, nextActive),
2232
- writeJsonFile(snapshot.files.verificationEvidencePath, defaultVerificationEvidence()),
2233
- ]);
2234
- }
2235
-
2236
- function deriveMissionAnchor(rawGoal: string, projectName: string): string {
2237
- const normalized = normalizeMissionAnchorText(rawGoal);
2238
- if (!normalized || isWeakMissionAnchor(normalized)) {
2239
- return `Drive ${projectName} to truthful, verifiable completion.`;
2240
- }
2241
-
2242
- let mission = normalized
2243
- .replace(/\b(end[- ]to[- ]end|for me|thanks|thank you)\b/gi, "")
2244
- .replace(/\s+/g, " ")
2245
- .trim();
2246
-
2247
- mission = mission
2248
- .replace(/\bwith tests and docs\b/gi, "with tests and docs parity")
2249
- .replace(/\bwith tests and documentation\b/gi, "with tests and docs parity")
2250
- .replace(/\bwith docs\b/gi, "with docs parity")
2251
- .trim();
2252
-
2253
- if (!/[.!?。!?]$/u.test(mission)) mission += ".";
2254
- return mission;
2255
- }
2256
-
2257
- function buildProfileRecord(args: {
2258
- projectName: string;
2259
- requiredStopJudges: number;
2260
- priorityPolicyId?: string;
2261
- docsSurfaces: string[];
2262
- taskType?: string;
2263
- evaluationProfile?: string;
2264
- }): JsonRecord {
2265
- return {
2266
- schema_version: 1,
2267
- protocol_id: PROTOCOL_ID,
2268
- project_name: args.projectName,
2269
- required_stop_judges: args.requiredStopJudges,
2270
- priority_policy_id: args.priorityPolicyId ?? "completion-default",
2271
- task_type: args.taskType ?? DEFAULT_TASK_TYPE,
2272
- evaluation_profile: args.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
2273
- docs_surfaces: args.docsSurfaces,
2274
- };
2275
- }
2276
-
2277
- function defaultState(
2278
- missionAnchor: string,
2279
- routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
2280
- ): JsonRecord {
2281
- return {
2282
- schema_version: 1,
2283
- mission_anchor: missionAnchor,
2284
- current_phase: "reground",
2285
- continuation_policy: "continue",
2286
- continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
2287
- project_done: false,
2288
- task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
2289
- evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
2290
- requires_reground: true,
2291
- slices_since_last_reground: 0,
2292
- remaining_release_blockers: null,
2293
- remaining_high_value_gaps: null,
2294
- unsatisfied_contract_ids: [],
2295
- release_blocker_ids: [],
2296
- next_mandatory_action: "Reconcile canonical state from current repo truth",
2297
- next_mandatory_role: "completion-regrounder",
2298
- remaining_stop_judges: 3,
2299
- last_reground_at: null,
2300
- last_auditor_verdict: null,
2301
- contract_status: "unknown",
2302
- latest_completed_slice: null,
2303
- latest_verified_slice: null,
2304
- };
447
+ return resolveContextProposalConfirmationAction(proposal, choice);
2305
448
  }
2306
449
 
2307
- function defaultPlan(
2308
- missionAnchor: string,
2309
- routing?: { taskType?: string; evaluationProfile?: string },
2310
- ): JsonRecord {
2311
- return {
2312
- schema_version: 1,
2313
- mission_anchor: missionAnchor,
2314
- task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
2315
- evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
2316
- last_reground_at: null,
2317
- plan_basis: "bootstrap",
2318
- candidate_slices: [],
2319
- };
2320
- }
2321
-
2322
- function defaultActiveSlice(
2323
- missionAnchor: string,
2324
- routing?: { taskType?: string; evaluationProfile?: string },
2325
- ): JsonRecord {
2326
- return {
2327
- schema_version: 1,
2328
- mission_anchor: missionAnchor,
2329
- task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
2330
- evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
2331
- status: "idle",
2332
- slice_id: null,
2333
- goal: null,
2334
- contract_ids: [],
2335
- acceptance_criteria: [],
2336
- priority: null,
2337
- why_now: null,
2338
- blocked_on: [],
2339
- locked_notes: [],
2340
- must_fix_findings: [],
2341
- implementation_surfaces: [],
2342
- verification_commands: [],
2343
- basis_commit: null,
2344
- remaining_contract_ids_before: [],
2345
- release_blocker_count_before: null,
2346
- high_value_gap_count_before: null,
2347
- };
2348
- }
2349
450
 
2350
- function defaultVerificationEvidence(): JsonRecord {
2351
- return {
2352
- schema_version: 1,
2353
- artifact_type: "completion-verification-evidence",
2354
- subject_type: "none",
2355
- slice_id: null,
2356
- goal: null,
2357
- contract_ids: [],
2358
- basis_commit: null,
2359
- head_sha: null,
2360
- verification_commands: [],
2361
- outcome: "not_recorded",
2362
- recorded_at: null,
2363
- summary: "No deterministic verification evidence is recorded yet because no selected slice or current-HEAD verification subject exists.",
2364
- };
2365
- }
2366
-
2367
- function buildAgentReadme(projectName: string): string {
2368
- return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/verification-evidence.json\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\n\`.agent/verification-evidence.json\` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
2369
- }
2370
-
2371
- function buildMission(projectName: string, missionAnchor: string): string {
2372
- return `# Mission\n\nProject: ${projectName}\n\nMission anchor:\n${missionAnchor}\n\nThis file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.\n`;
2373
- }
2374
-
2375
- function buildVerifyStopScript(verifierCommand?: string): string {
2376
- const repoCheck = verifierCommand
2377
- ? `echo "[completion] running repo-level verification: ${verifierCommand}"\n${verifierCommand}`
2378
- : `echo "[completion] no repo-specific verifier auto-detected; control-plane verification only"`;
2379
- return `#!/usr/bin/env bash\nset -euo pipefail\n\nbash .agent/verify_completion_control_plane.sh\n${repoCheck}\n`;
2380
- }
2381
-
2382
- function buildVerifyControlPlaneScript(): string {
2383
- return `#!/usr/bin/env bash
2384
- set -euo pipefail
2385
-
2386
- for file in \
2387
- .agent/README.md \
2388
- .agent/mission.md \
2389
- .agent/profile.json \
2390
- .agent/verify_completion_stop.sh \
2391
- .agent/verify_completion_control_plane.sh \
2392
- .agent/state.json \
2393
- .agent/plan.json \
2394
- .agent/active-slice.json \
2395
- .agent/verification-evidence.json; do
2396
- [[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
2397
- done
2398
-
2399
- node <<'NODE'
2400
- const childProcess = require('node:child_process');
2401
- const fs = require('node:fs');
2402
-
2403
- const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
2404
- const assert = (condition, message) => {
2405
- if (!condition) {
2406
- console.error(message);
2407
- process.exit(1);
2408
- }
2409
- };
2410
- const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
2411
- const isString = (value) => typeof value === 'string';
2412
- const isNonEmptyString = (value) => isString(value) && value.length > 0;
2413
- const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
2414
- const hasOnlyKeys = (object, allowed, label) => {
2415
- const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
2416
- assert(unknown.length === 0, label + ': unknown keys: ' + unknown.join(', '));
2417
- };
2418
- const requireKeys = (object, required, label) => {
2419
- for (const key of required) {
2420
- assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
2421
- }
2422
- };
2423
- const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
2424
- const sameStringArrays = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
2425
-
2426
- for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json', '.agent/verification-evidence.json']) {
2427
- readJson(file);
2428
- }
2429
-
2430
- const profile = readJson('.agent/profile.json');
2431
- const state = readJson('.agent/state.json');
2432
- const plan = readJson('.agent/plan.json');
2433
- const active = readJson('.agent/active-slice.json');
2434
- const evidence = readJson('.agent/verification-evidence.json');
2435
-
2436
- assert(isObject(profile), '.agent/profile.json must be an object');
2437
- assert(isObject(state), '.agent/state.json must be an object');
2438
- assert(isObject(plan), '.agent/plan.json must be an object');
2439
- assert(isObject(active), '.agent/active-slice.json must be an object');
2440
- assert(isObject(evidence), '.agent/verification-evidence.json must be an object');
2441
-
2442
- const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
2443
- requireKeys(profile, requiredProfile, '.agent/profile.json');
2444
- hasOnlyKeys(profile, requiredProfile, '.agent/profile.json');
2445
- assert(profile.protocol_id === 'completion', '.agent/profile.json: protocol_id must be completion');
2446
- assert(Array.isArray(profile.docs_surfaces), '.agent/profile.json: docs_surfaces must be an array');
2447
- assert(isNonEmptyString(profile.task_type), '.agent/profile.json: task_type must be a non-empty string');
2448
- assert(isNonEmptyString(profile.evaluation_profile), '.agent/profile.json: evaluation_profile must be a non-empty string');
2449
-
2450
- const requiredState = [
2451
- 'schema_version','mission_anchor','task_type','evaluation_profile','current_phase','continuation_policy','continuation_reason','project_done',
2452
- 'requires_reground','slices_since_last_reground','remaining_release_blockers','remaining_high_value_gaps',
2453
- 'unsatisfied_contract_ids','release_blocker_ids','next_mandatory_action','next_mandatory_role',
2454
- 'remaining_stop_judges','last_reground_at','last_auditor_verdict','contract_status','latest_completed_slice','latest_verified_slice'
2455
- ];
2456
- const continuationPolicies = ['continue', 'await_user_input', 'blocked', 'paused', 'done'];
2457
- const workflowRoles = ['completion-bootstrapper', 'completion-regrounder', 'completion-implementer', 'completion-reviewer', 'completion-auditor', 'completion-stop-judge', null];
2458
- const workflowPhases = ['reground', 'implement', 'post_commit_review', 'post_commit_audit', 'post_commit_reconcile', 'stop_wave', 'awaiting_user', 'blocked', 'done'];
2459
- requireKeys(state, requiredState, '.agent/state.json');
2460
- hasOnlyKeys(state, requiredState, '.agent/state.json');
2461
- assert(continuationPolicies.includes(state.continuation_policy), '.agent/state.json: invalid continuation_policy');
2462
- assert(workflowRoles.includes(state.next_mandatory_role), '.agent/state.json: invalid next_mandatory_role');
2463
- assert(workflowPhases.includes(state.current_phase), '.agent/state.json: invalid current_phase');
2464
- assert(isNonEmptyString(state.task_type), '.agent/state.json: task_type must be a non-empty string');
2465
- assert(isNonEmptyString(state.evaluation_profile), '.agent/state.json: evaluation_profile must be a non-empty string');
2466
- assert(isStringArray(state.unsatisfied_contract_ids), '.agent/state.json: unsatisfied_contract_ids must be an array of strings');
2467
- assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blocker_ids must be an array of strings');
2468
-
2469
- const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
2470
- const requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];
2471
- const planMirrorFields = ['locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
2472
- const allowedSlice = [...requiredSlice, ...planMirrorFields];
2473
- const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
2474
- requireKeys(plan, requiredPlan, '.agent/plan.json');
2475
- hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
2476
- assert(isNonEmptyString(plan.task_type), '.agent/plan.json: task_type must be a non-empty string');
2477
- assert(isNonEmptyString(plan.evaluation_profile), '.agent/plan.json: evaluation_profile must be a non-empty string');
2478
- assert(Array.isArray(plan.candidate_slices), '.agent/plan.json: candidate_slices must be an array');
2479
- for (const [index, slice] of plan.candidate_slices.entries()) {
2480
- const label = '.agent/plan.json candidate_slices[' + index + ']';
2481
- assert(isObject(slice), label + ' must be an object');
2482
- requireKeys(slice, requiredSlice, label);
2483
- hasOnlyKeys(slice, allowedSlice, label);
2484
- assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
2485
- assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
2486
- assert(Array.isArray(slice.acceptance_criteria) && slice.acceptance_criteria.length > 0 && slice.acceptance_criteria.every((item) => typeof item === 'string' && item.length > 0), label + ': acceptance_criteria must be a non-empty array of strings');
2487
- assert(isStringArray(slice.contract_ids), label + ': contract_ids must be an array of strings');
2488
- assert(typeof slice.priority === 'number' && Number.isFinite(slice.priority), label + ': priority must be a finite number');
2489
- assert(sliceStatuses.includes(slice.status), label + ': invalid status');
2490
- assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
2491
- assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
2492
- assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');
2493
- if (hasOwn(slice, 'locked_notes')) assert(isStringArray(slice.locked_notes), label + ': locked_notes must be an array of strings when present');
2494
- if (hasOwn(slice, 'must_fix_findings')) assert(isStringArray(slice.must_fix_findings), label + ': must_fix_findings must be an array of strings when present');
2495
- if (hasOwn(slice, 'implementation_surfaces')) assert(isStringArray(slice.implementation_surfaces), label + ': implementation_surfaces must be an array of strings when present');
2496
- if (hasOwn(slice, 'verification_commands')) assert(isStringArray(slice.verification_commands), label + ': verification_commands must be an array of strings when present');
2497
- if (hasOwn(slice, 'basis_commit')) assert(isNonEmptyString(slice.basis_commit), label + ': basis_commit must be a non-empty string when present');
2498
- if (hasOwn(slice, 'remaining_contract_ids_before')) assert(isStringArray(slice.remaining_contract_ids_before), label + ': remaining_contract_ids_before must be an array of strings when present');
2499
- if (hasOwn(slice, 'release_blocker_count_before')) assert(typeof slice.release_blocker_count_before === 'number' && Number.isFinite(slice.release_blocker_count_before), label + ': release_blocker_count_before must be a finite number when present');
2500
- if (hasOwn(slice, 'high_value_gap_count_before')) assert(typeof slice.high_value_gap_count_before === 'number' && Number.isFinite(slice.high_value_gap_count_before), label + ': high_value_gap_count_before must be a finite number when present');
2501
- }
2502
-
2503
- const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
2504
- const requiredActiveBase = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'status', 'slice_id', 'goal', 'contract_ids', 'acceptance_criteria', 'blocked_on', 'locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
2505
- const allowedActive = [...requiredActiveBase, 'priority', 'why_now'];
2506
- const activeStatuses = ['idle', 'selected', 'in_progress', 'committed', 'done'];
2507
- requireKeys(active, requiredActiveBase, '.agent/active-slice.json');
2508
- hasOnlyKeys(active, allowedActive, '.agent/active-slice.json');
2509
- assert(activeStatuses.includes(active.status), '.agent/active-slice.json: invalid status');
2510
- assert(isNonEmptyString(active.task_type), '.agent/active-slice.json: task_type must be a non-empty string');
2511
- assert(isNonEmptyString(active.evaluation_profile), '.agent/active-slice.json: evaluation_profile must be a non-empty string');
2512
- assert(isStringArray(active.contract_ids), '.agent/active-slice.json: contract_ids must be an array of strings');
2513
- assert(Array.isArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be an array');
2514
- assert(isStringArray(active.blocked_on), '.agent/active-slice.json: blocked_on must be an array of strings');
2515
- assert(isStringArray(active.locked_notes), '.agent/active-slice.json: locked_notes must be an array of strings');
2516
- assert(isStringArray(active.must_fix_findings), '.agent/active-slice.json: must_fix_findings must be an array of strings');
2517
- assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be an array of strings');
2518
- assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
2519
- assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
2520
-
2521
- const requiredEvidence = ['schema_version', 'artifact_type', 'subject_type', 'slice_id', 'goal', 'contract_ids', 'basis_commit', 'head_sha', 'verification_commands', 'outcome', 'recorded_at', 'summary'];
2522
- const evidenceSubjectTypes = ['none', 'selected_slice', 'current_head'];
2523
- const evidenceOutcomes = ['not_recorded', 'passed', 'failed'];
2524
- requireKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
2525
- hasOnlyKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
2526
- assert(evidence.artifact_type === 'completion-verification-evidence', '.agent/verification-evidence.json: artifact_type must be completion-verification-evidence');
2527
- assert(evidenceSubjectTypes.includes(evidence.subject_type), '.agent/verification-evidence.json: invalid subject_type');
2528
- assert(evidence.slice_id === null || isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be null or a non-empty string');
2529
- assert(evidence.goal === null || isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be null or a non-empty string');
2530
- assert(isStringArray(evidence.contract_ids), '.agent/verification-evidence.json: contract_ids must be an array of strings');
2531
- assert(evidence.basis_commit === null || isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be null or a non-empty string');
2532
- assert(evidence.head_sha === null || isNonEmptyString(evidence.head_sha), '.agent/verification-evidence.json: head_sha must be null or a non-empty string');
2533
- assert(isStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be an array of strings');
2534
- assert(evidenceOutcomes.includes(evidence.outcome), '.agent/verification-evidence.json: invalid outcome');
2535
- assert(evidence.recorded_at === null || (isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at))), '.agent/verification-evidence.json: recorded_at must be null or an ISO-8601 string');
2536
- assert(isNonEmptyString(evidence.summary), '.agent/verification-evidence.json: summary must be a non-empty string');
2537
-
2538
- assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
2539
- assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
2540
- assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
2541
- assert(state.evaluation_profile === profile.evaluation_profile, '.agent/state.json: evaluation_profile must match .agent/profile.json');
2542
- assert(plan.evaluation_profile === profile.evaluation_profile, '.agent/plan.json: evaluation_profile must match .agent/profile.json');
2543
- assert(active.evaluation_profile === profile.evaluation_profile, '.agent/active-slice.json: evaluation_profile must match .agent/profile.json');
2544
-
2545
- const requiresExactHandoff = ['selected', 'in_progress', 'committed', 'done'].includes(active.status);
2546
- if (requiresExactHandoff) {
2547
- assert(isNonEmptyStringArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be a non-empty array of strings when status carries an exact handoff');
2548
- assert(typeof active.priority === 'number' && Number.isFinite(active.priority), '.agent/active-slice.json: priority must be a finite number when status carries an exact handoff');
2549
- assert(isString(active.why_now) && active.why_now.length > 0, '.agent/active-slice.json: why_now must be a non-empty string when status carries an exact handoff');
2550
- assert(isNonEmptyStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be a non-empty array of strings when status carries an exact handoff');
2551
- assert(isNonEmptyStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be a non-empty array of strings when status carries an exact handoff');
2552
- assert(isString(active.basis_commit) && active.basis_commit.length > 0, '.agent/active-slice.json: basis_commit must be a non-empty string when status carries an exact handoff');
2553
- assert(typeof active.release_blocker_count_before === 'number' && Number.isFinite(active.release_blocker_count_before), '.agent/active-slice.json: release_blocker_count_before must be a finite number when status carries an exact handoff');
2554
- assert(typeof active.high_value_gap_count_before === 'number' && Number.isFinite(active.high_value_gap_count_before), '.agent/active-slice.json: high_value_gap_count_before must be a finite number when status carries an exact handoff');
2555
-
2556
- const planSlice = plan.candidate_slices.find((slice) => isObject(slice) && slice.slice_id === active.slice_id);
2557
- assert(isObject(planSlice), '.agent/active-slice.json: slice_id must match a slice in .agent/plan.json when status carries an exact handoff');
2558
- const drift = [];
2559
- if (planSlice.goal !== active.goal) drift.push('goal');
2560
- if (!sameStringArrays(planSlice.contract_ids, active.contract_ids)) drift.push('contract_ids');
2561
- if (!sameStringArrays(planSlice.acceptance_criteria, active.acceptance_criteria)) drift.push('acceptance_criteria');
2562
- if (!sameStringArrays(planSlice.blocked_on, active.blocked_on)) drift.push('blocked_on');
2563
- if (planSlice.priority !== active.priority) drift.push('priority');
2564
- if (planSlice.why_now !== active.why_now) drift.push('why_now');
2565
-
2566
- const expectPlanArrayMirror = (field) => {
2567
- if (!hasOwn(planSlice, field) || !sameStringArrays(planSlice[field], active[field])) drift.push(field);
2568
- };
2569
- const expectPlanStringMirror = (field) => {
2570
- if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
2571
- };
2572
- const expectPlanNumberMirror = (field) => {
2573
- if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
2574
- };
2575
-
2576
- expectPlanArrayMirror('implementation_surfaces');
2577
- expectPlanArrayMirror('verification_commands');
2578
- expectPlanArrayMirror('locked_notes');
2579
- expectPlanArrayMirror('must_fix_findings');
2580
- expectPlanStringMirror('basis_commit');
2581
- expectPlanArrayMirror('remaining_contract_ids_before');
2582
- expectPlanNumberMirror('release_blocker_count_before');
2583
- expectPlanNumberMirror('high_value_gap_count_before');
2584
- assert(drift.length === 0, '.agent/active-slice.json must match the selected .agent/plan.json slice across: ' + Array.from(new Set(drift)).join(', '));
2585
- }
2586
-
2587
- const currentHead = (() => {
2588
- try {
2589
- return childProcess.execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2590
- } catch {
2591
- return null;
2592
- }
2593
- })();
2594
-
2595
- if (requiresExactHandoff) {
2596
- assert(evidence.subject_type === 'selected_slice', '.agent/verification-evidence.json: subject_type must be selected_slice when active slice exact handoff requires verification evidence');
2597
- assert(evidence.slice_id === active.slice_id, '.agent/verification-evidence.json: slice_id must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2598
- assert(evidence.goal === active.goal, '.agent/verification-evidence.json: goal must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2599
- assert(sameStringArrays(evidence.contract_ids, active.contract_ids), '.agent/verification-evidence.json: contract_ids must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2600
- assert(evidence.basis_commit === active.basis_commit, '.agent/verification-evidence.json: basis_commit must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2601
- assert(sameStringArrays(evidence.verification_commands, active.verification_commands), '.agent/verification-evidence.json: verification_commands must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
2602
- assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when active slice exact handoff requires verification evidence');
2603
- assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when active slice exact handoff requires verification evidence');
2604
- if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when active slice exact handoff requires verification evidence');
2605
- } else if (evidence.subject_type === 'none') {
2606
- assert(evidence.slice_id === null, '.agent/verification-evidence.json: slice_id must be null when subject_type is none');
2607
- assert(evidence.goal === null, '.agent/verification-evidence.json: goal must be null when subject_type is none');
2608
- assert(evidence.contract_ids.length === 0, '.agent/verification-evidence.json: contract_ids must be empty when subject_type is none');
2609
- assert(evidence.basis_commit === null, '.agent/verification-evidence.json: basis_commit must be null when subject_type is none');
2610
- assert(evidence.head_sha === null, '.agent/verification-evidence.json: head_sha must be null when subject_type is none');
2611
- assert(evidence.verification_commands.length === 0, '.agent/verification-evidence.json: verification_commands must be empty when subject_type is none');
2612
- assert(evidence.outcome === 'not_recorded', '.agent/verification-evidence.json: outcome must be not_recorded when subject_type is none');
2613
- assert(evidence.recorded_at === null, '.agent/verification-evidence.json: recorded_at must be null when subject_type is none');
2614
- } else {
2615
- assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when verification evidence is recorded');
2616
- assert(isNonEmptyStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be a non-empty array when verification evidence is recorded');
2617
- assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when verification evidence is recorded');
2618
- if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when verification evidence is recorded');
2619
- if (evidence.subject_type === 'selected_slice') {
2620
- assert(isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be a non-empty string when subject_type is selected_slice');
2621
- assert(isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be a non-empty string when subject_type is selected_slice');
2622
- assert(isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be a non-empty string when subject_type is selected_slice');
2623
- } else {
2624
- assert(evidence.subject_type === 'current_head', '.agent/verification-evidence.json: only current_head or selected_slice may carry recorded verification evidence');
2625
- }
2626
- }
2627
-
2628
- if (!requiresExactHandoff) {
2629
- assert(active.priority === null || active.priority === undefined || (typeof active.priority === 'number' && Number.isFinite(active.priority)), '.agent/active-slice.json: idle priority must be null/undefined or a finite number');
2630
- assert(active.why_now === null || active.why_now === undefined || typeof active.why_now === 'string', '.agent/active-slice.json: idle why_now must be null/undefined or a string');
2631
- }
2632
- NODE
2633
- `;
2634
- }
2635
-
2636
- async function ensureGitignore(root: string): Promise<boolean> {
2637
- const gitignorePath = path.join(root, ".gitignore");
2638
- const blockLines = [
2639
- "# completion protocol",
2640
- ".agent/*",
2641
- "!.agent/README.md",
2642
- "!.agent/mission.md",
2643
- "!.agent/profile.json",
2644
- "!.agent/verify_completion_stop.sh",
2645
- "!.agent/verify_completion_control_plane.sh",
2646
- ".agent/tmp/",
2647
- ];
2648
- const block = blockLines.join("\n");
2649
- const existing = (await pathExists(gitignorePath)) ? await fsp.readFile(gitignorePath, "utf8") : "";
2650
- const filteredLines = existing
2651
- .split(/\r?\n/)
2652
- .filter((line) => !blockLines.includes(line.trim()));
2653
- while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1]?.trim() === "") {
2654
- filteredLines.pop();
2655
- }
2656
- const base = filteredLines.join("\n").trimEnd();
2657
- const content = base.length > 0 ? `${base}\n\n${block}\n` : `${block}\n`;
2658
- if (content === existing) return false;
2659
- await fsp.writeFile(gitignorePath, content, "utf8");
2660
- return true;
2661
- }
2662
-
2663
- type ScaffoldResult = {
2664
- root: string;
2665
- created: string[];
2666
- updated: string[];
2667
- missionAnchor: string;
2668
- };
2669
451
 
2670
452
  async function scaffoldCompletionFiles(
2671
453
  root: string,
2672
454
  missionAnchor: string,
2673
455
  options?: { analysis?: ContextProposalAnalysis; continuationReason?: string },
2674
- ): Promise<ScaffoldResult> {
2675
- const files = resolveFiles(root);
2676
- const created: string[] = [];
2677
- const updated: string[] = [];
2678
- await fsp.mkdir(files.agentDir, { recursive: true });
2679
- await fsp.mkdir(path.join(files.agentDir, "tmp"), { recursive: true });
2680
- const projectName = path.basename(root);
456
+ ) {
2681
457
  const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
2682
- const docsSurfaces = await detectDocsSurfaces(root);
2683
- const verifierCommand = await detectVerifierCommand(root);
2684
- const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
2685
- { path: path.join(files.agentDir, "README.md"), content: buildAgentReadme(projectName) },
2686
- { path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
2687
- {
2688
- path: files.profilePath,
2689
- content: `${JSON.stringify(buildProfileRecord({ projectName, requiredStopJudges: 3, docsSurfaces, taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n`,
2690
- },
2691
- { path: path.join(files.agentDir, "verify_completion_stop.sh"), content: buildVerifyStopScript(verifierCommand), executable: true },
2692
- { path: path.join(files.agentDir, "verify_completion_control_plane.sh"), content: buildVerifyControlPlaneScript(), executable: true },
2693
- {
2694
- path: files.statePath,
2695
- content: `${JSON.stringify(defaultState(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile, continuationReason: options?.continuationReason }), null, 2)}\n`,
2696
- },
2697
- { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
2698
- { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
2699
- { path: files.verificationEvidencePath, content: `${JSON.stringify(defaultVerificationEvidence(), null, 2)}\n` },
2700
- { path: files.sliceHistoryPath, content: "" },
2701
- { path: files.stopHistoryPath, content: "" },
2702
- ];
2703
- for (const file of trackedFiles) {
2704
- if (await pathExists(file.path)) continue;
2705
- await fsp.writeFile(file.path, file.content, "utf8");
2706
- if (file.executable) await fsp.chmod(file.path, 0o755);
2707
- created.push(path.relative(root, file.path));
2708
- }
2709
- if (await ensureGitignore(root)) updated.push(".gitignore");
2710
- return { root, created, updated, missionAnchor };
458
+ return await scaffoldCompletionFilesOnDisk(root, missionAnchor, {
459
+ analysis: { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile },
460
+ continuationReason: options?.continuationReason,
461
+ });
2711
462
  }
2712
463
 
2713
464
  function remainingSliceCount(plan: JsonRecord | undefined): number {
@@ -2787,37 +538,109 @@ function activeSliceContractDriftSummary(snapshot: CompletionStateSnapshot): str
2787
538
  return drift && drift.length > 0 ? drift.join(", ") : "none";
2788
539
  }
2789
540
 
2790
- function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
2791
- const activeId = asString(snapshot.active?.slice_id);
2792
- if (!activeId) return "unknown";
2793
- const drift = activeSliceContractDriftFields(snapshot);
2794
- if (!snapshot.activeSlice || drift === undefined) return "no";
2795
- return drift.length === 0 ? "yes" : "no";
541
+ function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
542
+ const activeId = asString(snapshot.active?.slice_id);
543
+ if (!activeId) return "unknown";
544
+ const drift = activeSliceContractDriftFields(snapshot);
545
+ if (!snapshot.activeSlice || drift === undefined) return "no";
546
+ return drift.length === 0 ? "yes" : "no";
547
+ }
548
+
549
+ function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
550
+ const exactArrays = [
551
+ asStringArray(active?.acceptance_criteria),
552
+ asStringArray(active?.implementation_surfaces),
553
+ asStringArray(active?.verification_commands),
554
+ ];
555
+ const required = [
556
+ active?.priority,
557
+ active?.why_now,
558
+ active?.blocked_on,
559
+ active?.locked_notes,
560
+ active?.must_fix_findings,
561
+ active?.basis_commit,
562
+ active?.remaining_contract_ids_before,
563
+ active?.release_blocker_count_before,
564
+ active?.high_value_gap_count_before,
565
+ ];
566
+ return activeCarriesExactHandoff(active) && exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
567
+ ? "present"
568
+ : "missing_or_unclear";
569
+ }
570
+
571
+ function hasRunningCompletionRole(rootKey: string): boolean {
572
+ return liveRoleActivityByRoot.get(rootKey)?.status === "running";
573
+ }
574
+
575
+ function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
576
+ return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
577
+ }
578
+
579
+ function activeSliceContext(snapshot: CompletionStateSnapshot) {
580
+ const active = snapshot.active;
581
+ const activeSlice = snapshot.activeSlice;
582
+ return {
583
+ sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
584
+ status: asString(active?.status) ?? asString(activeSlice?.status),
585
+ goal: asString(active?.goal) ?? asString(activeSlice?.goal),
586
+ contractIds:
587
+ asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
588
+ acceptance:
589
+ asStringArray(active?.acceptance_criteria).length > 0
590
+ ? asStringArray(active?.acceptance_criteria)
591
+ : asStringArray(activeSlice?.acceptance_criteria),
592
+ implementationSurfaces: asStringArray(active?.implementation_surfaces),
593
+ verificationCommands: asStringArray(active?.verification_commands),
594
+ lockedNotes: asStringArray(active?.locked_notes),
595
+ mustFixFindings: asStringArray(active?.must_fix_findings),
596
+ remainingBefore: asStringArray(active?.remaining_contract_ids_before),
597
+ basisCommit: asString(active?.basis_commit),
598
+ releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
599
+ highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
600
+ };
601
+ }
602
+
603
+ function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
604
+ const evidence = snapshot.verificationEvidence;
605
+ return {
606
+ path: path.relative(snapshot.files.root, snapshot.files.verificationEvidencePath) || ".agent/verification-evidence.json",
607
+ status: evidence ? "present" : "missing",
608
+ subjectType: asString(evidence?.subject_type),
609
+ sliceId: asString(evidence?.slice_id),
610
+ goal: asString(evidence?.goal),
611
+ contractIds: asStringArray(evidence?.contract_ids),
612
+ basisCommit: asString(evidence?.basis_commit),
613
+ headSha: asString(evidence?.head_sha),
614
+ verificationCommands: asStringArray(evidence?.verification_commands),
615
+ outcome: asString(evidence?.outcome),
616
+ recordedAt: asString(evidence?.recorded_at),
617
+ summary:
618
+ asString(evidence?.summary) ??
619
+ (evidence ? "Canonical verification evidence is present but its summary is missing." : "Canonical verification evidence is missing."),
620
+ };
621
+ }
622
+
623
+ function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
624
+ return buildExtractedEvaluationRoleContextLines(snapshot, role, {
625
+ asString,
626
+ currentTaskType,
627
+ currentEvaluationProfile,
628
+ activeSliceContext,
629
+ verificationEvidenceContext,
630
+ });
2796
631
  }
2797
632
 
2798
- function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
2799
- const exactArrays = [
2800
- asStringArray(active?.acceptance_criteria),
2801
- asStringArray(active?.implementation_surfaces),
2802
- asStringArray(active?.verification_commands),
2803
- ];
2804
- const required = [
2805
- active?.priority,
2806
- active?.why_now,
2807
- active?.blocked_on,
2808
- active?.locked_notes,
2809
- active?.must_fix_findings,
2810
- active?.basis_commit,
2811
- active?.remaining_contract_ids_before,
2812
- active?.release_blocker_count_before,
2813
- active?.high_value_gap_count_before,
2814
- ];
2815
- return activeCarriesExactHandoff(active) && exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
2816
- ? "present"
2817
- : "missing_or_unclear";
633
+ function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
634
+ return buildExtractedEvaluationRoleReminderText(snapshot, role, {
635
+ asString,
636
+ currentTaskType,
637
+ currentEvaluationProfile,
638
+ activeSliceContext,
639
+ verificationEvidenceContext,
640
+ });
2818
641
  }
2819
642
 
2820
- function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
643
+ function composeSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
2821
644
  const history = historyCounts(sliceHistory, stopHistory);
2822
645
  const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
2823
646
  const verificationCommands = asStringArray(snapshot.active?.verification_commands);
@@ -2827,42 +650,37 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
2827
650
  const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
2828
651
  const activeContractDrift = activeSliceContractDriftSummary(snapshot);
2829
652
  const evidence = verificationEvidenceContext(snapshot);
2830
- const lines = [
2831
- "Completion workflow detected.",
2832
- "Canonical truth lives in .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json.",
2833
- `Mission anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
2834
- `Task type: ${currentTaskType(snapshot) ?? "(missing)"}`,
2835
- `Evaluation profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
2836
- `Current phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
2837
- `Continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
2838
- `Continuation reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
2839
- `Next mandatory role: ${asString(snapshot.state?.next_mandatory_role) ?? "unknown"}`,
2840
- `Next mandatory action: ${asString(snapshot.state?.next_mandatory_action) ?? "unknown"}`,
2841
- `Remaining slice count: ${remainingSliceCount(snapshot.plan)}`,
2842
- `Remaining stop judges: ${asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)"}`,
2843
- `History counts: reviewed=${history.reviewed}, audited=${history.audited}, accepted=${history.accepted}, reopened=${history.reopened}, judgments=${history.judgments}.`,
2844
- "Re-read canonical .agent state after compaction or recovery instead of relying on conversation memory.",
2845
- "If continuation_policy == continue, do not stop after a slice or ask whether to continue; dispatch the next mandatory role directly.",
2846
- "Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
2847
- "If canonical state is stale, invalid, ambiguous, or missing, route to completion-regrounder.",
2848
- "When recovering from compaction, prefer a deterministic restart from canonical files over conversational inference.",
2849
- ];
2850
- if (exactActiveContract) {
2851
- lines.push("Selected/in-progress/committed/done .agent/active-slice.json is the canonical implementation contract.");
2852
- lines.push(`Active slice contract drift: ${activeContractDrift}`);
2853
- }
2854
- if (activePriority !== undefined) lines.push(`Active slice priority: ${activePriority}`);
2855
- if (activeWhyNow) lines.push(`Active slice why_now: ${activeWhyNow}`);
2856
- if (implementationSurfaces.length > 0) lines.push(`Active implementation surfaces: ${implementationSurfaces.join(", ")}`);
2857
- if (verificationCommands.length > 0) lines.push(`Active verification commands: ${verificationCommands.join(" | ")}`);
2858
- lines.push(`Verification evidence artifact: ${evidence.path} (${evidence.status})`);
2859
- if (evidence.subjectType) lines.push(`Verification evidence subject: ${evidence.subjectType}`);
2860
- if (evidence.outcome) lines.push(`Verification evidence outcome: ${evidence.outcome}`);
2861
- if (evidence.recordedAt) lines.push(`Verification evidence recorded_at: ${evidence.recordedAt}`);
2862
- if (evidence.verificationCommands.length > 0) lines.push(`Verification evidence commands: ${evidence.verificationCommands.join(" | ")}`);
2863
- lines.push(`Verification evidence summary: ${evidence.summary}`);
2864
- if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
2865
- return lines.join(" ");
653
+ const activePriorityLine = activePriority !== undefined ? `Active slice priority: ${activePriority}` : undefined;
654
+ const activeWhyNowLine = activeWhyNow ? `Active slice why_now: ${activeWhyNow}` : undefined;
655
+ const implementationSurfacesLine =
656
+ implementationSurfaces.length > 0 ? `Active implementation surfaces: ${implementationSurfaces.join(", ")}` : undefined;
657
+ const verificationCommandsLine =
658
+ verificationCommands.length > 0 ? `Active verification commands: ${verificationCommands.join(" | ")}` : undefined;
659
+ return buildExtractedSystemReminder({
660
+ missionAnchor: asString(snapshot.state?.mission_anchor),
661
+ taskType: currentTaskType(snapshot),
662
+ evaluationProfile: currentEvaluationProfile(snapshot),
663
+ currentPhase: asString(snapshot.state?.current_phase),
664
+ continuationPolicy: asString(snapshot.state?.continuation_policy),
665
+ continuationReason: asString(snapshot.state?.continuation_reason),
666
+ nextMandatoryRole: nextRole,
667
+ nextMandatoryAction: asString(snapshot.state?.next_mandatory_action),
668
+ remainingSliceCount: remainingSliceCount(snapshot.plan),
669
+ remainingStopJudges: asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)",
670
+ history,
671
+ exactActiveContract,
672
+ activeContractDrift,
673
+ activePriority,
674
+ activeWhyNow,
675
+ implementationSurfaces,
676
+ verificationCommands,
677
+ activePriorityLine,
678
+ activeWhyNowLine,
679
+ implementationSurfacesLine,
680
+ verificationCommandsLine,
681
+ evidence,
682
+ evaluationRoleReminderText: isRubricEvaluationRole(nextRole) ? buildEvaluationRoleReminderText(snapshot, nextRole) : undefined,
683
+ });
2866
684
  }
2867
685
 
2868
686
  function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot, marker: JsonRecord | undefined): string {
@@ -2978,7 +796,7 @@ function emitCommandText(ctx: { hasUI: boolean; ui: any }, text: string, level:
2978
796
  }
2979
797
  }
2980
798
 
2981
- function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
799
+ function composeResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
2982
800
  const history = historyCounts(sliceHistory, stopHistory);
2983
801
  const acceptance = asStringArray(snapshot.active?.acceptance_criteria).length > 0
2984
802
  ? asStringArray(snapshot.active?.acceptance_criteria)
@@ -2994,411 +812,51 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
2994
812
  const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
2995
813
  const verificationCommands = asStringArray(snapshot.active?.verification_commands);
2996
814
  const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
2997
- const activeContractDrift = activeSliceContractDriftSummary(snapshot);
2998
815
  const evidence = verificationEvidenceContext(snapshot);
2999
- const lines = [
3000
- "Authoritative completion resume capsule:",
3001
- "",
3002
- "<completion-state>",
3003
- `mission_anchor: ${asString(snapshot.state?.mission_anchor) ?? "(unknown)"}`,
3004
- `task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
3005
- `evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
3006
- `current_phase: ${asString(snapshot.state?.current_phase) ?? "unknown"}`,
3007
- `continuation_policy: ${asString(snapshot.state?.continuation_policy) ?? "unknown"}`,
3008
- `continuation_reason: ${asString(snapshot.state?.continuation_reason) ?? "(unknown)"}`,
3009
- `requires_reground: ${asBoolean(snapshot.state?.requires_reground) ?? "unknown"}`,
3010
- `next_mandatory_role: ${asString(snapshot.state?.next_mandatory_role) ?? "unknown"}`,
3011
- `next_mandatory_action: ${asString(snapshot.state?.next_mandatory_action) ?? "unknown"}`,
3012
- `remaining_slice_count: ${remainingSliceCount(snapshot.plan)}`,
3013
- `remaining_stop_judges: ${asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)"}`,
3014
- `active_slice_matches_plan: ${activeSliceMatchesPlan(snapshot)}`,
3015
- `active_slice_contract_drift_fields: ${activeContractDrift}`,
3016
- `implementer_handoff_snapshot: ${handoffSnapshotState(snapshot.active)}`,
3017
- `history_counts: reviewed=${history.reviewed}, audited=${history.audited}, accepted=${history.accepted}, reopened=${history.reopened}, judgments=${history.judgments}`,
3018
- "",
3019
- "verification_evidence:",
3020
- `- path: ${evidence.path}`,
3021
- `- status: ${evidence.status}`,
3022
- `- subject_type: ${evidence.subjectType ?? "(missing)"}`,
3023
- `- slice_id: ${evidence.sliceId ?? "(none)"}`,
3024
- `- contract_ids: ${evidence.contractIds.length > 0 ? evidence.contractIds.join(", ") : "(none)"}`,
3025
- `- outcome: ${evidence.outcome ?? "(missing)"}`,
3026
- `- recorded_at: ${evidence.recordedAt ?? "(missing)"}`,
3027
- `- head_sha: ${evidence.headSha ?? "(missing)"}`,
3028
- `- basis_commit: ${evidence.basisCommit ?? "(missing)"}`,
3029
- `- verification_commands: ${evidence.verificationCommands.length > 0 ? evidence.verificationCommands.join(" | ") : "(none)"}`,
3030
- `- summary: ${evidence.summary}`,
3031
- "",
3032
- "active_slice:",
3033
- `- slice_id: ${asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)"}`,
3034
- `- status: ${asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status) ?? "unknown"}`,
3035
- `- goal: ${asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)"}`,
3036
- `- priority: ${asNumber(snapshot.active?.priority) ?? "(unknown)"}`,
3037
- `- why_now: ${asString(snapshot.active?.why_now) ?? "(unknown)"}`,
3038
- `- contract_ids: ${contractIds.length > 0 ? contractIds.join(", ") : "(none)"}`,
3039
- ];
3040
- if (blockedOn.length > 0) lines.push(`- blocked_on: ${blockedOn.join(", ")}`);
3041
- if (lockedNotes.length > 0) lines.push(`- locked_notes: ${lockedNotes.join(" | ")}`);
3042
- if (mustFixFindings.length > 0) lines.push(`- must_fix_findings: ${mustFixFindings.join(" | ")}`);
3043
- if (implementationSurfaces.length > 0) lines.push(`- implementation_surfaces: ${implementationSurfaces.join(" | ")}`);
3044
- if (verificationCommands.length > 0) lines.push(`- verification_commands: ${verificationCommands.join(" | ")}`);
3045
- lines.push(`- basis_commit: ${asString(snapshot.active?.basis_commit) ?? "(none)"}`);
3046
- lines.push(`- remaining_contract_ids_before: ${remainingBefore.length > 0 ? remainingBefore.join(", ") : "(none)"}`);
3047
- lines.push(`- release_blocker_count_before: ${asNumber(snapshot.active?.release_blocker_count_before) ?? "(unknown)"}`);
3048
- lines.push(`- high_value_gap_count_before: ${asNumber(snapshot.active?.high_value_gap_count_before) ?? "(unknown)"}`);
3049
- lines.push("", "acceptance_criteria:");
3050
- if (acceptance.length === 0) lines.push("- (none)");
3051
- else lines.push(...acceptance.map((item) => `- ${item}`));
3052
- lines.push(
3053
- "",
3054
- "Rules:",
3055
- "- Treat this block as continuity support derived from canonical .agent state.",
3056
- "- For selected/in-progress/committed/done slices, .agent/active-slice.json is the canonical implementation contract and the selected plan slice must mirror it exactly.",
3057
- "- Preserve exact slice_id, goal, contract_ids, acceptance criteria, blocked_on, priority, why_now, implementation surfaces, verification commands, locked notes, must-fix findings, basis_commit, and before-slice counters where still true.",
3058
- "- When populated, .agent/verification-evidence.json is the durable canonical verification record for the selected slice or current HEAD and should be consumed instead of temp-only artifacts or conversational summaries.",
3059
- "- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json before resuming long-running completion work.",
3060
- "- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
3061
- "- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
3062
- "- Invoke completion-regrounder before continuing when active_slice_matches_plan is no, active_slice_contract_drift_fields is not none, or implementer_handoff_snapshot is missing_or_unclear.",
3063
- "- If continuation_policy is continue, do not stop after a slice or ask whether to continue. Dispatch the next mandatory role directly.",
3064
- "- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
3065
- "- If you are completion-implementer after compaction, resume from the canonical active-slice implementation contract instead of asking the user to resend the original caller payload.",
3066
- "- Do not replace canonical .agent state with summary inference.",
3067
- "</completion-state>",
3068
- );
3069
- return lines.join("\n");
3070
- }
3071
-
3072
- function formatCount(count: number, singular: string, plural = `${singular}s`): string {
3073
- return `${count} ${count === 1 ? singular : plural}`;
3074
- }
3075
-
3076
- function completionRemainingSummary(surface: {
3077
- remainingContractCount: number;
3078
- releaseBlockerCount: number;
3079
- highValueGapCount: number;
3080
- remainingStopJudgeCount: number;
3081
- }): string {
3082
- return [
3083
- formatCount(surface.remainingContractCount, "contract"),
3084
- formatCount(surface.releaseBlockerCount, "blocker"),
3085
- formatCount(surface.highValueGapCount, "gap"),
3086
- formatCount(surface.remainingStopJudgeCount, "stop judge", "stop judges"),
3087
- ].join(" · ");
3088
- }
3089
-
3090
- function envNumber(name: string): number | undefined {
3091
- const raw = asString(process.env[name]);
3092
- if (!raw) return undefined;
3093
- const parsed = Number(raw);
3094
- return Number.isFinite(parsed) ? parsed : undefined;
3095
- }
3096
-
3097
- function nowMs(): number {
3098
- return envNumber("PI_COMPLETION_TEST_NOW") ?? Date.now();
3099
- }
3100
-
3101
- type LiveActivitySignal = {
3102
- state: "active" | "waiting" | "stalled";
3103
- idleMs: number;
3104
- };
3105
-
3106
- function liveActivitySignal(activity: { status?: string; startedAt?: number; updatedAt?: number } | undefined): LiveActivitySignal | undefined {
3107
- if (!activity || activity.status !== "running") return undefined;
3108
- const anchor = activity.updatedAt ?? activity.startedAt;
3109
- if (anchor === undefined) return undefined;
3110
- const idleMs = Math.max(0, nowMs() - anchor);
3111
- return {
3112
- state: idleMs >= LIVE_ROLE_STALLED_MS ? "stalled" : idleMs >= LIVE_ROLE_WAITING_MS ? "waiting" : "active",
3113
- idleMs,
3114
- };
3115
- }
3116
-
3117
- function formatLiveActivitySignal(signal: LiveActivitySignal | undefined): string | undefined {
3118
- if (!signal) return undefined;
3119
- if (signal.state === "active") return "activity: active";
3120
- return `activity: ${signal.state} (${formatElapsed(signal.idleMs)} since update)`;
3121
- }
3122
-
3123
- function livePreviewForStatus(activity: LiveRoleActivity | undefined): string | undefined {
3124
- if (!activity || activity.status !== "running") return undefined;
3125
- return truncateInline(
3126
- activity.progress ?? activity.verifying ?? activity.toolActivity ?? activity.assistantSummary ?? activity.currentAction ?? activity.lastAssistantText ?? "",
3127
- 120,
3128
- ) || undefined;
3129
- }
3130
-
3131
- function completionRootKey(snapshot: CompletionStateSnapshot | undefined, cwd: string): string {
3132
- return snapshot?.files.root ?? findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? path.resolve(cwd);
3133
- }
3134
-
3135
- function cloneLiveRoleActivity(activity: LiveRoleActivity, overrides: Partial<LiveRoleActivity> = {}): LiveRoleActivity {
3136
- return {
3137
- ...activity,
3138
- ...overrides,
3139
- toolRecentActivity: [...(overrides.toolRecentActivity ?? activity.toolRecentActivity)],
3140
- recentActivity: [...(overrides.recentActivity ?? activity.recentActivity)],
3141
- stateDeltas: [...(overrides.stateDeltas ?? activity.stateDeltas)],
3142
- };
3143
- }
3144
-
3145
- function createLiveRoleActivity(role: string, startedAt = nowMs()): LiveRoleActivity {
3146
- const currentAction = "Starting role subprocess";
3147
- return {
3148
- role,
3149
- status: "running",
3150
- currentAction,
3151
- toolActivity: currentAction,
3152
- toolRecentActivity: [currentAction],
3153
- recentActivity: [currentAction],
3154
- stateDeltas: [],
3155
- startedAt,
3156
- updatedAt: startedAt,
3157
- };
3158
- }
3159
-
3160
- type RoleMessage = {
3161
- role: string;
3162
- content: Array<{ type: string; text?: string }>;
3163
- };
3164
-
3165
- function activityTimestampMs(event: JsonRecord | undefined): number | undefined {
3166
- return asNumber(event?.updatedAt) ?? asNumber(event?.timestampMs) ?? asNumber(event?.timestamp) ?? asNumber(event?.at);
3167
- }
3168
-
3169
- function asRoleMessage(value: unknown): RoleMessage | undefined {
3170
- if (!isRecord(value)) return undefined;
3171
- const role = asString(value.role);
3172
- const content = Array.isArray(value.content)
3173
- ? value.content.flatMap((item) => {
3174
- if (!isRecord(item)) return [];
3175
- const type = asString(item.type);
3176
- if (!type) return [];
3177
- return [{ type, text: asString(item.text) }];
3178
- })
3179
- : [];
3180
- if (!role) return undefined;
3181
- return { role, content };
3182
- }
3183
-
3184
- function applyAssistantTextToLiveRoleActivity(activity: LiveRoleActivity, text: string, activityAt = nowMs()): boolean {
3185
- if (!text) return false;
3186
- activity.lastAssistantText = text;
3187
- const parsed = parseStructuredProgress(text);
3188
- if (parsed.progress) activity.progress = parsed.progress;
3189
- if (parsed.rationale) activity.rationale = parsed.rationale;
3190
- if (parsed.nextStep) activity.nextStep = parsed.nextStep;
3191
- if (parsed.verifying) activity.verifying = parsed.verifying;
3192
- if (parsed.stateDeltas.length > 0) activity.stateDeltas = parsed.stateDeltas;
3193
- const preview = truncateInline(text, 140);
3194
- activity.assistantSummary = activity.progress ?? activity.verifying ?? preview;
3195
- activity.currentAction = activity.assistantSummary;
3196
- if (activity.assistantSummary) activity.recentActivity = pushRecentActivity(activity.recentActivity, `assistant: ${activity.assistantSummary}`);
3197
- activity.updatedAt = activityAt;
3198
- return true;
3199
- }
3200
-
3201
- function applyLiveRoleEvent(activity: LiveRoleActivity, event: JsonRecord, messages: RoleMessage[]): boolean {
3202
- const eventType = asString(event.type);
3203
- if (!eventType) return false;
3204
- const activityAt = activityTimestampMs(event) ?? nowMs();
3205
- if (eventType === "tool_execution_start") {
3206
- const toolName = asString(event.toolName) ?? "tool";
3207
- const toolArgs = isRecord(event.args) ? event.args : isRecord(event.input) ? event.input : {};
3208
- activity.toolActivity = formatToolActivity(toolName, toolArgs);
3209
- activity.currentAction = activity.toolActivity;
3210
- activity.toolRecentActivity = pushRecentActivity(activity.toolRecentActivity, activity.toolActivity, 6);
3211
- activity.recentActivity = pushRecentActivity(activity.recentActivity, activity.toolActivity);
3212
- activity.updatedAt = activityAt;
3213
- return true;
3214
- }
3215
- if (eventType === "tool_execution_end" || eventType === "tool_result_end") {
3216
- activity.updatedAt = activityAt;
3217
- return true;
3218
- }
3219
- if ((eventType === "message_update" || eventType === "message_end") && isRecord(event.message)) {
3220
- const message = asRoleMessage(event.message);
3221
- if (message && eventType === "message_end") messages.push(message);
3222
- const nextOutput = message ? lastAssistantText(eventType === "message_end" ? messages : [message]) : "";
3223
- if (nextOutput) return applyAssistantTextToLiveRoleActivity(activity, nextOutput, activityAt);
3224
- activity.updatedAt = activityAt;
3225
- return true;
3226
- }
3227
- return false;
3228
- }
3229
-
3230
- function maybeInjectTestLiveRoleActivity(rootKey: string): void {
3231
- const raw = asString(process.env.PI_COMPLETION_TEST_LIVE_ROLE_ACTIVITY_JSON);
3232
- if (!raw) return;
3233
- try {
3234
- const parsed = JSON.parse(raw);
3235
- if (!isRecord(parsed)) return;
3236
- const currentAction = asString(parsed.currentAction);
3237
- const recentActivity = asStringArray(parsed.recentActivity).length > 0 ? asStringArray(parsed.recentActivity) : currentAction ? [currentAction] : [];
3238
- const toolActivity =
3239
- asString(parsed.toolActivity) ??
3240
- (currentAction && !currentAction.startsWith("assistant:") && !currentAction.startsWith("progress:") ? currentAction : undefined);
3241
- const assistantSummary =
3242
- asString(parsed.assistantSummary) ??
3243
- (currentAction?.startsWith("assistant:") ? currentAction.slice("assistant:".length).trim() : undefined);
3244
- liveRoleActivityByRoot.set(rootKey, {
3245
- role: asString(parsed.role) ?? "completion-implementer",
3246
- status: asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running",
3247
- currentAction,
3248
- toolActivity,
3249
- toolRecentActivity: asStringArray(parsed.toolRecentActivity).length > 0 ? asStringArray(parsed.toolRecentActivity) : toolActivity ? [toolActivity] : [],
3250
- recentActivity,
3251
- assistantSummary,
3252
- lastAssistantText: asString(parsed.lastAssistantText),
3253
- progress: asString(parsed.progress),
3254
- rationale: asString(parsed.rationale),
3255
- nextStep: asString(parsed.nextStep),
3256
- verifying: asString(parsed.verifying),
3257
- stateDeltas: asStringArray(parsed.stateDeltas),
3258
- startedAt: asNumber(parsed.startedAt) ?? nowMs(),
3259
- updatedAt: asNumber(parsed.updatedAt) ?? nowMs(),
3260
- });
3261
- } catch {
3262
- // ignore malformed test override
3263
- }
3264
- }
3265
-
3266
- function maybeReplayTestLiveRoleEvents(rootKey: string): void {
3267
- const raw = asString(process.env.PI_COMPLETION_TEST_ROLE_EVENT_STREAM_JSON);
3268
- if (!raw) return;
3269
- try {
3270
- const parsed = JSON.parse(raw);
3271
- let role = "completion-implementer";
3272
- let status: LiveRoleActivity["status"] = "running";
3273
- let startedAt = nowMs();
3274
- let events: JsonRecord[] = [];
3275
- if (Array.isArray(parsed)) {
3276
- events = parsed.filter(isRecord);
3277
- } else if (isRecord(parsed)) {
3278
- role = asString(parsed.role) ?? role;
3279
- status = asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running";
3280
- startedAt = asNumber(parsed.startedAt) ?? asNumber(parsed.started_at) ?? startedAt;
3281
- events = Array.isArray(parsed.events) ? parsed.events.filter(isRecord) : [];
3282
- } else {
3283
- return;
3284
- }
3285
- const activity = createLiveRoleActivity(role, startedAt);
3286
- const messages: RoleMessage[] = [];
3287
- for (const event of events) applyLiveRoleEvent(activity, event, messages);
3288
- liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(activity, { status }));
3289
- } catch {
3290
- // ignore malformed event stream override
3291
- }
3292
- }
3293
-
3294
- function buildCompletionStatusSurface(
3295
- snapshot: CompletionStateSnapshot | undefined,
3296
- liveActivity: LiveRoleActivity | undefined,
3297
- ): CompletionStatusSurface {
3298
- if (!snapshot) return { snapshotPresent: false, widgetLines: [] };
3299
- const currentPhase = asString(snapshot.state?.current_phase) ?? "unknown";
3300
- const sliceId = asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)";
3301
- const sliceGoal = truncateInline(asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)", 140);
3302
- const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role) ?? "unknown";
3303
- const remainingContractCount = asStringArray(snapshot.state?.unsatisfied_contract_ids).length;
3304
- const releaseBlockerCount = asNumber(snapshot.state?.remaining_release_blockers) ?? 0;
3305
- const highValueGapCount = asNumber(snapshot.state?.remaining_high_value_gaps) ?? 0;
3306
- const remainingStopJudgeCount = asNumber(snapshot.state?.remaining_stop_judges) ?? 0;
3307
- const activeRole = liveActivity?.status === "running" ? liveActivity.role : undefined;
3308
- const liveSignal = liveActivitySignal(liveActivity);
3309
- const livePreview = livePreviewForStatus(liveActivity);
3310
- const liveDetailsLines = activeRole
3311
- ? buildInlineRunningLines({
3312
- role: activeRole,
3313
- currentAction: liveActivity?.currentAction,
3314
- toolActivity: liveActivity?.toolActivity,
3315
- toolRecentActivity: liveActivity?.toolRecentActivity,
3316
- recentActivity: liveActivity?.recentActivity,
3317
- assistantSummary: liveActivity?.assistantSummary,
3318
- progress: liveActivity?.progress,
3319
- rationale: liveActivity?.rationale,
3320
- nextStep: liveActivity?.nextStep,
3321
- verifying: liveActivity?.verifying,
3322
- stateDeltas: liveActivity?.stateDeltas,
3323
- startedAt: liveActivity?.startedAt,
3324
- updatedAt: liveActivity?.updatedAt,
3325
- })
3326
- : [];
3327
- const remainingSummary = completionRemainingSummary({
3328
- remainingContractCount,
3329
- releaseBlockerCount,
3330
- highValueGapCount,
3331
- remainingStopJudgeCount,
3332
- });
3333
- const widgetLines = activeRole
3334
- ? []
3335
- : [
3336
- "completion workflow",
3337
- `phase: ${currentPhase}`,
3338
- `slice: ${sliceId}`,
3339
- `goal: ${sliceGoal}`,
3340
- `next: ${nextMandatoryRole}`,
3341
- `remaining: ${remainingSummary}`,
3342
- ];
3343
- return {
3344
- snapshotPresent: true,
3345
- widgetLines,
3346
- currentPhase,
3347
- sliceId,
3348
- nextMandatoryRole,
3349
- remainingContractCount,
3350
- releaseBlockerCount,
3351
- highValueGapCount,
3352
- remainingStopJudgeCount,
3353
- activeRole,
3354
- livePreview,
3355
- liveState: liveSignal?.state,
3356
- liveIdleMs: liveSignal?.idleMs,
3357
- liveToolActivity: liveActivity?.toolActivity,
3358
- liveAssistantSummary: liveActivity?.assistantSummary,
3359
- liveProgress: liveActivity?.progress,
3360
- liveRationale: liveActivity?.rationale,
3361
- liveNextStep: liveActivity?.nextStep,
3362
- liveVerifying: liveActivity?.verifying,
3363
- liveStateDeltas: liveActivity?.stateDeltas ?? [],
3364
- liveDetailsLines,
3365
- };
3366
- }
3367
-
3368
- async function writeCompletionStatusProbe(surface: CompletionStatusSurface): Promise<void> {
3369
- const outputPath = asString(process.env.PI_COMPLETION_STATUS_SNAPSHOT_FILE);
3370
- if (!outputPath) return;
3371
- await fsp.mkdir(path.dirname(outputPath), { recursive: true });
3372
- await fsp.writeFile(outputPath, `${JSON.stringify(surface, null, 2)}\n`, "utf8");
3373
- }
3374
-
3375
- async function refreshStatus(ctx: { cwd: string; hasUI: boolean; ui: any }) {
3376
- const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
3377
- const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
3378
- maybeInjectTestLiveRoleActivity(rootKey);
3379
- maybeReplayTestLiveRoleEvents(rootKey);
3380
- const surface = buildCompletionStatusSurface(snapshot, liveRoleActivityByRoot.get(rootKey));
3381
- await writeCompletionStatusProbe(surface);
3382
- if (!getCtxHasUI(ctx)) return;
3383
- const ui = getCtxUi(ctx);
3384
- if (!ui) return;
3385
- safeUiCall(() => {
3386
- ui.setWidget(COMPLETION_STATUS_KEY, surface.widgetLines.length > 0 ? surface.widgetLines : undefined);
816
+ const implementationSurfacesLine =
817
+ implementationSurfaces.length > 0 ? `- implementation_surfaces: ${implementationSurfaces.join(" | ")}` : undefined;
818
+ const verificationCommandsLine =
819
+ verificationCommands.length > 0 ? `- verification_commands: ${verificationCommands.join(" | ")}` : undefined;
820
+ return buildExtractedResumeCapsule({
821
+ missionAnchor: asString(snapshot.state?.mission_anchor),
822
+ taskType: currentTaskType(snapshot),
823
+ evaluationProfile: currentEvaluationProfile(snapshot),
824
+ currentPhase: asString(snapshot.state?.current_phase),
825
+ continuationPolicy: asString(snapshot.state?.continuation_policy),
826
+ continuationReason: asString(snapshot.state?.continuation_reason),
827
+ requiresReground: asBoolean(snapshot.state?.requires_reground) ?? "unknown",
828
+ nextMandatoryRole: asString(snapshot.state?.next_mandatory_role),
829
+ nextMandatoryAction: asString(snapshot.state?.next_mandatory_action),
830
+ remainingSliceCount: remainingSliceCount(snapshot.plan),
831
+ remainingStopJudges: asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)",
832
+ history,
833
+ activeSliceMatchesPlan: activeSliceMatchesPlan(snapshot),
834
+ activeSliceContractDrift: activeSliceContractDriftSummary(snapshot),
835
+ implementerHandoffSnapshot: handoffSnapshotState(snapshot.active),
836
+ evidence,
837
+ activeSlice: {
838
+ sliceId: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id),
839
+ status: asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status),
840
+ goal: asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal),
841
+ priority: asNumber(snapshot.active?.priority),
842
+ whyNow: asString(snapshot.active?.why_now),
843
+ contractIds,
844
+ blockedOn,
845
+ lockedNotes,
846
+ mustFixFindings,
847
+ implementationSurfaces,
848
+ verificationCommands,
849
+ implementationSurfacesLine,
850
+ verificationCommandsLine,
851
+ basisCommit: asString(snapshot.active?.basis_commit),
852
+ remainingContractIdsBefore: remainingBefore,
853
+ releaseBlockerCountBefore: asNumber(snapshot.active?.release_blocker_count_before),
854
+ highValueGapCountBefore: asNumber(snapshot.active?.high_value_gap_count_before),
855
+ acceptanceCriteria: acceptance,
856
+ },
3387
857
  });
3388
858
  }
3389
859
 
3390
- function parseReportFields(text: string): Record<string, string> {
3391
- return roleReporting.parseReportFields(text);
3392
- }
3393
-
3394
- function parseYesNo(value: string | undefined): boolean | undefined {
3395
- return roleReporting.parseYesNo(value);
3396
- }
3397
-
3398
- function parseFirstNumber(value: string | undefined): number | undefined {
3399
- return roleReporting.parseFirstNumber(value);
3400
- }
3401
-
3402
860
  async function gitHeadSha(cwd: string): Promise<string | undefined> {
3403
861
  return await new Promise((resolve) => {
3404
862
  const proc = spawn("git", ["rev-parse", "HEAD"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
@@ -3413,311 +871,6 @@ async function gitHeadSha(cwd: string): Promise<string | undefined> {
3413
871
  });
3414
872
  }
3415
873
 
3416
- type TranscriptionResult = {
3417
- appended: string[];
3418
- skipped: string[];
3419
- errors: string[];
3420
- };
3421
-
3422
- async function appendJsonlRecord(filePath: string, record: JsonRecord): Promise<void> {
3423
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
3424
- await fsp.appendFile(filePath, `${JSON.stringify(record)}\n`, "utf8");
3425
- }
3426
-
3427
-
3428
- function formatElapsed(ms: number | undefined): string {
3429
- if (!ms || ms < 0) return "00:00";
3430
- const totalSeconds = Math.floor(ms / 1000);
3431
- const hours = Math.floor(totalSeconds / 3600);
3432
- const minutes = Math.floor((totalSeconds % 3600) / 60);
3433
- const seconds = totalSeconds % 60;
3434
- if (hours > 0) return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
3435
- return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
3436
- }
3437
-
3438
- function truncateInline(text: string, maxLength = 120): string {
3439
- const singleLine = text.replace(/\s+/g, " ").trim();
3440
- return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength - 3)}...` : singleLine;
3441
- }
3442
-
3443
-
3444
- function formatToolActivity(toolName: string, args: JsonRecord): string {
3445
- if (toolName === "bash") return `$ ${truncateInline(asString(args.command) ?? "...")}`;
3446
- if (toolName === "read") return `read ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
3447
- if (toolName === "write") return `write ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
3448
- if (toolName === "edit") return `edit ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
3449
- if (toolName === "grep") return `grep ${asString(args.pattern) ?? "..."}`;
3450
- if (toolName === "find") return `find ${asString(args.pattern) ?? "..."}`;
3451
- if (toolName === "ls") return `ls ${asString(args.path) ?? "."}`;
3452
- return `${toolName} ${truncateInline(JSON.stringify(args))}`;
3453
- }
3454
-
3455
- function pushRecentActivity(items: string[], line: string, maxItems = 8): string[] {
3456
- const normalized = truncateInline(line, 160);
3457
- if (!normalized) return items;
3458
- if (items[items.length - 1] === normalized) return items;
3459
- const next = [...items, normalized];
3460
- return next.slice(-maxItems);
3461
- }
3462
-
3463
- function collapseRecentActivity(items: string[], maxItems = 4): string[] {
3464
- const collapsed: string[] = [];
3465
- for (const rawItem of items) {
3466
- const item = truncateInline(rawItem, 120);
3467
- if (!item || item.startsWith("done ") || item.startsWith("result ")) continue;
3468
- if (item.startsWith("assistant:")) continue;
3469
- if (collapsed[collapsed.length - 1] === item) continue;
3470
- collapsed.push(item);
3471
- }
3472
- return collapsed.slice(-maxItems);
3473
- }
3474
-
3475
- function formatInlineRunningText(theme: any, lines: string[], options?: { primaryAssistant?: boolean }): string {
3476
- let text = "";
3477
- for (const [index, line] of lines.entries()) {
3478
- if (index > 0) text += "\n";
3479
- if (index === 0) {
3480
- const [prefix, ...rest] = line.split(" ");
3481
- text += theme.fg("warning", prefix);
3482
- if (rest.length > 0) text += ` ${theme.fg("accent", rest.join(" "))}`;
3483
- continue;
3484
- }
3485
- if (line.startsWith("tool:") || line.startsWith("progress:")) {
3486
- text += theme.fg("toolOutput", line);
3487
- continue;
3488
- }
3489
- if (line.startsWith("activity:")) {
3490
- text += line.includes("stalled") ? theme.fg("warning", line) : line;
3491
- continue;
3492
- }
3493
- if (line === "recent tools:") {
3494
- text += theme.fg("muted", line);
3495
- continue;
3496
- }
3497
- if (line.startsWith("- ")) {
3498
- text += `${theme.fg("muted", "- ")}${theme.fg("muted", line.slice(2))}`;
3499
- continue;
3500
- }
3501
- if (line.startsWith("elapsed:")) {
3502
- text += line;
3503
- continue;
3504
- }
3505
- if (line.startsWith("assistant:")) {
3506
- text += options?.primaryAssistant ? line : theme.fg("muted", line);
3507
- continue;
3508
- }
3509
- if (line.startsWith("next:") || line.startsWith("verifying:")) {
3510
- text += theme.fg("muted", line);
3511
- continue;
3512
- }
3513
- if (line.startsWith("rationale:") || line.startsWith("state-delta:")) {
3514
- text += line;
3515
- continue;
3516
- }
3517
- text += theme.fg("muted", line);
3518
- }
3519
- return text;
3520
- }
3521
-
3522
- function buildInlineRunningLines(details: {
3523
- role?: string;
3524
- startedAt?: number;
3525
- updatedAt?: number;
3526
- currentAction?: string;
3527
- toolActivity?: string;
3528
- toolRecentActivity?: string[];
3529
- recentActivity?: string[];
3530
- assistantSummary?: string;
3531
- progress?: string;
3532
- rationale?: string;
3533
- nextStep?: string;
3534
- verifying?: string;
3535
- stateDeltas?: string[];
3536
- }): string[] {
3537
- const lines: string[] = [];
3538
- let header = "running completion role";
3539
- if (details.role) header += ` ${details.role}`;
3540
- lines.push(header);
3541
- if (details.startedAt !== undefined) lines.push(`elapsed: ${formatElapsed(nowMs() - details.startedAt)}`);
3542
- const signalLine = formatLiveActivitySignal(
3543
- liveActivitySignal({ status: "running", startedAt: details.startedAt, updatedAt: details.updatedAt }),
3544
- );
3545
- if (signalLine) lines.push(signalLine);
3546
- const toolLine = details.toolActivity;
3547
- if (toolLine) lines.push(`tool: ${toolLine}`);
3548
- if (details.progress) lines.push(`progress: ${details.progress}`);
3549
- else if (details.assistantSummary) lines.push(`assistant: ${details.assistantSummary}`);
3550
- else if (details.currentAction && details.currentAction !== toolLine) {
3551
- lines.push(`assistant: ${details.currentAction.replace(/^assistant:\s*/, "")}`);
3552
- }
3553
- if (details.rationale) lines.push(`rationale: ${details.rationale}`);
3554
- if (details.nextStep) lines.push(`next: ${details.nextStep}`);
3555
- if (details.verifying) lines.push(`verifying: ${details.verifying}`);
3556
- for (const delta of (details.stateDeltas ?? []).slice(-4)) lines.push(`state-delta: ${delta}`);
3557
- const recentTools = collapseRecentActivity(details.toolRecentActivity ?? details.recentActivity ?? []);
3558
- const recentWithoutCurrent = recentTools.filter((item) => item !== toolLine);
3559
- if (recentWithoutCurrent.length > 0) {
3560
- lines.push("recent tools:");
3561
- for (const item of recentWithoutCurrent) lines.push(`- ${item}`);
3562
- }
3563
- return lines;
3564
- }
3565
-
3566
- function parseStructuredProgress(text: string): {
3567
- progress?: string;
3568
- rationale?: string;
3569
- nextStep?: string;
3570
- verifying?: string;
3571
- stateDeltas: string[];
3572
- } {
3573
- const result: { progress?: string; rationale?: string; nextStep?: string; verifying?: string; stateDeltas: string[] } = {
3574
- stateDeltas: [],
3575
- };
3576
- for (const rawLine of text.split("\n")) {
3577
- const line = rawLine.trim();
3578
- if (!line) continue;
3579
- const match = line.match(/^(PROGRESS|RATIONALE|NEXT|VERIFYING|STATE-DELTA):\s*(.+)$/i);
3580
- if (!match) continue;
3581
- const [, rawKey, rawValue] = match;
3582
- const key = rawKey.toUpperCase();
3583
- const value = rawValue.trim();
3584
- if (!value) continue;
3585
- if (key === "PROGRESS") result.progress = value;
3586
- else if (key === "RATIONALE") result.rationale = value;
3587
- else if (key === "NEXT") result.nextStep = value;
3588
- else if (key === "VERIFYING") result.verifying = value;
3589
- else if (key === "STATE-DELTA") result.stateDeltas.push(value);
3590
- }
3591
- if (result.stateDeltas.length > 6) result.stateDeltas = result.stateDeltas.slice(-6);
3592
- return result;
3593
- }
3594
-
3595
- async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: string, reportFields: Record<string, string>): Promise<TranscriptionResult> {
3596
- const snapshot = await loadCompletionSnapshot(cwd);
3597
- if (!snapshot) {
3598
- return { appended: [], skipped: ["No canonical completion snapshot found."], errors: [] };
3599
- }
3600
- const headSha = await gitHeadSha(snapshot.files.root);
3601
- if (!headSha) {
3602
- return { appended: [], skipped: [], errors: ["Could not resolve git HEAD for transcription."] };
3603
- }
3604
-
3605
- const sliceId =
3606
- asString(snapshot.active?.slice_id) ??
3607
- asString(snapshot.activeSlice?.slice_id) ??
3608
- asString(snapshot.state?.latest_completed_slice);
3609
-
3610
- return await roleReporting.transcribeCanonicalRoleReport({
3611
- role,
3612
- output,
3613
- reportFields,
3614
- snapshotFiles: snapshot.files,
3615
- headSha,
3616
- sliceId,
3617
- });
3618
- }
3619
-
3620
- function isPathInside(root: string, candidatePath: string): boolean {
3621
- const resolvedRoot = path.resolve(root);
3622
- const resolvedCandidate = path.resolve(candidatePath);
3623
- return resolvedCandidate === resolvedRoot || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`);
3624
- }
3625
-
3626
- function resolveToolPath(cwd: string, rawPath: string): string {
3627
- return path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
3628
- }
3629
-
3630
- function isAllowedControlPlanePath(root: string, rawPath: string): boolean {
3631
- const resolved = resolveToolPath(root, rawPath);
3632
- if (path.basename(resolved) === ".gitignore") return true;
3633
- return isPathInside(path.join(root, ".agent"), resolved);
3634
- }
3635
-
3636
- function startsWithAny(value: string, prefixes: string[]): boolean {
3637
- return prefixes.some((prefix) => value.startsWith(prefix));
3638
- }
3639
-
3640
- function normalizeCommand(command: string): string {
3641
- return command.trim().replace(/\s+/g, " ");
3642
- }
3643
-
3644
- function isMutatingBash(command: string): boolean {
3645
- const normalized = normalizeCommand(command);
3646
- return startsWithAny(normalized, [
3647
- "git add",
3648
- "git commit",
3649
- "git push",
3650
- "rm ",
3651
- "mv ",
3652
- "cp ",
3653
- "mkdir ",
3654
- "touch ",
3655
- "chmod ",
3656
- "chown ",
3657
- "sed -i",
3658
- "perl -pi",
3659
- "python -c",
3660
- "python3 -c",
3661
- "node -e",
3662
- "bun -e",
3663
- "tee ",
3664
- ]) || normalized.includes(">") || normalized.includes("| tee") || normalized.includes("apply_patch");
3665
- }
3666
-
3667
- async function loadAgentDefinition(cwd: string, role: CompletionRole): Promise<AgentDefinition> {
3668
- const projectAgent = walkUpForDir(cwd, [".pi", "agents", `${role}.md`]);
3669
- const packageAgent = PACKAGE_AGENTS_DIR ? path.join(PACKAGE_AGENTS_DIR, `${role}.md`) : undefined;
3670
- const candidates = [projectAgent, packageAgent, path.join(AGENT_HOME, "agents", `${role}.md`)].filter(
3671
- (candidate): candidate is string => Boolean(candidate),
3672
- );
3673
- for (const candidate of candidates) {
3674
- if (!fs.existsSync(candidate)) continue;
3675
- const raw = await fsp.readFile(candidate, "utf8");
3676
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(raw);
3677
- return {
3678
- name: frontmatter.name ?? role,
3679
- description: frontmatter.description,
3680
- tools: frontmatter.tools?.split(",").map((tool) => tool.trim()).filter(Boolean),
3681
- model: frontmatter.model,
3682
- systemPrompt: body.trim(),
3683
- filePath: candidate,
3684
- };
3685
- }
3686
- throw new Error(`Missing completion agent definition for ${role}`);
3687
- }
3688
-
3689
- async function writeTempFile(prefix: string, content: string): Promise<{ dir: string; filePath: string }> {
3690
- const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
3691
- const filePath = path.join(dir, "prompt.md");
3692
- await fsp.writeFile(filePath, content, { encoding: "utf8", mode: 0o600 });
3693
- return { dir, filePath };
3694
- }
3695
-
3696
- function getPiInvocation(args: string[]): { command: string; args: string[] } {
3697
- const currentScript = process.argv[1];
3698
- const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
3699
- if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
3700
- return { command: process.execPath, args: [currentScript, ...args] };
3701
- }
3702
- const execName = path.basename(process.execPath).toLowerCase();
3703
- const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
3704
- if (!isGenericRuntime) return { command: process.execPath, args };
3705
- return { command: "pi", args };
3706
- }
3707
-
3708
- function lastAssistantText(messages: Array<{ role: string; content: Array<{ type: string; text?: string }> }>): string {
3709
- for (let i = messages.length - 1; i >= 0; i--) {
3710
- const message = messages[i];
3711
- if (message.role !== "assistant") continue;
3712
- const texts = message.content
3713
- .filter((part) => part.type === "text" && typeof part.text === "string")
3714
- .map((part) => part.text?.trim())
3715
- .filter((part): part is string => Boolean(part));
3716
- if (texts.length > 0) return texts.join("\n\n");
3717
- }
3718
- return "";
3719
- }
3720
-
3721
874
  function completionKickoff(
3722
875
  goal: string,
3723
876
  taskType: string,
@@ -3739,15 +892,61 @@ function completionResumePrompt(taskType: string, evaluationProfile: string): st
3739
892
  }
3740
893
 
3741
894
  export default function completionExtension(pi: ExtensionAPI) {
895
+ const statusSurfaceArgs = {
896
+ liveRoleActivityByRoot,
897
+ completionStatusKey: COMPLETION_STATUS_KEY,
898
+ safeUiCall,
899
+ getCtxCwd,
900
+ getCtxHasUI,
901
+ getCtxUi,
902
+ };
903
+ const driverDeps = {
904
+ bareOnlyGuidance: COOK_BARE_ONLY_GUIDANCE,
905
+ structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
906
+ mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
907
+ cookCommandSpec: {
908
+ description: "Bare /cook workflow: start, continue, refocus, or start the next round",
909
+ },
910
+ buildContextProposalContinuationReason,
911
+ completionKickoff,
912
+ completionResumePrompt,
913
+ completionRootKey,
914
+ completionTestAutoContinuePromptPath,
915
+ completionTestDriverPromptPath,
916
+ completionTestExistingWorkflowChooserSnapshotPath,
917
+ completionTestWorkflowActionOverride,
918
+ completionTestWorkflowMissionOverride,
919
+ confirmContextProposal,
920
+ deriveCookContextProposal,
921
+ emitCommandText,
922
+ finalizeContextProposalAnalysis,
923
+ getCtxCwd,
924
+ getCtxHasUI,
925
+ getCtxUi,
926
+ hasRunningCompletionRole,
927
+ maybeWriteActiveWorkflowRoutingSnapshot,
928
+ activateCompletionRoutingForRoot,
929
+ maybeWriteTestSnapshot,
930
+ missionAnchorsLikelyEquivalent,
931
+ missionAnchorsStrictlyEquivalent,
932
+ scaffoldCompletionFiles,
933
+ shouldSkipDriverKickoffForTests,
934
+ shouldTestAutoContinueOnSessionStart,
935
+ shouldTreatBareActiveWorkflowProposalAsClearRefocus,
936
+ };
937
+
3742
938
  pi.on("session_start", async (_event, ctx) => {
3743
- await refreshStatus(ctx);
939
+ await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
3744
940
  if (shouldTestAutoContinueOnSessionStart()) {
3745
- await autoContinueWorkflowIfNeeded(pi, ctx);
941
+ const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
942
+ if (hasCompletionRoutingActivation(snapshot)) {
943
+ await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
944
+ }
3746
945
  }
3747
946
  });
3748
947
 
3749
948
  pi.on("turn_end", async (_event, ctx) => {
3750
- await refreshStatus(ctx);
949
+ await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
3751
950
  });
3752
951
 
3753
952
  pi.on("agent_end", async (_event, ctx) => {
@@ -3755,8 +954,10 @@ export default function completionExtension(pi: ExtensionAPI) {
3755
954
  if (snapshot && (await pathExists(snapshot.files.compactionMarkerPath))) {
3756
955
  await fsp.rm(snapshot.files.compactionMarkerPath, { force: true });
3757
956
  }
3758
- await refreshStatus(ctx);
3759
- await autoContinueWorkflowIfNeeded(pi, ctx);
957
+ await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
958
+ if (hasCompletionRoutingActivation(snapshot)) {
959
+ await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
960
+ }
3760
961
  });
3761
962
 
3762
963
  pi.on("before_agent_start", async (_event, ctx) => {
@@ -3769,7 +970,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3769
970
  if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot)) return;
3770
971
  const additions = isWorkflowDone(loaded.snapshot)
3771
972
  ? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
3772
- : [buildSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
973
+ : [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
3773
974
  if (!isWorkflowDone(loaded.snapshot)) {
3774
975
  const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
3775
976
  let marker: JsonRecord | undefined;
@@ -3795,7 +996,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3795
996
  const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
3796
997
  if (!loaded || isWorkflowDone(loaded.snapshot)) return;
3797
998
  const { preparation } = event;
3798
- const summary = buildResumeCapsule(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory);
999
+ const summary = composeResumeCapsule(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory);
3799
1000
  await fsp.mkdir(loaded.snapshot.files.tmpDir, { recursive: true });
3800
1001
  await fsp.writeFile(
3801
1002
  loaded.snapshot.files.compactionMarkerPath,
@@ -3826,44 +1027,14 @@ export default function completionExtension(pi: ExtensionAPI) {
3826
1027
  const snapshot = await loadCompletionSnapshot(cwd);
3827
1028
  const completionActive = Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) !== "done";
3828
1029
  const root = snapshot?.files.root ?? findRepoRoot(cwd) ?? cwd;
3829
-
3830
- if (event.toolName === "completion_role" && role) {
3831
- return { block: true, reason: `Nested completion role dispatch is forbidden for ${role}.` };
3832
- }
3833
-
3834
- if (event.toolName === "edit" || event.toolName === "write") {
3835
- const rawPath = asString((event.input as JsonRecord).path);
3836
- if (!rawPath) return;
3837
-
3838
- if (role === "completion-reviewer" || role === "completion-auditor" || role === "completion-stop-judge") {
3839
- return { block: true, reason: `${role} is read-only.` };
3840
- }
3841
-
3842
- if ((role === "completion-bootstrapper" || role === "completion-regrounder") && !isAllowedControlPlanePath(root, rawPath)) {
3843
- return { block: true, reason: `${role} may only edit .agent/** or .gitignore.` };
3844
- }
3845
-
3846
- if (!role && completionActive && !isAllowedControlPlanePath(root, rawPath)) {
3847
- return { block: true, reason: "The workflow driver may not edit tracked product files directly during completion." };
3848
- }
3849
- }
3850
-
3851
- if (event.toolName !== "bash") return;
3852
- const command = asString((event.input as JsonRecord).command);
3853
- if (!command) return;
3854
- const normalized = normalizeCommand(command);
3855
-
3856
- if (["completion-reviewer", "completion-auditor", "completion-stop-judge"].includes(role ?? "") && isMutatingBash(normalized)) {
3857
- return { block: true, reason: `${role} is read-only and cannot run mutating bash.` };
3858
- }
3859
-
3860
- if ((role === "completion-bootstrapper" || role === "completion-regrounder") && startsWithAny(normalized, ["git add", "git commit"])) {
3861
- return { block: true, reason: `${role} may not create commits.` };
3862
- }
3863
-
3864
- if (!role && completionActive && startsWithAny(normalized, ["git add", "git commit"])) {
3865
- return { block: true, reason: "The workflow driver may not create commits directly during completion." };
3866
- }
1030
+ const reason = toolCallBlockReason({
1031
+ toolName: event.toolName,
1032
+ input: isRecord(event.input) ? event.input : undefined,
1033
+ role,
1034
+ completionActive,
1035
+ root,
1036
+ });
1037
+ if (reason) return { block: true, reason };
3867
1038
  });
3868
1039
 
3869
1040
  pi.registerTool({
@@ -3885,8 +1056,6 @@ export default function completionExtension(pi: ExtensionAPI) {
3885
1056
  const cwd = getCtxCwd(ctx);
3886
1057
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
3887
1058
  const rootKey = runCwd;
3888
- const agent = await loadAgentDefinition(runCwd, role);
3889
- const loaded = await loadCompletionDataForReminder(runCwd);
3890
1059
  type RunningDetails = {
3891
1060
  role: string;
3892
1061
  status: "running" | "ok" | "error";
@@ -3908,150 +1077,87 @@ export default function completionExtension(pi: ExtensionAPI) {
3908
1077
  transcription?: TranscriptionResult;
3909
1078
  exitCode?: number;
3910
1079
  };
3911
- const systemPromptTemp = await writeTempFile("pi-completion-role-", agent.systemPrompt);
3912
- const taskLines = [
3913
- `Completion role: ${role}`,
3914
- "Before acting, read the completion protocol skill and reference:",
3915
- `- ${SKILL_PATH}`,
3916
- `- ${REFERENCE_PATH}`,
3917
- "Use canonical .agent/** state as the source of truth.",
3918
- ];
3919
- if (loaded && isRubricEvaluationRole(role)) {
3920
- taskLines.push("", ...buildEvaluationRoleContextLines(loaded.snapshot, role));
3921
- }
3922
- if (params.task?.trim()) {
3923
- taskLines.push("", "Supplemental task context:", params.task.trim());
3924
- }
3925
- const prompt = taskLines.join("\n");
3926
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath];
3927
- if (agent.model) args.push("--model", agent.model);
3928
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
3929
- args.push(prompt);
3930
-
3931
- const invocation = getPiInvocation(args);
3932
- let stderr = "";
3933
- const messages: RoleMessage[] = [];
3934
- const liveActivity = createLiveRoleActivity(role);
3935
- const emitRunningUpdate = (freshActivity = false) => {
3936
- if (freshActivity) liveActivity.updatedAt = nowMs();
1080
+ const emitActivityUpdate = (activity: LiveRoleActivity) => {
3937
1081
  const details: RunningDetails = {
3938
1082
  role,
3939
- status: "running",
3940
- currentAction: liveActivity.currentAction,
3941
- toolActivity: liveActivity.toolActivity,
3942
- toolRecentActivity: liveActivity.toolRecentActivity,
3943
- recentActivity: liveActivity.recentActivity,
3944
- assistantSummary: liveActivity.assistantSummary,
3945
- lastAssistantText: liveActivity.lastAssistantText,
3946
- progress: liveActivity.progress,
3947
- rationale: liveActivity.rationale,
3948
- nextStep: liveActivity.nextStep,
3949
- verifying: liveActivity.verifying,
3950
- stateDeltas: liveActivity.stateDeltas,
3951
- startedAt: liveActivity.startedAt,
3952
- updatedAt: liveActivity.updatedAt,
1083
+ status: activity.status,
1084
+ currentAction: activity.currentAction,
1085
+ toolActivity: activity.toolActivity,
1086
+ toolRecentActivity: activity.toolRecentActivity,
1087
+ recentActivity: activity.recentActivity,
1088
+ assistantSummary: activity.assistantSummary,
1089
+ lastAssistantText: activity.lastAssistantText,
1090
+ progress: activity.progress,
1091
+ rationale: activity.rationale,
1092
+ nextStep: activity.nextStep,
1093
+ verifying: activity.verifying,
1094
+ stateDeltas: activity.stateDeltas,
1095
+ startedAt: activity.startedAt,
1096
+ updatedAt: activity.updatedAt,
3953
1097
  };
3954
- liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
3955
- void refreshStatus(ctx as { cwd: string; hasUI: boolean; ui: any });
1098
+ liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(activity, { status: activity.status }));
1099
+ void refreshCompletionStatus({ ctx: ctx as { cwd: string; hasUI: boolean; ui: any }, ...statusSurfaceArgs });
3956
1100
  onUpdate?.({
3957
- content: [{ type: "text", text: liveActivity.lastAssistantText || liveActivity.currentAction || `Running ${role}...` }],
1101
+ content: [{ type: "text", text: activity.lastAssistantText || activity.currentAction || `Running ${role}...` }],
3958
1102
  details,
3959
1103
  });
3960
1104
  };
3961
- emitRunningUpdate(true);
3962
- const heartbeat = setInterval(() => emitRunningUpdate(false), LIVE_ROLE_HEARTBEAT_MS);
3963
-
3964
- try {
3965
- const exitCode = await new Promise<number>((resolve) => {
3966
- const proc = spawn(invocation.command, invocation.args, {
3967
- cwd: runCwd,
3968
- env: { ...process.env, PI_COMPLETION_ROLE: role },
3969
- stdio: ["ignore", "pipe", "pipe"],
3970
- shell: false,
3971
- });
3972
- let buffer = "";
3973
-
3974
- const processLine = (line: string) => {
3975
- if (!line.trim()) return;
3976
- try {
3977
- const event = JSON.parse(line) as JsonRecord;
3978
- if (applyLiveRoleEvent(liveActivity, event, messages)) emitRunningUpdate(true);
3979
- } catch {
3980
- // ignore malformed lines
3981
- }
3982
- };
3983
-
3984
- proc.stdout.on("data", (chunk) => {
3985
- buffer += chunk.toString();
3986
- const lines = buffer.split("\n");
3987
- buffer = lines.pop() ?? "";
3988
- for (const line of lines) processLine(line);
3989
- });
3990
-
3991
- proc.stderr.on("data", (chunk) => {
3992
- stderr += chunk.toString();
3993
- });
3994
-
3995
- proc.on("close", (code) => {
3996
- if (buffer.trim()) processLine(buffer);
3997
- resolve(code ?? 0);
3998
- });
3999
-
4000
- proc.on("error", () => resolve(1));
4001
-
4002
- if (signal) {
4003
- const abort = () => proc.kill("SIGTERM");
4004
- if (signal.aborted) abort();
4005
- else signal.addEventListener("abort", abort, { once: true });
4006
- }
4007
- });
1105
+ const loaded = await loadCompletionDataForReminder(runCwd);
1106
+ const result = await runCompletionRole({
1107
+ root: runCwd,
1108
+ role,
1109
+ task: params.task,
1110
+ signal,
1111
+ systemPromptPreamble: [
1112
+ `Completion role: ${role}`,
1113
+ "Before acting, read the completion protocol skill and reference:",
1114
+ `- ${SKILL_PATH}`,
1115
+ `- ${REFERENCE_PATH}`,
1116
+ "Use canonical .agent/** state as the source of truth.",
1117
+ ],
1118
+ evaluationContextLines: loaded && isRubricEvaluationRole(role) ? buildEvaluationRoleContextLines(loaded.snapshot, role) : undefined,
1119
+ onUpdate: emitActivityUpdate,
1120
+ onConsoleMessage: (level, message) => emitCommandText(ctx, message, level),
1121
+ createLiveRoleActivity: (name) => createLiveRoleActivity(name),
1122
+ cloneLiveRoleActivity,
1123
+ applyLiveRoleEvent,
1124
+ nowMs,
1125
+ heartbeatMs: LIVE_ROLE_HEARTBEAT_MS,
1126
+ });
4008
1127
 
4009
- const output = liveActivity.lastAssistantText || stderr.trim() || `${role} finished with no text output.`;
4010
- const reportFields = parseReportFields(output);
4011
- const transcription = exitCode === 0 ? await transcribeRoleOutput(role, runCwd, output, reportFields) : undefined;
4012
- if (transcription?.appended.length) {
4013
- emitCommandText(ctx, `Completion transcription appended: ${transcription.appended.join(", ")}`, "info");
4014
- }
4015
- if (transcription?.errors.length) {
4016
- emitCommandText(ctx, `Completion transcription warning: ${transcription.errors.join(" | ")}`, "warning");
1128
+ liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(result.activity, { status: result.ok ? "ok" : "error" }));
1129
+ await refreshCompletionStatus({ ctx: ctx as { cwd: string; hasUI: boolean; ui: any }, ...statusSurfaceArgs });
1130
+ setTimeout(() => {
1131
+ const current = liveRoleActivityByRoot.get(rootKey);
1132
+ if (current && current.role === role && current.status !== "running") {
1133
+ liveRoleActivityByRoot.delete(rootKey);
4017
1134
  }
4018
- liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: exitCode === 0 ? "ok" : "error" }));
4019
- await refreshStatus(ctx as { cwd: string; hasUI: boolean; ui: any });
4020
- return {
4021
- content: [{ type: "text", text: output }],
4022
- details: {
4023
- role,
4024
- status: exitCode === 0 ? "ok" : "error",
4025
- exitCode,
4026
- stderr: stderr.trim(),
4027
- reportFields,
4028
- transcription,
4029
- currentAction: liveActivity.currentAction,
4030
- toolActivity: liveActivity.toolActivity,
4031
- toolRecentActivity: liveActivity.toolRecentActivity,
4032
- recentActivity: liveActivity.recentActivity,
4033
- assistantSummary: liveActivity.assistantSummary,
4034
- lastAssistantText: liveActivity.lastAssistantText,
4035
- progress: liveActivity.progress,
4036
- rationale: liveActivity.rationale,
4037
- nextStep: liveActivity.nextStep,
4038
- verifying: liveActivity.verifying,
4039
- stateDeltas: liveActivity.stateDeltas,
4040
- startedAt: liveActivity.startedAt,
4041
- updatedAt: liveActivity.updatedAt,
4042
- },
4043
- isError: exitCode !== 0,
4044
- };
4045
- } finally {
4046
- clearInterval(heartbeat);
4047
- setTimeout(() => {
4048
- const current = liveRoleActivityByRoot.get(rootKey);
4049
- if (current && current.role === role && current.status !== "running") {
4050
- liveRoleActivityByRoot.delete(rootKey);
4051
- }
4052
- }, 10_000);
4053
- await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
4054
- }
1135
+ }, 10_000);
1136
+ return {
1137
+ content: [{ type: "text", text: result.output }],
1138
+ details: {
1139
+ role,
1140
+ status: result.ok ? "ok" : "error",
1141
+ exitCode: result.exitCode,
1142
+ stderr: result.stderr,
1143
+ reportFields: result.reportFields,
1144
+ transcription: result.transcription,
1145
+ currentAction: result.activity.currentAction,
1146
+ toolActivity: result.activity.toolActivity,
1147
+ toolRecentActivity: result.activity.toolRecentActivity,
1148
+ recentActivity: result.activity.recentActivity,
1149
+ assistantSummary: result.activity.assistantSummary,
1150
+ lastAssistantText: result.activity.lastAssistantText,
1151
+ progress: result.activity.progress,
1152
+ rationale: result.activity.rationale,
1153
+ nextStep: result.activity.nextStep,
1154
+ verifying: result.activity.verifying,
1155
+ stateDeltas: result.activity.stateDeltas,
1156
+ startedAt: result.activity.startedAt,
1157
+ updatedAt: result.activity.updatedAt,
1158
+ },
1159
+ isError: !result.ok,
1160
+ };
4055
1161
  },
4056
1162
  renderCall(args, theme) {
4057
1163
  const role = args.role || "completion-role";
@@ -4137,132 +1243,6 @@ export default function completionExtension(pi: ExtensionAPI) {
4137
1243
  },
4138
1244
  });
4139
1245
 
4140
- pi.registerCommand("cook", {
4141
- description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
4142
- handler: async (args, ctx) => {
4143
- const inlineHint = args.trim() || undefined;
4144
- let goal: string | undefined;
4145
- const cwd = getCtxCwd(ctx);
4146
- let snapshot = await loadCompletionSnapshot(cwd);
4147
- const workflowDone = isWorkflowDone(snapshot);
4148
- let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
4149
- let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
4150
- let kickoffAnalysis: ContextProposalAnalysis | undefined;
1246
+ registerCookCommand(pi, driverDeps);
4151
1247
 
4152
- if (!snapshot) {
4153
- const root = findRepoRoot(cwd) ?? cwd;
4154
- const projectName = path.basename(root);
4155
- const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
4156
- if (!proposal) {
4157
- emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(), "info");
4158
- return;
4159
- }
4160
- const decision = await confirmContextProposal(ctx, proposal, {
4161
- title: "Start a completion workflow from the recent discussion?",
4162
- });
4163
- if (!decision) {
4164
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
4165
- return;
4166
- }
4167
- goal = decision.goalText;
4168
- kickoffMissionAnchor = decision.missionAnchor;
4169
- kickoffAnalysis = decision.analysis;
4170
- const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
4171
- const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
4172
- analysis: startupRouting,
4173
- continuationReason: buildContextProposalContinuationReason(
4174
- "User started workflow via /cook:",
4175
- goal ?? kickoffMissionAnchor ?? projectName,
4176
- startupRouting,
4177
- ),
4178
- });
4179
- emitCommandText(
4180
- ctx,
4181
- `Initialized completion control plane in ${created.root}${created.created.length > 0 ? ` (${created.created.length} files created)` : ""}`,
4182
- "info",
4183
- );
4184
- snapshot = await loadCompletionSnapshot(root);
4185
- }
4186
- if (!snapshot) {
4187
- emitCommandText(ctx, "Failed to load completion workflow state", "error");
4188
- return;
4189
- }
4190
- if (!goal) {
4191
- if (workflowDone) {
4192
- const projectName = path.basename(snapshot.files.root);
4193
- const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
4194
- if (!proposal) {
4195
- emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage("The previous completion workflow is already done."), "info");
4196
- return;
4197
- }
4198
- const decision = await confirmContextProposal(ctx, proposal, {
4199
- title: "The previous completion workflow is done. Start the next workflow round from the recent discussion?",
4200
- });
4201
- if (!decision) {
4202
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
4203
- return;
4204
- }
4205
- goal = decision.goalText;
4206
- kickoffIntent = "refocus";
4207
- kickoffMissionAnchor = decision.missionAnchor;
4208
- await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
4209
- snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4210
- emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
4211
- } else {
4212
- const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, inlineHint);
4213
- if (assessment.action !== "refocus" || !assessment.proposal) {
4214
- await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4215
- return;
4216
- }
4217
- const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, {
4218
- intro: "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:",
4219
- proposedMissionLabel: "Proposed mission from recent discussion",
4220
- refocusChoiceLabel:
4221
- "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
4222
- });
4223
- if (!decision) {
4224
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
4225
- return;
4226
- }
4227
- if (decision.action === "continue") {
4228
- await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4229
- return;
4230
- }
4231
- const proposalDecision = await confirmContextProposal(ctx, assessment.proposal, {
4232
- title: "Start the replacement workflow from recent discussion?",
4233
- });
4234
- if (!proposalDecision) {
4235
- emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
4236
- return;
4237
- }
4238
- goal = proposalDecision.goalText;
4239
- kickoffIntent = "refocus";
4240
- kickoffMissionAnchor = proposalDecision.missionAnchor;
4241
- await refocusCompletionMission(snapshot, proposalDecision.missionAnchor, proposalDecision.goalText, proposalDecision.analysis);
4242
- snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4243
- emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
4244
- }
4245
- }
4246
- kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
4247
- activateCompletionRoutingForRoot(snapshot.files.root);
4248
- pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
4249
- const kickoffPrompt = completionKickoff(
4250
- goal,
4251
- currentTaskType(snapshot) ?? "(missing)",
4252
- currentEvaluationProfile(snapshot) ?? "(missing)",
4253
- kickoffIntent,
4254
- kickoffMissionAnchor,
4255
- );
4256
- const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
4257
- const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
4258
- kind: "kickoff",
4259
- mission_anchor: kickoffMissionAnchor,
4260
- goal,
4261
- intent: kickoffIntent,
4262
- task_type: currentTaskType(snapshot) ?? "(missing)",
4263
- evaluation_profile: currentEvaluationProfile(snapshot) ?? "(missing)",
4264
- });
4265
- await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, kickoffPrompt, "kickoff");
4266
- },
4267
- });
4268
1248
  }