@linimin/pi-letscook 0.1.44 → 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 +440 -3486
- 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 +10 -17
- 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 +29 -6
|
@@ -3,12 +3,81 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { promises as fsp } from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import * as roleReporting from "./role-reporting.js";
|
|
7
6
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
8
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import { DynamicBorder
|
|
10
|
-
import { Container, matchesKey,
|
|
8
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Container, matchesKey, SelectList, Text } from "@mariozechner/pi-tui";
|
|
11
10
|
import { Type } from "typebox";
|
|
11
|
+
import {
|
|
12
|
+
autoContinueWorkflowIfNeeded,
|
|
13
|
+
completionContinuationFingerprint,
|
|
14
|
+
markQueuedDriverPromptInFlight,
|
|
15
|
+
registerCookCommand,
|
|
16
|
+
} from "./driver";
|
|
17
|
+
import {
|
|
18
|
+
assessMissionAnchor,
|
|
19
|
+
collectRecentDiscussionEntries,
|
|
20
|
+
deriveCookContextProposalFromRecentDiscussion,
|
|
21
|
+
finalizeContextProposalAnalysis,
|
|
22
|
+
isWeakMissionAnchor,
|
|
23
|
+
missionAnchorsLikelyEquivalent,
|
|
24
|
+
missionAnchorsStrictlyEquivalent,
|
|
25
|
+
normalizeMissionAnchorText,
|
|
26
|
+
resolveContextProposalConfirmationAction,
|
|
27
|
+
shouldTreatBareActiveWorkflowProposalAsClearRefocus,
|
|
28
|
+
stripCodeBlocks,
|
|
29
|
+
} from "./proposal";
|
|
30
|
+
import type {
|
|
31
|
+
ContextProposal,
|
|
32
|
+
ContextProposalAnalysis,
|
|
33
|
+
ContextProposalConfirmAction,
|
|
34
|
+
ContextProposalConfirmOptions,
|
|
35
|
+
ContextProposalConfirmationLayout,
|
|
36
|
+
ContextProposalDecision,
|
|
37
|
+
} from "./proposal";
|
|
38
|
+
import {
|
|
39
|
+
buildContextProposalConfirmationLayout as buildExtractedContextProposalConfirmationLayout,
|
|
40
|
+
buildContextProposalConfirmationSelectItems,
|
|
41
|
+
buildContextProposalContinuationReason as buildExtractedContextProposalContinuationReason,
|
|
42
|
+
buildEvaluationRoleContextLines as buildExtractedEvaluationRoleContextLines,
|
|
43
|
+
buildEvaluationRoleReminderText as buildExtractedEvaluationRoleReminderText,
|
|
44
|
+
buildResumeCapsule as buildExtractedResumeCapsule,
|
|
45
|
+
buildSystemReminder as buildExtractedSystemReminder,
|
|
46
|
+
maybeWriteContextProposalConfirmationSnapshot,
|
|
47
|
+
maybeWriteContextProposalSnapshot,
|
|
48
|
+
} from "./prompt-surfaces";
|
|
49
|
+
import { toolCallBlockReason } from "./policy-guards";
|
|
50
|
+
import { analyzeContextProposalWithAgent, runCompletionRole } from "./role-runner";
|
|
51
|
+
import {
|
|
52
|
+
applyLiveRoleEvent,
|
|
53
|
+
buildInlineRunningLines,
|
|
54
|
+
cloneLiveRoleActivity,
|
|
55
|
+
createLiveRoleActivity,
|
|
56
|
+
formatElapsed,
|
|
57
|
+
formatInlineRunningText,
|
|
58
|
+
nowMs,
|
|
59
|
+
refreshCompletionStatus,
|
|
60
|
+
truncateInline,
|
|
61
|
+
} from "./status-surface";
|
|
62
|
+
import {
|
|
63
|
+
asNumber,
|
|
64
|
+
asString,
|
|
65
|
+
asStringArray,
|
|
66
|
+
completionRootKey,
|
|
67
|
+
currentEvaluationProfile,
|
|
68
|
+
currentTaskType,
|
|
69
|
+
findCompletionRoot,
|
|
70
|
+
findRepoRoot,
|
|
71
|
+
isRecord,
|
|
72
|
+
loadCompletionDataForReminder,
|
|
73
|
+
loadCompletionSnapshot,
|
|
74
|
+
pathExists,
|
|
75
|
+
readText,
|
|
76
|
+
scaffoldCompletionFiles as scaffoldCompletionFilesOnDisk,
|
|
77
|
+
} from "./state-store";
|
|
78
|
+
import { parseFirstNumber, parseYesNo } from "./transcription";
|
|
79
|
+
import type { TranscriptionResult } from "./transcription";
|
|
80
|
+
import type { CompletionStateSnapshot, CompletionRole, JsonRecord, LiveRoleActivity } from "./types";
|
|
12
81
|
|
|
13
82
|
const PROTOCOL_ID = "completion";
|
|
14
83
|
const ROLE_NAMES = [
|
|
@@ -28,7 +97,6 @@ const PACKAGE_SKILL_PATH = PACKAGE_ROOT ? path.join(PACKAGE_ROOT, "skills", "com
|
|
|
28
97
|
const PACKAGE_REFERENCE_PATH = PACKAGE_ROOT
|
|
29
98
|
? path.join(PACKAGE_ROOT, "skills", "completion-protocol", "references", "completion.md")
|
|
30
99
|
: undefined;
|
|
31
|
-
const PACKAGE_AGENTS_DIR = PACKAGE_ROOT ? path.join(PACKAGE_ROOT, "agents") : undefined;
|
|
32
100
|
const SKILL_PATH = PACKAGE_SKILL_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "SKILL.md");
|
|
33
101
|
const REFERENCE_PATH = PACKAGE_REFERENCE_PATH ?? path.join(AGENT_HOME, "skills", "completion-protocol", "references", "completion.md");
|
|
34
102
|
const DEFAULT_TASK_TYPE = "completion-workflow";
|
|
@@ -37,428 +105,23 @@ const RUBRIC_EVALUATION_ROLES = ["completion-reviewer", "completion-auditor", "c
|
|
|
37
105
|
|
|
38
106
|
type RubricEvaluationRole = (typeof RUBRIC_EVALUATION_ROLES)[number];
|
|
39
107
|
|
|
40
|
-
type CompletionRole = (typeof ROLE_NAMES)[number];
|
|
41
|
-
type JsonRecord = Record<string, unknown>;
|
|
42
|
-
|
|
43
|
-
type CompletionFiles = {
|
|
44
|
-
root: string;
|
|
45
|
-
agentDir: string;
|
|
46
|
-
tmpDir: string;
|
|
47
|
-
profilePath: string;
|
|
48
|
-
statePath: string;
|
|
49
|
-
planPath: string;
|
|
50
|
-
activePath: string;
|
|
51
|
-
sliceHistoryPath: string;
|
|
52
|
-
stopHistoryPath: string;
|
|
53
|
-
verificationEvidencePath: string;
|
|
54
|
-
compactionMarkerPath: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type CompletionStateSnapshot = {
|
|
58
|
-
files: CompletionFiles;
|
|
59
|
-
profile?: JsonRecord;
|
|
60
|
-
state?: JsonRecord;
|
|
61
|
-
plan?: JsonRecord;
|
|
62
|
-
active?: JsonRecord;
|
|
63
|
-
verificationEvidence?: JsonRecord;
|
|
64
|
-
activeSlice?: JsonRecord;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
type AgentDefinition = {
|
|
68
|
-
name: string;
|
|
69
|
-
description?: string;
|
|
70
|
-
tools?: string[];
|
|
71
|
-
model?: string;
|
|
72
|
-
systemPrompt: string;
|
|
73
|
-
filePath: string;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
type LiveRoleActivity = {
|
|
77
|
-
role: string;
|
|
78
|
-
status: "running" | "ok" | "error";
|
|
79
|
-
currentAction?: string;
|
|
80
|
-
toolActivity?: string;
|
|
81
|
-
toolRecentActivity: string[];
|
|
82
|
-
recentActivity: string[];
|
|
83
|
-
assistantSummary?: string;
|
|
84
|
-
lastAssistantText?: string;
|
|
85
|
-
progress?: string;
|
|
86
|
-
rationale?: string;
|
|
87
|
-
nextStep?: string;
|
|
88
|
-
verifying?: string;
|
|
89
|
-
stateDeltas: string[];
|
|
90
|
-
startedAt: number;
|
|
91
|
-
updatedAt: number;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type CompletionStatusSurface = {
|
|
95
|
-
snapshotPresent: boolean;
|
|
96
|
-
statusText?: string;
|
|
97
|
-
widgetLines: string[];
|
|
98
|
-
currentPhase?: string;
|
|
99
|
-
sliceId?: string;
|
|
100
|
-
nextMandatoryRole?: string;
|
|
101
|
-
remainingContractCount?: number;
|
|
102
|
-
releaseBlockerCount?: number;
|
|
103
|
-
highValueGapCount?: number;
|
|
104
|
-
remainingStopJudgeCount?: number;
|
|
105
|
-
activeRole?: string;
|
|
106
|
-
livePreview?: string;
|
|
107
|
-
liveState?: "active" | "waiting" | "stalled";
|
|
108
|
-
liveIdleMs?: number;
|
|
109
|
-
liveToolActivity?: string;
|
|
110
|
-
liveAssistantSummary?: string;
|
|
111
|
-
liveProgress?: string;
|
|
112
|
-
liveRationale?: string;
|
|
113
|
-
liveNextStep?: string;
|
|
114
|
-
liveVerifying?: string;
|
|
115
|
-
liveStateDeltas?: string[];
|
|
116
|
-
liveDetailsLines?: string[];
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
type ContextProposalAnalysis = {
|
|
120
|
-
taskType?: string;
|
|
121
|
-
evaluationProfile?: string;
|
|
122
|
-
critique: string[];
|
|
123
|
-
risks: string[];
|
|
124
|
-
possibleNoise: string[];
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
type ContextProposal = {
|
|
128
|
-
mission: string;
|
|
129
|
-
scope: string[];
|
|
130
|
-
constraints: string[];
|
|
131
|
-
acceptance: string[];
|
|
132
|
-
analysis: ContextProposalAnalysis;
|
|
133
|
-
goalText: string;
|
|
134
|
-
basisPreview: string;
|
|
135
|
-
source: "session" | "analyst";
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
type ContextProposalSection = "mission" | "scope" | "constraints" | "acceptance" | "critique" | "risks";
|
|
139
|
-
|
|
140
|
-
type RecentDiscussionEntry = {
|
|
141
|
-
role: "user" | "assistant" | "custom" | "summary";
|
|
142
|
-
text: string;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
type ContextProposalDecision = {
|
|
146
|
-
missionAnchor: string;
|
|
147
|
-
goalText: string;
|
|
148
|
-
analysis: ContextProposalAnalysis;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
type ContextProposalConfirmAction = "start" | "cancel";
|
|
152
|
-
|
|
153
|
-
type ContextProposalConfirmationActionItem = {
|
|
154
|
-
id: ContextProposalConfirmAction;
|
|
155
|
-
label: string;
|
|
156
|
-
description: string;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
type ContextProposalConfirmationLayout = {
|
|
160
|
-
title: string;
|
|
161
|
-
intro: string;
|
|
162
|
-
proposalHeading: string;
|
|
163
|
-
proposalBody: string;
|
|
164
|
-
critiqueHeading?: string;
|
|
165
|
-
critiqueBody?: string;
|
|
166
|
-
routingHeading?: string;
|
|
167
|
-
routingBody?: string;
|
|
168
|
-
actionsHeading: string;
|
|
169
|
-
actions: ContextProposalConfirmationActionItem[];
|
|
170
|
-
footer: string;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
type ContextProposalConfirmOptions = {
|
|
174
|
-
title: string;
|
|
175
|
-
nonInteractiveBehavior?: "accept" | "cancel";
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
class StartupAnalystOverlay extends Container {
|
|
179
|
-
private readonly border: DynamicBorder;
|
|
180
|
-
private readonly title: Text;
|
|
181
|
-
private readonly body: Text;
|
|
182
|
-
private readonly footer: Text;
|
|
183
|
-
private lines: string[] = [];
|
|
184
|
-
onAbort?: () => void;
|
|
185
|
-
|
|
186
|
-
constructor(private readonly theme: any) {
|
|
187
|
-
super();
|
|
188
|
-
this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
|
|
189
|
-
this.title = new Text("", 1, 0);
|
|
190
|
-
this.body = new Text("", 1, 1);
|
|
191
|
-
this.footer = new Text("", 1, 0);
|
|
192
|
-
this.addChild(this.border);
|
|
193
|
-
this.addChild(this.title);
|
|
194
|
-
this.addChild(this.body);
|
|
195
|
-
this.addChild(this.footer);
|
|
196
|
-
this.updateDisplay();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
setLines(lines: string[]): void {
|
|
200
|
-
this.lines = [...lines];
|
|
201
|
-
this.updateDisplay();
|
|
202
|
-
this.invalidate();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private updateDisplay(): void {
|
|
206
|
-
this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
|
|
207
|
-
this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
|
|
208
|
-
this.footer.setText(this.theme.fg("muted", "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state"));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
override handleInput(data: string): void {
|
|
212
|
-
if (data === "\u001b" || data === "\u0003") {
|
|
213
|
-
this.onAbort?.();
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
// Container does not implement handleInput; ignore all other keys.
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
override invalidate(): void {
|
|
220
|
-
super.invalidate();
|
|
221
|
-
this.updateDisplay();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
108
|
const liveRoleActivityByRoot = new Map<string, LiveRoleActivity>();
|
|
226
|
-
const
|
|
227
|
-
const LIVE_ROLE_STALLED_MS = 45_000;
|
|
109
|
+
const activatedCompletionRoutingRoots = new Set<string>();
|
|
228
110
|
const LIVE_ROLE_HEARTBEAT_MS = 5_000;
|
|
229
|
-
const DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS = 2;
|
|
230
|
-
|
|
231
|
-
type DriverContinuationTracker = {
|
|
232
|
-
fingerprint: string;
|
|
233
|
-
attempts: number;
|
|
234
|
-
inFlight: boolean;
|
|
235
|
-
warned: boolean;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const driverContinuationByRoot = new Map<string, DriverContinuationTracker>();
|
|
239
|
-
|
|
240
|
-
function isRecord(value: unknown): value is JsonRecord {
|
|
241
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function asString(value: unknown): string | undefined {
|
|
245
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
246
|
-
}
|
|
247
111
|
|
|
248
112
|
function asBoolean(value: unknown): boolean | undefined {
|
|
249
113
|
return typeof value === "boolean" ? value : undefined;
|
|
250
114
|
}
|
|
251
115
|
|
|
252
|
-
function asNumber(value: unknown): number | undefined {
|
|
253
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function asStringArray(value: unknown): string[] {
|
|
257
|
-
return Array.isArray(value)
|
|
258
|
-
? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
259
|
-
: [];
|
|
260
|
-
}
|
|
261
|
-
|
|
262
116
|
function roleFromEnv(): string | undefined {
|
|
263
117
|
return asString(process.env.PI_COMPLETION_ROLE);
|
|
264
118
|
}
|
|
265
119
|
|
|
266
|
-
function resolveFiles(root: string): CompletionFiles {
|
|
267
|
-
const agentDir = path.join(root, ".agent");
|
|
268
|
-
const tmpDir = path.join(agentDir, "tmp");
|
|
269
|
-
return {
|
|
270
|
-
root,
|
|
271
|
-
agentDir,
|
|
272
|
-
tmpDir,
|
|
273
|
-
profilePath: path.join(agentDir, "profile.json"),
|
|
274
|
-
statePath: path.join(agentDir, "state.json"),
|
|
275
|
-
planPath: path.join(agentDir, "plan.json"),
|
|
276
|
-
activePath: path.join(agentDir, "active-slice.json"),
|
|
277
|
-
sliceHistoryPath: path.join(agentDir, "slice-history.jsonl"),
|
|
278
|
-
stopHistoryPath: path.join(agentDir, "stop-check-history.jsonl"),
|
|
279
|
-
verificationEvidencePath: path.join(agentDir, "verification-evidence.json"),
|
|
280
|
-
compactionMarkerPath: path.join(tmpDir, "post-compaction-recovery.json"),
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function walkUpForDir(startCwd: string, segments: string[]): string | undefined {
|
|
285
|
-
let current = path.resolve(startCwd);
|
|
286
|
-
while (true) {
|
|
287
|
-
const candidate = path.join(current, ...segments);
|
|
288
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
289
|
-
const parent = path.dirname(current);
|
|
290
|
-
if (parent === current) return undefined;
|
|
291
|
-
current = parent;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function completionSearchRoots(startCwd: string): string[] {
|
|
296
|
-
return [...new Set([path.resolve(startCwd), path.resolve(process.cwd())])];
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function findCompletionRoot(startCwd: string): string | undefined {
|
|
300
|
-
for (const candidateRoot of completionSearchRoots(startCwd)) {
|
|
301
|
-
const profilePath = walkUpForDir(candidateRoot, [".agent", "profile.json"]);
|
|
302
|
-
if (profilePath) return path.dirname(path.dirname(profilePath));
|
|
303
|
-
}
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function findRepoRoot(startCwd: string): string | undefined {
|
|
308
|
-
for (const candidateRoot of completionSearchRoots(startCwd)) {
|
|
309
|
-
const gitPath = walkUpForDir(candidateRoot, [".git"]);
|
|
310
|
-
if (gitPath) return path.dirname(gitPath);
|
|
311
|
-
}
|
|
312
|
-
return undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function readJson(filePath: string): Promise<JsonRecord | undefined> {
|
|
316
|
-
try {
|
|
317
|
-
const raw = await fsp.readFile(filePath, "utf8");
|
|
318
|
-
const parsed = JSON.parse(raw);
|
|
319
|
-
return isRecord(parsed) ? parsed : undefined;
|
|
320
|
-
} catch {
|
|
321
|
-
return undefined;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function readJsonl(filePath: string): Promise<JsonRecord[]> {
|
|
326
|
-
try {
|
|
327
|
-
const raw = await fsp.readFile(filePath, "utf8");
|
|
328
|
-
return raw
|
|
329
|
-
.split("\n")
|
|
330
|
-
.map((line) => line.trim())
|
|
331
|
-
.filter(Boolean)
|
|
332
|
-
.flatMap((line) => {
|
|
333
|
-
try {
|
|
334
|
-
const parsed = JSON.parse(line);
|
|
335
|
-
return isRecord(parsed) ? [parsed] : [];
|
|
336
|
-
} catch {
|
|
337
|
-
return [];
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
} catch {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function writeJsonFile(filePath: string, value: JsonRecord): Promise<void> {
|
|
346
|
-
await fsp.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
120
|
function candidateSlices(plan: JsonRecord | undefined): JsonRecord[] {
|
|
350
121
|
const slices = plan?.candidate_slices;
|
|
351
122
|
return Array.isArray(slices) ? slices.filter(isRecord) : [];
|
|
352
123
|
}
|
|
353
124
|
|
|
354
|
-
function findActiveSlice(plan: JsonRecord | undefined, active: JsonRecord | undefined): JsonRecord | undefined {
|
|
355
|
-
const sliceId = asString(active?.slice_id);
|
|
356
|
-
if (!sliceId) return undefined;
|
|
357
|
-
return candidateSlices(plan).find((slice) => asString(slice.slice_id) === sliceId);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function loadCompletionSnapshot(startCwd: string): Promise<CompletionStateSnapshot | undefined> {
|
|
361
|
-
const root = findCompletionRoot(startCwd);
|
|
362
|
-
if (!root) return undefined;
|
|
363
|
-
const files = resolveFiles(root);
|
|
364
|
-
const profile = await readJson(files.profilePath);
|
|
365
|
-
if (asString(profile?.protocol_id) !== PROTOCOL_ID) return undefined;
|
|
366
|
-
const state = await readJson(files.statePath);
|
|
367
|
-
const plan = await readJson(files.planPath);
|
|
368
|
-
const active = await readJson(files.activePath);
|
|
369
|
-
const verificationEvidence = await readJson(files.verificationEvidencePath);
|
|
370
|
-
return {
|
|
371
|
-
files,
|
|
372
|
-
profile,
|
|
373
|
-
state,
|
|
374
|
-
plan,
|
|
375
|
-
active,
|
|
376
|
-
verificationEvidence,
|
|
377
|
-
activeSlice: findActiveSlice(plan, active),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function loadCompletionDataForReminder(startCwd: string) {
|
|
382
|
-
const snapshot = await loadCompletionSnapshot(startCwd);
|
|
383
|
-
if (!snapshot) return undefined;
|
|
384
|
-
const sliceHistory = await readJsonl(snapshot.files.sliceHistoryPath);
|
|
385
|
-
const stopHistory = await readJsonl(snapshot.files.stopHistoryPath);
|
|
386
|
-
return { snapshot, sliceHistory, stopHistory };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function pathExists(targetPath: string): Promise<boolean> {
|
|
390
|
-
try {
|
|
391
|
-
await fsp.access(targetPath);
|
|
392
|
-
return true;
|
|
393
|
-
} catch {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async function readText(filePath: string): Promise<string | undefined> {
|
|
399
|
-
try {
|
|
400
|
-
return await fsp.readFile(filePath, "utf8");
|
|
401
|
-
} catch {
|
|
402
|
-
return undefined;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function detectDocsSurfaces(root: string): Promise<string[]> {
|
|
407
|
-
const candidates = ["README.md", "docs/", "docs", "CHANGELOG.md"];
|
|
408
|
-
const found: string[] = [];
|
|
409
|
-
for (const candidate of candidates) {
|
|
410
|
-
if (await pathExists(path.join(root, candidate))) found.push(candidate.endsWith("/") ? candidate : candidate.replace(/\/$/, ""));
|
|
411
|
-
}
|
|
412
|
-
return found.length > 0 ? found : ["README.md"];
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async function detectVerifierCommand(root: string): Promise<string | undefined> {
|
|
416
|
-
const packageJsonPath = path.join(root, "package.json");
|
|
417
|
-
const packageJson = await readJson(packageJsonPath);
|
|
418
|
-
if (packageJson) {
|
|
419
|
-
const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : undefined;
|
|
420
|
-
const packageManager = asString((packageJson as JsonRecord).packageManager) ?? "";
|
|
421
|
-
const runner = packageManager.startsWith("pnpm") ? "pnpm" : packageManager.startsWith("yarn") ? "yarn" : packageManager.startsWith("bun") ? "bun" : "npm";
|
|
422
|
-
if (scripts && asString(scripts.test)) return runner === "npm" ? "npm test" : `${runner} test`;
|
|
423
|
-
if (scripts && asString(scripts.check)) return runner === "npm" ? "npm run check" : `${runner} check`;
|
|
424
|
-
if (scripts && asString(scripts.lint)) return runner === "npm" ? "npm run lint" : `${runner} lint`;
|
|
425
|
-
}
|
|
426
|
-
if (await pathExists(path.join(root, "pnpm-lock.yaml"))) return "pnpm test";
|
|
427
|
-
if (await pathExists(path.join(root, "bun.lockb")) || await pathExists(path.join(root, "bun.lock"))) return "bun test";
|
|
428
|
-
if (await pathExists(path.join(root, "yarn.lock"))) return "yarn test";
|
|
429
|
-
if (await pathExists(path.join(root, "Cargo.toml"))) return "cargo test";
|
|
430
|
-
if (await pathExists(path.join(root, "pyproject.toml")) || await pathExists(path.join(root, "pytest.ini"))) return "pytest";
|
|
431
|
-
if (await pathExists(path.join(root, "go.mod"))) return "go test ./...";
|
|
432
|
-
if (await pathExists(path.join(root, "Makefile"))) return "make test";
|
|
433
|
-
return undefined;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function normalizeMissionAnchorText(value: string): string {
|
|
437
|
-
return value
|
|
438
|
-
.replace(/^\/(?:cook|complete)\s+/i, "")
|
|
439
|
-
.replace(/^["'“”‘’]+|["'“”‘’]+$/g, "")
|
|
440
|
-
.replace(/^\s*(please|pls|can you|could you|help me|i want to|we need to|let'?s|continue to|continue|resume)\s+/i, "")
|
|
441
|
-
.replace(/\s+/g, " ")
|
|
442
|
-
.replace(/[。!?.!?]+$/u, "")
|
|
443
|
-
.trim();
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function isWeakMissionAnchor(value: string): boolean {
|
|
447
|
-
const normalized = value.trim().toLowerCase();
|
|
448
|
-
if (normalized.length < 8) return true;
|
|
449
|
-
if (["continue", "resume", "fix", "fix it", "work on this", "help", "do it", "try again"].includes(normalized)) return true;
|
|
450
|
-
if (/^(continue|resume|fix|help|work on)(\s+.*)?$/i.test(normalized) && normalized.split(/\s+/).length <= 3) return true;
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
type MissionAnchorAssessment = {
|
|
455
|
-
derived: string;
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
function assessMissionAnchor(rawGoal: string, projectName: string): MissionAnchorAssessment {
|
|
459
|
-
return { derived: deriveMissionAnchor(rawGoal, projectName) };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
125
|
type ExistingWorkflowDecision =
|
|
463
126
|
| { action: "continue"; currentMissionAnchor: string }
|
|
464
127
|
| { action: "refocus"; currentMissionAnchor: string; missionAnchor: string };
|
|
@@ -539,8 +202,10 @@ function maybeWriteTestSnapshot(targetPath: string | undefined, content: string)
|
|
|
539
202
|
}
|
|
540
203
|
|
|
541
204
|
const COOK_MAIN_CHAT_RERUN_GUIDANCE = "Discuss changes in the main chat and rerun /cook.";
|
|
205
|
+
const COOK_BARE_ONLY_GUIDANCE =
|
|
206
|
+
"/cook only supports the bare /cook entrypoint. Move mission text into the main chat, then rerun /cook.";
|
|
542
207
|
const COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL =
|
|
543
|
-
"/cook failed closed because recent discussion
|
|
208
|
+
"/cook failed closed because recent discussion did not produce a clear execution-ready Mission/Scope/Constraints/Acceptance proposal for concrete repo changes. Clarify the concrete repo changes in the main chat and rerun /cook.";
|
|
544
209
|
|
|
545
210
|
function buildCookCancellationMessage(prefix: string): string {
|
|
546
211
|
return `${prefix}. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
|
|
@@ -562,8 +227,19 @@ function isWorkflowDone(snapshot: CompletionStateSnapshot | undefined): boolean
|
|
|
562
227
|
return asString(snapshot?.state?.continuation_policy) === "done";
|
|
563
228
|
}
|
|
564
229
|
|
|
230
|
+
function activateCompletionRoutingForRoot(root: string | undefined): void {
|
|
231
|
+
if (!root) return;
|
|
232
|
+
activatedCompletionRoutingRoots.add(path.resolve(root));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasCompletionRoutingActivation(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
236
|
+
if (!snapshot) return false;
|
|
237
|
+
if (roleFromEnv()) return true;
|
|
238
|
+
return activatedCompletionRoutingRoots.has(path.resolve(snapshot.files.root));
|
|
239
|
+
}
|
|
240
|
+
|
|
565
241
|
function shouldInjectCompletionWorkflowContext(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
566
|
-
return
|
|
242
|
+
return hasCompletionRoutingActivation(snapshot);
|
|
567
243
|
}
|
|
568
244
|
|
|
569
245
|
function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): string {
|
|
@@ -581,419 +257,6 @@ function buildDoneWorkflowBoundaryReminder(snapshot: CompletionStateSnapshot): s
|
|
|
581
257
|
].join(" ");
|
|
582
258
|
}
|
|
583
259
|
|
|
584
|
-
function extractTextFromMessageContent(content: unknown): string {
|
|
585
|
-
if (typeof content === "string") return content.trim();
|
|
586
|
-
if (!Array.isArray(content)) return "";
|
|
587
|
-
return content
|
|
588
|
-
.map((item) => {
|
|
589
|
-
if (!isRecord(item)) return "";
|
|
590
|
-
if (item.type !== "text") return "";
|
|
591
|
-
return asString(item.text) ?? "";
|
|
592
|
-
})
|
|
593
|
-
.filter((item) => item.length > 0)
|
|
594
|
-
.join("\n")
|
|
595
|
-
.trim();
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function stripCodeBlocks(text: string): string {
|
|
599
|
-
return text.replace(/```[\s\S]*?```/g, " ");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function normalizeProposalLine(line: string): string {
|
|
603
|
-
return line
|
|
604
|
-
.replace(/^[-*+]\s+/, "")
|
|
605
|
-
.replace(/^\d+[.)]\s+/, "")
|
|
606
|
-
.replace(/^\[.?\]\s+/, "")
|
|
607
|
-
.replace(/^>\s*/, "")
|
|
608
|
-
.replace(/^[`*_~]+|[`*_~]+$/g, "")
|
|
609
|
-
.replace(/^\*\*(.+)\*\*$/u, "$1")
|
|
610
|
-
.replace(/^__([^_]+)__$/u, "$1")
|
|
611
|
-
.trim();
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function detectProposalSection(line: string): ContextProposalSection | undefined {
|
|
615
|
-
const normalized = normalizeProposalLine(line)
|
|
616
|
-
.toLowerCase()
|
|
617
|
-
.replace(/[::]$/, "")
|
|
618
|
-
.trim();
|
|
619
|
-
if (!normalized) return undefined;
|
|
620
|
-
if (["mission", "goal", "objective", "summary", "目標", "任務", "計劃", "计划", "方案"].includes(normalized)) return "mission";
|
|
621
|
-
if (["scope", "plan", "steps", "implementation", "範圍", "范围", "實作", "实现", "步驟", "步骤"].includes(normalized)) return "scope";
|
|
622
|
-
if (["constraints", "constraint", "guardrails", "non-goals", "限制", "約束", "约束", "非目標", "非目标"].includes(normalized)) return "constraints";
|
|
623
|
-
if (["acceptance", "acceptance criteria", "deliverables", "verification", "驗收", "验收", "交付", "驗證", "验证"].includes(normalized)) return "acceptance";
|
|
624
|
-
if (["critique", "critic", "concerns", "concern", "warnings", "warning", "notes", "note", "評論", "评论", "提醒"].includes(normalized)) return "critique";
|
|
625
|
-
if (["risk", "risks", "hazards", "hazard", "failure modes", "failure mode", "風險", "风险"].includes(normalized)) return "risks";
|
|
626
|
-
return undefined;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function matchInlineProposalSection(line: string): { section: ContextProposalSection; content: string } | undefined {
|
|
630
|
-
const normalized = normalizeProposalLine(line);
|
|
631
|
-
const match = normalized.match(/^([^::]+)[::]\s*(.+)$/u);
|
|
632
|
-
if (!match) return undefined;
|
|
633
|
-
const [, rawLabel, rawContent] = match;
|
|
634
|
-
const section = detectProposalSection(rawLabel);
|
|
635
|
-
const content = rawContent.trim();
|
|
636
|
-
if (!section || !content) return undefined;
|
|
637
|
-
return { section, content };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function bulletText(line: string): string | undefined {
|
|
641
|
-
if (!/^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(line)) return undefined;
|
|
642
|
-
const normalized = normalizeProposalLine(line);
|
|
643
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function looksLikeConstraint(text: string): boolean {
|
|
647
|
-
return /(do not|don't|must not|avoid|without|keep\b|preserve|retain|remain|不要|不可|不能|不應|不应|保持|保留|避免)/i.test(text);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function looksLikeAcceptance(text: string): boolean {
|
|
651
|
-
return /(test|tests|testing|verify|verification|validated|README|docs?|documentation|regression|observability|驗證|验证|測試|测试|文件|文檔|文档|回歸|回归)/i.test(text);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function uniqueProposalItems(items: string[]): string[] {
|
|
655
|
-
const seen = new Set<string>();
|
|
656
|
-
const result: string[] = [];
|
|
657
|
-
for (const item of items) {
|
|
658
|
-
const normalized = normalizeProposalLine(item).replace(/\s+/g, " ").trim();
|
|
659
|
-
if (!normalized) continue;
|
|
660
|
-
const key = normalized.toLowerCase();
|
|
661
|
-
if (seen.has(key)) continue;
|
|
662
|
-
seen.add(key);
|
|
663
|
-
result.push(normalized);
|
|
664
|
-
}
|
|
665
|
-
return result;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function normalizeContextProposalHint(value: unknown): string | undefined {
|
|
669
|
-
const normalized = asString(value)?.replace(/\s+/g, " ").trim();
|
|
670
|
-
return normalized || undefined;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function normalizeContextProposalTaskTypeHint(value: unknown): string | undefined {
|
|
674
|
-
const normalized = normalizeContextProposalHint(value);
|
|
675
|
-
if (!normalized) return undefined;
|
|
676
|
-
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
677
|
-
return canonical === DEFAULT_TASK_TYPE ? DEFAULT_TASK_TYPE : normalized;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function normalizeContextProposalEvaluationProfileHint(value: unknown): string | undefined {
|
|
681
|
-
const normalized = normalizeContextProposalHint(value);
|
|
682
|
-
if (!normalized) return undefined;
|
|
683
|
-
const canonical = normalized.toLowerCase().replace(/[\s/]+/g, "-");
|
|
684
|
-
return canonical === DEFAULT_EVALUATION_PROFILE ? DEFAULT_EVALUATION_PROFILE : normalized;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function inferContextProposalTaskType(texts: string[]): string | undefined {
|
|
688
|
-
const corpus = texts
|
|
689
|
-
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
690
|
-
.filter(Boolean)
|
|
691
|
-
.join("\n");
|
|
692
|
-
if (!corpus) return undefined;
|
|
693
|
-
return /(completion|\/cook|\/complete|\.agent|slice|reground|reviewer|auditor|stop judge|stop-judge|workflow)/i.test(corpus)
|
|
694
|
-
? DEFAULT_TASK_TYPE
|
|
695
|
-
: undefined;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function inferContextProposalEvaluationProfile(texts: string[], taskType?: string): string | undefined {
|
|
699
|
-
const corpus = texts
|
|
700
|
-
.map((text) => normalizeProposalLine(text).toLowerCase())
|
|
701
|
-
.filter(Boolean)
|
|
702
|
-
.join("\n");
|
|
703
|
-
if (!corpus) return undefined;
|
|
704
|
-
if (
|
|
705
|
-
/(rubric|evaluation[_\s-]*profile|pass\|concern\|fail|contract coverage|correctness risk|verification evidence|docs\/state parity|reviewer|auditor|stop judge|stop-judge)/i.test(
|
|
706
|
-
corpus,
|
|
707
|
-
)
|
|
708
|
-
) {
|
|
709
|
-
return DEFAULT_EVALUATION_PROFILE;
|
|
710
|
-
}
|
|
711
|
-
return taskType === DEFAULT_TASK_TYPE && /(completion|\/cook|\/complete|slice|workflow|review|audit)/i.test(corpus)
|
|
712
|
-
? DEFAULT_EVALUATION_PROFILE
|
|
713
|
-
: undefined;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function buildContextProposalAnalysis(args: {
|
|
717
|
-
taskType?: unknown;
|
|
718
|
-
evaluationProfile?: unknown;
|
|
719
|
-
critique?: string[];
|
|
720
|
-
risks?: string[];
|
|
721
|
-
possibleNoise?: string[];
|
|
722
|
-
hintTexts?: string[];
|
|
723
|
-
}): ContextProposalAnalysis {
|
|
724
|
-
const critique = uniqueProposalItems(args.critique ?? []);
|
|
725
|
-
const risks = uniqueProposalItems(args.risks ?? []);
|
|
726
|
-
const possibleNoise = uniqueProposalItems(args.possibleNoise ?? []);
|
|
727
|
-
const hintTexts = [...(args.hintTexts ?? []), ...critique, ...risks, ...possibleNoise];
|
|
728
|
-
const taskType = normalizeContextProposalTaskTypeHint(args.taskType) ?? inferContextProposalTaskType(hintTexts);
|
|
729
|
-
const evaluationProfile =
|
|
730
|
-
normalizeContextProposalEvaluationProfileHint(args.evaluationProfile) ??
|
|
731
|
-
inferContextProposalEvaluationProfile(hintTexts, taskType);
|
|
732
|
-
return {
|
|
733
|
-
taskType,
|
|
734
|
-
evaluationProfile,
|
|
735
|
-
critique,
|
|
736
|
-
risks,
|
|
737
|
-
possibleNoise,
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function mergeContextProposalAnalysis(
|
|
742
|
-
sources: Array<ContextProposalAnalysis | undefined>,
|
|
743
|
-
hintTexts: string[] = [],
|
|
744
|
-
): ContextProposalAnalysis {
|
|
745
|
-
const critique = uniqueProposalItems(sources.flatMap((source) => source?.critique ?? []));
|
|
746
|
-
const risks = uniqueProposalItems(sources.flatMap((source) => source?.risks ?? []));
|
|
747
|
-
const possibleNoise = uniqueProposalItems(sources.flatMap((source) => source?.possibleNoise ?? []));
|
|
748
|
-
const taskType =
|
|
749
|
-
sources.map((source) => source?.taskType).find((value): value is string => Boolean(value)) ??
|
|
750
|
-
inferContextProposalTaskType([...hintTexts, ...critique, ...risks, ...possibleNoise]);
|
|
751
|
-
const evaluationProfile =
|
|
752
|
-
sources.map((source) => source?.evaluationProfile).find((value): value is string => Boolean(value)) ??
|
|
753
|
-
inferContextProposalEvaluationProfile([...hintTexts, ...critique, ...risks, ...possibleNoise], taskType);
|
|
754
|
-
return {
|
|
755
|
-
taskType,
|
|
756
|
-
evaluationProfile,
|
|
757
|
-
critique,
|
|
758
|
-
risks,
|
|
759
|
-
possibleNoise,
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function matchContextProposalRoutingHint(
|
|
764
|
-
line: string,
|
|
765
|
-
): { field: "taskType" | "evaluationProfile"; value: string } | undefined {
|
|
766
|
-
const normalized = normalizeProposalLine(line);
|
|
767
|
-
const match = normalized.match(/^(task[\s_-]*type|evaluation[\s_-]*profile)[::]\s*(.+)$/iu);
|
|
768
|
-
if (!match) return undefined;
|
|
769
|
-
const label = match[1].toLowerCase().replace(/[\s_-]+/g, "");
|
|
770
|
-
const value = match[2].trim();
|
|
771
|
-
if (!value) return undefined;
|
|
772
|
-
return label === "tasktype" ? { field: "taskType", value } : { field: "evaluationProfile", value };
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX =
|
|
776
|
-
/(?:\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;
|
|
777
|
-
const CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX =
|
|
778
|
-
/(?:\b(?:write|draft|prepare|create|produce|share|deliver|document|review)\b.*\b(?:plan|spec(?:ification)?|design(?: doc(?:ument)?)?|migration plan|proposal)\b|(?:撰寫|撰写|編寫|编写|起草|準備|准备|產出|产出|整理|分享|交付|審查|审查).*(?:計畫|计划|規格|规格|設計文件|设计文档|提案|方案))/iu;
|
|
779
|
-
const CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX = /(?:\b(?:docs? only|documentation only)\b|(?:只改文件|僅文件|仅文件))/iu;
|
|
780
|
-
const CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX =
|
|
781
|
-
/(?:\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;
|
|
782
|
-
const CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX =
|
|
783
|
-
/(?:\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;
|
|
784
|
-
|
|
785
|
-
function contextProposalBodyTexts(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string[] {
|
|
786
|
-
return [...proposal.scope, ...proposal.constraints, ...proposal.acceptance];
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function isGenericPlanningMissionAnchor(text: string): boolean {
|
|
790
|
-
const normalized = normalizeMissionAnchorText(text);
|
|
791
|
-
if (!normalized) return false;
|
|
792
|
-
return CONTEXT_PROPOSAL_GENERIC_PLANNING_MISSION_REGEX.test(normalized);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function hasExplicitPlanningOnlyDeliverable(texts: string[]): boolean {
|
|
796
|
-
return texts.some((text) => CONTEXT_PROPOSAL_PLANNING_ONLY_DELIVERABLE_REGEX.test(normalizeProposalLine(text)));
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function normalizeImplementationMissionSourceText(text: string): string {
|
|
800
|
-
const normalized = normalizeProposalLine(text);
|
|
801
|
-
if (!normalized) return "";
|
|
802
|
-
return normalized
|
|
803
|
-
.replace(new RegExp(`${CONTEXT_PROPOSAL_DOCS_ONLY_SIGNAL_REGEX.source}[\\s::;,/\\-]*`, "giu"), " ")
|
|
804
|
-
.replace(/\s+([,.;:!?])/g, "$1")
|
|
805
|
-
.replace(/\s+/g, " ")
|
|
806
|
-
.trim();
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function hasClearNoImplementationSignal(texts: string[]): boolean {
|
|
810
|
-
return texts.some((text) => CONTEXT_PROPOSAL_NO_IMPLEMENTATION_SIGNAL_REGEX.test(normalizeProposalLine(text)));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function implementationMissionSourceCandidateText(text: string): string | undefined {
|
|
814
|
-
const normalized = normalizeImplementationMissionSourceText(text);
|
|
815
|
-
if (!normalized) return undefined;
|
|
816
|
-
if (hasExplicitPlanningOnlyDeliverable([normalized])) return undefined;
|
|
817
|
-
if (hasClearNoImplementationSignal([normalized])) return undefined;
|
|
818
|
-
if (!CONTEXT_PROPOSAL_IMPLEMENTATION_SOURCE_REGEX.test(normalized)) return undefined;
|
|
819
|
-
return normalized;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function pickImplementationMissionSource(proposal: Pick<ContextProposal, "scope" | "constraints" | "acceptance">): string | undefined {
|
|
823
|
-
for (const item of proposal.scope) {
|
|
824
|
-
const candidate = implementationMissionSourceCandidateText(item);
|
|
825
|
-
if (candidate) return candidate;
|
|
826
|
-
}
|
|
827
|
-
for (const item of proposal.acceptance) {
|
|
828
|
-
const candidate = implementationMissionSourceCandidateText(item);
|
|
829
|
-
if (candidate) return candidate;
|
|
830
|
-
}
|
|
831
|
-
return undefined;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function hasPlanningArtifactOnlyContext(
|
|
835
|
-
proposal: Pick<ContextProposal, "mission" | "scope" | "constraints" | "acceptance">,
|
|
836
|
-
): boolean {
|
|
837
|
-
const texts = [proposal.mission, ...contextProposalBodyTexts(proposal)];
|
|
838
|
-
if (!hasExplicitPlanningOnlyDeliverable(texts)) return false;
|
|
839
|
-
return !pickImplementationMissionSource(proposal);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function finalizeContextProposal(proposal: ContextProposal, projectName: string): ContextProposal | undefined {
|
|
843
|
-
if (hasPlanningArtifactOnlyContext(proposal)) return undefined;
|
|
844
|
-
if (!isGenericPlanningMissionAnchor(proposal.mission)) return proposal;
|
|
845
|
-
const missionSource = pickImplementationMissionSource(proposal);
|
|
846
|
-
if (!missionSource) return undefined;
|
|
847
|
-
const nextMission = assessMissionAnchor(missionSource, projectName).derived;
|
|
848
|
-
const normalizedNextMission = normalizeMissionAnchorText(nextMission);
|
|
849
|
-
if (!normalizedNextMission || isWeakMissionAnchor(normalizedNextMission)) return undefined;
|
|
850
|
-
if (missionAnchorsStrictlyEquivalent(nextMission, proposal.mission)) return proposal;
|
|
851
|
-
return {
|
|
852
|
-
...proposal,
|
|
853
|
-
mission: nextMission,
|
|
854
|
-
goalText: buildContextProposalGoalText({
|
|
855
|
-
mission: nextMission,
|
|
856
|
-
scope: proposal.scope,
|
|
857
|
-
constraints: proposal.constraints,
|
|
858
|
-
acceptance: proposal.acceptance,
|
|
859
|
-
}),
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const MISSION_SCOPE_FILTER_STOPWORDS = new Set([
|
|
864
|
-
"a",
|
|
865
|
-
"an",
|
|
866
|
-
"and",
|
|
867
|
-
"are",
|
|
868
|
-
"as",
|
|
869
|
-
"at",
|
|
870
|
-
"be",
|
|
871
|
-
"by",
|
|
872
|
-
"for",
|
|
873
|
-
"from",
|
|
874
|
-
"goal",
|
|
875
|
-
"goals",
|
|
876
|
-
"in",
|
|
877
|
-
"into",
|
|
878
|
-
"is",
|
|
879
|
-
"it",
|
|
880
|
-
"its",
|
|
881
|
-
"mission",
|
|
882
|
-
"of",
|
|
883
|
-
"on",
|
|
884
|
-
"or",
|
|
885
|
-
"scope",
|
|
886
|
-
"that",
|
|
887
|
-
"the",
|
|
888
|
-
"their",
|
|
889
|
-
"this",
|
|
890
|
-
"to",
|
|
891
|
-
"using",
|
|
892
|
-
"with",
|
|
893
|
-
"workflow",
|
|
894
|
-
]);
|
|
895
|
-
|
|
896
|
-
function missionScopeFilterTokens(text: string): string[] {
|
|
897
|
-
const normalized = normalizeProposalLine(text).toLowerCase();
|
|
898
|
-
const tokens = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
899
|
-
return tokens.filter((token) => {
|
|
900
|
-
if (/^[\p{Script=Han}]+$/u.test(token)) return token.length >= 2;
|
|
901
|
-
if (token.length < 2) return false;
|
|
902
|
-
return !MISSION_SCOPE_FILTER_STOPWORDS.has(token);
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function isSessionScopeItemMissionRelevant(item: string, mission: string): boolean {
|
|
907
|
-
const normalizedItem = normalizeProposalLine(item).toLowerCase();
|
|
908
|
-
const normalizedMission = normalizeMissionAnchorText(mission).toLowerCase();
|
|
909
|
-
if (!normalizedItem || !normalizedMission) return true;
|
|
910
|
-
if (normalizedItem.includes(normalizedMission) || normalizedMission.includes(normalizedItem)) return true;
|
|
911
|
-
const itemTokens = [...new Set(missionScopeFilterTokens(normalizedItem))];
|
|
912
|
-
const missionTokens = new Set(missionScopeFilterTokens(normalizedMission));
|
|
913
|
-
if (itemTokens.length === 0 || missionTokens.size === 0) return true;
|
|
914
|
-
const overlap = itemTokens.filter((token) => missionTokens.has(token));
|
|
915
|
-
if (overlap.length >= 2) return true;
|
|
916
|
-
return overlap.some((token) => token.length >= 6 || /[\p{Script=Han}]/u.test(token));
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function missionAnchorSemanticTokens(text: string): string[] {
|
|
920
|
-
return [...new Set(missionScopeFilterTokens(normalizeMissionAnchorText(text).toLowerCase()))];
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function missionAnchorOrderedTokenOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
|
|
924
|
-
if (leftTokens.length === 0 || rightTokens.length === 0) return 0;
|
|
925
|
-
const dp = new Array(rightTokens.length + 1).fill(0);
|
|
926
|
-
for (const leftToken of leftTokens) {
|
|
927
|
-
let previous = 0;
|
|
928
|
-
for (let index = 0; index < rightTokens.length; index += 1) {
|
|
929
|
-
const nextPrevious = dp[index + 1];
|
|
930
|
-
if (leftToken === rightTokens[index]) {
|
|
931
|
-
dp[index + 1] = previous + 1;
|
|
932
|
-
} else {
|
|
933
|
-
dp[index + 1] = Math.max(dp[index + 1], dp[index]);
|
|
934
|
-
}
|
|
935
|
-
previous = nextPrevious;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return dp[rightTokens.length] / Math.max(leftTokens.length, rightTokens.length);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function missionAnchorBigramOverlapRatio(leftTokens: string[], rightTokens: string[]): number {
|
|
942
|
-
if (leftTokens.length < 2 || rightTokens.length < 2) return 0;
|
|
943
|
-
const leftBigrams = new Set(leftTokens.slice(0, -1).map((token, index) => `${token} ${leftTokens[index + 1]}`));
|
|
944
|
-
const rightBigrams = new Set(rightTokens.slice(0, -1).map((token, index) => `${token} ${rightTokens[index + 1]}`));
|
|
945
|
-
if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
|
|
946
|
-
let overlap = 0;
|
|
947
|
-
for (const bigram of leftBigrams) {
|
|
948
|
-
if (rightBigrams.has(bigram)) overlap += 1;
|
|
949
|
-
}
|
|
950
|
-
return overlap / Math.max(leftBigrams.size, rightBigrams.size);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function missionAnchorsStrictlyEquivalent(left: string, right: string): boolean {
|
|
954
|
-
return normalizeMissionAnchorText(left).toLowerCase() === normalizeMissionAnchorText(right).toLowerCase();
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
const MISSION_NEGATION_CUE_REGEX = /(?:^|[^\p{L}\p{N}_])(?:no|not|without|never|cannot|don['’]?t)(?=$|[^\p{L}\p{N}_])/u;
|
|
958
|
-
|
|
959
|
-
function missionAnchorHasNegationCue(text: string): boolean {
|
|
960
|
-
return MISSION_NEGATION_CUE_REGEX.test(text);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function missionAnchorsLikelyEquivalent(left: string, right: string): boolean {
|
|
964
|
-
const normalizedLeft = normalizeMissionAnchorText(left).toLowerCase();
|
|
965
|
-
const normalizedRight = normalizeMissionAnchorText(right).toLowerCase();
|
|
966
|
-
if (!normalizedLeft || !normalizedRight) return false;
|
|
967
|
-
const leftHasNegationCue = missionAnchorHasNegationCue(normalizedLeft);
|
|
968
|
-
const rightHasNegationCue = missionAnchorHasNegationCue(normalizedRight);
|
|
969
|
-
if (leftHasNegationCue !== rightHasNegationCue) return false;
|
|
970
|
-
if (normalizedLeft === normalizedRight) return true;
|
|
971
|
-
if (!leftHasNegationCue && (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft))) return true;
|
|
972
|
-
const leftTokens = missionAnchorSemanticTokens(normalizedLeft);
|
|
973
|
-
const rightTokens = missionAnchorSemanticTokens(normalizedRight);
|
|
974
|
-
if (leftTokens.length === 0 || rightTokens.length === 0) return false;
|
|
975
|
-
const rightSet = new Set(rightTokens);
|
|
976
|
-
const overlap = leftTokens.filter((token) => rightSet.has(token));
|
|
977
|
-
if (overlap.length < 3) return false;
|
|
978
|
-
const maxLen = Math.max(leftTokens.length, rightTokens.length);
|
|
979
|
-
if (overlap.length / maxLen < 0.75) return false;
|
|
980
|
-
if (missionAnchorOrderedTokenOverlapRatio(leftTokens, rightTokens) < 0.75) return false;
|
|
981
|
-
if (Math.min(leftTokens.length, rightTokens.length) < 4) return true;
|
|
982
|
-
return missionAnchorBigramOverlapRatio(leftTokens, rightTokens) >= 0.5;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal: ContextProposal): boolean {
|
|
986
|
-
if (proposal.source === "session") {
|
|
987
|
-
return proposal.scope.length > 0 && proposal.constraints.length > 0 && proposal.acceptance.length > 0;
|
|
988
|
-
}
|
|
989
|
-
return (
|
|
990
|
-
proposal.scope.length > 0 &&
|
|
991
|
-
proposal.constraints.length > 0 &&
|
|
992
|
-
proposal.acceptance.length > 0 &&
|
|
993
|
-
proposal.analysis.possibleNoise.length === 0
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
260
|
function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowProposalAssessment): void {
|
|
998
261
|
const snapshotPath = completionTestActiveWorkflowRoutingSnapshotPath();
|
|
999
262
|
if (!snapshotPath) return;
|
|
@@ -1018,459 +281,26 @@ function maybeWriteActiveWorkflowRoutingSnapshot(assessment: ActiveWorkflowPropo
|
|
|
1018
281
|
);
|
|
1019
282
|
}
|
|
1020
283
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
"scope must contain only work items that directly support the mission.",
|
|
1027
|
-
"constraints must contain guardrails or non-goals explicitly stated or strongly implied by the discussion.",
|
|
1028
|
-
"acceptance must contain verifiable outcomes explicitly stated or strongly implied by the discussion.",
|
|
1029
|
-
"critique must contain operator-facing cautions, concerns, or reminders that should be shown separately from mission and scope later.",
|
|
1030
|
-
"risks must contain concrete failure modes or regressions that the later workflow should keep in view.",
|
|
1031
|
-
"task_type and evaluation_profile should be candidate routing hints only; reuse the existing completion vocabulary when it clearly fits instead of inventing new schema names.",
|
|
1032
|
-
"possible_noise should list discussion points that look stale, weakly related, or unsafe to promote into scope.",
|
|
1033
|
-
"When discussion is insufficient, prefer empty arrays and a low confidence value over invention.",
|
|
1034
|
-
].join(" ");
|
|
1035
|
-
|
|
1036
|
-
function collectRecentDiscussionEntries(ctx: { sessionManager: any }, limit = 8): RecentDiscussionEntry[] {
|
|
1037
|
-
let branch: any[] = [];
|
|
1038
|
-
try {
|
|
1039
|
-
branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
if (isStaleContextError(error)) return [];
|
|
1042
|
-
throw error;
|
|
1043
|
-
}
|
|
1044
|
-
const entries: RecentDiscussionEntry[] = [];
|
|
1045
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
1046
|
-
const entry = branch[index];
|
|
1047
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
|
|
1048
|
-
const message = entry.message as JsonRecord;
|
|
1049
|
-
let text = "";
|
|
1050
|
-
let role: RecentDiscussionEntry["role"] | undefined;
|
|
1051
|
-
const messageRole = asString(message.role);
|
|
1052
|
-
if (messageRole === "user" || messageRole === "custom") {
|
|
1053
|
-
text = extractTextFromMessageContent(message.content);
|
|
1054
|
-
role = messageRole;
|
|
1055
|
-
}
|
|
1056
|
-
if (!text || !role) continue;
|
|
1057
|
-
const trimmed = text.trim();
|
|
1058
|
-
if (!trimmed || /^\/(?:cook|complete)\b/i.test(trimmed)) continue;
|
|
1059
|
-
entries.push({ role, text: trimmed });
|
|
1060
|
-
if (entries.length >= limit) break;
|
|
1061
|
-
}
|
|
1062
|
-
return entries;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function serializeRecentDiscussionEntries(entries: RecentDiscussionEntry[]): string {
|
|
1066
|
-
return entries
|
|
1067
|
-
.slice()
|
|
1068
|
-
.reverse()
|
|
1069
|
-
.map((entry, index) => `[${index + 1}] ${entry.role.toUpperCase()}\n${entry.text}`)
|
|
1070
|
-
.join("\n\n");
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
function extractJsonObjectFromText(text: string): string | undefined {
|
|
1074
|
-
const trimmed = text.trim();
|
|
1075
|
-
if (!trimmed) return undefined;
|
|
1076
|
-
const unfenced = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
1077
|
-
if (unfenced.startsWith("{") && unfenced.endsWith("}")) return unfenced;
|
|
1078
|
-
const start = unfenced.indexOf("{");
|
|
1079
|
-
const end = unfenced.lastIndexOf("}");
|
|
1080
|
-
if (start < 0 || end <= start) return undefined;
|
|
1081
|
-
return unfenced.slice(start, end + 1);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function parseContextProposalAnalystOutput(raw: string, projectName: string): ContextProposal | undefined {
|
|
1085
|
-
const jsonText = extractJsonObjectFromText(raw);
|
|
1086
|
-
if (!jsonText) return undefined;
|
|
1087
|
-
let parsed: unknown;
|
|
1088
|
-
try {
|
|
1089
|
-
parsed = JSON.parse(jsonText);
|
|
1090
|
-
} catch {
|
|
1091
|
-
return undefined;
|
|
1092
|
-
}
|
|
1093
|
-
if (!isRecord(parsed)) return undefined;
|
|
1094
|
-
const missionSource = asString(parsed.mission);
|
|
1095
|
-
if (!missionSource) return undefined;
|
|
1096
|
-
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1097
|
-
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
1098
|
-
if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
1099
|
-
const mission = assessment.derived;
|
|
1100
|
-
const scope = uniqueProposalItems(asStringArray(parsed.scope));
|
|
1101
|
-
const constraints = uniqueProposalItems(asStringArray(parsed.constraints));
|
|
1102
|
-
const acceptance = uniqueProposalItems(asStringArray(parsed.acceptance));
|
|
1103
|
-
const analysis = buildContextProposalAnalysis({
|
|
1104
|
-
taskType: parsed.task_type ?? parsed.taskType,
|
|
1105
|
-
evaluationProfile: parsed.evaluation_profile ?? parsed.evaluationProfile,
|
|
1106
|
-
critique: asStringArray(parsed.critique),
|
|
1107
|
-
risks: asStringArray(parsed.risks ?? parsed.risk),
|
|
1108
|
-
possibleNoise: asStringArray(parsed.possible_noise ?? parsed.possibleNoise),
|
|
1109
|
-
hintTexts: [raw, mission, ...scope, ...constraints, ...acceptance],
|
|
284
|
+
function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
|
|
285
|
+
return buildExtractedContextProposalContinuationReason(prefix, goalText, analysis, {
|
|
286
|
+
defaultTaskType: DEFAULT_TASK_TYPE,
|
|
287
|
+
defaultEvaluationProfile: DEFAULT_EVALUATION_PROFILE,
|
|
288
|
+
truncateInline,
|
|
1110
289
|
});
|
|
1111
|
-
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1112
|
-
return finalizeContextProposal(
|
|
1113
|
-
{
|
|
1114
|
-
mission,
|
|
1115
|
-
scope,
|
|
1116
|
-
constraints,
|
|
1117
|
-
acceptance,
|
|
1118
|
-
analysis,
|
|
1119
|
-
goalText,
|
|
1120
|
-
basisPreview: raw.replace(/\s+/g, " ").trim(),
|
|
1121
|
-
source: "analyst",
|
|
1122
|
-
},
|
|
1123
|
-
projectName,
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function contextProposalAnalystModelArg(model: unknown): string | undefined {
|
|
1128
|
-
if (!isRecord(model)) return undefined;
|
|
1129
|
-
const provider = asString(model.provider);
|
|
1130
|
-
const id = asString(model.id);
|
|
1131
|
-
return provider && id ? `${provider}/${id}` : undefined;
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function buildContextProposalAnalystPrompt(projectName: string, recentEntries: RecentDiscussionEntry[], hintText?: string): string {
|
|
1135
|
-
const discussion = serializeRecentDiscussionEntries(recentEntries);
|
|
1136
|
-
const lines = [
|
|
1137
|
-
`Project: ${projectName}`,
|
|
1138
|
-
"Infer the current mission from the discussion.",
|
|
1139
|
-
"If an inline /cook hint is present, treat it as a high-priority user hint that may focus the mission, but do not ignore conflicting discussion or skip missing details.",
|
|
1140
|
-
];
|
|
1141
|
-
if (hintText) {
|
|
1142
|
-
lines.push("", "Inline /cook hint:", hintText);
|
|
1143
|
-
}
|
|
1144
|
-
lines.push("", "Recent discussion:", discussion || "(none)");
|
|
1145
|
-
return lines.join("\n");
|
|
1146
290
|
}
|
|
1147
291
|
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
rationale: activity.rationale,
|
|
1161
|
-
nextStep: activity.nextStep,
|
|
1162
|
-
verifying: activity.verifying,
|
|
1163
|
-
stateDeltas: activity.stateDeltas,
|
|
1164
|
-
}),
|
|
1165
|
-
"",
|
|
1166
|
-
"This step only prepares a proposal for confirmation.",
|
|
1167
|
-
];
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
async function runContextProposalAnalystSubprocess(
|
|
1171
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any },
|
|
1172
|
-
projectName: string,
|
|
1173
|
-
recentEntries: RecentDiscussionEntry[],
|
|
1174
|
-
hintText?: string,
|
|
1175
|
-
): Promise<string | undefined> {
|
|
1176
|
-
const modelArg = contextProposalAnalystModelArg(ctx.model);
|
|
1177
|
-
if (!modelArg) return undefined;
|
|
1178
|
-
const cwd = getCtxCwd(ctx);
|
|
1179
|
-
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
1180
|
-
const rootKey = completionRootKey(undefined, cwd);
|
|
1181
|
-
const prompt = buildContextProposalAnalystPrompt(projectName, recentEntries, hintText);
|
|
1182
|
-
const systemPromptTemp = await writeTempFile("pi-cook-proposal-analyst-", CONTEXT_PROPOSAL_ANALYST_SYSTEM_PROMPT);
|
|
1183
|
-
const analystRole = "cook-proposal-analyst";
|
|
1184
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
|
|
1185
|
-
const invocation = getPiInvocation(args);
|
|
1186
|
-
const liveActivity = createLiveRoleActivity(analystRole);
|
|
1187
|
-
liveActivity.progress = hintText ? "Analyzing recent discussion and inline hint" : "Analyzing recent discussion";
|
|
1188
|
-
liveActivity.currentAction = hintText
|
|
1189
|
-
? "Reading recent discussion plus the inline /cook hint and preparing a startup proposal"
|
|
1190
|
-
: "Reading recent discussion and preparing a startup proposal";
|
|
1191
|
-
liveActivity.assistantSummary = liveActivity.progress;
|
|
1192
|
-
liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
|
|
1193
|
-
const messages: RoleMessage[] = [];
|
|
1194
|
-
let stderr = "";
|
|
1195
|
-
let overlay: StartupAnalystOverlay | undefined;
|
|
1196
|
-
let finishOverlay: ((value: string | undefined) => void) | undefined;
|
|
1197
|
-
let overlaySettled = false;
|
|
1198
|
-
const settleOverlay = (value: string | undefined) => {
|
|
1199
|
-
if (overlaySettled) return;
|
|
1200
|
-
overlaySettled = true;
|
|
1201
|
-
finishOverlay?.(value);
|
|
1202
|
-
};
|
|
1203
|
-
const updateActivity = (fresh = false) => {
|
|
1204
|
-
if (fresh) liveActivity.updatedAt = nowMs();
|
|
1205
|
-
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
|
|
1206
|
-
void refreshStatus(ctx);
|
|
1207
|
-
overlay?.setLines(contextProposalAnalystProgressLines(liveActivity));
|
|
1208
|
-
};
|
|
1209
|
-
const heartbeat = setInterval(() => updateActivity(false), LIVE_ROLE_HEARTBEAT_MS);
|
|
1210
|
-
const run = async (): Promise<string | undefined> => {
|
|
1211
|
-
try {
|
|
1212
|
-
updateActivity(true);
|
|
1213
|
-
const output = await new Promise<string | undefined>((resolve) => {
|
|
1214
|
-
const proc = spawn(invocation.command, invocation.args, {
|
|
1215
|
-
cwd: runCwd,
|
|
1216
|
-
env: process.env,
|
|
1217
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1218
|
-
shell: false,
|
|
1219
|
-
});
|
|
1220
|
-
let settled = false;
|
|
1221
|
-
const resolveOnce = (value: string | undefined) => {
|
|
1222
|
-
if (settled) return;
|
|
1223
|
-
settled = true;
|
|
1224
|
-
resolve(value);
|
|
1225
|
-
};
|
|
1226
|
-
const abort = () => {
|
|
1227
|
-
proc.kill("SIGTERM");
|
|
1228
|
-
resolveOnce(undefined);
|
|
1229
|
-
};
|
|
1230
|
-
const handleSigint = () => abort();
|
|
1231
|
-
let buffer = "";
|
|
1232
|
-
const processLine = (line: string) => {
|
|
1233
|
-
if (!line.trim()) return;
|
|
1234
|
-
try {
|
|
1235
|
-
const event = JSON.parse(line) as JsonRecord;
|
|
1236
|
-
if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
|
|
1237
|
-
} catch {
|
|
1238
|
-
// ignore malformed lines
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
proc.stdout.on("data", (chunk) => {
|
|
1242
|
-
buffer += chunk.toString();
|
|
1243
|
-
const lines = buffer.split("\n");
|
|
1244
|
-
buffer = lines.pop() ?? "";
|
|
1245
|
-
for (const line of lines) processLine(line);
|
|
1246
|
-
});
|
|
1247
|
-
proc.stderr.on("data", (chunk) => {
|
|
1248
|
-
stderr += chunk.toString();
|
|
1249
|
-
});
|
|
1250
|
-
proc.on("close", (code) => {
|
|
1251
|
-
process.off("SIGINT", handleSigint);
|
|
1252
|
-
if (buffer.trim()) processLine(buffer);
|
|
1253
|
-
resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
|
|
1254
|
-
});
|
|
1255
|
-
proc.on("error", () => {
|
|
1256
|
-
process.off("SIGINT", handleSigint);
|
|
1257
|
-
resolveOnce(undefined);
|
|
1258
|
-
});
|
|
1259
|
-
process.once("SIGINT", handleSigint);
|
|
1260
|
-
if (overlay) {
|
|
1261
|
-
overlay.onAbort = () => {
|
|
1262
|
-
process.off("SIGINT", handleSigint);
|
|
1263
|
-
abort();
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
|
|
1268
|
-
await refreshStatus(ctx);
|
|
1269
|
-
return output;
|
|
1270
|
-
} finally {
|
|
1271
|
-
clearInterval(heartbeat);
|
|
1272
|
-
setTimeout(() => {
|
|
1273
|
-
const current = liveRoleActivityByRoot.get(rootKey);
|
|
1274
|
-
if (current && current.role === analystRole && current.status !== "running") {
|
|
1275
|
-
liveRoleActivityByRoot.delete(rootKey);
|
|
1276
|
-
void refreshStatus(ctx);
|
|
1277
|
-
}
|
|
1278
|
-
}, 10_000);
|
|
1279
|
-
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
if (getCtxHasUI(ctx)) {
|
|
1283
|
-
const ui = getCtxUi(ctx);
|
|
1284
|
-
if (ui) {
|
|
1285
|
-
return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
|
|
1286
|
-
finishOverlay = done;
|
|
1287
|
-
overlay = new StartupAnalystOverlay(theme);
|
|
1288
|
-
overlay.setLines(contextProposalAnalystProgressLines(liveActivity));
|
|
1289
|
-
run().then(settleOverlay).catch(() => settleOverlay(undefined));
|
|
1290
|
-
return overlay;
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
return await run();
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
async function analyzeContextProposalWithAgent(
|
|
1298
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; model?: any; modelRegistry?: any },
|
|
1299
|
-
projectName: string,
|
|
1300
|
-
recentEntries: RecentDiscussionEntry[],
|
|
1301
|
-
hintText?: string,
|
|
1302
|
-
): Promise<ContextProposal | undefined> {
|
|
1303
|
-
if (shouldDisableContextProposalAnalyst()) return undefined;
|
|
1304
|
-
const testOutput = completionTestContextProposalAnalystOutput();
|
|
1305
|
-
if (testOutput) {
|
|
1306
|
-
return parseContextProposalAnalystOutput(testOutput, projectName);
|
|
1307
|
-
}
|
|
1308
|
-
if (recentEntries.length === 0 && !hintText?.trim()) return undefined;
|
|
1309
|
-
try {
|
|
1310
|
-
const raw = await runContextProposalAnalystSubprocess(ctx, projectName, recentEntries, hintText);
|
|
1311
|
-
if (!raw) return undefined;
|
|
1312
|
-
return parseContextProposalAnalystOutput(raw, projectName);
|
|
1313
|
-
} catch (error) {
|
|
1314
|
-
console.warn("[completion] context proposal analyst failed", error);
|
|
1315
|
-
return undefined;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
function buildContextProposalGoalText(proposal: {
|
|
1320
|
-
mission: string;
|
|
1321
|
-
scope: string[];
|
|
1322
|
-
constraints: string[];
|
|
1323
|
-
acceptance: string[];
|
|
1324
|
-
}): string {
|
|
1325
|
-
const lines = [`Mission: ${proposal.mission}`];
|
|
1326
|
-
if (proposal.scope.length > 0) {
|
|
1327
|
-
lines.push("", "Scope:");
|
|
1328
|
-
for (const item of proposal.scope) lines.push(`- ${item}`);
|
|
1329
|
-
}
|
|
1330
|
-
if (proposal.constraints.length > 0) {
|
|
1331
|
-
lines.push("", "Constraints:");
|
|
1332
|
-
for (const item of proposal.constraints) lines.push(`- ${item}`);
|
|
1333
|
-
}
|
|
1334
|
-
if (proposal.acceptance.length > 0) {
|
|
1335
|
-
lines.push("", "Acceptance:");
|
|
1336
|
-
for (const item of proposal.acceptance) lines.push(`- ${item}`);
|
|
1337
|
-
}
|
|
1338
|
-
return lines.join("\n");
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
function buildContextProposalDisplayText(proposal: ContextProposal): string {
|
|
1342
|
-
const lines = ["Mission", proposal.mission];
|
|
1343
|
-
if (proposal.scope.length > 0) {
|
|
1344
|
-
lines.push("", "Scope");
|
|
1345
|
-
for (const item of proposal.scope) lines.push(`- ${item}`);
|
|
1346
|
-
}
|
|
1347
|
-
if (proposal.constraints.length > 0) {
|
|
1348
|
-
lines.push("", "Constraints");
|
|
1349
|
-
for (const item of proposal.constraints) lines.push(`- ${item}`);
|
|
1350
|
-
}
|
|
1351
|
-
if (proposal.acceptance.length > 0) {
|
|
1352
|
-
lines.push("", "Acceptance");
|
|
1353
|
-
for (const item of proposal.acceptance) lines.push(`- ${item}`);
|
|
1354
|
-
}
|
|
1355
|
-
return lines.join("\n");
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function finalizeContextProposalAnalysis(analysis: ContextProposalAnalysis | undefined, hintTexts: string[] = []): ContextProposalAnalysis {
|
|
1359
|
-
const merged = mergeContextProposalAnalysis(analysis ? [analysis] : [], hintTexts);
|
|
1360
|
-
return {
|
|
1361
|
-
taskType: merged.taskType ?? DEFAULT_TASK_TYPE,
|
|
1362
|
-
evaluationProfile: merged.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
1363
|
-
critique: merged.critique,
|
|
1364
|
-
risks: merged.risks,
|
|
1365
|
-
possibleNoise: merged.possibleNoise,
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
function buildContextProposalCritiqueText(analysis: ContextProposalAnalysis): string {
|
|
1370
|
-
const lines: string[] = [];
|
|
1371
|
-
if (analysis.critique.length > 0) {
|
|
1372
|
-
lines.push("Critique");
|
|
1373
|
-
for (const item of analysis.critique) lines.push(`- ${item}`);
|
|
1374
|
-
}
|
|
1375
|
-
if (analysis.risks.length > 0) {
|
|
1376
|
-
if (lines.length > 0) lines.push("");
|
|
1377
|
-
lines.push("Risks");
|
|
1378
|
-
for (const item of analysis.risks) lines.push(`- ${item}`);
|
|
1379
|
-
}
|
|
1380
|
-
if (analysis.possibleNoise.length > 0) {
|
|
1381
|
-
if (lines.length > 0) lines.push("");
|
|
1382
|
-
lines.push("Possible noise");
|
|
1383
|
-
for (const item of analysis.possibleNoise) lines.push(`- ${item}`);
|
|
1384
|
-
}
|
|
1385
|
-
if (lines.length === 0) {
|
|
1386
|
-
return "No critique, risk, or possible-noise notes were derived for this startup proposal.";
|
|
1387
|
-
}
|
|
1388
|
-
return lines.join("\n");
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
function buildContextProposalRoutingText(analysis: ContextProposalAnalysis): string {
|
|
1392
|
-
return [`- task_type: ${analysis.taskType ?? DEFAULT_TASK_TYPE}`, `- evaluation_profile: ${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}`].join(
|
|
1393
|
-
"\n",
|
|
1394
|
-
);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function summarizeContextProposalAnalysisItems(label: string, items: string[]): string | undefined {
|
|
1398
|
-
if (items.length === 0) return undefined;
|
|
1399
|
-
return `${label}=${truncateInline(items.join(" | "), 160)}`;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
function buildContextProposalContinuationReason(prefix: string, goalText: string, analysis: ContextProposalAnalysis): string {
|
|
1403
|
-
const critiqueParts = [
|
|
1404
|
-
analysis.critique.length > 0 ? `accepted critique=${truncateInline(analysis.critique.join(" | "), 160)}` : "accepted critique=none",
|
|
1405
|
-
summarizeContextProposalAnalysisItems("risks", analysis.risks),
|
|
1406
|
-
summarizeContextProposalAnalysisItems("possible_noise", analysis.possibleNoise),
|
|
1407
|
-
].filter((part): part is string => Boolean(part));
|
|
1408
|
-
return `${prefix} ${truncateInline(goalText, 220)} | startup routing: task_type=${analysis.taskType ?? DEFAULT_TASK_TYPE}; evaluation_profile=${analysis.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE}; critique outcome=${critiqueParts.join("; ")}`;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
function buildContextProposalConfirmationActions(): ContextProposalConfirmationActionItem[] {
|
|
1412
|
-
return [
|
|
1413
|
-
{
|
|
1414
|
-
id: "start",
|
|
1415
|
-
label: "Start",
|
|
1416
|
-
description: "Accept this proposal and let /cook write or refocus canonical workflow state.",
|
|
1417
|
-
},
|
|
1418
|
-
{
|
|
1419
|
-
id: "cancel",
|
|
1420
|
-
label: "Cancel",
|
|
1421
|
-
description: `Stop here without changing canonical workflow state. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`,
|
|
1422
|
-
},
|
|
1423
|
-
];
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function buildContextProposalConfirmationLayout(
|
|
1427
|
-
title: string,
|
|
1428
|
-
proposal: ContextProposal,
|
|
1429
|
-
): ContextProposalConfirmationLayout {
|
|
1430
|
-
const analysis = finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]);
|
|
1431
|
-
return {
|
|
1432
|
-
title,
|
|
1433
|
-
intro: "Review the proposed mission, scope, constraints, acceptance, critique, and routing details before /cook writes canonical workflow state. This gate is approval-only: either Start it as-is or Cancel, discuss changes in the main chat, and rerun /cook.",
|
|
1434
|
-
proposalHeading: "Proposed workflow",
|
|
1435
|
-
proposalBody: buildContextProposalDisplayText(proposal),
|
|
1436
|
-
critiqueHeading: "Critique and risks",
|
|
1437
|
-
critiqueBody: buildContextProposalCritiqueText(analysis),
|
|
1438
|
-
routingHeading: "Routing recommendations",
|
|
1439
|
-
routingBody: buildContextProposalRoutingText(analysis),
|
|
1440
|
-
actionsHeading: "Actions",
|
|
1441
|
-
actions: buildContextProposalConfirmationActions(),
|
|
1442
|
-
footer: "↑↓ navigate • enter select • esc cancel",
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
function maybeWriteContextProposalConfirmationSnapshot(layout: ContextProposalConfirmationLayout): void {
|
|
1447
|
-
const snapshotPath = completionTestContextProposalUiSnapshotPath();
|
|
1448
|
-
if (!snapshotPath) return;
|
|
1449
|
-
try {
|
|
1450
|
-
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
1451
|
-
fs.writeFileSync(snapshotPath, `${JSON.stringify(layout, null, 2)}\n`, "utf8");
|
|
1452
|
-
} catch {
|
|
1453
|
-
// ignore malformed or unwritable test snapshot paths
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
function maybeWriteContextProposalSnapshot(proposal: ContextProposal): void {
|
|
1458
|
-
const snapshotPath = completionTestContextProposalSnapshotPath();
|
|
1459
|
-
if (!snapshotPath) return;
|
|
1460
|
-
try {
|
|
1461
|
-
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
1462
|
-
fs.writeFileSync(snapshotPath, `${JSON.stringify(proposal, null, 2)}\n`, "utf8");
|
|
1463
|
-
} catch {
|
|
1464
|
-
// ignore malformed or unwritable test snapshot paths
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
function buildContextProposalConfirmationSelectItems(layout: ContextProposalConfirmationLayout): SelectItem[] {
|
|
1469
|
-
return layout.actions.map((action) => ({
|
|
1470
|
-
value: action.id,
|
|
1471
|
-
label: action.label,
|
|
1472
|
-
description: action.description,
|
|
1473
|
-
}));
|
|
292
|
+
function buildContextProposalConfirmationLayout(
|
|
293
|
+
title: string,
|
|
294
|
+
proposal: ContextProposal,
|
|
295
|
+
): ContextProposalConfirmationLayout {
|
|
296
|
+
return buildExtractedContextProposalConfirmationLayout({
|
|
297
|
+
title,
|
|
298
|
+
proposal,
|
|
299
|
+
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
300
|
+
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
301
|
+
defaultTaskType: DEFAULT_TASK_TYPE,
|
|
302
|
+
defaultEvaluationProfile: DEFAULT_EVALUATION_PROFILE,
|
|
303
|
+
});
|
|
1474
304
|
}
|
|
1475
305
|
|
|
1476
306
|
async function promptContextProposalConfirmationAction(
|
|
@@ -1526,258 +356,32 @@ async function promptContextProposalConfirmationAction(
|
|
|
1526
356
|
});
|
|
1527
357
|
}
|
|
1528
358
|
|
|
1529
|
-
async function resolveContextProposalConfirmationAction(
|
|
1530
|
-
proposal: ContextProposal,
|
|
1531
|
-
action: ContextProposalConfirmAction,
|
|
1532
|
-
): Promise<ContextProposalDecision | undefined> {
|
|
1533
|
-
if (action === "cancel") return undefined;
|
|
1534
|
-
return {
|
|
1535
|
-
missionAnchor: proposal.mission,
|
|
1536
|
-
goalText: proposal.goalText,
|
|
1537
|
-
analysis: finalizeContextProposalAnalysis(proposal.analysis, [proposal.goalText, proposal.mission]),
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
function parseContextProposal(text: string, projectName: string): ContextProposal | undefined {
|
|
1542
|
-
const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
1543
|
-
if (!cleaned) return undefined;
|
|
1544
|
-
const lines = cleaned
|
|
1545
|
-
.split("\n")
|
|
1546
|
-
.map((line) => line.trim())
|
|
1547
|
-
.filter((line) => line.length > 0);
|
|
1548
|
-
if (lines.length === 0) return undefined;
|
|
1549
|
-
|
|
1550
|
-
let section: ContextProposalSection | undefined;
|
|
1551
|
-
let missionLine: string | undefined;
|
|
1552
|
-
let taskTypeHint: string | undefined;
|
|
1553
|
-
let evaluationProfileHint: string | undefined;
|
|
1554
|
-
const scope: string[] = [];
|
|
1555
|
-
const constraints: string[] = [];
|
|
1556
|
-
const acceptance: string[] = [];
|
|
1557
|
-
const critique: string[] = [];
|
|
1558
|
-
const risks: string[] = [];
|
|
1559
|
-
let structuredSignalCount = 0;
|
|
1560
|
-
|
|
1561
|
-
for (const rawLine of lines) {
|
|
1562
|
-
const routingHint = matchContextProposalRoutingHint(rawLine);
|
|
1563
|
-
if (routingHint) {
|
|
1564
|
-
structuredSignalCount += 1;
|
|
1565
|
-
if (routingHint.field === "taskType") taskTypeHint = routingHint.value;
|
|
1566
|
-
else evaluationProfileHint = routingHint.value;
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
const inlineSection = matchInlineProposalSection(rawLine);
|
|
1570
|
-
if (inlineSection) {
|
|
1571
|
-
section = inlineSection.section;
|
|
1572
|
-
structuredSignalCount += 1;
|
|
1573
|
-
if (inlineSection.section === "mission" && !missionLine) {
|
|
1574
|
-
missionLine = inlineSection.content;
|
|
1575
|
-
} else if (inlineSection.section === "constraints") {
|
|
1576
|
-
constraints.push(inlineSection.content);
|
|
1577
|
-
} else if (inlineSection.section === "acceptance") {
|
|
1578
|
-
acceptance.push(inlineSection.content);
|
|
1579
|
-
} else if (inlineSection.section === "scope") {
|
|
1580
|
-
scope.push(inlineSection.content);
|
|
1581
|
-
} else if (inlineSection.section === "critique") {
|
|
1582
|
-
critique.push(inlineSection.content);
|
|
1583
|
-
} else if (inlineSection.section === "risks") {
|
|
1584
|
-
risks.push(inlineSection.content);
|
|
1585
|
-
}
|
|
1586
|
-
continue;
|
|
1587
|
-
}
|
|
1588
|
-
const headerSection = detectProposalSection(rawLine);
|
|
1589
|
-
if (headerSection) {
|
|
1590
|
-
section = headerSection;
|
|
1591
|
-
structuredSignalCount += 1;
|
|
1592
|
-
continue;
|
|
1593
|
-
}
|
|
1594
|
-
const bullet = bulletText(rawLine);
|
|
1595
|
-
if (bullet) {
|
|
1596
|
-
structuredSignalCount += 1;
|
|
1597
|
-
if (section === "mission" && !missionLine) {
|
|
1598
|
-
missionLine = bullet;
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
if (section === "constraints") {
|
|
1602
|
-
constraints.push(bullet);
|
|
1603
|
-
continue;
|
|
1604
|
-
}
|
|
1605
|
-
if (section === "acceptance") {
|
|
1606
|
-
acceptance.push(bullet);
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
if (section === "scope") {
|
|
1610
|
-
scope.push(bullet);
|
|
1611
|
-
continue;
|
|
1612
|
-
}
|
|
1613
|
-
if (section === "critique") {
|
|
1614
|
-
critique.push(bullet);
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
if (section === "risks") {
|
|
1618
|
-
risks.push(bullet);
|
|
1619
|
-
continue;
|
|
1620
|
-
}
|
|
1621
|
-
if (!missionLine) {
|
|
1622
|
-
missionLine = bullet;
|
|
1623
|
-
continue;
|
|
1624
|
-
}
|
|
1625
|
-
if (looksLikeAcceptance(bullet)) acceptance.push(bullet);
|
|
1626
|
-
else if (looksLikeConstraint(bullet)) constraints.push(bullet);
|
|
1627
|
-
else scope.push(bullet);
|
|
1628
|
-
continue;
|
|
1629
|
-
}
|
|
1630
|
-
const normalized = normalizeProposalLine(rawLine);
|
|
1631
|
-
if (!normalized) continue;
|
|
1632
|
-
if (!missionLine) {
|
|
1633
|
-
missionLine = normalized;
|
|
1634
|
-
continue;
|
|
1635
|
-
}
|
|
1636
|
-
if (section === "critique") {
|
|
1637
|
-
critique.push(normalized);
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
if (section === "risks") {
|
|
1641
|
-
risks.push(normalized);
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
if (section === "constraints" || looksLikeConstraint(normalized)) {
|
|
1645
|
-
constraints.push(normalized);
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
if (section === "acceptance" || looksLikeAcceptance(normalized)) {
|
|
1649
|
-
acceptance.push(normalized);
|
|
1650
|
-
continue;
|
|
1651
|
-
}
|
|
1652
|
-
if (section === "scope") {
|
|
1653
|
-
scope.push(normalized);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
const basisPreview = cleaned.replace(/\s+/g, " ").trim();
|
|
1658
|
-
const missionSource = missionLine ?? scope[0] ?? acceptance[0] ?? constraints[0] ?? basisPreview;
|
|
1659
|
-
const assessment = assessMissionAnchor(missionSource, projectName);
|
|
1660
|
-
const normalizedMission = normalizeMissionAnchorText(missionSource);
|
|
1661
|
-
const itemCount = scope.length + constraints.length + acceptance.length + critique.length + risks.length;
|
|
1662
|
-
const hasStrongStructure = structuredSignalCount >= 2 || itemCount >= 2;
|
|
1663
|
-
if (!normalizedMission || isWeakMissionAnchor(normalizedMission)) return undefined;
|
|
1664
|
-
if (!hasStrongStructure && basisPreview.length < 140) return undefined;
|
|
1665
|
-
const mission = assessment.derived;
|
|
1666
|
-
const analysis = buildContextProposalAnalysis({
|
|
1667
|
-
taskType: taskTypeHint,
|
|
1668
|
-
evaluationProfile: evaluationProfileHint,
|
|
1669
|
-
critique,
|
|
1670
|
-
risks,
|
|
1671
|
-
hintTexts: [cleaned, mission, ...scope, ...constraints, ...acceptance, ...critique, ...risks],
|
|
1672
|
-
});
|
|
1673
|
-
const goalText = buildContextProposalGoalText({ mission, scope, constraints, acceptance });
|
|
1674
|
-
return finalizeContextProposal(
|
|
1675
|
-
{
|
|
1676
|
-
mission,
|
|
1677
|
-
scope,
|
|
1678
|
-
constraints,
|
|
1679
|
-
acceptance,
|
|
1680
|
-
analysis,
|
|
1681
|
-
goalText,
|
|
1682
|
-
basisPreview,
|
|
1683
|
-
source: "session",
|
|
1684
|
-
},
|
|
1685
|
-
projectName,
|
|
1686
|
-
);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
function hasStructuredContextProposalSignal(text: string): boolean {
|
|
1690
|
-
const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
1691
|
-
if (!cleaned) return false;
|
|
1692
|
-
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(
|
|
1693
|
-
cleaned,
|
|
1694
|
-
);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
function parseStrictStructuredSessionProposal(text: string, projectName: string): ContextProposal | undefined {
|
|
1698
|
-
const cleaned = stripCodeBlocks(text).replace(/\r/g, "").trim();
|
|
1699
|
-
if (!cleaned) return undefined;
|
|
1700
|
-
const lines = cleaned
|
|
1701
|
-
.split("\n")
|
|
1702
|
-
.map((line) => line.trim())
|
|
1703
|
-
.filter((line) => line.length > 0);
|
|
1704
|
-
if (lines.length === 0) return undefined;
|
|
1705
|
-
|
|
1706
|
-
let section: ContextProposalSection | undefined;
|
|
1707
|
-
const sectionsPresent = new Set<ContextProposalSection>();
|
|
1708
|
-
const missionCandidates: string[] = [];
|
|
1709
|
-
|
|
1710
|
-
for (const rawLine of lines) {
|
|
1711
|
-
const inlineSection = matchInlineProposalSection(rawLine);
|
|
1712
|
-
if (inlineSection) {
|
|
1713
|
-
section = inlineSection.section;
|
|
1714
|
-
sectionsPresent.add(section);
|
|
1715
|
-
if (section === "mission") missionCandidates.push(inlineSection.content);
|
|
1716
|
-
continue;
|
|
1717
|
-
}
|
|
1718
|
-
const headerSection = detectProposalSection(rawLine);
|
|
1719
|
-
if (headerSection) {
|
|
1720
|
-
section = headerSection;
|
|
1721
|
-
sectionsPresent.add(section);
|
|
1722
|
-
continue;
|
|
1723
|
-
}
|
|
1724
|
-
const normalized = bulletText(rawLine) ?? normalizeProposalLine(rawLine);
|
|
1725
|
-
if (normalized && section === "mission") missionCandidates.push(normalized);
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
const requiredSections: ContextProposalSection[] = ["mission", "scope", "constraints", "acceptance"];
|
|
1729
|
-
if (requiredSections.some((candidate) => !sectionsPresent.has(candidate))) return undefined;
|
|
1730
|
-
|
|
1731
|
-
const distinctMissionAnchors = Array.from(
|
|
1732
|
-
new Set(
|
|
1733
|
-
missionCandidates
|
|
1734
|
-
.map((candidate) => normalizeMissionAnchorText(assessMissionAnchor(candidate, projectName).derived))
|
|
1735
|
-
.filter((candidate): candidate is string => Boolean(candidate)),
|
|
1736
|
-
),
|
|
1737
|
-
);
|
|
1738
|
-
if (distinctMissionAnchors.length !== 1) return undefined;
|
|
1739
|
-
|
|
1740
|
-
const proposal = parseContextProposal(cleaned, projectName);
|
|
1741
|
-
if (!proposal) return undefined;
|
|
1742
|
-
if (
|
|
1743
|
-
normalizeMissionAnchorText(proposal.mission) !== distinctMissionAnchors[0] &&
|
|
1744
|
-
!isGenericPlanningMissionAnchor(distinctMissionAnchors[0])
|
|
1745
|
-
) {
|
|
1746
|
-
return undefined;
|
|
1747
|
-
}
|
|
1748
|
-
if (proposal.scope.length === 0 || proposal.constraints.length === 0 || proposal.acceptance.length === 0) return undefined;
|
|
1749
|
-
return { ...proposal, source: "session" };
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
function extractContextProposalFromStructuredSession(
|
|
1753
|
-
recentEntries: RecentDiscussionEntry[],
|
|
1754
|
-
projectName: string,
|
|
1755
|
-
): ContextProposal | undefined {
|
|
1756
|
-
const structuredTexts = recentEntries
|
|
1757
|
-
.slice()
|
|
1758
|
-
.reverse()
|
|
1759
|
-
.map((entry) => entry.text.trim())
|
|
1760
|
-
.filter((text) => hasStructuredContextProposalSignal(text));
|
|
1761
|
-
if (structuredTexts.length === 0) return undefined;
|
|
1762
|
-
return parseStrictStructuredSessionProposal(structuredTexts.join("\n\n"), projectName);
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
async function extractContextProposalFromSession(
|
|
1766
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1767
|
-
projectName: string,
|
|
1768
|
-
hintText?: string,
|
|
1769
|
-
): Promise<ContextProposal | undefined> {
|
|
1770
|
-
const recentEntries = collectRecentDiscussionEntries(ctx);
|
|
1771
|
-
return (await analyzeContextProposalWithAgent(ctx, projectName, recentEntries, hintText)) ??
|
|
1772
|
-
extractContextProposalFromStructuredSession(recentEntries, projectName);
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
359
|
async function deriveCookContextProposal(
|
|
1776
360
|
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
1777
361
|
projectName: string,
|
|
1778
|
-
hintText?: string,
|
|
1779
362
|
): Promise<ContextProposal | undefined> {
|
|
1780
|
-
|
|
363
|
+
const recentEntries = collectRecentDiscussionEntries(ctx, { isRecord, asString, isStaleContextError });
|
|
364
|
+
return await deriveCookContextProposalFromRecentDiscussion(projectName, recentEntries, {
|
|
365
|
+
asString,
|
|
366
|
+
asStringArray,
|
|
367
|
+
analyzeContextProposal: async (entries) =>
|
|
368
|
+
await analyzeContextProposalWithAgent({
|
|
369
|
+
ctx,
|
|
370
|
+
projectName,
|
|
371
|
+
recentEntries: entries,
|
|
372
|
+
liveRoleActivityByRoot,
|
|
373
|
+
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
374
|
+
safeUiCall,
|
|
375
|
+
getCtxCwd,
|
|
376
|
+
getCtxHasUI,
|
|
377
|
+
getCtxUi,
|
|
378
|
+
}),
|
|
379
|
+
assessMissionAnchor,
|
|
380
|
+
isWeakMissionAnchor,
|
|
381
|
+
missionAnchorsStrictlyEquivalent,
|
|
382
|
+
normalizeMissionAnchorText,
|
|
383
|
+
stripCodeBlocks,
|
|
384
|
+
});
|
|
1781
385
|
}
|
|
1782
386
|
|
|
1783
387
|
async function confirmContextProposal(
|
|
@@ -1785,915 +389,42 @@ async function confirmContextProposal(
|
|
|
1785
389
|
proposal: ContextProposal,
|
|
1786
390
|
options: ContextProposalConfirmOptions,
|
|
1787
391
|
): Promise<ContextProposalDecision | undefined> {
|
|
1788
|
-
maybeWriteContextProposalSnapshot(proposal);
|
|
392
|
+
maybeWriteContextProposalSnapshot(proposal, completionTestContextProposalSnapshotPath());
|
|
1789
393
|
const actionOverride = completionTestContextProposalActionOverride();
|
|
1790
394
|
if (actionOverride === "cancel") return undefined;
|
|
1791
395
|
if (actionOverride === "accept") {
|
|
1792
|
-
return
|
|
396
|
+
return resolveContextProposalConfirmationAction(proposal, "start");
|
|
1793
397
|
}
|
|
1794
398
|
const layout = buildContextProposalConfirmationLayout(options.title, proposal);
|
|
1795
|
-
maybeWriteContextProposalConfirmationSnapshot(layout);
|
|
399
|
+
maybeWriteContextProposalConfirmationSnapshot(layout, completionTestContextProposalUiSnapshotPath());
|
|
1796
400
|
const uiActionOverride = completionTestContextProposalUiActionOverride();
|
|
1797
401
|
if (uiActionOverride) {
|
|
1798
|
-
return
|
|
402
|
+
return resolveContextProposalConfirmationAction(proposal, uiActionOverride);
|
|
1799
403
|
}
|
|
1800
404
|
if (!getCtxHasUI(ctx)) {
|
|
1801
|
-
return options.nonInteractiveBehavior === "accept"
|
|
1802
|
-
? await resolveContextProposalConfirmationAction(proposal, "start")
|
|
1803
|
-
: undefined;
|
|
405
|
+
return options.nonInteractiveBehavior === "accept" ? resolveContextProposalConfirmationAction(proposal, "start") : undefined;
|
|
1804
406
|
}
|
|
1805
407
|
const ui = getCtxUi(ctx);
|
|
1806
408
|
if (!ui) {
|
|
1807
|
-
return options.nonInteractiveBehavior === "accept"
|
|
1808
|
-
? await resolveContextProposalConfirmationAction(proposal, "start")
|
|
1809
|
-
: undefined;
|
|
409
|
+
return options.nonInteractiveBehavior === "accept" ? resolveContextProposalConfirmationAction(proposal, "start") : undefined;
|
|
1810
410
|
}
|
|
1811
411
|
const choice = await promptContextProposalConfirmationAction(ui, layout);
|
|
1812
412
|
if (!choice) return undefined;
|
|
1813
|
-
return
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
function currentMissionAnchor(snapshot: CompletionStateSnapshot): string {
|
|
1817
|
-
return (
|
|
1818
|
-
asString(snapshot.state?.mission_anchor) ??
|
|
1819
|
-
asString(snapshot.plan?.mission_anchor) ??
|
|
1820
|
-
asString(snapshot.active?.mission_anchor) ??
|
|
1821
|
-
path.basename(snapshot.files.root)
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function currentTaskType(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1826
|
-
return (
|
|
1827
|
-
asString(snapshot.active?.task_type) ??
|
|
1828
|
-
asString(snapshot.state?.task_type) ??
|
|
1829
|
-
asString(snapshot.plan?.task_type) ??
|
|
1830
|
-
asString(snapshot.profile?.task_type)
|
|
1831
|
-
);
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
function currentEvaluationProfile(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1835
|
-
return (
|
|
1836
|
-
asString(snapshot.active?.evaluation_profile) ??
|
|
1837
|
-
asString(snapshot.state?.evaluation_profile) ??
|
|
1838
|
-
asString(snapshot.plan?.evaluation_profile) ??
|
|
1839
|
-
asString(snapshot.profile?.evaluation_profile)
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function completionContinuationFingerprint(snapshot: CompletionStateSnapshot): string | undefined {
|
|
1844
|
-
if (asString(snapshot.state?.continuation_policy) !== "continue") return undefined;
|
|
1845
|
-
const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role);
|
|
1846
|
-
if (!nextMandatoryRole) return undefined;
|
|
1847
|
-
return JSON.stringify({
|
|
1848
|
-
mission_anchor: asString(snapshot.state?.mission_anchor) ?? asString(snapshot.plan?.mission_anchor) ?? null,
|
|
1849
|
-
task_type: currentTaskType(snapshot) ?? null,
|
|
1850
|
-
evaluation_profile: currentEvaluationProfile(snapshot) ?? null,
|
|
1851
|
-
current_phase: asString(snapshot.state?.current_phase) ?? null,
|
|
1852
|
-
next_mandatory_role: nextMandatoryRole,
|
|
1853
|
-
next_mandatory_action: asString(snapshot.state?.next_mandatory_action) ?? null,
|
|
1854
|
-
active_status: asString(snapshot.active?.status) ?? null,
|
|
1855
|
-
active_slice_id: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? null,
|
|
1856
|
-
latest_completed_slice: asString(snapshot.state?.latest_completed_slice) ?? null,
|
|
1857
|
-
latest_verified_slice: asString(snapshot.state?.latest_verified_slice) ?? null,
|
|
1858
|
-
});
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
function noteQueuedDriverPrompt(rootKey: string, fingerprint: string): void {
|
|
1862
|
-
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1863
|
-
if (tracker && tracker.fingerprint === fingerprint) {
|
|
1864
|
-
tracker.attempts += 1;
|
|
1865
|
-
tracker.inFlight = false;
|
|
1866
|
-
tracker.warned = false;
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
driverContinuationByRoot.set(rootKey, {
|
|
1870
|
-
fingerprint,
|
|
1871
|
-
attempts: 1,
|
|
1872
|
-
inFlight: false,
|
|
1873
|
-
warned: false,
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
function markQueuedDriverPromptInFlight(rootKey: string, fingerprint: string): void {
|
|
1878
|
-
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1879
|
-
if (!tracker || tracker.fingerprint !== fingerprint) return;
|
|
1880
|
-
tracker.inFlight = true;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function clearDriverContinuationTracker(rootKey: string): void {
|
|
1884
|
-
driverContinuationByRoot.delete(rootKey);
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function hasRunningCompletionRole(rootKey: string): boolean {
|
|
1888
|
-
return liveRoleActivityByRoot.get(rootKey)?.status === "running";
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
function isWorkflowDriverActive(snapshot: CompletionStateSnapshot | undefined): boolean {
|
|
1892
|
-
return Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) === "continue";
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
function isDriverContinuationStateParked(rootKey: string, fingerprint: string): boolean {
|
|
1896
|
-
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1897
|
-
if (!tracker || tracker.fingerprint !== fingerprint) return false;
|
|
1898
|
-
return tracker.warned;
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
function rememberParkedDriverContinuation(rootKey: string, fingerprint: string): void {
|
|
1902
|
-
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1903
|
-
if (!tracker || tracker.fingerprint !== fingerprint) return;
|
|
1904
|
-
tracker.warned = true;
|
|
1905
|
-
tracker.inFlight = false;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
async function queueCompletionDriverPrompt(
|
|
1909
|
-
pi: ExtensionAPI,
|
|
1910
|
-
ctx: { cwd: string; hasUI: boolean; ui: any },
|
|
1911
|
-
rootKey: string,
|
|
1912
|
-
fingerprint: string,
|
|
1913
|
-
prompt: string,
|
|
1914
|
-
kind: "kickoff" | "resume" | "auto-resume",
|
|
1915
|
-
): Promise<boolean> {
|
|
1916
|
-
const snapshotPath = kind === "auto-resume" ? completionTestAutoContinuePromptPath() : completionTestDriverPromptPath();
|
|
1917
|
-
maybeWriteTestSnapshot(snapshotPath, `${prompt}\n`);
|
|
1918
|
-
noteQueuedDriverPrompt(rootKey, fingerprint);
|
|
1919
|
-
if (shouldSkipDriverKickoffForTests()) {
|
|
1920
|
-
emitCommandText(ctx, `Skipped completion workflow ${kind} prompt (test mode)`, "info");
|
|
1921
|
-
return false;
|
|
1922
|
-
}
|
|
1923
|
-
pi.sendUserMessage(prompt);
|
|
1924
|
-
emitCommandText(ctx, `Queued completion workflow ${kind}`, "info");
|
|
1925
|
-
return true;
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
async function autoContinueWorkflowIfNeeded(pi: ExtensionAPI, ctx: { cwd: string; hasUI: boolean; ui: any }): Promise<void> {
|
|
1929
|
-
if (roleFromEnv()) return;
|
|
1930
|
-
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
1931
|
-
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
1932
|
-
if (!snapshot) {
|
|
1933
|
-
clearDriverContinuationTracker(rootKey);
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
const fingerprint = completionContinuationFingerprint(snapshot);
|
|
1937
|
-
if (!fingerprint) {
|
|
1938
|
-
clearDriverContinuationTracker(rootKey);
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (!isWorkflowDriverActive(snapshot) || hasRunningCompletionRole(rootKey)) return;
|
|
1942
|
-
const tracker = driverContinuationByRoot.get(rootKey);
|
|
1943
|
-
if (tracker && tracker.fingerprint === fingerprint) {
|
|
1944
|
-
if (tracker.inFlight) {
|
|
1945
|
-
tracker.inFlight = false;
|
|
1946
|
-
if (tracker.attempts >= DRIVER_AUTO_CONTINUE_MAX_ATTEMPTS) {
|
|
1947
|
-
if (!isDriverContinuationStateParked(rootKey, fingerprint)) {
|
|
1948
|
-
rememberParkedDriverContinuation(rootKey, fingerprint);
|
|
1949
|
-
emitCommandText(
|
|
1950
|
-
ctx,
|
|
1951
|
-
`Completion workflow is parked before mandatory role dispatch: ${asString(snapshot.state?.next_mandatory_role) ?? "(unknown)"}. Rerun /cook to continue from canonical state.`,
|
|
1952
|
-
"warning",
|
|
1953
|
-
);
|
|
1954
|
-
}
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
} else {
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
const resumePrompt = completionResumePrompt(currentTaskType(snapshot) ?? "(missing)", currentEvaluationProfile(snapshot) ?? "(missing)");
|
|
1962
|
-
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, "auto-resume");
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
|
|
1966
|
-
return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function activeSliceContext(snapshot: CompletionStateSnapshot) {
|
|
1970
|
-
const active = snapshot.active;
|
|
1971
|
-
const activeSlice = snapshot.activeSlice;
|
|
1972
|
-
return {
|
|
1973
|
-
sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
|
|
1974
|
-
status: asString(active?.status) ?? asString(activeSlice?.status),
|
|
1975
|
-
goal: asString(active?.goal) ?? asString(activeSlice?.goal),
|
|
1976
|
-
contractIds:
|
|
1977
|
-
asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
|
|
1978
|
-
acceptance:
|
|
1979
|
-
asStringArray(active?.acceptance_criteria).length > 0
|
|
1980
|
-
? asStringArray(active?.acceptance_criteria)
|
|
1981
|
-
: asStringArray(activeSlice?.acceptance_criteria),
|
|
1982
|
-
implementationSurfaces: asStringArray(active?.implementation_surfaces),
|
|
1983
|
-
verificationCommands: asStringArray(active?.verification_commands),
|
|
1984
|
-
lockedNotes: asStringArray(active?.locked_notes),
|
|
1985
|
-
mustFixFindings: asStringArray(active?.must_fix_findings),
|
|
1986
|
-
remainingBefore: asStringArray(active?.remaining_contract_ids_before),
|
|
1987
|
-
basisCommit: asString(active?.basis_commit),
|
|
1988
|
-
releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
|
|
1989
|
-
highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
|
|
1990
|
-
};
|
|
413
|
+
return resolveContextProposalConfirmationAction(proposal, choice);
|
|
1991
414
|
}
|
|
1992
415
|
|
|
1993
|
-
function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
|
|
1994
|
-
const evidence = snapshot.verificationEvidence;
|
|
1995
|
-
return {
|
|
1996
|
-
path: path.relative(snapshot.files.root, snapshot.files.verificationEvidencePath) || ".agent/verification-evidence.json",
|
|
1997
|
-
status: evidence ? "present" : "missing",
|
|
1998
|
-
subjectType: asString(evidence?.subject_type),
|
|
1999
|
-
sliceId: asString(evidence?.slice_id),
|
|
2000
|
-
goal: asString(evidence?.goal),
|
|
2001
|
-
contractIds: asStringArray(evidence?.contract_ids),
|
|
2002
|
-
basisCommit: asString(evidence?.basis_commit),
|
|
2003
|
-
headSha: asString(evidence?.head_sha),
|
|
2004
|
-
verificationCommands: asStringArray(evidence?.verification_commands),
|
|
2005
|
-
outcome: asString(evidence?.outcome),
|
|
2006
|
-
recordedAt: asString(evidence?.recorded_at),
|
|
2007
|
-
summary:
|
|
2008
|
-
asString(evidence?.summary) ??
|
|
2009
|
-
(evidence ? "Canonical verification evidence is present but its summary is missing." : "Canonical verification evidence is missing."),
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
|
|
2014
|
-
const context = activeSliceContext(snapshot);
|
|
2015
|
-
const evidence = verificationEvidenceContext(snapshot);
|
|
2016
|
-
const lines = [
|
|
2017
|
-
`Canonical evaluation handoff for ${role}:`,
|
|
2018
|
-
`- task_type: ${currentTaskType(snapshot) ?? "(missing)"}`,
|
|
2019
|
-
`- evaluation_profile: ${currentEvaluationProfile(snapshot) ?? "(missing)"}`,
|
|
2020
|
-
`- latest_completed_slice: ${asString(snapshot.state?.latest_completed_slice) ?? "(none)"}`,
|
|
2021
|
-
`- active_slice_id: ${context.sliceId ?? "(none)"}`,
|
|
2022
|
-
`- active_slice_status: ${context.status ?? "(unknown)"}`,
|
|
2023
|
-
`- active_slice_goal: ${context.goal ?? "(unknown)"}`,
|
|
2024
|
-
`- contract_ids: ${context.contractIds.length > 0 ? context.contractIds.join(", ") : "(none)"}`,
|
|
2025
|
-
`- acceptance_criteria: ${context.acceptance.length > 0 ? context.acceptance.join(" | ") : "(none)"}`,
|
|
2026
|
-
`- implementation_surfaces: ${context.implementationSurfaces.length > 0 ? context.implementationSurfaces.join(" | ") : "(none)"}`,
|
|
2027
|
-
`- verification_commands: ${context.verificationCommands.length > 0 ? context.verificationCommands.join(" | ") : "(none)"}`,
|
|
2028
|
-
`- locked_notes: ${context.lockedNotes.length > 0 ? context.lockedNotes.join(" | ") : "(none)"}`,
|
|
2029
|
-
`- must_fix_findings: ${context.mustFixFindings.length > 0 ? context.mustFixFindings.join(" | ") : "(none)"}`,
|
|
2030
|
-
`- basis_commit: ${context.basisCommit ?? "(none)"}`,
|
|
2031
|
-
`- remaining_contract_ids_before: ${context.remainingBefore.length > 0 ? context.remainingBefore.join(", ") : "(none)"}`,
|
|
2032
|
-
`- release_blocker_count_before: ${context.releaseBlockerCountBefore ?? "(unknown)"}`,
|
|
2033
|
-
`- high_value_gap_count_before: ${context.highValueGapCountBefore ?? "(unknown)"}`,
|
|
2034
|
-
`- verification_evidence_path: ${evidence.path}`,
|
|
2035
|
-
`- verification_evidence_status: ${evidence.status}`,
|
|
2036
|
-
`- verification_evidence_subject_type: ${evidence.subjectType ?? "(missing)"}`,
|
|
2037
|
-
`- verification_evidence_slice_id: ${evidence.sliceId ?? "(none)"}`,
|
|
2038
|
-
`- verification_evidence_contract_ids: ${evidence.contractIds.length > 0 ? evidence.contractIds.join(", ") : "(none)"}`,
|
|
2039
|
-
`- verification_evidence_outcome: ${evidence.outcome ?? "(missing)"}`,
|
|
2040
|
-
`- verification_evidence_recorded_at: ${evidence.recordedAt ?? "(missing)"}`,
|
|
2041
|
-
`- verification_evidence_head_sha: ${evidence.headSha ?? "(missing)"}`,
|
|
2042
|
-
`- verification_evidence_basis_commit: ${evidence.basisCommit ?? "(missing)"}`,
|
|
2043
|
-
`- verification_evidence_commands: ${evidence.verificationCommands.length > 0 ? evidence.verificationCommands.join(" | ") : "(none)"}`,
|
|
2044
|
-
`- verification_evidence_summary: ${evidence.summary}`,
|
|
2045
|
-
];
|
|
2046
|
-
return lines;
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
|
|
2050
|
-
return buildEvaluationRoleContextLines(snapshot, role).join(" ");
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
async function assessActiveWorkflowProposalRouting(
|
|
2054
|
-
ctx: { cwd: string; hasUI: boolean; ui: any; sessionManager: any; model?: any; modelRegistry?: any },
|
|
2055
|
-
snapshot: CompletionStateSnapshot,
|
|
2056
|
-
hintText?: string,
|
|
2057
|
-
): Promise<ActiveWorkflowProposalAssessment> {
|
|
2058
|
-
const currentMission = currentMissionAnchor(snapshot);
|
|
2059
|
-
const projectName = path.basename(snapshot.files.root);
|
|
2060
|
-
const proposal = await deriveCookContextProposal(ctx, projectName, hintText);
|
|
2061
|
-
if (!proposal) {
|
|
2062
|
-
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2063
|
-
action: "unclear",
|
|
2064
|
-
currentMissionAnchor: currentMission,
|
|
2065
|
-
reason: "missing_proposal",
|
|
2066
|
-
};
|
|
2067
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2068
|
-
return assessment;
|
|
2069
|
-
}
|
|
2070
|
-
if (missionAnchorsLikelyEquivalent(currentMission, proposal.mission)) {
|
|
2071
|
-
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2072
|
-
action: "continue",
|
|
2073
|
-
currentMissionAnchor: currentMission,
|
|
2074
|
-
proposal,
|
|
2075
|
-
reason: "matching_mission",
|
|
2076
|
-
};
|
|
2077
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2078
|
-
return assessment;
|
|
2079
|
-
}
|
|
2080
|
-
if (shouldTreatBareActiveWorkflowProposalAsClearRefocus(proposal)) {
|
|
2081
|
-
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2082
|
-
action: "refocus",
|
|
2083
|
-
currentMissionAnchor: currentMission,
|
|
2084
|
-
proposal,
|
|
2085
|
-
reason: "clear_refocus",
|
|
2086
|
-
};
|
|
2087
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2088
|
-
return assessment;
|
|
2089
|
-
}
|
|
2090
|
-
const assessment: ActiveWorkflowProposalAssessment = {
|
|
2091
|
-
action: "unclear",
|
|
2092
|
-
currentMissionAnchor: currentMission,
|
|
2093
|
-
proposal,
|
|
2094
|
-
reason: "ambiguous_discussion",
|
|
2095
|
-
};
|
|
2096
|
-
maybeWriteActiveWorkflowRoutingSnapshot(assessment);
|
|
2097
|
-
return assessment;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
async function resumeActiveWorkflowFromCanonicalState(
|
|
2101
|
-
pi: any,
|
|
2102
|
-
ctx: { cwd: string; hasUI: boolean; ui: any },
|
|
2103
|
-
snapshot: CompletionStateSnapshot,
|
|
2104
|
-
): Promise<void> {
|
|
2105
|
-
const mission = currentMissionAnchor(snapshot);
|
|
2106
|
-
pi.setSessionName(`completion: ${mission.slice(0, 60)}`);
|
|
2107
|
-
const resumePrompt = completionResumePrompt(
|
|
2108
|
-
currentTaskType(snapshot) ?? "(missing)",
|
|
2109
|
-
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
2110
|
-
);
|
|
2111
|
-
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
2112
|
-
const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
|
|
2113
|
-
kind: "resume",
|
|
2114
|
-
mission_anchor: mission,
|
|
2115
|
-
current_phase: asString(snapshot.state?.current_phase) ?? null,
|
|
2116
|
-
next_mandatory_role: asString(snapshot.state?.next_mandatory_role) ?? null,
|
|
2117
|
-
});
|
|
2118
|
-
const resumeKind = shouldTestAutoContinueOnSessionStart() && completionTestAutoContinuePromptPath() ? "auto-resume" : "resume";
|
|
2119
|
-
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, resumePrompt, resumeKind);
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
async function confirmExistingWorkflowProposal(
|
|
2123
|
-
ctx: { hasUI: boolean; ui: any },
|
|
2124
|
-
snapshot: CompletionStateSnapshot,
|
|
2125
|
-
proposal: ContextProposal,
|
|
2126
|
-
options: ExistingWorkflowChooserOptions = {},
|
|
2127
|
-
): Promise<ExistingWorkflowDecision | undefined> {
|
|
2128
|
-
const currentMission = currentMissionAnchor(snapshot);
|
|
2129
|
-
const comparison = options.comparison ?? "semantic";
|
|
2130
|
-
const missionsMatch =
|
|
2131
|
-
comparison === "strict"
|
|
2132
|
-
? missionAnchorsStrictlyEquivalent(currentMission, proposal.mission)
|
|
2133
|
-
: missionAnchorsLikelyEquivalent(currentMission, proposal.mission);
|
|
2134
|
-
if (missionsMatch) {
|
|
2135
|
-
return { action: "continue", currentMissionAnchor: currentMission };
|
|
2136
|
-
}
|
|
2137
|
-
const title = [
|
|
2138
|
-
"Existing completion workflow found",
|
|
2139
|
-
"",
|
|
2140
|
-
options.intro ?? "A workflow is already in progress. Choose how /cook should proceed:",
|
|
2141
|
-
"",
|
|
2142
|
-
"Current mission",
|
|
2143
|
-
currentMission,
|
|
2144
|
-
"",
|
|
2145
|
-
options.proposedMissionLabel ?? "New proposed mission",
|
|
2146
|
-
proposal.mission,
|
|
2147
|
-
].join("\n");
|
|
2148
|
-
const continueChoice = "Continue current workflow\n\nKeep the current mission and treat the new goal as extra direction only.";
|
|
2149
|
-
const refocusChoice =
|
|
2150
|
-
options.refocusChoiceLabel ??
|
|
2151
|
-
"Abandon current workflow and start this new one\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.";
|
|
2152
|
-
const cancelChoice = `Cancel\n\nKeep the current workflow unchanged. ${COOK_MAIN_CHAT_RERUN_GUIDANCE}`;
|
|
2153
|
-
maybeWriteTestSnapshot(
|
|
2154
|
-
completionTestExistingWorkflowChooserSnapshotPath(),
|
|
2155
|
-
`${JSON.stringify({ title, choices: [continueChoice, refocusChoice, cancelChoice] }, null, 2)}\n`,
|
|
2156
|
-
);
|
|
2157
|
-
const actionOverride = completionTestWorkflowActionOverride();
|
|
2158
|
-
if (actionOverride === "continue") {
|
|
2159
|
-
return { action: "continue", currentMissionAnchor: currentMission };
|
|
2160
|
-
}
|
|
2161
|
-
if (actionOverride === "refocus") {
|
|
2162
|
-
return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
|
|
2163
|
-
}
|
|
2164
|
-
if (actionOverride === "cancel") return undefined;
|
|
2165
|
-
if (!getCtxHasUI(ctx)) {
|
|
2166
|
-
return { action: "continue", currentMissionAnchor: currentMission };
|
|
2167
|
-
}
|
|
2168
|
-
const ui = getCtxUi(ctx);
|
|
2169
|
-
if (!ui) {
|
|
2170
|
-
return { action: "continue", currentMissionAnchor: currentMission };
|
|
2171
|
-
}
|
|
2172
|
-
const choice = await ui.select(title, [continueChoice, refocusChoice, cancelChoice]);
|
|
2173
|
-
if (!choice || choice === cancelChoice) return undefined;
|
|
2174
|
-
if (choice === refocusChoice) {
|
|
2175
|
-
return { action: "refocus", currentMissionAnchor: currentMission, missionAnchor: proposal.mission };
|
|
2176
|
-
}
|
|
2177
|
-
return { action: "continue", currentMissionAnchor: currentMission };
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
async function refocusCompletionMission(
|
|
2181
|
-
snapshot: CompletionStateSnapshot,
|
|
2182
|
-
missionAnchor: string,
|
|
2183
|
-
rawGoal: string,
|
|
2184
|
-
analysis?: ContextProposalAnalysis,
|
|
2185
|
-
): Promise<void> {
|
|
2186
|
-
const requiredStopJudges = asNumber(snapshot.profile?.required_stop_judges) ?? 3;
|
|
2187
|
-
const root = snapshot.files.root;
|
|
2188
|
-
const routing = finalizeContextProposalAnalysis(analysis, [rawGoal, missionAnchor]);
|
|
2189
|
-
const docsSurfaces = asStringArray(snapshot.profile?.docs_surfaces);
|
|
2190
|
-
const nextProfile = buildProfileRecord({
|
|
2191
|
-
projectName: asString(snapshot.profile?.project_name) ?? path.basename(root),
|
|
2192
|
-
requiredStopJudges,
|
|
2193
|
-
priorityPolicyId: asString(snapshot.profile?.priority_policy_id) ?? "completion-default",
|
|
2194
|
-
docsSurfaces: docsSurfaces.length > 0 ? docsSurfaces : await detectDocsSurfaces(root),
|
|
2195
|
-
taskType: routing.taskType,
|
|
2196
|
-
evaluationProfile: routing.evaluationProfile,
|
|
2197
|
-
});
|
|
2198
|
-
const nextState = {
|
|
2199
|
-
...defaultState(missionAnchor, {
|
|
2200
|
-
taskType: routing.taskType,
|
|
2201
|
-
evaluationProfile: routing.evaluationProfile,
|
|
2202
|
-
continuationReason: buildContextProposalContinuationReason("User refocused workflow via /cook:", rawGoal, routing),
|
|
2203
|
-
}),
|
|
2204
|
-
remaining_stop_judges: requiredStopJudges,
|
|
2205
|
-
next_mandatory_action: "Reconcile canonical state from current repo truth for the refocused mission",
|
|
2206
|
-
};
|
|
2207
|
-
const nextPlan = {
|
|
2208
|
-
...defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }),
|
|
2209
|
-
plan_basis: "user_refocus",
|
|
2210
|
-
};
|
|
2211
|
-
const nextActive = defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile });
|
|
2212
|
-
await Promise.all([
|
|
2213
|
-
fsp.writeFile(path.join(snapshot.files.agentDir, "mission.md"), buildMission(path.basename(root), missionAnchor), "utf8"),
|
|
2214
|
-
writeJsonFile(snapshot.files.profilePath, nextProfile),
|
|
2215
|
-
writeJsonFile(snapshot.files.statePath, nextState),
|
|
2216
|
-
writeJsonFile(snapshot.files.planPath, nextPlan),
|
|
2217
|
-
writeJsonFile(snapshot.files.activePath, nextActive),
|
|
2218
|
-
writeJsonFile(snapshot.files.verificationEvidencePath, defaultVerificationEvidence()),
|
|
2219
|
-
]);
|
|
2220
|
-
}
|
|
2221
416
|
|
|
2222
|
-
function deriveMissionAnchor(rawGoal: string, projectName: string): string {
|
|
2223
|
-
const normalized = normalizeMissionAnchorText(rawGoal);
|
|
2224
|
-
if (!normalized || isWeakMissionAnchor(normalized)) {
|
|
2225
|
-
return `Drive ${projectName} to truthful, verifiable completion.`;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
let mission = normalized
|
|
2229
|
-
.replace(/\b(end[- ]to[- ]end|for me|thanks|thank you)\b/gi, "")
|
|
2230
|
-
.replace(/\s+/g, " ")
|
|
2231
|
-
.trim();
|
|
2232
|
-
|
|
2233
|
-
mission = mission
|
|
2234
|
-
.replace(/\bwith tests and docs\b/gi, "with tests and docs parity")
|
|
2235
|
-
.replace(/\bwith tests and documentation\b/gi, "with tests and docs parity")
|
|
2236
|
-
.replace(/\bwith docs\b/gi, "with docs parity")
|
|
2237
|
-
.trim();
|
|
2238
|
-
|
|
2239
|
-
if (!/[.!?。!?]$/u.test(mission)) mission += ".";
|
|
2240
|
-
return mission;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
function buildProfileRecord(args: {
|
|
2244
|
-
projectName: string;
|
|
2245
|
-
requiredStopJudges: number;
|
|
2246
|
-
priorityPolicyId?: string;
|
|
2247
|
-
docsSurfaces: string[];
|
|
2248
|
-
taskType?: string;
|
|
2249
|
-
evaluationProfile?: string;
|
|
2250
|
-
}): JsonRecord {
|
|
2251
|
-
return {
|
|
2252
|
-
schema_version: 1,
|
|
2253
|
-
protocol_id: PROTOCOL_ID,
|
|
2254
|
-
project_name: args.projectName,
|
|
2255
|
-
required_stop_judges: args.requiredStopJudges,
|
|
2256
|
-
priority_policy_id: args.priorityPolicyId ?? "completion-default",
|
|
2257
|
-
task_type: args.taskType ?? DEFAULT_TASK_TYPE,
|
|
2258
|
-
evaluation_profile: args.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
2259
|
-
docs_surfaces: args.docsSurfaces,
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
function defaultState(
|
|
2264
|
-
missionAnchor: string,
|
|
2265
|
-
routing?: { taskType?: string; evaluationProfile?: string; continuationReason?: string },
|
|
2266
|
-
): JsonRecord {
|
|
2267
|
-
return {
|
|
2268
|
-
schema_version: 1,
|
|
2269
|
-
mission_anchor: missionAnchor,
|
|
2270
|
-
current_phase: "reground",
|
|
2271
|
-
continuation_policy: "continue",
|
|
2272
|
-
continuation_reason: routing?.continuationReason ?? "Fresh completion bootstrap requires canonical re-ground",
|
|
2273
|
-
project_done: false,
|
|
2274
|
-
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
2275
|
-
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
2276
|
-
requires_reground: true,
|
|
2277
|
-
slices_since_last_reground: 0,
|
|
2278
|
-
remaining_release_blockers: null,
|
|
2279
|
-
remaining_high_value_gaps: null,
|
|
2280
|
-
unsatisfied_contract_ids: [],
|
|
2281
|
-
release_blocker_ids: [],
|
|
2282
|
-
next_mandatory_action: "Reconcile canonical state from current repo truth",
|
|
2283
|
-
next_mandatory_role: "completion-regrounder",
|
|
2284
|
-
remaining_stop_judges: 3,
|
|
2285
|
-
last_reground_at: null,
|
|
2286
|
-
last_auditor_verdict: null,
|
|
2287
|
-
contract_status: "unknown",
|
|
2288
|
-
latest_completed_slice: null,
|
|
2289
|
-
latest_verified_slice: null,
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
function defaultPlan(
|
|
2294
|
-
missionAnchor: string,
|
|
2295
|
-
routing?: { taskType?: string; evaluationProfile?: string },
|
|
2296
|
-
): JsonRecord {
|
|
2297
|
-
return {
|
|
2298
|
-
schema_version: 1,
|
|
2299
|
-
mission_anchor: missionAnchor,
|
|
2300
|
-
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
2301
|
-
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
2302
|
-
last_reground_at: null,
|
|
2303
|
-
plan_basis: "bootstrap",
|
|
2304
|
-
candidate_slices: [],
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
function defaultActiveSlice(
|
|
2309
|
-
missionAnchor: string,
|
|
2310
|
-
routing?: { taskType?: string; evaluationProfile?: string },
|
|
2311
|
-
): JsonRecord {
|
|
2312
|
-
return {
|
|
2313
|
-
schema_version: 1,
|
|
2314
|
-
mission_anchor: missionAnchor,
|
|
2315
|
-
task_type: routing?.taskType ?? DEFAULT_TASK_TYPE,
|
|
2316
|
-
evaluation_profile: routing?.evaluationProfile ?? DEFAULT_EVALUATION_PROFILE,
|
|
2317
|
-
status: "idle",
|
|
2318
|
-
slice_id: null,
|
|
2319
|
-
goal: null,
|
|
2320
|
-
contract_ids: [],
|
|
2321
|
-
acceptance_criteria: [],
|
|
2322
|
-
priority: null,
|
|
2323
|
-
why_now: null,
|
|
2324
|
-
blocked_on: [],
|
|
2325
|
-
locked_notes: [],
|
|
2326
|
-
must_fix_findings: [],
|
|
2327
|
-
implementation_surfaces: [],
|
|
2328
|
-
verification_commands: [],
|
|
2329
|
-
basis_commit: null,
|
|
2330
|
-
remaining_contract_ids_before: [],
|
|
2331
|
-
release_blocker_count_before: null,
|
|
2332
|
-
high_value_gap_count_before: null,
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
function defaultVerificationEvidence(): JsonRecord {
|
|
2337
|
-
return {
|
|
2338
|
-
schema_version: 1,
|
|
2339
|
-
artifact_type: "completion-verification-evidence",
|
|
2340
|
-
subject_type: "none",
|
|
2341
|
-
slice_id: null,
|
|
2342
|
-
goal: null,
|
|
2343
|
-
contract_ids: [],
|
|
2344
|
-
basis_commit: null,
|
|
2345
|
-
head_sha: null,
|
|
2346
|
-
verification_commands: [],
|
|
2347
|
-
outcome: "not_recorded",
|
|
2348
|
-
recorded_at: null,
|
|
2349
|
-
summary: "No deterministic verification evidence is recorded yet because no selected slice or current-HEAD verification subject exists.",
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
function buildAgentReadme(projectName: string): string {
|
|
2354
|
-
return `# Completion Control Plane\n\nThis repository uses the \`completion\` workflow for long-running coding tasks.\n\n## Canonical tracked contract files\n\n- \`.agent/README.md\`\n- \`.agent/mission.md\`\n- \`.agent/profile.json\`\n- \`.agent/verify_completion_stop.sh\`\n- \`.agent/verify_completion_control_plane.sh\`\n\n## Ignored canonical execution state\n\n- \`.agent/state.json\`\n- \`.agent/plan.json\`\n- \`.agent/active-slice.json\`\n- \`.agent/slice-history.jsonl\`\n- \`.agent/stop-check-history.jsonl\`\n- \`.agent/verification-evidence.json\`\n- \`.agent/*.log\`\n- \`.agent/tmp/\`\n\n\`.agent/verification-evidence.json\` is the durable canonical record of deterministic verification for the selected slice or current HEAD. Recovery, review, audit, and stop-check reminder surfaces consume it instead of temp-only artifacts or conversational summaries when it is populated.\n\nThe source of truth for long-running completion work is canonical \`.agent/**\` state plus current repo truth.\n\nProject: ${projectName}\n`;
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
function buildMission(projectName: string, missionAnchor: string): string {
|
|
2358
|
-
return `# Mission\n\nProject: ${projectName}\n\nMission anchor:\n${missionAnchor}\n\nThis file is a tracked human-readable statement of the repo's completion mission. Re-grounders may refine this file when repo truth becomes clearer, but it must stay truthful to shipped behavior and the active completion objective.\n`;
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
function buildVerifyStopScript(verifierCommand?: string): string {
|
|
2362
|
-
const repoCheck = verifierCommand
|
|
2363
|
-
? `echo "[completion] running repo-level verification: ${verifierCommand}"\n${verifierCommand}`
|
|
2364
|
-
: `echo "[completion] no repo-specific verifier auto-detected; control-plane verification only"`;
|
|
2365
|
-
return `#!/usr/bin/env bash\nset -euo pipefail\n\nbash .agent/verify_completion_control_plane.sh\n${repoCheck}\n`;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
function buildVerifyControlPlaneScript(): string {
|
|
2369
|
-
return `#!/usr/bin/env bash
|
|
2370
|
-
set -euo pipefail
|
|
2371
|
-
|
|
2372
|
-
for file in \
|
|
2373
|
-
.agent/README.md \
|
|
2374
|
-
.agent/mission.md \
|
|
2375
|
-
.agent/profile.json \
|
|
2376
|
-
.agent/verify_completion_stop.sh \
|
|
2377
|
-
.agent/verify_completion_control_plane.sh \
|
|
2378
|
-
.agent/state.json \
|
|
2379
|
-
.agent/plan.json \
|
|
2380
|
-
.agent/active-slice.json \
|
|
2381
|
-
.agent/verification-evidence.json; do
|
|
2382
|
-
[[ -e "$file" ]] || { echo "missing required file: $file"; exit 1; }
|
|
2383
|
-
done
|
|
2384
|
-
|
|
2385
|
-
node <<'NODE'
|
|
2386
|
-
const childProcess = require('node:child_process');
|
|
2387
|
-
const fs = require('node:fs');
|
|
2388
|
-
|
|
2389
|
-
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2390
|
-
const assert = (condition, message) => {
|
|
2391
|
-
if (!condition) {
|
|
2392
|
-
console.error(message);
|
|
2393
|
-
process.exit(1);
|
|
2394
|
-
}
|
|
2395
|
-
};
|
|
2396
|
-
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2397
|
-
const isString = (value) => typeof value === 'string';
|
|
2398
|
-
const isNonEmptyString = (value) => isString(value) && value.length > 0;
|
|
2399
|
-
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
2400
|
-
const hasOnlyKeys = (object, allowed, label) => {
|
|
2401
|
-
const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
|
|
2402
|
-
assert(unknown.length === 0, label + ': unknown keys: ' + unknown.join(', '));
|
|
2403
|
-
};
|
|
2404
|
-
const requireKeys = (object, required, label) => {
|
|
2405
|
-
for (const key of required) {
|
|
2406
|
-
assert(Object.prototype.hasOwnProperty.call(object, key), label + ': missing required field: ' + key);
|
|
2407
|
-
}
|
|
2408
|
-
};
|
|
2409
|
-
const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
2410
|
-
const sameStringArrays = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
|
|
2411
|
-
|
|
2412
|
-
for (const file of ['.agent/profile.json', '.agent/state.json', '.agent/plan.json', '.agent/active-slice.json', '.agent/verification-evidence.json']) {
|
|
2413
|
-
readJson(file);
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
const profile = readJson('.agent/profile.json');
|
|
2417
|
-
const state = readJson('.agent/state.json');
|
|
2418
|
-
const plan = readJson('.agent/plan.json');
|
|
2419
|
-
const active = readJson('.agent/active-slice.json');
|
|
2420
|
-
const evidence = readJson('.agent/verification-evidence.json');
|
|
2421
|
-
|
|
2422
|
-
assert(isObject(profile), '.agent/profile.json must be an object');
|
|
2423
|
-
assert(isObject(state), '.agent/state.json must be an object');
|
|
2424
|
-
assert(isObject(plan), '.agent/plan.json must be an object');
|
|
2425
|
-
assert(isObject(active), '.agent/active-slice.json must be an object');
|
|
2426
|
-
assert(isObject(evidence), '.agent/verification-evidence.json must be an object');
|
|
2427
|
-
|
|
2428
|
-
const requiredProfile = ['schema_version', 'protocol_id', 'project_name', 'required_stop_judges', 'priority_policy_id', 'task_type', 'evaluation_profile', 'docs_surfaces'];
|
|
2429
|
-
requireKeys(profile, requiredProfile, '.agent/profile.json');
|
|
2430
|
-
hasOnlyKeys(profile, requiredProfile, '.agent/profile.json');
|
|
2431
|
-
assert(profile.protocol_id === 'completion', '.agent/profile.json: protocol_id must be completion');
|
|
2432
|
-
assert(Array.isArray(profile.docs_surfaces), '.agent/profile.json: docs_surfaces must be an array');
|
|
2433
|
-
assert(isNonEmptyString(profile.task_type), '.agent/profile.json: task_type must be a non-empty string');
|
|
2434
|
-
assert(isNonEmptyString(profile.evaluation_profile), '.agent/profile.json: evaluation_profile must be a non-empty string');
|
|
2435
|
-
|
|
2436
|
-
const requiredState = [
|
|
2437
|
-
'schema_version','mission_anchor','task_type','evaluation_profile','current_phase','continuation_policy','continuation_reason','project_done',
|
|
2438
|
-
'requires_reground','slices_since_last_reground','remaining_release_blockers','remaining_high_value_gaps',
|
|
2439
|
-
'unsatisfied_contract_ids','release_blocker_ids','next_mandatory_action','next_mandatory_role',
|
|
2440
|
-
'remaining_stop_judges','last_reground_at','last_auditor_verdict','contract_status','latest_completed_slice','latest_verified_slice'
|
|
2441
|
-
];
|
|
2442
|
-
const continuationPolicies = ['continue', 'await_user_input', 'blocked', 'paused', 'done'];
|
|
2443
|
-
const workflowRoles = ['completion-bootstrapper', 'completion-regrounder', 'completion-implementer', 'completion-reviewer', 'completion-auditor', 'completion-stop-judge', null];
|
|
2444
|
-
const workflowPhases = ['reground', 'implement', 'post_commit_review', 'post_commit_audit', 'post_commit_reconcile', 'stop_wave', 'awaiting_user', 'blocked', 'done'];
|
|
2445
|
-
requireKeys(state, requiredState, '.agent/state.json');
|
|
2446
|
-
hasOnlyKeys(state, requiredState, '.agent/state.json');
|
|
2447
|
-
assert(continuationPolicies.includes(state.continuation_policy), '.agent/state.json: invalid continuation_policy');
|
|
2448
|
-
assert(workflowRoles.includes(state.next_mandatory_role), '.agent/state.json: invalid next_mandatory_role');
|
|
2449
|
-
assert(workflowPhases.includes(state.current_phase), '.agent/state.json: invalid current_phase');
|
|
2450
|
-
assert(isNonEmptyString(state.task_type), '.agent/state.json: task_type must be a non-empty string');
|
|
2451
|
-
assert(isNonEmptyString(state.evaluation_profile), '.agent/state.json: evaluation_profile must be a non-empty string');
|
|
2452
|
-
assert(isStringArray(state.unsatisfied_contract_ids), '.agent/state.json: unsatisfied_contract_ids must be an array of strings');
|
|
2453
|
-
assert(isStringArray(state.release_blocker_ids), '.agent/state.json: release_blocker_ids must be an array of strings');
|
|
2454
|
-
|
|
2455
|
-
const requiredPlan = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'last_reground_at', 'plan_basis', 'candidate_slices'];
|
|
2456
|
-
const requiredSlice = ['slice_id', 'goal', 'acceptance_criteria', 'contract_ids', 'priority', 'status', 'why_now', 'blocked_on', 'evidence'];
|
|
2457
|
-
const planMirrorFields = ['locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
|
|
2458
|
-
const allowedSlice = [...requiredSlice, ...planMirrorFields];
|
|
2459
|
-
const sliceStatuses = ['planned', 'selected', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
2460
|
-
requireKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2461
|
-
hasOnlyKeys(plan, requiredPlan, '.agent/plan.json');
|
|
2462
|
-
assert(isNonEmptyString(plan.task_type), '.agent/plan.json: task_type must be a non-empty string');
|
|
2463
|
-
assert(isNonEmptyString(plan.evaluation_profile), '.agent/plan.json: evaluation_profile must be a non-empty string');
|
|
2464
|
-
assert(Array.isArray(plan.candidate_slices), '.agent/plan.json: candidate_slices must be an array');
|
|
2465
|
-
for (const [index, slice] of plan.candidate_slices.entries()) {
|
|
2466
|
-
const label = '.agent/plan.json candidate_slices[' + index + ']';
|
|
2467
|
-
assert(isObject(slice), label + ' must be an object');
|
|
2468
|
-
requireKeys(slice, requiredSlice, label);
|
|
2469
|
-
hasOnlyKeys(slice, allowedSlice, label);
|
|
2470
|
-
assert(isString(slice.slice_id) && slice.slice_id.length > 0, label + ': slice_id must be a non-empty string');
|
|
2471
|
-
assert(isString(slice.goal) && slice.goal.length > 0, label + ': goal must be a non-empty string');
|
|
2472
|
-
assert(Array.isArray(slice.acceptance_criteria) && slice.acceptance_criteria.length > 0 && slice.acceptance_criteria.every((item) => typeof item === 'string' && item.length > 0), label + ': acceptance_criteria must be a non-empty array of strings');
|
|
2473
|
-
assert(isStringArray(slice.contract_ids), label + ': contract_ids must be an array of strings');
|
|
2474
|
-
assert(typeof slice.priority === 'number' && Number.isFinite(slice.priority), label + ': priority must be a finite number');
|
|
2475
|
-
assert(sliceStatuses.includes(slice.status), label + ': invalid status');
|
|
2476
|
-
assert(isString(slice.why_now) && slice.why_now.length > 0, label + ': why_now must be a non-empty string');
|
|
2477
|
-
assert(isStringArray(slice.blocked_on), label + ': blocked_on must be an array of strings');
|
|
2478
|
-
assert(isStringArray(slice.evidence), label + ': evidence must be an array of strings');
|
|
2479
|
-
if (hasOwn(slice, 'locked_notes')) assert(isStringArray(slice.locked_notes), label + ': locked_notes must be an array of strings when present');
|
|
2480
|
-
if (hasOwn(slice, 'must_fix_findings')) assert(isStringArray(slice.must_fix_findings), label + ': must_fix_findings must be an array of strings when present');
|
|
2481
|
-
if (hasOwn(slice, 'implementation_surfaces')) assert(isStringArray(slice.implementation_surfaces), label + ': implementation_surfaces must be an array of strings when present');
|
|
2482
|
-
if (hasOwn(slice, 'verification_commands')) assert(isStringArray(slice.verification_commands), label + ': verification_commands must be an array of strings when present');
|
|
2483
|
-
if (hasOwn(slice, 'basis_commit')) assert(isNonEmptyString(slice.basis_commit), label + ': basis_commit must be a non-empty string when present');
|
|
2484
|
-
if (hasOwn(slice, 'remaining_contract_ids_before')) assert(isStringArray(slice.remaining_contract_ids_before), label + ': remaining_contract_ids_before must be an array of strings when present');
|
|
2485
|
-
if (hasOwn(slice, 'release_blocker_count_before')) assert(typeof slice.release_blocker_count_before === 'number' && Number.isFinite(slice.release_blocker_count_before), label + ': release_blocker_count_before must be a finite number when present');
|
|
2486
|
-
if (hasOwn(slice, 'high_value_gap_count_before')) assert(typeof slice.high_value_gap_count_before === 'number' && Number.isFinite(slice.high_value_gap_count_before), label + ': high_value_gap_count_before must be a finite number when present');
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
const isNonEmptyStringArray = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => isNonEmptyString(item));
|
|
2490
|
-
const requiredActiveBase = ['schema_version', 'mission_anchor', 'task_type', 'evaluation_profile', 'status', 'slice_id', 'goal', 'contract_ids', 'acceptance_criteria', 'blocked_on', 'locked_notes', 'must_fix_findings', 'implementation_surfaces', 'verification_commands', 'basis_commit', 'remaining_contract_ids_before', 'release_blocker_count_before', 'high_value_gap_count_before'];
|
|
2491
|
-
const allowedActive = [...requiredActiveBase, 'priority', 'why_now'];
|
|
2492
|
-
const activeStatuses = ['idle', 'selected', 'in_progress', 'committed', 'done'];
|
|
2493
|
-
requireKeys(active, requiredActiveBase, '.agent/active-slice.json');
|
|
2494
|
-
hasOnlyKeys(active, allowedActive, '.agent/active-slice.json');
|
|
2495
|
-
assert(activeStatuses.includes(active.status), '.agent/active-slice.json: invalid status');
|
|
2496
|
-
assert(isNonEmptyString(active.task_type), '.agent/active-slice.json: task_type must be a non-empty string');
|
|
2497
|
-
assert(isNonEmptyString(active.evaluation_profile), '.agent/active-slice.json: evaluation_profile must be a non-empty string');
|
|
2498
|
-
assert(isStringArray(active.contract_ids), '.agent/active-slice.json: contract_ids must be an array of strings');
|
|
2499
|
-
assert(Array.isArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be an array');
|
|
2500
|
-
assert(isStringArray(active.blocked_on), '.agent/active-slice.json: blocked_on must be an array of strings');
|
|
2501
|
-
assert(isStringArray(active.locked_notes), '.agent/active-slice.json: locked_notes must be an array of strings');
|
|
2502
|
-
assert(isStringArray(active.must_fix_findings), '.agent/active-slice.json: must_fix_findings must be an array of strings');
|
|
2503
|
-
assert(isStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be an array of strings');
|
|
2504
|
-
assert(isStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be an array of strings');
|
|
2505
|
-
assert(isStringArray(active.remaining_contract_ids_before), '.agent/active-slice.json: remaining_contract_ids_before must be an array of strings');
|
|
2506
|
-
|
|
2507
|
-
const requiredEvidence = ['schema_version', 'artifact_type', 'subject_type', 'slice_id', 'goal', 'contract_ids', 'basis_commit', 'head_sha', 'verification_commands', 'outcome', 'recorded_at', 'summary'];
|
|
2508
|
-
const evidenceSubjectTypes = ['none', 'selected_slice', 'current_head'];
|
|
2509
|
-
const evidenceOutcomes = ['not_recorded', 'passed', 'failed'];
|
|
2510
|
-
requireKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
|
|
2511
|
-
hasOnlyKeys(evidence, requiredEvidence, '.agent/verification-evidence.json');
|
|
2512
|
-
assert(evidence.artifact_type === 'completion-verification-evidence', '.agent/verification-evidence.json: artifact_type must be completion-verification-evidence');
|
|
2513
|
-
assert(evidenceSubjectTypes.includes(evidence.subject_type), '.agent/verification-evidence.json: invalid subject_type');
|
|
2514
|
-
assert(evidence.slice_id === null || isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be null or a non-empty string');
|
|
2515
|
-
assert(evidence.goal === null || isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be null or a non-empty string');
|
|
2516
|
-
assert(isStringArray(evidence.contract_ids), '.agent/verification-evidence.json: contract_ids must be an array of strings');
|
|
2517
|
-
assert(evidence.basis_commit === null || isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be null or a non-empty string');
|
|
2518
|
-
assert(evidence.head_sha === null || isNonEmptyString(evidence.head_sha), '.agent/verification-evidence.json: head_sha must be null or a non-empty string');
|
|
2519
|
-
assert(isStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be an array of strings');
|
|
2520
|
-
assert(evidenceOutcomes.includes(evidence.outcome), '.agent/verification-evidence.json: invalid outcome');
|
|
2521
|
-
assert(evidence.recorded_at === null || (isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at))), '.agent/verification-evidence.json: recorded_at must be null or an ISO-8601 string');
|
|
2522
|
-
assert(isNonEmptyString(evidence.summary), '.agent/verification-evidence.json: summary must be a non-empty string');
|
|
2523
|
-
|
|
2524
|
-
assert(state.task_type === profile.task_type, '.agent/state.json: task_type must match .agent/profile.json');
|
|
2525
|
-
assert(plan.task_type === profile.task_type, '.agent/plan.json: task_type must match .agent/profile.json');
|
|
2526
|
-
assert(active.task_type === profile.task_type, '.agent/active-slice.json: task_type must match .agent/profile.json');
|
|
2527
|
-
assert(state.evaluation_profile === profile.evaluation_profile, '.agent/state.json: evaluation_profile must match .agent/profile.json');
|
|
2528
|
-
assert(plan.evaluation_profile === profile.evaluation_profile, '.agent/plan.json: evaluation_profile must match .agent/profile.json');
|
|
2529
|
-
assert(active.evaluation_profile === profile.evaluation_profile, '.agent/active-slice.json: evaluation_profile must match .agent/profile.json');
|
|
2530
|
-
|
|
2531
|
-
const requiresExactHandoff = ['selected', 'in_progress', 'committed', 'done'].includes(active.status);
|
|
2532
|
-
if (requiresExactHandoff) {
|
|
2533
|
-
assert(isNonEmptyStringArray(active.acceptance_criteria), '.agent/active-slice.json: acceptance_criteria must be a non-empty array of strings when status carries an exact handoff');
|
|
2534
|
-
assert(typeof active.priority === 'number' && Number.isFinite(active.priority), '.agent/active-slice.json: priority must be a finite number when status carries an exact handoff');
|
|
2535
|
-
assert(isString(active.why_now) && active.why_now.length > 0, '.agent/active-slice.json: why_now must be a non-empty string when status carries an exact handoff');
|
|
2536
|
-
assert(isNonEmptyStringArray(active.implementation_surfaces), '.agent/active-slice.json: implementation_surfaces must be a non-empty array of strings when status carries an exact handoff');
|
|
2537
|
-
assert(isNonEmptyStringArray(active.verification_commands), '.agent/active-slice.json: verification_commands must be a non-empty array of strings when status carries an exact handoff');
|
|
2538
|
-
assert(isString(active.basis_commit) && active.basis_commit.length > 0, '.agent/active-slice.json: basis_commit must be a non-empty string when status carries an exact handoff');
|
|
2539
|
-
assert(typeof active.release_blocker_count_before === 'number' && Number.isFinite(active.release_blocker_count_before), '.agent/active-slice.json: release_blocker_count_before must be a finite number when status carries an exact handoff');
|
|
2540
|
-
assert(typeof active.high_value_gap_count_before === 'number' && Number.isFinite(active.high_value_gap_count_before), '.agent/active-slice.json: high_value_gap_count_before must be a finite number when status carries an exact handoff');
|
|
2541
|
-
|
|
2542
|
-
const planSlice = plan.candidate_slices.find((slice) => isObject(slice) && slice.slice_id === active.slice_id);
|
|
2543
|
-
assert(isObject(planSlice), '.agent/active-slice.json: slice_id must match a slice in .agent/plan.json when status carries an exact handoff');
|
|
2544
|
-
const drift = [];
|
|
2545
|
-
if (planSlice.goal !== active.goal) drift.push('goal');
|
|
2546
|
-
if (!sameStringArrays(planSlice.contract_ids, active.contract_ids)) drift.push('contract_ids');
|
|
2547
|
-
if (!sameStringArrays(planSlice.acceptance_criteria, active.acceptance_criteria)) drift.push('acceptance_criteria');
|
|
2548
|
-
if (!sameStringArrays(planSlice.blocked_on, active.blocked_on)) drift.push('blocked_on');
|
|
2549
|
-
if (planSlice.priority !== active.priority) drift.push('priority');
|
|
2550
|
-
if (planSlice.why_now !== active.why_now) drift.push('why_now');
|
|
2551
|
-
|
|
2552
|
-
const expectPlanArrayMirror = (field) => {
|
|
2553
|
-
if (!hasOwn(planSlice, field) || !sameStringArrays(planSlice[field], active[field])) drift.push(field);
|
|
2554
|
-
};
|
|
2555
|
-
const expectPlanStringMirror = (field) => {
|
|
2556
|
-
if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
|
|
2557
|
-
};
|
|
2558
|
-
const expectPlanNumberMirror = (field) => {
|
|
2559
|
-
if (!hasOwn(planSlice, field) || planSlice[field] !== active[field]) drift.push(field);
|
|
2560
|
-
};
|
|
2561
|
-
|
|
2562
|
-
expectPlanArrayMirror('implementation_surfaces');
|
|
2563
|
-
expectPlanArrayMirror('verification_commands');
|
|
2564
|
-
expectPlanArrayMirror('locked_notes');
|
|
2565
|
-
expectPlanArrayMirror('must_fix_findings');
|
|
2566
|
-
expectPlanStringMirror('basis_commit');
|
|
2567
|
-
expectPlanArrayMirror('remaining_contract_ids_before');
|
|
2568
|
-
expectPlanNumberMirror('release_blocker_count_before');
|
|
2569
|
-
expectPlanNumberMirror('high_value_gap_count_before');
|
|
2570
|
-
assert(drift.length === 0, '.agent/active-slice.json must match the selected .agent/plan.json slice across: ' + Array.from(new Set(drift)).join(', '));
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
const currentHead = (() => {
|
|
2574
|
-
try {
|
|
2575
|
-
return childProcess.execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
2576
|
-
} catch {
|
|
2577
|
-
return null;
|
|
2578
|
-
}
|
|
2579
|
-
})();
|
|
2580
|
-
|
|
2581
|
-
if (requiresExactHandoff) {
|
|
2582
|
-
assert(evidence.subject_type === 'selected_slice', '.agent/verification-evidence.json: subject_type must be selected_slice when active slice exact handoff requires verification evidence');
|
|
2583
|
-
assert(evidence.slice_id === active.slice_id, '.agent/verification-evidence.json: slice_id must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
|
|
2584
|
-
assert(evidence.goal === active.goal, '.agent/verification-evidence.json: goal must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
|
|
2585
|
-
assert(sameStringArrays(evidence.contract_ids, active.contract_ids), '.agent/verification-evidence.json: contract_ids must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
|
|
2586
|
-
assert(evidence.basis_commit === active.basis_commit, '.agent/verification-evidence.json: basis_commit must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
|
|
2587
|
-
assert(sameStringArrays(evidence.verification_commands, active.verification_commands), '.agent/verification-evidence.json: verification_commands must match .agent/active-slice.json when active slice exact handoff requires verification evidence');
|
|
2588
|
-
assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when active slice exact handoff requires verification evidence');
|
|
2589
|
-
assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when active slice exact handoff requires verification evidence');
|
|
2590
|
-
if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when active slice exact handoff requires verification evidence');
|
|
2591
|
-
} else if (evidence.subject_type === 'none') {
|
|
2592
|
-
assert(evidence.slice_id === null, '.agent/verification-evidence.json: slice_id must be null when subject_type is none');
|
|
2593
|
-
assert(evidence.goal === null, '.agent/verification-evidence.json: goal must be null when subject_type is none');
|
|
2594
|
-
assert(evidence.contract_ids.length === 0, '.agent/verification-evidence.json: contract_ids must be empty when subject_type is none');
|
|
2595
|
-
assert(evidence.basis_commit === null, '.agent/verification-evidence.json: basis_commit must be null when subject_type is none');
|
|
2596
|
-
assert(evidence.head_sha === null, '.agent/verification-evidence.json: head_sha must be null when subject_type is none');
|
|
2597
|
-
assert(evidence.verification_commands.length === 0, '.agent/verification-evidence.json: verification_commands must be empty when subject_type is none');
|
|
2598
|
-
assert(evidence.outcome === 'not_recorded', '.agent/verification-evidence.json: outcome must be not_recorded when subject_type is none');
|
|
2599
|
-
assert(evidence.recorded_at === null, '.agent/verification-evidence.json: recorded_at must be null when subject_type is none');
|
|
2600
|
-
} else {
|
|
2601
|
-
assert(evidence.outcome === 'passed', '.agent/verification-evidence.json: outcome must be passed when verification evidence is recorded');
|
|
2602
|
-
assert(isNonEmptyStringArray(evidence.verification_commands), '.agent/verification-evidence.json: verification_commands must be a non-empty array when verification evidence is recorded');
|
|
2603
|
-
assert(isNonEmptyString(evidence.recorded_at) && !Number.isNaN(Date.parse(evidence.recorded_at)), '.agent/verification-evidence.json: recorded_at must be an ISO-8601 string when verification evidence is recorded');
|
|
2604
|
-
if (currentHead) assert(evidence.head_sha === currentHead, '.agent/verification-evidence.json: head_sha must match current git HEAD when verification evidence is recorded');
|
|
2605
|
-
if (evidence.subject_type === 'selected_slice') {
|
|
2606
|
-
assert(isNonEmptyString(evidence.slice_id), '.agent/verification-evidence.json: slice_id must be a non-empty string when subject_type is selected_slice');
|
|
2607
|
-
assert(isNonEmptyString(evidence.goal), '.agent/verification-evidence.json: goal must be a non-empty string when subject_type is selected_slice');
|
|
2608
|
-
assert(isNonEmptyString(evidence.basis_commit), '.agent/verification-evidence.json: basis_commit must be a non-empty string when subject_type is selected_slice');
|
|
2609
|
-
} else {
|
|
2610
|
-
assert(evidence.subject_type === 'current_head', '.agent/verification-evidence.json: only current_head or selected_slice may carry recorded verification evidence');
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
if (!requiresExactHandoff) {
|
|
2615
|
-
assert(active.priority === null || active.priority === undefined || (typeof active.priority === 'number' && Number.isFinite(active.priority)), '.agent/active-slice.json: idle priority must be null/undefined or a finite number');
|
|
2616
|
-
assert(active.why_now === null || active.why_now === undefined || typeof active.why_now === 'string', '.agent/active-slice.json: idle why_now must be null/undefined or a string');
|
|
2617
|
-
}
|
|
2618
|
-
NODE
|
|
2619
|
-
`;
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
async function ensureGitignore(root: string): Promise<boolean> {
|
|
2623
|
-
const gitignorePath = path.join(root, ".gitignore");
|
|
2624
|
-
const blockLines = [
|
|
2625
|
-
"# completion protocol",
|
|
2626
|
-
".agent/*",
|
|
2627
|
-
"!.agent/README.md",
|
|
2628
|
-
"!.agent/mission.md",
|
|
2629
|
-
"!.agent/profile.json",
|
|
2630
|
-
"!.agent/verify_completion_stop.sh",
|
|
2631
|
-
"!.agent/verify_completion_control_plane.sh",
|
|
2632
|
-
".agent/tmp/",
|
|
2633
|
-
];
|
|
2634
|
-
const block = blockLines.join("\n");
|
|
2635
|
-
const existing = (await pathExists(gitignorePath)) ? await fsp.readFile(gitignorePath, "utf8") : "";
|
|
2636
|
-
const filteredLines = existing
|
|
2637
|
-
.split(/\r?\n/)
|
|
2638
|
-
.filter((line) => !blockLines.includes(line.trim()));
|
|
2639
|
-
while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1]?.trim() === "") {
|
|
2640
|
-
filteredLines.pop();
|
|
2641
|
-
}
|
|
2642
|
-
const base = filteredLines.join("\n").trimEnd();
|
|
2643
|
-
const content = base.length > 0 ? `${base}\n\n${block}\n` : `${block}\n`;
|
|
2644
|
-
if (content === existing) return false;
|
|
2645
|
-
await fsp.writeFile(gitignorePath, content, "utf8");
|
|
2646
|
-
return true;
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
type ScaffoldResult = {
|
|
2650
|
-
root: string;
|
|
2651
|
-
created: string[];
|
|
2652
|
-
updated: string[];
|
|
2653
|
-
missionAnchor: string;
|
|
2654
|
-
};
|
|
2655
417
|
|
|
2656
418
|
async function scaffoldCompletionFiles(
|
|
2657
419
|
root: string,
|
|
2658
420
|
missionAnchor: string,
|
|
2659
421
|
options?: { analysis?: ContextProposalAnalysis; continuationReason?: string },
|
|
2660
|
-
)
|
|
2661
|
-
const files = resolveFiles(root);
|
|
2662
|
-
const created: string[] = [];
|
|
2663
|
-
const updated: string[] = [];
|
|
2664
|
-
await fsp.mkdir(files.agentDir, { recursive: true });
|
|
2665
|
-
await fsp.mkdir(path.join(files.agentDir, "tmp"), { recursive: true });
|
|
2666
|
-
const projectName = path.basename(root);
|
|
422
|
+
) {
|
|
2667
423
|
const routing = finalizeContextProposalAnalysis(options?.analysis, [missionAnchor]);
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
{ path: path.join(files.agentDir, "mission.md"), content: buildMission(projectName, missionAnchor) },
|
|
2673
|
-
{
|
|
2674
|
-
path: files.profilePath,
|
|
2675
|
-
content: `${JSON.stringify(buildProfileRecord({ projectName, requiredStopJudges: 3, docsSurfaces, taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n`,
|
|
2676
|
-
},
|
|
2677
|
-
{ path: path.join(files.agentDir, "verify_completion_stop.sh"), content: buildVerifyStopScript(verifierCommand), executable: true },
|
|
2678
|
-
{ path: path.join(files.agentDir, "verify_completion_control_plane.sh"), content: buildVerifyControlPlaneScript(), executable: true },
|
|
2679
|
-
{
|
|
2680
|
-
path: files.statePath,
|
|
2681
|
-
content: `${JSON.stringify(defaultState(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile, continuationReason: options?.continuationReason }), null, 2)}\n`,
|
|
2682
|
-
},
|
|
2683
|
-
{ path: files.planPath, content: `${JSON.stringify(defaultPlan(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
2684
|
-
{ path: files.activePath, content: `${JSON.stringify(defaultActiveSlice(missionAnchor, { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile }), null, 2)}\n` },
|
|
2685
|
-
{ path: files.verificationEvidencePath, content: `${JSON.stringify(defaultVerificationEvidence(), null, 2)}\n` },
|
|
2686
|
-
{ path: files.sliceHistoryPath, content: "" },
|
|
2687
|
-
{ path: files.stopHistoryPath, content: "" },
|
|
2688
|
-
];
|
|
2689
|
-
for (const file of trackedFiles) {
|
|
2690
|
-
if (await pathExists(file.path)) continue;
|
|
2691
|
-
await fsp.writeFile(file.path, file.content, "utf8");
|
|
2692
|
-
if (file.executable) await fsp.chmod(file.path, 0o755);
|
|
2693
|
-
created.push(path.relative(root, file.path));
|
|
2694
|
-
}
|
|
2695
|
-
if (await ensureGitignore(root)) updated.push(".gitignore");
|
|
2696
|
-
return { root, created, updated, missionAnchor };
|
|
424
|
+
return await scaffoldCompletionFilesOnDisk(root, missionAnchor, {
|
|
425
|
+
analysis: { taskType: routing.taskType, evaluationProfile: routing.evaluationProfile },
|
|
426
|
+
continuationReason: options?.continuationReason,
|
|
427
|
+
});
|
|
2697
428
|
}
|
|
2698
429
|
|
|
2699
430
|
function remainingSliceCount(plan: JsonRecord | undefined): number {
|
|
@@ -2773,37 +504,109 @@ function activeSliceContractDriftSummary(snapshot: CompletionStateSnapshot): str
|
|
|
2773
504
|
return drift && drift.length > 0 ? drift.join(", ") : "none";
|
|
2774
505
|
}
|
|
2775
506
|
|
|
2776
|
-
function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
|
|
2777
|
-
const activeId = asString(snapshot.active?.slice_id);
|
|
2778
|
-
if (!activeId) return "unknown";
|
|
2779
|
-
const drift = activeSliceContractDriftFields(snapshot);
|
|
2780
|
-
if (!snapshot.activeSlice || drift === undefined) return "no";
|
|
2781
|
-
return drift.length === 0 ? "yes" : "no";
|
|
507
|
+
function activeSliceMatchesPlan(snapshot: CompletionStateSnapshot): "yes" | "no" | "unknown" {
|
|
508
|
+
const activeId = asString(snapshot.active?.slice_id);
|
|
509
|
+
if (!activeId) return "unknown";
|
|
510
|
+
const drift = activeSliceContractDriftFields(snapshot);
|
|
511
|
+
if (!snapshot.activeSlice || drift === undefined) return "no";
|
|
512
|
+
return drift.length === 0 ? "yes" : "no";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function handoffSnapshotState(active: JsonRecord | undefined): "present" | "missing_or_unclear" {
|
|
516
|
+
const exactArrays = [
|
|
517
|
+
asStringArray(active?.acceptance_criteria),
|
|
518
|
+
asStringArray(active?.implementation_surfaces),
|
|
519
|
+
asStringArray(active?.verification_commands),
|
|
520
|
+
];
|
|
521
|
+
const required = [
|
|
522
|
+
active?.priority,
|
|
523
|
+
active?.why_now,
|
|
524
|
+
active?.blocked_on,
|
|
525
|
+
active?.locked_notes,
|
|
526
|
+
active?.must_fix_findings,
|
|
527
|
+
active?.basis_commit,
|
|
528
|
+
active?.remaining_contract_ids_before,
|
|
529
|
+
active?.release_blocker_count_before,
|
|
530
|
+
active?.high_value_gap_count_before,
|
|
531
|
+
];
|
|
532
|
+
return activeCarriesExactHandoff(active) && exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
|
|
533
|
+
? "present"
|
|
534
|
+
: "missing_or_unclear";
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function hasRunningCompletionRole(rootKey: string): boolean {
|
|
538
|
+
return liveRoleActivityByRoot.get(rootKey)?.status === "running";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isRubricEvaluationRole(role: string | undefined): role is RubricEvaluationRole {
|
|
542
|
+
return RUBRIC_EVALUATION_ROLES.includes(role as RubricEvaluationRole);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function activeSliceContext(snapshot: CompletionStateSnapshot) {
|
|
546
|
+
const active = snapshot.active;
|
|
547
|
+
const activeSlice = snapshot.activeSlice;
|
|
548
|
+
return {
|
|
549
|
+
sliceId: asString(active?.slice_id) ?? asString(activeSlice?.slice_id),
|
|
550
|
+
status: asString(active?.status) ?? asString(activeSlice?.status),
|
|
551
|
+
goal: asString(active?.goal) ?? asString(activeSlice?.goal),
|
|
552
|
+
contractIds:
|
|
553
|
+
asStringArray(active?.contract_ids).length > 0 ? asStringArray(active?.contract_ids) : asStringArray(activeSlice?.contract_ids),
|
|
554
|
+
acceptance:
|
|
555
|
+
asStringArray(active?.acceptance_criteria).length > 0
|
|
556
|
+
? asStringArray(active?.acceptance_criteria)
|
|
557
|
+
: asStringArray(activeSlice?.acceptance_criteria),
|
|
558
|
+
implementationSurfaces: asStringArray(active?.implementation_surfaces),
|
|
559
|
+
verificationCommands: asStringArray(active?.verification_commands),
|
|
560
|
+
lockedNotes: asStringArray(active?.locked_notes),
|
|
561
|
+
mustFixFindings: asStringArray(active?.must_fix_findings),
|
|
562
|
+
remainingBefore: asStringArray(active?.remaining_contract_ids_before),
|
|
563
|
+
basisCommit: asString(active?.basis_commit),
|
|
564
|
+
releaseBlockerCountBefore: asNumber(active?.release_blocker_count_before),
|
|
565
|
+
highValueGapCountBefore: asNumber(active?.high_value_gap_count_before),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function verificationEvidenceContext(snapshot: CompletionStateSnapshot) {
|
|
570
|
+
const evidence = snapshot.verificationEvidence;
|
|
571
|
+
return {
|
|
572
|
+
path: path.relative(snapshot.files.root, snapshot.files.verificationEvidencePath) || ".agent/verification-evidence.json",
|
|
573
|
+
status: evidence ? "present" : "missing",
|
|
574
|
+
subjectType: asString(evidence?.subject_type),
|
|
575
|
+
sliceId: asString(evidence?.slice_id),
|
|
576
|
+
goal: asString(evidence?.goal),
|
|
577
|
+
contractIds: asStringArray(evidence?.contract_ids),
|
|
578
|
+
basisCommit: asString(evidence?.basis_commit),
|
|
579
|
+
headSha: asString(evidence?.head_sha),
|
|
580
|
+
verificationCommands: asStringArray(evidence?.verification_commands),
|
|
581
|
+
outcome: asString(evidence?.outcome),
|
|
582
|
+
recordedAt: asString(evidence?.recorded_at),
|
|
583
|
+
summary:
|
|
584
|
+
asString(evidence?.summary) ??
|
|
585
|
+
(evidence ? "Canonical verification evidence is present but its summary is missing." : "Canonical verification evidence is missing."),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function buildEvaluationRoleContextLines(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string[] {
|
|
590
|
+
return buildExtractedEvaluationRoleContextLines(snapshot, role, {
|
|
591
|
+
asString,
|
|
592
|
+
currentTaskType,
|
|
593
|
+
currentEvaluationProfile,
|
|
594
|
+
activeSliceContext,
|
|
595
|
+
verificationEvidenceContext,
|
|
596
|
+
});
|
|
2782
597
|
}
|
|
2783
598
|
|
|
2784
|
-
function
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
active?.why_now,
|
|
2793
|
-
active?.blocked_on,
|
|
2794
|
-
active?.locked_notes,
|
|
2795
|
-
active?.must_fix_findings,
|
|
2796
|
-
active?.basis_commit,
|
|
2797
|
-
active?.remaining_contract_ids_before,
|
|
2798
|
-
active?.release_blocker_count_before,
|
|
2799
|
-
active?.high_value_gap_count_before,
|
|
2800
|
-
];
|
|
2801
|
-
return activeCarriesExactHandoff(active) && exactArrays.every((items) => items.length > 0) && required.every((value) => value !== undefined && value !== null)
|
|
2802
|
-
? "present"
|
|
2803
|
-
: "missing_or_unclear";
|
|
599
|
+
function buildEvaluationRoleReminderText(snapshot: CompletionStateSnapshot, role: RubricEvaluationRole): string {
|
|
600
|
+
return buildExtractedEvaluationRoleReminderText(snapshot, role, {
|
|
601
|
+
asString,
|
|
602
|
+
currentTaskType,
|
|
603
|
+
currentEvaluationProfile,
|
|
604
|
+
activeSliceContext,
|
|
605
|
+
verificationEvidenceContext,
|
|
606
|
+
});
|
|
2804
607
|
}
|
|
2805
608
|
|
|
2806
|
-
function
|
|
609
|
+
function composeSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
|
|
2807
610
|
const history = historyCounts(sliceHistory, stopHistory);
|
|
2808
611
|
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2809
612
|
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
@@ -2813,42 +616,37 @@ function buildSystemReminder(snapshot: CompletionStateSnapshot, sliceHistory: Js
|
|
|
2813
616
|
const exactActiveContract = activeCarriesExactHandoff(snapshot.active);
|
|
2814
617
|
const activeContractDrift = activeSliceContractDriftSummary(snapshot);
|
|
2815
618
|
const evidence = verificationEvidenceContext(snapshot);
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
`
|
|
2820
|
-
|
|
2821
|
-
`
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
if (evidence.recordedAt) lines.push(`Verification evidence recorded_at: ${evidence.recordedAt}`);
|
|
2848
|
-
if (evidence.verificationCommands.length > 0) lines.push(`Verification evidence commands: ${evidence.verificationCommands.join(" | ")}`);
|
|
2849
|
-
lines.push(`Verification evidence summary: ${evidence.summary}`);
|
|
2850
|
-
if (isRubricEvaluationRole(nextRole)) lines.push(buildEvaluationRoleReminderText(snapshot, nextRole));
|
|
2851
|
-
return lines.join(" ");
|
|
619
|
+
const activePriorityLine = activePriority !== undefined ? `Active slice priority: ${activePriority}` : undefined;
|
|
620
|
+
const activeWhyNowLine = activeWhyNow ? `Active slice why_now: ${activeWhyNow}` : undefined;
|
|
621
|
+
const implementationSurfacesLine =
|
|
622
|
+
implementationSurfaces.length > 0 ? `Active implementation surfaces: ${implementationSurfaces.join(", ")}` : undefined;
|
|
623
|
+
const verificationCommandsLine =
|
|
624
|
+
verificationCommands.length > 0 ? `Active verification commands: ${verificationCommands.join(" | ")}` : undefined;
|
|
625
|
+
return buildExtractedSystemReminder({
|
|
626
|
+
missionAnchor: asString(snapshot.state?.mission_anchor),
|
|
627
|
+
taskType: currentTaskType(snapshot),
|
|
628
|
+
evaluationProfile: currentEvaluationProfile(snapshot),
|
|
629
|
+
currentPhase: asString(snapshot.state?.current_phase),
|
|
630
|
+
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
631
|
+
continuationReason: asString(snapshot.state?.continuation_reason),
|
|
632
|
+
nextMandatoryRole: nextRole,
|
|
633
|
+
nextMandatoryAction: asString(snapshot.state?.next_mandatory_action),
|
|
634
|
+
remainingSliceCount: remainingSliceCount(snapshot.plan),
|
|
635
|
+
remainingStopJudges: asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)",
|
|
636
|
+
history,
|
|
637
|
+
exactActiveContract,
|
|
638
|
+
activeContractDrift,
|
|
639
|
+
activePriority,
|
|
640
|
+
activeWhyNow,
|
|
641
|
+
implementationSurfaces,
|
|
642
|
+
verificationCommands,
|
|
643
|
+
activePriorityLine,
|
|
644
|
+
activeWhyNowLine,
|
|
645
|
+
implementationSurfacesLine,
|
|
646
|
+
verificationCommandsLine,
|
|
647
|
+
evidence,
|
|
648
|
+
evaluationRoleReminderText: isRubricEvaluationRole(nextRole) ? buildEvaluationRoleReminderText(snapshot, nextRole) : undefined,
|
|
649
|
+
});
|
|
2852
650
|
}
|
|
2853
651
|
|
|
2854
652
|
function buildPostCompactionDriverInstructions(snapshot: CompletionStateSnapshot, marker: JsonRecord | undefined): string {
|
|
@@ -2964,7 +762,7 @@ function emitCommandText(ctx: { hasUI: boolean; ui: any }, text: string, level:
|
|
|
2964
762
|
}
|
|
2965
763
|
}
|
|
2966
764
|
|
|
2967
|
-
function
|
|
765
|
+
function composeResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: JsonRecord[], stopHistory: JsonRecord[]): string {
|
|
2968
766
|
const history = historyCounts(sliceHistory, stopHistory);
|
|
2969
767
|
const acceptance = asStringArray(snapshot.active?.acceptance_criteria).length > 0
|
|
2970
768
|
? asStringArray(snapshot.active?.acceptance_criteria)
|
|
@@ -2980,411 +778,51 @@ function buildResumeCapsule(snapshot: CompletionStateSnapshot, sliceHistory: Jso
|
|
|
2980
778
|
const implementationSurfaces = asStringArray(snapshot.active?.implementation_surfaces);
|
|
2981
779
|
const verificationCommands = asStringArray(snapshot.active?.verification_commands);
|
|
2982
780
|
const remainingBefore = asStringArray(snapshot.active?.remaining_contract_ids_before);
|
|
2983
|
-
const activeContractDrift = activeSliceContractDriftSummary(snapshot);
|
|
2984
781
|
const evidence = verificationEvidenceContext(snapshot);
|
|
2985
|
-
const
|
|
2986
|
-
"
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
if (blockedOn.length > 0) lines.push(`- blocked_on: ${blockedOn.join(", ")}`);
|
|
3027
|
-
if (lockedNotes.length > 0) lines.push(`- locked_notes: ${lockedNotes.join(" | ")}`);
|
|
3028
|
-
if (mustFixFindings.length > 0) lines.push(`- must_fix_findings: ${mustFixFindings.join(" | ")}`);
|
|
3029
|
-
if (implementationSurfaces.length > 0) lines.push(`- implementation_surfaces: ${implementationSurfaces.join(" | ")}`);
|
|
3030
|
-
if (verificationCommands.length > 0) lines.push(`- verification_commands: ${verificationCommands.join(" | ")}`);
|
|
3031
|
-
lines.push(`- basis_commit: ${asString(snapshot.active?.basis_commit) ?? "(none)"}`);
|
|
3032
|
-
lines.push(`- remaining_contract_ids_before: ${remainingBefore.length > 0 ? remainingBefore.join(", ") : "(none)"}`);
|
|
3033
|
-
lines.push(`- release_blocker_count_before: ${asNumber(snapshot.active?.release_blocker_count_before) ?? "(unknown)"}`);
|
|
3034
|
-
lines.push(`- high_value_gap_count_before: ${asNumber(snapshot.active?.high_value_gap_count_before) ?? "(unknown)"}`);
|
|
3035
|
-
lines.push("", "acceptance_criteria:");
|
|
3036
|
-
if (acceptance.length === 0) lines.push("- (none)");
|
|
3037
|
-
else lines.push(...acceptance.map((item) => `- ${item}`));
|
|
3038
|
-
lines.push(
|
|
3039
|
-
"",
|
|
3040
|
-
"Rules:",
|
|
3041
|
-
"- Treat this block as continuity support derived from canonical .agent state.",
|
|
3042
|
-
"- For selected/in-progress/committed/done slices, .agent/active-slice.json is the canonical implementation contract and the selected plan slice must mirror it exactly.",
|
|
3043
|
-
"- Preserve exact slice_id, goal, contract_ids, acceptance criteria, blocked_on, priority, why_now, implementation surfaces, verification commands, locked notes, must-fix findings, basis_commit, and before-slice counters where still true.",
|
|
3044
|
-
"- When populated, .agent/verification-evidence.json is the durable canonical verification record for the selected slice or current HEAD and should be consumed instead of temp-only artifacts or conversational summaries.",
|
|
3045
|
-
"- After compaction, re-read .agent/state.json, .agent/plan.json, .agent/active-slice.json, .agent/slice-history.jsonl, .agent/stop-check-history.jsonl, and .agent/verification-evidence.json before resuming long-running completion work.",
|
|
3046
|
-
"- Invoke completion-regrounder before continuing when requires_reground is true or unknown.",
|
|
3047
|
-
"- Invoke completion-regrounder before continuing when next_mandatory_role or next_mandatory_action is unknown or ambiguous.",
|
|
3048
|
-
"- Invoke completion-regrounder before continuing when active_slice_matches_plan is no, active_slice_contract_drift_fields is not none, or implementer_handoff_snapshot is missing_or_unclear.",
|
|
3049
|
-
"- If continuation_policy is continue, do not stop after a slice or ask whether to continue. Dispatch the next mandatory role directly.",
|
|
3050
|
-
"- Only stop for the user when continuation_policy is await_user_input, blocked, paused, or done.",
|
|
3051
|
-
"- If you are completion-implementer after compaction, resume from the canonical active-slice implementation contract instead of asking the user to resend the original caller payload.",
|
|
3052
|
-
"- Do not replace canonical .agent state with summary inference.",
|
|
3053
|
-
"</completion-state>",
|
|
3054
|
-
);
|
|
3055
|
-
return lines.join("\n");
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
function formatCount(count: number, singular: string, plural = `${singular}s`): string {
|
|
3059
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
function completionRemainingSummary(surface: {
|
|
3063
|
-
remainingContractCount: number;
|
|
3064
|
-
releaseBlockerCount: number;
|
|
3065
|
-
highValueGapCount: number;
|
|
3066
|
-
remainingStopJudgeCount: number;
|
|
3067
|
-
}): string {
|
|
3068
|
-
return [
|
|
3069
|
-
formatCount(surface.remainingContractCount, "contract"),
|
|
3070
|
-
formatCount(surface.releaseBlockerCount, "blocker"),
|
|
3071
|
-
formatCount(surface.highValueGapCount, "gap"),
|
|
3072
|
-
formatCount(surface.remainingStopJudgeCount, "stop judge", "stop judges"),
|
|
3073
|
-
].join(" · ");
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
function envNumber(name: string): number | undefined {
|
|
3077
|
-
const raw = asString(process.env[name]);
|
|
3078
|
-
if (!raw) return undefined;
|
|
3079
|
-
const parsed = Number(raw);
|
|
3080
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
function nowMs(): number {
|
|
3084
|
-
return envNumber("PI_COMPLETION_TEST_NOW") ?? Date.now();
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
type LiveActivitySignal = {
|
|
3088
|
-
state: "active" | "waiting" | "stalled";
|
|
3089
|
-
idleMs: number;
|
|
3090
|
-
};
|
|
3091
|
-
|
|
3092
|
-
function liveActivitySignal(activity: { status?: string; startedAt?: number; updatedAt?: number } | undefined): LiveActivitySignal | undefined {
|
|
3093
|
-
if (!activity || activity.status !== "running") return undefined;
|
|
3094
|
-
const anchor = activity.updatedAt ?? activity.startedAt;
|
|
3095
|
-
if (anchor === undefined) return undefined;
|
|
3096
|
-
const idleMs = Math.max(0, nowMs() - anchor);
|
|
3097
|
-
return {
|
|
3098
|
-
state: idleMs >= LIVE_ROLE_STALLED_MS ? "stalled" : idleMs >= LIVE_ROLE_WAITING_MS ? "waiting" : "active",
|
|
3099
|
-
idleMs,
|
|
3100
|
-
};
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
function formatLiveActivitySignal(signal: LiveActivitySignal | undefined): string | undefined {
|
|
3104
|
-
if (!signal) return undefined;
|
|
3105
|
-
if (signal.state === "active") return "activity: active";
|
|
3106
|
-
return `activity: ${signal.state} (${formatElapsed(signal.idleMs)} since update)`;
|
|
3107
|
-
}
|
|
3108
|
-
|
|
3109
|
-
function livePreviewForStatus(activity: LiveRoleActivity | undefined): string | undefined {
|
|
3110
|
-
if (!activity || activity.status !== "running") return undefined;
|
|
3111
|
-
return truncateInline(
|
|
3112
|
-
activity.progress ?? activity.verifying ?? activity.toolActivity ?? activity.assistantSummary ?? activity.currentAction ?? activity.lastAssistantText ?? "",
|
|
3113
|
-
120,
|
|
3114
|
-
) || undefined;
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
function completionRootKey(snapshot: CompletionStateSnapshot | undefined, cwd: string): string {
|
|
3118
|
-
return snapshot?.files.root ?? findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? path.resolve(cwd);
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
function cloneLiveRoleActivity(activity: LiveRoleActivity, overrides: Partial<LiveRoleActivity> = {}): LiveRoleActivity {
|
|
3122
|
-
return {
|
|
3123
|
-
...activity,
|
|
3124
|
-
...overrides,
|
|
3125
|
-
toolRecentActivity: [...(overrides.toolRecentActivity ?? activity.toolRecentActivity)],
|
|
3126
|
-
recentActivity: [...(overrides.recentActivity ?? activity.recentActivity)],
|
|
3127
|
-
stateDeltas: [...(overrides.stateDeltas ?? activity.stateDeltas)],
|
|
3128
|
-
};
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
function createLiveRoleActivity(role: string, startedAt = nowMs()): LiveRoleActivity {
|
|
3132
|
-
const currentAction = "Starting role subprocess";
|
|
3133
|
-
return {
|
|
3134
|
-
role,
|
|
3135
|
-
status: "running",
|
|
3136
|
-
currentAction,
|
|
3137
|
-
toolActivity: currentAction,
|
|
3138
|
-
toolRecentActivity: [currentAction],
|
|
3139
|
-
recentActivity: [currentAction],
|
|
3140
|
-
stateDeltas: [],
|
|
3141
|
-
startedAt,
|
|
3142
|
-
updatedAt: startedAt,
|
|
3143
|
-
};
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
type RoleMessage = {
|
|
3147
|
-
role: string;
|
|
3148
|
-
content: Array<{ type: string; text?: string }>;
|
|
3149
|
-
};
|
|
3150
|
-
|
|
3151
|
-
function activityTimestampMs(event: JsonRecord | undefined): number | undefined {
|
|
3152
|
-
return asNumber(event?.updatedAt) ?? asNumber(event?.timestampMs) ?? asNumber(event?.timestamp) ?? asNumber(event?.at);
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
function asRoleMessage(value: unknown): RoleMessage | undefined {
|
|
3156
|
-
if (!isRecord(value)) return undefined;
|
|
3157
|
-
const role = asString(value.role);
|
|
3158
|
-
const content = Array.isArray(value.content)
|
|
3159
|
-
? value.content.flatMap((item) => {
|
|
3160
|
-
if (!isRecord(item)) return [];
|
|
3161
|
-
const type = asString(item.type);
|
|
3162
|
-
if (!type) return [];
|
|
3163
|
-
return [{ type, text: asString(item.text) }];
|
|
3164
|
-
})
|
|
3165
|
-
: [];
|
|
3166
|
-
if (!role) return undefined;
|
|
3167
|
-
return { role, content };
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
function applyAssistantTextToLiveRoleActivity(activity: LiveRoleActivity, text: string, activityAt = nowMs()): boolean {
|
|
3171
|
-
if (!text) return false;
|
|
3172
|
-
activity.lastAssistantText = text;
|
|
3173
|
-
const parsed = parseStructuredProgress(text);
|
|
3174
|
-
if (parsed.progress) activity.progress = parsed.progress;
|
|
3175
|
-
if (parsed.rationale) activity.rationale = parsed.rationale;
|
|
3176
|
-
if (parsed.nextStep) activity.nextStep = parsed.nextStep;
|
|
3177
|
-
if (parsed.verifying) activity.verifying = parsed.verifying;
|
|
3178
|
-
if (parsed.stateDeltas.length > 0) activity.stateDeltas = parsed.stateDeltas;
|
|
3179
|
-
const preview = truncateInline(text, 140);
|
|
3180
|
-
activity.assistantSummary = activity.progress ?? activity.verifying ?? preview;
|
|
3181
|
-
activity.currentAction = activity.assistantSummary;
|
|
3182
|
-
if (activity.assistantSummary) activity.recentActivity = pushRecentActivity(activity.recentActivity, `assistant: ${activity.assistantSummary}`);
|
|
3183
|
-
activity.updatedAt = activityAt;
|
|
3184
|
-
return true;
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
function applyLiveRoleEvent(activity: LiveRoleActivity, event: JsonRecord, messages: RoleMessage[]): boolean {
|
|
3188
|
-
const eventType = asString(event.type);
|
|
3189
|
-
if (!eventType) return false;
|
|
3190
|
-
const activityAt = activityTimestampMs(event) ?? nowMs();
|
|
3191
|
-
if (eventType === "tool_execution_start") {
|
|
3192
|
-
const toolName = asString(event.toolName) ?? "tool";
|
|
3193
|
-
const toolArgs = isRecord(event.args) ? event.args : isRecord(event.input) ? event.input : {};
|
|
3194
|
-
activity.toolActivity = formatToolActivity(toolName, toolArgs);
|
|
3195
|
-
activity.currentAction = activity.toolActivity;
|
|
3196
|
-
activity.toolRecentActivity = pushRecentActivity(activity.toolRecentActivity, activity.toolActivity, 6);
|
|
3197
|
-
activity.recentActivity = pushRecentActivity(activity.recentActivity, activity.toolActivity);
|
|
3198
|
-
activity.updatedAt = activityAt;
|
|
3199
|
-
return true;
|
|
3200
|
-
}
|
|
3201
|
-
if (eventType === "tool_execution_end" || eventType === "tool_result_end") {
|
|
3202
|
-
activity.updatedAt = activityAt;
|
|
3203
|
-
return true;
|
|
3204
|
-
}
|
|
3205
|
-
if ((eventType === "message_update" || eventType === "message_end") && isRecord(event.message)) {
|
|
3206
|
-
const message = asRoleMessage(event.message);
|
|
3207
|
-
if (message && eventType === "message_end") messages.push(message);
|
|
3208
|
-
const nextOutput = message ? lastAssistantText(eventType === "message_end" ? messages : [message]) : "";
|
|
3209
|
-
if (nextOutput) return applyAssistantTextToLiveRoleActivity(activity, nextOutput, activityAt);
|
|
3210
|
-
activity.updatedAt = activityAt;
|
|
3211
|
-
return true;
|
|
3212
|
-
}
|
|
3213
|
-
return false;
|
|
3214
|
-
}
|
|
3215
|
-
|
|
3216
|
-
function maybeInjectTestLiveRoleActivity(rootKey: string): void {
|
|
3217
|
-
const raw = asString(process.env.PI_COMPLETION_TEST_LIVE_ROLE_ACTIVITY_JSON);
|
|
3218
|
-
if (!raw) return;
|
|
3219
|
-
try {
|
|
3220
|
-
const parsed = JSON.parse(raw);
|
|
3221
|
-
if (!isRecord(parsed)) return;
|
|
3222
|
-
const currentAction = asString(parsed.currentAction);
|
|
3223
|
-
const recentActivity = asStringArray(parsed.recentActivity).length > 0 ? asStringArray(parsed.recentActivity) : currentAction ? [currentAction] : [];
|
|
3224
|
-
const toolActivity =
|
|
3225
|
-
asString(parsed.toolActivity) ??
|
|
3226
|
-
(currentAction && !currentAction.startsWith("assistant:") && !currentAction.startsWith("progress:") ? currentAction : undefined);
|
|
3227
|
-
const assistantSummary =
|
|
3228
|
-
asString(parsed.assistantSummary) ??
|
|
3229
|
-
(currentAction?.startsWith("assistant:") ? currentAction.slice("assistant:".length).trim() : undefined);
|
|
3230
|
-
liveRoleActivityByRoot.set(rootKey, {
|
|
3231
|
-
role: asString(parsed.role) ?? "completion-implementer",
|
|
3232
|
-
status: asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running",
|
|
3233
|
-
currentAction,
|
|
3234
|
-
toolActivity,
|
|
3235
|
-
toolRecentActivity: asStringArray(parsed.toolRecentActivity).length > 0 ? asStringArray(parsed.toolRecentActivity) : toolActivity ? [toolActivity] : [],
|
|
3236
|
-
recentActivity,
|
|
3237
|
-
assistantSummary,
|
|
3238
|
-
lastAssistantText: asString(parsed.lastAssistantText),
|
|
3239
|
-
progress: asString(parsed.progress),
|
|
3240
|
-
rationale: asString(parsed.rationale),
|
|
3241
|
-
nextStep: asString(parsed.nextStep),
|
|
3242
|
-
verifying: asString(parsed.verifying),
|
|
3243
|
-
stateDeltas: asStringArray(parsed.stateDeltas),
|
|
3244
|
-
startedAt: asNumber(parsed.startedAt) ?? nowMs(),
|
|
3245
|
-
updatedAt: asNumber(parsed.updatedAt) ?? nowMs(),
|
|
3246
|
-
});
|
|
3247
|
-
} catch {
|
|
3248
|
-
// ignore malformed test override
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
function maybeReplayTestLiveRoleEvents(rootKey: string): void {
|
|
3253
|
-
const raw = asString(process.env.PI_COMPLETION_TEST_ROLE_EVENT_STREAM_JSON);
|
|
3254
|
-
if (!raw) return;
|
|
3255
|
-
try {
|
|
3256
|
-
const parsed = JSON.parse(raw);
|
|
3257
|
-
let role = "completion-implementer";
|
|
3258
|
-
let status: LiveRoleActivity["status"] = "running";
|
|
3259
|
-
let startedAt = nowMs();
|
|
3260
|
-
let events: JsonRecord[] = [];
|
|
3261
|
-
if (Array.isArray(parsed)) {
|
|
3262
|
-
events = parsed.filter(isRecord);
|
|
3263
|
-
} else if (isRecord(parsed)) {
|
|
3264
|
-
role = asString(parsed.role) ?? role;
|
|
3265
|
-
status = asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running";
|
|
3266
|
-
startedAt = asNumber(parsed.startedAt) ?? asNumber(parsed.started_at) ?? startedAt;
|
|
3267
|
-
events = Array.isArray(parsed.events) ? parsed.events.filter(isRecord) : [];
|
|
3268
|
-
} else {
|
|
3269
|
-
return;
|
|
3270
|
-
}
|
|
3271
|
-
const activity = createLiveRoleActivity(role, startedAt);
|
|
3272
|
-
const messages: RoleMessage[] = [];
|
|
3273
|
-
for (const event of events) applyLiveRoleEvent(activity, event, messages);
|
|
3274
|
-
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(activity, { status }));
|
|
3275
|
-
} catch {
|
|
3276
|
-
// ignore malformed event stream override
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
function buildCompletionStatusSurface(
|
|
3281
|
-
snapshot: CompletionStateSnapshot | undefined,
|
|
3282
|
-
liveActivity: LiveRoleActivity | undefined,
|
|
3283
|
-
): CompletionStatusSurface {
|
|
3284
|
-
if (!snapshot) return { snapshotPresent: false, widgetLines: [] };
|
|
3285
|
-
const currentPhase = asString(snapshot.state?.current_phase) ?? "unknown";
|
|
3286
|
-
const sliceId = asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)";
|
|
3287
|
-
const sliceGoal = truncateInline(asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)", 140);
|
|
3288
|
-
const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role) ?? "unknown";
|
|
3289
|
-
const remainingContractCount = asStringArray(snapshot.state?.unsatisfied_contract_ids).length;
|
|
3290
|
-
const releaseBlockerCount = asNumber(snapshot.state?.remaining_release_blockers) ?? 0;
|
|
3291
|
-
const highValueGapCount = asNumber(snapshot.state?.remaining_high_value_gaps) ?? 0;
|
|
3292
|
-
const remainingStopJudgeCount = asNumber(snapshot.state?.remaining_stop_judges) ?? 0;
|
|
3293
|
-
const activeRole = liveActivity?.status === "running" ? liveActivity.role : undefined;
|
|
3294
|
-
const liveSignal = liveActivitySignal(liveActivity);
|
|
3295
|
-
const livePreview = livePreviewForStatus(liveActivity);
|
|
3296
|
-
const liveDetailsLines = activeRole
|
|
3297
|
-
? buildInlineRunningLines({
|
|
3298
|
-
role: activeRole,
|
|
3299
|
-
currentAction: liveActivity?.currentAction,
|
|
3300
|
-
toolActivity: liveActivity?.toolActivity,
|
|
3301
|
-
toolRecentActivity: liveActivity?.toolRecentActivity,
|
|
3302
|
-
recentActivity: liveActivity?.recentActivity,
|
|
3303
|
-
assistantSummary: liveActivity?.assistantSummary,
|
|
3304
|
-
progress: liveActivity?.progress,
|
|
3305
|
-
rationale: liveActivity?.rationale,
|
|
3306
|
-
nextStep: liveActivity?.nextStep,
|
|
3307
|
-
verifying: liveActivity?.verifying,
|
|
3308
|
-
stateDeltas: liveActivity?.stateDeltas,
|
|
3309
|
-
startedAt: liveActivity?.startedAt,
|
|
3310
|
-
updatedAt: liveActivity?.updatedAt,
|
|
3311
|
-
})
|
|
3312
|
-
: [];
|
|
3313
|
-
const remainingSummary = completionRemainingSummary({
|
|
3314
|
-
remainingContractCount,
|
|
3315
|
-
releaseBlockerCount,
|
|
3316
|
-
highValueGapCount,
|
|
3317
|
-
remainingStopJudgeCount,
|
|
3318
|
-
});
|
|
3319
|
-
const widgetLines = activeRole
|
|
3320
|
-
? []
|
|
3321
|
-
: [
|
|
3322
|
-
"completion workflow",
|
|
3323
|
-
`phase: ${currentPhase}`,
|
|
3324
|
-
`slice: ${sliceId}`,
|
|
3325
|
-
`goal: ${sliceGoal}`,
|
|
3326
|
-
`next: ${nextMandatoryRole}`,
|
|
3327
|
-
`remaining: ${remainingSummary}`,
|
|
3328
|
-
];
|
|
3329
|
-
return {
|
|
3330
|
-
snapshotPresent: true,
|
|
3331
|
-
widgetLines,
|
|
3332
|
-
currentPhase,
|
|
3333
|
-
sliceId,
|
|
3334
|
-
nextMandatoryRole,
|
|
3335
|
-
remainingContractCount,
|
|
3336
|
-
releaseBlockerCount,
|
|
3337
|
-
highValueGapCount,
|
|
3338
|
-
remainingStopJudgeCount,
|
|
3339
|
-
activeRole,
|
|
3340
|
-
livePreview,
|
|
3341
|
-
liveState: liveSignal?.state,
|
|
3342
|
-
liveIdleMs: liveSignal?.idleMs,
|
|
3343
|
-
liveToolActivity: liveActivity?.toolActivity,
|
|
3344
|
-
liveAssistantSummary: liveActivity?.assistantSummary,
|
|
3345
|
-
liveProgress: liveActivity?.progress,
|
|
3346
|
-
liveRationale: liveActivity?.rationale,
|
|
3347
|
-
liveNextStep: liveActivity?.nextStep,
|
|
3348
|
-
liveVerifying: liveActivity?.verifying,
|
|
3349
|
-
liveStateDeltas: liveActivity?.stateDeltas ?? [],
|
|
3350
|
-
liveDetailsLines,
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
|
-
async function writeCompletionStatusProbe(surface: CompletionStatusSurface): Promise<void> {
|
|
3355
|
-
const outputPath = asString(process.env.PI_COMPLETION_STATUS_SNAPSHOT_FILE);
|
|
3356
|
-
if (!outputPath) return;
|
|
3357
|
-
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
3358
|
-
await fsp.writeFile(outputPath, `${JSON.stringify(surface, null, 2)}\n`, "utf8");
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
async function refreshStatus(ctx: { cwd: string; hasUI: boolean; ui: any }) {
|
|
3362
|
-
const snapshot = await loadCompletionSnapshot(getCtxCwd(ctx));
|
|
3363
|
-
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
3364
|
-
maybeInjectTestLiveRoleActivity(rootKey);
|
|
3365
|
-
maybeReplayTestLiveRoleEvents(rootKey);
|
|
3366
|
-
const surface = buildCompletionStatusSurface(snapshot, liveRoleActivityByRoot.get(rootKey));
|
|
3367
|
-
await writeCompletionStatusProbe(surface);
|
|
3368
|
-
if (!getCtxHasUI(ctx)) return;
|
|
3369
|
-
const ui = getCtxUi(ctx);
|
|
3370
|
-
if (!ui) return;
|
|
3371
|
-
safeUiCall(() => {
|
|
3372
|
-
ui.setWidget(COMPLETION_STATUS_KEY, surface.widgetLines.length > 0 ? surface.widgetLines : undefined);
|
|
782
|
+
const implementationSurfacesLine =
|
|
783
|
+
implementationSurfaces.length > 0 ? `- implementation_surfaces: ${implementationSurfaces.join(" | ")}` : undefined;
|
|
784
|
+
const verificationCommandsLine =
|
|
785
|
+
verificationCommands.length > 0 ? `- verification_commands: ${verificationCommands.join(" | ")}` : undefined;
|
|
786
|
+
return buildExtractedResumeCapsule({
|
|
787
|
+
missionAnchor: asString(snapshot.state?.mission_anchor),
|
|
788
|
+
taskType: currentTaskType(snapshot),
|
|
789
|
+
evaluationProfile: currentEvaluationProfile(snapshot),
|
|
790
|
+
currentPhase: asString(snapshot.state?.current_phase),
|
|
791
|
+
continuationPolicy: asString(snapshot.state?.continuation_policy),
|
|
792
|
+
continuationReason: asString(snapshot.state?.continuation_reason),
|
|
793
|
+
requiresReground: asBoolean(snapshot.state?.requires_reground) ?? "unknown",
|
|
794
|
+
nextMandatoryRole: asString(snapshot.state?.next_mandatory_role),
|
|
795
|
+
nextMandatoryAction: asString(snapshot.state?.next_mandatory_action),
|
|
796
|
+
remainingSliceCount: remainingSliceCount(snapshot.plan),
|
|
797
|
+
remainingStopJudges: asNumber(snapshot.state?.remaining_stop_judges) ?? "(unknown)",
|
|
798
|
+
history,
|
|
799
|
+
activeSliceMatchesPlan: activeSliceMatchesPlan(snapshot),
|
|
800
|
+
activeSliceContractDrift: activeSliceContractDriftSummary(snapshot),
|
|
801
|
+
implementerHandoffSnapshot: handoffSnapshotState(snapshot.active),
|
|
802
|
+
evidence,
|
|
803
|
+
activeSlice: {
|
|
804
|
+
sliceId: asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id),
|
|
805
|
+
status: asString(snapshot.active?.status) ?? asString(snapshot.activeSlice?.status),
|
|
806
|
+
goal: asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal),
|
|
807
|
+
priority: asNumber(snapshot.active?.priority),
|
|
808
|
+
whyNow: asString(snapshot.active?.why_now),
|
|
809
|
+
contractIds,
|
|
810
|
+
blockedOn,
|
|
811
|
+
lockedNotes,
|
|
812
|
+
mustFixFindings,
|
|
813
|
+
implementationSurfaces,
|
|
814
|
+
verificationCommands,
|
|
815
|
+
implementationSurfacesLine,
|
|
816
|
+
verificationCommandsLine,
|
|
817
|
+
basisCommit: asString(snapshot.active?.basis_commit),
|
|
818
|
+
remainingContractIdsBefore: remainingBefore,
|
|
819
|
+
releaseBlockerCountBefore: asNumber(snapshot.active?.release_blocker_count_before),
|
|
820
|
+
highValueGapCountBefore: asNumber(snapshot.active?.high_value_gap_count_before),
|
|
821
|
+
acceptanceCriteria: acceptance,
|
|
822
|
+
},
|
|
3373
823
|
});
|
|
3374
824
|
}
|
|
3375
825
|
|
|
3376
|
-
function parseReportFields(text: string): Record<string, string> {
|
|
3377
|
-
return roleReporting.parseReportFields(text);
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
function parseYesNo(value: string | undefined): boolean | undefined {
|
|
3381
|
-
return roleReporting.parseYesNo(value);
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
function parseFirstNumber(value: string | undefined): number | undefined {
|
|
3385
|
-
return roleReporting.parseFirstNumber(value);
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
826
|
async function gitHeadSha(cwd: string): Promise<string | undefined> {
|
|
3389
827
|
return await new Promise((resolve) => {
|
|
3390
828
|
const proc = spawn("git", ["rev-parse", "HEAD"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -3399,311 +837,6 @@ async function gitHeadSha(cwd: string): Promise<string | undefined> {
|
|
|
3399
837
|
});
|
|
3400
838
|
}
|
|
3401
839
|
|
|
3402
|
-
type TranscriptionResult = {
|
|
3403
|
-
appended: string[];
|
|
3404
|
-
skipped: string[];
|
|
3405
|
-
errors: string[];
|
|
3406
|
-
};
|
|
3407
|
-
|
|
3408
|
-
async function appendJsonlRecord(filePath: string, record: JsonRecord): Promise<void> {
|
|
3409
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
3410
|
-
await fsp.appendFile(filePath, `${JSON.stringify(record)}\n`, "utf8");
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
function formatElapsed(ms: number | undefined): string {
|
|
3415
|
-
if (!ms || ms < 0) return "00:00";
|
|
3416
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
3417
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
3418
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
3419
|
-
const seconds = totalSeconds % 60;
|
|
3420
|
-
if (hours > 0) return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
3421
|
-
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
function truncateInline(text: string, maxLength = 120): string {
|
|
3425
|
-
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
3426
|
-
return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength - 3)}...` : singleLine;
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
function formatToolActivity(toolName: string, args: JsonRecord): string {
|
|
3431
|
-
if (toolName === "bash") return `$ ${truncateInline(asString(args.command) ?? "...")}`;
|
|
3432
|
-
if (toolName === "read") return `read ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
3433
|
-
if (toolName === "write") return `write ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
3434
|
-
if (toolName === "edit") return `edit ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
3435
|
-
if (toolName === "grep") return `grep ${asString(args.pattern) ?? "..."}`;
|
|
3436
|
-
if (toolName === "find") return `find ${asString(args.pattern) ?? "..."}`;
|
|
3437
|
-
if (toolName === "ls") return `ls ${asString(args.path) ?? "."}`;
|
|
3438
|
-
return `${toolName} ${truncateInline(JSON.stringify(args))}`;
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
function pushRecentActivity(items: string[], line: string, maxItems = 8): string[] {
|
|
3442
|
-
const normalized = truncateInline(line, 160);
|
|
3443
|
-
if (!normalized) return items;
|
|
3444
|
-
if (items[items.length - 1] === normalized) return items;
|
|
3445
|
-
const next = [...items, normalized];
|
|
3446
|
-
return next.slice(-maxItems);
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
function collapseRecentActivity(items: string[], maxItems = 4): string[] {
|
|
3450
|
-
const collapsed: string[] = [];
|
|
3451
|
-
for (const rawItem of items) {
|
|
3452
|
-
const item = truncateInline(rawItem, 120);
|
|
3453
|
-
if (!item || item.startsWith("done ") || item.startsWith("result ")) continue;
|
|
3454
|
-
if (item.startsWith("assistant:")) continue;
|
|
3455
|
-
if (collapsed[collapsed.length - 1] === item) continue;
|
|
3456
|
-
collapsed.push(item);
|
|
3457
|
-
}
|
|
3458
|
-
return collapsed.slice(-maxItems);
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
function formatInlineRunningText(theme: any, lines: string[], options?: { primaryAssistant?: boolean }): string {
|
|
3462
|
-
let text = "";
|
|
3463
|
-
for (const [index, line] of lines.entries()) {
|
|
3464
|
-
if (index > 0) text += "\n";
|
|
3465
|
-
if (index === 0) {
|
|
3466
|
-
const [prefix, ...rest] = line.split(" ");
|
|
3467
|
-
text += theme.fg("warning", prefix);
|
|
3468
|
-
if (rest.length > 0) text += ` ${theme.fg("accent", rest.join(" "))}`;
|
|
3469
|
-
continue;
|
|
3470
|
-
}
|
|
3471
|
-
if (line.startsWith("tool:") || line.startsWith("progress:")) {
|
|
3472
|
-
text += theme.fg("toolOutput", line);
|
|
3473
|
-
continue;
|
|
3474
|
-
}
|
|
3475
|
-
if (line.startsWith("activity:")) {
|
|
3476
|
-
text += line.includes("stalled") ? theme.fg("warning", line) : line;
|
|
3477
|
-
continue;
|
|
3478
|
-
}
|
|
3479
|
-
if (line === "recent tools:") {
|
|
3480
|
-
text += theme.fg("muted", line);
|
|
3481
|
-
continue;
|
|
3482
|
-
}
|
|
3483
|
-
if (line.startsWith("- ")) {
|
|
3484
|
-
text += `${theme.fg("muted", "- ")}${theme.fg("muted", line.slice(2))}`;
|
|
3485
|
-
continue;
|
|
3486
|
-
}
|
|
3487
|
-
if (line.startsWith("elapsed:")) {
|
|
3488
|
-
text += line;
|
|
3489
|
-
continue;
|
|
3490
|
-
}
|
|
3491
|
-
if (line.startsWith("assistant:")) {
|
|
3492
|
-
text += options?.primaryAssistant ? line : theme.fg("muted", line);
|
|
3493
|
-
continue;
|
|
3494
|
-
}
|
|
3495
|
-
if (line.startsWith("next:") || line.startsWith("verifying:")) {
|
|
3496
|
-
text += theme.fg("muted", line);
|
|
3497
|
-
continue;
|
|
3498
|
-
}
|
|
3499
|
-
if (line.startsWith("rationale:") || line.startsWith("state-delta:")) {
|
|
3500
|
-
text += line;
|
|
3501
|
-
continue;
|
|
3502
|
-
}
|
|
3503
|
-
text += theme.fg("muted", line);
|
|
3504
|
-
}
|
|
3505
|
-
return text;
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
function buildInlineRunningLines(details: {
|
|
3509
|
-
role?: string;
|
|
3510
|
-
startedAt?: number;
|
|
3511
|
-
updatedAt?: number;
|
|
3512
|
-
currentAction?: string;
|
|
3513
|
-
toolActivity?: string;
|
|
3514
|
-
toolRecentActivity?: string[];
|
|
3515
|
-
recentActivity?: string[];
|
|
3516
|
-
assistantSummary?: string;
|
|
3517
|
-
progress?: string;
|
|
3518
|
-
rationale?: string;
|
|
3519
|
-
nextStep?: string;
|
|
3520
|
-
verifying?: string;
|
|
3521
|
-
stateDeltas?: string[];
|
|
3522
|
-
}): string[] {
|
|
3523
|
-
const lines: string[] = [];
|
|
3524
|
-
let header = "running completion role";
|
|
3525
|
-
if (details.role) header += ` ${details.role}`;
|
|
3526
|
-
lines.push(header);
|
|
3527
|
-
if (details.startedAt !== undefined) lines.push(`elapsed: ${formatElapsed(nowMs() - details.startedAt)}`);
|
|
3528
|
-
const signalLine = formatLiveActivitySignal(
|
|
3529
|
-
liveActivitySignal({ status: "running", startedAt: details.startedAt, updatedAt: details.updatedAt }),
|
|
3530
|
-
);
|
|
3531
|
-
if (signalLine) lines.push(signalLine);
|
|
3532
|
-
const toolLine = details.toolActivity;
|
|
3533
|
-
if (toolLine) lines.push(`tool: ${toolLine}`);
|
|
3534
|
-
if (details.progress) lines.push(`progress: ${details.progress}`);
|
|
3535
|
-
else if (details.assistantSummary) lines.push(`assistant: ${details.assistantSummary}`);
|
|
3536
|
-
else if (details.currentAction && details.currentAction !== toolLine) {
|
|
3537
|
-
lines.push(`assistant: ${details.currentAction.replace(/^assistant:\s*/, "")}`);
|
|
3538
|
-
}
|
|
3539
|
-
if (details.rationale) lines.push(`rationale: ${details.rationale}`);
|
|
3540
|
-
if (details.nextStep) lines.push(`next: ${details.nextStep}`);
|
|
3541
|
-
if (details.verifying) lines.push(`verifying: ${details.verifying}`);
|
|
3542
|
-
for (const delta of (details.stateDeltas ?? []).slice(-4)) lines.push(`state-delta: ${delta}`);
|
|
3543
|
-
const recentTools = collapseRecentActivity(details.toolRecentActivity ?? details.recentActivity ?? []);
|
|
3544
|
-
const recentWithoutCurrent = recentTools.filter((item) => item !== toolLine);
|
|
3545
|
-
if (recentWithoutCurrent.length > 0) {
|
|
3546
|
-
lines.push("recent tools:");
|
|
3547
|
-
for (const item of recentWithoutCurrent) lines.push(`- ${item}`);
|
|
3548
|
-
}
|
|
3549
|
-
return lines;
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
function parseStructuredProgress(text: string): {
|
|
3553
|
-
progress?: string;
|
|
3554
|
-
rationale?: string;
|
|
3555
|
-
nextStep?: string;
|
|
3556
|
-
verifying?: string;
|
|
3557
|
-
stateDeltas: string[];
|
|
3558
|
-
} {
|
|
3559
|
-
const result: { progress?: string; rationale?: string; nextStep?: string; verifying?: string; stateDeltas: string[] } = {
|
|
3560
|
-
stateDeltas: [],
|
|
3561
|
-
};
|
|
3562
|
-
for (const rawLine of text.split("\n")) {
|
|
3563
|
-
const line = rawLine.trim();
|
|
3564
|
-
if (!line) continue;
|
|
3565
|
-
const match = line.match(/^(PROGRESS|RATIONALE|NEXT|VERIFYING|STATE-DELTA):\s*(.+)$/i);
|
|
3566
|
-
if (!match) continue;
|
|
3567
|
-
const [, rawKey, rawValue] = match;
|
|
3568
|
-
const key = rawKey.toUpperCase();
|
|
3569
|
-
const value = rawValue.trim();
|
|
3570
|
-
if (!value) continue;
|
|
3571
|
-
if (key === "PROGRESS") result.progress = value;
|
|
3572
|
-
else if (key === "RATIONALE") result.rationale = value;
|
|
3573
|
-
else if (key === "NEXT") result.nextStep = value;
|
|
3574
|
-
else if (key === "VERIFYING") result.verifying = value;
|
|
3575
|
-
else if (key === "STATE-DELTA") result.stateDeltas.push(value);
|
|
3576
|
-
}
|
|
3577
|
-
if (result.stateDeltas.length > 6) result.stateDeltas = result.stateDeltas.slice(-6);
|
|
3578
|
-
return result;
|
|
3579
|
-
}
|
|
3580
|
-
|
|
3581
|
-
async function transcribeRoleOutput(role: CompletionRole, cwd: string, output: string, reportFields: Record<string, string>): Promise<TranscriptionResult> {
|
|
3582
|
-
const snapshot = await loadCompletionSnapshot(cwd);
|
|
3583
|
-
if (!snapshot) {
|
|
3584
|
-
return { appended: [], skipped: ["No canonical completion snapshot found."], errors: [] };
|
|
3585
|
-
}
|
|
3586
|
-
const headSha = await gitHeadSha(snapshot.files.root);
|
|
3587
|
-
if (!headSha) {
|
|
3588
|
-
return { appended: [], skipped: [], errors: ["Could not resolve git HEAD for transcription."] };
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
const sliceId =
|
|
3592
|
-
asString(snapshot.active?.slice_id) ??
|
|
3593
|
-
asString(snapshot.activeSlice?.slice_id) ??
|
|
3594
|
-
asString(snapshot.state?.latest_completed_slice);
|
|
3595
|
-
|
|
3596
|
-
return await roleReporting.transcribeCanonicalRoleReport({
|
|
3597
|
-
role,
|
|
3598
|
-
output,
|
|
3599
|
-
reportFields,
|
|
3600
|
-
snapshotFiles: snapshot.files,
|
|
3601
|
-
headSha,
|
|
3602
|
-
sliceId,
|
|
3603
|
-
});
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
function isPathInside(root: string, candidatePath: string): boolean {
|
|
3607
|
-
const resolvedRoot = path.resolve(root);
|
|
3608
|
-
const resolvedCandidate = path.resolve(candidatePath);
|
|
3609
|
-
return resolvedCandidate === resolvedRoot || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`);
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
3613
|
-
return path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
|
-
function isAllowedControlPlanePath(root: string, rawPath: string): boolean {
|
|
3617
|
-
const resolved = resolveToolPath(root, rawPath);
|
|
3618
|
-
if (path.basename(resolved) === ".gitignore") return true;
|
|
3619
|
-
return isPathInside(path.join(root, ".agent"), resolved);
|
|
3620
|
-
}
|
|
3621
|
-
|
|
3622
|
-
function startsWithAny(value: string, prefixes: string[]): boolean {
|
|
3623
|
-
return prefixes.some((prefix) => value.startsWith(prefix));
|
|
3624
|
-
}
|
|
3625
|
-
|
|
3626
|
-
function normalizeCommand(command: string): string {
|
|
3627
|
-
return command.trim().replace(/\s+/g, " ");
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
function isMutatingBash(command: string): boolean {
|
|
3631
|
-
const normalized = normalizeCommand(command);
|
|
3632
|
-
return startsWithAny(normalized, [
|
|
3633
|
-
"git add",
|
|
3634
|
-
"git commit",
|
|
3635
|
-
"git push",
|
|
3636
|
-
"rm ",
|
|
3637
|
-
"mv ",
|
|
3638
|
-
"cp ",
|
|
3639
|
-
"mkdir ",
|
|
3640
|
-
"touch ",
|
|
3641
|
-
"chmod ",
|
|
3642
|
-
"chown ",
|
|
3643
|
-
"sed -i",
|
|
3644
|
-
"perl -pi",
|
|
3645
|
-
"python -c",
|
|
3646
|
-
"python3 -c",
|
|
3647
|
-
"node -e",
|
|
3648
|
-
"bun -e",
|
|
3649
|
-
"tee ",
|
|
3650
|
-
]) || normalized.includes(">") || normalized.includes("| tee") || normalized.includes("apply_patch");
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
async function loadAgentDefinition(cwd: string, role: CompletionRole): Promise<AgentDefinition> {
|
|
3654
|
-
const projectAgent = walkUpForDir(cwd, [".pi", "agents", `${role}.md`]);
|
|
3655
|
-
const packageAgent = PACKAGE_AGENTS_DIR ? path.join(PACKAGE_AGENTS_DIR, `${role}.md`) : undefined;
|
|
3656
|
-
const candidates = [projectAgent, packageAgent, path.join(AGENT_HOME, "agents", `${role}.md`)].filter(
|
|
3657
|
-
(candidate): candidate is string => Boolean(candidate),
|
|
3658
|
-
);
|
|
3659
|
-
for (const candidate of candidates) {
|
|
3660
|
-
if (!fs.existsSync(candidate)) continue;
|
|
3661
|
-
const raw = await fsp.readFile(candidate, "utf8");
|
|
3662
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(raw);
|
|
3663
|
-
return {
|
|
3664
|
-
name: frontmatter.name ?? role,
|
|
3665
|
-
description: frontmatter.description,
|
|
3666
|
-
tools: frontmatter.tools?.split(",").map((tool) => tool.trim()).filter(Boolean),
|
|
3667
|
-
model: frontmatter.model,
|
|
3668
|
-
systemPrompt: body.trim(),
|
|
3669
|
-
filePath: candidate,
|
|
3670
|
-
};
|
|
3671
|
-
}
|
|
3672
|
-
throw new Error(`Missing completion agent definition for ${role}`);
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
async function writeTempFile(prefix: string, content: string): Promise<{ dir: string; filePath: string }> {
|
|
3676
|
-
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
3677
|
-
const filePath = path.join(dir, "prompt.md");
|
|
3678
|
-
await fsp.writeFile(filePath, content, { encoding: "utf8", mode: 0o600 });
|
|
3679
|
-
return { dir, filePath };
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
3683
|
-
const currentScript = process.argv[1];
|
|
3684
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
3685
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
3686
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
3687
|
-
}
|
|
3688
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
3689
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
3690
|
-
if (!isGenericRuntime) return { command: process.execPath, args };
|
|
3691
|
-
return { command: "pi", args };
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
function lastAssistantText(messages: Array<{ role: string; content: Array<{ type: string; text?: string }> }>): string {
|
|
3695
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3696
|
-
const message = messages[i];
|
|
3697
|
-
if (message.role !== "assistant") continue;
|
|
3698
|
-
const texts = message.content
|
|
3699
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
3700
|
-
.map((part) => part.text?.trim())
|
|
3701
|
-
.filter((part): part is string => Boolean(part));
|
|
3702
|
-
if (texts.length > 0) return texts.join("\n\n");
|
|
3703
|
-
}
|
|
3704
|
-
return "";
|
|
3705
|
-
}
|
|
3706
|
-
|
|
3707
840
|
function completionKickoff(
|
|
3708
841
|
goal: string,
|
|
3709
842
|
taskType: string,
|
|
@@ -3725,15 +858,56 @@ function completionResumePrompt(taskType: string, evaluationProfile: string): st
|
|
|
3725
858
|
}
|
|
3726
859
|
|
|
3727
860
|
export default function completionExtension(pi: ExtensionAPI) {
|
|
861
|
+
const statusSurfaceArgs = {
|
|
862
|
+
liveRoleActivityByRoot,
|
|
863
|
+
completionStatusKey: COMPLETION_STATUS_KEY,
|
|
864
|
+
safeUiCall,
|
|
865
|
+
getCtxCwd,
|
|
866
|
+
getCtxHasUI,
|
|
867
|
+
getCtxUi,
|
|
868
|
+
};
|
|
869
|
+
const driverDeps = {
|
|
870
|
+
bareOnlyGuidance: COOK_BARE_ONLY_GUIDANCE,
|
|
871
|
+
structuredDiscussionFailureDetail: COOK_STRUCTURED_DISCUSSION_FAILURE_DETAIL,
|
|
872
|
+
mainChatRerunGuidance: COOK_MAIN_CHAT_RERUN_GUIDANCE,
|
|
873
|
+
cookCommandSpec: {
|
|
874
|
+
description: "Bare /cook workflow: start, continue, refocus, or start the next round",
|
|
875
|
+
},
|
|
876
|
+
buildContextProposalContinuationReason,
|
|
877
|
+
completionKickoff,
|
|
878
|
+
completionResumePrompt,
|
|
879
|
+
completionRootKey,
|
|
880
|
+
completionTestAutoContinuePromptPath,
|
|
881
|
+
completionTestDriverPromptPath,
|
|
882
|
+
completionTestExistingWorkflowChooserSnapshotPath,
|
|
883
|
+
completionTestWorkflowActionOverride,
|
|
884
|
+
confirmContextProposal,
|
|
885
|
+
deriveCookContextProposal,
|
|
886
|
+
emitCommandText,
|
|
887
|
+
finalizeContextProposalAnalysis,
|
|
888
|
+
getCtxCwd,
|
|
889
|
+
getCtxHasUI,
|
|
890
|
+
getCtxUi,
|
|
891
|
+
hasRunningCompletionRole,
|
|
892
|
+
maybeWriteActiveWorkflowRoutingSnapshot,
|
|
893
|
+
maybeWriteTestSnapshot,
|
|
894
|
+
missionAnchorsLikelyEquivalent,
|
|
895
|
+
missionAnchorsStrictlyEquivalent,
|
|
896
|
+
scaffoldCompletionFiles,
|
|
897
|
+
shouldSkipDriverKickoffForTests,
|
|
898
|
+
shouldTestAutoContinueOnSessionStart,
|
|
899
|
+
shouldTreatBareActiveWorkflowProposalAsClearRefocus,
|
|
900
|
+
};
|
|
901
|
+
|
|
3728
902
|
pi.on("session_start", async (_event, ctx) => {
|
|
3729
|
-
await
|
|
903
|
+
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
3730
904
|
if (shouldTestAutoContinueOnSessionStart()) {
|
|
3731
|
-
await autoContinueWorkflowIfNeeded(pi, ctx);
|
|
905
|
+
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
3732
906
|
}
|
|
3733
907
|
});
|
|
3734
908
|
|
|
3735
909
|
pi.on("turn_end", async (_event, ctx) => {
|
|
3736
|
-
await
|
|
910
|
+
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
3737
911
|
});
|
|
3738
912
|
|
|
3739
913
|
pi.on("agent_end", async (_event, ctx) => {
|
|
@@ -3741,8 +915,8 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3741
915
|
if (snapshot && (await pathExists(snapshot.files.compactionMarkerPath))) {
|
|
3742
916
|
await fsp.rm(snapshot.files.compactionMarkerPath, { force: true });
|
|
3743
917
|
}
|
|
3744
|
-
await
|
|
3745
|
-
await autoContinueWorkflowIfNeeded(pi, ctx);
|
|
918
|
+
await refreshCompletionStatus({ ctx, ...statusSurfaceArgs });
|
|
919
|
+
await autoContinueWorkflowIfNeeded(pi, ctx, driverDeps);
|
|
3746
920
|
});
|
|
3747
921
|
|
|
3748
922
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
@@ -3755,7 +929,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3755
929
|
if (!loaded || !shouldInjectCompletionWorkflowContext(loaded.snapshot)) return;
|
|
3756
930
|
const additions = isWorkflowDone(loaded.snapshot)
|
|
3757
931
|
? [buildDoneWorkflowBoundaryReminder(loaded.snapshot)]
|
|
3758
|
-
: [
|
|
932
|
+
: [composeSystemReminder(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory)];
|
|
3759
933
|
if (!isWorkflowDone(loaded.snapshot)) {
|
|
3760
934
|
const markerText = await readText(loaded.snapshot.files.compactionMarkerPath);
|
|
3761
935
|
let marker: JsonRecord | undefined;
|
|
@@ -3781,7 +955,7 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3781
955
|
const loaded = await loadCompletionDataForReminder(getCtxCwd(ctx));
|
|
3782
956
|
if (!loaded || isWorkflowDone(loaded.snapshot)) return;
|
|
3783
957
|
const { preparation } = event;
|
|
3784
|
-
const summary =
|
|
958
|
+
const summary = composeResumeCapsule(loaded.snapshot, loaded.sliceHistory, loaded.stopHistory);
|
|
3785
959
|
await fsp.mkdir(loaded.snapshot.files.tmpDir, { recursive: true });
|
|
3786
960
|
await fsp.writeFile(
|
|
3787
961
|
loaded.snapshot.files.compactionMarkerPath,
|
|
@@ -3812,44 +986,14 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3812
986
|
const snapshot = await loadCompletionSnapshot(cwd);
|
|
3813
987
|
const completionActive = Boolean(snapshot) && asString(snapshot?.state?.continuation_policy) !== "done";
|
|
3814
988
|
const root = snapshot?.files.root ?? findRepoRoot(cwd) ?? cwd;
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
if (role === "completion-reviewer" || role === "completion-auditor" || role === "completion-stop-judge") {
|
|
3825
|
-
return { block: true, reason: `${role} is read-only.` };
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
if ((role === "completion-bootstrapper" || role === "completion-regrounder") && !isAllowedControlPlanePath(root, rawPath)) {
|
|
3829
|
-
return { block: true, reason: `${role} may only edit .agent/** or .gitignore.` };
|
|
3830
|
-
}
|
|
3831
|
-
|
|
3832
|
-
if (!role && completionActive && !isAllowedControlPlanePath(root, rawPath)) {
|
|
3833
|
-
return { block: true, reason: "The workflow driver may not edit tracked product files directly during completion." };
|
|
3834
|
-
}
|
|
3835
|
-
}
|
|
3836
|
-
|
|
3837
|
-
if (event.toolName !== "bash") return;
|
|
3838
|
-
const command = asString((event.input as JsonRecord).command);
|
|
3839
|
-
if (!command) return;
|
|
3840
|
-
const normalized = normalizeCommand(command);
|
|
3841
|
-
|
|
3842
|
-
if (["completion-reviewer", "completion-auditor", "completion-stop-judge"].includes(role ?? "") && isMutatingBash(normalized)) {
|
|
3843
|
-
return { block: true, reason: `${role} is read-only and cannot run mutating bash.` };
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
|
-
if ((role === "completion-bootstrapper" || role === "completion-regrounder") && startsWithAny(normalized, ["git add", "git commit"])) {
|
|
3847
|
-
return { block: true, reason: `${role} may not create commits.` };
|
|
3848
|
-
}
|
|
3849
|
-
|
|
3850
|
-
if (!role && completionActive && startsWithAny(normalized, ["git add", "git commit"])) {
|
|
3851
|
-
return { block: true, reason: "The workflow driver may not create commits directly during completion." };
|
|
3852
|
-
}
|
|
989
|
+
const reason = toolCallBlockReason({
|
|
990
|
+
toolName: event.toolName,
|
|
991
|
+
input: isRecord(event.input) ? event.input : undefined,
|
|
992
|
+
role,
|
|
993
|
+
completionActive,
|
|
994
|
+
root,
|
|
995
|
+
});
|
|
996
|
+
if (reason) return { block: true, reason };
|
|
3853
997
|
});
|
|
3854
998
|
|
|
3855
999
|
pi.registerTool({
|
|
@@ -3871,8 +1015,6 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3871
1015
|
const cwd = getCtxCwd(ctx);
|
|
3872
1016
|
const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
|
|
3873
1017
|
const rootKey = runCwd;
|
|
3874
|
-
const agent = await loadAgentDefinition(runCwd, role);
|
|
3875
|
-
const loaded = await loadCompletionDataForReminder(runCwd);
|
|
3876
1018
|
type RunningDetails = {
|
|
3877
1019
|
role: string;
|
|
3878
1020
|
status: "running" | "ok" | "error";
|
|
@@ -3894,150 +1036,87 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
3894
1036
|
transcription?: TranscriptionResult;
|
|
3895
1037
|
exitCode?: number;
|
|
3896
1038
|
};
|
|
3897
|
-
const
|
|
3898
|
-
const taskLines = [
|
|
3899
|
-
`Completion role: ${role}`,
|
|
3900
|
-
"Before acting, read the completion protocol skill and reference:",
|
|
3901
|
-
`- ${SKILL_PATH}`,
|
|
3902
|
-
`- ${REFERENCE_PATH}`,
|
|
3903
|
-
"Use canonical .agent/** state as the source of truth.",
|
|
3904
|
-
];
|
|
3905
|
-
if (loaded && isRubricEvaluationRole(role)) {
|
|
3906
|
-
taskLines.push("", ...buildEvaluationRoleContextLines(loaded.snapshot, role));
|
|
3907
|
-
}
|
|
3908
|
-
if (params.task?.trim()) {
|
|
3909
|
-
taskLines.push("", "Supplemental task context:", params.task.trim());
|
|
3910
|
-
}
|
|
3911
|
-
const prompt = taskLines.join("\n");
|
|
3912
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session", "--append-system-prompt", systemPromptTemp.filePath];
|
|
3913
|
-
if (agent.model) args.push("--model", agent.model);
|
|
3914
|
-
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
3915
|
-
args.push(prompt);
|
|
3916
|
-
|
|
3917
|
-
const invocation = getPiInvocation(args);
|
|
3918
|
-
let stderr = "";
|
|
3919
|
-
const messages: RoleMessage[] = [];
|
|
3920
|
-
const liveActivity = createLiveRoleActivity(role);
|
|
3921
|
-
const emitRunningUpdate = (freshActivity = false) => {
|
|
3922
|
-
if (freshActivity) liveActivity.updatedAt = nowMs();
|
|
1039
|
+
const emitActivityUpdate = (activity: LiveRoleActivity) => {
|
|
3923
1040
|
const details: RunningDetails = {
|
|
3924
1041
|
role,
|
|
3925
|
-
status:
|
|
3926
|
-
currentAction:
|
|
3927
|
-
toolActivity:
|
|
3928
|
-
toolRecentActivity:
|
|
3929
|
-
recentActivity:
|
|
3930
|
-
assistantSummary:
|
|
3931
|
-
lastAssistantText:
|
|
3932
|
-
progress:
|
|
3933
|
-
rationale:
|
|
3934
|
-
nextStep:
|
|
3935
|
-
verifying:
|
|
3936
|
-
stateDeltas:
|
|
3937
|
-
startedAt:
|
|
3938
|
-
updatedAt:
|
|
1042
|
+
status: activity.status,
|
|
1043
|
+
currentAction: activity.currentAction,
|
|
1044
|
+
toolActivity: activity.toolActivity,
|
|
1045
|
+
toolRecentActivity: activity.toolRecentActivity,
|
|
1046
|
+
recentActivity: activity.recentActivity,
|
|
1047
|
+
assistantSummary: activity.assistantSummary,
|
|
1048
|
+
lastAssistantText: activity.lastAssistantText,
|
|
1049
|
+
progress: activity.progress,
|
|
1050
|
+
rationale: activity.rationale,
|
|
1051
|
+
nextStep: activity.nextStep,
|
|
1052
|
+
verifying: activity.verifying,
|
|
1053
|
+
stateDeltas: activity.stateDeltas,
|
|
1054
|
+
startedAt: activity.startedAt,
|
|
1055
|
+
updatedAt: activity.updatedAt,
|
|
3939
1056
|
};
|
|
3940
|
-
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(
|
|
3941
|
-
void
|
|
1057
|
+
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(activity, { status: activity.status }));
|
|
1058
|
+
void refreshCompletionStatus({ ctx: ctx as { cwd: string; hasUI: boolean; ui: any }, ...statusSurfaceArgs });
|
|
3942
1059
|
onUpdate?.({
|
|
3943
|
-
content: [{ type: "text", text:
|
|
1060
|
+
content: [{ type: "text", text: activity.lastAssistantText || activity.currentAction || `Running ${role}...` }],
|
|
3944
1061
|
details,
|
|
3945
1062
|
});
|
|
3946
1063
|
};
|
|
3947
|
-
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
proc.stdout.on("data", (chunk) => {
|
|
3971
|
-
buffer += chunk.toString();
|
|
3972
|
-
const lines = buffer.split("\n");
|
|
3973
|
-
buffer = lines.pop() ?? "";
|
|
3974
|
-
for (const line of lines) processLine(line);
|
|
3975
|
-
});
|
|
3976
|
-
|
|
3977
|
-
proc.stderr.on("data", (chunk) => {
|
|
3978
|
-
stderr += chunk.toString();
|
|
3979
|
-
});
|
|
3980
|
-
|
|
3981
|
-
proc.on("close", (code) => {
|
|
3982
|
-
if (buffer.trim()) processLine(buffer);
|
|
3983
|
-
resolve(code ?? 0);
|
|
3984
|
-
});
|
|
3985
|
-
|
|
3986
|
-
proc.on("error", () => resolve(1));
|
|
3987
|
-
|
|
3988
|
-
if (signal) {
|
|
3989
|
-
const abort = () => proc.kill("SIGTERM");
|
|
3990
|
-
if (signal.aborted) abort();
|
|
3991
|
-
else signal.addEventListener("abort", abort, { once: true });
|
|
3992
|
-
}
|
|
3993
|
-
});
|
|
1064
|
+
const loaded = await loadCompletionDataForReminder(runCwd);
|
|
1065
|
+
const result = await runCompletionRole({
|
|
1066
|
+
root: runCwd,
|
|
1067
|
+
role,
|
|
1068
|
+
task: params.task,
|
|
1069
|
+
signal,
|
|
1070
|
+
systemPromptPreamble: [
|
|
1071
|
+
`Completion role: ${role}`,
|
|
1072
|
+
"Before acting, read the completion protocol skill and reference:",
|
|
1073
|
+
`- ${SKILL_PATH}`,
|
|
1074
|
+
`- ${REFERENCE_PATH}`,
|
|
1075
|
+
"Use canonical .agent/** state as the source of truth.",
|
|
1076
|
+
],
|
|
1077
|
+
evaluationContextLines: loaded && isRubricEvaluationRole(role) ? buildEvaluationRoleContextLines(loaded.snapshot, role) : undefined,
|
|
1078
|
+
onUpdate: emitActivityUpdate,
|
|
1079
|
+
onConsoleMessage: (level, message) => emitCommandText(ctx, message, level),
|
|
1080
|
+
createLiveRoleActivity: (name) => createLiveRoleActivity(name),
|
|
1081
|
+
cloneLiveRoleActivity,
|
|
1082
|
+
applyLiveRoleEvent,
|
|
1083
|
+
nowMs,
|
|
1084
|
+
heartbeatMs: LIVE_ROLE_HEARTBEAT_MS,
|
|
1085
|
+
});
|
|
3994
1086
|
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
if (transcription?.errors.length) {
|
|
4002
|
-
emitCommandText(ctx, `Completion transcription warning: ${transcription.errors.join(" | ")}`, "warning");
|
|
1087
|
+
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(result.activity, { status: result.ok ? "ok" : "error" }));
|
|
1088
|
+
await refreshCompletionStatus({ ctx: ctx as { cwd: string; hasUI: boolean; ui: any }, ...statusSurfaceArgs });
|
|
1089
|
+
setTimeout(() => {
|
|
1090
|
+
const current = liveRoleActivityByRoot.get(rootKey);
|
|
1091
|
+
if (current && current.role === role && current.status !== "running") {
|
|
1092
|
+
liveRoleActivityByRoot.delete(rootKey);
|
|
4003
1093
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
};
|
|
4031
|
-
} finally {
|
|
4032
|
-
clearInterval(heartbeat);
|
|
4033
|
-
setTimeout(() => {
|
|
4034
|
-
const current = liveRoleActivityByRoot.get(rootKey);
|
|
4035
|
-
if (current && current.role === role && current.status !== "running") {
|
|
4036
|
-
liveRoleActivityByRoot.delete(rootKey);
|
|
4037
|
-
}
|
|
4038
|
-
}, 10_000);
|
|
4039
|
-
await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
|
|
4040
|
-
}
|
|
1094
|
+
}, 10_000);
|
|
1095
|
+
return {
|
|
1096
|
+
content: [{ type: "text", text: result.output }],
|
|
1097
|
+
details: {
|
|
1098
|
+
role,
|
|
1099
|
+
status: result.ok ? "ok" : "error",
|
|
1100
|
+
exitCode: result.exitCode,
|
|
1101
|
+
stderr: result.stderr,
|
|
1102
|
+
reportFields: result.reportFields,
|
|
1103
|
+
transcription: result.transcription,
|
|
1104
|
+
currentAction: result.activity.currentAction,
|
|
1105
|
+
toolActivity: result.activity.toolActivity,
|
|
1106
|
+
toolRecentActivity: result.activity.toolRecentActivity,
|
|
1107
|
+
recentActivity: result.activity.recentActivity,
|
|
1108
|
+
assistantSummary: result.activity.assistantSummary,
|
|
1109
|
+
lastAssistantText: result.activity.lastAssistantText,
|
|
1110
|
+
progress: result.activity.progress,
|
|
1111
|
+
rationale: result.activity.rationale,
|
|
1112
|
+
nextStep: result.activity.nextStep,
|
|
1113
|
+
verifying: result.activity.verifying,
|
|
1114
|
+
stateDeltas: result.activity.stateDeltas,
|
|
1115
|
+
startedAt: result.activity.startedAt,
|
|
1116
|
+
updatedAt: result.activity.updatedAt,
|
|
1117
|
+
},
|
|
1118
|
+
isError: !result.ok,
|
|
1119
|
+
};
|
|
4041
1120
|
},
|
|
4042
1121
|
renderCall(args, theme) {
|
|
4043
1122
|
const role = args.role || "completion-role";
|
|
@@ -4123,131 +1202,6 @@ export default function completionExtension(pi: ExtensionAPI) {
|
|
|
4123
1202
|
},
|
|
4124
1203
|
});
|
|
4125
1204
|
|
|
4126
|
-
pi
|
|
4127
|
-
description: "Discussion-driven /cook workflow: start, continue, refocus, or start the next round",
|
|
4128
|
-
handler: async (args, ctx) => {
|
|
4129
|
-
const inlineHint = args.trim() || undefined;
|
|
4130
|
-
let goal: string | undefined;
|
|
4131
|
-
const cwd = getCtxCwd(ctx);
|
|
4132
|
-
let snapshot = await loadCompletionSnapshot(cwd);
|
|
4133
|
-
const workflowDone = isWorkflowDone(snapshot);
|
|
4134
|
-
let kickoffIntent: "auto" | "continue" | "refocus" = "auto";
|
|
4135
|
-
let kickoffMissionAnchor = snapshot ? currentMissionAnchor(snapshot) : undefined;
|
|
4136
|
-
let kickoffAnalysis: ContextProposalAnalysis | undefined;
|
|
1205
|
+
registerCookCommand(pi, driverDeps);
|
|
4137
1206
|
|
|
4138
|
-
if (!snapshot) {
|
|
4139
|
-
const root = findRepoRoot(cwd) ?? cwd;
|
|
4140
|
-
const projectName = path.basename(root);
|
|
4141
|
-
const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
|
|
4142
|
-
if (!proposal) {
|
|
4143
|
-
emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage(), "info");
|
|
4144
|
-
return;
|
|
4145
|
-
}
|
|
4146
|
-
const decision = await confirmContextProposal(ctx, proposal, {
|
|
4147
|
-
title: "Start a completion workflow from the recent discussion?",
|
|
4148
|
-
});
|
|
4149
|
-
if (!decision) {
|
|
4150
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled recent-discussion workflow proposal"), "info");
|
|
4151
|
-
return;
|
|
4152
|
-
}
|
|
4153
|
-
goal = decision.goalText;
|
|
4154
|
-
kickoffMissionAnchor = decision.missionAnchor;
|
|
4155
|
-
kickoffAnalysis = decision.analysis;
|
|
4156
|
-
const startupRouting = finalizeContextProposalAnalysis(kickoffAnalysis, [goal ?? kickoffMissionAnchor ?? projectName]);
|
|
4157
|
-
const created = await scaffoldCompletionFiles(root, kickoffMissionAnchor ?? projectName, {
|
|
4158
|
-
analysis: startupRouting,
|
|
4159
|
-
continuationReason: buildContextProposalContinuationReason(
|
|
4160
|
-
"User started workflow via /cook:",
|
|
4161
|
-
goal ?? kickoffMissionAnchor ?? projectName,
|
|
4162
|
-
startupRouting,
|
|
4163
|
-
),
|
|
4164
|
-
});
|
|
4165
|
-
emitCommandText(
|
|
4166
|
-
ctx,
|
|
4167
|
-
`Initialized completion control plane in ${created.root}${created.created.length > 0 ? ` (${created.created.length} files created)` : ""}`,
|
|
4168
|
-
"info",
|
|
4169
|
-
);
|
|
4170
|
-
snapshot = await loadCompletionSnapshot(root);
|
|
4171
|
-
}
|
|
4172
|
-
if (!snapshot) {
|
|
4173
|
-
emitCommandText(ctx, "Failed to load completion workflow state", "error");
|
|
4174
|
-
return;
|
|
4175
|
-
}
|
|
4176
|
-
if (!goal) {
|
|
4177
|
-
if (workflowDone) {
|
|
4178
|
-
const projectName = path.basename(snapshot.files.root);
|
|
4179
|
-
const proposal = await deriveCookContextProposal(ctx, projectName, inlineHint);
|
|
4180
|
-
if (!proposal) {
|
|
4181
|
-
emitCommandText(ctx, buildCookStructuredDiscussionFailureMessage("The previous completion workflow is already done."), "info");
|
|
4182
|
-
return;
|
|
4183
|
-
}
|
|
4184
|
-
const decision = await confirmContextProposal(ctx, proposal, {
|
|
4185
|
-
title: "The previous completion workflow is done. Start the next workflow round from the recent discussion?",
|
|
4186
|
-
});
|
|
4187
|
-
if (!decision) {
|
|
4188
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled next workflow round proposal"), "info");
|
|
4189
|
-
return;
|
|
4190
|
-
}
|
|
4191
|
-
goal = decision.goalText;
|
|
4192
|
-
kickoffIntent = "refocus";
|
|
4193
|
-
kickoffMissionAnchor = decision.missionAnchor;
|
|
4194
|
-
await refocusCompletionMission(snapshot, decision.missionAnchor, decision.goalText, decision.analysis);
|
|
4195
|
-
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
4196
|
-
emitCommandText(ctx, `Started a new completion workflow round from recent discussion: ${decision.missionAnchor}`, "info");
|
|
4197
|
-
} else {
|
|
4198
|
-
const assessment = await assessActiveWorkflowProposalRouting(ctx, snapshot, inlineHint);
|
|
4199
|
-
if (assessment.action !== "refocus" || !assessment.proposal) {
|
|
4200
|
-
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
|
|
4201
|
-
return;
|
|
4202
|
-
}
|
|
4203
|
-
const decision = await confirmExistingWorkflowProposal(ctx, snapshot, assessment.proposal, {
|
|
4204
|
-
intro: "Recent non-command discussion suggests a different workflow. Choose how /cook should proceed:",
|
|
4205
|
-
proposedMissionLabel: "Proposed mission from recent discussion",
|
|
4206
|
-
refocusChoiceLabel:
|
|
4207
|
-
"Start new workflow from recent discussion\n\nReview the proposed replacement in a final Start/Cancel confirmation before /cook rewrites canonical workflow state.",
|
|
4208
|
-
});
|
|
4209
|
-
if (!decision) {
|
|
4210
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled existing workflow confirmation"), "info");
|
|
4211
|
-
return;
|
|
4212
|
-
}
|
|
4213
|
-
if (decision.action === "continue") {
|
|
4214
|
-
await resumeActiveWorkflowFromCanonicalState(pi, ctx, snapshot);
|
|
4215
|
-
return;
|
|
4216
|
-
}
|
|
4217
|
-
const proposalDecision = await confirmContextProposal(ctx, assessment.proposal, {
|
|
4218
|
-
title: "Start the replacement workflow from recent discussion?",
|
|
4219
|
-
});
|
|
4220
|
-
if (!proposalDecision) {
|
|
4221
|
-
emitCommandText(ctx, buildCookCancellationMessage("Cancelled replacement workflow proposal"), "info");
|
|
4222
|
-
return;
|
|
4223
|
-
}
|
|
4224
|
-
goal = proposalDecision.goalText;
|
|
4225
|
-
kickoffIntent = "refocus";
|
|
4226
|
-
kickoffMissionAnchor = proposalDecision.missionAnchor;
|
|
4227
|
-
await refocusCompletionMission(snapshot, proposalDecision.missionAnchor, proposalDecision.goalText, proposalDecision.analysis);
|
|
4228
|
-
snapshot = (await loadCompletionSnapshot(snapshot.files.root)) ?? snapshot;
|
|
4229
|
-
emitCommandText(ctx, `Refocused completion mission from recent discussion to: ${proposalDecision.missionAnchor}`, "info");
|
|
4230
|
-
}
|
|
4231
|
-
}
|
|
4232
|
-
kickoffMissionAnchor = kickoffMissionAnchor ?? currentMissionAnchor(snapshot);
|
|
4233
|
-
pi.setSessionName(`completion: ${kickoffMissionAnchor.slice(0, 60)}`);
|
|
4234
|
-
const kickoffPrompt = completionKickoff(
|
|
4235
|
-
goal,
|
|
4236
|
-
currentTaskType(snapshot) ?? "(missing)",
|
|
4237
|
-
currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
4238
|
-
kickoffIntent,
|
|
4239
|
-
kickoffMissionAnchor,
|
|
4240
|
-
);
|
|
4241
|
-
const rootKey = completionRootKey(snapshot, getCtxCwd(ctx));
|
|
4242
|
-
const fingerprint = completionContinuationFingerprint(snapshot) ?? JSON.stringify({
|
|
4243
|
-
kind: "kickoff",
|
|
4244
|
-
mission_anchor: kickoffMissionAnchor,
|
|
4245
|
-
goal,
|
|
4246
|
-
intent: kickoffIntent,
|
|
4247
|
-
task_type: currentTaskType(snapshot) ?? "(missing)",
|
|
4248
|
-
evaluation_profile: currentEvaluationProfile(snapshot) ?? "(missing)",
|
|
4249
|
-
});
|
|
4250
|
-
await queueCompletionDriverPrompt(pi, ctx, rootKey, fingerprint, kickoffPrompt, "kickoff");
|
|
4251
|
-
},
|
|
4252
|
-
});
|
|
4253
1207
|
}
|