@linimin/pi-letscook 0.1.60 → 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/CHANGELOG.md +5 -8
- package/README.md +28 -32
- package/extensions/completion/driver.ts +41 -28
- 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 +49 -154
- package/scripts/refocus-test.sh +4 -4
- package/scripts/release-check.sh +24 -31
- package/scripts/smoke-test.sh +64 -24
- package/skills/cook-handoff-boundary/SKILL.md +54 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 0.1.
|
|
6
|
-
|
|
7
|
-
## 0.1.59
|
|
5
|
+
## 0.1.61
|
|
8
6
|
|
|
9
7
|
### Changed
|
|
10
8
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- kept
|
|
14
|
-
-
|
|
15
|
-
- updated public parity and packaged release verification so README/help/changelog/release-check all describe and gate the shipped mixed model truthfully, while still packaging the tracked `.agent` contract files
|
|
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
|
|
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
|
|
16
13
|
|
|
17
14
|
## 0.1.58
|
|
18
15
|
|
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,22 +43,22 @@ Then run `/reload` in Pi.
|
|
|
43
43
|
|
|
44
44
|
| If you want to... | Do this |
|
|
45
45
|
|---|---|
|
|
46
|
-
| Start a long-running task | Discuss the concrete repo change in the main chat, then run `/cook`
|
|
46
|
+
| Start a long-running task | Discuss the concrete repo change in the main chat, then run `/cook` 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, then run `/cook`
|
|
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
|
-
- recent
|
|
53
|
-
-
|
|
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
|
|
54
55
|
- README/CHANGELOG updates still count as concrete repo changes
|
|
55
|
-
- assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts still do not
|
|
56
|
-
-
|
|
57
|
-
- active-workflow replacement still stays conservative: `/cook` resumes from canonical state unless a fresh explicit handoff proposes a different concrete repo change and you confirm that replacement
|
|
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
|
-
If
|
|
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
|
|
|
63
63
|
If you pass inline arguments to `/cook`, it also fails closed and tells you to move that intent into the main chat before rerunning bare `/cook`.
|
|
64
64
|
|
|
@@ -66,41 +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
|
-
Bare `/cook` is still the canonical workflow boundary: it synthesizes the startup brief from recent ordinary-chat discussion at `/cook` time, then waits for **Start** or **Cancel** before any canonical `.agent/**` write.
|
|
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
|
|
81
|
-
- any pre-`/cook` preview or capsule is explicit-request-only and non-canonical
|
|
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
|
|
82
79
|
- explicit slash commands other than `/cook` continue normally in the main chat
|
|
83
|
-
- 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`
|
|
84
81
|
|
|
85
82
|
## Typical examples
|
|
86
83
|
|
|
87
|
-
Start a new workflow from recent discussion:
|
|
84
|
+
Start a new workflow from recent main-chat discussion:
|
|
88
85
|
|
|
89
86
|
```text
|
|
90
87
|
I want to add login redirect handling and tests.
|
|
91
|
-
# discuss
|
|
88
|
+
# discuss/refine in main chat
|
|
92
89
|
/cook
|
|
93
90
|
```
|
|
94
91
|
|
|
95
92
|
## What happens when you run `/cook`
|
|
96
93
|
|
|
97
|
-
|
|
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.
|
|
98
95
|
|
|
99
96
|
| Repo state | What you'll see |
|
|
100
97
|
|---|---|
|
|
101
|
-
| No workflow yet | `/cook` synthesizes a startup brief from recent discussion and
|
|
102
|
-
| Active workflow exists | Usually a resume of the current workflow from canonical `.agent/**` state. If a
|
|
103
|
-
| Previous workflow is `done` | `/cook`
|
|
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. |
|
|
104
101
|
|
|
105
102
|
## Confirmation and fail-closed behavior
|
|
106
103
|
|
|
@@ -108,10 +105,9 @@ When no workflow is active, bare `/cook` synthesizes a startup brief from recent
|
|
|
108
105
|
|
|
109
106
|
- startup, next-round, and refocus proposals are approval-only
|
|
110
107
|
- actions are **Start** and **Cancel**
|
|
111
|
-
- **Cancel** is side-effect free:
|
|
108
|
+
- **Cancel** is side-effect free: discuss changes in the main chat and rerun `/cook`
|
|
112
109
|
- weak, ambiguous, stale, invalid, assistant-produced, or planning-only intake does not start a workflow
|
|
113
|
-
-
|
|
114
|
-
- when a fresh explicit handoff suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
|
|
110
|
+
- when a concrete replacement mission suggests replacing an active workflow, `/cook` shows a chooser before any canonical state rewrite
|
|
115
111
|
|
|
116
112
|
When you accept startup or refocus, `/cook` persists the chosen workflow state in canonical `.agent/**` files before the re-ground round begins.
|
|
117
113
|
|
|
@@ -264,7 +260,7 @@ npm run rubric-contract-test
|
|
|
264
260
|
npm run release-check
|
|
265
261
|
```
|
|
266
262
|
|
|
267
|
-
`npm run release-check` is the broad packaged-release verifier. It begins with `bash .agent/verify_completion_control_plane.sh`, so missing or stale `.agent/verification-evidence.json` parity fails closed before the broader suite runs, then asserts the shipped
|
|
263
|
+
`npm run release-check` is the broad packaged-release verifier. It begins with `bash .agent/verify_completion_control_plane.sh`, so missing or stale `.agent/verification-evidence.json` parity fails closed before the broader suite runs, then asserts the shipped `/cook` public parity surfaces in `README.md`, `CHANGELOG.md`, and the `/cook` help/fail-closed copy in `extensions/completion/index.ts`, reruns the startup/refocus/context checks — including the critique-aware `/cook` confirmation regression and the smoke auto-resume prompt path — includes deterministic canonical evidence artifact coverage and includes deterministic active-slice contract coverage plus observability coverage, evaluator calibration, and the rubric-contract regression, and finishes with `npm pack --dry-run`.
|
|
268
264
|
|
|
269
265
|
The direct package-root verifier commands above intentionally self-isolate the repo-local extension when they shell back into `pi`, so you should not need to wrap them with `pi --no-extensions` even if `@linimin/pi-letscook` is also installed globally on the same machine.
|
|
270
266
|
|
|
@@ -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 recent discussion did not produce a clear execution-ready startup brief with Mission/Scope/Constraints/Acceptance for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
|
|
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;
|
|
@@ -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.deriveCookStartupProposal(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, buildCookStartupDerivationFailureMessage(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.deriveCookStartupProposal(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, buildCookStartupDerivationFailureMessage(deps, "The previous completion workflow is already done."), "info");
|
|
592
592
|
return;
|
|
593
593
|
}
|
|
594
594
|
const decision = await deps.confirmContextProposal(ctx, proposal, {
|
|
@@ -614,7 +614,7 @@ export async function runCookEntry(
|
|
|
614
614
|
ctx,
|
|
615
615
|
proposal.source === "handoff_capsule"
|
|
616
616
|
? `Started a new completion workflow round from explicit primary-agent handoff: ${decision.missionAnchor}`
|
|
617
|
-
: `Started a new completion workflow round from
|
|
617
|
+
: `Started a new completion workflow round from deferred primary-agent handoff: ${decision.missionAnchor}`,
|
|
618
618
|
"info",
|
|
619
619
|
);
|
|
620
620
|
} else {
|
|
@@ -627,20 +627,29 @@ export async function runCookEntry(
|
|
|
627
627
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
628
628
|
return;
|
|
629
629
|
}
|
|
630
|
-
const explicitReplacement = assessment.reason === "
|
|
630
|
+
const explicitReplacement = assessment.reason === "explicit_handoff_replacement";
|
|
631
|
+
const deferredReplacement = assessment.reason === "deferred_replacement";
|
|
631
632
|
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
|
|
632
633
|
intro: explicitReplacement
|
|
633
634
|
? "A fresh explicit primary-agent handoff proposes replacing the current workflow. Choose how /cook should proceed:"
|
|
634
|
-
:
|
|
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:",
|
|
635
638
|
proposedMissionLabel: explicitReplacement
|
|
636
639
|
? "Proposed mission from explicit primary-agent handoff"
|
|
637
|
-
:
|
|
640
|
+
: deferredReplacement
|
|
641
|
+
? "Proposed mission from deferred primary-agent handoff"
|
|
642
|
+
: "Proposed mission",
|
|
638
643
|
refocusChoiceLabel: explicitReplacement
|
|
639
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."
|
|
640
|
-
:
|
|
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.",
|
|
641
648
|
alternateChoiceLabel: explicitReplacement
|
|
642
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."
|
|
643
|
-
:
|
|
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,
|
|
644
653
|
comparison: "strict",
|
|
645
654
|
});
|
|
646
655
|
if (!decision) {
|
|
@@ -653,9 +662,11 @@ export async function runCookEntry(
|
|
|
653
662
|
}
|
|
654
663
|
const selectedProposal = decision.proposal;
|
|
655
664
|
const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
|
|
656
|
-
title:
|
|
665
|
+
title: explicitReplacement
|
|
657
666
|
? "Start the replacement workflow from this explicit startup brief?"
|
|
658
|
-
:
|
|
667
|
+
: deferredReplacement
|
|
668
|
+
? "Start the replacement workflow from this deferred startup brief?"
|
|
669
|
+
: "Start the replacement workflow from this startup brief?",
|
|
659
670
|
});
|
|
660
671
|
if (!proposalDecision) {
|
|
661
672
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
|
|
@@ -675,9 +686,11 @@ export async function runCookEntry(
|
|
|
675
686
|
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
676
687
|
deps.emitCommandText(
|
|
677
688
|
ctx,
|
|
678
|
-
|
|
689
|
+
explicitReplacement
|
|
679
690
|
? `Refocused completion mission from explicit primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
680
|
-
:
|
|
691
|
+
: deferredReplacement
|
|
692
|
+
? `Refocused completion mission from deferred primary-agent handoff to: ${proposalDecision.missionAnchor}`
|
|
693
|
+
: `Refocused completion mission to: ${proposalDecision.missionAnchor}`,
|
|
681
694
|
"info",
|
|
682
695
|
);
|
|
683
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: synthesize
|
|
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,19 +27,16 @@ 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
|
-
"If the user explicitly asks for a /cook preview or capsule before running /cook, you may 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, first_slice_goal, first_slice_non_goals, implementation_surfaces, verification_commands, why_this_slice_first, and optional task_type/evaluation_profile/why_cook_now.",
|
|
39
|
-
"Use handoff_kind implementation_workflow_handoff for that opt-in preview capsule.",
|
|
40
|
-
"If later ordinary-chat discussion materially changes the startup brief before /cook runs, update or replace the preview capsule in a later assistant reply instead of pretending the workflow already started.",
|
|
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.",
|
|
41
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
|
-
"If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook
|
|
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
|
}
|
|
45
42
|
|
|
@@ -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,
|