@linimin/pi-letscook 0.1.46 → 0.1.47
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 +4 -0
- package/README.md +7 -6
- package/extensions/completion/driver.ts +79 -21
- package/extensions/completion/index.ts +43 -2
- package/extensions/completion/prompt-surfaces.ts +27 -3
- package/extensions/completion/proposal.ts +282 -9
- package/extensions/completion/role-runner.ts +7 -2
- package/package.json +1 -1
- package/scripts/context-proposal-test.sh +274 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.47
|
|
6
|
+
|
|
5
7
|
### Changed
|
|
6
8
|
|
|
7
9
|
- removed inline `/cook <text>` argument support so bare `/cook` is now the only supported workflow entrypoint
|
|
8
10
|
- made runtime, deterministic regressions, README guidance, and packaged release parity fail closed when command arguments are passed instead of discussion driving proposal derivation
|
|
11
|
+
- made bare `/cook` weight the latest clear implementation intent ahead of older background discussion, preserve alternate recent missions for chooser-driven disambiguation, and summarize each candidate directly in the active-workflow chooser instead of forcing a single guessed replacement path
|
|
12
|
+
- made bare `/cook` suppress reopening already completed or already verified work by comparing recent discussion against canonical mission, active-slice, and verification-evidence context before startup/refocus proposal confirmation
|
|
9
13
|
|
|
10
14
|
## 0.1.44
|
|
11
15
|
|
package/README.md
CHANGED
|
@@ -55,9 +55,9 @@ Bare `/cook` is the only supported workflow entrypoint.
|
|
|
55
55
|
|
|
56
56
|
| Repo state | `/cook` behavior |
|
|
57
57
|
|---|---|
|
|
58
|
-
| No workflow yet | Summarizes recent main-chat discussion into a startup proposal, then asks for approval with **Start** or **Cancel**. If the discussion is weak, ambiguous, assistant-produced, or only a plan/spec/design-doc/proposal artifact instead of concrete repo changes, `/cook` fails closed without writing `.agent/**` state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. |
|
|
59
|
-
| Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. `/cook <text>` is rejected without running proposal routing or rewriting workflow state. |
|
|
60
|
-
| Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. `/cook <text>` is rejected before any next-round proposal is derived. |
|
|
58
|
+
| No workflow yet | Summarizes recent main-chat discussion into a startup proposal, weighting the latest clear implementation intent ahead of older background discussion, then asks for approval with **Start** or **Cancel**. If the discussion is weak, ambiguous, assistant-produced, or only a plan/spec/design-doc/proposal artifact instead of concrete repo changes, `/cook` fails closed without writing `.agent/**` state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. |
|
|
59
|
+
| Active workflow exists | Reads the current mission plus recent non-command main-chat discussion. Matching or unclear discussion resumes from canonical `.agent/**` state. Clear replacement discussion about different concrete repo changes opens a chooser first, then only rewrites canonical state after the follow-on **Start** confirmation. If recent discussion implies more than one plausible replacement mission, `/cook` keeps the current workflow parked behind a multi-candidate chooser instead of silently resuming or guessing. Assistant/summary artifacts or plan/spec/design-doc/proposal-only context do not refocus the workflow. `/cook <text>` is rejected without running proposal routing or rewriting workflow state. |
|
|
60
|
+
| Previous workflow is `done` | Starts the next round from recent main-chat discussion, then asks for approval with **Start** or **Cancel**. Weak, ambiguous, assistant-produced, or planning-artifact-only discussion fails closed without rewriting canonical state and tells you to clarify the mission in the main chat before rerunning bare `/cook`. Recent discussion that only restates already-completed or already-verified work also fails closed instead of reopening the finished mission. `/cook <text>` is rejected before any next-round proposal is derived. |
|
|
61
61
|
|
|
62
62
|
## Approval-only confirmation and fail-closed behavior
|
|
63
63
|
|
|
@@ -69,15 +69,16 @@ All startup, next-round, and replacement proposals are **approval-only**:
|
|
|
69
69
|
|
|
70
70
|
When `/cook` cannot derive a clear startup, next-round, or replacement proposal for concrete repo changes from recent main-chat discussion, it fails closed instead of guessing. That means no canonical `.agent/**` state is created or rewritten until the discussion is clarified in the main chat and you rerun `/cook`. Tracked docs-only work such as README/CHANGELOG updates is still execution-ready, but assistant-produced summaries and plan/spec/design-doc/proposal-only artifacts are not enough to start or refocus a workflow on their own. `/cook <text>` also fails closed immediately and tells you to move that text into the main chat before rerunning bare `/cook`.
|
|
71
71
|
|
|
72
|
-
When an active workflow already exists and recent discussion
|
|
72
|
+
When an active workflow already exists and recent discussion suggests a different workflow, `/cook` shows a separate chooser first. The chooser can stay conservative or list multiple candidate replacements when the latest discussion contains more than one plausible implementation goal:
|
|
73
73
|
|
|
74
74
|
- **Continue current workflow**
|
|
75
75
|
- **Start new workflow from recent discussion**
|
|
76
|
+
- **Start alternate workflow from recent discussion** (when a second plausible mission exists)
|
|
76
77
|
- **Cancel**
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
Chooser options summarize each candidate mission with its latest scope/constraint/acceptance highlights before the follow-on approval-only Start/Cancel gate. Canonical `.agent/**` state changes still happen only after **Start** is accepted.
|
|
79
80
|
|
|
80
|
-
When you accept startup or refocus from that flow, `/cook` persists the chosen `task_type` and `evaluation_profile` across `.agent/profile.json`, `.agent/state.json`, `.agent/plan.json`, and `.agent/active-slice.json`, and records the accepted critique outcome in canonical continuation state before the re-ground round begins.
|
|
81
|
+
When you accept startup or refocus from that flow, `/cook` persists the chosen `task_type` and `evaluation_profile` across `.agent/profile.json`, `.agent/state.json`, `.agent/plan.json`, and `.agent/active-slice.json`, and records the accepted critique outcome plus any alternate-mission / suppression notes in canonical continuation state before the re-ground round begins.
|
|
81
82
|
|
|
82
83
|
## Observability
|
|
83
84
|
|
|
@@ -20,9 +20,12 @@ type ContextProposalAnalysis = {
|
|
|
20
20
|
critique: string[];
|
|
21
21
|
risks: string[];
|
|
22
22
|
possibleNoise: string[];
|
|
23
|
+
alternateMissions: string[];
|
|
24
|
+
suppressedCompletedTopics: string[];
|
|
25
|
+
suppressedNegatedTopics: string[];
|
|
23
26
|
};
|
|
24
27
|
|
|
25
|
-
type
|
|
28
|
+
type ContextProposalAlternate = {
|
|
26
29
|
mission: string;
|
|
27
30
|
scope: string[];
|
|
28
31
|
constraints: string[];
|
|
@@ -33,6 +36,10 @@ type ContextProposal = {
|
|
|
33
36
|
source: "session" | "analyst";
|
|
34
37
|
};
|
|
35
38
|
|
|
39
|
+
type ContextProposal = ContextProposalAlternate & {
|
|
40
|
+
alternateProposals: ContextProposalAlternate[];
|
|
41
|
+
};
|
|
42
|
+
|
|
36
43
|
type ContextProposalDecision = {
|
|
37
44
|
missionAnchor: string;
|
|
38
45
|
goalText: string;
|
|
@@ -41,7 +48,7 @@ type ContextProposalDecision = {
|
|
|
41
48
|
|
|
42
49
|
type ExistingWorkflowDecision =
|
|
43
50
|
| { action: "continue"; currentMissionAnchor: string }
|
|
44
|
-
| { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
|
|
51
|
+
| { action: "refocus"; currentMissionAnchor: string; missionAnchor: string; proposal: ContextProposal };
|
|
45
52
|
|
|
46
53
|
type ActiveWorkflowProposalAssessment = {
|
|
47
54
|
action: "continue" | "refocus" | "unclear";
|
|
@@ -55,6 +62,7 @@ type ExistingWorkflowChooserOptions = {
|
|
|
55
62
|
comparison?: "strict" | "semantic";
|
|
56
63
|
proposedMissionLabel?: string;
|
|
57
64
|
refocusChoiceLabel?: string;
|
|
65
|
+
alternateChoiceLabel?: string;
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
type DriverContext = {
|
|
@@ -115,11 +123,13 @@ export type CompletionDriverDeps = {
|
|
|
115
123
|
missionAnchorsLikelyEquivalent: (left: string, right: string) => boolean;
|
|
116
124
|
missionAnchorsStrictlyEquivalent: (left: string, right: string) => boolean;
|
|
117
125
|
shouldTreatBareActiveWorkflowProposalAsClearRefocus: (proposal: ContextProposal) => boolean;
|
|
126
|
+
activateCompletionRoutingForRoot: (root: string | undefined) => void;
|
|
118
127
|
maybeWriteTestSnapshot: (targetPath: string | undefined, content: string) => void;
|
|
119
128
|
completionTestDriverPromptPath: () => string | undefined;
|
|
120
129
|
completionTestAutoContinuePromptPath: () => string | undefined;
|
|
121
130
|
completionTestExistingWorkflowChooserSnapshotPath: () => string | undefined;
|
|
122
131
|
completionTestWorkflowActionOverride: () => "continue" | "refocus" | "cancel" | undefined;
|
|
132
|
+
completionTestWorkflowMissionOverride: () => string | undefined;
|
|
123
133
|
shouldSkipDriverKickoffForTests: () => boolean;
|
|
124
134
|
shouldTestAutoContinueOnSessionStart: () => boolean;
|
|
125
135
|
};
|
|
@@ -241,6 +251,14 @@ function rememberParkedDriverContinuation(rootKey: string, fingerprint: string):
|
|
|
241
251
|
tracker.inFlight = false;
|
|
242
252
|
}
|
|
243
253
|
|
|
254
|
+
function summarizeProposalForChoice(proposal: ContextProposalAlternate): string {
|
|
255
|
+
const parts: string[] = [`Mission\n${proposal.mission}`];
|
|
256
|
+
if (proposal.scope.length > 0) parts.push(`Scope\n- ${proposal.scope.slice(0, 2).join("\n- ")}`);
|
|
257
|
+
if (proposal.constraints.length > 0) parts.push(`Constraints\n- ${proposal.constraints.slice(0, 1).join("\n- ")}`);
|
|
258
|
+
if (proposal.acceptance.length > 0) parts.push(`Acceptance\n- ${proposal.acceptance.slice(0, 1).join("\n- ")}`);
|
|
259
|
+
return parts.join("\n\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
244
262
|
async function queueCompletionDriverPrompt(
|
|
245
263
|
pi: ExtensionAPI,
|
|
246
264
|
ctx: { cwd: string; hasUI: boolean; ui: any },
|
|
@@ -379,14 +397,17 @@ async function confirmExistingWorkflowProposal(
|
|
|
379
397
|
): Promise<ExistingWorkflowDecision | undefined> {
|
|
380
398
|
const currentMission = currentMissionAnchor(snapshot);
|
|
381
399
|
const comparison = options.comparison ?? "semantic";
|
|
382
|
-
const
|
|
400
|
+
const candidateProposals = [proposal, ...(proposal.alternateProposals ?? [])].filter((candidate, index, list) =>
|
|
401
|
+
list.findIndex((other) => deps.missionAnchorsStrictlyEquivalent(other.mission, candidate.mission)) === index,
|
|
402
|
+
);
|
|
403
|
+
const missionMatches = (candidate: ContextProposalAlternate): boolean =>
|
|
383
404
|
comparison === "strict"
|
|
384
|
-
? deps.missionAnchorsStrictlyEquivalent(currentMission,
|
|
385
|
-
: deps.missionAnchorsLikelyEquivalent(currentMission,
|
|
386
|
-
if (
|
|
405
|
+
? deps.missionAnchorsStrictlyEquivalent(currentMission, candidate.mission)
|
|
406
|
+
: deps.missionAnchorsLikelyEquivalent(currentMission, candidate.mission);
|
|
407
|
+
if (candidateProposals.some((candidate) => missionMatches(candidate))) {
|
|
387
408
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
388
409
|
}
|
|
389
|
-
const
|
|
410
|
+
const titleLines = [
|
|
390
411
|
"Existing completion workflow found",
|
|
391
412
|
"",
|
|
392
413
|
options.intro ?? "A workflow is already in progress. Choose how /cook should proceed:",
|
|
@@ -394,24 +415,42 @@ async function confirmExistingWorkflowProposal(
|
|
|
394
415
|
"Current mission",
|
|
395
416
|
currentMission,
|
|
396
417
|
"",
|
|
397
|
-
options.proposedMissionLabel ?? "
|
|
418
|
+
options.proposedMissionLabel ?? "Primary proposed mission",
|
|
398
419
|
proposal.mission,
|
|
399
|
-
]
|
|
420
|
+
];
|
|
421
|
+
if (candidateProposals.length > 1) {
|
|
422
|
+
titleLines.push("", "Alternate recent missions", ...candidateProposals.slice(1).map((candidate) => candidate.mission));
|
|
423
|
+
}
|
|
424
|
+
const title = titleLines.join("\n");
|
|
400
425
|
const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
const buildRefocusChoice = (candidate: ContextProposalAlternate, variant: "primary" | "alternate") =>
|
|
427
|
+
variant === "primary"
|
|
428
|
+
? `${options.refocusChoiceLabel ?? "Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."}\n\n${summarizeProposalForChoice(candidate)}`
|
|
429
|
+
: `${options.alternateChoiceLabel ?? "Start alternate workflow from recent discussion\n\nReview this alternate replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state."}\n\n${summarizeProposalForChoice(candidate)}`;
|
|
430
|
+
const refocusChoices = candidateProposals.map((candidate, index) => buildRefocusChoice(candidate, index === 0 ? "primary" : "alternate"));
|
|
404
431
|
const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${deps.mainChatRerunGuidance}`;
|
|
405
432
|
deps.maybeWriteTestSnapshot(
|
|
406
433
|
deps.completionTestExistingWorkflowChooserSnapshotPath(),
|
|
407
|
-
`${JSON.stringify({ title, choices: [continueChoice,
|
|
434
|
+
`${JSON.stringify({ title, candidateMissions: candidateProposals.map((candidate) => candidate.mission), choices: [continueChoice, ...refocusChoices, cancelChoice] }, null, 2)}\n`,
|
|
408
435
|
);
|
|
436
|
+
const missionOverride = deps.completionTestWorkflowMissionOverride();
|
|
437
|
+
if (missionOverride) {
|
|
438
|
+
const matched = candidateProposals.find((candidate) => deps.missionAnchorsStrictlyEquivalent(candidate.mission, missionOverride));
|
|
439
|
+
if (matched) {
|
|
440
|
+
return {
|
|
441
|
+
action: "refocus",
|
|
442
|
+
currentMissionAnchor: currentMission,
|
|
443
|
+
missionAnchor: matched.mission,
|
|
444
|
+
proposal: { ...matched, alternateProposals: [] },
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
409
448
|
const actionOverride = deps.completionTestWorkflowActionOverride();
|
|
410
449
|
if (actionOverride === "continue") {
|
|
411
450
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
412
451
|
}
|
|
413
452
|
if (actionOverride === "refocus") {
|
|
414
|
-
return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
|
|
453
|
+
return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission, proposal };
|
|
415
454
|
}
|
|
416
455
|
if (actionOverride === "cancel") return undefined;
|
|
417
456
|
if (!deps.getCtxHasUI(ctx)) {
|
|
@@ -421,10 +460,20 @@ async function confirmExistingWorkflowProposal(
|
|
|
421
460
|
if (!ui) {
|
|
422
461
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
423
462
|
}
|
|
424
|
-
const choice = await ui.select(title, [continueChoice,
|
|
463
|
+
const choice = await ui.select(title, [continueChoice, ...refocusChoices, cancelChoice]);
|
|
425
464
|
if (!choice || choice === cancelChoice) return undefined;
|
|
426
|
-
if (choice ===
|
|
427
|
-
return { action: "
|
|
465
|
+
if (choice === continueChoice) {
|
|
466
|
+
return { action: "continue", currentMissionAnchor: currentMission };
|
|
467
|
+
}
|
|
468
|
+
const matchedIndex = refocusChoices.indexOf(choice);
|
|
469
|
+
if (matchedIndex >= 0) {
|
|
470
|
+
const selected = candidateProposals[matchedIndex];
|
|
471
|
+
return {
|
|
472
|
+
action: "refocus",
|
|
473
|
+
currentMissionAnchor: currentMission,
|
|
474
|
+
missionAnchor: selected.mission,
|
|
475
|
+
proposal: matchedIndex === 0 ? proposal : { ...selected, alternateProposals: [] },
|
|
476
|
+
};
|
|
428
477
|
}
|
|
429
478
|
return { action: "continue", currentMissionAnchor: currentMission };
|
|
430
479
|
}
|
|
@@ -534,6 +583,7 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
534
583
|
deps.emitCommandText(ctx, "Failed to load completion workflow state", "error");
|
|
535
584
|
return;
|
|
536
585
|
}
|
|
586
|
+
deps.activateCompletionRoutingForRoot(snapshot.files.root);
|
|
537
587
|
if (!goal) {
|
|
538
588
|
if (workflowDone) {
|
|
539
589
|
const projectName = path.basename(snapshot.files.root);
|
|
@@ -557,15 +607,19 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
557
607
|
deps.emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
|
|
558
608
|
} else {
|
|
559
609
|
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, deps);
|
|
560
|
-
if (assessment.action
|
|
610
|
+
if (!assessment.proposal || assessment.action === "continue") {
|
|
561
611
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
562
612
|
return;
|
|
563
613
|
}
|
|
564
614
|
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, deps, {
|
|
565
|
-
intro:
|
|
615
|
+
intro:
|
|
616
|
+
assessment.action === "refocus"
|
|
617
|
+
? "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:"
|
|
618
|
+
: "Recent discussion may point to a different implementation goal. Review the current mission and the latest inferred mission before deciding how /cook should proceed:",
|
|
566
619
|
proposedMissionLabel: "Proposed mission from recent discussion",
|
|
567
620
|
refocusChoiceLabel:
|
|
568
621
|
"Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
|
|
622
|
+
comparison: assessment.action === "refocus" ? "semantic" : "strict",
|
|
569
623
|
});
|
|
570
624
|
if (!decision) {
|
|
571
625
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation", deps), "info");
|
|
@@ -575,8 +629,12 @@ export function registerCookCommand(pi: ExtensionAPI, deps: CompletionDriverDeps
|
|
|
575
629
|
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot, deps);
|
|
576
630
|
return;
|
|
577
631
|
}
|
|
578
|
-
const
|
|
579
|
-
|
|
632
|
+
const selectedProposal = decision.proposal;
|
|
633
|
+
const proposalDecision = await deps.confirmContextProposal(ctx, selectedProposal, {
|
|
634
|
+
title:
|
|
635
|
+
assessment.action === "refocus"
|
|
636
|
+
? "Start the replacement workflow from recent discussion?"
|
|
637
|
+
: "Start the latest inferred workflow from recent discussion?",
|
|
580
638
|
});
|
|
581
639
|
if (!proposalDecision) {
|
|
582
640
|
deps.emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal", deps), "info");
|
|
@@ -145,6 +145,10 @@ function completionTestWorkflowActionOverride(): "continue" | "refocus" | "cance
|
|
|
145
145
|
return raw === "continue" || raw === "refocus" || raw === "cancel" ? raw : undefined;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function completionTestWorkflowMissionOverride(): string | undefined {
|
|
149
|
+
return asString(process.env.PI_COMPLETION_EXISTING_WORKFLOW_MISSION);
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
function shouldSkipDriverKickoffForTests(): boolean {
|
|
149
153
|
return process.env.PI_COMPLETION_SKIP_DRIVER_KICKOFF === "1";
|
|
150
154
|
}
|
|
@@ -271,6 +275,9 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
|
|
|
271
275
|
proposedMissionAnchor: assessment.proposal?.mission ?? null,
|
|
272
276
|
proposalSource: assessment.proposal?.source ?? null,
|
|
273
277
|
possibleNoise: assessment.proposal?.analysis.possibleNoise ?? [],
|
|
278
|
+
alternateMissions: assessment.proposal?.analysis.alternateMissions ?? [],
|
|
279
|
+
suppressedCompletedTopics: assessment.proposal?.analysis.suppressedCompletedTopics ?? [],
|
|
280
|
+
suppressedNegatedTopics: assessment.proposal?.analysis.suppressedNegatedTopics ?? [],
|
|
274
281
|
scope: assessment.proposal?.scope ?? [],
|
|
275
282
|
constraints: assessment.proposal?.constraints ?? [],
|
|
276
283
|
acceptance: assessment.proposal?.acceptance ?? [],
|
|
@@ -361,14 +368,41 @@ async function deriveCookContextProposal(
|
|
|
361
368
|
projectName: string,
|
|
362
369
|
): Promise<ContextProposal | undefined> {
|
|
363
370
|
const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
|
|
371
|
+
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
372
|
+
const workflowContextLines = snapshot
|
|
373
|
+
? [
|
|
374
|
+
`current mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor) ?? "(none)"}`,
|
|
375
|
+
`continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
|
|
376
|
+
`latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
377
|
+
`latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
|
|
378
|
+
`active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
|
|
379
|
+
`active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
|
|
380
|
+
`verification goal: ${asString(snapshot.verificationEvidence?.goal) ?? "(none)"}`,
|
|
381
|
+
`verification summary: ${asString(snapshot.verificationEvidence?.summary) ?? "(none)"}`,
|
|
382
|
+
]
|
|
383
|
+
: [];
|
|
364
384
|
return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
365
385
|
asString,
|
|
366
386
|
asStringArray,
|
|
387
|
+
workflowContext: snapshot
|
|
388
|
+
? {
|
|
389
|
+
currentMissionAnchor:
|
|
390
|
+
asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor),
|
|
391
|
+
latestCompletedSlice: asString(snapshot.state?.latest_completed_slice),
|
|
392
|
+
latestVerifiedSlice: asString(snapshot.state?.latest_verified_slice),
|
|
393
|
+
activeSliceGoal: asString(snapshot.active?.goal),
|
|
394
|
+
activeSliceWhyNow: asString(snapshot.active?.why_now),
|
|
395
|
+
verificationGoal: asString(snapshot.verificationEvidence?.goal),
|
|
396
|
+
verificationSummary: asString(snapshot.verificationEvidence?.summary),
|
|
397
|
+
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
398
|
+
}
|
|
399
|
+
: undefined,
|
|
367
400
|
analyzeContextProposal: async (entries) =>
|
|
368
401
|
await analyzeContextProposalWithAgent({
|
|
369
402
|
ctx,
|
|
370
403
|
projectName,
|
|
371
404
|
recentEntries: entries,
|
|
405
|
+
workflowContextLines,
|
|
372
406
|
liveRoleActivityByRoot,
|
|
373
407
|
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
374
408
|
safeUiCall,
|
|
@@ -881,6 +915,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
881
915
|
completionTestDriverPromptPath,
|
|
882
916
|
completionTestExistingWorkflowChooserSnapshotPath,
|
|
883
917
|
completionTestWorkflowActionOverride,
|
|
918
|
+
completionTestWorkflowMissionOverride,
|
|
884
919
|
confirmContextProposal,
|
|
885
920
|
deriveCookContextProposal,
|
|
886
921
|
emitCommandText,
|
|
@@ -890,6 +925,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
890
925
|
getCtxUi,
|
|
891
926
|
hasRunningCompletionRole,
|
|
892
927
|
maybeWriteActiveWorkflowRoutingSnapshot,
|
|
928
|
+
activateCompletionRoutingForRoot,
|
|
893
929
|
maybeWriteTestSnapshot,
|
|
894
930
|
missionAnchorsLikelyEquivalent,
|
|
895
931
|
missionAnchorsStrictlyEquivalent,
|
|
@@ -902,7 +938,10 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
902
938
|
pi.on("session_start", async (_event, ctx) => {
|
|
903
939
|
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
904
940
|
if (shouldTestAutoContinueOnSessionStart()) {
|
|
905
|
-
await
|
|
941
|
+
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
942
|
+
if (hasCompletionRoutingActivation(snapshot)) {
|
|
943
|
+
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
944
|
+
}
|
|
906
945
|
}
|
|
907
946
|
});
|
|
908
947
|
|
|
@@ -916,7 +955,9 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
916
955
|
await fsp.rm(snapshot.files.compactionMarkerPath, { force: true });
|
|
917
956
|
}
|
|
918
957
|
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
919
|
-
|
|
958
|
+
if (hasCompletionRoutingActivation(snapshot)) {
|
|
959
|
+
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
960
|
+
}
|
|
920
961
|
});
|
|
921
962
|
|
|
922
963
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
@@ -63,8 +63,23 @@ export function buildContextProposalCritiqueText(analysis: ContextProposalAnalys
|
|
|
63
63
|
lines.push("Possible noise");
|
|
64
64
|
for (const item of analysis.possibleNoise) lines.push(`- ${item}`);
|
|
65
65
|
}
|
|
66
|
+
if (analysis.alternateMissions.length > 0) {
|
|
67
|
+
if (lines.length > 0) lines.push("");
|
|
68
|
+
lines.push("Alternate recent missions");
|
|
69
|
+
for (const item of analysis.alternateMissions) lines.push(`- ${item}`);
|
|
70
|
+
}
|
|
71
|
+
if (analysis.suppressedCompletedTopics.length > 0) {
|
|
72
|
+
if (lines.length > 0) lines.push("");
|
|
73
|
+
lines.push("Suppressed completed topics");
|
|
74
|
+
for (const item of analysis.suppressedCompletedTopics) lines.push(`- ${item}`);
|
|
75
|
+
}
|
|
76
|
+
if (analysis.suppressedNegatedTopics.length > 0) {
|
|
77
|
+
if (lines.length > 0) lines.push("");
|
|
78
|
+
lines.push("Suppressed negated topics");
|
|
79
|
+
for (const item of analysis.suppressedNegatedTopics) lines.push(`- ${item}`);
|
|
80
|
+
}
|
|
66
81
|
if (lines.length === 0) {
|
|
67
|
-
return "No critique, risk,
|
|
82
|
+
return "No critique, risk, noise, alternate-mission, or suppression notes were derived for this startup proposal.";
|
|
68
83
|
}
|
|
69
84
|
return lines.join("\n");
|
|
70
85
|
}
|
|
@@ -101,6 +116,9 @@ export function buildContextProposalContinuationReason(
|
|
|
101
116
|
analysis.critique.length > 0 ? `accepted critique=${deps.truncateInline(analysis.critique.join(" | "), 160)}` : "accepted critique=none",
|
|
102
117
|
summarizeContextProposalAnalysisItems("risks", analysis.risks, deps.truncateInline),
|
|
103
118
|
summarizeContextProposalAnalysisItems("possible_noise", analysis.possibleNoise, deps.truncateInline),
|
|
119
|
+
summarizeContextProposalAnalysisItems("alternate_missions", analysis.alternateMissions, deps.truncateInline),
|
|
120
|
+
summarizeContextProposalAnalysisItems("suppressed_completed", analysis.suppressedCompletedTopics, deps.truncateInline),
|
|
121
|
+
summarizeContextProposalAnalysisItems("suppressed_negated", analysis.suppressedNegatedTopics, deps.truncateInline),
|
|
104
122
|
].filter((part): part is string => Boolean(part));
|
|
105
123
|
return `${prefix} ${deps.truncateInline(goalText, 220)} | startup routing: task_type=${analysis.taskType ?? deps.defaultTaskType}; evaluation_profile=${analysis.evaluationProfile ?? deps.defaultEvaluationProfile}; critique outcome=${critiqueParts.join("; ")}`;
|
|
106
124
|
}
|
|
@@ -177,8 +195,14 @@ export function buildContextProposalConfirmationSelectItems(layout: ContextPropo
|
|
|
177
195
|
}));
|
|
178
196
|
}
|
|
179
197
|
|
|
180
|
-
export function buildContextProposalAnalystPrompt(projectName: string, discussion: string): string {
|
|
181
|
-
const lines = [
|
|
198
|
+
export function buildContextProposalAnalystPrompt(projectName: string, discussion: string, contextLines: string[] = []): string {
|
|
199
|
+
const lines = [
|
|
200
|
+
`Project: ${projectName}`,
|
|
201
|
+
"Infer the current implementation mission from the discussion.",
|
|
202
|
+
"Prefer the latest clear user implementation intent over older background context.",
|
|
203
|
+
"Treat stale, completed, or explicitly negated topics as context to ignore unless the latest discussion clearly reopens them.",
|
|
204
|
+
];
|
|
205
|
+
if (contextLines.length > 0) lines.push("", "Canonical workflow context:", ...contextLines);
|
|
182
206
|
lines.push("", "Recent discussion:", discussion || "(none)");
|
|
183
207
|
return lines.join("\n");
|
|
184
208
|
}
|
|
@@ -14,9 +14,12 @@ export type ContextProposalAnalysis = {
|
|
|
14
14
|
critique: string[];
|
|
15
15
|
risks: string[];
|
|
16
16
|
possibleNoise: string[];
|
|
17
|
+
alternateMissions: string[];
|
|
18
|
+
suppressedCompletedTopics: string[];
|
|
19
|
+
suppressedNegatedTopics: string[];
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
export type
|
|
22
|
+
export type ContextProposalAlternate = {
|
|
20
23
|
mission: string;
|
|
21
24
|
scope: string[];
|
|
22
25
|
constraints: string[];
|
|
@@ -27,6 +30,10 @@ export type ContextProposal = {
|
|
|
27
30
|
source: "session" | "analyst";
|
|
28
31
|
};
|
|
29
32
|
|
|
33
|
+
export type ContextProposal = ContextProposalAlternate & {
|
|
34
|
+
alternateProposals: ContextProposalAlternate[];
|
|
35
|
+
};
|
|
36
|
+
|
|
30
37
|
export type ContextProposalSection = "mission" | "scope" | "constraints" | "acceptance" | "critique" | "risks";
|
|
31
38
|
|
|
32
39
|
export type RecentDiscussionEntry = {
|
|
@@ -67,6 +74,17 @@ export type ContextProposalConfirmOptions = {
|
|
|
67
74
|
nonInteractiveBehavior?: "accept" | "cancel";
|
|
68
75
|
};
|
|
69
76
|
|
|
77
|
+
export type ContextProposalWorkflowContext = {
|
|
78
|
+
currentMissionAnchor?: string;
|
|
79
|
+
latestCompletedSlice?: string;
|
|
80
|
+
latestVerifiedSlice?: string;
|
|
81
|
+
activeSliceGoal?: string;
|
|
82
|
+
activeSliceWhyNow?: string;
|
|
83
|
+
verificationGoal?: string;
|
|
84
|
+
verificationSummary?: string;
|
|
85
|
+
continuationPolicy?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
70
88
|
type ProposalCommonDeps = {
|
|
71
89
|
asString: (value: unknown) => string | undefined;
|
|
72
90
|
asStringArray: (value: unknown) => string[];
|
|
@@ -324,6 +342,41 @@ export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[
|
|
|
324
342
|
.join("\n\n");
|
|
325
343
|
}
|
|
326
344
|
|
|
345
|
+
const RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX =
|
|
346
|
+
/(?:\b(?:fix|update|add|remove|restore|refactor|ship|support|wire|route|rewrite|replace|preserve|filter|separate|refresh|reroute|suppress|align|convert|reconcile|repair|correct|implement|build|land|block|allow|keep|edit|document|write)\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|過濾|过滤|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
|
|
347
|
+
|
|
348
|
+
function hasRecentDiscussionImplementationIntent(text: string, stripCodeBlocksFn: (text: string) => string): boolean {
|
|
349
|
+
const cleaned = stripCodeBlocksFn(text).replace(/\r/g, " ").trim();
|
|
350
|
+
if (!cleaned) return false;
|
|
351
|
+
return hasStructuredContextProposalSignal(cleaned, stripCodeBlocksFn) || RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX.test(cleaned);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function recentDiscussionWindows(
|
|
355
|
+
recentEntries: RecentDiscussionEntry[],
|
|
356
|
+
stripCodeBlocksFn: (text: string) => string,
|
|
357
|
+
): RecentDiscussionEntry[][] {
|
|
358
|
+
if (recentEntries.length === 0) return [];
|
|
359
|
+
const windows: RecentDiscussionEntry[][] = [];
|
|
360
|
+
const seen = new Set<string>();
|
|
361
|
+
const pushWindow = (entries: RecentDiscussionEntry[]) => {
|
|
362
|
+
if (entries.length === 0) return;
|
|
363
|
+
const key = entries.map((entry) => `${entry.role}:${entry.text}`).join("\n---\n");
|
|
364
|
+
if (seen.has(key)) return;
|
|
365
|
+
seen.add(key);
|
|
366
|
+
windows.push(entries);
|
|
367
|
+
};
|
|
368
|
+
const latestEntry = recentEntries[0];
|
|
369
|
+
if (hasRecentDiscussionImplementationIntent(latestEntry.text, stripCodeBlocksFn)) {
|
|
370
|
+
pushWindow([latestEntry]);
|
|
371
|
+
}
|
|
372
|
+
const recentIntentWindow = recentEntries.filter((entry, index) => index < 3);
|
|
373
|
+
if (recentIntentWindow.some((entry) => hasRecentDiscussionImplementationIntent(entry.text, stripCodeBlocksFn))) {
|
|
374
|
+
pushWindow(recentIntentWindow);
|
|
375
|
+
}
|
|
376
|
+
pushWindow(recentEntries);
|
|
377
|
+
return windows;
|
|
378
|
+
}
|
|
379
|
+
|
|
327
380
|
export function extractJsonObjectFromText(text: string): string | undefined {
|
|
328
381
|
const trimmed = text.trim();
|
|
329
382
|
if (!trimmed) return undefined;
|
|
@@ -455,12 +508,26 @@ export function buildContextProposalAnalysis(args: {
|
|
|
455
508
|
critique?: string[];
|
|
456
509
|
risks?: string[];
|
|
457
510
|
possibleNoise?: string[];
|
|
511
|
+
alternateMissions?: string[];
|
|
512
|
+
suppressedCompletedTopics?: string[];
|
|
513
|
+
suppressedNegatedTopics?: string[];
|
|
458
514
|
hintTexts?: string[];
|
|
459
515
|
}, deps: Pick<ProposalCommonDeps, "asString">): ContextProposalAnalysis {
|
|
460
516
|
const critique = uniqueProposalItems(args.critique ?? []);
|
|
461
517
|
const risks = uniqueProposalItems(args.risks ?? []);
|
|
462
518
|
const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
|
|
463
|
-
const
|
|
519
|
+
const alternateMissions = uniqueProposalItems(args.alternateMissions ?? []);
|
|
520
|
+
const suppressedCompletedTopics = uniqueProposalItems(args.suppressedCompletedTopics ?? []);
|
|
521
|
+
const suppressedNegatedTopics = uniqueProposalItems(args.suppressedNegatedTopics ?? []);
|
|
522
|
+
const hintTexts = [
|
|
523
|
+
...(args.hintTexts ?? []),
|
|
524
|
+
...critique,
|
|
525
|
+
...risks,
|
|
526
|
+
...possibleNoise,
|
|
527
|
+
...alternateMissions,
|
|
528
|
+
...suppressedCompletedTopics,
|
|
529
|
+
...suppressedNegatedTopics,
|
|
530
|
+
];
|
|
464
531
|
const taskType = normalizeContextProposalTaskTypeHint(args.taskType, deps.asString) ?? inferContextProposalTaskType(hintTexts);
|
|
465
532
|
const evaluationProfile =
|
|
466
533
|
normalizeContextProposalEvaluationProfileHint(args.evaluationProfile, deps.asString) ??
|
|
@@ -471,6 +538,9 @@ export function buildContextProposalAnalysis(args: {
|
|
|
471
538
|
critique,
|
|
472
539
|
risks,
|
|
473
540
|
possibleNoise,
|
|
541
|
+
alternateMissions,
|
|
542
|
+
suppressedCompletedTopics,
|
|
543
|
+
suppressedNegatedTopics,
|
|
474
544
|
};
|
|
475
545
|
}
|
|
476
546
|
|
|
@@ -481,18 +551,33 @@ function mergeContextProposalAnalysis(
|
|
|
481
551
|
const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
|
|
482
552
|
const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
|
|
483
553
|
const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
|
|
554
|
+
const alternateMissions = uniqueProposalItems(sources.flatMap((source) => source?.alternateMissions ?? []));
|
|
555
|
+
const suppressedCompletedTopics = uniqueProposalItems(sources.flatMap((source) => source?.suppressedCompletedTopics ?? []));
|
|
556
|
+
const suppressedNegatedTopics = uniqueProposalItems(sources.flatMap((source) => source?.suppressedNegatedTopics ?? []));
|
|
557
|
+
const mergedHints = [
|
|
558
|
+
...hintTexts,
|
|
559
|
+
...critique,
|
|
560
|
+
...risks,
|
|
561
|
+
...possibleNoise,
|
|
562
|
+
...alternateMissions,
|
|
563
|
+
...suppressedCompletedTopics,
|
|
564
|
+
...suppressedNegatedTopics,
|
|
565
|
+
];
|
|
484
566
|
const taskType =
|
|
485
567
|
sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
|
|
486
|
-
inferContextProposalTaskType(
|
|
568
|
+
inferContextProposalTaskType(mergedHints);
|
|
487
569
|
const evaluationProfile =
|
|
488
570
|
sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
|
|
489
|
-
inferContextProposalEvaluationProfile(
|
|
571
|
+
inferContextProposalEvaluationProfile(mergedHints, taskType);
|
|
490
572
|
return {
|
|
491
573
|
taskType,
|
|
492
574
|
evaluationProfile,
|
|
493
575
|
critique,
|
|
494
576
|
risks,
|
|
495
577
|
possibleNoise,
|
|
578
|
+
alternateMissions,
|
|
579
|
+
suppressedCompletedTopics,
|
|
580
|
+
suppressedNegatedTopics,
|
|
496
581
|
};
|
|
497
582
|
}
|
|
498
583
|
|
|
@@ -507,6 +592,9 @@ export function finalizeContextProposalAnalysis(
|
|
|
507
592
|
critique: merged.critique,
|
|
508
593
|
risks: merged.risks,
|
|
509
594
|
possibleNoise: merged.possibleNoise,
|
|
595
|
+
alternateMissions: merged.alternateMissions,
|
|
596
|
+
suppressedCompletedTopics: merged.suppressedCompletedTopics,
|
|
597
|
+
suppressedNegatedTopics: merged.suppressedNegatedTopics,
|
|
510
598
|
};
|
|
511
599
|
}
|
|
512
600
|
|
|
@@ -610,7 +698,109 @@ function finalizeContextProposal(proposal: ContextProposal, projectName: string,
|
|
|
610
698
|
};
|
|
611
699
|
}
|
|
612
700
|
|
|
701
|
+
function proposalLikelyReopensCompletedWork(proposal: ContextProposal): boolean {
|
|
702
|
+
const corpus = [proposal.mission, proposal.basisPreview, ...proposal.scope, ...proposal.constraints, ...proposal.acceptance]
|
|
703
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
704
|
+
.filter(Boolean)
|
|
705
|
+
.join("\n");
|
|
706
|
+
return /(again|reopen|follow[- ]?up|next round|another round|rerun|revisit|再次|重新|下一輪|下一轮|延續|延续|回歸|回归)/iu.test(corpus);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function missionTextOverlapsTopic(mission: string, topic: string): boolean {
|
|
710
|
+
if (!mission || !topic) return false;
|
|
711
|
+
const missionTokens = missionAnchorSemanticTokens(mission);
|
|
712
|
+
const topicTokens = missionAnchorSemanticTokens(topic);
|
|
713
|
+
if (missionTokens.length === 0 || topicTokens.length === 0) return false;
|
|
714
|
+
const topicSet = new Set(topicTokens);
|
|
715
|
+
const overlap = missionTokens.filter((token) => topicSet.has(token));
|
|
716
|
+
return overlap.length >= Math.min(2, Math.min(missionTokens.length, topicTokens.length));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function proposalOverlapsTopic(proposal: ContextProposal, topic: string): boolean {
|
|
720
|
+
if (!topic.trim()) return false;
|
|
721
|
+
if (missionTextOverlapsTopic(proposal.mission, topic)) return true;
|
|
722
|
+
const bodyTexts = [proposal.basisPreview, ...proposal.scope, ...proposal.constraints, ...proposal.acceptance].filter(Boolean);
|
|
723
|
+
return bodyTexts.some((text) => missionTextOverlapsTopic(text, topic) || missionTextOverlapsTopic(topic, text));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function extractSuppressedNegatedTopics(proposal: ContextProposal): string[] {
|
|
727
|
+
return uniqueProposalItems(
|
|
728
|
+
proposal.constraints.filter((item) => looksLikeConstraint(item) && CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalizeProposalLine(item))),
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function applyWorkflowContextToProposal(
|
|
733
|
+
proposal: ContextProposal | undefined,
|
|
734
|
+
context: ContextProposalWorkflowContext | undefined,
|
|
735
|
+
deps: ProposalCommonDeps,
|
|
736
|
+
): ContextProposal | undefined {
|
|
737
|
+
if (!proposal) return proposal;
|
|
738
|
+
const possibleNoise = [...proposal.analysis.possibleNoise];
|
|
739
|
+
const alternateMissions = [...proposal.analysis.alternateMissions];
|
|
740
|
+
const suppressedCompletedTopics = [...proposal.analysis.suppressedCompletedTopics];
|
|
741
|
+
const suppressedNegatedTopics = [...proposal.analysis.suppressedNegatedTopics, ...extractSuppressedNegatedTopics(proposal)];
|
|
742
|
+
if (!context) {
|
|
743
|
+
return {
|
|
744
|
+
...proposal,
|
|
745
|
+
analysis: finalizeContextProposalAnalysis(
|
|
746
|
+
{
|
|
747
|
+
...proposal.analysis,
|
|
748
|
+
possibleNoise,
|
|
749
|
+
alternateMissions,
|
|
750
|
+
suppressedCompletedTopics,
|
|
751
|
+
suppressedNegatedTopics,
|
|
752
|
+
},
|
|
753
|
+
[proposal.goalText, proposal.mission],
|
|
754
|
+
),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const completedTopics = [
|
|
758
|
+
context.latestCompletedSlice?.trim(),
|
|
759
|
+
context.latestVerifiedSlice?.trim(),
|
|
760
|
+
context.verificationGoal?.trim(),
|
|
761
|
+
context.verificationSummary?.trim(),
|
|
762
|
+
].filter((value): value is string => Boolean(value));
|
|
763
|
+
for (const topic of completedTopics) {
|
|
764
|
+
if (proposalOverlapsTopic(proposal, topic) && !proposalLikelyReopensCompletedWork(proposal)) {
|
|
765
|
+
suppressedCompletedTopics.push(topic);
|
|
766
|
+
possibleNoise.push(`already completed: ${topic}`);
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const activeTopics = [context.activeSliceGoal?.trim(), context.activeSliceWhyNow?.trim()].filter((value): value is string => Boolean(value));
|
|
771
|
+
for (const topic of activeTopics) {
|
|
772
|
+
if (proposalOverlapsTopic(proposal, topic) && proposal.analysis.alternateMissions.length === 0) {
|
|
773
|
+
possibleNoise.push(`overlaps canonical active slice: ${topic}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const currentMissionAnchor = context.currentMissionAnchor?.trim();
|
|
777
|
+
if (
|
|
778
|
+
context.continuationPolicy === "done" &&
|
|
779
|
+
currentMissionAnchor &&
|
|
780
|
+
deps.missionAnchorsStrictlyEquivalent(proposal.mission, currentMissionAnchor) &&
|
|
781
|
+
!proposalLikelyReopensCompletedWork(proposal)
|
|
782
|
+
) {
|
|
783
|
+
suppressedCompletedTopics.push(currentMissionAnchor);
|
|
784
|
+
possibleNoise.push(`historical completed mission: ${currentMissionAnchor}`);
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...proposal,
|
|
789
|
+
analysis: finalizeContextProposalAnalysis(
|
|
790
|
+
{
|
|
791
|
+
...proposal.analysis,
|
|
792
|
+
possibleNoise,
|
|
793
|
+
alternateMissions,
|
|
794
|
+
suppressedCompletedTopics,
|
|
795
|
+
suppressedNegatedTopics,
|
|
796
|
+
},
|
|
797
|
+
[proposal.goalText, proposal.mission, ...completedTopics, ...activeTopics, currentMissionAnchor ?? ""],
|
|
798
|
+
),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
613
802
|
export function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
|
|
803
|
+
if (proposal.analysis.alternateMissions.length > 0) return false;
|
|
614
804
|
if (proposal.source === "session") {
|
|
615
805
|
return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
|
|
616
806
|
}
|
|
@@ -654,6 +844,7 @@ export function parseContextProposalAnalystOutput(
|
|
|
654
844
|
const scope = uniqueProposalItems(deps.asStringArray(parsed.scope));
|
|
655
845
|
const constraints = uniqueProposalItems(deps.asStringArray(parsed.constraints));
|
|
656
846
|
const acceptance = uniqueProposalItems(deps.asStringArray(parsed.acceptance));
|
|
847
|
+
const alternateMissions = deps.asStringArray(parsed.alternate_missions ?? parsed.alternateMissions);
|
|
657
848
|
const analysis = buildContextProposalAnalysis(
|
|
658
849
|
{
|
|
659
850
|
taskType: parsed.task_type ?? parsed.taskType,
|
|
@@ -661,6 +852,9 @@ export function parseContextProposalAnalystOutput(
|
|
|
661
852
|
critique: deps.asStringArray(parsed.critique),
|
|
662
853
|
risks: deps.asStringArray(parsed.risks ?? parsed.risk),
|
|
663
854
|
possibleNoise: deps.asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
855
|
+
alternateMissions,
|
|
856
|
+
suppressedCompletedTopics: deps.asStringArray(parsed.completed_topics ?? parsed.completedTopics),
|
|
857
|
+
suppressedNegatedTopics: deps.asStringArray(parsed.negated_topics ?? parsed.negatedTopics),
|
|
664
858
|
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
665
859
|
},
|
|
666
860
|
deps,
|
|
@@ -676,6 +870,16 @@ export function parseContextProposalAnalystOutput(
|
|
|
676
870
|
goalText,
|
|
677
871
|
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
678
872
|
source: "analyst",
|
|
873
|
+
alternateProposals: alternateMissions.map((alternateMission) => ({
|
|
874
|
+
mission: alternateMission,
|
|
875
|
+
scope: [],
|
|
876
|
+
constraints: [],
|
|
877
|
+
acceptance: [],
|
|
878
|
+
analysis: finalizeContextProposalAnalysis(undefined, [alternateMission]),
|
|
879
|
+
goalText: buildContextProposalGoalText({ mission: alternateMission, scope: [], constraints: [], acceptance: [] }),
|
|
880
|
+
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
881
|
+
source: "analyst",
|
|
882
|
+
})),
|
|
679
883
|
},
|
|
680
884
|
projectName,
|
|
681
885
|
deps,
|
|
@@ -685,9 +889,10 @@ export function parseContextProposalAnalystOutput(
|
|
|
685
889
|
export function buildContextProposalAnalystPromptFromEntries(
|
|
686
890
|
projectName: string,
|
|
687
891
|
recentEntries: RecentDiscussionEntry[],
|
|
892
|
+
contextLines: string[] = [],
|
|
688
893
|
serializeEntries: (entries: RecentDiscussionEntry[]) => string = serializeRecentDiscussionEntries,
|
|
689
894
|
): string {
|
|
690
|
-
return buildContextProposalAnalystPrompt(projectName, serializeEntries(recentEntries));
|
|
895
|
+
return buildContextProposalAnalystPrompt(projectName, serializeEntries(recentEntries), contextLines);
|
|
691
896
|
}
|
|
692
897
|
|
|
693
898
|
export function parseContextProposal(text: string, projectName: string, deps: ProposalParseDeps): ContextProposal | undefined {
|
|
@@ -836,6 +1041,7 @@ export function parseContextProposal(text: string, projectName: string, deps: Pr
|
|
|
836
1041
|
goalText,
|
|
837
1042
|
basisPreview,
|
|
838
1043
|
source: "session",
|
|
1044
|
+
alternateProposals: [],
|
|
839
1045
|
},
|
|
840
1046
|
projectName,
|
|
841
1047
|
deps,
|
|
@@ -850,7 +1056,26 @@ export function hasStructuredContextProposalSignal(text: string, stripCodeBlocks
|
|
|
850
1056
|
);
|
|
851
1057
|
}
|
|
852
1058
|
|
|
853
|
-
|
|
1059
|
+
function splitStructuredProposalBlocks(text: string): string[] {
|
|
1060
|
+
const lines = text.split("\n");
|
|
1061
|
+
const blocks: string[] = [];
|
|
1062
|
+
let startIndex = 0;
|
|
1063
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1064
|
+
const rawLine = lines[index].trim();
|
|
1065
|
+
const inlineSection = matchInlineProposalSection(rawLine);
|
|
1066
|
+
const headerSection = inlineSection?.section ?? detectProposalSection(rawLine);
|
|
1067
|
+
if (index > 0 && headerSection === "mission") {
|
|
1068
|
+
const block = lines.slice(startIndex, index).join("\n").trim();
|
|
1069
|
+
if (block) blocks.push(block);
|
|
1070
|
+
startIndex = index;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const tail = lines.slice(startIndex).join("\n").trim();
|
|
1074
|
+
if (tail) blocks.push(tail);
|
|
1075
|
+
return blocks;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function parseStrictSingleStructuredSessionProposal(text: string, projectName: string, deps: ProposalParseDeps): ContextProposal | undefined {
|
|
854
1079
|
const cleaned = deps.stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
855
1080
|
if (!cleaned) return undefined;
|
|
856
1081
|
const lines = cleaned
|
|
@@ -902,7 +1127,45 @@ export function parseStrictStructuredSessionProposal(text: string, projectName:
|
|
|
902
1127
|
return undefined;
|
|
903
1128
|
}
|
|
904
1129
|
if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
|
|
905
|
-
return { ...proposal, source: "session" };
|
|
1130
|
+
return { ...proposal, source: "session", alternateProposals: proposal.alternateProposals ?? [] };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export function parseStrictStructuredSessionProposal(text: string, projectName: string, deps: ProposalParseDeps): ContextProposal | undefined {
|
|
1134
|
+
const cleaned = deps.stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
1135
|
+
if (!cleaned) return undefined;
|
|
1136
|
+
const blocks = splitStructuredProposalBlocks(cleaned);
|
|
1137
|
+
const proposals = blocks
|
|
1138
|
+
.map((block) => parseStrictSingleStructuredSessionProposal(block, projectName, deps))
|
|
1139
|
+
.filter((proposal): proposal is ContextProposal => Boolean(proposal));
|
|
1140
|
+
if (proposals.length === 0) return undefined;
|
|
1141
|
+
const primary = proposals[proposals.length - 1];
|
|
1142
|
+
const alternateProposals = proposals
|
|
1143
|
+
.slice(0, -1)
|
|
1144
|
+
.filter((proposal) => !deps.missionAnchorsStrictlyEquivalent(proposal.mission, primary.mission))
|
|
1145
|
+
.map((proposal) => ({
|
|
1146
|
+
mission: proposal.mission,
|
|
1147
|
+
scope: proposal.scope,
|
|
1148
|
+
constraints: proposal.constraints,
|
|
1149
|
+
acceptance: proposal.acceptance,
|
|
1150
|
+
analysis: proposal.analysis,
|
|
1151
|
+
goalText: proposal.goalText,
|
|
1152
|
+
basisPreview: proposal.basisPreview,
|
|
1153
|
+
source: proposal.source,
|
|
1154
|
+
}));
|
|
1155
|
+
const alternateMissions = uniqueProposalItems(alternateProposals.map((proposal) => proposal.mission));
|
|
1156
|
+
if (alternateMissions.length === 0) return { ...primary, alternateProposals: [] };
|
|
1157
|
+
return {
|
|
1158
|
+
...primary,
|
|
1159
|
+
alternateProposals,
|
|
1160
|
+
analysis: finalizeContextProposalAnalysis(
|
|
1161
|
+
{
|
|
1162
|
+
...primary.analysis,
|
|
1163
|
+
alternateMissions,
|
|
1164
|
+
possibleNoise: [...primary.analysis.possibleNoise, ...alternateMissions.map((mission) => `alternate recent mission: ${mission}`)],
|
|
1165
|
+
},
|
|
1166
|
+
[primary.goalText, primary.mission, ...alternateMissions],
|
|
1167
|
+
),
|
|
1168
|
+
};
|
|
906
1169
|
}
|
|
907
1170
|
|
|
908
1171
|
export function extractContextProposalFromStructuredSession(
|
|
@@ -924,11 +1187,21 @@ export async function deriveCookContextProposalFromRecentDiscussion(
|
|
|
924
1187
|
recentEntries: RecentDiscussionEntry[],
|
|
925
1188
|
deps: ProposalParseDeps & {
|
|
926
1189
|
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]) => Promise<ContextProposal | undefined>;
|
|
1190
|
+
workflowContext?: ContextProposalWorkflowContext;
|
|
927
1191
|
},
|
|
928
1192
|
): Promise<ContextProposal | undefined> {
|
|
929
1193
|
if (recentEntries.length === 0) return undefined;
|
|
930
|
-
|
|
931
|
-
|
|
1194
|
+
for (const candidateEntries of recentDiscussionWindows(recentEntries, deps.stripCodeBlocks)) {
|
|
1195
|
+
const analyzed = applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries), deps.workflowContext, deps);
|
|
1196
|
+
if (analyzed) return analyzed;
|
|
1197
|
+
const structured = applyWorkflowContextToProposal(
|
|
1198
|
+
extractContextProposalFromStructuredSession(candidateEntries, projectName, deps),
|
|
1199
|
+
deps.workflowContext,
|
|
1200
|
+
deps,
|
|
1201
|
+
);
|
|
1202
|
+
if (structured) return structured;
|
|
1203
|
+
}
|
|
1204
|
+
return undefined;
|
|
932
1205
|
}
|
|
933
1206
|
|
|
934
1207
|
export function resolveContextProposalConfirmationAction(
|
|
@@ -58,6 +58,7 @@ export type AnalyzeContextProposalWithAgentParams = {
|
|
|
58
58
|
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any };
|
|
59
59
|
projectName: string;
|
|
60
60
|
recentEntries: RecentDiscussionEntry[];
|
|
61
|
+
workflowContextLines?: string[];
|
|
61
62
|
liveRoleActivityByRoot: Map<string, LiveRoleActivity>;
|
|
62
63
|
completionStatusKey: string;
|
|
63
64
|
safeUiCall: (action: () => void) => void;
|
|
@@ -75,14 +76,18 @@ const CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT = [
|
|
|
75
76
|
"You analyze recent /cook startup discussion and return a strict JSON object.",
|
|
76
77
|
"Do not emit markdown, code fences, or commentary.",
|
|
77
78
|
"Return exactly one JSON object with keys: mission, scope, constraints, acceptance, critique, risks, task_type, evaluation_profile, confidence, possible_noise.",
|
|
79
|
+
"You may additionally include optional keys alternate_missions, completed_topics, and negated_topics when they are clearly supported by the discussion and canonical workflow context.",
|
|
78
80
|
"mission must be a concise implementation mission anchor sentence.",
|
|
81
|
+
"Prefer the latest clear user implementation intent over older background context when they differ.",
|
|
82
|
+
"Do not reopen work that the canonical workflow context says is done, completed, historical, or already covered unless the latest discussion clearly asks to revisit it.",
|
|
83
|
+
"Treat stale, weakly related, or explicitly negated topics as noise instead of mission scope.",
|
|
79
84
|
"scope must contain only work items that directly support the mission.",
|
|
80
85
|
"constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
|
|
81
86
|
"acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
|
|
82
87
|
"critique must contain operator-facing cautions, concerns, or reminders that should be shown separately from mission and scope later.",
|
|
83
88
|
"risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
|
|
84
89
|
"task_type and evaluation_profile should be candidate routing hints only; reuse the existing completion vocabulary when it clearly fits instead of inventing new schema names.",
|
|
85
|
-
"possible_noise should list discussion points that look stale, weakly related,
|
|
90
|
+
"possible_noise should list discussion points that look stale, weakly related, unsafe to promote into scope, or already completed elsewhere.",
|
|
86
91
|
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
87
92
|
].join(" ");
|
|
88
93
|
const STARTUP_ANALYST_ROLE = "cook-proposal-analyst";
|
|
@@ -167,7 +172,7 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
|
|
|
167
172
|
const cwd = params.getCtxCwd(ctx);
|
|
168
173
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
169
174
|
const rootKey = completionRootKey(undefined, cwd);
|
|
170
|
-
const prompt = buildContextProposalAnalystPromptFromEntries(projectName, recentEntries);
|
|
175
|
+
const prompt = buildContextProposalAnalystPromptFromEntries(projectName, recentEntries, params.workflowContextLines);
|
|
171
176
|
const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
|
|
172
177
|
const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
173
178
|
const invocation = getPiInvocation(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
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,
|
|
@@ -158,6 +158,85 @@ PY
|
|
|
158
158
|
|
|
159
159
|
rm -rf .agent
|
|
160
160
|
|
|
161
|
+
# No workflow yet: when multiple structured discussions exist, bare /cook should prioritize the latest
|
|
162
|
+
# concrete implementation mission instead of failing closed on older structured context.
|
|
163
|
+
SESSION_ZERO_LATEST_WINDOW="$TMPDIR/session-zero-latest-window.jsonl"
|
|
164
|
+
DISCUSSION_ZERO_LATEST_OLDER=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Keep observability regression coverage truthful.'
|
|
165
|
+
DISCUSSION_ZERO_LATEST_NEWER=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page.'
|
|
166
|
+
DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW="$TMPDIR/context-proposal-latest-window.json"
|
|
167
|
+
python3 - "$SESSION_ZERO_LATEST_WINDOW" "$ROOT" "$DISCUSSION_ZERO_LATEST_OLDER" "$DISCUSSION_ZERO_LATEST_NEWER" <<'PY'
|
|
168
|
+
import json
|
|
169
|
+
import sys
|
|
170
|
+
from pathlib import Path
|
|
171
|
+
|
|
172
|
+
session_path = Path(sys.argv[1])
|
|
173
|
+
cwd = sys.argv[2]
|
|
174
|
+
older = sys.argv[3]
|
|
175
|
+
newer = sys.argv[4]
|
|
176
|
+
session_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
entries = [
|
|
178
|
+
{
|
|
179
|
+
"type": "session",
|
|
180
|
+
"version": 3,
|
|
181
|
+
"id": "11111111-1111-4111-8111-111111111111",
|
|
182
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
183
|
+
"cwd": cwd,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"type": "message",
|
|
187
|
+
"id": "a1b2c3d4",
|
|
188
|
+
"parentId": None,
|
|
189
|
+
"timestamp": "2026-01-01T00:00:01.000Z",
|
|
190
|
+
"message": {
|
|
191
|
+
"role": "user",
|
|
192
|
+
"content": older,
|
|
193
|
+
"timestamp": 1767225601000,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"type": "message",
|
|
198
|
+
"id": "b2c3d4e5",
|
|
199
|
+
"parentId": "a1b2c3d4",
|
|
200
|
+
"timestamp": "2026-01-01T00:00:02.000Z",
|
|
201
|
+
"message": {
|
|
202
|
+
"role": "user",
|
|
203
|
+
"content": newer,
|
|
204
|
+
"timestamp": 1767225602000,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
with session_path.open('w', encoding='utf-8') as fh:
|
|
209
|
+
for entry in entries:
|
|
210
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
211
|
+
PY
|
|
212
|
+
|
|
213
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
214
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
215
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW" \
|
|
216
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
217
|
+
pi --session "$SESSION_ZERO_LATEST_WINDOW" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-latest-window.out" 2>"$TMPDIR/pi-completion-context-proposal-latest-window.err"
|
|
218
|
+
|
|
219
|
+
python3 - "$DISCUSSION_SNAPSHOT_ZERO_LATEST_WINDOW" <<'PY'
|
|
220
|
+
import json
|
|
221
|
+
import sys
|
|
222
|
+
from pathlib import Path
|
|
223
|
+
|
|
224
|
+
mission = 'Fix login redirect callback behavior.'
|
|
225
|
+
proposal = json.loads(Path(sys.argv[1]).read_text())
|
|
226
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
227
|
+
plan = json.loads(Path('.agent/plan.json').read_text())
|
|
228
|
+
active = json.loads(Path('.agent/active-slice.json').read_text())
|
|
229
|
+
|
|
230
|
+
assert proposal['mission'] == mission, 'latest structured discussion should win over older structured context'
|
|
231
|
+
assert proposal['scope'] == ['Update the callback redirect decision logic.'], 'latest structured discussion should preserve the latest scope only'
|
|
232
|
+
assert proposal['analysis']['suppressedNegatedTopics'] == ['Do not refactor the broader auth flow.'], 'latest structured discussion should preserve negated implementation topics separately from the mission'
|
|
233
|
+
assert state['mission_anchor'] == mission, 'latest structured discussion should initialize state.json with the latest mission'
|
|
234
|
+
assert plan['mission_anchor'] == mission, 'latest structured discussion should initialize plan.json with the latest mission'
|
|
235
|
+
assert active['mission_anchor'] == mission, 'latest structured discussion should initialize active-slice.json with the latest mission'
|
|
236
|
+
PY
|
|
237
|
+
|
|
238
|
+
rm -rf .agent
|
|
239
|
+
|
|
161
240
|
# No workflow yet: bare /cook should fail closed when a required structured section is missing and analyst output is unavailable.
|
|
162
241
|
SESSION_ZERO_MISSING="$TMPDIR/session-zero-missing-section.jsonl"
|
|
163
242
|
DISCUSSION_ZERO_MISSING=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\n- Suppress the widget while a completion role is active.\nConstraints:\n- Do not reintroduce any other completion status surface.'
|
|
@@ -182,26 +261,35 @@ assert '/cook failed closed' in output, 'missing-section structured discussion s
|
|
|
182
261
|
assert 'Mission/Scope/Constraints/Acceptance' in output, 'missing-section structured discussion should explain the strict fallback requirement'
|
|
183
262
|
PY
|
|
184
263
|
|
|
185
|
-
# No workflow yet:
|
|
264
|
+
# No workflow yet: when one structured discussion message contains multiple complete mission blocks,
|
|
265
|
+
# bare /cook should prioritize the latest block and preserve earlier blocks as alternate missions.
|
|
186
266
|
SESSION_ZERO_AMBIG="$TMPDIR/session-zero-ambiguous.jsonl"
|
|
187
|
-
DISCUSSION_ZERO_AMBIG=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Update README to match the shipped behavior.\nMission: Ship an unrelated widget overhaul.\nScope:\n- Replace the widget entirely.'
|
|
267
|
+
DISCUSSION_ZERO_AMBIG=$'Mission: Remove the completion status line while keeping the completion widget.\nScope:\n- Keep the non-running completion widget.\nConstraints:\n- Do not reintroduce any other completion status surface.\nAcceptance:\n- Update README to match the shipped behavior.\nMission: Ship an unrelated widget overhaul.\nScope:\n- Replace the widget entirely.\nConstraints:\n- Do not modify the completion widget.\nAcceptance:\n- Land the unrelated overhaul changes only.'
|
|
268
|
+
DISCUSSION_SNAPSHOT_ZERO_AMBIG="$TMPDIR/context-proposal-ambiguous-latest-block.json"
|
|
188
269
|
write_session "$SESSION_ZERO_AMBIG" "$ROOT" "$DISCUSSION_ZERO_AMBIG"
|
|
189
270
|
|
|
190
271
|
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
191
272
|
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
273
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_ZERO_AMBIG" \
|
|
192
274
|
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
193
275
|
pi --session "$SESSION_ZERO_AMBIG" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-ambiguous.out" 2>"$TMPDIR/pi-completion-context-proposal-ambiguous.err"
|
|
194
276
|
|
|
195
|
-
python3 - "$
|
|
277
|
+
python3 - "$DISCUSSION_SNAPSHOT_ZERO_AMBIG" <<'PY'
|
|
278
|
+
import json
|
|
196
279
|
import sys
|
|
197
280
|
from pathlib import Path
|
|
198
281
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
282
|
+
mission = 'Ship an unrelated widget overhaul.'
|
|
283
|
+
proposal = json.loads(Path(sys.argv[1]).read_text())
|
|
284
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
285
|
+
|
|
286
|
+
assert proposal['mission'] == mission, 'latest complete mission block should win inside a single structured discussion message'
|
|
287
|
+
assert proposal['analysis']['alternateMissions'] == ['Remove the completion status line while keeping the completion widget.'], 'earlier complete mission blocks should be preserved as alternate missions'
|
|
288
|
+
assert state['mission_anchor'] == mission, 'latest complete mission block should initialize canonical mission state'
|
|
203
289
|
PY
|
|
204
290
|
|
|
291
|
+
rm -rf .agent
|
|
292
|
+
|
|
205
293
|
# No workflow yet: bare /cook structured fallback should normalize placeholder planning phrasing
|
|
206
294
|
# into the concrete implementation mission when scope/acceptance clearly describe shipped work.
|
|
207
295
|
SESSION_ZERO_NORMALIZED="$TMPDIR/session-zero-normalized.jsonl"
|
|
@@ -699,6 +787,100 @@ assert plan['mission_anchor'] == mission, 'summary-only active bare /cook should
|
|
|
699
787
|
assert active['mission_anchor'] == mission, 'summary-only active bare /cook should keep active-slice.json unchanged'
|
|
700
788
|
PY
|
|
701
789
|
|
|
790
|
+
# Active workflow: when recent discussion suggests a different implementation goal but the proposal is
|
|
791
|
+
# still incomplete, bare /cook should surface the chooser instead of silently resuming the current workflow.
|
|
792
|
+
SESSION_ONE_AMBIGUOUS_CHOOSER="$TMPDIR/session-one-ambiguous-chooser.jsonl"
|
|
793
|
+
DISCUSSION_ONE_AMBIGUOUS_CHOOSER=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic for the current auth flow.'
|
|
794
|
+
AMBIGUOUS_ROUTING_ONE="$TMPDIR/active-ambiguous-routing.json"
|
|
795
|
+
AMBIGUOUS_CHOOSER_ONE="$TMPDIR/active-ambiguous-chooser.json"
|
|
796
|
+
AMBIGUOUS_RESUME_PROMPT_ONE="$TMPDIR/unexpected-active-ambiguous-resume.txt"
|
|
797
|
+
AMBIGUOUS_PROPOSAL_ONE="$TMPDIR/unexpected-active-ambiguous-proposal.json"
|
|
798
|
+
write_session "$SESSION_ONE_AMBIGUOUS_CHOOSER" "$ROOT" "$DISCUSSION_ONE_AMBIGUOUS_CHOOSER"
|
|
799
|
+
|
|
800
|
+
PI_COMPLETION_EXISTING_WORKFLOW_ACTION=cancel \
|
|
801
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ANALYST_OUTPUT='{"mission":"Fix login redirect callback behavior.","scope":["Update the callback redirect decision logic for the current auth flow."],"constraints":[],"acceptance":[],"task_type":"completion-workflow","evaluation_profile":"completion-rubric-v1","confidence":0.72,"possible_noise":["older completion widget cleanup"]}' \
|
|
802
|
+
PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$AMBIGUOUS_ROUTING_ONE" \
|
|
803
|
+
PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$AMBIGUOUS_CHOOSER_ONE" \
|
|
804
|
+
PI_COMPLETION_TEST_DRIVER_PROMPT_PATH="$AMBIGUOUS_RESUME_PROMPT_ONE" \
|
|
805
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$AMBIGUOUS_PROPOSAL_ONE" \
|
|
806
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
807
|
+
pi --session "$SESSION_ONE_AMBIGUOUS_CHOOSER" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.out" 2>"$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.err"
|
|
808
|
+
|
|
809
|
+
python3 - "$AMBIGUOUS_ROUTING_ONE" "$AMBIGUOUS_CHOOSER_ONE" "$AMBIGUOUS_RESUME_PROMPT_ONE" "$AMBIGUOUS_PROPOSAL_ONE" "$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.out" "$TMPDIR/pi-completion-context-proposal-active-ambiguous-chooser.err" <<'PY'
|
|
810
|
+
import json
|
|
811
|
+
import sys
|
|
812
|
+
from pathlib import Path
|
|
813
|
+
|
|
814
|
+
mission = 'Remove the completion status line while keeping the completion widget.'
|
|
815
|
+
routing = json.loads(Path(sys.argv[1]).read_text())
|
|
816
|
+
chooser = json.loads(Path(sys.argv[2]).read_text())
|
|
817
|
+
resume_path = Path(sys.argv[3])
|
|
818
|
+
proposal_path = Path(sys.argv[4])
|
|
819
|
+
output = Path(sys.argv[5]).read_text() + Path(sys.argv[6]).read_text()
|
|
820
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
821
|
+
plan = json.loads(Path('.agent/plan.json').read_text())
|
|
822
|
+
active = json.loads(Path('.agent/active-slice.json').read_text())
|
|
823
|
+
|
|
824
|
+
assert routing['mode'] == 'bare', 'ambiguous active bare /cook should snapshot bare routing mode'
|
|
825
|
+
assert routing['action'] == 'unclear', 'incomplete replacement discussion should stay ambiguous until the chooser resolves it'
|
|
826
|
+
assert routing['reason'] == 'ambiguous_discussion', 'incomplete replacement discussion should record the ambiguous-discussion reason'
|
|
827
|
+
assert routing['currentMissionAnchor'] == mission, 'ambiguous chooser routing should preserve the current mission anchor'
|
|
828
|
+
assert routing['proposedMissionAnchor'] == 'Fix login redirect callback behavior.', 'ambiguous chooser routing should expose the latest inferred mission'
|
|
829
|
+
assert chooser['title'].startswith('Existing completion workflow found'), 'ambiguous active bare /cook should still open the existing-workflow chooser'
|
|
830
|
+
assert chooser['choices'][0].startswith('Continue current workflow'), 'ambiguous chooser should keep the continue option'
|
|
831
|
+
assert chooser['choices'][1].startswith('Start new workflow from recent discussion'), 'ambiguous chooser should offer the recent-discussion replacement option'
|
|
832
|
+
assert not resume_path.exists(), 'ambiguous active bare /cook should not silently queue a resume prompt before the chooser resolves it'
|
|
833
|
+
assert not proposal_path.exists(), 'ambiguous chooser cancel should not open the final proposal confirmation'
|
|
834
|
+
assert 'Discuss changes in the main chat and rerun /cook.' in output, 'ambiguous chooser cancel should redirect users back to the main chat and rerun /cook'
|
|
835
|
+
assert state['mission_anchor'] == mission, 'ambiguous chooser cancel should keep state.json unchanged'
|
|
836
|
+
assert plan['mission_anchor'] == mission, 'ambiguous chooser cancel should keep plan.json unchanged'
|
|
837
|
+
assert active['mission_anchor'] == mission, 'ambiguous chooser cancel should keep active-slice.json unchanged'
|
|
838
|
+
PY
|
|
839
|
+
|
|
840
|
+
# Active workflow: when recent discussion contains multiple complete replacement missions, bare /cook should
|
|
841
|
+
# surface all candidates and allow the chooser to select a non-primary alternate mission.
|
|
842
|
+
SESSION_ONE_MULTI_CANDIDATE="$TMPDIR/session-one-multi-candidate.jsonl"
|
|
843
|
+
DISCUSSION_ONE_MULTI_CANDIDATE=$'Mission: Fix login redirect callback behavior.\nScope:\n- Update the callback redirect decision logic for the current auth flow.\nConstraints:\n- Do not refactor the broader auth flow.\nAcceptance:\n- Add a regression test for returning to the requested page.\nMission: Add logout redirect regression coverage.\nScope:\n- Add coverage for logout redirect behavior.\nConstraints:\n- Do not change login redirect behavior in this pass.\nAcceptance:\n- Land a dedicated logout redirect regression test.'
|
|
844
|
+
MULTI_ROUTING_ONE="$TMPDIR/active-multi-routing.json"
|
|
845
|
+
MULTI_CHOOSER_ONE="$TMPDIR/active-multi-chooser.json"
|
|
846
|
+
MULTI_PROPOSAL_ONE="$TMPDIR/active-multi-proposal.json"
|
|
847
|
+
write_session "$SESSION_ONE_MULTI_CANDIDATE" "$ROOT" "$DISCUSSION_ONE_MULTI_CANDIDATE"
|
|
848
|
+
|
|
849
|
+
PI_COMPLETION_EXISTING_WORKFLOW_MISSION='Fix login redirect callback behavior.' \
|
|
850
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
851
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
852
|
+
PI_COMPLETION_TEST_ACTIVE_WORKFLOW_ROUTING_PATH="$MULTI_ROUTING_ONE" \
|
|
853
|
+
PI_COMPLETION_TEST_EXISTING_WORKFLOW_CHOOSER_PATH="$MULTI_CHOOSER_ONE" \
|
|
854
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$MULTI_PROPOSAL_ONE" \
|
|
855
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
856
|
+
pi --session "$SESSION_ONE_MULTI_CANDIDATE" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-active-multi.out" 2>"$TMPDIR/pi-completion-context-proposal-active-multi.err"
|
|
857
|
+
|
|
858
|
+
python3 - "$MULTI_ROUTING_ONE" "$MULTI_CHOOSER_ONE" "$MULTI_PROPOSAL_ONE" <<'PY'
|
|
859
|
+
import json
|
|
860
|
+
import sys
|
|
861
|
+
from pathlib import Path
|
|
862
|
+
|
|
863
|
+
selected = 'Fix login redirect callback behavior.'
|
|
864
|
+
routing = json.loads(Path(sys.argv[1]).read_text())
|
|
865
|
+
chooser = json.loads(Path(sys.argv[2]).read_text())
|
|
866
|
+
proposal = json.loads(Path(sys.argv[3]).read_text())
|
|
867
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
868
|
+
plan = json.loads(Path('.agent/plan.json').read_text())
|
|
869
|
+
active = json.loads(Path('.agent/active-slice.json').read_text())
|
|
870
|
+
|
|
871
|
+
assert routing['action'] == 'unclear', 'multi-candidate replacement discussion should stay ambiguous until the chooser selects one mission'
|
|
872
|
+
assert routing['reason'] == 'ambiguous_discussion', 'multi-candidate replacement discussion should record ambiguous-discussion routing'
|
|
873
|
+
assert routing['alternateMissions'] == ['Fix login redirect callback behavior.'], 'routing snapshot should preserve alternate candidate missions'
|
|
874
|
+
assert chooser['candidateMissions'] == ['Add logout redirect regression coverage.', 'Fix login redirect callback behavior.'], 'chooser snapshot should list the primary and alternate candidate missions'
|
|
875
|
+
assert len(chooser['choices']) == 4, 'chooser should expose continue, primary, alternate, and cancel choices'
|
|
876
|
+
assert 'Scope\n- Add coverage for logout redirect behavior.' in chooser['choices'][1], 'primary chooser option should summarize the candidate scope'
|
|
877
|
+
assert 'Acceptance\n- Add a regression test for returning to the requested page.' in chooser['choices'][2], 'alternate chooser option should summarize the candidate acceptance'
|
|
878
|
+
assert proposal['mission'] == selected, 'selected alternate mission should flow into the final proposal confirmation'
|
|
879
|
+
assert state['mission_anchor'] == selected, 'selected alternate mission should rewrite state.json after approval'
|
|
880
|
+
assert plan['mission_anchor'] == selected, 'selected alternate mission should rewrite plan.json after approval'
|
|
881
|
+
assert active['mission_anchor'] == selected, 'selected alternate mission should rewrite active-slice.json after approval'
|
|
882
|
+
PY
|
|
883
|
+
|
|
702
884
|
# Active workflow: bare /cook with a placeholder planning mission should still route through the existing
|
|
703
885
|
# refocus chooser and final Start/Cancel gate before canonical state is rewritten.
|
|
704
886
|
SESSION_ONE_REFOCUS_NORMALIZED="$TMPDIR/session-one-refocus-normalized.jsonl"
|
|
@@ -747,10 +929,93 @@ assert plan['mission_anchor'] == mission, 'active bare /cook refocus should rewr
|
|
|
747
929
|
assert active['mission_anchor'] == mission, 'active bare /cook refocus should rewrite active-slice.json only after approval'
|
|
748
930
|
PY
|
|
749
931
|
|
|
750
|
-
# Completed workflow: bare /cook should
|
|
751
|
-
#
|
|
932
|
+
# Completed workflow: bare /cook should suppress proposals that simply restate the completed mission
|
|
933
|
+
# without a clear reopen or next-round signal.
|
|
752
934
|
mark_done
|
|
753
935
|
|
|
936
|
+
SESSION_TWO_COMPLETED_SUPPRESS="$TMPDIR/session-two-completed-suppress.jsonl"
|
|
937
|
+
CURRENT_DONE_MISSION="$(python3 - <<'PY'
|
|
938
|
+
import json
|
|
939
|
+
from pathlib import Path
|
|
940
|
+
print(json.loads(Path('.agent/state.json').read_text())['mission_anchor'])
|
|
941
|
+
PY
|
|
942
|
+
)"
|
|
943
|
+
DISCUSSION_TWO_COMPLETED_SUPPRESS="Mission: ${CURRENT_DONE_MISSION}
|
|
944
|
+
Scope:
|
|
945
|
+
- Keep the current completed mission exactly as-is.
|
|
946
|
+
Constraints:
|
|
947
|
+
- Do not start a different workflow from this discussion.
|
|
948
|
+
Acceptance:
|
|
949
|
+
- Keep the finished mission closed and unchanged."
|
|
950
|
+
DISCUSSION_SNAPSHOT_TWO_COMPLETED_SUPPRESS="$TMPDIR/context-proposal-next-round-completed-suppress.json"
|
|
951
|
+
write_session "$SESSION_TWO_COMPLETED_SUPPRESS" "$ROOT" "$DISCUSSION_TWO_COMPLETED_SUPPRESS"
|
|
952
|
+
|
|
953
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
954
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
955
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_COMPLETED_SUPPRESS" \
|
|
956
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
957
|
+
pi --session "$SESSION_TWO_COMPLETED_SUPPRESS" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.err"
|
|
958
|
+
|
|
959
|
+
python3 - "$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.out" "$TMPDIR/pi-completion-context-proposal-next-round-completed-suppress.err" "$DISCUSSION_SNAPSHOT_TWO_COMPLETED_SUPPRESS" "$CURRENT_DONE_MISSION" <<'PY'
|
|
960
|
+
import json
|
|
961
|
+
import sys
|
|
962
|
+
from pathlib import Path
|
|
963
|
+
|
|
964
|
+
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
965
|
+
snapshot = Path(sys.argv[3])
|
|
966
|
+
expected = sys.argv[4]
|
|
967
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
968
|
+
|
|
969
|
+
assert state['mission_anchor'] == expected, 'completed-topic suppression should keep the done workflow mission anchor unchanged'
|
|
970
|
+
assert state['continuation_policy'] == 'done', 'completed-topic suppression should keep the workflow closed'
|
|
971
|
+
assert not snapshot.exists(), 'completed-topic suppression should not emit a proposal snapshot when the latest discussion only repeats finished work'
|
|
972
|
+
assert '/cook failed closed' in output, 'completed-topic suppression should fail closed instead of reopening the finished mission'
|
|
973
|
+
PY
|
|
974
|
+
|
|
975
|
+
# Completed workflow: bare /cook should also suppress proposals that merely restate canonical
|
|
976
|
+
# verification evidence for already verified work.
|
|
977
|
+
python3 - <<'PY'
|
|
978
|
+
import json
|
|
979
|
+
from pathlib import Path
|
|
980
|
+
|
|
981
|
+
state = json.loads(Path('.agent/state.json').read_text())
|
|
982
|
+
state['latest_verified_slice'] = 'verified-logout-redirect'
|
|
983
|
+
Path('.agent/state.json').write_text(json.dumps(state, indent=2) + '\n')
|
|
984
|
+
|
|
985
|
+
evidence = json.loads(Path('.agent/verification-evidence.json').read_text())
|
|
986
|
+
evidence.update({
|
|
987
|
+
'subject_type': 'selected_slice',
|
|
988
|
+
'slice_id': 'verified-logout-redirect',
|
|
989
|
+
'goal': 'Add logout redirect regression coverage.',
|
|
990
|
+
'summary': 'Verified logout redirect regression coverage already matches the selected slice and current HEAD.',
|
|
991
|
+
'outcome': 'pass',
|
|
992
|
+
})
|
|
993
|
+
Path('.agent/verification-evidence.json').write_text(json.dumps(evidence, indent=2) + '\n')
|
|
994
|
+
PY
|
|
995
|
+
|
|
996
|
+
SESSION_TWO_VERIFIED_SUPPRESS="$TMPDIR/session-two-verified-suppress.jsonl"
|
|
997
|
+
DISCUSSION_TWO_VERIFIED_SUPPRESS=$'Mission: Add logout redirect regression coverage.\nScope:\n- Add coverage for logout redirect behavior.\nConstraints:\n- Do not change the verified logout redirect work.\nAcceptance:\n- Keep the verified logout redirect regression coverage unchanged.'
|
|
998
|
+
DISCUSSION_SNAPSHOT_TWO_VERIFIED_SUPPRESS="$TMPDIR/context-proposal-next-round-verified-suppress.json"
|
|
999
|
+
write_session "$SESSION_TWO_VERIFIED_SUPPRESS" "$ROOT" "$DISCUSSION_TWO_VERIFIED_SUPPRESS"
|
|
1000
|
+
|
|
1001
|
+
PI_COMPLETION_CONTEXT_PROPOSAL_ACTION=accept \
|
|
1002
|
+
PI_COMPLETION_DISABLE_CONTEXT_PROPOSAL_ANALYST=1 \
|
|
1003
|
+
PI_COMPLETION_TEST_CONTEXT_PROPOSAL_PATH="$DISCUSSION_SNAPSHOT_TWO_VERIFIED_SUPPRESS" \
|
|
1004
|
+
PI_COMPLETION_SKIP_DRIVER_KICKOFF=1 \
|
|
1005
|
+
pi --session "$SESSION_TWO_VERIFIED_SUPPRESS" -e "$PKG_ROOT" -p "/cook" >"$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.out" 2>"$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.err"
|
|
1006
|
+
|
|
1007
|
+
python3 - "$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.out" "$TMPDIR/pi-completion-context-proposal-next-round-verified-suppress.err" "$DISCUSSION_SNAPSHOT_TWO_VERIFIED_SUPPRESS" <<'PY'
|
|
1008
|
+
import sys
|
|
1009
|
+
from pathlib import Path
|
|
1010
|
+
|
|
1011
|
+
output = Path(sys.argv[1]).read_text() + Path(sys.argv[2]).read_text()
|
|
1012
|
+
snapshot = Path(sys.argv[3])
|
|
1013
|
+
assert not snapshot.exists(), 'verification-evidence overlap suppression should not emit a proposal snapshot for already verified work'
|
|
1014
|
+
assert '/cook failed closed' in output, 'verification-evidence overlap suppression should fail closed when the latest discussion only repeats verified work'
|
|
1015
|
+
PY
|
|
1016
|
+
|
|
1017
|
+
# Completed workflow: bare /cook should normalize placeholder planning phrasing for the next workflow
|
|
1018
|
+
# round too, not only for fresh startup.
|
|
754
1019
|
SESSION_TWO_NORMALIZED="$TMPDIR/session-two-normalized.jsonl"
|
|
755
1020
|
DISCUSSION_TWO_NORMALIZED=$'Mission: 開始實作這個方案\nScope:\n- Normalize bare /cook planning phrasing for the next workflow round.\n- Reset canonical state for the new implementation mission.\nConstraints:\n- Do not resume the completed workflow when the new round is clearly different.\nAcceptance:\n- Start a new round with the normalized mission anchor.'
|
|
756
1021
|
DISCUSSION_SNAPSHOT_TWO_NORMALIZED="$TMPDIR/context-proposal-next-round-normalized.json"
|