@fiale-plus/pi-rogue 0.2.2 → 0.2.4
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-core/src/context-broker.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +8 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +7 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +26 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +10 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +20 -2
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +81 -3
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +72 -10
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +32 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +32 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +37 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +39 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +3 -3
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +3 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +3 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +65 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +84 -4
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +43 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +96 -11
- package/node_modules/@fiale-plus/pi-rogue-router/README.md +46 -5
- package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.test.ts +88 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.ts +9 -1
- package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +123 -9
- package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +39 -16
- package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +145 -6
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +51 -11
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +67 -7
- package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +27 -12
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +87 -9
- package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +130 -6
- package/node_modules/@fiale-plus/pi-rogue-router/src/reports.test.ts +92 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/reports.ts +116 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.test.ts +223 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.ts +344 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.ts +238 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +59 -2
- package/package.json +1 -1
|
@@ -40,6 +40,43 @@ describe("createSqliteContextBroker", () => {
|
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it("persists age-based tier cooling without deleting artifacts", () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
45
|
+
try {
|
|
46
|
+
const path = join(dir, "artifacts.sqlite");
|
|
47
|
+
let broker = createSqliteContextBroker({ path, defaultTtlMs: 0, hotToWarmMs: 100, warmToColdMs: 200, briefBytes: 900 });
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const oldHot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "old hot", summary: "old hot", tier: "hot", createdAt: now - 300 });
|
|
50
|
+
const pinned = broker.publish({ sessionId: "s", kind: "tool_output", payload: "pinned", summary: "pinned", tier: "hot", pinned: true, createdAt: now - 300 });
|
|
51
|
+
|
|
52
|
+
broker.prune(now);
|
|
53
|
+
broker = createSqliteContextBroker({ path, defaultTtlMs: 0, hotToWarmMs: 100, warmToColdMs: 200, briefBytes: 900 });
|
|
54
|
+
|
|
55
|
+
expect(broker.lookup({ handle: oldHot.handle })[0]?.tier).toBe("cold");
|
|
56
|
+
expect(broker.lookup({ handle: pinned.handle })[0]?.tier).toBe("hot");
|
|
57
|
+
expect(broker.renderBrief({ sessionId: "s" })).not.toContain(oldHot.handle);
|
|
58
|
+
expect(broker.renderBrief({ sessionId: "s" })).toContain(pinned.handle);
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(dir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("cools protected new artifacts before enforcing durable tier caps", () => {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
66
|
+
try {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const broker = createSqliteContextBroker({ path: join(dir, "artifacts.sqlite"), defaultTtlMs: 0, maxRecords: 10, hotMaxRecords: 1, hotToWarmMs: 10_000, warmToColdMs: 20_000 });
|
|
69
|
+
const fresh = broker.publish({ sessionId: "s", kind: "tool_output", payload: "fresh", summary: "fresh", tier: "hot", createdAt: now - 1_000 });
|
|
70
|
+
const aged = broker.publish({ sessionId: "s", kind: "tool_output", payload: "aged", summary: "aged", tier: "hot", createdAt: now - 30_000 });
|
|
71
|
+
|
|
72
|
+
expect(aged.tier).toBe("cold");
|
|
73
|
+
expect(broker.lookup({ handle: fresh.handle })[0]?.tier).toBe("hot");
|
|
74
|
+
expect(broker.lookup({ handle: aged.handle })[0]?.tier).toBe("cold");
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
43
80
|
it("dedupes replayed source artifacts so durable handles survive caps", () => {
|
|
44
81
|
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
45
82
|
try {
|
|
@@ -29,6 +29,10 @@ const DEFAULT_BRIEF_BYTES = 2_000;
|
|
|
29
29
|
const TIER_ORDER: Record<ContextArtifactTier, number> = { hot: 0, warm: 1, cold: 2 };
|
|
30
30
|
const TIER_REMOVAL_ORDER: Record<ContextArtifactTier, number> = { cold: 0, warm: 1, hot: 2 };
|
|
31
31
|
|
|
32
|
+
function optionMs(value: number | undefined): number {
|
|
33
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : Number.POSITIVE_INFINITY;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
function defaultStoreDir(): string {
|
|
33
37
|
return join(homedir(), ".pi", "agent", "fiale-plus", "context-broker");
|
|
34
38
|
}
|
|
@@ -226,6 +230,8 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
226
230
|
warm: Math.max(1, Math.floor(options.warmMaxBytes ?? maxBytes)),
|
|
227
231
|
cold: Math.max(1, Math.floor(options.coldMaxBytes ?? maxBytes)),
|
|
228
232
|
};
|
|
233
|
+
const hotToWarmMs = optionMs(options.hotToWarmMs);
|
|
234
|
+
const warmToColdMs = optionMs(options.warmToColdMs);
|
|
229
235
|
const summaryBytes = Math.max(16, Math.floor(options.summaryBytes ?? DEFAULT_SUMMARY_BYTES));
|
|
230
236
|
const defaultBriefBytes = Math.max(64, Math.floor(options.briefBytes ?? DEFAULT_BRIEF_BYTES));
|
|
231
237
|
|
|
@@ -241,6 +247,31 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
241
247
|
db.prepare("DELETE FROM artifacts WHERE id = ?").run(id);
|
|
242
248
|
}
|
|
243
249
|
|
|
250
|
+
function cooledTier(artifact: ContextArtifact & { baseTier: ContextArtifactTier }, now = Date.now()): ContextArtifactTier {
|
|
251
|
+
if (artifact.pinned) return "hot";
|
|
252
|
+
if (artifact.baseTier === "cold") return "cold";
|
|
253
|
+
const age = Math.max(0, now - artifact.createdAt);
|
|
254
|
+
if (age >= warmToColdMs) return "cold";
|
|
255
|
+
if (artifact.baseTier === "hot" && age >= hotToWarmMs) return "warm";
|
|
256
|
+
return artifact.baseTier;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function applyCooling(now = Date.now(), _protectedIds = new Set<string>()): void {
|
|
260
|
+
const rows = db.prepare("SELECT id, createdAt, tier, baseTier, pinned FROM artifacts WHERE pinned = 0").all();
|
|
261
|
+
const update = db.prepare("UPDATE artifacts SET tier = ?, updatedAt = ? WHERE id = ?");
|
|
262
|
+
for (const row of rows) {
|
|
263
|
+
const artifact = {
|
|
264
|
+
id: String(row.id),
|
|
265
|
+
createdAt: Number(row.createdAt),
|
|
266
|
+
tier: String(row.tier) as ContextArtifactTier,
|
|
267
|
+
baseTier: String(row.baseTier ?? row.tier) as ContextArtifactTier,
|
|
268
|
+
pinned: Boolean(row.pinned),
|
|
269
|
+
};
|
|
270
|
+
const nextTier = cooledTier(artifact as ContextArtifact & { baseTier: ContextArtifactTier }, now);
|
|
271
|
+
if (artifact.tier !== nextTier) update.run(nextTier, now, artifact.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
244
275
|
function currentStatus(): ContextBrokerStatus {
|
|
245
276
|
const row = db.prepare(`
|
|
246
277
|
SELECT
|
|
@@ -269,6 +300,8 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
269
300
|
coldBytes: Number(row.coldBytes ?? 0),
|
|
270
301
|
maxRecords,
|
|
271
302
|
maxBytes,
|
|
303
|
+
globalMaxRecords,
|
|
304
|
+
globalMaxBytes,
|
|
272
305
|
};
|
|
273
306
|
}
|
|
274
307
|
|
|
@@ -329,6 +362,7 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
329
362
|
|
|
330
363
|
function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
|
|
331
364
|
dropExpired(now, protectedIds);
|
|
365
|
+
applyCooling(now, protectedIds);
|
|
332
366
|
const sessions = db.prepare("SELECT DISTINCT sessionId FROM artifacts").all().map((row) => String(row.sessionId));
|
|
333
367
|
for (const sessionId of sessions) {
|
|
334
368
|
for (const tier of ["cold", "warm", "hot"] as ContextArtifactTier[]) {
|
|
@@ -356,11 +390,13 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
356
390
|
|
|
357
391
|
function status(): ContextBrokerStatus {
|
|
358
392
|
dropExpired();
|
|
393
|
+
applyCooling();
|
|
359
394
|
return currentStatus();
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
function purge(options: ContextPurgeOptions = {}): ContextBrokerStatus {
|
|
363
398
|
dropExpired();
|
|
399
|
+
applyCooling();
|
|
364
400
|
const keepPinned = options.keepPinned ?? true;
|
|
365
401
|
const clauses: string[] = [];
|
|
366
402
|
const params: Array<string | number> = [];
|
|
@@ -468,12 +504,13 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
468
504
|
throw error;
|
|
469
505
|
}
|
|
470
506
|
|
|
471
|
-
prune(now, new Set([artifact.id]));
|
|
472
|
-
return artifact;
|
|
507
|
+
prune(Date.now(), new Set([artifact.id]));
|
|
508
|
+
return lookup({ id: artifact.id })[0] ?? artifact;
|
|
473
509
|
}
|
|
474
510
|
|
|
475
511
|
function lookup(query: ContextLookupQuery = {}): ContextArtifact[] {
|
|
476
512
|
dropExpired();
|
|
513
|
+
applyCooling();
|
|
477
514
|
const storedCount = Number(db.prepare("SELECT COUNT(*) AS count FROM artifacts").get()?.count ?? 1) || 1;
|
|
478
515
|
const limit = Math.max(1, Math.floor(query.limit ?? storedCount));
|
|
479
516
|
const clauses: string[] = [];
|
|
@@ -47,10 +47,10 @@ npm install --workspace packages/orchestration
|
|
|
47
47
|
## Behavior notes
|
|
48
48
|
|
|
49
49
|
- `loop` supports minimum interval `1m`.
|
|
50
|
-
- `
|
|
50
|
+
- Active goals can be completed with the model-callable `goal_complete` tool, which requires a summary and verification evidence; `GOAL_DONE` / `GOAL_CONTINUE` sentinel loop checks are preserved for compatibility.
|
|
51
51
|
- `autoresearch` and `autoresearch-lab` are thin facades over `/goal + /loop`.
|
|
52
52
|
- A goal or loop activation enables scheduled advisor check-ins; stopping or clearing the active goal/loop disables them again.
|
|
53
53
|
- Check-ins are part of orchestration lifecycle, not a standalone advisor command. They use the advisor interval, higher/advanced advisor models first, and regular model fallback by default.
|
|
54
|
-
- A
|
|
55
|
-
- There are no hidden flow budgets. Long loops run until `/loop off`, `/goal clear`, or a `GOAL_DONE` response clears the active goal and loop.
|
|
54
|
+
- A bounded no-progress guard detects repeated assistant output or repeated planning-only turns during active orchestration, then nudges one concrete alternative action and eventually stops retry churn instead of stacking recovery prompts.
|
|
55
|
+
- There are no hidden flow budgets. Long loops run until `/loop off`, `/goal clear`, `goal_complete`, or a `GOAL_DONE` response clears the active goal and loop.
|
|
56
56
|
- Stale research state is cleared when `goal` or `loop` are cleared.
|
|
@@ -28,12 +28,13 @@ Use this skill to run measurable, bounded workflow loops inside a Pi session.
|
|
|
28
28
|
## Behavior rules
|
|
29
29
|
|
|
30
30
|
- `loop` is the primitive; `goal` is the execution intent.
|
|
31
|
-
- Goal completion
|
|
31
|
+
- Goal completion should use the `goal_complete` tool when available, with a summary and verification evidence; `GOAL_DONE` / `GOAL_CONTINUE` remain valid loop-check sentinels for compatibility.
|
|
32
32
|
- `autoresearch` / `autoresearch-lab` are facades over goal+loop.
|
|
33
33
|
- Goal or loop activation enables scheduled advisor check-ins; stopping or clearing either disables them.
|
|
34
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
35
|
- `autoresearch` enforces multi-cycle + evidence-aware completion.
|
|
36
|
-
- Clearing goal/loop clears stale autoresearch state.
|
|
36
|
+
- Clearing goal/loop or completing a goal clears stale autoresearch state.
|
|
37
|
+
- Bounded no-progress recovery may steer one concrete alternative action after repeated self-talk/repetition, then stops retry churn instead of stacking prompts.
|
|
37
38
|
|
|
38
39
|
## Safety and agentic flow
|
|
39
40
|
|
|
@@ -2,8 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { resetAdvisorSessionContext, setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
4
4
|
import { endGoalCheck } from "./goal-resolution.js";
|
|
5
|
-
import { activeGoal, clearGoal, registerGoal, setGoal, startGoalProcessing } from "./goal.js";
|
|
6
|
-
import { featureFile, readText } from "./internal.js";
|
|
5
|
+
import { activeGoal, clearGoal, completeActiveGoal, registerGoal, setGoal, startGoalProcessing } from "./goal.js";
|
|
6
|
+
import { featureFile, readText, sessionFile, writeText } from "./internal.js";
|
|
7
7
|
|
|
8
8
|
vi.mock("./advisor-checkins.js", () => ({
|
|
9
9
|
resetAdvisorSessionContext: vi.fn(),
|
|
@@ -111,6 +111,20 @@ describe("goal processing", () => {
|
|
|
111
111
|
endGoalCheck(ctx);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it("clears stale no-progress recovery state when goal lifecycle changes", () => {
|
|
115
|
+
const ctx = fakeCtx();
|
|
116
|
+
const guardFile = sessionFile("orchestration", ctx, "repetition-guard.json");
|
|
117
|
+
writeText(guardFile, `${JSON.stringify({
|
|
118
|
+
recentAssistantTurns: [],
|
|
119
|
+
noProgress: { at: new Date().toISOString(), count: 3, text: "I will plan next.", reason: "test" },
|
|
120
|
+
})}\n`);
|
|
121
|
+
|
|
122
|
+
setGoal(ctx, "fresh goal after stale recovery");
|
|
123
|
+
|
|
124
|
+
expect(JSON.parse(readText(guardFile)).noProgress).toBeUndefined();
|
|
125
|
+
clearGoal(ctx);
|
|
126
|
+
});
|
|
127
|
+
|
|
114
128
|
it("resets advisor context when a goal is cleared", () => {
|
|
115
129
|
const ctx = fakeCtx();
|
|
116
130
|
|
|
@@ -158,6 +172,55 @@ describe("goal processing", () => {
|
|
|
158
172
|
expect(setAdvisorCheckinsEnabledMock).toHaveBeenCalledWith(false);
|
|
159
173
|
});
|
|
160
174
|
|
|
175
|
+
it("completes an active goal through the explicit completion signal", () => {
|
|
176
|
+
const ctx = fakeCtx();
|
|
177
|
+
const goal = `complete with tool ${randomUUID()}`;
|
|
178
|
+
|
|
179
|
+
setGoal(ctx, goal);
|
|
180
|
+
const result = completeActiveGoal(ctx, {
|
|
181
|
+
summary: "Implemented the requested behavior.",
|
|
182
|
+
verification: "Ran focused unit tests.",
|
|
183
|
+
source: "tool",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.completed).toBe(true);
|
|
187
|
+
expect(activeGoal(ctx)).toBe("");
|
|
188
|
+
expect(readText(featureFile("orchestration", "goal-completions.jsonl"))).toContain(goal);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("rejects explicit goal completion without verification", () => {
|
|
192
|
+
const ctx = fakeCtx();
|
|
193
|
+
setGoal(ctx, "needs verification");
|
|
194
|
+
|
|
195
|
+
const result = completeActiveGoal(ctx, { summary: "Done", verification: "" });
|
|
196
|
+
|
|
197
|
+
expect(result.completed).toBe(false);
|
|
198
|
+
expect(activeGoal(ctx)).toBe("needs verification");
|
|
199
|
+
clearGoal(ctx);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("registers a goal completion tool", async () => {
|
|
203
|
+
let tool: any;
|
|
204
|
+
const pi = {
|
|
205
|
+
on: () => undefined,
|
|
206
|
+
registerCommand: () => undefined,
|
|
207
|
+
registerTool: (definition: any) => { tool = definition; },
|
|
208
|
+
sendUserMessage: () => undefined,
|
|
209
|
+
} as any;
|
|
210
|
+
const ctx = fakeCtx();
|
|
211
|
+
|
|
212
|
+
registerGoal(pi);
|
|
213
|
+
setGoal(ctx, "finish explicit tool path");
|
|
214
|
+
const response = await tool.execute("call", {
|
|
215
|
+
summary: "Finished explicit path.",
|
|
216
|
+
verification: "Verified with a fake focused check.",
|
|
217
|
+
}, undefined, undefined, ctx);
|
|
218
|
+
|
|
219
|
+
expect(tool.name).toBe("goal_complete");
|
|
220
|
+
expect(response.details.completed).toBe(true);
|
|
221
|
+
expect(activeGoal(ctx)).toBe("");
|
|
222
|
+
});
|
|
223
|
+
|
|
161
224
|
it("clears the active goal immediately when a pending check returns GOAL_DONE", async () => {
|
|
162
225
|
const handlers: Record<string, Array<(event: any, ctx: any) => Promise<void> | void>> = {};
|
|
163
226
|
const pi = {
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
2
3
|
import { appendText, contentText, featureFile, readText, sessionFile, truncate, writeText } from "./internal.js";
|
|
3
4
|
import { clearResearchStateForGoal, readResearchState, writeResearchState, type ResearchState } from "./autoresearch-state.js";
|
|
4
5
|
import { beginGoalCheck, buildGoalCheckPrompt, endGoalCheck, goalCheckResult, hasGoalCheckPending } from "./goal-resolution.js";
|
|
5
6
|
import { clearLoop, triggerLoopTick } from "./loop.js";
|
|
7
|
+
import { clearNoProgressRecovery } from "./novelty-guard.js";
|
|
6
8
|
import { resetAdvisorSessionContext, setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
7
9
|
import { goalArgumentCompletions } from "./completions.js";
|
|
8
10
|
|
|
9
11
|
const FEATURE = "orchestration";
|
|
10
12
|
const CURRENT_FILE = "goal.md";
|
|
11
13
|
const HISTORY_FILE = featureFile(FEATURE, "goal-history.jsonl");
|
|
14
|
+
const COMPLETION_HISTORY_FILE = featureFile(FEATURE, "goal-completions.jsonl");
|
|
12
15
|
|
|
13
16
|
type GoalHistoryEntry = {
|
|
14
17
|
at: string;
|
|
15
18
|
goal: string;
|
|
16
19
|
};
|
|
17
20
|
|
|
21
|
+
export type GoalCompletionInput = {
|
|
22
|
+
summary: string;
|
|
23
|
+
verification: string;
|
|
24
|
+
source?: "tool" | "sentinel";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GoalCompletionResult = {
|
|
28
|
+
completed: boolean;
|
|
29
|
+
goal?: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
18
33
|
export type GoalSetResult = "updated" | "duplicate";
|
|
19
34
|
export type GoalProcessingStartResult = "loop" | "standalone" | "pending";
|
|
20
35
|
|
|
@@ -53,6 +68,7 @@ export function setGoal(ctx: any, goal: string, options: { restartDuplicate?: bo
|
|
|
53
68
|
clearResearchStateForGoal(ctx, previous);
|
|
54
69
|
}
|
|
55
70
|
clearLoop(ctx, { clearResearch: true, preserveCheckins: true });
|
|
71
|
+
clearNoProgressRecovery(ctx);
|
|
56
72
|
writeText(sessionFile(FEATURE, ctx, CURRENT_FILE), note ? `${note}\n` : "");
|
|
57
73
|
resetAdvisorSessionContext(ctx);
|
|
58
74
|
if (note) {
|
|
@@ -68,14 +84,47 @@ export function setGoal(ctx: any, goal: string, options: { restartDuplicate?: bo
|
|
|
68
84
|
|
|
69
85
|
export function clearGoal(ctx: any): void {
|
|
70
86
|
writeText(sessionFile(FEATURE, ctx, CURRENT_FILE), "");
|
|
87
|
+
clearNoProgressRecovery(ctx);
|
|
71
88
|
resetAdvisorSessionContext(ctx);
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
function completionLine(goal: string, input: GoalCompletionInput): string {
|
|
92
|
+
return `${JSON.stringify({
|
|
93
|
+
at: new Date().toISOString(),
|
|
94
|
+
goal,
|
|
95
|
+
summary: input.summary.trim(),
|
|
96
|
+
verification: input.verification.trim(),
|
|
97
|
+
source: input.source ?? "tool",
|
|
98
|
+
})}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function completeActiveGoal(ctx: any, input: GoalCompletionInput): GoalCompletionResult {
|
|
102
|
+
const goal = activeGoal(ctx);
|
|
103
|
+
if (!goal) return { completed: false, reason: "No active goal." };
|
|
104
|
+
|
|
105
|
+
const summary = input.summary.trim();
|
|
106
|
+
const verification = input.verification.trim();
|
|
107
|
+
if (!summary) return { completed: false, goal, reason: "Goal completion requires a summary." };
|
|
108
|
+
if (!verification) return { completed: false, goal, reason: "Goal completion requires verification evidence or an explicit not-verified statement." };
|
|
109
|
+
|
|
110
|
+
appendText(COMPLETION_HISTORY_FILE, completionLine(goal, { ...input, summary, verification }));
|
|
111
|
+
|
|
112
|
+
const research = researchForGoal(ctx, goal);
|
|
113
|
+
if (research) recordResearchResult(ctx, research, "done");
|
|
114
|
+
|
|
115
|
+
endGoalCheck(ctx);
|
|
116
|
+
clearGoal(ctx);
|
|
117
|
+
setGoalStatus(ctx, null);
|
|
118
|
+
clearLoop(ctx, { clearResearch: true });
|
|
119
|
+
return { completed: true, goal };
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
function goalBlock(goal: string): string {
|
|
75
123
|
return [
|
|
76
124
|
"## Pi-Rogue Goal",
|
|
77
125
|
`Current goal: ${goal}`,
|
|
78
|
-
"When
|
|
126
|
+
"When the goal is complete, prefer the `goal_complete` tool with a summary and verification evidence. If that tool is unavailable during a loop tick, answer exactly with `GOAL_DONE: ...`.",
|
|
127
|
+
"When the goal is not complete during a loop tick, answer exactly with `GOAL_CONTINUE: ...` and then take one concrete next action.",
|
|
79
128
|
].join("\n");
|
|
80
129
|
}
|
|
81
130
|
|
|
@@ -154,9 +203,13 @@ export function registerGoal(pi: ExtensionAPI): void {
|
|
|
154
203
|
if (research) recordResearchResult(ctx, research, result);
|
|
155
204
|
|
|
156
205
|
if (result === "done") {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
const text = assistantText(event);
|
|
207
|
+
const summary = text.replace(/^GOAL_DONE:\s*/i, "").trim() || "Goal marked done by sentinel response.";
|
|
208
|
+
completeActiveGoal(ctx, {
|
|
209
|
+
summary: truncate(summary, 1200),
|
|
210
|
+
verification: "GOAL_DONE sentinel response; see assistant message for final state and evidence.",
|
|
211
|
+
source: "sentinel",
|
|
212
|
+
});
|
|
160
213
|
ctx.ui.notify(`🎯 Goal completed: ${truncate(goal, 160)}`, "info");
|
|
161
214
|
}
|
|
162
215
|
});
|
|
@@ -169,6 +222,33 @@ export function registerGoal(pi: ExtensionAPI): void {
|
|
|
169
222
|
return { systemPrompt: `${event.systemPrompt}\n\n${goalBlock(goal)}` };
|
|
170
223
|
});
|
|
171
224
|
|
|
225
|
+
const registerTool = (pi as any).registerTool;
|
|
226
|
+
if (typeof registerTool === "function") registerTool.call(pi, {
|
|
227
|
+
name: "goal_complete",
|
|
228
|
+
label: "Goal Complete",
|
|
229
|
+
description: "Mark the active Pi-Rogue goal complete. Requires a completion summary and verification evidence.",
|
|
230
|
+
parameters: Type.Object({
|
|
231
|
+
summary: Type.String({ description: "What was completed for the active goal" }),
|
|
232
|
+
verification: Type.String({ description: "How completion was verified, or an explicit not-verified statement with reason" }),
|
|
233
|
+
}),
|
|
234
|
+
async execute(_id: unknown, params: { summary?: unknown; verification?: unknown }, _signal: unknown, onUpdate: ((update: unknown) => void) | undefined, ctx: any) {
|
|
235
|
+
const result = completeActiveGoal(ctx, {
|
|
236
|
+
summary: String(params.summary ?? ""),
|
|
237
|
+
verification: String(params.verification ?? ""),
|
|
238
|
+
source: "tool",
|
|
239
|
+
});
|
|
240
|
+
if (!result.completed) {
|
|
241
|
+
const message = result.reason || "Goal completion failed.";
|
|
242
|
+
onUpdate?.({ content: [{ type: "text", text: message }], details: { completed: false } });
|
|
243
|
+
return { content: [{ type: "text", text: message }], details: { completed: false } };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const message = `Goal completed: ${truncate(result.goal || "", 160)}`;
|
|
247
|
+
ctx.ui.notify(`🎯 ${message}`, "info");
|
|
248
|
+
return { content: [{ type: "text", text: message }], details: { completed: true, goal: result.goal } };
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
172
252
|
pi.registerCommand("goal", {
|
|
173
253
|
description: "Set, show, clear, or list the current session goal",
|
|
174
254
|
getArgumentCompletions: (prefix: string) => goalArgumentCompletions(prefix),
|
|
@@ -3,6 +3,7 @@ import { appendText, featureFile, readText, sessionFile, sessionKey, truncate }
|
|
|
3
3
|
import { clearResearchState, hasActiveResearch } from "./autoresearch-state.js";
|
|
4
4
|
import { setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
|
|
5
5
|
import { buildGoalCheckPrompt, beginGoalCheck, hasGoalCheckPending } from "./goal-resolution.js";
|
|
6
|
+
import { clearNoProgressRecovery } from "./novelty-guard.js";
|
|
6
7
|
import { readSessionJson, writeSessionJson } from "./state.js";
|
|
7
8
|
import { loopArgumentCompletions } from "./completions.js";
|
|
8
9
|
|
|
@@ -60,6 +61,7 @@ export function clearLoop(ctx: any, options: { clearResearch?: boolean; preserve
|
|
|
60
61
|
const current = readLoopState(ctx);
|
|
61
62
|
archiveLoopState(ctx, current);
|
|
62
63
|
const next = clearLoopState(ctx);
|
|
64
|
+
clearNoProgressRecovery(ctx);
|
|
63
65
|
stopLoopTimer(sessionKey(ctx));
|
|
64
66
|
setLoopStatus(ctx, next);
|
|
65
67
|
if (!options.preserveCheckins) {
|
|
@@ -202,6 +204,7 @@ export function startLoop(pi: ExtensionAPI, ctx: any, interval: string, instruct
|
|
|
202
204
|
return null;
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
clearNoProgressRecovery(ctx);
|
|
205
208
|
const next = writeLoopState(ctx, {
|
|
206
209
|
enabled: true,
|
|
207
210
|
interval,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import {
|
|
3
4
|
detectAssistantRepetition,
|
|
5
|
+
looksLikeNoProgressTurn,
|
|
4
6
|
normalizeTurn,
|
|
5
7
|
recordAssistantTurn,
|
|
8
|
+
registerNoveltyGuard,
|
|
6
9
|
turnSimilarity,
|
|
7
10
|
type RepetitionGuardState,
|
|
8
11
|
} from "./novelty-guard.js";
|
|
12
|
+
import { readText, sessionFile, writeText } from "./internal.js";
|
|
9
13
|
|
|
10
14
|
describe("repetition guard", () => {
|
|
11
15
|
it("normalizes noisy assistant text", () => {
|
|
@@ -21,6 +25,45 @@ describe("repetition guard", () => {
|
|
|
21
25
|
expect(similarity).toBeGreaterThan(0.8);
|
|
22
26
|
});
|
|
23
27
|
|
|
28
|
+
it("identifies planning-only no-progress turns", () => {
|
|
29
|
+
expect(looksLikeNoProgressTurn("I will think through the approach and plan the next steps.")).toBe(true);
|
|
30
|
+
expect(looksLikeNoProgressTurn("I ran npm test and found one failing assertion.")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("tracks bounded no-progress only while orchestration is active", () => {
|
|
34
|
+
const base: RepetitionGuardState = { recentAssistantTurns: [] };
|
|
35
|
+
const first = recordAssistantTurn(base, "I will think through the approach and plan the next step.", { activeOrchestration: true });
|
|
36
|
+
const second = recordAssistantTurn(first, "I will think through the approach and plan the next step.", { activeOrchestration: true });
|
|
37
|
+
const third = recordAssistantTurn(second, "I will think through the approach and plan the next step.", { activeOrchestration: true });
|
|
38
|
+
|
|
39
|
+
expect(third.noProgress?.count).toBe(3);
|
|
40
|
+
|
|
41
|
+
const inactive = recordAssistantTurn(base, "I will think through the approach and plan the next step.", { activeOrchestration: false });
|
|
42
|
+
expect(inactive.noProgress).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not inject stale no-progress recovery after orchestration is inactive", async () => {
|
|
46
|
+
const handlers: Record<string, (event: any, ctx: any) => Promise<any> | any> = {};
|
|
47
|
+
const pi = {
|
|
48
|
+
on: (name: string, handler: (event: any, ctx: any) => Promise<any> | any) => { handlers[name] = handler; },
|
|
49
|
+
} as any;
|
|
50
|
+
const sessionPath = `/tmp/pi-rogue-novelty-${randomUUID()}.jsonl`;
|
|
51
|
+
const ctx = {
|
|
52
|
+
sessionManager: { getSessionFile: () => sessionPath },
|
|
53
|
+
ui: { notify: () => undefined },
|
|
54
|
+
};
|
|
55
|
+
writeText(sessionFile("orchestration", ctx, "repetition-guard.json"), `${JSON.stringify({
|
|
56
|
+
recentAssistantTurns: [],
|
|
57
|
+
noProgress: { at: new Date().toISOString(), count: 3, text: "I will plan the next step.", reason: "test" },
|
|
58
|
+
})}\n`);
|
|
59
|
+
|
|
60
|
+
registerNoveltyGuard(pi);
|
|
61
|
+
const result = await handlers.before_agent_start?.({ systemPrompt: "base" }, ctx);
|
|
62
|
+
|
|
63
|
+
expect(result.systemPrompt).toBe("base");
|
|
64
|
+
expect(JSON.parse(readText(sessionFile("orchestration", ctx, "repetition-guard.json"))).noProgress).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
24
67
|
it("detects repeated assistant output", () => {
|
|
25
68
|
const base: RepetitionGuardState = { recentAssistantTurns: [] };
|
|
26
69
|
const first = recordAssistantTurn(base, "Now let me build the session-flow analyzer and workflow clustering pipeline.");
|