@fiale-plus/pi-rogue-bundle 0.1.12 → 0.1.14
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/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +23399 -23399
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +54 -13
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +16 -1
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +10 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +4 -37
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/package.json +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { registerAdvisor } from "./extension.js";
|
|
6
|
+
|
|
7
|
+
const testHome = vi.hoisted(() => `/tmp/pi-rogue-advisor-state-versioning-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
8
|
+
|
|
9
|
+
vi.mock("node:os", async () => {
|
|
10
|
+
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
11
|
+
return { ...actual, homedir: () => testHome };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.mock("@earendil-works/pi-ai", async () => {
|
|
15
|
+
const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
|
|
16
|
+
return { ...actual, completeSimple: vi.fn() };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type Handler = (event: any, ctx: any) => any;
|
|
20
|
+
type HandlerMap = Record<string, Handler[]>;
|
|
21
|
+
type CommandMap = Record<string, { handler: (args: string, ctx: any) => any }>;
|
|
22
|
+
|
|
23
|
+
function makeHandlers() {
|
|
24
|
+
const handlers: HandlerMap = {};
|
|
25
|
+
const commands: CommandMap = {};
|
|
26
|
+
const sendMessage = vi.fn();
|
|
27
|
+
const pi = {
|
|
28
|
+
on: (event: string, handler: Handler) => { handlers[event] ??= []; handlers[event].push(handler); },
|
|
29
|
+
registerMessageRenderer: () => undefined,
|
|
30
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => { commands[name] = command; },
|
|
31
|
+
registerTool: vi.fn(),
|
|
32
|
+
sendMessage,
|
|
33
|
+
sendUserMessage: () => undefined,
|
|
34
|
+
ui: { setStatus: () => undefined, notify: () => undefined },
|
|
35
|
+
};
|
|
36
|
+
return { handlers, commands, pi: pi as any, sendMessage };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
40
|
+
const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
|
|
41
|
+
const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
|
|
42
|
+
|
|
43
|
+
function readAdvisorState(): any {
|
|
44
|
+
return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mkCtx() {
|
|
48
|
+
return {
|
|
49
|
+
sessionManager: {
|
|
50
|
+
getSessionFile: () => join(homedir(), ".pi", "agent", "pi-rogue", "advisor", "session.jsonl"),
|
|
51
|
+
},
|
|
52
|
+
isIdle: () => true,
|
|
53
|
+
modelRegistry: {
|
|
54
|
+
find: (provider: string, model: string) => {
|
|
55
|
+
if (provider === "openai-codex") return { id: `${provider}/${model}`, provider, input: ["text"] };
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
getAvailable: () => [{ id: "provider/text-light", provider: "provider", input: ["text"] }],
|
|
59
|
+
getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "k", headers: {} }),
|
|
60
|
+
},
|
|
61
|
+
ui: { setStatus: () => undefined, notify: () => undefined },
|
|
62
|
+
} as any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("state versioning and recovery", () => {
|
|
66
|
+
let priorState: string | null = null;
|
|
67
|
+
let priorConfig: string | null = null;
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
|
|
71
|
+
priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
|
|
72
|
+
|
|
73
|
+
const setup = makeHandlers();
|
|
74
|
+
const { handlers, commands, pi } = setup;
|
|
75
|
+
handlers; commands;
|
|
76
|
+
|
|
77
|
+
mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
|
|
78
|
+
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
|
|
79
|
+
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
|
|
80
|
+
turns: 0,
|
|
81
|
+
lastTask: "",
|
|
82
|
+
notes: [],
|
|
83
|
+
files: [],
|
|
84
|
+
errors: [],
|
|
85
|
+
advisorCalls: 0,
|
|
86
|
+
cacheHits: 0,
|
|
87
|
+
followUp: "",
|
|
88
|
+
router: {},
|
|
89
|
+
checkin: { queued: false },
|
|
90
|
+
reviewControl: { status: "idle", pending: false, consumed: true, running: false },
|
|
91
|
+
}, null, 2), "utf8");
|
|
92
|
+
|
|
93
|
+
registerAdvisor(pi);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
if (priorState === null) {
|
|
98
|
+
writeFileSync(ADVISOR_STATE_PATH, "{}", "utf8");
|
|
99
|
+
} else {
|
|
100
|
+
writeFileSync(ADVISOR_STATE_PATH, priorState, "utf8");
|
|
101
|
+
}
|
|
102
|
+
if (priorConfig === null) {
|
|
103
|
+
writeFileSync(ADVISOR_CONFIG_PATH, "{}", "utf8");
|
|
104
|
+
} else {
|
|
105
|
+
writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("loads state with version field and preserves existing data", () => {
|
|
110
|
+
// Write a state without _v to simulate old state
|
|
111
|
+
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
|
|
112
|
+
turns: 0,
|
|
113
|
+
lastTask: "",
|
|
114
|
+
notes: [],
|
|
115
|
+
files: [],
|
|
116
|
+
errors: [],
|
|
117
|
+
advisorCalls: 0,
|
|
118
|
+
cacheHits: 0,
|
|
119
|
+
followUp: "",
|
|
120
|
+
router: {},
|
|
121
|
+
checkin: { queued: false },
|
|
122
|
+
reviewControl: { status: "idle", pending: false, consumed: true, running: false },
|
|
123
|
+
}, null, 2), "utf8");
|
|
124
|
+
|
|
125
|
+
// Load state directly (simulates what loadState does)
|
|
126
|
+
const raw = JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
127
|
+
expect(raw._v).toBeUndefined(); // Old state has no _v
|
|
128
|
+
|
|
129
|
+
// After loadState + saveState cycle, _v should be present
|
|
130
|
+
const setup = makeHandlers();
|
|
131
|
+
const { handlers: h, pi } = setup;
|
|
132
|
+
registerAdvisor(pi);
|
|
133
|
+
const ctx = mkCtx();
|
|
134
|
+
void h.session_start?.[0]?.({}, ctx);
|
|
135
|
+
|
|
136
|
+
const state = JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
137
|
+
// The session_start handler calls loadState() which adds _v, then saveState() writes it
|
|
138
|
+
expect(state._v).toBe(1);
|
|
139
|
+
expect(state.turns).toBe(0);
|
|
140
|
+
expect(state.lastTask).toBe("");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("recovers corrupted state gracefully", () => {
|
|
144
|
+
// Write corrupted state
|
|
145
|
+
writeFileSync(ADVISOR_STATE_PATH, "{ corrupted json", "utf8");
|
|
146
|
+
const handlers = makeHandlers();
|
|
147
|
+
const { handlers: h, pi } = handlers;
|
|
148
|
+
registerAdvisor(pi);
|
|
149
|
+
// Loading state should not throw
|
|
150
|
+
const ctx = mkCtx();
|
|
151
|
+
void h.session_start?.[0]?.({}, ctx);
|
|
152
|
+
// State should be recovered to default
|
|
153
|
+
const recovered = readAdvisorState();
|
|
154
|
+
expect(recovered._v).toBe(1);
|
|
155
|
+
expect(recovered.turns).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("migrates old state without _v field to current version", () => {
|
|
159
|
+
// Write state without _v field
|
|
160
|
+
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
|
|
161
|
+
turns: 5,
|
|
162
|
+
lastTask: "old task",
|
|
163
|
+
notes: [],
|
|
164
|
+
files: [],
|
|
165
|
+
errors: [],
|
|
166
|
+
advisorCalls: 3,
|
|
167
|
+
cacheHits: 1,
|
|
168
|
+
followUp: "",
|
|
169
|
+
router: {},
|
|
170
|
+
checkin: { queued: false },
|
|
171
|
+
reviewControl: { status: "idle", pending: false, consumed: true, running: false },
|
|
172
|
+
}, null, 2), "utf8");
|
|
173
|
+
|
|
174
|
+
const handlers = makeHandlers();
|
|
175
|
+
const { handlers: h, pi } = handlers;
|
|
176
|
+
registerAdvisor(pi);
|
|
177
|
+
const ctx = mkCtx();
|
|
178
|
+
void h.session_start?.[0]?.({}, ctx);
|
|
179
|
+
|
|
180
|
+
const migrated = readAdvisorState();
|
|
181
|
+
expect(migrated._v).toBe(1);
|
|
182
|
+
expect(migrated.turns).toBe(5);
|
|
183
|
+
expect(migrated.lastTask).toBe("old task");
|
|
184
|
+
expect(migrated.advisorCalls).toBe(3);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("handles missing state file by creating default", () => {
|
|
188
|
+
writeFileSync(ADVISOR_STATE_PATH, "{}", "utf8");
|
|
189
|
+
const handlers = makeHandlers();
|
|
190
|
+
const { handlers: h, pi } = handlers;
|
|
191
|
+
registerAdvisor(pi);
|
|
192
|
+
const ctx = mkCtx();
|
|
193
|
+
void h.session_start?.[0]?.({}, ctx);
|
|
194
|
+
|
|
195
|
+
const loaded = readAdvisorState();
|
|
196
|
+
expect(loaded._v).toBe(1);
|
|
197
|
+
expect(loaded.turns).toBe(0);
|
|
198
|
+
expect(loaded.reviewControl.status).toBe("idle");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("preserves reviewControl state across loads", () => {
|
|
202
|
+
const state = readAdvisorState();
|
|
203
|
+
state.reviewControl = {
|
|
204
|
+
status: "needed",
|
|
205
|
+
pending: true,
|
|
206
|
+
consumed: false,
|
|
207
|
+
running: false,
|
|
208
|
+
lastDecision: "review",
|
|
209
|
+
lastMaterialSignature: "test-sig",
|
|
210
|
+
lastReason: "test reason",
|
|
211
|
+
lastTrigger: "turn-1",
|
|
212
|
+
lastAppliedAt: new Date().toISOString(),
|
|
213
|
+
};
|
|
214
|
+
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
215
|
+
|
|
216
|
+
const handlers = makeHandlers();
|
|
217
|
+
const { handlers: h, pi } = handlers;
|
|
218
|
+
registerAdvisor(pi);
|
|
219
|
+
const ctx = mkCtx();
|
|
220
|
+
void h.session_start?.[0]?.({}, ctx);
|
|
221
|
+
|
|
222
|
+
const recovered = readAdvisorState();
|
|
223
|
+
expect(recovered.reviewControl.status).toBe("needed");
|
|
224
|
+
expect(recovered.reviewControl.pending).toBe(true);
|
|
225
|
+
expect(recovered.reviewControl.lastDecision).toBe("review");
|
|
226
|
+
});
|
|
227
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-bundle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Public Pi-Rogue bundle for advisor and orchestration. Single consolidated artefact (advisor and orchestration releases paused; their packages are private and bundled here).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|