pi-goal-x 0.6.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/LICENSE +21 -0
- package/README.md +307 -0
- package/docs/agent-flow-design.md +432 -0
- package/docs/agentic-runtime-prd.md +764 -0
- package/docs/architecture.md +239 -0
- package/docs/goal-ts-refactor-test-strategy.md +82 -0
- package/docs/pi-autoresearch-survey.md +45 -0
- package/extensions/goal-auditor.ts +341 -0
- package/extensions/goal-compaction.ts +124 -0
- package/extensions/goal-core.ts +77 -0
- package/extensions/goal-draft.ts +148 -0
- package/extensions/goal-ledger.ts +319 -0
- package/extensions/goal-policy.ts +152 -0
- package/extensions/goal-pool.ts +94 -0
- package/extensions/goal-questionnaire.ts +533 -0
- package/extensions/goal-record.ts +171 -0
- package/extensions/goal-tool-names.ts +69 -0
- package/extensions/goal.ts +2610 -0
- package/extensions/prompts/goal-prompts.ts +166 -0
- package/extensions/storage/goal-files.ts +267 -0
- package/extensions/widgets/goal-notifications.ts +9 -0
- package/extensions/widgets/goal-widget.ts +219 -0
- package/package.json +57 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { normalizeRelPath, nowIso, safeIdPart, type GoalRecord } from "./goal-record.ts";
|
|
4
|
+
|
|
5
|
+
export const GOAL_LEDGER_FILE = ".pi/goals/goal_events.jsonl";
|
|
6
|
+
|
|
7
|
+
export type GoalLedgerEvent =
|
|
8
|
+
| { type: "goal_created"; goalId: string; objective: string; sisyphus: boolean; autoContinue: boolean; at: string }
|
|
9
|
+
| { type: "goal_focused"; goalId: string; reason: string; at: string }
|
|
10
|
+
| { type: "goal_unfocused"; reason: string; at: string }
|
|
11
|
+
| { type: "goal_paused"; goalId: string; reason: string; suggestedAction?: string; status?: "paused"; at: string }
|
|
12
|
+
| { type: "goal_resumed"; goalId: string; reason: string; at: string }
|
|
13
|
+
| { type: "goal_tweaked"; goalId: string; changeSummary: string; at: string }
|
|
14
|
+
| { type: "completion_requested"; goalId: string; summary?: string; at: string }
|
|
15
|
+
| { type: "audit_started"; goalId: string; provider?: string; model?: string; thinkingLevel?: string; at: string }
|
|
16
|
+
| { type: "audit_result"; goalId: string; verdict: "approved" | "disapproved" | "error"; report: string; at: string }
|
|
17
|
+
| { type: "audit_skipped"; goalId: string; reason: "disabled" | "user_aborted"; provider?: string; model?: string; thinkingLevel?: string; at: string }
|
|
18
|
+
| { type: "goal_completed"; goalId: string; archivePath?: string; at: string }
|
|
19
|
+
| { type: "goal_aborted"; goalId: string; reason: string; archivePath?: string; at: string };
|
|
20
|
+
|
|
21
|
+
export interface GoalLedgerContext {
|
|
22
|
+
cwd: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GoalLedgerReadResult {
|
|
26
|
+
events: GoalLedgerEvent[];
|
|
27
|
+
malformed: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ReconstructedGoalState {
|
|
31
|
+
goalId: string;
|
|
32
|
+
latestStatus: "active" | "paused" | "complete" | "aborted" | "unknown";
|
|
33
|
+
latestFocus: boolean;
|
|
34
|
+
latestPauseReason?: string;
|
|
35
|
+
latestPauseSuggestedAction?: string;
|
|
36
|
+
latestAuditorResult?: { verdict: "approved" | "disapproved" | "error"; report: string; at: string };
|
|
37
|
+
createdAt?: string;
|
|
38
|
+
completedAt?: string;
|
|
39
|
+
abortedAt?: string;
|
|
40
|
+
tweakedAt?: string;
|
|
41
|
+
resumedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ReconstructedLedgerState {
|
|
45
|
+
focusedGoalId: string | null;
|
|
46
|
+
goals: Map<string, ReconstructedGoalState>;
|
|
47
|
+
terminalGoals: Map<string, ReconstructedGoalState>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeGoalId(value: string): string {
|
|
51
|
+
return safeIdPart(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function goalLedgerPath(ctx: GoalLedgerContext): string {
|
|
55
|
+
return path.resolve(ctx.cwd, normalizeRelPath(GOAL_LEDGER_FILE));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function appendGoalEvent(ctx: GoalLedgerContext, event: GoalLedgerEvent): void {
|
|
59
|
+
const filePath = goalLedgerPath(ctx);
|
|
60
|
+
const dir = path.dirname(filePath);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const line = JSON.stringify(event) + "\n";
|
|
64
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
65
|
+
let appended = false;
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(tempPath, line, { flag: "wx", encoding: "utf8" });
|
|
68
|
+
fs.appendFileSync(filePath, fs.readFileSync(tempPath, "utf8"), "utf8");
|
|
69
|
+
appended = true;
|
|
70
|
+
} catch {
|
|
71
|
+
// If temp write fails, try direct append as fallback.
|
|
72
|
+
// Skip fallback only if the primary append already succeeded.
|
|
73
|
+
if (!appended) {
|
|
74
|
+
try {
|
|
75
|
+
fs.appendFileSync(filePath, line, "utf8");
|
|
76
|
+
appended = true;
|
|
77
|
+
} catch {
|
|
78
|
+
// Ledger append failure should not crash the transaction.
|
|
79
|
+
// Callers that need strict durability can check the return.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
try {
|
|
84
|
+
fs.unlinkSync(tempPath);
|
|
85
|
+
} catch {
|
|
86
|
+
// Temp file may not exist; ignore cleanup failure.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readGoalLedger(ctx: GoalLedgerContext): GoalLedgerReadResult {
|
|
92
|
+
const filePath = goalLedgerPath(ctx);
|
|
93
|
+
let content: string;
|
|
94
|
+
try {
|
|
95
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
96
|
+
} catch {
|
|
97
|
+
return { events: [], malformed: 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const events: GoalLedgerEvent[] = [];
|
|
101
|
+
let malformed = 0;
|
|
102
|
+
for (const line of content.split("\n")) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed) continue;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
107
|
+
if (isValidLedgerEvent(parsed)) {
|
|
108
|
+
events.push(sanitizeEvent(parsed));
|
|
109
|
+
} else {
|
|
110
|
+
malformed++;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
malformed++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { events, malformed };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isValidLedgerEvent(value: unknown): value is GoalLedgerEvent {
|
|
120
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
121
|
+
const obj = value as Record<string, unknown>;
|
|
122
|
+
if (typeof obj.type !== "string") return false;
|
|
123
|
+
if (typeof obj.at !== "string") return false;
|
|
124
|
+
const type = obj.type as GoalLedgerEvent["type"];
|
|
125
|
+
switch (type) {
|
|
126
|
+
case "goal_created":
|
|
127
|
+
return typeof obj.goalId === "string" && typeof obj.objective === "string" && typeof obj.sisyphus === "boolean" && typeof obj.autoContinue === "boolean";
|
|
128
|
+
case "goal_focused":
|
|
129
|
+
return typeof obj.goalId === "string" && typeof obj.reason === "string";
|
|
130
|
+
case "goal_unfocused":
|
|
131
|
+
return typeof obj.reason === "string";
|
|
132
|
+
case "goal_paused":
|
|
133
|
+
return typeof obj.goalId === "string" && typeof obj.reason === "string" && (obj.suggestedAction === undefined || typeof obj.suggestedAction === "string") && (obj.status === undefined || obj.status === "paused");
|
|
134
|
+
case "goal_resumed":
|
|
135
|
+
return typeof obj.goalId === "string" && typeof obj.reason === "string";
|
|
136
|
+
case "goal_tweaked":
|
|
137
|
+
return typeof obj.goalId === "string" && typeof obj.changeSummary === "string";
|
|
138
|
+
case "completion_requested":
|
|
139
|
+
return typeof obj.goalId === "string" && (obj.summary === undefined || typeof obj.summary === "string");
|
|
140
|
+
case "audit_started":
|
|
141
|
+
return typeof obj.goalId === "string" && (obj.provider === undefined || typeof obj.provider === "string") && (obj.model === undefined || typeof obj.model === "string") && (obj.thinkingLevel === undefined || typeof obj.thinkingLevel === "string");
|
|
142
|
+
case "audit_result":
|
|
143
|
+
return typeof obj.goalId === "string" && (obj.verdict === "approved" || obj.verdict === "disapproved" || obj.verdict === "error") && typeof obj.report === "string";
|
|
144
|
+
case "audit_skipped":
|
|
145
|
+
return typeof obj.goalId === "string" && (obj.reason === "disabled" || obj.reason === "user_aborted") && (obj.provider === undefined || typeof obj.provider === "string") && (obj.model === undefined || typeof obj.model === "string") && (obj.thinkingLevel === undefined || typeof obj.thinkingLevel === "string");
|
|
146
|
+
case "goal_completed":
|
|
147
|
+
return typeof obj.goalId === "string" && (obj.archivePath === undefined || typeof obj.archivePath === "string");
|
|
148
|
+
case "goal_aborted":
|
|
149
|
+
return typeof obj.goalId === "string" && typeof obj.reason === "string" && (obj.archivePath === undefined || typeof obj.archivePath === "string");
|
|
150
|
+
default:
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sanitizeEvent(event: GoalLedgerEvent): GoalLedgerEvent {
|
|
156
|
+
switch (event.type) {
|
|
157
|
+
case "goal_created":
|
|
158
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
159
|
+
case "goal_focused":
|
|
160
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
161
|
+
case "goal_paused":
|
|
162
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
163
|
+
case "goal_resumed":
|
|
164
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
165
|
+
case "goal_tweaked":
|
|
166
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
167
|
+
case "completion_requested":
|
|
168
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
169
|
+
case "audit_started":
|
|
170
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
171
|
+
case "audit_result":
|
|
172
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
173
|
+
case "audit_skipped":
|
|
174
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
175
|
+
case "goal_completed":
|
|
176
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
177
|
+
case "goal_aborted":
|
|
178
|
+
return { ...event, goalId: safeGoalId(event.goalId) };
|
|
179
|
+
case "goal_unfocused":
|
|
180
|
+
return event;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function reconstructGoalLedger(events: GoalLedgerEvent[]): ReconstructedLedgerState {
|
|
185
|
+
const goals = new Map<string, ReconstructedGoalState>();
|
|
186
|
+
const terminalGoals = new Map<string, ReconstructedGoalState>();
|
|
187
|
+
let focusedGoalId: string | null = null;
|
|
188
|
+
|
|
189
|
+
for (const event of events) {
|
|
190
|
+
switch (event.type) {
|
|
191
|
+
case "goal_created": {
|
|
192
|
+
const state: ReconstructedGoalState = {
|
|
193
|
+
goalId: event.goalId,
|
|
194
|
+
latestStatus: "active",
|
|
195
|
+
latestFocus: false,
|
|
196
|
+
createdAt: event.at,
|
|
197
|
+
};
|
|
198
|
+
goals.set(event.goalId, state);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "goal_focused": {
|
|
202
|
+
focusedGoalId = event.goalId;
|
|
203
|
+
for (const g of goals.values()) g.latestFocus = false;
|
|
204
|
+
for (const g of terminalGoals.values()) g.latestFocus = false;
|
|
205
|
+
const state = goals.get(event.goalId) ?? terminalGoals.get(event.goalId);
|
|
206
|
+
if (state) state.latestFocus = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "goal_unfocused": {
|
|
210
|
+
focusedGoalId = null;
|
|
211
|
+
for (const g of goals.values()) g.latestFocus = false;
|
|
212
|
+
for (const g of terminalGoals.values()) g.latestFocus = false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case "goal_paused": {
|
|
216
|
+
const state = goals.get(event.goalId);
|
|
217
|
+
if (state) {
|
|
218
|
+
state.latestStatus = event.status ?? "paused";
|
|
219
|
+
state.latestPauseReason = event.reason;
|
|
220
|
+
state.latestPauseSuggestedAction = event.suggestedAction;
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "goal_resumed": {
|
|
225
|
+
const state = goals.get(event.goalId);
|
|
226
|
+
if (state) {
|
|
227
|
+
state.latestStatus = "active";
|
|
228
|
+
state.resumedAt = event.at;
|
|
229
|
+
delete state.latestPauseReason;
|
|
230
|
+
delete state.latestPauseSuggestedAction;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "goal_tweaked": {
|
|
235
|
+
const state = goals.get(event.goalId);
|
|
236
|
+
if (state) state.tweakedAt = event.at;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "completion_requested": {
|
|
240
|
+
// No status change until audit_result or goal_completed
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "audit_started": {
|
|
244
|
+
// No state change
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "audit_skipped": {
|
|
248
|
+
// audit was skipped; goal continues as-is
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "audit_result": {
|
|
252
|
+
const state = goals.get(event.goalId) ?? terminalGoals.get(event.goalId);
|
|
253
|
+
if (state) {
|
|
254
|
+
state.latestAuditorResult = { verdict: event.verdict, report: event.report, at: event.at };
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "goal_completed": {
|
|
259
|
+
let state = goals.get(event.goalId);
|
|
260
|
+
if (!state) {
|
|
261
|
+
state = { goalId: event.goalId, latestStatus: "complete", latestFocus: false }; }
|
|
262
|
+
state.latestStatus = "complete";
|
|
263
|
+
state.completedAt = event.at;
|
|
264
|
+
terminalGoals.set(event.goalId, state);
|
|
265
|
+
goals.delete(event.goalId);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "goal_aborted": {
|
|
269
|
+
let state = goals.get(event.goalId);
|
|
270
|
+
if (!state) {
|
|
271
|
+
state = { goalId: event.goalId, latestStatus: "aborted", latestFocus: false }; }
|
|
272
|
+
state.latestStatus = "aborted";
|
|
273
|
+
state.abortedAt = event.at;
|
|
274
|
+
terminalGoals.set(event.goalId, state);
|
|
275
|
+
goals.delete(event.goalId);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// If the focused goal was moved to terminal (e.g., aborted/completed), clear focus.
|
|
282
|
+
if (focusedGoalId && !goals.has(focusedGoalId)) {
|
|
283
|
+
focusedGoalId = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { focusedGoalId, goals, terminalGoals };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function latestAuditorResultForGoal(events: GoalLedgerEvent[], goalId: string): { verdict: "approved" | "disapproved" | "error"; report: string; at: string } | undefined {
|
|
290
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
291
|
+
const event = events[i];
|
|
292
|
+
if (event.type === "audit_result" && event.goalId === goalId) {
|
|
293
|
+
return { verdict: event.verdict, report: event.report, at: event.at };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function latestEventsForGoal(events: GoalLedgerEvent[], goalId: string, limit = 10): GoalLedgerEvent[] {
|
|
300
|
+
const result: GoalLedgerEvent[] = [];
|
|
301
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
302
|
+
const event = events[i];
|
|
303
|
+
if ("goalId" in event && event.goalId === goalId) {
|
|
304
|
+
result.unshift(event);
|
|
305
|
+
if (result.length >= limit) break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function latestGoalLifecycleEvent(events: GoalLedgerEvent[], goalId: string): GoalLedgerEvent | undefined {
|
|
312
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
313
|
+
const event = events[i];
|
|
314
|
+
if ("goalId" in event && event.goalId === goalId) {
|
|
315
|
+
return event;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { statusLabel, type GoalDisplayRecordLike } from "./goal-core.ts";
|
|
2
|
+
|
|
3
|
+
export type GoalStatusLike = "active" | "paused" | "complete";
|
|
4
|
+
export type StopReasonLike = "user" | "agent";
|
|
5
|
+
|
|
6
|
+
export interface GoalPolicyRecordLike extends GoalDisplayRecordLike {
|
|
7
|
+
id: string;
|
|
8
|
+
status: GoalStatusLike;
|
|
9
|
+
updatedAt?: string;
|
|
10
|
+
pauseReason?: string;
|
|
11
|
+
pauseSuggestedAction?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PolicyValidation =
|
|
15
|
+
| { ok: true }
|
|
16
|
+
| { ok: false; message: string };
|
|
17
|
+
|
|
18
|
+
export function isGoalUnfinished(goal: Pick<GoalPolicyRecordLike, "status"> | null | undefined): boolean {
|
|
19
|
+
return !!goal && goal.status !== "complete";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isRunnableStatus(status: GoalStatusLike): boolean {
|
|
23
|
+
return status === "active";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isCompletableStatus(status: GoalStatusLike): boolean {
|
|
27
|
+
return status === "active" || status === "paused";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateGoalCreationSlot(goal: Pick<GoalPolicyRecordLike, "status"> | null): PolicyValidation {
|
|
31
|
+
void goal;
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateGoalCompletion(args: {
|
|
36
|
+
goal: GoalPolicyRecordLike | null;
|
|
37
|
+
runningGoalId?: string | null;
|
|
38
|
+
}): PolicyValidation {
|
|
39
|
+
const { goal, runningGoalId } = args;
|
|
40
|
+
if (!goal) return { ok: false, message: "No goal is set." };
|
|
41
|
+
if (runningGoalId && goal.id !== runningGoalId) return { ok: false, message: "The active goal changed during this run; not marking it complete." };
|
|
42
|
+
if (!isCompletableStatus(goal.status)) return { ok: false, message: `Goal is ${statusLabel(goal)}; update_goal does not apply.` };
|
|
43
|
+
return { ok: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateGoalAbort(args: {
|
|
47
|
+
goal: GoalPolicyRecordLike | null;
|
|
48
|
+
runningGoalId?: string | null;
|
|
49
|
+
reason: string;
|
|
50
|
+
}): PolicyValidation {
|
|
51
|
+
const { goal, runningGoalId } = args;
|
|
52
|
+
if (!goal) return { ok: false, message: "No goal is set; abort_goal is a no-op." };
|
|
53
|
+
if (runningGoalId && goal.id !== runningGoalId) return { ok: false, message: "The active goal changed during this run; not aborting." };
|
|
54
|
+
if (goal.status === "complete") return { ok: false, message: "Goal is complete; abort_goal does not apply." };
|
|
55
|
+
if (!args.reason.trim()) return { ok: false, message: "abort_goal requires a non-empty reason." };
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validatePauseGoal(args: {
|
|
60
|
+
goal: GoalPolicyRecordLike | null;
|
|
61
|
+
runningGoalId?: string | null;
|
|
62
|
+
reason: string;
|
|
63
|
+
}): PolicyValidation {
|
|
64
|
+
const { goal, runningGoalId } = args;
|
|
65
|
+
if (!goal) return { ok: false, message: "No goal is set; pause_goal is a no-op." };
|
|
66
|
+
if (runningGoalId && goal.id !== runningGoalId) return { ok: false, message: "The active goal changed during this run; not pausing." };
|
|
67
|
+
if (!isRunnableStatus(goal.status)) return { ok: false, message: `Goal is ${statusLabel(goal)}; pause_goal does not apply.` };
|
|
68
|
+
if (!args.reason.trim()) return { ok: false, message: "pause_goal requires a non-empty reason." };
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildPausedByAgentGoal<T extends GoalPolicyRecordLike>(goal: T, args: {
|
|
73
|
+
reason: string;
|
|
74
|
+
suggestedAction?: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
}): T {
|
|
77
|
+
const suggested = args.suggestedAction?.trim() || undefined;
|
|
78
|
+
return {
|
|
79
|
+
...goal,
|
|
80
|
+
status: "paused",
|
|
81
|
+
autoContinue: false,
|
|
82
|
+
stopReason: "agent",
|
|
83
|
+
pauseReason: args.reason.trim(),
|
|
84
|
+
pauseSuggestedAction: suggested,
|
|
85
|
+
updatedAt: args.updatedAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildAbortedByAgentGoal<T extends GoalPolicyRecordLike>(goal: T, args: {
|
|
90
|
+
reason: string;
|
|
91
|
+
updatedAt: string;
|
|
92
|
+
}): T {
|
|
93
|
+
return {
|
|
94
|
+
...goal,
|
|
95
|
+
status: "paused",
|
|
96
|
+
autoContinue: false,
|
|
97
|
+
stopReason: "agent",
|
|
98
|
+
pauseReason: `Aborted: ${args.reason.trim()}`,
|
|
99
|
+
pauseSuggestedAction: undefined,
|
|
100
|
+
updatedAt: args.updatedAt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function validateResumeGoal(goal: GoalPolicyRecordLike | null): PolicyValidation {
|
|
105
|
+
if (!goal) return { ok: false, message: "No goal is set. Use /goals or /sisyphus to discuss, or /goals-set / /sisyphus-set to start immediately." };
|
|
106
|
+
if (goal.status === "complete") return { ok: false, message: "Goal is complete. Use /goals to discuss a new one or /goals-set to start immediately." };
|
|
107
|
+
if (goal.status === "active" && goal.autoContinue) return { ok: false, message: "Goal is already running." };
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function clearGoalCommandMessage(args: { archived: boolean; wasDrafting: boolean }): string {
|
|
112
|
+
return args.archived ? "Goal cleared and archived." : args.wasDrafting ? "Drafting cancelled." : "No goal is set.";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function abortGoalCommandMessage(args: { archived: boolean; wasDrafting: boolean }): string {
|
|
116
|
+
return args.archived ? "Goal aborted and archived." : args.wasDrafting ? "Drafting cancelled." : "No goal is set.";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildCompletionReport(args: { detailedSummary: string; completionSummary?: string | null; auditorReport?: string | null }): string {
|
|
120
|
+
const auditorReport = args.auditorReport?.trim();
|
|
121
|
+
const lines = auditorReport
|
|
122
|
+
? ["Goal audit approved.", "", "Auditor approval:", auditorReport, "", "Goal complete."]
|
|
123
|
+
: ["Goal complete."];
|
|
124
|
+
const summary = args.completionSummary?.trim();
|
|
125
|
+
if (summary) {
|
|
126
|
+
lines.push("", "Completion summary:", summary);
|
|
127
|
+
}
|
|
128
|
+
lines.push("", args.detailedSummary);
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildGoalCreatedReport(args: { objective: string; detailedSummary?: string | null }): string {
|
|
133
|
+
const lines = ["Goal confirmed and created.", "", "Finalized goal:", "", args.objective.trim()];
|
|
134
|
+
const summary = args.detailedSummary?.trim();
|
|
135
|
+
if (summary) {
|
|
136
|
+
lines.push("", "Goal details:", summary);
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function shouldQueueContinuation(goal: Pick<GoalPolicyRecordLike, "status" | "autoContinue"> | null): boolean {
|
|
142
|
+
return !!goal && goal.status === "active" && goal.autoContinue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
export function shouldArmPostCompactReminder(goal: Pick<GoalPolicyRecordLike, "sisyphus" | "status"> | null): boolean {
|
|
147
|
+
return !!goal && isRunnableStatus(goal.status);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function shouldInjectPostCompactReminder(args: { pending: boolean; goal: Pick<GoalPolicyRecordLike, "sisyphus"> | null }): boolean {
|
|
151
|
+
return args.pending && !!args.goal;
|
|
152
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
displayObjectiveTitle,
|
|
3
|
+
formatDuration,
|
|
4
|
+
formatTokenValue,
|
|
5
|
+
statusLabel,
|
|
6
|
+
truncateText,
|
|
7
|
+
} from "./goal-core.ts";
|
|
8
|
+
import { cloneGoal, type GoalFocusEntry, type GoalRecord } from "./goal-record.ts";
|
|
9
|
+
|
|
10
|
+
export function goalPoolFromGoals(goals: Iterable<GoalRecord>): Map<string, GoalRecord> {
|
|
11
|
+
const pool = new Map<string, GoalRecord>();
|
|
12
|
+
for (const goal of goals) {
|
|
13
|
+
if (goal.status !== "complete") pool.set(goal.id, cloneGoal(goal));
|
|
14
|
+
}
|
|
15
|
+
return pool;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function openGoalsFromPool(pool: Map<string, GoalRecord>): GoalRecord[] {
|
|
19
|
+
return Array.from(pool.values())
|
|
20
|
+
.filter((goal) => goal.status !== "complete")
|
|
21
|
+
.sort((a, b) => {
|
|
22
|
+
const byCreated = a.createdAt.localeCompare(b.createdAt);
|
|
23
|
+
return byCreated !== 0 ? byCreated : a.id.localeCompare(b.id);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function focusedGoalFromPool(pool: Map<string, GoalRecord>, focusedGoalId: string | null): GoalRecord | null {
|
|
28
|
+
if (!focusedGoalId) return null;
|
|
29
|
+
const goal = pool.get(focusedGoalId) ?? null;
|
|
30
|
+
return goal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function otherOpenGoalCount(pool: Map<string, GoalRecord>, focusedGoalId: string | null): number {
|
|
34
|
+
return openGoalsFromPool(pool).filter((goal) => goal.id !== focusedGoalId).length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveSessionFocus(args: {
|
|
38
|
+
pool: Map<string, GoalRecord>;
|
|
39
|
+
focusEntry?: GoalFocusEntry | null;
|
|
40
|
+
legacyGoal?: GoalRecord | null;
|
|
41
|
+
}): string | null {
|
|
42
|
+
const focusedGoalId = args.focusEntry?.focusedGoalId ?? null;
|
|
43
|
+
const focused = focusedGoalId ? focusedGoalFromPool(args.pool, focusedGoalId) : null;
|
|
44
|
+
if (focused && focused.status !== "complete") {
|
|
45
|
+
return focusedGoalId;
|
|
46
|
+
}
|
|
47
|
+
if (args.focusEntry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (args.legacyGoal && args.legacyGoal.status !== "complete") {
|
|
51
|
+
if (args.pool.has(args.legacyGoal.id)) return args.legacyGoal.id;
|
|
52
|
+
args.pool.set(args.legacyGoal.id, cloneGoal(args.legacyGoal));
|
|
53
|
+
return args.legacyGoal.id;
|
|
54
|
+
}
|
|
55
|
+
const open = openGoalsFromPool(args.pool);
|
|
56
|
+
return open.length === 1 ? open[0]?.id ?? null : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function goalSelectorLabel(goal: GoalRecord, focusedGoalId: string | null): string {
|
|
60
|
+
const marker = goal.id === focusedGoalId ? "*" : " ";
|
|
61
|
+
const mode = goal.sisyphus ? "sisyphus" : "goal";
|
|
62
|
+
const path = goal.activePath ? ` ${goal.activePath}` : "";
|
|
63
|
+
return `${marker} ${goal.id} | ${statusLabel(goal)} | ${mode} | ${truncateText(displayObjectiveTitle(goal.objective), 72)}${path}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildGoalListText(pool: Map<string, GoalRecord>, focusedGoalId: string | null): string {
|
|
67
|
+
const open = openGoalsFromPool(pool);
|
|
68
|
+
if (open.length === 0) return "No open goals. Use /goals <topic> or /sisyphus <topic> to discuss, or /goals-set <objective> / /sisyphus-set <objective> to start immediately.";
|
|
69
|
+
const lines = [`Open goals: ${open.length}`, ""];
|
|
70
|
+
for (const goal of open) {
|
|
71
|
+
const focused = goal.id === focusedGoalId ? "*" : " ";
|
|
72
|
+
const mode = goal.sisyphus ? "sisyphus" : "goal";
|
|
73
|
+
const usage = goal.usage.tokensUsed > 0 || goal.usage.activeSeconds > 0
|
|
74
|
+
? ` · ${formatDuration(goal.usage.activeSeconds)} · ${formatTokenValue(goal.usage.tokensUsed).split(" ")[0]}`
|
|
75
|
+
: "";
|
|
76
|
+
lines.push(`${focused} ${goal.id} — ${statusLabel(goal)} · ${mode}${usage}`);
|
|
77
|
+
lines.push(` ${displayObjectiveTitle(goal.objective)}`);
|
|
78
|
+
if (goal.activePath) lines.push(` ${goal.activePath}`);
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildUnfocusedOpenGoalsSummary(openGoalCount: number): string {
|
|
84
|
+
return `No goal is focused in this session. ${openGoalCount} open goal${openGoalCount === 1 ? "" : "s"} exist in .pi/goals. Use /goal-focus to choose the session focus before doing goal work.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function mergeFocusedGoalWithDisk(args: { memoryGoal: GoalRecord; diskGoal: GoalRecord }): GoalRecord {
|
|
88
|
+
const tokensUsed = Math.max(args.memoryGoal.usage.tokensUsed, args.diskGoal.usage.tokensUsed);
|
|
89
|
+
const activeSeconds = Math.max(args.memoryGoal.usage.activeSeconds, args.diskGoal.usage.activeSeconds);
|
|
90
|
+
return {
|
|
91
|
+
...args.diskGoal,
|
|
92
|
+
usage: { tokensUsed, activeSeconds },
|
|
93
|
+
};
|
|
94
|
+
}
|