@fiale-plus/pi-rogue 0.2.1 → 0.2.3
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/README.md +2 -1
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +24 -5
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +119 -7
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +124 -16
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +32 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +32 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +37 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +39 -2
- package/node_modules/@fiale-plus/pi-rogue-router/README.md +34 -0
- package/node_modules/@fiale-plus/pi-rogue-router/package.json +30 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.test.ts +84 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.ts +363 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +277 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +34 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +165 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +193 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/dataset.ts +154 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision-ledger.test.ts +148 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision.ts +138 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +134 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/hash.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +15 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.test.ts +241 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.ts +382 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/ledger.ts +94 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +128 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/progress.ts +93 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/session-reader.ts +217 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/subagents.ts +178 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/types.ts +150 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +297 -0
- package/package.json +5 -3
- package/src/extension.test.ts +1 -0
- package/src/extension.ts +2 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, writeSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { hashMaybe, hashText, normalizeText } from "./hash.js";
|
|
4
|
+
import { routerSessionKey } from "./config.js";
|
|
5
|
+
import { diffChurnScore, EMPTY_DIFF_STATS, readGitDiffStats } from "./git-features.js";
|
|
6
|
+
import { touchedFileHashesFromEvent } from "./progress.js";
|
|
7
|
+
import { readPiSession, sessionIdFromPath, streamPiSessionEvents, type PiSession, type RawPiSessionEvent } from "./session-reader.js";
|
|
8
|
+
import { RAW_SESSION_REF_SCHEMA, ROUTER_CHECKPOINT_SCHEMA, type ProgressSignals, type RawSessionRef, type RouterCheckpoint, type SessionCommandEvent, type SessionToolResultEvent } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function textFromEvent(event: RawPiSessionEvent): string {
|
|
11
|
+
const message = event.raw.message;
|
|
12
|
+
if (!message || typeof message !== "object") return "";
|
|
13
|
+
const content = (message as { content?: unknown }).content;
|
|
14
|
+
if (!Array.isArray(content)) return "";
|
|
15
|
+
return content.flatMap((item) => {
|
|
16
|
+
if (!item || typeof item !== "object") return [];
|
|
17
|
+
const text = (item as Record<string, unknown>).text;
|
|
18
|
+
return typeof text === "string" ? [text] : [];
|
|
19
|
+
}).join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function phaseFromText(text: string): RouterCheckpoint["phase"] {
|
|
23
|
+
const normalized = normalizeText(text);
|
|
24
|
+
if (/\b(debug|bug|error|fail(?:ed|ing|ure)?|broken|crash|traceback|stack)\b/.test(normalized)) return "debug";
|
|
25
|
+
if (/\b(review|diff|pr|pull request|audit|looks good)\b/.test(normalized)) return "review";
|
|
26
|
+
if (/\b(research|docs?|look up|what is|compare|benchmark)\b/.test(normalized)) return "research";
|
|
27
|
+
if (/\b(install|config|configure|status|logs?|deploy|environment|shell)\b/.test(normalized)) return "ops";
|
|
28
|
+
if (/\b(plan|design|architecture|strategy|scope)\b/.test(normalized)) return "planning";
|
|
29
|
+
if (/\b(implement|build|add|edit|refactor|fix|write|change)\b/.test(normalized)) return "implementation";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function contextTokensFromUsage(usage: Record<string, unknown> | undefined): number | null {
|
|
34
|
+
if (!usage) return null;
|
|
35
|
+
const candidates = ["inputTokens", "input_tokens", "promptTokens", "prompt_tokens", "totalTokens", "total_tokens"];
|
|
36
|
+
for (const key of candidates) {
|
|
37
|
+
const value = usage[key];
|
|
38
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SessionContext {
|
|
44
|
+
id: string;
|
|
45
|
+
path: string;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rawSessionRef(session: SessionContext, refEvents: RawPiSessionEvent[], last: RawPiSessionEvent | undefined): RawSessionRef {
|
|
50
|
+
const first = refEvents[0];
|
|
51
|
+
const fromByte = first?.byteStart ?? 0;
|
|
52
|
+
const toByte = last?.byteEnd ?? 0;
|
|
53
|
+
return {
|
|
54
|
+
schema: RAW_SESSION_REF_SCHEMA,
|
|
55
|
+
path: session.path,
|
|
56
|
+
fromEvent: first?.index ?? 0,
|
|
57
|
+
toEvent: last?.index ?? 0,
|
|
58
|
+
fromByte,
|
|
59
|
+
toByte,
|
|
60
|
+
contentHash: hashText(...refEvents.map((event) => event.rawLineHash)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function repoHashFromCwd(cwd?: string): string | undefined {
|
|
65
|
+
return cwd ? hashText(resolve(cwd)) : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clamp01(value: number): number {
|
|
69
|
+
return Math.max(0, Math.min(1, value));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const RAW_REF_EVENT_WINDOW = 30;
|
|
73
|
+
|
|
74
|
+
interface BuildState {
|
|
75
|
+
activeModel?: string;
|
|
76
|
+
provider?: string;
|
|
77
|
+
contextTokensApprox: number | null;
|
|
78
|
+
lastUserGoalHash?: string;
|
|
79
|
+
phase: RouterCheckpoint["phase"];
|
|
80
|
+
lastCommandHash?: string;
|
|
81
|
+
sameCommandRepeatedCount: number;
|
|
82
|
+
lastErrorHash?: string;
|
|
83
|
+
previousErrorHash?: string;
|
|
84
|
+
lastErrorFingerprintHash?: string;
|
|
85
|
+
previousErrorFingerprintHash?: string;
|
|
86
|
+
sameErrorRepeatedCount: number;
|
|
87
|
+
verifierUsed: boolean;
|
|
88
|
+
commandCount: number;
|
|
89
|
+
recentCommands: string[];
|
|
90
|
+
touchedFileHashes: Set<string>;
|
|
91
|
+
diffStats: import("./types.js").DiffStats;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateCommandState(state: BuildState, command: SessionCommandEvent): void {
|
|
95
|
+
if (command.normalizedCommandHash) {
|
|
96
|
+
if (state.lastCommandHash === command.normalizedCommandHash) state.sameCommandRepeatedCount++;
|
|
97
|
+
else state.sameCommandRepeatedCount = 1;
|
|
98
|
+
state.lastCommandHash = command.normalizedCommandHash;
|
|
99
|
+
state.recentCommands.push(command.normalizedCommandHash);
|
|
100
|
+
state.recentCommands = state.recentCommands.slice(-10);
|
|
101
|
+
}
|
|
102
|
+
state.commandCount++;
|
|
103
|
+
state.verifierUsed = state.verifierUsed || command.isVerifier;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateToolResultState(state: BuildState, result: SessionToolResultEvent): void {
|
|
107
|
+
const errorKey = result.errorFingerprintHash ?? result.errorHash;
|
|
108
|
+
if (!errorKey) return;
|
|
109
|
+
state.previousErrorHash = state.lastErrorHash;
|
|
110
|
+
state.previousErrorFingerprintHash = state.lastErrorFingerprintHash;
|
|
111
|
+
if ((state.lastErrorFingerprintHash ?? state.lastErrorHash) === errorKey) state.sameErrorRepeatedCount++;
|
|
112
|
+
else state.sameErrorRepeatedCount = 1;
|
|
113
|
+
state.lastErrorHash = result.errorHash;
|
|
114
|
+
state.lastErrorFingerprintHash = result.errorFingerprintHash;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function signalsFromState(state: BuildState): ProgressSignals {
|
|
118
|
+
const uniqueRecentCommands = new Set(state.recentCommands);
|
|
119
|
+
const commandRepeatPressure = clamp01((state.sameCommandRepeatedCount - 1) / 3);
|
|
120
|
+
const errorRepeatPressure = clamp01((state.sameErrorRepeatedCount - 1) / 3);
|
|
121
|
+
const toolThrashScore = state.recentCommands.length === 0 ? 0 : clamp01(1 - uniqueRecentCommands.size / state.recentCommands.length);
|
|
122
|
+
const changedFiles = state.touchedFileHashes.size + state.diffStats.filesChanged;
|
|
123
|
+
const phaseWantsVerifier = state.phase === "implementation" || state.phase === "debug" || state.phase === "review";
|
|
124
|
+
const noVerifierUsed = phaseWantsVerifier && changedFiles > 0 && state.commandCount >= 4 && !state.verifierUsed;
|
|
125
|
+
const noVerifierPressure = noVerifierUsed ? 0.2 : 0;
|
|
126
|
+
const loopScore = clamp01(commandRepeatPressure * 0.35 + errorRepeatPressure * 0.4 + toolThrashScore * 0.2 + noVerifierPressure);
|
|
127
|
+
const progressScore = clamp01(1 - loopScore - (noVerifierUsed ? 0.1 : 0));
|
|
128
|
+
return {
|
|
129
|
+
sameCommandRepeatedCount: state.sameCommandRepeatedCount,
|
|
130
|
+
sameErrorRepeatedCount: state.sameErrorRepeatedCount,
|
|
131
|
+
errorChanged: Boolean(
|
|
132
|
+
(state.lastErrorFingerprintHash ?? state.lastErrorHash)
|
|
133
|
+
&& (state.previousErrorFingerprintHash ?? state.previousErrorHash)
|
|
134
|
+
&& (state.lastErrorFingerprintHash ?? state.lastErrorHash) !== (state.previousErrorFingerprintHash ?? state.previousErrorHash),
|
|
135
|
+
),
|
|
136
|
+
testsImproved: null,
|
|
137
|
+
filesTouched: state.touchedFileHashes.size,
|
|
138
|
+
diffLines: state.diffStats.totalLines,
|
|
139
|
+
diffFilesChanged: state.diffStats.filesChanged,
|
|
140
|
+
diffLinesAdded: state.diffStats.linesAdded,
|
|
141
|
+
diffLinesDeleted: state.diffStats.linesDeleted,
|
|
142
|
+
diffChurnScore: diffChurnScore(state.diffStats),
|
|
143
|
+
toolThrashScore,
|
|
144
|
+
goalDriftScore: 0,
|
|
145
|
+
loopScore,
|
|
146
|
+
progressScore,
|
|
147
|
+
verifierUsed: state.verifierUsed,
|
|
148
|
+
noVerifierUsed,
|
|
149
|
+
toolCallsLast10Turns: state.recentCommands.length,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkpointFromState(session: SessionContext, event: RawPiSessionEvent, refEvents: RawPiSessionEvent[], state: BuildState): RouterCheckpoint {
|
|
154
|
+
const signals = signalsFromState(state);
|
|
155
|
+
return {
|
|
156
|
+
schema: ROUTER_CHECKPOINT_SCHEMA,
|
|
157
|
+
sessionId: session.id,
|
|
158
|
+
checkpointId: `${session.id}:event-${event.index}`,
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
rawSessionRef: rawSessionRef(session, refEvents, event),
|
|
161
|
+
harness: "pi",
|
|
162
|
+
repoHash: repoHashFromCwd(session.cwd),
|
|
163
|
+
goalHash: state.lastUserGoalHash,
|
|
164
|
+
phase: state.phase,
|
|
165
|
+
activeModel: state.activeModel,
|
|
166
|
+
provider: state.provider,
|
|
167
|
+
features: {
|
|
168
|
+
...signals,
|
|
169
|
+
turnIndex: event.index,
|
|
170
|
+
contextTokensApprox: state.contextTokensApprox,
|
|
171
|
+
gitDirty: null,
|
|
172
|
+
},
|
|
173
|
+
recent: {
|
|
174
|
+
lastUserGoalHash: state.lastUserGoalHash,
|
|
175
|
+
lastCommandHash: state.lastCommandHash,
|
|
176
|
+
lastErrorHash: state.lastErrorHash,
|
|
177
|
+
lastErrorFingerprintHash: state.lastErrorFingerprintHash,
|
|
178
|
+
touchedFileHashes: [...state.touchedFileHashes].sort(),
|
|
179
|
+
diffFileHashes: state.diffStats.fileHashes,
|
|
180
|
+
},
|
|
181
|
+
sourceEvent: event.pointer,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function initialBuildState(): BuildState {
|
|
186
|
+
return {
|
|
187
|
+
contextTokensApprox: null,
|
|
188
|
+
phase: "unknown",
|
|
189
|
+
sameCommandRepeatedCount: 0,
|
|
190
|
+
sameErrorRepeatedCount: 0,
|
|
191
|
+
verifierUsed: false,
|
|
192
|
+
commandCount: 0,
|
|
193
|
+
recentCommands: [],
|
|
194
|
+
touchedFileHashes: new Set(),
|
|
195
|
+
diffStats: EMPTY_DIFF_STATS,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function updateStateFromEvent(state: BuildState, event: RawPiSessionEvent): void {
|
|
200
|
+
state.activeModel = event.model ?? state.activeModel;
|
|
201
|
+
state.provider = event.provider ?? state.provider;
|
|
202
|
+
state.contextTokensApprox = contextTokensFromUsage(event.usage) ?? state.contextTokensApprox;
|
|
203
|
+
|
|
204
|
+
if (event.role === "user") {
|
|
205
|
+
const text = textFromEvent(event);
|
|
206
|
+
state.lastUserGoalHash = hashMaybe(text);
|
|
207
|
+
state.phase = phaseFromText(text);
|
|
208
|
+
}
|
|
209
|
+
for (const fileHash of touchedFileHashesFromEvent(event)) state.touchedFileHashes.add(fileHash);
|
|
210
|
+
for (const command of event.commandEvents) updateCommandState(state, command);
|
|
211
|
+
if (event.toolResult) updateToolResultState(state, event.toolResult);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isCheckpointEvent(event: RawPiSessionEvent): boolean {
|
|
215
|
+
return event.role === "user" || event.role === "assistant" || event.role === "toolResult";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pushRefWindow(refEvents: RawPiSessionEvent[], event: RawPiSessionEvent): void {
|
|
219
|
+
refEvents.push(event);
|
|
220
|
+
if (refEvents.length > RAW_REF_EVENT_WINDOW) refEvents.shift();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function* iterateCheckpoints(session: PiSession): Generator<RouterCheckpoint> {
|
|
224
|
+
const state = initialBuildState();
|
|
225
|
+
const refEvents: RawPiSessionEvent[] = [];
|
|
226
|
+
for (const event of session.events) {
|
|
227
|
+
pushRefWindow(refEvents, event);
|
|
228
|
+
updateStateFromEvent(state, event);
|
|
229
|
+
if (!isCheckpointEvent(event)) continue;
|
|
230
|
+
yield checkpointFromState(session, event, refEvents, state);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function* streamCheckpointsFromSessionPath(sessionPath: string): AsyncGenerator<RouterCheckpoint> {
|
|
235
|
+
const session: SessionContext = { id: sessionIdFromPath(resolve(sessionPath)), path: resolve(sessionPath) };
|
|
236
|
+
const state = initialBuildState();
|
|
237
|
+
const refEvents: RawPiSessionEvent[] = [];
|
|
238
|
+
for await (const event of streamPiSessionEvents(session.path)) {
|
|
239
|
+
if (event.raw.type === "session" && typeof event.raw.cwd === "string") session.cwd = event.raw.cwd;
|
|
240
|
+
pushRefWindow(refEvents, event);
|
|
241
|
+
updateStateFromEvent(state, event);
|
|
242
|
+
if (!isCheckpointEvent(event)) continue;
|
|
243
|
+
yield checkpointFromState(session, event, refEvents, state);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function clampFeature(value: number): number {
|
|
248
|
+
return Math.max(0, Math.min(1, Number(value.toFixed(3))));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function checkpointWithDiffStats(checkpoint: RouterCheckpoint, cwd?: string, excludePaths: string[] = []): RouterCheckpoint {
|
|
252
|
+
const stats = readGitDiffStats(cwd, { excludePaths });
|
|
253
|
+
if (stats.filesChanged === 0) return checkpoint;
|
|
254
|
+
const phaseWantsVerifier = checkpoint.phase === "implementation" || checkpoint.phase === "debug" || checkpoint.phase === "review";
|
|
255
|
+
const noVerifierUsed = checkpoint.features.noVerifierUsed
|
|
256
|
+
|| (phaseWantsVerifier && !checkpoint.features.verifierUsed && checkpoint.features.toolCallsLast10Turns >= 4);
|
|
257
|
+
const loopScore = noVerifierUsed && !checkpoint.features.noVerifierUsed
|
|
258
|
+
? clampFeature(checkpoint.features.loopScore + 0.2)
|
|
259
|
+
: checkpoint.features.loopScore;
|
|
260
|
+
const progressScore = noVerifierUsed && !checkpoint.features.noVerifierUsed
|
|
261
|
+
? clampFeature(checkpoint.features.progressScore - 0.1)
|
|
262
|
+
: checkpoint.features.progressScore;
|
|
263
|
+
return {
|
|
264
|
+
...checkpoint,
|
|
265
|
+
features: {
|
|
266
|
+
...checkpoint.features,
|
|
267
|
+
diffLines: stats.totalLines,
|
|
268
|
+
diffFilesChanged: stats.filesChanged,
|
|
269
|
+
diffLinesAdded: stats.linesAdded,
|
|
270
|
+
diffLinesDeleted: stats.linesDeleted,
|
|
271
|
+
diffChurnScore: diffChurnScore(stats),
|
|
272
|
+
noVerifierUsed,
|
|
273
|
+
loopScore,
|
|
274
|
+
progressScore,
|
|
275
|
+
},
|
|
276
|
+
recent: { ...checkpoint.recent, diffFileHashes: stats.fileHashes },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function applyWorkspaceDiffToLatest(checkpoints: RouterCheckpoint[], cwd?: string, excludePaths: string[] = []): RouterCheckpoint[] {
|
|
281
|
+
if (checkpoints.length === 0) return checkpoints;
|
|
282
|
+
const next = [...checkpoints];
|
|
283
|
+
next[next.length - 1] = checkpointWithDiffStats(next[next.length - 1], cwd, excludePaths);
|
|
284
|
+
return next;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function buildCheckpoints(session: PiSession): RouterCheckpoint[] {
|
|
288
|
+
return [...iterateCheckpoints(session)];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function rebuildCheckpointsFromSession(sessionPath: string): RouterCheckpoint[] {
|
|
292
|
+
return buildCheckpoints(readPiSession(sessionPath));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function writeCheckpointsJsonl(checkpoints: RouterCheckpoint[], outputPath: string): void {
|
|
296
|
+
const resolved = resolve(outputPath);
|
|
297
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
298
|
+
const fd = openSync(resolved, "w");
|
|
299
|
+
try {
|
|
300
|
+
for (const checkpoint of checkpoints) writeSync(fd, `${JSON.stringify(checkpoint)}\n`);
|
|
301
|
+
} finally {
|
|
302
|
+
closeSync(fd);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface SessionCheckpointWriteSummary {
|
|
307
|
+
sessions: string[];
|
|
308
|
+
output: string;
|
|
309
|
+
checkpoints: number;
|
|
310
|
+
firstCheckpointId?: string;
|
|
311
|
+
lastCheckpointId?: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function writeSessionCheckpointsJsonl(sessionPaths: string[], outputPath: string, options: { workspaceDiff?: boolean } = {}): Promise<SessionCheckpointWriteSummary> {
|
|
315
|
+
if (options.workspaceDiff && sessionPaths.length !== 1) {
|
|
316
|
+
throw new Error("--workspace-diff can only be used with exactly one current session");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const resolved = resolve(outputPath);
|
|
320
|
+
// Compute live workspace diff before opening/truncating the output so the output artifact cannot count itself.
|
|
321
|
+
const workspaceDiffCheckpoints = options.workspaceDiff
|
|
322
|
+
? (() => {
|
|
323
|
+
const session = readPiSession(sessionPaths[0]);
|
|
324
|
+
const routerDir = session.cwd ? resolve(session.cwd, ".pi", "router") : undefined;
|
|
325
|
+
const routerSessionDir = routerDir ? resolve(routerDir, "sessions", routerSessionKey(session.path)) : undefined;
|
|
326
|
+
const routerArtifacts = routerDir ? [
|
|
327
|
+
routerDir,
|
|
328
|
+
resolve(routerDir, "config.json"),
|
|
329
|
+
resolve(routerDir, "state.json"),
|
|
330
|
+
resolve(routerDir, "events.jsonl"),
|
|
331
|
+
...(routerSessionDir ? [resolve(routerSessionDir, "state.json"), resolve(routerSessionDir, "events.jsonl")] : []),
|
|
332
|
+
] : [];
|
|
333
|
+
return applyWorkspaceDiffToLatest(buildCheckpoints(session), session.cwd, [session.path, resolved, ...routerArtifacts]);
|
|
334
|
+
})()
|
|
335
|
+
: null;
|
|
336
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
337
|
+
const fd = openSync(resolved, "w");
|
|
338
|
+
let checkpoints = 0;
|
|
339
|
+
let firstCheckpointId: string | undefined;
|
|
340
|
+
let lastCheckpointId: string | undefined;
|
|
341
|
+
try {
|
|
342
|
+
if (workspaceDiffCheckpoints) {
|
|
343
|
+
for (const checkpoint of workspaceDiffCheckpoints) {
|
|
344
|
+
firstCheckpointId ??= checkpoint.checkpointId;
|
|
345
|
+
lastCheckpointId = checkpoint.checkpointId;
|
|
346
|
+
checkpoints++;
|
|
347
|
+
writeSync(fd, `${JSON.stringify(checkpoint)}\n`);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
for (const sessionPath of sessionPaths) {
|
|
351
|
+
for await (const checkpoint of streamCheckpointsFromSessionPath(sessionPath)) {
|
|
352
|
+
firstCheckpointId ??= checkpoint.checkpointId;
|
|
353
|
+
lastCheckpointId = checkpoint.checkpointId;
|
|
354
|
+
checkpoints++;
|
|
355
|
+
writeSync(fd, `${JSON.stringify(checkpoint)}\n`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
closeSync(fd);
|
|
361
|
+
}
|
|
362
|
+
return { sessions: sessionPaths, output: resolved, checkpoints, firstCheckpointId, lastCheckpointId };
|
|
363
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { decideRoute, readCheckpointJsonl, selectCheckpoint } from "./decision.js";
|
|
5
|
+
import { writeSessionCheckpointsJsonl } from "./checkpoints.js";
|
|
6
|
+
import { appendRouteEvent, buildRouteEvent } from "./ledger.js";
|
|
7
|
+
import { writeCapabilityCards, writeShadowEval, writeTeacherPromptRequests, writeTeacherReflection } from "./learning.js";
|
|
8
|
+
import { writeTrainingRows } from "./dataset.js";
|
|
9
|
+
import { writeInferredOutcomes } from "./outcomes.js";
|
|
10
|
+
|
|
11
|
+
interface Args {
|
|
12
|
+
command?: string;
|
|
13
|
+
sessions: string[];
|
|
14
|
+
sessionDir?: string;
|
|
15
|
+
output?: string;
|
|
16
|
+
checkpointFile?: string;
|
|
17
|
+
checkpointId?: string;
|
|
18
|
+
ledger?: string;
|
|
19
|
+
events?: string;
|
|
20
|
+
labels?: string;
|
|
21
|
+
reflection?: string;
|
|
22
|
+
teacher?: string;
|
|
23
|
+
teacherOutput?: string;
|
|
24
|
+
teacherPrompts?: string;
|
|
25
|
+
outcomes?: string;
|
|
26
|
+
includeLocalRuleLabels?: boolean;
|
|
27
|
+
workspaceDiff?: boolean;
|
|
28
|
+
pretty: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usage(): never {
|
|
32
|
+
console.error(`Usage:
|
|
33
|
+
npm run router:rebuild -- --session <session.jsonl> [--session <session2.jsonl>] [--output <path>] [--workspace-diff] [--pretty]
|
|
34
|
+
npm run router:rebuild -- --session-dir <dir> [--output <path>] [--workspace-diff] [--pretty]
|
|
35
|
+
npm run router:decide -- --checkpoint-file <checkpoints.jsonl> [--checkpoint-id <id>] [--ledger <events.jsonl>] [--pretty]
|
|
36
|
+
npm run router:cards -- --events <events.jsonl> --output <model-cards.jsonl> [--outcomes <outcomes.jsonl>] [--pretty]
|
|
37
|
+
npm run router:outcomes -- --checkpoint-file <checkpoints.jsonl> --events <events.jsonl> --output <outcomes.jsonl> [--pretty]
|
|
38
|
+
npm run router:teacher-requests -- --checkpoint-file <checkpoints.jsonl> --output <requests.jsonl> [--teacher openai-codex/gpt-5.5] [--pretty]
|
|
39
|
+
npm run router:reflect -- --checkpoint-file <checkpoints.jsonl> --labels <labels.jsonl> --reflection <reflection.md> [--teacher local-rule] [--teacher-output <decisions.jsonl>] [--teacher-prompts <requests.jsonl>] [--pretty]
|
|
40
|
+
npm run router:dataset -- --checkpoint-file <checkpoints.jsonl> --output <training.jsonl> [--events <events.jsonl>] [--outcomes <outcomes.jsonl>] [--labels <labels.jsonl>] [--include-local-rule-labels] [--pretty]
|
|
41
|
+
npm run router:shadow -- --checkpoint-file <checkpoints.jsonl> --output <report.json> [--ledger <events.jsonl>] [--pretty]
|
|
42
|
+
|
|
43
|
+
Commands:
|
|
44
|
+
rebuild Rebuild derived router checkpoints from raw Pi session JSONL files.
|
|
45
|
+
decide Emit a strict JSON route decision for a checkpoint and optionally append a route event.
|
|
46
|
+
cards Generate local observed model capability cards from route events and optional outcomes.
|
|
47
|
+
outcomes Infer conservative outcome skeletons that can be manually enriched.
|
|
48
|
+
teacher-requests Generate local JSONL prompt requests for explicit teacher labeling.
|
|
49
|
+
reflect Generate command-triggered soft routing labels and a reflection artifact.
|
|
50
|
+
dataset Export trainable rows for a conservative continue-vs-intervene gate.
|
|
51
|
+
shadow Shadow-evaluate the current rule policy over historical checkpoints.
|
|
52
|
+
`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv: string[]): Args {
|
|
57
|
+
const args: Args = { command: argv[0], sessions: [], pretty: false };
|
|
58
|
+
for (let index = 1; index < argv.length; index++) {
|
|
59
|
+
const arg = argv[index];
|
|
60
|
+
const next = argv[index + 1];
|
|
61
|
+
if (arg === "--help" || arg === "-h") usage();
|
|
62
|
+
if (arg === "--session" && next) {
|
|
63
|
+
args.sessions.push(next);
|
|
64
|
+
index++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === "--session-dir" && next) {
|
|
68
|
+
args.sessionDir = next;
|
|
69
|
+
index++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--output" && next) {
|
|
73
|
+
args.output = next;
|
|
74
|
+
index++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--checkpoint-file" && next) {
|
|
78
|
+
args.checkpointFile = next;
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--checkpoint-id" && next) {
|
|
83
|
+
args.checkpointId = next;
|
|
84
|
+
index++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === "--ledger" && next) {
|
|
88
|
+
args.ledger = next;
|
|
89
|
+
index++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === "--events" && next) {
|
|
93
|
+
args.events = next;
|
|
94
|
+
index++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg === "--labels" && next) {
|
|
98
|
+
args.labels = next;
|
|
99
|
+
index++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg === "--reflection" && next) {
|
|
103
|
+
args.reflection = next;
|
|
104
|
+
index++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg === "--teacher" && next) {
|
|
108
|
+
args.teacher = next;
|
|
109
|
+
index++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--teacher-output" && next) {
|
|
113
|
+
args.teacherOutput = next;
|
|
114
|
+
index++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (arg === "--teacher-prompts" && next) {
|
|
118
|
+
args.teacherPrompts = next;
|
|
119
|
+
index++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg === "--outcomes" && next) {
|
|
123
|
+
args.outcomes = next;
|
|
124
|
+
index++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "--include-local-rule-labels") {
|
|
128
|
+
args.includeLocalRuleLabels = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (arg === "--workspace-diff") {
|
|
132
|
+
args.workspaceDiff = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--pretty") {
|
|
136
|
+
args.pretty = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
usage();
|
|
140
|
+
}
|
|
141
|
+
return args;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function defaultOutput(): string {
|
|
145
|
+
return join(process.cwd(), ".pi", "router", "checkpoints.jsonl");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sessionFilesFromDir(dir: string): string[] {
|
|
149
|
+
const resolved = resolve(dir);
|
|
150
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
151
|
+
throw new Error(`--session-dir is not a directory: ${dir}`);
|
|
152
|
+
}
|
|
153
|
+
const files: string[] = [];
|
|
154
|
+
const visit = (current: string): void => {
|
|
155
|
+
for (const entry of readdirSync(current)) {
|
|
156
|
+
const path = join(current, entry);
|
|
157
|
+
const stat = statSync(path);
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
visit(path);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (stat.isFile() && path.endsWith(".jsonl")) files.push(path);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
visit(resolved);
|
|
166
|
+
return files.sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveSessions(args: Args): string[] {
|
|
170
|
+
const sessions = [...args.sessions];
|
|
171
|
+
if (args.sessionDir) sessions.push(...sessionFilesFromDir(args.sessionDir));
|
|
172
|
+
return [...new Set(sessions.map((session) => resolve(session)))];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function rebuild(args: Args): Promise<unknown> {
|
|
176
|
+
const sessions = resolveSessions(args);
|
|
177
|
+
if (sessions.length === 0) usage();
|
|
178
|
+
const output = args.output ?? defaultOutput();
|
|
179
|
+
const result = await writeSessionCheckpointsJsonl(sessions, output, { workspaceDiff: args.workspaceDiff });
|
|
180
|
+
return {
|
|
181
|
+
schema: "pi-router.rebuild-summary.v1",
|
|
182
|
+
sessions: result.sessions,
|
|
183
|
+
output: result.output,
|
|
184
|
+
checkpoints: result.checkpoints,
|
|
185
|
+
firstCheckpointId: result.firstCheckpointId,
|
|
186
|
+
lastCheckpointId: result.lastCheckpointId,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function decide(args: Args): unknown {
|
|
191
|
+
if (!args.checkpointFile) usage();
|
|
192
|
+
const checkpoints = readCheckpointJsonl(args.checkpointFile);
|
|
193
|
+
const checkpoint = selectCheckpoint(checkpoints, args.checkpointId);
|
|
194
|
+
const decision = decideRoute(checkpoint);
|
|
195
|
+
if (args.ledger) appendRouteEvent(args.ledger, buildRouteEvent(checkpoint, decision));
|
|
196
|
+
return decision;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cards(args: Args): unknown {
|
|
200
|
+
if (!args.events || !args.output) usage();
|
|
201
|
+
const cards = writeCapabilityCards(args.events, args.output, args.outcomes);
|
|
202
|
+
return { schema: "pi-router.cards-summary.v1", output: resolve(args.output), cards: cards.length };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function outcomes(args: Args): unknown {
|
|
206
|
+
if (!args.checkpointFile || !args.events || !args.output) usage();
|
|
207
|
+
return writeInferredOutcomes({ checkpointPath: args.checkpointFile, eventsPath: args.events, outputPath: args.output });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function teacherRequests(args: Args): unknown {
|
|
211
|
+
if (!args.checkpointFile || !args.output) usage();
|
|
212
|
+
const requests = writeTeacherPromptRequests(args.checkpointFile, args.output, args.teacher ?? "openai-codex/gpt-5.5");
|
|
213
|
+
return { schema: "pi-router.teacher-requests-summary.v1", output: resolve(args.output), requests: requests.length, teacher: args.teacher ?? "openai-codex/gpt-5.5" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function reflect(args: Args): unknown {
|
|
217
|
+
if (!args.checkpointFile || !args.labels || !args.reflection) usage();
|
|
218
|
+
const result = writeTeacherReflection({
|
|
219
|
+
checkpointPath: args.checkpointFile,
|
|
220
|
+
labelsPath: args.labels,
|
|
221
|
+
reflectionPath: args.reflection,
|
|
222
|
+
teacher: args.teacher ?? "local-rule",
|
|
223
|
+
teacherOutputPath: args.teacherOutput,
|
|
224
|
+
teacherPromptPath: args.teacherPrompts,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
schema: "pi-router.reflect-summary.v1",
|
|
228
|
+
labels: resolve(args.labels),
|
|
229
|
+
reflection: resolve(args.reflection),
|
|
230
|
+
labelCount: result.labels.length,
|
|
231
|
+
teacher: args.teacher ?? "local-rule",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function dataset(args: Args): unknown {
|
|
236
|
+
if (!args.checkpointFile || !args.output) usage();
|
|
237
|
+
return writeTrainingRows({
|
|
238
|
+
checkpointPath: args.checkpointFile,
|
|
239
|
+
outputPath: args.output,
|
|
240
|
+
eventsPath: args.events,
|
|
241
|
+
outcomesPath: args.outcomes,
|
|
242
|
+
labelsPath: args.labels,
|
|
243
|
+
includeLocalRuleLabels: args.includeLocalRuleLabels,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function shadow(args: Args): unknown {
|
|
248
|
+
if (!args.checkpointFile || !args.output) usage();
|
|
249
|
+
return writeShadowEval(args.checkpointFile, args.output, args.ledger);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function main(): Promise<void> {
|
|
253
|
+
const args = parseArgs(process.argv.slice(2));
|
|
254
|
+
const result = args.command === "rebuild"
|
|
255
|
+
? await rebuild(args)
|
|
256
|
+
: args.command === "decide"
|
|
257
|
+
? decide(args)
|
|
258
|
+
: args.command === "cards"
|
|
259
|
+
? cards(args)
|
|
260
|
+
: args.command === "outcomes"
|
|
261
|
+
? outcomes(args)
|
|
262
|
+
: args.command === "teacher-requests"
|
|
263
|
+
? teacherRequests(args)
|
|
264
|
+
: args.command === "reflect"
|
|
265
|
+
? reflect(args)
|
|
266
|
+
: args.command === "dataset"
|
|
267
|
+
? dataset(args)
|
|
268
|
+
: args.command === "shadow"
|
|
269
|
+
? shadow(args)
|
|
270
|
+
: usage();
|
|
271
|
+
console.log(args.pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main().catch((error: unknown) => {
|
|
275
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_ROUTER_CONFIG, loadRouterConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
type CompletionItem = { value: string; label: string; description?: string };
|
|
4
|
+
|
|
5
|
+
function item(value: string, description?: string): CompletionItem {
|
|
6
|
+
return { value, label: value, ...(description ? { description } : {}) };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function filter(items: CompletionItem[], prefix: string): CompletionItem[] | null {
|
|
10
|
+
const q = prefix.trimStart().toLowerCase();
|
|
11
|
+
const out = q ? items.filter((entry) => entry.value.toLowerCase().startsWith(q)) : items;
|
|
12
|
+
return out.length ? out : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function routerArgumentCompletions(prefix: string, ctx?: any): CompletionItem[] | null {
|
|
16
|
+
const trimmed = prefix.trimStart();
|
|
17
|
+
const [cmd, rest = ""] = trimmed.split(/\s+/, 2);
|
|
18
|
+
const top = [
|
|
19
|
+
item("on", "enable observe-only router summaries"),
|
|
20
|
+
item("off", "disable router summaries"),
|
|
21
|
+
item("status", "show router state and active profile"),
|
|
22
|
+
item("profile", "show or set active router profile"),
|
|
23
|
+
item("profiles", "list router profiles"),
|
|
24
|
+
item("models", "show active role to model mapping"),
|
|
25
|
+
item("configure", "write default local config if missing"),
|
|
26
|
+
item("cycle", "cycle to the next router profile"),
|
|
27
|
+
];
|
|
28
|
+
if (!cmd || !trimmed.includes(" ")) return filter(top, trimmed);
|
|
29
|
+
if (cmd === "profile") {
|
|
30
|
+
const config = ctx ? loadRouterConfig(ctx) : DEFAULT_ROUTER_CONFIG;
|
|
31
|
+
return filter(config.profileOrder.map((name) => item(`profile ${name}`, config.profiles[name]?.worker)), `profile ${rest}`);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|