@linimin/pi-letscook 0.1.45 → 0.1.46
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 +6 -1
- package/README.md +9 -10
- package/extensions/completion/driver.ts +615 -0
- package/extensions/completion/index.ts +427 -3488
- package/extensions/completion/policy-guards.ts +110 -0
- package/extensions/completion/prompt-surfaces.ts +512 -0
- package/extensions/completion/proposal.ts +944 -0
- package/extensions/completion/role-runner.ts +455 -0
- package/extensions/completion/state-store.ts +458 -0
- package/extensions/completion/status-surface.ts +516 -0
- package/extensions/completion/transcription.ts +77 -0
- package/extensions/completion/types.ts +87 -0
- package/package.json +1 -1
- package/scripts/active-slice-contract-test.sh +5 -4
- package/scripts/canonical-evidence-artifact-test.sh +4 -4
- package/scripts/context-proposal-test.sh +21 -14
- package/scripts/legacy-cleanup-test.sh +107 -0
- package/scripts/observability-status-test.sh +39 -0
- package/scripts/refocus-test.sh +6 -6
- package/scripts/release-check.sh +17 -16
- package/scripts/role-runner-contract-test.sh +44 -0
- package/scripts/rubric-contract-test.sh +8 -6
- package/scripts/smoke-test.sh +5 -5
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildContextProposalAnalystPrompt,
|
|
3
|
+
buildContextProposalGoalText,
|
|
4
|
+
} from "./prompt-surfaces";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TASK_TYPE = "completion-workflow";
|
|
7
|
+
const DEFAULT_EVALUATION_PROFILE = "completion-rubric-v1";
|
|
8
|
+
|
|
9
|
+
type JsonRecord = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export type ContextProposalAnalysis = {
|
|
12
|
+
taskType?: string;
|
|
13
|
+
evaluationProfile?: string;
|
|
14
|
+
critique: string[];
|
|
15
|
+
risks: string[];
|
|
16
|
+
possibleNoise: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ContextProposal = {
|
|
20
|
+
mission: string;
|
|
21
|
+
scope: string[];
|
|
22
|
+
constraints: string[];
|
|
23
|
+
acceptance: string[];
|
|
24
|
+
analysis: ContextProposalAnalysis;
|
|
25
|
+
goalText: string;
|
|
26
|
+
basisPreview: string;
|
|
27
|
+
source: "session" | "analyst";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ContextProposalSection = "mission" | "scope" | "constraints" | "acceptance" | "critique" | "risks";
|
|
31
|
+
|
|
32
|
+
export type RecentDiscussionEntry = {
|
|
33
|
+
role: "user" | "assistant" | "custom" | "summary";
|
|
34
|
+
text: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ContextProposalDecision = {
|
|
38
|
+
missionAnchor: string;
|
|
39
|
+
goalText: string;
|
|
40
|
+
analysis: ContextProposalAnalysis;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ContextProposalConfirmAction = "start" | "cancel";
|
|
44
|
+
|
|
45
|
+
export type ContextProposalConfirmationActionItem = {
|
|
46
|
+
id: ContextProposalConfirmAction;
|
|
47
|
+
label: string;
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ContextProposalConfirmationLayout = {
|
|
52
|
+
title: string;
|
|
53
|
+
intro: string;
|
|
54
|
+
proposalHeading: string;
|
|
55
|
+
proposalBody: string;
|
|
56
|
+
critiqueHeading?: string;
|
|
57
|
+
critiqueBody?: string;
|
|
58
|
+
routingHeading?: string;
|
|
59
|
+
routingBody?: string;
|
|
60
|
+
actionsHeading: string;
|
|
61
|
+
actions: ContextProposalConfirmationActionItem[];
|
|
62
|
+
footer: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ContextProposalConfirmOptions = {
|
|
66
|
+
title: string;
|
|
67
|
+
nonInteractiveBehavior?: "accept" | "cancel";
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ProposalCommonDeps = {
|
|
71
|
+
asString: (value: unknown) => string | undefined;
|
|
72
|
+
asStringArray: (value: unknown) => string[];
|
|
73
|
+
assessMissionAnchor: (text: string, projectName: string) => { derived: string };
|
|
74
|
+
normalizeMissionAnchorText: (text: string) => string;
|
|
75
|
+
isWeakMissionAnchor: (text: string) => boolean;
|
|
76
|
+
missionAnchorsStrictlyEquivalent: (left: string, right: string) => boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ProposalParseDeps = ProposalCommonDeps & {
|
|
80
|
+
stripCodeBlocks: (text: string) => string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type AnalystParseDeps = ProposalCommonDeps & {
|
|
84
|
+
extractJsonObjectFromText: (text: string) => string | undefined;
|
|
85
|
+
isRecord: (value: unknown) => value is JsonRecord;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function localAsString(value: unknown): string | undefined {
|
|
89
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function localAsStringArray(value: unknown): string[] {
|
|
93
|
+
return Array.isArray(value)
|
|
94
|
+
? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
95
|
+
: [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function localIsRecord(value: unknown): value is JsonRecord {
|
|
99
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractTextFromMessageContent(content: unknown): string {
|
|
103
|
+
if (typeof content === "string") return content.trim();
|
|
104
|
+
if (!Array.isArray(content)) return "";
|
|
105
|
+
return content
|
|
106
|
+
.map((item) => {
|
|
107
|
+
if (!localIsRecord(item)) return "";
|
|
108
|
+
if (item.type !== "text") return "";
|
|
109
|
+
return localAsString(item.text) ?? "";
|
|
110
|
+
})
|
|
111
|
+
.filter((item) => item.length > 0)
|
|
112
|
+
.join("\n")
|
|
113
|
+
.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function normalizeMissionAnchorText(value: string): string {
|
|
117
|
+
return value
|
|
118
|
+
.replace(/^\/(?:cook|complete)\s+/i, "")
|
|
119
|
+
.replace(/^["'“”‘’]+|["'“”‘’]+$/g, "")
|
|
120
|
+
.replace(/^\s*(please|pls|can you|could you|help me|i want to|we need to|let'?s|continue to|continue|resume)\s+/i, "")
|
|
121
|
+
.replace(/\s+/g, " ")
|
|
122
|
+
.replace(/[。!?.!?]+$/u, "")
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isWeakMissionAnchor(value: string): boolean {
|
|
127
|
+
const normalized = value.trim().toLowerCase();
|
|
128
|
+
if (normalized.length < 8) return true;
|
|
129
|
+
if (["continue", "resume", "fix", "fix it", "work on this", "help", "do it", "try again"].includes(normalized)) return true;
|
|
130
|
+
if (/^(continue|resume|fix|help|work on)(\s+.*)?$/i.test(normalized) && normalized.split(/\s+/).length <= 3) return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function deriveMissionAnchor(rawGoal: string, projectName: string): string {
|
|
135
|
+
const normalized = normalizeMissionAnchorText(rawGoal);
|
|
136
|
+
if (!normalized || isWeakMissionAnchor(normalized)) {
|
|
137
|
+
return `Drive ${projectName} to truthful, verifiable completion.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let mission = normalized
|
|
141
|
+
.replace(/\b(end[- ]to[- ]end|for me|thanks|thank you)\b/gi, "")
|
|
142
|
+
.replace(/\s+/g, " ")
|
|
143
|
+
.trim();
|
|
144
|
+
|
|
145
|
+
mission = mission
|
|
146
|
+
.replace(/\bwith tests and docs\b/gi, "with tests and docs parity")
|
|
147
|
+
.replace(/\bwith tests and documentation\b/gi, "with tests and docs parity")
|
|
148
|
+
.replace(/\bwith docs\b/gi, "with docs parity")
|
|
149
|
+
.trim();
|
|
150
|
+
|
|
151
|
+
if (!/[.!?。!?]$/u.test(mission)) mission += ".";
|
|
152
|
+
return mission;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function assessMissionAnchor(rawGoal: string, projectName: string): { derived: string } {
|
|
156
|
+
return { derived: deriveMissionAnchor(rawGoal, projectName) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function stripCodeBlocks(text: string): string {
|
|
160
|
+
return text.replace(/```[\s\S]*?```/g, " ");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
164
|
+
"a",
|
|
165
|
+
"an",
|
|
166
|
+
"and",
|
|
167
|
+
"are",
|
|
168
|
+
"as",
|
|
169
|
+
"at",
|
|
170
|
+
"be",
|
|
171
|
+
"by",
|
|
172
|
+
"for",
|
|
173
|
+
"from",
|
|
174
|
+
"goal",
|
|
175
|
+
"goals",
|
|
176
|
+
"in",
|
|
177
|
+
"into",
|
|
178
|
+
"is",
|
|
179
|
+
"it",
|
|
180
|
+
"its",
|
|
181
|
+
"mission",
|
|
182
|
+
"of",
|
|
183
|
+
"on",
|
|
184
|
+
"or",
|
|
185
|
+
"scope",
|
|
186
|
+
"that",
|
|
187
|
+
"the",
|
|
188
|
+
"their",
|
|
189
|
+
"this",
|
|
190
|
+
"to",
|
|
191
|
+
"using",
|
|
192
|
+
"with",
|
|
193
|
+
"workflow",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
function missionScopeFilterTokens(text: string): string[] {
|
|
197
|
+
const normalized = normalizeProposalLine(text).toLowerCase();
|
|
198
|
+
const tokens = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
199
|
+
return tokens.filter((token) => {
|
|
200
|
+
if (/^[\p{Script=Han}]+$/u.test(token)) return token.length >= 2;
|
|
201
|
+
if (token.length < 2) return false;
|
|
202
|
+
return !MISSION_SCOPE_FILTER_STOPWORDS.has(token);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function isSessionScopeItemMissionRelevant(item: string, mission: string): boolean {
|
|
207
|
+
const normalizedItem = normalizeProposalLine(item).toLowerCase();
|
|
208
|
+
const normalizedMission = normalizeMissionAnchorText(mission).toLowerCase();
|
|
209
|
+
if (!normalizedItem || !normalizedMission) return true;
|
|
210
|
+
if (normalizedItem.includes(normalizedMission) || normalizedMission.includes(normalizedItem)) return true;
|
|
211
|
+
const itemTokens = [...new Set(missionScopeFilterTokens(normalizedItem))];
|
|
212
|
+
const missionTokens = new Set(missionScopeFilterTokens(normalizedMission));
|
|
213
|
+
if (itemTokens.length === 0 || missionTokens.size === 0) return true;
|
|
214
|
+
const overlap = itemTokens.filter((token) => missionTokens.has(token));
|
|
215
|
+
if (overlap.length >= 2) return true;
|
|
216
|
+
return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function missionAnchorSemanticTokens(text: string): string[] {
|
|
220
|
+
return [...new Set(missionScopeFilterTokens(normalizeMissionAnchorText(text).toLowerCase()))];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function missionAnchorOrderedTokenOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
|
|
224
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
|
|
225
|
+
const dp = new Array(rightTokens.length + 1).fill(0);
|
|
226
|
+
for (const leftToken of leftTokens) {
|
|
227
|
+
let previous = 0;
|
|
228
|
+
for (let index = 0; index < rightTokens.length; index += 1) {
|
|
229
|
+
const nextPrevious = dp[index + 1];
|
|
230
|
+
if (leftToken === rightTokens[index]) {
|
|
231
|
+
dp[index + 1] = previous + 1;
|
|
232
|
+
} else {
|
|
233
|
+
dp[index + 1] = Math.max(dp[index + 1], dp[index]);
|
|
234
|
+
}
|
|
235
|
+
previous = nextPrevious;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return dp[rightTokens.length] / Math.max(leftTokens.length, rightTokens.length);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function missionAnchorBigramOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
|
|
242
|
+
if (leftTokens.length < 2 || rightTokens.length < 2) return 0;
|
|
243
|
+
const leftBigrams = new Set(leftTokens.slice(0, -1).map((token, index) => `${token} ${leftTokens[index + 1]}`));
|
|
244
|
+
const rightBigrams = new Set(rightTokens.slice(0, -1).map((token, index) => `${token} ${rightTokens[index + 1]}`));
|
|
245
|
+
if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
|
|
246
|
+
let overlap = 0;
|
|
247
|
+
for (const bigram of leftBigrams) {
|
|
248
|
+
if (rightBigrams.has(bigram)) overlap += 1;
|
|
249
|
+
}
|
|
250
|
+
return overlap / Math.max(leftBigrams.size, rightBigrams.size);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function missionAnchorsStrictlyEquivalent(left: string, right: string): boolean {
|
|
254
|
+
return normalizeMissionAnchorText(left).toLowerCase() === normalizeMissionAnchorText(right).toLowerCase();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const MISSION_NEGATION_CUE_REGEX = /(?:^|[^\p{L}\p{N}_])(?:no|not|without|never|cannot|don['’]?t)(?=$|[^\p{L}\p{N}_])/u;
|
|
258
|
+
|
|
259
|
+
function missionAnchorHasNegationCue(text: string): boolean {
|
|
260
|
+
return MISSION_NEGATION_CUE_REGEX.test(text);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function missionAnchorsLikelyEquivalent(left: string, right: string): boolean {
|
|
264
|
+
const normalizedLeft = normalizeMissionAnchorText(left).toLowerCase();
|
|
265
|
+
const normalizedRight = normalizeMissionAnchorText(right).toLowerCase();
|
|
266
|
+
if (!normalizedLeft || !normalizedRight) return false;
|
|
267
|
+
const leftHasNegationCue = missionAnchorHasNegationCue(normalizedLeft);
|
|
268
|
+
const rightHasNegationCue = missionAnchorHasNegationCue(normalizedRight);
|
|
269
|
+
if (leftHasNegationCue !== rightHasNegationCue) return false;
|
|
270
|
+
if (normalizedLeft === normalizedRight) return true;
|
|
271
|
+
if (!leftHasNegationCue && (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft))) return true;
|
|
272
|
+
const leftTokens = missionAnchorSemanticTokens(normalizedLeft);
|
|
273
|
+
const rightTokens = missionAnchorSemanticTokens(normalizedRight);
|
|
274
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) return false;
|
|
275
|
+
const rightSet = new Set(rightTokens);
|
|
276
|
+
const overlap = leftTokens.filter((token) => rightSet.has(token));
|
|
277
|
+
if (overlap.length < 3) return false;
|
|
278
|
+
const maxLen = Math.max(leftTokens.length, rightTokens.length);
|
|
279
|
+
if (overlap.length / maxLen < 0.75) return false;
|
|
280
|
+
if (missionAnchorOrderedTokenOverlapRatio(leftTokens, rightTokens) < 0.75) return false;
|
|
281
|
+
if (Math.min(leftTokens.length, rightTokens.length) < 4) return true;
|
|
282
|
+
return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function collectRecentDiscussionEntries(ctx: { sessionManager: any }, deps: {
|
|
286
|
+
isRecord: (value: unknown) => boolean;
|
|
287
|
+
asString?: (value: unknown) => string | undefined;
|
|
288
|
+
isStaleContextError?: (error: unknown) => boolean;
|
|
289
|
+
}, limit = 8): RecentDiscussionEntry[] {
|
|
290
|
+
let branch: any[] = [];
|
|
291
|
+
try {
|
|
292
|
+
branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (deps.isStaleContextError?.(error)) return [];
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
const asStringValue = deps.asString ?? localAsString;
|
|
298
|
+
const entries: RecentDiscussionEntry[] = [];
|
|
299
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
300
|
+
const entry = branch[index];
|
|
301
|
+
if (!deps.isRecord(entry) || entry.type !== "message" || !deps.isRecord(entry.message)) continue;
|
|
302
|
+
const message = entry.message as JsonRecord;
|
|
303
|
+
let text = "";
|
|
304
|
+
let role: RecentDiscussionEntry["role"] | undefined;
|
|
305
|
+
const messageRole = asStringValue(message.role);
|
|
306
|
+
if (messageRole === "user" || messageRole === "custom") {
|
|
307
|
+
text = extractTextFromMessageContent(message.content);
|
|
308
|
+
role = messageRole;
|
|
309
|
+
}
|
|
310
|
+
if (!text || !role) continue;
|
|
311
|
+
const trimmed = text.trim();
|
|
312
|
+
if (!trimmed || /^\/(?:cook|complete)\b/i.test(trimmed)) continue;
|
|
313
|
+
entries.push({ role, text: trimmed });
|
|
314
|
+
if (entries.length >= limit) break;
|
|
315
|
+
}
|
|
316
|
+
return entries;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[]): string {
|
|
320
|
+
return entries
|
|
321
|
+
.slice()
|
|
322
|
+
.reverse()
|
|
323
|
+
.map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
|
|
324
|
+
.join("\n\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function extractJsonObjectFromText(text: string): string | undefined {
|
|
328
|
+
const trimmed = text.trim();
|
|
329
|
+
if (!trimmed) return undefined;
|
|
330
|
+
const unfenced = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
331
|
+
if (unfenced.startsWith("{") && unfenced.endsWith("}")) return unfenced;
|
|
332
|
+
const start = unfenced.indexOf("{");
|
|
333
|
+
const end = unfenced.lastIndexOf("}");
|
|
334
|
+
if (start < 0 || end <= start) return undefined;
|
|
335
|
+
return unfenced.slice(start, end + 1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function normalizeProposalLine(line: string): string {
|
|
339
|
+
return line
|
|
340
|
+
.replace(/^[-*+]\s+/, "")
|
|
341
|
+
.replace(/^\d+[.)]\s+/, "")
|
|
342
|
+
.replace(/^\[.?\]\s+/, "")
|
|
343
|
+
.replace(/^>\s*/, "")
|
|
344
|
+
.replace(/^[`*_~]+|[`*_~]+$/g, "")
|
|
345
|
+
.replace(/^\*\*(.+)\*\*$/u, "$1")
|
|
346
|
+
.replace(/^__([^_]+)__$/u, "$1")
|
|
347
|
+
.trim();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function detectProposalSection(line: string): ContextProposalSection | undefined {
|
|
351
|
+
const normalized = normalizeProposalLine(line)
|
|
352
|
+
.toLowerCase()
|
|
353
|
+
.replace(/[::]$/, "")
|
|
354
|
+
.trim();
|
|
355
|
+
if (!normalized) return undefined;
|
|
356
|
+
if (["mission", "goal", "objective", "summary", "目標", "任務", "計劃", "计划", "方案"].includes(normalized)) return "mission";
|
|
357
|
+
if (["scope", "plan", "steps", "implementation", "範圍", "范围", "實作", "实现", "步驟", "步骤"].includes(normalized)) return "scope";
|
|
358
|
+
if (["constraints", "constraint", "guardrails", "non-goals", "限制", "約束", "约束", "非目標", "非目标"].includes(normalized)) return "constraints";
|
|
359
|
+
if (["acceptance", "acceptance criteria", "deliverables", "verification", "驗收", "验收", "交付", "驗證", "验证"].includes(normalized)) return "acceptance";
|
|
360
|
+
if (["critique", "critic", "concerns", "concern", "warnings", "warning", "notes", "note", "評論", "评论", "提醒"].includes(normalized)) return "critique";
|
|
361
|
+
if (["risk", "risks", "hazards", "hazard", "failure modes", "failure mode", "風險", "风险"].includes(normalized)) return "risks";
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function matchInlineProposalSection(line: string): { section: ContextProposalSection; content: string } | undefined {
|
|
366
|
+
const normalized = normalizeProposalLine(line);
|
|
367
|
+
const match = normalized.match(/^([^::]+)[::]\s*(.+)$/u);
|
|
368
|
+
if (!match) return undefined;
|
|
369
|
+
const [, rawLabel, rawContent] = match;
|
|
370
|
+
const section = detectProposalSection(rawLabel);
|
|
371
|
+
const content = rawContent.trim();
|
|
372
|
+
if (!section || !content) return undefined;
|
|
373
|
+
return { section, content };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function bulletText(line: string): string | undefined {
|
|
377
|
+
if (!/^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(line)) return undefined;
|
|
378
|
+
const normalized = normalizeProposalLine(line);
|
|
379
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function looksLikeConstraint(text: string): boolean {
|
|
383
|
+
return /(do not|don't|must not|avoid|without|keep\b|preserve|retain|remain|不要|不可|不能|不應|不应|保持|保留|避免)/i.test(text);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function looksLikeAcceptance(text: string): boolean {
|
|
387
|
+
return /(test|tests|testing|verify|verification|validated|README|docs?|documentation|regression|observability|驗證|验证|測試|测试|文件|文檔|文档|回歸|回归)/i.test(text);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function uniqueProposalItems(items: string[]): string[] {
|
|
391
|
+
const seen = new Set<string>();
|
|
392
|
+
const result: string[] = [];
|
|
393
|
+
for (const item of items) {
|
|
394
|
+
const normalized = normalizeProposalLine(item).replace(/\s+/g, " ").trim();
|
|
395
|
+
if (!normalized) continue;
|
|
396
|
+
const key = normalized.toLowerCase();
|
|
397
|
+
if (seen.has(key)) continue;
|
|
398
|
+
seen.add(key);
|
|
399
|
+
result.push(normalized);
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function normalizeContextProposalHint(value: unknown, asString: (value: unknown) => string | undefined): string | undefined {
|
|
405
|
+
const normalized = asString(value)?.replace(/\s+/g, " ").trim();
|
|
406
|
+
return normalized || undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizeContextProposalTaskTypeHint(value: unknown, asString: (value: unknown) => string | undefined): string | undefined {
|
|
410
|
+
const normalized = normalizeContextProposalHint(value, asString);
|
|
411
|
+
if (!normalized) return undefined;
|
|
412
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
413
|
+
return canonical === DEFAULT_TASK_TYPE ? DEFAULT_TASK_TYPE : normalized;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeContextProposalEvaluationProfileHint(value: unknown, asString: (value: unknown) => string | undefined): string | undefined {
|
|
417
|
+
const normalized = normalizeContextProposalHint(value, asString);
|
|
418
|
+
if (!normalized) return undefined;
|
|
419
|
+
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
420
|
+
return canonical === DEFAULT_EVALUATION_PROFILE ? DEFAULT_EVALUATION_PROFILE : normalized;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function inferContextProposalTaskType(texts: string[]): string | undefined {
|
|
424
|
+
const corpus = texts
|
|
425
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
426
|
+
.filter(Boolean)
|
|
427
|
+
.join("\n");
|
|
428
|
+
if (!corpus) return undefined;
|
|
429
|
+
return /(completion|\/cook|\/complete|\.agent|slice|reground|reviewer|auditor|stop judge|stop-judge|workflow)/i.test(corpus)
|
|
430
|
+
? DEFAULT_TASK_TYPE
|
|
431
|
+
: undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function inferContextProposalEvaluationProfile(texts: string[], taskType?: string): string | undefined {
|
|
435
|
+
const corpus = texts
|
|
436
|
+
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
437
|
+
.filter(Boolean)
|
|
438
|
+
.join("\n");
|
|
439
|
+
if (!corpus) return undefined;
|
|
440
|
+
if (
|
|
441
|
+
/(rubric|evaluation[_\s-]*profile|pass\|concern\|fail|contract coverage|correctness risk|verification evidence|docs\/state parity|reviewer|auditor|stop judge|stop-judge)/i.test(
|
|
442
|
+
corpus,
|
|
443
|
+
)
|
|
444
|
+
) {
|
|
445
|
+
return DEFAULT_EVALUATION_PROFILE;
|
|
446
|
+
}
|
|
447
|
+
return taskType === DEFAULT_TASK_TYPE && /(completion|\/cook|\/complete|slice|workflow|review|audit)/i.test(corpus)
|
|
448
|
+
? DEFAULT_EVALUATION_PROFILE
|
|
449
|
+
: undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function buildContextProposalAnalysis(args: {
|
|
453
|
+
taskType?: unknown;
|
|
454
|
+
evaluationProfile?: unknown;
|
|
455
|
+
critique?: string[];
|
|
456
|
+
risks?: string[];
|
|
457
|
+
possibleNoise?: string[];
|
|
458
|
+
hintTexts?: string[];
|
|
459
|
+
}, deps: Pick<ProposalCommonDeps, "asString">): ContextProposalAnalysis {
|
|
460
|
+
const critique = uniqueProposalItems(args.critique ?? []);
|
|
461
|
+
const risks = uniqueProposalItems(args.risks ?? []);
|
|
462
|
+
const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
|
|
463
|
+
const hintTexts = [...(args.hintTexts ?? []), ...critique, ...risks, ...possibleNoise];
|
|
464
|
+
const taskType = normalizeContextProposalTaskTypeHint(args.taskType, deps.asString) ?? inferContextProposalTaskType(hintTexts);
|
|
465
|
+
const evaluationProfile =
|
|
466
|
+
normalizeContextProposalEvaluationProfileHint(args.evaluationProfile, deps.asString) ??
|
|
467
|
+
inferContextProposalEvaluationProfile(hintTexts, taskType);
|
|
468
|
+
return {
|
|
469
|
+
taskType,
|
|
470
|
+
evaluationProfile,
|
|
471
|
+
critique,
|
|
472
|
+
risks,
|
|
473
|
+
possibleNoise,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function mergeContextProposalAnalysis(
|
|
478
|
+
sources: Array<ContextProposalAnalysis | undefined>,
|
|
479
|
+
hintTexts: string[] = [],
|
|
480
|
+
): ContextProposalAnalysis {
|
|
481
|
+
const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
|
|
482
|
+
const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
|
|
483
|
+
const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
|
|
484
|
+
const taskType =
|
|
485
|
+
sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
|
|
486
|
+
inferContextProposalTaskType([...hintTexts, ...critique, ...risks, ...possibleNoise]);
|
|
487
|
+
const evaluationProfile =
|
|
488
|
+
sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
|
|
489
|
+
inferContextProposalEvaluationProfile([...hintTexts, ...critique, ...risks, ...possibleNoise], taskType);
|
|
490
|
+
return {
|
|
491
|
+
taskType,
|
|
492
|
+
evaluationProfile,
|
|
493
|
+
critique,
|
|
494
|
+
risks,
|
|
495
|
+
possibleNoise,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function finalizeContextProposalAnalysis(
|
|
500
|
+
analysis: ContextProposalAnalysis | undefined,
|
|
501
|
+
hintTexts: string[] = [],
|
|
502
|
+
): ContextProposalAnalysis {
|
|
503
|
+
const merged = mergeContextProposalAnalysis(analysis ? [analysis] : [], hintTexts);
|
|
504
|
+
return {
|
|
505
|
+
taskType: merged.taskType ?? DEFAULT_TASK_TYPE,
|
|
506
|
+
evaluationProfile: merged.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
507
|
+
critique: merged.critique,
|
|
508
|
+
risks: merged.risks,
|
|
509
|
+
possibleNoise: merged.possibleNoise,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function matchContextProposalRoutingHint(
|
|
514
|
+
line: string,
|
|
515
|
+
): { field: "taskType" | "evaluationProfile"; value: string } | undefined {
|
|
516
|
+
const normalized = normalizeProposalLine(line);
|
|
517
|
+
const match = normalized.match(/^(task[\s_-]*type|evaluation[\s_-]*profile)[::]\s*(.+)$/iu);
|
|
518
|
+
if (!match) return undefined;
|
|
519
|
+
const label = match[1].toLowerCase().replace(/[\s_-]+/g, "");
|
|
520
|
+
const value = match[2].trim();
|
|
521
|
+
if (!value) return undefined;
|
|
522
|
+
return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX =
|
|
526
|
+
/(?:\b(?:start(?:ing)?|begin|continue|continu(?:e|ing)|resume|implement(?:ing)?|execute|execut(?:e|ing)|carry out|work on|ship|build(?:ing)?)\b.*\b(?:this|that|the|current|latest)\s+(?:plan|proposal|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan)\b|(?:開始|著手|繼續|继续|恢復|恢复)?(?:實作|实现|執行|执行|落地|完成)(?:這個|这个|此|該|该)?(?:方案|計畫|计划|提案|規劃|规划|設計|设计))/iu;
|
|
527
|
+
const CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX =
|
|
528
|
+
/(?:\b(?:write|draft|prepare|create|produce|share|deliver|document|review)\b.*\b(?:plan|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan|proposal)\b|(?:撰寫|撰写|編寫|编写|起草|準備|准备|產出|产出|整理|分享|交付|審查|审查).*(?:計畫|计划|規格|规格|設計文件|设计文档|提案|方案))/iu;
|
|
529
|
+
const CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX = /(?:\b(?:docs? only|documentation only)\b|(?:只改文件|僅文件|仅文件))/iu;
|
|
530
|
+
const CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX =
|
|
531
|
+
/(?:\b(?:no code(?: changes?)?|without code(?: changes?)?|do not implement|don't implement|planning only|proposal only|spec only|design[- ]doc only|no runtime changes?)\b|(?:不改(?:動)?代碼|不改代码|不要實作|不要实现|只規劃|只规划|僅規劃|仅规划|不改(?:動)?執行|不改运行))/iu;
|
|
532
|
+
const CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX =
|
|
533
|
+
/(?:\b(?:normalize|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(?:ing)?|document(?:ing)?|writ(?:e|ing))\b|(?:修正|修復|修复|更新|新增|移除|恢復|恢复|重構|重构|調整|调整|正規化|规范化|規範化|过滤|過濾|分離|分离|刷新|替換|替换|抑制|對齊|对齐|實作|实现|落地|修補|修补|阻止|允許|允许|轉換|转换|保留|保持))/iu;
|
|
534
|
+
|
|
535
|
+
function contextProposalBodyTexts(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string[] {
|
|
536
|
+
return [...proposal.scope, ...proposal.constraints, ...proposal.acceptance];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isGenericPlanningMissionAnchor(text: string, deps: Pick<ProposalCommonDeps, "normalizeMissionAnchorText">): boolean {
|
|
540
|
+
const normalized = deps.normalizeMissionAnchorText(text);
|
|
541
|
+
if (!normalized) return false;
|
|
542
|
+
return CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX.test(normalized);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function hasExplicitPlanningOnlyDeliverable(texts: string[]): boolean {
|
|
546
|
+
return texts.some((text) => CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX.test(normalizeProposalLine(text)));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeImplementationMissionSourceText(text: string): string {
|
|
550
|
+
const normalized = normalizeProposalLine(text);
|
|
551
|
+
if (!normalized) return "";
|
|
552
|
+
return normalized
|
|
553
|
+
.replace(new RegExp(`${CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX.source}[\\s::;,/\\-]*`, "giu"), " ")
|
|
554
|
+
.replace(/\s+([,.;:!?])/g, "$1")
|
|
555
|
+
.replace(/\s+/g, " ")
|
|
556
|
+
.trim();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function hasClearNoImplementationSignal(texts: string[]): boolean {
|
|
560
|
+
return texts.some((text) => CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX.test(normalizeProposalLine(text)));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function implementationMissionSourceCandidateText(text: string): string | undefined {
|
|
564
|
+
const normalized = normalizeImplementationMissionSourceText(text);
|
|
565
|
+
if (!normalized) return undefined;
|
|
566
|
+
if (hasExplicitPlanningOnlyDeliverable([normalized])) return undefined;
|
|
567
|
+
if (hasClearNoImplementationSignal([normalized])) return undefined;
|
|
568
|
+
if (!CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalized)) return undefined;
|
|
569
|
+
return normalized;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function pickImplementationMissionSource(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string | undefined {
|
|
573
|
+
for (const item of proposal.scope) {
|
|
574
|
+
const candidate = implementationMissionSourceCandidateText(item);
|
|
575
|
+
if (candidate) return candidate;
|
|
576
|
+
}
|
|
577
|
+
for (const item of proposal.acceptance) {
|
|
578
|
+
const candidate = implementationMissionSourceCandidateText(item);
|
|
579
|
+
if (candidate) return candidate;
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function hasPlanningArtifactOnlyContext(
|
|
585
|
+
proposal: Pick<ContextProposal, "mission" | "scope" | "constraints" | "acceptance">,
|
|
586
|
+
): boolean {
|
|
587
|
+
const texts = [proposal.mission, ...contextProposalBodyTexts(proposal)];
|
|
588
|
+
if (!hasExplicitPlanningOnlyDeliverable(texts)) return false;
|
|
589
|
+
return !pickImplementationMissionSource(proposal);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function finalizeContextProposal(proposal: ContextProposal, projectName: string, deps: ProposalCommonDeps): ContextProposal | undefined {
|
|
593
|
+
if (hasPlanningArtifactOnlyContext(proposal)) return undefined;
|
|
594
|
+
if (!isGenericPlanningMissionAnchor(proposal.mission, deps)) return proposal;
|
|
595
|
+
const missionSource = pickImplementationMissionSource(proposal);
|
|
596
|
+
if (!missionSource) return undefined;
|
|
597
|
+
const nextMission = deps.assessMissionAnchor(missionSource, projectName).derived;
|
|
598
|
+
const normalizedNextMission = deps.normalizeMissionAnchorText(nextMission);
|
|
599
|
+
if (!normalizedNextMission || deps.isWeakMissionAnchor(normalizedNextMission)) return undefined;
|
|
600
|
+
if (deps.missionAnchorsStrictlyEquivalent(nextMission, proposal.mission)) return proposal;
|
|
601
|
+
return {
|
|
602
|
+
...proposal,
|
|
603
|
+
mission: nextMission,
|
|
604
|
+
goalText: buildContextProposalGoalText({
|
|
605
|
+
mission: nextMission,
|
|
606
|
+
scope: proposal.scope,
|
|
607
|
+
constraints: proposal.constraints,
|
|
608
|
+
acceptance: proposal.acceptance,
|
|
609
|
+
}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
|
|
614
|
+
if (proposal.source === "session") {
|
|
615
|
+
return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
|
|
616
|
+
}
|
|
617
|
+
return (
|
|
618
|
+
proposal.scope.length > 0 &&
|
|
619
|
+
proposal.constraints.length > 0 &&
|
|
620
|
+
proposal.acceptance.length > 0 &&
|
|
621
|
+
proposal.analysis.possibleNoise.length === 0
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function parseContextProposalAnalystOutput(
|
|
626
|
+
raw: string,
|
|
627
|
+
projectName: string,
|
|
628
|
+
deps: AnalystParseDeps = {
|
|
629
|
+
extractJsonObjectFromText,
|
|
630
|
+
isRecord: localIsRecord,
|
|
631
|
+
asString: localAsString,
|
|
632
|
+
asStringArray: localAsStringArray,
|
|
633
|
+
assessMissionAnchor,
|
|
634
|
+
normalizeMissionAnchorText,
|
|
635
|
+
isWeakMissionAnchor,
|
|
636
|
+
missionAnchorsStrictlyEquivalent,
|
|
637
|
+
},
|
|
638
|
+
): ContextProposal | undefined {
|
|
639
|
+
const jsonText = deps.extractJsonObjectFromText(raw);
|
|
640
|
+
if (!jsonText) return undefined;
|
|
641
|
+
let parsed: unknown;
|
|
642
|
+
try {
|
|
643
|
+
parsed = JSON.parse(jsonText);
|
|
644
|
+
} catch {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
if (!deps.isRecord(parsed)) return undefined;
|
|
648
|
+
const missionSource = deps.asString(parsed.mission) ?? deps.asString(parsed.goal) ?? deps.asString(parsed.summary);
|
|
649
|
+
if (!missionSource) return undefined;
|
|
650
|
+
const assessment = deps.assessMissionAnchor(missionSource, projectName);
|
|
651
|
+
const normalizedMission = deps.normalizeMissionAnchorText(missionSource);
|
|
652
|
+
if (!normalizedMission || deps.isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
653
|
+
const mission = assessment.derived;
|
|
654
|
+
const scope = uniqueProposalItems(deps.asStringArray(parsed.scope));
|
|
655
|
+
const constraints = uniqueProposalItems(deps.asStringArray(parsed.constraints));
|
|
656
|
+
const acceptance = uniqueProposalItems(deps.asStringArray(parsed.acceptance));
|
|
657
|
+
const analysis = buildContextProposalAnalysis(
|
|
658
|
+
{
|
|
659
|
+
taskType: parsed.task_type ?? parsed.taskType,
|
|
660
|
+
evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
|
|
661
|
+
critique: deps.asStringArray(parsed.critique),
|
|
662
|
+
risks: deps.asStringArray(parsed.risks ?? parsed.risk),
|
|
663
|
+
possibleNoise: deps.asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
664
|
+
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
665
|
+
},
|
|
666
|
+
deps,
|
|
667
|
+
);
|
|
668
|
+
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
669
|
+
return finalizeContextProposal(
|
|
670
|
+
{
|
|
671
|
+
mission,
|
|
672
|
+
scope,
|
|
673
|
+
constraints,
|
|
674
|
+
acceptance,
|
|
675
|
+
analysis,
|
|
676
|
+
goalText,
|
|
677
|
+
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
678
|
+
source: "analyst",
|
|
679
|
+
},
|
|
680
|
+
projectName,
|
|
681
|
+
deps,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function buildContextProposalAnalystPromptFromEntries(
|
|
686
|
+
projectName: string,
|
|
687
|
+
recentEntries: RecentDiscussionEntry[],
|
|
688
|
+
serializeEntries: (entries: RecentDiscussionEntry[]) => string = serializeRecentDiscussionEntries,
|
|
689
|
+
): string {
|
|
690
|
+
return buildContextProposalAnalystPrompt(projectName, serializeEntries(recentEntries));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function parseContextProposal(text: string, projectName: string, deps: ProposalParseDeps): ContextProposal | undefined {
|
|
694
|
+
const cleaned = deps.stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
695
|
+
if (!cleaned) return undefined;
|
|
696
|
+
const lines = cleaned
|
|
697
|
+
.split("\n")
|
|
698
|
+
.map((line) => line.trim())
|
|
699
|
+
.filter((line) => line.length > 0);
|
|
700
|
+
if (lines.length === 0) return undefined;
|
|
701
|
+
|
|
702
|
+
let section: ContextProposalSection | undefined;
|
|
703
|
+
let missionLine: string | undefined;
|
|
704
|
+
let taskTypeHint: string | undefined;
|
|
705
|
+
let evaluationProfileHint: string | undefined;
|
|
706
|
+
const scope: string[] = [];
|
|
707
|
+
const constraints: string[] = [];
|
|
708
|
+
const acceptance: string[] = [];
|
|
709
|
+
const critique: string[] = [];
|
|
710
|
+
const risks: string[] = [];
|
|
711
|
+
let structuredSignalCount = 0;
|
|
712
|
+
|
|
713
|
+
for (const rawLine of lines) {
|
|
714
|
+
const routingHint = matchContextProposalRoutingHint(rawLine);
|
|
715
|
+
if (routingHint) {
|
|
716
|
+
structuredSignalCount += 1;
|
|
717
|
+
if (routingHint.field === "taskType") taskTypeHint = routingHint.value;
|
|
718
|
+
else evaluationProfileHint = routingHint.value;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const inlineSection = matchInlineProposalSection(rawLine);
|
|
722
|
+
if (inlineSection) {
|
|
723
|
+
section = inlineSection.section;
|
|
724
|
+
structuredSignalCount += 1;
|
|
725
|
+
if (inlineSection.section === "mission" && !missionLine) {
|
|
726
|
+
missionLine = inlineSection.content;
|
|
727
|
+
} else if (inlineSection.section === "constraints") {
|
|
728
|
+
constraints.push(inlineSection.content);
|
|
729
|
+
} else if (inlineSection.section === "acceptance") {
|
|
730
|
+
acceptance.push(inlineSection.content);
|
|
731
|
+
} else if (inlineSection.section === "scope") {
|
|
732
|
+
scope.push(inlineSection.content);
|
|
733
|
+
} else if (inlineSection.section === "critique") {
|
|
734
|
+
critique.push(inlineSection.content);
|
|
735
|
+
} else if (inlineSection.section === "risks") {
|
|
736
|
+
risks.push(inlineSection.content);
|
|
737
|
+
}
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const headerSection = detectProposalSection(rawLine);
|
|
741
|
+
if (headerSection) {
|
|
742
|
+
section = headerSection;
|
|
743
|
+
structuredSignalCount += 1;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const bullet = bulletText(rawLine);
|
|
747
|
+
if (bullet) {
|
|
748
|
+
structuredSignalCount += 1;
|
|
749
|
+
if (section === "mission" && !missionLine) {
|
|
750
|
+
missionLine = bullet;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (section === "constraints") {
|
|
754
|
+
constraints.push(bullet);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (section === "acceptance") {
|
|
758
|
+
acceptance.push(bullet);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (section === "scope") {
|
|
762
|
+
scope.push(bullet);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (section === "critique") {
|
|
766
|
+
critique.push(bullet);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (section === "risks") {
|
|
770
|
+
risks.push(bullet);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (!missionLine) {
|
|
774
|
+
missionLine = bullet;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (looksLikeAcceptance(bullet)) acceptance.push(bullet);
|
|
778
|
+
else if (looksLikeConstraint(bullet)) constraints.push(bullet);
|
|
779
|
+
else scope.push(bullet);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const normalized = normalizeProposalLine(rawLine);
|
|
783
|
+
if (!normalized) continue;
|
|
784
|
+
if (!missionLine) {
|
|
785
|
+
missionLine = normalized;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (section === "critique") {
|
|
789
|
+
critique.push(normalized);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (section === "risks") {
|
|
793
|
+
risks.push(normalized);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (section === "constraints" || looksLikeConstraint(normalized)) {
|
|
797
|
+
constraints.push(normalized);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (section === "acceptance" || looksLikeAcceptance(normalized)) {
|
|
801
|
+
acceptance.push(normalized);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (section === "scope") {
|
|
805
|
+
scope.push(normalized);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const basisPreview = cleaned.replace(/\s+/g, " ").trim();
|
|
810
|
+
const missionSource = missionLine ?? scope[0] ?? acceptance[0] ?? constraints[0] ?? basisPreview;
|
|
811
|
+
const assessment = deps.assessMissionAnchor(missionSource, projectName);
|
|
812
|
+
const normalizedMission = deps.normalizeMissionAnchorText(missionSource);
|
|
813
|
+
const itemCount = scope.length + constraints.length + acceptance.length + critique.length + risks.length;
|
|
814
|
+
const hasStrongStructure = structuredSignalCount >= 2 || itemCount >= 2;
|
|
815
|
+
if (!normalizedMission || deps.isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
816
|
+
if (!hasStrongStructure && basisPreview.length < 140) return undefined;
|
|
817
|
+
const mission = assessment.derived;
|
|
818
|
+
const analysis = buildContextProposalAnalysis(
|
|
819
|
+
{
|
|
820
|
+
taskType: taskTypeHint,
|
|
821
|
+
evaluationProfile: evaluationProfileHint,
|
|
822
|
+
critique,
|
|
823
|
+
risks,
|
|
824
|
+
hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
|
|
825
|
+
},
|
|
826
|
+
deps,
|
|
827
|
+
);
|
|
828
|
+
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
829
|
+
return finalizeContextProposal(
|
|
830
|
+
{
|
|
831
|
+
mission,
|
|
832
|
+
scope,
|
|
833
|
+
constraints,
|
|
834
|
+
acceptance,
|
|
835
|
+
analysis,
|
|
836
|
+
goalText,
|
|
837
|
+
basisPreview,
|
|
838
|
+
source: "session",
|
|
839
|
+
},
|
|
840
|
+
projectName,
|
|
841
|
+
deps,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function hasStructuredContextProposalSignal(text: string, stripCodeBlocks: (text: string) => string): boolean {
|
|
846
|
+
const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
847
|
+
if (!cleaned) return false;
|
|
848
|
+
return /(^|\n)\s*(mission|goal|objective|summary|scope|plan|steps|implementation|constraints?|guardrails|non-goals|acceptance|acceptance criteria|deliverables|verification|critique|concerns?|warnings?|notes?|risks?|hazards?|task[\s_-]*type|evaluation[\s_-]*profile)\s*(?:[::]\s*|$)/imu.test(
|
|
849
|
+
cleaned,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function parseStrictStructuredSessionProposal(text: string, projectName: string, deps: ProposalParseDeps): ContextProposal | undefined {
|
|
854
|
+
const cleaned = deps.stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
855
|
+
if (!cleaned) return undefined;
|
|
856
|
+
const lines = cleaned
|
|
857
|
+
.split("\n")
|
|
858
|
+
.map((line) => line.trim())
|
|
859
|
+
.filter((line) => line.length > 0);
|
|
860
|
+
if (lines.length === 0) return undefined;
|
|
861
|
+
|
|
862
|
+
let section: ContextProposalSection | undefined;
|
|
863
|
+
const sectionsPresent = new Set<ContextProposalSection>();
|
|
864
|
+
const missionCandidates: string[] = [];
|
|
865
|
+
|
|
866
|
+
for (const rawLine of lines) {
|
|
867
|
+
const inlineSection = matchInlineProposalSection(rawLine);
|
|
868
|
+
if (inlineSection) {
|
|
869
|
+
section = inlineSection.section;
|
|
870
|
+
sectionsPresent.add(section);
|
|
871
|
+
if (section === "mission") missionCandidates.push(inlineSection.content);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const headerSection = detectProposalSection(rawLine);
|
|
875
|
+
if (headerSection) {
|
|
876
|
+
section = headerSection;
|
|
877
|
+
sectionsPresent.add(section);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const normalized = bulletText(rawLine) ?? normalizeProposalLine(rawLine);
|
|
881
|
+
if (normalized && section === "mission") missionCandidates.push(normalized);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const requiredSections: ContextProposalSection[] = ["mission", "scope", "constraints", "acceptance"];
|
|
885
|
+
if (requiredSections.some((candidate) => !sectionsPresent.has(candidate))) return undefined;
|
|
886
|
+
|
|
887
|
+
const distinctMissionAnchors = Array.from(
|
|
888
|
+
new Set(
|
|
889
|
+
missionCandidates
|
|
890
|
+
.map((candidate) => deps.normalizeMissionAnchorText(deps.assessMissionAnchor(candidate, projectName).derived))
|
|
891
|
+
.filter((candidate): candidate is string => Boolean(candidate)),
|
|
892
|
+
),
|
|
893
|
+
);
|
|
894
|
+
if (distinctMissionAnchors.length !== 1) return undefined;
|
|
895
|
+
|
|
896
|
+
const proposal = parseContextProposal(cleaned, projectName, deps);
|
|
897
|
+
if (!proposal) return undefined;
|
|
898
|
+
if (
|
|
899
|
+
deps.normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0] &&
|
|
900
|
+
!isGenericPlanningMissionAnchor(distinctMissionAnchors[0], deps)
|
|
901
|
+
) {
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
|
|
905
|
+
return { ...proposal, source: "session" };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function extractContextProposalFromStructuredSession(
|
|
909
|
+
recentEntries: RecentDiscussionEntry[],
|
|
910
|
+
projectName: string,
|
|
911
|
+
deps: ProposalParseDeps,
|
|
912
|
+
): ContextProposal | undefined {
|
|
913
|
+
const structuredTexts = recentEntries
|
|
914
|
+
.slice()
|
|
915
|
+
.reverse()
|
|
916
|
+
.map((entry) => entry.text.trim())
|
|
917
|
+
.filter((text) => hasStructuredContextProposalSignal(text, deps.stripCodeBlocks));
|
|
918
|
+
if (structuredTexts.length === 0) return undefined;
|
|
919
|
+
return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName, deps);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export async function deriveCookContextProposalFromRecentDiscussion(
|
|
923
|
+
projectName: string,
|
|
924
|
+
recentEntries: RecentDiscussionEntry[],
|
|
925
|
+
deps: ProposalParseDeps & {
|
|
926
|
+
analyzeContextProposal?: (recentEntries: RecentDiscussionEntry[]) => Promise<ContextProposal | undefined>;
|
|
927
|
+
},
|
|
928
|
+
): Promise<ContextProposal | undefined> {
|
|
929
|
+
if (recentEntries.length === 0) return undefined;
|
|
930
|
+
return (await deps.analyzeContextProposal?.(recentEntries)) ??
|
|
931
|
+
extractContextProposalFromStructuredSession(recentEntries, projectName, deps);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function resolveContextProposalConfirmationAction(
|
|
935
|
+
proposal: ContextProposal,
|
|
936
|
+
action: ContextProposalConfirmAction,
|
|
937
|
+
): ContextProposalDecision | undefined {
|
|
938
|
+
if (action === "cancel") return undefined;
|
|
939
|
+
return {
|
|
940
|
+
missionAnchor: proposal.mission,
|
|
941
|
+
goalText: proposal.goalText,
|
|
942
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
943
|
+
};
|
|
944
|
+
}
|