@linimin/pi-letscook 0.1.50 → 0.1.51
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 +11 -0
- package/README.md +80 -52
- package/extensions/completion/driver.ts +151 -131
- package/extensions/completion/index.ts +8 -3
- package/extensions/completion/input-routing.ts +350 -0
- package/extensions/completion/prompt-surfaces.ts +99 -1
- package/extensions/completion/proposal.ts +1 -1
- package/extensions/completion/role-runner.ts +285 -2
- package/extensions/completion/types.ts +42 -0
- package/package.json +1 -1
- package/scripts/cook-trigger-routing-test.sh +314 -0
- package/scripts/release-check.sh +12 -5
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { runCookEntry, type CompletionDriverDeps } from "./driver";
|
|
4
|
+
import {
|
|
5
|
+
buildCookTriggerAssistConfirmationLayout,
|
|
6
|
+
maybeWriteCookTriggerConfirmationSnapshot,
|
|
7
|
+
maybeWriteCookTriggerRoutingSnapshot,
|
|
8
|
+
} from "./prompt-surfaces";
|
|
9
|
+
import {
|
|
10
|
+
collectRecentDiscussionEntries,
|
|
11
|
+
hasRecentDiscussionImplementationIntent,
|
|
12
|
+
stripCodeBlocks,
|
|
13
|
+
} from "./proposal";
|
|
14
|
+
import {
|
|
15
|
+
classifyCookTriggerIntentWithAgent,
|
|
16
|
+
type CookTriggerClassifierResult,
|
|
17
|
+
} from "./role-runner";
|
|
18
|
+
import { asString, loadCompletionSnapshot } from "./state-store";
|
|
19
|
+
import type {
|
|
20
|
+
CompletionStateSnapshot,
|
|
21
|
+
CookTriggerClassification,
|
|
22
|
+
CookTriggerConfirmationAction,
|
|
23
|
+
CookTriggerDecision,
|
|
24
|
+
NaturalLanguageCookTriggerMode,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
type InputRoutingEvent = {
|
|
28
|
+
text: string;
|
|
29
|
+
images?: unknown[];
|
|
30
|
+
source?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type InputRoutingContext = {
|
|
34
|
+
cwd: string;
|
|
35
|
+
hasUI: boolean;
|
|
36
|
+
ui: any;
|
|
37
|
+
sessionManager: any;
|
|
38
|
+
model?: any;
|
|
39
|
+
modelRegistry?: any;
|
|
40
|
+
isIdle: () => boolean;
|
|
41
|
+
hasPendingMessages: () => boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MAX_TRIGGER_CANDIDATE_LENGTH = 120;
|
|
45
|
+
const MAX_TRIGGER_CANDIDATE_LINES = 3;
|
|
46
|
+
const CLEAR_TRIGGER_PATTERNS = [
|
|
47
|
+
/^(?:go ahead|please go ahead|proceed|let'?s do it|let'?s start|start(?: implementing| implementation)?|begin(?: implementing| implementation)?|continue(?: with implementation| implementing)?|ship it|work on it|do it)\b/i,
|
|
48
|
+
/^(?:開始(?:做|實作|实现|落地)|开始(?:做|实作|实现|落地)|那就做吧|照(?:剛剛|刚刚|這個|这个|上述|上面的)?(?:討論|讨论|方向).*(?:做|實作|实现|落地)|可以開始(?:做|實作|实现)?|可以开始(?:做|实作|实现)?|繼續(?:做|實作|实现|往下做)|继续(?:做|实作|实现|往下做))/u,
|
|
49
|
+
];
|
|
50
|
+
const AMBIGUOUS_ACK_PATTERNS = [/^(?:ok|okay|sure|fine|yes|yeah|yep)$/i, /^(?:好|好的|可以|嗯|那就這樣|那就这样|就這樣|就这样|先這樣|先这样|收到)$/u];
|
|
51
|
+
|
|
52
|
+
function roleFromEnv(): string | undefined {
|
|
53
|
+
return asString(process.env.PI_COMPLETION_ROLE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function configuredTriggerMode(): NaturalLanguageCookTriggerMode {
|
|
57
|
+
const raw =
|
|
58
|
+
asString(process.env.PI_COMPLETION_TEST_TRIGGER_MODE)?.toLowerCase() ??
|
|
59
|
+
asString(process.env.PI_COMPLETION_TRIGGER_MODE)?.toLowerCase() ??
|
|
60
|
+
"assist";
|
|
61
|
+
return raw === "off" || raw === "assist" || raw === "auto" ? raw : "assist";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function effectiveTriggerMode(mode: NaturalLanguageCookTriggerMode): "off" | "assist" {
|
|
65
|
+
return mode === "off" ? "off" : "assist";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function triggerRoutingSnapshotPath(): string | undefined {
|
|
69
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_ROUTING_PATH);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function triggerConfirmationSnapshotPath(): string | undefined {
|
|
73
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_CONFIRMATION_PATH);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function triggerConfirmationOverride(): CookTriggerConfirmationAction | undefined {
|
|
77
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_TRIGGER_CONFIRM_ACTION)?.toLowerCase();
|
|
78
|
+
if (!raw) return undefined;
|
|
79
|
+
if (raw === "start" || raw === "start_cook" || raw === "cook") return "start_cook";
|
|
80
|
+
if (raw === "continue" || raw === "keep_chatting" || raw === "keep-chatting") return "keep_chatting";
|
|
81
|
+
if (raw === "cancel") return "cancel";
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeTriggerText(text: string): string {
|
|
86
|
+
return text.replace(/\s+/g, " ").trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasImages(event: InputRoutingEvent): boolean {
|
|
90
|
+
return Array.isArray(event.images) && event.images.length > 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function activeWorkflowContext(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
94
|
+
return Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) !== "done";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function looksLikeTriggerCandidate(text: string): boolean {
|
|
98
|
+
const normalized = normalizeTriggerText(text);
|
|
99
|
+
if (!normalized) return false;
|
|
100
|
+
if (normalized.length > MAX_TRIGGER_CANDIDATE_LENGTH) return false;
|
|
101
|
+
if (text.split(/\r?\n/).length > MAX_TRIGGER_CANDIDATE_LINES) return false;
|
|
102
|
+
if (normalized.startsWith("/") || normalized.startsWith("!")) return false;
|
|
103
|
+
if (AMBIGUOUS_ACK_PATTERNS.some((pattern) => pattern.test(normalized))) return false;
|
|
104
|
+
return CLEAR_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasRecentImplementationContext(entries: Array<{ text: string }>): boolean {
|
|
108
|
+
return entries.some((entry) => hasRecentDiscussionImplementationIntent(entry.text, stripCodeBlocks));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildTriggerWorkflowContextLines(snapshot: CompletionStateSnapshot | undefined): string[] {
|
|
112
|
+
if (!snapshot) return [];
|
|
113
|
+
return [
|
|
114
|
+
`mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? "(none)"}`,
|
|
115
|
+
`continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
|
|
116
|
+
`current phase: ${asString(snapshot.state?.current_phase) ?? "(none)"}`,
|
|
117
|
+
`next mandatory role: ${asString(snapshot.state?.next_mandatory_role) ?? "(none)"}`,
|
|
118
|
+
`active slice id: ${asString(snapshot.active?.slice_id) ?? "(none)"}`,
|
|
119
|
+
`active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
|
|
120
|
+
`active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
|
|
121
|
+
`latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
122
|
+
`latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeRoutingDecision(event: InputRoutingEvent, decision: CookTriggerDecision, extras?: Record<string, unknown>): void {
|
|
127
|
+
maybeWriteCookTriggerRoutingSnapshot(
|
|
128
|
+
{
|
|
129
|
+
text: event.text,
|
|
130
|
+
source: event.source ?? null,
|
|
131
|
+
configuredMode: decision.mode,
|
|
132
|
+
action: decision.action,
|
|
133
|
+
reason: decision.reason,
|
|
134
|
+
bypassReason: decision.bypassReason ?? null,
|
|
135
|
+
classificationIntent: decision.classification?.intent ?? null,
|
|
136
|
+
confidence: decision.classification?.confidence ?? null,
|
|
137
|
+
classifierReason: decision.classification?.reason ?? null,
|
|
138
|
+
focusHint: decision.classification?.focusHint ?? null,
|
|
139
|
+
evidence: decision.classification?.evidence ?? [],
|
|
140
|
+
riskFlags: decision.classification?.riskFlags ?? [],
|
|
141
|
+
...extras,
|
|
142
|
+
},
|
|
143
|
+
triggerRoutingSnapshotPath(),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function classifierFailureReason(result: CookTriggerClassifierResult): string {
|
|
148
|
+
switch (result.status) {
|
|
149
|
+
case "timeout":
|
|
150
|
+
return "classifier_timeout";
|
|
151
|
+
case "invalid_output":
|
|
152
|
+
return "classifier_invalid_output";
|
|
153
|
+
case "error":
|
|
154
|
+
default:
|
|
155
|
+
return "classifier_error";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function promptCookTriggerTakeover(
|
|
160
|
+
ctx: InputRoutingContext,
|
|
161
|
+
classification: CookTriggerClassification,
|
|
162
|
+
deps: CompletionDriverDeps,
|
|
163
|
+
): Promise<CookTriggerConfirmationAction> {
|
|
164
|
+
const override = triggerConfirmationOverride();
|
|
165
|
+
const layout = buildCookTriggerAssistConfirmationLayout({
|
|
166
|
+
classification,
|
|
167
|
+
mainChatRerunGuidance: deps.mainChatRerunGuidance,
|
|
168
|
+
});
|
|
169
|
+
maybeWriteCookTriggerConfirmationSnapshot(layout, triggerConfirmationSnapshotPath());
|
|
170
|
+
if (override) return override;
|
|
171
|
+
if (!ctx.hasUI || !ctx.ui) return "cancel";
|
|
172
|
+
const choices = layout.actions.map((action) => `${action.label}\n\n${action.description}`);
|
|
173
|
+
const titleParts = [layout.title, "", layout.intro];
|
|
174
|
+
if (layout.evidenceHeading && layout.evidenceBody) titleParts.push("", layout.evidenceHeading, layout.evidenceBody);
|
|
175
|
+
if (layout.riskHeading && layout.riskBody) titleParts.push("", layout.riskHeading, layout.riskBody);
|
|
176
|
+
if (layout.focusHintHeading && layout.focusHintBody) titleParts.push("", layout.focusHintHeading, layout.focusHintBody);
|
|
177
|
+
const choice = await ctx.ui.select(titleParts.join("\n"), choices);
|
|
178
|
+
if (!choice) return "cancel";
|
|
179
|
+
const index = choices.indexOf(choice);
|
|
180
|
+
return index >= 0 ? layout.actions[index].id : "cancel";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function guidanceForClassifierFailure(result: CookTriggerClassifierResult): string {
|
|
184
|
+
if (result.status === "timeout") {
|
|
185
|
+
return "Could not safely determine whether /cook should take over before implementation work started because the trigger classifier timed out. If you want the completion workflow boundary, run /cook explicitly.";
|
|
186
|
+
}
|
|
187
|
+
return "Could not safely determine whether /cook should take over before implementation work started. If you want the completion workflow boundary, run /cook explicitly.";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function handleCookNaturalLanguageTrigger(
|
|
191
|
+
pi: ExtensionAPI,
|
|
192
|
+
event: InputRoutingEvent,
|
|
193
|
+
ctx: InputRoutingContext,
|
|
194
|
+
deps: CompletionDriverDeps,
|
|
195
|
+
): Promise<{ action: "continue" | "handled" }> {
|
|
196
|
+
const configuredMode = configuredTriggerMode();
|
|
197
|
+
const mode = effectiveTriggerMode(configuredMode);
|
|
198
|
+
if (mode === "off") {
|
|
199
|
+
writeRoutingDecision(event, { mode: configuredMode, action: "continue", reason: "mode_off" });
|
|
200
|
+
return { action: "continue" };
|
|
201
|
+
}
|
|
202
|
+
if (roleFromEnv()) {
|
|
203
|
+
writeRoutingDecision(event, {
|
|
204
|
+
mode: configuredMode,
|
|
205
|
+
action: "continue",
|
|
206
|
+
reason: "completion_role_subprocess",
|
|
207
|
+
bypassReason: "completion_role_subprocess",
|
|
208
|
+
});
|
|
209
|
+
return { action: "continue" };
|
|
210
|
+
}
|
|
211
|
+
if ((event.text ?? "").trimStart().startsWith("/")) {
|
|
212
|
+
writeRoutingDecision(event, {
|
|
213
|
+
mode: configuredMode,
|
|
214
|
+
action: "continue",
|
|
215
|
+
reason: "slash_command",
|
|
216
|
+
bypassReason: "slash_command",
|
|
217
|
+
});
|
|
218
|
+
return { action: "continue" };
|
|
219
|
+
}
|
|
220
|
+
if (event.source === "extension") {
|
|
221
|
+
writeRoutingDecision(event, {
|
|
222
|
+
mode: configuredMode,
|
|
223
|
+
action: "continue",
|
|
224
|
+
reason: "extension_source",
|
|
225
|
+
bypassReason: "extension_source",
|
|
226
|
+
});
|
|
227
|
+
return { action: "continue" };
|
|
228
|
+
}
|
|
229
|
+
if (hasImages(event)) {
|
|
230
|
+
writeRoutingDecision(event, {
|
|
231
|
+
mode: configuredMode,
|
|
232
|
+
action: "continue",
|
|
233
|
+
reason: "image_turn",
|
|
234
|
+
bypassReason: "image_turn",
|
|
235
|
+
});
|
|
236
|
+
return { action: "continue" };
|
|
237
|
+
}
|
|
238
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
239
|
+
writeRoutingDecision(event, {
|
|
240
|
+
mode: configuredMode,
|
|
241
|
+
action: "continue",
|
|
242
|
+
reason: "non_idle_turn",
|
|
243
|
+
bypassReason: "non_idle_turn",
|
|
244
|
+
});
|
|
245
|
+
return { action: "continue" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const snapshot = await loadCompletionSnapshot(ctx.cwd);
|
|
249
|
+
const recentEntries = collectRecentDiscussionEntries(ctx, {
|
|
250
|
+
asString,
|
|
251
|
+
isRecord: (value) => typeof value === "object" && value !== null && !Array.isArray(value),
|
|
252
|
+
}, 6);
|
|
253
|
+
if (!activeWorkflowContext(snapshot) && !hasRecentImplementationContext(recentEntries)) {
|
|
254
|
+
writeRoutingDecision(event, {
|
|
255
|
+
mode: configuredMode,
|
|
256
|
+
action: "continue",
|
|
257
|
+
reason: "no_workflow_or_recent_implementation_context",
|
|
258
|
+
bypassReason: "no_workflow_or_recent_implementation_context",
|
|
259
|
+
});
|
|
260
|
+
return { action: "continue" };
|
|
261
|
+
}
|
|
262
|
+
if (!looksLikeTriggerCandidate(event.text)) {
|
|
263
|
+
writeRoutingDecision(event, {
|
|
264
|
+
mode: configuredMode,
|
|
265
|
+
action: "continue",
|
|
266
|
+
reason: "not_candidate",
|
|
267
|
+
bypassReason: "not_candidate",
|
|
268
|
+
});
|
|
269
|
+
return { action: "continue" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const classifier = await classifyCookTriggerIntentWithAgent({
|
|
273
|
+
ctx,
|
|
274
|
+
projectName: path.basename(snapshot?.files.root ?? ctx.cwd),
|
|
275
|
+
inputText: normalizeTriggerText(event.text),
|
|
276
|
+
recentEntries,
|
|
277
|
+
workflowContextLines: buildTriggerWorkflowContextLines(snapshot),
|
|
278
|
+
});
|
|
279
|
+
if (classifier.status !== "classified" || !classifier.classification) {
|
|
280
|
+
deps.emitCommandText(ctx, guidanceForClassifierFailure(classifier), "info");
|
|
281
|
+
writeRoutingDecision(event, {
|
|
282
|
+
mode: configuredMode,
|
|
283
|
+
action: "handled",
|
|
284
|
+
reason: classifierFailureReason(classifier),
|
|
285
|
+
}, {
|
|
286
|
+
errorMessage: classifier.errorMessage ?? null,
|
|
287
|
+
rawOutput: classifier.rawOutput ?? null,
|
|
288
|
+
});
|
|
289
|
+
return { action: "handled" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const classification = classifier.classification;
|
|
293
|
+
if (classification.intent === "normal_prompt") {
|
|
294
|
+
writeRoutingDecision(event, {
|
|
295
|
+
mode: configuredMode,
|
|
296
|
+
action: "continue",
|
|
297
|
+
reason: "classifier_normal_prompt",
|
|
298
|
+
classification,
|
|
299
|
+
});
|
|
300
|
+
return { action: "continue" };
|
|
301
|
+
}
|
|
302
|
+
if (classification.intent === "unclear") {
|
|
303
|
+
writeRoutingDecision(event, {
|
|
304
|
+
mode: configuredMode,
|
|
305
|
+
action: "continue",
|
|
306
|
+
reason: "classifier_unclear",
|
|
307
|
+
classification,
|
|
308
|
+
});
|
|
309
|
+
return { action: "continue" };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const confirmation = await promptCookTriggerTakeover(ctx, classification, deps);
|
|
313
|
+
if (confirmation === "keep_chatting") {
|
|
314
|
+
writeRoutingDecision(event, {
|
|
315
|
+
mode: configuredMode,
|
|
316
|
+
action: "continue",
|
|
317
|
+
reason: "user_declined_takeover",
|
|
318
|
+
classification,
|
|
319
|
+
});
|
|
320
|
+
return { action: "continue" };
|
|
321
|
+
}
|
|
322
|
+
if (confirmation === "cancel") {
|
|
323
|
+
deps.emitCommandText(
|
|
324
|
+
ctx,
|
|
325
|
+
"Cancelled natural-language /cook takeover. If you want the workflow boundary, rerun /cook explicitly.",
|
|
326
|
+
"info",
|
|
327
|
+
);
|
|
328
|
+
writeRoutingDecision(event, {
|
|
329
|
+
mode: configuredMode,
|
|
330
|
+
action: "handled",
|
|
331
|
+
reason: ctx.hasUI ? "user_cancelled_takeover" : "assist_confirmation_unavailable",
|
|
332
|
+
classification,
|
|
333
|
+
});
|
|
334
|
+
return { action: "handled" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
deps.emitCommandText(ctx, "Routing natural-language handoff into /cook.", "info");
|
|
338
|
+
writeRoutingDecision(event, {
|
|
339
|
+
mode: configuredMode,
|
|
340
|
+
action: "routed_to_cook",
|
|
341
|
+
reason: "accepted_takeover",
|
|
342
|
+
classification,
|
|
343
|
+
});
|
|
344
|
+
await runCookEntry(pi, ctx, deps, {
|
|
345
|
+
origin: "natural-language-trigger",
|
|
346
|
+
hintText: classification.focusHint,
|
|
347
|
+
originalInput: event.text,
|
|
348
|
+
});
|
|
349
|
+
return { action: "handled" };
|
|
350
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CompletionStateSnapshot,
|
|
5
|
+
CookTriggerClassification,
|
|
6
|
+
CookTriggerConfirmationActionItem,
|
|
7
|
+
CookTriggerConfirmationLayout,
|
|
8
|
+
LiveRoleActivity,
|
|
9
|
+
} from "./types";
|
|
4
10
|
import type {
|
|
5
11
|
ContextProposal,
|
|
6
12
|
ContextProposalAnalysis,
|
|
@@ -187,6 +193,98 @@ export function maybeWriteContextProposalSnapshot(proposal: ContextProposal, sna
|
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
195
|
|
|
196
|
+
function writeJsonSnapshot(snapshotPath: string | undefined, value: unknown): void {
|
|
197
|
+
if (!snapshotPath) return;
|
|
198
|
+
try {
|
|
199
|
+
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
200
|
+
fs.writeFileSync(snapshotPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
201
|
+
} catch {
|
|
202
|
+
// ignore malformed or unwritable test snapshot paths
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildCookTriggerClassifierPrompt(args: {
|
|
207
|
+
projectName: string;
|
|
208
|
+
inputText: string;
|
|
209
|
+
recentDiscussion: string;
|
|
210
|
+
workflowContextLines?: string[];
|
|
211
|
+
}): string {
|
|
212
|
+
const lines = [
|
|
213
|
+
`Project: ${args.projectName}`,
|
|
214
|
+
"Classify whether the current input is a natural-language handoff to the canonical /cook workflow before the primary agent starts implementation work.",
|
|
215
|
+
"Return JSON only with keys: intent, confidence, reason, evidence, riskFlags, focusHint.",
|
|
216
|
+
"intent must be exactly one of route_to_cook, normal_prompt, or unclear.",
|
|
217
|
+
"Use route_to_cook only when the user is handing control from recent discussion into workflow execution or explicitly asking to let /cook take over.",
|
|
218
|
+
"Use normal_prompt for ordinary questions, explanations, or direct agent requests that should stay in the main chat.",
|
|
219
|
+
"Use unclear for ambiguous approvals, short acknowledgements, or cases where false-positive routing risk is material.",
|
|
220
|
+
"focusHint is optional, must stay short, and must never rewrite the workflow mission or invent scope.",
|
|
221
|
+
"evidence and riskFlags must be arrays of short grounded strings.",
|
|
222
|
+
];
|
|
223
|
+
if (args.workflowContextLines?.length) lines.push("", "Canonical workflow context:", ...args.workflowContextLines);
|
|
224
|
+
lines.push("", `Current input: ${args.inputText}`, "", "Recent discussion:", args.recentDiscussion || "(none)");
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildCookTriggerConfirmationActions(mainChatRerunGuidance: string): CookTriggerConfirmationActionItem[] {
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
id: "start_cook",
|
|
232
|
+
label: "Start /cook",
|
|
233
|
+
description: "Let /cook take over before the primary agent starts implementation work.",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "keep_chatting",
|
|
237
|
+
label: "Keep chatting",
|
|
238
|
+
description: "Send the original message to the primary agent instead.",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "cancel",
|
|
242
|
+
label: "Cancel",
|
|
243
|
+
description: `Stop here without routing the message. ${mainChatRerunGuidance}`,
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function buildCookTriggerAssistConfirmationLayout(args: {
|
|
249
|
+
classification: CookTriggerClassification;
|
|
250
|
+
mainChatRerunGuidance: string;
|
|
251
|
+
}): CookTriggerConfirmationLayout {
|
|
252
|
+
const evidenceBody =
|
|
253
|
+
args.classification.evidence.length > 0
|
|
254
|
+
? args.classification.evidence.map((item) => `- ${item}`).join("\n")
|
|
255
|
+
: "- No additional evidence was captured beyond the current handoff signal.";
|
|
256
|
+
const riskBody = args.classification.riskFlags.length > 0 ? args.classification.riskFlags.map((item) => `- ${item}`).join("\n") : undefined;
|
|
257
|
+
return {
|
|
258
|
+
title: "Let /cook take over from the recent discussion?",
|
|
259
|
+
intro:
|
|
260
|
+
"This input looks like a natural-language handoff into the completion workflow. /cook would keep the existing approval-only startup, continue, refocus, and next-round semantics before canonical state changes.",
|
|
261
|
+
evidenceHeading: "Why it matched",
|
|
262
|
+
evidenceBody,
|
|
263
|
+
riskHeading: riskBody ? "Risk checks" : undefined,
|
|
264
|
+
riskBody,
|
|
265
|
+
focusHintHeading: args.classification.focusHint ? "Optional focus hint" : undefined,
|
|
266
|
+
focusHintBody: args.classification.focusHint,
|
|
267
|
+
actionsHeading: "Actions",
|
|
268
|
+
actions: buildCookTriggerConfirmationActions(args.mainChatRerunGuidance),
|
|
269
|
+
footer: "↑↓ navigate • enter select • esc cancel",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function maybeWriteCookTriggerClassifierSnapshot(snapshot: Record<string, unknown>, snapshotPath: string | undefined): void {
|
|
274
|
+
writeJsonSnapshot(snapshotPath, snapshot);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function maybeWriteCookTriggerConfirmationSnapshot(
|
|
278
|
+
layout: CookTriggerConfirmationLayout,
|
|
279
|
+
snapshotPath: string | undefined,
|
|
280
|
+
): void {
|
|
281
|
+
writeJsonSnapshot(snapshotPath, layout);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function maybeWriteCookTriggerRoutingSnapshot(snapshot: Record<string, unknown>, snapshotPath: string | undefined): void {
|
|
285
|
+
writeJsonSnapshot(snapshotPath, snapshot);
|
|
286
|
+
}
|
|
287
|
+
|
|
190
288
|
export function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout) {
|
|
191
289
|
return layout.actions.map((action) => ({
|
|
192
290
|
value: action.id,
|
|
@@ -350,7 +350,7 @@ function contextHintEntry(hintText: string | undefined): RecentDiscussionEntry[]
|
|
|
350
350
|
const RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX =
|
|
351
351
|
/(?:\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
352
|
|
|
353
|
-
function hasRecentDiscussionImplementationIntent(text: string, stripCodeBlocksFn: (text: string) => string): boolean {
|
|
353
|
+
export function hasRecentDiscussionImplementationIntent(text: string, stripCodeBlocksFn: (text: string) => string): boolean {
|
|
354
354
|
const cleaned = stripCodeBlocksFn(text).replace(/\r/g, " ").trim();
|
|
355
355
|
if (!cleaned) return false;
|
|
356
356
|
return hasStructuredContextProposalSignal(cleaned, stripCodeBlocksFn) || RECENT_DISCUSSION_IMPLEMENTATION_INTENT_REGEX.test(cleaned);
|