@linimin/pi-letscook 0.1.57 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,12 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.58
6
+
7
+ ### Changed
8
+
9
+ - tightened implementation-ready explicit `/cook` handoffs so fresh capsules must already carry a bounded first slice, repo-change-oriented acceptance, implementation surfaces, verification commands, and why-that-slice-first structure before workflow startup
10
+ - made fresh explicit but non-startable `/cook` handoffs fail closed with a dedicated operator message instead of falling back to broader recent discussion or silently drifting into planning
11
+ - expanded regressions and public parity so valid, vague, stale, done-workflow, and negative explicit-handoff cases stay truthful across runtime behavior, docs, and `npm run release-check`
12
+
5
13
  ## 0.1.57
6
14
 
7
15
  ### Changed
8
16
 
9
- - made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh and valid
10
- - kept context-derived startup as a fallback only, so stale, drifted, or non-startable handoff capsules still fail closed or fall back to recent discussion instead of silently rewriting canonical state
17
+ - made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh, valid, and implementation-startable
18
+ - tightened implementation-ready explicit handoffs so the structured capsule must already carry a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first` before `/cook` will start workflow from it
19
+ - kept the pre-`/cook` handoff capsule as advisory startup intake only, not canonical `.agent/**` workflow state, while still using context-derived startup as the fallback only when no fresh explicit handoff is blocking startup
20
+ - kept context-derived startup as a fallback only when there is no fresh explicit handoff blocking startup, so stale or invalidated capsules can still fall back to recent discussion while fresh non-startable handoffs fail closed instead of silently rewriting canonical state
11
21
  - made finished-workflow suppression stay a safety layer instead of a replacement mission when a fresh explicit `/cook` handoff exists, and blocked negative rejection/suppression text from becoming a Startable startup mission
12
22
  - removed inline `/cook` arguments from the shipped entry path again so explicit bare `/cook` is the only public command, and fail closed when recent discussion is insufficient or unreliable
13
23
  - added a pre-`/cook` ordinary-chat handoff boundary so the primary agent is instructed to stop at `/cook` once a task has matured into completion-workflow scope instead of starting long-running implementation directly in ordinary chat
package/README.md CHANGED
@@ -50,13 +50,16 @@ Then run `/reload` in Pi.
50
50
  ## What `/cook` expects
51
51
 
52
52
  - preferably a fresh explicit primary-agent `/cook` handoff capsule from the immediately preceding ordinary-chat turn
53
- - otherwise recent main-chat discussion about concrete repo changes
53
+ - for that handoff capsule to start workflow immediately, it must already be implementation-startable: a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first`
54
+ - otherwise, when no fresh explicit handoff is blocking startup, recent main-chat discussion about concrete repo changes
54
55
  - enough detail to derive a startup brief with mission, scope, constraints or non-goals, acceptance, and notes or risks
55
56
  - README/CHANGELOG updates still count as concrete repo changes
56
57
  - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they include the explicit structured `/cook` handoff capsule
57
58
 
58
59
  If no fresh valid handoff exists and recent discussion is missing, weak, ambiguous, assistant-produced, or only describes planning artifacts instead of concrete repo changes, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to clarify the mission in the main chat before rerunning `/cook`.
59
60
 
61
+ If a fresh explicit handoff exists but is still workflow-worthy rather than implementation-startable, `/cook` also fails closed instead of silently treating that capsule as planning support or canonical workflow state.
62
+
60
63
  If you pass inline arguments to `/cook`, it also fails closed and tells you to move that intent into the main chat before rerunning bare `/cook`.
61
64
 
62
65
  ## Workflow entry
@@ -67,6 +70,8 @@ If a task has clearly matured into completion-workflow scope, the primary agent
67
70
 
68
71
  That handoff should include an explicit structured `/cook` capsule in the assistant reply so `/cook` can confirm the already-formed mission instead of re-deriving it from broad ambient context.
69
72
 
73
+ The preferred capsule is still advisory startup intake, not canonical workflow state, and it only counts as implementation-ready when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.
74
+
70
75
  Important behavior:
71
76
  - `/cook` is the canonical workflow boundary and manual entry point
72
77
  - startup, refocus, and next-round routing stay confirm-first; nothing silently starts a workflow
@@ -84,13 +89,13 @@ I want to add login redirect handling and tests.
84
89
 
85
90
  ## What happens when you run `/cook`
86
91
 
87
- `/cook` first looks for a fresh explicit primary-agent handoff capsule. If one is valid, `/cook` builds the startup brief from that handoff and only uses recent discussion as validation or supplemental notes. If no valid handoff exists, `/cook` falls back to deriving a startup brief from recent discussion before showing the existing approval-only Start/Cancel gate.
92
+ `/cook` first looks for a fresh explicit primary-agent handoff capsule. If one is valid and implementation-startable, `/cook` builds the startup brief from that handoff and only uses recent discussion as validation or supplemental notes. `/cook` falls back to deriving a startup brief from recent discussion only when no fresh explicit handoff is blocking startup—for example, when there is no fresh capsule or only stale or invalidated capsules—before showing the existing approval-only Start/Cancel gate.
88
93
 
89
94
  | Repo state | What you'll see |
90
95
  |---|---|
91
- | No workflow yet | If a fresh explicit handoff capsule exists, a startup brief built from that handoff. Otherwise a startup brief built from recent main-chat discussion. You choose **Start** or **Cancel**. Weak, unreliable, stale, or planning-only intake fails closed. |
96
+ | No workflow yet | If a fresh explicit handoff capsule exists and is implementation-startable, a startup brief built from that handoff. Otherwise, when no fresh explicit handoff is blocking startup, a startup brief built from recent main-chat discussion. You choose **Start** or **Cancel**. Weak, unreliable, stale, planning-only, or non-startable explicit-handoff intake fails closed. |
92
97
  | Active workflow exists | Usually a resume of the current workflow. If a fresh explicit handoff capsule or recent discussion clearly points to a different concrete repo change, `/cook` shows a chooser first and only rewrites canonical state after you confirm the new startup brief. Ambiguous intake stays conservative. |
93
- | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without that, `/cook` falls back to recent discussion. Discussion that only restates already-finished work still fails closed. |
98
+ | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without a fresh explicit handoff blocking startup, `/cook` can fall back to recent discussion. Discussion that only restates already-finished work still fails closed. |
94
99
 
95
100
  ## Confirmation and fail-closed behavior
96
101
 
@@ -38,7 +38,7 @@ type ContextProposalAlternate = {
38
38
  analysis: ContextProposalAnalysis;
39
39
  goalText: string;
40
40
  basisPreview: string;
41
- source: "session" | "analyst";
41
+ source: "session" | "analyst" | "handoff_capsule";
42
42
  };
43
43
 
44
44
  type ContextProposal = ContextProposalAlternate & {
@@ -55,11 +55,22 @@ type ExistingWorkflowDecision =
55
55
  | { action: "continue"; currentMissionAnchor: string }
56
56
  | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string; proposal: ContextProposal };
57
57
 
58
+ type CookContextProposalResult = {
59
+ proposal?: ContextProposal;
60
+ blockedFailureMessage?: string;
61
+ };
62
+
58
63
  type ActiveWorkflowProposalAssessment = {
59
- action: "continue" | "refocus" | "unclear";
64
+ action: "continue" | "refocus" | "unclear" | "blocked";
60
65
  currentMissionAnchor: string;
61
66
  proposal?: ContextProposal;
62
- reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
67
+ blockedFailureMessage?: string;
68
+ reason:
69
+ | "matching_mission"
70
+ | "clear_refocus"
71
+ | "missing_proposal"
72
+ | "ambiguous_discussion"
73
+ | "fresh_explicit_handoff_not_startable";
63
74
  };
64
75
 
65
76
  type ExistingWorkflowChooserOptions = {
@@ -110,7 +121,7 @@ export type CompletionDriverDeps = {
110
121
  missionAnchor?: string,
111
122
  ) => string;
112
123
  completionResumePrompt: (taskType: string, evaluationProfile: string) => string;
113
- deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<ContextProposal | undefined>;
124
+ deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<CookContextProposalResult>;
114
125
  confirmContextProposal: (
115
126
  ctx: { hasUI: boolean; ui: any },
116
127
  proposal: ContextProposal,
@@ -305,7 +316,18 @@ async function assessActiveWorkflowProposalRouting(
305
316
  ): Promise<ActiveWorkflowProposalAssessment> {
306
317
  const currentMission = currentMissionAnchor(snapshot);
307
318
  const projectName = path.basename(snapshot.files.root);
308
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
319
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
320
+ if (derived.blockedFailureMessage) {
321
+ const assessment: ActiveWorkflowProposalAssessment = {
322
+ action: "blocked",
323
+ currentMissionAnchor: currentMission,
324
+ blockedFailureMessage: derived.blockedFailureMessage,
325
+ reason: "fresh_explicit_handoff_not_startable",
326
+ };
327
+ deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
328
+ return assessment;
329
+ }
330
+ const proposal = derived.proposal;
309
331
  if (!proposal) {
310
332
  const assessment: ActiveWorkflowProposalAssessment = {
311
333
  action: "unclear",
@@ -519,7 +541,12 @@ export async function runCookEntry(
519
541
  if (!snapshot) {
520
542
  const root = findRepoRoot(cwd) ?? cwd;
521
543
  const projectName = path.basename(root);
522
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
544
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
545
+ if (derived.blockedFailureMessage) {
546
+ deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
547
+ return;
548
+ }
549
+ const proposal = derived.proposal;
523
550
  if (!proposal) {
524
551
  deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps), "info");
525
552
  return;
@@ -559,7 +586,12 @@ export async function runCookEntry(
559
586
  if (!goal) {
560
587
  if (workflowDone) {
561
588
  const projectName = path.basename(snapshot.files.root);
562
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
589
+ const derived = await deps.deriveCookContextProposal(ctx, projectName);
590
+ if (derived.blockedFailureMessage) {
591
+ deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
592
+ return;
593
+ }
594
+ const proposal = derived.proposal;
563
595
  if (!proposal) {
564
596
  deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps, "The previous completion workflow is already done."), "info");
565
597
  return;
@@ -586,6 +618,10 @@ export async function runCookEntry(
586
618
  deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
587
619
  } else {
588
620
  const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
621
+ if (assessment.action === "blocked") {
622
+ deps.emitCommandText(ctx, assessment.blockedFailureMessage ?? buildCookStructuredDiscussionFailureMessage(deps), "info");
623
+ return;
624
+ }
589
625
  if (!assessment.proposal || assessment.action === "continue") {
590
626
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
591
627
  return;
@@ -18,7 +18,7 @@ import {
18
18
  collectRecentDiscussionEntries,
19
19
  collectRecentSessionMessages,
20
20
  deriveCookContextProposalFromRecentDiscussion,
21
- extractLatestCookHandoffProposal,
21
+ assessLatestCookHandoffProposal,
22
22
  finalizeContextProposalAnalysis,
23
23
  isWeakMissionAnchor,
24
24
  missionAnchorsLikelyEquivalent,
@@ -122,11 +122,22 @@ function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
122
122
  return Array.isArray(slices) ? slices.filter(isRecord) : [];
123
123
  }
124
124
 
125
+ type CookContextProposalResult = {
126
+ proposal?: ContextProposal;
127
+ blockedFailureMessage?: string;
128
+ };
129
+
125
130
  type ActiveWorkflowProposalAssessment = {
126
- action: "continue" | "refocus" | "unclear";
131
+ action: "continue" | "refocus" | "unclear" | "blocked";
127
132
  currentMissionAnchor: string;
128
133
  proposal?: ContextProposal;
129
- reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
134
+ blockedFailureMessage?: string;
135
+ reason:
136
+ | "matching_mission"
137
+ | "clear_refocus"
138
+ | "missing_proposal"
139
+ | "ambiguous_discussion"
140
+ | "fresh_explicit_handoff_not_startable";
130
141
  };
131
142
 
132
143
  function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
@@ -271,6 +282,7 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
271
282
  action: assessment.action,
272
283
  reason: assessment.reason,
273
284
  currentMissionAnchor: assessment.currentMissionAnchor,
285
+ blockedFailureMessage: assessment.blockedFailureMessage ?? null,
274
286
  proposedMissionAnchor: assessment.proposal?.mission ?? null,
275
287
  proposalSource: assessment.proposal?.source ?? null,
276
288
  possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
@@ -365,7 +377,7 @@ async function promptContextProposalConfirmationAction(
365
377
  async function deriveCookContextProposal(
366
378
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
367
379
  projectName: string,
368
- ): Promise<ContextProposal | undefined> {
380
+ ): Promise<CookContextProposalResult> {
369
381
  const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
370
382
  const recentEntries = recentMessages
371
383
  .filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
@@ -384,7 +396,7 @@ async function deriveCookContextProposal(
384
396
  `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
385
397
  ]
386
398
  : [];
387
- const explicitHandoff = extractLatestCookHandoffProposal(recentMessages, projectName, {
399
+ const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
388
400
  asString,
389
401
  asStringArray,
390
402
  assessMissionAnchor,
@@ -393,42 +405,47 @@ async function deriveCookContextProposal(
393
405
  missionAnchorsStrictlyEquivalent,
394
406
  stripCodeBlocks,
395
407
  });
396
- if (explicitHandoff) return explicitHandoff;
397
- return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
398
- asString,
399
- asStringArray,
400
- workflowContext: snapshot
401
- ? {
402
- currentMissionAnchor:
403
- asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
404
- latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
405
- latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
406
- activeSliceGoal: asString(snapshot.active?.goal),
407
- activeSliceWhyNow: asString(snapshot.active?.why_now),
408
- verificationGoal: asString(snapshot.verificationEvidence?.goal),
409
- verificationSummary: asString(snapshot.verificationEvidence?.summary),
410
- continuationPolicy: asString(snapshot.state?.continuation_policy),
411
- }
412
- : undefined,
413
- analyzeContextProposal: async (entries) =>
414
- await analyzeContextProposalWithAgent({
415
- ctx,
416
- projectName,
417
- recentEntries: entries,
418
- workflowContextLines,
419
- liveRoleActivityByRoot,
420
- completionStatusKey: COMPLETION_STATUS_KEY,
421
- safeUiCall,
422
- getCtxCwd,
423
- getCtxHasUI,
424
- getCtxUi,
425
- }),
426
- assessMissionAnchor,
427
- isWeakMissionAnchor,
428
- missionAnchorsStrictlyEquivalent,
429
- normalizeMissionAnchorText,
430
- stripCodeBlocks,
431
- });
408
+ if (explicitHandoff.status === "startable") return { proposal: explicitHandoff.proposal };
409
+ if (explicitHandoff.status === "fresh_but_not_startable") {
410
+ return { blockedFailureMessage: explicitHandoff.message };
411
+ }
412
+ return {
413
+ proposal: await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
414
+ asString,
415
+ asStringArray,
416
+ workflowContext: snapshot
417
+ ? {
418
+ currentMissionAnchor:
419
+ asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
420
+ latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
421
+ latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
422
+ activeSliceGoal: asString(snapshot.active?.goal),
423
+ activeSliceWhyNow: asString(snapshot.active?.why_now),
424
+ verificationGoal: asString(snapshot.verificationEvidence?.goal),
425
+ verificationSummary: asString(snapshot.verificationEvidence?.summary),
426
+ continuationPolicy: asString(snapshot.state?.continuation_policy),
427
+ }
428
+ : undefined,
429
+ analyzeContextProposal: async (entries) =>
430
+ await analyzeContextProposalWithAgent({
431
+ ctx,
432
+ projectName,
433
+ recentEntries: entries,
434
+ workflowContextLines,
435
+ liveRoleActivityByRoot,
436
+ completionStatusKey: COMPLETION_STATUS_KEY,
437
+ safeUiCall,
438
+ getCtxCwd,
439
+ getCtxHasUI,
440
+ getCtxUi,
441
+ }),
442
+ assessMissionAnchor,
443
+ isWeakMissionAnchor,
444
+ missionAnchorsStrictlyEquivalent,
445
+ normalizeMissionAnchorText,
446
+ stripCodeBlocks,
447
+ }),
448
+ };
432
449
  }
433
450
 
434
451
  async function confirmContextProposal(
@@ -31,8 +31,11 @@ export function buildCookHandoffBoundaryReminder(): string {
31
31
  "/cook is the only explicit entrypoint into long-running completion workflow.",
32
32
  "When you judge that the task has matured into completion-workflow scope — for example the user has clearly shifted from exploration into implementation intent, you have just produced a concrete plan or proposal whose next step would naturally be implementation, or the task spans multiple files, steps, or verification surfaces — stop short of long-running implementation and tell the user to run /cook.",
33
33
  "At that handoff point, do not begin long-running product implementation in ordinary chat, do not edit tracked product files for that workflow-level task, and do not act as though /cook had already been invoked.",
34
+ "Distinguish a workflow-worthy handoff from an implementation-ready handoff: only emit the implementation-ready capsule when the first bounded implementation slice is concrete enough to start immediately.",
34
35
  "When handing off, explain that /cook will first look for a fresh explicit primary-agent handoff capsule and otherwise fall back to recent discussion.",
35
- "Also append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON with kind/source/handoff_kind plus mission, scope, constraints or non_goals, acceptance, risks, notes, captured_at, source_turn_id, and optional task_type/evaluation_profile/why_cook_now.",
36
+ "If the task is workflow-worthy but that first slice is still vague, tell the user to run /cook without emitting an implementation-ready capsule yet.",
37
+ "Otherwise append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON with kind/source/handoff_kind plus mission, scope, constraints or non_goals, acceptance, risks, notes, captured_at, source_turn_id, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, why_this_slice_first, and optional task_type/evaluation_profile/why_cook_now.",
38
+ "Use handoff_kind implementation_workflow_handoff for that implementation-ready capsule.",
36
39
  "The capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
37
40
  "If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
38
41
  ].join(" ");
@@ -59,12 +59,30 @@ export type CookHandoffCapsule = {
59
59
  acceptance: string[];
60
60
  risks: string[];
61
61
  notes: string[];
62
- handoff_kind: "implementation_workflow_ready";
62
+ handoff_kind: "implementation_workflow_handoff";
63
+ first_slice_goal: string;
64
+ first_slice_non_goals: string[];
65
+ implementation_surfaces: string[];
66
+ verification_commands: string[];
67
+ why_this_slice_first: string;
63
68
  task_type?: string;
64
69
  evaluation_profile?: string;
65
70
  why_cook_now?: string;
66
71
  };
67
72
 
73
+ export type CookHandoffProposalAssessment =
74
+ | {
75
+ status: "none";
76
+ }
77
+ | {
78
+ status: "startable";
79
+ proposal: ContextProposal;
80
+ }
81
+ | {
82
+ status: "fresh_but_not_startable";
83
+ message: string;
84
+ };
85
+
68
86
  export type ContextProposalDecision = {
69
87
  missionAnchor: string;
70
88
  goalText: string;
@@ -1231,6 +1249,12 @@ const COOK_HANDOFF_MAX_AGE_MS = 45 * 60 * 1000;
1231
1249
  const COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES = 2;
1232
1250
  const COOK_HANDOFF_NEGATIVE_MISSION_REGEX =
1233
1251
  /(?:\b(?:do not|don't|dont|not|never|avoid|skip|refuse|recognize that|suppress|ignore|block|prevent)\b|(?:不要|別|别|勿|禁止|避免|忽略|阻止))/iu;
1252
+ const COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX =
1253
+ /(?:\b(?:confirm|discuss|clarify|decide|review|align(?: on)?|agree(?: on)?|explain|summari(?:s|z)e|describe|plan|proposal|spec(?:ification)?|design(?: doc(?:ument)?)?|next step|handoff|workflow|readiness)\b|(?:確認|确认|討論|讨论|釐清|厘清|決定|决定|審查|审查|對齊|对齐|同意|说明|說明|總結|总结|描述|規劃|规划|提案|方案|工作流|就緒|就绪))/iu;
1254
+ const COOK_HANDOFF_VERIFICATION_ACCEPTANCE_REGEX =
1255
+ /(?:\b(?:test|tests|testing|verify|verification|validated?|regression|coverage|assert(?:ion)?s?|check|checks|smoke|snapshot(?:s)?)\b|(?:測試|测试|驗證|验证|回歸|回归|覆蓋|覆盖|斷言|断言|檢查|检查|快照))/iu;
1256
+ const COOK_HANDOFF_VERIFICATION_ACTION_REGEX =
1257
+ /(?:\b(?:add|update|keep|run|rerun|cover|verify|validate|check|assert|exercise|prove)\b|(?:新增|更新|保持|執行|执行|重跑|覆蓋|覆盖|驗證|验证|檢查|检查|斷言|断言|證明|证明))/iu;
1234
1258
 
1235
1259
  function parseCookHandoffCapsulesFromText(
1236
1260
  text: string,
@@ -1251,15 +1275,20 @@ function parseCookHandoffCapsulesFromText(
1251
1275
  if (!localIsRecord(parsed)) continue;
1252
1276
  if (deps.asString(parsed.kind) !== "cook_handoff") continue;
1253
1277
  if (deps.asString(parsed.source) !== "primary_agent") continue;
1254
- if (deps.asString(parsed.handoff_kind) !== "implementation_workflow_ready") continue;
1278
+ if (deps.asString(parsed.handoff_kind) !== "implementation_workflow_handoff") continue;
1255
1279
  const mission = deps.asString(parsed.mission);
1256
- if (!mission) continue;
1280
+ const firstSliceGoal = deps.asString(parsed.first_slice_goal ?? parsed.firstSliceGoal);
1281
+ const whyThisSliceFirst = deps.asString(parsed.why_this_slice_first ?? parsed.whyThisSliceFirst);
1282
+ if (!mission || !firstSliceGoal || !whyThisSliceFirst) continue;
1257
1283
  const scope = deps.asStringArray(parsed.scope);
1258
1284
  const constraints = deps.asStringArray(parsed.constraints);
1259
1285
  const nonGoals = deps.asStringArray(parsed.non_goals ?? parsed.nonGoals);
1260
1286
  const acceptance = deps.asStringArray(parsed.acceptance);
1261
1287
  const risks = deps.asStringArray(parsed.risks);
1262
1288
  const notes = deps.asStringArray(parsed.notes);
1289
+ const firstSliceNonGoals = deps.asStringArray(parsed.first_slice_non_goals ?? parsed.firstSliceNonGoals);
1290
+ const implementationSurfaces = deps.asStringArray(parsed.implementation_surfaces ?? parsed.implementationSurfaces);
1291
+ const verificationCommands = deps.asStringArray(parsed.verification_commands ?? parsed.verificationCommands);
1263
1292
  const capturedAt = deps.asString(parsed.captured_at) ?? (timestampMs ? new Date(timestampMs).toISOString() : undefined);
1264
1293
  const sourceTurnId = deps.asString(parsed.source_turn_id) ?? messageId;
1265
1294
  if (!capturedAt || !sourceTurnId) continue;
@@ -1275,7 +1304,12 @@ function parseCookHandoffCapsulesFromText(
1275
1304
  acceptance,
1276
1305
  risks,
1277
1306
  notes,
1278
- handoff_kind: "implementation_workflow_ready",
1307
+ handoff_kind: "implementation_workflow_handoff",
1308
+ first_slice_goal: firstSliceGoal,
1309
+ first_slice_non_goals: firstSliceNonGoals,
1310
+ implementation_surfaces: implementationSurfaces,
1311
+ verification_commands: verificationCommands,
1312
+ why_this_slice_first: whyThisSliceFirst,
1279
1313
  task_type: deps.asString(parsed.task_type),
1280
1314
  evaluation_profile: deps.asString(parsed.evaluation_profile),
1281
1315
  why_cook_now: deps.asString(parsed.why_cook_now),
@@ -1285,20 +1319,74 @@ function parseCookHandoffCapsulesFromText(
1285
1319
  }
1286
1320
 
1287
1321
  function buildCookHandoffBasisPreview(capsule: CookHandoffCapsule): string {
1288
- const parts = [capsule.mission, ...capsule.scope, ...capsule.constraints, ...capsule.non_goals, ...capsule.acceptance];
1322
+ const parts = [
1323
+ capsule.mission,
1324
+ ...capsule.scope,
1325
+ ...capsule.constraints,
1326
+ ...capsule.non_goals,
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
+ ];
1289
1334
  if (capsule.why_cook_now) parts.push(`why_cook_now: ${capsule.why_cook_now}`);
1290
1335
  return parts.join("\n").trim();
1291
1336
  }
1292
1337
 
1338
+ function cookHandoffAcceptanceItemIsRepoChangeOrVerificationOriented(item: string): boolean {
1339
+ const normalized = normalizeProposalLine(item);
1340
+ if (!normalized) return false;
1341
+ if (hasExplicitPlanningOnlyDeliverable([normalized])) return false;
1342
+ if (hasClearNoImplementationSignal([normalized])) return false;
1343
+ if (implementationMissionSourceCandidateText(normalized)) return true;
1344
+ if (COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX.test(normalized)) return false;
1345
+ return COOK_HANDOFF_VERIFICATION_ACCEPTANCE_REGEX.test(normalized) && COOK_HANDOFF_VERIFICATION_ACTION_REGEX.test(normalized);
1346
+ }
1347
+
1348
+ function cookHandoffAcceptanceIsRepoChangeOriented(capsule: CookHandoffCapsule): boolean {
1349
+ if (capsule.acceptance.length === 0) return false;
1350
+ return capsule.acceptance.some((item) => cookHandoffAcceptanceItemIsRepoChangeOrVerificationOriented(item));
1351
+ }
1352
+
1353
+ function cookHandoffStartabilityFailures(
1354
+ capsule: CookHandoffCapsule,
1355
+ deps: Pick<ProposalParseDeps, "normalizeMissionAnchorText" | "isWeakMissionAnchor">,
1356
+ ): string[] {
1357
+ const failures: string[] = [];
1358
+ const mission = deps.normalizeMissionAnchorText(capsule.mission);
1359
+ if (!mission || deps.isWeakMissionAnchor(mission)) failures.push("mission is missing a concrete implementation anchor");
1360
+ else if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission)) failures.push("mission is negative or workflow-suppression-only");
1361
+ if (capsule.scope.length === 0) failures.push("scope is empty");
1362
+ if (capsule.acceptance.length === 0) failures.push("acceptance is empty");
1363
+ else if (!cookHandoffAcceptanceIsRepoChangeOriented(capsule)) {
1364
+ failures.push("acceptance is not anchored to concrete repo changes or verification");
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");
1371
+ }
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
+ return failures;
1375
+ }
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(" ");
1383
+ }
1384
+
1293
1385
  function isStartableCookHandoffCapsule(
1294
1386
  capsule: CookHandoffCapsule,
1295
1387
  deps: Pick<ProposalParseDeps, "normalizeMissionAnchorText" | "isWeakMissionAnchor">,
1296
1388
  ): boolean {
1297
- const mission = deps.normalizeMissionAnchorText(capsule.mission);
1298
- if (!mission || deps.isWeakMissionAnchor(mission)) return false;
1299
- if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission)) return false;
1300
- if (capsule.scope.length === 0 || capsule.acceptance.length === 0) return false;
1301
- return true;
1389
+ return cookHandoffStartabilityFailures(capsule, deps).length === 0;
1302
1390
  }
1303
1391
 
1304
1392
  function laterMessagesInvalidateCookHandoff(
@@ -1347,6 +1435,11 @@ function buildContextProposalFromCookHandoffCapsule(
1347
1435
  evaluationProfile: capsule.evaluation_profile,
1348
1436
  critique: [
1349
1437
  ...capsule.notes,
1438
+ `First slice goal: ${capsule.first_slice_goal}`,
1439
+ ...(capsule.first_slice_non_goals.length > 0 ? [`First slice non-goals: ${capsule.first_slice_non_goals.join(" | ")}`] : []),
1440
+ ...(capsule.implementation_surfaces.length > 0 ? [`Implementation surfaces: ${capsule.implementation_surfaces.join(" | ")}`] : []),
1441
+ ...(capsule.verification_commands.length > 0 ? [`Verification commands: ${capsule.verification_commands.join(" | ")}`] : []),
1442
+ `Why this slice first: ${capsule.why_this_slice_first}`,
1350
1443
  ...(capsule.why_cook_now ? [`Primary-agent /cook handoff rationale: ${capsule.why_cook_now}`] : []),
1351
1444
  ],
1352
1445
  risks: capsule.risks,
@@ -1355,7 +1448,19 @@ function buildContextProposalFromCookHandoffCapsule(
1355
1448
  suppressedCompletedTopics: [],
1356
1449
  suppressedNegatedTopics: [],
1357
1450
  },
1358
- [mission, goalText, capsule.mission, ...capsule.scope, ...constraints, ...capsule.acceptance],
1451
+ [
1452
+ mission,
1453
+ goalText,
1454
+ capsule.mission,
1455
+ ...capsule.scope,
1456
+ ...constraints,
1457
+ ...capsule.acceptance,
1458
+ capsule.first_slice_goal,
1459
+ ...capsule.first_slice_non_goals,
1460
+ ...capsule.implementation_surfaces,
1461
+ ...capsule.verification_commands,
1462
+ capsule.why_this_slice_first,
1463
+ ],
1359
1464
  ),
1360
1465
  goalText,
1361
1466
  basisPreview: buildCookHandoffBasisPreview(capsule),
@@ -1365,11 +1470,11 @@ function buildContextProposalFromCookHandoffCapsule(
1365
1470
  return finalizeContextProposal(proposal, projectName, deps);
1366
1471
  }
1367
1472
 
1368
- export function extractLatestCookHandoffProposal(
1473
+ export function assessLatestCookHandoffProposal(
1369
1474
  recentMessages: RecentSessionMessage[],
1370
1475
  projectName: string,
1371
1476
  deps: ProposalParseDeps,
1372
- ): ContextProposal | undefined {
1477
+ ): CookHandoffProposalAssessment {
1373
1478
  for (let index = 0; index < recentMessages.length; index += 1) {
1374
1479
  const entry = recentMessages[index];
1375
1480
  if (entry.role !== "assistant" || entry.isCommand) continue;
@@ -1380,11 +1485,27 @@ export function extractLatestCookHandoffProposal(
1380
1485
  const laterMessages = recentMessages.slice(0, index);
1381
1486
  if (!cookHandoffIsFreshEnough(capsule, laterMessages)) continue;
1382
1487
  if (laterMessagesInvalidateCookHandoff(laterMessages, deps)) continue;
1488
+ const failures = cookHandoffStartabilityFailures(capsule, deps);
1489
+ if (failures.length > 0) {
1490
+ return {
1491
+ status: "fresh_but_not_startable",
1492
+ message: buildNonStartableCookHandoffMessage(failures),
1493
+ };
1494
+ }
1383
1495
  const proposal = buildContextProposalFromCookHandoffCapsule(capsule, projectName, deps);
1384
- if (proposal) return proposal;
1496
+ if (proposal) return { status: "startable", proposal };
1385
1497
  }
1386
1498
  }
1387
- return undefined;
1499
+ return { status: "none" };
1500
+ }
1501
+
1502
+ export function extractLatestCookHandoffProposal(
1503
+ recentMessages: RecentSessionMessage[],
1504
+ projectName: string,
1505
+ deps: ProposalParseDeps,
1506
+ ): ContextProposal | undefined {
1507
+ const assessment = assessLatestCookHandoffProposal(recentMessages, projectName, deps);
1508
+ return assessment.status === "startable" ? assessment.proposal : undefined;
1388
1509
  }
1389
1510
 
1390
1511
  export async function deriveCookContextProposalFromRecentDiscussion(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
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,
@@ -1433,7 +1433,19 @@ capsule = {
1433
1433
  "notes": [
1434
1434
  "Keep the startup brief aligned with the explicit primary-agent plan."
1435
1435
  ],
1436
- "handoff_kind": "implementation_workflow_ready",
1436
+ "handoff_kind": "implementation_workflow_handoff",
1437
+ "first_slice_goal": "Land the redirect callback fix and its regression coverage.",
1438
+ "first_slice_non_goals": [
1439
+ "Do not refactor the broader auth flow."
1440
+ ],
1441
+ "implementation_surfaces": [
1442
+ "src/auth/redirect.ts",
1443
+ "tests/auth/redirect.spec.ts"
1444
+ ],
1445
+ "verification_commands": [
1446
+ "npm test -- redirect.spec.ts"
1447
+ ],
1448
+ "why_this_slice_first": "The redirect callback bug is already bounded enough to start implementation safely.",
1437
1449
  "task_type": "completion-workflow",
1438
1450
  "evaluation_profile": "completion-rubric-v1",
1439
1451
  "why_cook_now": "The implementation plan is concrete and ready for repo changes."
@@ -1465,9 +1477,166 @@ assert snapshot['mission'] == 'Fix login redirect callback behavior.', 'explicit
1465
1477
  assert state['mission_anchor'] == 'Fix login redirect callback behavior.', 'explicit handoff startup should use the handoff mission as canonical mission_anchor'
1466
1478
  assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'explicit handoff startup should preserve the advisory intake source'
1467
1479
  assert state['advisory_startup_brief']['risks'] == ['Stale auth discussion could broaden the startup brief if the handoff is ignored.'], 'explicit handoff startup should preserve handoff risks'
1480
+ assert 'First slice goal: Land the redirect callback fix and its regression coverage.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve first_slice_goal in advisory notes'
1481
+ assert 'First slice non-goals: Do not refactor the broader auth flow.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve first_slice_non_goals in advisory notes'
1482
+ assert 'Implementation surfaces: src/auth/redirect.ts | tests/auth/redirect.spec.ts' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve implementation_surfaces in advisory notes'
1483
+ assert 'Verification commands: npm test -- redirect.spec.ts' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve verification_commands in advisory notes'
1484
+ assert 'Why this slice first: The redirect callback bug is already bounded enough to start implementation safely.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve why_this_slice_first in advisory notes'
1468
1485
  assert 'Primary-agent /cook handoff rationale: The implementation plan is concrete and ready for repo changes.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve why_cook_now as notes'
1469
1486
  PY
1470
1487
 
1488
+ # Fresh but non-startable explicit handoff: /cook should fail closed instead of falling back
1489
+ # to a broad recent-discussion startup brief when the explicit capsule is still too vague.
1490
+ HANDOFF_ROOT_VAGUE="$TMPDIR/handoff-root-vague"
1491
+ mkdir -p "$HANDOFF_ROOT_VAGUE"
1492
+ cd "$HANDOFF_ROOT_VAGUE"
1493
+ git init -q
1494
+
1495
+ HANDOFF_SESSION_VAGUE="$TMPDIR/handoff-session-vague.jsonl"
1496
+ HANDOFF_SNAPSHOT_VAGUE="$TMPDIR/handoff-proposal-vague.json"
1497
+ HANDOFF_MESSAGES_VAGUE="$(python3 - <<'PY'
1498
+ import json
1499
+ capsule = {
1500
+ "kind": "cook_handoff",
1501
+ "source": "primary_agent",
1502
+ "captured_at": "2026-01-01T00:00:02.000Z",
1503
+ "source_turn_id": "m0002",
1504
+ "mission": "Fix login redirect callback behavior.",
1505
+ "scope": [
1506
+ "Update the callback redirect decision logic."
1507
+ ],
1508
+ "constraints": [
1509
+ "Do not refactor the broader auth flow."
1510
+ ],
1511
+ "acceptance": [
1512
+ "Confirm the final implementation breakdown before coding."
1513
+ ],
1514
+ "risks": [
1515
+ "Broad recent context could be reused if the vague explicit handoff is ignored."
1516
+ ],
1517
+ "notes": [
1518
+ "This handoff is still too vague to start implementation directly."
1519
+ ],
1520
+ "handoff_kind": "implementation_workflow_handoff",
1521
+ "first_slice_goal": "Patch the callback redirect decision logic.",
1522
+ "first_slice_non_goals": [
1523
+ "Do not refactor the broader auth flow."
1524
+ ],
1525
+ "implementation_surfaces": [],
1526
+ "verification_commands": [],
1527
+ "why_this_slice_first": "The callback redirect path is the likely first slice, but the handoff still lacks execution detail.",
1528
+ "task_type": "completion-workflow",
1529
+ "evaluation_profile": "completion-rubric-v1",
1530
+ "why_cook_now": "The task is workflow-worthy, but the implementation slice is not concrete enough yet."
1531
+ }
1532
+ recent_discussion = "Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page."
1533
+ messages = [
1534
+ {"role": "user", "content": recent_discussion},
1535
+ {"role": "assistant", "content": "This follow-up might soon be ready for /cook.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1536
+ ]
1537
+ print(json.dumps(messages, ensure_ascii=False))
1538
+ PY
1539
+ )"
1540
+ write_session_messages "$HANDOFF_SESSION_VAGUE" "$HANDOFF_ROOT_VAGUE" "$HANDOFF_MESSAGES_VAGUE"
1541
+
1542
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
1543
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_VAGUE" \
1544
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1545
+ pi --session "$HANDOFF_SESSION_VAGUE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-vague.out" 2>"$TMPDIR/pi-completion-handoff-vague.err"
1546
+
1547
+ python3 - "$HANDOFF_SNAPSHOT_VAGUE" "$TMPDIR/pi-completion-handoff-vague.out" "$TMPDIR/pi-completion-handoff-vague.err" <<'PY'
1548
+ import sys
1549
+ from pathlib import Path
1550
+
1551
+ snapshot = Path(sys.argv[1])
1552
+ output = Path(sys.argv[2]).read_text() + Path(sys.argv[3]).read_text()
1553
+
1554
+ assert not snapshot.exists(), 'fresh non-startable handoff should not emit a startup proposal snapshot'
1555
+ assert not Path('.agent').exists(), 'fresh non-startable handoff should fail closed without writing canonical state'
1556
+ assert 'fresh explicit primary-agent handoff exists' in output, 'fresh non-startable handoff should explain that the explicit capsule blocked startup'
1557
+ assert 'acceptance is not anchored to concrete repo changes or verification' in output, 'fresh non-startable handoff should explain the workflow-only acceptance failure'
1558
+ assert 'implementation_surfaces is empty' in output, 'fresh non-startable handoff should explain the missing implementation_surfaces requirement'
1559
+ assert 'verification_commands is empty' in output, 'fresh non-startable handoff should explain the missing verification_commands requirement'
1560
+ PY
1561
+
1562
+ # Fresh explicit handoff with complete first-slice fields but vague acceptance: /cook should still fail closed
1563
+ # with the dedicated explicit-handoff message instead of bootstrapping canonical state.
1564
+ HANDOFF_ROOT_VAGUE_ACCEPTANCE="$TMPDIR/handoff-root-vague-acceptance"
1565
+ mkdir -p "$HANDOFF_ROOT_VAGUE_ACCEPTANCE"
1566
+ cd "$HANDOFF_ROOT_VAGUE_ACCEPTANCE"
1567
+ git init -q
1568
+
1569
+ HANDOFF_SESSION_VAGUE_ACCEPTANCE="$TMPDIR/handoff-session-vague-acceptance.jsonl"
1570
+ HANDOFF_SNAPSHOT_VAGUE_ACCEPTANCE="$TMPDIR/handoff-proposal-vague-acceptance.json"
1571
+ HANDOFF_MESSAGES_VAGUE_ACCEPTANCE="$(python3 - <<'PY'
1572
+ import json
1573
+ capsule = {
1574
+ "kind": "cook_handoff",
1575
+ "source": "primary_agent",
1576
+ "captured_at": "2026-01-01T00:00:02.000Z",
1577
+ "source_turn_id": "m0002",
1578
+ "mission": "Fix login redirect callback behavior.",
1579
+ "scope": [
1580
+ "Update the callback redirect decision logic.",
1581
+ "Preserve the broader auth flow."
1582
+ ],
1583
+ "constraints": [
1584
+ "Do not refactor the broader auth flow."
1585
+ ],
1586
+ "acceptance": [
1587
+ "Current behavior stays understandable."
1588
+ ],
1589
+ "risks": [
1590
+ "Broad recent context could be reused if the vague explicit handoff is ignored."
1591
+ ],
1592
+ "notes": [
1593
+ "This handoff includes first-slice fields but still lacks concrete acceptance."
1594
+ ],
1595
+ "handoff_kind": "implementation_workflow_handoff",
1596
+ "first_slice_goal": "Land the redirect callback fix and its regression coverage.",
1597
+ "first_slice_non_goals": [
1598
+ "Do not refactor the broader auth flow."
1599
+ ],
1600
+ "implementation_surfaces": [
1601
+ "src/auth/redirect.ts",
1602
+ "tests/auth/redirect.spec.ts"
1603
+ ],
1604
+ "verification_commands": [
1605
+ "npm test -- redirect.spec.ts"
1606
+ ],
1607
+ "why_this_slice_first": "The redirect callback bug is already bounded enough to start implementation safely once acceptance is concrete.",
1608
+ "task_type": "completion-workflow",
1609
+ "evaluation_profile": "completion-rubric-v1",
1610
+ "why_cook_now": "The task is workflow-worthy, but the acceptance still needs concrete repo-change detail."
1611
+ }
1612
+ recent_discussion = "Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page."
1613
+ messages = [
1614
+ {"role": "user", "content": recent_discussion},
1615
+ {"role": "assistant", "content": "This follow-up might soon be ready for /cook.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1616
+ ]
1617
+ print(json.dumps(messages, ensure_ascii=False))
1618
+ PY
1619
+ )"
1620
+ write_session_messages "$HANDOFF_SESSION_VAGUE_ACCEPTANCE" "$HANDOFF_ROOT_VAGUE_ACCEPTANCE" "$HANDOFF_MESSAGES_VAGUE_ACCEPTANCE"
1621
+
1622
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
1623
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_VAGUE_ACCEPTANCE" \
1624
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1625
+ pi --session "$HANDOFF_SESSION_VAGUE_ACCEPTANCE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-vague-acceptance.out" 2>"$TMPDIR/pi-completion-handoff-vague-acceptance.err"
1626
+
1627
+ python3 - "$HANDOFF_SNAPSHOT_VAGUE_ACCEPTANCE" "$TMPDIR/pi-completion-handoff-vague-acceptance.out" "$TMPDIR/pi-completion-handoff-vague-acceptance.err" <<'PY'
1628
+ import sys
1629
+ from pathlib import Path
1630
+
1631
+ snapshot = Path(sys.argv[1])
1632
+ output = Path(sys.argv[2]).read_text() + Path(sys.argv[3]).read_text()
1633
+
1634
+ assert not snapshot.exists(), 'fresh explicit handoff with vague acceptance should not emit a startup proposal snapshot'
1635
+ assert not Path('.agent').exists(), 'fresh explicit handoff with vague acceptance should fail closed without writing canonical state'
1636
+ assert 'fresh explicit primary-agent handoff exists' in output, 'fresh explicit handoff with vague acceptance should use the dedicated explicit-handoff fail-closed message'
1637
+ assert 'acceptance is not anchored to concrete repo changes or verification' in output, 'fresh explicit handoff with vague acceptance should explain the vague acceptance failure'
1638
+ PY
1639
+
1471
1640
  # Done workflow + fresh handoff: the fresh explicit handoff should override done-state suppression and start the new round.
1472
1641
  HANDOFF_ROOT_DONE="$TMPDIR/handoff-root-done"
1473
1642
  mkdir -p "$HANDOFF_ROOT_DONE"
@@ -1509,7 +1678,19 @@ capsule = {
1509
1678
  "notes": [
1510
1679
  "This is a fresh implementation round, not a summary of the finished workflow."
1511
1680
  ],
1512
- "handoff_kind": "implementation_workflow_ready",
1681
+ "handoff_kind": "implementation_workflow_handoff",
1682
+ "first_slice_goal": "Patch the callback edge case and cover it with a focused regression test.",
1683
+ "first_slice_non_goals": [
1684
+ "Do not turn done-state suppression into the startup mission."
1685
+ ],
1686
+ "implementation_surfaces": [
1687
+ "src/auth/redirect.ts",
1688
+ "tests/auth/redirect-edge.spec.ts"
1689
+ ],
1690
+ "verification_commands": [
1691
+ "npm test -- redirect-edge.spec.ts"
1692
+ ],
1693
+ "why_this_slice_first": "The new callback edge case is the smallest fresh implementation slice after the prior round closed.",
1513
1694
  "task_type": "completion-workflow",
1514
1695
  "evaluation_profile": "completion-rubric-v1",
1515
1696
  "why_cook_now": "A new implementation-ready edge case was identified after the previous round closed."
@@ -1541,6 +1722,8 @@ assert snapshot['mission'] == 'Reopen the login redirect work for the callback e
1541
1722
  assert state['mission_anchor'] == 'Reopen the login redirect work for the callback edge case.', 'done-workflow handoff should override done-state suppression with the fresh mission'
1542
1723
  assert state['continuation_policy'] == 'continue', 'done-workflow handoff should reopen canonical workflow state for the new round'
1543
1724
  assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'done-workflow handoff should preserve the handoff advisory source'
1725
+ assert 'First slice goal: Patch the callback edge case and cover it with a focused regression test.' in state['advisory_startup_brief']['notes'], 'done-workflow handoff should preserve first_slice_goal in advisory notes'
1726
+ assert 'Verification commands: npm test -- redirect-edge.spec.ts' in state['advisory_startup_brief']['notes'], 'done-workflow handoff should preserve verification_commands in advisory notes'
1544
1727
  PY
1545
1728
 
1546
1729
  # Stale handoff: later discussion should invalidate the older handoff capsule and fall back to the newer discussion mission.
@@ -1564,7 +1747,12 @@ capsule = {
1564
1747
  "acceptance": ["Add the original callback regression test."],
1565
1748
  "risks": [],
1566
1749
  "notes": [],
1567
- "handoff_kind": "implementation_workflow_ready"
1750
+ "handoff_kind": "implementation_workflow_handoff",
1751
+ "first_slice_goal": "Ship the original login callback follow-up.",
1752
+ "first_slice_non_goals": ["Do not refactor the auth stack."],
1753
+ "implementation_surfaces": ["src/auth/login-redirect.ts"],
1754
+ "verification_commands": ["npm test -- login-redirect.spec.ts"],
1755
+ "why_this_slice_first": "The original callback follow-up was the first bounded implementation slice before later discussion replaced it."
1568
1756
  }
1569
1757
  newer_discussion = "Mission: Ship logout redirect consistency instead.\nScope:\n- Update the logout redirect path.\nConstraints:\n- Leave the login callback flow unchanged.\nAcceptance:\n- Add a logout redirect regression test."
1570
1758
  messages = [
@@ -1618,7 +1806,12 @@ capsule = {
1618
1806
  "acceptance": ["Explain that the finished workflow should stay closed."],
1619
1807
  "risks": [],
1620
1808
  "notes": [],
1621
- "handoff_kind": "implementation_workflow_ready"
1809
+ "handoff_kind": "implementation_workflow_handoff",
1810
+ "first_slice_goal": "Keep the finished workflow closed.",
1811
+ "first_slice_non_goals": ["Do not start repo changes."],
1812
+ "implementation_surfaces": ["docs/workflow-status.md"],
1813
+ "verification_commands": ["npm test -- workflow-status"],
1814
+ "why_this_slice_first": "This is the only bounded next step being proposed, even though the mission itself is invalid."
1622
1815
  }
1623
1816
  messages = [
1624
1817
  {"role": "user", "content": "Should we reopen the finished workflow?"},
@@ -3,6 +3,7 @@ set -euo pipefail
3
3
 
4
4
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
5
  cd "$ROOT"
6
+ export PI_COMPLETION_RUNNING_RELEASE_CHECK=1
6
7
 
7
8
  echo "[release-check] running control-plane validation, tracked .agent contract coverage, slice-surface parity, explicit-/cook parity, startup/refocus/context regressions, canonical evidence artifact, active-slice contract, observability, legacy cleanup, evaluator calibration, and rubric contract coverage"
8
9
  bash .agent/verify_completion_control_plane.sh
@@ -19,18 +20,24 @@ checks = {
19
20
  "Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.",
20
21
  "If a task has clearly matured into completion-workflow scope, the primary agent should hand you off to `/cook` instead of starting long-running implementation directly in ordinary chat.",
21
22
  "That handoff should include an explicit structured `/cook` capsule in the assistant reply so `/cook` can confirm the already-formed mission instead of re-deriving it from broad ambient context.",
22
- "`/cook` first looks for a fresh explicit primary-agent handoff capsule.",
23
+ "for that handoff capsule to start workflow immediately, it must already be implementation-startable: a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first`",
24
+ "The preferred capsule is still advisory startup intake, not canonical workflow state, and it only counts as implementation-ready when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.",
25
+ "`/cook` first looks for a fresh explicit primary-agent handoff capsule. If one is valid and implementation-startable, `/cook` builds the startup brief from that handoff and only uses recent discussion as validation or supplemental notes.",
26
+ "`/cook` falls back to deriving a startup brief from recent discussion only when no fresh explicit handoff is blocking startup—for example, when there is no fresh capsule or only stale or invalidated capsules—before showing the existing approval-only Start/Cancel gate.",
23
27
  "The pre-`/cook` handoff capsule itself is not canonical workflow state. It is only startup intake for `/cook`.",
24
28
  ],
25
29
  "CHANGELOG.md": [
26
- "made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh and valid",
27
- "kept context-derived startup as a fallback only, so stale, drifted, or non-startable handoff capsules still fail closed or fall back to recent discussion instead of silently rewriting canonical state",
30
+ "made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh, valid, and implementation-startable",
31
+ "tightened implementation-ready explicit handoffs so the structured capsule must already carry a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first` before `/cook` will start workflow from it",
32
+ "kept the pre-`/cook` handoff capsule as advisory startup intake only, not canonical `.agent/**` workflow state, while still using context-derived startup as the fallback only when no fresh explicit handoff is blocking startup",
33
+ "kept context-derived startup as a fallback only when there is no fresh explicit handoff blocking startup, so stale or invalidated capsules can still fall back to recent discussion while fresh non-startable handoffs fail closed instead of silently rewriting canonical state",
28
34
  "made finished-workflow suppression stay a safety layer instead of a replacement mission when a fresh explicit `/cook` handoff exists, and blocked negative rejection/suppression text from becoming a Startable startup mission",
29
35
  ],
30
36
  "extensions/completion/prompt-surfaces.ts": [
31
37
  '"/cook is the only explicit entrypoint into long-running completion workflow."',
32
38
  '"When you judge that the task has matured into completion-workflow scope',
33
- '"Also append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON',
39
+ '"Distinguish a workflow-worthy handoff from an implementation-ready handoff: only emit the implementation-ready capsule when the first bounded implementation slice is concrete enough to start immediately."',
40
+ '"Otherwise append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON with kind/source/handoff_kind plus mission, scope, constraints or non_goals, acceptance, risks, notes, captured_at, source_turn_id, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, why_this_slice_first, and optional task_type/evaluation_profile/why_cook_now."',
34
41
  '"The capsule is startup intake for /cook only: do not present it as canonical .agent state',
35
42
  ],
36
43
  }
@@ -171,6 +171,8 @@ handoff_text = handoff.read_text()
171
171
  assert '/cook is the only explicit entrypoint into long-running completion workflow.' in handoff_text, 'ordinary handoff reminder should preserve the explicit /cook workflow boundary'
172
172
  assert 'stop short of long-running implementation and tell the user to run /cook.' in handoff_text, 'ordinary handoff reminder should require primary-agent handoff before implementation'
173
173
  assert '```cook_handoff ... ``` JSON' in handoff_text, 'ordinary handoff reminder should require the explicit structured /cook handoff capsule'
174
+ assert 'implementation_workflow_handoff' in handoff_text, 'ordinary handoff reminder should require the implementation-ready handoff kind'
175
+ assert 'first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, why_this_slice_first' in handoff_text, 'ordinary handoff reminder should require first-slice startability fields'
174
176
  assert 'The capsule is startup intake for /cook only' in handoff_text, 'ordinary handoff reminder should keep the capsule non-canonical'
175
177
  assert not auto_resume.exists(), 'ordinary non-/cook turn should not queue auto-resume before /cook activation'
176
178
  assert 'Skipped completion workflow auto-resume prompt (test mode)' not in output, 'ordinary non-/cook turn should not attempt auto-resume'
@@ -36,7 +36,8 @@ When the task is judged ready for completion workflow, the primary agent must:
36
36
  - not edit tracked product files in ordinary chat for that workflow-level task
37
37
  - tell the user to run `/cook`
38
38
  - explain that `/cook` will first look for a fresh explicit primary-agent handoff and otherwise fall back to recent discussion before asking for confirmation
39
- - append one exact structured `/cook` handoff capsule in the same assistant reply
39
+ - distinguish a workflow-worthy handoff from an implementation-ready handoff
40
+ - only append an implementation-ready `/cook` handoff capsule when the first bounded implementation slice is concrete enough to start immediately
40
41
 
41
42
  Required capsule format:
42
43
 
@@ -50,10 +51,16 @@ Required capsule format:
50
51
  "mission": "<startable implementation mission>",
51
52
  "scope": ["..."],
52
53
  "constraints": ["..."],
54
+ "non_goals": ["..."],
53
55
  "acceptance": ["..."],
54
56
  "risks": ["..."],
55
57
  "notes": ["..."],
56
- "handoff_kind": "implementation_workflow_ready",
58
+ "handoff_kind": "implementation_workflow_handoff",
59
+ "first_slice_goal": "<bounded first slice goal>",
60
+ "first_slice_non_goals": ["..."],
61
+ "implementation_surfaces": ["path/or/surface"],
62
+ "verification_commands": ["npm test -- example"],
63
+ "why_this_slice_first": "<why this first slice should start the workflow>",
57
64
  "task_type": "completion-workflow",
58
65
  "evaluation_profile": "completion-rubric-v1",
59
66
  "why_cook_now": "<why the task is ready for /cook now>"
@@ -64,6 +71,7 @@ Required capsule format:
64
71
  Notes:
65
72
 
66
73
  - `constraints` may be replaced or supplemented by `non_goals` when clearer.
74
+ - `first_slice_goal`, `first_slice_non_goals`, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first` are required only for implementation-ready handoffs; if the work is workflow-worthy but that first slice is still vague, tell the user to run `/cook` without emitting this implementation-ready capsule.
67
75
  - The mission must be positively startable implementation work; do not use rejection or suppression text as the mission.
68
76
  - The capsule is startup intake for `/cook` only. It is not canonical `.agent/**` state, not active-slice state, and not a second repo contract source.
69
77