@linimin/pi-letscook 0.1.62 → 0.1.64
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/README.md +18 -18
- package/extensions/completion/driver.ts +27 -46
- package/extensions/completion/index.ts +51 -69
- package/extensions/completion/prompt-surfaces.ts +2 -1
- package/extensions/completion/role-runner.ts +105 -0
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +141 -42
- package/scripts/refocus-test.sh +81 -4
- package/scripts/release-check.sh +15 -20
- package/scripts/smoke-test.sh +3 -3
- package/skills/cook-handoff-boundary/SKILL.md +22 -22
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ Then run `/reload` in Pi.
|
|
|
34
34
|
2. Run `/reload` in Pi.
|
|
35
35
|
3. In the main chat, either implement directly with the agent or refine the concrete repo change you want.
|
|
36
36
|
4. When you want workflow mode, run `/cook`.
|
|
37
|
-
5. Review the
|
|
38
|
-
6. Later, run `/cook` again to resume from canonical state or confirm a
|
|
37
|
+
5. Review the startup brief and choose **Start** or **Cancel**.
|
|
38
|
+
6. Later, run `/cook` again to resume from canonical state or confirm a replacement or next-round handoff.
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
41
|
/cook
|
|
@@ -46,20 +46,20 @@ Then run `/reload` in Pi.
|
|
|
46
46
|
| If you want to... | Do this |
|
|
47
47
|
|---|---|
|
|
48
48
|
| Implement directly without workflow | Ask in ordinary chat and let the agent modify the repo directly |
|
|
49
|
-
| Start a tracked workflow | Discuss the concrete repo change in
|
|
49
|
+
| Start a tracked workflow | Discuss the concrete repo change in ordinary chat, then run `/cook` when you want workflow mode |
|
|
50
50
|
| Continue the current workflow | Run `/cook` |
|
|
51
|
-
| Refocus or start the next round | Discuss the new concrete repo change in
|
|
51
|
+
| Refocus or start the next round | Discuss the new concrete repo change in ordinary chat, then run `/cook` when you want workflow mode |
|
|
52
52
|
|
|
53
53
|
## What `/cook` expects
|
|
54
54
|
|
|
55
|
-
- enough
|
|
56
|
-
- a mission
|
|
55
|
+
- enough current task context for a primary-agent handoff synthesis step to produce a concrete workflow startup handoff
|
|
56
|
+
- a mission and first slice concrete enough for the primary-agent handoff step to author a truthful implementation-startable handoff
|
|
57
57
|
- acceptance and verification intent that can support a truthful first workflow round
|
|
58
58
|
- README/CHANGELOG updates still count as concrete repo changes
|
|
59
|
-
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless
|
|
60
|
-
-
|
|
59
|
+
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless the primary-agent handoff step turns them into a concrete `cook_handoff` capsule
|
|
60
|
+
- `/cook` first prefers a fresh explicit `cook_handoff` capsule when one already exists, but otherwise calls the primary-agent handoff synthesis step in the same `/cook` entry
|
|
61
61
|
|
|
62
|
-
If
|
|
62
|
+
If the primary-agent handoff step still cannot prepare a concrete handoff, `/cook` fails closed, leaves canonical `.agent/**` state unchanged, and tells you to refine the mission, first slice, or verification intent in the main chat before rerunning `/cook`.
|
|
63
63
|
|
|
64
64
|
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.
|
|
65
65
|
|
|
@@ -69,16 +69,16 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
|
|
|
69
69
|
|
|
70
70
|
Only explicit `/cook` enters workflow mode. Ordinary prompts stay in the main chat and go straight to the primary agent.
|
|
71
71
|
|
|
72
|
-
Ordinary chat can still directly implement repo changes. `/cook` is for the cases where you want workflow control rather than just implementation help.
|
|
72
|
+
Ordinary chat can still directly implement repo changes. `/cook` is for the cases where you want workflow control rather than just implementation help, and the primary agent should prepare the handoff before you run it.
|
|
73
73
|
|
|
74
|
-
When you explicitly run `/cook`, it
|
|
74
|
+
When you explicitly run `/cook`, it first checks for a fresh explicit primary-agent handoff. If one is missing, it calls a same-entry primary-agent handoff synthesis step from the current task context, then asks you to **Start** or **Cancel** before rewriting canonical workflow state.
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Explicit `/cook` capsules are still valid startup intake, but they are no longer the only path because `/cook` can synthesize the primary-agent handoff in the same entry when needed.
|
|
77
77
|
|
|
78
78
|
Important behavior:
|
|
79
79
|
- `/cook` is an optional workflow boundary and manual entry point
|
|
80
|
-
- startup and next-round entry stay confirm-first,
|
|
81
|
-
- active workflows resume from canonical `.agent/**` state unless
|
|
80
|
+
- startup and next-round entry stay confirm-first, using explicit primary-agent handoff data when present and otherwise running the primary-agent handoff synthesis step in the same `/cook` entry
|
|
81
|
+
- active workflows resume from canonical `.agent/**` state unless a concrete replacement handoff is available or synthesized in the same `/cook` entry
|
|
82
82
|
- explicit slash commands other than `/cook` continue normally in the main chat
|
|
83
83
|
- ordinary main-chat discussion may clarify, propose, or directly implement repo changes without entering workflow mode
|
|
84
84
|
|
|
@@ -94,13 +94,13 @@ I want to add login redirect handling and tests.
|
|
|
94
94
|
|
|
95
95
|
## What happens when you run `/cook`
|
|
96
96
|
|
|
97
|
-
`/cook` first checks for a fresh explicit primary-agent handoff capsule
|
|
97
|
+
`/cook` first checks for a fresh explicit primary-agent handoff capsule. New-workflow entry and done-workflow next-round entry use that handoff when it already exists; otherwise `/cook` calls a same-entry primary-agent handoff synthesis step, then immediately continues to Start / Cancel if the generated handoff is concrete enough. Active workflows still resume canonical state by default unless a concrete replacement handoff is available or synthesized in the same `/cook` entry. None of this prevents ordinary-chat implementation when you choose not to enter workflow mode.
|
|
98
98
|
|
|
99
99
|
| Repo state | What you'll see |
|
|
100
100
|
|---|---|
|
|
101
|
-
| No workflow yet | `/cook`
|
|
102
|
-
| Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If
|
|
103
|
-
| Previous workflow is `done` | `/cook` can
|
|
101
|
+
| No workflow yet | `/cook` consumes a fresh explicit primary-agent handoff when one already exists, or synthesizes one from the primary-agent view in the same entry, then asks you to choose **Start** or **Cancel**. Stale, planning-only, or non-startable handoffs still fail closed. |
|
|
102
|
+
| Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a concrete replacement handoff exists already or is synthesized in the same `/cook` entry and points to a different mission, `/cook` shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous or missing replacement handoff stays conservative. |
|
|
103
|
+
| Previous workflow is `done` | `/cook` can start the next implementation round from a fresh explicit primary-agent handoff or from the same-entry primary-agent handoff synthesis step behind **Start** or **Cancel**. Weak or planning-only next-round handoffs still fail closed. |
|
|
104
104
|
|
|
105
105
|
## Confirmation and fail-closed behavior
|
|
106
106
|
|
|
@@ -38,7 +38,7 @@ type ContextProposalAlternate = {
|
|
|
38
38
|
analysis: ContextProposalAnalysis;
|
|
39
39
|
goalText: string;
|
|
40
40
|
basisPreview: string;
|
|
41
|
-
source: "session" | "analyst" | "handoff_capsule"
|
|
41
|
+
source: "session" | "analyst" | "handoff_capsule";
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
type ContextProposal = ContextProposalAlternate & {
|
|
@@ -60,7 +60,7 @@ type CookContextProposalResult = {
|
|
|
60
60
|
blockedFailureMessage?: string;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
function
|
|
63
|
+
function buildCookStartupBriefRequiredMessage(deps: CompletionDriverDeps, prefix?: string): string {
|
|
64
64
|
const requirement = deps.structuredDiscussionFailureDetail;
|
|
65
65
|
return prefix ? `${prefix} ${requirement}` : requirement;
|
|
66
66
|
}
|
|
@@ -72,10 +72,9 @@ type ActiveWorkflowProposalAssessment = {
|
|
|
72
72
|
blockedFailureMessage?: string;
|
|
73
73
|
reason:
|
|
74
74
|
| "matching_mission"
|
|
75
|
-
| "
|
|
76
|
-
| "
|
|
77
|
-
| "
|
|
78
|
-
| "replacement_not_startable";
|
|
75
|
+
| "missing_explicit_handoff"
|
|
76
|
+
| "fresh_explicit_handoff"
|
|
77
|
+
| "fresh_explicit_handoff_not_startable";
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
type ExistingWorkflowChooserOptions = {
|
|
@@ -183,6 +182,7 @@ function buildCookStructuredDiscussionFailureMessage(deps: CompletionDriverDeps,
|
|
|
183
182
|
return prefix ? `${prefix} ${deps.structuredDiscussionFailureDetail}` : deps.structuredDiscussionFailureDetail;
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
|
|
186
186
|
export function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
|
|
187
187
|
if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
|
|
188
188
|
const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
|
|
@@ -321,23 +321,23 @@ async function assessActiveWorkflowProposalRouting(
|
|
|
321
321
|
): Promise<ActiveWorkflowProposalAssessment> {
|
|
322
322
|
const currentMission = currentMissionAnchor(snapshot);
|
|
323
323
|
const projectName = path.basename(snapshot.files.root);
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
324
|
+
const proposalResult = await deps.deriveCookContextProposal(ctx, projectName);
|
|
325
|
+
if (proposalResult.blockedFailureMessage) {
|
|
326
326
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
327
327
|
action: "blocked",
|
|
328
328
|
currentMissionAnchor: currentMission,
|
|
329
|
-
blockedFailureMessage:
|
|
330
|
-
reason: "
|
|
329
|
+
blockedFailureMessage: proposalResult.blockedFailureMessage,
|
|
330
|
+
reason: "fresh_explicit_handoff_not_startable",
|
|
331
331
|
};
|
|
332
332
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
333
333
|
return assessment;
|
|
334
334
|
}
|
|
335
|
-
const proposal =
|
|
335
|
+
const proposal = proposalResult.proposal;
|
|
336
336
|
if (!proposal) {
|
|
337
337
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
338
338
|
action: "continue",
|
|
339
339
|
currentMissionAnchor: currentMission,
|
|
340
|
-
reason: "
|
|
340
|
+
reason: "missing_explicit_handoff",
|
|
341
341
|
};
|
|
342
342
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
343
343
|
return assessment;
|
|
@@ -356,7 +356,7 @@ async function assessActiveWorkflowProposalRouting(
|
|
|
356
356
|
action: "refocus",
|
|
357
357
|
currentMissionAnchor: currentMission,
|
|
358
358
|
proposal,
|
|
359
|
-
reason:
|
|
359
|
+
reason: "fresh_explicit_handoff",
|
|
360
360
|
};
|
|
361
361
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
362
362
|
return assessment;
|
|
@@ -536,14 +536,14 @@ export async function runCookEntry(
|
|
|
536
536
|
if (!snapshot) {
|
|
537
537
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
538
538
|
const projectName = path.basename(root);
|
|
539
|
-
const derived = await deps.
|
|
539
|
+
const derived = await deps.deriveCookContextProposal(ctx, projectName);
|
|
540
540
|
if (derived.blockedFailureMessage) {
|
|
541
541
|
deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
|
|
542
542
|
return;
|
|
543
543
|
}
|
|
544
544
|
const proposal = derived.proposal;
|
|
545
545
|
if (!proposal) {
|
|
546
|
-
deps.emitCommandText(ctx,
|
|
546
|
+
deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps), "info");
|
|
547
547
|
return;
|
|
548
548
|
}
|
|
549
549
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -581,14 +581,14 @@ export async function runCookEntry(
|
|
|
581
581
|
if (!goal) {
|
|
582
582
|
if (workflowDone) {
|
|
583
583
|
const projectName = path.basename(snapshot.files.root);
|
|
584
|
-
const derived = await deps.
|
|
584
|
+
const derived = await deps.deriveCookContextProposal(ctx, projectName);
|
|
585
585
|
if (derived.blockedFailureMessage) {
|
|
586
586
|
deps.emitCommandText(ctx, derived.blockedFailureMessage, "info");
|
|
587
587
|
return;
|
|
588
588
|
}
|
|
589
589
|
const proposal = derived.proposal;
|
|
590
590
|
if (!proposal) {
|
|
591
|
-
deps.emitCommandText(ctx,
|
|
591
|
+
deps.emitCommandText(ctx, buildCookStartupBriefRequiredMessage(deps, "The previous completion workflow is already done."), "info");
|
|
592
592
|
return;
|
|
593
593
|
}
|
|
594
594
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -610,13 +610,7 @@ export async function runCookEntry(
|
|
|
610
610
|
buildAdvisoryStartupBrief({ proposal, analysis: decision.analysis }),
|
|
611
611
|
);
|
|
612
612
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
613
|
-
deps.emitCommandText(
|
|
614
|
-
ctx,
|
|
615
|
-
proposal.source === "handoff_capsule"
|
|
616
|
-
? `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`
|
|
617
|
-
: `Started a new completion workflow round from deferred primary-agent handoff: ${decision.missionAnchor}`,
|
|
618
|
-
"info",
|
|
619
|
-
);
|
|
613
|
+
deps.emitCommandText(ctx, `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`, "info");
|
|
620
614
|
} else {
|
|
621
615
|
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
|
|
622
616
|
if (assessment.action === "blocked") {
|
|
@@ -627,29 +621,20 @@ export async function runCookEntry(
|
|
|
627
621
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
628
622
|
return;
|
|
629
623
|
}
|
|
630
|
-
const explicitReplacement = assessment.reason === "
|
|
631
|
-
const deferredReplacement = assessment.reason === "deferred_replacement";
|
|
624
|
+
const explicitReplacement = assessment.reason === "fresh_explicit_handoff";
|
|
632
625
|
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
|
|
633
626
|
intro: explicitReplacement
|
|
634
627
|
? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
|
|
635
|
-
:
|
|
636
|
-
? "A deferred primary-agent handoff synthesized from your recent discussion proposes replacing the current workflow. Choose how /cook should proceed:"
|
|
637
|
-
: "A replacement workflow is ready. Choose how /cook should proceed:",
|
|
628
|
+
: "A replacement workflow is ready. Choose how /cook should proceed:",
|
|
638
629
|
proposedMissionLabel: explicitReplacement
|
|
639
630
|
? "Proposed mission from explicit primary-agent handoff"
|
|
640
|
-
:
|
|
641
|
-
? "Proposed mission from deferred primary-agent handoff"
|
|
642
|
-
: "Proposed mission",
|
|
631
|
+
: "Proposed mission",
|
|
643
632
|
refocusChoiceLabel: explicitReplacement
|
|
644
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."
|
|
645
|
-
:
|
|
646
|
-
? "Start new workflow from deferred primary-agent handoff\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
|
|
647
|
-
: "Start new workflow\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.",
|
|
648
635
|
alternateChoiceLabel: explicitReplacement
|
|
649
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."
|
|
650
|
-
:
|
|
651
|
-
? "Start alternate workflow from deferred primary-agent handoff\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
|
|
652
|
-
: undefined,
|
|
637
|
+
: undefined,
|
|
653
638
|
comparison: "strict",
|
|
654
639
|
});
|
|
655
640
|
if (!decision) {
|
|
@@ -662,11 +647,9 @@ export async function runCookEntry(
|
|
|
662
647
|
}
|
|
663
648
|
const selectedProposal = decision.proposal;
|
|
664
649
|
const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
|
|
665
|
-
title:
|
|
650
|
+
title: assessment.reason === "fresh_explicit_handoff"
|
|
666
651
|
? "Start the replacement workflow from this explicit startup brief?"
|
|
667
|
-
:
|
|
668
|
-
? "Start the replacement workflow from this deferred startup brief?"
|
|
669
|
-
: "Start the replacement workflow from this startup brief?",
|
|
652
|
+
: "Start the replacement workflow from this startup brief?",
|
|
670
653
|
});
|
|
671
654
|
if (!proposalDecision) {
|
|
672
655
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
|
|
@@ -686,11 +669,9 @@ export async function runCookEntry(
|
|
|
686
669
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
687
670
|
deps.emitCommandText(
|
|
688
671
|
ctx,
|
|
689
|
-
|
|
672
|
+
assessment.reason === "fresh_explicit_handoff"
|
|
690
673
|
? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
691
|
-
:
|
|
692
|
-
? `Refocused completion mission from deferred primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
693
|
-
: `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
|
|
674
|
+
: `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
|
|
694
675
|
"info",
|
|
695
676
|
);
|
|
696
677
|
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
assessMissionAnchor,
|
|
18
18
|
collectRecentDiscussionEntries,
|
|
19
19
|
collectRecentSessionMessages,
|
|
20
|
-
deriveCookContextProposalFromRecentDiscussion,
|
|
21
20
|
assessLatestCookHandoffProposal,
|
|
22
21
|
finalizeContextProposalAnalysis,
|
|
23
22
|
isWeakMissionAnchor,
|
|
@@ -25,7 +24,6 @@ import {
|
|
|
25
24
|
missionAnchorsStrictlyEquivalent,
|
|
26
25
|
normalizeMissionAnchorText,
|
|
27
26
|
resolveContextProposalConfirmationAction,
|
|
28
|
-
retagContextProposalSource,
|
|
29
27
|
stripCodeBlocks,
|
|
30
28
|
} from "./proposal";
|
|
31
29
|
import type {
|
|
@@ -49,7 +47,7 @@ import {
|
|
|
49
47
|
maybeWriteContextProposalSnapshot,
|
|
50
48
|
} from "./prompt-surfaces";
|
|
51
49
|
import { toolCallBlockReason } from "./policy-guards";
|
|
52
|
-
import { analyzeContextProposalWithAgent, runCompletionRole } from "./role-runner";
|
|
50
|
+
import { analyzeContextProposalWithAgent, generateCookHandoffWithAgent, runCompletionRole } from "./role-runner";
|
|
53
51
|
import {
|
|
54
52
|
applyLiveRoleEvent,
|
|
55
53
|
buildInlineRunningLines,
|
|
@@ -135,10 +133,9 @@ type ActiveWorkflowProposalAssessment = {
|
|
|
135
133
|
blockedFailureMessage?: string;
|
|
136
134
|
reason:
|
|
137
135
|
| "matching_mission"
|
|
138
|
-
| "
|
|
139
|
-
| "
|
|
140
|
-
| "
|
|
141
|
-
| "replacement_not_startable";
|
|
136
|
+
| "missing_explicit_handoff"
|
|
137
|
+
| "fresh_explicit_handoff"
|
|
138
|
+
| "fresh_explicit_handoff_not_startable";
|
|
142
139
|
};
|
|
143
140
|
|
|
144
141
|
function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
|
|
@@ -212,7 +209,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
212
209
|
|
|
213
210
|
const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
|
|
214
211
|
const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
|
|
215
|
-
"/cook failed closed because
|
|
212
|
+
"/cook failed closed because the primary-agent handoff step could not prepare a concrete startup handoff from the current task context. Clarify the mission, first slice, or verification intent in the main chat, then rerun /cook.";
|
|
216
213
|
|
|
217
214
|
function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
218
215
|
return asString(snapshot?.state?.continuation_policy) === "done";
|
|
@@ -379,15 +376,37 @@ function stripCookHandoffBlocks(text: string): string {
|
|
|
379
376
|
return text.replace(COOK_HANDOFF_BLOCK_REGEX, " ").replace(/\s+/g, " ").trim();
|
|
380
377
|
}
|
|
381
378
|
|
|
382
|
-
async function
|
|
379
|
+
async function deriveCookStartupProposal(
|
|
383
380
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
384
381
|
projectName: string,
|
|
385
|
-
): Promise<
|
|
382
|
+
): Promise<CookContextProposalResult> {
|
|
383
|
+
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
384
|
+
const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
|
|
385
|
+
asString,
|
|
386
|
+
asStringArray,
|
|
387
|
+
assessMissionAnchor,
|
|
388
|
+
normalizeMissionAnchorText,
|
|
389
|
+
isWeakMissionAnchor,
|
|
390
|
+
missionAnchorsStrictlyEquivalent,
|
|
391
|
+
stripCodeBlocks,
|
|
392
|
+
});
|
|
393
|
+
if (explicitHandoff.status === "startable") return { proposal: explicitHandoff.proposal };
|
|
394
|
+
if (explicitHandoff.status === "fresh_but_not_startable") {
|
|
395
|
+
return { blockedFailureMessage: explicitHandoff.message };
|
|
396
|
+
}
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function deriveCookContextProposal(
|
|
401
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
402
|
+
projectName: string,
|
|
403
|
+
): Promise<CookContextProposalResult> {
|
|
404
|
+
const explicit = await deriveCookStartupProposal(ctx, projectName);
|
|
405
|
+
if (explicit.proposal || explicit.blockedFailureMessage) return explicit;
|
|
386
406
|
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
387
407
|
const recentEntries = recentMessages
|
|
388
|
-
.filter((entry) => (entry.role === "user" || entry.role === "custom"
|
|
389
|
-
.
|
|
390
|
-
.slice(0, 8)
|
|
408
|
+
.filter((entry) => !entry.isCommand && (entry.role === "user" || entry.role === "assistant" || entry.role === "custom" || entry.role === "summary"))
|
|
409
|
+
.slice(0, 12)
|
|
391
410
|
.map((entry) => ({ role: entry.role, text: stripCookHandoffBlocks(entry.text) }))
|
|
392
411
|
.filter((entry) => entry.text.length > 0);
|
|
393
412
|
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
@@ -403,50 +422,22 @@ async function deriveCookRecentDiscussionProposal(
|
|
|
403
422
|
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
404
423
|
]
|
|
405
424
|
: [];
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
verificationGoal: asString(snapshot.verificationEvidence?.goal),
|
|
418
|
-
verificationSummary: asString(snapshot.verificationEvidence?.summary),
|
|
419
|
-
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
420
|
-
}
|
|
421
|
-
: undefined,
|
|
422
|
-
analyzeContextProposal: async (entries) =>
|
|
423
|
-
await analyzeContextProposalWithAgent({
|
|
424
|
-
ctx,
|
|
425
|
-
projectName,
|
|
426
|
-
recentEntries: entries,
|
|
427
|
-
workflowContextLines,
|
|
428
|
-
liveRoleActivityByRoot,
|
|
429
|
-
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
430
|
-
safeUiCall,
|
|
431
|
-
getCtxCwd,
|
|
432
|
-
getCtxHasUI,
|
|
433
|
-
getCtxUi,
|
|
434
|
-
}),
|
|
435
|
-
assessMissionAnchor,
|
|
436
|
-
isWeakMissionAnchor,
|
|
437
|
-
missionAnchorsStrictlyEquivalent,
|
|
438
|
-
normalizeMissionAnchorText,
|
|
439
|
-
stripCodeBlocks,
|
|
425
|
+
const raw = await generateCookHandoffWithAgent({
|
|
426
|
+
ctx,
|
|
427
|
+
projectName,
|
|
428
|
+
recentEntries,
|
|
429
|
+
workflowContextLines,
|
|
430
|
+
liveRoleActivityByRoot,
|
|
431
|
+
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
432
|
+
safeUiCall,
|
|
433
|
+
getCtxCwd,
|
|
434
|
+
getCtxHasUI,
|
|
435
|
+
getCtxUi,
|
|
440
436
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
446
|
-
projectName: string,
|
|
447
|
-
): Promise<CookContextProposalResult> {
|
|
448
|
-
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
449
|
-
const explicitHandoff = assessLatestCookHandoffProposal(recentMessages, projectName, {
|
|
437
|
+
if (!raw) return {};
|
|
438
|
+
const generated = assessLatestCookHandoffProposal([
|
|
439
|
+
{ role: "assistant", text: raw, messageId: "generated-primary-agent-handoff", timestampMs: Date.now(), isCommand: false },
|
|
440
|
+
], projectName, {
|
|
450
441
|
asString,
|
|
451
442
|
asStringArray,
|
|
452
443
|
assessMissionAnchor,
|
|
@@ -455,18 +446,9 @@ async function deriveCookStartupProposal(
|
|
|
455
446
|
missionAnchorsStrictlyEquivalent,
|
|
456
447
|
stripCodeBlocks,
|
|
457
448
|
});
|
|
458
|
-
if (
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
return { proposal: await deriveCookRecentDiscussionProposal(ctx, projectName) };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function deriveCookContextProposal(
|
|
466
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
467
|
-
projectName: string,
|
|
468
|
-
): Promise<CookContextProposalResult> {
|
|
469
|
-
return await deriveCookStartupProposal(ctx, projectName);
|
|
449
|
+
if (generated.status === "startable") return { proposal: generated.proposal };
|
|
450
|
+
if (generated.status === "fresh_but_not_startable") return { blockedFailureMessage: generated.message };
|
|
451
|
+
return {};
|
|
470
452
|
}
|
|
471
453
|
|
|
472
454
|
async function confirmContextProposal(
|
|
@@ -942,7 +924,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
942
924
|
structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
|
|
943
925
|
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
944
926
|
cookCommandSpec: {
|
|
945
|
-
description: "/cook workflow:
|
|
927
|
+
description: "/cook workflow: start or replace workflow only from an explicit primary-agent handoff, or resume the current workflow from canonical state",
|
|
946
928
|
},
|
|
947
929
|
buildContextProposalContinuationReason,
|
|
948
930
|
completionKickoff,
|
|
@@ -32,7 +32,8 @@ export function buildCookHandoffBoundaryReminder(): string {
|
|
|
32
32
|
"/cook is optional workflow mode for resumability, review, audit, canonical .agent state, or deliberate multi-session control; it is not required just to edit repo files in ordinary chat.",
|
|
33
33
|
"If the user wants direct implementation now, stay in ordinary chat and help directly instead of blocking on /cook.",
|
|
34
34
|
"If the user asks follow-up questions or wants to keep refining scope, continue helping naturally in ordinary chat.",
|
|
35
|
-
"If the user explicitly runs /cook,
|
|
35
|
+
"If the user explicitly runs /cook, the extension should call a primary-agent handoff synthesis step from the current task context, then show Start/Cancel confirmation without making the user rerun /cook.",
|
|
36
|
+
"Do not expect /cook to infer or guess startup intent from recent discussion alone; /cook should use explicit primary-agent handoff data, whether it already exists or is synthesized in the same /cook entry.",
|
|
36
37
|
"Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
|
|
37
38
|
"Any preview capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
|
|
38
39
|
"When you continue in ordinary chat, do not pretend /cook already started and do not silently rewrite discussion into canonical workflow state.",
|
|
@@ -67,6 +67,8 @@ export type AnalyzeContextProposalWithAgentParams = {
|
|
|
67
67
|
getCtxUi: <T extends { ui: any }>(ctx: T) => any | undefined;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
export type GenerateCookHandoffWithAgentParams = AnalyzeContextProposalWithAgentParams;
|
|
71
|
+
|
|
70
72
|
const AGENT_HOME = path.join(os.homedir(), ".pi", "agent");
|
|
71
73
|
const EXTENSION_DIR = typeof __dirname === "string" ? __dirname : process.cwd();
|
|
72
74
|
const PACKAGE_ROOT_CANDIDATE = path.resolve(EXTENSION_DIR, "..", "..");
|
|
@@ -94,6 +96,18 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
|
94
96
|
const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
|
|
95
97
|
const ANALYST_HEARTBEAT_MS = 5_000;
|
|
96
98
|
|
|
99
|
+
const PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT = [
|
|
100
|
+
"You are the primary agent preparing an explicit /cook handoff after the user already chose workflow mode.",
|
|
101
|
+
"Return either exactly one fenced ```cook_handoff JSON block or one brief plain sentence explaining why no concrete handoff can be prepared.",
|
|
102
|
+
"If you can prepare a handoff, the JSON must use kind cook_handoff, source primary_agent, and handoff_kind implementation_workflow_handoff.",
|
|
103
|
+
"When the user has clearly accepted a concrete assistant-proposed slice, carry that slice forward into the handoff instead of broadening or re-guessing the mission.",
|
|
104
|
+
"Do not make /cook infer or rediscover the mission from recent discussion later; author the handoff now from the primary-agent view of the task.",
|
|
105
|
+
"Do not emit markdown commentary before or after the capsule.",
|
|
106
|
+
"If the task is not concrete enough for implementation workflow, do not invent the slice.",
|
|
107
|
+
"A valid implementation-ready handoff must include mission, scope, constraints or non_goals, acceptance, risks, notes, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, and why_this_slice_first.",
|
|
108
|
+
].join(" ");
|
|
109
|
+
const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
|
|
110
|
+
|
|
97
111
|
class StartupAnalystOverlay extends Container {
|
|
98
112
|
private readonly border: DynamicBorder;
|
|
99
113
|
private readonly title: Text;
|
|
@@ -309,6 +323,97 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
|
|
|
309
323
|
return await run();
|
|
310
324
|
}
|
|
311
325
|
|
|
326
|
+
function serializeRecentConversationEntries(entries: RecentDiscussionEntry[]): string {
|
|
327
|
+
return entries
|
|
328
|
+
.slice()
|
|
329
|
+
.reverse()
|
|
330
|
+
.map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
|
|
331
|
+
.join("\n\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildPrimaryAgentHandoffPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], workflowContextLines: string[] = []): string {
|
|
335
|
+
const lines = [
|
|
336
|
+
`Project: ${projectName}`,
|
|
337
|
+
"",
|
|
338
|
+
"Recent session transcript:",
|
|
339
|
+
serializeRecentConversationEntries(recentEntries),
|
|
340
|
+
];
|
|
341
|
+
if (workflowContextLines.length > 0) lines.push("", "Canonical workflow context:", ...workflowContextLines);
|
|
342
|
+
lines.push(
|
|
343
|
+
"",
|
|
344
|
+
"Task:",
|
|
345
|
+
"The user explicitly invoked /cook. Prepare the primary-agent handoff that /cook should consume immediately for Start/Cancel confirmation.",
|
|
346
|
+
);
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
|
|
351
|
+
const { ctx, projectName, recentEntries } = params;
|
|
352
|
+
const modelArg = contextProposalAnalystModelArg(ctx.model);
|
|
353
|
+
if (!modelArg) return undefined;
|
|
354
|
+
const cwd = params.getCtxCwd(ctx);
|
|
355
|
+
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
356
|
+
const prompt = buildPrimaryAgentHandoffPrompt(projectName, recentEntries, params.workflowContextLines ?? []);
|
|
357
|
+
const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-primary-agent-handoff-", PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT);
|
|
358
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
359
|
+
const invocation = getPiInvocation(args);
|
|
360
|
+
const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
|
|
361
|
+
liveActivity.progress = "Preparing primary-agent /cook handoff";
|
|
362
|
+
liveActivity.currentAction = "Authoring explicit startup handoff from current task context";
|
|
363
|
+
liveActivity.assistantSummary = liveActivity.progress;
|
|
364
|
+
try {
|
|
365
|
+
const output = await new Promise<string | undefined>((resolve) => {
|
|
366
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
367
|
+
cwd: runCwd,
|
|
368
|
+
env: process.env,
|
|
369
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
370
|
+
shell: false,
|
|
371
|
+
});
|
|
372
|
+
let buffer = "";
|
|
373
|
+
const messages: RoleMessage[] = [];
|
|
374
|
+
const processLine = (line: string) => {
|
|
375
|
+
if (!line.trim()) return;
|
|
376
|
+
try {
|
|
377
|
+
const event = JSON.parse(line) as JsonRecord;
|
|
378
|
+
applyLiveRoleEvent(liveActivity, event, messages);
|
|
379
|
+
} catch {
|
|
380
|
+
// ignore malformed lines
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
proc.stdout.on("data", (chunk) => {
|
|
384
|
+
buffer += chunk.toString();
|
|
385
|
+
const lines = buffer.split("\n");
|
|
386
|
+
buffer = lines.pop() ?? "";
|
|
387
|
+
for (const line of lines) processLine(line);
|
|
388
|
+
});
|
|
389
|
+
proc.stderr.on("data", () => {
|
|
390
|
+
// ignore stderr unless no assistant output arrives
|
|
391
|
+
});
|
|
392
|
+
proc.on("close", (code) => {
|
|
393
|
+
if (buffer.trim()) processLine(buffer);
|
|
394
|
+
resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
|
|
395
|
+
});
|
|
396
|
+
proc.on("error", () => resolve(undefined));
|
|
397
|
+
});
|
|
398
|
+
return output;
|
|
399
|
+
} finally {
|
|
400
|
+
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
|
|
405
|
+
if (process.env.PI_COMPLETION_DISABLE_PRIMARY_HANDOFF_SYNTHESIS === "1") return undefined;
|
|
406
|
+
const testOutput = asString(process.env.PI_COMPLETION_PRIMARY_HANDOFF_OUTPUT);
|
|
407
|
+
if (testOutput) return testOutput;
|
|
408
|
+
if (params.recentEntries.length === 0) return undefined;
|
|
409
|
+
try {
|
|
410
|
+
return await runPrimaryAgentHandoffSubprocess(params);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.warn("[completion] primary-agent handoff generation failed", error);
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
312
417
|
export async function analyzeContextProposalWithAgent(params: AnalyzeContextProposalWithAgentParams): Promise<ContextProposal | undefined> {
|
|
313
418
|
if (process.env.PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST === "1") return undefined;
|
|
314
419
|
const testOutput = asString(process.env.PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.64",
|
|
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,
|