@linimin/pi-letscook 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/README.md +9 -10
- package/extensions/completion/driver.ts +615 -0
- package/extensions/completion/index.ts +427 -3488
- package/extensions/completion/policy-guards.ts +110 -0
- package/extensions/completion/prompt-surfaces.ts +512 -0
- package/extensions/completion/proposal.ts +944 -0
- package/extensions/completion/role-runner.ts +455 -0
- package/extensions/completion/state-store.ts +458 -0
- package/extensions/completion/status-surface.ts +516 -0
- package/extensions/completion/transcription.ts +77 -0
- package/extensions/completion/types.ts +87 -0
- package/package.json +1 -1
- package/scripts/active-slice-contract-test.sh +5 -4
- package/scripts/canonical-evidence-artifact-test.sh +4 -4
- package/scripts/context-proposal-test.sh +21 -14
- package/scripts/legacy-cleanup-test.sh +107 -0
- package/scripts/observability-status-test.sh +39 -0
- package/scripts/refocus-test.sh +6 -6
- package/scripts/release-check.sh +17 -16
- package/scripts/role-runner-contract-test.sh +44 -0
- package/scripts/rubric-contract-test.sh +8 -6
- package/scripts/smoke-test.sh +5 -5
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
asNumber,
|
|
5
|
+
asString,
|
|
6
|
+
asStringArray,
|
|
7
|
+
completionRootKey,
|
|
8
|
+
isRecord,
|
|
9
|
+
loadCompletionSnapshot,
|
|
10
|
+
} from "./state-store";
|
|
11
|
+
import type { CompletionStatusSurface, CompletionStateSnapshot, JsonRecord, LiveRoleActivity } from "./types";
|
|
12
|
+
|
|
13
|
+
export const LIVE_ROLE_WAITING_MS = 15_000;
|
|
14
|
+
export const LIVE_ROLE_STALLED_MS = 45_000;
|
|
15
|
+
|
|
16
|
+
type LiveActivitySignal = {
|
|
17
|
+
state: "active" | "waiting" | "stalled";
|
|
18
|
+
idleMs: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RoleMessage = {
|
|
22
|
+
role: string;
|
|
23
|
+
content: Array<{ type: string; text?: string }>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function formatCount(count: number, singular: string, plural = `${singular}s`): string {
|
|
27
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function completionRemainingSummary(surface: {
|
|
31
|
+
remainingContractCount: number;
|
|
32
|
+
releaseBlockerCount: number;
|
|
33
|
+
highValueGapCount: number;
|
|
34
|
+
remainingStopJudgeCount: number;
|
|
35
|
+
}): string {
|
|
36
|
+
return [
|
|
37
|
+
formatCount(surface.remainingContractCount, "contract"),
|
|
38
|
+
formatCount(surface.releaseBlockerCount, "blocker"),
|
|
39
|
+
formatCount(surface.highValueGapCount, "gap"),
|
|
40
|
+
formatCount(surface.remainingStopJudgeCount, "stop judge", "stop judges"),
|
|
41
|
+
].join(" · ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function envNumber(name: string): number | undefined {
|
|
45
|
+
const raw = asString(process.env[name]);
|
|
46
|
+
if (!raw) return undefined;
|
|
47
|
+
const parsed = Number(raw);
|
|
48
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function nowMs(): number {
|
|
52
|
+
return envNumber("PI_COMPLETION_TEST_NOW") ?? Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatElapsed(ms: number | undefined): string {
|
|
56
|
+
if (!ms || ms < 0) return "00:00";
|
|
57
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
58
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
59
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
60
|
+
const seconds = totalSeconds % 60;
|
|
61
|
+
if (hours > 0) return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
62
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function truncateInline(text: string, maxLength = 120): string {
|
|
66
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
67
|
+
return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength - 3)}...` : singleLine;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatToolActivity(toolName: string, args: JsonRecord): string {
|
|
71
|
+
if (toolName === "bash") return `$ ${truncateInline(asString(args.command) ?? "...")}`;
|
|
72
|
+
if (toolName === "read") return `read ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
73
|
+
if (toolName === "write") return `write ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
74
|
+
if (toolName === "edit") return `edit ${asString(args.filePath) ?? asString(args.path) ?? "..."}`;
|
|
75
|
+
if (toolName === "grep") return `grep ${asString(args.pattern) ?? "..."}`;
|
|
76
|
+
if (toolName === "find") return `find ${asString(args.pattern) ?? "..."}`;
|
|
77
|
+
if (toolName === "ls") return `ls ${asString(args.path) ?? "."}`;
|
|
78
|
+
return `${toolName} ${truncateInline(JSON.stringify(args))}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function pushRecentActivity(items: string[], line: string, maxItems = 8): string[] {
|
|
82
|
+
const normalized = truncateInline(line, 160);
|
|
83
|
+
if (!normalized) return items;
|
|
84
|
+
if (items[items.length - 1] === normalized) return items;
|
|
85
|
+
const next = [...items, normalized];
|
|
86
|
+
return next.slice(-maxItems);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collapseRecentActivity(items: string[], maxItems = 4): string[] {
|
|
90
|
+
const collapsed: string[] = [];
|
|
91
|
+
for (const rawItem of items) {
|
|
92
|
+
const item = truncateInline(rawItem, 120);
|
|
93
|
+
if (!item || item.startsWith("done ") || item.startsWith("result ")) continue;
|
|
94
|
+
if (item.startsWith("assistant:")) continue;
|
|
95
|
+
if (collapsed[collapsed.length - 1] === item) continue;
|
|
96
|
+
collapsed.push(item);
|
|
97
|
+
}
|
|
98
|
+
return collapsed.slice(-maxItems);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function liveActivitySignal(activity: { status?: string; startedAt?: number; updatedAt?: number } | undefined): LiveActivitySignal | undefined {
|
|
102
|
+
if (!activity || activity.status !== "running") return undefined;
|
|
103
|
+
const anchor = activity.updatedAt ?? activity.startedAt;
|
|
104
|
+
if (anchor === undefined) return undefined;
|
|
105
|
+
const idleMs = Math.max(0, nowMs() - anchor);
|
|
106
|
+
return {
|
|
107
|
+
state: idleMs >= LIVE_ROLE_STALLED_MS ? "stalled" : idleMs >= LIVE_ROLE_WAITING_MS ? "waiting" : "active",
|
|
108
|
+
idleMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatLiveActivitySignal(signal: LiveActivitySignal | undefined): string | undefined {
|
|
113
|
+
if (!signal) return undefined;
|
|
114
|
+
if (signal.state === "active") return "activity: active";
|
|
115
|
+
return `activity: ${signal.state} (${formatElapsed(signal.idleMs)} since update)`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function livePreviewForStatus(activity: LiveRoleActivity | undefined): string | undefined {
|
|
119
|
+
if (!activity || activity.status !== "running") return undefined;
|
|
120
|
+
return truncateInline(
|
|
121
|
+
activity.progress ?? activity.verifying ?? activity.toolActivity ?? activity.assistantSummary ?? activity.currentAction ?? activity.lastAssistantText ?? "",
|
|
122
|
+
120,
|
|
123
|
+
) || undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function cloneLiveRoleActivity(activity: LiveRoleActivity, overrides: Partial<LiveRoleActivity> = {}): LiveRoleActivity {
|
|
127
|
+
return {
|
|
128
|
+
...activity,
|
|
129
|
+
...overrides,
|
|
130
|
+
toolRecentActivity: [...(overrides.toolRecentActivity ?? activity.toolRecentActivity)],
|
|
131
|
+
recentActivity: [...(overrides.recentActivity ?? activity.recentActivity)],
|
|
132
|
+
stateDeltas: [...(overrides.stateDeltas ?? activity.stateDeltas)],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createLiveRoleActivity(role: string, startedAt = nowMs()): LiveRoleActivity {
|
|
137
|
+
const currentAction = "Starting role subprocess";
|
|
138
|
+
return {
|
|
139
|
+
role,
|
|
140
|
+
status: "running",
|
|
141
|
+
currentAction,
|
|
142
|
+
toolActivity: currentAction,
|
|
143
|
+
toolRecentActivity: [currentAction],
|
|
144
|
+
recentActivity: [currentAction],
|
|
145
|
+
stateDeltas: [],
|
|
146
|
+
startedAt,
|
|
147
|
+
updatedAt: startedAt,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function activityTimestampMs(event: JsonRecord | undefined): number | undefined {
|
|
152
|
+
return asNumber(event?.updatedAt) ?? asNumber(event?.timestampMs) ?? asNumber(event?.timestamp) ?? asNumber(event?.at);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function asRoleMessage(value: unknown): RoleMessage | undefined {
|
|
156
|
+
if (!isRecord(value)) return undefined;
|
|
157
|
+
const role = asString(value.role);
|
|
158
|
+
const content = Array.isArray(value.content)
|
|
159
|
+
? value.content.flatMap((item) => {
|
|
160
|
+
if (!isRecord(item)) return [];
|
|
161
|
+
const type = asString(item.type);
|
|
162
|
+
if (!type) return [];
|
|
163
|
+
return [{ type, text: asString(item.text) }];
|
|
164
|
+
})
|
|
165
|
+
: [];
|
|
166
|
+
if (!role) return undefined;
|
|
167
|
+
return { role, content };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseStructuredProgress(text: string): {
|
|
171
|
+
progress?: string;
|
|
172
|
+
rationale?: string;
|
|
173
|
+
nextStep?: string;
|
|
174
|
+
verifying?: string;
|
|
175
|
+
stateDeltas: string[];
|
|
176
|
+
} {
|
|
177
|
+
const result: { progress?: string; rationale?: string; nextStep?: string; verifying?: string; stateDeltas: string[] } = {
|
|
178
|
+
stateDeltas: [],
|
|
179
|
+
};
|
|
180
|
+
for (const rawLine of text.split("\n")) {
|
|
181
|
+
const line = rawLine.trim();
|
|
182
|
+
if (!line) continue;
|
|
183
|
+
const match = line.match(/^(PROGRESS|RATIONALE|NEXT|VERIFYING|STATE-DELTA):\s*(.+)$/i);
|
|
184
|
+
if (!match) continue;
|
|
185
|
+
const [, rawKey, rawValue] = match;
|
|
186
|
+
const key = rawKey.toUpperCase();
|
|
187
|
+
const value = rawValue.trim();
|
|
188
|
+
if (!value) continue;
|
|
189
|
+
if (key === "PROGRESS") result.progress = value;
|
|
190
|
+
else if (key === "RATIONALE") result.rationale = value;
|
|
191
|
+
else if (key === "NEXT") result.nextStep = value;
|
|
192
|
+
else if (key === "VERIFYING") result.verifying = value;
|
|
193
|
+
else if (key === "STATE-DELTA") result.stateDeltas.push(value);
|
|
194
|
+
}
|
|
195
|
+
if (result.stateDeltas.length > 6) result.stateDeltas = result.stateDeltas.slice(-6);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function lastAssistantText(messages: RoleMessage[]): string {
|
|
200
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
201
|
+
const message = messages[i];
|
|
202
|
+
if (message.role !== "assistant") continue;
|
|
203
|
+
const texts = message.content
|
|
204
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
205
|
+
.map((part) => part.text?.trim())
|
|
206
|
+
.filter((part): part is string => Boolean(part));
|
|
207
|
+
if (texts.length > 0) return texts.join("\n\n");
|
|
208
|
+
}
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function applyAssistantTextToLiveRoleActivity(activity: LiveRoleActivity, text: string, activityAt = nowMs()): boolean {
|
|
213
|
+
if (!text) return false;
|
|
214
|
+
activity.lastAssistantText = text;
|
|
215
|
+
const parsed = parseStructuredProgress(text);
|
|
216
|
+
if (parsed.progress) activity.progress = parsed.progress;
|
|
217
|
+
if (parsed.rationale) activity.rationale = parsed.rationale;
|
|
218
|
+
if (parsed.nextStep) activity.nextStep = parsed.nextStep;
|
|
219
|
+
if (parsed.verifying) activity.verifying = parsed.verifying;
|
|
220
|
+
if (parsed.stateDeltas.length > 0) activity.stateDeltas = parsed.stateDeltas;
|
|
221
|
+
const preview = truncateInline(text, 140);
|
|
222
|
+
activity.assistantSummary = activity.progress ?? activity.verifying ?? preview;
|
|
223
|
+
activity.currentAction = activity.assistantSummary;
|
|
224
|
+
if (activity.assistantSummary) activity.recentActivity = pushRecentActivity(activity.recentActivity, `assistant: ${activity.assistantSummary}`);
|
|
225
|
+
activity.updatedAt = activityAt;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function applyLiveRoleEvent(activity: LiveRoleActivity, event: JsonRecord, messages: RoleMessage[]): boolean {
|
|
230
|
+
const eventType = asString(event.type);
|
|
231
|
+
if (!eventType) return false;
|
|
232
|
+
const activityAt = activityTimestampMs(event) ?? nowMs();
|
|
233
|
+
if (eventType === "tool_execution_start") {
|
|
234
|
+
const toolName = asString(event.toolName) ?? "tool";
|
|
235
|
+
const toolArgs = isRecord(event.args) ? event.args : isRecord(event.input) ? event.input : {};
|
|
236
|
+
activity.toolActivity = formatToolActivity(toolName, toolArgs);
|
|
237
|
+
activity.currentAction = activity.toolActivity;
|
|
238
|
+
activity.toolRecentActivity = pushRecentActivity(activity.toolRecentActivity, activity.toolActivity, 6);
|
|
239
|
+
activity.recentActivity = pushRecentActivity(activity.recentActivity, activity.toolActivity);
|
|
240
|
+
activity.updatedAt = activityAt;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (eventType === "tool_execution_end" || eventType === "tool_result_end") {
|
|
244
|
+
activity.updatedAt = activityAt;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if ((eventType === "message_update" || eventType === "message_end") && isRecord(event.message)) {
|
|
248
|
+
const message = asRoleMessage(event.message);
|
|
249
|
+
if (message && eventType === "message_end") messages.push(message);
|
|
250
|
+
const nextOutput = message ? lastAssistantText(eventType === "message_end" ? messages : [message]) : "";
|
|
251
|
+
if (nextOutput) return applyAssistantTextToLiveRoleActivity(activity, nextOutput, activityAt);
|
|
252
|
+
activity.updatedAt = activityAt;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function maybeInjectTestLiveRoleActivity(liveRoleActivityByRoot: Map<string, LiveRoleActivity>, rootKey: string): void {
|
|
259
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_LIVE_ROLE_ACTIVITY_JSON);
|
|
260
|
+
if (!raw) return;
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(raw);
|
|
263
|
+
if (!isRecord(parsed)) return;
|
|
264
|
+
const currentAction = asString(parsed.currentAction);
|
|
265
|
+
const recentActivity = asStringArray(parsed.recentActivity).length > 0 ? asStringArray(parsed.recentActivity) : currentAction ? [currentAction] : [];
|
|
266
|
+
const toolActivity =
|
|
267
|
+
asString(parsed.toolActivity) ??
|
|
268
|
+
(currentAction && !currentAction.startsWith("assistant:") && !currentAction.startsWith("progress:") ? currentAction : undefined);
|
|
269
|
+
const assistantSummary =
|
|
270
|
+
asString(parsed.assistantSummary) ??
|
|
271
|
+
(currentAction?.startsWith("assistant:") ? currentAction.slice("assistant:".length).trim() : undefined);
|
|
272
|
+
liveRoleActivityByRoot.set(rootKey, {
|
|
273
|
+
role: asString(parsed.role) ?? "completion-implementer",
|
|
274
|
+
status: asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running",
|
|
275
|
+
currentAction,
|
|
276
|
+
toolActivity,
|
|
277
|
+
toolRecentActivity: asStringArray(parsed.toolRecentActivity).length > 0 ? asStringArray(parsed.toolRecentActivity) : toolActivity ? [toolActivity] : [],
|
|
278
|
+
recentActivity,
|
|
279
|
+
assistantSummary,
|
|
280
|
+
lastAssistantText: asString(parsed.lastAssistantText),
|
|
281
|
+
progress: asString(parsed.progress),
|
|
282
|
+
rationale: asString(parsed.rationale),
|
|
283
|
+
nextStep: asString(parsed.nextStep),
|
|
284
|
+
verifying: asString(parsed.verifying),
|
|
285
|
+
stateDeltas: asStringArray(parsed.stateDeltas),
|
|
286
|
+
startedAt: asNumber(parsed.startedAt) ?? nowMs(),
|
|
287
|
+
updatedAt: asNumber(parsed.updatedAt) ?? nowMs(),
|
|
288
|
+
});
|
|
289
|
+
} catch {
|
|
290
|
+
// ignore malformed test override
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function maybeReplayTestLiveRoleEvents(liveRoleActivityByRoot: Map<string, LiveRoleActivity>, rootKey: string): void {
|
|
295
|
+
const raw = asString(process.env.PI_COMPLETION_TEST_ROLE_EVENT_STREAM_JSON);
|
|
296
|
+
if (!raw) return;
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(raw);
|
|
299
|
+
let role = "completion-implementer";
|
|
300
|
+
let status: LiveRoleActivity["status"] = "running";
|
|
301
|
+
let startedAt = nowMs();
|
|
302
|
+
let events: JsonRecord[] = [];
|
|
303
|
+
if (Array.isArray(parsed)) {
|
|
304
|
+
events = parsed.filter(isRecord);
|
|
305
|
+
} else if (isRecord(parsed)) {
|
|
306
|
+
role = asString(parsed.role) ?? role;
|
|
307
|
+
status = asString(parsed.status) === "ok" ? "ok" : asString(parsed.status) === "error" ? "error" : "running";
|
|
308
|
+
startedAt = asNumber(parsed.startedAt) ?? asNumber(parsed.started_at) ?? startedAt;
|
|
309
|
+
events = Array.isArray(parsed.events) ? parsed.events.filter(isRecord) : [];
|
|
310
|
+
} else {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const activity = createLiveRoleActivity(role, startedAt);
|
|
314
|
+
const messages: RoleMessage[] = [];
|
|
315
|
+
for (const event of events) applyLiveRoleEvent(activity, event, messages);
|
|
316
|
+
liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(activity, { status }));
|
|
317
|
+
} catch {
|
|
318
|
+
// ignore malformed event stream override
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function buildInlineRunningLines(details: {
|
|
323
|
+
role?: string;
|
|
324
|
+
startedAt?: number;
|
|
325
|
+
updatedAt?: number;
|
|
326
|
+
currentAction?: string;
|
|
327
|
+
toolActivity?: string;
|
|
328
|
+
toolRecentActivity?: string[];
|
|
329
|
+
recentActivity?: string[];
|
|
330
|
+
assistantSummary?: string;
|
|
331
|
+
progress?: string;
|
|
332
|
+
rationale?: string;
|
|
333
|
+
nextStep?: string;
|
|
334
|
+
verifying?: string;
|
|
335
|
+
stateDeltas?: string[];
|
|
336
|
+
}): string[] {
|
|
337
|
+
const lines: string[] = [];
|
|
338
|
+
let header = "running completion role";
|
|
339
|
+
if (details.role) header += ` ${details.role}`;
|
|
340
|
+
lines.push(header);
|
|
341
|
+
if (details.startedAt !== undefined) lines.push(`elapsed: ${formatElapsed(nowMs() - details.startedAt)}`);
|
|
342
|
+
const signalLine = formatLiveActivitySignal(
|
|
343
|
+
liveActivitySignal({ status: "running", startedAt: details.startedAt, updatedAt: details.updatedAt }),
|
|
344
|
+
);
|
|
345
|
+
if (signalLine) lines.push(signalLine);
|
|
346
|
+
const toolLine = details.toolActivity;
|
|
347
|
+
if (toolLine) lines.push(`tool: ${toolLine}`);
|
|
348
|
+
if (details.progress) lines.push(`progress: ${details.progress}`);
|
|
349
|
+
else if (details.assistantSummary) lines.push(`assistant: ${details.assistantSummary}`);
|
|
350
|
+
else if (details.currentAction && details.currentAction !== toolLine) {
|
|
351
|
+
lines.push(`assistant: ${details.currentAction.replace(/^assistant:\s*/, "")}`);
|
|
352
|
+
}
|
|
353
|
+
if (details.rationale) lines.push(`rationale: ${details.rationale}`);
|
|
354
|
+
if (details.nextStep) lines.push(`next: ${details.nextStep}`);
|
|
355
|
+
if (details.verifying) lines.push(`verifying: ${details.verifying}`);
|
|
356
|
+
for (const delta of (details.stateDeltas ?? []).slice(-4)) lines.push(`state-delta: ${delta}`);
|
|
357
|
+
const recentTools = collapseRecentActivity(details.toolRecentActivity ?? details.recentActivity ?? []);
|
|
358
|
+
const recentWithoutCurrent = recentTools.filter((item) => item !== toolLine);
|
|
359
|
+
if (recentWithoutCurrent.length > 0) {
|
|
360
|
+
lines.push("recent tools:");
|
|
361
|
+
for (const item of recentWithoutCurrent) lines.push(`- ${item}`);
|
|
362
|
+
}
|
|
363
|
+
return lines;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function formatInlineRunningText(theme: any, lines: string[], options?: { primaryAssistant?: boolean }): string {
|
|
367
|
+
let text = "";
|
|
368
|
+
for (const [index, line] of lines.entries()) {
|
|
369
|
+
if (index > 0) text += "\n";
|
|
370
|
+
if (index === 0) {
|
|
371
|
+
const [prefix, ...rest] = line.split(" ");
|
|
372
|
+
text += theme.fg("warning", prefix);
|
|
373
|
+
if (rest.length > 0) text += ` ${theme.fg("accent", rest.join(" "))}`;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (line.startsWith("tool:") || line.startsWith("progress:")) {
|
|
377
|
+
text += theme.fg("toolOutput", line);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (line.startsWith("activity:")) {
|
|
381
|
+
text += line.includes("stalled") ? theme.fg("warning", line) : line;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (line === "recent tools:") {
|
|
385
|
+
text += theme.fg("muted", line);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (line.startsWith("- ")) {
|
|
389
|
+
text += `${theme.fg("muted", "- ")}${theme.fg("muted", line.slice(2))}`;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (line.startsWith("elapsed:")) {
|
|
393
|
+
text += line;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (line.startsWith("assistant:")) {
|
|
397
|
+
text += options?.primaryAssistant ? line : theme.fg("muted", line);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (line.startsWith("next:") || line.startsWith("verifying:")) {
|
|
401
|
+
text += theme.fg("muted", line);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (line.startsWith("rationale:") || line.startsWith("state-delta:")) {
|
|
405
|
+
text += line;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
text += theme.fg("muted", line);
|
|
409
|
+
}
|
|
410
|
+
return text;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function buildCompletionStatusSurface(
|
|
414
|
+
snapshot: CompletionStateSnapshot | undefined,
|
|
415
|
+
liveActivity: LiveRoleActivity | undefined,
|
|
416
|
+
): CompletionStatusSurface {
|
|
417
|
+
if (!snapshot) return { snapshotPresent: false, widgetLines: [] };
|
|
418
|
+
const currentPhase = asString(snapshot.state?.current_phase) ?? "unknown";
|
|
419
|
+
const sliceId = asString(snapshot.active?.slice_id) ?? asString(snapshot.activeSlice?.slice_id) ?? "(none)";
|
|
420
|
+
const sliceGoal = truncateInline(asString(snapshot.active?.goal) ?? asString(snapshot.activeSlice?.goal) ?? "(unknown)", 140);
|
|
421
|
+
const nextMandatoryRole = asString(snapshot.state?.next_mandatory_role) ?? "unknown";
|
|
422
|
+
const remainingContractCount = asStringArray(snapshot.state?.unsatisfied_contract_ids).length;
|
|
423
|
+
const releaseBlockerCount = asNumber(snapshot.state?.remaining_release_blockers) ?? 0;
|
|
424
|
+
const highValueGapCount = asNumber(snapshot.state?.remaining_high_value_gaps) ?? 0;
|
|
425
|
+
const remainingStopJudgeCount = asNumber(snapshot.state?.remaining_stop_judges) ?? 0;
|
|
426
|
+
const activeRole = liveActivity?.status === "running" ? liveActivity.role : undefined;
|
|
427
|
+
const liveSignal = liveActivitySignal(liveActivity);
|
|
428
|
+
const livePreview = livePreviewForStatus(liveActivity);
|
|
429
|
+
const liveDetailsLines = activeRole
|
|
430
|
+
? buildInlineRunningLines({
|
|
431
|
+
role: activeRole,
|
|
432
|
+
currentAction: liveActivity?.currentAction,
|
|
433
|
+
toolActivity: liveActivity?.toolActivity,
|
|
434
|
+
toolRecentActivity: liveActivity?.toolRecentActivity,
|
|
435
|
+
recentActivity: liveActivity?.recentActivity,
|
|
436
|
+
assistantSummary: liveActivity?.assistantSummary,
|
|
437
|
+
progress: liveActivity?.progress,
|
|
438
|
+
rationale: liveActivity?.rationale,
|
|
439
|
+
nextStep: liveActivity?.nextStep,
|
|
440
|
+
verifying: liveActivity?.verifying,
|
|
441
|
+
stateDeltas: liveActivity?.stateDeltas,
|
|
442
|
+
startedAt: liveActivity?.startedAt,
|
|
443
|
+
updatedAt: liveActivity?.updatedAt,
|
|
444
|
+
})
|
|
445
|
+
: [];
|
|
446
|
+
const remainingSummary = completionRemainingSummary({
|
|
447
|
+
remainingContractCount,
|
|
448
|
+
releaseBlockerCount,
|
|
449
|
+
highValueGapCount,
|
|
450
|
+
remainingStopJudgeCount,
|
|
451
|
+
});
|
|
452
|
+
const widgetLines = activeRole
|
|
453
|
+
? []
|
|
454
|
+
: [
|
|
455
|
+
"completion workflow",
|
|
456
|
+
`phase: ${currentPhase}`,
|
|
457
|
+
`slice: ${sliceId}`,
|
|
458
|
+
`goal: ${sliceGoal}`,
|
|
459
|
+
`next: ${nextMandatoryRole}`,
|
|
460
|
+
`remaining: ${remainingSummary}`,
|
|
461
|
+
];
|
|
462
|
+
return {
|
|
463
|
+
snapshotPresent: true,
|
|
464
|
+
widgetLines,
|
|
465
|
+
currentPhase,
|
|
466
|
+
sliceId,
|
|
467
|
+
nextMandatoryRole,
|
|
468
|
+
remainingContractCount,
|
|
469
|
+
releaseBlockerCount,
|
|
470
|
+
highValueGapCount,
|
|
471
|
+
remainingStopJudgeCount,
|
|
472
|
+
activeRole,
|
|
473
|
+
livePreview,
|
|
474
|
+
liveState: liveSignal?.state,
|
|
475
|
+
liveIdleMs: liveSignal?.idleMs,
|
|
476
|
+
liveToolActivity: liveActivity?.toolActivity,
|
|
477
|
+
liveAssistantSummary: liveActivity?.assistantSummary,
|
|
478
|
+
liveProgress: liveActivity?.progress,
|
|
479
|
+
liveRationale: liveActivity?.rationale,
|
|
480
|
+
liveNextStep: liveActivity?.nextStep,
|
|
481
|
+
liveVerifying: liveActivity?.verifying,
|
|
482
|
+
liveStateDeltas: liveActivity?.stateDeltas ?? [],
|
|
483
|
+
liveDetailsLines,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function writeCompletionStatusProbe(surface: CompletionStatusSurface): Promise<void> {
|
|
488
|
+
const outputPath = asString(process.env.PI_COMPLETION_STATUS_SNAPSHOT_FILE);
|
|
489
|
+
if (!outputPath) return;
|
|
490
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
491
|
+
await fsp.writeFile(outputPath, `${JSON.stringify(surface, null, 2)}\n`, "utf8");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function refreshCompletionStatus(args: {
|
|
495
|
+
ctx: { cwd: string; hasUI: boolean; ui: any };
|
|
496
|
+
liveRoleActivityByRoot: Map<string, LiveRoleActivity>;
|
|
497
|
+
completionStatusKey: string;
|
|
498
|
+
safeUiCall: (action: () => void) => void;
|
|
499
|
+
getCtxCwd: (ctx: { cwd: string }) => string;
|
|
500
|
+
getCtxHasUI: (ctx: { hasUI: boolean }) => boolean;
|
|
501
|
+
getCtxUi: <T extends { ui: any }>(ctx: T) => any | undefined;
|
|
502
|
+
}): Promise<void> {
|
|
503
|
+
const cwd = args.getCtxCwd(args.ctx);
|
|
504
|
+
const snapshot = await loadCompletionSnapshot(cwd);
|
|
505
|
+
const rootKey = completionRootKey(snapshot, cwd);
|
|
506
|
+
maybeInjectTestLiveRoleActivity(args.liveRoleActivityByRoot, rootKey);
|
|
507
|
+
maybeReplayTestLiveRoleEvents(args.liveRoleActivityByRoot, rootKey);
|
|
508
|
+
const surface = buildCompletionStatusSurface(snapshot, args.liveRoleActivityByRoot.get(rootKey));
|
|
509
|
+
await writeCompletionStatusProbe(surface);
|
|
510
|
+
if (!args.getCtxHasUI(args.ctx)) return;
|
|
511
|
+
const ui = args.getCtxUi(args.ctx);
|
|
512
|
+
if (!ui) return;
|
|
513
|
+
args.safeUiCall(() => {
|
|
514
|
+
ui.setWidget(args.completionStatusKey, surface.widgetLines.length > 0 ? surface.widgetLines : undefined);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as roleReporting from "./role-reporting.js";
|
|
3
|
+
import { loadCompletionSnapshot } from "./state-store";
|
|
4
|
+
import type { CompletionRole, JsonRecord } from "./types";
|
|
5
|
+
|
|
6
|
+
function asString(value: unknown): string | undefined {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TranscriptionResult = {
|
|
11
|
+
appended: string[];
|
|
12
|
+
skipped: string[];
|
|
13
|
+
errors: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function parseReportFields(text: string): Record<string, string> {
|
|
17
|
+
return roleReporting.parseReportFields(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseYesNo(value: string | undefined): boolean | undefined {
|
|
21
|
+
return roleReporting.parseYesNo(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseFirstNumber(value: string | undefined): number | undefined {
|
|
25
|
+
return roleReporting.parseFirstNumber(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function gitHeadSha(cwd: string): Promise<string | undefined> {
|
|
29
|
+
return await new Promise((resolve) => {
|
|
30
|
+
const proc = spawn("git", ["rev-parse", "HEAD"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
31
|
+
let stdout = "";
|
|
32
|
+
proc.stdout.on("data", (chunk) => {
|
|
33
|
+
stdout += chunk.toString();
|
|
34
|
+
});
|
|
35
|
+
proc.on("close", (code) => {
|
|
36
|
+
resolve(code === 0 ? asString(stdout) : undefined);
|
|
37
|
+
});
|
|
38
|
+
proc.on("error", () => resolve(undefined));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function transcribeRoleOutput(
|
|
43
|
+
role: CompletionRole,
|
|
44
|
+
cwd: string,
|
|
45
|
+
output: string,
|
|
46
|
+
reportFields: Record<string, string>,
|
|
47
|
+
): Promise<TranscriptionResult> {
|
|
48
|
+
const snapshot = await loadCompletionSnapshot(cwd);
|
|
49
|
+
if (!snapshot) {
|
|
50
|
+
return { appended: [], skipped: ["No canonical completion snapshot found."], errors: [] };
|
|
51
|
+
}
|
|
52
|
+
const headSha = await gitHeadSha(snapshot.files.root);
|
|
53
|
+
if (!headSha) {
|
|
54
|
+
return { appended: [], skipped: [], errors: ["Could not resolve git HEAD for transcription."] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sliceId =
|
|
58
|
+
asString(snapshot.active?.slice_id) ??
|
|
59
|
+
asString(snapshot.activeSlice?.slice_id) ??
|
|
60
|
+
asString(snapshot.state?.latest_completed_slice);
|
|
61
|
+
|
|
62
|
+
return await roleReporting.transcribeCanonicalRoleReport({
|
|
63
|
+
role,
|
|
64
|
+
output,
|
|
65
|
+
reportFields,
|
|
66
|
+
snapshotFiles: snapshot.files,
|
|
67
|
+
headSha,
|
|
68
|
+
sliceId,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function appendJsonlRecord(filePath: string, record: JsonRecord): Promise<void> {
|
|
73
|
+
const fs = await import("node:fs/promises");
|
|
74
|
+
const path = await import("node:path");
|
|
75
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
76
|
+
await fs.appendFile(filePath, `${JSON.stringify(record)}\n`, "utf8");
|
|
77
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const ROLE_NAMES = [
|
|
2
|
+
"completion-bootstrapper",
|
|
3
|
+
"completion-regrounder",
|
|
4
|
+
"completion-implementer",
|
|
5
|
+
"completion-reviewer",
|
|
6
|
+
"completion-auditor",
|
|
7
|
+
"completion-stop-judge",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type CompletionRole = (typeof ROLE_NAMES)[number];
|
|
11
|
+
export type JsonRecord = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
export type CompletionFiles = {
|
|
14
|
+
root: string;
|
|
15
|
+
agentDir: string;
|
|
16
|
+
tmpDir: string;
|
|
17
|
+
profilePath: string;
|
|
18
|
+
statePath: string;
|
|
19
|
+
planPath: string;
|
|
20
|
+
activePath: string;
|
|
21
|
+
sliceHistoryPath: string;
|
|
22
|
+
stopHistoryPath: string;
|
|
23
|
+
verificationEvidencePath: string;
|
|
24
|
+
compactionMarkerPath: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CompletionStateSnapshot = {
|
|
28
|
+
files: CompletionFiles;
|
|
29
|
+
profile?: JsonRecord;
|
|
30
|
+
state?: JsonRecord;
|
|
31
|
+
plan?: JsonRecord;
|
|
32
|
+
active?: JsonRecord;
|
|
33
|
+
verificationEvidence?: JsonRecord;
|
|
34
|
+
activeSlice?: JsonRecord;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type AgentDefinition = {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
tools?: string[];
|
|
41
|
+
model?: string;
|
|
42
|
+
systemPrompt: string;
|
|
43
|
+
filePath: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type LiveRoleActivity = {
|
|
47
|
+
role: string;
|
|
48
|
+
status: "running" | "ok" | "error";
|
|
49
|
+
currentAction?: string;
|
|
50
|
+
toolActivity?: string;
|
|
51
|
+
toolRecentActivity: string[];
|
|
52
|
+
recentActivity: string[];
|
|
53
|
+
assistantSummary?: string;
|
|
54
|
+
lastAssistantText?: string;
|
|
55
|
+
progress?: string;
|
|
56
|
+
rationale?: string;
|
|
57
|
+
nextStep?: string;
|
|
58
|
+
verifying?: string;
|
|
59
|
+
stateDeltas: string[];
|
|
60
|
+
startedAt: number;
|
|
61
|
+
updatedAt: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type CompletionStatusSurface = {
|
|
65
|
+
snapshotPresent: boolean;
|
|
66
|
+
statusText?: string;
|
|
67
|
+
widgetLines: string[];
|
|
68
|
+
currentPhase?: string;
|
|
69
|
+
sliceId?: string;
|
|
70
|
+
nextMandatoryRole?: string;
|
|
71
|
+
remainingContractCount?: number;
|
|
72
|
+
releaseBlockerCount?: number;
|
|
73
|
+
highValueGapCount?: number;
|
|
74
|
+
remainingStopJudgeCount?: number;
|
|
75
|
+
activeRole?: string;
|
|
76
|
+
livePreview?: string;
|
|
77
|
+
liveState?: "active" | "waiting" | "stalled";
|
|
78
|
+
liveIdleMs?: number;
|
|
79
|
+
liveToolActivity?: string;
|
|
80
|
+
liveAssistantSummary?: string;
|
|
81
|
+
liveProgress?: string;
|
|
82
|
+
liveRationale?: string;
|
|
83
|
+
liveNextStep?: string;
|
|
84
|
+
liveVerifying?: string;
|
|
85
|
+
liveStateDeltas?: string[];
|
|
86
|
+
liveDetailsLines?: string[];
|
|
87
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linimin/pi-letscook",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|