@fiale-plus/pi-rogue 0.2.0
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 +50 -0
- package/node_modules/@fiale-plus/pi-core/README.md +13 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- 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/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/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 +364 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -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 +63 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -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 +126 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +749 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -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 +102 -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 +51 -0
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/context-broker.ts +1 -0
- package/src/extension.test.ts +68 -0
- package/src/extension.ts +27 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orchestration
|
|
3
|
+
description: Session orchestration for Pi; use when you want to manage loop cadence, goals, or opt-in autoresearch in the current session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pi-Rogue Orchestration
|
|
7
|
+
|
|
8
|
+
Use this skill to run measurable, bounded workflow loops inside a Pi session.
|
|
9
|
+
|
|
10
|
+
## Command surface
|
|
11
|
+
|
|
12
|
+
| Command | What it does |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `/goal set <text>` | Set or update the current goal |
|
|
15
|
+
| `/goal show` | Show current goal |
|
|
16
|
+
| `/goal clear` | Clear goal |
|
|
17
|
+
| `/goal list` | Show recent goal history |
|
|
18
|
+
| `/loop <interval> <instruction>` | Run periodic checks (`1m` minimum) |
|
|
19
|
+
| `/loop status` | Show current loop |
|
|
20
|
+
| `/loop off` / `/loop clear` / `/loop stop` | Stop and clear loop |
|
|
21
|
+
| `/autoresearch <instruction>` | Solo iterative research on top of `/goal + /loop` |
|
|
22
|
+
| `/autoresearch status` | Show research counters and backing state |
|
|
23
|
+
| `/autoresearch clear` | Clear research and stop backing loop |
|
|
24
|
+
| `/autoresearch-lab <instruction>` | Parallel research mode (lab) |
|
|
25
|
+
| `/autoresearch-lab status` | Show lab state |
|
|
26
|
+
| `/autoresearch-lab clear` | Clear lab and stop backing loop |
|
|
27
|
+
|
|
28
|
+
## Behavior rules
|
|
29
|
+
|
|
30
|
+
- `loop` is the primitive; `goal` is the execution intent.
|
|
31
|
+
- Goal completion is explicit through `GOAL_DONE` / `GOAL_CONTINUE` in loop checks.
|
|
32
|
+
- `autoresearch` / `autoresearch-lab` are facades over goal+loop.
|
|
33
|
+
- Goal or loop activation enables scheduled advisor check-ins; stopping or clearing either disables them.
|
|
34
|
+
- Check-ins belong to orchestration lifecycle, not the advisor command surface, and use higher/advanced advisor models first, with regular model fallback enabled by default.
|
|
35
|
+
- `autoresearch` enforces multi-cycle + evidence-aware completion.
|
|
36
|
+
- Clearing goal/loop clears stale autoresearch state.
|
|
37
|
+
|
|
38
|
+
## Safety and agentic flow
|
|
39
|
+
|
|
40
|
+
- Auto-detect opportunities are proposals first, not silent launches.
|
|
41
|
+
- `autoresearch-lab` requires explicit confirmation for escalation.
|
|
42
|
+
- Commands remain distinct:
|
|
43
|
+
- `/autoresearch` = solo optimization
|
|
44
|
+
- `/autoresearch-lab` = parallel lab mode
|
|
@@ -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,102 @@
|
|
|
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
|
+
followUpTask?: string;
|
|
16
|
+
reviewSignals?: unknown[];
|
|
17
|
+
reviewSignalsTask?: string;
|
|
18
|
+
router?: Record<string, unknown>;
|
|
19
|
+
checkin?: Record<string, unknown>;
|
|
20
|
+
reviewControl?: {
|
|
21
|
+
status?: "idle" | "needed" | "running" | "consumed";
|
|
22
|
+
pending?: boolean;
|
|
23
|
+
consumed?: boolean;
|
|
24
|
+
running?: boolean;
|
|
25
|
+
lastDecision?: string;
|
|
26
|
+
lastMaterialSignature?: string;
|
|
27
|
+
lastReason?: string;
|
|
28
|
+
lastTrigger?: string;
|
|
29
|
+
lastAppliedAt?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ADVISOR_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
34
|
+
const ADVISOR_CONFIG_PATH = join(homedir(), ".pi", "agent", "pi-rogue", "advisor", "config.json");
|
|
35
|
+
const ADVISOR_STATE_PATH = join(ADVISOR_DIR, "state.json");
|
|
36
|
+
|
|
37
|
+
function readJson<T>(file: string): T {
|
|
38
|
+
if (!existsSync(file)) return {} as T;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(file, "utf8") || "{}") as T;
|
|
41
|
+
} catch {
|
|
42
|
+
return {} as T;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanAdvisorConfig(config: AdvisorConfig): AdvisorConfig {
|
|
47
|
+
const cleaned = { ...config };
|
|
48
|
+
delete cleaned.checkinIntervalTurns;
|
|
49
|
+
delete cleaned.advisorAutoRunCooldownUntilTurn;
|
|
50
|
+
return cleaned;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setAdvisorCheckinsEnabled(enabled: boolean, configPath = ADVISOR_CONFIG_PATH): AdvisorConfig {
|
|
54
|
+
const current = cleanAdvisorConfig(readJson<AdvisorConfig>(configPath));
|
|
55
|
+
const next: AdvisorConfig = {
|
|
56
|
+
...current,
|
|
57
|
+
checkins: enabled ? "mid-hour" : "off",
|
|
58
|
+
checkinStartedAt: enabled ? Date.now() : undefined,
|
|
59
|
+
};
|
|
60
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
61
|
+
writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resetAdvisorSessionContext(
|
|
66
|
+
configPath = ADVISOR_CONFIG_PATH,
|
|
67
|
+
statePath = ADVISOR_STATE_PATH,
|
|
68
|
+
): { config: AdvisorConfig; state: AdvisorState } {
|
|
69
|
+
const currentConfig = cleanAdvisorConfig(readJson<AdvisorConfig>(configPath));
|
|
70
|
+
const nextConfig: AdvisorConfig = {
|
|
71
|
+
...currentConfig,
|
|
72
|
+
checkinStartedAt: currentConfig.checkins === "mid-hour" ? Date.now() : undefined,
|
|
73
|
+
};
|
|
74
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
75
|
+
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
76
|
+
|
|
77
|
+
const currentState = readJson<AdvisorState>(statePath);
|
|
78
|
+
const nextState: AdvisorState = {
|
|
79
|
+
...currentState,
|
|
80
|
+
turns: 0,
|
|
81
|
+
lastTask: "",
|
|
82
|
+
notes: [],
|
|
83
|
+
files: [],
|
|
84
|
+
errors: [],
|
|
85
|
+
followUp: "",
|
|
86
|
+
followUpTask: undefined,
|
|
87
|
+
reviewSignals: [],
|
|
88
|
+
reviewSignalsTask: undefined,
|
|
89
|
+
reviewControl: {
|
|
90
|
+
status: "idle",
|
|
91
|
+
pending: false,
|
|
92
|
+
consumed: true,
|
|
93
|
+
running: false,
|
|
94
|
+
},
|
|
95
|
+
router: {},
|
|
96
|
+
checkin: { queued: false },
|
|
97
|
+
};
|
|
98
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
99
|
+
writeFileSync(statePath, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
|
|
100
|
+
|
|
101
|
+
return { config: nextConfig, state: nextState };
|
|
102
|
+
}
|
|
@@ -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
|
+
});
|