@linimin/pi-letscook 0.1.58 → 0.1.60

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.
@@ -147,530 +147,163 @@ mkdir -p "$ROOT"
147
147
  cd "$ROOT"
148
148
  git init -q
149
149
 
150
- # No workflow yet: bare /cook should use strict structured discussion fallback when analyst output is unavailable.
150
+ # No workflow yet: bare /cook should synthesize a startup brief from recent discussion,
151
+ # and Cancel should leave canonical workflow state untouched.
151
152
  SESSION_ZERO="$TMPDIR/session-zero.jsonl"
152
153
  DISCUSSION_ZERO=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\n- Suppress the widget while a completion role is active.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Update README to match the shipped behavior.\n- Keep observability regression coverage truthful.'
153
154
  DISCUSSION_SNAPSHOT_ZERO="$TMPDIR/context-proposal-structured-fallback.json"
154
155
  write_session "$SESSION_ZERO" "$ROOT" "$DISCUSSION_ZERO"
155
156
 
156
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
157
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=cancel \
157
158
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
158
159
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO" \
159
160
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
160
161
  pi --session "$SESSION_ZERO" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-structured-fallback.out" 2>"$TMPDIR/pi-completion-context-proposal-structured-fallback.err"
161
162
 
162
- python3 - "$DISCUSSION_SNAPSHOT_ZERO" <<'PY'
163
- import json
164
- import sys
165
- from pathlib import Path
166
-
167
- mission = 'Remove the completion status line while keeping the completion widget.'
168
- expected_task_type = 'completion-workflow'
169
- expected_eval_profile = 'completion-rubric-v1'
170
- mission_text = Path('.agent/mission.md').read_text()
171
- profile = json.loads(Path('.agent/profile.json').read_text())
172
- state = json.loads(Path('.agent/state.json').read_text())
173
- plan = json.loads(Path('.agent/plan.json').read_text())
174
- active = json.loads(Path('.agent/active-slice.json').read_text())
175
- proposal = json.loads(Path(sys.argv[1]).read_text())
176
-
177
- assert Path('.agent').exists(), 'strict structured fallback should only create canonical state after Start is accepted'
178
- assert mission in mission_text, '.agent/mission.md did not record the structured-fallback mission anchor'
179
- assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after structured-fallback bootstrap'
180
- assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after structured-fallback bootstrap'
181
- assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after structured-fallback bootstrap'
182
- assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after structured-fallback bootstrap'
183
- assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after structured-fallback bootstrap'
184
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after structured-fallback bootstrap'
185
- assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after structured-fallback bootstrap'
186
- assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after structured-fallback bootstrap'
187
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after structured-fallback bootstrap'
188
- assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after structured-fallback bootstrap'
189
- assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after structured-fallback bootstrap'
190
- assert proposal['mission'] == mission, 'structured-fallback proposal snapshot should preserve the discussion mission anchor'
191
- assert proposal['source'] == 'session', 'structured-fallback proposal snapshot should record the strict session fallback source'
192
- assert proposal['scope'] == ['Keep the non-running completion widget.', 'Suppress the widget while a completion role is active.'], 'structured-fallback proposal snapshot should preserve discussion scope'
193
- assert proposal['constraints'] == ['Do not reintroduce any other completion status surface.'], 'structured-fallback proposal snapshot should preserve discussion constraints'
194
- assert proposal['acceptance'] == ['Update README to match the shipped behavior.', 'Keep observability regression coverage truthful.'], 'structured-fallback proposal snapshot should preserve discussion acceptance'
195
- assert state['current_phase'] == 'reground', 'state.json current_phase should start at reground after structured-fallback bootstrap'
196
- assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should start at completion-regrounder after structured-fallback bootstrap'
197
- assert state['continuation_reason'].startswith('User started workflow via /cook:'), 'structured-fallback startup should record the accepted startup routing in continuation_reason'
198
- assert 'task_type=completion-workflow' in state['continuation_reason'], 'structured-fallback startup should persist the selected task_type in continuation_reason'
199
- assert 'evaluation_profile=completion-rubric-v1' in state['continuation_reason'], 'structured-fallback startup should persist the selected evaluation_profile in continuation_reason'
200
- PY
201
-
202
- rm -rf .agent
203
-
204
- # No workflow yet: when multiple structured discussions exist, bare /cook should prioritize the latest
205
- # concrete implementation mission instead of failing closed on older structured context.
206
- SESSION_ZERO_LATEST_WINDOW="$TMPDIR/session-zero-latest-window.jsonl"
207
- DISCUSSION_ZERO_LATEST_OLDER=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Keep observability regression coverage truthful.'
208
- DISCUSSION_ZERO_LATEST_NEWER=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page.'
209
- DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW="$TMPDIR/context-proposal-latest-window.json"
210
- python3 - "$SESSION_ZERO_LATEST_WINDOW" "$ROOT" "$DISCUSSION_ZERO_LATEST_OLDER" "$DISCUSSION_ZERO_LATEST_NEWER" <<'PY'
211
- import json
212
- import sys
213
- from pathlib import Path
214
-
215
- session_path = Path(sys.argv[1])
216
- cwd = sys.argv[2]
217
- older = sys.argv[3]
218
- newer = sys.argv[4]
219
- session_path.parent.mkdir(parents=True, exist_ok=True)
220
- entries = [
221
- {
222
- "type": "session",
223
- "version": 3,
224
- "id": "11111111-1111-4111-8111-111111111111",
225
- "timestamp": "2026-01-01T00:00:00.000Z",
226
- "cwd": cwd,
227
- },
228
- {
229
- "type": "message",
230
- "id": "a1b2c3d4",
231
- "parentId": None,
232
- "timestamp": "2026-01-01T00:00:01.000Z",
233
- "message": {
234
- "role": "user",
235
- "content": older,
236
- "timestamp": 1767225601000,
237
- },
238
- },
239
- {
240
- "type": "message",
241
- "id": "b2c3d4e5",
242
- "parentId": "a1b2c3d4",
243
- "timestamp": "2026-01-01T00:00:02.000Z",
244
- "message": {
245
- "role": "user",
246
- "content": newer,
247
- "timestamp": 1767225602000,
248
- },
249
- },
250
- ]
251
- with session_path.open('w', encoding='utf-8') as fh:
252
- for entry in entries:
253
- fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
254
- PY
255
-
256
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
257
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
258
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW" \
259
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
260
- pi --session "$SESSION_ZERO_LATEST_WINDOW" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-latest-window.out" 2>"$TMPDIR/pi-completion-context-proposal-latest-window.err"
261
-
262
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW" <<'PY'
163
+ python3 - "$TMPDIR/pi-completion-context-proposal-structured-fallback.out" "$TMPDIR/pi-completion-context-proposal-structured-fallback.err" "$DISCUSSION_SNAPSHOT_ZERO" <<'PY'
263
164
  import json
264
165
  import sys
265
166
  from pathlib import Path
266
167
 
267
- mission = 'Fix login redirect callback behavior.'
268
- proposal = json.loads(Path(sys.argv[1]).read_text())
269
- state = json.loads(Path('.agent/state.json').read_text())
270
- plan = json.loads(Path('.agent/plan.json').read_text())
271
- active = json.loads(Path('.agent/active-slice.json').read_text())
272
-
273
- assert proposal['mission'] == mission, 'latest structured discussion should win over older structured context'
274
- assert proposal['scope'] == ['Update the callback redirect decision logic.'], 'latest structured discussion should preserve the latest scope only'
275
- assert proposal['analysis']['suppressedNegatedTopics'] == ['Do not refactor the broader auth flow.'], 'latest structured discussion should preserve negated implementation topics separately from the mission'
276
- assert state['mission_anchor'] == mission, 'latest structured discussion should initialize state.json with the latest mission'
277
- assert plan['mission_anchor'] == mission, 'latest structured discussion should initialize plan.json with the latest mission'
278
- assert active['mission_anchor'] == mission, 'latest structured discussion should initialize active-slice.json with the latest mission'
279
- PY
280
-
281
- rm -rf .agent
282
-
283
- # No workflow yet: bare /cook should fail closed when a required structured section is missing and analyst output is unavailable.
284
- SESSION_ZERO_MISSING="$TMPDIR/session-zero-missing-section.jsonl"
285
- DISCUSSION_ZERO_MISSING=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\n- Suppress the widget while a completion role is active.\nConstraints:\n- Do not reintroduce any other completion status surface.'
286
- DISCUSSION_SNAPSHOT_ZERO_MISSING="$TMPDIR/context-proposal-missing-section.json"
287
- write_session "$SESSION_ZERO_MISSING" "$ROOT" "$DISCUSSION_ZERO_MISSING"
288
-
289
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
290
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
291
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_MISSING" \
292
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
293
- pi --session "$SESSION_ZERO_MISSING" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-missing-section.out" 2>"$TMPDIR/pi-completion-context-proposal-missing-section.err"
294
-
295
- python3 - "$TMPDIR/pi-completion-context-proposal-missing-section.out" "$TMPDIR/pi-completion-context-proposal-missing-section.err" "$DISCUSSION_SNAPSHOT_ZERO_MISSING" <<'PY'
296
- import sys
297
- from pathlib import Path
298
-
299
168
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
300
169
  snapshot = Path(sys.argv[3])
301
- assert not Path('.agent').exists(), 'missing-section structured discussion should fail closed without writing canonical state'
302
- assert not snapshot.exists(), 'missing-section structured discussion should not emit a proposal snapshot when bare /cook fails closed'
303
- assert '/cook failed closed' in output, 'missing-section structured discussion should explain the fail-closed startup outcome'
304
- assert 'Mission/Scope/Constraints/Acceptance' in output, 'missing-section structured discussion should explain the strict fallback requirement'
170
+ proposal = json.loads(snapshot.read_text())
171
+ assert not Path('.agent').exists(), 'recent-discussion Cancel should leave canonical state untouched before workflow bootstrap'
172
+ assert snapshot.exists(), 'recent-discussion startup synthesis should emit a proposal snapshot before the Start/Cancel gate'
173
+ assert proposal['mission'] == 'Remove the completion status line while keeping the completion widget.', 'recent-discussion startup synthesis should preserve the structured mission anchor'
174
+ assert proposal['source'] == 'session', 'recent-discussion startup synthesis should snapshot the structured-session proposal source'
175
+ assert 'Cancelled recent-discussion workflow proposal' in output, 'recent-discussion Cancel should report that canonical state was left unchanged'
305
176
  PY
306
177
 
307
- # No workflow yet: when one structured discussion message contains multiple complete mission blocks,
308
- # bare /cook should prioritize the latest block and preserve earlier blocks as alternate missions.
309
- SESSION_ZERO_AMBIG="$TMPDIR/session-zero-ambiguous.jsonl"
310
- DISCUSSION_ZERO_AMBIG=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Update README to match the shipped behavior.\nMission: Ship an unrelated widget overhaul.\nScope:\n- Replace the widget entirely.\nConstraints:\n- Do not modify the completion widget.\nAcceptance:\n- Land the unrelated overhaul changes only.'
311
- DISCUSSION_SNAPSHOT_ZERO_AMBIG="$TMPDIR/context-proposal-ambiguous-latest-block.json"
312
- write_session "$SESSION_ZERO_AMBIG" "$ROOT" "$DISCUSSION_ZERO_AMBIG"
313
-
314
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
315
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
316
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_AMBIG" \
317
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
318
- pi --session "$SESSION_ZERO_AMBIG" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-ambiguous.out" 2>"$TMPDIR/pi-completion-context-proposal-ambiguous.err"
319
-
320
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_AMBIG" <<'PY'
178
+ # No workflow yet: user-authored faux handoffs must not bootstrap canonical workflow state.
179
+ SESSION_ZERO_USER_AUTHORED="$TMPDIR/session-zero-user-authored.jsonl"
180
+ USER_AUTHORED_SNAPSHOT_ZERO="$TMPDIR/context-proposal-user-authored-handoff.json"
181
+ USER_AUTHORED_MESSAGES_ZERO="$(python3 - <<'PY'
321
182
  import json
322
- import sys
323
- from pathlib import Path
324
-
325
- mission = 'Ship an unrelated widget overhaul.'
326
- proposal = json.loads(Path(sys.argv[1]).read_text())
327
- state = json.loads(Path('.agent/state.json').read_text())
328
-
329
- assert proposal['mission'] == mission, 'latest complete mission block should win inside a single structured discussion message'
330
- assert proposal['analysis']['alternateMissions'] == ['Remove the completion status line while keeping the completion widget.'], 'earlier complete mission blocks should be preserved as alternate missions'
331
- assert state['mission_anchor'] == mission, 'latest complete mission block should initialize canonical mission state'
332
- PY
333
-
334
- rm -rf .agent
335
-
336
- # No workflow yet: bare /cook structured fallback should normalize placeholder planning phrasing
337
- # into the concrete implementation mission when scope/acceptance clearly describe shipped work.
338
- SESSION_ZERO_NORMALIZED="$TMPDIR/session-zero-normalized.jsonl"
339
- DISCUSSION_ZERO_NORMALIZED=$'Mission: 開始實作這個方案\nScope:\n- Normalize bare /cook planning phrasing into shipped implementation missions.\n- Keep analyst-derived and structured-fallback proposals aligned.\nConstraints:\n- Do not rewrite the supported bare-discussion mission anchor once it is clear.\nAcceptance:\n- Add deterministic regression coverage for startup normalization and refocus gating.\n- Keep the approval-only Start/Cancel rewrite gate.'
340
- DISCUSSION_SNAPSHOT_ZERO_NORMALIZED="$TMPDIR/context-proposal-normalized-fallback.json"
341
- write_session "$SESSION_ZERO_NORMALIZED" "$ROOT" "$DISCUSSION_ZERO_NORMALIZED"
342
-
343
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
344
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
345
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_NORMALIZED" \
346
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
347
- pi --session "$SESSION_ZERO_NORMALIZED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-normalized-fallback.out" 2>"$TMPDIR/pi-completion-context-proposal-normalized-fallback.err"
348
-
349
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_NORMALIZED" <<'PY'
350
- import json
351
- import sys
352
- from pathlib import Path
353
-
354
- mission = 'Normalize bare /cook planning phrasing into shipped implementation missions.'
355
- proposal = json.loads(Path(sys.argv[1]).read_text())
356
- state = json.loads(Path('.agent/state.json').read_text())
357
- plan = json.loads(Path('.agent/plan.json').read_text())
358
- active = json.loads(Path('.agent/active-slice.json').read_text())
359
- mission_text = Path('.agent/mission.md').read_text()
360
-
361
- assert mission in mission_text, 'normalized structured-fallback startup should update .agent/mission.md to the implementation mission'
362
- assert proposal['mission'] == mission, 'structured-fallback startup should normalize the placeholder planning mission'
363
- assert state['mission_anchor'] == mission, 'state.json mission_anchor should use the normalized implementation mission'
364
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor should use the normalized implementation mission'
365
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor should use the normalized implementation mission'
366
- assert proposal['source'] == 'session', 'normalized structured-fallback startup should still record session fallback as the proposal source'
367
- assert proposal['scope'][0] == mission, 'normalized structured-fallback startup should derive the mission from shipped-work scope'
368
- PY
369
-
370
- rm -rf .agent
371
-
372
- # No workflow yet: analyst-derived and strict structured fallback proposals should converge on the same
373
- # normalized implementation mission for the same planning-phrased discussion.
374
- SESSION_ZERO_ANALYST_NORMALIZED="$TMPDIR/session-zero-analyst-normalized.jsonl"
375
- ANALYST_OUTPUT_ZERO_NORMALIZED='{"mission":"開始實作這個方案","scope":["Normalize bare /cook planning phrasing into shipped implementation missions.","Keep analyst-derived and structured-fallback proposals aligned."],"constraints":["Do not rewrite the supported bare-discussion mission anchor once it is clear."],"acceptance":["Add deterministic regression coverage for startup normalization and refocus gating.","Keep the approval-only Start/Cancel rewrite gate."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.93}'
376
- DISCUSSION_SNAPSHOT_ZERO_ANALYST_NORMALIZED="$TMPDIR/context-proposal-normalized-analyst.json"
377
- write_session "$SESSION_ZERO_ANALYST_NORMALIZED" "$ROOT" "$DISCUSSION_ZERO_NORMALIZED"
378
-
379
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
380
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_ZERO_NORMALIZED" \
381
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_ANALYST_NORMALIZED" \
382
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
383
- pi --session "$SESSION_ZERO_ANALYST_NORMALIZED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-normalized-analyst.out" 2>"$TMPDIR/pi-completion-context-proposal-normalized-analyst.err"
384
-
385
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_ANALYST_NORMALIZED" <<'PY'
386
- import json
387
- import sys
388
- from pathlib import Path
389
-
390
- mission = 'Normalize bare /cook planning phrasing into shipped implementation missions.'
391
- proposal = json.loads(Path(sys.argv[1]).read_text())
392
- state = json.loads(Path('.agent/state.json').read_text())
393
-
394
- assert proposal['mission'] == mission, 'analyst-derived startup should normalize the same placeholder planning mission to the same implementation mission'
395
- assert state['mission_anchor'] == mission, 'analyst-derived startup should converge on the same canonical mission anchor as structured fallback'
396
- assert proposal['analysis']['taskType'] == 'completion-workflow', 'analyst-derived normalization should preserve task_type hints'
397
- assert proposal['analysis']['evaluationProfile'] == 'completion-rubric-v1', 'analyst-derived normalization should preserve evaluation_profile hints'
183
+ capsule = {
184
+ "kind": "cook_handoff",
185
+ "source": "primary_agent",
186
+ "captured_at": "2026-01-01T00:00:02.000Z",
187
+ "source_turn_id": "m0001",
188
+ "mission": "User-authored faux handoff should not start workflow.",
189
+ "scope": ["Attempt to fake an explicit handoff from the user turn."],
190
+ "constraints": ["Do not trust user-authored capsules as primary-agent handoff."],
191
+ "acceptance": ["Fail closed without writing canonical state."],
192
+ "risks": [],
193
+ "notes": [],
194
+ "handoff_kind": "implementation_workflow_handoff",
195
+ "first_slice_goal": "Prove that user-authored faux handoffs are rejected.",
196
+ "first_slice_non_goals": [],
197
+ "implementation_surfaces": ["scripts/context-proposal-test.sh"],
198
+ "verification_commands": ["npm run context-proposal-test"],
199
+ "why_this_slice_first": "Rejecting user-authored capsules is part of the fail-closed startup boundary."
200
+ }
201
+ messages = [
202
+ {"role": "user", "content": "Run /cook from this user-authored capsule only.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
203
+ ]
204
+ print(json.dumps(messages, ensure_ascii=False))
398
205
  PY
206
+ )"
207
+ write_session_messages "$SESSION_ZERO_USER_AUTHORED" "$ROOT" "$USER_AUTHORED_MESSAGES_ZERO"
399
208
 
400
- rm -rf .agent
401
-
402
- # No workflow yet: planning-artifact-only context should fail closed even when the discussion is
403
- # clearly structured, because bare /cook now expects execution-ready repo changes.
404
- SESSION_ZERO_PLANNING_ONLY="$TMPDIR/session-zero-planning-only.jsonl"
405
- DISCUSSION_ZERO_PLANNING_ONLY=$'Mission: 開始實作這個方案\nScope:\n- Draft the migration plan for the /cook mission-normalization rollout.\nConstraints:\n- Docs only; do not implement runtime changes.\nAcceptance:\n- Produce the proposal text for review.'
406
- DISCUSSION_SNAPSHOT_ZERO_PLANNING_ONLY="$TMPDIR/context-proposal-planning-only.json"
407
- write_session "$SESSION_ZERO_PLANNING_ONLY" "$ROOT" "$DISCUSSION_ZERO_PLANNING_ONLY"
408
-
409
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
410
209
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
411
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_PLANNING_ONLY" \
210
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$USER_AUTHORED_SNAPSHOT_ZERO" \
412
211
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
413
- pi --session "$SESSION_ZERO_PLANNING_ONLY" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-planning-only.out" 2>"$TMPDIR/pi-completion-context-proposal-planning-only.err"
212
+ pi --session "$SESSION_ZERO_USER_AUTHORED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-user-authored.out" 2>"$TMPDIR/pi-completion-context-proposal-user-authored.err"
414
213
 
415
- python3 - "$TMPDIR/pi-completion-context-proposal-planning-only.out" "$TMPDIR/pi-completion-context-proposal-planning-only.err" "$DISCUSSION_SNAPSHOT_ZERO_PLANNING_ONLY" <<'PY'
214
+ python3 - "$TMPDIR/pi-completion-context-proposal-user-authored.out" "$TMPDIR/pi-completion-context-proposal-user-authored.err" "$USER_AUTHORED_SNAPSHOT_ZERO" <<'PY'
416
215
  import sys
417
216
  from pathlib import Path
418
217
 
419
218
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
420
219
  snapshot = Path(sys.argv[3])
421
- assert not Path('.agent').exists(), 'planning-only startup should fail closed without writing canonical state'
422
- assert not snapshot.exists(), 'planning-only startup should not emit a proposal snapshot when bare /cook fails closed'
423
- assert '/cook failed closed' in output, 'planning-only startup should explain the fail-closed startup outcome'
424
- assert 'Mission/Scope/Constraints/Acceptance' in output, 'planning-only startup should still explain the structured discussion requirement'
425
- assert 'concrete repo changes' in output, 'planning-only startup should explain that bare /cook now expects execution-ready repo changes'
426
- PY
427
-
428
- # No workflow yet: docs-only tracked deliverables such as README/CHANGELOG updates should
429
- # normalize placeholder planning missions into concrete repo-change missions.
430
- SESSION_ZERO_SUPPORT_DOCS_ONLY="$TMPDIR/session-zero-support-docs-only.jsonl"
431
- DISCUSSION_ZERO_SUPPORT_DOCS_ONLY=$'Mission: 開始實作這個方案\nScope:\n- Update README and CHANGELOG for the /cook mission-normalization behavior.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Add documentation for the operator-facing refocus flow.'
432
- DISCUSSION_SNAPSHOT_ZERO_SUPPORT_DOCS_ONLY="$TMPDIR/context-proposal-support-docs-only.json"
433
- write_session "$SESSION_ZERO_SUPPORT_DOCS_ONLY" "$ROOT" "$DISCUSSION_ZERO_SUPPORT_DOCS_ONLY"
434
-
435
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
436
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
437
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_SUPPORT_DOCS_ONLY" \
438
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
439
- pi --session "$SESSION_ZERO_SUPPORT_DOCS_ONLY" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-support-docs-only.out" 2>"$TMPDIR/pi-completion-context-proposal-support-docs-only.err"
440
-
441
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_SUPPORT_DOCS_ONLY" <<'PY'
442
- import json
443
- import sys
444
- from pathlib import Path
445
-
446
- mission = 'Update README and CHANGELOG for the /cook mission-normalization behavior.'
447
- proposal = json.loads(Path(sys.argv[1]).read_text())
448
- state = json.loads(Path('.agent/state.json').read_text())
449
-
450
- assert proposal['mission'] == mission, 'docs-only startup should normalize the placeholder planning mission to the tracked-doc repo change'
451
- assert state['mission_anchor'] == mission, 'docs-only startup should write the normalized tracked-doc mission into canonical state'
452
- assert proposal['scope'][0] == mission, 'docs-only startup should derive the mission from the tracked-doc scope item'
453
- assert proposal['acceptance'] == ['Add documentation for the operator-facing refocus flow.'], 'docs-only startup should keep the documentation acceptance item'
454
- PY
455
-
456
- rm -rf .agent
457
-
458
- # No workflow yet: reviewer-reproduced docs-only phrasing with edit/document wording should
459
- # also normalize placeholder planning missions into concrete repo-change missions.
460
- SESSION_ZERO_EDIT_DOCUMENT_DOCS_ONLY="$TMPDIR/session-zero-edit-document-docs-only.jsonl"
461
- DISCUSSION_ZERO_EDIT_DOCUMENT_DOCS_ONLY=$'Mission: 開始實作這個方案\nScope:\n- Edit README to explain the /cook mission-normalization behavior.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Document the operator-facing refocus flow.'
462
- DISCUSSION_SNAPSHOT_ZERO_EDIT_DOCUMENT_DOCS_ONLY="$TMPDIR/context-proposal-edit-document-docs-only.json"
463
- write_session "$SESSION_ZERO_EDIT_DOCUMENT_DOCS_ONLY" "$ROOT" "$DISCUSSION_ZERO_EDIT_DOCUMENT_DOCS_ONLY"
464
-
465
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
466
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
467
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_EDIT_DOCUMENT_DOCS_ONLY" \
468
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
469
- pi --session "$SESSION_ZERO_EDIT_DOCUMENT_DOCS_ONLY" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-edit-document-docs-only.out" 2>"$TMPDIR/pi-completion-context-proposal-edit-document-docs-only.err"
470
-
471
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_EDIT_DOCUMENT_DOCS_ONLY" <<'PY'
472
- import json
473
- import sys
474
- from pathlib import Path
475
-
476
- mission = 'Edit README to explain the /cook mission-normalization behavior.'
477
- proposal = json.loads(Path(sys.argv[1]).read_text())
478
- state = json.loads(Path('.agent/state.json').read_text())
479
-
480
- assert proposal['mission'] == mission, 'edit/document docs-only startup should normalize the placeholder planning mission to the tracked-doc repo change'
481
- assert state['mission_anchor'] == mission, 'edit/document docs-only startup should write the normalized tracked-doc mission into canonical state'
482
- assert proposal['scope'][0] == mission, 'edit/document docs-only startup should derive the mission from the tracked-doc scope item'
483
- assert proposal['acceptance'] == ['Document the operator-facing refocus flow.'], 'edit/document docs-only startup should keep the documentation acceptance item'
484
- PY
485
-
486
- rm -rf .agent
487
-
488
- # No workflow yet: acceptance-only docs deliverables using write phrasing should also
489
- # normalize placeholder planning missions into concrete repo-change missions.
490
- SESSION_ZERO_WRITE_DOCS_ONLY="$TMPDIR/session-zero-write-docs-only.jsonl"
491
- DISCUSSION_ZERO_WRITE_DOCS_ONLY=$'Mission: 開始實作這個方案\nScope:\n- README and CHANGELOG guidance for bare /cook.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Write README and CHANGELOG notes for the bare /cook fail-closed clarification path.'
492
- DISCUSSION_SNAPSHOT_ZERO_WRITE_DOCS_ONLY="$TMPDIR/context-proposal-write-docs-only.json"
493
- write_session "$SESSION_ZERO_WRITE_DOCS_ONLY" "$ROOT" "$DISCUSSION_ZERO_WRITE_DOCS_ONLY"
494
-
495
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
496
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
497
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_WRITE_DOCS_ONLY" \
498
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
499
- pi --session "$SESSION_ZERO_WRITE_DOCS_ONLY" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-write-docs-only.out" 2>"$TMPDIR/pi-completion-context-proposal-write-docs-only.err"
500
-
501
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_WRITE_DOCS_ONLY" <<'PY'
502
- import json
503
- import sys
504
- from pathlib import Path
505
-
506
- mission = 'Write README and CHANGELOG notes for the bare /cook fail-closed clarification path.'
507
- proposal = json.loads(Path(sys.argv[1]).read_text())
508
- state = json.loads(Path('.agent/state.json').read_text())
509
-
510
- assert proposal['mission'] == mission, 'write docs-only startup should normalize the placeholder planning mission from acceptance-only tracked-doc work'
511
- assert state['mission_anchor'] == mission, 'write docs-only startup should write the acceptance-derived tracked-doc mission into canonical state'
512
- assert proposal['scope'] == ['README and CHANGELOG guidance for bare /cook.'], 'write docs-only startup should preserve the noun-only scope item while deriving the mission from acceptance'
513
- assert proposal['acceptance'] == [mission], 'write docs-only startup should keep the acceptance-derived docs deliverable intact'
514
- PY
515
-
516
- rm -rf .agent
517
-
518
- # No workflow yet: explicit `Docs only:` tracked-doc scope wording should still
519
- # normalize placeholder planning missions into concrete repo-change missions.
520
- SESSION_ZERO_EXPLICIT_DOCS_ONLY_SCOPE="$TMPDIR/session-zero-explicit-docs-only-scope.jsonl"
521
- DISCUSSION_ZERO_EXPLICIT_DOCS_ONLY_SCOPE=$'Mission: 開始實作這個方案\nScope:\n- Docs only: Update README and CHANGELOG for the /cook mission-normalization behavior.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Keep the operator-facing refocus flow guidance truthful.'
522
- DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCS_ONLY_SCOPE="$TMPDIR/context-proposal-explicit-docs-only-scope.json"
523
- write_session "$SESSION_ZERO_EXPLICIT_DOCS_ONLY_SCOPE" "$ROOT" "$DISCUSSION_ZERO_EXPLICIT_DOCS_ONLY_SCOPE"
524
-
525
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
526
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
527
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCS_ONLY_SCOPE" \
528
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
529
- pi --session "$SESSION_ZERO_EXPLICIT_DOCS_ONLY_SCOPE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-explicit-docs-only-scope.out" 2>"$TMPDIR/pi-completion-context-proposal-explicit-docs-only-scope.err"
530
-
531
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCS_ONLY_SCOPE" <<'PY'
532
- import json
533
- import sys
534
- from pathlib import Path
535
-
536
- mission = 'Update README and CHANGELOG for the /cook mission-normalization behavior.'
537
- proposal = json.loads(Path(sys.argv[1]).read_text())
538
- state = json.loads(Path('.agent/state.json').read_text())
539
-
540
- assert proposal['mission'] == mission, 'explicit docs only scope should strip the docs-only qualifier while normalizing to the tracked-doc repo change'
541
- assert state['mission_anchor'] == mission, 'explicit docs only scope should write the stripped tracked-doc mission into canonical state'
542
- assert proposal['scope'] == ['Docs only: Update README and CHANGELOG for the /cook mission-normalization behavior.'], 'explicit docs only scope should preserve the original scope wording in the proposal body'
543
- assert proposal['acceptance'] == ['Keep the operator-facing refocus flow guidance truthful.'], 'explicit docs only scope should preserve the non-mission acceptance item'
544
- PY
545
-
546
- rm -rf .agent
547
-
548
- # No workflow yet: explicit `Documentation only:` tracked-doc acceptance wording should also
549
- # normalize placeholder planning missions into concrete repo-change missions.
550
- SESSION_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE="$TMPDIR/session-zero-explicit-documentation-only-acceptance.jsonl"
551
- DISCUSSION_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE=$'Mission: 開始實作這個方案\nScope:\n- README and CHANGELOG guidance for bare /cook.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Documentation only: Write README and CHANGELOG notes for the bare /cook fail-closed clarification path.'
552
- DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE="$TMPDIR/context-proposal-explicit-documentation-only-acceptance.json"
553
- write_session "$SESSION_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE" "$ROOT" "$DISCUSSION_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE"
554
-
555
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
556
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
557
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE" \
558
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
559
- pi --session "$SESSION_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-explicit-documentation-only-acceptance.out" 2>"$TMPDIR/pi-completion-context-proposal-explicit-documentation-only-acceptance.err"
560
-
561
- python3 - "$DISCUSSION_SNAPSHOT_ZERO_EXPLICIT_DOCUMENTATION_ONLY_ACCEPTANCE" <<'PY'
562
- import json
563
- import sys
564
- from pathlib import Path
565
-
566
- mission = 'Write README and CHANGELOG notes for the bare /cook fail-closed clarification path.'
567
- proposal = json.loads(Path(sys.argv[1]).read_text())
568
- state = json.loads(Path('.agent/state.json').read_text())
569
-
570
- assert proposal['mission'] == mission, 'explicit documentation only acceptance should strip the docs-only qualifier while normalizing to the tracked-doc repo change'
571
- assert state['mission_anchor'] == mission, 'explicit documentation only acceptance should write the stripped tracked-doc mission into canonical state'
572
- assert proposal['scope'] == ['README and CHANGELOG guidance for bare /cook.'], 'explicit documentation only acceptance should preserve the noun-only scope item while deriving the mission from acceptance'
573
- assert proposal['acceptance'] == ['Documentation only: Write README and CHANGELOG notes for the bare /cook fail-closed clarification path.'], 'explicit documentation only acceptance should preserve the original acceptance wording in the proposal body'
220
+ assert not Path('.agent').exists(), 'user-authored faux handoff should fail closed without writing canonical state'
221
+ assert not snapshot.exists(), 'user-authored faux handoff should not emit a startup proposal snapshot'
222
+ assert 'recent discussion did not produce a clear execution-ready startup brief' in output, 'user-authored faux handoff should still fail closed unless ordinary discussion is concretely startable'
574
223
  PY
575
224
 
576
- rm -rf .agent
577
-
578
- # No workflow yet: assistant-authored completed-plan summaries should fail closed instead of
579
- # seeding startup proposals when the user has not restated an execution-ready mission.
580
- SESSION_ZERO_ASSISTANT_SUMMARY="$TMPDIR/session-zero-assistant-summary.jsonl"
581
- DISCUSSION_SNAPSHOT_ZERO_ASSISTANT_SUMMARY="$TMPDIR/context-proposal-assistant-summary.json"
582
- python3 - "$SESSION_ZERO_ASSISTANT_SUMMARY" "$ROOT" <<'PY'
583
- import json
584
- import sys
585
- from pathlib import Path
225
+ # No workflow yet: malformed or invalid assistant handoff capsules must also fail closed.
226
+ SESSION_ZERO_INVALID="$TMPDIR/session-zero-invalid-handoff.jsonl"
227
+ INVALID_SNAPSHOT_ZERO="$TMPDIR/context-proposal-invalid-handoff.json"
228
+ INVALID_MESSAGES_ZERO='[{"role":"assistant","content":"This is not a valid startup capsule.\n\n```cook_handoff\n{\"kind\":\"cook_handoff\",\"source\":\"primary_agent\",\"mission\":\"Broken JSON handoff\"\n```"}]'
229
+ write_session_messages "$SESSION_ZERO_INVALID" "$ROOT" "$INVALID_MESSAGES_ZERO"
586
230
 
587
- session_path = Path(sys.argv[1])
588
- cwd = sys.argv[2]
589
- session_path.parent.mkdir(parents=True, exist_ok=True)
590
- entries = [
591
- {
592
- "type": "session",
593
- "version": 3,
594
- "id": "11111111-1111-4111-8111-111111111111",
595
- "timestamp": "2026-01-01T00:00:00.000Z",
596
- "cwd": cwd,
597
- },
598
- {
599
- "type": "message",
600
- "id": "b2c3d4e5",
601
- "parentId": None,
602
- "timestamp": "2026-01-01T00:00:02.000Z",
603
- "message": {
604
- "role": "assistant",
605
- "content": "Mission: Ship the replacement workflow from the completed plan.\nScope:\n- Rewrite bare /cook around the finished plan summary.\nConstraints:\n- Keep the approval-only Start/Cancel gate unchanged.\nAcceptance:\n- Start immediately from this summary without more user clarification.",
606
- "timestamp": 1767225602000,
607
- },
608
- },
609
- ]
610
- with session_path.open('w', encoding='utf-8') as fh:
611
- for entry in entries:
612
- fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
613
- PY
614
-
615
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
616
231
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
617
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_ASSISTANT_SUMMARY" \
232
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$INVALID_SNAPSHOT_ZERO" \
618
233
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
619
- pi --session "$SESSION_ZERO_ASSISTANT_SUMMARY" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-assistant-summary.out" 2>"$TMPDIR/pi-completion-context-proposal-assistant-summary.err"
234
+ pi --session "$SESSION_ZERO_INVALID" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-invalid-handoff.out" 2>"$TMPDIR/pi-completion-context-proposal-invalid-handoff.err"
620
235
 
621
- python3 - "$TMPDIR/pi-completion-context-proposal-assistant-summary.out" "$TMPDIR/pi-completion-context-proposal-assistant-summary.err" "$DISCUSSION_SNAPSHOT_ZERO_ASSISTANT_SUMMARY" <<'PY'
236
+ python3 - "$TMPDIR/pi-completion-context-proposal-invalid-handoff.out" "$TMPDIR/pi-completion-context-proposal-invalid-handoff.err" "$INVALID_SNAPSHOT_ZERO" <<'PY'
622
237
  import sys
623
238
  from pathlib import Path
624
239
 
625
240
  output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
626
241
  snapshot = Path(sys.argv[3])
627
- assert not Path('.agent').exists(), 'assistant-only startup summary should fail closed without writing canonical state'
628
- assert not snapshot.exists(), 'assistant-only startup summary should not emit a proposal snapshot when bare /cook fails closed'
629
- assert '/cook failed closed' in output, 'assistant-only startup summary should explain the fail-closed startup outcome'
630
- assert 'concrete repo changes' in output, 'assistant-only startup summary should explain that bare /cook expects execution-ready repo changes from main-chat discussion'
242
+ assert not Path('.agent').exists(), 'invalid assistant handoff should fail closed without writing canonical state'
243
+ assert not snapshot.exists(), 'invalid assistant handoff should not emit a startup proposal snapshot'
244
+ assert 'recent discussion did not produce a clear execution-ready startup brief' in output, 'invalid assistant handoff should still fail closed when no clear recent-discussion startup brief exists'
631
245
  PY
632
246
 
633
- rm -rf .agent
634
-
635
- # No workflow yet: analyst-derived generic planning missions should still fail closed when discussion
636
- # never provides a clear implementation anchor, instead of promoting vague non-doc scope.
637
- SESSION_ZERO_ANALYST_AMBIGUOUS_GENERIC="$TMPDIR/session-zero-analyst-ambiguous-generic.jsonl"
638
- DISCUSSION_ZERO_ANALYST_AMBIGUOUS_GENERIC=$'We should revisit the completion widget while roles are active and make the outcome easier to follow without deciding the exact implementation yet.'
639
- ANALYST_OUTPUT_ZERO_AMBIGUOUS_GENERIC='{"mission":"開始實作這個方案","scope":["The completion widget during active roles."],"constraints":["Keep the approval-only Start/Cancel gate unchanged."],"acceptance":["Current behavior stays understandable."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.74}'
640
- DISCUSSION_SNAPSHOT_ZERO_ANALYST_AMBIGUOUS_GENERIC="$TMPDIR/context-proposal-analyst-ambiguous-generic.json"
641
- write_session "$SESSION_ZERO_ANALYST_AMBIGUOUS_GENERIC" "$ROOT" "$DISCUSSION_ZERO_ANALYST_AMBIGUOUS_GENERIC"
642
-
643
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
644
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_ZERO_AMBIGUOUS_GENERIC" \
645
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_ANALYST_AMBIGUOUS_GENERIC" \
646
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
647
- pi --session "$SESSION_ZERO_ANALYST_AMBIGUOUS_GENERIC" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-analyst-ambiguous-generic.out" 2>"$TMPDIR/pi-completion-context-proposal-analyst-ambiguous-generic.err"
648
-
649
- python3 - "$TMPDIR/pi-completion-context-proposal-analyst-ambiguous-generic.out" "$TMPDIR/pi-completion-context-proposal-analyst-ambiguous-generic.err" "$DISCUSSION_SNAPSHOT_ZERO_ANALYST_AMBIGUOUS_GENERIC" <<'PY'
650
- import sys
651
- from pathlib import Path
652
-
653
- output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
654
- snapshot = Path(sys.argv[3])
655
- assert not Path('.agent').exists(), 'analyst-derived ambiguous generic discussion should fail closed without writing canonical state'
656
- assert not snapshot.exists(), 'analyst-derived ambiguous generic discussion should not emit a proposal snapshot when bare /cook fails closed'
657
- assert '/cook failed closed' in output, 'analyst-derived ambiguous generic discussion should explain the fail-closed startup outcome'
247
+ # No workflow yet: a fresh explicit primary-agent handoff should still bootstrap canonical startup state.
248
+ SESSION_ONE="$TMPDIR/session-one.jsonl"
249
+ HANDOFF_SNAPSHOT_ONE="$TMPDIR/context-proposal-explicit-startup.json"
250
+ HANDOFF_MESSAGES_ONE="$(python3 - <<'PY'
251
+ import json
252
+ capsule = {
253
+ "kind": "cook_handoff",
254
+ "source": "primary_agent",
255
+ "captured_at": "2026-01-01T00:00:02.000Z",
256
+ "source_turn_id": "m0002",
257
+ "mission": "Remove the completion status line while keeping the completion widget.",
258
+ "scope": [
259
+ "Keep the non-running completion widget.",
260
+ "Suppress the widget while a completion role is active."
261
+ ],
262
+ "constraints": [
263
+ "Do not reintroduce any other completion status surface."
264
+ ],
265
+ "acceptance": [
266
+ "Update README to match the shipped behavior.",
267
+ "Keep observability regression coverage truthful."
268
+ ],
269
+ "risks": [
270
+ "Stale widget-removal discussion could broaden the startup plan if the handoff is ignored."
271
+ ],
272
+ "notes": [
273
+ "Keep the startup brief aligned with the explicit primary-agent plan."
274
+ ],
275
+ "handoff_kind": "implementation_workflow_handoff",
276
+ "first_slice_goal": "Land the completion-status removal and keep the completion widget coverage truthful.",
277
+ "first_slice_non_goals": [
278
+ "Do not reintroduce any other completion status surface."
279
+ ],
280
+ "implementation_surfaces": [
281
+ "extensions/completion/index.ts",
282
+ "scripts/context-proposal-test.sh"
283
+ ],
284
+ "verification_commands": [
285
+ "npm run context-proposal-test"
286
+ ],
287
+ "why_this_slice_first": "The startup boundary regression is already bounded enough to implement safely.",
288
+ "task_type": "completion-workflow",
289
+ "evaluation_profile": "completion-rubric-v1",
290
+ "why_cook_now": "The explicit startup brief is concrete and ready for repo changes."
291
+ }
292
+ messages = [
293
+ {"role": "user", "content": "Please think through the completion widget startup boundary and tell me when it is ready for /cook."},
294
+ {"role": "assistant", "content": "This task is now ready for /cook. Run /cook to confirm the startup brief.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
295
+ ]
296
+ print(json.dumps(messages, ensure_ascii=False))
658
297
  PY
659
-
660
- # No workflow yet: /cook with no goal should infer from recent discussion through analyst output.
661
- SESSION_ONE="$TMPDIR/session-one.jsonl"
662
- DISCUSSION_ONE="$DISCUSSION_ZERO"
663
- ANALYST_OUTPUT_ONE='{"mission":"Remove the completion status line while keeping the completion widget.","scope":["Keep the non-running completion widget.","Suppress the widget while a completion role is active."],"constraints":["Do not reintroduce any other completion status surface."],"acceptance":["Update README to match the shipped behavior.","Keep observability regression coverage truthful."],"critique":["Keep critique separate from the mission anchor so startup analysis does not rewrite the workflow goal."],"risks":["Stale widget-removal discussion could broaden the startup plan if it gets treated as mission text."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","possible_noise":["older widget restyle ideas"],"confidence":0.94}'
664
- DISCUSSION_SNAPSHOT_ONE="$TMPDIR/context-proposal-discussion-hints.json"
665
- write_session "$SESSION_ONE" "$ROOT" "$DISCUSSION_ONE"
298
+ )"
299
+ write_session_messages "$SESSION_ONE" "$ROOT" "$HANDOFF_MESSAGES_ONE"
666
300
 
667
301
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
668
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_ONE" \
669
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ONE" \
302
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_ONE" \
670
303
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
671
- pi --session "$SESSION_ONE" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-bootstrap.out 2>/tmp/pi-completion-context-proposal-bootstrap.err
304
+ pi --session "$SESSION_ONE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-explicit-startup.out" 2>"$TMPDIR/pi-completion-context-proposal-explicit-startup.err"
672
305
 
673
- python3 - "$DISCUSSION_SNAPSHOT_ONE" <<'PY'
306
+ python3 - "$HANDOFF_SNAPSHOT_ONE" <<'PY'
674
307
  import json
675
308
  import sys
676
309
  from pathlib import Path
@@ -685,61 +318,107 @@ plan = json.loads(Path('.agent/plan.json').read_text())
685
318
  active = json.loads(Path('.agent/active-slice.json').read_text())
686
319
  proposal = json.loads(Path(sys.argv[1]).read_text())
687
320
 
688
- assert mission in mission_text, '.agent/mission.md did not record the analyst-derived mission anchor'
689
- assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after analyst-derived bootstrap'
690
- assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after analyst-derived bootstrap'
691
- assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after analyst-derived bootstrap'
692
- assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after analyst-derived bootstrap'
693
- assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after analyst-derived bootstrap'
694
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after analyst-derived bootstrap'
695
- assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after analyst-derived bootstrap'
696
- assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after analyst-derived bootstrap'
697
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after analyst-derived bootstrap'
698
- assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after analyst-derived bootstrap'
699
- assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after analyst-derived bootstrap'
321
+ assert mission in mission_text, '.agent/mission.md did not record the explicit-handoff mission anchor'
322
+ assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after explicit-handoff bootstrap'
323
+ assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after explicit-handoff bootstrap'
324
+ assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after explicit-handoff bootstrap'
325
+ assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after explicit-handoff bootstrap'
326
+ assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after explicit-handoff bootstrap'
327
+ assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after explicit-handoff bootstrap'
328
+ assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after explicit-handoff bootstrap'
329
+ assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after explicit-handoff bootstrap'
330
+ assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after explicit-handoff bootstrap'
331
+ assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after explicit-handoff bootstrap'
332
+ assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after explicit-handoff bootstrap'
700
333
  brief = state['advisory_startup_brief']
701
334
  assert brief['kind'] == 'startup_brief', 'state.json should preserve the confirmed startup brief as advisory intake'
335
+ assert brief['source'] == 'primary_agent_handoff', 'explicit startup should record the handoff source in advisory intake'
702
336
  assert brief['mission'] == mission, 'advisory startup brief mission should match the accepted mission anchor'
703
337
  assert brief['scope'] == ['Keep the non-running completion widget.', 'Suppress the widget while a completion role is active.'], 'advisory startup brief should preserve scope items separately from canonical planning state'
704
338
  assert brief['constraints'] == ['Do not reintroduce any other completion status surface.'], 'advisory startup brief should preserve constraints separately from canonical planning state'
705
339
  assert brief['acceptance'] == ['Update README to match the shipped behavior.', 'Keep observability regression coverage truthful.'], 'advisory startup brief should preserve acceptance separately from canonical planning state'
706
- assert brief['risks'] == ['Stale widget-removal discussion could broaden the startup plan if it gets treated as mission text.'], 'advisory startup brief should preserve derived risks'
707
- assert brief['notes'] == ['Keep critique separate from the mission anchor so startup analysis does not rewrite the workflow goal.', 'Possible noise: older widget restyle ideas'], 'advisory startup brief should preserve operator notes and possible-noise cautions'
340
+ assert brief['risks'] == ['Stale widget-removal discussion could broaden the startup plan if the handoff is ignored.'], 'advisory startup brief should preserve handoff risks'
341
+ assert 'First slice goal: Land the completion-status removal and keep the completion widget coverage truthful.' in brief['notes'], 'advisory startup brief should preserve first_slice_goal in notes'
342
+ assert 'Verification commands: npm run context-proposal-test' in brief['notes'], 'advisory startup brief should preserve verification_commands in notes'
708
343
  assert plan['candidate_slices'] == [], 'startup brief should remain advisory intake only until regrounder owns plan selection'
709
344
  assert active['status'] == 'idle', 'startup brief should not become the active-slice source before regrounder runs'
710
- assert proposal['mission'] == mission, 'discussion-only proposal snapshot should keep the inferred mission anchor'
711
- assert proposal['analysis']['taskType'] == expected_task_type, 'discussion-only proposal snapshot should expose task_type hints separately'
712
- assert proposal['analysis']['evaluationProfile'] == expected_eval_profile, 'discussion-only proposal snapshot should expose evaluation_profile hints separately'
713
- assert proposal['analysis']['critique'] == ['Keep critique separate from the mission anchor so startup analysis does not rewrite the workflow goal.'], 'discussion-only proposal snapshot should preserve critique hints'
714
- assert proposal['analysis']['risks'] == ['Stale widget-removal discussion could broaden the startup plan if it gets treated as mission text.'], 'discussion-only proposal snapshot should preserve risk hints'
715
- assert proposal['analysis']['possibleNoise'] == ['older widget restyle ideas'], 'discussion-only proposal snapshot should preserve possible_noise hints'
716
- assert 'Critique:' not in proposal['goalText'], 'goalText should keep critique separate from mission/scope/constraints/acceptance'
717
- assert 'Task type:' not in proposal['goalText'], 'goalText should keep task_type hints separate from the mission body'
718
- assert state['current_phase'] == 'reground', 'state.json current_phase should start at reground after analyst-derived bootstrap'
719
- assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should start at completion-regrounder after analyst-derived bootstrap'
345
+ assert proposal['mission'] == mission, 'explicit startup proposal snapshot should keep the handoff mission anchor'
346
+ assert proposal['source'] == 'handoff_capsule', 'explicit startup proposal snapshot should expose the handoff capsule source'
347
+ assert proposal['analysis']['taskType'] == expected_task_type, 'explicit startup proposal snapshot should expose task_type hints separately'
348
+ assert proposal['analysis']['evaluationProfile'] == expected_eval_profile, 'explicit startup proposal snapshot should expose evaluation_profile hints separately'
349
+ assert state['current_phase'] == 'reground', 'state.json current_phase should start at reground after explicit-handoff bootstrap'
350
+ assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should start at completion-regrounder after explicit-handoff bootstrap'
720
351
  assert state['continuation_reason'].startswith('User started workflow via /cook:'), 'initial startup should record the accepted startup routing in continuation_reason'
721
352
  assert 'task_type=completion-workflow' in state['continuation_reason'], 'initial startup should persist the selected task_type in continuation_reason'
722
353
  assert 'evaluation_profile=completion-rubric-v1' in state['continuation_reason'], 'initial startup should persist the selected evaluation_profile in continuation_reason'
723
- assert 'Keep critique separate from the mission anchor so startup analysis does not rewrite the workflow goal.' in state['continuation_reason'], 'initial startup should persist the accepted critique outcome in continuation_reason'
724
354
  PY
725
355
 
726
- # Active workflow: bare /cook with matching structured discussion should classify as continue
727
- # and resume the current workflow without opening the chooser or rewriting canonical state.
356
+ # Active workflow: bare /cook should resume from canonical state when no fresh explicit handoff exists,
357
+ # even if recent discussion restates the current mission in a structured way.
728
358
  SESSION_ONE_CONTINUE="$TMPDIR/session-one-continue.jsonl"
729
359
  DISCUSSION_ONE_CONTINUE=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the current mission focused on the non-running completion widget.\nConstraints:\n- Do not start a different workflow from this discussion.\nAcceptance:\n- Resume the current workflow from canonical state without rewriting it.'
730
360
  CONTINUE_ROUTING_ONE="$TMPDIR/active-continue-routing.json"
731
361
  CONTINUE_RESUME_PROMPT_ONE="$TMPDIR/active-continue-resume.txt"
732
362
  CONTINUE_CHOOSER_ONE="$TMPDIR/unexpected-active-continue-chooser.json"
363
+ CONTINUE_PROPOSAL_ONE="$TMPDIR/unexpected-active-continue-proposal.json"
733
364
  write_session "$SESSION_ONE_CONTINUE" "$ROOT" "$DISCUSSION_ONE_CONTINUE"
734
365
 
735
366
  PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
736
367
  PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$CONTINUE_ROUTING_ONE" \
737
368
  PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$CONTINUE_RESUME_PROMPT_ONE" \
738
369
  PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$CONTINUE_CHOOSER_ONE" \
370
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$CONTINUE_PROPOSAL_ONE" \
739
371
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
740
372
  pi --session "$SESSION_ONE_CONTINUE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-continue.out" 2>"$TMPDIR/pi-completion-context-proposal-active-continue.err"
741
373
 
742
- python3 - "$CONTINUE_ROUTING_ONE" "$CONTINUE_RESUME_PROMPT_ONE" "$CONTINUE_CHOOSER_ONE" <<'PY'
374
+ python3 - "$CONTINUE_ROUTING_ONE" "$CONTINUE_RESUME_PROMPT_ONE" "$CONTINUE_CHOOSER_ONE" "$CONTINUE_PROPOSAL_ONE" <<'PY'
375
+ import json
376
+ import sys
377
+ from pathlib import Path
378
+
379
+ mission = 'Remove the completion status line while keeping the completion widget.'
380
+ routing = json.loads(Path(sys.argv[1]).read_text())
381
+ resume = Path(sys.argv[2]).read_text()
382
+ chooser_path = Path(sys.argv[3])
383
+ proposal_path = Path(sys.argv[4])
384
+ state = json.loads(Path('.agent/state.json').read_text())
385
+ plan = json.loads(Path('.agent/plan.json').read_text())
386
+ active = json.loads(Path('.agent/active-slice.json').read_text())
387
+
388
+ assert routing['mode'] == 'bare', 'active bare /cook resume regression should snapshot bare routing mode'
389
+ assert 'explicitGoal' not in routing, 'active bare /cook resume routing should not expose removed explicit-goal shim fields'
390
+ assert 'explicitGoalProvided' not in routing, 'active bare /cook resume routing should not expose removed explicit-goal shim fields'
391
+ assert routing['action'] == 'continue', 'active bare /cook should resume when no fresh explicit handoff exists'
392
+ assert routing['reason'] == 'missing_explicit_handoff', 'active bare /cook should explain that resume happened because no fresh explicit handoff existed'
393
+ assert routing['currentMissionAnchor'] == mission, 'resume routing should preserve the current mission anchor'
394
+ assert routing['proposedMissionAnchor'] is None, 'resume routing should not derive a replacement mission from recent discussion'
395
+ assert 'Resume the completion workflow from canonical state.' in resume, 'active bare /cook resume should still use the canonical resume prompt'
396
+ assert not chooser_path.exists(), 'active bare /cook resume should not open the replacement chooser without a fresh explicit handoff'
397
+ assert not proposal_path.exists(), 'active bare /cook resume should not open replacement proposal confirmation without a fresh explicit handoff'
398
+ assert state['mission_anchor'] == mission, 'active bare /cook resume should keep state.json unchanged'
399
+ assert plan['mission_anchor'] == mission, 'active bare /cook resume should keep plan.json unchanged'
400
+ assert active['mission_anchor'] == mission, 'active bare /cook resume should keep active-slice.json unchanged'
401
+ PY
402
+
403
+ # Active workflow: even strongly different recent discussion should no longer open chooser/refocus startup
404
+ # when no fresh valid explicit handoff is present.
405
+ SESSION_ONE_DISCUSSION_REFOCUS="$TMPDIR/session-one-discussion-refocus.jsonl"
406
+ DISCUSSION_ONE_DISCUSSION_REFOCUS=$'Mission: Normalize bare /cook planning phrasing into implementation-result missions.\nScope:\n- Replace the current workflow from recent discussion only.\n- Keep the approval-only Start/Cancel gate before rewriting canonical state.\nConstraints:\n- Do not require a fresh explicit handoff.\nAcceptance:\n- Rewrite canonical state from recent discussion.'
407
+ DISCUSSION_REFOCUS_ROUTING_ONE="$TMPDIR/active-discussion-refocus-routing.json"
408
+ DISCUSSION_REFOCUS_RESUME_ONE="$TMPDIR/active-discussion-refocus-resume.txt"
409
+ DISCUSSION_REFOCUS_CHOOSER_ONE="$TMPDIR/unexpected-active-discussion-refocus-chooser.json"
410
+ DISCUSSION_REFOCUS_PROPOSAL_ONE="$TMPDIR/unexpected-active-discussion-refocus-proposal.json"
411
+ write_session "$SESSION_ONE_DISCUSSION_REFOCUS" "$ROOT" "$DISCUSSION_ONE_DISCUSSION_REFOCUS"
412
+
413
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
414
+ PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$DISCUSSION_REFOCUS_ROUTING_ONE" \
415
+ PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$DISCUSSION_REFOCUS_RESUME_ONE" \
416
+ PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$DISCUSSION_REFOCUS_CHOOSER_ONE" \
417
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_REFOCUS_PROPOSAL_ONE" \
418
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
419
+ pi --session "$SESSION_ONE_DISCUSSION_REFOCUS" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-discussion-refocus.out" 2>"$TMPDIR/pi-completion-context-proposal-active-discussion-refocus.err"
420
+
421
+ python3 - "$DISCUSSION_REFOCUS_ROUTING_ONE" "$DISCUSSION_REFOCUS_RESUME_ONE" "$DISCUSSION_REFOCUS_CHOOSER_ONE" "$DISCUSSION_REFOCUS_PROPOSAL_ONE" <<'PY'
743
422
  import json
744
423
  import sys
745
424
  from pathlib import Path
@@ -748,26 +427,26 @@ mission = 'Remove the completion status line while keeping the completion widget
748
427
  routing = json.loads(Path(sys.argv[1]).read_text())
749
428
  resume = Path(sys.argv[2]).read_text()
750
429
  chooser_path = Path(sys.argv[3])
430
+ proposal_path = Path(sys.argv[4])
751
431
  state = json.loads(Path('.agent/state.json').read_text())
752
432
  plan = json.loads(Path('.agent/plan.json').read_text())
753
433
  active = json.loads(Path('.agent/active-slice.json').read_text())
754
434
 
755
- assert routing['mode'] == 'bare', 'active bare /cook continue regression should snapshot bare routing mode'
756
- assert 'explicitGoal' not in routing, 'active bare /cook continue routing should not expose removed explicit-goal shim fields'
757
- assert 'explicitGoalProvided' not in routing, 'active bare /cook continue routing should not expose removed explicit-goal shim fields'
758
- assert routing['action'] == 'continue', 'matching structured discussion should classify active bare /cook as continue'
759
- assert routing['reason'] == 'matching_mission', 'matching structured discussion should keep the current mission rather than refocus'
760
- assert routing['currentMissionAnchor'] == mission, 'continue routing should preserve the current mission anchor'
761
- assert routing['proposedMissionAnchor'] == mission, 'continue routing should keep the proposed mission anchored to the current mission'
762
- assert 'Resume the completion workflow from canonical state.' in resume, 'active bare /cook continue should still use the canonical resume prompt'
763
- assert not chooser_path.exists(), 'active bare /cook continue should not open the refocus chooser'
764
- assert state['mission_anchor'] == mission, 'active bare /cook continue should keep state.json unchanged'
765
- assert plan['mission_anchor'] == mission, 'active bare /cook continue should keep plan.json unchanged'
766
- assert active['mission_anchor'] == mission, 'active bare /cook continue should keep active-slice.json unchanged'
435
+ assert routing['mode'] == 'bare', 'discussion-driven refocus removal should snapshot bare routing mode'
436
+ assert routing['action'] == 'continue', 'bare /cook should resume instead of deriving a replacement workflow from recent discussion'
437
+ assert routing['reason'] == 'missing_explicit_handoff', 'discussion-driven refocus removal should explain that no fresh explicit handoff existed'
438
+ assert routing['currentMissionAnchor'] == mission, 'discussion-driven refocus removal should preserve the current mission anchor'
439
+ assert routing['proposedMissionAnchor'] is None, 'discussion-driven refocus removal should not preserve a replacement mission from recent discussion'
440
+ assert 'Resume the completion workflow from canonical state.' in resume, 'discussion-driven refocus removal should still queue the canonical resume prompt'
441
+ assert not chooser_path.exists(), 'discussion-driven refocus removal should not open the chooser'
442
+ assert not proposal_path.exists(), 'discussion-driven refocus removal should not open final proposal confirmation'
443
+ assert state['mission_anchor'] == mission, 'discussion-driven refocus removal should keep state.json unchanged'
444
+ assert plan['mission_anchor'] == mission, 'discussion-driven refocus removal should keep plan.json unchanged'
445
+ assert active['mission_anchor'] == mission, 'discussion-driven refocus removal should keep active-slice.json unchanged'
767
446
  PY
768
447
 
769
- # Active workflow: summary-only replacement artifacts should fail closed and keep the current
770
- # workflow instead of opening the refocus chooser.
448
+ # Active workflow: summary-only replacement artifacts should also resume the current workflow when no fresh
449
+ # explicit handoff exists.
771
450
  SESSION_ONE_SUMMARY_ONLY="$TMPDIR/session-one-summary-only.jsonl"
772
451
  SUMMARY_ROUTING_ONE="$TMPDIR/active-summary-only-routing.json"
773
452
  SUMMARY_RESUME_PROMPT_ONE="$TMPDIR/active-summary-only-resume.txt"
@@ -828,8 +507,8 @@ plan = json.loads(Path('.agent/plan.json').read_text())
828
507
  active = json.loads(Path('.agent/active-slice.json').read_text())
829
508
 
830
509
  assert routing['mode'] == 'bare', 'summary-only active bare /cook regression should snapshot bare routing mode'
831
- assert routing['action'] == 'unclear', 'summary-only active bare /cook should fail closed instead of refocusing'
832
- assert routing['reason'] == 'missing_proposal', 'summary-only active bare /cook should treat the summary artifact as unreadiness, not a new proposal'
510
+ assert routing['action'] == 'continue', 'summary-only active bare /cook should resume rather than derive replacement startup'
511
+ assert routing['reason'] == 'missing_explicit_handoff', 'summary-only active bare /cook should explain that no fresh explicit handoff existed'
833
512
  assert routing['currentMissionAnchor'] == mission, 'summary-only active bare /cook should preserve the current mission anchor'
834
513
  assert routing['proposedMissionAnchor'] is None, 'summary-only active bare /cook should not derive a replacement mission from summary artifacts alone'
835
514
  assert 'Resume the completion workflow from canonical state.' in resume, 'summary-only active bare /cook should still resume the canonical workflow'
@@ -840,146 +519,108 @@ assert plan['mission_anchor'] == mission, 'summary-only active bare /cook should
840
519
  assert active['mission_anchor'] == mission, 'summary-only active bare /cook should keep active-slice.json unchanged'
841
520
  PY
842
521
 
843
- # Active workflow: when recent discussion suggests a different implementation goal but the proposal is
844
- # still incomplete, bare /cook should surface the chooser instead of silently resuming the current workflow.
845
- SESSION_ONE_AMBIGUOUS_CHOOSER="$TMPDIR/session-one-ambiguous-chooser.jsonl"
846
- DISCUSSION_ONE_AMBIGUOUS_CHOOSER=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic for the current auth flow.'
847
- AMBIGUOUS_ROUTING_ONE="$TMPDIR/active-ambiguous-routing.json"
848
- AMBIGUOUS_CHOOSER_ONE="$TMPDIR/active-ambiguous-chooser.json"
849
- AMBIGUOUS_RESUME_PROMPT_ONE="$TMPDIR/unexpected-active-ambiguous-resume.txt"
850
- AMBIGUOUS_PROPOSAL_ONE="$TMPDIR/unexpected-active-ambiguous-proposal.json"
851
- write_session "$SESSION_ONE_AMBIGUOUS_CHOOSER" "$ROOT" "$DISCUSSION_ONE_AMBIGUOUS_CHOOSER"
852
-
853
- PI_COMPLETION_EXISTING_WORKFLOW_ACTION=cancel \
854
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT='{"mission":"Fix login redirect callback behavior.","scope":["Update the callback redirect decision logic for the current auth flow."],"constraints":[],"acceptance":[],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.72,"possible_noise":["older completion widget cleanup"]}' \
855
- PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$AMBIGUOUS_ROUTING_ONE" \
856
- PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$AMBIGUOUS_CHOOSER_ONE" \
857
- PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$AMBIGUOUS_RESUME_PROMPT_ONE" \
858
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$AMBIGUOUS_PROPOSAL_ONE" \
859
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
860
- pi --session "$SESSION_ONE_AMBIGUOUS_CHOOSER" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.out" 2>"$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.err"
861
-
862
- python3 - "$AMBIGUOUS_ROUTING_ONE" "$AMBIGUOUS_CHOOSER_ONE" "$AMBIGUOUS_RESUME_PROMPT_ONE" "$AMBIGUOUS_PROPOSAL_ONE" "$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.out" "$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.err" <<'PY'
522
+ # Active workflow: a fresh explicit handoff that is not implementation-startable should still fail closed
523
+ # without rewriting canonical state.
524
+ SESSION_ONE_NON_STARTABLE_ACTIVE="$TMPDIR/session-one-non-startable-active.jsonl"
525
+ NON_STARTABLE_ACTIVE_ROUTING="$TMPDIR/active-non-startable-routing.json"
526
+ NON_STARTABLE_ACTIVE_RESUME="$TMPDIR/unexpected-active-non-startable-resume.txt"
527
+ NON_STARTABLE_ACTIVE_CHOOSER="$TMPDIR/unexpected-active-non-startable-chooser.json"
528
+ NON_STARTABLE_ACTIVE_PROPOSAL="$TMPDIR/unexpected-active-non-startable-proposal.json"
529
+ NON_STARTABLE_ACTIVE_MESSAGES="$(python3 - <<'PY'
863
530
  import json
864
- import sys
865
- from pathlib import Path
866
-
867
- mission = 'Remove the completion status line while keeping the completion widget.'
868
- routing = json.loads(Path(sys.argv[1]).read_text())
869
- chooser = json.loads(Path(sys.argv[2]).read_text())
870
- resume_path = Path(sys.argv[3])
871
- proposal_path = Path(sys.argv[4])
872
- output = Path(sys.argv[5]).read_text() + Path(sys.argv[6]).read_text()
873
- state = json.loads(Path('.agent/state.json').read_text())
874
- plan = json.loads(Path('.agent/plan.json').read_text())
875
- active = json.loads(Path('.agent/active-slice.json').read_text())
876
-
877
- assert routing['mode'] == 'bare', 'ambiguous active bare /cook should snapshot bare routing mode'
878
- assert routing['action'] == 'unclear', 'incomplete replacement discussion should stay ambiguous until the chooser resolves it'
879
- assert routing['reason'] == 'ambiguous_discussion', 'incomplete replacement discussion should record the ambiguous-discussion reason'
880
- assert routing['currentMissionAnchor'] == mission, 'ambiguous chooser routing should preserve the current mission anchor'
881
- assert routing['proposedMissionAnchor'] == 'Fix login redirect callback behavior.', 'ambiguous chooser routing should expose the latest inferred mission'
882
- assert chooser['title'].startswith('Existing completion workflow found'), 'ambiguous active bare /cook should still open the existing-workflow chooser'
883
- assert chooser['choices'][0].startswith('Continue current workflow'), 'ambiguous chooser should keep the continue option'
884
- assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'ambiguous chooser should offer the recent-discussion replacement option'
885
- assert not resume_path.exists(), 'ambiguous active bare /cook should not silently queue a resume prompt before the chooser resolves it'
886
- assert not proposal_path.exists(), 'ambiguous chooser cancel should not open the final proposal confirmation'
887
- assert 'Discuss changes in the main chat and rerun /cook.' in output, 'ambiguous chooser cancel should redirect users back to the main chat and rerun /cook'
888
- assert state['mission_anchor'] == mission, 'ambiguous chooser cancel should keep state.json unchanged'
889
- assert plan['mission_anchor'] == mission, 'ambiguous chooser cancel should keep plan.json unchanged'
890
- assert active['mission_anchor'] == mission, 'ambiguous chooser cancel should keep active-slice.json unchanged'
531
+ capsule = {
532
+ "kind": "cook_handoff",
533
+ "source": "primary_agent",
534
+ "captured_at": "2026-01-01T00:00:02.000Z",
535
+ "source_turn_id": "m0002",
536
+ "mission": "Replace the current widget mission from a vague explicit handoff.",
537
+ "scope": [
538
+ "Replace the active workflow from a fresh explicit handoff."
539
+ ],
540
+ "constraints": [
541
+ "Do not rely on recent discussion to fill in missing implementation details."
542
+ ],
543
+ "acceptance": [
544
+ "Current behavior stays understandable."
545
+ ],
546
+ "risks": [],
547
+ "notes": [
548
+ "This capsule is intentionally non-startable for active-workflow fail-closed coverage."
549
+ ],
550
+ "handoff_kind": "implementation_workflow_handoff",
551
+ "first_slice_goal": "Attempt to replace the active workflow from a vague capsule.",
552
+ "first_slice_non_goals": [],
553
+ "implementation_surfaces": [
554
+ "extensions/completion/driver.ts"
555
+ ],
556
+ "verification_commands": [
557
+ "npm run context-proposal-test"
558
+ ],
559
+ "why_this_slice_first": "Active-workflow replacement should fail closed when the capsule is fresh but not startable.",
560
+ "task_type": "completion-workflow",
561
+ "evaluation_profile": "completion-rubric-v1"
562
+ }
563
+ messages = [
564
+ {"role": "user", "content": "We may need a different active workflow, but only if there is a fresh explicit handoff."},
565
+ {"role": "assistant", "content": "Only use this capsule if it is concrete enough.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
566
+ ]
567
+ print(json.dumps(messages, ensure_ascii=False))
891
568
  PY
569
+ )"
570
+ write_session_messages "$SESSION_ONE_NON_STARTABLE_ACTIVE" "$ROOT" "$NON_STARTABLE_ACTIVE_MESSAGES"
892
571
 
893
- # Active workflow: when recent discussion contains multiple complete replacement missions, bare /cook should
894
- # surface all candidates and allow the chooser to select a non-primary alternate mission.
895
- SESSION_ONE_MULTI_CANDIDATE="$TMPDIR/session-one-multi-candidate.jsonl"
896
- DISCUSSION_ONE_MULTI_CANDIDATE=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic for the current auth flow.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page.\nMission: Add logout redirect regression coverage.\nScope:\n- Add coverage for logout redirect behavior.\nConstraints:\n- Do not change login redirect behavior in this pass.\nAcceptance:\n- Land a dedicated logout redirect regression test.'
897
- MULTI_ROUTING_ONE="$TMPDIR/active-multi-routing.json"
898
- MULTI_CHOOSER_ONE="$TMPDIR/active-multi-chooser.json"
899
- MULTI_PROPOSAL_ONE="$TMPDIR/active-multi-proposal.json"
900
- write_session "$SESSION_ONE_MULTI_CANDIDATE" "$ROOT" "$DISCUSSION_ONE_MULTI_CANDIDATE"
901
-
902
- PI_COMPLETION_EXISTING_WORKFLOW_MISSION='Fix login redirect callback behavior.' \
903
- PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
904
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
905
- PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$MULTI_ROUTING_ONE" \
906
- PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$MULTI_CHOOSER_ONE" \
907
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$MULTI_PROPOSAL_ONE" \
908
- PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
909
- pi --session "$SESSION_ONE_MULTI_CANDIDATE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-multi.out" 2>"$TMPDIR/pi-completion-context-proposal-active-multi.err"
910
-
911
- python3 - "$MULTI_ROUTING_ONE" "$MULTI_CHOOSER_ONE" "$MULTI_PROPOSAL_ONE" <<'PY'
572
+ python3 - "$TMPDIR/active-non-startable-before.json" <<'PY'
912
573
  import json
913
574
  import sys
914
575
  from pathlib import Path
915
-
916
- selected = 'Fix login redirect callback behavior.'
917
- routing = json.loads(Path(sys.argv[1]).read_text())
918
- chooser = json.loads(Path(sys.argv[2]).read_text())
919
- proposal = json.loads(Path(sys.argv[3]).read_text())
920
- state = json.loads(Path('.agent/state.json').read_text())
921
- plan = json.loads(Path('.agent/plan.json').read_text())
922
- active = json.loads(Path('.agent/active-slice.json').read_text())
923
-
924
- assert routing['action'] == 'unclear', 'multi-candidate replacement discussion should stay ambiguous until the chooser selects one mission'
925
- assert routing['reason'] == 'ambiguous_discussion', 'multi-candidate replacement discussion should record ambiguous-discussion routing'
926
- assert routing['alternateMissions'] == ['Fix login redirect callback behavior.'], 'routing snapshot should preserve alternate candidate missions'
927
- assert chooser['candidateMissions'] == ['Add logout redirect regression coverage.', 'Fix login redirect callback behavior.'], 'chooser snapshot should list the primary and alternate candidate missions'
928
- assert len(chooser['choices']) == 4, 'chooser should expose continue, primary, alternate, and cancel choices'
929
- assert 'Scope\n- Add coverage for logout redirect behavior.' in chooser['choices'][1], 'primary chooser option should summarize the candidate scope'
930
- assert 'Acceptance\n- Add a regression test for returning to the requested page.' in chooser['choices'][2], 'alternate chooser option should summarize the candidate acceptance'
931
- assert proposal['mission'] == selected, 'selected alternate mission should flow into the final proposal confirmation'
932
- assert state['mission_anchor'] == selected, 'selected alternate mission should rewrite state.json after approval'
933
- assert plan['mission_anchor'] == selected, 'selected alternate mission should rewrite plan.json after approval'
934
- assert active['mission_anchor'] == selected, 'selected alternate mission should rewrite active-slice.json after approval'
576
+ tracked = {
577
+ 'mission.md': Path('.agent/mission.md').read_text(),
578
+ 'profile.json': Path('.agent/profile.json').read_text(),
579
+ 'state.json': Path('.agent/state.json').read_text(),
580
+ 'plan.json': Path('.agent/plan.json').read_text(),
581
+ 'active-slice.json': Path('.agent/active-slice.json').read_text(),
582
+ 'verification-evidence.json': Path('.agent/verification-evidence.json').read_text(),
583
+ }
584
+ Path(sys.argv[1]).write_text(json.dumps(tracked, indent=2) + '\n')
935
585
  PY
936
586
 
937
- # Active workflow: bare /cook with a placeholder planning mission should still route through the existing
938
- # refocus chooser and final Start/Cancel gate before canonical state is rewritten.
939
- SESSION_ONE_REFOCUS_NORMALIZED="$TMPDIR/session-one-refocus-normalized.jsonl"
940
- DISCUSSION_ONE_REFOCUS_NORMALIZED=$'Mission: 開始實作這個方案\nScope:\n- Normalize bare /cook planning phrasing into implementation-result missions.\n- Keep the approval-only Start/Cancel gate before rewriting canonical state.\nConstraints:\n- Do not resume the current widget mission.\nAcceptance:\n- Route through chooser-driven refocus before rewriting canonical state.'
941
- REFOCUS_ROUTING_ONE="$TMPDIR/active-refocus-routing.json"
942
- REFOCUS_CHOOSER_ONE="$TMPDIR/active-refocus-chooser.json"
943
- REFOCUS_PROPOSAL_ONE="$TMPDIR/active-refocus-proposal.json"
944
- REFOCUS_UI_ONE="$TMPDIR/active-refocus-ui.json"
945
- write_session "$SESSION_ONE_REFOCUS_NORMALIZED" "$ROOT" "$DISCUSSION_ONE_REFOCUS_NORMALIZED"
946
-
947
- PI_COMPLETION_EXISTING_WORKFLOW_ACTION=refocus \
948
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=start \
949
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
950
- PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$REFOCUS_ROUTING_ONE" \
951
- PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$REFOCUS_CHOOSER_ONE" \
952
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$REFOCUS_PROPOSAL_ONE" \
953
- PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH="$REFOCUS_UI_ONE" \
587
+ PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$NON_STARTABLE_ACTIVE_ROUTING" \
588
+ PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$NON_STARTABLE_ACTIVE_RESUME" \
589
+ PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$NON_STARTABLE_ACTIVE_CHOOSER" \
590
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$NON_STARTABLE_ACTIVE_PROPOSAL" \
954
591
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
955
- pi --session "$SESSION_ONE_REFOCUS_NORMALIZED" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-refocus-normalized.out" 2>"$TMPDIR/pi-completion-context-proposal-active-refocus-normalized.err"
592
+ pi --session "$SESSION_ONE_NON_STARTABLE_ACTIVE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-non-startable.out" 2>"$TMPDIR/pi-completion-context-proposal-active-non-startable.err"
956
593
 
957
- python3 - "$REFOCUS_ROUTING_ONE" "$REFOCUS_CHOOSER_ONE" "$REFOCUS_PROPOSAL_ONE" "$REFOCUS_UI_ONE" <<'PY'
594
+ python3 - "$NON_STARTABLE_ACTIVE_ROUTING" "$NON_STARTABLE_ACTIVE_RESUME" "$NON_STARTABLE_ACTIVE_CHOOSER" "$NON_STARTABLE_ACTIVE_PROPOSAL" "$TMPDIR/pi-completion-context-proposal-active-non-startable.out" "$TMPDIR/pi-completion-context-proposal-active-non-startable.err" "$TMPDIR/active-non-startable-before.json" <<'PY'
958
595
  import json
959
596
  import sys
960
597
  from pathlib import Path
961
598
 
962
- mission = 'Normalize bare /cook planning phrasing into implementation-result missions.'
963
599
  routing = json.loads(Path(sys.argv[1]).read_text())
964
- chooser = json.loads(Path(sys.argv[2]).read_text())
965
- proposal = json.loads(Path(sys.argv[3]).read_text())
966
- ui = json.loads(Path(sys.argv[4]).read_text())
967
- state = json.loads(Path('.agent/state.json').read_text())
968
- plan = json.loads(Path('.agent/plan.json').read_text())
969
- active = json.loads(Path('.agent/active-slice.json').read_text())
600
+ resume_path = Path(sys.argv[2])
601
+ chooser_path = Path(sys.argv[3])
602
+ proposal_path = Path(sys.argv[4])
603
+ output = Path(sys.argv[5]).read_text() + Path(sys.argv[6]).read_text()
604
+ before = json.loads(Path(sys.argv[7]).read_text())
605
+ after = {
606
+ 'mission.md': Path('.agent/mission.md').read_text(),
607
+ 'profile.json': Path('.agent/profile.json').read_text(),
608
+ 'state.json': Path('.agent/state.json').read_text(),
609
+ 'plan.json': Path('.agent/plan.json').read_text(),
610
+ 'active-slice.json': Path('.agent/active-slice.json').read_text(),
611
+ 'verification-evidence.json': Path('.agent/verification-evidence.json').read_text(),
612
+ }
970
613
 
971
- assert routing['mode'] == 'bare', 'active bare /cook refocus normalization should snapshot bare routing mode'
972
- assert 'explicitGoal' not in routing, 'active bare /cook refocus routing should not expose removed explicit-goal shim fields'
973
- assert 'explicitGoalProvided' not in routing, 'active bare /cook refocus routing should not expose removed explicit-goal shim fields'
974
- assert routing['action'] == 'refocus', 'placeholder planning mission should still classify active bare /cook as refocus when the normalized mission changes'
975
- assert routing['reason'] == 'clear_refocus', 'active bare /cook refocus normalization should keep the clear_refocus routing reason'
976
- assert routing['proposedMissionAnchor'] == mission, 'active bare /cook refocus should normalize the proposed mission before canonical rewrite'
977
- assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'active bare /cook refocus should still route through the existing chooser copy before rewrite'
978
- assert [action['id'] for action in ui['actions']] == ['start', 'cancel'], 'active bare /cook refocus should still end at the approval-only Start/Cancel gate'
979
- assert proposal['mission'] == mission, 'active bare /cook refocus proposal snapshot should expose the normalized implementation mission'
980
- assert state['mission_anchor'] == mission, 'active bare /cook refocus should rewrite canonical state to the normalized mission only after approval'
981
- assert plan['mission_anchor'] == mission, 'active bare /cook refocus should rewrite plan.json only after approval'
982
- assert active['mission_anchor'] == mission, 'active bare /cook refocus should rewrite active-slice.json only after approval'
614
+ assert routing['mode'] == 'bare', 'fresh non-startable explicit handoff should snapshot bare routing mode'
615
+ assert routing['action'] == 'blocked', 'fresh non-startable explicit handoff should fail closed for active bare /cook'
616
+ assert routing['reason'] == 'fresh_explicit_handoff_not_startable', 'fresh non-startable explicit handoff should keep the dedicated explicit-handoff fail-closed reason'
617
+ assert 'fresh explicit primary-agent handoff exists' in routing['blockedFailureMessage'], 'fresh non-startable explicit handoff should surface the dedicated fail-closed message'
618
+ assert 'acceptance is not anchored to concrete repo changes or verification' in routing['blockedFailureMessage'], 'fresh non-startable explicit handoff should explain why the capsule is not startable'
619
+ assert not resume_path.exists(), 'fresh non-startable explicit handoff should not queue a resume prompt'
620
+ assert not chooser_path.exists(), 'fresh non-startable explicit handoff should not open the replacement chooser'
621
+ assert not proposal_path.exists(), 'fresh non-startable explicit handoff should not open final proposal confirmation'
622
+ assert 'fresh explicit primary-agent handoff exists' in output, 'fresh non-startable explicit handoff should explain that the explicit capsule blocked active-workflow replacement'
623
+ assert before == after, 'fresh non-startable explicit handoff should leave canonical state unchanged'
983
624
  PY
984
625
 
985
626
  # Completed workflow: bare /cook should suppress proposals that simply restate the completed mission
@@ -1067,10 +708,9 @@ assert not snapshot.exists(), 'verification-evidence overlap suppression should
1067
708
  assert '/cook failed closed' in output, 'verification-evidence overlap suppression should fail closed when the latest discussion only repeats verified work'
1068
709
  PY
1069
710
 
1070
- # Completed workflow: bare /cook should normalize placeholder planning phrasing for the next workflow
1071
- # round too, not only for fresh startup.
711
+ # Completed workflow: bare /cook should synthesize the next-round startup brief from recent discussion.
1072
712
  SESSION_TWO_NORMALIZED="$TMPDIR/session-two-normalized.jsonl"
1073
- DISCUSSION_TWO_NORMALIZED=$'Mission: 開始實作這個方案\nScope:\n- Normalize bare /cook planning phrasing for the next workflow round.\n- Reset canonical state for the new implementation mission.\nConstraints:\n- Do not resume the completed workflow when the new round is clearly different.\nAcceptance:\n- Start a new round with the normalized mission anchor.'
713
+ DISCUSSION_TWO_NORMALIZED=$'Mission: Ship the next workflow round from recent discussion.\nScope:\n- Reset canonical state back to reground for the new mission.\n- Preserve the tracked completion control-plane files.\nConstraints:\n- Do not resume the completed workflow when the new round is clearly different.\nAcceptance:\n- Reset canonical state back to reground for the new mission.\n- Preserve the tracked completion control-plane files.'
1074
714
  DISCUSSION_SNAPSHOT_TWO_NORMALIZED="$TMPDIR/context-proposal-next-round-normalized.json"
1075
715
  write_session "$SESSION_TWO_NORMALIZED" "$ROOT" "$DISCUSSION_TWO_NORMALIZED"
1076
716
 
@@ -1085,29 +725,99 @@ import json
1085
725
  import sys
1086
726
  from pathlib import Path
1087
727
 
1088
- mission = 'Normalize bare /cook planning phrasing for the next workflow round.'
1089
- proposal = json.loads(Path(sys.argv[1]).read_text())
728
+ mission = 'Ship the next workflow round from recent discussion.'
729
+ expected_task_type = 'completion-workflow'
730
+ expected_eval_profile = 'completion-rubric-v1'
731
+ mission_text = Path('.agent/mission.md').read_text()
732
+ profile = json.loads(Path('.agent/profile.json').read_text())
1090
733
  state = json.loads(Path('.agent/state.json').read_text())
1091
734
  plan = json.loads(Path('.agent/plan.json').read_text())
1092
735
  active = json.loads(Path('.agent/active-slice.json').read_text())
736
+ proposal = json.loads(Path(sys.argv[1]).read_text())
1093
737
 
1094
- assert proposal['mission'] == mission, 'done-workflow structured fallback should normalize the placeholder planning mission for the next round'
1095
- assert state['mission_anchor'] == mission, 'done-workflow startup should rewrite canonical state to the normalized next-round mission'
1096
- assert plan['mission_anchor'] == mission, 'done-workflow startup should rewrite plan.json to the normalized next-round mission'
1097
- assert active['mission_anchor'] == mission, 'done-workflow startup should rewrite active-slice.json to the normalized next-round mission'
1098
- assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'done-workflow normalization should still route through refocus semantics for the next round'
738
+ assert mission in mission_text, '.agent/mission.md did not update to the recent-discussion next-round mission anchor'
739
+ assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after recent-discussion next-round startup'
740
+ assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after recent-discussion next-round startup'
741
+ assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after starting the next workflow round from recent discussion'
742
+ assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after starting the next workflow round from recent discussion'
743
+ assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after starting the next workflow round from recent discussion'
744
+ assert state['advisory_startup_brief']['mission'] == mission, 'recent-discussion next round should preserve the confirmed startup brief as advisory intake'
745
+ assert state['advisory_startup_brief']['source'] == 'recent_discussion', 'recent-discussion next round should preserve the advisory source'
746
+ assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after starting the next workflow round from recent discussion'
747
+ assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after starting the next workflow round from recent discussion'
748
+ assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after starting the next workflow round from recent discussion'
749
+ assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after starting the next workflow round from recent discussion'
750
+ assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after starting the next workflow round from recent discussion'
751
+ assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after starting the next workflow round from recent discussion'
752
+ assert proposal['mission'] == mission, 'recent-discussion next-round proposal snapshot should preserve the synthesized mission anchor'
753
+ assert proposal['source'] == 'session', 'recent-discussion next-round proposal snapshot should record the structured-session source'
754
+ assert state['current_phase'] == 'reground', 'state.json current_phase should reset to reground for the recent-discussion next workflow round'
755
+ assert state['continuation_policy'] == 'continue', 'continuation_policy should reset to continue for the recent-discussion next workflow round'
756
+ assert state['requires_reground'] is True, 'requires_reground should reset to true for the recent-discussion next workflow round'
757
+ assert state['project_done'] is False, 'project_done should reset to false for the recent-discussion next workflow round'
758
+ assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should reset to completion-regrounder for the recent-discussion next workflow round'
759
+ assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'continuation_reason should record the recent-discussion next-round refocus'
760
+ assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_refocus for the recent-discussion next workflow round'
761
+ assert active['status'] == 'idle', 'active-slice should reset to idle for the recent-discussion next workflow round'
1099
762
  PY
1100
763
 
1101
- # Completed workflow: bare /cook should use the same strict structured fallback for the next workflow round when analyst output is unavailable.
764
+ # Completed workflow: a fresh explicit primary-agent handoff should still start the next round.
1102
765
  mark_done
1103
766
 
1104
767
  SESSION_TWO="$TMPDIR/session-two.jsonl"
1105
- DISCUSSION_TWO=$'Mission: Ship the next workflow round for richer context-derived /cook startup.\nScope:\n- Start a new workflow round from recent discussion after the previous one is done.\n- Keep using canonical .agent state after confirmation.\nConstraints:\n- Do not resume the completed workflow when the new round is clearly different.\nAcceptance:\n- Reset canonical state back to reground for the new mission.\n- Preserve the tracked completion control-plane files.'
1106
- DISCUSSION_SNAPSHOT_TWO="$TMPDIR/context-proposal-next-round-structured-fallback.json"
1107
- write_session "$SESSION_TWO" "$ROOT" "$DISCUSSION_TWO"
768
+ DISCUSSION_SNAPSHOT_TWO="$TMPDIR/context-proposal-next-round-explicit-handoff.json"
769
+ HANDOFF_MESSAGES_TWO="$(python3 - <<'PY'
770
+ import json
771
+ capsule = {
772
+ "kind": "cook_handoff",
773
+ "source": "primary_agent",
774
+ "captured_at": "2026-01-01T00:00:02.000Z",
775
+ "source_turn_id": "m0002",
776
+ "mission": "Ship the next workflow round from a fresh explicit handoff.",
777
+ "scope": [
778
+ "Reset canonical state back to reground for the fresh mission.",
779
+ "Preserve the tracked completion control-plane files."
780
+ ],
781
+ "constraints": [
782
+ "Do not resume the completed workflow when the new round is clearly different."
783
+ ],
784
+ "acceptance": [
785
+ "Reset canonical state back to reground for the new mission.",
786
+ "Preserve the tracked completion control-plane files."
787
+ ],
788
+ "risks": [
789
+ "Done-state history could override the fresh mission if the explicit handoff is ignored."
790
+ ],
791
+ "notes": [
792
+ "This next round must come from the fresh explicit handoff rather than recent discussion."
793
+ ],
794
+ "handoff_kind": "implementation_workflow_handoff",
795
+ "first_slice_goal": "Start the next round from the fresh explicit handoff and preserve canonical control-plane files.",
796
+ "first_slice_non_goals": [
797
+ "Do not resume the completed workflow when the new round is clearly different."
798
+ ],
799
+ "implementation_surfaces": [
800
+ "extensions/completion/driver.ts",
801
+ "scripts/context-proposal-test.sh"
802
+ ],
803
+ "verification_commands": [
804
+ "npm run context-proposal-test"
805
+ ],
806
+ "why_this_slice_first": "The fresh explicit handoff is the smallest truthful next-round startup after the previous workflow closed.",
807
+ "task_type": "completion-workflow",
808
+ "evaluation_profile": "completion-rubric-v1",
809
+ "why_cook_now": "A new implementation-ready mission was identified after the previous round closed."
810
+ }
811
+ messages = [
812
+ {"role": "user", "content": "The previous round is done, but there is a fresh next round ready for /cook."},
813
+ {"role": "assistant", "content": "The next round is ready for /cook. Run /cook to confirm this fresh implementation mission.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
814
+ ]
815
+ print(json.dumps(messages, ensure_ascii=False))
816
+ PY
817
+ )"
818
+ write_session_messages "$SESSION_TWO" "$ROOT" "$HANDOFF_MESSAGES_TWO"
1108
819
 
1109
820
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
1110
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
1111
821
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO" \
1112
822
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1113
823
  pi --session "$SESSION_TWO" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round.err"
@@ -1117,7 +827,7 @@ import json
1117
827
  import sys
1118
828
  from pathlib import Path
1119
829
 
1120
- mission = 'Ship the next workflow round for richer context-derived /cook startup.'
830
+ mission = 'Ship the next workflow round from a fresh explicit handoff.'
1121
831
  expected_task_type = 'completion-workflow'
1122
832
  expected_eval_profile = 'completion-rubric-v1'
1123
833
  mission_text = Path('.agent/mission.md').read_text()
@@ -1127,21 +837,22 @@ plan = json.loads(Path('.agent/plan.json').read_text())
1127
837
  active = json.loads(Path('.agent/active-slice.json').read_text())
1128
838
  proposal = json.loads(Path(sys.argv[1]).read_text())
1129
839
 
1130
- assert mission in mission_text, '.agent/mission.md did not update to the next-round context-derived mission anchor'
1131
- assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after next-round startup'
1132
- assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after next-round startup'
1133
- assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after starting the next workflow round'
1134
- assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after starting the next workflow round'
1135
- assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after starting the next workflow round'
1136
- assert state['advisory_startup_brief']['mission'] == mission, 'next-round startup should preserve the confirmed startup brief as advisory intake'
1137
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after starting the next workflow round'
1138
- assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after starting the next workflow round'
1139
- assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after starting the next workflow round'
1140
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after starting the next workflow round'
1141
- assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after starting the next workflow round'
1142
- assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after starting the next workflow round'
1143
- assert proposal['mission'] == mission, 'next-round structured-fallback proposal snapshot should preserve the discussion mission anchor'
1144
- assert proposal['source'] == 'session', 'next-round structured-fallback proposal snapshot should record the strict session fallback source'
840
+ assert mission in mission_text, '.agent/mission.md did not update to the next-round explicit-handoff mission anchor'
841
+ assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after next-round explicit handoff startup'
842
+ assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after next-round explicit handoff startup'
843
+ assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after starting the next workflow round from explicit handoff'
844
+ assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after starting the next workflow round from explicit handoff'
845
+ assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after starting the next workflow round from explicit handoff'
846
+ assert state['advisory_startup_brief']['mission'] == mission, 'next-round explicit handoff should preserve the confirmed startup brief as advisory intake'
847
+ assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'next-round explicit handoff should preserve the handoff advisory source'
848
+ assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after starting the next workflow round from explicit handoff'
849
+ assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after starting the next workflow round from explicit handoff'
850
+ assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after starting the next workflow round from explicit handoff'
851
+ assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after starting the next workflow round from explicit handoff'
852
+ assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after starting the next workflow round from explicit handoff'
853
+ assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after starting the next workflow round from explicit handoff'
854
+ assert proposal['mission'] == mission, 'next-round explicit handoff proposal snapshot should preserve the handoff mission anchor'
855
+ assert proposal['source'] == 'handoff_capsule', 'next-round explicit handoff proposal snapshot should record the handoff capsule source'
1145
856
  assert state['current_phase'] == 'reground', 'state.json current_phase should reset to reground for the next workflow round'
1146
857
  assert state['continuation_policy'] == 'continue', 'continuation_policy should reset to continue for the next workflow round'
1147
858
  assert state['requires_reground'] is True, 'requires_reground should reset to true for the next workflow round'
@@ -1150,7 +861,6 @@ assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_
1150
861
  assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'continuation_reason should record the next-round refocus'
1151
862
  assert 'task_type=completion-workflow' in state['continuation_reason'], 'next-round refocus should persist the selected task_type'
1152
863
  assert 'evaluation_profile=completion-rubric-v1' in state['continuation_reason'], 'next-round refocus should persist the selected evaluation_profile'
1153
- assert 'critique outcome=accepted critique=none' in state['continuation_reason'], 'next-round refocus should persist that no critique notes were accepted'
1154
864
  assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_refocus for the next workflow round'
1155
865
  assert active['status'] == 'idle', 'active-slice should reset to idle for the next workflow round'
1156
866
  PY
@@ -1267,53 +977,35 @@ after = {path.name: path.read_text() for path in tracked}
1267
977
  assert before == after, 'done /cook inline-args rejection should leave canonical files unchanged'
1268
978
  PY
1269
979
 
1270
- # Completed workflow again: /cook with no goal should be able to use model-assisted
1271
- # analysis of natural discussion when discussion-only startup depends on analyst output.
980
+ # Completed workflow again: model-assisted discussion analysis should also synthesize the next-round startup brief.
1272
981
  mark_done
1273
982
 
1274
983
  SESSION_FIVE="$TMPDIR/session-five.jsonl"
1275
984
  DISCUSSION_FIVE=$'I do not want to rewrite the parser. The safer path is to let /cook analyze the discussion first, keep the discussion-derived mission anchored once it is clear, and ignore stale scope that drifted in from earlier turns. We should still prove it with a regression test before writing canonical state.'
1276
985
  ANALYST_OUTPUT_FIVE='{"mission":"Use a proposal analyst to summarize natural discussion before /cook writes canonical state.","scope":["Keep the discussion-derived mission anchored once it is clear.","Drop stale scope from earlier turns."],"constraints":["Do not rewrite the parser."],"acceptance":["Add a regression test."],"confidence":0.91,"possible_noise":["old unrelated scope"]}'
986
+ DISCUSSION_SNAPSHOT_FIVE="$TMPDIR/context-proposal-analyst-restart-rejected.json"
1277
987
  write_session "$SESSION_FIVE" "$ROOT" "$DISCUSSION_FIVE"
1278
988
 
1279
989
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
1280
990
  PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_FIVE" \
991
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_FIVE" \
1281
992
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1282
- pi --session "$SESSION_FIVE" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-analyst.out 2>/tmp/pi-completion-context-proposal-analyst.err
993
+ pi --session "$SESSION_FIVE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-analyst.out" 2>"$TMPDIR/pi-completion-context-proposal-analyst.err"
1283
994
 
1284
- python3 - <<'PY'
995
+ python3 - "$DISCUSSION_SNAPSHOT_FIVE" <<'PY'
1285
996
  import json
997
+ import sys
1286
998
  from pathlib import Path
1287
999
 
1288
1000
  mission = 'Use a proposal analyst to summarize natural discussion before /cook writes canonical state.'
1289
- expected_task_type = 'completion-workflow'
1290
- expected_eval_profile = 'completion-rubric-v1'
1291
- mission_text = Path('.agent/mission.md').read_text()
1292
- profile = json.loads(Path('.agent/profile.json').read_text())
1293
1001
  state = json.loads(Path('.agent/state.json').read_text())
1294
- plan = json.loads(Path('.agent/plan.json').read_text())
1295
- active = json.loads(Path('.agent/active-slice.json').read_text())
1296
- continuation_reason = state['continuation_reason']
1297
-
1298
- assert mission in mission_text, '.agent/mission.md did not record the analyst-derived mission anchor'
1299
- assert profile['task_type'] == expected_task_type, 'profile.json task_type mismatch after analyst-derived restart'
1300
- assert profile['evaluation_profile'] == expected_eval_profile, 'profile.json evaluation_profile mismatch after analyst-derived restart'
1301
- assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after analyst-derived bootstrap'
1302
- assert state['task_type'] == expected_task_type, 'state.json task_type mismatch after analyst-derived bootstrap'
1303
- assert state['evaluation_profile'] == expected_eval_profile, 'state.json evaluation_profile mismatch after analyst-derived bootstrap'
1304
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after analyst-derived bootstrap'
1305
- assert plan['task_type'] == expected_task_type, 'plan.json task_type mismatch after analyst-derived bootstrap'
1306
- assert plan['evaluation_profile'] == expected_eval_profile, 'plan.json evaluation_profile mismatch after analyst-derived bootstrap'
1307
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after analyst-derived bootstrap'
1308
- assert active['task_type'] == expected_task_type, 'active-slice.json task_type mismatch after analyst-derived bootstrap'
1309
- assert active['evaluation_profile'] == expected_eval_profile, 'active-slice.json evaluation_profile mismatch after analyst-derived bootstrap'
1310
- assert state['current_phase'] == 'reground', 'current_phase should reset to reground after analyst-derived bootstrap'
1311
- assert state['next_mandatory_role'] == 'completion-regrounder', 'next role should reset to completion-regrounder after analyst-derived bootstrap'
1312
- assert continuation_reason.startswith('User refocused workflow via /cook:'), 'continuation_reason should record the analyst-derived restart'
1313
- assert 'task_type=completion-workflow' in continuation_reason, 'analyst-derived restart should persist the selected task_type'
1314
- assert 'evaluation_profile=completion-rubric-v1' in continuation_reason, 'analyst-derived restart should persist the selected evaluation_profile'
1315
- assert 'critique outcome=accepted critique=none' in continuation_reason, 'analyst-derived restart should persist that no critique notes were accepted'
1316
- assert 'Keep the discussion-derived mission anchored once it is clear.' in continuation_reason, 'analyst-derived scope should be preserved'
1002
+ proposal = json.loads(Path(sys.argv[1]).read_text())
1003
+
1004
+ assert proposal['mission'] == mission, 'analyst-driven restart should emit the synthesized mission in the proposal snapshot'
1005
+ assert proposal['source'] == 'analyst', 'analyst-driven restart should preserve the analyst proposal source'
1006
+ assert state['mission_anchor'] == mission, 'analyst-driven restart should promote the synthesized mission into canonical state after Start'
1007
+ assert state['continuation_policy'] == 'continue', 'analyst-driven restart should reopen the workflow after Start'
1008
+ assert state['advisory_startup_brief']['source'] == 'recent_discussion', 'analyst-driven restart should still record recent-discussion advisory intake'
1317
1009
  PY
1318
1010
 
1319
1011
  # Custom confirmation UI: start should render proposal content separately from approval-only Start/Cancel actions.
@@ -1323,14 +1015,41 @@ cd "$UI_ROOT_START"
1323
1015
  git init -q
1324
1016
 
1325
1017
  UI_SESSION_START="$TMPDIR/ui-session-start.jsonl"
1326
- UI_DISCUSSION_START=$'Mission: Replace the crowded selector with a clearer action layout.\nScope:\n- Separate proposal text from actions.\nConstraints:\n- Preserve approval-only Start/Cancel behavior.\nAcceptance:\n- Add regression coverage.'
1327
- UI_ANALYST_OUTPUT_START='{"mission":"Replace the crowded selector with a clearer action layout.","scope":["Separate proposal text from actions."],"constraints":["Preserve approval-only Start/Cancel behavior."],"acceptance":["Add regression coverage."],"critique":["Keep critique details separate from the approval-only proposal summary."],"risks":["Bundling critique into the action list would make the confirmation harder to scan."],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","possible_noise":["old selector wording"],"confidence":0.95}'
1328
1018
  UI_SNAPSHOT_START="$TMPDIR/context-proposal-ui-start.json"
1329
- write_session "$UI_SESSION_START" "$UI_ROOT_START" "$UI_DISCUSSION_START"
1019
+ UI_MESSAGES_START="$(python3 - <<'PY'
1020
+ import json
1021
+ capsule = {
1022
+ "kind": "cook_handoff",
1023
+ "source": "primary_agent",
1024
+ "captured_at": "2026-01-01T00:00:02.000Z",
1025
+ "source_turn_id": "m0002",
1026
+ "mission": "Replace the crowded selector with a clearer action layout.",
1027
+ "scope": ["Separate proposal text from actions."],
1028
+ "constraints": ["Preserve approval-only Start/Cancel behavior."],
1029
+ "acceptance": ["Add regression coverage."],
1030
+ "risks": ["Bundling critique into the action list would make the confirmation harder to scan."],
1031
+ "notes": ["Keep critique details separate from the approval-only proposal summary.", "Possible noise: old selector wording"],
1032
+ "handoff_kind": "implementation_workflow_handoff",
1033
+ "first_slice_goal": "Separate the proposal text from the approval-only Start/Cancel actions.",
1034
+ "first_slice_non_goals": [],
1035
+ "implementation_surfaces": ["extensions/completion/prompt-surfaces.ts"],
1036
+ "verification_commands": ["npm run context-proposal-test"],
1037
+ "why_this_slice_first": "The confirmation layout regression is small and directly testable.",
1038
+ "task_type": "completion-workflow",
1039
+ "evaluation_profile": "completion-rubric-v1",
1040
+ "why_cook_now": "The explicit handoff is concrete enough to exercise the startup confirmation UI."
1041
+ }
1042
+ messages = [
1043
+ {"role": "user", "content": "Prepare the confirmation-layout work and tell me when it is ready for /cook."},
1044
+ {"role": "assistant", "content": "The confirmation-layout work is ready for /cook.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1045
+ ]
1046
+ print(json.dumps(messages, ensure_ascii=False))
1047
+ PY
1048
+ )"
1049
+ write_session_messages "$UI_SESSION_START" "$UI_ROOT_START" "$UI_MESSAGES_START"
1330
1050
 
1331
1051
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=start \
1332
1052
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH="$UI_SNAPSHOT_START" \
1333
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$UI_ANALYST_OUTPUT_START" \
1334
1053
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1335
1054
  pi --session "$UI_SESSION_START" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-ui-start.out" 2>"$TMPDIR/pi-completion-context-proposal-ui-start.err"
1336
1055
 
@@ -1352,7 +1071,7 @@ assert 'Mission\nReplace the crowded selector with a clearer action layout.' in
1352
1071
  assert 'Keep critique details separate from the approval-only proposal summary.' not in snapshot['proposalBody'], 'critique notes should not be embedded in the startup-brief body'
1353
1072
  assert 'Critique\n- Keep critique details separate from the approval-only proposal summary.' in snapshot['critiqueBody'], 'notes section should render accepted critique notes separately'
1354
1073
  assert 'Risks\n- Bundling critique into the action list would make the confirmation harder to scan.' in snapshot['critiqueBody'], 'critique section should render risk notes separately'
1355
- assert 'Possible noise\n- old selector wording' in snapshot['critiqueBody'], 'critique section should render possible-noise notes separately'
1074
+ assert '- Possible noise: old selector wording' in snapshot['critiqueBody'], 'critique section should preserve additional operator notes separately from the startup-brief body'
1356
1075
  assert '- task_type: completion-workflow' in snapshot['routingBody'], 'routing section should render the recommended task_type'
1357
1076
  assert '- evaluation_profile: completion-rubric-v1' in snapshot['routingBody'], 'routing section should render the recommended evaluation_profile'
1358
1077
  assert [action['id'] for action in snapshot['actions']] == ['start', 'cancel'], 'custom confirmation actions should stay Start/Cancel only'
@@ -1375,14 +1094,41 @@ cd "$UI_ROOT_CANCEL"
1375
1094
  git init -q
1376
1095
 
1377
1096
  UI_SESSION_CANCEL="$TMPDIR/ui-session-cancel.jsonl"
1378
- UI_DISCUSSION_CANCEL=$'Mission: Cancel from the custom confirmation UI without writing state.\nScope:\n- Show the proposal separately from the approval-only actions.\nConstraints:\n- Keep cancellation side-effect free.\nAcceptance:\n- Leave .agent absent after cancel.'
1379
- UI_ANALYST_OUTPUT_CANCEL='{"mission":"Cancel from the custom confirmation UI without writing state.","scope":["Show the proposal separately from the approval-only actions."],"constraints":["Keep cancellation side-effect free."],"acceptance":["Leave .agent absent after cancel."],"confidence":0.92}'
1380
1097
  UI_SNAPSHOT_CANCEL="$TMPDIR/context-proposal-ui-cancel.json"
1381
- write_session "$UI_SESSION_CANCEL" "$UI_ROOT_CANCEL" "$UI_DISCUSSION_CANCEL"
1098
+ UI_MESSAGES_CANCEL="$(python3 - <<'PY'
1099
+ import json
1100
+ capsule = {
1101
+ "kind": "cook_handoff",
1102
+ "source": "primary_agent",
1103
+ "captured_at": "2026-01-01T00:00:02.000Z",
1104
+ "source_turn_id": "m0002",
1105
+ "mission": "Cancel from the custom confirmation UI without writing state.",
1106
+ "scope": ["Show the proposal separately from the approval-only actions."],
1107
+ "constraints": ["Keep cancellation side-effect free."],
1108
+ "acceptance": ["Add regression coverage proving cancel leaves .agent absent."],
1109
+ "risks": [],
1110
+ "notes": [],
1111
+ "handoff_kind": "implementation_workflow_handoff",
1112
+ "first_slice_goal": "Exercise the cancel path without writing canonical state.",
1113
+ "first_slice_non_goals": [],
1114
+ "implementation_surfaces": ["extensions/completion/prompt-surfaces.ts"],
1115
+ "verification_commands": ["npm run context-proposal-test"],
1116
+ "why_this_slice_first": "The cancel path is a direct regression around the startup confirmation UI.",
1117
+ "task_type": "completion-workflow",
1118
+ "evaluation_profile": "completion-rubric-v1",
1119
+ "why_cook_now": "The explicit handoff is concrete enough to exercise the cancel confirmation UI."
1120
+ }
1121
+ messages = [
1122
+ {"role": "user", "content": "Prepare the cancel-path confirmation work and tell me when it is ready for /cook."},
1123
+ {"role": "assistant", "content": "The cancel-path confirmation work is ready for /cook.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1124
+ ]
1125
+ print(json.dumps(messages, ensure_ascii=False))
1126
+ PY
1127
+ )"
1128
+ write_session_messages "$UI_SESSION_CANCEL" "$UI_ROOT_CANCEL" "$UI_MESSAGES_CANCEL"
1382
1129
 
1383
1130
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=cancel \
1384
1131
  PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH="$UI_SNAPSHOT_CANCEL" \
1385
- PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$UI_ANALYST_OUTPUT_CANCEL" \
1386
1132
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1387
1133
  pi --session "$UI_SESSION_CANCEL" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-ui-cancel.out" 2>"$TMPDIR/pi-completion-context-proposal-ui-cancel.err"
1388
1134
 
@@ -1485,6 +1231,85 @@ assert 'Why this slice first: The redirect callback bug is already bounded enoug
1485
1231
  assert 'Primary-agent /cook handoff rationale: The implementation plan is concrete and ready for repo changes.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve why_cook_now as notes'
1486
1232
  PY
1487
1233
 
1234
+ # Fresh explicit handoff: later ordinary-chat follow-up before /cook should not invalidate startup.
1235
+ HANDOFF_ROOT_FOLLOWUP="$TMPDIR/handoff-root-followup"
1236
+ mkdir -p "$HANDOFF_ROOT_FOLLOWUP"
1237
+ cd "$HANDOFF_ROOT_FOLLOWUP"
1238
+ git init -q
1239
+
1240
+ HANDOFF_SESSION_FOLLOWUP="$TMPDIR/handoff-session-followup.jsonl"
1241
+ HANDOFF_SNAPSHOT_FOLLOWUP="$TMPDIR/handoff-proposal-followup.json"
1242
+ HANDOFF_MESSAGES_FOLLOWUP="$(python3 - <<'PY'
1243
+ import json
1244
+ capsule = {
1245
+ "kind": "cook_handoff",
1246
+ "source": "primary_agent",
1247
+ "captured_at": "2026-01-01T00:00:02.000Z",
1248
+ "source_turn_id": "m0002",
1249
+ "mission": "Fix login redirect callback behavior.",
1250
+ "scope": [
1251
+ "Update the callback redirect decision logic.",
1252
+ "Preserve the broader auth flow."
1253
+ ],
1254
+ "constraints": [
1255
+ "Do not refactor the broader auth flow."
1256
+ ],
1257
+ "acceptance": [
1258
+ "Add a regression test for returning to the requested page."
1259
+ ],
1260
+ "risks": [
1261
+ "Late ordinary-chat clarifications should not force the user into a fresh handoff-only retry loop."
1262
+ ],
1263
+ "notes": [
1264
+ "Keep the startup brief anchored to the explicit primary-agent handoff until a later assistant reply replaces it."
1265
+ ],
1266
+ "handoff_kind": "implementation_workflow_handoff",
1267
+ "first_slice_goal": "Land the redirect callback fix and its regression coverage.",
1268
+ "first_slice_non_goals": [
1269
+ "Do not refactor the broader auth flow."
1270
+ ],
1271
+ "implementation_surfaces": [
1272
+ "src/auth/redirect.ts",
1273
+ "tests/auth/redirect.spec.ts"
1274
+ ],
1275
+ "verification_commands": [
1276
+ "npm test -- redirect.spec.ts"
1277
+ ],
1278
+ "why_this_slice_first": "The redirect callback bug is already bounded enough to start implementation safely.",
1279
+ "task_type": "completion-workflow",
1280
+ "evaluation_profile": "completion-rubric-v1",
1281
+ "why_cook_now": "The implementation plan is concrete and ready for repo changes."
1282
+ }
1283
+ messages = [
1284
+ {"role": "user", "content": "Please think through the login redirect fix and tell me when it is ready for /cook."},
1285
+ {"role": "assistant", "content": "This task is now ready for /cook whenever you want to start implementation.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1286
+ {"role": "user", "content": "Before I run /cook, one more clarification: keep the broader auth flow unchanged and keep the later verification focused on the redirect regression."},
1287
+ ]
1288
+ print(json.dumps(messages, ensure_ascii=False))
1289
+ PY
1290
+ )"
1291
+ write_session_messages "$HANDOFF_SESSION_FOLLOWUP" "$HANDOFF_ROOT_FOLLOWUP" "$HANDOFF_MESSAGES_FOLLOWUP"
1292
+
1293
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
1294
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_FOLLOWUP" \
1295
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1296
+ pi --session "$HANDOFF_SESSION_FOLLOWUP" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-followup.out" 2>"$TMPDIR/pi-completion-handoff-followup.err"
1297
+
1298
+ python3 - "$HANDOFF_SNAPSHOT_FOLLOWUP" <<'PY'
1299
+ import json
1300
+ import sys
1301
+ from pathlib import Path
1302
+
1303
+ snapshot = json.loads(Path(sys.argv[1]).read_text())
1304
+ state = json.loads(Path('.agent/state.json').read_text())
1305
+
1306
+ assert snapshot['source'] == 'handoff_capsule', 'later ordinary-chat follow-up should still use the explicit handoff capsule as startup source'
1307
+ assert snapshot['mission'] == 'Fix login redirect callback behavior.', 'later ordinary-chat follow-up should preserve the handoff mission'
1308
+ assert state['mission_anchor'] == 'Fix login redirect callback behavior.', 'later ordinary-chat follow-up should still start from the explicit handoff mission'
1309
+ assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'later ordinary-chat follow-up should keep the advisory intake source as the explicit handoff'
1310
+ assert 'Verification commands: npm test -- redirect.spec.ts' in state['advisory_startup_brief']['notes'], 'later ordinary-chat follow-up should preserve the handoff verification commands'
1311
+ PY
1312
+
1488
1313
  # Fresh but non-startable explicit handoff: /cook should fail closed instead of falling back
1489
1314
  # to a broad recent-discussion startup brief when the explicit capsule is still too vague.
1490
1315
  HANDOFF_ROOT_VAGUE="$TMPDIR/handoff-root-vague"
@@ -1644,10 +1469,35 @@ cd "$HANDOFF_ROOT_DONE"
1644
1469
  git init -q
1645
1470
 
1646
1471
  DONE_SEED_SESSION="$TMPDIR/handoff-done-seed-session.jsonl"
1647
- DONE_SEED_DISCUSSION=$'Mission: Seed a finished workflow before testing fresh handoff priority.\nScope:\n- Create canonical workflow state.\nConstraints:\n- Keep the seed minimal.\nAcceptance:\n- Mark the workflow done before the next test step.'
1648
- write_session "$DONE_SEED_SESSION" "$HANDOFF_ROOT_DONE" "$DONE_SEED_DISCUSSION"
1472
+ DONE_SEED_MESSAGES="$(python3 - <<'PY'
1473
+ import json
1474
+ capsule = {
1475
+ "kind": "cook_handoff",
1476
+ "source": "primary_agent",
1477
+ "captured_at": "2026-01-01T00:00:02.000Z",
1478
+ "source_turn_id": "m0002",
1479
+ "mission": "Seed a finished workflow before testing fresh handoff priority.",
1480
+ "scope": ["Create canonical workflow state."],
1481
+ "constraints": ["Keep the seed minimal."],
1482
+ "acceptance": ["Add regression coverage for marking the seeded workflow done before the next step."],
1483
+ "risks": [],
1484
+ "notes": [],
1485
+ "handoff_kind": "implementation_workflow_handoff",
1486
+ "first_slice_goal": "Bootstrap the done-workflow seed fixture from an explicit handoff.",
1487
+ "first_slice_non_goals": [],
1488
+ "implementation_surfaces": ["scripts/context-proposal-test.sh"],
1489
+ "verification_commands": ["npm run context-proposal-test"],
1490
+ "why_this_slice_first": "The done-workflow handoff test needs canonical state before it can be marked done."
1491
+ }
1492
+ messages = [
1493
+ {"role": "user", "content": "Prepare the done-workflow seed fixture and tell me when it is ready for /cook."},
1494
+ {"role": "assistant", "content": "The done-workflow seed fixture is ready for /cook.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
1495
+ ]
1496
+ print(json.dumps(messages, ensure_ascii=False))
1497
+ PY
1498
+ )"
1499
+ write_session_messages "$DONE_SEED_SESSION" "$HANDOFF_ROOT_DONE" "$DONE_SEED_MESSAGES"
1649
1500
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
1650
- PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
1651
1501
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
1652
1502
  pi --session "$DONE_SEED_SESSION" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-done-seed.out" 2>"$TMPDIR/pi-completion-handoff-done-seed.err"
1653
1503
  mark_done
@@ -1726,7 +1576,7 @@ assert 'First slice goal: Patch the callback edge case and cover it with a focus
1726
1576
  assert 'Verification commands: npm test -- redirect-edge.spec.ts' in state['advisory_startup_brief']['notes'], 'done-workflow handoff should preserve verification_commands in advisory notes'
1727
1577
  PY
1728
1578
 
1729
- # Stale handoff: later discussion should invalidate the older handoff capsule and fall back to the newer discussion mission.
1579
+ # Stale handoff: an aged-out capsule should no longer block /cook from synthesizing a fresh startup brief from later discussion.
1730
1580
  HANDOFF_ROOT_STALE="$TMPDIR/handoff-root-stale"
1731
1581
  mkdir -p "$HANDOFF_ROOT_STALE"
1732
1582
  cd "$HANDOFF_ROOT_STALE"
@@ -1739,7 +1589,7 @@ import json
1739
1589
  capsule = {
1740
1590
  "kind": "cook_handoff",
1741
1591
  "source": "primary_agent",
1742
- "captured_at": "2026-01-01T00:00:02.000Z",
1592
+ "captured_at": "2025-12-31T22:00:02.000Z",
1743
1593
  "source_turn_id": "m0002",
1744
1594
  "mission": "Fix the original login redirect callback behavior.",
1745
1595
  "scope": ["Update the original callback redirect logic."],
@@ -1752,7 +1602,7 @@ capsule = {
1752
1602
  "first_slice_non_goals": ["Do not refactor the auth stack."],
1753
1603
  "implementation_surfaces": ["src/auth/login-redirect.ts"],
1754
1604
  "verification_commands": ["npm test -- login-redirect.spec.ts"],
1755
- "why_this_slice_first": "The original callback follow-up was the first bounded implementation slice before later discussion replaced it."
1605
+ "why_this_slice_first": "The original callback follow-up was the first bounded implementation slice before it aged out."
1756
1606
  }
1757
1607
  newer_discussion = "Mission: Ship logout redirect consistency instead.\nScope:\n- Update the logout redirect path.\nConstraints:\n- Leave the login callback flow unchanged.\nAcceptance:\n- Add a logout redirect regression test."
1758
1608
  messages = [
@@ -1779,10 +1629,10 @@ from pathlib import Path
1779
1629
  snapshot = json.loads(Path(sys.argv[1]).read_text())
1780
1630
  state = json.loads(Path('.agent/state.json').read_text())
1781
1631
 
1782
- assert snapshot['source'] == 'session', 'stale handoff should fall back to the newer user discussion proposal'
1783
- assert snapshot['mission'] == 'Ship logout redirect consistency instead.', 'stale handoff fallback should prefer the newer discussion mission'
1784
- assert state['mission_anchor'] == 'Ship logout redirect consistency instead.', 'stale handoff fallback should not keep the older handoff mission'
1785
- assert state['advisory_startup_brief']['source'] == 'recent_discussion', 'stale handoff fallback should preserve that the accepted startup came from discussion'
1632
+ assert snapshot['mission'] == 'Ship logout redirect consistency instead.', 'aged-out handoff should fall back to the newer recent-discussion mission'
1633
+ assert snapshot['source'] == 'session', 'aged-out handoff fallback should preserve the structured-session proposal source'
1634
+ assert state['mission_anchor'] == 'Ship logout redirect consistency instead.', 'aged-out handoff fallback should promote the newer recent-discussion mission after Start'
1635
+ assert state['advisory_startup_brief']['source'] == 'recent_discussion', 'aged-out handoff fallback should keep the advisory startup source non-canonical'
1786
1636
  PY
1787
1637
 
1788
1638
  # Negative handoff rationale: a non-startable capsule must not become the startup mission.