@linimin/pi-letscook 0.1.54 → 0.1.56
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 +3 -2
- package/README.md +15 -17
- package/extensions/completion/driver.ts +44 -110
- package/extensions/completion/index.ts +54 -89
- package/extensions/completion/prompt-surfaces.ts +65 -380
- package/extensions/completion/proposal.ts +5 -65
- package/extensions/completion/role-runner.ts +4 -311
- package/extensions/completion/state-store.ts +212 -5
- package/extensions/completion/transcription.ts +0 -8
- package/extensions/completion/types.ts +0 -114
- package/package.json +15 -4
- package/scripts/active-slice-contract-test.sh +61 -6
- package/scripts/context-proposal-test.sh +33 -29
- package/scripts/legacy-cleanup-test.sh +11 -0
- package/scripts/refocus-test.sh +10 -11
- package/scripts/release-check.sh +15 -12
- package/scripts/role-runner-contract-test.sh +1 -2
- package/scripts/rubric-contract-test.sh +0 -1
- package/scripts/smoke-test.sh +24 -13
- package/skills/cook-handoff-boundary/SKILL.md +64 -0
- package/extensions/completion/input-routing.ts +0 -819
- package/scripts/cook-trigger-routing-test.sh +0 -1122
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
CompletionStateSnapshot,
|
|
5
|
-
CookNaturalLanguageHandoff,
|
|
6
|
-
CookTriggerAdoptedArtifact,
|
|
7
|
-
CookTriggerClarificationActionItem,
|
|
8
|
-
CookTriggerClarificationLayout,
|
|
9
|
-
CookTriggerClassification,
|
|
10
|
-
CookTriggerConfirmationActionItem,
|
|
11
|
-
CookTriggerConfirmationLayout,
|
|
12
|
-
CookTriggerRecoveryActionItem,
|
|
13
|
-
CookTriggerRecoveryLayout,
|
|
14
|
-
CookTriggerWorkflowBias,
|
|
15
|
-
LiveRoleActivity,
|
|
16
|
-
} from "./types";
|
|
3
|
+
import type { CompletionStateSnapshot, LiveRoleActivity } from "./types";
|
|
17
4
|
import type {
|
|
18
5
|
ContextProposal,
|
|
19
6
|
ContextProposalAnalysis,
|
|
@@ -21,6 +8,34 @@ import type {
|
|
|
21
8
|
ContextProposalConfirmationLayout,
|
|
22
9
|
} from "./proposal";
|
|
23
10
|
|
|
11
|
+
export type AdvisoryStartupBrief = {
|
|
12
|
+
kind: "startup_brief";
|
|
13
|
+
source: "recent_discussion";
|
|
14
|
+
confirmed: true;
|
|
15
|
+
captured_at: string;
|
|
16
|
+
goal_text: string;
|
|
17
|
+
mission: string;
|
|
18
|
+
scope: string[];
|
|
19
|
+
constraints: string[];
|
|
20
|
+
acceptance: string[];
|
|
21
|
+
risks: string[];
|
|
22
|
+
notes: string[];
|
|
23
|
+
task_type?: string;
|
|
24
|
+
evaluation_profile?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function buildCookHandoffBoundaryReminder(): string {
|
|
28
|
+
return [
|
|
29
|
+
"You are still in ordinary main chat before any explicit /cook workflow entry.",
|
|
30
|
+
"Use ordinary chat to clarify requirements, discuss tradeoffs, and propose implementation approaches.",
|
|
31
|
+
"/cook is the only explicit entrypoint into long-running completion workflow.",
|
|
32
|
+
"When you judge that the task has matured into completion-workflow scope — for example the user has clearly shifted from exploration into implementation intent, you have just produced a concrete plan or proposal whose next step would naturally be implementation, or the task spans multiple files, steps, or verification surfaces — stop short of long-running implementation and tell the user to run /cook.",
|
|
33
|
+
"At that handoff point, do not begin long-running product implementation in ordinary chat, do not edit tracked product files for that workflow-level task, and do not act as though /cook had already been invoked.",
|
|
34
|
+
"When handing off, explain that /cook will derive a startup brief from recent discussion and ask for confirmation before workflow start.",
|
|
35
|
+
"If the task is still ordinary Q&A, lightweight brainstorming, or a tiny one-off fix, continue normally without forcing /cook.",
|
|
36
|
+
].join(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export function buildContextProposalGoalText(proposal: {
|
|
25
40
|
mission: string;
|
|
26
41
|
scope: string[];
|
|
@@ -60,6 +75,36 @@ export function buildContextProposalDisplayText(proposal: ContextProposal): stri
|
|
|
60
75
|
return lines.join("\n");
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
function buildAdvisoryStartupBriefNotes(analysis: ContextProposalAnalysis): string[] {
|
|
79
|
+
const notes = [
|
|
80
|
+
...analysis.critique,
|
|
81
|
+
...analysis.possibleNoise.map((item) => `Possible noise: ${item}`),
|
|
82
|
+
];
|
|
83
|
+
return notes.length > 0 ? notes : ["No additional operator notes were derived from recent discussion."];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildAdvisoryStartupBrief(args: {
|
|
87
|
+
proposal: Pick<ContextProposal, "goalText" | "mission" | "scope" | "constraints" | "acceptance">;
|
|
88
|
+
analysis: ContextProposalAnalysis;
|
|
89
|
+
capturedAt?: string;
|
|
90
|
+
}): AdvisoryStartupBrief {
|
|
91
|
+
return {
|
|
92
|
+
kind: "startup_brief",
|
|
93
|
+
source: "recent_discussion",
|
|
94
|
+
confirmed: true,
|
|
95
|
+
captured_at: args.capturedAt ?? new Date().toISOString(),
|
|
96
|
+
goal_text: args.proposal.goalText,
|
|
97
|
+
mission: args.proposal.mission,
|
|
98
|
+
scope: [...args.proposal.scope],
|
|
99
|
+
constraints: [...args.proposal.constraints],
|
|
100
|
+
acceptance: [...args.proposal.acceptance],
|
|
101
|
+
risks: [...args.analysis.risks],
|
|
102
|
+
notes: buildAdvisoryStartupBriefNotes(args.analysis),
|
|
103
|
+
task_type: args.analysis.taskType,
|
|
104
|
+
evaluation_profile: args.analysis.evaluationProfile,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
export function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
|
|
64
109
|
const lines: string[] = [];
|
|
65
110
|
if (analysis.critique.length > 0) {
|
|
@@ -92,7 +137,7 @@ export function buildContextProposalCritiqueText(analysis: ContextProposalAnalys
|
|
|
92
137
|
for (const item of analysis.suppressedNegatedTopics) lines.push(`- ${item}`);
|
|
93
138
|
}
|
|
94
139
|
if (lines.length === 0) {
|
|
95
|
-
return "No
|
|
140
|
+
return "No additional operator notes or risks were derived for this startup brief.";
|
|
96
141
|
}
|
|
97
142
|
return lines.join("\n");
|
|
98
143
|
}
|
|
@@ -141,7 +186,7 @@ export function buildContextProposalConfirmationActions(mainChatRerunGuidance: s
|
|
|
141
186
|
{
|
|
142
187
|
id: "start",
|
|
143
188
|
label: "Start",
|
|
144
|
-
description: "Accept this
|
|
189
|
+
description: "Accept this startup brief and let /cook write or refocus canonical workflow state.",
|
|
145
190
|
},
|
|
146
191
|
{
|
|
147
192
|
id: "cancel",
|
|
@@ -161,10 +206,10 @@ export function buildContextProposalConfirmationLayout(args: {
|
|
|
161
206
|
}): ContextProposalConfirmationLayout {
|
|
162
207
|
return {
|
|
163
208
|
title: args.title,
|
|
164
|
-
intro: "Review the
|
|
165
|
-
proposalHeading: "
|
|
209
|
+
intro: "Review the startup brief (mission, scope, constraints, acceptance, and notes/risks) plus the routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
|
|
210
|
+
proposalHeading: "Startup brief",
|
|
166
211
|
proposalBody: buildContextProposalDisplayText(args.proposal),
|
|
167
|
-
critiqueHeading: "
|
|
212
|
+
critiqueHeading: "Notes and risks",
|
|
168
213
|
critiqueBody: buildContextProposalCritiqueText(args.analysis),
|
|
169
214
|
routingHeading: "Routing recommendations",
|
|
170
215
|
routingBody: buildContextProposalRoutingText(args.analysis, {
|
|
@@ -200,366 +245,6 @@ export function maybeWriteContextProposalSnapshot(proposal: ContextProposal, sna
|
|
|
200
245
|
}
|
|
201
246
|
}
|
|
202
247
|
|
|
203
|
-
function writeJsonSnapshot(snapshotPath: string | undefined, value: unknown): void {
|
|
204
|
-
if (!snapshotPath) return;
|
|
205
|
-
try {
|
|
206
|
-
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
207
|
-
fs.writeFileSync(snapshotPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
208
|
-
} catch {
|
|
209
|
-
// ignore malformed or unwritable test snapshot paths
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function buildCookTriggerClassifierPrompt(args: {
|
|
214
|
-
projectName: string;
|
|
215
|
-
inputText: string;
|
|
216
|
-
recentDiscussion: string;
|
|
217
|
-
workflowContextLines?: string[];
|
|
218
|
-
}): string {
|
|
219
|
-
const lines = [
|
|
220
|
-
`Project: ${args.projectName}`,
|
|
221
|
-
"Classify whether the current input should stay in the main chat or be intercepted by the workflow-aware router into the canonical /cook workflow before the primary agent starts implementation work.",
|
|
222
|
-
"Assume router mode reviews every non-bypass normal user turn. Do not require short trigger phrases or explicit /cook text before choosing offer_workflow.",
|
|
223
|
-
"Return JSON only with keys: decision, confidence, workflow_bias, reason, evidence, riskFlags, focusHint. You may also include optional keys requires_clarification, clarification_slots, and adopted_artifact when clearly supported.",
|
|
224
|
-
"decision must be exactly one of offer_workflow, normal_prompt, or unclear.",
|
|
225
|
-
"Use offer_workflow when the user is directly asking to start, resume, refocus, or continue workflow-worthy repo work through the completion boundary, or explicitly asking to let /cook take over.",
|
|
226
|
-
"Use normal_prompt for ordinary questions, explanations, analysis-only requests, or direct agent requests that should stay in the main chat.",
|
|
227
|
-
"Use unclear for ambiguous approvals, short acknowledgements, or cases where false-positive routing risk is material.",
|
|
228
|
-
"workflow_bias must be exactly one of startup, resume, refocus, next_round, or unknown.",
|
|
229
|
-
"Use startup when there is no active workflow yet, resume when the user is clearly continuing the current workflow, refocus when the user is clearly switching the active workflow to a different goal, and next_round when the previous workflow is done and the user is starting a new round.",
|
|
230
|
-
"When decision is not offer_workflow, prefer workflow_bias=unknown unless a stronger routing hint is still useful for later debugging.",
|
|
231
|
-
"focusHint is optional, must stay short, and must never rewrite the workflow mission or invent scope.",
|
|
232
|
-
"When explicit user adoption of a recent plan or repo markdown artifact is evident, adopted_artifact may describe it with kind recent_plan|repo_markdown, path when known, and basis explicit_user_adoption.",
|
|
233
|
-
"requires_clarification may be true when chooser-style disambiguation is safer than guessing, and clarification_slots may list short needs such as goal, scope, or non_goal.",
|
|
234
|
-
"evidence and riskFlags must be arrays of short grounded strings.",
|
|
235
|
-
];
|
|
236
|
-
if (args.workflowContextLines?.length) lines.push("", "Canonical workflow context:", ...args.workflowContextLines);
|
|
237
|
-
lines.push("", `Current input: ${args.inputText}`, "", "Recent discussion:", args.recentDiscussion || "(none)");
|
|
238
|
-
return lines.join("\n");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function cookTriggerOfferCopyForBias(
|
|
242
|
-
workflowBias: CookTriggerWorkflowBias,
|
|
243
|
-
mainChatRerunGuidance: string,
|
|
244
|
-
): { title: string; intro: string; startAction: CookTriggerConfirmationActionItem; sendAsNormalChat: CookTriggerConfirmationActionItem; cancel: CookTriggerConfirmationActionItem } {
|
|
245
|
-
switch (workflowBias) {
|
|
246
|
-
case "startup":
|
|
247
|
-
return {
|
|
248
|
-
title: "Start a completion workflow from the recent discussion?",
|
|
249
|
-
intro:
|
|
250
|
-
"This input looks like a startup handoff into the completion workflow. The shared /cook entry would initialize or continue the canonical workflow boundary only after you confirm.",
|
|
251
|
-
startAction: {
|
|
252
|
-
id: "start_workflow",
|
|
253
|
-
label: "Start workflow",
|
|
254
|
-
description: "Enter the shared /cook workflow entry from the recent discussion before the primary agent starts implementation work.",
|
|
255
|
-
},
|
|
256
|
-
sendAsNormalChat: {
|
|
257
|
-
id: "send_as_normal_chat",
|
|
258
|
-
label: "Send as normal chat",
|
|
259
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
260
|
-
},
|
|
261
|
-
cancel: {
|
|
262
|
-
id: "cancel",
|
|
263
|
-
label: "Cancel",
|
|
264
|
-
description: `Stop here without routing or replaying the original message. ${mainChatRerunGuidance}`,
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
case "resume":
|
|
268
|
-
return {
|
|
269
|
-
title: "Resume the current completion workflow?",
|
|
270
|
-
intro:
|
|
271
|
-
"This input looks like a resume handoff for the current completion workflow. The shared /cook entry would continue from canonical state only after you confirm.",
|
|
272
|
-
startAction: {
|
|
273
|
-
id: "start_workflow",
|
|
274
|
-
label: "Resume workflow",
|
|
275
|
-
description: "Resume the current canonical completion workflow through the shared /cook entry.",
|
|
276
|
-
},
|
|
277
|
-
sendAsNormalChat: {
|
|
278
|
-
id: "send_as_normal_chat",
|
|
279
|
-
label: "Send as normal chat",
|
|
280
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
281
|
-
},
|
|
282
|
-
cancel: {
|
|
283
|
-
id: "cancel",
|
|
284
|
-
label: "Cancel",
|
|
285
|
-
description: `Stop here without resuming or replaying the original message. ${mainChatRerunGuidance}`,
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
-
case "refocus":
|
|
289
|
-
return {
|
|
290
|
-
title: "Refocus the completion workflow from the recent discussion?",
|
|
291
|
-
intro:
|
|
292
|
-
"This input looks like a refocus handoff. The shared /cook entry would keep the existing chooser and confirmation semantics before any canonical workflow state is rewritten.",
|
|
293
|
-
startAction: {
|
|
294
|
-
id: "start_workflow",
|
|
295
|
-
label: "Refocus workflow",
|
|
296
|
-
description: "Review the recent discussion through the shared /cook entry and refocus the canonical workflow only if the follow-up confirmations agree.",
|
|
297
|
-
},
|
|
298
|
-
sendAsNormalChat: {
|
|
299
|
-
id: "send_as_normal_chat",
|
|
300
|
-
label: "Send as normal chat",
|
|
301
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
302
|
-
},
|
|
303
|
-
cancel: {
|
|
304
|
-
id: "cancel",
|
|
305
|
-
label: "Cancel",
|
|
306
|
-
description: `Stop here without refocusing or replaying the original message. ${mainChatRerunGuidance}`,
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
case "next_round":
|
|
310
|
-
return {
|
|
311
|
-
title: "Start the next completion workflow round from the recent discussion?",
|
|
312
|
-
intro:
|
|
313
|
-
"This input looks like a next-round handoff after a completed workflow. The shared /cook entry would preserve the same canonical workflow boundary while starting the next round only after you confirm.",
|
|
314
|
-
startAction: {
|
|
315
|
-
id: "start_workflow",
|
|
316
|
-
label: "Start next round",
|
|
317
|
-
description: "Start the next workflow round through the shared /cook entry using the recent discussion as the new focus.",
|
|
318
|
-
},
|
|
319
|
-
sendAsNormalChat: {
|
|
320
|
-
id: "send_as_normal_chat",
|
|
321
|
-
label: "Send as normal chat",
|
|
322
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
323
|
-
},
|
|
324
|
-
cancel: {
|
|
325
|
-
id: "cancel",
|
|
326
|
-
label: "Cancel",
|
|
327
|
-
description: `Stop here without starting a new workflow round or replaying the original message. ${mainChatRerunGuidance}`,
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
case "unknown":
|
|
331
|
-
default:
|
|
332
|
-
return {
|
|
333
|
-
title: "Let the completion workflow take over from the recent discussion?",
|
|
334
|
-
intro:
|
|
335
|
-
"This input looks like a natural-language handoff into the completion workflow. The shared /cook entry would keep the existing approval-only startup, continue, refocus, and next-round semantics before canonical state changes.",
|
|
336
|
-
startAction: {
|
|
337
|
-
id: "start_workflow",
|
|
338
|
-
label: "Start workflow",
|
|
339
|
-
description: "Enter the shared /cook workflow entry before the primary agent starts implementation work.",
|
|
340
|
-
},
|
|
341
|
-
sendAsNormalChat: {
|
|
342
|
-
id: "send_as_normal_chat",
|
|
343
|
-
label: "Send as normal chat",
|
|
344
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
345
|
-
},
|
|
346
|
-
cancel: {
|
|
347
|
-
id: "cancel",
|
|
348
|
-
label: "Cancel",
|
|
349
|
-
description: `Stop here without routing or replaying the original message. ${mainChatRerunGuidance}`,
|
|
350
|
-
},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function buildCookTriggerConfirmationActions(
|
|
356
|
-
workflowBias: CookTriggerWorkflowBias,
|
|
357
|
-
mainChatRerunGuidance: string,
|
|
358
|
-
): CookTriggerConfirmationActionItem[] {
|
|
359
|
-
const copy = cookTriggerOfferCopyForBias(workflowBias, mainChatRerunGuidance);
|
|
360
|
-
return [copy.startAction, copy.sendAsNormalChat, copy.cancel];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function summarizeAdoptedArtifact(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): string | undefined {
|
|
364
|
-
if (!adoptedArtifact) return undefined;
|
|
365
|
-
const lines = [
|
|
366
|
-
`- kind: ${adoptedArtifact.kind}`,
|
|
367
|
-
`- basis: ${adoptedArtifact.basis}`,
|
|
368
|
-
`- title: ${adoptedArtifact.title}`,
|
|
369
|
-
];
|
|
370
|
-
if (adoptedArtifact.path) lines.push(`- path: ${adoptedArtifact.path}`);
|
|
371
|
-
if (adoptedArtifact.preview) lines.push(`- preview: ${adoptedArtifact.preview}`);
|
|
372
|
-
return lines.join("\n");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function buildCookTriggerClarificationLayout(args: {
|
|
376
|
-
currentMission?: string;
|
|
377
|
-
candidateMission?: string;
|
|
378
|
-
workflowBiases: CookTriggerWorkflowBias[];
|
|
379
|
-
mainChatRerunGuidance: string;
|
|
380
|
-
adoptedArtifact?: CookTriggerAdoptedArtifact;
|
|
381
|
-
}): CookTriggerClarificationLayout {
|
|
382
|
-
const actions: CookTriggerClarificationActionItem[] = [];
|
|
383
|
-
if (args.workflowBiases.includes("startup")) {
|
|
384
|
-
actions.push({
|
|
385
|
-
id: "route_startup",
|
|
386
|
-
label: "Start workflow",
|
|
387
|
-
description: "Treat this as a startup handoff into the shared /cook workflow from the recent discussion.",
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
if (args.workflowBiases.includes("resume")) {
|
|
391
|
-
actions.push({
|
|
392
|
-
id: "route_resume",
|
|
393
|
-
label: "Resume workflow",
|
|
394
|
-
description: "Keep the current canonical mission and resume the active workflow through the shared /cook entry.",
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
if (args.workflowBiases.includes("refocus")) {
|
|
398
|
-
actions.push({
|
|
399
|
-
id: "route_refocus",
|
|
400
|
-
label: "Refocus from recent discussion",
|
|
401
|
-
description: "Route into the shared /cook entry and keep its existing chooser + approval flow before any canonical state rewrite.",
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
if (args.workflowBiases.includes("next_round")) {
|
|
405
|
-
actions.push({
|
|
406
|
-
id: "route_next_round",
|
|
407
|
-
label: "Start next round",
|
|
408
|
-
description: "Treat this as a next-round handoff into the shared /cook entry after the finished workflow.",
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
actions.push(
|
|
412
|
-
{
|
|
413
|
-
id: "send_as_normal_chat",
|
|
414
|
-
label: "Send as normal chat",
|
|
415
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
id: "cancel",
|
|
419
|
-
label: "Cancel",
|
|
420
|
-
description: `Stop here without routing or replaying the original message. ${args.mainChatRerunGuidance}`,
|
|
421
|
-
},
|
|
422
|
-
);
|
|
423
|
-
return {
|
|
424
|
-
title: "Clarify how the completion workflow should proceed",
|
|
425
|
-
intro:
|
|
426
|
-
"This start-intent looks workflow-related, but not enough to safely choose startup, resume, refocus, or next-round automatically. Pick the minimal next step or cancel without changing canonical workflow state.",
|
|
427
|
-
currentMissionHeading: args.currentMission ? "Current mission" : undefined,
|
|
428
|
-
currentMissionBody: args.currentMission,
|
|
429
|
-
candidateMissionHeading: args.candidateMission ? "Recent-discussion candidate" : undefined,
|
|
430
|
-
candidateMissionBody: args.candidateMission,
|
|
431
|
-
adoptedArtifactHeading: args.adoptedArtifact ? "Adopted artifact" : undefined,
|
|
432
|
-
adoptedArtifactBody: summarizeAdoptedArtifact(args.adoptedArtifact),
|
|
433
|
-
actionsHeading: "Actions",
|
|
434
|
-
actions,
|
|
435
|
-
footer: "↑↓ navigate • enter select • esc cancel",
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export function buildCookTriggerConfirmationLayout(args: {
|
|
440
|
-
classification: CookTriggerClassification;
|
|
441
|
-
mainChatRerunGuidance: string;
|
|
442
|
-
}): CookTriggerConfirmationLayout {
|
|
443
|
-
const evidenceBody =
|
|
444
|
-
args.classification.evidence.length > 0
|
|
445
|
-
? args.classification.evidence.map((item) => `- ${item}`).join("\n")
|
|
446
|
-
: "- No additional evidence was captured beyond the current handoff signal.";
|
|
447
|
-
const riskBody = args.classification.riskFlags.length > 0 ? args.classification.riskFlags.map((item) => `- ${item}`).join("\n") : undefined;
|
|
448
|
-
const copy = cookTriggerOfferCopyForBias(args.classification.workflowBias, args.mainChatRerunGuidance);
|
|
449
|
-
return {
|
|
450
|
-
title: copy.title,
|
|
451
|
-
intro: copy.intro,
|
|
452
|
-
evidenceHeading: "Why it matched",
|
|
453
|
-
evidenceBody,
|
|
454
|
-
riskHeading: riskBody ? "Risk checks" : undefined,
|
|
455
|
-
riskBody,
|
|
456
|
-
focusHintHeading: args.classification.focusHint ? "Optional focus hint" : undefined,
|
|
457
|
-
focusHintBody: args.classification.focusHint,
|
|
458
|
-
actionsHeading: "Actions",
|
|
459
|
-
actions: [copy.startAction, copy.sendAsNormalChat, copy.cancel],
|
|
460
|
-
footer: "↑↓ navigate • enter select • esc cancel",
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
export function buildCookTriggerRecoveryLayout(args: {
|
|
465
|
-
failureLabel: string;
|
|
466
|
-
mainChatRerunGuidance: string;
|
|
467
|
-
}): CookTriggerRecoveryLayout {
|
|
468
|
-
const actions: CookTriggerRecoveryActionItem[] = [
|
|
469
|
-
{
|
|
470
|
-
id: "retry_routing",
|
|
471
|
-
label: "Retry routing",
|
|
472
|
-
description: "Run the workflow-aware router classifier once more before deciding whether /cook should take over.",
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
id: "send_as_normal_chat",
|
|
476
|
-
label: "Send as normal chat",
|
|
477
|
-
description: "Replay the original message exactly once to the primary agent and bypass router interception for that replay.",
|
|
478
|
-
},
|
|
479
|
-
{
|
|
480
|
-
id: "cancel",
|
|
481
|
-
label: "Cancel",
|
|
482
|
-
description: `Stop here without routing or replaying the original message. ${args.mainChatRerunGuidance}`,
|
|
483
|
-
},
|
|
484
|
-
];
|
|
485
|
-
return {
|
|
486
|
-
title: "Router recovery needed before this prompt can continue",
|
|
487
|
-
intro:
|
|
488
|
-
"The workflow-aware router could not safely classify this prompt, so it stayed fail-closed instead of silently sending the prompt to the primary agent. Choose an explicit recovery path or cancel.",
|
|
489
|
-
failureHeading: "Router failure",
|
|
490
|
-
failureBody: args.failureLabel,
|
|
491
|
-
actionsHeading: "Actions",
|
|
492
|
-
actions,
|
|
493
|
-
footer: "↑↓ navigate • enter select • esc cancel",
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export function maybeWriteCookTriggerClassifierSnapshot(snapshot: Record<string, unknown>, snapshotPath: string | undefined): void {
|
|
498
|
-
writeJsonSnapshot(snapshotPath, snapshot);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
export function maybeWriteCookTriggerConfirmationSnapshot(
|
|
502
|
-
layout: CookTriggerConfirmationLayout,
|
|
503
|
-
snapshotPath: string | undefined,
|
|
504
|
-
): void {
|
|
505
|
-
writeJsonSnapshot(snapshotPath, layout);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
export function maybeWriteCookTriggerClarificationSnapshot(
|
|
509
|
-
layout: CookTriggerClarificationLayout,
|
|
510
|
-
snapshotPath: string | undefined,
|
|
511
|
-
): void {
|
|
512
|
-
writeJsonSnapshot(snapshotPath, layout);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
export function maybeWriteCookTriggerRecoverySnapshot(layout: CookTriggerRecoveryLayout, snapshotPath: string | undefined): void {
|
|
516
|
-
writeJsonSnapshot(snapshotPath, layout);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export function maybeWriteCookTriggerRoutingSnapshot(snapshot: Record<string, unknown>, snapshotPath: string | undefined): void {
|
|
520
|
-
writeJsonSnapshot(snapshotPath, snapshot);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function buildNaturalLanguageHandoffArtifactLines(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): string[] {
|
|
524
|
-
if (!adoptedArtifact) return [];
|
|
525
|
-
const lines = [
|
|
526
|
-
`- adopted_artifact_kind: ${adoptedArtifact.kind}`,
|
|
527
|
-
`- adopted_artifact_basis: ${adoptedArtifact.basis}`,
|
|
528
|
-
`- adopted_artifact_title: ${adoptedArtifact.title}`,
|
|
529
|
-
];
|
|
530
|
-
if (adoptedArtifact.path) lines.push(`- adopted_artifact_path: ${adoptedArtifact.path}`);
|
|
531
|
-
if (adoptedArtifact.preview) lines.push(`- adopted_artifact_preview: ${adoptedArtifact.preview}`);
|
|
532
|
-
return lines;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function buildNaturalLanguageHandoffClarificationLines(
|
|
536
|
-
clarificationCapsule: CookNaturalLanguageHandoff["clarificationCapsule"] | undefined,
|
|
537
|
-
): string[] {
|
|
538
|
-
if (!clarificationCapsule) return [];
|
|
539
|
-
const lines = [
|
|
540
|
-
`- clarification_selected_bias: ${clarificationCapsule.selectedWorkflowBias}`,
|
|
541
|
-
`- clarification_reason: ${clarificationCapsule.reason}`,
|
|
542
|
-
];
|
|
543
|
-
if (clarificationCapsule.goal) lines.push(`- clarification_goal: ${clarificationCapsule.goal}`);
|
|
544
|
-
if (clarificationCapsule.scope?.length) lines.push(`- clarification_scope: ${clarificationCapsule.scope.join(" | ")}`);
|
|
545
|
-
if (clarificationCapsule.nonGoal?.length) lines.push(`- clarification_non_goal: ${clarificationCapsule.nonGoal.join(" | ")}`);
|
|
546
|
-
if (clarificationCapsule.doneWhen?.length) lines.push(`- clarification_done_when: ${clarificationCapsule.doneWhen.join(" | ")}`);
|
|
547
|
-
return lines;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export function buildNaturalLanguageHandoffMetadataLines(handoff: CookNaturalLanguageHandoff | undefined): string[] {
|
|
551
|
-
if (!handoff) return [];
|
|
552
|
-
return [
|
|
553
|
-
"Natural-language handoff metadata:",
|
|
554
|
-
`- source: natural_language_handoff`,
|
|
555
|
-
`- preferred_routing_bias: ${handoff.preferredRoutingBias ?? "unknown"}`,
|
|
556
|
-
`- trigger_text: ${handoff.triggerText ?? "(none)"}`,
|
|
557
|
-
`- focus_hint: ${handoff.hintText ?? "(none)"}`,
|
|
558
|
-
...buildNaturalLanguageHandoffArtifactLines(handoff.adoptedArtifact),
|
|
559
|
-
...buildNaturalLanguageHandoffClarificationLines(handoff.clarificationCapsule),
|
|
560
|
-
"",
|
|
561
|
-
];
|
|
562
|
-
}
|
|
563
248
|
|
|
564
249
|
export function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout) {
|
|
565
250
|
return layout.actions.map((action) => ({
|
|
@@ -575,7 +260,7 @@ export function buildContextProposalAnalystPrompt(projectName: string, discussio
|
|
|
575
260
|
"Infer the current implementation mission from the discussion.",
|
|
576
261
|
"Prefer the latest clear user implementation intent over older background context.",
|
|
577
262
|
"Treat stale, completed, or explicitly negated topics as context to ignore unless the latest discussion clearly reopens them.",
|
|
578
|
-
"
|
|
263
|
+
"Use only recent user/custom discussion plus canonical workflow context; do not infer startup intent from slash-command arguments or planning-only artifacts.",
|
|
579
264
|
];
|
|
580
265
|
if (contextLines.length > 0) lines.push("", "Canonical workflow context:", ...contextLines);
|
|
581
266
|
lines.push("", "Recent discussion:", discussion || "(none)");
|
|
@@ -342,11 +342,6 @@ export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[
|
|
|
342
342
|
.join("\n\n");
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
function contextHintEntry(hintText: string | undefined): RecentDiscussionEntry[] {
|
|
346
|
-
const normalized = normalizeProposalLine(hintText ?? "");
|
|
347
|
-
return normalized ? [{ role: "user", text: `Hint: ${normalized}` }] : [];
|
|
348
|
-
}
|
|
349
|
-
|
|
350
345
|
const RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX =
|
|
351
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;
|
|
352
347
|
|
|
@@ -728,56 +723,6 @@ function proposalOverlapsTopic(proposal: ContextProposal | ContextProposalAltern
|
|
|
728
723
|
return bodyTexts.some((text) => missionTextOverlapsTopic(text, topic) || missionTextOverlapsTopic(topic, text));
|
|
729
724
|
}
|
|
730
725
|
|
|
731
|
-
function hintOverlapScore(text: string, hintText: string): number {
|
|
732
|
-
const normalizedText = normalizeMissionAnchorText(text).toLowerCase();
|
|
733
|
-
const normalizedHint = normalizeMissionAnchorText(hintText).toLowerCase();
|
|
734
|
-
if (!normalizedText || !normalizedHint) return 0;
|
|
735
|
-
if (normalizedText === normalizedHint) return 10;
|
|
736
|
-
if (normalizedText.includes(normalizedHint) || normalizedHint.includes(normalizedText)) return 6;
|
|
737
|
-
const textTokens = missionAnchorSemanticTokens(normalizedText);
|
|
738
|
-
const hintTokens = missionAnchorSemanticTokens(normalizedHint);
|
|
739
|
-
if (textTokens.length === 0 || hintTokens.length === 0) return 0;
|
|
740
|
-
const hintSet = new Set(hintTokens);
|
|
741
|
-
const overlap = textTokens.filter((token) => hintSet.has(token));
|
|
742
|
-
if (overlap.length === 0) return 0;
|
|
743
|
-
return overlap.length / Math.max(textTokens.length, hintTokens.length);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function proposalHintScore(proposal: ContextProposal | ContextProposalAlternate, hintText: string): number {
|
|
747
|
-
return (
|
|
748
|
-
hintOverlapScore(proposal.mission, hintText) * 4 +
|
|
749
|
-
proposal.scope.reduce((sum, item) => sum + hintOverlapScore(item, hintText) * 2, 0) +
|
|
750
|
-
proposal.constraints.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0) +
|
|
751
|
-
proposal.acceptance.reduce((sum, item) => sum + hintOverlapScore(item, hintText), 0)
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function selectHintPreferredProposal(proposal: ContextProposal | undefined, hintText: string | undefined): ContextProposal | undefined {
|
|
756
|
-
if (!proposal || !hintText) return proposal;
|
|
757
|
-
const candidates = [proposal, ...(proposal.alternateProposals ?? [])].filter((candidate, index, list) =>
|
|
758
|
-
list.findIndex((other) => missionAnchorsStrictlyEquivalent(other.mission, candidate.mission)) === index,
|
|
759
|
-
);
|
|
760
|
-
if (candidates.length <= 1) return proposal;
|
|
761
|
-
const scored = candidates.map((candidate, index) => ({ candidate, index, score: proposalHintScore(candidate, hintText) }));
|
|
762
|
-
const best = scored.reduce((current, item) => (item.score > current.score ? item : current), scored[0]);
|
|
763
|
-
if (best.score <= 0 || best.index === 0) return proposal;
|
|
764
|
-
const selected = best.candidate;
|
|
765
|
-
const alternates = candidates
|
|
766
|
-
.filter((_, index) => index !== best.index)
|
|
767
|
-
.map((candidate) => ({ ...candidate, analysis: finalizeContextProposalAnalysis(candidate.analysis, [candidate.goalText, candidate.mission]) }));
|
|
768
|
-
return {
|
|
769
|
-
...selected,
|
|
770
|
-
alternateProposals: alternates,
|
|
771
|
-
analysis: finalizeContextProposalAnalysis(
|
|
772
|
-
{
|
|
773
|
-
...selected.analysis,
|
|
774
|
-
alternateMissions: alternates.map((candidate) => candidate.mission),
|
|
775
|
-
},
|
|
776
|
-
[selected.goalText, selected.mission, hintText, ...alternates.map((candidate) => candidate.mission)],
|
|
777
|
-
),
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
|
|
781
726
|
function extractSuppressedNegatedTopics(proposal: ContextProposal): string[] {
|
|
782
727
|
return uniqueProposalItems(
|
|
783
728
|
proposal.constraints.filter((item) => looksLikeConstraint(item) && CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalizeProposalLine(item))),
|
|
@@ -1241,25 +1186,20 @@ export async function deriveCookContextProposalFromRecentDiscussion(
|
|
|
1241
1186
|
projectName: string,
|
|
1242
1187
|
recentEntries: RecentDiscussionEntry[],
|
|
1243
1188
|
deps: ProposalParseDeps & {
|
|
1244
|
-
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]
|
|
1189
|
+
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]) => Promise<ContextProposal | undefined>;
|
|
1245
1190
|
workflowContext?: ContextProposalWorkflowContext;
|
|
1246
|
-
hintText?: string;
|
|
1247
1191
|
},
|
|
1248
1192
|
): Promise<ContextProposal | undefined> {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const analyzed = selectHintPreferredProposal(
|
|
1253
|
-
applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries, deps.hintText), deps.workflowContext, deps) ?? undefined,
|
|
1254
|
-
deps.hintText,
|
|
1255
|
-
);
|
|
1193
|
+
if (recentEntries.length === 0) return undefined;
|
|
1194
|
+
for (const candidateEntries of recentDiscussionWindows(recentEntries, deps.stripCodeBlocks)) {
|
|
1195
|
+
const analyzed = applyWorkflowContextToProposal(await deps.analyzeContextProposal?.(candidateEntries), deps.workflowContext, deps) ?? undefined;
|
|
1256
1196
|
if (analyzed) return analyzed;
|
|
1257
1197
|
const structured = applyWorkflowContextToProposal(
|
|
1258
1198
|
extractContextProposalFromStructuredSession(candidateEntries, projectName, deps),
|
|
1259
1199
|
deps.workflowContext,
|
|
1260
1200
|
deps,
|
|
1261
1201
|
);
|
|
1262
|
-
if (structured) return
|
|
1202
|
+
if (structured) return structured;
|
|
1263
1203
|
}
|
|
1264
1204
|
return undefined;
|
|
1265
1205
|
}
|