@linimin/pi-letscook 0.1.55 → 0.1.57
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/CHANGELOG.md +6 -1
- package/README.md +16 -8
- package/extensions/completion/index.ts +61 -20
- package/extensions/completion/prompt-surfaces.ts +17 -3
- package/extensions/completion/proposal.ts +218 -13
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +289 -0
- package/scripts/release-check.sh +12 -8
- package/scripts/smoke-test.sh +12 -3
- package/skills/cook-handoff-boundary/SKILL.md +94 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.57
|
|
6
|
+
|
|
5
7
|
### Changed
|
|
6
8
|
|
|
7
|
-
- made `/cook`
|
|
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
|
|
11
|
+
- 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
|
|
8
12
|
- 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
|
+
- 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
|
|
9
14
|
|
|
10
15
|
## 0.1.54
|
|
11
16
|
|
package/README.md
CHANGED
|
@@ -49,12 +49,13 @@ Then run `/reload` in Pi.
|
|
|
49
49
|
|
|
50
50
|
## What `/cook` expects
|
|
51
51
|
|
|
52
|
-
-
|
|
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
|
|
53
54
|
- enough detail to derive a startup brief with mission, scope, constraints or non-goals, acceptance, and notes or risks
|
|
54
55
|
- README/CHANGELOG updates still count as concrete repo changes
|
|
55
|
-
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts do not
|
|
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
|
|
56
57
|
|
|
57
|
-
If 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`.
|
|
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`.
|
|
58
59
|
|
|
59
60
|
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`.
|
|
60
61
|
|
|
@@ -62,10 +63,15 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
|
|
|
62
63
|
|
|
63
64
|
Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.
|
|
64
65
|
|
|
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.
|
|
67
|
+
|
|
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.
|
|
69
|
+
|
|
65
70
|
Important behavior:
|
|
66
71
|
- `/cook` is the canonical workflow boundary and manual entry point
|
|
67
72
|
- startup, refocus, and next-round routing stay confirm-first; nothing silently starts a workflow
|
|
68
73
|
- explicit slash commands other than `/cook` continue normally in the main chat
|
|
74
|
+
- ordinary main-chat discussion may clarify or propose, but mature long-running implementation should be handed off to `/cook`
|
|
69
75
|
|
|
70
76
|
## Typical examples
|
|
71
77
|
|
|
@@ -78,13 +84,13 @@ I want to add login redirect handling and tests.
|
|
|
78
84
|
|
|
79
85
|
## What happens when you run `/cook`
|
|
80
86
|
|
|
81
|
-
`/cook` first
|
|
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.
|
|
82
88
|
|
|
83
89
|
| Repo state | What you'll see |
|
|
84
90
|
|---|---|
|
|
85
|
-
| No workflow yet |
|
|
86
|
-
| Active workflow exists | Usually a resume of the current workflow. If 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
|
|
87
|
-
| Previous workflow is `done` | A
|
|
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. |
|
|
88
94
|
|
|
89
95
|
## Confirmation and fail-closed behavior
|
|
90
96
|
|
|
@@ -93,13 +99,15 @@ I want to add login redirect handling and tests.
|
|
|
93
99
|
- startup, next-round, and refocus proposals are approval-only
|
|
94
100
|
- actions are **Start** and **Cancel**
|
|
95
101
|
- **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
|
|
96
|
-
- weak, ambiguous, assistant-produced, or planning-only
|
|
102
|
+
- weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
|
|
97
103
|
- when recent discussion suggests a different workflow, `/cook` shows a chooser before any canonical state rewrite
|
|
98
104
|
|
|
99
105
|
When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
|
|
100
106
|
|
|
101
107
|
The confirmed startup brief is also preserved there as advisory intake for later re-grounding. It does not replace `.agent/plan.json` or `.agent/active-slice.json`, which remain under regrounder authority.
|
|
102
108
|
|
|
109
|
+
The pre-`/cook` handoff capsule itself is not canonical workflow state. It is only startup intake for `/cook`.
|
|
110
|
+
|
|
103
111
|
## Observability
|
|
104
112
|
|
|
105
113
|
When canonical `.agent/**` state exists and no role is actively running, the extension shows a completion widget sourced from that state. The widget summarizes:
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
assessMissionAnchor,
|
|
18
18
|
collectRecentDiscussionEntries,
|
|
19
|
+
collectRecentSessionMessages,
|
|
19
20
|
deriveCookContextProposalFromRecentDiscussion,
|
|
21
|
+
extractLatestCookHandoffProposal,
|
|
20
22
|
finalizeContextProposalAnalysis,
|
|
21
23
|
isWeakMissionAnchor,
|
|
22
24
|
missionAnchorsLikelyEquivalent,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
buildContextProposalConfirmationLayout as buildExtractedContextProposalConfirmationLayout,
|
|
39
41
|
buildContextProposalConfirmationSelectItems,
|
|
40
42
|
buildContextProposalContinuationReason as buildExtractedContextProposalContinuationReason,
|
|
43
|
+
buildCookHandoffBoundaryReminder as buildExtractedCookHandoffBoundaryReminder,
|
|
41
44
|
buildEvaluationRoleContextLines as buildExtractedEvaluationRoleContextLines,
|
|
42
45
|
buildEvaluationRoleReminderText as buildExtractedEvaluationRoleReminderText,
|
|
43
46
|
buildResumeCapsule as buildExtractedResumeCapsule,
|
|
@@ -181,6 +184,10 @@ function completionTestSystemReminderPath(): string | undefined {
|
|
|
181
184
|
return asString(process.env.PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH);
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
function completionTestCookHandoffReminderPath(): string | undefined {
|
|
188
|
+
return asString(process.env.PI_COMPLETION_TEST_COOK_HANDOFF_REMINDER_PATH);
|
|
189
|
+
}
|
|
190
|
+
|
|
184
191
|
function maybeWriteTestSnapshot(targetPath: string | undefined, content: string): void {
|
|
185
192
|
if (!targetPath) return;
|
|
186
193
|
try {
|
|
@@ -225,6 +232,19 @@ function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot
|
|
|
225
232
|
return hasCompletionRoutingActivation(snapshot) && isCompletionDriverPromptTurn(ctx);
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
function shouldInjectCookHandoffBoundary(event: { prompt?: string }, ctx: { sessionManager?: any }): boolean {
|
|
236
|
+
if (roleFromEnv()) return false;
|
|
237
|
+
if (isCompletionDriverPromptTurn(ctx)) return false;
|
|
238
|
+
const prompt = typeof event.prompt === "string" ? event.prompt.trim() : "";
|
|
239
|
+
if (!prompt) return false;
|
|
240
|
+
if (prompt.startsWith("/")) return false;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildCookHandoffBoundaryReminder(): string {
|
|
245
|
+
return buildExtractedCookHandoffBoundaryReminder();
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): string {
|
|
229
249
|
const missionAnchor = asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? "(unknown)";
|
|
230
250
|
const continuationReason = asString(snapshot.state?.continuation_reason) ?? "(unknown)";
|
|
@@ -346,7 +366,11 @@ async function deriveCookContextProposal(
|
|
|
346
366
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
347
367
|
projectName: string,
|
|
348
368
|
): Promise<ContextProposal | undefined> {
|
|
349
|
-
const
|
|
369
|
+
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
370
|
+
const recentEntries = recentMessages
|
|
371
|
+
.filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
|
|
372
|
+
.slice(0, 8)
|
|
373
|
+
.map((entry) => ({ role: entry.role, text: entry.text }));
|
|
350
374
|
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
351
375
|
const workflowContextLines = snapshot
|
|
352
376
|
? [
|
|
@@ -360,6 +384,16 @@ async function deriveCookContextProposal(
|
|
|
360
384
|
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
361
385
|
]
|
|
362
386
|
: [];
|
|
387
|
+
const explicitHandoff = extractLatestCookHandoffProposal(recentMessages, projectName, {
|
|
388
|
+
asString,
|
|
389
|
+
asStringArray,
|
|
390
|
+
assessMissionAnchor,
|
|
391
|
+
normalizeMissionAnchorText,
|
|
392
|
+
isWeakMissionAnchor,
|
|
393
|
+
missionAnchorsStrictlyEquivalent,
|
|
394
|
+
stripCodeBlocks,
|
|
395
|
+
});
|
|
396
|
+
if (explicitHandoff) return explicitHandoff;
|
|
363
397
|
return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
364
398
|
asString,
|
|
365
399
|
asStringArray,
|
|
@@ -926,7 +960,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
926
960
|
}
|
|
927
961
|
});
|
|
928
962
|
|
|
929
|
-
pi.on("before_agent_start", async (
|
|
963
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
930
964
|
const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
|
|
931
965
|
const driverPromptTurn = isCompletionDriverPromptTurn(ctx);
|
|
932
966
|
if (loaded && driverPromptTurn) {
|
|
@@ -934,28 +968,35 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
934
968
|
const fingerprint = completionContinuationFingerprint(loaded.snapshot);
|
|
935
969
|
if (fingerprint) markQueuedDriverPromptInFlight(rootKey, fingerprint);
|
|
936
970
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
971
|
+
const systemPrompt = getSystemPromptSafe(ctx);
|
|
972
|
+
if (!systemPrompt) return;
|
|
973
|
+
if (loaded && shouldInjectCompletionWorkflowContext(loaded.snapshot, ctx)) {
|
|
974
|
+
const additions = isWorkflowDone(loaded.snapshot)
|
|
975
|
+
? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
|
|
976
|
+
: [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
|
|
977
|
+
if (!isWorkflowDone(loaded.snapshot)) {
|
|
978
|
+
const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
|
|
979
|
+
let marker: JsonRecord | undefined;
|
|
980
|
+
if (markerText) {
|
|
981
|
+
try {
|
|
982
|
+
const parsed = JSON.parse(markerText);
|
|
983
|
+
marker = isRecord(parsed) ? parsed : undefined;
|
|
984
|
+
} catch {
|
|
985
|
+
marker = undefined;
|
|
986
|
+
}
|
|
950
987
|
}
|
|
988
|
+
if (marker) additions.push(buildPostCompactionDriverInstructions(loaded.snapshot, marker));
|
|
951
989
|
}
|
|
952
|
-
|
|
990
|
+
maybeWriteTestSnapshot(completionTestSystemReminderPath(), additions.join("\n\n"));
|
|
991
|
+
return {
|
|
992
|
+
systemPrompt: `${systemPrompt}\n\n${additions.join("\n\n")}`,
|
|
993
|
+
};
|
|
953
994
|
}
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
|
|
995
|
+
if (!shouldInjectCookHandoffBoundary(event, ctx)) return;
|
|
996
|
+
const handoffReminder = buildCookHandoffBoundaryReminder();
|
|
997
|
+
maybeWriteTestSnapshot(completionTestCookHandoffReminderPath(), handoffReminder);
|
|
957
998
|
return {
|
|
958
|
-
systemPrompt: `${systemPrompt}\n\n${
|
|
999
|
+
systemPrompt: `${systemPrompt}\n\n${handoffReminder}`,
|
|
959
1000
|
};
|
|
960
1001
|
});
|
|
961
1002
|
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
|
|
11
11
|
export type AdvisoryStartupBrief = {
|
|
12
12
|
kind: "startup_brief";
|
|
13
|
-
source: "recent_discussion";
|
|
13
|
+
source: "recent_discussion" | "primary_agent_handoff";
|
|
14
14
|
confirmed: true;
|
|
15
15
|
captured_at: string;
|
|
16
16
|
goal_text: string;
|
|
@@ -24,6 +24,20 @@ export type AdvisoryStartupBrief = {
|
|
|
24
24
|
evaluation_profile?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
export function buildCookHandoffBoundaryReminder(): string {
|
|
28
|
+
return [
|
|
29
|
+
"You are still in ordinary main chat before any explicit /cook workflow entry.",
|
|
30
|
+
"Use ordinary chat to clarify requirements, discuss tradeoffs, and propose implementation approaches.",
|
|
31
|
+
"/cook is the only explicit entrypoint into long-running completion workflow.",
|
|
32
|
+
"When you judge that the task has matured into completion-workflow scope — for example the user has clearly shifted from exploration into implementation intent, you have just produced a concrete plan or proposal whose next step would naturally be implementation, or the task spans multiple files, steps, or verification surfaces — stop short of long-running implementation and tell the user to run /cook.",
|
|
33
|
+
"At that handoff point, do not begin long-running product implementation in ordinary chat, do not edit tracked product files for that workflow-level task, and do not act as though /cook had already been invoked.",
|
|
34
|
+
"When handing off, explain that /cook will first look for a fresh explicit primary-agent handoff capsule and otherwise fall back to recent discussion.",
|
|
35
|
+
"Also append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON with kind/source/handoff_kind plus mission, scope, constraints or non_goals, acceptance, risks, notes, captured_at, source_turn_id, and optional task_type/evaluation_profile/why_cook_now.",
|
|
36
|
+
"The capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
|
|
37
|
+
"If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
|
|
38
|
+
].join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export function buildContextProposalGoalText(proposal: {
|
|
28
42
|
mission: string;
|
|
29
43
|
scope: string[];
|
|
@@ -72,13 +86,13 @@ function buildAdvisoryStartupBriefNotes(analysis: ContextProposalAnalysis): stri
|
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
export function buildAdvisoryStartupBrief(args: {
|
|
75
|
-
proposal: Pick<ContextProposal, "goalText" | "mission" | "scope" | "constraints" | "acceptance">;
|
|
89
|
+
proposal: Pick<ContextProposal, "goalText" | "mission" | "scope" | "constraints" | "acceptance" | "source">;
|
|
76
90
|
analysis: ContextProposalAnalysis;
|
|
77
91
|
capturedAt?: string;
|
|
78
92
|
}): AdvisoryStartupBrief {
|
|
79
93
|
return {
|
|
80
94
|
kind: "startup_brief",
|
|
81
|
-
source: "recent_discussion",
|
|
95
|
+
source: args.proposal.source === "handoff_capsule" ? "primary_agent_handoff" : "recent_discussion",
|
|
82
96
|
confirmed: true,
|
|
83
97
|
captured_at: args.capturedAt ?? new Date().toISOString(),
|
|
84
98
|
goal_text: args.proposal.goalText,
|
|
@@ -27,7 +27,7 @@ export type ContextProposalAlternate = {
|
|
|
27
27
|
analysis: ContextProposalAnalysis;
|
|
28
28
|
goalText: string;
|
|
29
29
|
basisPreview: string;
|
|
30
|
-
source: "session" | "analyst";
|
|
30
|
+
source: "session" | "analyst" | "handoff_capsule";
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type ContextProposal = ContextProposalAlternate & {
|
|
@@ -41,6 +41,30 @@ export type RecentDiscussionEntry = {
|
|
|
41
41
|
text: string;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
export type RecentSessionMessage = RecentDiscussionEntry & {
|
|
45
|
+
messageId?: string;
|
|
46
|
+
timestampMs?: number;
|
|
47
|
+
isCommand: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CookHandoffCapsule = {
|
|
51
|
+
kind: "cook_handoff";
|
|
52
|
+
source: "primary_agent";
|
|
53
|
+
captured_at: string;
|
|
54
|
+
source_turn_id: string;
|
|
55
|
+
mission: string;
|
|
56
|
+
scope: string[];
|
|
57
|
+
constraints: string[];
|
|
58
|
+
non_goals: string[];
|
|
59
|
+
acceptance: string[];
|
|
60
|
+
risks: string[];
|
|
61
|
+
notes: string[];
|
|
62
|
+
handoff_kind: "implementation_workflow_ready";
|
|
63
|
+
task_type?: string;
|
|
64
|
+
evaluation_profile?: string;
|
|
65
|
+
why_cook_now?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
44
68
|
export type ContextProposalDecision = {
|
|
45
69
|
missionAnchor: string;
|
|
46
70
|
goalText: string;
|
|
@@ -113,6 +137,10 @@ function localAsStringArray(value: unknown): string[] {
|
|
|
113
137
|
: [];
|
|
114
138
|
}
|
|
115
139
|
|
|
140
|
+
function localAsNumber(value: unknown): number | undefined {
|
|
141
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
function localIsRecord(value: unknown): value is JsonRecord {
|
|
117
145
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
146
|
}
|
|
@@ -300,11 +328,12 @@ export function missionAnchorsLikelyEquivalent(left: string, right: string): boo
|
|
|
300
328
|
return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
|
|
301
329
|
}
|
|
302
330
|
|
|
303
|
-
export function
|
|
331
|
+
export function collectRecentSessionMessages(ctx: { sessionManager: any }, deps: {
|
|
304
332
|
isRecord: (value: unknown) => boolean;
|
|
305
333
|
asString?: (value: unknown) => string | undefined;
|
|
334
|
+
asNumber?: (value: unknown) => number | undefined;
|
|
306
335
|
isStaleContextError?: (error: unknown) => boolean;
|
|
307
|
-
}, limit =
|
|
336
|
+
}, limit = 24): RecentSessionMessage[] {
|
|
308
337
|
let branch: any[] = [];
|
|
309
338
|
try {
|
|
310
339
|
branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
@@ -313,27 +342,42 @@ export function collectRecentDiscussionEntries(ctx: { sessionManager: any }, dep
|
|
|
313
342
|
throw error;
|
|
314
343
|
}
|
|
315
344
|
const asStringValue = deps.asString ?? localAsString;
|
|
316
|
-
const
|
|
345
|
+
const asNumberValue = deps.asNumber ?? localAsNumber;
|
|
346
|
+
const entries: RecentSessionMessage[] = [];
|
|
317
347
|
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
318
348
|
const entry = branch[index];
|
|
319
349
|
if (!deps.isRecord(entry) || entry.type !== "message" || !deps.isRecord(entry.message)) continue;
|
|
320
350
|
const message = entry.message as JsonRecord;
|
|
321
|
-
let text = "";
|
|
322
|
-
let role: RecentDiscussionEntry["role"] | undefined;
|
|
323
351
|
const messageRole = asStringValue(message.role);
|
|
324
|
-
if (messageRole
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
if (!text || !role) continue;
|
|
352
|
+
if (messageRole !== "user" && messageRole !== "assistant" && messageRole !== "custom" && messageRole !== "summary") continue;
|
|
353
|
+
const text = extractTextFromMessageContent(message.content);
|
|
354
|
+
if (!text) continue;
|
|
329
355
|
const trimmed = text.trim();
|
|
330
|
-
if (!trimmed
|
|
331
|
-
entries.push({
|
|
356
|
+
if (!trimmed) continue;
|
|
357
|
+
entries.push({
|
|
358
|
+
role: messageRole as RecentDiscussionEntry["role"],
|
|
359
|
+
text: trimmed,
|
|
360
|
+
messageId: asStringValue((entry as JsonRecord).id),
|
|
361
|
+
timestampMs: asNumberValue(message.timestamp),
|
|
362
|
+
isCommand: /^\//.test(trimmed),
|
|
363
|
+
});
|
|
332
364
|
if (entries.length >= limit) break;
|
|
333
365
|
}
|
|
334
366
|
return entries;
|
|
335
367
|
}
|
|
336
368
|
|
|
369
|
+
export function collectRecentDiscussionEntries(ctx: { sessionManager: any }, deps: {
|
|
370
|
+
isRecord: (value: unknown) => boolean;
|
|
371
|
+
asString?: (value: unknown) => string | undefined;
|
|
372
|
+
asNumber?: (value: unknown) => number | undefined;
|
|
373
|
+
isStaleContextError?: (error: unknown) => boolean;
|
|
374
|
+
}, limit = 8): RecentDiscussionEntry[] {
|
|
375
|
+
return collectRecentSessionMessages(ctx, deps, Math.max(limit * 3, limit))
|
|
376
|
+
.filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
|
|
377
|
+
.slice(0, limit)
|
|
378
|
+
.map((entry) => ({ role: entry.role, text: entry.text }));
|
|
379
|
+
}
|
|
380
|
+
|
|
337
381
|
export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[]): string {
|
|
338
382
|
return entries
|
|
339
383
|
.slice()
|
|
@@ -1182,6 +1226,167 @@ export function extractContextProposalFromStructuredSession(
|
|
|
1182
1226
|
return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName, deps);
|
|
1183
1227
|
}
|
|
1184
1228
|
|
|
1229
|
+
const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*([\s\S]*?)```/giu;
|
|
1230
|
+
const COOK_HANDOFF_MAX_AGE_MS = 45 * 60 * 1000;
|
|
1231
|
+
const COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES = 2;
|
|
1232
|
+
const COOK_HANDOFF_NEGATIVE_MISSION_REGEX =
|
|
1233
|
+
/(?:\b(?:do not|don't|dont|not|never|avoid|skip|refuse|recognize that|suppress|ignore|block|prevent)\b|(?:不要|別|别|勿|禁止|避免|忽略|阻止))/iu;
|
|
1234
|
+
|
|
1235
|
+
function parseCookHandoffCapsulesFromText(
|
|
1236
|
+
text: string,
|
|
1237
|
+
messageId: string | undefined,
|
|
1238
|
+
timestampMs: number | undefined,
|
|
1239
|
+
deps: Pick<ProposalParseDeps, "asString" | "asStringArray">,
|
|
1240
|
+
): CookHandoffCapsule[] {
|
|
1241
|
+
const capsules: CookHandoffCapsule[] = [];
|
|
1242
|
+
for (const match of text.matchAll(COOK_HANDOFF_BLOCK_REGEX)) {
|
|
1243
|
+
const rawJson = deps.asString(match[1]);
|
|
1244
|
+
if (!rawJson) continue;
|
|
1245
|
+
let parsed: unknown;
|
|
1246
|
+
try {
|
|
1247
|
+
parsed = JSON.parse(rawJson);
|
|
1248
|
+
} catch {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (!localIsRecord(parsed)) continue;
|
|
1252
|
+
if (deps.asString(parsed.kind) !== "cook_handoff") continue;
|
|
1253
|
+
if (deps.asString(parsed.source) !== "primary_agent") continue;
|
|
1254
|
+
if (deps.asString(parsed.handoff_kind) !== "implementation_workflow_ready") continue;
|
|
1255
|
+
const mission = deps.asString(parsed.mission);
|
|
1256
|
+
if (!mission) continue;
|
|
1257
|
+
const scope = deps.asStringArray(parsed.scope);
|
|
1258
|
+
const constraints = deps.asStringArray(parsed.constraints);
|
|
1259
|
+
const nonGoals = deps.asStringArray(parsed.non_goals ?? parsed.nonGoals);
|
|
1260
|
+
const acceptance = deps.asStringArray(parsed.acceptance);
|
|
1261
|
+
const risks = deps.asStringArray(parsed.risks);
|
|
1262
|
+
const notes = deps.asStringArray(parsed.notes);
|
|
1263
|
+
const capturedAt = deps.asString(parsed.captured_at) ?? (timestampMs ? new Date(timestampMs).toISOString() : undefined);
|
|
1264
|
+
const sourceTurnId = deps.asString(parsed.source_turn_id) ?? messageId;
|
|
1265
|
+
if (!capturedAt || !sourceTurnId) continue;
|
|
1266
|
+
capsules.push({
|
|
1267
|
+
kind: "cook_handoff",
|
|
1268
|
+
source: "primary_agent",
|
|
1269
|
+
captured_at: capturedAt,
|
|
1270
|
+
source_turn_id: sourceTurnId,
|
|
1271
|
+
mission,
|
|
1272
|
+
scope,
|
|
1273
|
+
constraints,
|
|
1274
|
+
non_goals: nonGoals,
|
|
1275
|
+
acceptance,
|
|
1276
|
+
risks,
|
|
1277
|
+
notes,
|
|
1278
|
+
handoff_kind: "implementation_workflow_ready",
|
|
1279
|
+
task_type: deps.asString(parsed.task_type),
|
|
1280
|
+
evaluation_profile: deps.asString(parsed.evaluation_profile),
|
|
1281
|
+
why_cook_now: deps.asString(parsed.why_cook_now),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return capsules;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function buildCookHandoffBasisPreview(capsule: CookHandoffCapsule): string {
|
|
1288
|
+
const parts = [capsule.mission, ...capsule.scope, ...capsule.constraints, ...capsule.non_goals, ...capsule.acceptance];
|
|
1289
|
+
if (capsule.why_cook_now) parts.push(`why_cook_now: ${capsule.why_cook_now}`);
|
|
1290
|
+
return parts.join("\n").trim();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function isStartableCookHandoffCapsule(
|
|
1294
|
+
capsule: CookHandoffCapsule,
|
|
1295
|
+
deps: Pick<ProposalParseDeps, "normalizeMissionAnchorText" | "isWeakMissionAnchor">,
|
|
1296
|
+
): boolean {
|
|
1297
|
+
const mission = deps.normalizeMissionAnchorText(capsule.mission);
|
|
1298
|
+
if (!mission || deps.isWeakMissionAnchor(mission)) return false;
|
|
1299
|
+
if (COOK_HANDOFF_NEGATIVE_MISSION_REGEX.test(mission)) return false;
|
|
1300
|
+
if (capsule.scope.length === 0 || capsule.acceptance.length === 0) return false;
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function laterMessagesInvalidateCookHandoff(
|
|
1305
|
+
laterMessages: RecentSessionMessage[],
|
|
1306
|
+
deps: Pick<ProposalParseDeps, "stripCodeBlocks">,
|
|
1307
|
+
): boolean {
|
|
1308
|
+
const laterNonCommandMessages = laterMessages.filter((entry) => !entry.isCommand);
|
|
1309
|
+
if (laterNonCommandMessages.length > COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES) return true;
|
|
1310
|
+
return laterNonCommandMessages.some((entry) => {
|
|
1311
|
+
if (entry.role === "summary") return false;
|
|
1312
|
+
if (!hasRecentDiscussionImplementationIntent(entry.text, deps.stripCodeBlocks)) return false;
|
|
1313
|
+
return true;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function cookHandoffIsFreshEnough(capsule: CookHandoffCapsule, laterMessages: RecentSessionMessage[]): boolean {
|
|
1318
|
+
const capturedAtMs = Date.parse(capsule.captured_at);
|
|
1319
|
+
if (!Number.isFinite(capturedAtMs)) return false;
|
|
1320
|
+
const laterTimestamps = laterMessages.map((entry) => entry.timestampMs).filter((value): value is number => value !== undefined);
|
|
1321
|
+
if (laterTimestamps.length === 0) return true;
|
|
1322
|
+
return Math.max(...laterTimestamps) - capturedAtMs <= COOK_HANDOFF_MAX_AGE_MS;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function buildContextProposalFromCookHandoffCapsule(
|
|
1326
|
+
capsule: CookHandoffCapsule,
|
|
1327
|
+
projectName: string,
|
|
1328
|
+
deps: ProposalParseDeps,
|
|
1329
|
+
): ContextProposal | undefined {
|
|
1330
|
+
if (!isStartableCookHandoffCapsule(capsule, deps)) return undefined;
|
|
1331
|
+
const constraints = uniqueProposalItems([...capsule.constraints, ...capsule.non_goals]);
|
|
1332
|
+
const mission = deps.assessMissionAnchor(capsule.mission, projectName).derived;
|
|
1333
|
+
const goalText = buildContextProposalGoalText({
|
|
1334
|
+
mission,
|
|
1335
|
+
scope: capsule.scope,
|
|
1336
|
+
constraints,
|
|
1337
|
+
acceptance: capsule.acceptance,
|
|
1338
|
+
});
|
|
1339
|
+
const proposal: ContextProposal = {
|
|
1340
|
+
mission,
|
|
1341
|
+
scope: [...capsule.scope],
|
|
1342
|
+
constraints,
|
|
1343
|
+
acceptance: [...capsule.acceptance],
|
|
1344
|
+
analysis: finalizeContextProposalAnalysis(
|
|
1345
|
+
{
|
|
1346
|
+
taskType: capsule.task_type,
|
|
1347
|
+
evaluationProfile: capsule.evaluation_profile,
|
|
1348
|
+
critique: [
|
|
1349
|
+
...capsule.notes,
|
|
1350
|
+
...(capsule.why_cook_now ? [`Primary-agent /cook handoff rationale: ${capsule.why_cook_now}`] : []),
|
|
1351
|
+
],
|
|
1352
|
+
risks: capsule.risks,
|
|
1353
|
+
possibleNoise: [],
|
|
1354
|
+
alternateMissions: [],
|
|
1355
|
+
suppressedCompletedTopics: [],
|
|
1356
|
+
suppressedNegatedTopics: [],
|
|
1357
|
+
},
|
|
1358
|
+
[mission, goalText, capsule.mission, ...capsule.scope, ...constraints, ...capsule.acceptance],
|
|
1359
|
+
),
|
|
1360
|
+
goalText,
|
|
1361
|
+
basisPreview: buildCookHandoffBasisPreview(capsule),
|
|
1362
|
+
source: "handoff_capsule",
|
|
1363
|
+
alternateProposals: [],
|
|
1364
|
+
};
|
|
1365
|
+
return finalizeContextProposal(proposal, projectName, deps);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export function extractLatestCookHandoffProposal(
|
|
1369
|
+
recentMessages: RecentSessionMessage[],
|
|
1370
|
+
projectName: string,
|
|
1371
|
+
deps: ProposalParseDeps,
|
|
1372
|
+
): ContextProposal | undefined {
|
|
1373
|
+
for (let index = 0; index < recentMessages.length; index += 1) {
|
|
1374
|
+
const entry = recentMessages[index];
|
|
1375
|
+
if (entry.role !== "assistant" || entry.isCommand) continue;
|
|
1376
|
+
const capsules = parseCookHandoffCapsulesFromText(entry.text, entry.messageId, entry.timestampMs, deps);
|
|
1377
|
+
if (capsules.length === 0) continue;
|
|
1378
|
+
for (let capsuleIndex = capsules.length - 1; capsuleIndex >= 0; capsuleIndex -= 1) {
|
|
1379
|
+
const capsule = capsules[capsuleIndex];
|
|
1380
|
+
const laterMessages = recentMessages.slice(0, index);
|
|
1381
|
+
if (!cookHandoffIsFreshEnough(capsule, laterMessages)) continue;
|
|
1382
|
+
if (laterMessagesInvalidateCookHandoff(laterMessages, deps)) continue;
|
|
1383
|
+
const proposal = buildContextProposalFromCookHandoffCapsule(capsule, projectName, deps);
|
|
1384
|
+
if (proposal) return proposal;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return undefined;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1185
1390
|
export async function deriveCookContextProposalFromRecentDiscussion(
|
|
1186
1391
|
projectName: string,
|
|
1187
1392
|
recentEntries: RecentDiscussionEntry[],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -47,6 +47,49 @@ with session_path.open('w', encoding='utf-8') as fh:
|
|
|
47
47
|
PY
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
write_session_messages() {
|
|
51
|
+
local session_path="$1"
|
|
52
|
+
local cwd="$2"
|
|
53
|
+
local messages_json="$3"
|
|
54
|
+
python3 - "$session_path" "$cwd" "$messages_json" <<'PY'
|
|
55
|
+
import json
|
|
56
|
+
import sys
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
session_path = Path(sys.argv[1])
|
|
60
|
+
cwd = sys.argv[2]
|
|
61
|
+
messages = json.loads(sys.argv[3])
|
|
62
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
entries = [
|
|
64
|
+
{
|
|
65
|
+
"type": "session",
|
|
66
|
+
"version": 3,
|
|
67
|
+
"id": "11111111-1111-4111-8111-111111111111",
|
|
68
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
69
|
+
"cwd": cwd,
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
parent_id = None
|
|
73
|
+
for index, message in enumerate(messages, start=1):
|
|
74
|
+
entry_id = f"m{index:04d}"
|
|
75
|
+
entries.append({
|
|
76
|
+
"type": "message",
|
|
77
|
+
"id": entry_id,
|
|
78
|
+
"parentId": parent_id,
|
|
79
|
+
"timestamp": f"2026-01-01T00:00:{index:02d}.000Z",
|
|
80
|
+
"message": {
|
|
81
|
+
"role": message["role"],
|
|
82
|
+
"content": message["content"],
|
|
83
|
+
"timestamp": 1767225600000 + index * 1000,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
parent_id = entry_id
|
|
87
|
+
with session_path.open('w', encoding='utf-8') as fh:
|
|
88
|
+
for entry in entries:
|
|
89
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
90
|
+
PY
|
|
91
|
+
}
|
|
92
|
+
|
|
50
93
|
mark_done() {
|
|
51
94
|
python3 - <<'PY'
|
|
52
95
|
import json
|
|
@@ -1358,7 +1401,253 @@ assert 'Discuss changes in the main chat and rerun /cook.' in output, 'cancel co
|
|
|
1358
1401
|
assert not Path('.agent').exists(), 'cancel action should not write canonical workflow state'
|
|
1359
1402
|
PY
|
|
1360
1403
|
|
|
1404
|
+
# Explicit primary-agent handoff: /cook should prefer the structured handoff capsule over broad context re-inference.
|
|
1405
|
+
HANDOFF_ROOT_START="$TMPDIR/handoff-root-start"
|
|
1406
|
+
mkdir -p "$HANDOFF_ROOT_START"
|
|
1407
|
+
cd "$HANDOFF_ROOT_START"
|
|
1408
|
+
git init -q
|
|
1409
|
+
|
|
1410
|
+
HANDOFF_SESSION_START="$TMPDIR/handoff-session-start.jsonl"
|
|
1411
|
+
HANDOFF_SNAPSHOT_START="$TMPDIR/handoff-proposal-start.json"
|
|
1412
|
+
HANDOFF_MESSAGES_START="$(python3 - <<'PY'
|
|
1413
|
+
import json
|
|
1414
|
+
capsule = {
|
|
1415
|
+
"kind": "cook_handoff",
|
|
1416
|
+
"source": "primary_agent",
|
|
1417
|
+
"captured_at": "2026-01-01T00:00:02.000Z",
|
|
1418
|
+
"source_turn_id": "m0002",
|
|
1419
|
+
"mission": "Fix login redirect callback behavior.",
|
|
1420
|
+
"scope": [
|
|
1421
|
+
"Update the callback redirect decision logic.",
|
|
1422
|
+
"Preserve the broader auth flow."
|
|
1423
|
+
],
|
|
1424
|
+
"constraints": [
|
|
1425
|
+
"Do not refactor the broader auth flow."
|
|
1426
|
+
],
|
|
1427
|
+
"acceptance": [
|
|
1428
|
+
"Add a regression test for returning to the requested page."
|
|
1429
|
+
],
|
|
1430
|
+
"risks": [
|
|
1431
|
+
"Stale auth discussion could broaden the startup brief if the handoff is ignored."
|
|
1432
|
+
],
|
|
1433
|
+
"notes": [
|
|
1434
|
+
"Keep the startup brief aligned with the explicit primary-agent plan."
|
|
1435
|
+
],
|
|
1436
|
+
"handoff_kind": "implementation_workflow_ready",
|
|
1437
|
+
"task_type": "completion-workflow",
|
|
1438
|
+
"evaluation_profile": "completion-rubric-v1",
|
|
1439
|
+
"why_cook_now": "The implementation plan is concrete and ready for repo changes."
|
|
1440
|
+
}
|
|
1441
|
+
messages = [
|
|
1442
|
+
{"role": "user", "content": "Please think through the login redirect fix and tell me when it is ready for /cook."},
|
|
1443
|
+
{"role": "assistant", "content": "This task is now ready for /cook. Run /cook to confirm the startup brief.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
|
|
1444
|
+
]
|
|
1445
|
+
print(json.dumps(messages, ensure_ascii=False))
|
|
1446
|
+
PY
|
|
1447
|
+
)"
|
|
1448
|
+
write_session_messages "$HANDOFF_SESSION_START" "$HANDOFF_ROOT_START" "$HANDOFF_MESSAGES_START"
|
|
1449
|
+
|
|
1450
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
1451
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_START" \
|
|
1452
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1453
|
+
pi --session "$HANDOFF_SESSION_START" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-start.out" 2>"$TMPDIR/pi-completion-handoff-start.err"
|
|
1454
|
+
|
|
1455
|
+
python3 - "$HANDOFF_SNAPSHOT_START" <<'PY'
|
|
1456
|
+
import json
|
|
1457
|
+
import sys
|
|
1458
|
+
from pathlib import Path
|
|
1459
|
+
|
|
1460
|
+
snapshot = json.loads(Path(sys.argv[1]).read_text())
|
|
1461
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
1462
|
+
|
|
1463
|
+
assert snapshot['source'] == 'handoff_capsule', 'explicit handoff startup should snapshot the handoff capsule as the proposal source'
|
|
1464
|
+
assert snapshot['mission'] == 'Fix login redirect callback behavior.', 'explicit handoff startup should preserve the primary-agent mission'
|
|
1465
|
+
assert state['mission_anchor'] == 'Fix login redirect callback behavior.', 'explicit handoff startup should use the handoff mission as canonical mission_anchor'
|
|
1466
|
+
assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'explicit handoff startup should preserve the advisory intake source'
|
|
1467
|
+
assert state['advisory_startup_brief']['risks'] == ['Stale auth discussion could broaden the startup brief if the handoff is ignored.'], 'explicit handoff startup should preserve handoff risks'
|
|
1468
|
+
assert 'Primary-agent /cook handoff rationale: The implementation plan is concrete and ready for repo changes.' in state['advisory_startup_brief']['notes'], 'explicit handoff startup should preserve why_cook_now as notes'
|
|
1469
|
+
PY
|
|
1470
|
+
|
|
1471
|
+
# Done workflow + fresh handoff: the fresh explicit handoff should override done-state suppression and start the new round.
|
|
1472
|
+
HANDOFF_ROOT_DONE="$TMPDIR/handoff-root-done"
|
|
1473
|
+
mkdir -p "$HANDOFF_ROOT_DONE"
|
|
1474
|
+
cd "$HANDOFF_ROOT_DONE"
|
|
1475
|
+
git init -q
|
|
1476
|
+
|
|
1477
|
+
DONE_SEED_SESSION="$TMPDIR/handoff-done-seed-session.jsonl"
|
|
1478
|
+
DONE_SEED_DISCUSSION=$'Mission: Seed a finished workflow before testing fresh handoff priority.\nScope:\n- Create canonical workflow state.\nConstraints:\n- Keep the seed minimal.\nAcceptance:\n- Mark the workflow done before the next test step.'
|
|
1479
|
+
write_session "$DONE_SEED_SESSION" "$HANDOFF_ROOT_DONE" "$DONE_SEED_DISCUSSION"
|
|
1480
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
1481
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
1482
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1483
|
+
pi --session "$DONE_SEED_SESSION" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-done-seed.out" 2>"$TMPDIR/pi-completion-handoff-done-seed.err"
|
|
1484
|
+
mark_done
|
|
1485
|
+
|
|
1486
|
+
HANDOFF_SESSION_DONE="$TMPDIR/handoff-session-done.jsonl"
|
|
1487
|
+
HANDOFF_SNAPSHOT_DONE="$TMPDIR/handoff-proposal-done.json"
|
|
1488
|
+
HANDOFF_MESSAGES_DONE="$(python3 - <<'PY'
|
|
1489
|
+
import json
|
|
1490
|
+
capsule = {
|
|
1491
|
+
"kind": "cook_handoff",
|
|
1492
|
+
"source": "primary_agent",
|
|
1493
|
+
"captured_at": "2026-01-01T00:00:02.000Z",
|
|
1494
|
+
"source_turn_id": "m0002",
|
|
1495
|
+
"mission": "Reopen the login redirect work for the callback edge case.",
|
|
1496
|
+
"scope": [
|
|
1497
|
+
"Handle the callback edge case in the redirect logic.",
|
|
1498
|
+
"Keep the finished workflow as historical context only."
|
|
1499
|
+
],
|
|
1500
|
+
"constraints": [
|
|
1501
|
+
"Do not turn done-state suppression into the startup mission."
|
|
1502
|
+
],
|
|
1503
|
+
"acceptance": [
|
|
1504
|
+
"Add a regression test for the callback edge case."
|
|
1505
|
+
],
|
|
1506
|
+
"risks": [
|
|
1507
|
+
"Done-state context could override the new mission if the handoff is ignored."
|
|
1508
|
+
],
|
|
1509
|
+
"notes": [
|
|
1510
|
+
"This is a fresh implementation round, not a summary of the finished workflow."
|
|
1511
|
+
],
|
|
1512
|
+
"handoff_kind": "implementation_workflow_ready",
|
|
1513
|
+
"task_type": "completion-workflow",
|
|
1514
|
+
"evaluation_profile": "completion-rubric-v1",
|
|
1515
|
+
"why_cook_now": "A new implementation-ready edge case was identified after the previous round closed."
|
|
1516
|
+
}
|
|
1517
|
+
messages = [
|
|
1518
|
+
{"role": "user", "content": "The previous round is done, but there is a fresh callback edge case to implement."},
|
|
1519
|
+
{"role": "assistant", "content": "The next round is ready for /cook. Run /cook to confirm this fresh implementation mission.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
|
|
1520
|
+
]
|
|
1521
|
+
print(json.dumps(messages, ensure_ascii=False))
|
|
1522
|
+
PY
|
|
1523
|
+
)"
|
|
1524
|
+
write_session_messages "$HANDOFF_SESSION_DONE" "$HANDOFF_ROOT_DONE" "$HANDOFF_MESSAGES_DONE"
|
|
1525
|
+
|
|
1526
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
1527
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_DONE" \
|
|
1528
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1529
|
+
pi --session "$HANDOFF_SESSION_DONE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-done.out" 2>"$TMPDIR/pi-completion-handoff-done.err"
|
|
1530
|
+
|
|
1531
|
+
python3 - "$HANDOFF_SNAPSHOT_DONE" <<'PY'
|
|
1532
|
+
import json
|
|
1533
|
+
import sys
|
|
1534
|
+
from pathlib import Path
|
|
1535
|
+
|
|
1536
|
+
snapshot = json.loads(Path(sys.argv[1]).read_text())
|
|
1537
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
1538
|
+
|
|
1539
|
+
assert snapshot['source'] == 'handoff_capsule', 'done-workflow handoff should still use the explicit handoff capsule'
|
|
1540
|
+
assert snapshot['mission'] == 'Reopen the login redirect work for the callback edge case.', 'done-workflow handoff should preserve the fresh mission'
|
|
1541
|
+
assert state['mission_anchor'] == 'Reopen the login redirect work for the callback edge case.', 'done-workflow handoff should override done-state suppression with the fresh mission'
|
|
1542
|
+
assert state['continuation_policy'] == 'continue', 'done-workflow handoff should reopen canonical workflow state for the new round'
|
|
1543
|
+
assert state['advisory_startup_brief']['source'] == 'primary_agent_handoff', 'done-workflow handoff should preserve the handoff advisory source'
|
|
1544
|
+
PY
|
|
1545
|
+
|
|
1546
|
+
# Stale handoff: later discussion should invalidate the older handoff capsule and fall back to the newer discussion mission.
|
|
1547
|
+
HANDOFF_ROOT_STALE="$TMPDIR/handoff-root-stale"
|
|
1548
|
+
mkdir -p "$HANDOFF_ROOT_STALE"
|
|
1549
|
+
cd "$HANDOFF_ROOT_STALE"
|
|
1550
|
+
git init -q
|
|
1551
|
+
|
|
1552
|
+
HANDOFF_SESSION_STALE="$TMPDIR/handoff-session-stale.jsonl"
|
|
1553
|
+
HANDOFF_SNAPSHOT_STALE="$TMPDIR/handoff-proposal-stale.json"
|
|
1554
|
+
HANDOFF_MESSAGES_STALE="$(python3 - <<'PY'
|
|
1555
|
+
import json
|
|
1556
|
+
capsule = {
|
|
1557
|
+
"kind": "cook_handoff",
|
|
1558
|
+
"source": "primary_agent",
|
|
1559
|
+
"captured_at": "2026-01-01T00:00:02.000Z",
|
|
1560
|
+
"source_turn_id": "m0002",
|
|
1561
|
+
"mission": "Fix the original login redirect callback behavior.",
|
|
1562
|
+
"scope": ["Update the original callback redirect logic."],
|
|
1563
|
+
"constraints": ["Do not refactor the auth stack."],
|
|
1564
|
+
"acceptance": ["Add the original callback regression test."],
|
|
1565
|
+
"risks": [],
|
|
1566
|
+
"notes": [],
|
|
1567
|
+
"handoff_kind": "implementation_workflow_ready"
|
|
1568
|
+
}
|
|
1569
|
+
newer_discussion = "Mission: Ship logout redirect consistency instead.\nScope:\n- Update the logout redirect path.\nConstraints:\n- Leave the login callback flow unchanged.\nAcceptance:\n- Add a logout redirect regression test."
|
|
1570
|
+
messages = [
|
|
1571
|
+
{"role": "user", "content": "Please plan the login redirect follow-up."},
|
|
1572
|
+
{"role": "assistant", "content": "Run /cook if you want to start the original follow-up.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
|
|
1573
|
+
{"role": "user", "content": newer_discussion},
|
|
1574
|
+
]
|
|
1575
|
+
print(json.dumps(messages, ensure_ascii=False))
|
|
1576
|
+
PY
|
|
1577
|
+
)"
|
|
1578
|
+
write_session_messages "$HANDOFF_SESSION_STALE" "$HANDOFF_ROOT_STALE" "$HANDOFF_MESSAGES_STALE"
|
|
1579
|
+
|
|
1580
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
1581
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
1582
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_STALE" \
|
|
1583
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1584
|
+
pi --session "$HANDOFF_SESSION_STALE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-stale.out" 2>"$TMPDIR/pi-completion-handoff-stale.err"
|
|
1585
|
+
|
|
1586
|
+
python3 - "$HANDOFF_SNAPSHOT_STALE" <<'PY'
|
|
1587
|
+
import json
|
|
1588
|
+
import sys
|
|
1589
|
+
from pathlib import Path
|
|
1590
|
+
|
|
1591
|
+
snapshot = json.loads(Path(sys.argv[1]).read_text())
|
|
1592
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
1593
|
+
|
|
1594
|
+
assert snapshot['source'] == 'session', 'stale handoff should fall back to the newer user discussion proposal'
|
|
1595
|
+
assert snapshot['mission'] == 'Ship logout redirect consistency instead.', 'stale handoff fallback should prefer the newer discussion mission'
|
|
1596
|
+
assert state['mission_anchor'] == 'Ship logout redirect consistency instead.', 'stale handoff fallback should not keep the older handoff mission'
|
|
1597
|
+
assert state['advisory_startup_brief']['source'] == 'recent_discussion', 'stale handoff fallback should preserve that the accepted startup came from discussion'
|
|
1598
|
+
PY
|
|
1599
|
+
|
|
1600
|
+
# Negative handoff rationale: a non-startable capsule must not become the startup mission.
|
|
1601
|
+
HANDOFF_ROOT_NEGATIVE="$TMPDIR/handoff-root-negative"
|
|
1602
|
+
mkdir -p "$HANDOFF_ROOT_NEGATIVE"
|
|
1603
|
+
cd "$HANDOFF_ROOT_NEGATIVE"
|
|
1604
|
+
git init -q
|
|
1605
|
+
|
|
1606
|
+
HANDOFF_SESSION_NEGATIVE="$TMPDIR/handoff-session-negative.jsonl"
|
|
1607
|
+
HANDOFF_SNAPSHOT_NEGATIVE="$TMPDIR/handoff-proposal-negative.json"
|
|
1608
|
+
HANDOFF_MESSAGES_NEGATIVE="$(python3 - <<'PY'
|
|
1609
|
+
import json
|
|
1610
|
+
capsule = {
|
|
1611
|
+
"kind": "cook_handoff",
|
|
1612
|
+
"source": "primary_agent",
|
|
1613
|
+
"captured_at": "2026-01-01T00:00:02.000Z",
|
|
1614
|
+
"source_turn_id": "m0002",
|
|
1615
|
+
"mission": "Do not reopen implementation for the finished workflow.",
|
|
1616
|
+
"scope": ["Keep the old workflow closed."],
|
|
1617
|
+
"constraints": ["Do not start repo changes."],
|
|
1618
|
+
"acceptance": ["Explain that the finished workflow should stay closed."],
|
|
1619
|
+
"risks": [],
|
|
1620
|
+
"notes": [],
|
|
1621
|
+
"handoff_kind": "implementation_workflow_ready"
|
|
1622
|
+
}
|
|
1623
|
+
messages = [
|
|
1624
|
+
{"role": "user", "content": "Should we reopen the finished workflow?"},
|
|
1625
|
+
{"role": "assistant", "content": "Do not reopen it directly.\n\n```cook_handoff\n" + json.dumps(capsule, ensure_ascii=False, indent=2) + "\n```"},
|
|
1626
|
+
]
|
|
1627
|
+
print(json.dumps(messages, ensure_ascii=False))
|
|
1628
|
+
PY
|
|
1629
|
+
)"
|
|
1630
|
+
write_session_messages "$HANDOFF_SESSION_NEGATIVE" "$HANDOFF_ROOT_NEGATIVE" "$HANDOFF_MESSAGES_NEGATIVE"
|
|
1631
|
+
|
|
1632
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
1633
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$HANDOFF_SNAPSHOT_NEGATIVE" \
|
|
1634
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1635
|
+
pi --session "$HANDOFF_SESSION_NEGATIVE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-handoff-negative.out" 2>"$TMPDIR/pi-completion-handoff-negative.err"
|
|
1636
|
+
|
|
1637
|
+
python3 - "$HANDOFF_SNAPSHOT_NEGATIVE" "$TMPDIR/pi-completion-handoff-negative.out" "$TMPDIR/pi-completion-handoff-negative.err" <<'PY'
|
|
1638
|
+
import sys
|
|
1639
|
+
from pathlib import Path
|
|
1640
|
+
|
|
1641
|
+
snapshot = Path(sys.argv[1])
|
|
1642
|
+
output = Path(sys.argv[2]).read_text() + Path(sys.argv[3]).read_text()
|
|
1643
|
+
|
|
1644
|
+
assert not snapshot.exists(), 'negative handoff rationale should not emit a startup proposal snapshot'
|
|
1645
|
+
assert not Path('.agent').exists(), 'negative handoff rationale should fail closed without writing canonical state'
|
|
1646
|
+
assert '/cook failed closed' in output, 'negative handoff rationale should fail closed instead of becoming the startup mission'
|
|
1647
|
+
PY
|
|
1648
|
+
|
|
1361
1649
|
grep -q 'export async function deriveCookContextProposalFromRecentDiscussion' "$PKG_ROOT/extensions/completion/proposal.ts"
|
|
1650
|
+
grep -q 'export function extractLatestCookHandoffProposal' "$PKG_ROOT/extensions/completion/proposal.ts"
|
|
1362
1651
|
grep -q 'export function parseContextProposalAnalystOutput' "$PKG_ROOT/extensions/completion/proposal.ts"
|
|
1363
1652
|
grep -q 'export function buildContextProposalConfirmationLayout' "$PKG_ROOT/extensions/completion/prompt-surfaces.ts"
|
|
1364
1653
|
grep -q 'export function buildEvaluationRoleContextLines' "$PKG_ROOT/extensions/completion/prompt-surfaces.ts"
|
package/scripts/release-check.sh
CHANGED
|
@@ -17,17 +17,21 @@ checks = {
|
|
|
17
17
|
"README.md": [
|
|
18
18
|
"`/cook` is the explicit workflow boundary for starting, continuing, refocusing, or beginning the next round of long-running repo work.",
|
|
19
19
|
"Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.",
|
|
20
|
-
"`/cook`
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"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.",
|
|
21
|
+
"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.",
|
|
22
|
+
"`/cook` first looks for a fresh explicit primary-agent handoff capsule.",
|
|
23
|
+
"The pre-`/cook` handoff capsule itself is not canonical workflow state. It is only startup intake for `/cook`.",
|
|
23
24
|
],
|
|
24
25
|
"CHANGELOG.md": [
|
|
25
|
-
"made `/cook`
|
|
26
|
-
"
|
|
26
|
+
"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",
|
|
27
|
+
"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",
|
|
28
|
+
"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",
|
|
27
29
|
],
|
|
28
|
-
"extensions/completion/
|
|
29
|
-
'
|
|
30
|
-
'"
|
|
30
|
+
"extensions/completion/prompt-surfaces.ts": [
|
|
31
|
+
'"/cook is the only explicit entrypoint into long-running completion workflow."',
|
|
32
|
+
'"When you judge that the task has matured into completion-workflow scope',
|
|
33
|
+
'"Also append one exact fenced block in the same assistant reply using ```cook_handoff ... ``` JSON',
|
|
34
|
+
'"The capsule is startup intake for /cook only: do not present it as canonical .agent state',
|
|
31
35
|
],
|
|
32
36
|
}
|
|
33
37
|
|
package/scripts/smoke-test.sh
CHANGED
|
@@ -51,6 +51,7 @@ ROOT="$TMPDIR/repo"
|
|
|
51
51
|
KICKOFF_PROMPT="$TMPDIR/kickoff-prompt.txt"
|
|
52
52
|
RESUME_PROMPT="$TMPDIR/resume-prompt.txt"
|
|
53
53
|
ORDINARY_SYSTEM_REMINDER="$TMPDIR/ordinary-system-reminder.txt"
|
|
54
|
+
ORDINARY_HANDOFF_REMINDER="$TMPDIR/ordinary-handoff-reminder.txt"
|
|
54
55
|
UNCLEAR_ROUTING_SNAPSHOT="$TMPDIR/active-unclear-routing.json"
|
|
55
56
|
UNCLEAR_CHOOSER_SNAPSHOT="$TMPDIR/unexpected-existing-workflow-chooser.json"
|
|
56
57
|
ORDINARY_AUTO_RESUME_PROMPT="$TMPDIR/ordinary-auto-resume-prompt.txt"
|
|
@@ -146,23 +147,31 @@ assert f'- task_type: {expected_task_type}' in kickoff, 'kickoff prompt missing
|
|
|
146
147
|
assert f'- evaluation_profile: {expected_eval_profile}' in kickoff, 'kickoff prompt missing canonical evaluation_profile'
|
|
147
148
|
PY
|
|
148
149
|
|
|
149
|
-
rm -f "$ORDINARY_SYSTEM_REMINDER" "$ORDINARY_AUTO_RESUME_PROMPT"
|
|
150
|
+
rm -f "$ORDINARY_SYSTEM_REMINDER" "$ORDINARY_HANDOFF_REMINDER" "$ORDINARY_AUTO_RESUME_PROMPT"
|
|
150
151
|
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
151
152
|
PI_COMPLETION_TEST_SYSTEM_REMINDER_PATH="$ORDINARY_SYSTEM_REMINDER" \
|
|
153
|
+
PI_COMPLETION_TEST_COOK_HANDOFF_REMINDER_PATH="$ORDINARY_HANDOFF_REMINDER" \
|
|
152
154
|
PI_COMPLETION_TEST_AUTO_CONTINUE_ON_SESSION_START=1 \
|
|
153
155
|
PI_COMPLETION_TEST_AUTO_CONTINUE_PROMPT_PATH="$ORDINARY_AUTO_RESUME_PROMPT" \
|
|
154
156
|
pi -e "$PKG_ROOT" -p "Summarize the repo briefly." \
|
|
155
157
|
>"$TMPDIR/pi-completion-smoke-ordinary.out" 2>"$TMPDIR/pi-completion-smoke-ordinary.err"
|
|
156
158
|
|
|
157
|
-
python3 - "$TMPDIR/pi-completion-smoke-ordinary.out" "$TMPDIR/pi-completion-smoke-ordinary.err" "$ORDINARY_SYSTEM_REMINDER" "$ORDINARY_AUTO_RESUME_PROMPT" <<'PY'
|
|
159
|
+
python3 - "$TMPDIR/pi-completion-smoke-ordinary.out" "$TMPDIR/pi-completion-smoke-ordinary.err" "$ORDINARY_SYSTEM_REMINDER" "$ORDINARY_HANDOFF_REMINDER" "$ORDINARY_AUTO_RESUME_PROMPT" <<'PY'
|
|
158
160
|
import sys
|
|
159
161
|
from pathlib import Path
|
|
160
162
|
|
|
161
163
|
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
162
164
|
reminder = Path(sys.argv[3])
|
|
163
|
-
|
|
165
|
+
handoff = Path(sys.argv[4])
|
|
166
|
+
auto_resume = Path(sys.argv[5])
|
|
164
167
|
|
|
165
168
|
assert not reminder.exists(), 'ordinary non-/cook turn should not inject completion reminder solely from canonical state'
|
|
169
|
+
assert handoff.exists(), 'ordinary non-/cook turn should inject the /cook handoff boundary reminder'
|
|
170
|
+
handoff_text = handoff.read_text()
|
|
171
|
+
assert '/cook is the only explicit entrypoint into long-running completion workflow.' in handoff_text, 'ordinary handoff reminder should preserve the explicit /cook workflow boundary'
|
|
172
|
+
assert 'stop short of long-running implementation and tell the user to run /cook.' in handoff_text, 'ordinary handoff reminder should require primary-agent handoff before implementation'
|
|
173
|
+
assert '```cook_handoff ... ``` JSON' in handoff_text, 'ordinary handoff reminder should require the explicit structured /cook handoff capsule'
|
|
174
|
+
assert 'The capsule is startup intake for /cook only' in handoff_text, 'ordinary handoff reminder should keep the capsule non-canonical'
|
|
166
175
|
assert not auto_resume.exists(), 'ordinary non-/cook turn should not queue auto-resume before /cook activation'
|
|
167
176
|
assert 'Skipped completion workflow auto-resume prompt (test mode)' not in output, 'ordinary non-/cook turn should not attempt auto-resume'
|
|
168
177
|
PY
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cook-handoff-boundary
|
|
3
|
+
description: Ordinary-chat boundary contract for deciding when a task has matured enough that the primary agent must stop short of long-running implementation and hand the user off to `/cook`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /cook Handoff Boundary
|
|
7
|
+
|
|
8
|
+
Load or summarize this contract when the primary agent is operating in ordinary main chat before the user has explicitly entered `/cook`.
|
|
9
|
+
|
|
10
|
+
This skill governs the boundary between:
|
|
11
|
+
|
|
12
|
+
- ordinary main-chat discussion, clarification, and proposal work
|
|
13
|
+
- explicit transition into long-running completion workflow through `/cook`
|
|
14
|
+
|
|
15
|
+
## Core Contract
|
|
16
|
+
|
|
17
|
+
- Ordinary chat may be used to clarify requirements, discuss tradeoffs, and propose implementation approaches.
|
|
18
|
+
- `/cook` is the only explicit entrypoint into long-running completion workflow.
|
|
19
|
+
- When the primary agent judges that a task has matured into completion-workflow scope, it must stop short of implementation and direct the user to `/cook`.
|
|
20
|
+
|
|
21
|
+
## When To Hand Off To `/cook`
|
|
22
|
+
|
|
23
|
+
The primary agent should consider `/cook` handoff appropriate when one or more of the following are true:
|
|
24
|
+
|
|
25
|
+
- the user has clearly shifted from exploration into implementation intent
|
|
26
|
+
- the agent has just produced a concrete plan or proposal whose natural next step would be implementation
|
|
27
|
+
- the task spans multiple files, steps, or verification surfaces
|
|
28
|
+
- the task needs resumability, review, audit, or canonical workflow state
|
|
29
|
+
- the task is better treated as a long-running repo mission than a one-off answer or tiny fix
|
|
30
|
+
|
|
31
|
+
## Required Handoff Behavior
|
|
32
|
+
|
|
33
|
+
When the task is judged ready for completion workflow, the primary agent must:
|
|
34
|
+
|
|
35
|
+
- stop before long-running implementation
|
|
36
|
+
- not edit tracked product files in ordinary chat for that workflow-level task
|
|
37
|
+
- tell the user to run `/cook`
|
|
38
|
+
- explain that `/cook` will first look for a fresh explicit primary-agent handoff and otherwise fall back to recent discussion before asking for confirmation
|
|
39
|
+
- append one exact structured `/cook` handoff capsule in the same assistant reply
|
|
40
|
+
|
|
41
|
+
Required capsule format:
|
|
42
|
+
|
|
43
|
+
````text
|
|
44
|
+
```cook_handoff
|
|
45
|
+
{
|
|
46
|
+
"kind": "cook_handoff",
|
|
47
|
+
"source": "primary_agent",
|
|
48
|
+
"captured_at": "<ISO-8601 timestamp>",
|
|
49
|
+
"source_turn_id": "<current assistant turn id>",
|
|
50
|
+
"mission": "<startable implementation mission>",
|
|
51
|
+
"scope": ["..."],
|
|
52
|
+
"constraints": ["..."],
|
|
53
|
+
"acceptance": ["..."],
|
|
54
|
+
"risks": ["..."],
|
|
55
|
+
"notes": ["..."],
|
|
56
|
+
"handoff_kind": "implementation_workflow_ready",
|
|
57
|
+
"task_type": "completion-workflow",
|
|
58
|
+
"evaluation_profile": "completion-rubric-v1",
|
|
59
|
+
"why_cook_now": "<why the task is ready for /cook now>"
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
````
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
|
|
66
|
+
- `constraints` may be replaced or supplemented by `non_goals` when clearer.
|
|
67
|
+
- The mission must be positively startable implementation work; do not use rejection or suppression text as the mission.
|
|
68
|
+
- The capsule is startup intake for `/cook` only. It is not canonical `.agent/**` state, not active-slice state, and not a second repo contract source.
|
|
69
|
+
|
|
70
|
+
Suggested wording:
|
|
71
|
+
|
|
72
|
+
> This task is now mature enough for the `/cook` workflow. If you want me to start implementation, run `/cook`. I’ve also attached an explicit `/cook` handoff capsule so `/cook` can confirm this plan directly before the workflow begins.
|
|
73
|
+
|
|
74
|
+
A short recap may include mission, scope, or acceptance, but that recap must not be presented as canonical plan state.
|
|
75
|
+
|
|
76
|
+
## Forbidden Behaviors
|
|
77
|
+
|
|
78
|
+
Once the task is judged ready for completion workflow, the primary agent must not:
|
|
79
|
+
|
|
80
|
+
- directly begin long-running implementation in ordinary chat
|
|
81
|
+
- modify tracked product files as part of that workflow-level task
|
|
82
|
+
- act as though `/cook` had already been invoked
|
|
83
|
+
- silently rewrite ordinary-chat discussion into active workflow state
|
|
84
|
+
|
|
85
|
+
## Relationship To `completion-protocol`
|
|
86
|
+
|
|
87
|
+
This skill is only about pre-`/cook` ordinary-chat handoff behavior.
|
|
88
|
+
|
|
89
|
+
After the user explicitly enters `/cook`, the separate `completion-protocol` skill governs:
|
|
90
|
+
|
|
91
|
+
- canonical `.agent/**` workflow state
|
|
92
|
+
- workflow-driver behavior
|
|
93
|
+
- mandatory completion-role dispatch
|
|
94
|
+
- review, audit, and stop-wave rules
|