@linimin/pi-letscook 0.1.59 → 0.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/mission.md +1 -1
- package/CHANGELOG.md +4 -5
- package/README.md +26 -28
- package/extensions/completion/driver.ts +45 -26
- package/extensions/completion/index.ts +76 -63
- package/extensions/completion/prompt-surfaces.ts +15 -13
- package/extensions/completion/proposal.ts +28 -1
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +40 -112
- package/scripts/refocus-test.sh +4 -4
- package/scripts/release-check.sh +17 -23
- package/scripts/smoke-test.sh +9 -13
- package/skills/cook-handoff-boundary/SKILL.md +56 -27
package/.agent/mission.md
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
Project: pi-letscook
|
|
4
4
|
|
|
5
5
|
Mission anchor:
|
|
6
|
-
|
|
6
|
+
Refactor the /cook startup boundary into the agreed mixed model: ordinary chat stays advisory-first by default with no default pre-/cook handoff capsule formation, while explicit /cook performs structured startup synthesis from recent discussion and preserves the approval-only Start/Cancel gate.
|
|
7
7
|
|
|
8
8
|
This file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 0.1.
|
|
5
|
+
## 0.1.61
|
|
6
6
|
|
|
7
7
|
### Changed
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- kept bare `/cook` startup and done-workflow next-round entry fail-closed on missing or non-startable explicit handoffs, while active workflows still resume from canonical `.agent/**` state unless a fresh explicit handoff proposes replacement
|
|
9
|
+
- removed proactive primary-agent `/cook` prompting and default ordinary-chat `cook_handoff` emission so main chat stays advisory until the user explicitly runs `/cook`
|
|
10
|
+
- changed bare `/cook` startup and done-workflow next-round entry to synthesize a deferred primary-agent startup brief from recent discussion instead of requiring a pre-authored explicit handoff capsule
|
|
11
|
+
- kept active-workflow bare `/cook` resumable from canonical `.agent/**` state by default while allowing `/cook` to confirm a concrete replacement mission derived from explicit entry context
|
|
13
12
|
- updated public parity and shipped package contents so the tracked `.agent` contract files are included in package tarballs and packaged smoke/release verification can scaffold canonical state truthfully
|
|
14
13
|
|
|
15
14
|
## 0.1.58
|
package/README.md
CHANGED
|
@@ -30,10 +30,10 @@ Then run `/reload` in Pi.
|
|
|
30
30
|
1. Install the package:
|
|
31
31
|
`pi install npm:@linimin/pi-letscook`
|
|
32
32
|
2. Run `/reload` in Pi.
|
|
33
|
-
3. In the main chat, describe the concrete repo change you want
|
|
34
|
-
4.
|
|
35
|
-
5. Review the startup brief and choose **Start** or **Cancel**.
|
|
36
|
-
6. Later, run `/cook` again to resume from canonical state or confirm
|
|
33
|
+
3. In the main chat, describe and refine the concrete repo change you want.
|
|
34
|
+
4. When you want to enter workflow, run `/cook`.
|
|
35
|
+
5. Review the synthesized startup brief and choose **Start** or **Cancel**.
|
|
36
|
+
6. Later, run `/cook` again to resume from canonical state or confirm a synthesized replacement or next-round startup brief.
|
|
37
37
|
|
|
38
38
|
```text
|
|
39
39
|
/cook
|
|
@@ -43,20 +43,20 @@ Then run `/reload` in Pi.
|
|
|
43
43
|
|
|
44
44
|
| If you want to... | Do this |
|
|
45
45
|
|---|---|
|
|
46
|
-
| Start a long-running task | Discuss the concrete repo change in the main chat,
|
|
46
|
+
| Start a long-running task | Discuss the concrete repo change in the main chat, then run `/cook` when you want workflow to begin |
|
|
47
47
|
| Continue the current workflow | Run `/cook` |
|
|
48
|
-
| Refocus or start the next round | Discuss the new concrete repo change in the main chat,
|
|
48
|
+
| Refocus or start the next round | Discuss the new concrete repo change in the main chat, then run `/cook` when you want a new startup brief synthesized |
|
|
49
49
|
|
|
50
50
|
## What `/cook` expects
|
|
51
51
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
52
|
+
- enough recent main-chat discussion for `/cook` to synthesize a concrete startup brief when you explicitly invoke it
|
|
53
|
+
- a mission that is concrete enough to anchor bounded repo work rather than planning-only discussion
|
|
54
|
+
- acceptance and verification intent that can support a truthful first workflow round
|
|
55
55
|
- README/CHANGELOG updates still count as concrete repo changes
|
|
56
|
-
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they
|
|
57
|
-
-
|
|
56
|
+
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not count unless they can be turned into a concrete startup brief
|
|
57
|
+
- optional explicit `cook_handoff` capsules may still be consumed as a compatibility intake path, but they are no longer required for new-workflow or next-round entry
|
|
58
58
|
|
|
59
|
-
If
|
|
59
|
+
If `/cook` cannot derive a concrete startup brief, it 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`.
|
|
60
60
|
|
|
61
61
|
If a fresh explicit handoff exists but is still workflow-worthy rather than implementation-startable, `/cook` also fails closed instead of silently treating that capsule as planning support or canonical workflow state.
|
|
62
62
|
|
|
@@ -66,40 +66,38 @@ If you pass inline arguments to `/cook`, it also fails closed and tells you to m
|
|
|
66
66
|
|
|
67
67
|
Only explicit `/cook` enters the workflow. Ordinary prompts stay in the main chat and go straight to the primary agent.
|
|
68
68
|
|
|
69
|
-
If a task has clearly matured into completion-workflow scope, the primary agent should
|
|
69
|
+
If a task has clearly matured into completion-workflow scope, the primary agent should still avoid starting long-running implementation directly in ordinary chat.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Ordinary chat remains advisory until you explicitly run `/cook`. At that point `/cook` synthesizes a startup brief from recent discussion using primary-agent-style context, then asks you to **Start** or **Cancel** before rewriting canonical workflow state.
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The capsule is still advisory startup intake, not canonical workflow state, and new-workflow or next-round entry only proceeds when it already names the first bounded slice, repo-change-oriented acceptance, implementation surfaces, and verification commands.
|
|
73
|
+
Optional explicit `/cook` capsules may still be used as compatibility startup intake, but they are no longer the default path and are not required for new-workflow or next-round entry.
|
|
76
74
|
|
|
77
75
|
Important behavior:
|
|
78
76
|
- `/cook` is the canonical workflow boundary and manual entry point
|
|
79
|
-
- startup and next-round entry stay confirm-first
|
|
80
|
-
- active workflows resume from canonical `.agent/**` state unless
|
|
77
|
+
- startup and next-round entry stay confirm-first, but they now derive startup from explicit user `/cook` entry plus recent discussion when needed
|
|
78
|
+
- active workflows resume from canonical `.agent/**` state unless `/cook` synthesizes or receives a concrete replacement mission
|
|
81
79
|
- explicit slash commands other than `/cook` continue normally in the main chat
|
|
82
|
-
- ordinary main-chat discussion may clarify or propose, but mature long-running implementation
|
|
80
|
+
- ordinary main-chat discussion may clarify or propose, but mature long-running implementation still must not start before explicit `/cook`
|
|
83
81
|
|
|
84
82
|
## Typical examples
|
|
85
83
|
|
|
86
|
-
Start a new workflow
|
|
84
|
+
Start a new workflow from recent main-chat discussion:
|
|
87
85
|
|
|
88
86
|
```text
|
|
89
87
|
I want to add login redirect handling and tests.
|
|
90
|
-
#
|
|
88
|
+
# discuss/refine in main chat
|
|
91
89
|
/cook
|
|
92
90
|
```
|
|
93
91
|
|
|
94
92
|
## What happens when you run `/cook`
|
|
95
93
|
|
|
96
|
-
`/cook` first
|
|
94
|
+
`/cook` first checks for a fresh explicit primary-agent handoff capsule as a compatibility intake path. If none is present, `/cook` synthesizes a startup brief from recent discussion using primary-agent-style context. New-workflow entry and done-workflow next-round entry still fail closed when that synthesis is too weak or planning-only. When a workflow is already active and no concrete replacement mission is available, `/cook` resumes from canonical `.agent/**` state.
|
|
97
95
|
|
|
98
96
|
| Repo state | What you'll see |
|
|
99
97
|
|---|---|
|
|
100
|
-
| No workflow yet |
|
|
101
|
-
| Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a
|
|
102
|
-
| Previous workflow is `done` |
|
|
98
|
+
| No workflow yet | `/cook` synthesizes a startup brief from recent discussion and asks you to choose **Start** or **Cancel**. A fresh explicit handoff capsule may still be used if present. Weak, unreliable, stale, planning-only, or non-startable intake still fails closed. |
|
|
99
|
+
| Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If `/cook` finds a different concrete replacement mission from a compatibility capsule or deferred synthesis, it shows a chooser first and only rewrites canonical state after you confirm the replacement. Ambiguous intake stays conservative. |
|
|
100
|
+
| Previous workflow is `done` | `/cook` can synthesize the next implementation round from recent discussion behind **Start** or **Cancel**. Weak or planning-only next-round intake still fails closed. |
|
|
103
101
|
|
|
104
102
|
## Confirmation and fail-closed behavior
|
|
105
103
|
|
|
@@ -107,9 +105,9 @@ I want to add login redirect handling and tests.
|
|
|
107
105
|
|
|
108
106
|
- startup, next-round, and refocus proposals are approval-only
|
|
109
107
|
- actions are **Start** and **Cancel**
|
|
110
|
-
- **Cancel** is side-effect free:
|
|
108
|
+
- **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
|
|
111
109
|
- weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
|
|
112
|
-
- when a
|
|
110
|
+
- when a concrete replacement mission suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
|
|
113
111
|
|
|
114
112
|
When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
|
|
115
113
|
|
|
@@ -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" | "deferred_primary_agent_handoff";
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
type ContextProposal = ContextProposalAlternate & {
|
|
@@ -60,9 +60,8 @@ type CookContextProposalResult = {
|
|
|
60
60
|
blockedFailureMessage?: string;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
function
|
|
64
|
-
const requirement =
|
|
65
|
-
"/cook failed closed because starting a new completion workflow now requires a fresh valid explicit primary-agent handoff. Ask the primary agent to emit a fresh ```cook_handoff``` capsule in the main chat, then rerun /cook.";
|
|
63
|
+
function buildCookStartupDerivationFailureMessage(deps: CompletionDriverDeps, prefix?: string): string {
|
|
64
|
+
const requirement = deps.structuredDiscussionFailureDetail;
|
|
66
65
|
return prefix ? `${prefix} ${requirement}` : requirement;
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -73,9 +72,10 @@ type ActiveWorkflowProposalAssessment = {
|
|
|
73
72
|
blockedFailureMessage?: string;
|
|
74
73
|
reason:
|
|
75
74
|
| "matching_mission"
|
|
76
|
-
| "
|
|
77
|
-
| "
|
|
78
|
-
| "
|
|
75
|
+
| "no_replacement_proposal"
|
|
76
|
+
| "explicit_handoff_replacement"
|
|
77
|
+
| "deferred_replacement"
|
|
78
|
+
| "replacement_not_startable";
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
type ExistingWorkflowChooserOptions = {
|
|
@@ -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 derived = await deps.deriveCookContextProposal(ctx, projectName);
|
|
325
|
+
if (derived.blockedFailureMessage) {
|
|
326
326
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
327
327
|
action: "blocked",
|
|
328
328
|
currentMissionAnchor: currentMission,
|
|
329
|
-
blockedFailureMessage:
|
|
330
|
-
reason: "
|
|
329
|
+
blockedFailureMessage: derived.blockedFailureMessage,
|
|
330
|
+
reason: "replacement_not_startable",
|
|
331
331
|
};
|
|
332
332
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
333
333
|
return assessment;
|
|
334
334
|
}
|
|
335
|
-
const proposal =
|
|
335
|
+
const proposal = derived.proposal;
|
|
336
336
|
if (!proposal) {
|
|
337
337
|
const assessment: ActiveWorkflowProposalAssessment = {
|
|
338
338
|
action: "continue",
|
|
339
339
|
currentMissionAnchor: currentMission,
|
|
340
|
-
reason: "
|
|
340
|
+
reason: "no_replacement_proposal",
|
|
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: proposal.source === "handoff_capsule" ? "explicit_handoff_replacement" : "deferred_replacement",
|
|
360
360
|
};
|
|
361
361
|
deps.maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
362
362
|
return assessment;
|
|
@@ -543,7 +543,7 @@ export async function runCookEntry(
|
|
|
543
543
|
}
|
|
544
544
|
const proposal = derived.proposal;
|
|
545
545
|
if (!proposal) {
|
|
546
|
-
deps.emitCommandText(ctx,
|
|
546
|
+
deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps), "info");
|
|
547
547
|
return;
|
|
548
548
|
}
|
|
549
549
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -588,7 +588,7 @@ export async function runCookEntry(
|
|
|
588
588
|
}
|
|
589
589
|
const proposal = derived.proposal;
|
|
590
590
|
if (!proposal) {
|
|
591
|
-
deps.emitCommandText(ctx,
|
|
591
|
+
deps.emitCommandText(ctx, buildCookStartupDerivationFailureMessage(deps, "The previous completion workflow is already done."), "info");
|
|
592
592
|
return;
|
|
593
593
|
}
|
|
594
594
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -610,7 +610,13 @@ 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(
|
|
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
|
+
);
|
|
614
620
|
} else {
|
|
615
621
|
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
|
|
616
622
|
if (assessment.action === "blocked") {
|
|
@@ -621,20 +627,29 @@ export async function runCookEntry(
|
|
|
621
627
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
622
628
|
return;
|
|
623
629
|
}
|
|
624
|
-
const explicitReplacement = assessment.reason === "
|
|
630
|
+
const explicitReplacement = assessment.reason === "explicit_handoff_replacement";
|
|
631
|
+
const deferredReplacement = assessment.reason === "deferred_replacement";
|
|
625
632
|
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
|
|
626
633
|
intro: explicitReplacement
|
|
627
634
|
? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
|
|
628
|
-
:
|
|
635
|
+
: deferredReplacement
|
|
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:",
|
|
629
638
|
proposedMissionLabel: explicitReplacement
|
|
630
639
|
? "Proposed mission from explicit primary-agent handoff"
|
|
631
|
-
:
|
|
640
|
+
: deferredReplacement
|
|
641
|
+
? "Proposed mission from deferred primary-agent handoff"
|
|
642
|
+
: "Proposed mission",
|
|
632
643
|
refocusChoiceLabel: explicitReplacement
|
|
633
644
|
? "Start new workflow from explicit primary-agent handoff\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
|
|
634
|
-
:
|
|
645
|
+
: deferredReplacement
|
|
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.",
|
|
635
648
|
alternateChoiceLabel: explicitReplacement
|
|
636
649
|
? "Start alternate workflow from explicit primary-agent handoff\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."
|
|
637
|
-
:
|
|
650
|
+
: deferredReplacement
|
|
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,
|
|
638
653
|
comparison: "strict",
|
|
639
654
|
});
|
|
640
655
|
if (!decision) {
|
|
@@ -647,9 +662,11 @@ export async function runCookEntry(
|
|
|
647
662
|
}
|
|
648
663
|
const selectedProposal = decision.proposal;
|
|
649
664
|
const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
|
|
650
|
-
title:
|
|
665
|
+
title: explicitReplacement
|
|
651
666
|
? "Start the replacement workflow from this explicit startup brief?"
|
|
652
|
-
:
|
|
667
|
+
: deferredReplacement
|
|
668
|
+
? "Start the replacement workflow from this deferred startup brief?"
|
|
669
|
+
: "Start the replacement workflow from this startup brief?",
|
|
653
670
|
});
|
|
654
671
|
if (!proposalDecision) {
|
|
655
672
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
|
|
@@ -669,9 +686,11 @@ export async function runCookEntry(
|
|
|
669
686
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
670
687
|
deps.emitCommandText(
|
|
671
688
|
ctx,
|
|
672
|
-
|
|
689
|
+
explicitReplacement
|
|
673
690
|
? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
674
|
-
:
|
|
691
|
+
: deferredReplacement
|
|
692
|
+
? `Refocused completion mission from deferred primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
693
|
+
: `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
|
|
675
694
|
"info",
|
|
676
695
|
);
|
|
677
696
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
missionAnchorsStrictlyEquivalent,
|
|
26
26
|
normalizeMissionAnchorText,
|
|
27
27
|
resolveContextProposalConfirmationAction,
|
|
28
|
+
retagContextProposalSource,
|
|
28
29
|
stripCodeBlocks,
|
|
29
30
|
} from "./proposal";
|
|
30
31
|
import type {
|
|
@@ -107,6 +108,7 @@ type RubricEvaluationRole = (typeof RUBRIC_EVALUATION_ROLES)[number];
|
|
|
107
108
|
const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
|
|
108
109
|
const activatedCompletionRoutingRoots = new Set<string>();
|
|
109
110
|
const LIVE_ROLE_HEARTBEAT_MS = 5_000;
|
|
111
|
+
const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*[\s\S]*?```/giu;
|
|
110
112
|
|
|
111
113
|
function asBoolean(value: unknown): boolean | undefined {
|
|
112
114
|
return typeof value === "boolean" ? value : undefined;
|
|
@@ -133,9 +135,10 @@ type ActiveWorkflowProposalAssessment = {
|
|
|
133
135
|
blockedFailureMessage?: string;
|
|
134
136
|
reason:
|
|
135
137
|
| "matching_mission"
|
|
136
|
-
| "
|
|
137
|
-
| "
|
|
138
|
-
| "
|
|
138
|
+
| "no_replacement_proposal"
|
|
139
|
+
| "explicit_handoff_replacement"
|
|
140
|
+
| "deferred_replacement"
|
|
141
|
+
| "replacement_not_startable";
|
|
139
142
|
};
|
|
140
143
|
|
|
141
144
|
function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cancel" | undefined {
|
|
@@ -209,7 +212,7 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
209
212
|
|
|
210
213
|
const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
|
|
211
214
|
const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
|
|
212
|
-
"/cook failed closed because
|
|
215
|
+
"/cook failed closed because it could not derive a concrete startup brief from recent discussion. Clarify the mission, first slice, or verification intent in the main chat, then rerun /cook.";
|
|
213
216
|
|
|
214
217
|
function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
215
218
|
return asString(snapshot?.state?.continuation_policy) === "done";
|
|
@@ -372,6 +375,72 @@ async function promptContextProposalConfirmationAction(
|
|
|
372
375
|
});
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
function stripCookHandoffBlocks(text: string): string {
|
|
379
|
+
return text.replace(COOK_HANDOFF_BLOCK_REGEX, " ").replace(/\s+/g, " ").trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function deriveCookRecentDiscussionProposal(
|
|
383
|
+
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
384
|
+
projectName: string,
|
|
385
|
+
): Promise<ContextProposal | undefined> {
|
|
386
|
+
const recentMessages = collectRecentSessionMessages(ctx, { isRecord, asString, asNumber, isStaleContextError });
|
|
387
|
+
const recentEntries = recentMessages
|
|
388
|
+
.filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
|
|
389
|
+
.filter((entry) => !/```cook_handoff\b/i.test(entry.text))
|
|
390
|
+
.slice(0, 8)
|
|
391
|
+
.map((entry) => ({ role: entry.role, text: stripCookHandoffBlocks(entry.text) }))
|
|
392
|
+
.filter((entry) => entry.text.length > 0);
|
|
393
|
+
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
394
|
+
const workflowContextLines = snapshot
|
|
395
|
+
? [
|
|
396
|
+
`current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
|
|
397
|
+
`continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
|
|
398
|
+
`latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
399
|
+
`latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
|
|
400
|
+
`active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
|
|
401
|
+
`active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
|
|
402
|
+
`verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
|
|
403
|
+
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
404
|
+
]
|
|
405
|
+
: [];
|
|
406
|
+
const proposal = await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
407
|
+
asString,
|
|
408
|
+
asStringArray,
|
|
409
|
+
workflowContext: snapshot
|
|
410
|
+
? {
|
|
411
|
+
currentMissionAnchor:
|
|
412
|
+
asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
|
|
413
|
+
latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
|
|
414
|
+
latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
|
|
415
|
+
activeSliceGoal: asString(snapshot.active?.goal),
|
|
416
|
+
activeSliceWhyNow: asString(snapshot.active?.why_now),
|
|
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,
|
|
440
|
+
});
|
|
441
|
+
return retagContextProposalSource(proposal, "deferred_primary_agent_handoff");
|
|
442
|
+
}
|
|
443
|
+
|
|
375
444
|
async function deriveCookStartupProposal(
|
|
376
445
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
377
446
|
projectName: string,
|
|
@@ -390,70 +459,14 @@ async function deriveCookStartupProposal(
|
|
|
390
459
|
if (explicitHandoff.status === "fresh_but_not_startable") {
|
|
391
460
|
return { blockedFailureMessage: explicitHandoff.message };
|
|
392
461
|
}
|
|
393
|
-
return {};
|
|
462
|
+
return { proposal: await deriveCookRecentDiscussionProposal(ctx, projectName) };
|
|
394
463
|
}
|
|
395
464
|
|
|
396
465
|
async function deriveCookContextProposal(
|
|
397
466
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
398
467
|
projectName: string,
|
|
399
468
|
): Promise<CookContextProposalResult> {
|
|
400
|
-
|
|
401
|
-
const recentEntries = recentMessages
|
|
402
|
-
.filter((entry) => (entry.role === "user" || entry.role === "custom") && !entry.isCommand)
|
|
403
|
-
.slice(0, 8)
|
|
404
|
-
.map((entry) => ({ role: entry.role, text: entry.text }));
|
|
405
|
-
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
406
|
-
const workflowContextLines = snapshot
|
|
407
|
-
? [
|
|
408
|
-
`current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
|
|
409
|
-
`continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
|
|
410
|
-
`latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
411
|
-
`latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
|
|
412
|
-
`active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
|
|
413
|
-
`active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
|
|
414
|
-
`verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
|
|
415
|
-
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
416
|
-
]
|
|
417
|
-
: [];
|
|
418
|
-
const explicitHandoff = await deriveCookStartupProposal(ctx, projectName);
|
|
419
|
-
if (explicitHandoff.proposal || explicitHandoff.blockedFailureMessage) return explicitHandoff;
|
|
420
|
-
return {
|
|
421
|
-
proposal: await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
422
|
-
asString,
|
|
423
|
-
asStringArray,
|
|
424
|
-
workflowContext: snapshot
|
|
425
|
-
? {
|
|
426
|
-
currentMissionAnchor:
|
|
427
|
-
asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
|
|
428
|
-
latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
|
|
429
|
-
latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
|
|
430
|
-
activeSliceGoal: asString(snapshot.active?.goal),
|
|
431
|
-
activeSliceWhyNow: asString(snapshot.active?.why_now),
|
|
432
|
-
verificationGoal: asString(snapshot.verificationEvidence?.goal),
|
|
433
|
-
verificationSummary: asString(snapshot.verificationEvidence?.summary),
|
|
434
|
-
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
435
|
-
}
|
|
436
|
-
: undefined,
|
|
437
|
-
analyzeContextProposal: async (entries) =>
|
|
438
|
-
await analyzeContextProposalWithAgent({
|
|
439
|
-
ctx,
|
|
440
|
-
projectName,
|
|
441
|
-
recentEntries: entries,
|
|
442
|
-
workflowContextLines,
|
|
443
|
-
liveRoleActivityByRoot,
|
|
444
|
-
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
445
|
-
safeUiCall,
|
|
446
|
-
getCtxCwd,
|
|
447
|
-
getCtxHasUI,
|
|
448
|
-
getCtxUi,
|
|
449
|
-
}),
|
|
450
|
-
assessMissionAnchor,
|
|
451
|
-
isWeakMissionAnchor,
|
|
452
|
-
missionAnchorsStrictlyEquivalent,
|
|
453
|
-
normalizeMissionAnchorText,
|
|
454
|
-
stripCodeBlocks,
|
|
455
|
-
}),
|
|
456
|
-
};
|
|
469
|
+
return await deriveCookStartupProposal(ctx, projectName);
|
|
457
470
|
}
|
|
458
471
|
|
|
459
472
|
async function confirmContextProposal(
|
|
@@ -929,7 +942,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
929
942
|
structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
|
|
930
943
|
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
931
944
|
cookCommandSpec: {
|
|
932
|
-
description: "/cook workflow:
|
|
945
|
+
description: "/cook workflow: synthesize a startup brief when the user explicitly enters /cook, resume the current workflow from canonical state, or confirm a replacement mission from explicit /cook entry",
|
|
933
946
|
},
|
|
934
947
|
buildContextProposalContinuationReason,
|
|
935
948
|
completionKickoff,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
|
|
11
11
|
export type AdvisoryStartupBrief = {
|
|
12
12
|
kind: "startup_brief";
|
|
13
|
-
source: "recent_discussion" | "primary_agent_handoff";
|
|
13
|
+
source: "recent_discussion" | "primary_agent_handoff" | "deferred_primary_agent_handoff";
|
|
14
14
|
confirmed: true;
|
|
15
15
|
captured_at: string;
|
|
16
16
|
goal_text: string;
|
|
@@ -27,18 +27,15 @@ export type AdvisoryStartupBrief = {
|
|
|
27
27
|
export function buildCookHandoffBoundaryReminder(): string {
|
|
28
28
|
return [
|
|
29
29
|
"You are still in ordinary main chat before any explicit /cook workflow entry.",
|
|
30
|
-
"Use ordinary chat to clarify requirements, discuss tradeoffs, propose implementation approaches, and refine scope
|
|
30
|
+
"Use ordinary chat to clarify requirements, discuss tradeoffs, propose implementation approaches, and refine scope naturally.",
|
|
31
31
|
"/cook is the only explicit entrypoint into long-running completion workflow.",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"Use handoff_kind implementation_workflow_handoff for that implementation-ready capsule.",
|
|
40
|
-
"If later ordinary-chat discussion materially changes the startup brief before /cook runs, update or replace the capsule in a later assistant reply instead of pretending the workflow already started.",
|
|
41
|
-
"The capsule is startup intake for /cook only: do not present it as canonical .agent state, an active slice, or a persistent repo contract.",
|
|
32
|
+
"Do not proactively tell the user to run /cook just because a task looks workflow-worthy, and do not emit a ```cook_handoff``` capsule by default in ordinary chat.",
|
|
33
|
+
"Even when the task has matured into workflow-level implementation work, ordinary chat remains ordinary chat until the user explicitly runs /cook.",
|
|
34
|
+
"Before that explicit /cook entry, 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.",
|
|
35
|
+
"If the user asks follow-up questions or wants to keep refining scope, continue helping in ordinary chat instead of steering them into workflow mode.",
|
|
36
|
+
"Once the user explicitly runs /cook, /cook will synthesize a startup brief from recent discussion using primary-agent-style context, then show Start/Cancel confirmation before canonical workflow state is rewritten.",
|
|
37
|
+
"Only provide a preview startup brief or ```cook_handoff``` capsule in ordinary chat when the user explicitly asks for that preview behavior.",
|
|
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.",
|
|
42
39
|
"If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
|
|
43
40
|
].join(" ");
|
|
44
41
|
}
|
|
@@ -97,7 +94,12 @@ export function buildAdvisoryStartupBrief(args: {
|
|
|
97
94
|
}): AdvisoryStartupBrief {
|
|
98
95
|
return {
|
|
99
96
|
kind: "startup_brief",
|
|
100
|
-
source:
|
|
97
|
+
source:
|
|
98
|
+
args.proposal.source === "handoff_capsule"
|
|
99
|
+
? "primary_agent_handoff"
|
|
100
|
+
: args.proposal.source === "deferred_primary_agent_handoff"
|
|
101
|
+
? "deferred_primary_agent_handoff"
|
|
102
|
+
: "recent_discussion",
|
|
101
103
|
confirmed: true,
|
|
102
104
|
captured_at: args.capturedAt ?? new Date().toISOString(),
|
|
103
105
|
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" | "handoff_capsule";
|
|
30
|
+
source: "session" | "analyst" | "handoff_capsule" | "deferred_primary_agent_handoff";
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type ContextProposal = ContextProposalAlternate & {
|
|
@@ -1246,6 +1246,7 @@ export function extractContextProposalFromStructuredSession(
|
|
|
1246
1246
|
|
|
1247
1247
|
const COOK_HANDOFF_BLOCK_REGEX = /```cook_handoff\s*([\s\S]*?)```/giu;
|
|
1248
1248
|
const COOK_HANDOFF_MAX_AGE_MS = 45 * 60 * 1000;
|
|
1249
|
+
const COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES = 2;
|
|
1249
1250
|
const COOK_HANDOFF_NEGATIVE_MISSION_REGEX =
|
|
1250
1251
|
/(?:\b(?:do not|don't|dont|not|never|avoid|skip|refuse|recognize that|suppress|ignore|block|prevent)\b|(?:不要|別|别|勿|禁止|避免|忽略|阻止))/iu;
|
|
1251
1252
|
const COOK_HANDOFF_WORKFLOW_ONLY_ACCEPTANCE_REGEX =
|
|
@@ -1388,6 +1389,19 @@ function isStartableCookHandoffCapsule(
|
|
|
1388
1389
|
return cookHandoffStartabilityFailures(capsule, deps).length === 0;
|
|
1389
1390
|
}
|
|
1390
1391
|
|
|
1392
|
+
function laterMessagesInvalidateCookHandoff(
|
|
1393
|
+
laterMessages: RecentSessionMessage[],
|
|
1394
|
+
deps: Pick<ProposalParseDeps, "stripCodeBlocks">,
|
|
1395
|
+
): boolean {
|
|
1396
|
+
const laterNonCommandMessages = laterMessages.filter((entry) => !entry.isCommand);
|
|
1397
|
+
if (laterNonCommandMessages.length > COOK_HANDOFF_MAX_LATER_NON_COMMAND_MESSAGES) return true;
|
|
1398
|
+
return laterNonCommandMessages.some((entry) => {
|
|
1399
|
+
if (entry.role === "summary") return false;
|
|
1400
|
+
if (!hasRecentDiscussionImplementationIntent(entry.text, deps.stripCodeBlocks)) return false;
|
|
1401
|
+
return true;
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1391
1405
|
function cookHandoffIsFreshEnough(capsule: CookHandoffCapsule, laterMessages: RecentSessionMessage[]): boolean {
|
|
1392
1406
|
const capturedAtMs = Date.parse(capsule.captured_at);
|
|
1393
1407
|
if (!Number.isFinite(capturedAtMs)) return false;
|
|
@@ -1470,6 +1484,7 @@ export function assessLatestCookHandoffProposal(
|
|
|
1470
1484
|
const capsule = capsules[capsuleIndex];
|
|
1471
1485
|
const laterMessages = recentMessages.slice(0, index);
|
|
1472
1486
|
if (!cookHandoffIsFreshEnough(capsule, laterMessages)) continue;
|
|
1487
|
+
if (laterMessagesInvalidateCookHandoff(laterMessages, deps)) continue;
|
|
1473
1488
|
const failures = cookHandoffStartabilityFailures(capsule, deps);
|
|
1474
1489
|
if (failures.length > 0) {
|
|
1475
1490
|
return {
|
|
@@ -1515,6 +1530,18 @@ export async function deriveCookContextProposalFromRecentDiscussion(
|
|
|
1515
1530
|
return undefined;
|
|
1516
1531
|
}
|
|
1517
1532
|
|
|
1533
|
+
export function retagContextProposalSource(
|
|
1534
|
+
proposal: ContextProposal | undefined,
|
|
1535
|
+
source: ContextProposalAlternate["source"],
|
|
1536
|
+
): ContextProposal | undefined {
|
|
1537
|
+
if (!proposal) return undefined;
|
|
1538
|
+
return {
|
|
1539
|
+
...proposal,
|
|
1540
|
+
source,
|
|
1541
|
+
alternateProposals: proposal.alternateProposals.map((alternate) => ({ ...alternate, source })),
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1518
1545
|
export function resolveContextProposalConfirmationAction(
|
|
1519
1546
|
proposal: ContextProposal,
|
|
1520
1547
|
action: ContextProposalConfirmAction,
|