@fiale-plus/pi-rogue 0.2.0 → 0.2.2

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.
Files changed (39) hide show
  1. package/README.md +2 -1
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +75 -31
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +2 -2
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +25 -4
  5. package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +4 -3
  6. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +38 -4
  7. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +52 -6
  8. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +10 -0
  9. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +17 -2
  10. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +2 -2
  11. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +11 -2
  12. package/node_modules/@fiale-plus/pi-rogue-router/README.md +32 -0
  13. package/node_modules/@fiale-plus/pi-rogue-router/package.json +30 -0
  14. package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.test.ts +84 -0
  15. package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.ts +355 -0
  16. package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +277 -0
  17. package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +34 -0
  18. package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +133 -0
  19. package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +168 -0
  20. package/node_modules/@fiale-plus/pi-rogue-router/src/dataset.ts +154 -0
  21. package/node_modules/@fiale-plus/pi-rogue-router/src/decision-ledger.test.ts +148 -0
  22. package/node_modules/@fiale-plus/pi-rogue-router/src/decision.ts +138 -0
  23. package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +139 -0
  24. package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +119 -0
  25. package/node_modules/@fiale-plus/pi-rogue-router/src/hash.ts +19 -0
  26. package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +15 -0
  27. package/node_modules/@fiale-plus/pi-rogue-router/src/learning.test.ts +241 -0
  28. package/node_modules/@fiale-plus/pi-rogue-router/src/learning.ts +382 -0
  29. package/node_modules/@fiale-plus/pi-rogue-router/src/ledger.ts +94 -0
  30. package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +119 -0
  31. package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +128 -0
  32. package/node_modules/@fiale-plus/pi-rogue-router/src/progress.ts +93 -0
  33. package/node_modules/@fiale-plus/pi-rogue-router/src/session-reader.ts +217 -0
  34. package/node_modules/@fiale-plus/pi-rogue-router/src/subagents.ts +178 -0
  35. package/node_modules/@fiale-plus/pi-rogue-router/src/types.ts +150 -0
  36. package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +293 -0
  37. package/package.json +5 -3
  38. package/src/extension.test.ts +1 -0
  39. package/src/extension.ts +2 -0
@@ -0,0 +1,154 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { decideRoute, readCheckpointJsonl } from "./decision.js";
4
+ import { readRouteEvents, type RouteEvent } from "./ledger.js";
5
+ import { readOutcomes, type RouterOutcome } from "./outcomes.js";
6
+ import type { RouterCheckpoint, RouteAction } from "./types.js";
7
+ import { readTeacherLabels, type TeacherLabel } from "./learning.js";
8
+
9
+ export const ROUTER_TRAINING_ROW_SCHEMA = "pi-router.training-row.v1" as const;
10
+
11
+ export type BinaryGateLabel = "continue" | "intervene" | "unknown";
12
+
13
+ export interface RouterTrainingRow {
14
+ schema: typeof ROUTER_TRAINING_ROW_SCHEMA;
15
+ checkpointId: string;
16
+ sessionId: string;
17
+ rawSessionRef: RouterCheckpoint["rawSessionRef"];
18
+ features: {
19
+ phase: RouterCheckpoint["phase"];
20
+ activeModel?: string;
21
+ provider?: string;
22
+ contextTokensApprox: number | null;
23
+ sameCommandRepeatedCount: number;
24
+ sameErrorRepeatedCount: number;
25
+ loopScore: number;
26
+ progressScore: number;
27
+ verifierUsed: boolean;
28
+ noVerifierUsed: boolean;
29
+ diffLines: number;
30
+ diffFilesChanged: number;
31
+ diffChurnScore: number;
32
+ filesTouched: number;
33
+ };
34
+ labels: {
35
+ routeAction: RouteAction | null;
36
+ binaryGate: BinaryGateLabel;
37
+ source: "teacher" | "human" | "outcome" | "local-rule" | "unknown";
38
+ confidence: number | null;
39
+ };
40
+ outcome: {
41
+ taskStatus: RouterOutcome["taskStatus"] | "unknown";
42
+ testsPassedAfter: boolean | null;
43
+ acceptedDiff: boolean | null;
44
+ userOverrodeDecision: boolean | null;
45
+ reworkTurns: number | null;
46
+ };
47
+ provenance: {
48
+ routeEventId?: string;
49
+ teacherLabelId?: string;
50
+ localRuleAction: RouteAction;
51
+ excludedLocalRuleAsTruth: boolean;
52
+ };
53
+ }
54
+
55
+ function routeToGate(action: RouteAction | null | undefined): BinaryGateLabel {
56
+ if (!action) return "unknown";
57
+ return action === "continue_current" || action === "continue_local" ? "continue" : "intervene";
58
+ }
59
+
60
+ function labelSource(label?: TeacherLabel): RouterTrainingRow["labels"]["source"] {
61
+ if (!label) return "unknown";
62
+ if (label.source === "local-rule") return "local-rule";
63
+ return label.teacher === "human" ? "human" : "teacher";
64
+ }
65
+
66
+ export function buildTrainingRows(options: {
67
+ checkpoints: RouterCheckpoint[];
68
+ routeEvents?: RouteEvent[];
69
+ outcomes?: RouterOutcome[];
70
+ labels?: TeacherLabel[];
71
+ includeLocalRuleLabels?: boolean;
72
+ }): RouterTrainingRow[] {
73
+ const eventByCheckpoint = new Map((options.routeEvents ?? []).map((event) => [event.checkpointId, event]));
74
+ const outcomeByCheckpoint = new Map((options.outcomes ?? []).flatMap((outcome) => outcome.checkpointId && !outcome.routeEventId ? [[outcome.checkpointId, outcome] as const] : []));
75
+ const outcomeByRouteEvent = new Map((options.outcomes ?? []).flatMap((outcome) => outcome.routeEventId ? [[outcome.routeEventId, outcome] as const] : []));
76
+ const labelByCheckpoint = new Map((options.labels ?? []).map((label) => [label.checkpointId, label]));
77
+
78
+ return options.checkpoints.map((checkpoint) => {
79
+ const routeEvent = eventByCheckpoint.get(checkpoint.checkpointId);
80
+ const outcome = (routeEvent ? outcomeByRouteEvent.get(routeEvent.eventId) : undefined) ?? outcomeByCheckpoint.get(checkpoint.checkpointId);
81
+ const teacherLabel = labelByCheckpoint.get(checkpoint.checkpointId);
82
+ const canUseLabel = Boolean(teacherLabel && (options.includeLocalRuleLabels || teacherLabel.source !== "local-rule"));
83
+ const routeAction = canUseLabel ? teacherLabel!.suggestedAction : null;
84
+ const ruleAction = decideRoute(checkpoint).action;
85
+ return {
86
+ schema: ROUTER_TRAINING_ROW_SCHEMA,
87
+ checkpointId: checkpoint.checkpointId,
88
+ sessionId: checkpoint.sessionId,
89
+ rawSessionRef: checkpoint.rawSessionRef,
90
+ features: {
91
+ phase: checkpoint.phase,
92
+ activeModel: checkpoint.activeModel,
93
+ provider: checkpoint.provider,
94
+ contextTokensApprox: checkpoint.features.contextTokensApprox,
95
+ sameCommandRepeatedCount: checkpoint.features.sameCommandRepeatedCount,
96
+ sameErrorRepeatedCount: checkpoint.features.sameErrorRepeatedCount,
97
+ loopScore: checkpoint.features.loopScore,
98
+ progressScore: checkpoint.features.progressScore,
99
+ verifierUsed: checkpoint.features.verifierUsed,
100
+ noVerifierUsed: checkpoint.features.noVerifierUsed,
101
+ diffLines: checkpoint.features.diffLines ?? 0,
102
+ diffFilesChanged: checkpoint.features.diffFilesChanged ?? 0,
103
+ diffChurnScore: checkpoint.features.diffChurnScore ?? 0,
104
+ filesTouched: checkpoint.features.filesTouched,
105
+ },
106
+ labels: {
107
+ routeAction,
108
+ binaryGate: routeToGate(routeAction),
109
+ source: canUseLabel ? labelSource(teacherLabel) : "unknown",
110
+ confidence: canUseLabel ? teacherLabel!.confidence : null,
111
+ },
112
+ outcome: {
113
+ taskStatus: outcome?.taskStatus ?? "unknown",
114
+ testsPassedAfter: outcome?.testsPassedAfter ?? null,
115
+ acceptedDiff: outcome?.acceptedDiff ?? null,
116
+ userOverrodeDecision: outcome?.userOverrodeDecision ?? null,
117
+ reworkTurns: outcome?.reworkTurns ?? null,
118
+ },
119
+ provenance: {
120
+ routeEventId: routeEvent?.eventId,
121
+ teacherLabelId: canUseLabel ? teacherLabel!.labelId : undefined,
122
+ localRuleAction: ruleAction,
123
+ excludedLocalRuleAsTruth: Boolean(teacherLabel?.source === "local-rule" && !options.includeLocalRuleLabels),
124
+ },
125
+ } satisfies RouterTrainingRow;
126
+ });
127
+ }
128
+
129
+ export function writeTrainingRows(options: {
130
+ checkpointPath: string;
131
+ outputPath: string;
132
+ eventsPath?: string;
133
+ outcomesPath?: string;
134
+ labelsPath?: string;
135
+ includeLocalRuleLabels?: boolean;
136
+ }): { schema: "pi-router.dataset-summary.v1"; output: string; rows: number; labeledRows: number } {
137
+ if (options.eventsPath && !existsSync(options.eventsPath)) throw new Error(`route events file not found: ${options.eventsPath}`);
138
+ const rows = buildTrainingRows({
139
+ checkpoints: readCheckpointJsonl(options.checkpointPath),
140
+ routeEvents: options.eventsPath ? readRouteEvents(options.eventsPath) : [],
141
+ outcomes: readOutcomes(options.outcomesPath),
142
+ labels: options.labelsPath ? readTeacherLabels(options.labelsPath) : [],
143
+ includeLocalRuleLabels: options.includeLocalRuleLabels,
144
+ });
145
+ const resolved = resolve(options.outputPath);
146
+ mkdirSync(dirname(resolved), { recursive: true });
147
+ writeFileSync(resolved, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length ? "\n" : ""));
148
+ return {
149
+ schema: "pi-router.dataset-summary.v1",
150
+ output: resolved,
151
+ rows: rows.length,
152
+ labeledRows: rows.filter((row) => row.labels.binaryGate !== "unknown").length,
153
+ };
154
+ }
@@ -0,0 +1,148 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { decideRoute, readCheckpointJsonl, selectCheckpoint } from "./decision.js";
6
+ import { appendRouteEvent, buildRouteEvent, readRouteEvents } from "./ledger.js";
7
+ import type { RouterCheckpoint } from "./types.js";
8
+
9
+ type CheckpointOverrides = Partial<Omit<RouterCheckpoint, "features" | "recent">> & {
10
+ features?: Partial<RouterCheckpoint["features"]>;
11
+ recent?: Partial<RouterCheckpoint["recent"]>;
12
+ };
13
+
14
+ function checkpoint(overrides: CheckpointOverrides = {}): RouterCheckpoint {
15
+ const base: RouterCheckpoint = {
16
+ schema: "pi-router.checkpoint.v1",
17
+ sessionId: "session-1",
18
+ checkpointId: "session-1:event-10",
19
+ createdAt: "2026-06-12T00:00:00.000Z",
20
+ rawSessionRef: {
21
+ schema: "pi-router.raw-session-ref.v1",
22
+ path: "/tmp/raw-session.jsonl",
23
+ fromEvent: 1,
24
+ toEvent: 10,
25
+ fromByte: 100,
26
+ toByte: 200,
27
+ contentHash: "hash-only",
28
+ },
29
+ harness: "pi",
30
+ repoHash: "repo-hash",
31
+ goalHash: "goal-hash",
32
+ phase: "debug",
33
+ activeModel: "local/qwen",
34
+ provider: "local",
35
+ features: {
36
+ turnIndex: 10,
37
+ sameCommandRepeatedCount: 2,
38
+ sameErrorRepeatedCount: 2,
39
+ errorChanged: false,
40
+ testsImproved: null,
41
+ filesTouched: 1,
42
+ diffLines: 12,
43
+ diffFilesChanged: 1,
44
+ diffLinesAdded: 8,
45
+ diffLinesDeleted: 4,
46
+ diffChurnScore: 0,
47
+ toolThrashScore: 0.25,
48
+ goalDriftScore: 0,
49
+ loopScore: 0.55,
50
+ progressScore: 0.45,
51
+ verifierUsed: true,
52
+ noVerifierUsed: false,
53
+ toolCallsLast10Turns: 4,
54
+ contextTokensApprox: 1234,
55
+ gitDirty: null,
56
+ },
57
+ recent: {
58
+ lastUserGoalHash: "goal-hash",
59
+ lastCommandHash: "command-hash",
60
+ lastErrorHash: "error-hash",
61
+ touchedFileHashes: ["file-hash"],
62
+ },
63
+ sourceEvent: {
64
+ index: 10,
65
+ byteStart: 100,
66
+ byteEnd: 200,
67
+ id: "event-id",
68
+ timestamp: "2026-06-12T00:00:01.000Z",
69
+ type: "message",
70
+ role: "toolResult",
71
+ },
72
+ };
73
+ return { ...base, ...overrides, features: { ...base.features, ...overrides.features }, recent: { ...base.recent, ...overrides.recent } };
74
+ }
75
+
76
+ describe("trajectory router decision and ledger", () => {
77
+ it("emits strict JSON decisions from conservative local-first rules", () => {
78
+ const decision = decideRoute(checkpoint());
79
+
80
+ expect(decision).toEqual({
81
+ schema: "pi-router.decision.v1",
82
+ checkpointId: "session-1:event-10",
83
+ action: "escalate_debug_diagnosis",
84
+ adviceShape: "debug_diagnosis",
85
+ contextPolicy: "focused_error_and_diff",
86
+ confidence: 0.82,
87
+ reason: "same error repeated in debug phase; ask stronger/different model for diagnosis",
88
+ policyVersion: "pi-router.rule-policy.v0",
89
+ });
90
+ });
91
+
92
+ it("keeps normal progress local/current", () => {
93
+ const decision = decideRoute(checkpoint({
94
+ phase: "implementation",
95
+ features: {
96
+ sameCommandRepeatedCount: 1,
97
+ sameErrorRepeatedCount: 0,
98
+ loopScore: 0.1,
99
+ progressScore: 0.8,
100
+ },
101
+ }));
102
+
103
+ expect(decision.action).toBe("continue_current");
104
+ expect(decision.adviceShape).toBe("none");
105
+ expect(decision.contextPolicy).toBe("none");
106
+ });
107
+
108
+ it("selects the last checkpoint by default or an explicit checkpoint id", () => {
109
+ const dir = mkdtempSync(join(tmpdir(), "pi-router-decision-"));
110
+ const file = join(dir, "checkpoints.jsonl");
111
+ const first = checkpoint({ checkpointId: "first" });
112
+ const second = checkpoint({ checkpointId: "second" });
113
+ writeFileSync(file, `${JSON.stringify(first)}\n${JSON.stringify(second)}\n`);
114
+
115
+ const checkpoints = readCheckpointJsonl(file);
116
+
117
+ expect(selectCheckpoint(checkpoints).checkpointId).toBe("second");
118
+ expect(selectCheckpoint(checkpoints, "first").checkpointId).toBe("first");
119
+ expect(() => selectCheckpoint(checkpoints, "missing")).toThrow(/checkpoint not found/);
120
+ });
121
+
122
+ it("appends route ledger events without raw transcript content", () => {
123
+ const dir = mkdtempSync(join(tmpdir(), "pi-router-ledger-"));
124
+ const file = join(dir, "events.jsonl");
125
+ const sensitiveText = "npm test src/secret.test.ts failed with API_TOKEN=abc";
126
+ const routeCheckpoint = checkpoint();
127
+ const decision = decideRoute(routeCheckpoint);
128
+ const event = buildRouteEvent(routeCheckpoint, decision, "2026-06-12T00:00:02.000Z");
129
+
130
+ appendRouteEvent(file, event);
131
+ const events = readRouteEvents(file);
132
+ const raw = readFileSync(file, "utf8");
133
+
134
+ expect(events).toHaveLength(1);
135
+ expect(events[0]).toMatchObject({
136
+ schema: "pi-router.route-event.v1",
137
+ checkpointId: routeCheckpoint.checkpointId,
138
+ sessionId: routeCheckpoint.sessionId,
139
+ decision,
140
+ runtime: { activeModel: "local/qwen", provider: "local", contextTokensApprox: 1234 },
141
+ observed: { followed: null },
142
+ });
143
+ expect(raw).not.toContain(sensitiveText);
144
+ expect(raw).not.toContain("npm test");
145
+ expect(raw).not.toContain("API_TOKEN");
146
+ expect(raw).toContain("hash-only");
147
+ });
148
+ });
@@ -0,0 +1,138 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { hashText } from "./hash.js";
3
+ import type { AdviceShape, ContextPolicy, RouteAction, RouteDecision, RouterCheckpoint } from "./types.js";
4
+
5
+ export const ROUTER_POLICY_VERSION = "pi-router.rule-policy.v0";
6
+ export const ROUTER_DECISION_SCHEMA = "pi-router.decision.v1" as const;
7
+
8
+ export interface DecideOptions {
9
+ policyVersion?: string;
10
+ }
11
+
12
+ interface RuleResult {
13
+ action: RouteAction;
14
+ adviceShape: AdviceShape;
15
+ contextPolicy: ContextPolicy;
16
+ confidence: number;
17
+ reason: string;
18
+ }
19
+
20
+ function clampConfidence(value: number): number {
21
+ return Math.max(0, Math.min(1, Number(value.toFixed(3))));
22
+ }
23
+
24
+ function hasLargeDiff(checkpoint: RouterCheckpoint): boolean {
25
+ return checkpoint.features.diffLines >= 400;
26
+ }
27
+
28
+ function isContextPressure(checkpoint: RouterCheckpoint): boolean {
29
+ const tokens = checkpoint.features.contextTokensApprox;
30
+ return typeof tokens === "number" && tokens >= 100_000;
31
+ }
32
+
33
+ function ruleFor(checkpoint: RouterCheckpoint): RuleResult {
34
+ const { features, phase } = checkpoint;
35
+
36
+ if (features.loopScore >= 0.9) {
37
+ return {
38
+ action: "stop_and_ask_user",
39
+ adviceShape: "none",
40
+ contextPolicy: "minimal",
41
+ confidence: clampConfidence(0.82 + features.loopScore * 0.12),
42
+ reason: "high loop score; stop before compounding repeated failures",
43
+ };
44
+ }
45
+
46
+ if (isContextPressure(checkpoint)) {
47
+ return {
48
+ action: "summarize_context",
49
+ adviceShape: "none",
50
+ contextPolicy: "session_summary",
51
+ confidence: 0.78,
52
+ reason: "context token pressure is high; summarize before continuing or escalating",
53
+ };
54
+ }
55
+
56
+ if (phase === "review" && hasLargeDiff(checkpoint)) {
57
+ return {
58
+ action: "escalate_diff_review",
59
+ adviceShape: "diff_review",
60
+ contextPolicy: "diff_only",
61
+ confidence: 0.76,
62
+ reason: "review phase with large diff; request focused diff review",
63
+ };
64
+ }
65
+
66
+ if (phase === "debug" && features.sameErrorRepeatedCount >= 2) {
67
+ return {
68
+ action: "escalate_debug_diagnosis",
69
+ adviceShape: "debug_diagnosis",
70
+ contextPolicy: "focused_error_and_diff",
71
+ confidence: clampConfidence(0.72 + Math.min(features.sameErrorRepeatedCount, 4) * 0.05),
72
+ reason: "same error repeated in debug phase; ask stronger/different model for diagnosis",
73
+ };
74
+ }
75
+
76
+ if (features.noVerifierUsed) {
77
+ return {
78
+ action: "run_verifier",
79
+ adviceShape: "none",
80
+ contextPolicy: "minimal",
81
+ confidence: 0.74,
82
+ reason: "multiple tool steps without verifier; run tests/checks before more edits",
83
+ };
84
+ }
85
+
86
+ if (features.loopScore >= 0.65) {
87
+ return {
88
+ action: "ask_micro_hint",
89
+ adviceShape: "micro_hint",
90
+ contextPolicy: "recent_events",
91
+ confidence: clampConfidence(0.62 + features.loopScore * 0.18),
92
+ reason: "moderate loop signal; request a cheap micro-hint while staying local-first",
93
+ };
94
+ }
95
+
96
+ return {
97
+ action: checkpoint.activeModel ? "continue_current" : "continue_local",
98
+ adviceShape: "none",
99
+ contextPolicy: "none",
100
+ confidence: clampConfidence(0.66 + features.progressScore * 0.18),
101
+ reason: "progress signals are acceptable; continue with the current/local worker",
102
+ };
103
+ }
104
+
105
+ export function decideRoute(checkpoint: RouterCheckpoint, options: DecideOptions = {}): RouteDecision {
106
+ const result = ruleFor(checkpoint);
107
+ return {
108
+ schema: ROUTER_DECISION_SCHEMA,
109
+ checkpointId: checkpoint.checkpointId,
110
+ action: result.action,
111
+ adviceShape: result.adviceShape,
112
+ contextPolicy: result.contextPolicy,
113
+ confidence: result.confidence,
114
+ reason: result.reason,
115
+ policyVersion: options.policyVersion ?? ROUTER_POLICY_VERSION,
116
+ };
117
+ }
118
+
119
+ export function readCheckpointJsonl(path: string): RouterCheckpoint[] {
120
+ return readFileSync(path, "utf8")
121
+ .split("\n")
122
+ .filter((line) => line.trim())
123
+ .map((line) => JSON.parse(line) as RouterCheckpoint);
124
+ }
125
+
126
+ export function selectCheckpoint(checkpoints: RouterCheckpoint[], checkpointId?: string): RouterCheckpoint {
127
+ const checkpoint = checkpointId
128
+ ? checkpoints.find((candidate) => candidate.checkpointId === checkpointId)
129
+ : checkpoints.at(-1);
130
+ if (!checkpoint) {
131
+ throw new Error(checkpointId ? `checkpoint not found: ${checkpointId}` : "checkpoint file contains no checkpoints");
132
+ }
133
+ return checkpoint;
134
+ }
135
+
136
+ export function decisionId(decision: RouteDecision, checkpoint: RouterCheckpoint): string {
137
+ return hashText(decision.policyVersion, decision.checkpointId, decision.action, checkpoint.rawSessionRef.contentHash);
138
+ }
@@ -0,0 +1,139 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ activeProfile,
4
+ cycleRouterProfile,
5
+ ensureRouterConfig,
6
+ formatProfile,
7
+ loadRouterConfig,
8
+ routerConfigPath,
9
+ routerEventsPath,
10
+ saveRouterConfig,
11
+ setRouterProfile,
12
+ type RouterConfig,
13
+ } from "./config.js";
14
+ import { observeRouterTurn } from "./observe.js";
15
+ import { routerArgumentCompletions } from "./completions.js";
16
+
17
+ function statusText(ctx: any, config: RouterConfig): string {
18
+ const profile = activeProfile(config);
19
+ return [
20
+ `router: ${config.enabled ? "observe on" : "off"}`,
21
+ `mode: ${config.mode}`,
22
+ `print: ${config.print}`,
23
+ `profile: ${config.activeProfile}`,
24
+ `worker: ${profile.worker}`,
25
+ `smart: ${profile.smart}`,
26
+ `teacher: ${profile.teacher}`,
27
+ `reviewer: ${profile.reviewer}`,
28
+ `config: ${routerConfigPath(ctx)}`,
29
+ `ledger: ${routerEventsPath(ctx)}`,
30
+ ].join("\n");
31
+ }
32
+
33
+ function notifyProfile(ctx: any, config: RouterConfig, prefix = "router profile"): void {
34
+ const profile = activeProfile(config);
35
+ ctx.ui.notify(`${prefix}: ${config.activeProfile}\nworker: ${profile.worker}\nsmart: ${profile.smart}\nteacher: ${profile.teacher}\nreviewer: ${profile.reviewer}`, "info");
36
+ }
37
+
38
+ function setEnabled(ctx: any, enabled: boolean): void {
39
+ const config = ensureRouterConfig(ctx);
40
+ const next = { ...config, enabled };
41
+ saveRouterConfig(ctx, next);
42
+ ctx.ui.notify(enabled ? "router observe mode enabled" : "router disabled", "info");
43
+ }
44
+
45
+ export function registerRouter(pi: ExtensionAPI): void {
46
+ const p = pi as any;
47
+ if (p.__piRogueRouterRegistered) return;
48
+ p.__piRogueRouterRegistered = true;
49
+
50
+ pi.registerCommand("router", {
51
+ description: "Observe-only trajectory router. Usage: /router on|off|status|profile|profiles|models|configure",
52
+ getArgumentCompletions: (prefix: string, ctx?: any) => routerArgumentCompletions(prefix, ctx),
53
+ handler: async (args, ctx) => {
54
+ const input = String(args ?? "").trim();
55
+ const [cmdRaw, ...rest] = input.split(/\s+/);
56
+ const cmd = cmdRaw || "status";
57
+
58
+ if (cmd === "on") {
59
+ setEnabled(ctx, true);
60
+ return;
61
+ }
62
+ if (cmd === "off") {
63
+ setEnabled(ctx, false);
64
+ return;
65
+ }
66
+ if (cmd === "configure" || cmd === "config") {
67
+ const config = ensureRouterConfig(ctx);
68
+ ctx.ui.notify(["router config ready", "", statusText(ctx, config)].join("\n"), "info");
69
+ return;
70
+ }
71
+
72
+ const config = ensureRouterConfig(ctx);
73
+ if (cmd === "status" || cmd === "show") {
74
+ ctx.ui.notify(statusText(ctx, config), "info");
75
+ return;
76
+ }
77
+ if (cmd === "models") {
78
+ notifyProfile(ctx, config, "router models");
79
+ return;
80
+ }
81
+ if (cmd === "profiles") {
82
+ ctx.ui.notify(config.profileOrder.map((name) => {
83
+ const marker = name === config.activeProfile ? "*" : " ";
84
+ const profile = config.profiles[name];
85
+ return `${marker} ${formatProfile(name, profile)}`;
86
+ }).join("\n"), "info");
87
+ return;
88
+ }
89
+ if (cmd === "profile") {
90
+ const name = rest[0];
91
+ if (!name) {
92
+ notifyProfile(ctx, config);
93
+ return;
94
+ }
95
+ const next = setRouterProfile(config, name);
96
+ if (!next) {
97
+ ctx.ui.notify(`unknown router profile: ${name}`, "error");
98
+ return;
99
+ }
100
+ saveRouterConfig(ctx, next);
101
+ notifyProfile(ctx, next, "router profile set");
102
+ return;
103
+ }
104
+ if (cmd === "cycle" || cmd === "next") {
105
+ const next = cycleRouterProfile(config, 1);
106
+ saveRouterConfig(ctx, next);
107
+ notifyProfile(ctx, next, "router profile cycled");
108
+ return;
109
+ }
110
+
111
+ ctx.ui.notify("Usage: /router on|off|status|profile [name]|profiles|models|configure|cycle", "error");
112
+ },
113
+ });
114
+
115
+ // Ctrl-P is reserved by Pi's built-in model cycle action, so the
116
+ // extension uses an unreserved chord and exposes `/router cycle` for
117
+ // command-palette/typed rotation over the same profile set.
118
+ pi.registerShortcut("ctrl+alt+p", {
119
+ description: "Cycle router profile",
120
+ handler: async (ctx: any) => {
121
+ const config = ensureRouterConfig(ctx);
122
+ const next = cycleRouterProfile(config, 1);
123
+ saveRouterConfig(ctx, next);
124
+ notifyProfile(ctx, next, "router profile cycled");
125
+ },
126
+ });
127
+
128
+ pi.on("turn_end", async (_event: any, ctx: any) => {
129
+ try {
130
+ await observeRouterTurn(ctx);
131
+ } catch (error) {
132
+ ctx.ui?.notify?.(`router observe failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
133
+ }
134
+ });
135
+ }
136
+
137
+ export default function routerExtension(pi: ExtensionAPI): void {
138
+ registerRouter(pi);
139
+ }