@linimin/pi-letscook 0.1.66 → 0.1.68

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.
@@ -60,11 +60,11 @@ export type CookHandoffCapsule = {
60
60
  risks: string[];
61
61
  notes: string[];
62
62
  handoff_kind: "implementation_workflow_handoff";
63
- first_slice_goal: string;
63
+ first_slice_goal?: string;
64
64
  first_slice_non_goals: string[];
65
65
  implementation_surfaces: string[];
66
66
  verification_commands: string[];
67
- why_this_slice_first: string;
67
+ why_this_slice_first?: string;
68
68
  task_type?: string;
69
69
  evaluation_profile?: string;
70
70
  why_cook_now?: string;
@@ -1279,7 +1279,7 @@ function parseCookHandoffCapsulesFromText(
1279
1279
  const mission = deps.asString(parsed.mission);
1280
1280
  const firstSliceGoal = deps.asString(parsed.first_slice_goal ?? parsed.firstSliceGoal);
1281
1281
  const whyThisSliceFirst = deps.asString(parsed.why_this_slice_first ?? parsed.whyThisSliceFirst);
1282
- if (!mission || !firstSliceGoal || !whyThisSliceFirst) continue;
1282
+ if (!mission) continue;
1283
1283
  const scope = deps.asStringArray(parsed.scope);
1284
1284
  const constraints = deps.asStringArray(parsed.constraints);
1285
1285
  const nonGoals = deps.asStringArray(parsed.non_goals ?? parsed.nonGoals);
@@ -1325,12 +1325,12 @@ function buildCookHandoffBasisPreview(capsule: CookHandoffCapsule): string {
1325
1325
  ...capsule.constraints,
1326
1326
  ...capsule.non_goals,
1327
1327
  ...capsule.acceptance,
1328
- `first_slice_goal: ${capsule.first_slice_goal}`,
1329
- ...capsule.first_slice_non_goals.map((item) => `first_slice_non_goals: ${item}`),
1330
- ...capsule.implementation_surfaces.map((item) => `implementation_surfaces: ${item}`),
1331
- ...capsule.verification_commands.map((item) => `verification_commands: ${item}`),
1332
- `why_this_slice_first: ${capsule.why_this_slice_first}`,
1333
1328
  ];
1329
+ if (capsule.first_slice_goal) parts.push(`first_slice_goal: ${capsule.first_slice_goal}`);
1330
+ parts.push(...capsule.first_slice_non_goals.map((item) => `first_slice_non_goals: ${item}`));
1331
+ parts.push(...capsule.implementation_surfaces.map((item) => `implementation_surfaces: ${item}`));
1332
+ parts.push(...capsule.verification_commands.map((item) => `verification_commands: ${item}`));
1333
+ if (capsule.why_this_slice_first) parts.push(`why_this_slice_first: ${capsule.why_this_slice_first}`);
1334
1334
  if (capsule.why_cook_now) parts.push(`why_cook_now: ${capsule.why_cook_now}`);
1335
1335
  return parts.join("\n").trim();
1336
1336
  }
@@ -1363,23 +1363,32 @@ function cookHandoffStartabilityFailures(
1363
1363
  else if (!cookHandoffAcceptanceIsRepoChangeOriented(capsule)) {
1364
1364
  failures.push("acceptance is not anchored to concrete repo changes or verification");
1365
1365
  }
1366
- const firstSliceGoal = deps.normalizeMissionAnchorText(capsule.first_slice_goal);
1367
- if (!firstSliceGoal || deps.isWeakMissionAnchor(firstSliceGoal) || COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(firstSliceGoal)) {
1368
- failures.push("first_slice_goal is not a bounded implementation slice");
1369
- } else if (hasExplicitPlanningOnlyDeliverable([capsule.first_slice_goal]) || hasClearNoImplementationSignal([capsule.first_slice_goal])) {
1370
- failures.push("first_slice_goal is planning-only instead of a repo-change slice");
1366
+ if (capsule.first_slice_goal) {
1367
+ const firstSliceGoal = deps.normalizeMissionAnchorText(capsule.first_slice_goal);
1368
+ if (!firstSliceGoal || deps.isWeakMissionAnchor(firstSliceGoal) || COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(firstSliceGoal)) {
1369
+ failures.push("first_slice_goal is not a useful sequencing hint");
1370
+ } else if (hasExplicitPlanningOnlyDeliverable([capsule.first_slice_goal]) || hasClearNoImplementationSignal([capsule.first_slice_goal])) {
1371
+ failures.push("first_slice_goal is planning-only instead of a repo-change sequencing hint");
1372
+ }
1371
1373
  }
1372
- if (capsule.implementation_surfaces.length === 0) failures.push("implementation_surfaces is empty");
1373
- if (capsule.verification_commands.length === 0) failures.push("verification_commands is empty");
1374
1374
  return failures;
1375
1375
  }
1376
1376
 
1377
- function buildNonStartableCookHandoffMessage(failures: string[]): string {
1378
- return [
1379
- "/cook failed closed because a fresh explicit primary-agent handoff exists, but it is not concrete enough to start implementation workflow yet.",
1380
- "Tighten the handoff in the main chat so it names a bounded first implementation slice, repo-change-oriented acceptance, implementation_surfaces, and verification_commands, then rerun /cook.",
1381
- `Blocking details: ${failures.join("; ")}.`,
1382
- ].join(" ");
1377
+ function buildNonStartableCookHandoffMessage(
1378
+ failures: string[],
1379
+ context: "explicit_preview" | "same_entry_synthesis" = "explicit_preview",
1380
+ ): string {
1381
+ return context === "same_entry_synthesis"
1382
+ ? [
1383
+ "/cook failed closed because the same-entry primary-agent startup-plan synthesis step returned a startup plan that is still not concrete enough to seed workflow planning yet.",
1384
+ "Clarify the mission, scope, acceptance, or verification intent in the main chat, then rerun /cook so the primary agent can synthesize a tighter startup plan.",
1385
+ `Blocking details: ${failures.join("; ")}.`,
1386
+ ].join(" ")
1387
+ : [
1388
+ "/cook failed closed because a fresh explicit primary-agent startup plan exists, but it is not concrete enough to seed workflow planning yet.",
1389
+ "Tighten the startup plan in the main chat so it captures a concrete mission, repo-change-oriented acceptance, and truthful verification intent, then rerun /cook.",
1390
+ `Blocking details: ${failures.join("; ")}.`,
1391
+ ].join(" ");
1383
1392
  }
1384
1393
 
1385
1394
  function isStartableCookHandoffCapsule(
@@ -1435,11 +1444,11 @@ function buildContextProposalFromCookHandoffCapsule(
1435
1444
  evaluationProfile: capsule.evaluation_profile,
1436
1445
  critique: [
1437
1446
  ...capsule.notes,
1438
- `First slice goal: ${capsule.first_slice_goal}`,
1447
+ ...(capsule.first_slice_goal ? [`First slice goal: ${capsule.first_slice_goal}`] : []),
1439
1448
  ...(capsule.first_slice_non_goals.length > 0 ? [`First slice non-goals: ${capsule.first_slice_non_goals.join(" | ")}`] : []),
1440
1449
  ...(capsule.implementation_surfaces.length > 0 ? [`Implementation surfaces: ${capsule.implementation_surfaces.join(" | ")}`] : []),
1441
1450
  ...(capsule.verification_commands.length > 0 ? [`Verification commands: ${capsule.verification_commands.join(" | ")}`] : []),
1442
- `Why this slice first: ${capsule.why_this_slice_first}`,
1451
+ ...(capsule.why_this_slice_first ? [`Why this slice first: ${capsule.why_this_slice_first}`] : []),
1443
1452
  ...(capsule.why_cook_now ? [`Primary-agent /cook handoff rationale: ${capsule.why_cook_now}`] : []),
1444
1453
  ],
1445
1454
  risks: capsule.risks,
@@ -1455,11 +1464,11 @@ function buildContextProposalFromCookHandoffCapsule(
1455
1464
  ...capsule.scope,
1456
1465
  ...constraints,
1457
1466
  ...capsule.acceptance,
1458
- capsule.first_slice_goal,
1467
+ ...(capsule.first_slice_goal ? [capsule.first_slice_goal] : []),
1459
1468
  ...capsule.first_slice_non_goals,
1460
1469
  ...capsule.implementation_surfaces,
1461
1470
  ...capsule.verification_commands,
1462
- capsule.why_this_slice_first,
1471
+ ...(capsule.why_this_slice_first ? [capsule.why_this_slice_first] : []),
1463
1472
  ],
1464
1473
  ),
1465
1474
  goalText,
@@ -1470,6 +1479,32 @@ function buildContextProposalFromCookHandoffCapsule(
1470
1479
  return finalizeContextProposal(proposal, projectName, deps);
1471
1480
  }
1472
1481
 
1482
+ export function assessCookHandoffText(
1483
+ text: string,
1484
+ projectName: string,
1485
+ deps: ProposalParseDeps,
1486
+ options?: {
1487
+ messageId?: string;
1488
+ timestampMs?: number;
1489
+ context?: "explicit_preview" | "same_entry_synthesis";
1490
+ },
1491
+ ): CookHandoffProposalAssessment {
1492
+ const capsules = parseCookHandoffCapsulesFromText(text, options?.messageId, options?.timestampMs, deps);
1493
+ for (let capsuleIndex = capsules.length - 1; capsuleIndex >= 0; capsuleIndex -= 1) {
1494
+ const capsule = capsules[capsuleIndex];
1495
+ const failures = cookHandoffStartabilityFailures(capsule, deps);
1496
+ if (failures.length > 0) {
1497
+ return {
1498
+ status: "fresh_but_not_startable",
1499
+ message: buildNonStartableCookHandoffMessage(failures, options?.context ?? "explicit_preview"),
1500
+ };
1501
+ }
1502
+ const proposal = buildContextProposalFromCookHandoffCapsule(capsule, projectName, deps);
1503
+ if (proposal) return { status: "startable", proposal };
1504
+ }
1505
+ return { status: "none" };
1506
+ }
1507
+
1473
1508
  export function assessLatestCookHandoffProposal(
1474
1509
  recentMessages: RecentSessionMessage[],
1475
1510
  projectName: string,
@@ -1489,7 +1524,7 @@ export function assessLatestCookHandoffProposal(
1489
1524
  if (failures.length > 0) {
1490
1525
  return {
1491
1526
  status: "fresh_but_not_startable",
1492
- message: buildNonStartableCookHandoffMessage(failures),
1527
+ message: buildNonStartableCookHandoffMessage(failures, "explicit_preview"),
1493
1528
  };
1494
1529
  }
1495
1530
  const proposal = buildContextProposalFromCookHandoffCapsule(capsule, projectName, deps);
@@ -97,14 +97,18 @@ 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 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.",
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.",
105
110
  "Do not emit markdown commentary before or after the capsule.",
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.",
111
+ "If the task is not concrete enough for workflow startup, do not invent missing detail.",
108
112
  ].join(" ");
109
113
  const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
110
114
 
@@ -342,7 +346,8 @@ function buildPrimaryAgentHandoffPrompt(projectName: string, recentEntries: Rece
342
346
  lines.push(
343
347
  "",
344
348
  "Task:",
345
- "The user explicitly invoked /cook. Prepare the primary-agent handoff that /cook should consume immediately for Start/Cancel confirmation.",
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.",
346
351
  );
347
352
  return lines.join("\n");
348
353
  }
@@ -359,7 +364,7 @@ async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithA
359
364
  const invocation = getPiInvocation(args);
360
365
  const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
361
366
  liveActivity.progress = "Preparing primary-agent /cook handoff";
362
- liveActivity.currentAction = "Authoring explicit startup handoff from current task context";
367
+ liveActivity.currentAction = "Authoring explicit startup plan from current task context";
363
368
  liveActivity.assistantSummary = liveActivity.progress;
364
369
  try {
365
370
  const output = await new Promise<string | undefined>((resolve) => {
@@ -409,7 +414,7 @@ export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWi
409
414
  try {
410
415
  return await runPrimaryAgentHandoffSubprocess(params);
411
416
  } catch (error) {
412
- console.warn("[completion] primary-agent handoff generation failed", error);
417
+ console.warn("[completion] primary-agent startup-plan generation failed", error);
413
418
  return undefined;
414
419
  }
415
420
  }
@@ -3,6 +3,7 @@ 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";
6
7
  import type { CompletionStateSnapshot, JsonRecord } from "./types";
7
8
 
8
9
  const PROTOCOL_ID = "completion";
@@ -45,6 +46,8 @@ export function resolveFiles(root: string) {
45
46
  statePath: path.join(agentDir, "state.json"),
46
47
  planPath: path.join(agentDir, "plan.json"),
47
48
  activePath: path.join(agentDir, "active-slice.json"),
49
+ startupPlanPath: path.join(agentDir, "startup-plan.json"),
50
+ startupPlanMarkdownPath: path.join(agentDir, "startup-plan.md"),
48
51
  sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
49
52
  stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
50
53
  verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
@@ -141,6 +144,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
141
144
  const state = await readJson(files.statePath);
142
145
  const plan = await readJson(files.planPath);
143
146
  const active = await readJson(files.activePath);
147
+ const startupPlan = await readJson(files.startupPlanPath);
144
148
  const verificationEvidence = await readJson(files.verificationEvidencePath);
145
149
  return {
146
150
  files,
@@ -148,6 +152,7 @@ export async function loadCompletionSnapshot(startCwd: string): Promise<Completi
148
152
  state,
149
153
  plan,
150
154
  active,
155
+ startupPlan,
151
156
  verificationEvidence,
152
157
  activeSlice: findActiveSlice(plan, active),
153
158
  };
@@ -275,6 +280,32 @@ export function defaultPlan(
275
280
  };
276
281
  }
277
282
 
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
+
278
309
  export function defaultActiveSlice(
279
310
  missionAnchor: string,
280
311
  routing?: { taskType?: string; evaluationProfile?: string },
@@ -321,7 +352,7 @@ export function defaultVerificationEvidence(): JsonRecord {
321
352
  }
322
353
 
323
354
  export function buildAgentReadme(projectName: string): string {
324
- 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`;
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`;
325
356
  }
326
357
 
327
358
  export function buildMission(projectName: string, missionAnchor: string): string {
@@ -424,15 +455,23 @@ function trackedDiffFiles(fromCommit, toCommit) {
424
455
 
425
456
  const profile = readJson('.agent/profile.json');
426
457
  const state = readJson('.agent/state.json');
458
+ const startupPlan = readJson('.agent/startup-plan.json');
427
459
  const plan = readJson('.agent/plan.json');
428
460
  const active = readJson('.agent/active-slice.json');
429
461
  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
+ }
430
468
 
431
469
  ensureTrackedContractFiles();
432
470
 
433
471
  for (const [file, record] of [
434
472
  ['.agent/profile.json', profile],
435
473
  ['.agent/state.json', state],
474
+ ['.agent/startup-plan.json', startupPlan],
436
475
  ['.agent/plan.json', plan],
437
476
  ['.agent/active-slice.json', active],
438
477
  ]) {
@@ -443,12 +482,38 @@ for (const [file, record] of [
443
482
  const taskType = asString(profile.task_type);
444
483
  const evaluationProfile = asString(profile.evaluation_profile);
445
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');
446
486
  if (asString(plan.task_type) !== taskType) fail('.agent/plan.json task_type must match .agent/profile.json task_type');
447
487
  if (asString(active.task_type) !== taskType) fail('.agent/active-slice.json task_type must match .agent/profile.json task_type');
448
488
  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');
449
490
  if (asString(plan.evaluation_profile) !== evaluationProfile) fail('.agent/plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
450
491
  if (asString(active.evaluation_profile) !== evaluationProfile) fail('.agent/active-slice.json evaluation_profile must match .agent/profile.json evaluation_profile');
451
492
 
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
+
452
517
  if (asString(evidence.artifact_type) !== 'completion-verification-evidence') {
453
518
  fail('.agent/verification-evidence.json artifact_type must be completion-verification-evidence');
454
519
  }
@@ -595,7 +660,12 @@ export type ScaffoldResult = {
595
660
  export async function scaffoldCompletionFiles(
596
661
  root: string,
597
662
  missionAnchor: string,
598
- options?: { analysis?: { taskType?: string; evaluationProfile?: string }; continuationReason?: string; advisoryStartupBrief?: JsonRecord },
663
+ options?: {
664
+ analysis?: { taskType?: string; evaluationProfile?: string };
665
+ continuationReason?: string;
666
+ advisoryStartupBrief?: JsonRecord;
667
+ approvedStartupPlan?: JsonRecord;
668
+ },
599
669
  ): Promise<ScaffoldResult> {
600
670
  const files = resolveFiles(root);
601
671
  const created: string[] = [];
@@ -605,6 +675,10 @@ export async function scaffoldCompletionFiles(
605
675
  const projectName = path.basename(root);
606
676
  const docsSurfaces = await detectDocsSurfaces(root);
607
677
  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 });
608
682
  const trackedFiles: Array<{ path: string; content: string; executable?: boolean }> = [
609
683
  { path: path.join(files.agentDir, "README.md"), content: buildAgentReadme(projectName) },
610
684
  { path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
@@ -618,6 +692,14 @@ export async function scaffoldCompletionFiles(
618
692
  path: files.statePath,
619
693
  content: `${JSON.stringify(defaultState(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile, continuationReason: options?.continuationReason }, options?.advisoryStartupBrief), null, 2)}\n`,
620
694
  },
695
+ {
696
+ path: files.startupPlanPath,
697
+ content: `${JSON.stringify(startupPlanRecord, null, 2)}\n`,
698
+ },
699
+ {
700
+ path: files.startupPlanMarkdownPath,
701
+ content: buildApprovedStartupPlanMarkdown(startupPlanRecord as any),
702
+ },
621
703
  { path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
622
704
  { path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: options?.analysis?.taskType, evaluationProfile: options?.analysis?.evaluationProfile }), null, 2)}\n` },
623
705
  { path: files.verificationEvidencePath, content: `${JSON.stringify(defaultVerificationEvidence(), null, 2)}\n` },
@@ -639,6 +721,7 @@ export function currentTaskType(snapshot: CompletionStateSnapshot): string | und
639
721
  return (
640
722
  asString(snapshot.active?.task_type) ??
641
723
  asString(snapshot.state?.task_type) ??
724
+ asString(snapshot.startupPlan?.task_type) ??
642
725
  asString(snapshot.plan?.task_type) ??
643
726
  asString(snapshot.profile?.task_type)
644
727
  );
@@ -648,6 +731,7 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
648
731
  return (
649
732
  asString(snapshot.active?.evaluation_profile) ??
650
733
  asString(snapshot.state?.evaluation_profile) ??
734
+ asString(snapshot.startupPlan?.evaluation_profile) ??
651
735
  asString(snapshot.plan?.evaluation_profile) ??
652
736
  asString(snapshot.profile?.evaluation_profile)
653
737
  );
@@ -656,6 +740,7 @@ export function currentEvaluationProfile(snapshot: CompletionStateSnapshot): str
656
740
  export function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
657
741
  return (
658
742
  asString(snapshot.state?.mission_anchor) ??
743
+ asString(snapshot.startupPlan?.mission_anchor) ??
659
744
  asString(snapshot.plan?.mission_anchor) ??
660
745
  asString(snapshot.active?.mission_anchor) ??
661
746
  path.basename(snapshot.files.root)
@@ -18,6 +18,8 @@ export type CompletionFiles = {
18
18
  statePath: string;
19
19
  planPath: string;
20
20
  activePath: string;
21
+ startupPlanPath: string;
22
+ startupPlanMarkdownPath: string;
21
23
  sliceHistoryPath: string;
22
24
  stopHistoryPath: string;
23
25
  verificationEvidencePath: string;
@@ -30,6 +32,7 @@ export type CompletionStateSnapshot = {
30
32
  state?: JsonRecord;
31
33
  plan?: JsonRecord;
32
34
  active?: JsonRecord;
35
+ startupPlan?: JsonRecord;
33
36
  verificationEvidence?: JsonRecord;
34
37
  activeSlice?: JsonRecord;
35
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
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,
@@ -137,17 +137,16 @@ NODE
137
137
  ROOT="$TMPDIR/repo"
138
138
  PROMPT="$TMPDIR/resume-prompt.txt"
139
139
  BOOTSTRAP_SESSION="$TMPDIR/session-active-slice-bootstrap.jsonl"
140
- BOOTSTRAP_MESSAGES="$(python3 - <<'PY'
140
+ BOOTSTRAP_DISCUSSION=$'Prepare the active-slice contract bootstrap fixture and tell me when it is ready for /cook.'
141
+ GENERATED_HANDOFF="$(python3 - <<'PY'
141
142
  import json
142
143
  capsule = {
143
144
  "kind": "cook_handoff",
144
145
  "source": "primary_agent",
145
- "captured_at": "2026-01-01T00:00:02.000Z",
146
- "source_turn_id": "m0002",
147
146
  "mission": "Exercise active-slice contract parity.",
148
147
  "scope": [
149
148
  "Bootstrap canonical completion files for the active-slice contract fixture.",
150
- "Keep the fixture on the shipped explicit-handoff startup path."
149
+ "Keep the fixture on the shipped same-entry synthesis startup path."
151
150
  ],
152
151
  "constraints": [
153
152
  "Use supported bare /cook startup only."
@@ -157,7 +156,7 @@ capsule = {
157
156
  "Keep scripts/active-slice-contract-test.sh aligned with the packaged startup contract."
158
157
  ],
159
158
  "risks": [
160
- "Active-slice fixture bootstrap must stay anchored to the fresh explicit handoff."
159
+ "Active-slice fixture bootstrap must stay anchored to same-entry primary-agent startup-plan synthesis."
161
160
  ],
162
161
  "notes": [
163
162
  "This handoff exists only to scaffold canonical files before the fixture rewrites them for contract parity coverage."
@@ -179,20 +178,16 @@ capsule = {
179
178
  "evaluation_profile": "completion-rubric-v1",
180
179
  "why_cook_now": "The fixture bootstrap is concrete enough to scaffold canonical control-plane files."
181
180
  }
182
- messages = [
183
- {"role": "user", "content": "Prepare the active-slice contract bootstrap fixture and tell me when it is ready for /cook."},
184
- {"role": "assistant", "content": "The active-slice contract bootstrap fixture is ready for /cook. Run /cook to confirm it.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
185
- ]
186
- print(json.dumps(messages, ensure_ascii=False))
181
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
187
182
  PY
188
183
  )"
189
184
  mkdir -p "$ROOT"
190
185
  cd "$ROOT"
191
186
  git init -q
192
- write_session_messages "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_MESSAGES"
187
+ write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"
193
188
 
194
189
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
195
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
190
+ PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$GENERATED_HANDOFF" \
196
191
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
197
192
  pi --session "$BOOTSTRAP_SESSION" -e "$PKG_ROOT" -p "/cook" \
198
193
  >"$TMPDIR/pi-active-slice-bootstrap.out" 2>"$TMPDIR/pi-active-slice-bootstrap.err"
@@ -134,11 +134,11 @@ assertIncludes('.agent/README.md', 'durable canonical record of deterministic ve
134
134
  assertSectionIncludes('skills/completion-protocol/SKILL.md', '## Canonical Files', '- `.agent/verification-evidence.json`');
135
135
  assertSectionIncludes('skills/completion-protocol/SKILL.md', '## Canonical Inputs', '- `.agent/verification-evidence.json`');
136
136
  assertSectionIncludes('skills/completion-protocol/SKILL.md', '## Compaction And Recovery', '- `.agent/verification-evidence.json`');
137
- assertSectionIncludes('skills/completion-protocol/SKILL.md', '## Compaction And Recovery', '`completion-implementer` must also re-read canonical `.agent/state.json`, `.agent/plan.json`, `.agent/active-slice.json`, and `.agent/verification-evidence.json` before resuming work.');
137
+ assertSectionIncludes('skills/completion-protocol/SKILL.md', '## Compaction And Recovery', '`completion-implementer` must also re-read canonical `.agent/state.json`, `.agent/startup-plan.json`, `.agent/plan.json`, `.agent/active-slice.json`, and `.agent/verification-evidence.json` before resuming work.');
138
138
  assertSectionIncludes('skills/completion-protocol/references/completion.md', '## Ignored Canonical Execution State', '- `.agent/verification-evidence.json`');
139
139
  assertSectionIncludes('skills/completion-protocol/references/completion.md', '## Canonical Inputs', '- `.agent/verification-evidence.json`');
140
140
  assertSectionIncludes('skills/completion-protocol/references/completion.md', '## Compaction And Recovery', '- `.agent/verification-evidence.json`');
141
- assertSectionIncludes('skills/completion-protocol/references/completion.md', '## Compaction And Recovery', '`completion-implementer` must also re-read canonical `.agent/state.json`, `.agent/plan.json`, `.agent/active-slice.json`, and `.agent/verification-evidence.json` before resuming work.');
141
+ assertSectionIncludes('skills/completion-protocol/references/completion.md', '## Compaction And Recovery', '`completion-implementer` must also re-read canonical `.agent/state.json`, `.agent/startup-plan.json`, `.agent/plan.json`, `.agent/active-slice.json`, and `.agent/verification-evidence.json` before resuming work.');
142
142
  assertIncludes('extensions/completion/prompt-surfaces.ts', 'Verification evidence artifact: ${args.evidence.path} (${args.evidence.status})');
143
143
  assertIncludes('extensions/completion/prompt-surfaces.ts', 'Verification evidence summary: ${args.evidence.summary}');
144
144
  assertIncludes('extensions/completion/index.ts', 'Canonical verification evidence artifact is currently: ${evidence.path} (${evidence.status})');
@@ -182,17 +182,16 @@ bash .agent/verify_completion_control_plane.sh >/dev/null
182
182
  ROOT="$TMPDIR/repo"
183
183
  SYSTEM_REMINDER="$TMPDIR/system-reminder.txt"
184
184
  BOOTSTRAP_SESSION="$TMPDIR/session-canonical-evidence-bootstrap.jsonl"
185
- BOOTSTRAP_MESSAGES="$(python3 - <<'PY'
185
+ BOOTSTRAP_DISCUSSION=$'Prepare the canonical evidence bootstrap fixture and tell me when it is ready for /cook.'
186
+ GENERATED_HANDOFF="$(python3 - <<'PY'
186
187
  import json
187
188
  capsule = {
188
189
  "kind": "cook_handoff",
189
190
  "source": "primary_agent",
190
- "captured_at": "2026-01-01T00:00:02.000Z",
191
- "source_turn_id": "m0002",
192
191
  "mission": "Exercise canonical evidence fixture bootstrap.",
193
192
  "scope": [
194
193
  "Materialize canonical completion files for the evidence artifact fixture.",
195
- "Keep the verification-evidence bootstrap on the supported explicit-handoff startup path."
194
+ "Keep the verification-evidence bootstrap on the supported same-entry synthesis startup path."
196
195
  ],
197
196
  "constraints": [
198
197
  "Use supported bare /cook startup only."
@@ -202,7 +201,7 @@ capsule = {
202
201
  "Keep scripts/canonical-evidence-artifact-test.sh aligned with packaged bootstrap behavior."
203
202
  ],
204
203
  "risks": [
205
- "Evidence-artifact bootstrap must stay anchored to the fresh explicit handoff."
204
+ "Evidence-artifact bootstrap must stay anchored to same-entry primary-agent startup-plan synthesis."
206
205
  ],
207
206
  "notes": [
208
207
  "This fixture exists only to scaffold canonical files before rewriting them for evidence parity coverage."
@@ -224,25 +223,21 @@ capsule = {
224
223
  "evaluation_profile": "completion-rubric-v1",
225
224
  "why_cook_now": "The fixture bootstrap is concrete enough to create canonical control-plane files."
226
225
  }
227
- messages = [
228
- {"role": "user", "content": "Prepare the canonical evidence bootstrap fixture and tell me when it is ready for /cook."},
229
- {"role": "assistant", "content": "The canonical evidence bootstrap fixture is ready for /cook. Run /cook to confirm it.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
230
- ]
231
- print(json.dumps(messages, ensure_ascii=False))
226
+ print("```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```")
232
227
  PY
233
228
  )"
234
229
  mkdir -p "$ROOT"
235
230
  cd "$ROOT"
236
231
  git init -q
237
- write_session_messages "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_MESSAGES"
232
+ write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"
238
233
 
239
234
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
240
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
235
+ PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT="$GENERATED_HANDOFF" \
241
236
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
242
237
  pi --session "$BOOTSTRAP_SESSION" -e "$PKG_ROOT" -p "/cook" \
243
238
  >"$TMPDIR/pi-canonical-evidence-bootstrap.out" 2>"$TMPDIR/pi-canonical-evidence-bootstrap.err"
244
239
 
245
- for file in .agent/profile.json .agent/state.json .agent/plan.json .agent/active-slice.json .agent/verification-evidence.json; do
240
+ for file in .agent/profile.json .agent/state.json .agent/startup-plan.json .agent/startup-plan.md .agent/plan.json .agent/active-slice.json .agent/verification-evidence.json; do
246
241
  [[ -f "$file" ]] || { echo "missing canonical bootstrap file: $file" >&2; exit 1; }
247
242
  done
248
243
 
@@ -288,6 +283,36 @@ acceptance = [
288
283
  'Canonical verification evidence is recorded for the selected slice.',
289
284
  'Fail-closed verification rejects missing or stale evidence.',
290
285
  ]
286
+ startup_plan = {
287
+ 'schema_version': 1,
288
+ 'artifact_type': 'completion-startup-plan',
289
+ 'status': 'approved',
290
+ 'source': 'deferred_primary_agent_handoff',
291
+ 'captured_at': '2026-05-03T00:00:00Z',
292
+ 'mission_anchor': mission,
293
+ 'goal_text': 'Mission: Exercise canonical verification evidence parity.\n\nScope:\n- Persist canonical verification evidence for the selected slice.\n- Keep the verifier fail-closed on stale or missing evidence.\n\nAcceptance:\n- Canonical verification evidence is recorded for the selected slice.\n- Fail-closed verification rejects missing or stale evidence.',
294
+ 'task_type': task_type,
295
+ 'evaluation_profile': evaluation_profile,
296
+ 'scope': [
297
+ 'Persist canonical verification evidence for the selected slice.',
298
+ 'Keep the verifier fail-closed on stale or missing evidence.',
299
+ ],
300
+ 'constraints': [
301
+ 'Keep the fixture scoped to canonical verification evidence parity.',
302
+ ],
303
+ 'acceptance': acceptance,
304
+ 'risks': [
305
+ 'Stale startup-plan parity could mask canonical evidence regressions.',
306
+ ],
307
+ 'notes': [
308
+ 'Use startup-plan parity to prove the verifier reads the approved startup plan alongside other canonical state.',
309
+ ],
310
+ 'planned_surfaces': implementation_surfaces,
311
+ 'verification_intent': verification_commands,
312
+ 'sequencing_hints': [
313
+ 'First slice goal: Persist canonical verification evidence for the selected slice.',
314
+ ],
315
+ }
291
316
  state = {
292
317
  'schema_version': 1,
293
318
  'mission_anchor': mission,
@@ -365,6 +390,21 @@ active = {
365
390
  }
366
391
 
367
392
  Path('.agent/state.json').write_text(json.dumps(state, indent=2) + '\n')
393
+ Path('.agent/startup-plan.json').write_text(json.dumps(startup_plan, indent=2) + '\n')
394
+ Path('.agent/startup-plan.md').write_text(
395
+ '# Approved Startup Plan\n\n'
396
+ f'Mission anchor: {mission}\n'
397
+ 'Source: deferred_primary_agent_handoff\n'
398
+ 'Captured at: 2026-05-03T00:00:00Z\n'
399
+ f'Task type: {task_type}\n'
400
+ f'Evaluation profile: {evaluation_profile}\n\n'
401
+ '## Goal\n\n'
402
+ f"{startup_plan['goal_text']}\n\n"
403
+ '## Planned surfaces\n\n'
404
+ + ''.join(f'- {item}\n' for item in startup_plan['planned_surfaces'])
405
+ + '\n## Verification intent\n\n'
406
+ + ''.join(f'- {item}\n' for item in startup_plan['verification_intent'])
407
+ )
368
408
  Path('.agent/plan.json').write_text(json.dumps(plan, indent=2) + '\n')
369
409
  Path('.agent/active-slice.json').write_text(json.dumps(active, indent=2) + '\n')
370
410
  PY