@fiale-plus/pi-rogue-bundle 0.1.9 → 0.1.10
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 +24 -13
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +10 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { resetAdvisorSessionContext, setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
6
|
+
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
function tempConfig() {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-rogue-advisor-checkins-"));
|
|
11
|
+
dirs.push(dir);
|
|
12
|
+
const file = join(dir, "advisor", "config.json");
|
|
13
|
+
mkdirSync(join(dir, "advisor"), { recursive: true });
|
|
14
|
+
return file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tempState() {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-rogue-advisor-state-"));
|
|
19
|
+
dirs.push(dir);
|
|
20
|
+
const base = join(dir, "advisor");
|
|
21
|
+
mkdirSync(base, { recursive: true });
|
|
22
|
+
return {
|
|
23
|
+
config: join(base, "config.json"),
|
|
24
|
+
state: join(base, "state.json"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("advisor check-in lifecycle bridge", () => {
|
|
33
|
+
it("turns advisor check-ins on while preserving existing config and captures start time", () => {
|
|
34
|
+
const file = tempConfig();
|
|
35
|
+
writeFileSync(file, JSON.stringify({
|
|
36
|
+
mode: "auto",
|
|
37
|
+
review: "light",
|
|
38
|
+
model: "openai-codex/gpt-5.5",
|
|
39
|
+
checkinIntervalTurns: 3,
|
|
40
|
+
}), "utf8");
|
|
41
|
+
const startedAt = Date.now();
|
|
42
|
+
|
|
43
|
+
const next = setAdvisorCheckinsEnabled(true, file);
|
|
44
|
+
|
|
45
|
+
expect(next).toMatchObject({ mode: "auto", review: "light", model: "openai-codex/gpt-5.5", checkins: "mid-hour" });
|
|
46
|
+
expect(next.checkinIntervalTurns).toBeUndefined();
|
|
47
|
+
expect(next.checkinStartedAt).toBeTypeOf("number");
|
|
48
|
+
expect(next.checkinStartedAt).toBeGreaterThanOrEqual(startedAt);
|
|
49
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
50
|
+
expect(parsed.checkins).toBe("mid-hour");
|
|
51
|
+
expect(parsed.checkinIntervalTurns).toBeUndefined();
|
|
52
|
+
expect(parsed.checkinStartedAt).toBeGreaterThanOrEqual(startedAt);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("turns advisor check-ins off", () => {
|
|
56
|
+
const file = tempConfig();
|
|
57
|
+
writeFileSync(file, JSON.stringify({ checkins: "mid-hour", checkinIntervalMinutes: 30 }), "utf8");
|
|
58
|
+
|
|
59
|
+
const next = setAdvisorCheckinsEnabled(false, file);
|
|
60
|
+
|
|
61
|
+
expect(next).toMatchObject({ checkins: "off", checkinIntervalMinutes: 30 });
|
|
62
|
+
expect(JSON.parse(readFileSync(file, "utf8")).checkins).toBe("off");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("resets advisor brief context and check-in timing for a new goal", () => {
|
|
66
|
+
const { config, state } = tempState();
|
|
67
|
+
const startedAt = Date.now();
|
|
68
|
+
writeFileSync(config, JSON.stringify({
|
|
69
|
+
mode: "auto",
|
|
70
|
+
review: "light",
|
|
71
|
+
checkins: "mid-hour",
|
|
72
|
+
checkinIntervalMinutes: 30,
|
|
73
|
+
checkinIntervalTurns: 3,
|
|
74
|
+
checkinStartedAt: 1,
|
|
75
|
+
}), "utf8");
|
|
76
|
+
writeFileSync(state, JSON.stringify({
|
|
77
|
+
turns: 9,
|
|
78
|
+
lastTask: "old task",
|
|
79
|
+
notes: ["old note"],
|
|
80
|
+
files: ["old.ts"],
|
|
81
|
+
errors: ["old error"],
|
|
82
|
+
advisorCalls: 3,
|
|
83
|
+
cacheHits: 7,
|
|
84
|
+
followUp: "old follow-up",
|
|
85
|
+
reviewControl: {
|
|
86
|
+
status: "running",
|
|
87
|
+
pending: true,
|
|
88
|
+
consumed: false,
|
|
89
|
+
running: true,
|
|
90
|
+
lastDecision: "review",
|
|
91
|
+
lastReason: "manual checkpoint",
|
|
92
|
+
},
|
|
93
|
+
router: { preflight: { label: "continue" } },
|
|
94
|
+
checkin: {
|
|
95
|
+
lastAt: "2026-05-29T00:00:00.000Z",
|
|
96
|
+
lastTurn: 8,
|
|
97
|
+
lastReason: "mid-hour check-in after 1 new turn(s)",
|
|
98
|
+
queued: true,
|
|
99
|
+
queuedReason: "queued mid-session check-in",
|
|
100
|
+
},
|
|
101
|
+
}), "utf8");
|
|
102
|
+
|
|
103
|
+
const next = resetAdvisorSessionContext(config, state);
|
|
104
|
+
|
|
105
|
+
expect(next.state).toMatchObject({
|
|
106
|
+
turns: 0,
|
|
107
|
+
lastTask: "",
|
|
108
|
+
notes: [],
|
|
109
|
+
files: [],
|
|
110
|
+
errors: [],
|
|
111
|
+
advisorCalls: 3,
|
|
112
|
+
cacheHits: 7,
|
|
113
|
+
followUp: "",
|
|
114
|
+
router: {},
|
|
115
|
+
checkin: { queued: false },
|
|
116
|
+
reviewControl: {
|
|
117
|
+
status: "idle",
|
|
118
|
+
pending: false,
|
|
119
|
+
consumed: true,
|
|
120
|
+
running: false,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
expect(next.config.checkinStartedAt).toBeTypeOf("number");
|
|
124
|
+
expect(next.config.checkinStartedAt).toBeGreaterThanOrEqual(startedAt);
|
|
125
|
+
|
|
126
|
+
const parsedState = JSON.parse(readFileSync(state, "utf8"));
|
|
127
|
+
expect(parsedState.lastTask).toBe("");
|
|
128
|
+
expect(parsedState.notes).toEqual([]);
|
|
129
|
+
expect(parsedState.checkin).toEqual({ queued: false });
|
|
130
|
+
expect(parsedState.reviewControl).toEqual({
|
|
131
|
+
status: "idle",
|
|
132
|
+
pending: false,
|
|
133
|
+
consumed: true,
|
|
134
|
+
running: false,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const parsedConfig = JSON.parse(readFileSync(config, "utf8"));
|
|
138
|
+
expect(parsedConfig.checkins).toBe("mid-hour");
|
|
139
|
+
expect(parsedConfig.checkinIntervalTurns).toBeUndefined();
|
|
140
|
+
expect(parsedConfig.checkinStartedAt).toBeGreaterThanOrEqual(startedAt);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
type AdvisorConfig = Record<string, unknown> & { checkins?: "mid-hour" | "off"; checkinStartedAt?: number };
|
|
6
|
+
type AdvisorState = Record<string, unknown> & {
|
|
7
|
+
turns?: number;
|
|
8
|
+
lastTask?: string;
|
|
9
|
+
notes?: unknown[];
|
|
10
|
+
files?: unknown[];
|
|
11
|
+
errors?: unknown[];
|
|
12
|
+
advisorCalls?: number;
|
|
13
|
+
cacheHits?: number;
|
|
14
|
+
followUp?: string;
|
|
15
|
+
router?: Record<string, unknown>;
|
|
16
|
+
checkin?: Record<string, unknown>;
|
|
17
|
+
reviewControl?: {
|
|
18
|
+
status?: "idle" | "needed" | "running" | "consumed";
|
|
19
|
+
pending?: boolean;
|
|
20
|
+
consumed?: boolean;
|
|
21
|
+
running?: boolean;
|
|
22
|
+
lastDecision?: string;
|
|
23
|
+
lastMaterialSignature?: string;
|
|
24
|
+
lastReason?: string;
|
|
25
|
+
lastTrigger?: string;
|
|
26
|
+
lastAppliedAt?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ADVISOR_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
31
|
+
const ADVISOR_CONFIG_PATH = join(homedir(), ".pi", "agent", "pi-rogue", "advisor", "config.json");
|
|
32
|
+
const ADVISOR_STATE_PATH = join(ADVISOR_DIR, "state.json");
|
|
33
|
+
|
|
34
|
+
function readJson<T>(file: string): T {
|
|
35
|
+
if (!existsSync(file)) return {} as T;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(file, "utf8") || "{}") as T;
|
|
38
|
+
} catch {
|
|
39
|
+
return {} as T;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanAdvisorConfig(config: AdvisorConfig): AdvisorConfig {
|
|
44
|
+
const cleaned = { ...config };
|
|
45
|
+
delete cleaned.checkinIntervalTurns;
|
|
46
|
+
delete cleaned.advisorAutoRunCooldownUntilTurn;
|
|
47
|
+
return cleaned;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setAdvisorCheckinsEnabled(enabled: boolean, configPath = ADVISOR_CONFIG_PATH): AdvisorConfig {
|
|
51
|
+
const current = cleanAdvisorConfig(readJson<AdvisorConfig>(configPath));
|
|
52
|
+
const next: AdvisorConfig = {
|
|
53
|
+
...current,
|
|
54
|
+
checkins: enabled ? "mid-hour" : "off",
|
|
55
|
+
checkinStartedAt: enabled ? Date.now() : undefined,
|
|
56
|
+
};
|
|
57
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
58
|
+
writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resetAdvisorSessionContext(
|
|
63
|
+
configPath = ADVISOR_CONFIG_PATH,
|
|
64
|
+
statePath = ADVISOR_STATE_PATH,
|
|
65
|
+
): { config: AdvisorConfig; state: AdvisorState } {
|
|
66
|
+
const currentConfig = cleanAdvisorConfig(readJson<AdvisorConfig>(configPath));
|
|
67
|
+
const nextConfig: AdvisorConfig = {
|
|
68
|
+
...currentConfig,
|
|
69
|
+
checkinStartedAt: currentConfig.checkins === "mid-hour" ? Date.now() : undefined,
|
|
70
|
+
};
|
|
71
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
72
|
+
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
73
|
+
|
|
74
|
+
const currentState = readJson<AdvisorState>(statePath);
|
|
75
|
+
const nextState: AdvisorState = {
|
|
76
|
+
...currentState,
|
|
77
|
+
turns: 0,
|
|
78
|
+
lastTask: "",
|
|
79
|
+
notes: [],
|
|
80
|
+
files: [],
|
|
81
|
+
errors: [],
|
|
82
|
+
followUp: "",
|
|
83
|
+
reviewControl: {
|
|
84
|
+
status: "idle",
|
|
85
|
+
pending: false,
|
|
86
|
+
consumed: true,
|
|
87
|
+
running: false,
|
|
88
|
+
},
|
|
89
|
+
router: {},
|
|
90
|
+
checkin: { queued: false },
|
|
91
|
+
};
|
|
92
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
93
|
+
writeFileSync(statePath, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
|
|
94
|
+
|
|
95
|
+
return { config: nextConfig, state: nextState };
|
|
96
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { truncate } from "./internal.js";
|
|
2
|
+
import { readSessionJson, writeSessionJson } from "./state.js";
|
|
3
|
+
|
|
4
|
+
export const FEATURE = "orchestration";
|
|
5
|
+
export const RESEARCH_FILE = "autoresearch.json";
|
|
6
|
+
export const DEFAULT_RESEARCH_INTERVAL = "5m";
|
|
7
|
+
|
|
8
|
+
export type ResearchKind = "autoresearch" | "autoresearch-lab";
|
|
9
|
+
|
|
10
|
+
export type ResearchState = {
|
|
11
|
+
kind: ResearchKind;
|
|
12
|
+
instruction: string;
|
|
13
|
+
goal?: string;
|
|
14
|
+
loopInstruction?: string;
|
|
15
|
+
interval?: string;
|
|
16
|
+
cycles?: number;
|
|
17
|
+
lastResult?: "done" | "continue" | "unknown";
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function defaultResearchState(kind: ResearchKind = "autoresearch"): ResearchState {
|
|
22
|
+
return {
|
|
23
|
+
kind,
|
|
24
|
+
instruction: "",
|
|
25
|
+
goal: "",
|
|
26
|
+
loopInstruction: "",
|
|
27
|
+
interval: DEFAULT_RESEARCH_INTERVAL,
|
|
28
|
+
cycles: 0,
|
|
29
|
+
updatedAt: "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readResearchState(ctx: any): ResearchState {
|
|
34
|
+
return readSessionJson(FEATURE, ctx, RESEARCH_FILE, defaultResearchState("autoresearch"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeResearchState(ctx: any, state: ResearchState): ResearchState {
|
|
38
|
+
const next: ResearchState = { ...state, updatedAt: new Date().toISOString() };
|
|
39
|
+
writeSessionJson(FEATURE, ctx, RESEARCH_FILE, next);
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearResearchState(ctx: any): ResearchState {
|
|
44
|
+
return writeResearchState(ctx, defaultResearchState("autoresearch"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearResearchStateForGoal(ctx: any, goal: string): boolean {
|
|
48
|
+
const state = readResearchState(ctx);
|
|
49
|
+
if (!state.instruction || !state.goal || state.goal !== goal) return false;
|
|
50
|
+
clearResearchState(ctx);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasActiveResearch(ctx: any): boolean {
|
|
55
|
+
return Boolean(readResearchState(ctx).instruction);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function label(kind: ResearchKind): string {
|
|
59
|
+
return kind === "autoresearch-lab" ? "🧪 Autoresearch lab" : "🔎 Autoresearch";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatResearchState(state: ResearchState): string {
|
|
63
|
+
if (!state.instruction) {
|
|
64
|
+
return `${label(state.kind)} is off.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cycles = state.cycles ?? 0;
|
|
68
|
+
const last = state.lastResult ? `, last=${state.lastResult}` : "";
|
|
69
|
+
return `${label(state.kind)} active: ${truncate(state.instruction, 160)} — /loop ${state.interval || DEFAULT_RESEARCH_INTERVAL}; cycles=${cycles}${last}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
4
|
+
import { buildResearchGoal, buildResearchLoopInstruction, registerAutoresearch } from "./autoresearch.js";
|
|
5
|
+
import { formatResearchState, type ResearchState } from "./autoresearch-state.js";
|
|
6
|
+
import { clearLoop } from "./loop.js";
|
|
7
|
+
|
|
8
|
+
vi.mock("./advisor-checkins.js", () => ({
|
|
9
|
+
resetAdvisorSessionContext: vi.fn(),
|
|
10
|
+
setAdvisorCheckinsEnabled: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const setAdvisorCheckinsEnabledMock = vi.mocked(setAdvisorCheckinsEnabled);
|
|
14
|
+
|
|
15
|
+
function fakeCtx(id = randomUUID()) {
|
|
16
|
+
const notifications: string[] = [];
|
|
17
|
+
return {
|
|
18
|
+
notifications,
|
|
19
|
+
isIdle: () => true,
|
|
20
|
+
sessionManager: {
|
|
21
|
+
getSessionFile: () => `/tmp/pi-rogue-autoresearch-test-${id}.jsonl`,
|
|
22
|
+
},
|
|
23
|
+
ui: {
|
|
24
|
+
setStatus: () => undefined,
|
|
25
|
+
notify: (message: string) => notifications.push(message),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("autoresearch status", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("surfaces backing loop and cycle count", () => {
|
|
36
|
+
const state: ResearchState = {
|
|
37
|
+
kind: "autoresearch",
|
|
38
|
+
instruction: "possible improvements for pi-rogue-orchestration",
|
|
39
|
+
goal: "Autoresearch: possible improvements for pi-rogue-orchestration",
|
|
40
|
+
loopInstruction: "Run one autoresearch cycle",
|
|
41
|
+
interval: "5m",
|
|
42
|
+
cycles: 1,
|
|
43
|
+
lastResult: "done",
|
|
44
|
+
updatedAt: "2026-05-26T00:00:00.000Z",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const text = formatResearchState(state);
|
|
48
|
+
|
|
49
|
+
expect(text).toContain("/loop 5m");
|
|
50
|
+
expect(text).toContain("cycles=1");
|
|
51
|
+
expect(text).toContain("last=done");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps empty state concise", () => {
|
|
55
|
+
expect(formatResearchState({ kind: "autoresearch", instruction: "", updatedAt: "" })).toBe("🔎 Autoresearch is off.");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps autoresearch prompts direct", () => {
|
|
59
|
+
const goal = buildResearchGoal("autoresearch", "improve advisor escalation");
|
|
60
|
+
const loop = buildResearchLoopInstruction("autoresearch", "improve advisor escalation");
|
|
61
|
+
|
|
62
|
+
expect(goal).toContain("measurable target");
|
|
63
|
+
expect(goal).toContain("eval/check command");
|
|
64
|
+
expect(goal).toContain("durable artifact/log");
|
|
65
|
+
expect(goal).toContain("Preserve the user objective");
|
|
66
|
+
expect(goal).toContain("summarized with evidence");
|
|
67
|
+
expect(loop).toContain("Confirm/update hypothesis");
|
|
68
|
+
expect(loop).toContain("take one concrete high-leverage step");
|
|
69
|
+
expect(loop).toContain("record result");
|
|
70
|
+
expect(loop).toContain("Do not simplify or re-aim");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("keeps autoresearch-lab prompts direct", () => {
|
|
74
|
+
const goal = buildResearchGoal("autoresearch-lab", "compare advisor lanes");
|
|
75
|
+
const loop = buildResearchLoopInstruction("autoresearch-lab", "compare advisor lanes");
|
|
76
|
+
|
|
77
|
+
expect(goal).toContain("source objective");
|
|
78
|
+
expect(goal).toContain("lane split");
|
|
79
|
+
expect(goal).toContain("evaluate evidence before integration");
|
|
80
|
+
expect(goal).toContain("convergent findings");
|
|
81
|
+
expect(loop).toContain("Advance the most useful lane comparison");
|
|
82
|
+
expect(loop).toContain("integrate only safe improvements");
|
|
83
|
+
expect(loop).toContain("Do not simplify or re-aim");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not queue a duplicate cycle for the same active autoresearch instruction", async () => {
|
|
87
|
+
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
|
|
88
|
+
const sent: string[] = [];
|
|
89
|
+
const pi = {
|
|
90
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => Promise<void> }) => {
|
|
91
|
+
if (name === "autoresearch") handler = command.handler;
|
|
92
|
+
},
|
|
93
|
+
sendUserMessage: (text: string) => sent.push(text),
|
|
94
|
+
} as any;
|
|
95
|
+
const ctx = fakeCtx();
|
|
96
|
+
|
|
97
|
+
registerAutoresearch(pi);
|
|
98
|
+
expect(handler).toBeTypeOf("function");
|
|
99
|
+
await handler?.("improve repetition handling", ctx);
|
|
100
|
+
await handler?.("improve repetition handling", ctx);
|
|
101
|
+
|
|
102
|
+
expect(sent).toHaveLength(1);
|
|
103
|
+
expect(ctx.notifications.at(-1)).toContain("already active");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("requeues the same autoresearch instruction when the backing loop is stale", async () => {
|
|
107
|
+
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
|
|
108
|
+
const sent: string[] = [];
|
|
109
|
+
const pi = {
|
|
110
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => Promise<void> }) => {
|
|
111
|
+
if (name === "autoresearch") handler = command.handler;
|
|
112
|
+
},
|
|
113
|
+
sendUserMessage: (text: string) => sent.push(text),
|
|
114
|
+
} as any;
|
|
115
|
+
const ctx = fakeCtx();
|
|
116
|
+
|
|
117
|
+
registerAutoresearch(pi);
|
|
118
|
+
await handler?.("improve stale loop recovery", ctx);
|
|
119
|
+
clearLoop(ctx, { preserveCheckins: true });
|
|
120
|
+
await handler?.("improve stale loop recovery", ctx);
|
|
121
|
+
|
|
122
|
+
expect(sent).toHaveLength(2);
|
|
123
|
+
expect(sent[1]).toContain("improve stale loop recovery");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("disables advisor check-ins when /autoresearch clear stops the loop", async () => {
|
|
127
|
+
let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
|
|
128
|
+
const pi = {
|
|
129
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => Promise<void> }) => {
|
|
130
|
+
if (name === "autoresearch") handler = command.handler;
|
|
131
|
+
},
|
|
132
|
+
sendUserMessage: () => undefined,
|
|
133
|
+
} as any;
|
|
134
|
+
const ctx = fakeCtx();
|
|
135
|
+
|
|
136
|
+
registerAutoresearch(pi);
|
|
137
|
+
await handler?.("improve lifecycle cleanup", ctx);
|
|
138
|
+
setAdvisorCheckinsEnabledMock.mockClear();
|
|
139
|
+
await handler?.("clear", ctx);
|
|
140
|
+
|
|
141
|
+
expect(setAdvisorCheckinsEnabledMock).toHaveBeenCalledWith(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { activeGoal, clearGoal, setGoal, setGoalStatus } from "./goal.js";
|
|
3
|
+
import { clearLoop, readLoopState, startLoop } from "./loop.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RESEARCH_INTERVAL,
|
|
6
|
+
formatResearchState,
|
|
7
|
+
label,
|
|
8
|
+
readResearchState,
|
|
9
|
+
writeResearchState,
|
|
10
|
+
type ResearchKind,
|
|
11
|
+
} from "./autoresearch-state.js";
|
|
12
|
+
import { autoresearchArgumentCompletions } from "./completions.js";
|
|
13
|
+
|
|
14
|
+
export function buildResearchGoal(kind: ResearchKind, instruction: string): string {
|
|
15
|
+
if (kind === "autoresearch-lab") {
|
|
16
|
+
return [
|
|
17
|
+
`Autoresearch lab: ${instruction}`,
|
|
18
|
+
"Define source objective, hypotheses, lane split, measurement method, baseline, artifacts, and stop condition.",
|
|
19
|
+
"Run independent lanes where useful; evaluate evidence before integration; preserve the user objective unless explicitly changed.",
|
|
20
|
+
"Finish with convergent findings, rejected hypotheses, limitations, checks, and follow-up seeds.",
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
`Autoresearch: ${instruction}`,
|
|
26
|
+
"Define hypothesis/objective, measurable target, baseline, eval/check command, durable artifact/log, and stop condition.",
|
|
27
|
+
"Iterate: inspect evidence, make one high-leverage change, run the relevant check/eval, record result, choose next hypothesis.",
|
|
28
|
+
"Preserve the user objective unless explicitly changed; stop only when materially improved and summarized with evidence.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildResearchLoopInstruction(kind: ResearchKind, instruction: string): string {
|
|
33
|
+
if (kind === "autoresearch-lab") {
|
|
34
|
+
return [
|
|
35
|
+
"Run one autoresearch-lab cycle toward the active goal.",
|
|
36
|
+
`User instruction: ${instruction}`,
|
|
37
|
+
"Confirm/update source objective, hypotheses, lane split, measurement method, baseline, artifacts, and stop condition.",
|
|
38
|
+
"Advance the most useful lane comparison, evaluate evidence, integrate only safe improvements, run checks, and record the next hypothesis.",
|
|
39
|
+
"Do not simplify or re-aim the objective unless the user explicitly asks.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
"Run one autoresearch cycle toward the active goal.",
|
|
45
|
+
`User instruction: ${instruction}`,
|
|
46
|
+
"Confirm/update hypothesis, measurable target, baseline, eval/check command, artifact/log, and stop condition.",
|
|
47
|
+
"Inspect evidence, take one concrete high-leverage step, run the relevant check/eval when possible, record result, and choose the next hypothesis.",
|
|
48
|
+
"Do not simplify or re-aim the objective unless the user explicitly asks.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerResearchCommand(pi: ExtensionAPI, commandName: ResearchKind): void {
|
|
53
|
+
const prefix = label(commandName);
|
|
54
|
+
|
|
55
|
+
pi.registerCommand(commandName, {
|
|
56
|
+
description: commandName === "autoresearch-lab"
|
|
57
|
+
? "Parallel multi-agent research mode backed by goal + loop"
|
|
58
|
+
: "Iterative optimization/research mode backed by goal + loop",
|
|
59
|
+
getArgumentCompletions: (prefix: string) => autoresearchArgumentCompletions(prefix),
|
|
60
|
+
handler: async (args, ctx) => {
|
|
61
|
+
const input = String(args ?? "").trim();
|
|
62
|
+
const [cmd] = input.split(/\s+/);
|
|
63
|
+
const resolved = !input ? "status" : ["status", "show"].includes(cmd) ? cmd : ["off", "clear", "stop"].includes(cmd) ? "clear" : "set";
|
|
64
|
+
|
|
65
|
+
if (resolved === "status" || resolved === "show") {
|
|
66
|
+
ctx.ui.notify(formatResearchState(readResearchState(ctx)), "info");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (resolved === "clear") {
|
|
71
|
+
const previous = readResearchState(ctx);
|
|
72
|
+
clearLoop(ctx, { clearResearch: true });
|
|
73
|
+
const clearedGoal = Boolean(previous.goal && activeGoal(ctx) === previous.goal);
|
|
74
|
+
if (clearedGoal) {
|
|
75
|
+
clearGoal(ctx);
|
|
76
|
+
setGoalStatus(ctx, null);
|
|
77
|
+
}
|
|
78
|
+
ctx.ui.notify(`${prefix} cleared; underlying loop stopped${clearedGoal ? " and matching goal cleared" : ""}.`, "info");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const instruction = input;
|
|
83
|
+
if (!instruction) {
|
|
84
|
+
ctx.ui.notify(`Usage: /${commandName} <instruction>`, "error");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const goal = buildResearchGoal(commandName, instruction);
|
|
89
|
+
const loopInstruction = buildResearchLoopInstruction(commandName, instruction);
|
|
90
|
+
const previous = readResearchState(ctx);
|
|
91
|
+
const currentLoop = readLoopState(ctx);
|
|
92
|
+
if (
|
|
93
|
+
previous.kind === commandName
|
|
94
|
+
&& previous.instruction === instruction
|
|
95
|
+
&& previous.goal === goal
|
|
96
|
+
&& activeGoal(ctx) === goal
|
|
97
|
+
&& currentLoop.enabled
|
|
98
|
+
&& currentLoop.instruction === loopInstruction
|
|
99
|
+
) {
|
|
100
|
+
ctx.ui.notify(`${prefix} already active for this instruction. No duplicate cycle queued.`, "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const restartSameGoal = activeGoal(ctx) === goal;
|
|
105
|
+
setGoal(ctx, goal, { restartDuplicate: restartSameGoal });
|
|
106
|
+
|
|
107
|
+
setGoalStatus(ctx, goal);
|
|
108
|
+
const next = writeResearchState(ctx, {
|
|
109
|
+
kind: commandName,
|
|
110
|
+
instruction,
|
|
111
|
+
goal,
|
|
112
|
+
loopInstruction,
|
|
113
|
+
interval: DEFAULT_RESEARCH_INTERVAL,
|
|
114
|
+
cycles: 0,
|
|
115
|
+
updatedAt: "",
|
|
116
|
+
});
|
|
117
|
+
const loop = startLoop(pi, ctx, DEFAULT_RESEARCH_INTERVAL, loopInstruction, { triggerNow: true });
|
|
118
|
+
if (!loop) {
|
|
119
|
+
ctx.ui.notify(`${prefix} could not start: invalid loop interval.`, "error");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
ctx.ui.notify(`${formatResearchState(next)}. First cycle queued now.`, "info");
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function registerAutoresearch(pi: ExtensionAPI): void {
|
|
128
|
+
const p = pi as any;
|
|
129
|
+
if (p.__piRogueAutoresearchRegistered) return;
|
|
130
|
+
p.__piRogueAutoresearchRegistered = true;
|
|
131
|
+
registerResearchCommand(pi, "autoresearch");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function registerAutoresearchLab(pi: ExtensionAPI): void {
|
|
135
|
+
const p = pi as any;
|
|
136
|
+
if (p.__piRogueAutoresearchLabRegistered) return;
|
|
137
|
+
p.__piRogueAutoresearchLabRegistered = true;
|
|
138
|
+
registerResearchCommand(pi, "autoresearch-lab");
|
|
139
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { autoresearchArgumentCompletions, goalArgumentCompletions, loopArgumentCompletions } from "./completions.js";
|
|
3
|
+
|
|
4
|
+
describe("goal completions", () => {
|
|
5
|
+
it("offers goal management choices", () => {
|
|
6
|
+
const values = goalArgumentCompletions("")?.map((i) => i.value);
|
|
7
|
+
expect(values).toEqual(expect.arrayContaining(["show", "clear", "list", "set"]));
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("loop completions", () => {
|
|
12
|
+
it("offers loop management and cadence choices", () => {
|
|
13
|
+
const values = loopArgumentCompletions("")?.map((i) => i.value);
|
|
14
|
+
expect(values).toEqual(expect.arrayContaining(["status", "off", "1m", "5m", "1h"]));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("autoresearch completions", () => {
|
|
19
|
+
it("offers research management choices", () => {
|
|
20
|
+
const values = autoresearchArgumentCompletions("")?.map((i) => i.value);
|
|
21
|
+
expect(values).toEqual(expect.arrayContaining(["status", "clear"]));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type CompletionItem = { value: string; label: string; description?: string };
|
|
2
|
+
|
|
3
|
+
function item(value: string, description?: string): CompletionItem {
|
|
4
|
+
return { value, label: value, ...(description ? { description } : {}) };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function complete(values: Array<[string, string?]>, prefix: string): CompletionItem[] | null {
|
|
8
|
+
const q = prefix.trimStart().toLowerCase();
|
|
9
|
+
const items = values.map(([value, description]) => item(value, description));
|
|
10
|
+
const filtered = q ? items.filter((i) => i.value.startsWith(q)) : items;
|
|
11
|
+
return filtered.length > 0 ? filtered : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function goalArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
15
|
+
return complete(
|
|
16
|
+
[
|
|
17
|
+
["show", "show current goal"],
|
|
18
|
+
["status", "show current goal"],
|
|
19
|
+
["clear", "clear current goal"],
|
|
20
|
+
["list", "list recent goals"],
|
|
21
|
+
["set", "set a goal"],
|
|
22
|
+
],
|
|
23
|
+
prefix,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loopArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
28
|
+
return complete(
|
|
29
|
+
[
|
|
30
|
+
["status", "show current loop"],
|
|
31
|
+
["show", "show current loop"],
|
|
32
|
+
["off", "clear current loop"],
|
|
33
|
+
["clear", "clear current loop"],
|
|
34
|
+
["stop", "clear current loop"],
|
|
35
|
+
["1m", "minimum cadence"],
|
|
36
|
+
["5m", "default cadence"],
|
|
37
|
+
["1h", "slower cadence"],
|
|
38
|
+
],
|
|
39
|
+
prefix,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function autoresearchArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
44
|
+
return complete(
|
|
45
|
+
[
|
|
46
|
+
["status", "show research status"],
|
|
47
|
+
["show", "show research status"],
|
|
48
|
+
["clear", "clear research state"],
|
|
49
|
+
["stop", "clear research state"],
|
|
50
|
+
],
|
|
51
|
+
prefix,
|
|
52
|
+
);
|
|
53
|
+
}
|