@linimin/pi-letscook 0.1.50 → 0.1.52
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 +19 -0
- package/README.md +87 -52
- package/extensions/completion/driver.ts +199 -134
- package/extensions/completion/index.ts +20 -7
- package/extensions/completion/input-routing.ts +818 -0
- package/extensions/completion/prompt-surfaces.ts +375 -1
- package/extensions/completion/proposal.ts +1 -1
- package/extensions/completion/role-runner.ts +310 -3
- package/extensions/completion/types.ts +114 -0
- package/package.json +1 -1
- package/scripts/cook-trigger-routing-test.sh +1122 -0
- package/scripts/release-check.sh +29 -21
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { runCookEntry, type CompletionDriverDeps } from "./driver";
|
|
5
|
+
import {
|
|
6
|
+
buildCookTriggerAssistConfirmationLayout,
|
|
7
|
+
buildCookTriggerClarificationLayout,
|
|
8
|
+
buildCookTriggerRecoveryLayout,
|
|
9
|
+
maybeWriteCookTriggerClarificationSnapshot,
|
|
10
|
+
maybeWriteCookTriggerConfirmationSnapshot,
|
|
11
|
+
maybeWriteCookTriggerRecoverySnapshot,
|
|
12
|
+
maybeWriteCookTriggerRoutingSnapshot,
|
|
13
|
+
} from "./prompt-surfaces";
|
|
14
|
+
import {
|
|
15
|
+
collectRecentDiscussionEntries,
|
|
16
|
+
hasRecentDiscussionImplementationIntent,
|
|
17
|
+
hasStructuredContextProposalSignal,
|
|
18
|
+
stripCodeBlocks,
|
|
19
|
+
} from "./proposal";
|
|
20
|
+
import {
|
|
21
|
+
classifyCookTriggerIntentWithAgent,
|
|
22
|
+
type CookTriggerClassifierResult,
|
|
23
|
+
} from "./role-runner";
|
|
24
|
+
import { asString, loadCompletionSnapshot } from "./state-store";
|
|
25
|
+
import type {
|
|
26
|
+
CompletionStateSnapshot,
|
|
27
|
+
CookNaturalLanguageHandoff,
|
|
28
|
+
CookTriggerAdoptedArtifact,
|
|
29
|
+
CookTriggerClarificationAction,
|
|
30
|
+
CookTriggerClarificationCapsule,
|
|
31
|
+
CookTriggerClassification,
|
|
32
|
+
CookTriggerConfirmationAction,
|
|
33
|
+
CookTriggerDecision,
|
|
34
|
+
CookTriggerRecoveryAction,
|
|
35
|
+
CookTriggerWorkflowBias,
|
|
36
|
+
NaturalLanguageCookTriggerMode,
|
|
37
|
+
} from "./types";
|
|
38
|
+
|
|
39
|
+
type InputRoutingEvent = {
|
|
40
|
+
text: string;
|
|
41
|
+
images?: unknown[];
|
|
42
|
+
source?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type InputRoutingContext = {
|
|
46
|
+
cwd: string;
|
|
47
|
+
hasUI: boolean;
|
|
48
|
+
ui: any;
|
|
49
|
+
sessionManager: any;
|
|
50
|
+
model?: any;
|
|
51
|
+
modelRegistry?: any;
|
|
52
|
+
isIdle: () => boolean;
|
|
53
|
+
hasPendingMessages: () => boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type ContextProposal = Awaited<ReturnType<CompletionDriverDeps["deriveCookContextProposal"]>>;
|
|
57
|
+
|
|
58
|
+
type RecentSessionMessage = {
|
|
59
|
+
role: "user" | "assistant" | "custom";
|
|
60
|
+
text: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const MAX_TRIGGER_CANDIDATE_LENGTH = 120;
|
|
64
|
+
const MAX_TRIGGER_CANDIDATE_LINES = 3;
|
|
65
|
+
const ADOPTED_ARTIFACT_PREVIEW_LIMIT = 280;
|
|
66
|
+
const ROUTER_BYPASS_REPLAY_PREFIX = "__pi_completion_router_bypass__:";
|
|
67
|
+
const ROUTER_FAILURE_RETRY_LIMIT = 2;
|
|
68
|
+
const CLEAR_TRIGGER_PATTERNS = [
|
|
69
|
+
/^(?:go ahead|please go ahead|proceed|let'?s do it|let'?s start|start(?: implementing| implementation| the workflow| the next round)?|begin(?: implementing| implementation| the workflow| the next round)?|continue(?: with implementation| implementing| the workflow)?|resume(?: the workflow| where we left off)?|next step|work on it|do it|ship it|let'?s do this instead|switch to this)\b/i,
|
|
70
|
+
/^(?:開始(?:做|實作|实现|落地|下一輪)|开始(?:做|实作|实现|落地|下一轮)|那就做吧|照(?:剛剛|刚刚|這個|这个|上述|上面的)?(?:討論|讨论|方向).*(?:做|實作|实现|落地)|可以開始(?:做|實作|实现|下一輪)?|可以开始(?:做|实作|实现|下一轮)?|繼續(?:做|實作|实现|往下做)|继续(?:做|实作|实现|往下做)|接著(?:做|實作|实现)|接着(?:做|实作|实现)|下一步|那改做(?:這個|这个)|先做新的那個方向|先做新的那个方向|好,開始做這個|好,开始做这个)/u,
|
|
71
|
+
];
|
|
72
|
+
const ADOPTED_PLAN_TRIGGER_PATTERNS = [
|
|
73
|
+
/^(?:use|follow|start from|begin from|work from|go with|implement from)\b.*\b(?:plan|proposal|spec|summary|notes|[\w./-]+\.md)\b/i,
|
|
74
|
+
/^(?:start|begin|implement|do)\b.*\b(?:from|using)\b.*\b(?:plan|proposal|spec|summary|[\w./-]+\.md)\b/i,
|
|
75
|
+
/^(?:照|依|按照|就照|跟著|跟着|用).*(?:剛剛|刚刚|最新|上面|上述|那份|這份|这份|這個|这个|方案|計劃|计划|提案|規格|规格|總結|总结|[\w./-]+\.md).*(?:做|開始|开始|實作|实现|落地)/u,
|
|
76
|
+
];
|
|
77
|
+
const EXPLICIT_ARTIFACT_ADOPTION_PATTERNS = [
|
|
78
|
+
/(?:\b(?:use|follow|start from|begin from|work from|go with|implement from)\b.*\b(?:plan|proposal|spec|summary|notes|[\w./-]+\.md)\b)/i,
|
|
79
|
+
/(?:照|依|按照|就照|跟著|跟着|用).*(?:剛剛|刚刚|最新|上面|上述|那份|這份|这份|這個|这个|方案|計劃|计划|提案|規格|规格|總結|总结|[\w./-]+\.md)/u,
|
|
80
|
+
];
|
|
81
|
+
const AMBIGUOUS_ACK_PATTERNS = [/^(?:ok|okay|sure|fine|yes|yeah|yep)$/i, /^(?:好|好的|可以|嗯|那就這樣|那就这样|就這樣|就这样|先這樣|先这样|收到)$/u];
|
|
82
|
+
const MARKDOWN_PATH_PATTERN = /(?:^|[\s("'`])((?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\.md)(?=$|[\s)"'`.,;:])/g;
|
|
83
|
+
|
|
84
|
+
function roleFromEnv(): string | undefined {
|
|
85
|
+
return asString(process.env.PI_COMPLETION_ROLE);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function configuredTriggerMode(): NaturalLanguageCookTriggerMode {
|
|
89
|
+
const raw =
|
|
90
|
+
asString(process.env.PI_COMPLETION_TEST_TRIGGER_MODE)?.toLowerCase() ??
|
|
91
|
+
asString(process.env.PI_COMPLETION_TRIGGER_MODE)?.toLowerCase() ??
|
|
92
|
+
"assist";
|
|
93
|
+
return raw === "off" || raw === "assist" || raw === "router" || raw === "auto" ? raw : "assist";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function effectiveTriggerMode(mode: NaturalLanguageCookTriggerMode): "off" | "assist" | "router" {
|
|
97
|
+
if (mode === "off") return "off";
|
|
98
|
+
if (mode === "assist") return "assist";
|
|
99
|
+
return "router";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function triggerRoutingSnapshotPath(): string | undefined {
|
|
103
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_ROUTING_PATH);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function triggerConfirmationSnapshotPath(): string | undefined {
|
|
107
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_CONFIRMATION_PATH);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function triggerClarificationSnapshotPath(): string | undefined {
|
|
111
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_CLARIFICATION_PATH);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function triggerRecoverySnapshotPath(): string | undefined {
|
|
115
|
+
return asString(process.env.PI_COMPLETION_TEST_TRIGGER_RECOVERY_PATH);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function triggerConfirmationOverride(): CookTriggerConfirmationAction | undefined {
|
|
119
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_TRIGGER_CONFIRM_ACTION)?.toLowerCase();
|
|
120
|
+
if (!raw) return undefined;
|
|
121
|
+
if (raw === "start" || raw === "start_cook" || raw === "start_workflow" || raw === "cook") return "start_workflow";
|
|
122
|
+
if (raw === "send_as_normal_chat" || raw === "send-as-normal-chat" || raw === "normal_chat" || raw === "normal-chat") return "send_as_normal_chat";
|
|
123
|
+
if (raw === "cancel" || raw === "dismiss") return "cancel";
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function triggerClarificationOverride(): CookTriggerClarificationAction | undefined {
|
|
128
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_TRIGGER_CLARIFICATION_ACTION)?.toLowerCase();
|
|
129
|
+
if (!raw) return undefined;
|
|
130
|
+
if (raw === "startup" || raw === "route_startup") return "route_startup";
|
|
131
|
+
if (raw === "resume" || raw === "route_resume") return "route_resume";
|
|
132
|
+
if (raw === "refocus" || raw === "route_refocus") return "route_refocus";
|
|
133
|
+
if (raw === "next_round" || raw === "next-round" || raw === "route_next_round") return "route_next_round";
|
|
134
|
+
if (raw === "send_as_normal_chat" || raw === "send-as-normal-chat" || raw === "normal_chat" || raw === "normal-chat") return "send_as_normal_chat";
|
|
135
|
+
if (raw === "cancel" || raw === "dismiss") return "cancel";
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function triggerRecoveryOverride(): CookTriggerRecoveryAction | undefined {
|
|
140
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_TRIGGER_RECOVERY_ACTION)?.toLowerCase();
|
|
141
|
+
if (!raw) return undefined;
|
|
142
|
+
if (raw === "retry" || raw === "retry_routing" || raw === "retry-routing") return "retry_routing";
|
|
143
|
+
if (raw === "send_as_normal_chat" || raw === "send-as-normal-chat" || raw === "normal_chat" || raw === "normal-chat") return "send_as_normal_chat";
|
|
144
|
+
if (raw === "cancel" || raw === "dismiss") return "cancel";
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeTriggerText(text: string): string {
|
|
149
|
+
return text.replace(/\s+/g, " ").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function truncateInline(text: string, maxLength = ADOPTED_ARTIFACT_PREVIEW_LIMIT): string {
|
|
153
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
154
|
+
return normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…` : normalized;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hasImages(event: InputRoutingEvent): boolean {
|
|
158
|
+
return Array.isArray(event.images) && event.images.length > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function activeWorkflowContext(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
162
|
+
return Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) !== "done";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isExplicitArtifactAdoption(text: string): boolean {
|
|
166
|
+
return EXPLICIT_ARTIFACT_ADOPTION_PATTERNS.some((pattern) => pattern.test(text));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function looksLikeTriggerCandidate(text: string): boolean {
|
|
170
|
+
const normalized = normalizeTriggerText(text);
|
|
171
|
+
if (!normalized) return false;
|
|
172
|
+
if (normalized.length > MAX_TRIGGER_CANDIDATE_LENGTH) return false;
|
|
173
|
+
if (text.split(/\r?\n/).length > MAX_TRIGGER_CANDIDATE_LINES) return false;
|
|
174
|
+
if (normalized.startsWith("/") || normalized.startsWith("!")) return false;
|
|
175
|
+
if (AMBIGUOUS_ACK_PATTERNS.some((pattern) => pattern.test(normalized))) return false;
|
|
176
|
+
return CLEAR_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized)) || ADOPTED_PLAN_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hasRecentImplementationContext(entries: Array<{ text: string }>): boolean {
|
|
180
|
+
return entries.some((entry) => hasRecentDiscussionImplementationIntent(entry.text, stripCodeBlocks));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildTriggerWorkflowContextLines(snapshot: CompletionStateSnapshot | undefined): string[] {
|
|
184
|
+
if (!snapshot) return [];
|
|
185
|
+
return [
|
|
186
|
+
`mission anchor: ${asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? "(none)"}`,
|
|
187
|
+
`continuation policy: ${asString(snapshot.state?.continuation_policy) ?? "(none)"}`,
|
|
188
|
+
`current phase: ${asString(snapshot.state?.current_phase) ?? "(none)"}`,
|
|
189
|
+
`next mandatory role: ${asString(snapshot.state?.next_mandatory_role) ?? "(none)"}`,
|
|
190
|
+
`active slice id: ${asString(snapshot.active?.slice_id) ?? "(none)"}`,
|
|
191
|
+
`active slice goal: ${asString(snapshot.active?.goal) ?? "(none)"}`,
|
|
192
|
+
`active slice why_now: ${asString(snapshot.active?.why_now) ?? "(none)"}`,
|
|
193
|
+
`latest completed slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
194
|
+
`latest verified slice: ${asString(snapshot.state?.latest_verified_slice) ?? "(none)"}`,
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeRoutingDecision(event: InputRoutingEvent, decision: CookTriggerDecision, extras?: Record<string, unknown>): void {
|
|
199
|
+
maybeWriteCookTriggerRoutingSnapshot(
|
|
200
|
+
{
|
|
201
|
+
text: event.text,
|
|
202
|
+
source: event.source ?? null,
|
|
203
|
+
configuredMode: decision.mode,
|
|
204
|
+
action: decision.action,
|
|
205
|
+
reason: decision.reason,
|
|
206
|
+
bypassReason: decision.bypassReason ?? null,
|
|
207
|
+
classificationDecision: decision.classification?.decision ?? null,
|
|
208
|
+
workflowBias: decision.classification?.workflowBias ?? null,
|
|
209
|
+
confidence: decision.classification?.confidence ?? null,
|
|
210
|
+
classifierReason: decision.classification?.reason ?? null,
|
|
211
|
+
focusHint: decision.classification?.focusHint ?? null,
|
|
212
|
+
evidence: decision.classification?.evidence ?? [],
|
|
213
|
+
riskFlags: decision.classification?.riskFlags ?? [],
|
|
214
|
+
...extras,
|
|
215
|
+
},
|
|
216
|
+
triggerRoutingSnapshotPath(),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function classifierFailureReason(result: CookTriggerClassifierResult): string {
|
|
221
|
+
switch (result.status) {
|
|
222
|
+
case "timeout":
|
|
223
|
+
return "classifier_timeout";
|
|
224
|
+
case "invalid_output":
|
|
225
|
+
return "classifier_invalid_output";
|
|
226
|
+
case "error":
|
|
227
|
+
default:
|
|
228
|
+
return "classifier_error";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function classifierFailureLabel(result: CookTriggerClassifierResult): string {
|
|
233
|
+
switch (result.status) {
|
|
234
|
+
case "timeout":
|
|
235
|
+
return "The router classifier timed out before it could decide whether /cook should take over.";
|
|
236
|
+
case "invalid_output":
|
|
237
|
+
return "The router classifier returned invalid JSON output, so the router refused to guess.";
|
|
238
|
+
case "error":
|
|
239
|
+
default:
|
|
240
|
+
return result.errorMessage?.trim() || "The router classifier failed before it could return a valid decision.";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function routerBypassReplayText(text: string): string {
|
|
245
|
+
return `${ROUTER_BYPASS_REPLAY_PREFIX}${text}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function consumeRouterBypassReplay(event: InputRoutingEvent): string | undefined {
|
|
249
|
+
if (event.source !== "extension") return undefined;
|
|
250
|
+
if (typeof event.text !== "string" || !event.text.startsWith(ROUTER_BYPASS_REPLAY_PREFIX)) return undefined;
|
|
251
|
+
return event.text.slice(ROUTER_BYPASS_REPLAY_PREFIX.length);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function replayOriginalMessageToPrimaryAgent(
|
|
255
|
+
pi: ExtensionAPI,
|
|
256
|
+
event: InputRoutingEvent,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
await pi.sendUserMessage(routerBypassReplayText(event.text));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function extractMessageText(content: unknown): string {
|
|
262
|
+
if (typeof content === "string") return content.trim();
|
|
263
|
+
if (!Array.isArray(content)) return "";
|
|
264
|
+
return content
|
|
265
|
+
.map((item) => {
|
|
266
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return "";
|
|
267
|
+
return item.type === "text" && typeof item.text === "string" ? item.text.trim() : "";
|
|
268
|
+
})
|
|
269
|
+
.filter((item) => item.length > 0)
|
|
270
|
+
.join("\n")
|
|
271
|
+
.trim();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function recentSessionMessages(ctx: InputRoutingContext, limit = 12): RecentSessionMessage[] {
|
|
275
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
276
|
+
const entries: RecentSessionMessage[] = [];
|
|
277
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
278
|
+
const entry = branch[index];
|
|
279
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
280
|
+
if (entry.type !== "message" || typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) continue;
|
|
281
|
+
const role = asString(entry.message.role);
|
|
282
|
+
if (role !== "user" && role !== "assistant" && role !== "custom") continue;
|
|
283
|
+
const text = extractMessageText(entry.message.content);
|
|
284
|
+
if (!text || /^\/(?:cook|complete)\b/i.test(text)) continue;
|
|
285
|
+
entries.push({ role, text });
|
|
286
|
+
if (entries.length >= limit) break;
|
|
287
|
+
}
|
|
288
|
+
return entries;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function extractMarkdownPath(text: string): string | undefined {
|
|
292
|
+
let match: RegExpExecArray | null;
|
|
293
|
+
while ((match = MARKDOWN_PATH_PATTERN.exec(text)) !== null) {
|
|
294
|
+
const candidate = match[1]?.trim();
|
|
295
|
+
if (candidate) return candidate;
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function readRepoMarkdownArtifact(root: string, candidatePath: string): Promise<CookTriggerAdoptedArtifact | undefined> {
|
|
301
|
+
const resolved = path.resolve(root, candidatePath);
|
|
302
|
+
const relative = path.relative(root, resolved);
|
|
303
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
|
|
304
|
+
try {
|
|
305
|
+
const raw = await fsp.readFile(resolved, "utf8");
|
|
306
|
+
return {
|
|
307
|
+
kind: "repo_markdown",
|
|
308
|
+
basis: "explicit_user_adoption",
|
|
309
|
+
title: candidatePath,
|
|
310
|
+
path: candidatePath,
|
|
311
|
+
preview: truncateInline(raw),
|
|
312
|
+
};
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function findRecentPlanArtifact(recentMessages: RecentSessionMessage[]): CookTriggerAdoptedArtifact | undefined {
|
|
319
|
+
for (const entry of recentMessages) {
|
|
320
|
+
if (entry.role !== "assistant" && entry.role !== "custom") continue;
|
|
321
|
+
if (!hasStructuredContextProposalSignal(entry.text, stripCodeBlocks) && !/(?:plan|proposal|spec|方案|計劃|计划|提案|規格|规格)/iu.test(entry.text)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
kind: "recent_plan",
|
|
326
|
+
basis: "explicit_user_adoption",
|
|
327
|
+
title: entry.role === "assistant" ? "latest discussed assistant plan" : "latest discussed plan",
|
|
328
|
+
preview: truncateInline(entry.text),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function detectExplicitAdoptedArtifact(
|
|
335
|
+
eventText: string,
|
|
336
|
+
ctx: InputRoutingContext,
|
|
337
|
+
root: string,
|
|
338
|
+
recentMessages: RecentSessionMessage[],
|
|
339
|
+
): Promise<CookTriggerAdoptedArtifact | undefined> {
|
|
340
|
+
if (!isExplicitArtifactAdoption(eventText)) return undefined;
|
|
341
|
+
const markdownPath = extractMarkdownPath(eventText);
|
|
342
|
+
if (markdownPath) {
|
|
343
|
+
return readRepoMarkdownArtifact(root, markdownPath);
|
|
344
|
+
}
|
|
345
|
+
return findRecentPlanArtifact(recentMessages);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildAdoptedArtifactHint(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): string | undefined {
|
|
349
|
+
if (!adoptedArtifact) return undefined;
|
|
350
|
+
const lines = [`User explicitly adopted ${adoptedArtifact.kind === "repo_markdown" ? "repo markdown artifact" : "recent plan"}: ${adoptedArtifact.title}`];
|
|
351
|
+
if (adoptedArtifact.path) lines.push(`Artifact path: ${adoptedArtifact.path}`);
|
|
352
|
+
if (adoptedArtifact.preview) lines.push(`Artifact preview: ${adoptedArtifact.preview}`);
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildClarificationWorkflowBiases(
|
|
357
|
+
snapshot: CompletionStateSnapshot | undefined,
|
|
358
|
+
proposal: ContextProposal,
|
|
359
|
+
): CookTriggerWorkflowBias[] {
|
|
360
|
+
if (!snapshot) return ["startup"];
|
|
361
|
+
if (!activeWorkflowContext(snapshot)) return ["next_round"];
|
|
362
|
+
const currentMission = asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? asString(snapshot.active?.mission_anchor);
|
|
363
|
+
if (proposal?.mission && currentMission && proposal.mission.trim() === currentMission.trim()) {
|
|
364
|
+
return ["resume"];
|
|
365
|
+
}
|
|
366
|
+
if (proposal?.mission && currentMission && proposal.mission.trim() !== currentMission.trim()) {
|
|
367
|
+
return ["resume", "refocus"];
|
|
368
|
+
}
|
|
369
|
+
return ["resume"];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clarificationBiasFromAction(action: CookTriggerClarificationAction): CookTriggerWorkflowBias | undefined {
|
|
373
|
+
switch (action) {
|
|
374
|
+
case "route_startup":
|
|
375
|
+
return "startup";
|
|
376
|
+
case "route_resume":
|
|
377
|
+
return "resume";
|
|
378
|
+
case "route_refocus":
|
|
379
|
+
return "refocus";
|
|
380
|
+
case "route_next_round":
|
|
381
|
+
return "next_round";
|
|
382
|
+
default:
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildClarificationCapsule(
|
|
388
|
+
action: CookTriggerClarificationAction,
|
|
389
|
+
classification: CookTriggerClassification,
|
|
390
|
+
proposal: ContextProposal,
|
|
391
|
+
): CookTriggerClarificationCapsule | undefined {
|
|
392
|
+
const selectedWorkflowBias = clarificationBiasFromAction(action);
|
|
393
|
+
if (!selectedWorkflowBias) return undefined;
|
|
394
|
+
return {
|
|
395
|
+
selectedWorkflowBias,
|
|
396
|
+
reason: classification.reason,
|
|
397
|
+
goal: proposal?.mission ?? classification.focusHint,
|
|
398
|
+
scope: proposal?.scope?.slice(0, 3),
|
|
399
|
+
nonGoal: proposal?.constraints?.slice(0, 2),
|
|
400
|
+
doneWhen: proposal?.acceptance?.slice(0, 2),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function routingExtrasForArtifact(adoptedArtifact: CookTriggerAdoptedArtifact | undefined): Record<string, unknown> {
|
|
405
|
+
return adoptedArtifact
|
|
406
|
+
? {
|
|
407
|
+
adoptedArtifactKind: adoptedArtifact.kind,
|
|
408
|
+
adoptedArtifactBasis: adoptedArtifact.basis,
|
|
409
|
+
adoptedArtifactTitle: adoptedArtifact.title,
|
|
410
|
+
adoptedArtifactPath: adoptedArtifact.path ?? null,
|
|
411
|
+
adoptedArtifactPreview: adoptedArtifact.preview ?? null,
|
|
412
|
+
}
|
|
413
|
+
: {
|
|
414
|
+
adoptedArtifactKind: null,
|
|
415
|
+
adoptedArtifactBasis: null,
|
|
416
|
+
adoptedArtifactTitle: null,
|
|
417
|
+
adoptedArtifactPath: null,
|
|
418
|
+
adoptedArtifactPreview: null,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function routingExtrasForClarification(clarificationCapsule: CookNaturalLanguageHandoff["clarificationCapsule"] | undefined): Record<string, unknown> {
|
|
423
|
+
return clarificationCapsule
|
|
424
|
+
? {
|
|
425
|
+
clarificationSelectedBias: clarificationCapsule.selectedWorkflowBias,
|
|
426
|
+
clarificationReason: clarificationCapsule.reason,
|
|
427
|
+
clarificationGoal: clarificationCapsule.goal ?? null,
|
|
428
|
+
clarificationScope: clarificationCapsule.scope ?? [],
|
|
429
|
+
clarificationNonGoal: clarificationCapsule.nonGoal ?? [],
|
|
430
|
+
clarificationDoneWhen: clarificationCapsule.doneWhen ?? [],
|
|
431
|
+
}
|
|
432
|
+
: {
|
|
433
|
+
clarificationSelectedBias: null,
|
|
434
|
+
clarificationReason: null,
|
|
435
|
+
clarificationGoal: null,
|
|
436
|
+
clarificationScope: [],
|
|
437
|
+
clarificationNonGoal: [],
|
|
438
|
+
clarificationDoneWhen: [],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function promptCookTriggerTakeover(
|
|
443
|
+
ctx: InputRoutingContext,
|
|
444
|
+
classification: CookTriggerClassification,
|
|
445
|
+
deps: CompletionDriverDeps,
|
|
446
|
+
): Promise<CookTriggerConfirmationAction> {
|
|
447
|
+
const override = triggerConfirmationOverride();
|
|
448
|
+
const layout = buildCookTriggerAssistConfirmationLayout({
|
|
449
|
+
classification,
|
|
450
|
+
mainChatRerunGuidance: deps.mainChatRerunGuidance,
|
|
451
|
+
});
|
|
452
|
+
maybeWriteCookTriggerConfirmationSnapshot(layout, triggerConfirmationSnapshotPath());
|
|
453
|
+
if (override) return override;
|
|
454
|
+
if (!ctx.hasUI || !ctx.ui) return "cancel";
|
|
455
|
+
const choices = layout.actions.map((action) => `${action.label}\n\n${action.description}`);
|
|
456
|
+
const titleParts = [layout.title, "", layout.intro];
|
|
457
|
+
if (layout.evidenceHeading && layout.evidenceBody) titleParts.push("", layout.evidenceHeading, layout.evidenceBody);
|
|
458
|
+
if (layout.riskHeading && layout.riskBody) titleParts.push("", layout.riskHeading, layout.riskBody);
|
|
459
|
+
if (layout.focusHintHeading && layout.focusHintBody) titleParts.push("", layout.focusHintHeading, layout.focusHintBody);
|
|
460
|
+
const choice = await ctx.ui.select(titleParts.join("\n"), choices);
|
|
461
|
+
if (!choice) return "cancel";
|
|
462
|
+
const index = choices.indexOf(choice);
|
|
463
|
+
return index >= 0 ? layout.actions[index].id : "cancel";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function promptCookTriggerClarification(
|
|
467
|
+
ctx: InputRoutingContext,
|
|
468
|
+
snapshot: CompletionStateSnapshot | undefined,
|
|
469
|
+
proposal: ContextProposal,
|
|
470
|
+
adoptedArtifact: CookTriggerAdoptedArtifact | undefined,
|
|
471
|
+
deps: CompletionDriverDeps,
|
|
472
|
+
): Promise<CookTriggerClarificationAction> {
|
|
473
|
+
const workflowBiases = buildClarificationWorkflowBiases(snapshot, proposal);
|
|
474
|
+
const override = triggerClarificationOverride();
|
|
475
|
+
const layout = buildCookTriggerClarificationLayout({
|
|
476
|
+
currentMission: asString(snapshot?.state?.mission_anchor) ?? asString(snapshot?.plan?.mission_anchor),
|
|
477
|
+
candidateMission: proposal?.mission,
|
|
478
|
+
workflowBiases,
|
|
479
|
+
mainChatRerunGuidance: deps.mainChatRerunGuidance,
|
|
480
|
+
adoptedArtifact,
|
|
481
|
+
});
|
|
482
|
+
maybeWriteCookTriggerClarificationSnapshot(layout, triggerClarificationSnapshotPath());
|
|
483
|
+
if (override) return override;
|
|
484
|
+
if (!ctx.hasUI || !ctx.ui) return "cancel";
|
|
485
|
+
const choices = layout.actions.map((action) => `${action.label}\n\n${action.description}`);
|
|
486
|
+
const titleParts = [layout.title, "", layout.intro];
|
|
487
|
+
if (layout.currentMissionHeading && layout.currentMissionBody) titleParts.push("", layout.currentMissionHeading, layout.currentMissionBody);
|
|
488
|
+
if (layout.candidateMissionHeading && layout.candidateMissionBody) titleParts.push("", layout.candidateMissionHeading, layout.candidateMissionBody);
|
|
489
|
+
if (layout.adoptedArtifactHeading && layout.adoptedArtifactBody) titleParts.push("", layout.adoptedArtifactHeading, layout.adoptedArtifactBody);
|
|
490
|
+
const choice = await ctx.ui.select(titleParts.join("\n"), choices);
|
|
491
|
+
if (!choice) return "cancel";
|
|
492
|
+
const index = choices.indexOf(choice);
|
|
493
|
+
return index >= 0 ? layout.actions[index].id : "cancel";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function promptCookTriggerRecovery(
|
|
497
|
+
ctx: InputRoutingContext,
|
|
498
|
+
result: CookTriggerClassifierResult,
|
|
499
|
+
deps: CompletionDriverDeps,
|
|
500
|
+
): Promise<CookTriggerRecoveryAction> {
|
|
501
|
+
const override = triggerRecoveryOverride();
|
|
502
|
+
const layout = buildCookTriggerRecoveryLayout({
|
|
503
|
+
failureLabel: classifierFailureLabel(result),
|
|
504
|
+
mainChatRerunGuidance: deps.mainChatRerunGuidance,
|
|
505
|
+
});
|
|
506
|
+
maybeWriteCookTriggerRecoverySnapshot(layout, triggerRecoverySnapshotPath());
|
|
507
|
+
if (override) return override;
|
|
508
|
+
if (!ctx.hasUI || !ctx.ui) return "cancel";
|
|
509
|
+
const choices = layout.actions.map((action) => `${action.label}\n\n${action.description}`);
|
|
510
|
+
const titleParts = [layout.title, "", layout.intro];
|
|
511
|
+
if (layout.failureHeading && layout.failureBody) titleParts.push("", layout.failureHeading, layout.failureBody);
|
|
512
|
+
const choice = await ctx.ui.select(titleParts.join("\n"), choices);
|
|
513
|
+
if (!choice) return "cancel";
|
|
514
|
+
const index = choices.indexOf(choice);
|
|
515
|
+
return index >= 0 ? layout.actions[index].id : "cancel";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildHandoffHintText(
|
|
519
|
+
classification: CookTriggerClassification,
|
|
520
|
+
clarificationCapsule: CookNaturalLanguageHandoff["clarificationCapsule"] | undefined,
|
|
521
|
+
adoptedArtifact: CookTriggerAdoptedArtifact | undefined,
|
|
522
|
+
): string | undefined {
|
|
523
|
+
return clarificationCapsule?.goal ?? classification.focusHint ?? adoptedArtifact?.title;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function handleCookNaturalLanguageTrigger(
|
|
527
|
+
pi: ExtensionAPI,
|
|
528
|
+
event: InputRoutingEvent,
|
|
529
|
+
ctx: InputRoutingContext,
|
|
530
|
+
deps: CompletionDriverDeps,
|
|
531
|
+
): Promise<{ action: "continue" | "handled" } | { action: "transform"; text: string; images?: unknown[] }> {
|
|
532
|
+
const replayText = consumeRouterBypassReplay(event);
|
|
533
|
+
if (replayText !== undefined) {
|
|
534
|
+
return { action: "transform", text: replayText, images: event.images };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const configuredMode = configuredTriggerMode();
|
|
538
|
+
const mode = effectiveTriggerMode(configuredMode);
|
|
539
|
+
if (mode === "off") {
|
|
540
|
+
writeRoutingDecision(event, { mode: configuredMode, action: "continue", reason: "mode_off" });
|
|
541
|
+
return { action: "continue" };
|
|
542
|
+
}
|
|
543
|
+
if (roleFromEnv()) {
|
|
544
|
+
writeRoutingDecision(event, {
|
|
545
|
+
mode: configuredMode,
|
|
546
|
+
action: "continue",
|
|
547
|
+
reason: "completion_role_subprocess",
|
|
548
|
+
bypassReason: "completion_role_subprocess",
|
|
549
|
+
});
|
|
550
|
+
return { action: "continue" };
|
|
551
|
+
}
|
|
552
|
+
if ((event.text ?? "").trimStart().startsWith("/")) {
|
|
553
|
+
writeRoutingDecision(event, {
|
|
554
|
+
mode: configuredMode,
|
|
555
|
+
action: "continue",
|
|
556
|
+
reason: "slash_command",
|
|
557
|
+
bypassReason: "slash_command",
|
|
558
|
+
});
|
|
559
|
+
return { action: "continue" };
|
|
560
|
+
}
|
|
561
|
+
if (event.source === "extension") {
|
|
562
|
+
writeRoutingDecision(event, {
|
|
563
|
+
mode: configuredMode,
|
|
564
|
+
action: "continue",
|
|
565
|
+
reason: "extension_source",
|
|
566
|
+
bypassReason: "extension_source",
|
|
567
|
+
});
|
|
568
|
+
return { action: "continue" };
|
|
569
|
+
}
|
|
570
|
+
if (hasImages(event)) {
|
|
571
|
+
writeRoutingDecision(event, {
|
|
572
|
+
mode: configuredMode,
|
|
573
|
+
action: "continue",
|
|
574
|
+
reason: "image_turn",
|
|
575
|
+
bypassReason: "image_turn",
|
|
576
|
+
});
|
|
577
|
+
return { action: "continue" };
|
|
578
|
+
}
|
|
579
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
580
|
+
writeRoutingDecision(event, {
|
|
581
|
+
mode: configuredMode,
|
|
582
|
+
action: "continue",
|
|
583
|
+
reason: "non_idle_turn",
|
|
584
|
+
bypassReason: "non_idle_turn",
|
|
585
|
+
});
|
|
586
|
+
return { action: "continue" };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const snapshot = await loadCompletionSnapshot(ctx.cwd);
|
|
590
|
+
const root = snapshot?.files.root ?? ctx.cwd;
|
|
591
|
+
const projectName = path.basename(root);
|
|
592
|
+
const recentEntries = collectRecentDiscussionEntries(ctx, {
|
|
593
|
+
asString,
|
|
594
|
+
isRecord: (value) => typeof value === "object" && value !== null && !Array.isArray(value),
|
|
595
|
+
}, 6);
|
|
596
|
+
const recentMessages = recentSessionMessages(ctx, 12);
|
|
597
|
+
const adoptedArtifact = await detectExplicitAdoptedArtifact(event.text, ctx, root, recentMessages);
|
|
598
|
+
const routerMode = mode === "router";
|
|
599
|
+
if (!routerMode && !activeWorkflowContext(snapshot) && !hasRecentImplementationContext(recentEntries) && !adoptedArtifact) {
|
|
600
|
+
writeRoutingDecision(event, {
|
|
601
|
+
mode: configuredMode,
|
|
602
|
+
action: "continue",
|
|
603
|
+
reason: "no_workflow_or_recent_implementation_context",
|
|
604
|
+
bypassReason: "no_workflow_or_recent_implementation_context",
|
|
605
|
+
}, routingExtrasForArtifact(adoptedArtifact));
|
|
606
|
+
return { action: "continue" };
|
|
607
|
+
}
|
|
608
|
+
if (!routerMode && !looksLikeTriggerCandidate(event.text)) {
|
|
609
|
+
writeRoutingDecision(event, {
|
|
610
|
+
mode: configuredMode,
|
|
611
|
+
action: "continue",
|
|
612
|
+
reason: "not_candidate",
|
|
613
|
+
bypassReason: "not_candidate",
|
|
614
|
+
}, routingExtrasForArtifact(adoptedArtifact));
|
|
615
|
+
return { action: "continue" };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let classifier: CookTriggerClassifierResult | undefined;
|
|
619
|
+
for (let attempt = 0; attempt < ROUTER_FAILURE_RETRY_LIMIT; attempt += 1) {
|
|
620
|
+
classifier = await classifyCookTriggerIntentWithAgent({
|
|
621
|
+
ctx,
|
|
622
|
+
projectName,
|
|
623
|
+
inputText: normalizeTriggerText(event.text),
|
|
624
|
+
recentEntries,
|
|
625
|
+
workflowContextLines: buildTriggerWorkflowContextLines(snapshot),
|
|
626
|
+
});
|
|
627
|
+
if (classifier.status === "classified" && classifier.classification) break;
|
|
628
|
+
const recovery = await promptCookTriggerRecovery(ctx, classifier, deps);
|
|
629
|
+
if (recovery === "retry_routing" && attempt + 1 < ROUTER_FAILURE_RETRY_LIMIT) {
|
|
630
|
+
deps.emitCommandText(ctx, "Retrying workflow-aware router once before deciding whether /cook should take over.", "info");
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (recovery === "send_as_normal_chat") {
|
|
634
|
+
await replayOriginalMessageToPrimaryAgent(pi, event);
|
|
635
|
+
deps.emitCommandText(ctx, "Replayed the original message once to the main chat path and bypassed router interception for that replay.", "info");
|
|
636
|
+
writeRoutingDecision(event, {
|
|
637
|
+
mode: configuredMode,
|
|
638
|
+
action: "handled",
|
|
639
|
+
reason: `${classifierFailureReason(classifier)}_send_as_normal_chat`,
|
|
640
|
+
}, {
|
|
641
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
642
|
+
recoveryAction: recovery,
|
|
643
|
+
errorMessage: classifier.errorMessage ?? null,
|
|
644
|
+
rawOutput: classifier.rawOutput ?? null,
|
|
645
|
+
replayedToPrimaryAgent: true,
|
|
646
|
+
replayBypassMarkerApplied: true,
|
|
647
|
+
});
|
|
648
|
+
return { action: "handled" };
|
|
649
|
+
}
|
|
650
|
+
deps.emitCommandText(
|
|
651
|
+
ctx,
|
|
652
|
+
"Cancelled router recovery without replaying the original message. If you want the completion workflow boundary, rerun /cook explicitly.",
|
|
653
|
+
"info",
|
|
654
|
+
);
|
|
655
|
+
writeRoutingDecision(event, {
|
|
656
|
+
mode: configuredMode,
|
|
657
|
+
action: "handled",
|
|
658
|
+
reason: `${classifierFailureReason(classifier)}_cancelled`,
|
|
659
|
+
}, {
|
|
660
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
661
|
+
recoveryAction: recovery,
|
|
662
|
+
errorMessage: classifier.errorMessage ?? null,
|
|
663
|
+
rawOutput: classifier.rawOutput ?? null,
|
|
664
|
+
replayedToPrimaryAgent: false,
|
|
665
|
+
replayBypassMarkerApplied: false,
|
|
666
|
+
});
|
|
667
|
+
return { action: "handled" };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!classifier || classifier.status !== "classified" || !classifier.classification) {
|
|
671
|
+
deps.emitCommandText(ctx, "Router recovery stopped without replaying the original message. If you still want the workflow boundary, rerun /cook explicitly.", "info");
|
|
672
|
+
writeRoutingDecision(event, {
|
|
673
|
+
mode: configuredMode,
|
|
674
|
+
action: "handled",
|
|
675
|
+
reason: classifier ? `${classifierFailureReason(classifier)}_retry_exhausted` : "classifier_error_retry_exhausted",
|
|
676
|
+
}, {
|
|
677
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
678
|
+
recoveryAction: "retry_routing",
|
|
679
|
+
errorMessage: classifier?.errorMessage ?? null,
|
|
680
|
+
rawOutput: classifier?.rawOutput ?? null,
|
|
681
|
+
replayedToPrimaryAgent: false,
|
|
682
|
+
replayBypassMarkerApplied: false,
|
|
683
|
+
});
|
|
684
|
+
return { action: "handled" };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const classification = classifier.classification;
|
|
688
|
+
if (classification.decision === "normal_prompt") {
|
|
689
|
+
writeRoutingDecision(event, {
|
|
690
|
+
mode: configuredMode,
|
|
691
|
+
action: "continue",
|
|
692
|
+
reason: "classifier_normal_prompt",
|
|
693
|
+
classification,
|
|
694
|
+
}, routingExtrasForArtifact(adoptedArtifact));
|
|
695
|
+
return { action: "continue" };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const proposalHint = buildAdoptedArtifactHint(adoptedArtifact);
|
|
699
|
+
if (classification.decision === "unclear") {
|
|
700
|
+
const proposal = await deps.deriveCookContextProposal(ctx, projectName, proposalHint);
|
|
701
|
+
const clarification = await promptCookTriggerClarification(ctx, snapshot, proposal, adoptedArtifact, deps);
|
|
702
|
+
if (clarification === "send_as_normal_chat") {
|
|
703
|
+
await replayOriginalMessageToPrimaryAgent(pi, event);
|
|
704
|
+
deps.emitCommandText(ctx, "Replayed the original message once to the main chat path and bypassed router interception for that clarification replay.", "info");
|
|
705
|
+
writeRoutingDecision(event, {
|
|
706
|
+
mode: configuredMode,
|
|
707
|
+
action: "handled",
|
|
708
|
+
reason: "user_sent_as_normal_chat_after_clarification",
|
|
709
|
+
classification,
|
|
710
|
+
}, {
|
|
711
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
712
|
+
clarificationAction: clarification,
|
|
713
|
+
replayedToPrimaryAgent: true,
|
|
714
|
+
replayBypassMarkerApplied: true,
|
|
715
|
+
});
|
|
716
|
+
return { action: "handled" };
|
|
717
|
+
}
|
|
718
|
+
if (clarification === "cancel") {
|
|
719
|
+
deps.emitCommandText(
|
|
720
|
+
ctx,
|
|
721
|
+
"Cancelled commandless workflow clarification. If you want the workflow boundary, rerun /cook explicitly.",
|
|
722
|
+
"info",
|
|
723
|
+
);
|
|
724
|
+
writeRoutingDecision(event, {
|
|
725
|
+
mode: configuredMode,
|
|
726
|
+
action: "handled",
|
|
727
|
+
reason: triggerClarificationOverride() ? "user_cancelled_clarification" : ctx.hasUI ? "user_cancelled_clarification" : "clarification_unavailable",
|
|
728
|
+
classification,
|
|
729
|
+
}, {
|
|
730
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
731
|
+
clarificationAction: clarification,
|
|
732
|
+
replayedToPrimaryAgent: false,
|
|
733
|
+
replayBypassMarkerApplied: false,
|
|
734
|
+
});
|
|
735
|
+
return { action: "handled" };
|
|
736
|
+
}
|
|
737
|
+
const clarificationCapsule = buildClarificationCapsule(clarification, classification, proposal);
|
|
738
|
+
const selectedBias = clarificationBiasFromAction(clarification) ?? classification.workflowBias;
|
|
739
|
+
deps.emitCommandText(ctx, "Routing clarified natural-language handoff into /cook.", "info");
|
|
740
|
+
writeRoutingDecision(event, {
|
|
741
|
+
mode: configuredMode,
|
|
742
|
+
action: "routed_to_cook",
|
|
743
|
+
reason: "clarification_resolved",
|
|
744
|
+
classification,
|
|
745
|
+
}, {
|
|
746
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
747
|
+
...routingExtrasForClarification(clarificationCapsule),
|
|
748
|
+
clarificationAction: clarification,
|
|
749
|
+
});
|
|
750
|
+
await runCookEntry(pi, ctx, deps, {
|
|
751
|
+
origin: "natural-language-trigger",
|
|
752
|
+
hintText: buildHandoffHintText(classification, clarificationCapsule, adoptedArtifact),
|
|
753
|
+
originalInput: event.text,
|
|
754
|
+
triggerText: event.text,
|
|
755
|
+
preferredRoutingBias: selectedBias,
|
|
756
|
+
clarificationCapsule,
|
|
757
|
+
adoptedArtifact,
|
|
758
|
+
});
|
|
759
|
+
return { action: "handled" };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const confirmation = await promptCookTriggerTakeover(ctx, classification, deps);
|
|
763
|
+
if (confirmation === "send_as_normal_chat") {
|
|
764
|
+
await replayOriginalMessageToPrimaryAgent(pi, event);
|
|
765
|
+
deps.emitCommandText(ctx, "Replayed the original message once to the main chat path and bypassed router interception for that workflow-offer replay.", "info");
|
|
766
|
+
writeRoutingDecision(event, {
|
|
767
|
+
mode: configuredMode,
|
|
768
|
+
action: "handled",
|
|
769
|
+
reason: "user_sent_as_normal_chat",
|
|
770
|
+
classification,
|
|
771
|
+
}, {
|
|
772
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
773
|
+
confirmationAction: confirmation,
|
|
774
|
+
replayedToPrimaryAgent: true,
|
|
775
|
+
replayBypassMarkerApplied: true,
|
|
776
|
+
});
|
|
777
|
+
return { action: "handled" };
|
|
778
|
+
}
|
|
779
|
+
if (confirmation === "cancel") {
|
|
780
|
+
deps.emitCommandText(
|
|
781
|
+
ctx,
|
|
782
|
+
"Cancelled natural-language /cook takeover. If you want the workflow boundary, rerun /cook explicitly.",
|
|
783
|
+
"info",
|
|
784
|
+
);
|
|
785
|
+
writeRoutingDecision(event, {
|
|
786
|
+
mode: configuredMode,
|
|
787
|
+
action: "handled",
|
|
788
|
+
reason: ctx.hasUI ? "user_cancelled_takeover" : "assist_confirmation_unavailable",
|
|
789
|
+
classification,
|
|
790
|
+
}, {
|
|
791
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
792
|
+
confirmationAction: confirmation,
|
|
793
|
+
replayedToPrimaryAgent: false,
|
|
794
|
+
replayBypassMarkerApplied: false,
|
|
795
|
+
});
|
|
796
|
+
return { action: "handled" };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
deps.emitCommandText(ctx, "Routing natural-language handoff into /cook.", "info");
|
|
800
|
+
writeRoutingDecision(event, {
|
|
801
|
+
mode: configuredMode,
|
|
802
|
+
action: "routed_to_cook",
|
|
803
|
+
reason: "accepted_takeover",
|
|
804
|
+
classification,
|
|
805
|
+
}, {
|
|
806
|
+
...routingExtrasForArtifact(adoptedArtifact),
|
|
807
|
+
confirmationAction: confirmation,
|
|
808
|
+
});
|
|
809
|
+
await runCookEntry(pi, ctx, deps, {
|
|
810
|
+
origin: "natural-language-trigger",
|
|
811
|
+
hintText: buildHandoffHintText(classification, undefined, adoptedArtifact),
|
|
812
|
+
originalInput: event.text,
|
|
813
|
+
triggerText: event.text,
|
|
814
|
+
preferredRoutingBias: classification.workflowBias,
|
|
815
|
+
adoptedArtifact,
|
|
816
|
+
});
|
|
817
|
+
return { action: "handled" };
|
|
818
|
+
}
|