@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.
Files changed (39) hide show
  1. package/README.md +24 -13
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  7. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  8. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  9. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
  10. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
  11. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
  18. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  19. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  20. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  21. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  22. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  23. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
  24. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  25. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  26. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  27. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  28. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  29. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  30. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  31. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  32. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  33. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  34. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  35. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  36. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  37. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  38. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  39. package/package.json +10 -2
@@ -0,0 +1,23 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerAutoresearch, registerAutoresearchLab } from "./autoresearch.js";
3
+ import { registerGoal } from "./goal.js";
4
+ import { registerLoop } from "./loop.js";
5
+ import { registerNoveltyGuard } from "./novelty-guard.js";
6
+
7
+ export function registerOrchestration(pi: ExtensionAPI): void {
8
+ const p = pi as any;
9
+ if (p.__piRogueOrchestrationRegistered) return;
10
+ p.__piRogueOrchestrationRegistered = true;
11
+
12
+ registerNoveltyGuard(pi);
13
+ registerGoal(pi);
14
+ registerLoop(pi);
15
+ registerAutoresearch(pi);
16
+ registerAutoresearchLab(pi);
17
+ }
18
+
19
+ export { registerAutoresearch, registerAutoresearchLab, registerGoal, registerLoop };
20
+
21
+ export default function orchestrationExtension(pi: ExtensionAPI): void {
22
+ registerOrchestration(pi);
23
+ }
@@ -0,0 +1,36 @@
1
+ import { sessionKey, truncate } from "./internal.js";
2
+
3
+ const goalChecks = new Map<string, boolean>();
4
+
5
+ export function hasGoalCheckPending(ctx: any): boolean {
6
+ return goalChecks.get(sessionKey(ctx)) === true;
7
+ }
8
+
9
+ export function beginGoalCheck(ctx: any): void {
10
+ goalChecks.set(sessionKey(ctx), true);
11
+ }
12
+
13
+ export function endGoalCheck(ctx: any): void {
14
+ goalChecks.delete(sessionKey(ctx));
15
+ }
16
+
17
+ export function buildGoalCheckPrompt(goal: string, instruction: string): string {
18
+ return [
19
+ "Goal check and work request:",
20
+ `Current goal: ${goal}`,
21
+ "Are we done? Ignore step/phase details; judge only whether the goal itself is complete.",
22
+ "If done, start your response with `GOAL_DONE: <short reason>` and summarize final state.",
23
+ "If not done, start your response with `GOAL_CONTINUE: <short reason>` and then continue working.",
24
+ "After `GOAL_CONTINUE`, immediately take the next concrete action toward the goal. Do not only record, restate, or summarize the goal.",
25
+ instruction ? `Current loop instruction: ${truncate(instruction, 400)}` : "",
26
+ ]
27
+ .filter(Boolean)
28
+ .join("\n");
29
+ }
30
+
31
+ export function goalCheckResult(text: string): "done" | "continue" | "unknown" {
32
+ const raw = text.trim();
33
+ if (/^GOAL_DONE\b/i.test(raw)) return "done";
34
+ if (/^GOAL_CONTINUE\b/i.test(raw)) return "continue";
35
+ return "unknown";
36
+ }
@@ -0,0 +1,182 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { resetAdvisorSessionContext, setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
4
+ import { endGoalCheck } from "./goal-resolution.js";
5
+ import { activeGoal, clearGoal, registerGoal, setGoal, startGoalProcessing } from "./goal.js";
6
+ import { featureFile, readText } from "./internal.js";
7
+
8
+ vi.mock("./advisor-checkins.js", () => ({
9
+ resetAdvisorSessionContext: vi.fn(),
10
+ setAdvisorCheckinsEnabled: vi.fn(),
11
+ }));
12
+
13
+ const resetAdvisorSessionContextMock = vi.mocked(resetAdvisorSessionContext);
14
+ const setAdvisorCheckinsEnabledMock = vi.mocked(setAdvisorCheckinsEnabled);
15
+
16
+ function fakeCtx(id = randomUUID(), idle = true) {
17
+ return {
18
+ isIdle: () => idle,
19
+ sessionManager: {
20
+ getSessionFile: () => `/tmp/pi-rogue-goal-test-${id}.jsonl`,
21
+ },
22
+ ui: {
23
+ setStatus: () => undefined,
24
+ notify: () => undefined,
25
+ },
26
+ };
27
+ }
28
+
29
+ describe("goal processing", () => {
30
+ beforeEach(() => {
31
+ resetAdvisorSessionContextMock.mockClear();
32
+ setAdvisorCheckinsEnabledMock.mockClear();
33
+ });
34
+
35
+ function countGoalEntries(text: string, goal: string): number {
36
+ return text
37
+ .split("\n")
38
+ .filter((line) => line.includes(goal))
39
+ .length;
40
+ }
41
+
42
+ it("starts an immediate standalone goal check when no loop is active", () => {
43
+ const ctx = fakeCtx();
44
+ const sent: Array<{ text: string; options?: unknown }> = [];
45
+ const pi = {
46
+ sendUserMessage: (text: string, options?: unknown) => sent.push({ text, options }),
47
+ } as any;
48
+
49
+ expect(setGoal(ctx, "ship a small fix")).toBe("updated");
50
+ const result = startGoalProcessing(pi, ctx, "ship a small fix");
51
+
52
+ expect(result).toBe("standalone");
53
+ expect(sent).toHaveLength(1);
54
+ expect(sent[0].text).toContain("Goal check and work request:");
55
+ expect(sent[0].text).toContain("Current goal: ship a small fix");
56
+ expect(sent[0].text).toContain("Start processing the goal immediately.");
57
+ expect(sent[0].text).toContain("Take the first concrete step now");
58
+ expect(sent[0].text).toContain("Do not only record, restate, or summarize the goal.");
59
+ endGoalCheck(ctx);
60
+ });
61
+
62
+ it("does not append history or reset orchestration for an exact active goal duplicate", () => {
63
+ const ctx = fakeCtx();
64
+ const goal = `dedupe goal ${randomUUID()}`;
65
+ const before = readText(featureFile("orchestration", "goal-history.jsonl"));
66
+
67
+ expect(setGoal(ctx, goal)).toBe("updated");
68
+ setAdvisorCheckinsEnabledMock.mockClear();
69
+ const afterFirst = readText(featureFile("orchestration", "goal-history.jsonl"));
70
+ expect(setGoal(ctx, goal)).toBe("duplicate");
71
+ const afterSecond = readText(featureFile("orchestration", "goal-history.jsonl"));
72
+
73
+ expect(countGoalEntries(before, goal)).toBe(0);
74
+ expect(countGoalEntries(afterFirst, goal)).toBe(1);
75
+ expect(countGoalEntries(afterSecond, goal)).toBe(1);
76
+ expect(resetAdvisorSessionContextMock).toHaveBeenCalledTimes(1);
77
+ expect(setAdvisorCheckinsEnabledMock).toHaveBeenCalledWith(true);
78
+ clearGoal(ctx);
79
+ });
80
+
81
+ it("allows explicit goal changes without cycle heuristics", () => {
82
+ const ctx = fakeCtx();
83
+ const first = `cycle-a ${randomUUID()}`;
84
+ const second = `cycle-b ${randomUUID()}`;
85
+
86
+ expect(setGoal(ctx, first)).toBe("updated");
87
+ expect(setGoal(ctx, second)).toBe("updated");
88
+ expect(setGoal(ctx, first)).toBe("updated");
89
+ expect(setGoal(ctx, second)).toBe("updated");
90
+ expect(setGoal(ctx, first)).toBe("updated");
91
+ expect(setGoal(ctx, second)).toBe("updated");
92
+
93
+ clearGoal(ctx);
94
+ expect(setGoal(ctx, second)).toBe("updated");
95
+ clearGoal(ctx);
96
+ });
97
+
98
+ it("queues immediate standalone goal processing as follow-up when busy", () => {
99
+ const ctx = fakeCtx(undefined, false);
100
+ const sent: Array<{ text: string; options?: unknown }> = [];
101
+ const pi = {
102
+ sendUserMessage: (text: string, options?: unknown) => sent.push({ text, options }),
103
+ } as any;
104
+
105
+ setGoal(ctx, "finish benchmark report");
106
+ const result = startGoalProcessing(pi, ctx, "finish benchmark report");
107
+
108
+ expect(result).toBe("standalone");
109
+ expect(sent).toHaveLength(1);
110
+ expect(sent[0].options).toEqual({ deliverAs: "followUp" });
111
+ endGoalCheck(ctx);
112
+ });
113
+
114
+ it("resets advisor context when a goal is cleared", () => {
115
+ const ctx = fakeCtx();
116
+
117
+ clearGoal(ctx);
118
+
119
+ expect(resetAdvisorSessionContextMock).toHaveBeenCalledTimes(1);
120
+ });
121
+
122
+ it("re-arms advisor check-ins when a session resumes with an active goal", () => {
123
+ const handlers: Record<string, Array<(event: any, ctx: any) => Promise<void> | void>> = {};
124
+ const pi = {
125
+ on: (name: string, handler: (event: any, ctx: any) => Promise<void> | void) => {
126
+ handlers[name] = [...(handlers[name] ?? []), handler];
127
+ },
128
+ registerCommand: () => undefined,
129
+ sendUserMessage: () => undefined,
130
+ } as any;
131
+ const ctx = fakeCtx();
132
+
133
+ registerGoal(pi);
134
+ setGoal(ctx, "resume heartbeat after compaction");
135
+ setAdvisorCheckinsEnabledMock.mockClear();
136
+
137
+ void handlers.session_start?.[0]?.({}, ctx);
138
+
139
+ expect(setAdvisorCheckinsEnabledMock).toHaveBeenCalledWith(true);
140
+ });
141
+
142
+ it("disables advisor check-ins when /goal clear stops the loop", async () => {
143
+ let handler: ((args: string, ctx: any) => Promise<void>) | undefined;
144
+ const pi = {
145
+ on: () => undefined,
146
+ registerCommand: (name: string, command: { handler: (args: string, ctx: any) => Promise<void> }) => {
147
+ if (name === "goal") handler = command.handler;
148
+ },
149
+ sendUserMessage: () => undefined,
150
+ } as any;
151
+ const ctx = fakeCtx();
152
+
153
+ registerGoal(pi);
154
+ setGoal(ctx, "clear lifecycle test");
155
+ setAdvisorCheckinsEnabledMock.mockClear();
156
+ await handler?.("clear", ctx);
157
+
158
+ expect(setAdvisorCheckinsEnabledMock).toHaveBeenCalledWith(false);
159
+ });
160
+
161
+ it("clears the active goal immediately when a pending check returns GOAL_DONE", async () => {
162
+ const handlers: Record<string, Array<(event: any, ctx: any) => Promise<void> | void>> = {};
163
+ const pi = {
164
+ on: (name: string, handler: (event: any, ctx: any) => Promise<void> | void) => {
165
+ handlers[name] = [...(handlers[name] ?? []), handler];
166
+ },
167
+ registerCommand: () => undefined,
168
+ sendUserMessage: () => undefined,
169
+ } as any;
170
+ const ctx = fakeCtx();
171
+
172
+ registerGoal(pi);
173
+ setGoal(ctx, "ship the thing");
174
+ startGoalProcessing(pi, ctx, "ship the thing");
175
+ await handlers.agent_end?.[0]?.({
176
+ messages: [{ role: "assistant", content: "GOAL_DONE: shipped with evidence" }],
177
+ }, ctx);
178
+
179
+ expect(activeGoal(ctx)).toBe("");
180
+ });
181
+
182
+ });
@@ -0,0 +1,232 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { appendText, contentText, featureFile, readText, sessionFile, truncate, writeText } from "./internal.js";
3
+ import { clearResearchStateForGoal, readResearchState, writeResearchState, type ResearchState } from "./autoresearch-state.js";
4
+ import { beginGoalCheck, buildGoalCheckPrompt, endGoalCheck, goalCheckResult, hasGoalCheckPending } from "./goal-resolution.js";
5
+ import { clearLoop, triggerLoopTick } from "./loop.js";
6
+ import { resetAdvisorSessionContext, setAdvisorCheckinsEnabled } from "./advisor-checkins.js";
7
+ import { goalArgumentCompletions } from "./completions.js";
8
+
9
+ const FEATURE = "orchestration";
10
+ const CURRENT_FILE = "goal.md";
11
+ const HISTORY_FILE = featureFile(FEATURE, "goal-history.jsonl");
12
+
13
+ type GoalHistoryEntry = {
14
+ at: string;
15
+ goal: string;
16
+ };
17
+
18
+ export type GoalSetResult = "updated" | "duplicate";
19
+ export type GoalProcessingStartResult = "loop" | "standalone" | "pending";
20
+
21
+ export function activeGoal(ctx: any): string {
22
+ return readText(sessionFile(FEATURE, ctx, CURRENT_FILE)).trim();
23
+ }
24
+
25
+ function historyEntries(limit = 10): GoalHistoryEntry[] {
26
+ const raw = readText(HISTORY_FILE).trim();
27
+ if (!raw) return [];
28
+
29
+ return raw
30
+ .split("\n")
31
+ .filter(Boolean)
32
+ .slice(-limit)
33
+ .map((line) => {
34
+ try {
35
+ return JSON.parse(line) as GoalHistoryEntry;
36
+ } catch {
37
+ return { at: new Date().toISOString(), goal: line };
38
+ }
39
+ });
40
+ }
41
+
42
+ export function setGoal(ctx: any, goal: string, options: { restartDuplicate?: boolean } = {}): GoalSetResult {
43
+ const note = goal.trim();
44
+ const previous = activeGoal(ctx);
45
+ if (previous === note && !options.restartDuplicate) {
46
+ if (note) {
47
+ setAdvisorCheckinsEnabled(true);
48
+ }
49
+ return "duplicate";
50
+ }
51
+
52
+ if (previous && previous !== note) {
53
+ clearResearchStateForGoal(ctx, previous);
54
+ }
55
+ clearLoop(ctx, { clearResearch: true, preserveCheckins: true });
56
+ writeText(sessionFile(FEATURE, ctx, CURRENT_FILE), note ? `${note}\n` : "");
57
+ resetAdvisorSessionContext();
58
+ if (note) {
59
+ setAdvisorCheckinsEnabled(true);
60
+ }
61
+ endGoalCheck(ctx);
62
+
63
+ if (note) {
64
+ appendText(HISTORY_FILE, `${JSON.stringify({ at: new Date().toISOString(), goal: note })}\n`);
65
+ }
66
+ return "updated";
67
+ }
68
+
69
+ export function clearGoal(ctx: any): void {
70
+ writeText(sessionFile(FEATURE, ctx, CURRENT_FILE), "");
71
+ resetAdvisorSessionContext();
72
+ }
73
+
74
+ function goalBlock(goal: string): string {
75
+ return [
76
+ "## Pi-Rogue Goal",
77
+ `Current goal: ${goal}`,
78
+ "When a loop tick asks whether the goal is done, answer exactly with `GOAL_DONE: ...` or `GOAL_CONTINUE: ...`.",
79
+ ].join("\n");
80
+ }
81
+
82
+ export function setGoalStatus(ctx: any, goal: string | null): void {
83
+ ctx.ui.setStatus("orchestration-goal", goal ? `🎯 ${truncate(goal, 60)}` : undefined);
84
+ }
85
+
86
+ function assistantText(event: any): string {
87
+ const messages = Array.isArray(event?.messages) ? event.messages : [];
88
+ const lastAssistant = [...messages].reverse().find((m: any) => m?.role === "assistant");
89
+ return contentText(lastAssistant?.content);
90
+ }
91
+
92
+ function researchForGoal(ctx: any, goal: string): ResearchState | null {
93
+ const state = readResearchState(ctx);
94
+ if (!state.instruction || !state.goal || state.goal !== goal) return null;
95
+ return state;
96
+ }
97
+
98
+ function recordResearchResult(ctx: any, state: ResearchState, result: "done" | "continue" | "unknown"): void {
99
+ writeResearchState(ctx, {
100
+ ...state,
101
+ cycles: (state.cycles ?? 0) + 1,
102
+ lastResult: result,
103
+ });
104
+ }
105
+
106
+ export function startGoalProcessing(pi: ExtensionAPI, ctx: any, goal: string): GoalProcessingStartResult {
107
+ if (hasGoalCheckPending(ctx)) {
108
+ return "pending";
109
+ }
110
+
111
+ if (triggerLoopTick(pi, ctx)) {
112
+ return "loop";
113
+ }
114
+
115
+ beginGoalCheck(ctx);
116
+ const prompt = buildGoalCheckPrompt(
117
+ goal,
118
+ "Start processing the goal immediately. Take the first concrete step now: inspect, run, edit, or ask only if a specific blocker prevents action.",
119
+ );
120
+ if (ctx.isIdle?.() === false) {
121
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
122
+ } else {
123
+ pi.sendUserMessage(prompt);
124
+ }
125
+ return "standalone";
126
+ }
127
+
128
+ export function registerGoal(pi: ExtensionAPI): void {
129
+ const p = pi as any;
130
+ if (p.__piRogueGoalRegistered) return;
131
+ p.__piRogueGoalRegistered = true;
132
+
133
+ pi.on("session_start", (_event, ctx) => {
134
+ endGoalCheck(ctx);
135
+ const goal = activeGoal(ctx);
136
+ setGoalStatus(ctx, goal || null);
137
+ if (goal) {
138
+ setAdvisorCheckinsEnabled(true);
139
+ }
140
+ });
141
+
142
+ pi.on("session_shutdown", (_event, ctx) => {
143
+ endGoalCheck(ctx);
144
+ });
145
+
146
+ pi.on("agent_end", async (event, ctx) => {
147
+ const goal = activeGoal(ctx);
148
+ if (!goal || !hasGoalCheckPending(ctx)) return;
149
+
150
+ const result = goalCheckResult(assistantText(event));
151
+ endGoalCheck(ctx);
152
+
153
+ const research = researchForGoal(ctx, goal);
154
+ if (research) recordResearchResult(ctx, research, result);
155
+
156
+ if (result === "done") {
157
+ clearGoal(ctx);
158
+ setGoalStatus(ctx, null);
159
+ clearLoop(ctx, { clearResearch: true });
160
+ ctx.ui.notify(`🎯 Goal completed: ${truncate(goal, 160)}`, "info");
161
+ }
162
+ });
163
+
164
+ pi.on("before_agent_start", async (event, ctx) => {
165
+ const goal = activeGoal(ctx);
166
+ setGoalStatus(ctx, goal || null);
167
+ if (!goal) return { systemPrompt: event.systemPrompt };
168
+
169
+ return { systemPrompt: `${event.systemPrompt}\n\n${goalBlock(goal)}` };
170
+ });
171
+
172
+ pi.registerCommand("goal", {
173
+ description: "Set, show, clear, or list the current session goal",
174
+ getArgumentCompletions: (prefix: string) => goalArgumentCompletions(prefix),
175
+ handler: async (args, ctx) => {
176
+ const input = String(args ?? "").trim();
177
+ const [cmd, ...rest] = input.split(/\s+/);
178
+ const known = new Set(["set", "show", "clear", "list"]);
179
+ const resolved = !input ? "show" : known.has(cmd) ? cmd : "set";
180
+ const text = resolved === "set" && known.has(cmd) ? rest.join(" ").trim() : input;
181
+
182
+ if (resolved === "show") {
183
+ const goal = activeGoal(ctx);
184
+ setGoalStatus(ctx, goal || null);
185
+ ctx.ui.notify(goal ? `🎯 ${truncate(goal, 160)}` : "No active goal.", "info");
186
+ return;
187
+ }
188
+
189
+ if (resolved === "clear") {
190
+ const goal = activeGoal(ctx);
191
+ const clearedResearch = goal ? clearResearchStateForGoal(ctx, goal) : false;
192
+ clearGoal(ctx);
193
+ endGoalCheck(ctx);
194
+ setGoalStatus(ctx, null);
195
+ clearLoop(ctx, { clearResearch: true });
196
+ ctx.ui.notify(goal ? `Goal cleared${clearedResearch ? "; matching autoresearch status cleared" : ""}.` : "No goal to clear.", "info");
197
+ return;
198
+ }
199
+
200
+ if (resolved === "list") {
201
+ const entries = historyEntries();
202
+ if (entries.length === 0) {
203
+ ctx.ui.notify("No goal history yet.", "info");
204
+ return;
205
+ }
206
+
207
+ ctx.ui.notify(
208
+ entries
209
+ .map((entry, index) => `${index + 1}. ${truncate(entry.goal, 120)} (${new Date(entry.at).toLocaleDateString()})`)
210
+ .join("\n"),
211
+ "info",
212
+ );
213
+ return;
214
+ }
215
+
216
+ if (!text) {
217
+ ctx.ui.notify("Usage: /goal set <text>", "error");
218
+ return;
219
+ }
220
+
221
+ const result = setGoal(ctx, text);
222
+ if (result === "duplicate") {
223
+ ctx.ui.notify(`🎯 Goal already active: ${truncate(text, 160)}.`, "info");
224
+ return;
225
+ }
226
+
227
+ setGoalStatus(ctx, text);
228
+ const started = startGoalProcessing(pi, ctx, text);
229
+ ctx.ui.notify(`🎯 Goal set: ${truncate(text, 160)}${started === "pending" ? " (goal processing already pending)" : " — processing started"}`, "info");
230
+ },
231
+ });
232
+ }
@@ -0,0 +1 @@
1
+ export { default, registerAutoresearch, registerGoal, registerLoop, registerOrchestration } from "./extension.js";
@@ -0,0 +1,98 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const ROOT_DIR = join(homedir(), ".pi", "agent", "fiale-plus");
6
+
7
+ export function truncate(text: string, max: number): string {
8
+ if (text.length <= max) return text;
9
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
10
+ }
11
+
12
+ export function contentText(content: unknown): string {
13
+ if (typeof content === "string") return content.trim();
14
+ if (content && typeof content === "object" && !Array.isArray(content)) {
15
+ const block = content as Record<string, unknown>;
16
+ if (typeof block.text === "string") return block.text.trim();
17
+ if (block.content !== undefined) return contentText(block.content);
18
+ if (block.message !== undefined) return contentText(block.message);
19
+ return "";
20
+ }
21
+ if (!Array.isArray(content)) return String(content ?? "").trim();
22
+
23
+ const parts: string[] = [];
24
+ for (const item of content) {
25
+ if (!item) continue;
26
+ if (typeof item === "string") {
27
+ parts.push(item);
28
+ continue;
29
+ }
30
+
31
+ const block = item as Record<string, unknown>;
32
+ if (typeof block.text === "string") {
33
+ parts.push(block.text);
34
+ } else if (block.content !== undefined) {
35
+ const nested = contentText(block.content);
36
+ if (nested) parts.push(nested);
37
+ } else if (block.message !== undefined) {
38
+ const nested = contentText(block.message);
39
+ if (nested) parts.push(nested);
40
+ }
41
+ }
42
+
43
+ return parts.join("\n").replace(/\s+/g, " ").trim();
44
+ }
45
+
46
+ function ensureParent(filePath: string): string {
47
+ mkdirSync(dirname(filePath), { recursive: true });
48
+ return filePath;
49
+ }
50
+
51
+ export function readText(filePath: string, fallback = ""): string {
52
+ try {
53
+ return readFileSync(filePath, "utf8");
54
+ } catch {
55
+ return fallback;
56
+ }
57
+ }
58
+
59
+ export function writeText(filePath: string, text: string): void {
60
+ ensureParent(filePath);
61
+ writeFileSync(filePath, text, "utf8");
62
+ }
63
+
64
+ export function appendText(filePath: string, text: string): void {
65
+ ensureParent(filePath);
66
+ appendFileSync(filePath, text, "utf8");
67
+ }
68
+
69
+ export function appDir(): string {
70
+ mkdirSync(ROOT_DIR, { recursive: true });
71
+ return ROOT_DIR;
72
+ }
73
+
74
+ export function featureDir(feature: string): string {
75
+ const dir = join(appDir(), feature);
76
+ mkdirSync(dir, { recursive: true });
77
+ return dir;
78
+ }
79
+
80
+ export function sessionKey(ctx: any): string {
81
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
82
+ if (!sessionFile) return "session";
83
+ return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
84
+ }
85
+
86
+ export function sessionDir(feature: string, ctx: any): string {
87
+ const dir = join(featureDir(feature), sessionKey(ctx));
88
+ mkdirSync(dir, { recursive: true });
89
+ return dir;
90
+ }
91
+
92
+ export function featureFile(feature: string, filename: string): string {
93
+ return join(featureDir(feature), filename);
94
+ }
95
+
96
+ export function sessionFile(feature: string, ctx: any, filename: string): string {
97
+ return join(sessionDir(feature, ctx), filename);
98
+ }