@linimin/pi-letscook 0.1.48 → 0.1.50

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,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.50
6
+
7
+ ### Changed
8
+
9
+ - simplified the README opening so people can tell at a glance whether this extension helps with their workflow, while preserving the existing `/cook` behavior and release-parity guidance
10
+
11
+ ## 0.1.49
12
+
13
+ ### Changed
14
+
15
+ - restored optional `/cook <hint>` support as a soft intent hint that biases context analysis, proposal ranking, active-workflow disambiguation, and next-round startup without bypassing fail-closed routing or the approval-only Start/Cancel gate
16
+
5
17
  ## 0.1.48
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # @linimin/pi-letscook
2
2
 
3
- A Pi extension that turns `/cook` into a discussion-driven repo-local workflow command for long-running coding work.
3
+ A Pi extension for long-running repo work.
4
4
 
5
- ## Why this exists
5
+ It gives you `/cook`: a discussion-driven workflow command that keeps mission, progress, and verification in repo-local `.agent/**` state instead of chat memory.
6
6
 
7
- Normal chat is good for one-off tasks. It is much worse for work that needs to:
7
+ ## Is this for you?
8
8
 
9
+ **Useful if your work needs to:**
9
10
  - continue across sessions
10
11
  - stay anchored to one mission
11
12
  - resume from repo state instead of chat memory
12
13
  - keep review, audit, and verification tied to the repo
13
14
 
14
- `@linimin/pi-letscook` solves that by storing canonical workflow state in `.agent/**` and using `/cook` as one discussion-first command to start, continue, refocus, or advance the workflow.
15
+ **Probably overkill if you mostly do:**
16
+ - one-off chat tasks
17
+ - brainstorming
18
+ - planning docs without immediate implementation
15
19
 
16
20
  ## What you get
17
21
 
@@ -32,32 +36,39 @@ Then run `/reload` in Pi.
32
36
 
33
37
  ## Quick start
34
38
 
35
- Primary entrypoint:
39
+ `/cook` supports both bare discussion-driven startup and optional inline intent hints.
36
40
 
37
41
  ```text
38
42
  /cook
43
+ /cook login redirect
39
44
  ```
40
45
 
41
- Use bare `/cook` after you discuss the mission in the main chat. The same command can:
46
+ Use `/cook` after you discuss the mission in the main chat.
42
47
 
48
+ What it can do:
43
49
  - start a brand-new workflow from recent discussion
44
50
  - continue the current workflow when recent discussion still matches it, or when discussion is too weak or ambiguous to justify a refocus
45
51
  - surface a conservative refocus chooser when recent discussion clearly points to a different workflow
46
52
  - start the next workflow round after the previous one is `done`
47
53
 
48
- `/cook` expects recent main-chat discussion to describe concrete repo changes. README/CHANGELOG updates still count as concrete repo changes, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not. `/cook <text>` is no longer supported; put mission text in the main chat, then rerun bare `/cook`.
54
+ What it expects:
55
+ - recent main-chat discussion about concrete repo changes
56
+ - README/CHANGELOG updates still count as concrete repo changes
57
+ - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not
58
+
59
+ `/cook <hint>` acts as a high-priority intent hint that helps proposal derivation interpret the recent discussion, but it still goes through the same fail-closed routing and approval-only Start/Cancel confirmation flow.
49
60
 
50
61
  On startup and next-round flows, if 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`.
51
62
 
52
63
  ## How `/cook` works
53
64
 
54
- Bare `/cook` is the only supported workflow entrypoint.
65
+ `/cook` supports both bare discussion-driven startup and optional inline intent hints.
55
66
 
56
67
  | Repo state | `/cook` behavior |
57
68
  |---|---|
58
- | No workflow yet | Summarizes recent main-chat discussion into a startup proposal, weighting the latest clear implementation intent ahead of older background discussion, then asks for approval with **Start** or **Cancel**. If the discussion is weak, ambiguous, assistant-produced, or only a plan/spec/design-doc/proposal artifact instead of concrete repo changes, `/cook` fails closed without writing `.agent/**` state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. |
59
- | Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. If recent discussion implies more than one plausible replacement mission, `/cook` keeps the current workflow parked behind a multi-candidate chooser instead of silently resuming or guessing. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. `/cook <text>` is rejected without running proposal routing or rewriting workflow state. |
60
- | Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. Recent discussion that only restates already-completed or already-verified work also fails closed instead of reopening the finished mission. `/cook <text>` is rejected before any next-round proposal is derived. |
69
+ | No workflow yet | Summarizes recent main-chat discussion into a startup proposal, weighting the latest clear implementation intent ahead of older background discussion. Optional `/cook <hint>` text is treated as a high-priority cue for how to interpret that discussion, not as an unconditional mission override. The result still asks for approval with **Start** or **Cancel**. If the discussion is weak, ambiguous, assistant-produced, or only a plan/spec/design-doc/proposal artifact instead of concrete repo changes, `/cook` fails closed without writing `.agent/**` state and tells you to clarify the mission in the main chat before rerunning `/cook`. |
70
+ | Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. If recent discussion implies more than one plausible replacement mission, `/cook` keeps the current workflow parked behind a multi-candidate chooser instead of silently resuming or guessing. Optional `/cook <hint>` text biases that routing and candidate ranking toward the hinted implementation intent without bypassing the chooser or final confirmation. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. |
71
+ | Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Optional `/cook <hint>` text biases next-round proposal derivation toward the hinted intent while still preserving fail-closed behavior. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning `/cook`. Recent discussion that only restates already-completed or already-verified work also fails closed instead of reopening the finished mission. |
61
72
 
62
73
  ## Approval-only confirmation and fail-closed behavior
63
74
 
@@ -67,7 +78,7 @@ All startup, next-round, and replacement proposals are **approval-only**:
67
78
  - actions are only **Start** and **Cancel**
68
79
  - **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
69
80
 
70
- When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion, it fails closed instead of guessing. That means no canonical `.agent/**` state is created or rewritten until the discussion is clarified in the main chat and you rerun `/cook`. Tracked docs-only work such as README/CHANGELOG updates is still execution-ready, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts are not enough to start or refocus a workflow on their own. `/cook <text>` also fails closed immediately and tells you to move that text into the main chat before rerunning bare `/cook`.
81
+ When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion, it fails closed instead of guessing. That means no canonical `.agent/**` state is created or rewritten until the discussion is clarified in the main chat and you rerun `/cook`. Tracked docs-only work such as README/CHANGELOG updates is still execution-ready, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts are not enough to start or refocus a workflow on their own. Optional `/cook <hint>` text can bias proposal ranking, but it still fails closed when repo truth or recent discussion does not support a clear executable mission.
71
82
 
72
83
  When an active workflow already exists and recent discussion suggests a different workflow, `/cook` shows a separate chooser first. The chooser can stay conservative or list multiple candidate replacements when the latest discussion contains more than one plausible implementation goal:
73
84
 
@@ -106,7 +106,7 @@ export type CompletionDriverDeps = {
106
106
  missionAnchor?: string,
107
107
  ) => string;
108
108
  completionResumePrompt: (taskType: string, evaluationProfile: string) => string;
109
- deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<ContextProposal | undefined>;
109
+ deriveCookContextProposal: (ctx: DriverContext, projectName: string, hintText?: string) => Promise<ContextProposal | undefined>;
110
110
  confirmContextProposal: (
111
111
  ctx: { hasUI: boolean; ui: any },
112
112
  proposal: ContextProposal,
@@ -325,10 +325,11 @@ async function assessActiveWorkflowProposalRouting(
325
325
  ctx: DriverContext,
326
326
  snapshot: CompletionStateSnapshot,
327
327
  deps: CompletionDriverDeps,
328
+ hintText?: string,
328
329
  ): Promise<ActiveWorkflowProposalAssessment> {
329
330
  const currentMission = currentMissionAnchor(snapshot);
330
331
  const projectName = path.basename(snapshot.files.root);
331
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
332
+ const proposal = await deps.deriveCookContextProposal(ctx, projectName, hintText);
332
333
  if (!proposal) {
333
334
  const assessment: ActiveWorkflowProposalAssessment = {
334
335
  action: "unclear",
@@ -533,10 +534,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
533
534
  pi.registerCommand("cook", {
534
535
  description: deps.cookCommandSpec.description,
535
536
  handler: async (args, ctx) => {
536
- if (args.trim().length > 0) {
537
- deps.emitCommandText(ctx, deps.bareOnlyGuidance, "info");
538
- return;
539
- }
537
+ const explicitHint = args.trim().length > 0 ? args.trim() : undefined;
540
538
  let goal: string | undefined;
541
539
  const cwd = deps.getCtxCwd(ctx);
542
540
  let snapshot = await loadCompletionSnapshot(cwd);
@@ -548,7 +546,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
548
546
  if (!snapshot) {
549
547
  const root = findRepoRoot(cwd) ?? cwd;
550
548
  const projectName = path.basename(root);
551
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
549
+ const proposal = await deps.deriveCookContextProposal(ctx, projectName, explicitHint);
552
550
  if (!proposal) {
553
551
  deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps), "info");
554
552
  return;
@@ -587,7 +585,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
587
585
  if (!goal) {
588
586
  if (workflowDone) {
589
587
  const projectName = path.basename(snapshot.files.root);
590
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
588
+ const proposal = await deps.deriveCookContextProposal(ctx, projectName, explicitHint);
591
589
  if (!proposal) {
592
590
  deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps, "The previous completion workflow is already done."), "info");
593
591
  return;
@@ -606,7 +604,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
606
604
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
607
605
  deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
608
606
  } else {
609
- const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
607
+ const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps, explicitHint);
610
608
  if (!assessment.proposal || assessment.action === "continue") {
611
609
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
612
610
  return;
@@ -207,7 +207,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
207
207
 
208
208
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
209
209
  const COOK_BARE_ONLY_GUIDANCE =
210
- "/cook only supports the bare /cook entrypoint. Move mission text into the main chat, then rerun /cook.";
210
+ "/cook supports optional inline hints as high-priority intent cues, but mission selection still comes from recent discussion, repo truth, and the approval-only confirmation flow.";
211
211
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
212
212
  "/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.";
213
213
 
@@ -377,6 +377,7 @@ async function promptContextProposalConfirmationAction(
377
377
  async function deriveCookContextProposal(
378
378
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
379
379
  projectName: string,
380
+ hintText?: string,
380
381
  ): Promise<ContextProposal | undefined> {
381
382
  const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
382
383
  const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
@@ -392,9 +393,11 @@ async function deriveCookContextProposal(
392
393
  `verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
393
394
  ]
394
395
  : [];
396
+ if (hintText) workflowContextLines.push(`cook hint: ${hintText}`);
395
397
  return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
396
398
  asString,
397
399
  asStringArray,
400
+ hintText,
398
401
  workflowContext: snapshot
399
402
  ? {
400
403
  currentMissionAnchor:
@@ -408,12 +411,15 @@ async function deriveCookContextProposal(
408
411
  continuationPolicy: asString(snapshot.state?.continuation_policy),
409
412
  }
410
413
  : undefined,
411
- analyzeContextProposal: async (entries) =>
414
+ analyzeContextProposal: async (entries, derivedHintText) =>
412
415
  await analyzeContextProposalWithAgent({
413
416
  ctx,
414
417
  projectName,
415
418
  recentEntries: entries,
416
- workflowContextLines,
419
+ workflowContextLines:
420
+ derivedHintText && !workflowContextLines.includes(`cook hint: ${derivedHintText}`)
421
+ ? [...workflowContextLines, `cook hint: ${derivedHintText}`]
422
+ : workflowContextLines,
417
423
  liveRoleActivityByRoot,
418
424
  completionStatusKey: COMPLETION_STATUS_KEY,
419
425
  safeUiCall,
@@ -916,7 +922,7 @@ export default function completionExtension(pi: ExtensionAPI) {
916
922
  structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
917
923
  mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
918
924
  cookCommandSpec: {
919
- description: "Bare /cook workflow: start, continue, refocus, or start the next round",
925
+ description: "/cook workflow: start, continue, refocus, or start the next round (optional hint supported)",
920
926
  },
921
927
  buildContextProposalContinuationReason,
922
928
  completionKickoff,
@@ -201,6 +201,7 @@ export function buildContextProposalAnalystPrompt(projectName: string, discussio
201
201
  "Infer the current implementation mission from the discussion.",
202
202
  "Prefer the latest clear user implementation intent over older background context.",
203
203
  "Treat stale, completed, or explicitly negated topics as context to ignore unless the latest discussion clearly reopens them.",
204
+ "If canonical workflow context includes a /cook hint, use it as a high-priority cue for how to interpret the recent discussion without treating it as an unconditional override.",
204
205
  ];
205
206
  if (contextLines.length > 0) lines.push("", "Canonical workflow context:", ...contextLines);
206
207
  lines.push("", "Recent discussion:", discussion || "(none)");
@@ -342,6 +342,11 @@ export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[
342
342
  .join("\n\n");
343
343
  }
344
344
 
345
+ function contextHintEntry(hintText: string | undefined): RecentDiscussionEntry[] {
346
+ const normalized = normalizeProposalLine(hintText ?? "");
347
+ return normalized ? [{ role: "user", text: `Hint: ${normalized}` }] : [];
348
+ }
349
+
345
350
  const RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX =
346
351
  /(?:\b(?:fix|update|add|remove|restore|refactor|ship|support|wire|route|rewrite|replace|preserve|filter|separate|refresh|reroute|suppress|align|convert|reconcile|repair|correct|implement|build|land|block|allow|keep|edit|document|write)\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|過濾|过滤|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
347
352
 
@@ -716,13 +721,63 @@ function missionTextOverlapsTopic(mission: string, topic: string): boolean {
716
721
  return overlap.length >= Math.min(2, Math.min(missionTokens.length, topicTokens.length));
717
722
  }
718
723
 
719
- function proposalOverlapsTopic(proposal: ContextProposal, topic: string): boolean {
724
+ function proposalOverlapsTopic(proposal: ContextProposal | ContextProposalAlternate, topic: string): boolean {
720
725
  if (!topic.trim()) return false;
721
726
  if (missionTextOverlapsTopic(proposal.mission, topic)) return true;
722
727
  const bodyTexts = [proposal.basisPreview, ...proposal.scope, ...proposal.constraints, ...proposal.acceptance].filter(Boolean);
723
728
  return bodyTexts.some((text) => missionTextOverlapsTopic(text, topic) || missionTextOverlapsTopic(topic, text));
724
729
  }
725
730
 
731
+ function hintOverlapScore(text: string, hintText: string): number {
732
+ const normalizedText = normalizeMissionAnchorText(text).toLowerCase();
733
+ const normalizedHint = normalizeMissionAnchorText(hintText).toLowerCase();
734
+ if (!normalizedText || !normalizedHint) return 0;
735
+ if (normalizedText === normalizedHint) return 10;
736
+ if (normalizedText.includes(normalizedHint) || normalizedHint.includes(normalizedText)) return 6;
737
+ const textTokens = missionAnchorSemanticTokens(normalizedText);
738
+ const hintTokens = missionAnchorSemanticTokens(normalizedHint);
739
+ if (textTokens.length === 0 || hintTokens.length === 0) return 0;
740
+ const hintSet = new Set(hintTokens);
741
+ const overlap = textTokens.filter((token) => hintSet.has(token));
742
+ if (overlap.length === 0) return 0;
743
+ return overlap.length / Math.max(textTokens.length, hintTokens.length);
744
+ }
745
+
746
+ function proposalHintScore(proposal: ContextProposal | ContextProposalAlternate, hintText: string): number {
747
+ return (
748
+ hintOverlapScore(proposal.mission, hintText) * 4 +
749
+ proposal.scope.reduce((sum, item) => sum + hintOverlapScore(item, hintText) * 2, 0) +
750
+ proposal.constraints.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0) +
751
+ proposal.acceptance.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0)
752
+ );
753
+ }
754
+
755
+ function selectHintPreferredProposal(proposal: ContextProposal | undefined, hintText: string | undefined): ContextProposal | undefined {
756
+ if (!proposal || !hintText) return proposal;
757
+ const candidates = [proposal, ...(proposal.alternateProposals ?? [])].filter((candidate, index, list) =>
758
+ list.findIndex((other) => missionAnchorsStrictlyEquivalent(other.mission, candidate.mission)) === index,
759
+ );
760
+ if (candidates.length <= 1) return proposal;
761
+ const scored = candidates.map((candidate, index) => ({ candidate, index, score: proposalHintScore(candidate, hintText) }));
762
+ const best = scored.reduce((current, item) => (item.score > current.score ? item : current), scored[0]);
763
+ if (best.score <= 0 || best.index === 0) return proposal;
764
+ const selected = best.candidate;
765
+ const alternates = candidates
766
+ .filter((_, index) => index !== best.index)
767
+ .map((candidate) => ({ ...candidate, analysis: finalizeContextProposalAnalysis(candidate.analysis, [candidate.goalText, candidate.mission]) }));
768
+ return {
769
+ ...selected,
770
+ alternateProposals: alternates,
771
+ analysis: finalizeContextProposalAnalysis(
772
+ {
773
+ ...selected.analysis,
774
+ alternateMissions: alternates.map((candidate) => candidate.mission),
775
+ },
776
+ [selected.goalText, selected.mission, hintText, ...alternates.map((candidate) => candidate.mission)],
777
+ ),
778
+ };
779
+ }
780
+
726
781
  function extractSuppressedNegatedTopics(proposal: ContextProposal): string[] {
727
782
  return uniqueProposalItems(
728
783
  proposal.constraints.filter((item) => looksLikeConstraint(item) && CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalizeProposalLine(item))),
@@ -1186,20 +1241,25 @@ export async function deriveCookContextProposalFromRecentDiscussion(
1186
1241
  projectName: string,
1187
1242
  recentEntries: RecentDiscussionEntry[],
1188
1243
  deps: ProposalParseDeps & {
1189
- analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]) => Promise<ContextProposal | undefined>;
1244
+ analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[], hintText?: string) => Promise<ContextProposal | undefined>;
1190
1245
  workflowContext?: ContextProposalWorkflowContext;
1246
+ hintText?: string;
1191
1247
  },
1192
1248
  ): Promise<ContextProposal | undefined> {
1193
- if (recentEntries.length === 0) return undefined;
1194
- for (const candidateEntries of recentDiscussionWindows(recentEntries, deps.stripCodeBlocks)) {
1195
- const analyzed = applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries), deps.workflowContext, deps);
1249
+ const effectiveEntries = [...contextHintEntry(deps.hintText), ...recentEntries];
1250
+ if (effectiveEntries.length === 0) return undefined;
1251
+ for (const candidateEntries of recentDiscussionWindows(effectiveEntries, deps.stripCodeBlocks)) {
1252
+ const analyzed = selectHintPreferredProposal(
1253
+ applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries, deps.hintText), deps.workflowContext, deps) ?? undefined,
1254
+ deps.hintText,
1255
+ );
1196
1256
  if (analyzed) return analyzed;
1197
1257
  const structured = applyWorkflowContextToProposal(
1198
1258
  extractContextProposalFromStructuredSession(candidateEntries, projectName, deps),
1199
1259
  deps.workflowContext,
1200
1260
  deps,
1201
1261
  );
1202
- if (structured) return structured;
1262
+ if (structured) return selectHintPreferredProposal(structured, deps.hintText);
1203
1263
  }
1204
1264
  return undefined;
1205
1265
  }
@@ -79,6 +79,7 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
79
79
  "You may additionally include optional keys alternate_missions, completed_topics, and negated_topics when they are clearly supported by the discussion and canonical workflow context.",
80
80
  "mission must be a concise implementation mission anchor sentence.",
81
81
  "Prefer the latest clear user implementation intent over older background context when they differ.",
82
+ "If canonical workflow context includes a /cook hint, treat it as a high-priority disambiguation signal, but do not let it bypass clear contradictory repo truth or approval-only confirmation.",
82
83
  "Do not reopen work that the canonical workflow context says is done, completed, historical, or already covered unless the latest discussion clearly asks to revisit it.",
83
84
  "Treat stale, weakly related, or explicitly negated topics as noise instead of mission scope.",
84
85
  "scope must contain only work items that directly support the mission.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
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,
@@ -1101,8 +1101,8 @@ assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_re
1101
1101
  assert active['status'] == 'idle', 'active-slice should reset to idle for the next workflow round'
1102
1102
  PY
1103
1103
 
1104
- # Active workflow: /cook <text> should be rejected before routing or proposal confirmation
1105
- # and still leave canonical state unchanged.
1104
+ # Active workflow: /cook <hint> should bias active-workflow proposal derivation,
1105
+ # still route through the chooser, and leave canonical state unchanged when the user cancels.
1106
1106
  ACTIVE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-active-inline-arg-routing.json"
1107
1107
  ACTIVE_INLINE_REJECTION_PROPOSAL="$TMPDIR/context-proposal-active-inline-arg-proposal.json"
1108
1108
  ACTIVE_INLINE_REJECTION_CHOOSER="$TMPDIR/context-proposal-active-inline-arg-chooser.json"
@@ -1138,9 +1138,9 @@ import sys
1138
1138
  from pathlib import Path
1139
1139
 
1140
1140
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
1141
- routing = Path(sys.argv[3])
1141
+ routing = json.loads(Path(sys.argv[3]).read_text())
1142
1142
  proposal = Path(sys.argv[4])
1143
- chooser = Path(sys.argv[5])
1143
+ chooser = json.loads(Path(sys.argv[5]).read_text())
1144
1144
  before = json.loads(Path(sys.argv[6]).read_text())
1145
1145
  tracked = [
1146
1146
  Path('.agent/mission.md'),
@@ -1151,16 +1151,17 @@ tracked = [
1151
1151
  Path('.agent/verification-evidence.json'),
1152
1152
  ]
1153
1153
 
1154
- assert '/cook only supports the bare /cook entrypoint.' in output, 'active /cook <text> rejection should explain the bare-only contract'
1155
- assert not routing.exists(), 'active /cook <text> rejection should not run active-workflow routing'
1156
- assert not proposal.exists(), 'active /cook <text> rejection should not open proposal confirmation'
1157
- assert not chooser.exists(), 'active /cook <text> rejection should not open the existing-workflow chooser'
1154
+ assert routing['action'] == 'refocus', 'active /cook <hint> should run active-workflow routing'
1155
+ assert routing['proposedMissionAnchor'] == 'Replacement mission for the active workflow.', 'active /cook <hint> should bias toward the hinted replacement mission'
1156
+ assert json.loads(proposal.read_text())['mission'] == 'Replacement mission for the active workflow.', 'active /cook <hint> should carry the hinted mission into the final replacement proposal'
1157
+ assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'active /cook <hint> should open the existing-workflow chooser'
1158
+ assert 'Cancelled replacement workflow proposal.' in output or 'Cancelled existing workflow confirmation.' in output, 'active /cook <hint> cancel should report cancellation'
1158
1159
  after = {path.name: path.read_text() for path in tracked}
1159
- assert before == after, 'active /cook <text> rejection should leave canonical files unchanged'
1160
+ assert before == after, 'active /cook <hint> cancel should leave canonical files unchanged'
1160
1161
  PY
1161
1162
 
1162
- # Completed workflow: /cook <text> should be rejected before any next-round proposal is derived
1163
- # and still leave canonical state unchanged.
1163
+ # Completed workflow: /cook <hint> should bias next-round proposal derivation and still leave canonical state
1164
+ # unchanged when the user cancels the approval-only proposal.
1164
1165
  mark_done
1165
1166
 
1166
1167
  DONE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-done-inline-arg-routing.json"
@@ -1198,7 +1199,7 @@ from pathlib import Path
1198
1199
 
1199
1200
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
1200
1201
  routing = Path(sys.argv[3])
1201
- proposal = Path(sys.argv[4])
1202
+ proposal = json.loads(Path(sys.argv[4]).read_text())
1202
1203
  chooser = Path(sys.argv[5])
1203
1204
  before = json.loads(Path(sys.argv[6]).read_text())
1204
1205
  tracked = [
@@ -1210,14 +1211,14 @@ tracked = [
1210
1211
  Path('.agent/verification-evidence.json'),
1211
1212
  ]
1212
1213
  state_before = json.loads(before['state.json'])
1213
- assert state_before['current_phase'] == 'done', 'done /cook <text> rejection should start from a completed workflow'
1214
- assert state_before['project_done'] is True, 'done /cook <text> rejection should start from project_done=true'
1215
- assert '/cook only supports the bare /cook entrypoint.' in output, 'done /cook <text> rejection should explain the bare-only contract'
1216
- assert not routing.exists(), 'done /cook <text> rejection should not run active/done workflow routing'
1217
- assert not proposal.exists(), 'done /cook <text> rejection should not open next-round proposal confirmation'
1218
- assert not chooser.exists(), 'done /cook <text> rejection should not open the chooser flow'
1214
+ assert state_before['current_phase'] == 'done', 'done /cook <hint> should start from a completed workflow'
1215
+ assert state_before['project_done'] is True, 'done /cook <hint> should start from project_done=true'
1216
+ assert not routing.exists(), 'done /cook <hint> should not run active-workflow routing while starting the next round'
1217
+ assert proposal['mission'] == 'Update README guidance for the next workflow round.', 'done /cook <hint> should bias next-round mission derivation toward the hint'
1218
+ assert not chooser.exists(), 'done /cook <hint> should not open the existing-workflow chooser when starting the next round'
1219
+ assert 'Cancelled next workflow round proposal.' in output, 'done /cook <hint> cancel should report next-round proposal cancellation'
1219
1220
  after = {path.name: path.read_text() for path in tracked}
1220
- assert before == after, 'done /cook <text> rejection should leave canonical files unchanged'
1221
+ assert before == after, 'done /cook <hint> cancel should leave canonical files unchanged'
1221
1222
  PY
1222
1223
 
1223
1224
  # Completed workflow again: /cook with no goal should be able to use model-assisted
@@ -117,9 +117,9 @@ import sys
117
117
  from pathlib import Path
118
118
 
119
119
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
120
- routing = Path(sys.argv[3])
120
+ routing = json.loads(Path(sys.argv[3]).read_text())
121
121
  proposal = Path(sys.argv[4])
122
- chooser = Path(sys.argv[5])
122
+ chooser = json.loads(Path(sys.argv[5]).read_text())
123
123
  initial_mission = sys.argv[6]
124
124
  before = json.loads(Path(sys.argv[7]).read_text())
125
125
  tracked = [
@@ -131,13 +131,14 @@ tracked = [
131
131
  Path('.agent/verification-evidence.json'),
132
132
  ]
133
133
  current_state = json.loads(before['state.json'])
134
- assert current_state['mission_anchor'] == initial_mission, 'active /cook <text> rejection should start from the current mission anchor'
135
- assert '/cook only supports the bare /cook entrypoint.' in output, 'active /cook <text> rejection should explain the bare-only contract'
136
- assert not routing.exists(), 'active /cook <text> rejection should not run active-workflow routing'
137
- assert not proposal.exists(), 'active /cook <text> rejection should not open final proposal confirmation'
138
- assert not chooser.exists(), 'active /cook <text> rejection should not open the existing-workflow chooser'
134
+ assert current_state['mission_anchor'] == initial_mission, 'active /cook <hint> should start from the current mission anchor'
135
+ assert routing['action'] == 'refocus', 'active /cook <hint> should route through active-workflow replacement assessment'
136
+ assert routing['proposedMissionAnchor'] == 'Replacement mission that should stay in the main chat.', 'active /cook <hint> should preserve the hinted replacement mission'
137
+ assert not proposal.exists(), 'active /cook <hint> chooser cancel should not open final proposal confirmation'
138
+ assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'active /cook <hint> should open the existing-workflow chooser'
139
+ assert 'Cancelled existing workflow confirmation.' in output, 'active /cook <hint> chooser cancel should report cancellation'
139
140
  after = {path.name: path.read_text() for path in tracked}
140
- assert before == after, 'active /cook <text> rejection should leave canonical files unchanged'
141
+ assert before == after, 'active /cook <hint> chooser cancel should leave canonical files unchanged'
141
142
  PY
142
143
 
143
144
  SESSION_INITIAL_REFOCUS="$TMPDIR/session-initial-bare-refocus.jsonl"
@@ -14,9 +14,9 @@ from pathlib import Path
14
14
 
15
15
  checks = {
16
16
  "README.md": [
17
- "Bare `/cook` is the only supported workflow entrypoint.",
18
- "`/cook <text>` is no longer supported; put mission text in the main chat, then rerun bare `/cook`.",
19
- "clarify the mission in the main chat before rerunning bare `/cook`",
17
+ "`/cook` supports both bare discussion-driven startup and optional inline intent hints.",
18
+ "`/cook <hint>` acts as a high-priority intent hint that helps proposal derivation interpret the recent discussion",
19
+ "clarify the mission in the main chat before rerunning `/cook`",
20
20
  "Matching or unclear discussion resumes from canonical `.agent/**` state.",
21
21
  "approval-only Start/Cancel gate",
22
22
  "Start new workflow from recent discussion",
@@ -24,26 +24,25 @@ checks = {
24
24
  "README/CHANGELOG updates still count as concrete repo changes",
25
25
  "assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not",
26
26
  "Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow.",
27
- "`/cook <text>` is rejected without running proposal routing or rewriting workflow state.",
27
+ "Optional `/cook <hint>` text biases that routing and candidate ranking toward the hinted implementation intent",
28
28
  ],
29
29
  "CHANGELOG.md": [
30
+ "restored optional `/cook <hint>` support as a soft intent hint that biases context analysis, proposal ranking, active-workflow disambiguation, and next-round startup without bypassing fail-closed routing or the approval-only Start/Cancel gate",
30
31
  "removed inline `/cook <text>` argument support so bare `/cook` is now the only supported workflow entrypoint",
31
- "packaged release parity fail closed when command arguments are passed instead of discussion driving proposal derivation",
32
32
  "historically allowed `/cook <hint>` as an analyst-only high-priority prompt",
33
- "that inline-argument path has since been removed so bare `/cook` is now the only supported entrypoint",
34
33
  ],
35
34
  "extensions/completion/index.ts": [
36
- 'description: "Bare /cook workflow: start, continue, refocus, or start the next round"',
35
+ 'description: "/cook workflow: start, continue, refocus, or start the next round (optional hint supported)"',
37
36
  'const COOK_BARE_ONLY_GUIDANCE =',
38
- '"/cook only supports the bare /cook entrypoint. Move mission text into the main chat, then rerun /cook."',
37
+ '"/cook supports optional inline hints as high-priority intent cues, but mission selection still comes from recent discussion, repo truth, and the approval-only confirmation flow."',
39
38
  '"/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."',
40
39
  ],
41
40
  }
42
41
 
43
42
  forbidden = {
44
- "README.md": ["compatibility" + " shim", "/cook <hint>", "optional inline /cook hint"],
43
+ "README.md": ["compatibility" + " shim", "optional inline /cook hint"],
45
44
  "CHANGELOG.md": ["compatibility" + " shim"],
46
- "extensions/completion/index.ts": ["temporary" + " compatibility" + " shim, pass /cook", "inline /cook hint", "optional inline /cook hint"],
45
+ "extensions/completion/index.ts": ["temporary" + " compatibility" + " shim, pass /cook", "optional inline /cook hint"],
47
46
  }
48
47
 
49
48
  for path, needles in checks.items():
@@ -76,19 +76,20 @@ pi -e "$PKG_ROOT" -p "/cook smoke-test mission" \
76
76
  >"$TMPDIR/pi-completion-smoke-inline-arg.out" 2>"$TMPDIR/pi-completion-smoke-inline-arg.err"
77
77
 
78
78
  python3 - "$TMPDIR/pi-completion-smoke-inline-arg.out" "$TMPDIR/pi-completion-smoke-inline-arg.err" "$INLINE_REJECTION_ROUTING_SNAPSHOT" "$INLINE_REJECTION_PROPOSAL_SNAPSHOT" "$INLINE_REJECTION_CHOOSER_SNAPSHOT" <<'PY'
79
+ import json
79
80
  import sys
80
81
  from pathlib import Path
81
82
 
82
83
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
83
84
  routing = Path(sys.argv[3])
84
- proposal = Path(sys.argv[4])
85
+ proposal = json.loads(Path(sys.argv[4]).read_text())
85
86
  chooser = Path(sys.argv[5])
86
87
 
87
- assert not Path('.agent').exists(), 'startup /cook <text> rejection should leave canonical state untouched'
88
- assert not routing.exists(), 'startup /cook <text> rejection should not open active-workflow routing'
89
- assert not proposal.exists(), 'startup /cook <text> rejection should not prepare a proposal snapshot'
90
- assert not chooser.exists(), 'startup /cook <text> rejection should not open the chooser flow'
91
- assert '/cook only supports the bare /cook entrypoint.' in output, 'startup /cook <text> rejection should explain the bare-only contract'
88
+ assert not Path('.agent').exists(), 'startup /cook <hint> cancel should leave canonical state untouched'
89
+ assert not routing.exists(), 'startup /cook <hint> should not open active-workflow routing before a workflow exists'
90
+ assert proposal['mission'] == 'Smoke-test inline hint startup mission.', 'startup /cook <hint> should bias proposal derivation toward the hinted mission'
91
+ assert not chooser.exists(), 'startup /cook <hint> should not open the existing-workflow chooser before a workflow exists'
92
+ assert 'Cancelled recent-discussion workflow proposal.' in output, 'startup /cook <hint> cancel should report proposal cancellation'
92
93
  PY
93
94
 
94
95
  write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"