@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.
@@ -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
+ Refactor the /cook startup boundary into the agreed mixed model: ordinary chat stays advisory-first by default with no default pre-/cook handoff capsule formation, while explicit /cook performs structured startup synthesis from recent discussion and preserves the approval-only Start/Cancel gate.
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,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.60
6
+
7
+ ## 0.1.59
8
+
9
+ ### Changed
10
+
11
+ - 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
12
+ - kept `/cook` as the only explicit workflow boundary, while moving default startup and done-workflow next-round synthesis to bare `/cook` from recent ordinary-chat discussion behind the existing **Start** / **Cancel** approval gate
13
+ - kept pre-`/cook` previews or `cook_handoff` capsules opt-in only, non-canonical, and advisory until the user explicitly runs `/cook`; bare `/cook` no longer depends on a default prebuilt capsule for new-workflow startup
14
+ - kept active workflows resuming from canonical `.agent/**` state unless a fresh explicit handoff proposes a replacement, so discussion-only context does not silently rewrite an in-progress workflow
15
+ - updated public parity and packaged release verification so README/help/changelog/release-check all describe and gate the shipped mixed model truthfully, while still packaging the tracked `.agent` contract files
16
+
5
17
  ## 0.1.58
6
18
 
7
19
  ### Changed
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,22 +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, then run `/cook` once the recent discussion is specific enough for a startup brief. If you explicitly want a pre-`/cook` preview or capsule first, ask for one. |
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, then run `/cook` to synthesize the next startup brief. Active-workflow replacement still stays explicit and confirm-first. |
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
- - 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
- - otherwise, when no fresh explicit handoff is blocking startup, recent main-chat discussion about concrete repo changes
55
- - enough detail to derive a startup brief with mission, scope, constraints or non-goals, acceptance, and notes or risks
52
+ - recent ordinary-chat discussion concrete enough for bare `/cook` to synthesize a startup brief for a new workflow or the next round after a completed workflow
53
+ - enough repo-change detail for that startup brief to stay implementation-oriented once you review it behind **Start** or **Cancel**
56
54
  - README/CHANGELOG updates still count as concrete repo changes
57
- - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they include the explicit structured `/cook` handoff capsule
55
+ - assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not become canonical workflow state by themselves
56
+ - any pre-`/cook` preview or `cook_handoff` capsule only when you explicitly ask for it; that preview stays advisory startup intake, not canonical `.agent/**` state
57
+ - active-workflow replacement still stays conservative: `/cook` resumes from canonical state unless a fresh explicit handoff proposes a different concrete repo change and you confirm that replacement
58
58
 
59
- 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 recent discussion is too weak, ambiguous, stale, or planning-only for new-workflow or next-round entry, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to clarify the concrete repo change in the main chat before rerunning `/cook`.
60
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.
61
+ If you explicitly asked for a preview capsule and it is still workflow-worthy rather than implementation-startable, `/cook` also fails closed instead of silently treating that preview as planning support or canonical workflow state.
62
62
 
63
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`.
64
64
 
@@ -66,15 +66,19 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
66
66
 
67
67
  Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.
68
68
 
69
- 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
70
 
71
- 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.
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
72
 
73
- The preferred capsule is still advisory startup intake, not canonical workflow state, and it only counts as implementation-ready when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.
73
+ If you explicitly ask for a pre-`/cook` preview or capsule, the primary agent may provide one, but that preview is opt-in only and stays non-canonical until you later run `/cook` and choose **Start**.
74
+
75
+ Bare `/cook` is still the canonical workflow boundary: it synthesizes the startup brief from recent ordinary-chat discussion at `/cook` time, then waits for **Start** or **Cancel** before any canonical `.agent/**` write.
74
76
 
75
77
  Important behavior:
76
78
  - `/cook` is the canonical workflow boundary and manual entry point
77
- - startup, refocus, and next-round routing stay confirm-first; nothing silently starts a workflow
79
+ - startup and next-round entry stay confirm-first: bare `/cook` synthesizes the startup brief from recent discussion, then waits for **Start** or **Cancel**
80
+ - active workflows resume from canonical `.agent/**` state unless a fresh valid explicit handoff proposes a replacement
81
+ - any pre-`/cook` preview or capsule is explicit-request-only and non-canonical
78
82
  - explicit slash commands other than `/cook` continue normally in the main chat
79
83
  - ordinary main-chat discussion may clarify or propose, but mature long-running implementation should be handed off to `/cook`
80
84
 
@@ -84,18 +88,19 @@ Start a new workflow from recent discussion:
84
88
 
85
89
  ```text
86
90
  I want to add login redirect handling and tests.
91
+ # discuss scope until the startup brief is clear enough
87
92
  /cook
88
93
  ```
89
94
 
90
95
  ## What happens when you run `/cook`
91
96
 
92
- `/cook` first looks for a fresh explicit primary-agent handoff capsule. If one is valid and implementation-startable, `/cook` builds the startup brief from that handoff and only uses recent discussion as validation or supplemental notes. `/cook` falls back to deriving a startup brief from recent discussion only when no fresh explicit handoff is blocking startup—for example, when there is no fresh capsule or only stale or invalidated capsules—before showing the existing approval-only Start/Cancel gate.
97
+ When no workflow is active, bare `/cook` synthesizes a startup brief from recent ordinary-chat discussion and then waits for **Start** or **Cancel**. If recent discussion is too weak, ambiguous, stale, or planning-only, `/cook` fails closed instead of guessing. If you explicitly asked for a preview capsule first and that preview is fresh but still non-startable, `/cook` also fails closed instead of silently treating it as canonical state. When a workflow is already active and no fresh valid explicit replacement handoff is present, `/cook` resumes from canonical `.agent/**` state instead of deriving replacement startup from recent discussion.
93
98
 
94
99
  | Repo state | What you'll see |
95
100
  |---|---|
96
- | No workflow yet | If a fresh explicit handoff capsule exists and is implementation-startable, a startup brief built from that handoff. Otherwise, when no fresh explicit handoff is blocking startup, a startup brief built from recent main-chat discussion. You choose **Start** or **Cancel**. Weak, unreliable, stale, planning-only, or non-startable explicit-handoff intake fails closed. |
97
- | 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. |
98
- | Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without a fresh explicit handoff blocking startup, `/cook` can fall back to recent discussion. Discussion that only restates already-finished work still fails closed. |
101
+ | No workflow yet | `/cook` synthesizes a startup brief from recent discussion and shows **Start** / **Cancel**. If recent discussion is too weak, ambiguous, stale, or planning-only, `/cook` fails closed and leaves canonical state unchanged. An explicit-request preview capsule can inform that startup brief, but it is still non-canonical until you choose **Start**. |
102
+ | 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. |
103
+ | Previous workflow is `done` | `/cook` synthesizes the next implementation round from recent discussion behind **Start** / **Cancel**. If that recent discussion is too weak or ambiguous, `/cook` fails closed and leaves the finished workflow state unchanged. |
99
104
 
100
105
  ## Confirmation and fail-closed behavior
101
106
 
@@ -103,9 +108,10 @@ I want to add login redirect handling and tests.
103
108
 
104
109
  - startup, next-round, and refocus proposals are approval-only
105
110
  - actions are **Start** and **Cancel**
106
- - **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
111
+ - **Cancel** is side-effect free: canonical workflow state stays unchanged, so you can discuss changes in the main chat and rerun `/cook`
107
112
  - weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
108
- - when recent discussion suggests a different workflow, `/cook` shows a chooser before any canonical state rewrite
113
+ - any pre-`/cook` preview or capsule is advisory only and never writes canonical workflow state by itself
114
+ - when a fresh explicit handoff suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
109
115
 
110
116
  When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
111
117
 
@@ -258,7 +264,7 @@ npm run rubric-contract-test
258
264
  npm run release-check
259
265
  ```
260
266
 
261
- `npm run release-check` is the broad packaged-release verifier. It begins with `bash .agent/verify_completion_control_plane.sh`, so missing or stale `.agent/verification-evidence.json` parity fails closed before the broader suite runs, then asserts the shipped `/cook` public parity surfaces in `README.md`, `CHANGELOG.md`, and the `/cook` help/fail-closed copy in `extensions/completion/index.ts`, reruns the startup/refocus/context checks — including the critique-aware `/cook` confirmation regression and the smoke auto-resume prompt path — includes deterministic canonical evidence artifact coverage and includes deterministic active-slice contract coverage plus observability coverage, evaluator calibration, and the rubric-contract regression, and finishes with `npm pack --dry-run`.
267
+ `npm run release-check` is the broad packaged-release verifier. It begins with `bash .agent/verify_completion_control_plane.sh`, so missing or stale `.agent/verification-evidence.json` parity fails closed before the broader suite runs, then asserts the shipped mixed-model `/cook` public parity surfaces in `README.md`, `CHANGELOG.md`, and the `/cook` help/fail-closed copy in `extensions/completion/index.ts`, reruns the startup/refocus/context checks — including the critique-aware `/cook` confirmation regression and the smoke auto-resume prompt path — includes deterministic canonical evidence artifact coverage and includes deterministic active-slice contract coverage plus observability coverage, evaluator calibration, and the rubric-contract regression, and finishes with `npm pack --dry-run`.
262
268
 
263
269
  The direct package-root verifier commands above intentionally self-isolate the repo-local extension when they shell back into `pi`, so you should not need to wrap them with `pi --no-extensions` even if `@linimin/pi-letscook` is also installed globally on the same machine.
264
270
 
@@ -60,16 +60,21 @@ type CookContextProposalResult = {
60
60
  blockedFailureMessage?: string;
61
61
  };
62
62
 
63
+ function buildCookStartupBriefRequiredMessage(deps: CompletionDriverDeps, prefix?: string): string {
64
+ const requirement =
65
+ "/cook failed closed because recent discussion did not produce a clear execution-ready startup brief with Mission/Scope/Constraints/Acceptance for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
66
+ return prefix ? `${prefix} ${requirement}` : requirement;
67
+ }
68
+
63
69
  type ActiveWorkflowProposalAssessment = {
64
- action: "continue" | "refocus" | "unclear" | "blocked";
70
+ action: "continue" | "refocus" | "blocked";
65
71
  currentMissionAnchor: string;
66
72
  proposal?: ContextProposal;
67
73
  blockedFailureMessage?: string;
68
74
  reason:
69
75
  | "matching_mission"
70
- | "clear_refocus"
71
- | "missing_proposal"
72
- | "ambiguous_discussion"
76
+ | "missing_explicit_handoff"
77
+ | "fresh_explicit_handoff"
73
78
  | "fresh_explicit_handoff_not_startable";
74
79
  };
75
80
 
@@ -122,6 +127,7 @@ export type CompletionDriverDeps = {
122
127
  ) => string;
123
128
  completionResumePrompt: (taskType: string, evaluationProfile: string) => string;
124
129
  deriveCookContextProposal: (ctx: DriverContext, projectName: string) => Promise<CookContextProposalResult>;
130
+ deriveCookStartupProposal: (ctx: DriverContext, projectName: string) => Promise<CookContextProposalResult>;
125
131
  confirmContextProposal: (
126
132
  ctx: { hasUI: boolean; ui: any },
127
133
  proposal: ContextProposal,
@@ -137,7 +143,6 @@ export type CompletionDriverDeps = {
137
143
  maybeWriteActiveWorkflowRoutingSnapshot: (assessment: ActiveWorkflowProposalAssessment) => void;
138
144
  missionAnchorsLikelyEquivalent: (left: string, right: string) => boolean;
139
145
  missionAnchorsStrictlyEquivalent: (left: string, right: string) => boolean;
140
- shouldTreatBareActiveWorkflowProposalAsClearRefocus: (proposal: ContextProposal) => boolean;
141
146
  activateCompletionRoutingForRoot: (root: string | undefined) => void;
142
147
  maybeWriteTestSnapshot: (targetPath: string | undefined, content: string) => void;
143
148
  completionTestDriverPromptPath: () => string | undefined;
@@ -316,23 +321,23 @@ async function assessActiveWorkflowProposalRouting(
316
321
  ): Promise<ActiveWorkflowProposalAssessment> {
317
322
  const currentMission = currentMissionAnchor(snapshot);
318
323
  const projectName = path.basename(snapshot.files.root);
319
- const derived = await deps.deriveCookContextProposal(ctx, projectName);
320
- if (derived.blockedFailureMessage) {
324
+ const explicitHandoff = await deps.deriveCookStartupProposal(ctx, projectName);
325
+ if (explicitHandoff.blockedFailureMessage) {
321
326
  const assessment: ActiveWorkflowProposalAssessment = {
322
327
  action: "blocked",
323
328
  currentMissionAnchor: currentMission,
324
- blockedFailureMessage: derived.blockedFailureMessage,
329
+ blockedFailureMessage: explicitHandoff.blockedFailureMessage,
325
330
  reason: "fresh_explicit_handoff_not_startable",
326
331
  };
327
332
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
328
333
  return assessment;
329
334
  }
330
- const proposal = derived.proposal;
335
+ const proposal = explicitHandoff.proposal;
331
336
  if (!proposal) {
332
337
  const assessment: ActiveWorkflowProposalAssessment = {
333
- action: "unclear",
338
+ action: "continue",
334
339
  currentMissionAnchor: currentMission,
335
- reason: "missing_proposal",
340
+ reason: "missing_explicit_handoff",
336
341
  };
337
342
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
338
343
  return assessment;
@@ -347,21 +352,11 @@ async function assessActiveWorkflowProposalRouting(
347
352
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
348
353
  return assessment;
349
354
  }
350
- if (deps.shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
351
- const assessment: ActiveWorkflowProposalAssessment = {
352
- action: "refocus",
353
- currentMissionAnchor: currentMission,
354
- proposal,
355
- reason: "clear_refocus",
356
- };
357
- deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
358
- return assessment;
359
- }
360
355
  const assessment: ActiveWorkflowProposalAssessment = {
361
- action: "unclear",
356
+ action: "refocus",
362
357
  currentMissionAnchor: currentMission,
363
358
  proposal,
364
- reason: "ambiguous_discussion",
359
+ reason: "fresh_explicit_handoff",
365
360
  };
366
361
  deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
367
362
  return assessment;
@@ -548,7 +543,7 @@ export async function runCookEntry(
548
543
  }
549
544
  const proposal = derived.proposal;
550
545
  if (!proposal) {
551
- deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps), "info");
546
+ deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps), "info");
552
547
  return;
553
548
  }
554
549
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -593,7 +588,7 @@ export async function runCookEntry(
593
588
  }
594
589
  const proposal = derived.proposal;
595
590
  if (!proposal) {
596
- deps.emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(deps, "The previous completion workflow is already done."), "info");
591
+ deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps, "The previous completion workflow is already done."), "info");
597
592
  return;
598
593
  }
599
594
  const decision = await deps.confirmContextProposal(ctx, proposal, {
@@ -615,7 +610,13 @@ export async function runCookEntry(
615
610
  buildAdvisoryStartupBrief({ proposal, analysis: decision.analysis }),
616
611
  );
617
612
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
618
- deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
613
+ deps.emitCommandText(
614
+ ctx,
615
+ proposal.source === "handoff_capsule"
616
+ ? `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`
617
+ : `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`,
618
+ "info",
619
+ );
619
620
  } else {
620
621
  const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
621
622
  if (assessment.action === "blocked") {
@@ -626,15 +627,21 @@ export async function runCookEntry(
626
627
  await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
627
628
  return;
628
629
  }
630
+ const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
629
631
  const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
630
- intro:
631
- assessment.action === "refocus"
632
- ? "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:"
633
- : "Recent discussion may point to a different implementation goal. Review the current mission and the latest inferred mission before deciding how /cook should proceed:",
634
- proposedMissionLabel: "Proposed mission from recent discussion",
635
- refocusChoiceLabel:
636
- "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
637
- comparison: assessment.action === "refocus" ? "semantic" : "strict",
632
+ intro: explicitReplacement
633
+ ? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
634
+ : "A replacement workflow is ready. Choose how /cook should proceed:",
635
+ proposedMissionLabel: explicitReplacement
636
+ ? "Proposed mission from explicit primary-agent handoff"
637
+ : "Proposed mission",
638
+ refocusChoiceLabel: explicitReplacement
639
+ ? "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."
640
+ : "Start new workflow\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
641
+ alternateChoiceLabel: explicitReplacement
642
+ ? "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."
643
+ : undefined,
644
+ comparison: "strict",
638
645
  });
639
646
  if (!decision) {
640
647
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation", deps), "info");
@@ -646,10 +653,9 @@ export async function runCookEntry(
646
653
  }
647
654
  const selectedProposal = decision.proposal;
648
655
  const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
649
- title:
650
- assessment.action === "refocus"
651
- ? "Start the replacement workflow from this startup brief?"
652
- : "Start the latest inferred workflow from this startup brief?",
656
+ title: assessment.reason === "fresh_explicit_handoff"
657
+ ? "Start the replacement workflow from this explicit startup brief?"
658
+ : "Start the replacement workflow from this startup brief?",
653
659
  });
654
660
  if (!proposalDecision) {
655
661
  deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
@@ -667,7 +673,13 @@ export async function runCookEntry(
667
673
  buildAdvisoryStartupBrief({ proposal: selectedProposal, analysis: proposalDecision.analysis }),
668
674
  );
669
675
  snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
670
- deps.emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
676
+ deps.emitCommandText(
677
+ ctx,
678
+ assessment.reason === "fresh_explicit_handoff"
679
+ ? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
680
+ : `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
681
+ "info",
682
+ );
671
683
  }
672
684
  }
673
685
  kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);