@linimin/pi-letscook 0.1.57 → 0.1.59

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.
@@ -0,0 +1,28 @@
1
+ # Completion Control Plane
2
+
3
+ This repository uses the `completion` workflow for long-running coding tasks.
4
+
5
+ ## Canonical tracked contract files
6
+
7
+ - `.agent/README.md`
8
+ - `.agent/mission.md`
9
+ - `.agent/profile.json`
10
+ - `.agent/verify_completion_stop.sh`
11
+ - `.agent/verify_completion_control_plane.sh`
12
+
13
+ ## Ignored canonical execution state
14
+
15
+ - `.agent/state.json`
16
+ - `.agent/plan.json`
17
+ - `.agent/active-slice.json`
18
+ - `.agent/slice-history.jsonl`
19
+ - `.agent/stop-check-history.jsonl`
20
+ - `.agent/verification-evidence.json`
21
+ - `.agent/*.log`
22
+ - `.agent/tmp/`
23
+
24
+ `.agent/verification-evidence.json` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.
25
+
26
+ The source of truth for long-running completion work is canonical `.agent/**` state plus current repo truth.
27
+
28
+ Project: pi-letscook
@@ -0,0 +1,8 @@
1
+ # Mission
2
+
3
+ Project: pi-letscook
4
+
5
+ Mission anchor:
6
+ Implement the primary-agent → /cook handoff pipeline refactor so /cook only starts implementation workflow from structurally startable handoffs, while preserving explicit handoff as the preferred startup-intake path.
7
+
8
+ This file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema_version": 1,
3
+ "protocol_id": "completion",
4
+ "project_name": "pi-letscook",
5
+ "required_stop_judges": 3,
6
+ "priority_policy_id": "completion-default",
7
+ "task_type": "completion-workflow",
8
+ "evaluation_profile": "completion-rubric-v1",
9
+ "docs_surfaces": [
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ]
13
+ }
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env bash
2
+ ':' //; exec node "$0" "$@"
3
+ const fs = require('node:fs');
4
+ const { spawnSync } = require('node:child_process');
5
+
6
+ const REQUIRED_TRACKED_CONTRACT_FILES = [
7
+ '.agent/README.md',
8
+ '.agent/mission.md',
9
+ '.agent/profile.json',
10
+ '.agent/verify_completion_stop.sh',
11
+ '.agent/verify_completion_control_plane.sh',
12
+ ];
13
+
14
+ function fail(message) {
15
+ console.error(message);
16
+ process.exit(1);
17
+ }
18
+
19
+ function readJson(file) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
22
+ } catch (error) {
23
+ fail('Failed to read ' + file + ': ' + error.message);
24
+ }
25
+ }
26
+
27
+ function asString(value) {
28
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
29
+ }
30
+
31
+ function asNumber(value) {
32
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
33
+ }
34
+
35
+ function asStringArray(value) {
36
+ return Array.isArray(value)
37
+ ? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
38
+ : [];
39
+ }
40
+
41
+ function sameStringArrays(left, right) {
42
+ return left.length === right.length && left.every((item, index) => item === right[index]);
43
+ }
44
+
45
+ function runGit(args, options = {}) {
46
+ const result = spawnSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
47
+ if (!options.allowFailure && result.status !== 0) {
48
+ const stderr = asString(result.stderr) ?? 'git command failed';
49
+ fail(`git ${args.join(' ')} failed: ${stderr}`);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function gitHeadSha() {
55
+ const result = runGit(['rev-parse', 'HEAD'], { allowFailure: true });
56
+ return result.status === 0 ? asString(result.stdout) : undefined;
57
+ }
58
+
59
+ function ensureTrackedContractFiles() {
60
+ for (const file of REQUIRED_TRACKED_CONTRACT_FILES) {
61
+ const result = runGit(['ls-files', '--error-unmatch', file], { allowFailure: true });
62
+ if (result.status !== 0) {
63
+ fail(`Required tracked completion contract file is missing from git index: ${file}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ function ensureCommitExists(commitish, label) {
69
+ const result = runGit(['rev-parse', '--verify', `${commitish}^{commit}`], { allowFailure: true });
70
+ if (result.status !== 0) {
71
+ fail(`${label} must resolve to an existing commit: ${commitish}`);
72
+ }
73
+ }
74
+
75
+ function trackedDiffFiles(fromCommit, toCommit) {
76
+ const result = runGit(['diff', '--name-only', '--diff-filter=ACMR', `${fromCommit}..${toCommit}`]);
77
+ return result.stdout
78
+ .split(/\r?\n/)
79
+ .map((line) => line.trim())
80
+ .filter(Boolean);
81
+ }
82
+
83
+ const profile = readJson('.agent/profile.json');
84
+ const state = readJson('.agent/state.json');
85
+ const plan = readJson('.agent/plan.json');
86
+ const active = readJson('.agent/active-slice.json');
87
+ const evidence = readJson('.agent/verification-evidence.json');
88
+
89
+ ensureTrackedContractFiles();
90
+
91
+ for (const [file, record] of [
92
+ ['.agent/profile.json', profile],
93
+ ['.agent/state.json', state],
94
+ ['.agent/plan.json', plan],
95
+ ['.agent/active-slice.json', active],
96
+ ]) {
97
+ if (!asString(record.task_type)) fail(file + ' is missing task_type');
98
+ if (!asString(record.evaluation_profile)) fail(file + ' is missing evaluation_profile');
99
+ }
100
+
101
+ const taskType = asString(profile.task_type);
102
+ const evaluationProfile = asString(profile.evaluation_profile);
103
+ if (asString(state.task_type) !== taskType) fail('.agent/state.json task_type must match .agent/profile.json task_type');
104
+ if (asString(plan.task_type) !== taskType) fail('.agent/plan.json task_type must match .agent/profile.json task_type');
105
+ if (asString(active.task_type) !== taskType) fail('.agent/active-slice.json task_type must match .agent/profile.json task_type');
106
+ if (asString(state.evaluation_profile) !== evaluationProfile) fail('.agent/state.json evaluation_profile must match .agent/profile.json evaluation_profile');
107
+ if (asString(plan.evaluation_profile) !== evaluationProfile) fail('.agent/plan.json evaluation_profile must match .agent/profile.json evaluation_profile');
108
+ if (asString(active.evaluation_profile) !== evaluationProfile) fail('.agent/active-slice.json evaluation_profile must match .agent/profile.json evaluation_profile');
109
+
110
+ if (asString(evidence.artifact_type) !== 'completion-verification-evidence') {
111
+ fail('.agent/verification-evidence.json artifact_type must be completion-verification-evidence');
112
+ }
113
+
114
+ const exactStatuses = new Set(['selected', 'in_progress', 'committed', 'done']);
115
+ const activeStatus = asString(active.status);
116
+ const exactHandoff = exactStatuses.has(activeStatus || '');
117
+ const planSlices = Array.isArray(plan.candidate_slices) ? plan.candidate_slices : [];
118
+ const activeSliceId = asString(active.slice_id);
119
+ const planSlice = activeSliceId ? planSlices.find((slice) => asString(slice && slice.slice_id) === activeSliceId) : undefined;
120
+
121
+ if (exactHandoff && !planSlice) {
122
+ fail('slice_id must match a slice in .agent/plan.json when status carries an exact handoff');
123
+ }
124
+
125
+ if (exactHandoff) {
126
+ const requiredStringFields = ['goal', 'why_now', 'basis_commit'];
127
+ for (const field of requiredStringFields) {
128
+ if (!asString(active[field])) fail('.agent/active-slice.json is missing ' + field + ' when status carries an exact handoff');
129
+ }
130
+ const requiredArrayFields = ['contract_ids', 'acceptance_criteria', 'blocked_on', 'locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'remaining_contract_ids_before'];
131
+ for (const field of requiredArrayFields) {
132
+ if (!Array.isArray(active[field])) fail('.agent/active-slice.json is missing ' + field + ' when status carries an exact handoff');
133
+ }
134
+ const requiredNumberFields = ['priority', 'release_blocker_count_before', 'high_value_gap_count_before'];
135
+ for (const field of requiredNumberFields) {
136
+ if (asNumber(active[field]) === undefined) fail('.agent/active-slice.json is missing ' + field + ' when status carries an exact handoff');
137
+ }
138
+
139
+ const mismatchFields = [];
140
+ if (asString(planSlice.slice_id) !== activeSliceId) mismatchFields.push('slice_id');
141
+ if (asString(planSlice.goal) !== asString(active.goal)) mismatchFields.push('goal');
142
+ if (!sameStringArrays(asStringArray(planSlice.contract_ids), asStringArray(active.contract_ids))) mismatchFields.push('contract_ids');
143
+ if (!sameStringArrays(asStringArray(planSlice.acceptance_criteria), asStringArray(active.acceptance_criteria))) mismatchFields.push('acceptance_criteria');
144
+ if (!sameStringArrays(asStringArray(planSlice.blocked_on), asStringArray(active.blocked_on))) mismatchFields.push('blocked_on');
145
+ if (asNumber(planSlice.priority) !== asNumber(active.priority)) mismatchFields.push('priority');
146
+ if (asString(planSlice.why_now) !== asString(active.why_now)) mismatchFields.push('why_now');
147
+ const planMirrorFields = ['locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
148
+ for (const field of planMirrorFields) {
149
+ const planValue = planSlice[field];
150
+ const activeValue = active[field];
151
+ if (Array.isArray(planValue) || Array.isArray(activeValue)) {
152
+ if (!sameStringArrays(asStringArray(planValue), asStringArray(activeValue))) mismatchFields.push(field);
153
+ continue;
154
+ }
155
+ if (typeof planValue === 'number' || typeof activeValue === 'number') {
156
+ if (asNumber(planValue) !== asNumber(activeValue)) mismatchFields.push(field);
157
+ continue;
158
+ }
159
+ if (asString(planValue) !== asString(activeValue)) mismatchFields.push(field);
160
+ }
161
+ if (mismatchFields.length > 0) {
162
+ fail('.agent/active-slice.json must match the selected .agent/plan.json slice across: ' + mismatchFields.join(', '));
163
+ }
164
+
165
+ if (asString(evidence.subject_type) !== 'selected_slice') {
166
+ fail('subject_type must be selected_slice when active slice exact handoff requires verification evidence');
167
+ }
168
+ if (asString(evidence.slice_id) !== activeSliceId) fail('.agent/verification-evidence.json slice_id must match .agent/active-slice.json slice_id');
169
+ if (asString(evidence.goal) !== asString(active.goal)) fail('.agent/verification-evidence.json goal must match .agent/active-slice.json goal');
170
+ if (!sameStringArrays(asStringArray(evidence.contract_ids), asStringArray(active.contract_ids))) fail('.agent/verification-evidence.json contract_ids must match .agent/active-slice.json contract_ids');
171
+ if (asString(evidence.basis_commit) !== asString(active.basis_commit)) fail('.agent/verification-evidence.json basis_commit must match .agent/active-slice.json basis_commit');
172
+ if (!sameStringArrays(asStringArray(evidence.verification_commands), asStringArray(active.verification_commands))) {
173
+ fail('.agent/verification-evidence.json verification_commands must match .agent/active-slice.json verification_commands');
174
+ }
175
+ if (!asString(evidence.recorded_at)) fail('.agent/verification-evidence.json recorded_at must be present for selected-slice evidence');
176
+ if (asString(evidence.outcome) === 'not_recorded') fail('.agent/verification-evidence.json outcome must not be not_recorded for selected-slice evidence');
177
+ const headSha = gitHeadSha();
178
+ if (headSha && asString(evidence.head_sha) !== headSha) {
179
+ fail('.agent/verification-evidence.json head_sha must match current HEAD');
180
+ }
181
+
182
+ const basisCommit = asString(active.basis_commit);
183
+ if (basisCommit && headSha) {
184
+ ensureCommitExists(basisCommit, '.agent/active-slice.json basis_commit');
185
+ const ancestorCheck = runGit(['merge-base', '--is-ancestor', basisCommit, headSha], { allowFailure: true });
186
+ if (ancestorCheck.status !== 0) {
187
+ fail(`.agent/active-slice.json basis_commit must be an ancestor of current HEAD: ${basisCommit} -> ${headSha}`);
188
+ }
189
+ const changedFiles = trackedDiffFiles(basisCommit, headSha);
190
+ const implementationSurfaces = new Set(asStringArray(active.implementation_surfaces));
191
+ const missingSurfaces = changedFiles.filter((file) => !implementationSurfaces.has(file));
192
+ if (missingSurfaces.length > 0) {
193
+ fail('.agent/active-slice.json implementation_surfaces must cover every tracked file changed from basis_commit to current HEAD; missing: ' + missingSurfaces.join(', '));
194
+ }
195
+ }
196
+ } else {
197
+ const subjectType = asString(evidence.subject_type);
198
+ if (subjectType === 'none') {
199
+ if (asString(evidence.outcome) && asString(evidence.outcome) !== 'not_recorded') {
200
+ fail('.agent/verification-evidence.json outcome must stay not_recorded when subject_type=none');
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ echo "[completion-stop] verifying control plane and .agent/verification-evidence.json parity"
8
+ bash .agent/verify_completion_control_plane.sh
9
+
10
+ if [[ "${PI_COMPLETION_RUNNING_RELEASE_CHECK:-0}" == "1" ]]; then
11
+ echo "[completion-stop] release-check is already in progress; skipping nested npm run release-check >/dev/null recursion"
12
+ npm run evaluator-calibration-test >/dev/null
13
+ echo "completion stop verification passed"
14
+ exit 0
15
+ fi
16
+
17
+ echo "[completion-stop] delegating to npm run release-check >/dev/null for broad packaged verification, evaluator calibration, and contract coverage"
18
+ PI_COMPLETION_RUNNING_RELEASE_CHECK=1 npm run release-check >/dev/null
19
+
20
+ echo "completion stop verification passed"
package/CHANGELOG.md CHANGED
@@ -2,12 +2,32 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.59
6
+
7
+ ### Changed
8
+
9
+ - relaxed the pre-`/cook` ordinary-chat boundary so the primary agent can keep discussing and refining requirements before explicit `/cook` instead of switching into a hard handoff-only refusal mode as soon as workflow-worthiness is detected
10
+ - kept `/cook` as the only explicit workflow boundary, while teaching the pre-`/cook` prompt surfaces to recommend `/cook` advisory-first and only emit implementation-ready capsules once the first bounded slice is concrete enough
11
+ - made fresh explicit `/cook` handoffs reusable from recent ordinary-chat discussion instead of requiring the immediately preceding turn, so Cancel can return users cleanly to ordinary discussion before they rerun `/cook`
12
+ - kept bare `/cook` startup and done-workflow next-round entry fail-closed on missing or non-startable explicit handoffs, while active workflows still resume from canonical `.agent/**` state unless a fresh explicit handoff proposes replacement
13
+ - updated public parity and shipped package contents so the tracked `.agent` contract files are included in package tarballs and packaged smoke/release verification can scaffold canonical state truthfully
14
+
15
+ ## 0.1.58
16
+
17
+ ### Changed
18
+
19
+ - tightened implementation-ready explicit `/cook` handoffs so fresh capsules must already carry a bounded first slice, repo-change-oriented acceptance, implementation surfaces, verification commands, and why-that-slice-first structure before workflow startup
20
+ - made fresh explicit but non-startable `/cook` handoffs fail closed with a dedicated operator message instead of falling back to broader recent discussion or silently drifting into planning
21
+ - expanded regressions and public parity so valid, vague, stale, done-workflow, and negative explicit-handoff cases stay truthful across runtime behavior, docs, and `npm run release-check`
22
+
5
23
  ## 0.1.57
6
24
 
7
25
  ### Changed
8
26
 
9
- - made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh and valid
10
- - kept context-derived startup as a fallback only, so stale, drifted, or non-startable handoff capsules still fail closed or fall back to recent discussion instead of silently rewriting canonical state
27
+ - made explicit primary-agent `/cook` handoff the preferred startup-intake path by teaching ordinary-chat handoff turns to emit a structured `cook_handoff` capsule and letting `/cook` prefer that capsule over broad context re-inference when it is fresh, valid, and implementation-startable
28
+ - tightened implementation-ready explicit handoffs so the structured capsule must already carry a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first` before `/cook` will start workflow from it
29
+ - kept the pre-`/cook` handoff capsule as advisory startup intake only, not canonical `.agent/**` workflow state, while still using context-derived startup as the fallback only when no fresh explicit handoff is blocking startup
30
+ - kept context-derived startup as a fallback only when there is no fresh explicit handoff blocking startup, so stale or invalidated capsules can still fall back to recent discussion while fresh non-startable handoffs fail closed instead of silently rewriting canonical state
11
31
  - made finished-workflow suppression stay a safety layer instead of a replacement mission when a fresh explicit `/cook` handoff exists, and blocked negative rejection/suppression text from becoming a Startable startup mission
12
32
  - removed inline `/cook` arguments from the shipped entry path again so explicit bare `/cook` is the only public command, and fail closed when recent discussion is insufficient or unreliable
13
33
  - added a pre-`/cook` ordinary-chat handoff boundary so the primary agent is instructed to stop at `/cook` once a task has matured into completion-workflow scope instead of starting long-running implementation directly in ordinary chat
package/README.md CHANGED
@@ -30,10 +30,10 @@ Then run `/reload` in Pi.
30
30
  1. Install the package:
31
31
  `pi install npm:@linimin/pi-letscook`
32
32
  2. Run `/reload` in Pi.
33
- 3. In the main chat, describe the concrete repo change you want.
33
+ 3. In the main chat, describe the concrete repo change you want and let the primary agent help refine it until the first slice is ready for `/cook`.
34
34
  4. Run `/cook`.
35
35
  5. Review the startup brief and choose **Start** or **Cancel**.
36
- 6. Later, run `/cook` again to continue, refocus, or start the next round.
36
+ 6. Later, run `/cook` again to resume from canonical state or confirm an explicit replacement or next-round handoff.
37
37
 
38
38
  ```text
39
39
  /cook
@@ -43,19 +43,22 @@ Then run `/reload` in Pi.
43
43
 
44
44
  | If you want to... | Do this |
45
45
  |---|---|
46
- | Start a long-running task | Discuss the concrete repo change in the main chat, then run `/cook` |
46
+ | Start a long-running task | Discuss the concrete repo change in the main chat, wait for a fresh primary-agent handoff, then run `/cook` |
47
47
  | Continue the current workflow | Run `/cook` |
48
- | Refocus or start the next round | Discuss the new concrete repo change in the main chat, then run `/cook` |
48
+ | Refocus or start the next round | Discuss the new concrete repo change in the main chat, wait for a fresh primary-agent handoff, then run `/cook` |
49
49
 
50
50
  ## What `/cook` expects
51
51
 
52
- - preferably a fresh explicit primary-agent `/cook` handoff capsule from the immediately preceding ordinary-chat turn
53
- - otherwise recent main-chat discussion about concrete repo changes
54
- - enough detail to derive a startup brief with mission, scope, constraints or non-goals, acceptance, and notes or risks
52
+ - a fresh valid explicit primary-agent `/cook` handoff capsule from recent ordinary-chat discussion whenever `/cook` is starting a new workflow or the next round after a completed workflow
53
+ - for that handoff capsule to start workflow immediately, it must already be implementation-startable: a bounded `first_slice_goal`, repo-change-oriented acceptance, `implementation_surfaces`, `verification_commands`, and `why_this_slice_first`
54
+ - enough detail in the main chat for the primary agent to form that bounded handoff capsule before you run `/cook`
55
55
  - README/CHANGELOG updates still count as concrete repo changes
56
56
  - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they include the explicit structured `/cook` handoff capsule
57
+ - recent main-chat discussion can still validate or supplement an accepted explicit handoff, but it no longer starts a new workflow by itself
57
58
 
58
- If no fresh valid handoff exists and recent discussion is missing, weak, ambiguous, assistant-produced, or only describes planning artifacts instead of concrete repo changes, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to clarify the mission in the main chat before rerunning `/cook`.
59
+ If no fresh valid handoff exists for new-workflow or next-round entry, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to get an explicit primary-agent handoff in the main chat before rerunning `/cook`.
60
+
61
+ If a fresh explicit handoff exists but is still workflow-worthy rather than implementation-startable, `/cook` also fails closed instead of silently treating that capsule as planning support or canonical workflow state.
59
62
 
60
63
  If you pass inline arguments to `/cook`, it also fails closed and tells you to move that intent into the main chat before rerunning bare `/cook`.
61
64
 
@@ -63,34 +66,40 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
63
66
 
64
67
  Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.
65
68
 
66
- If a task has clearly matured into completion-workflow scope, the primary agent should hand you off to `/cook` instead of starting long-running implementation directly in ordinary chat.
69
+ If a task has clearly matured into completion-workflow scope, the primary agent should recommend `/cook` instead of starting long-running implementation directly in ordinary chat.
70
+
71
+ Before you explicitly run `/cook`, the conversation can still stay in ordinary chat: the primary agent may keep answering follow-up questions and refining requirements rather than switching into a hard handoff-only refusal mode.
72
+
73
+ That handoff should include an explicit structured `/cook` capsule in the assistant reply once the first slice is implementation-ready, so `/cook` can confirm the already-formed mission instead of re-deriving it from broad ambient context.
67
74
 
68
- That handoff should include an explicit structured `/cook` capsule in the assistant reply so `/cook` can confirm the already-formed mission instead of re-deriving it from broad ambient context.
75
+ The capsule is still advisory startup intake, not canonical workflow state, and new-workflow or next-round entry only proceeds when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.
69
76
 
70
77
  Important behavior:
71
78
  - `/cook` is the canonical workflow boundary and manual entry point
72
- - startup, refocus, and next-round routing stay confirm-first; nothing silently starts a workflow
79
+ - startup and next-round entry stay confirm-first and require a fresh valid explicit primary-agent handoff
80
+ - active workflows resume from canonical `.agent/**` state unless a fresh valid explicit handoff proposes a replacement
73
81
  - explicit slash commands other than `/cook` continue normally in the main chat
74
82
  - ordinary main-chat discussion may clarify or propose, but mature long-running implementation should be handed off to `/cook`
75
83
 
76
84
  ## Typical examples
77
85
 
78
- Start a new workflow from recent discussion:
86
+ Start a new workflow after a fresh primary-agent handoff:
79
87
 
80
88
  ```text
81
89
  I want to add login redirect handling and tests.
90
+ # let the primary agent hand you off to /cook
82
91
  /cook
83
92
  ```
84
93
 
85
94
  ## What happens when you run `/cook`
86
95
 
87
- `/cook` first looks for a fresh explicit primary-agent handoff capsule. If one is valid, `/cook` builds the startup brief from that handoff and only uses recent discussion as validation or supplemental notes. If no valid handoff exists, `/cook` falls back to deriving a startup brief from recent discussion before showing the existing approval-only Start/Cancel gate.
96
+ `/cook` first looks for a fresh explicit primary-agent handoff capsule from recent ordinary-chat discussion. New-workflow entry and done-workflow next-round entry start only when that capsule is fresh, valid, and implementation-startable; otherwise `/cook` fails closed instead of deriving startup from recent discussion. When a workflow is already active and no fresh valid explicit handoff is present, `/cook` resumes from canonical `.agent/**` state instead of deriving replacement startup from recent discussion.
88
97
 
89
98
  | Repo state | What you'll see |
90
99
  |---|---|
91
- | No workflow yet | If a fresh explicit handoff capsule exists, a startup brief built from that handoff. Otherwise a startup brief built from recent main-chat discussion. You choose **Start** or **Cancel**. Weak, unreliable, stale, or planning-only intake fails closed. |
92
- | Active workflow exists | Usually a resume of the current workflow. If a fresh explicit handoff capsule or recent discussion clearly points to a different concrete repo change, `/cook` shows a chooser first and only rewrites canonical state after you confirm the new startup brief. Ambiguous intake stays conservative. |
93
- | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without that, `/cook` falls back to recent discussion. Discussion that only restates already-finished work still fails closed. |
100
+ | No workflow yet | If a fresh explicit handoff capsule exists and is implementation-startable, you get a startup brief built from that handoff and choose **Start** or **Cancel**. Otherwise `/cook` fails closed, leaves canonical state unchanged, and tells you to get a fresh explicit primary-agent handoff. Weak, unreliable, stale, planning-only, or non-startable explicit-handoff intake also fails closed. |
101
+ | Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a fresh explicit handoff capsule points to a different concrete repo change, `/cook` shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous intake stays conservative. |
102
+ | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without one, `/cook` fails closed instead of deriving the next round from recent discussion. |
94
103
 
95
104
  ## Confirmation and fail-closed behavior
96
105
 
@@ -98,9 +107,9 @@ I want to add login redirect handling and tests.
98
107
 
99
108
  - startup, next-round, and refocus proposals are approval-only
100
109
  - actions are **Start** and **Cancel**
101
- - **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
110
+ - **Cancel** is side-effect free: canonical workflow state stays unchanged, so you can discuss changes in the main chat and rerun `/cook`
102
111
  - weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
103
- - when recent discussion suggests a different workflow, `/cook` shows a chooser before any canonical state rewrite
112
+ - when a fresh explicit handoff suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
104
113
 
105
114
  When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
106
115
 
@@ -38,7 +38,7 @@ type ContextProposalAlternate = {
38
38
  analysis: ContextProposalAnalysis;
39
39
  goalText: string;
40
40
  basisPreview: string;
41
- source: "session" | "analyst";
41
+ source: "session" | "analyst" | "handoff_capsule";
42
42
  };
43
43
 
44
44
  type ContextProposal = ContextProposalAlternate & {
@@ -55,11 +55,27 @@ type ExistingWorkflowDecision =
55
55
  | { action: "continue"; currentMissionAnchor: string }
56
56
  | { action: "refocus"; currentMissionAnchor: string; missionAnchor: string; proposal: ContextProposal };
57
57
 
58
+ type CookContextProposalResult = {
59
+ proposal?: ContextProposal;
60
+ blockedFailureMessage?: string;
61
+ };
62
+
63
+ function buildCookExplicitHandoffRequiredMessage(deps: CompletionDriverDeps, prefix?: string): string {
64
+ const requirement =
65
+ "/cook failed closed because starting a new completion workflow now requires a fresh valid explicit primary-agent handoff. Ask the primary agent to emit a fresh ```cook_handoff``` capsule in the main chat, then rerun /cook.";
66
+ return prefix ? `${prefix} ${requirement}` : requirement;
67
+ }
68
+
58
69
  type ActiveWorkflowProposalAssessment = {
59
- action: "continue" | "refocus" | "unclear";
70
+ action: "continue" | "refocus" | "blocked";
60
71
  currentMissionAnchor: string;
61
72
  proposal?: ContextProposal;
62
- reason: "matching_mission" | "clear_refocus" | "missing_proposal" | "ambiguous_discussion";
73
+ blockedFailureMessage?: string;
74
+ reason:
75
+ | "matching_mission"
76
+ | "missing_explicit_handoff"
77
+ | "fresh_explicit_handoff"
78
+ | "fresh_explicit_handoff_not_startable";
63
79
  };
64
80
 
65
81
  type ExistingWorkflowChooserOptions = {
@@ -110,7 +126,8 @@ export type CompletionDriverDeps = {
110
126
  missionAnchor?: string,
111
127
  ) => string;
112
128
  completionResumePrompt: (taskType: string, evaluationProfile: string) => string;
113
- deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<ContextProposal | undefined>;
129
+ deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<CookContextProposalResult>;
130
+ deriveCookStartupProposal: (ctx: DriverContext, projectName: string) => Promise<CookContextProposalResult>;
114
131
  confirmContextProposal: (
115
132
  ctx: { hasUI: boolean; ui: any },
116
133
  proposal: ContextProposal,
@@ -126,7 +143,6 @@ export type CompletionDriverDeps = {
126
143
  maybeWriteActiveWorkflowRoutingSnapshot: (assessment: ActiveWorkflowProposalAssessment) => void;
127
144
  missionAnchorsLikelyEquivalent: (left: string, right: string) => boolean;
128
145
  missionAnchorsStrictlyEquivalent: (left: string, right: string) => boolean;
129
- shouldTreatBareActiveWorkflowProposalAsClearRefocus: (proposal: ContextProposal) => boolean;
130
146
  activateCompletionRoutingForRoot: (root: string | undefined) => void;
131
147
  maybeWriteTestSnapshot: (targetPath: string | undefined, content: string) => void;
132
148
  completionTestDriverPromptPath: () => string | undefined;
@@ -305,41 +321,42 @@ async function assessActiveWorkflowProposalRouting(
305
321
  ): Promise<ActiveWorkflowProposalAssessment> {
306
322
  const currentMission = currentMissionAnchor(snapshot);
307
323
  const projectName = path.basename(snapshot.files.root);
308
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
309
- if (!proposal) {
324
+ const explicitHandoff = await deps.deriveCookStartupProposal(ctx, projectName);
325
+ if (explicitHandoff.blockedFailureMessage) {
310
326
  const assessment: ActiveWorkflowProposalAssessment = {
311
- action: "unclear",
327
+ action: "blocked",
312
328
  currentMissionAnchor: currentMission,
313
- reason: "missing_proposal",
329
+ blockedFailureMessage: explicitHandoff.blockedFailureMessage,
330
+ reason: "fresh_explicit_handoff_not_startable",
314
331
  };
315
332
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
316
333
  return assessment;
317
334
  }
318
- if (deps.missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
335
+ const proposal = explicitHandoff.proposal;
336
+ if (!proposal) {
319
337
  const assessment: ActiveWorkflowProposalAssessment = {
320
338
  action: "continue",
321
339
  currentMissionAnchor: currentMission,
322
- proposal,
323
- reason: "matching_mission",
340
+ reason: "missing_explicit_handoff",
324
341
  };
325
342
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
326
343
  return assessment;
327
344
  }
328
- if (deps.shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
345
+ if (deps.missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
329
346
  const assessment: ActiveWorkflowProposalAssessment = {
330
- action: "refocus",
347
+ action: "continue",
331
348
  currentMissionAnchor: currentMission,
332
349
  proposal,
333
- reason: "clear_refocus",
350
+ reason: "matching_mission",
334
351
  };
335
352
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
336
353
  return assessment;
337
354
  }
338
355
  const assessment: ActiveWorkflowProposalAssessment = {
339
- action: "unclear",
356
+ action: "refocus",
340
357
  currentMissionAnchor: currentMission,
341
358
  proposal,
342
- reason: "ambiguous_discussion",
359
+ reason: "fresh_explicit_handoff",
343
360
  };
344
361
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
345
362
  return assessment;
@@ -519,9 +536,14 @@ export async function runCookEntry(
519
536
  if (!snapshot) {
520
537
  const root = findRepoRoot(cwd) ?? cwd;
521
538
  const projectName = path.basename(root);
522
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
539
+ const derived = await deps.deriveCookStartupProposal(ctx, projectName);
540
+ if (derived.blockedFailureMessage) {
541
+ deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
542
+ return;
543
+ }
544
+ const proposal = derived.proposal;
523
545
  if (!proposal) {
524
- deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps), "info");
546
+ deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(deps), "info");
525
547
  return;
526
548
  }
527
549
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -559,9 +581,14 @@ export async function runCookEntry(
559
581
  if (!goal) {
560
582
  if (workflowDone) {
561
583
  const projectName = path.basename(snapshot.files.root);
562
- const proposal = await deps.deriveCookContextProposal(ctx, projectName);
584
+ const derived = await deps.deriveCookStartupProposal(ctx, projectName);
585
+ if (derived.blockedFailureMessage) {
586
+ deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
587
+ return;
588
+ }
589
+ const proposal = derived.proposal;
563
590
  if (!proposal) {
564
- deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps, "The previous completion workflow is already done."), "info");
591
+ deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(deps, "The previous completion workflow is already done."), "info");
565
592
  return;
566
593
  }
567
594
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -583,22 +610,32 @@ export async function runCookEntry(
583
610
  buildAdvisoryStartupBrief({ proposal, analysis: decision.analysis }),
584
611
  );
585
612
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
586
- deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
613
+ deps.emitCommandText(ctx, `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`, "info");
587
614
  } else {
588
615
  const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
616
+ if (assessment.action === "blocked") {
617
+ deps.emitCommandText(ctx, assessment.blockedFailureMessage ?? buildCookStructuredDiscussionFailureMessage(deps), "info");
618
+ return;
619
+ }
589
620
  if (!assessment.proposal || assessment.action === "continue") {
590
621
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
591
622
  return;
592
623
  }
624
+ const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
593
625
  const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
594
- intro:
595
- assessment.action === "refocus"
596
- ? "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:"
597
- : "Recent discussion may point to a different implementation goal. Review the current mission and the latest inferred mission before deciding how /cook should proceed:",
598
- proposedMissionLabel: "Proposed mission from recent discussion",
599
- refocusChoiceLabel:
600
- "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
601
- comparison: assessment.action === "refocus" ? "semantic" : "strict",
626
+ intro: explicitReplacement
627
+ ? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
628
+ : "A replacement workflow is ready. Choose how /cook should proceed:",
629
+ proposedMissionLabel: explicitReplacement
630
+ ? "Proposed mission from explicit primary-agent handoff"
631
+ : "Proposed mission",
632
+ refocusChoiceLabel: explicitReplacement
633
+ ? "Start new workflow from explicit primary-agent handoff\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
634
+ : "Start new workflow\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
635
+ alternateChoiceLabel: explicitReplacement
636
+ ? "Start alternate workflow from explicit primary-agent handoff\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
637
+ : undefined,
638
+ comparison: "strict",
602
639
  });
603
640
  if (!decision) {
604
641
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation", deps), "info");
@@ -610,10 +647,9 @@ export async function runCookEntry(
610
647
  }
611
648
  const selectedProposal = decision.proposal;
612
649
  const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
613
- title:
614
- assessment.action === "refocus"
615
- ? "Start the replacement workflow from this startup brief?"
616
- : "Start the latest inferred workflow from this startup brief?",
650
+ title: assessment.reason === "fresh_explicit_handoff"
651
+ ? "Start the replacement workflow from this explicit startup brief?"
652
+ : "Start the replacement workflow from this startup brief?",
617
653
  });
618
654
  if (!proposalDecision) {
619
655
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
@@ -631,7 +667,13 @@ export async function runCookEntry(
631
667
  buildAdvisoryStartupBrief({ proposal: selectedProposal, analysis: proposalDecision.analysis }),
632
668
  );
633
669
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
634
- deps.emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
670
+ deps.emitCommandText(
671
+ ctx,
672
+ assessment.reason === "fresh_explicit_handoff"
673
+ ? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
674
+ : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
675
+ "info",
676
+ );
635
677
  }
636
678
  }
637
679
  kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);