@linimin/pi-letscook 0.1.58 → 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.
- package/.agent/README.md +28 -0
- package/.agent/mission.md +8 -0
- package/.agent/profile.json +13 -0
- package/.agent/verify_completion_control_plane.sh +203 -0
- package/.agent/verify_completion_stop.sh +20 -0
- package/CHANGELOG.md +10 -0
- package/README.md +23 -19
- package/extensions/completion/driver.ts +47 -41
- package/extensions/completion/index.ts +29 -21
- package/extensions/completion/prompt-surfaces.ts +8 -6
- package/extensions/completion/proposal.ts +0 -15
- package/package.json +6 -1
- package/scripts/active-slice-contract-test.sh +93 -2
- package/scripts/canonical-evidence-artifact-test.sh +93 -2
- package/scripts/context-proposal-test.sh +553 -736
- package/scripts/refocus-test.sh +196 -28
- package/scripts/release-check.sh +50 -28
- package/scripts/smoke-test.sh +113 -10
- package/skills/cook-handoff-boundary/SKILL.md +11 -6
package/.agent/README.md
ADDED
|
@@ -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,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## 0.1.58
|
|
6
16
|
|
|
7
17
|
### 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
|
|
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,20 +43,20 @@ 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
|
-
-
|
|
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
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
|
-
-
|
|
55
|
-
- enough detail to derive a startup brief with mission, scope, constraints or non-goals, acceptance, and notes or risks
|
|
54
|
+
- enough detail in the main chat for the primary agent to form that bounded handoff capsule before you run `/cook`
|
|
56
55
|
- README/CHANGELOG updates still count as concrete repo changes
|
|
57
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
|
|
58
58
|
|
|
59
|
-
If no fresh valid handoff exists
|
|
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
60
|
|
|
61
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.
|
|
62
62
|
|
|
@@ -66,36 +66,40 @@ 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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
74
|
+
|
|
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.
|
|
74
76
|
|
|
75
77
|
Important behavior:
|
|
76
78
|
- `/cook` is the canonical workflow boundary and manual entry point
|
|
77
|
-
- startup
|
|
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
|
|
78
81
|
- explicit slash commands other than `/cook` continue normally in the main chat
|
|
79
82
|
- ordinary main-chat discussion may clarify or propose, but mature long-running implementation should be handed off to `/cook`
|
|
80
83
|
|
|
81
84
|
## Typical examples
|
|
82
85
|
|
|
83
|
-
Start a new workflow
|
|
86
|
+
Start a new workflow after a fresh primary-agent handoff:
|
|
84
87
|
|
|
85
88
|
```text
|
|
86
89
|
I want to add login redirect handling and tests.
|
|
90
|
+
# let the primary agent hand you off to /cook
|
|
87
91
|
/cook
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
## What happens when you run `/cook`
|
|
91
95
|
|
|
92
|
-
`/cook` first looks for a fresh explicit primary-agent handoff capsule
|
|
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.
|
|
93
97
|
|
|
94
98
|
| Repo state | What you'll see |
|
|
95
99
|
|---|---|
|
|
96
|
-
| No workflow yet | If a fresh explicit handoff capsule exists and is implementation-startable, a startup brief built from that handoff
|
|
97
|
-
| Active workflow exists | Usually a resume of the current workflow. If a fresh explicit handoff capsule
|
|
98
|
-
| Previous workflow is `done` | A fresh explicit handoff capsule can still start the next implementation round behind **Start** or **Cancel**. Without
|
|
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. |
|
|
99
103
|
|
|
100
104
|
## Confirmation and fail-closed behavior
|
|
101
105
|
|
|
@@ -103,9 +107,9 @@ I want to add login redirect handling and tests.
|
|
|
103
107
|
|
|
104
108
|
- startup, next-round, and refocus proposals are approval-only
|
|
105
109
|
- actions are **Start** and **Cancel**
|
|
106
|
-
- **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`
|
|
107
111
|
- weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
|
|
108
|
-
- when
|
|
112
|
+
- when a fresh explicit handoff suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
|
|
109
113
|
|
|
110
114
|
When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
|
|
111
115
|
|
|
@@ -60,16 +60,21 @@ type CookContextProposalResult = {
|
|
|
60
60
|
blockedFailureMessage?: string;
|
|
61
61
|
};
|
|
62
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
|
+
|
|
63
69
|
type ActiveWorkflowProposalAssessment = {
|
|
64
|
-
action: "continue" | "refocus" | "
|
|
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
|
-
| "
|
|
71
|
-
| "
|
|
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
|
|
320
|
-
if (
|
|
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:
|
|
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 =
|
|
335
|
+
const proposal = explicitHandoff.proposal;
|
|
331
336
|
if (!proposal) {
|
|
332
337
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
333
|
-
action: "
|
|
338
|
+
action: "continue",
|
|
334
339
|
currentMissionAnchor: currentMission,
|
|
335
|
-
reason: "
|
|
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: "
|
|
356
|
+
action: "refocus",
|
|
362
357
|
currentMissionAnchor: currentMission,
|
|
363
358
|
proposal,
|
|
364
|
-
reason: "
|
|
359
|
+
reason: "fresh_explicit_handoff",
|
|
365
360
|
};
|
|
366
361
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
367
362
|
return assessment;
|
|
@@ -541,14 +536,14 @@ export async function runCookEntry(
|
|
|
541
536
|
if (!snapshot) {
|
|
542
537
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
543
538
|
const projectName = path.basename(root);
|
|
544
|
-
const derived = await deps.
|
|
539
|
+
const derived = await deps.deriveCookStartupProposal(ctx, projectName);
|
|
545
540
|
if (derived.blockedFailureMessage) {
|
|
546
541
|
deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
|
|
547
542
|
return;
|
|
548
543
|
}
|
|
549
544
|
const proposal = derived.proposal;
|
|
550
545
|
if (!proposal) {
|
|
551
|
-
deps.emitCommandText(ctx,
|
|
546
|
+
deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(deps), "info");
|
|
552
547
|
return;
|
|
553
548
|
}
|
|
554
549
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -586,14 +581,14 @@ export async function runCookEntry(
|
|
|
586
581
|
if (!goal) {
|
|
587
582
|
if (workflowDone) {
|
|
588
583
|
const projectName = path.basename(snapshot.files.root);
|
|
589
|
-
const derived = await deps.
|
|
584
|
+
const derived = await deps.deriveCookStartupProposal(ctx, projectName);
|
|
590
585
|
if (derived.blockedFailureMessage) {
|
|
591
586
|
deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
|
|
592
587
|
return;
|
|
593
588
|
}
|
|
594
589
|
const proposal = derived.proposal;
|
|
595
590
|
if (!proposal) {
|
|
596
|
-
deps.emitCommandText(ctx,
|
|
591
|
+
deps.emitCommandText(ctx, buildCookExplicitHandoffRequiredMessage(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,7 @@ 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
|
|
613
|
+
deps.emitCommandText(ctx, `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`, "info");
|
|
619
614
|
} else {
|
|
620
615
|
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
|
|
621
616
|
if (assessment.action === "blocked") {
|
|
@@ -626,15 +621,21 @@ export async function runCookEntry(
|
|
|
626
621
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
627
622
|
return;
|
|
628
623
|
}
|
|
624
|
+
const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
|
|
629
625
|
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
|
|
630
|
-
intro:
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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",
|
|
638
639
|
});
|
|
639
640
|
if (!decision) {
|
|
640
641
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation", deps), "info");
|
|
@@ -646,10 +647,9 @@ export async function runCookEntry(
|
|
|
646
647
|
}
|
|
647
648
|
const selectedProposal = decision.proposal;
|
|
648
649
|
const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
|
|
649
|
-
title:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
: "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?",
|
|
653
653
|
});
|
|
654
654
|
if (!proposalDecision) {
|
|
655
655
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
|
|
@@ -667,7 +667,13 @@ export async function runCookEntry(
|
|
|
667
667
|
buildAdvisoryStartupBrief({ proposal: selectedProposal, analysis: proposalDecision.analysis }),
|
|
668
668
|
);
|
|
669
669
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
670
|
-
deps.emitCommandText(
|
|
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
|
+
);
|
|
671
677
|
}
|
|
672
678
|
}
|
|
673
679
|
kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
|