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