@linimin/pi-letscook 0.1.51 → 0.1.53

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.
@@ -2,9 +2,16 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type {
4
4
  CompletionStateSnapshot,
5
+ CookNaturalLanguageHandoff,
6
+ CookTriggerAdoptedArtifact,
7
+ CookTriggerClarificationActionItem,
8
+ CookTriggerClarificationLayout,
5
9
  CookTriggerClassification,
6
10
  CookTriggerConfirmationActionItem,
7
11
  CookTriggerConfirmationLayout,
12
+ CookTriggerRecoveryActionItem,
13
+ CookTriggerRecoveryLayout,
14
+ CookTriggerWorkflowBias,
8
15
  LiveRoleActivity,
9
16
  } from "./types";
10
17
  import type {
@@ -211,13 +218,19 @@ export function buildCookTriggerClassifierPrompt(args: {
211
218
  }): string {
212
219
  const lines = [
213
220
  `Project: ${args.projectName}`,
214
- "Classify whether the current input is a natural-language handoff to the canonical /cook workflow before the primary agent starts implementation work.",
215
- "Return JSON only with keys: intent, confidence, reason, evidence, riskFlags, focusHint.",
216
- "intent must be exactly one of route_to_cook, normal_prompt, or unclear.",
217
- "Use route_to_cook only when the user is handing control from recent discussion into workflow execution or explicitly asking to let /cook take over.",
218
- "Use normal_prompt for ordinary questions, explanations, or direct agent requests that should stay in the main chat.",
221
+ "Classify whether the current input should stay in the main chat or be intercepted by the workflow-aware router into the canonical /cook workflow before the primary agent starts implementation work.",
222
+ "Assume router mode reviews every non-bypass normal user turn. Do not require short trigger phrases or explicit /cook text before choosing offer_workflow.",
223
+ "Return JSON only with keys: decision, confidence, workflow_bias, reason, evidence, riskFlags, focusHint. You may also include optional keys requires_clarification, clarification_slots, and adopted_artifact when clearly supported.",
224
+ "decision must be exactly one of offer_workflow, normal_prompt, or unclear.",
225
+ "Use offer_workflow when the user is directly asking to start, resume, refocus, or continue workflow-worthy repo work through the completion boundary, or explicitly asking to let /cook take over.",
226
+ "Use normal_prompt for ordinary questions, explanations, analysis-only requests, or direct agent requests that should stay in the main chat.",
219
227
  "Use unclear for ambiguous approvals, short acknowledgements, or cases where false-positive routing risk is material.",
228
+ "workflow_bias must be exactly one of startup, resume, refocus, next_round, or unknown.",
229
+ "Use startup when there is no active workflow yet, resume when the user is clearly continuing the current workflow, refocus when the user is clearly switching the active workflow to a different goal, and next_round when the previous workflow is done and the user is starting a new round.",
230
+ "When decision is not offer_workflow, prefer workflow_bias=unknown unless a stronger routing hint is still useful for later debugging.",
220
231
  "focusHint is optional, must stay short, and must never rewrite the workflow mission or invent scope.",
232
+ "When explicit user adoption of a recent plan or repo markdown artifact is evident, adopted_artifact may describe it with kind recent_plan|repo_markdown, path when known, and basis explicit_user_adoption.",
233
+ "requires_clarification may be true when chooser-style disambiguation is safer than guessing, and clarification_slots may list short needs such as goal, scope, or non_goal.",
221
234
  "evidence and riskFlags must be arrays of short grounded strings.",
222
235
  ];
223
236
  if (args.workflowContextLines?.length) lines.push("", "Canonical workflow context:", ...args.workflowContextLines);
@@ -225,27 +238,205 @@ export function buildCookTriggerClassifierPrompt(args: {
225
238
  return lines.join("\n");
226
239
  }
227
240
 
228
- export function buildCookTriggerConfirmationActions(mainChatRerunGuidance: string): CookTriggerConfirmationActionItem[] {
229
- return [
230
- {
231
- id: "start_cook",
232
- label: "Start /cook",
233
- description: "Let /cook take over before the primary agent starts implementation work.",
234
- },
241
+ function cookTriggerOfferCopyForBias(
242
+ workflowBias: CookTriggerWorkflowBias,
243
+ mainChatRerunGuidance: string,
244
+ ): { title: string; intro: string; startAction: CookTriggerConfirmationActionItem; sendAsNormalChat: CookTriggerConfirmationActionItem; cancel: CookTriggerConfirmationActionItem } {
245
+ switch (workflowBias) {
246
+ case "startup":
247
+ return {
248
+ title: "Start a completion workflow from the recent discussion?",
249
+ intro:
250
+ "This input looks like a startup handoff into the completion workflow. The shared /cook entry would initialize or continue the canonical workflow boundary only after you confirm.",
251
+ startAction: {
252
+ id: "start_workflow",
253
+ label: "Start workflow",
254
+ description: "Enter the shared /cook workflow entry from the recent discussion before the primary agent starts implementation work.",
255
+ },
256
+ sendAsNormalChat: {
257
+ id: "send_as_normal_chat",
258
+ label: "Send as normal chat",
259
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
260
+ },
261
+ cancel: {
262
+ id: "cancel",
263
+ label: "Cancel",
264
+ description: `Stop here without routing or replaying the original message. ${mainChatRerunGuidance}`,
265
+ },
266
+ };
267
+ case "resume":
268
+ return {
269
+ title: "Resume the current completion workflow?",
270
+ intro:
271
+ "This input looks like a resume handoff for the current completion workflow. The shared /cook entry would continue from canonical state only after you confirm.",
272
+ startAction: {
273
+ id: "start_workflow",
274
+ label: "Resume workflow",
275
+ description: "Resume the current canonical completion workflow through the shared /cook entry.",
276
+ },
277
+ sendAsNormalChat: {
278
+ id: "send_as_normal_chat",
279
+ label: "Send as normal chat",
280
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
281
+ },
282
+ cancel: {
283
+ id: "cancel",
284
+ label: "Cancel",
285
+ description: `Stop here without resuming or replaying the original message. ${mainChatRerunGuidance}`,
286
+ },
287
+ };
288
+ case "refocus":
289
+ return {
290
+ title: "Refocus the completion workflow from the recent discussion?",
291
+ intro:
292
+ "This input looks like a refocus handoff. The shared /cook entry would keep the existing chooser and confirmation semantics before any canonical workflow state is rewritten.",
293
+ startAction: {
294
+ id: "start_workflow",
295
+ label: "Refocus workflow",
296
+ description: "Review the recent discussion through the shared /cook entry and refocus the canonical workflow only if the follow-up confirmations agree.",
297
+ },
298
+ sendAsNormalChat: {
299
+ id: "send_as_normal_chat",
300
+ label: "Send as normal chat",
301
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
302
+ },
303
+ cancel: {
304
+ id: "cancel",
305
+ label: "Cancel",
306
+ description: `Stop here without refocusing or replaying the original message. ${mainChatRerunGuidance}`,
307
+ },
308
+ };
309
+ case "next_round":
310
+ return {
311
+ title: "Start the next completion workflow round from the recent discussion?",
312
+ intro:
313
+ "This input looks like a next-round handoff after a completed workflow. The shared /cook entry would preserve the same canonical workflow boundary while starting the next round only after you confirm.",
314
+ startAction: {
315
+ id: "start_workflow",
316
+ label: "Start next round",
317
+ description: "Start the next workflow round through the shared /cook entry using the recent discussion as the new focus.",
318
+ },
319
+ sendAsNormalChat: {
320
+ id: "send_as_normal_chat",
321
+ label: "Send as normal chat",
322
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
323
+ },
324
+ cancel: {
325
+ id: "cancel",
326
+ label: "Cancel",
327
+ description: `Stop here without starting a new workflow round or replaying the original message. ${mainChatRerunGuidance}`,
328
+ },
329
+ };
330
+ case "unknown":
331
+ default:
332
+ return {
333
+ title: "Let the completion workflow take over from the recent discussion?",
334
+ intro:
335
+ "This input looks like a natural-language handoff into the completion workflow. The shared /cook entry would keep the existing approval-only startup, continue, refocus, and next-round semantics before canonical state changes.",
336
+ startAction: {
337
+ id: "start_workflow",
338
+ label: "Start workflow",
339
+ description: "Enter the shared /cook workflow entry before the primary agent starts implementation work.",
340
+ },
341
+ sendAsNormalChat: {
342
+ id: "send_as_normal_chat",
343
+ label: "Send as normal chat",
344
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
345
+ },
346
+ cancel: {
347
+ id: "cancel",
348
+ label: "Cancel",
349
+ description: `Stop here without routing or replaying the original message. ${mainChatRerunGuidance}`,
350
+ },
351
+ };
352
+ }
353
+ }
354
+
355
+ export function buildCookTriggerConfirmationActions(
356
+ workflowBias: CookTriggerWorkflowBias,
357
+ mainChatRerunGuidance: string,
358
+ ): CookTriggerConfirmationActionItem[] {
359
+ const copy = cookTriggerOfferCopyForBias(workflowBias, mainChatRerunGuidance);
360
+ return [copy.startAction, copy.sendAsNormalChat, copy.cancel];
361
+ }
362
+
363
+ function summarizeAdoptedArtifact(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): string | undefined {
364
+ if (!adoptedArtifact) return undefined;
365
+ const lines = [
366
+ `- kind: ${adoptedArtifact.kind}`,
367
+ `- basis: ${adoptedArtifact.basis}`,
368
+ `- title: ${adoptedArtifact.title}`,
369
+ ];
370
+ if (adoptedArtifact.path) lines.push(`- path: ${adoptedArtifact.path}`);
371
+ if (adoptedArtifact.preview) lines.push(`- preview: ${adoptedArtifact.preview}`);
372
+ return lines.join("\n");
373
+ }
374
+
375
+ export function buildCookTriggerClarificationLayout(args: {
376
+ currentMission?: string;
377
+ candidateMission?: string;
378
+ workflowBiases: CookTriggerWorkflowBias[];
379
+ mainChatRerunGuidance: string;
380
+ adoptedArtifact?: CookTriggerAdoptedArtifact;
381
+ }): CookTriggerClarificationLayout {
382
+ const actions: CookTriggerClarificationActionItem[] = [];
383
+ if (args.workflowBiases.includes("startup")) {
384
+ actions.push({
385
+ id: "route_startup",
386
+ label: "Start workflow",
387
+ description: "Treat this as a startup handoff into the shared /cook workflow from the recent discussion.",
388
+ });
389
+ }
390
+ if (args.workflowBiases.includes("resume")) {
391
+ actions.push({
392
+ id: "route_resume",
393
+ label: "Resume workflow",
394
+ description: "Keep the current canonical mission and resume the active workflow through the shared /cook entry.",
395
+ });
396
+ }
397
+ if (args.workflowBiases.includes("refocus")) {
398
+ actions.push({
399
+ id: "route_refocus",
400
+ label: "Refocus from recent discussion",
401
+ description: "Route into the shared /cook entry and keep its existing chooser + approval flow before any canonical state rewrite.",
402
+ });
403
+ }
404
+ if (args.workflowBiases.includes("next_round")) {
405
+ actions.push({
406
+ id: "route_next_round",
407
+ label: "Start next round",
408
+ description: "Treat this as a next-round handoff into the shared /cook entry after the finished workflow.",
409
+ });
410
+ }
411
+ actions.push(
235
412
  {
236
- id: "keep_chatting",
237
- label: "Keep chatting",
238
- description: "Send the original message to the primary agent instead.",
413
+ id: "send_as_normal_chat",
414
+ label: "Send as normal chat",
415
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
239
416
  },
240
417
  {
241
418
  id: "cancel",
242
419
  label: "Cancel",
243
- description: `Stop here without routing the message. ${mainChatRerunGuidance}`,
420
+ description: `Stop here without routing or replaying the original message. ${args.mainChatRerunGuidance}`,
244
421
  },
245
- ];
422
+ );
423
+ return {
424
+ title: "Clarify how the completion workflow should proceed",
425
+ intro:
426
+ "This start-intent looks workflow-related, but not enough to safely choose startup, resume, refocus, or next-round automatically. Pick the minimal next step or cancel without changing canonical workflow state.",
427
+ currentMissionHeading: args.currentMission ? "Current mission" : undefined,
428
+ currentMissionBody: args.currentMission,
429
+ candidateMissionHeading: args.candidateMission ? "Recent-discussion candidate" : undefined,
430
+ candidateMissionBody: args.candidateMission,
431
+ adoptedArtifactHeading: args.adoptedArtifact ? "Adopted artifact" : undefined,
432
+ adoptedArtifactBody: summarizeAdoptedArtifact(args.adoptedArtifact),
433
+ actionsHeading: "Actions",
434
+ actions,
435
+ footer: "↑↓ navigate • enter select • esc cancel",
436
+ };
246
437
  }
247
438
 
248
- export function buildCookTriggerAssistConfirmationLayout(args: {
439
+ export function buildCookTriggerConfirmationLayout(args: {
249
440
  classification: CookTriggerClassification;
250
441
  mainChatRerunGuidance: string;
251
442
  }): CookTriggerConfirmationLayout {
@@ -254,10 +445,10 @@ export function buildCookTriggerAssistConfirmationLayout(args: {
254
445
  ? args.classification.evidence.map((item) => `- ${item}`).join("\n")
255
446
  : "- No additional evidence was captured beyond the current handoff signal.";
256
447
  const riskBody = args.classification.riskFlags.length > 0 ? args.classification.riskFlags.map((item) => `- ${item}`).join("\n") : undefined;
448
+ const copy = cookTriggerOfferCopyForBias(args.classification.workflowBias, args.mainChatRerunGuidance);
257
449
  return {
258
- title: "Let /cook take over from the recent discussion?",
259
- intro:
260
- "This input looks like a natural-language handoff into the completion workflow. /cook would keep the existing approval-only startup, continue, refocus, and next-round semantics before canonical state changes.",
450
+ title: copy.title,
451
+ intro: copy.intro,
261
452
  evidenceHeading: "Why it matched",
262
453
  evidenceBody,
263
454
  riskHeading: riskBody ? "Risk checks" : undefined,
@@ -265,7 +456,40 @@ export function buildCookTriggerAssistConfirmationLayout(args: {
265
456
  focusHintHeading: args.classification.focusHint ? "Optional focus hint" : undefined,
266
457
  focusHintBody: args.classification.focusHint,
267
458
  actionsHeading: "Actions",
268
- actions: buildCookTriggerConfirmationActions(args.mainChatRerunGuidance),
459
+ actions: [copy.startAction, copy.sendAsNormalChat, copy.cancel],
460
+ footer: "↑↓ navigate • enter select • esc cancel",
461
+ };
462
+ }
463
+
464
+ export function buildCookTriggerRecoveryLayout(args: {
465
+ failureLabel: string;
466
+ mainChatRerunGuidance: string;
467
+ }): CookTriggerRecoveryLayout {
468
+ const actions: CookTriggerRecoveryActionItem[] = [
469
+ {
470
+ id: "retry_routing",
471
+ label: "Retry routing",
472
+ description: "Run the workflow-aware router classifier once more before deciding whether /cook should take over.",
473
+ },
474
+ {
475
+ id: "send_as_normal_chat",
476
+ label: "Send as normal chat",
477
+ description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
478
+ },
479
+ {
480
+ id: "cancel",
481
+ label: "Cancel",
482
+ description: `Stop here without routing or replaying the original message. ${args.mainChatRerunGuidance}`,
483
+ },
484
+ ];
485
+ return {
486
+ title: "Router recovery needed before this prompt can continue",
487
+ intro:
488
+ "The workflow-aware router could not safely classify this prompt, so it stayed fail-closed instead of silently sending the prompt to the primary agent. Choose an explicit recovery path or cancel.",
489
+ failureHeading: "Router failure",
490
+ failureBody: args.failureLabel,
491
+ actionsHeading: "Actions",
492
+ actions,
269
493
  footer: "↑↓ navigate • enter select • esc cancel",
270
494
  };
271
495
  }
@@ -281,10 +505,62 @@ export function maybeWriteCookTriggerConfirmationSnapshot(
281
505
  writeJsonSnapshot(snapshotPath, layout);
282
506
  }
283
507
 
508
+ export function maybeWriteCookTriggerClarificationSnapshot(
509
+ layout: CookTriggerClarificationLayout,
510
+ snapshotPath: string | undefined,
511
+ ): void {
512
+ writeJsonSnapshot(snapshotPath, layout);
513
+ }
514
+
515
+ export function maybeWriteCookTriggerRecoverySnapshot(layout: CookTriggerRecoveryLayout, snapshotPath: string | undefined): void {
516
+ writeJsonSnapshot(snapshotPath, layout);
517
+ }
518
+
284
519
  export function maybeWriteCookTriggerRoutingSnapshot(snapshot: Record<string, unknown>, snapshotPath: string | undefined): void {
285
520
  writeJsonSnapshot(snapshotPath, snapshot);
286
521
  }
287
522
 
523
+ function buildNaturalLanguageHandoffArtifactLines(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): string[] {
524
+ if (!adoptedArtifact) return [];
525
+ const lines = [
526
+ `- adopted_artifact_kind: ${adoptedArtifact.kind}`,
527
+ `- adopted_artifact_basis: ${adoptedArtifact.basis}`,
528
+ `- adopted_artifact_title: ${adoptedArtifact.title}`,
529
+ ];
530
+ if (adoptedArtifact.path) lines.push(`- adopted_artifact_path: ${adoptedArtifact.path}`);
531
+ if (adoptedArtifact.preview) lines.push(`- adopted_artifact_preview: ${adoptedArtifact.preview}`);
532
+ return lines;
533
+ }
534
+
535
+ function buildNaturalLanguageHandoffClarificationLines(
536
+ clarificationCapsule: CookNaturalLanguageHandoff["clarificationCapsule"] | undefined,
537
+ ): string[] {
538
+ if (!clarificationCapsule) return [];
539
+ const lines = [
540
+ `- clarification_selected_bias: ${clarificationCapsule.selectedWorkflowBias}`,
541
+ `- clarification_reason: ${clarificationCapsule.reason}`,
542
+ ];
543
+ if (clarificationCapsule.goal) lines.push(`- clarification_goal: ${clarificationCapsule.goal}`);
544
+ if (clarificationCapsule.scope?.length) lines.push(`- clarification_scope: ${clarificationCapsule.scope.join(" | ")}`);
545
+ if (clarificationCapsule.nonGoal?.length) lines.push(`- clarification_non_goal: ${clarificationCapsule.nonGoal.join(" | ")}`);
546
+ if (clarificationCapsule.doneWhen?.length) lines.push(`- clarification_done_when: ${clarificationCapsule.doneWhen.join(" | ")}`);
547
+ return lines;
548
+ }
549
+
550
+ export function buildNaturalLanguageHandoffMetadataLines(handoff: CookNaturalLanguageHandoff | undefined): string[] {
551
+ if (!handoff) return [];
552
+ return [
553
+ "Natural-language handoff metadata:",
554
+ `- source: natural_language_handoff`,
555
+ `- preferred_routing_bias: ${handoff.preferredRoutingBias ?? "unknown"}`,
556
+ `- trigger_text: ${handoff.triggerText ?? "(none)"}`,
557
+ `- focus_hint: ${handoff.hintText ?? "(none)"}`,
558
+ ...buildNaturalLanguageHandoffArtifactLines(handoff.adoptedArtifact),
559
+ ...buildNaturalLanguageHandoffClarificationLines(handoff.clarificationCapsule),
560
+ "",
561
+ ];
562
+ }
563
+
288
564
  export function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout) {
289
565
  return layout.actions.map((action) => ({
290
566
  value: action.id,
@@ -121,18 +121,24 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
121
121
  const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
122
122
  const ANALYST_HEARTBEAT_MS = 5_000;
123
123
  const COOK_TRIGGER_CLASSIFIER_SYSTEM_PROMPT = [
124
- "You classify whether the latest user input should hand control to the canonical /cook workflow before the primary agent starts implementation work.",
124
+ "You classify whether the latest user input should stay in the main chat or be intercepted by the workflow-aware router into the canonical /cook workflow before the primary agent starts implementation work.",
125
+ "Assume router mode reviews every non-bypass normal user turn. Do not require short trigger phrases or explicit /cook text before choosing offer_workflow.",
125
126
  "Do not emit markdown, code fences, or commentary.",
126
- "Return exactly one JSON object with keys: intent, confidence, reason, evidence, riskFlags, focusHint.",
127
- "intent must be exactly one of route_to_cook, normal_prompt, or unclear.",
128
- "Use route_to_cook only when the latest input is handing control from discussion into workflow execution or explicitly asking to let /cook take over.",
129
- "Use normal_prompt for ordinary questions, explanations, or direct requests that should stay with the primary agent.",
127
+ "Return exactly one JSON object with keys: decision, confidence, workflow_bias, reason, evidence, riskFlags, focusHint. You may also include optional keys requires_clarification, clarification_slots, and adopted_artifact when clearly supported.",
128
+ "decision must be exactly one of offer_workflow, normal_prompt, or unclear.",
129
+ "Use offer_workflow when the latest input is directly asking to start, resume, refocus, or continue workflow-worthy repo work through the completion boundary, or explicitly asking to let /cook take over.",
130
+ "Use normal_prompt for ordinary questions, explanations, analysis-only requests, or direct requests that should stay with the primary agent.",
130
131
  "Use unclear for ambiguous approvals, acknowledgements, or mixed signals where false-positive routing risk is material.",
132
+ "workflow_bias must be exactly one of startup, resume, refocus, next_round, or unknown.",
133
+ "Use startup when there is no active workflow yet, resume when the user is clearly continuing the current workflow, refocus when the user is clearly switching the active workflow to a different goal, and next_round when the previous workflow is done and the user is starting a new round.",
134
+ "When decision is not offer_workflow, prefer workflow_bias=unknown unless a stronger routing hint would still aid debugging.",
131
135
  "confidence must be a number from 0 to 1.",
132
136
  "reason must be a single concise sentence.",
133
137
  "evidence must be an array of short grounded strings.",
134
138
  "riskFlags must be an array of short machine-readable strings such as ambiguous-approval, possible-normal-agent-request, or active-workflow-refocus-risk.",
135
139
  "focusHint is optional, must stay short, and must never rewrite the workflow mission or invent scope.",
140
+ "When explicit user adoption of a recent plan or repo markdown artifact is evident, adopted_artifact may describe it with kind recent_plan|repo_markdown, path when known, and basis explicit_user_adoption.",
141
+ "requires_clarification may be true when chooser-style disambiguation is safer than guessing, and clarification_slots may list short needs such as goal, scope, or non_goal.",
136
142
  "Short acknowledgements like 好, 可以, ok, sure, or 那就這樣 should usually be unclear unless the surrounding context makes the handoff explicit.",
137
143
  ].join(" ");
138
144
  const COOK_TRIGGER_CLASSIFIER_TIMEOUT_MS = 10_000;
@@ -218,7 +224,7 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
218
224
  const rootKey = completionRootKey(undefined, cwd);
219
225
  const prompt = buildContextProposalAnalystPromptFromEntries(projectName, recentEntries, params.workflowContextLines);
220
226
  const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
221
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
227
+ const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
222
228
  const invocation = getPiInvocation(args);
223
229
  const liveActivity = createLiveRoleActivity(STARTUP_ANALYST_ROLE);
224
230
  liveActivity.progress = "Analyzing recent discussion";
@@ -408,15 +414,33 @@ function parseCookTriggerClassification(raw: string): CookTriggerClassification
408
414
  return undefined;
409
415
  }
410
416
  if (!isRecord(parsed)) return undefined;
411
- const intent = asString(parsed.intent);
412
- if (intent !== "route_to_cook" && intent !== "normal_prompt" && intent !== "unclear") return undefined;
417
+ const rawDecision = asString(parsed.decision ?? parsed.intent);
418
+ const decision =
419
+ rawDecision === "offer_workflow" || rawDecision === "normal_prompt" || rawDecision === "unclear"
420
+ ? rawDecision
421
+ : rawDecision === "route_to_cook"
422
+ ? "offer_workflow"
423
+ : undefined;
424
+ if (!decision) return undefined;
425
+ const rawWorkflowBias = asString(parsed.workflow_bias ?? parsed.workflowBias ?? parsed.routing_bias ?? parsed.routingBias);
426
+ const workflowBias =
427
+ rawWorkflowBias === "startup" ||
428
+ rawWorkflowBias === "resume" ||
429
+ rawWorkflowBias === "refocus" ||
430
+ rawWorkflowBias === "next_round" ||
431
+ rawWorkflowBias === "unknown"
432
+ ? rawWorkflowBias
433
+ : decision === "offer_workflow" && rawDecision === "route_to_cook"
434
+ ? "unknown"
435
+ : "unknown";
413
436
  const evidence = localAsStringArray(parsed.evidence);
414
437
  const riskFlags = localAsStringArray(parsed.riskFlags ?? parsed.risk_flags);
415
- const reason = asString(parsed.reason) ?? asString(parsed.rationale) ?? evidence[0] ?? `Classifier returned ${intent}.`;
438
+ const reason = asString(parsed.reason) ?? asString(parsed.rationale) ?? evidence[0] ?? `Classifier returned ${decision}.`;
416
439
  const focusHint = asString(parsed.focusHint ?? parsed.focus_hint);
417
440
  return {
418
- intent,
441
+ decision,
419
442
  confidence: confidenceFromUnknown(parsed.confidence),
443
+ workflowBias,
420
444
  reason,
421
445
  focusHint,
422
446
  evidence: evidence.length > 0 ? evidence : [reason],
@@ -440,7 +464,7 @@ async function runCookTriggerClassifierSubprocess(
440
464
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
441
465
  const modelArg = contextProposalAnalystModelArg(params.ctx.model);
442
466
  const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-trigger-classifier-", COOK_TRIGGER_CLASSIFIER_SYSTEM_PROMPT);
443
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath];
467
+ const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath];
444
468
  if (modelArg) args.push("--model", modelArg);
445
469
  args.push(params.prompt);
446
470
  const invocation = getPiInvocation(args);
@@ -86,19 +86,30 @@ export type CompletionStatusSurface = {
86
86
  liveDetailsLines?: string[];
87
87
  };
88
88
 
89
- export type NaturalLanguageCookTriggerMode = "off" | "assist" | "auto";
90
- export type CookTriggerIntent = "route_to_cook" | "normal_prompt" | "unclear";
89
+ export type NaturalLanguageCookTriggerMode = "off" | "router" | "auto";
90
+ export type CookTriggerClassifierDecision = "offer_workflow" | "normal_prompt" | "unclear";
91
+ export type CookTriggerWorkflowBias = "startup" | "resume" | "refocus" | "next_round" | "unknown";
91
92
 
92
93
  export type CookTriggerClassification = {
93
- intent: CookTriggerIntent;
94
+ decision: CookTriggerClassifierDecision;
94
95
  confidence: number;
96
+ workflowBias: CookTriggerWorkflowBias;
95
97
  reason: string;
96
98
  focusHint?: string;
97
99
  evidence: string[];
98
100
  riskFlags: string[];
99
101
  };
100
102
 
101
- export type CookTriggerConfirmationAction = "start_cook" | "keep_chatting" | "cancel";
103
+ export type CookTriggerConfirmationAction = "start_workflow" | "send_as_normal_chat" | "cancel";
104
+ export type CookTriggerClarificationAction =
105
+ | "route_startup"
106
+ | "route_resume"
107
+ | "route_refocus"
108
+ | "route_next_round"
109
+ | "send_as_normal_chat"
110
+ | "cancel";
111
+ export type CookTriggerRecoveryAction = "retry_routing" | "send_as_normal_chat" | "cancel";
112
+ export type CookTriggerAdoptedArtifactKind = "recent_plan" | "repo_markdown";
102
113
 
103
114
  export type CookTriggerConfirmationActionItem = {
104
115
  id: CookTriggerConfirmationAction;
@@ -106,6 +117,12 @@ export type CookTriggerConfirmationActionItem = {
106
117
  description: string;
107
118
  };
108
119
 
120
+ export type CookTriggerClarificationActionItem = {
121
+ id: CookTriggerClarificationAction;
122
+ label: string;
123
+ description: string;
124
+ };
125
+
109
126
  export type CookTriggerConfirmationLayout = {
110
127
  title: string;
111
128
  intro: string;
@@ -120,6 +137,61 @@ export type CookTriggerConfirmationLayout = {
120
137
  footer: string;
121
138
  };
122
139
 
140
+ export type CookTriggerClarificationCapsule = {
141
+ goal?: string;
142
+ scope?: string[];
143
+ nonGoal?: string[];
144
+ doneWhen?: string[];
145
+ selectedWorkflowBias: CookTriggerWorkflowBias;
146
+ reason: string;
147
+ };
148
+
149
+ export type CookTriggerAdoptedArtifact = {
150
+ kind: CookTriggerAdoptedArtifactKind;
151
+ basis: "explicit_user_adoption";
152
+ title: string;
153
+ path?: string;
154
+ preview?: string;
155
+ };
156
+
157
+ export type CookNaturalLanguageHandoff = {
158
+ preferredRoutingBias?: CookTriggerWorkflowBias;
159
+ triggerText?: string;
160
+ hintText?: string;
161
+ clarificationCapsule?: CookTriggerClarificationCapsule;
162
+ adoptedArtifact?: CookTriggerAdoptedArtifact;
163
+ };
164
+
165
+ export type CookTriggerClarificationLayout = {
166
+ title: string;
167
+ intro: string;
168
+ currentMissionHeading?: string;
169
+ currentMissionBody?: string;
170
+ candidateMissionHeading?: string;
171
+ candidateMissionBody?: string;
172
+ adoptedArtifactHeading?: string;
173
+ adoptedArtifactBody?: string;
174
+ actionsHeading: string;
175
+ actions: CookTriggerClarificationActionItem[];
176
+ footer: string;
177
+ };
178
+
179
+ export type CookTriggerRecoveryActionItem = {
180
+ id: CookTriggerRecoveryAction;
181
+ label: string;
182
+ description: string;
183
+ };
184
+
185
+ export type CookTriggerRecoveryLayout = {
186
+ title: string;
187
+ intro: string;
188
+ failureHeading?: string;
189
+ failureBody?: string;
190
+ actionsHeading: string;
191
+ actions: CookTriggerRecoveryActionItem[];
192
+ footer: string;
193
+ };
194
+
123
195
  export type CookTriggerDecision = {
124
196
  mode: NaturalLanguageCookTriggerMode;
125
197
  action: "continue" | "handled" | "routed_to_cook";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
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,