@linimin/pi-letscook 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -101,12 +101,30 @@ mkdir -p "$ROOT"
101
101
  cd "$ROOT"
102
102
  git init -q
103
103
 
104
- # No workflow yet: /cook with no goal should infer from recent discussion.
104
+ # No workflow yet: /cook with no goal should not bootstrap from discussion alone when analyst output is unavailable.
105
+ SESSION_ZERO="$TMPDIR/session-zero.jsonl"
106
+ 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.'
107
+ write_session "$SESSION_ZERO" "$ROOT" "$DISCUSSION_ZERO"
108
+
109
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
110
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
111
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
112
+ pi --session "$SESSION_ZERO" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-no-analyst.out 2>/tmp/pi-completion-context-proposal-no-analyst.err
113
+
114
+ python3 - <<'PY'
115
+ from pathlib import Path
116
+
117
+ assert not Path('.agent').exists(), '/cook should not bootstrap canonical state from discussion alone without analyst output'
118
+ PY
119
+
120
+ # No workflow yet: /cook with no goal should infer from recent discussion through analyst output.
105
121
  SESSION_ONE="$TMPDIR/session-one.jsonl"
106
- DISCUSSION_ONE=$'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.'
122
+ DISCUSSION_ONE="$DISCUSSION_ZERO"
123
+ 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."],"confidence":0.94}'
107
124
  write_session "$SESSION_ONE" "$ROOT" "$DISCUSSION_ONE"
108
125
 
109
126
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
127
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_ONE" \
110
128
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
111
129
  pi --session "$SESSION_ONE" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-bootstrap.out 2>/tmp/pi-completion-context-proposal-bootstrap.err
112
130
 
@@ -120,22 +138,24 @@ state = json.loads(Path('.agent/state.json').read_text())
120
138
  plan = json.loads(Path('.agent/plan.json').read_text())
121
139
  active = json.loads(Path('.agent/active-slice.json').read_text())
122
140
 
123
- assert mission in mission_text, '.agent/mission.md did not record the context-derived mission anchor'
124
- assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after context-derived bootstrap'
125
- assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after context-derived bootstrap'
126
- assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after context-derived bootstrap'
127
- assert state['current_phase'] == 'reground', 'state.json current_phase should start at reground after context-derived bootstrap'
128
- assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should start at completion-regrounder after context-derived bootstrap'
141
+ assert mission in mission_text, '.agent/mission.md did not record the analyst-derived mission anchor'
142
+ assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after analyst-derived bootstrap'
143
+ assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after analyst-derived bootstrap'
144
+ assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after analyst-derived bootstrap'
145
+ assert state['current_phase'] == 'reground', 'state.json current_phase should start at reground after analyst-derived bootstrap'
146
+ assert state['next_mandatory_role'] == 'completion-regrounder', 'next_mandatory_role should start at completion-regrounder after analyst-derived bootstrap'
129
147
  PY
130
148
 
131
- # Completed workflow: /cook with no goal should infer the next round from recent discussion.
149
+ # Completed workflow: /cook with no goal should infer the next round from recent discussion through analyst output.
132
150
  mark_done
133
151
 
134
152
  SESSION_TWO="$TMPDIR/session-two.jsonl"
135
153
  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.'
154
+ ANALYST_OUTPUT_TWO='{"mission":"Ship the next workflow round for richer context-derived /cook startup.","scope":["Start a new workflow round from recent discussion after the previous one is done.","Keep using canonical .agent state after confirmation."],"constraints":["Do not resume the completed workflow when the new round is clearly different."],"acceptance":["Reset canonical state back to reground for the new mission.","Preserve the tracked completion control-plane files."],"confidence":0.93}'
136
155
  write_session "$SESSION_TWO" "$ROOT" "$DISCUSSION_TWO"
137
156
 
138
157
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
158
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_TWO" \
139
159
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
140
160
  pi --session "$SESSION_TWO" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-next-round.out 2>/tmp/pi-completion-context-proposal-next-round.err
141
161
 
@@ -163,14 +183,15 @@ assert plan['plan_basis'] == 'user_refocus', 'plan_basis should reset to user_re
163
183
  assert active['status'] == 'idle', 'active-slice should reset to idle for the next workflow round'
164
184
  PY
165
185
 
166
- # Active workflow: /cook <goal> plus refocus should use the explicit goal as the mission anchor,
167
- # while still allowing recent discussion to enrich the proposal before confirmation.
186
+ # Active workflow: /cook <goal> plus refocus should use the explicit goal as the mission anchor
187
+ # even when analyst output is unavailable, without falling back to session-derived proposal parsing.
168
188
  SESSION_THREE="$TMPDIR/session-three.jsonl"
169
189
  DISCUSSION_THREE=$'Scope:\n- Preserve the richer proposal structure from discussion.\nConstraints:\n- Keep explicit goals as the mission anchor when they conflict with earlier text.\nAcceptance:\n- Refresh canonical state from the replacement mission.'
170
190
  write_session "$SESSION_THREE" "$ROOT" "$DISCUSSION_THREE"
171
191
 
172
192
  PI_COMPLETION_EXISTING_WORKFLOW_ACTION=refocus \
173
193
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
194
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
174
195
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
175
196
  pi --session "$SESSION_THREE" -e "$PKG_ROOT" -p "/cook Explicit replacement mission for the active workflow" >/tmp/pi-completion-context-proposal-active-goal.out 2>/tmp/pi-completion-context-proposal-active-goal.err
176
197
 
@@ -192,31 +213,37 @@ assert state['current_phase'] == 'reground', 'current_phase should reset to regr
192
213
  assert state['continuation_policy'] == 'continue', 'continuation_policy should stay continue after explicit-goal replacement'
193
214
  assert state['next_mandatory_role'] == 'completion-regrounder', 'next role should reset to completion-regrounder after explicit-goal replacement'
194
215
  assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'continuation_reason should record the explicit-goal replacement'
216
+ assert 'Preserve the richer proposal structure from discussion.' not in state['continuation_reason'], 'session scope should not be merged when analyst output is unavailable'
217
+ assert 'Keep explicit goals as the mission anchor when they conflict with earlier text.' not in state['continuation_reason'], 'session constraints should not be merged when analyst output is unavailable'
218
+ assert 'Refresh canonical state from the replacement mission.' not in state['continuation_reason'], 'session acceptance should not be merged when analyst output is unavailable'
195
219
  assert plan['plan_basis'] == 'user_refocus', 'plan_basis should be user_refocus after explicit-goal replacement'
196
220
  assert active['status'] == 'idle', 'active slice should reset to idle after explicit-goal replacement'
197
221
  PY
198
222
 
199
223
  # Completed workflow again: /cook <goal> should start the next round directly from the explicit goal
200
- # without requiring existing-workflow continue/refocus confirmation.
224
+ # even when analyst output is unavailable, without merging session-derived scope, constraints, or acceptance.
201
225
  mark_done
202
226
 
203
227
  SESSION_FOUR="$TMPDIR/session-four.jsonl"
204
- DISCUSSION_FOUR=$'Mission: This older discussion should not override the explicit next-round goal.\nScope:\n- Reuse discussion details only as supplemental proposal context.\nAcceptance:\n- Start the next round from the explicit goal.'
228
+ DISCUSSION_FOUR=$'Scope:\n- Add session-only scope.\n- Restyle widget.\nConstraints:\n- Keep rules.\nAcceptance:\n- Add test.'
229
+ EXPLICIT_GOAL_FOUR=$'Mission: Filter scope by mission.\nScope:\n- Keep explicit scope.'
205
230
  write_session "$SESSION_FOUR" "$ROOT" "$DISCUSSION_FOUR"
206
231
 
207
232
  PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
233
+ PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
208
234
  PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
209
- pi --session "$SESSION_FOUR" -e "$PKG_ROOT" -p "/cook Explicit goal for the next completed-workflow round" >/tmp/pi-completion-context-proposal-done-goal.out 2>/tmp/pi-completion-context-proposal-done-goal.err
235
+ pi --session "$SESSION_FOUR" -e "$PKG_ROOT" -p "/cook $EXPLICIT_GOAL_FOUR" >/tmp/pi-completion-context-proposal-done-goal.out 2>/tmp/pi-completion-context-proposal-done-goal.err
210
236
 
211
237
  python3 - <<'PY'
212
238
  import json
213
239
  from pathlib import Path
214
240
 
215
- mission = 'Explicit goal for the next completed-workflow round.'
241
+ mission = 'Filter scope by mission.'
216
242
  mission_text = Path('.agent/mission.md').read_text()
217
243
  state = json.loads(Path('.agent/state.json').read_text())
218
244
  plan = json.loads(Path('.agent/plan.json').read_text())
219
245
  active = json.loads(Path('.agent/active-slice.json').read_text())
246
+ continuation_reason = state['continuation_reason']
220
247
 
221
248
  assert mission in mission_text, '.agent/mission.md did not update to the explicit next-round mission anchor'
222
249
  assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after explicit-goal next-round start'
@@ -227,9 +254,146 @@ assert state['continuation_policy'] == 'continue', 'continuation_policy should r
227
254
  assert state['project_done'] is False, 'project_done should reset to false after explicit-goal next-round start'
228
255
  assert state['requires_reground'] is True, 'requires_reground should reset to true after explicit-goal next-round start'
229
256
  assert state['next_mandatory_role'] == 'completion-regrounder', 'next role should reset to completion-regrounder after explicit-goal next-round start'
230
- assert state['continuation_reason'].startswith('User refocused workflow via /cook:'), 'continuation_reason should record the explicit-goal next-round start'
257
+ assert continuation_reason.startswith('User refocused workflow via /cook:'), 'continuation_reason should record the explicit-goal next-round start'
258
+ assert 'Keep explicit scope.' in continuation_reason, 'explicit scope should remain in the explicit-goal proposal'
259
+ assert 'Add session-only scope.' not in continuation_reason, 'session-derived scope should not be merged when analyst output is unavailable'
260
+ assert 'Restyle widget.' not in continuation_reason, 'unrelated session-derived scope should not be merged when analyst output is unavailable'
261
+ assert 'Keep rules.' not in continuation_reason, 'session-derived constraints should not merge when analyst output is unavailable'
262
+ assert 'Add test.' not in continuation_reason, 'session-derived acceptance should not merge when analyst output is unavailable'
231
263
  assert plan['plan_basis'] == 'user_refocus', 'plan_basis should be user_refocus after explicit-goal next-round start'
232
264
  assert active['status'] == 'idle', 'active slice should reset to idle after explicit-goal next-round start'
233
265
  PY
234
266
 
267
+ # Completed workflow again: /cook with no goal should be able to use model-assisted
268
+ # analysis of natural discussion when discussion-only startup depends on analyst output.
269
+ mark_done
270
+
271
+ SESSION_FIVE="$TMPDIR/session-five.jsonl"
272
+ DISCUSSION_FIVE=$'I do not want to rewrite the parser. The safer path is to let /cook analyze the discussion first, keep the user\'s explicit mission if they provided one, and ignore stale scope that drifted in from earlier turns. We should still prove it with a regression test before writing canonical state.'
273
+ ANALYST_OUTPUT_FIVE='{"mission":"Use a proposal analyst to summarize natural discussion before /cook writes canonical state.","scope":["Keep explicit goals anchored.","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"]}'
274
+ write_session "$SESSION_FIVE" "$ROOT" "$DISCUSSION_FIVE"
275
+
276
+ PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
277
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$ANALYST_OUTPUT_FIVE" \
278
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
279
+ pi --session "$SESSION_FIVE" -e "$PKG_ROOT" -p "/cook" >/tmp/pi-completion-context-proposal-analyst.out 2>/tmp/pi-completion-context-proposal-analyst.err
280
+
281
+ python3 - <<'PY'
282
+ import json
283
+ from pathlib import Path
284
+
285
+ mission = 'Use a proposal analyst to summarize natural discussion before /cook writes canonical state.'
286
+ mission_text = Path('.agent/mission.md').read_text()
287
+ state = json.loads(Path('.agent/state.json').read_text())
288
+ plan = json.loads(Path('.agent/plan.json').read_text())
289
+ active = json.loads(Path('.agent/active-slice.json').read_text())
290
+ continuation_reason = state['continuation_reason']
291
+
292
+ assert mission in mission_text, '.agent/mission.md did not record the analyst-derived mission anchor'
293
+ assert state['mission_anchor'] == mission, 'state.json mission_anchor mismatch after analyst-derived bootstrap'
294
+ assert plan['mission_anchor'] == mission, 'plan.json mission_anchor mismatch after analyst-derived bootstrap'
295
+ assert active['mission_anchor'] == mission, 'active-slice.json mission_anchor mismatch after analyst-derived bootstrap'
296
+ assert state['current_phase'] == 'reground', 'current_phase should reset to reground after analyst-derived bootstrap'
297
+ assert state['next_mandatory_role'] == 'completion-regrounder', 'next role should reset to completion-regrounder after analyst-derived bootstrap'
298
+ assert continuation_reason.startswith('User refocused workflow via /cook:'), 'continuation_reason should record the analyst-derived restart'
299
+ assert 'Keep explicit goals anchored.' in continuation_reason, 'analyst-derived scope should be preserved'
300
+ PY
301
+
302
+ # Custom confirmation UI: start should render proposal content separately from explicit Start/Edit/Cancel actions.
303
+ UI_ROOT_START="$TMPDIR/ui-root-start"
304
+ mkdir -p "$UI_ROOT_START"
305
+ cd "$UI_ROOT_START"
306
+ git init -q
307
+
308
+ UI_SESSION_START="$TMPDIR/ui-session-start.jsonl"
309
+ UI_DISCUSSION_START=$'Mission: Replace the crowded selector with a clearer action layout.\nScope:\n- Separate proposal text from actions.\nConstraints:\n- Preserve Start/Edit/Cancel behavior.\nAcceptance:\n- Add regression coverage.'
310
+ UI_ANALYST_OUTPUT_START='{"mission":"Replace the crowded selector with a clearer action layout.","scope":["Separate proposal text from actions."],"constraints":["Preserve Start/Edit/Cancel behavior."],"acceptance":["Add regression coverage."],"confidence":0.95}'
311
+ UI_SNAPSHOT_START="$TMPDIR/context-proposal-ui-start.json"
312
+ write_session "$UI_SESSION_START" "$UI_ROOT_START" "$UI_DISCUSSION_START"
313
+
314
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=start \
315
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH="$UI_SNAPSHOT_START" \
316
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$UI_ANALYST_OUTPUT_START" \
317
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
318
+ 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"
319
+
320
+ python3 - "$UI_SNAPSHOT_START" <<'PY'
321
+ import json
322
+ import sys
323
+ from pathlib import Path
324
+
325
+ snapshot = json.loads(Path(sys.argv[1]).read_text())
326
+ state = json.loads(Path('.agent/state.json').read_text())
327
+
328
+ assert snapshot['proposalHeading'] == 'Proposed workflow', 'custom confirmation snapshot should expose a dedicated proposal section'
329
+ assert 'Mission\nReplace the crowded selector with a clearer action layout.' in snapshot['proposalBody'], 'proposal body should be captured separately from the action list'
330
+ assert [action['id'] for action in snapshot['actions']] == ['start', 'edit', 'cancel'], 'custom confirmation actions should stay Start/Edit/Cancel'
331
+ assert [action['label'] for action in snapshot['actions']] == ['Start', 'Edit', 'Cancel'], 'custom confirmation action labels should be concise'
332
+ for action in snapshot['actions']:
333
+ assert 'Replace the crowded selector with a clearer action layout.' not in action['label'], 'proposal mission should not be embedded in action labels'
334
+ assert 'Separate proposal text from actions.' not in action['description'], 'proposal scope should not be embedded in action descriptions'
335
+ assert state['mission_anchor'] == 'Replace the crowded selector with a clearer action layout.', 'start action should still accept the proposed mission'
336
+ PY
337
+
338
+ # Custom confirmation UI: edit should keep the existing editor/parsing flow when the action comes from the new layout.
339
+ UI_ROOT_EDIT="$TMPDIR/ui-root-edit"
340
+ mkdir -p "$UI_ROOT_EDIT"
341
+ cd "$UI_ROOT_EDIT"
342
+ git init -q
343
+
344
+ UI_SESSION_EDIT="$TMPDIR/ui-session-edit.jsonl"
345
+ UI_DISCUSSION_EDIT=$'Mission: Keep editing support in the custom confirmation UI.\nScope:\n- Preserve the proposal editor.\nConstraints:\n- Keep parsing structured proposal text.\nAcceptance:\n- Update the mission anchor after edit.'
346
+ UI_ANALYST_OUTPUT_EDIT='{"mission":"Keep editing support in the custom confirmation UI.","scope":["Preserve the proposal editor."],"constraints":["Keep parsing structured proposal text."],"acceptance":["Update the mission anchor after edit."],"confidence":0.94}'
347
+ UI_EDIT_TEXT=$'Mission: Edited mission from the custom confirmation UI.\nScope:\n- Preserve parsing after edit.\nConstraints:\n- Keep the shared confirmation flow.\nAcceptance:\n- Start the workflow from the edited proposal.'
348
+ write_session "$UI_SESSION_EDIT" "$UI_ROOT_EDIT" "$UI_DISCUSSION_EDIT"
349
+
350
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=edit \
351
+ PI_COMPLETION_CONTEXT_PROPOSAL_EDIT_TEXT="$UI_EDIT_TEXT" \
352
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$UI_ANALYST_OUTPUT_EDIT" \
353
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
354
+ pi --session "$UI_SESSION_EDIT" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-ui-edit.out" 2>"$TMPDIR/pi-completion-context-proposal-ui-edit.err"
355
+
356
+ python3 - <<'PY'
357
+ import json
358
+ from pathlib import Path
359
+
360
+ state = json.loads(Path('.agent/state.json').read_text())
361
+ plan = json.loads(Path('.agent/plan.json').read_text())
362
+ active = json.loads(Path('.agent/active-slice.json').read_text())
363
+ mission = 'Edited mission from the custom confirmation UI.'
364
+
365
+ assert state['mission_anchor'] == mission, 'edit action should still route through the proposal parser and update state.json'
366
+ assert plan['mission_anchor'] == mission, 'edit action should still route through the proposal parser and update plan.json'
367
+ assert active['mission_anchor'] == mission, 'edit action should still route through the proposal parser and update active-slice.json'
368
+ assert state['current_phase'] == 'reground', 'edit action should still bootstrap/reground the workflow'
369
+ PY
370
+
371
+ # Custom confirmation UI: cancel should exit without writing canonical state.
372
+ UI_ROOT_CANCEL="$TMPDIR/ui-root-cancel"
373
+ mkdir -p "$UI_ROOT_CANCEL"
374
+ cd "$UI_ROOT_CANCEL"
375
+ git init -q
376
+
377
+ UI_SESSION_CANCEL="$TMPDIR/ui-session-cancel.jsonl"
378
+ UI_DISCUSSION_CANCEL=$'Mission: Cancel from the custom confirmation UI without writing state.\nScope:\n- Show the proposal separately from the actions.\nConstraints:\n- Keep cancellation side-effect free.\nAcceptance:\n- Leave .agent absent after cancel.'
379
+ UI_ANALYST_OUTPUT_CANCEL='{"mission":"Cancel from the custom confirmation UI without writing state.","scope":["Show the proposal separately from the actions."],"constraints":["Keep cancellation side-effect free."],"acceptance":["Leave .agent absent after cancel."],"confidence":0.92}'
380
+ UI_SNAPSHOT_CANCEL="$TMPDIR/context-proposal-ui-cancel.json"
381
+ write_session "$UI_SESSION_CANCEL" "$UI_ROOT_CANCEL" "$UI_DISCUSSION_CANCEL"
382
+
383
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_ACTION=cancel \
384
+ PI_COMPLETION_TEST_CONTEXT_PROPOSAL_UI_PATH="$UI_SNAPSHOT_CANCEL" \
385
+ PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT="$UI_ANALYST_OUTPUT_CANCEL" \
386
+ PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
387
+ 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"
388
+
389
+ python3 - "$UI_SNAPSHOT_CANCEL" <<'PY'
390
+ import json
391
+ import sys
392
+ from pathlib import Path
393
+
394
+ snapshot = json.loads(Path(sys.argv[1]).read_text())
395
+ assert [action['id'] for action in snapshot['actions']] == ['start', 'edit', 'cancel'], 'cancel snapshot should still expose Start/Edit/Cancel actions'
396
+ assert not Path('.agent').exists(), 'cancel action should not write canonical workflow state'
397
+ PY
398
+
235
399
  echo "context proposal test passed: $ROOT"