@linimin/pi-letscook 0.1.54 → 0.1.56

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.
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import * as fs from "node:fs";
3
2
  import { promises as fsp } from "node:fs";
4
3
  import * as os from "node:os";
@@ -39,9 +38,9 @@ import {
39
38
  buildContextProposalConfirmationLayout as buildExtractedContextProposalConfirmationLayout,
40
39
  buildContextProposalConfirmationSelectItems,
41
40
  buildContextProposalContinuationReason as buildExtractedContextProposalContinuationReason,
41
+ buildCookHandoffBoundaryReminder as buildExtractedCookHandoffBoundaryReminder,
42
42
  buildEvaluationRoleContextLines as buildExtractedEvaluationRoleContextLines,
43
43
  buildEvaluationRoleReminderText as buildExtractedEvaluationRoleReminderText,
44
- buildNaturalLanguageHandoffMetadataLines,
45
44
  buildResumeCapsule as buildExtractedResumeCapsule,
46
45
  buildSystemReminder as buildExtractedSystemReminder,
47
46
  maybeWriteContextProposalConfirmationSnapshot,
@@ -76,11 +75,9 @@ import {
76
75
  readText,
77
76
  scaffoldCompletionFiles as scaffoldCompletionFilesOnDisk,
78
77
  } from "./state-store";
79
- import { parseFirstNumber, parseYesNo } from "./transcription";
80
78
  import type { TranscriptionResult } from "./transcription";
81
- import type { CompletionStateSnapshot, CompletionRole, CookNaturalLanguageHandoff, JsonRecord, LiveRoleActivity } from "./types";
79
+ import type { CompletionStateSnapshot, CompletionRole, JsonRecord, LiveRoleActivity } from "./types";
82
80
 
83
- const PROTOCOL_ID = "completion";
84
81
  const ROLE_NAMES = [
85
82
  "completion-bootstrapper",
86
83
  "completion-regrounder",
@@ -123,10 +120,6 @@ function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
123
120
  return Array.isArray(slices) ? slices.filter(isRecord) : [];
124
121
  }
125
122
 
126
- type ExistingWorkflowDecision =
127
- | { action: "continue"; currentMissionAnchor: string }
128
- | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
129
-
130
123
  type ActiveWorkflowProposalAssessment = {
131
124
  action: "continue" | "refocus" | "unclear";
132
125
  currentMissionAnchor: string;
@@ -134,13 +127,6 @@ type ActiveWorkflowProposalAssessment = {
134
127
  reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
135
128
  };
136
129
 
137
- type ExistingWorkflowChooserOptions = {
138
- intro?: string;
139
- proposedMissionLabel?: string;
140
- refocusChoiceLabel?: string;
141
- comparison?: "semantic" | "strict";
142
- };
143
-
144
130
  function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
145
131
  const raw = process.env.PI_COMPLETION_EXISTING_WORKFLOW_ACTION?.trim().toLowerCase();
146
132
  return raw === "continue" || raw === "refocus" || raw === "cancel" ? raw : undefined;
@@ -196,6 +182,10 @@ function completionTestSystemReminderPath(): string | undefined {
196
182
  return asString(process.env.PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH);
197
183
  }
198
184
 
185
+ function completionTestCookHandoffReminderPath(): string | undefined {
186
+ return asString(process.env.PI_COMPLETION_TEST_COOK_HANDOFF_REMINDER_PATH);
187
+ }
188
+
199
189
  function maybeWriteTestSnapshot(targetPath: string | undefined, content: string): void {
200
190
  if (!targetPath) return;
201
191
  try {
@@ -207,26 +197,8 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
207
197
  }
208
198
 
209
199
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
210
- const COOK_BARE_ONLY_GUIDANCE =
211
- "/cook is the canonical workflow boundary. Discuss the concrete repo changes in the main chat, then run /cook when you want to start, continue, refocus, or begin the next workflow round.";
212
200
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
213
- "/cook failed closed because recent discussion did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
214
-
215
- function buildCookCancellationMessage(prefix: string): string {
216
- return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
217
- }
218
-
219
- function buildCookStructuredDiscussionFailureMessage(prefix?: string): string {
220
- return prefix ? `${prefix} ${COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL}` : COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL;
221
- }
222
-
223
- function shouldDisableContextProposalAnalyst(): boolean {
224
- return process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1";
225
- }
226
-
227
- function completionTestContextProposalAnalystOutput(): string | undefined {
228
- return asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT);
229
- }
201
+ "/cook failed closed because recent discussion did not produce a clear execution-ready startup brief with Mission/Scope/Constraints/Acceptance for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
230
202
 
231
203
  function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
232
204
  return asString(snapshot?.state?.continuation_policy) === "done";
@@ -258,6 +230,19 @@ function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot
258
230
  return hasCompletionRoutingActivation(snapshot) && isCompletionDriverPromptTurn(ctx);
259
231
  }
260
232
 
233
+ function shouldInjectCookHandoffBoundary(event: { prompt?: string }, ctx: { sessionManager?: any }): boolean {
234
+ if (roleFromEnv()) return false;
235
+ if (isCompletionDriverPromptTurn(ctx)) return false;
236
+ const prompt = typeof event.prompt === "string" ? event.prompt.trim() : "";
237
+ if (!prompt) return false;
238
+ if (prompt.startsWith("/")) return false;
239
+ return true;
240
+ }
241
+
242
+ function buildCookHandoffBoundaryReminder(): string {
243
+ return buildExtractedCookHandoffBoundaryReminder();
244
+ }
245
+
261
246
  function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): string {
262
247
  const missionAnchor = asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? "(unknown)";
263
248
  const continuationReason = asString(snapshot.state?.continuation_reason) ?? "(unknown)";
@@ -378,7 +363,6 @@ async function promptContextProposalConfirmationAction(
378
363
  async function deriveCookContextProposal(
379
364
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
380
365
  projectName: string,
381
- hintText?: string,
382
366
  ): Promise<ContextProposal | undefined> {
383
367
  const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
384
368
  const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
@@ -394,11 +378,9 @@ async function deriveCookContextProposal(
394
378
  `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
395
379
  ]
396
380
  : [];
397
- if (hintText) workflowContextLines.push(`cook hint: ${hintText}`);
398
381
  return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
399
382
  asString,
400
383
  asStringArray,
401
- hintText,
402
384
  workflowContext: snapshot
403
385
  ? {
404
386
  currentMissionAnchor:
@@ -412,15 +394,12 @@ async function deriveCookContextProposal(
412
394
  continuationPolicy: asString(snapshot.state?.continuation_policy),
413
395
  }
414
396
  : undefined,
415
- analyzeContextProposal: async (entries, derivedHintText) =>
397
+ analyzeContextProposal: async (entries) =>
416
398
  await analyzeContextProposalWithAgent({
417
399
  ctx,
418
400
  projectName,
419
401
  recentEntries: entries,
420
- workflowContextLines:
421
- derivedHintText && !workflowContextLines.includes(`cook hint: ${derivedHintText}`)
422
- ? [...workflowContextLines, `cook hint: ${derivedHintText}`]
423
- : workflowContextLines,
402
+ workflowContextLines,
424
403
  liveRoleActivityByRoot,
425
404
  completionStatusKey: COMPLETION_STATUS_KEY,
426
405
  safeUiCall,
@@ -470,12 +449,13 @@ async function confirmContextProposal(
470
449
  async function scaffoldCompletionFiles(
471
450
  root: string,
472
451
  missionAnchor: string,
473
- options?: { analysis?: ContextProposalAnalysis; continuationReason?: string },
452
+ options?: { analysis?: ContextProposalAnalysis; continuationReason?: string; advisoryStartupBrief?: JsonRecord },
474
453
  ) {
475
454
  const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
476
455
  return await scaffoldCompletionFilesOnDisk(root, missionAnchor, {
477
456
  analysis: { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile },
478
457
  continuationReason: options?.continuationReason,
458
+ advisoryStartupBrief: options?.advisoryStartupBrief,
479
459
  });
480
460
  }
481
461
 
@@ -875,45 +855,24 @@ function composeResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: J
875
855
  });
876
856
  }
877
857
 
878
- async function gitHeadSha(cwd: string): Promise<string | undefined> {
879
- return await new Promise((resolve) => {
880
- const proc = spawn("git", ["rev-parse", "HEAD"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
881
- let stdout = "";
882
- proc.stdout.on("data", (chunk) => {
883
- stdout += chunk.toString();
884
- });
885
- proc.on("close", (code) => {
886
- resolve(code === 0 ? asString(stdout) : undefined);
887
- });
888
- proc.on("error", () => resolve(undefined));
889
- });
890
- }
891
-
892
858
  function completionKickoff(
893
859
  goal: string,
894
860
  taskType: string,
895
861
  evaluationProfile: string,
896
862
  intent: "auto" | "continue" | "refocus" = "auto",
897
863
  missionAnchor?: string,
898
- naturalLanguageHandoff?: CookNaturalLanguageHandoff,
899
864
  ): string {
900
- const naturalLanguageHandoffBlock = buildNaturalLanguageHandoffMetadataLines(naturalLanguageHandoff).join("\n");
901
865
  const intentBlock =
902
866
  intent === "continue" && missionAnchor
903
867
  ? `Existing canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- Continue the existing workflow.\n- Treat the new user text as supplemental direction unless canonical reconciliation proves the mission itself must change.\n\n`
904
868
  : intent === "refocus" && missionAnchor
905
869
  ? `Updated canonical mission anchor:\n${missionAnchor}\n\nWorkflow intent:\n- The user explicitly refocused the workflow before this kickoff.\n- Re-read canonical .agent/** state and continue from the refocused mission.\n\n`
906
870
  : "";
907
- return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${naturalLanguageHandoffBlock}${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
871
+ return `/skill:completion-protocol Start or continue the completion workflow for this repo.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nUser goal:\n${goal}\n\n${intentBlock}Driver instructions:\n- Canonical truth is in .agent/**. Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting when they exist.\n- If tracked completion contract files are missing or onboarding is required, invoke completion_role with role completion-bootstrapper.\n- Otherwise follow the mandatory dispatch rules from completion-protocol.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Use completion_role for all completion-* role work. Do not directly implement tracked product changes yourself.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
908
872
  }
909
873
 
910
- function completionResumePrompt(
911
- taskType: string,
912
- evaluationProfile: string,
913
- naturalLanguageHandoff?: CookNaturalLanguageHandoff,
914
- ): string {
915
- const naturalLanguageHandoffBlock = buildNaturalLanguageHandoffMetadataLines(naturalLanguageHandoff).join("\n");
916
- return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\n${naturalLanguageHandoffBlock}Resume instructions:\n- Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
874
+ function completionResumePrompt(taskType: string, evaluationProfile: string): string {
875
+ return `/skill:completion-protocol Resume the completion workflow from canonical state.\n\nBefore acting, read:\n- ${SKILL_PATH}\n- ${REFERENCE_PATH}\n\nCanonical routing profile:\n- task_type: ${taskType}\n- evaluation_profile: ${evaluationProfile}\n\nResume instructions:\n- Re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, and .agent/verification-evidence.json before acting.\n- If canonical state is missing, invalid, contradictory, stale, or ambiguous, route to completion-regrounder first.\n- For selected, in-progress, committed, or done slices, treat .agent/active-slice.json as the canonical implementation contract and route to completion-regrounder if it drifts from the selected plan slice or the exact handoff is unclear.\n- Consume .agent/verification-evidence.json instead of temp-only verification summaries when it is populated.\n- Continue from next_mandatory_role and next_mandatory_action.\n- Use completion_role for all completion-* role work.\n- Continue dispatching mandatory roles while continuation_policy == continue.\n- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.`;
917
876
  }
918
877
 
919
878
  export default function completionExtension(pi: ExtensionAPI) {
@@ -926,11 +885,10 @@ export default function completionExtension(pi: ExtensionAPI) {
926
885
  getCtxUi,
927
886
  };
928
887
  const driverDeps = {
929
- bareOnlyGuidance: COOK_BARE_ONLY_GUIDANCE,
930
888
  structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
931
889
  mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
932
890
  cookCommandSpec: {
933
- description: "/cook workflow: start, continue, refocus, or start the next round from an explicit /cook command",
891
+ description: "/cook workflow: derive a startup brief from recent discussion, then start, continue, refocus, or start the next round from the explicit /cook command",
934
892
  },
935
893
  buildContextProposalContinuationReason,
936
894
  completionKickoff,
@@ -986,7 +944,7 @@ export default function completionExtension(pi: ExtensionAPI) {
986
944
  }
987
945
  });
988
946
 
989
- pi.on("before_agent_start", async (_event, ctx) => {
947
+ pi.on("before_agent_start", async (event, ctx) => {
990
948
  const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
991
949
  const driverPromptTurn = isCompletionDriverPromptTurn(ctx);
992
950
  if (loaded && driverPromptTurn) {
@@ -994,28 +952,35 @@ export default function completionExtension(pi: ExtensionAPI) {
994
952
  const fingerprint = completionContinuationFingerprint(loaded.snapshot);
995
953
  if (fingerprint) markQueuedDriverPromptInFlight(rootKey, fingerprint);
996
954
  }
997
- if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot, ctx)) return;
998
- const additions = isWorkflowDone(loaded.snapshot)
999
- ? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
1000
- : [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
1001
- if (!isWorkflowDone(loaded.snapshot)) {
1002
- const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
1003
- let marker: JsonRecord | undefined;
1004
- if (markerText) {
1005
- try {
1006
- const parsed = JSON.parse(markerText);
1007
- marker = isRecord(parsed) ? parsed : undefined;
1008
- } catch {
1009
- marker = undefined;
955
+ const systemPrompt = getSystemPromptSafe(ctx);
956
+ if (!systemPrompt) return;
957
+ if (loaded && shouldInjectCompletionWorkflowContext(loaded.snapshot, ctx)) {
958
+ const additions = isWorkflowDone(loaded.snapshot)
959
+ ? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
960
+ : [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
961
+ if (!isWorkflowDone(loaded.snapshot)) {
962
+ const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
963
+ let marker: JsonRecord | undefined;
964
+ if (markerText) {
965
+ try {
966
+ const parsed = JSON.parse(markerText);
967
+ marker = isRecord(parsed) ? parsed : undefined;
968
+ } catch {
969
+ marker = undefined;
970
+ }
1010
971
  }
972
+ if (marker) additions.push(buildPostCompactionDriverInstructions(loaded.snapshot, marker));
1011
973
  }
1012
- if (marker) additions.push(buildPostCompactionDriverInstructions(loaded.snapshot, marker));
974
+ maybeWriteTestSnapshot(completionTestSystemReminderPath(), additions.join("\n\n"));
975
+ return {
976
+ systemPrompt: `${systemPrompt}\n\n${additions.join("\n\n")}`,
977
+ };
1013
978
  }
1014
- maybeWriteTestSnapshot(completionTestSystemReminderPath(), additions.join("\n\n"));
1015
- const systemPrompt = getSystemPromptSafe(ctx);
1016
- if (!systemPrompt) return;
979
+ if (!shouldInjectCookHandoffBoundary(event, ctx)) return;
980
+ const handoffReminder = buildCookHandoffBoundaryReminder();
981
+ maybeWriteTestSnapshot(completionTestCookHandoffReminderPath(), handoffReminder);
1017
982
  return {
1018
- systemPrompt: `${systemPrompt}\n\n${additions.join("\n\n")}`,
983
+ systemPrompt: `${systemPrompt}\n\n${handoffReminder}`,
1019
984
  };
1020
985
  });
1021
986