@quantmind-br/pi-goal-x 0.0.0-stage → 0.35.1
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 +131 -2
- package/assets/badge-dark.svg +18 -0
- package/assets/badge-light.svg +18 -0
- package/docs/advanced-usage.md +106 -0
- package/docs/agent-flow-design.md +306 -0
- package/docs/agentic-runtime-prd.md +773 -0
- package/docs/architecture.md +368 -0
- package/docs/controls.md +140 -0
- package/docs/pi-autoresearch-survey.md +50 -0
- package/docs/pi-goal-x-unified-dashboard-plan.md +1667 -0
- package/docs/prompt-caching.md +31 -0
- package/docs/unified-dashboard.md +286 -0
- package/extensions/auditor-selector.ts +74 -0
- package/extensions/goal-accounting.ts +107 -0
- package/extensions/goal-activity.ts +202 -0
- package/extensions/goal-auditor.ts +480 -0
- package/extensions/goal-commands.ts +866 -0
- package/extensions/goal-compaction.ts +197 -0
- package/extensions/goal-completion.ts +173 -0
- package/extensions/goal-context.ts +12 -0
- package/extensions/goal-contract.ts +58 -0
- package/extensions/goal-controls.ts +202 -0
- package/extensions/goal-core-tools.ts +549 -0
- package/extensions/goal-core.ts +78 -0
- package/extensions/goal-detail.ts +65 -0
- package/extensions/goal-dialog-controls.ts +31 -0
- package/extensions/goal-draft.ts +244 -0
- package/extensions/goal-drafting.ts +535 -0
- package/extensions/goal-events.ts +692 -0
- package/extensions/goal-format.ts +376 -0
- package/extensions/goal-interruption.ts +53 -0
- package/extensions/goal-ledger-index.ts +68 -0
- package/extensions/goal-ledger.ts +1007 -0
- package/extensions/goal-live-retention.ts +239 -0
- package/extensions/goal-oracle.ts +346 -0
- package/extensions/goal-policy.ts +370 -0
- package/extensions/goal-pool.ts +103 -0
- package/extensions/goal-prompt-cache.ts +80 -0
- package/extensions/goal-questionnaire.ts +1073 -0
- package/extensions/goal-record.ts +388 -0
- package/extensions/goal-recovery.ts +256 -0
- package/extensions/goal-runtime.ts +277 -0
- package/extensions/goal-scheduler-state.ts +100 -0
- package/extensions/goal-scheduler.ts +436 -0
- package/extensions/goal-service.ts +843 -0
- package/extensions/goal-session-health.ts +130 -0
- package/extensions/goal-session-safety.ts +65 -0
- package/extensions/goal-settings.ts +1370 -0
- package/extensions/goal-state.ts +1069 -0
- package/extensions/goal-status.ts +305 -0
- package/extensions/goal-task-confirmation.ts +174 -0
- package/extensions/goal-task-count.ts +59 -0
- package/extensions/goal-task-derive.ts +46 -0
- package/extensions/goal-task-index.ts +57 -0
- package/extensions/goal-task-tools.ts +595 -0
- package/extensions/goal-tool-inputs.ts +102 -0
- package/extensions/goal-tool-names.ts +73 -0
- package/extensions/goal-tools.ts +16 -0
- package/extensions/goal-widget.ts +455 -0
- package/extensions/goal.ts +42 -0
- package/extensions/network-error-backoff.ts +55 -0
- package/extensions/prompts/goal-prompts.ts +330 -0
- package/extensions/storage/atomic-rename.ts +52 -0
- package/extensions/storage/goal-files.ts +695 -0
- package/extensions/storage/goal-lock.ts +131 -0
- package/extensions/storage/goal-root.ts +95 -0
- package/extensions/widgets/auditor-dashboard-model.ts +174 -0
- package/extensions/widgets/dialog-scaffold.ts +26 -0
- package/extensions/widgets/goal-controls-dialog.ts +329 -0
- package/extensions/widgets/goal-dashboard-model.ts +530 -0
- package/extensions/widgets/goal-dashboard-renderer.ts +674 -0
- package/extensions/widgets/goal-escape-dialog.ts +148 -0
- package/extensions/widgets/goal-notifications.ts +9 -0
- package/extensions/widgets/goal-widget.ts +723 -0
- package/extensions/widgets/task-list-overlay.ts +409 -0
- package/extensions/widgets/text-cache.ts +30 -0
- package/package.json +117 -4
- package/scripts/recover-session-checkpoints.mjs +289 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pi-goal-x-recover — offline repair for session files affected by issue #30.
|
|
4
|
+
*
|
|
5
|
+
* Before the v2 bounded-checkpoint fix, every auto-continue turn persisted a
|
|
6
|
+
* full continuation prompt (~6.4K chars) as a custom_message entry. This tool
|
|
7
|
+
* rewrites ONLY those legacy checkpoint entries to the tiny v2 marker form:
|
|
8
|
+
*
|
|
9
|
+
* - line count unchanged; entry ids/parentIds unchanged;
|
|
10
|
+
* - header and non-goal lines byte-identical (malformed lines preserved);
|
|
11
|
+
* - only checkpoint `content` and `details` change;
|
|
12
|
+
* - idempotent; dry-run default.
|
|
13
|
+
*
|
|
14
|
+
* SAFETY: never run against a live Pi session. Close Pi first. --apply is
|
|
15
|
+
* refused without --confirm-pi-closed. A timestamped backup is created before
|
|
16
|
+
* replacement, which itself is an atomic same-directory rename.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* pi-goal-x-recover --session <session.jsonl> # dry-run
|
|
20
|
+
* pi-goal-x-recover --session <session.jsonl> --apply --confirm-pi-closed
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
|
|
26
|
+
const GOAL_EVENT_ENTRY = "pi-goal-event";
|
|
27
|
+
const V2_MARKER_PATTERN =
|
|
28
|
+
/^<pi_goal_continuation\s+goal_id="[^"]+"\s+kind="checkpoint"\s+v="2"\s*\/>$/;
|
|
29
|
+
|
|
30
|
+
function escapeXmlAttribute(value) {
|
|
31
|
+
return value
|
|
32
|
+
.replaceAll("&", "&")
|
|
33
|
+
.replaceAll('"', """)
|
|
34
|
+
.replaceAll("<", "<")
|
|
35
|
+
.replaceAll(">", ">");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkpointTriggerPrompt(goalId) {
|
|
39
|
+
const content = `<pi_goal_continuation goal_id="${escapeXmlAttribute(goalId)}" kind="checkpoint" v="2"/>`;
|
|
40
|
+
if (content.length > 160) throw new Error(`checkpoint marker too large: ${content.length}`);
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function asRecord(value) {
|
|
45
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Extract goalId from a checkpoint entry's details or content (v2 + legacy forms). */
|
|
49
|
+
function checkpointGoalId(entry) {
|
|
50
|
+
const raw = asRecord(entry);
|
|
51
|
+
if (!raw) return null;
|
|
52
|
+
const details = asRecord(raw.details);
|
|
53
|
+
if (details && typeof details.goalId === "string" && details.goalId) return details.goalId;
|
|
54
|
+
if (typeof raw.content !== "string") return null;
|
|
55
|
+
const xml = raw.content.match(/^<pi_goal_continuation\s+goal_id="([^"]+)"/);
|
|
56
|
+
if (xml?.[1]) return xml[1];
|
|
57
|
+
const bracket = raw.content.match(/^\[(?:GOAL CHECKPOINT|GOAL CONTINUATION|GOAL STALE) goalId=([^\]\s]+)\]/);
|
|
58
|
+
return bracket?.[1] ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isGoalCheckpointCustomMessage(entry) {
|
|
62
|
+
const raw = asRecord(entry);
|
|
63
|
+
return Boolean(
|
|
64
|
+
raw
|
|
65
|
+
&& raw.type === "custom_message"
|
|
66
|
+
&& raw.customType === GOAL_EVENT_ENTRY
|
|
67
|
+
&& typeof raw.content === "string"
|
|
68
|
+
&& !V2_MARKER_PATTERN.test(raw.content),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function graphIdentity(entry) {
|
|
73
|
+
const raw = asRecord(entry);
|
|
74
|
+
return { id: raw ? raw.id : undefined, parentId: raw ? raw.parentId : undefined };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function splitPreservingFinalNewline(text) {
|
|
78
|
+
const lines = text.split("\n");
|
|
79
|
+
// split() drops knowledge of a trailing newline; re-add an empty final
|
|
80
|
+
// segment so join("\n") reproduces the input byte count exactly.
|
|
81
|
+
if (text.endsWith("\n")) return lines; // last element is "" already
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pure line-level transform used by both the CLI flow and benchmarks.
|
|
87
|
+
* Returns the rewritten lines plus counters. Never touches the filesystem.
|
|
88
|
+
*/
|
|
89
|
+
export function transformSessionLines(originalText) {
|
|
90
|
+
const outputLines = [];
|
|
91
|
+
const originalGraph = [];
|
|
92
|
+
const outputGraph = [];
|
|
93
|
+
let changed = 0;
|
|
94
|
+
let savedChars = 0;
|
|
95
|
+
let malformed = 0;
|
|
96
|
+
|
|
97
|
+
for (const rawLine of splitPreservingFinalNewline(originalText)) {
|
|
98
|
+
if (!rawLine.trim()) {
|
|
99
|
+
outputLines.push(rawLine);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
let entry;
|
|
103
|
+
try {
|
|
104
|
+
entry = JSON.parse(rawLine);
|
|
105
|
+
} catch {
|
|
106
|
+
malformed += 1;
|
|
107
|
+
outputLines.push(rawLine); // never discard unrelated malformed data
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
originalGraph.push(graphIdentity(entry));
|
|
112
|
+
|
|
113
|
+
if (!isGoalCheckpointCustomMessage(entry)) {
|
|
114
|
+
outputLines.push(rawLine); // byte-identical for untouched entries
|
|
115
|
+
outputGraph.push(graphIdentity(entry));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const goalId = checkpointGoalId(entry);
|
|
120
|
+
if (!goalId) {
|
|
121
|
+
outputLines.push(rawLine);
|
|
122
|
+
outputGraph.push(graphIdentity(entry));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rewritten = { ...entry };
|
|
127
|
+
const details = { ...asRecord(entry.details) };
|
|
128
|
+
delete details.objective;
|
|
129
|
+
details.version = 2;
|
|
130
|
+
details.kind = "checkpoint";
|
|
131
|
+
details.goalId = goalId;
|
|
132
|
+
if (typeof details.timestamp !== "number") details.timestamp = Date.now();
|
|
133
|
+
rewritten.details = details;
|
|
134
|
+
rewritten.content = checkpointTriggerPrompt(goalId);
|
|
135
|
+
|
|
136
|
+
const rewrittenLine = JSON.stringify(rewritten);
|
|
137
|
+
outputLines.push(rewrittenLine);
|
|
138
|
+
outputGraph.push(graphIdentity(rewritten));
|
|
139
|
+
|
|
140
|
+
changed += 1;
|
|
141
|
+
savedChars += rawLine.length - rewrittenLine.length;
|
|
142
|
+
}
|
|
143
|
+
return { outputLines, originalGraph, outputGraph, changed, savedChars, malformed };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function recoverSession(inputPath, apply) {
|
|
147
|
+
const absolute = path.resolve(inputPath);
|
|
148
|
+
let lstat;
|
|
149
|
+
try {
|
|
150
|
+
lstat = fs.lstatSync(absolute);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new Error(`cannot stat ${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
}
|
|
154
|
+
if (lstat.isSymbolicLink()) {
|
|
155
|
+
throw new Error(`refusing symbolic link: ${absolute}`);
|
|
156
|
+
}
|
|
157
|
+
if (!lstat.isFile()) {
|
|
158
|
+
throw new Error(`not a regular file: ${absolute}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const original = fs.readFileSync(absolute, "utf8");
|
|
162
|
+
const originalMode = lstat.mode;
|
|
163
|
+
|
|
164
|
+
const { outputLines, originalGraph, outputGraph, changed, savedChars, malformed } =
|
|
165
|
+
transformSessionLines(original);
|
|
166
|
+
|
|
167
|
+
// ── invariants before any write ──────────────────────────────────────
|
|
168
|
+
if (JSON.stringify(outputGraph) !== JSON.stringify(originalGraph)) {
|
|
169
|
+
throw new Error("graph identity mismatch: ids/parentIds would change — aborting");
|
|
170
|
+
}
|
|
171
|
+
const originalLines = original.split("\n");
|
|
172
|
+
if (originalLines.length !== outputLines.length) {
|
|
173
|
+
throw new Error("line count would change — aborting");
|
|
174
|
+
}
|
|
175
|
+
for (let i = 0; i < originalLines.length; i += 1) {
|
|
176
|
+
if (originalLines[i] === outputLines[i]) continue;
|
|
177
|
+
const parsed = JSON.parse(originalLines[i] ?? "{}");
|
|
178
|
+
const isGoalEvent = asRecord(parsed)?.type === "custom_message" && asRecord(parsed)?.customType === GOAL_EVENT_ENTRY;
|
|
179
|
+
if (!isGoalEvent) {
|
|
180
|
+
throw new Error(`non-goal line ${i + 1} would change — aborting`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Every non-header parentId must reference some id in the file.
|
|
184
|
+
const ids = new Set(outputGraph.map((g) => g.id).filter(Boolean));
|
|
185
|
+
for (const g of outputGraph) {
|
|
186
|
+
if (g.parentId != null && !ids.has(g.parentId)) {
|
|
187
|
+
throw new Error(`parentId ${g.parentId} has no matching entry — aborting`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`Session checkpoints: ${changed} legacy checkpoint(s) rewriteable`);
|
|
192
|
+
console.log(`Estimated savings: ${savedChars} chars (${malformed} malformed unrelated line(s) preserved)`);
|
|
193
|
+
|
|
194
|
+
if (!apply) {
|
|
195
|
+
console.log("dry-run: no changes written.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
200
|
+
const backup = `${absolute}.backup-${timestamp}`;
|
|
201
|
+
fs.copyFileSync(absolute, backup);
|
|
202
|
+
fs.chmodSync(backup, originalMode & 0o777);
|
|
203
|
+
|
|
204
|
+
const temp = path.join(
|
|
205
|
+
path.dirname(absolute),
|
|
206
|
+
`.${path.basename(absolute)}.${process.pid}.${Date.now()}.tmp`,
|
|
207
|
+
);
|
|
208
|
+
let fd;
|
|
209
|
+
try {
|
|
210
|
+
fd = fs.openSync(temp, "wx", originalMode & 0o777);
|
|
211
|
+
fs.writeFileSync(fd, outputLines.join("\n"), "utf8");
|
|
212
|
+
fs.fsyncSync(fd);
|
|
213
|
+
fs.closeSync(fd);
|
|
214
|
+
fd = undefined;
|
|
215
|
+
fs.renameSync(temp, absolute);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (fd !== undefined) {
|
|
218
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
219
|
+
}
|
|
220
|
+
try { fs.unlinkSync(temp); } catch { /* best effort */ }
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
fs.fsyncSync(fs.openSync(path.dirname(absolute), "r"));
|
|
226
|
+
} catch { /* directory fsync is platform-dependent; best effort */ }
|
|
227
|
+
|
|
228
|
+
// Post-write validation by re-reading.
|
|
229
|
+
const reread = fs.readFileSync(absolute, "utf8");
|
|
230
|
+
const rereadLines = reread.split("\n");
|
|
231
|
+
if (rereadLines.length !== outputLines.length) throw new Error("post-write validation failed: line count changed");
|
|
232
|
+
for (let i = 0; i < rereadLines.length; i += 1) {
|
|
233
|
+
if (rereadLines[i] !== outputLines[i]) throw new Error(`post-write validation failed at line ${i + 1}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`Applied: ${changed} checkpoint(s) repaired.`);
|
|
237
|
+
console.log(`Backup: ${backup}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── CLI parsing ────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function main(argv) {
|
|
243
|
+
let session = null;
|
|
244
|
+
let apply = false;
|
|
245
|
+
let confirmPiClosed = false;
|
|
246
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
247
|
+
const arg = argv[i];
|
|
248
|
+
if (arg === "--session") {
|
|
249
|
+
session = argv[++i] ?? null;
|
|
250
|
+
} else if (arg?.startsWith("--session=")) {
|
|
251
|
+
session = arg.slice("--session=".length);
|
|
252
|
+
} else if (arg === "--apply") {
|
|
253
|
+
apply = true;
|
|
254
|
+
} else if (arg === "--confirm-pi-closed") {
|
|
255
|
+
confirmPiClosed = true;
|
|
256
|
+
} else if (arg === "--dry-run") {
|
|
257
|
+
apply = false;
|
|
258
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
259
|
+
console.log("Usage: pi-goal-x-recover --session <session.jsonl> [--dry-run]");
|
|
260
|
+
console.log(" pi-goal-x-recover --session <session.jsonl> --apply --confirm-pi-closed");
|
|
261
|
+
return 0;
|
|
262
|
+
} else {
|
|
263
|
+
console.error(`error: unknown argument: ${arg}`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!session) {
|
|
269
|
+
console.error("error: --session <path> is required");
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
if (apply && !confirmPiClosed) {
|
|
273
|
+
console.error("error: --apply requires --confirm-pi-closed.");
|
|
274
|
+
console.error("Close Pi first: rewriting the session behind Pi's in-memory state is unsafe.");
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
recoverSession(session, apply);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? "")) {
|
|
288
|
+
process.exit(main(process.argv.slice(2)));
|
|
289
|
+
}
|