@linimin/pi-letscook 0.1.41 → 0.1.43

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.43
6
+
7
+ ### Fixed
8
+
9
+ - stopped injecting completion-workflow reminder and compaction-resume context into ordinary primary-agent turns after canonical `continuation_policy` reaches `done`, so users must rerun `/cook` before the workflow protocol reactivates
10
+
11
+ ## 0.1.42
12
+
13
+ ### Changed
14
+
15
+ - allowed `/cook <hint>` as an analyst-only high-priority prompt that focuses proposal derivation without bypassing the existing approval-only Start/Cancel confirmation gate or canonical fail-closed routing
16
+
5
17
  ## 0.1.41
6
18
 
7
19
  ### Changed
package/README.md CHANGED
@@ -32,32 +32,33 @@ Then run `/reload` in Pi.
32
32
 
33
33
  ## Quick start
34
34
 
35
- Primary entrypoint:
35
+ Primary entrypoints:
36
36
 
37
37
  ```text
38
38
  /cook
39
+ /cook <hint>
39
40
  ```
40
41
 
41
- Use bare `/cook` after you discuss the mission in the main chat. The same command can:
42
+ Use `/cook` after you discuss the mission in the main chat. If the recent context is fuzzy, `/cook <hint>` can provide a short high-priority user hint for the proposal analyst while still relying on recent discussion to fill in scope, constraints, and acceptance. The same command can:
42
43
 
43
44
  - start a brand-new workflow from recent discussion
44
45
  - continue the current workflow when recent discussion still matches it, or when discussion is too weak or ambiguous to justify a refocus
45
46
  - surface a conservative refocus chooser when recent discussion clearly points to a different workflow
46
47
  - start the next workflow round after the previous one is `done`
47
48
 
48
- Bare `/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.
49
+ `/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. An inline `/cook <hint>` only guides proposal derivation; it does not bypass confirmation or become canonical state on its own.
49
50
 
50
- 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, bare `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to clarify the mission in the main chat before rerunning bare `/cook`.
51
+ 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`. If an inline hint conflicts with the recent discussion, `/cook` also fails closed instead of guessing.
51
52
 
52
53
  ## How `/cook` works
53
54
 
54
- Bare `/cook` is the only supported workflow entrypoint.
55
+ Bare `/cook` and `/cook <hint>` are the supported workflow entrypoints.
55
56
 
56
- | Repo state | Bare `/cook` |
57
+ | Repo state | `/cook` behavior |
57
58
  |---|---|
58
- | No workflow yet | Summarizes recent main-chat discussion into a startup proposal, 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. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. |
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`. |
59
+ | No workflow yet | Summarizes recent main-chat discussion, optionally focused by `/cook <hint>`, into a startup proposal, 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 `/cook`. |
60
+ | Active workflow exists | Reads the current mission plus recent non-command main-chat discussion, with any inline hint treated as extra proposal-analysis guidance only. 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. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. |
61
+ | Previous workflow is `done` | Starts the next round from recent main-chat discussion, optionally focused by `/cook <hint>`, 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 `/cook`. |
61
62
 
62
63
  ## Approval-only confirmation and fail-closed behavior
63
64
 
@@ -67,7 +68,7 @@ All startup, next-round, and replacement proposals are **approval-only**:
67
68
  - actions are only **Start** and **Cancel**
68
69
  - **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
69
70
 
70
- When bare `/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 bare `/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.
71
+ When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion plus any optional inline hint, 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.
71
72
 
72
73
  When an active workflow already exists and recent discussion clearly suggests a different workflow, `/cook` shows a separate chooser first:
73
74
 
@@ -539,10 +539,8 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
539
539
  }
540
540
 
541
541
  const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
542
- const COOK_INLINE_ARG_REJECTION_MESSAGE =
543
- "Inline /cook arguments are no longer supported. Clarify the mission in the main chat and rerun bare /cook.";
544
542
  const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
545
- "Bare /cook failed closed because recent discussion did not contain a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun bare /cook.";
543
+ "/cook failed closed because recent discussion and any optional inline /cook hint 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.";
546
544
 
547
545
  function buildCookCancellationMessage(prefix: string): string {
548
546
  return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
@@ -564,6 +562,10 @@ function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean
564
562
  return asString(snapshot?.state?.continuation_policy) === "done";
565
563
  }
566
564
 
565
+ function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot | undefined): boolean {
566
+ return Boolean(snapshot) && !isWorkflowDone(snapshot);
567
+ }
568
+
567
569
  function extractTextFromMessageContent(content: unknown): string {
568
570
  if (typeof content === "string") return content.trim();
569
571
  if (!Array.isArray(content)) return "";
@@ -1114,9 +1116,18 @@ function contextProposalAnalystModelArg(model: unknown): string | undefined {
1114
1116
  return provider && id ? `${provider}/${id}` : undefined;
1115
1117
  }
1116
1118
 
1117
- function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[]): string {
1119
+ function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], hintText?: string): string {
1118
1120
  const discussion = serializeRecentDiscussionEntries(recentEntries);
1119
- return [`Project: ${projectName}`, "Infer the current mission from the discussion.", "", "Recent discussion:", discussion].join("\n");
1121
+ const lines = [
1122
+ `Project: ${projectName}`,
1123
+ "Infer the current mission from the discussion.",
1124
+ "If an inline /cook hint is present, treat it as a high-priority user hint that may focus the mission, but do not ignore conflicting discussion or skip missing details.",
1125
+ ];
1126
+ if (hintText) {
1127
+ lines.push("", "Inline /cook hint:", hintText);
1128
+ }
1129
+ lines.push("", "Recent discussion:", discussion || "(none)");
1130
+ return lines.join("\n");
1120
1131
  }
1121
1132
 
1122
1133
  function contextProposalAnalystProgressLines(activity: LiveRoleActivity): string[] {
@@ -1145,20 +1156,23 @@ async function runContextProposalAnalystSubprocess(
1145
1156
  ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
1146
1157
  projectName: string,
1147
1158
  recentEntries: RecentDiscussionEntry[],
1159
+ hintText?: string,
1148
1160
  ): Promise<string | undefined> {
1149
1161
  const modelArg = contextProposalAnalystModelArg(ctx.model);
1150
1162
  if (!modelArg) return undefined;
1151
1163
  const cwd = getCtxCwd(ctx);
1152
1164
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
1153
1165
  const rootKey = completionRootKey(undefined, cwd);
1154
- const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries);
1166
+ const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries, hintText);
1155
1167
  const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
1156
1168
  const analystRole = "cook-proposal-analyst";
1157
1169
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
1158
1170
  const invocation = getPiInvocation(args);
1159
1171
  const liveActivity = createLiveRoleActivity(analystRole);
1160
- liveActivity.progress = "Analyzing recent discussion";
1161
- liveActivity.currentAction = "Reading recent discussion and preparing a startup proposal";
1172
+ liveActivity.progress = hintText ? "Analyzing recent discussion and inline hint" : "Analyzing recent discussion";
1173
+ liveActivity.currentAction = hintText
1174
+ ? "Reading recent discussion plus the inline /cook hint and preparing a startup proposal"
1175
+ : "Reading recent discussion and preparing a startup proposal";
1162
1176
  liveActivity.assistantSummary = liveActivity.progress;
1163
1177
  liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
1164
1178
  const messages: RoleMessage[] = [];
@@ -1188,6 +1202,17 @@ async function runContextProposalAnalystSubprocess(
1188
1202
  stdio: ["ignore", "pipe", "pipe"],
1189
1203
  shell: false,
1190
1204
  });
1205
+ let settled = false;
1206
+ const resolveOnce = (value: string | undefined) => {
1207
+ if (settled) return;
1208
+ settled = true;
1209
+ resolve(value);
1210
+ };
1211
+ const abort = () => {
1212
+ proc.kill("SIGTERM");
1213
+ resolveOnce(undefined);
1214
+ };
1215
+ const handleSigint = () => abort();
1191
1216
  let buffer = "";
1192
1217
  const processLine = (line: string) => {
1193
1218
  if (!line.trim()) return;
@@ -1208,14 +1233,19 @@ async function runContextProposalAnalystSubprocess(
1208
1233
  stderr += chunk.toString();
1209
1234
  });
1210
1235
  proc.on("close", (code) => {
1236
+ process.off("SIGINT", handleSigint);
1211
1237
  if (buffer.trim()) processLine(buffer);
1212
- resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
1238
+ resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
1239
+ });
1240
+ proc.on("error", () => {
1241
+ process.off("SIGINT", handleSigint);
1242
+ resolveOnce(undefined);
1213
1243
  });
1214
- proc.on("error", () => resolve(undefined));
1244
+ process.once("SIGINT", handleSigint);
1215
1245
  if (overlay) {
1216
1246
  overlay.onAbort = () => {
1217
- proc.kill("SIGTERM");
1218
- resolve(undefined);
1247
+ process.off("SIGINT", handleSigint);
1248
+ abort();
1219
1249
  };
1220
1250
  }
1221
1251
  });
@@ -1253,15 +1283,16 @@ async function analyzeContextProposalWithAgent(
1253
1283
  ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
1254
1284
  projectName: string,
1255
1285
  recentEntries: RecentDiscussionEntry[],
1286
+ hintText?: string,
1256
1287
  ): Promise<ContextProposal | undefined> {
1257
1288
  if (shouldDisableContextProposalAnalyst()) return undefined;
1258
1289
  const testOutput = completionTestContextProposalAnalystOutput();
1259
1290
  if (testOutput) {
1260
1291
  return parseContextProposalAnalystOutput(testOutput, projectName);
1261
1292
  }
1262
- if (recentEntries.length === 0) return undefined;
1293
+ if (recentEntries.length === 0 && !hintText?.trim()) return undefined;
1263
1294
  try {
1264
- const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries);
1295
+ const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries, hintText);
1265
1296
  if (!raw) return undefined;
1266
1297
  return parseContextProposalAnalystOutput(raw, projectName);
1267
1298
  } catch (error) {
@@ -1719,17 +1750,19 @@ function extractContextProposalFromStructuredSession(
1719
1750
  async function extractContextProposalFromSession(
1720
1751
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1721
1752
  projectName: string,
1753
+ hintText?: string,
1722
1754
  ): Promise<ContextProposal | undefined> {
1723
1755
  const recentEntries = collectRecentDiscussionEntries(ctx);
1724
- return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries)) ??
1756
+ return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, hintText)) ??
1725
1757
  extractContextProposalFromStructuredSession(recentEntries, projectName);
1726
1758
  }
1727
1759
 
1728
1760
  async function deriveCookContextProposal(
1729
1761
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
1730
1762
  projectName: string,
1763
+ hintText?: string,
1731
1764
  ): Promise<ContextProposal | undefined> {
1732
- return await extractContextProposalFromSession(ctx, projectName);
1765
+ return await extractContextProposalFromSession(ctx, projectName, hintText);
1733
1766
  }
1734
1767
 
1735
1768
  async function confirmContextProposal(
@@ -2005,10 +2038,11 @@ function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role
2005
2038
  async function assessActiveWorkflowProposalRouting(
2006
2039
  ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
2007
2040
  snapshot: CompletionStateSnapshot,
2041
+ hintText?: string,
2008
2042
  ): Promise<ActiveWorkflowProposalAssessment> {
2009
2043
  const currentMission = currentMissionAnchor(snapshot);
2010
2044
  const projectName = path.basename(snapshot.files.root);
2011
- const proposal = await deriveCookContextProposal(ctx, projectName);
2045
+ const proposal = await deriveCookContextProposal(ctx, projectName, hintText);
2012
2046
  if (!proposal) {
2013
2047
  const assessment: ActiveWorkflowProposalAssessment = {
2014
2048
  action: "unclear",
@@ -3703,7 +3737,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3703
3737
  const fingerprint = completionContinuationFingerprint(loaded.snapshot);
3704
3738
  if (fingerprint) markQueuedDriverPromptInFlight(rootKey, fingerprint);
3705
3739
  }
3706
- if (!loaded) return;
3740
+ if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot)) return;
3707
3741
  const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
3708
3742
  let marker: JsonRecord | undefined;
3709
3743
  if (markerText) {
@@ -3726,7 +3760,7 @@ export default function completionExtension(pi: ExtensionAPI) {
3726
3760
 
3727
3761
  pi.on("session_before_compact", async (event, ctx) => {
3728
3762
  const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
3729
- if (!loaded) return;
3763
+ if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot)) return;
3730
3764
  const { preparation } = event;
3731
3765
  const summary = buildResumeCapsule(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory);
3732
3766
  await fsp.mkdir(loaded.snapshot.files.tmpDir, { recursive: true });
@@ -4073,11 +4107,7 @@ export default function completionExtension(pi: ExtensionAPI) {
4073
4107
  pi.registerCommand("cook", {
4074
4108
  description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
4075
4109
  handler: async (args, ctx) => {
4076
- const inlineArgs = args.trim();
4077
- if (inlineArgs) {
4078
- emitCommandText(ctx, COOK_INLINE_ARG_REJECTION_MESSAGE, "info");
4079
- return;
4080
- }
4110
+ const inlineHint = args.trim() || undefined;
4081
4111
  let goal: string | undefined;
4082
4112
  const cwd = getCtxCwd(ctx);
4083
4113
  let snapshot = await loadCompletionSnapshot(cwd);
@@ -4089,7 +4119,7 @@ export default function completionExtension(pi: ExtensionAPI) {
4089
4119
  if (!snapshot) {
4090
4120
  const root = findRepoRoot(cwd) ?? cwd;
4091
4121
  const projectName = path.basename(root);
4092
- const proposal = await deriveCookContextProposal(ctx, projectName);
4122
+ const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
4093
4123
  if (!proposal) {
4094
4124
  emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(), "info");
4095
4125
  return;
@@ -4127,7 +4157,7 @@ export default function completionExtension(pi: ExtensionAPI) {
4127
4157
  if (!goal) {
4128
4158
  if (workflowDone) {
4129
4159
  const projectName = path.basename(snapshot.files.root);
4130
- const proposal = await deriveCookContextProposal(ctx, projectName);
4160
+ const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
4131
4161
  if (!proposal) {
4132
4162
  emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage("The previous completion workflow is already done."), "info");
4133
4163
  return;
@@ -4146,7 +4176,7 @@ export default function completionExtension(pi: ExtensionAPI) {
4146
4176
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
4147
4177
  emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
4148
4178
  } else {
4149
- const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot);
4179
+ const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, inlineHint);
4150
4180
  if (assessment.action !== "refocus" || !assessment.proposal) {
4151
4181
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
4152
4182
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
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,
@@ -392,4 +392,48 @@ assert 'Verification evidence summary:' in text, text
392
392
  assert 'selected_slice' in text, text
393
393
  PY
394
394
 
395
+ python3 - <<'PY'
396
+ import json
397
+ from pathlib import Path
398
+
399
+ state_path = Path('.agent/state.json')
400
+ plan_path = Path('.agent/plan.json')
401
+ active_path = Path('.agent/active-slice.json')
402
+
403
+ state = json.loads(state_path.read_text())
404
+ state.update({
405
+ 'current_phase': 'done',
406
+ 'continuation_policy': 'done',
407
+ 'continuation_reason': 'Fixture is complete; ordinary primary-agent turns should stay outside completion until /cook runs again.',
408
+ 'project_done': True,
409
+ 'remaining_high_value_gaps': 0,
410
+ 'unsatisfied_contract_ids': [],
411
+ 'next_mandatory_action': None,
412
+ 'next_mandatory_role': None,
413
+ 'remaining_stop_judges': 0,
414
+ 'contract_status': 'done',
415
+ })
416
+ state_path.write_text(json.dumps(state, indent=2) + '\n')
417
+
418
+ plan = json.loads(plan_path.read_text())
419
+ for slice_data in plan.get('candidate_slices', []):
420
+ if slice_data.get('slice_id') == 'evidence-fixture':
421
+ slice_data['status'] = 'done'
422
+ plan_path.write_text(json.dumps(plan, indent=2) + '\n')
423
+
424
+ active = json.loads(active_path.read_text())
425
+ active['status'] = 'done'
426
+ active_path.write_text(json.dumps(active, indent=2) + '\n')
427
+ PY
428
+
429
+ rm -f "$SYSTEM_REMINDER"
430
+ PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH="$SYSTEM_REMINDER" \
431
+ pi -e "$PKG_ROOT" -p "Summarize the latest release briefly." \
432
+ >"$TMPDIR/pi-canonical-evidence-done-reminder.out" 2>"$TMPDIR/pi-canonical-evidence-done-reminder.err"
433
+
434
+ [[ ! -f "$SYSTEM_REMINDER" ]] || {
435
+ echo "expected no completion reminder snapshot when continuation_policy is done" >&2
436
+ exit 1
437
+ }
438
+
395
439
  echo "canonical evidence artifact test passed: $TMPDIR"
@@ -178,7 +178,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
178
178
  snapshot = Path(sys.argv[3])
179
179
  assert not Path('.agent').exists(), 'missing-section structured discussion should fail closed without writing canonical state'
180
180
  assert not snapshot.exists(), 'missing-section structured discussion should not emit a proposal snapshot when bare /cook fails closed'
181
- assert 'Bare /cook failed closed' in output, 'missing-section structured discussion should explain the fail-closed startup outcome'
181
+ assert '/cook failed closed' in output, 'missing-section structured discussion should explain the fail-closed startup outcome'
182
182
  assert 'Mission/Scope/Constraints/Acceptance' in output, 'missing-section structured discussion should explain the strict fallback requirement'
183
183
  PY
184
184
 
@@ -198,7 +198,7 @@ from pathlib import Path
198
198
 
199
199
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
200
200
  assert not Path('.agent').exists(), 'ambiguous structured discussion should fail closed without writing canonical state'
201
- assert 'Bare /cook failed closed' in output, 'ambiguous structured discussion should explain the fail-closed startup outcome'
201
+ assert '/cook failed closed' in output, 'ambiguous structured discussion should explain the fail-closed startup outcome'
202
202
  assert 'Mission/Scope/Constraints/Acceptance' in output, 'ambiguous structured discussion should explain the strict fallback requirement'
203
203
  PY
204
204
 
@@ -289,7 +289,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
289
289
  snapshot = Path(sys.argv[3])
290
290
  assert not Path('.agent').exists(), 'planning-only startup should fail closed without writing canonical state'
291
291
  assert not snapshot.exists(), 'planning-only startup should not emit a proposal snapshot when bare /cook fails closed'
292
- assert 'Bare /cook failed closed' in output, 'planning-only startup should explain the fail-closed startup outcome'
292
+ assert '/cook failed closed' in output, 'planning-only startup should explain the fail-closed startup outcome'
293
293
  assert 'Mission/Scope/Constraints/Acceptance' in output, 'planning-only startup should still explain the structured discussion requirement'
294
294
  assert 'concrete repo changes' in output, 'planning-only startup should explain that bare /cook now expects execution-ready repo changes'
295
295
  PY
@@ -495,7 +495,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
495
495
  snapshot = Path(sys.argv[3])
496
496
  assert not Path('.agent').exists(), 'assistant-only startup summary should fail closed without writing canonical state'
497
497
  assert not snapshot.exists(), 'assistant-only startup summary should not emit a proposal snapshot when bare /cook fails closed'
498
- assert 'Bare /cook failed closed' in output, 'assistant-only startup summary should explain the fail-closed startup outcome'
498
+ assert '/cook failed closed' in output, 'assistant-only startup summary should explain the fail-closed startup outcome'
499
499
  assert 'concrete repo changes' in output, 'assistant-only startup summary should explain that bare /cook expects execution-ready repo changes from main-chat discussion'
500
500
  PY
501
501
 
@@ -523,7 +523,7 @@ output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
523
523
  snapshot = Path(sys.argv[3])
524
524
  assert not Path('.agent').exists(), 'analyst-derived ambiguous generic discussion should fail closed without writing canonical state'
525
525
  assert not snapshot.exists(), 'analyst-derived ambiguous generic discussion should not emit a proposal snapshot when bare /cook fails closed'
526
- assert 'Bare /cook failed closed' in output, 'analyst-derived ambiguous generic discussion should explain the fail-closed startup outcome'
526
+ assert '/cook failed closed' in output, 'analyst-derived ambiguous generic discussion should explain the fail-closed startup outcome'
527
527
  PY
528
528
 
529
529
  # No workflow yet: /cook with no goal should infer from recent discussion through analyst output.
@@ -836,8 +836,8 @@ assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_re
836
836
  assert active['status'] == 'idle', 'active-slice should reset to idle for the next workflow round'
837
837
  PY
838
838
 
839
- # Active workflow: inline /cook args should be rejected before proposal/routing helpers run
840
- # and should leave canonical state unchanged.
839
+ # Active workflow: inline /cook hint should flow through routing plus final proposal confirmation
840
+ # and still leave canonical state unchanged when the final proposal is cancelled.
841
841
  ACTIVE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-active-inline-arg-routing.json"
842
842
  ACTIVE_INLINE_REJECTION_PROPOSAL="$TMPDIR/context-proposal-active-inline-arg-proposal.json"
843
843
  ACTIVE_INLINE_REJECTION_CHOOSER="$TMPDIR/context-proposal-active-inline-arg-chooser.json"
@@ -859,8 +859,8 @@ Path(sys.argv[1]).write_text(json.dumps({path.name: path.read_text() for path in
859
859
  PY
860
860
 
861
861
  PI_COMPLETION_EXISTING_WORKFLOW_ACTION=refocus \
862
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
863
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
862
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=cancel \
863
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT='{"mission":"Replacement mission for the active workflow.","scope":["Review the replacement through the existing workflow chooser first."],"constraints":["Do not rewrite canonical state before the final Start confirmation."],"acceptance":["Show the final replacement proposal only after the chooser selects refocus."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.9}' \
864
864
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$ACTIVE_INLINE_REJECTION_PROPOSAL" \
865
865
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$ACTIVE_INLINE_REJECTION_ROUTING" \
866
866
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$ACTIVE_INLINE_REJECTION_CHOOSER" \
@@ -886,16 +886,15 @@ tracked = [
886
886
  Path('.agent/verification-evidence.json'),
887
887
  ]
888
888
 
889
- assert 'Inline /cook arguments are no longer supported.' in output, 'active inline /cook args should explain the hard rejection'
890
- assert 'Clarify the mission in the main chat and rerun bare /cook.' in output, 'active inline /cook args should redirect users back to main chat plus bare /cook'
891
- assert not routing.exists(), 'active inline /cook args should not run active-workflow routing'
892
- assert not proposal.exists(), 'active inline /cook args should not open proposal confirmation'
893
- assert not chooser.exists(), 'active inline /cook args should not open the existing-workflow chooser'
889
+ assert routing.exists(), 'active inline /cook hint should run active-workflow routing'
890
+ assert proposal.exists(), 'active inline /cook hint should open final proposal confirmation after refocus is chosen'
891
+ assert chooser.exists(), 'active inline /cook hint should open the existing-workflow chooser'
894
892
  after = {path.name: path.read_text() for path in tracked}
895
- assert before == after, 'active inline /cook args should leave canonical files unchanged'
893
+ assert before == after, 'active inline /cook hint should leave canonical files unchanged when confirmation is cancelled'
896
894
  PY
897
895
 
898
- # Completed workflow: inline /cook args should also fail closed without starting the next round.
896
+ # Completed workflow: inline /cook hint should also drive the next-round proposal and leave
897
+ # canonical state unchanged when the proposal is cancelled.
899
898
  mark_done
900
899
 
901
900
  DONE_INLINE_REJECTION_ROUTING="$TMPDIR/context-proposal-done-inline-arg-routing.json"
@@ -918,8 +917,8 @@ tracked = [
918
917
  Path(sys.argv[1]).write_text(json.dumps({path.name: path.read_text() for path in tracked}, indent=2) + '\n')
919
918
  PY
920
919
 
921
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
922
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
920
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=cancel \
921
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT='{"mission":"Update README guidance for the next workflow round.","scope":["Refresh README guidance for /cook hint-driven startup behavior."],"constraints":["Do not rewrite canonical state before Start is accepted."],"acceptance":["Keep the next-round proposal scoped to README updates only."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.9}' \
923
922
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DONE_INLINE_REJECTION_PROPOSAL" \
924
923
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$DONE_INLINE_REJECTION_ROUTING" \
925
924
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$DONE_INLINE_REJECTION_CHOOSER" \
@@ -947,13 +946,11 @@ tracked = [
947
946
  state_before = json.loads(before['state.json'])
948
947
  assert state_before['current_phase'] == 'done', 'done inline /cook rejection should start from a completed workflow'
949
948
  assert state_before['project_done'] is True, 'done inline /cook rejection should start from project_done=true'
950
- assert 'Inline /cook arguments are no longer supported.' in output, 'done inline /cook args should explain the hard rejection'
951
- assert 'Clarify the mission in the main chat and rerun bare /cook.' in output, 'done inline /cook args should redirect users back to main chat plus bare /cook'
952
949
  assert not routing.exists(), 'done inline /cook args should not run active/done workflow routing'
953
- assert not proposal.exists(), 'done inline /cook args should not open next-round proposal confirmation'
950
+ assert proposal.exists(), 'done inline /cook hint should open next-round proposal confirmation'
954
951
  assert not chooser.exists(), 'done inline /cook args should not open the chooser flow'
955
952
  after = {path.name: path.read_text() for path in tracked}
956
- assert before == after, 'done inline /cook args should leave canonical files unchanged'
953
+ assert before == after, 'done inline /cook hint cancel should leave canonical files unchanged'
957
954
  PY
958
955
 
959
956
  # Completed workflow again: /cook with no goal should be able to use model-assisted
@@ -103,6 +103,7 @@ Path(sys.argv[1]).write_text(json.dumps({path.name: path.read_text() for path in
103
103
  PY
104
104
 
105
105
  PI_COMPLETION_EXISTING_WORKFLOW_ACTION=cancel \
106
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT='{"mission":"Replacement mission that should stay in the main chat.","scope":["Replace the current workflow only after explicit approval."],"constraints":["Do not rewrite canonical state before the final Start confirmation."],"acceptance":["Surface the chooser before any replacement workflow rewrite."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.9}' \
106
107
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$INLINE_REJECTION_ROUTING" \
107
108
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$INLINE_REJECTION_PROPOSAL" \
108
109
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$INLINE_REJECTION_CHOOSER" \
@@ -131,13 +132,12 @@ tracked = [
131
132
  ]
132
133
  current_state = json.loads(before['state.json'])
133
134
  assert current_state['mission_anchor'] == initial_mission, 'active inline /cook args should start from the current mission anchor'
134
- assert 'Inline /cook arguments are no longer supported.' in output, 'active inline /cook args should explain the hard rejection'
135
- assert 'Clarify the mission in the main chat and rerun bare /cook.' in output, 'active inline /cook args should redirect users back to main chat plus bare /cook'
136
- assert not routing.exists(), 'active inline /cook args should not run active-workflow routing'
137
- assert not proposal.exists(), 'active inline /cook args should not open proposal confirmation'
138
- assert not chooser.exists(), 'active inline /cook args should not open the existing-workflow chooser'
135
+ assert 'Cancelled existing workflow confirmation.' in output, 'active inline /cook hint cancel should surface chooser cancellation'
136
+ assert routing.exists(), 'active inline /cook hint should run active-workflow routing'
137
+ assert not proposal.exists(), 'active inline /cook hint cancel at chooser should not open final proposal confirmation'
138
+ assert chooser.exists(), 'active inline /cook hint should open the existing-workflow chooser'
139
139
  after = {path.name: path.read_text() for path in tracked}
140
- assert before == after, 'active inline /cook args should leave canonical files unchanged'
140
+ assert before == after, 'active inline /cook hint cancel should leave canonical files unchanged'
141
141
  PY
142
142
 
143
143
  SESSION_INITIAL_REFOCUS="$TMPDIR/session-initial-bare-refocus.jsonl"
@@ -7,15 +7,16 @@ cd "$ROOT"
7
7
  echo "[release-check] running control-plane validation, bare /cook parity, startup/refocus/context regressions, canonical evidence artifact, active-slice contract, observability, evaluator calibration, and rubric contract coverage"
8
8
  bash .agent/verify_completion_control_plane.sh
9
9
 
10
- echo "[release-check] verifying public bare /cook parity"
10
+ echo "[release-check] verifying public /cook parity"
11
11
  python3 - <<'PY'
12
12
  import re
13
13
  from pathlib import Path
14
14
 
15
15
  checks = {
16
16
  "README.md": [
17
- "Bare `/cook` is the only supported workflow entrypoint.",
18
- "clarify the mission in the main chat before rerunning bare `/cook`",
17
+ "Bare `/cook` and `/cook <hint>` are the supported workflow entrypoints.",
18
+ "If the recent context is fuzzy, `/cook <hint>` can provide a short high-priority user hint for the proposal analyst",
19
+ "clarify the mission in the main chat before rerunning `/cook`",
19
20
  "Matching or unclear discussion resumes from canonical `.agent/**` state.",
20
21
  "approval-only Start/Cancel gate",
21
22
  "Start new workflow from recent discussion",
@@ -35,8 +36,8 @@ checks = {
35
36
  ],
36
37
  "extensions/completion/index.ts": [
37
38
  'description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round"',
38
- "Inline /cook arguments are no longer supported. Clarify the mission in the main chat and rerun bare /cook.",
39
- "Bare /cook failed closed because recent discussion did not contain a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun bare /cook.",
39
+ "If an inline /cook hint is present, treat it as a high-priority user hint that may focus the mission, but do not ignore conflicting discussion or skip missing details.",
40
+ "/cook failed closed because recent discussion and any optional inline /cook hint 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
41
  ],
41
42
  }
42
43
 
@@ -46,32 +47,17 @@ forbidden = {
46
47
  "extensions/completion/index.ts": ["temporary" + " compatibility" + " shim, pass /cook"],
47
48
  }
48
49
 
49
- public_doc_paths = [
50
- Path("README.md"),
51
- Path("CHANGELOG.md"),
52
- Path("PUBLISHING.md"),
53
- ]
54
- inline_cook_pattern = re.compile(r"/cook\s*<[^>]+>")
55
-
56
50
  for path, needles in checks.items():
57
51
  text = Path(path).read_text()
58
52
  for needle in needles:
59
53
  if needle not in text:
60
- raise SystemExit(f"[release-check] missing expected bare /cook parity text in {path}: {needle}")
54
+ raise SystemExit(f"[release-check] missing expected /cook parity text in {path}: {needle}")
61
55
 
62
56
  for path, needles in forbidden.items():
63
57
  text = Path(path).read_text()
64
58
  for needle in needles:
65
59
  if needle in text:
66
60
  raise SystemExit(f"[release-check] found stale compatibility wording in {path}: {needle}")
67
-
68
- for path in public_doc_paths:
69
- text = path.read_text()
70
- match = inline_cook_pattern.search(text)
71
- if match:
72
- raise SystemExit(
73
- f"[release-check] found unsupported inline /cook syntax in {path}: {match.group(0)}"
74
- )
75
61
  PY
76
62
 
77
63
  npm run smoke-test
@@ -57,12 +57,15 @@ INLINE_REJECTION_ROUTING_SNAPSHOT="$TMPDIR/inline-arg-routing.json"
57
57
  INLINE_REJECTION_PROPOSAL_SNAPSHOT="$TMPDIR/inline-arg-proposal.json"
58
58
  INLINE_REJECTION_CHOOSER_SNAPSHOT="$TMPDIR/inline-arg-chooser.json"
59
59
  BOOTSTRAP_SESSION="$TMPDIR/session-smoke-bootstrap.jsonl"
60
- BOOTSTRAP_DISCUSSION=$'Mission: Exercise smoke-test bootstrap.\nScope:\n- Materialize the canonical completion control-plane files.\n- Keep the smoke test on supported bare /cook behavior.\nConstraints:\n- Do not rely on inline /cook arguments.\nAcceptance:\n- Scaffold canonical files and kickoff prompts for the smoke fixture.'
60
+ BOOTSTRAP_DISCUSSION=$'Mission: Exercise smoke-test bootstrap.\nScope:\n- Materialize the canonical completion control-plane files.\n- Keep the smoke test on supported /cook startup behavior.\nConstraints:\n- Keep startup proposal confirmation approval-only.\nAcceptance:\n- Scaffold canonical files and kickoff prompts for the smoke fixture.'
61
+ INLINE_HINT_ANALYST_OUTPUT='{"mission":"Smoke-test inline hint startup mission.","scope":["Materialize the canonical completion control-plane files."],"constraints":["Keep startup proposal confirmation approval-only."],"acceptance":["Scaffold canonical files and kickoff prompts for the smoke fixture."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.91}'
61
62
 
62
63
  mkdir -p "$ROOT"
63
64
  cd "$ROOT"
64
65
  git init -q
65
66
 
67
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=cancel \
68
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$INLINE_HINT_ANALYST_OUTPUT" \
66
69
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
67
70
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$INLINE_REJECTION_ROUTING_SNAPSHOT" \
68
71
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$INLINE_REJECTION_PROPOSAL_SNAPSHOT" \
@@ -79,12 +82,11 @@ routing = Path(sys.argv[3])
79
82
  proposal = Path(sys.argv[4])
80
83
  chooser = Path(sys.argv[5])
81
84
 
82
- assert not Path('.agent').exists(), 'startup inline /cook args should fail closed without creating canonical state'
83
- assert not routing.exists(), 'startup inline /cook args should not open active-workflow routing'
84
- assert not proposal.exists(), 'startup inline /cook args should not open the proposal confirmation flow'
85
- assert not chooser.exists(), 'startup inline /cook args should not open the chooser flow'
86
- assert 'Inline /cook arguments are no longer supported.' in output, 'startup inline /cook args should explain the hard rejection'
87
- assert 'Clarify the mission in the main chat and rerun bare /cook.' in output, 'startup inline /cook args should redirect users back to main chat plus bare /cook'
85
+ assert not Path('.agent').exists(), 'startup inline /cook hint cancel should leave canonical state untouched'
86
+ assert not routing.exists(), 'startup inline /cook hint should not open active-workflow routing'
87
+ assert proposal.exists(), 'startup inline /cook hint should still prepare a proposal for confirmation'
88
+ assert not chooser.exists(), 'startup inline /cook hint should not open the chooser flow during startup'
89
+ assert 'Cancelled recent-discussion workflow proposal.' in output, 'startup inline /cook hint cancel should surface the proposal cancellation message'
88
90
  PY
89
91
 
90
92
  write_session "$BOOTSTRAP_SESSION" "$ROOT" "$BOOTSTRAP_DISCUSSION"