@linimin/pi-letscook 0.1.44 → 0.1.46

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