@linimin/pi-letscook 0.1.68 → 0.1.70

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.
@@ -11,7 +11,7 @@ import {
11
11
  type ContextProposal,
12
12
  type RecentDiscussionEntry,
13
13
  } from "./proposal";
14
- import { contextProposalAnalystProgressLines } from "./prompt-surfaces";
14
+ import { contextProposalAnalystProgressLines, primaryAgentHandoffProgressLines } from "./prompt-surfaces";
15
15
  import {
16
16
  applyLiveRoleEvent,
17
17
  buildInlineRunningLines,
@@ -97,22 +97,23 @@ const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
97
97
  const ANALYST_HEARTBEAT_MS = 5_000;
98
98
 
99
99
  const PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT = [
100
- "You are the primary agent preparing an explicit /cook startup plan after the user already chose workflow mode.",
101
- "Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete startup plan can be prepared.",
102
- "If you can prepare a plan, the JSON must use kind cook_handoff, source primary_agent, and handoff_kind implementation_workflow_handoff.",
103
- "Author the approved workflow startup plan now from the primary-agent view of the task so /cook can persist it under .agent before completion-regrounder derives canonical slices.",
104
- "Capture the agreed mission, scope, constraints or non_goals, acceptance, risks, notes, and any concrete planning hints that will help completion-regrounder split slices later.",
105
- "If a bounded first slice, likely implementation surfaces, or likely verification commands are already obvious, include first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first as optional hints only. They are not required when the overall startup plan is already concrete enough to begin workflow planning.",
106
- "Prefer the latest user-authored task context plus canonical workflow context over older assistant-authored previews or stale planning text.",
107
- "Do not directly reuse an old preview capsule as-is; either synthesize a fresh startup plan from the current task context or return a brief plain sentence saying no concrete startup plan should replace canonical state yet.",
108
- "If canonical workflow context already exists and the latest discussion does not clearly ask to replace the mission or start the next round, return a brief plain sentence instead of inventing a replacement startup plan.",
109
- "Do not make /cook infer or rediscover the mission later; author the startup plan now from the primary-agent view of the task.",
100
+ "You are the primary agent preparing an explicit /cook handoff after the user already chose workflow mode.",
101
+ "Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete handoff can be prepared.",
102
+ "If you can prepare a handoff, the JSON must use kind cook_handoff, source primary_agent, and handoff_kind implementation_workflow_handoff.",
103
+ "When the user has clearly accepted a concrete assistant-proposed slice, carry that slice forward into the handoff instead of broadening or re-guessing the mission.",
104
+ "Do not make /cook infer or rediscover the mission from recent discussion later; author the handoff now from the primary-agent view of the task.",
110
105
  "Do not emit markdown commentary before or after the capsule.",
111
- "If the task is not concrete enough for workflow startup, do not invent missing detail.",
106
+ "If the task is not concrete enough for implementation workflow, do not invent the slice.",
107
+ "A valid implementation-ready handoff must include mission, scope, constraints or non_goals, acceptance, risks, notes, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first.",
112
108
  ].join(" ");
113
109
  const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
114
110
 
115
- class StartupAnalystOverlay extends Container {
111
+ type CookStartupOverlayOptions = {
112
+ title: string;
113
+ footer: string;
114
+ };
115
+
116
+ class CookStartupOverlay extends Container {
116
117
  private readonly border: DynamicBorder;
117
118
  private readonly title: Text;
118
119
  private readonly body: Text;
@@ -120,7 +121,10 @@ class StartupAnalystOverlay extends Container {
120
121
  private lines: string[] = [];
121
122
  onAbort?: () => void;
122
123
 
123
- constructor(private readonly theme: any) {
124
+ constructor(
125
+ private readonly theme: any,
126
+ private readonly options: CookStartupOverlayOptions,
127
+ ) {
124
128
  super();
125
129
  this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
126
130
  this.title = new Text("", 1, 0);
@@ -140,9 +144,9 @@ class StartupAnalystOverlay extends Container {
140
144
  }
141
145
 
142
146
  private updateDisplay(): void {
143
- this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
147
+ this.title.setText(this.theme.fg("accent", this.theme.bold(this.options.title)));
144
148
  this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
145
- this.footer.setText(this.theme.fg("muted", "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state"));
149
+ this.footer.setText(this.theme.fg("muted", this.options.footer));
146
150
  }
147
151
 
148
152
  override handleInput(data: string): void {
@@ -196,12 +200,15 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
196
200
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
197
201
  const invocation = getPiInvocation(args);
198
202
  const liveActivity = createLiveRoleActivity(STARTUP_ANALYST_ROLE);
203
+ liveActivity.toolActivity = undefined;
204
+ liveActivity.toolRecentActivity = [];
205
+ liveActivity.recentActivity = [];
199
206
  liveActivity.progress = "Analyzing recent discussion";
200
207
  liveActivity.currentAction = "Reading recent discussion and preparing a startup proposal";
201
208
  liveActivity.assistantSummary = liveActivity.progress;
202
209
  liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
203
210
  const messages: RoleMessage[] = [];
204
- let overlay: StartupAnalystOverlay | undefined;
211
+ let overlay: CookStartupOverlay | undefined;
205
212
  let finishOverlay: ((value: string | undefined) => void) | undefined;
206
213
  let overlaySettled = false;
207
214
  const settleOverlay = (value: string | undefined) => {
@@ -317,7 +324,10 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
317
324
  if (ui) {
318
325
  return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
319
326
  finishOverlay = done;
320
- overlay = new StartupAnalystOverlay(theme);
327
+ overlay = new CookStartupOverlay(theme, {
328
+ title: "/cook proposal analyst",
329
+ footer: "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state",
330
+ });
321
331
  overlay.setLines(contextProposalAnalystProgressLines(liveActivity, buildInlineRunningLines));
322
332
  run().then(settleOverlay).catch(() => settleOverlay(undefined));
323
333
  return overlay;
@@ -346,8 +356,7 @@ function buildPrimaryAgentHandoffPrompt(projectName: string, recentEntries: Rece
346
356
  lines.push(
347
357
  "",
348
358
  "Task:",
349
- "The user explicitly invoked /cook. Prepare the primary-agent startup plan that /cook should synthesize immediately for Start/Cancel confirmation, persistence under .agent, and later slice derivation by completion-regrounder.",
350
- "If the latest discussion does not justify a concrete new startup plan, return a brief plain sentence instead of speculative JSON.",
359
+ "The user explicitly invoked /cook. Prepare the primary-agent handoff that /cook should consume immediately for Start/Cancel confirmation.",
351
360
  );
352
361
  return lines.join("\n");
353
362
  }
@@ -358,52 +367,147 @@ async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithA
358
367
  if (!modelArg) return undefined;
359
368
  const cwd = params.getCtxCwd(ctx);
360
369
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
370
+ const rootKey = completionRootKey(undefined, cwd);
361
371
  const prompt = buildPrimaryAgentHandoffPrompt(projectName, recentEntries, params.workflowContextLines ?? []);
362
372
  const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-primary-agent-handoff-", PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT);
363
373
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
364
374
  const invocation = getPiInvocation(args);
365
375
  const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
366
- liveActivity.progress = "Preparing primary-agent /cook handoff";
367
- liveActivity.currentAction = "Authoring explicit startup plan from current task context";
368
- liveActivity.assistantSummary = liveActivity.progress;
369
- try {
370
- const output = await new Promise<string | undefined>((resolve) => {
371
- const proc = spawn(invocation.command, invocation.args, {
372
- cwd: runCwd,
373
- env: process.env,
374
- stdio: ["ignore", "pipe", "pipe"],
375
- shell: false,
376
- });
377
- let buffer = "";
378
- const messages: RoleMessage[] = [];
379
- const processLine = (line: string) => {
380
- if (!line.trim()) return;
381
- try {
382
- const event = JSON.parse(line) as JsonRecord;
383
- applyLiveRoleEvent(liveActivity, event, messages);
384
- } catch {
385
- // ignore malformed lines
376
+ liveActivity.toolActivity = undefined;
377
+ liveActivity.toolRecentActivity = [];
378
+ liveActivity.recentActivity = [];
379
+ liveActivity.currentAction = "Reading current task context";
380
+ liveActivity.rationale = "Loading canonical workflow context";
381
+ liveActivity.nextStep = "Synthesizing startup plan";
382
+ liveActivity.verifying = "Waiting for model response...";
383
+ let overlay: CookStartupOverlay | undefined;
384
+ let finishOverlay: ((value: string | undefined) => void) | undefined;
385
+ let overlaySettled = false;
386
+ const settleOverlay = (value: string | undefined) => {
387
+ if (overlaySettled) return;
388
+ overlaySettled = true;
389
+ finishOverlay?.(value);
390
+ };
391
+ const updateActivity = (fresh = false) => {
392
+ if (fresh) liveActivity.updatedAt = nowMs();
393
+ params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
394
+ void refreshCompletionStatus({
395
+ ctx,
396
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
397
+ completionStatusKey: params.completionStatusKey,
398
+ safeUiCall: params.safeUiCall,
399
+ getCtxCwd: params.getCtxCwd,
400
+ getCtxHasUI: params.getCtxHasUI,
401
+ getCtxUi: params.getCtxUi,
402
+ });
403
+ overlay?.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
404
+ };
405
+ const heartbeat = setInterval(() => updateActivity(false), ANALYST_HEARTBEAT_MS);
406
+ const run = async (): Promise<string | undefined> => {
407
+ try {
408
+ updateActivity(true);
409
+ const output = await new Promise<string | undefined>((resolve) => {
410
+ const proc = spawn(invocation.command, invocation.args, {
411
+ cwd: runCwd,
412
+ env: process.env,
413
+ stdio: ["ignore", "pipe", "pipe"],
414
+ shell: false,
415
+ });
416
+ let settled = false;
417
+ const resolveOnce = (value: string | undefined) => {
418
+ if (settled) return;
419
+ settled = true;
420
+ resolve(value);
421
+ };
422
+ const abort = () => {
423
+ proc.kill("SIGTERM");
424
+ resolveOnce(undefined);
425
+ };
426
+ const handleSigint = () => abort();
427
+ let buffer = "";
428
+ const messages: RoleMessage[] = [];
429
+ const processLine = (line: string) => {
430
+ if (!line.trim()) return;
431
+ try {
432
+ const event = JSON.parse(line) as JsonRecord;
433
+ if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
434
+ } catch {
435
+ // ignore malformed lines
436
+ }
437
+ };
438
+ proc.stdout.on("data", (chunk) => {
439
+ buffer += chunk.toString();
440
+ const lines = buffer.split("\n");
441
+ buffer = lines.pop() ?? "";
442
+ for (const line of lines) processLine(line);
443
+ });
444
+ proc.stderr.on("data", (_chunk) => {
445
+ // ignore handoff stderr unless the subprocess exits without assistant output
446
+ });
447
+ proc.on("close", (code) => {
448
+ process.off("SIGINT", handleSigint);
449
+ if (buffer.trim()) processLine(buffer);
450
+ resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
451
+ });
452
+ proc.on("error", () => {
453
+ process.off("SIGINT", handleSigint);
454
+ resolveOnce(undefined);
455
+ });
456
+ process.once("SIGINT", handleSigint);
457
+ if (overlay) {
458
+ overlay.onAbort = () => {
459
+ process.off("SIGINT", handleSigint);
460
+ abort();
461
+ };
386
462
  }
387
- };
388
- proc.stdout.on("data", (chunk) => {
389
- buffer += chunk.toString();
390
- const lines = buffer.split("\n");
391
- buffer = lines.pop() ?? "";
392
- for (const line of lines) processLine(line);
393
463
  });
394
- proc.stderr.on("data", () => {
395
- // ignore stderr unless no assistant output arrives
464
+ params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
465
+ await refreshCompletionStatus({
466
+ ctx,
467
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
468
+ completionStatusKey: params.completionStatusKey,
469
+ safeUiCall: params.safeUiCall,
470
+ getCtxCwd: params.getCtxCwd,
471
+ getCtxHasUI: params.getCtxHasUI,
472
+ getCtxUi: params.getCtxUi,
396
473
  });
397
- proc.on("close", (code) => {
398
- if (buffer.trim()) processLine(buffer);
399
- resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
474
+ return output;
475
+ } finally {
476
+ clearInterval(heartbeat);
477
+ setTimeout(() => {
478
+ const current = params.liveRoleActivityByRoot.get(rootKey);
479
+ if (current && current.role === PRIMARY_AGENT_HANDOFF_ROLE && current.status !== "running") {
480
+ params.liveRoleActivityByRoot.delete(rootKey);
481
+ void refreshCompletionStatus({
482
+ ctx,
483
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
484
+ completionStatusKey: params.completionStatusKey,
485
+ safeUiCall: params.safeUiCall,
486
+ getCtxCwd: params.getCtxCwd,
487
+ getCtxHasUI: params.getCtxHasUI,
488
+ getCtxUi: params.getCtxUi,
489
+ });
490
+ }
491
+ }, 10_000);
492
+ await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
493
+ }
494
+ };
495
+ if (params.getCtxHasUI(ctx)) {
496
+ const ui = params.getCtxUi(ctx);
497
+ if (ui) {
498
+ return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
499
+ finishOverlay = done;
500
+ overlay = new CookStartupOverlay(theme, {
501
+ title: "/cook startup plan",
502
+ footer: "Esc/Ctrl+C cancel • This startup-plan synthesis runs before /cook writes canonical workflow state",
503
+ });
504
+ overlay.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
505
+ run().then(settleOverlay).catch(() => settleOverlay(undefined));
506
+ return overlay;
400
507
  });
401
- proc.on("error", () => resolve(undefined));
402
- });
403
- return output;
404
- } finally {
405
- await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
508
+ }
406
509
  }
510
+ return await run();
407
511
  }
408
512
 
409
513
  export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
@@ -414,7 +518,7 @@ export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWi
414
518
  try {
415
519
  return await runPrimaryAgentHandoffSubprocess(params);
416
520
  } catch (error) {
417
- console.warn("[completion] primary-agent startup-plan generation failed", error);
521
+ console.warn("[completion] primary-agent handoff generation failed", error);
418
522
  return undefined;
419
523
  }
420
524
  }
@@ -3,7 +3,6 @@ import { spawnSync } from "node:child_process";
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 { buildApprovedStartupPlanMarkdown } from "./prompt-surfaces";
7
6
  import type { CompletionStateSnapshot, JsonRecord } from "./types";
8
7
 
9
8
  const PROTOCOL_ID = "completion";
@@ -46,10 +45,9 @@ export function resolveFiles(root: string) {
46
45
  statePath: path.join(agentDir, "state.json"),
47
46
  planPath: path.join(agentDir, "plan.json"),
48
47
  activePath: path.join(agentDir, "active-slice.json"),
49
- startupPlanPath: path.join(agentDir, "startup-plan.json"),
50
- startupPlanMarkdownPath: path.join(agentDir, "startup-plan.md"),
51
48
  sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
52
49
  stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
50
+ startupBriefPath: path.join(agentDir, "startup-brief.json"),
53
51
  verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
54
52
  compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
55
53
  };
@@ -144,7 +142,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
144
142
  const state = await readJson(files.statePath);
145
143
  const plan = await readJson(files.planPath);
146
144
  const active = await readJson(files.activePath);
147
- const startupPlan = await readJson(files.startupPlanPath);
145
+ const startupBrief = await readJson(files.startupBriefPath);
148
146
  const verificationEvidence = await readJson(files.verificationEvidencePath);
149
147
  return {
150
148
  files,
@@ -152,7 +150,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
152
150
  state,
153
151
  plan,
154
152
  active,
155
- startupPlan,
153
+ startupBrief,
156
154
  verificationEvidence,
157
155
  activeSlice: findActiveSlice(plan, active),
158
156
  };
@@ -233,15 +231,25 @@ export function buildProfileRecord(args: {
233
231
  };
234
232
  }
235
233
 
234
+ function buildWorkflowSessionId(): string {
235
+ return `wf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
236
+ }
237
+
236
238
  export function defaultState(
237
239
  missionAnchor: string,
238
240
  routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
239
241
  advisoryStartupBrief?: JsonRecord,
240
242
  ): JsonRecord {
243
+ const confirmedAt = asString(advisoryStartupBrief?.captured_at) ?? new Date().toISOString();
241
244
  return {
242
245
  schema_version: 1,
243
246
  mission_anchor: missionAnchor,
244
247
  advisory_startup_brief: advisoryStartupBrief ?? null,
248
+ workflow_entry_status: "active",
249
+ workflow_entry_source: "/cook",
250
+ workflow_entry_confirmed_at: confirmedAt,
251
+ workflow_session_id: buildWorkflowSessionId(),
252
+ startup_brief_path: ".agent/startup-brief.json",
245
253
  current_phase: "reground",
246
254
  continuation_policy: "continue",
247
255
  continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
@@ -280,32 +288,6 @@ export function defaultPlan(
280
288
  };
281
289
  }
282
290
 
283
- export function defaultStartupPlan(
284
- missionAnchor: string,
285
- routing?: { taskType?: string; evaluationProfile?: string },
286
- approvedStartupPlan?: JsonRecord,
287
- ): JsonRecord {
288
- return approvedStartupPlan ?? {
289
- schema_version: 1,
290
- artifact_type: "completion-startup-plan",
291
- status: "approved",
292
- source: "recent_discussion",
293
- captured_at: null,
294
- mission_anchor: missionAnchor,
295
- goal_text: `Mission: ${missionAnchor}`,
296
- task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
297
- evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
298
- scope: [],
299
- constraints: [],
300
- acceptance: [],
301
- risks: [],
302
- notes: ["No approved startup plan has been recorded yet."],
303
- planned_surfaces: [],
304
- verification_intent: [],
305
- sequencing_hints: [],
306
- };
307
- }
308
-
309
291
  export function defaultActiveSlice(
310
292
  missionAnchor: string,
311
293
  routing?: { taskType?: string; evaluationProfile?: string },
@@ -334,6 +316,32 @@ export function defaultActiveSlice(
334
316
  };
335
317
  }
336
318
 
319
+ export function defaultStartupBrief(
320
+ missionAnchor: string,
321
+ routing?: { taskType?: string; evaluationProfile?: string },
322
+ advisoryStartupBrief?: JsonRecord,
323
+ ): JsonRecord {
324
+ return {
325
+ schema_version: 1,
326
+ artifact_type: "completion-startup-brief",
327
+ source: asString(advisoryStartupBrief?.source) ?? "primary_agent",
328
+ confirmed: true,
329
+ confirmed_at: asString(advisoryStartupBrief?.captured_at) ?? new Date().toISOString(),
330
+ mission: asString(advisoryStartupBrief?.mission) ?? missionAnchor,
331
+ goal_text: asString(advisoryStartupBrief?.goal_text) ?? `Mission: ${missionAnchor}`,
332
+ scope: asStringArray(advisoryStartupBrief?.scope),
333
+ constraints: asStringArray(advisoryStartupBrief?.constraints),
334
+ acceptance: asStringArray(advisoryStartupBrief?.acceptance),
335
+ risks: asStringArray(advisoryStartupBrief?.risks),
336
+ notes:
337
+ asStringArray(advisoryStartupBrief?.notes).length > 0
338
+ ? asStringArray(advisoryStartupBrief?.notes)
339
+ : ["No additional startup notes were preserved for this workflow entry."],
340
+ task_type: asString(advisoryStartupBrief?.task_type) ?? routing?.taskType ?? DEFAULT_TASK_TYPE,
341
+ evaluation_profile: asString(advisoryStartupBrief?.evaluation_profile) ?? routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
342
+ };
343
+ }
344
+
337
345
  export function defaultVerificationEvidence(): JsonRecord {
338
346
  return {
339
347
  schema_version: 1,
@@ -352,7 +360,7 @@ export function defaultVerificationEvidence(): JsonRecord {
352
360
  }
353
361
 
354
362
  export function buildAgentReadme(projectName: string): string {
355
- 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/startup-plan.json\`\n- \`.agent/startup-plan.md\`\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/startup-plan.json\` plus \`.agent/startup-plan.md\` preserve the approved workflow startup plan captured at \`/cook\`. \`completion-regrounder\` consumes that plan as planning input, then derives canonical slices in \`.agent/plan.json\` from current repo truth.\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`;
363
+ 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/startup-brief.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/startup-brief.json\` preserves the confirmed \`/cook\` startup intent as canonical intake for re-grounding. It does not replace \`.agent/plan.json\` or \`.agent/active-slice.json\`, which remain under regrounder authority.\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`;
356
364
  }
357
365
 
358
366
  export function buildMission(projectName: string, missionAnchor: string): string {
@@ -455,23 +463,15 @@ function trackedDiffFiles(fromCommit, toCommit) {
455
463
 
456
464
  const profile = readJson('.agent/profile.json');
457
465
  const state = readJson('.agent/state.json');
458
- const startupPlan = readJson('.agent/startup-plan.json');
459
466
  const plan = readJson('.agent/plan.json');
460
467
  const active = readJson('.agent/active-slice.json');
461
468
  const evidence = readJson('.agent/verification-evidence.json');
462
- let startupPlanMarkdown = '';
463
- try {
464
- startupPlanMarkdown = fs.readFileSync('.agent/startup-plan.md', 'utf8');
465
- } catch (error) {
466
- fail('.agent/startup-plan.md must be present and readable: ' + error.message);
467
- }
468
469
 
469
470
  ensureTrackedContractFiles();
470
471
 
471
472
  for (const [file, record] of [
472
473
  ['.agent/profile.json', profile],
473
474
  ['.agent/state.json', state],
474
- ['.agent/startup-plan.json', startupPlan],
475
475
  ['.agent/plan.json', plan],
476
476
  ['.agent/active-slice.json', active],
477
477
  ]) {
@@ -482,38 +482,12 @@ for (const [file, record] of [
482
482
  const taskType = asString(profile.task_type);
483
483
  const evaluationProfile = asString(profile.evaluation_profile);
484
484
  if (asString(state.task_type) !== taskType) fail('.agent/state.json task_type must match .agent/profile.json task_type');
485
- if (asString(startupPlan.task_type) !== taskType) fail('.agent/startup-plan.json task_type must match .agent/profile.json task_type');
486
485
  if (asString(plan.task_type) !== taskType) fail('.agent/plan.json task_type must match .agent/profile.json task_type');
487
486
  if (asString(active.task_type) !== taskType) fail('.agent/active-slice.json task_type must match .agent/profile.json task_type');
488
487
  if (asString(state.evaluation_profile) !== evaluationProfile) fail('.agent/state.json evaluation_profile must match .agent/profile.json evaluation_profile');
489
- if (asString(startupPlan.evaluation_profile) !== evaluationProfile) fail('.agent/startup-plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
490
488
  if (asString(plan.evaluation_profile) !== evaluationProfile) fail('.agent/plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
491
489
  if (asString(active.evaluation_profile) !== evaluationProfile) fail('.agent/active-slice.json evaluation_profile must match .agent/profile.json evaluation_profile');
492
490
 
493
- if (asString(startupPlan.artifact_type) !== 'completion-startup-plan') {
494
- fail('.agent/startup-plan.json artifact_type must be completion-startup-plan');
495
- }
496
- if (asString(startupPlan.status) !== 'approved') {
497
- fail('.agent/startup-plan.json status must be approved');
498
- }
499
- const startupPlanMissionAnchor = asString(startupPlan.mission_anchor);
500
- if (!startupPlanMissionAnchor) fail('.agent/startup-plan.json mission_anchor must be present');
501
- if (startupPlanMissionAnchor !== asString(state.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/state.json mission_anchor');
502
- if (startupPlanMissionAnchor !== asString(plan.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/plan.json mission_anchor');
503
- if (startupPlanMissionAnchor !== asString(active.mission_anchor)) fail('.agent/startup-plan.json mission_anchor must match .agent/active-slice.json mission_anchor');
504
- if (!asString(startupPlan.goal_text)) fail('.agent/startup-plan.json goal_text must be present');
505
- for (const field of ['scope', 'constraints', 'acceptance', 'risks', 'notes', 'planned_surfaces', 'verification_intent', 'sequencing_hints']) {
506
- if (!Array.isArray(startupPlan[field])) fail('.agent/startup-plan.json is missing ' + field);
507
- }
508
- if (startupPlanMarkdown.trim().length === 0) fail('.agent/startup-plan.md must not be empty');
509
- if (startupPlanMissionAnchor && !startupPlanMarkdown.includes(startupPlanMissionAnchor)) {
510
- fail('.agent/startup-plan.md must mention the startup-plan mission_anchor');
511
- }
512
- const startupPlanGoalText = asString(startupPlan.goal_text);
513
- if (startupPlanGoalText && !startupPlanMarkdown.includes(startupPlanGoalText)) {
514
- fail('.agent/startup-plan.md must render the startup-plan goal_text');
515
- }
516
-
517
491
  if (asString(evidence.artifact_type) !== 'completion-verification-evidence') {
518
492
  fail('.agent/verification-evidence.json artifact_type must be completion-verification-evidence');
519
493
  }
@@ -660,12 +634,7 @@ export type ScaffoldResult = {
660
634
  export async function scaffoldCompletionFiles(
661
635
  root: string,
662
636
  missionAnchor: string,
663
- options?: {
664
- analysis?: { taskType?: string; evaluationProfile?: string };
665
- continuationReason?: string;
666
- advisoryStartupBrief?: JsonRecord;
667
- approvedStartupPlan?: JsonRecord;
668
- },
637
+ options?: { analysis?: { taskType?: string; evaluationProfile?: string }; continuationReason?: string; advisoryStartupBrief?: JsonRecord },
669
638
  ): Promise<ScaffoldResult> {
670
639
  const files = resolveFiles(root);
671
640
  const created: string[] = [];
@@ -675,10 +644,6 @@ export async function scaffoldCompletionFiles(
675
644
  const projectName = path.basename(root);
676
645
  const docsSurfaces = await detectDocsSurfaces(root);
677
646
  const verifierCommand = await detectVerifierCommand(root);
678
- const startupPlanRecord =
679
- options?.approvedStartupPlan ??
680
- (await readJson(files.startupPlanPath)) ??
681
- defaultStartupPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile });
682
647
  const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
683
648
  { path: path.join(files.agentDir, "README.md"), content: buildAgentReadme(projectName) },
684
649
  { path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
@@ -693,12 +658,8 @@ export async function scaffoldCompletionFiles(
693
658
  content: `${JSON.stringify(defaultState(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile, continuationReason: options?.continuationReason }, options?.advisoryStartupBrief), null, 2)}\n`,
694
659
  },
695
660
  {
696
- path: files.startupPlanPath,
697
- content: `${JSON.stringify(startupPlanRecord, null, 2)}\n`,
698
- },
699
- {
700
- path: files.startupPlanMarkdownPath,
701
- content: buildApprovedStartupPlanMarkdown(startupPlanRecord as any),
661
+ path: files.startupBriefPath,
662
+ content: `${JSON.stringify(defaultStartupBrief(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }, options?.advisoryStartupBrief), null, 2)}\n`,
702
663
  },
703
664
  { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
704
665
  { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
@@ -721,7 +682,6 @@ export function currentTaskType(snapshot: CompletionStateSnapshot): string | und
721
682
  return (
722
683
  asString(snapshot.active?.task_type) ??
723
684
  asString(snapshot.state?.task_type) ??
724
- asString(snapshot.startupPlan?.task_type) ??
725
685
  asString(snapshot.plan?.task_type) ??
726
686
  asString(snapshot.profile?.task_type)
727
687
  );
@@ -731,7 +691,6 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
731
691
  return (
732
692
  asString(snapshot.active?.evaluation_profile) ??
733
693
  asString(snapshot.state?.evaluation_profile) ??
734
- asString(snapshot.startupPlan?.evaluation_profile) ??
735
694
  asString(snapshot.plan?.evaluation_profile) ??
736
695
  asString(snapshot.profile?.evaluation_profile)
737
696
  );
@@ -740,7 +699,6 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
740
699
  export function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
741
700
  return (
742
701
  asString(snapshot.state?.mission_anchor) ??
743
- asString(snapshot.startupPlan?.mission_anchor) ??
744
702
  asString(snapshot.plan?.mission_anchor) ??
745
703
  asString(snapshot.active?.mission_anchor) ??
746
704
  path.basename(snapshot.files.root)
@@ -18,10 +18,9 @@ export type CompletionFiles = {
18
18
  statePath: string;
19
19
  planPath: string;
20
20
  activePath: string;
21
- startupPlanPath: string;
22
- startupPlanMarkdownPath: string;
23
21
  sliceHistoryPath: string;
24
22
  stopHistoryPath: string;
23
+ startupBriefPath: string;
25
24
  verificationEvidencePath: string;
26
25
  compactionMarkerPath: string;
27
26
  };
@@ -32,7 +31,7 @@ export type CompletionStateSnapshot = {
32
31
  state?: JsonRecord;
33
32
  plan?: JsonRecord;
34
33
  active?: JsonRecord;
35
- startupPlan?: JsonRecord;
34
+ startupBrief?: JsonRecord;
36
35
  verificationEvidence?: JsonRecord;
37
36
  activeSlice?: JsonRecord;
38
37
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -242,6 +242,11 @@ acceptance = [
242
242
  state = {
243
243
  'schema_version': 1,
244
244
  'mission_anchor': mission,
245
+ 'workflow_entry_status': 'active',
246
+ 'workflow_entry_source': '/cook',
247
+ 'workflow_entry_confirmed_at': '2026-05-03T00:00:00Z',
248
+ 'workflow_session_id': 'active-slice-fixture-session',
249
+ 'startup_brief_path': '.agent/startup-brief.json',
245
250
  'current_phase': 'implement',
246
251
  'continuation_policy': 'continue',
247
252
  'continuation_reason': 'Fixture for active-slice contract regression coverage.',
@@ -316,6 +321,22 @@ active = {
316
321
  }
317
322
 
318
323
  Path('.agent/state.json').write_text(json.dumps(state, indent=2) + '\n')
324
+ Path('.agent/startup-brief.json').write_text(json.dumps({
325
+ 'schema_version': 1,
326
+ 'artifact_type': 'completion-startup-brief',
327
+ 'source': 'primary_agent',
328
+ 'confirmed': True,
329
+ 'confirmed_at': '2026-05-03T00:00:00Z',
330
+ 'mission': mission,
331
+ 'goal_text': f'Mission: {mission}',
332
+ 'scope': ['Exercise active-slice contract parity.'],
333
+ 'constraints': ['Keep the fixture scoped to active-slice contract coverage.'],
334
+ 'acceptance': acceptance,
335
+ 'risks': ['Fixture drift can hide active-slice contract regressions.'],
336
+ 'notes': ['Fixture startup brief for active-slice contract regression coverage.'],
337
+ 'task_type': task_type,
338
+ 'evaluation_profile': evaluation_profile,
339
+ }, indent=2) + '\n')
319
340
  Path('.agent/plan.json').write_text(json.dumps(plan, indent=2) + '\n')
320
341
  Path('.agent/active-slice.json').write_text(json.dumps(active, indent=2) + '\n')
321
342
  Path('.agent/verification-evidence.json').write_text(json.dumps({