portable-agent-layer 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -20
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +27 -3
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +0 -1
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +14 -2
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +9 -8
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/context.ts +45 -117
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +3 -15
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +22 -18
- package/src/hooks/lib/semi-static.ts +4 -2
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -60
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +13 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/lib.ts +140 -14
- package/src/targets/opencode/plugin.ts +22 -11
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +1 -1
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +13 -11
- package/src/tools/self-model.ts +20 -16
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Project — register and manage user projects via
|
|
3
|
+
* Project — register and manage user projects via ISA.md files.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the user is the escape hatch for fine-grained control.
|
|
8
|
-
*
|
|
9
|
-
* State: `~/.pal/memory/state/progress/{slug}.json`. Auto-touched on Stop hook
|
|
10
|
-
* when cwd resolves into a registered project.
|
|
5
|
+
* Each project is stored at `~/.pal/memory/projects/{slug}/ISA.md`.
|
|
6
|
+
* Frontmatter holds operational state; body holds ISA spec sections.
|
|
11
7
|
*
|
|
12
8
|
* Usage:
|
|
13
9
|
* bun ~/.pal/tools/project.ts list
|
|
14
10
|
* bun ~/.pal/tools/project.ts create [name] [--path PATH] [--objectives "..."]
|
|
15
11
|
* bun ~/.pal/tools/project.ts resume <name>
|
|
16
12
|
* bun ~/.pal/tools/project.ts complete | archive | pause | unpause <name>
|
|
17
|
-
* bun ~/.pal/tools/project.ts add-fact <name> "text"
|
|
18
|
-
* bun ~/.pal/tools/project.ts add-objective <name> "text"
|
|
19
13
|
* bun ~/.pal/tools/project.ts add-next <name> "text"
|
|
20
14
|
* bun ~/.pal/tools/project.ts add-blocker <name> "text"
|
|
21
15
|
* bun ~/.pal/tools/project.ts add-decision <name> "decision" "rationale"
|
|
22
16
|
* bun ~/.pal/tools/project.ts add-handoff <name> "text"
|
|
23
|
-
* bun ~/.pal/tools/project.ts rm-
|
|
17
|
+
* bun ~/.pal/tools/project.ts rm-next | rm-blocker <name> <index>
|
|
18
|
+
* bun ~/.pal/tools/project.ts update-section <name> <section> "content"
|
|
19
|
+
* bun ~/.pal/tools/project.ts criteria <name>
|
|
20
|
+
* bun ~/.pal/tools/project.ts isa-init <name>
|
|
21
|
+
* bun ~/.pal/tools/project.ts migrate
|
|
24
22
|
*/
|
|
25
23
|
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
25
|
import { resolve } from "node:path";
|
|
27
26
|
import { parseArgs } from "node:util";
|
|
27
|
+
import { paths } from "../../hooks/lib/paths";
|
|
28
28
|
import {
|
|
29
29
|
defaultSlug,
|
|
30
30
|
deleteProject,
|
|
@@ -65,8 +65,7 @@ function cmdList(): void {
|
|
|
65
65
|
path: p.path,
|
|
66
66
|
updated: p.updated,
|
|
67
67
|
stale: isStale(p),
|
|
68
|
-
|
|
69
|
-
next_steps: p.next_steps?.length ?? 0,
|
|
68
|
+
next: p.next?.length ?? 0,
|
|
70
69
|
blockers: p.blockers?.length ?? 0,
|
|
71
70
|
}));
|
|
72
71
|
ok({ count: all.length, projects: rows });
|
|
@@ -100,11 +99,13 @@ function cmdCreate(args: string[]): void {
|
|
|
100
99
|
);
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
const
|
|
102
|
+
const goalLines = values.objectives
|
|
104
103
|
? values.objectives
|
|
105
104
|
.split(/[\n;|]/)
|
|
106
105
|
.map((s) => s.trim())
|
|
107
106
|
.filter(Boolean)
|
|
107
|
+
.map((s) => `- ${s}`)
|
|
108
|
+
.join("\n")
|
|
108
109
|
: undefined;
|
|
109
110
|
|
|
110
111
|
const project: ProjectProgress = {
|
|
@@ -113,7 +114,7 @@ function cmdCreate(args: string[]): void {
|
|
|
113
114
|
status: "active",
|
|
114
115
|
created: now(),
|
|
115
116
|
updated: now(),
|
|
116
|
-
|
|
117
|
+
...(goalLines ? { goal: goalLines } : {}),
|
|
117
118
|
};
|
|
118
119
|
writeProject(project);
|
|
119
120
|
ok({ created: true, project });
|
|
@@ -137,14 +138,10 @@ function setStatus(name: string, status: ProjectStatus): void {
|
|
|
137
138
|
ok({ updated: true, name, status });
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
// ── append
|
|
141
|
+
// ── append/remove for array fields ───────────────────────────────
|
|
141
142
|
|
|
142
|
-
function appendItem(
|
|
143
|
-
|
|
144
|
-
field: "facts" | "objectives" | "next_steps" | "blockers",
|
|
145
|
-
text: string
|
|
146
|
-
): void {
|
|
147
|
-
if (!text?.trim()) fail(`Empty ${field.replace("_", " ")} text.`);
|
|
143
|
+
function appendItem(name: string, field: "next" | "blockers", text: string): void {
|
|
144
|
+
if (!text?.trim()) fail(`Empty ${field} text.`);
|
|
148
145
|
const p = requireProject(name);
|
|
149
146
|
const list = p[field] ?? [];
|
|
150
147
|
list.push(text.trim());
|
|
@@ -154,11 +151,7 @@ function appendItem(
|
|
|
154
151
|
ok({ updated: true, name, field, count: list.length });
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
function removeItem(
|
|
158
|
-
name: string,
|
|
159
|
-
field: "facts" | "objectives" | "next_steps" | "blockers",
|
|
160
|
-
indexArg: string
|
|
161
|
-
): void {
|
|
154
|
+
function removeItem(name: string, field: "next" | "blockers", indexArg: string): void {
|
|
162
155
|
const idx = parseInt(indexArg, 10);
|
|
163
156
|
if (!Number.isInteger(idx) || idx < 0) fail(`Invalid index "${indexArg}".`);
|
|
164
157
|
const p = requireProject(name);
|
|
@@ -171,18 +164,22 @@ function removeItem(
|
|
|
171
164
|
ok({ updated: true, name, field, removed, count: list.length });
|
|
172
165
|
}
|
|
173
166
|
|
|
167
|
+
// ── decisions (body section append) ──────────────────────────────
|
|
168
|
+
|
|
174
169
|
function addDecision(name: string, decision: string, rationale: string): void {
|
|
175
170
|
if (!decision?.trim() || !rationale?.trim())
|
|
176
171
|
fail("Usage: add-decision <name> <decision> <rationale>");
|
|
177
172
|
const p = requireProject(name);
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
p.decisions =
|
|
173
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
174
|
+
const line = `- ${date}: ${decision.trim()} (${rationale.trim()})`;
|
|
175
|
+
p.decisions = p.decisions ? `${p.decisions}\n${line}` : line;
|
|
181
176
|
p.updated = now();
|
|
182
177
|
writeProject(p);
|
|
183
|
-
ok({ updated: true, name
|
|
178
|
+
ok({ updated: true, name });
|
|
184
179
|
}
|
|
185
180
|
|
|
181
|
+
// ── handoff ───────────────────────────────────────────────────────
|
|
182
|
+
|
|
186
183
|
function addHandoff(name: string, text: string): void {
|
|
187
184
|
if (!text?.trim()) fail("Empty handoff text.");
|
|
188
185
|
const p = requireProject(name);
|
|
@@ -192,43 +189,348 @@ function addHandoff(name: string, text: string): void {
|
|
|
192
189
|
ok({ updated: true, name });
|
|
193
190
|
}
|
|
194
191
|
|
|
192
|
+
// ── set-path ──────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function cmdSetPath(args: string[]): void {
|
|
195
|
+
const [name, ...rest] = args;
|
|
196
|
+
if (!name || rest.length === 0) fail("Usage: set-path <name> <new-path>");
|
|
197
|
+
const newPath = resolve(rest.join(" ").trim());
|
|
198
|
+
const p = requireProject(name);
|
|
199
|
+
p.path = newPath;
|
|
200
|
+
p.updated = now();
|
|
201
|
+
writeProject(p);
|
|
202
|
+
ok({ updated: true, name, path: newPath });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── update-section ────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
const VALID_SECTIONS = [
|
|
208
|
+
"problem",
|
|
209
|
+
"goal",
|
|
210
|
+
"criteria",
|
|
211
|
+
"vision",
|
|
212
|
+
"constraints",
|
|
213
|
+
"out_of_scope",
|
|
214
|
+
"context",
|
|
215
|
+
"decisions",
|
|
216
|
+
"changelog",
|
|
217
|
+
] as const;
|
|
218
|
+
type Section = (typeof VALID_SECTIONS)[number];
|
|
219
|
+
|
|
220
|
+
function cmdUpdateSection(args: string[]): void {
|
|
221
|
+
const [name, section, ...rest] = args;
|
|
222
|
+
if (!name || !section) fail("Usage: update-section <name> <section> <content>");
|
|
223
|
+
const key = section.toLowerCase().replace(/\s+/g, "_") as Section;
|
|
224
|
+
if (!(VALID_SECTIONS as readonly string[]).includes(key)) {
|
|
225
|
+
fail(`Unknown section "${section}". Valid: ${VALID_SECTIONS.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
const content = rest.join(" ").trim();
|
|
228
|
+
if (!content) fail("Empty content.");
|
|
229
|
+
const p = requireProject(name);
|
|
230
|
+
(p as unknown as Record<string, unknown>)[key] = content;
|
|
231
|
+
p.updated = now();
|
|
232
|
+
writeProject(p);
|
|
233
|
+
ok({ updated: true, name, section: key });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── criteria ──────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function cmdCriteria(args: string[]): void {
|
|
239
|
+
const name = args[0];
|
|
240
|
+
if (!name) fail("Usage: criteria <name>");
|
|
241
|
+
const p = requireProject(name);
|
|
242
|
+
ok({ name, criteria: p.criteria ?? "" });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── isa-init ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function cmdIsaInit(args: string[]): void {
|
|
248
|
+
const name = args[0];
|
|
249
|
+
if (!name) fail("Usage: isa-init <name>");
|
|
250
|
+
const p = requireProject(name);
|
|
251
|
+
const sections: Array<keyof ProjectProgress> = [
|
|
252
|
+
"problem",
|
|
253
|
+
"goal",
|
|
254
|
+
"criteria",
|
|
255
|
+
"vision",
|
|
256
|
+
"constraints",
|
|
257
|
+
"out_of_scope",
|
|
258
|
+
"context",
|
|
259
|
+
];
|
|
260
|
+
let scaffolded = 0;
|
|
261
|
+
const pr = p as unknown as Record<string, unknown>;
|
|
262
|
+
for (const s of sections) {
|
|
263
|
+
if (!pr[s as string]) {
|
|
264
|
+
pr[s as string] = "";
|
|
265
|
+
scaffolded++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Remove empty strings so they don't clutter the ISA body
|
|
269
|
+
for (const s of sections) {
|
|
270
|
+
if (pr[s as string] === "") pr[s as string] = undefined;
|
|
271
|
+
}
|
|
272
|
+
p.updated = now();
|
|
273
|
+
writeProject(p);
|
|
274
|
+
ok({ initialized: true, name, scaffolded });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── migrate (from old JSON format) ───────────────────────────────
|
|
278
|
+
|
|
279
|
+
interface LegacyDecision {
|
|
280
|
+
ts: string;
|
|
281
|
+
decision: string;
|
|
282
|
+
rationale: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface LegacyProject {
|
|
286
|
+
name: string;
|
|
287
|
+
path: string;
|
|
288
|
+
status: ProjectStatus;
|
|
289
|
+
created: string;
|
|
290
|
+
updated: string;
|
|
291
|
+
facts?: string[];
|
|
292
|
+
objectives?: string[];
|
|
293
|
+
next_steps?: string[];
|
|
294
|
+
blockers?: string[];
|
|
295
|
+
handoff?: string;
|
|
296
|
+
decisions?: LegacyDecision[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function cmdMigrate(): void {
|
|
300
|
+
const progressDir = paths.progress();
|
|
301
|
+
if (!existsSync(progressDir)) {
|
|
302
|
+
ok({ migrated: 0, skipped: 0, results: [] });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const files = readdirSync(progressDir).filter((f) => f.endsWith(".json"));
|
|
307
|
+
if (files.length === 0) {
|
|
308
|
+
ok({ migrated: 0, skipped: 0, results: [] });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let migrated = 0;
|
|
313
|
+
let skipped = 0;
|
|
314
|
+
const results: string[] = [];
|
|
315
|
+
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
const slug = file.slice(0, -5);
|
|
318
|
+
const filePath = resolve(progressDir, file);
|
|
319
|
+
|
|
320
|
+
if (readProject(slug)) {
|
|
321
|
+
skipped++;
|
|
322
|
+
results.push(`${slug}: skipped (ISA.md already exists)`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8")) as LegacyProject;
|
|
328
|
+
if (!raw?.name || !raw?.path || !raw?.status) {
|
|
329
|
+
skipped++;
|
|
330
|
+
results.push(`${slug}: skipped (malformed JSON)`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const p: ProjectProgress = {
|
|
335
|
+
name: raw.name,
|
|
336
|
+
path: raw.path,
|
|
337
|
+
status: raw.status,
|
|
338
|
+
created: raw.created,
|
|
339
|
+
updated: raw.updated,
|
|
340
|
+
...(raw.handoff ? { handoff: raw.handoff } : {}),
|
|
341
|
+
...(raw.next_steps?.length ? { next: raw.next_steps } : {}),
|
|
342
|
+
...(raw.blockers?.length ? { blockers: raw.blockers } : {}),
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (raw.facts?.length) p.context = raw.facts.join("\n");
|
|
346
|
+
if (raw.objectives?.length) p.goal = raw.objectives.map((o) => `- ${o}`).join("\n");
|
|
347
|
+
if (raw.decisions?.length) {
|
|
348
|
+
p.decisions = raw.decisions
|
|
349
|
+
.map((d) => `- ${d.ts.slice(0, 10)}: ${d.decision} (${d.rationale})`)
|
|
350
|
+
.join("\n");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
writeProject(p);
|
|
354
|
+
migrated++;
|
|
355
|
+
results.push(`${slug}: migrated`);
|
|
356
|
+
} catch {
|
|
357
|
+
skipped++;
|
|
358
|
+
results.push(`${slug}: skipped (read/write error)`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
ok({ migrated, skipped, results });
|
|
363
|
+
}
|
|
364
|
+
|
|
195
365
|
// ── rm (project) ──────────────────────────────────────────────────
|
|
196
366
|
|
|
197
367
|
function cmdRm(args: string[]): void {
|
|
198
368
|
const name = args[0];
|
|
199
|
-
if (!name) fail("Usage: rm <name> (deletes the entire project
|
|
369
|
+
if (!name) fail("Usage: rm <name> (deletes the entire project directory)");
|
|
200
370
|
const removed = deleteProject(name);
|
|
201
371
|
if (!removed) fail(`No project named "${name}".`);
|
|
202
372
|
ok({ deleted: true, name });
|
|
203
373
|
}
|
|
204
374
|
|
|
375
|
+
// ── ISC helpers ──────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
interface Isc {
|
|
378
|
+
id: number;
|
|
379
|
+
text: string;
|
|
380
|
+
checked: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseIscs(criteria: string): Isc[] {
|
|
384
|
+
const out: Isc[] = [];
|
|
385
|
+
for (const line of criteria.split("\n")) {
|
|
386
|
+
const m = new RegExp(/^-\s+\[( |x)\]\s+ISC-(\d+):\s+(.+)$/i).exec(line);
|
|
387
|
+
if (m) out.push({ id: Number(m[2]), text: m[3].trim(), checked: m[1] === "x" });
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function nextIscId(criteria: string): number {
|
|
393
|
+
const ids = parseIscs(criteria).map((i) => i.id);
|
|
394
|
+
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function patchIsc(criteria: string, id: number, checked: boolean): string {
|
|
398
|
+
const marker = checked ? "[x]" : "[ ]";
|
|
399
|
+
return criteria.replace(
|
|
400
|
+
new RegExp(String.raw`^(-\s+)\[[ x]\](\s+ISC-${id}:)`, "m"),
|
|
401
|
+
`$1${marker}$2`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function cmdAddIsc(args: string[]): void {
|
|
406
|
+
const name = args[0] ?? fail("Usage: add-isc <name> <title>");
|
|
407
|
+
const title = args.slice(1).join(" ").trim();
|
|
408
|
+
if (!title) fail("Usage: add-isc <name> <title>");
|
|
409
|
+
const p = requireProject(name);
|
|
410
|
+
const current = p.criteria ?? "";
|
|
411
|
+
const id = nextIscId(current);
|
|
412
|
+
const newLine = `- [ ] ISC-${id}: ${title}`;
|
|
413
|
+
p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
|
|
414
|
+
p.updated = now();
|
|
415
|
+
writeProject(p);
|
|
416
|
+
ok({ added: true, id, title });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function cmdCheckIsc(args: string[]): void {
|
|
420
|
+
const name = args[0] ?? fail("Usage: check-isc <name> <id>");
|
|
421
|
+
const id = Number(args[1] ?? fail("Usage: check-isc <name> <id>"));
|
|
422
|
+
if (!Number.isInteger(id) || id < 1) fail("ISC id must be a positive integer");
|
|
423
|
+
const p = requireProject(name);
|
|
424
|
+
const current = p.criteria ?? "";
|
|
425
|
+
const existing = parseIscs(current).find((i) => i.id === id);
|
|
426
|
+
if (!existing) fail(`ISC-${id} not found in project "${name}"`);
|
|
427
|
+
if (existing.checked) {
|
|
428
|
+
ok({ checked: true, id, alreadyDone: true });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
p.criteria = patchIsc(current, id, true);
|
|
432
|
+
p.updated = now();
|
|
433
|
+
writeProject(p);
|
|
434
|
+
ok({ checked: true, id });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function cmdListIsc(args: string[]): void {
|
|
438
|
+
const name = args[0] ?? fail("Usage: list-isc <name>");
|
|
439
|
+
const p = requireProject(name);
|
|
440
|
+
const iscs = parseIscs(p.criteria ?? "");
|
|
441
|
+
const open = iscs.filter((i) => !i.checked);
|
|
442
|
+
const done = iscs.filter((i) => i.checked);
|
|
443
|
+
ok({ name, total: iscs.length, open: open.length, done: done.length, iscs });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Task ISA (work/) ──────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
function taskSlug(title: string): string {
|
|
449
|
+
const sanitized = title
|
|
450
|
+
.toLowerCase()
|
|
451
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
452
|
+
.replace(/^-+|-+$/g, "")
|
|
453
|
+
.slice(0, 40);
|
|
454
|
+
return `${sanitized}-${Date.now().toString(36)}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function taskIsaPath(slug: string): string {
|
|
458
|
+
const dir = resolve(paths.work(), slug);
|
|
459
|
+
mkdirSync(dir, { recursive: true });
|
|
460
|
+
return resolve(dir, "ISA.md");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function cmdScaffoldTaskIsa(args: string[]): void {
|
|
464
|
+
const title = args.join(" ").trim();
|
|
465
|
+
if (!title) fail("Usage: scaffold-task-isa <title>");
|
|
466
|
+
const slug = taskSlug(title);
|
|
467
|
+
const ts = new Date().toISOString();
|
|
468
|
+
const content = [
|
|
469
|
+
"---",
|
|
470
|
+
`task: "${title}"`,
|
|
471
|
+
`slug: "${slug}"`,
|
|
472
|
+
"phase: active",
|
|
473
|
+
`started: "${ts}"`,
|
|
474
|
+
`updated: "${ts}"`,
|
|
475
|
+
"---",
|
|
476
|
+
"",
|
|
477
|
+
"## Goal",
|
|
478
|
+
"",
|
|
479
|
+
"",
|
|
480
|
+
"## Criteria",
|
|
481
|
+
"",
|
|
482
|
+
"",
|
|
483
|
+
].join("\n");
|
|
484
|
+
const filePath = taskIsaPath(slug);
|
|
485
|
+
writeFileSync(filePath, content, "utf-8");
|
|
486
|
+
ok({ created: true, slug, path: filePath });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function cmdCompleteTaskIsa(args: string[]): void {
|
|
490
|
+
const slug = args[0] ?? fail("Usage: complete-task-isa <slug>");
|
|
491
|
+
const filePath = resolve(paths.work(), slug, "ISA.md");
|
|
492
|
+
if (!existsSync(filePath)) fail(`Task ISA not found: ${slug}`);
|
|
493
|
+
const content = readFileSync(filePath, "utf-8");
|
|
494
|
+
const updated = content
|
|
495
|
+
.replace(/^phase: .+$/m, "phase: complete")
|
|
496
|
+
.replace(/^updated: .+$/m, `updated: "${new Date().toISOString()}"`);
|
|
497
|
+
writeFileSync(filePath, updated, "utf-8");
|
|
498
|
+
ok({ completed: true, slug });
|
|
499
|
+
}
|
|
500
|
+
|
|
205
501
|
// ── dispatch ──────────────────────────────────────────────────────
|
|
206
502
|
|
|
207
503
|
function help(): void {
|
|
208
|
-
console.log(`Project — manage PAL project state.
|
|
504
|
+
console.log(`Project — manage PAL project state (ISA.md backed).
|
|
209
505
|
|
|
210
506
|
Commands:
|
|
211
507
|
list show all registered projects
|
|
212
|
-
create [name] [--path PATH] [--objectives X] register a project
|
|
213
|
-
resume <name> print full project
|
|
508
|
+
create [name] [--path PATH] [--objectives X] register a project
|
|
509
|
+
resume <name> print full project ISA
|
|
214
510
|
complete <name> mark complete
|
|
215
511
|
archive <name> mark archived
|
|
216
512
|
pause <name> | unpause <name> toggle paused/active
|
|
217
|
-
|
|
218
|
-
add-objective <name> "text" append objective
|
|
513
|
+
set-path <name> <new-path> update the registered path
|
|
219
514
|
add-next <name> "text" append next step
|
|
220
515
|
add-blocker <name> "text" append blocker
|
|
221
|
-
add-decision <name> "decision" "rationale" log a decision
|
|
516
|
+
add-decision <name> "decision" "rationale" log a dated decision entry
|
|
222
517
|
add-handoff <name> "text" overwrite handoff field
|
|
223
|
-
rm-fact <name> <index> remove fact by index
|
|
224
|
-
rm-objective <name> <index> remove objective by index
|
|
225
518
|
rm-next <name> <index> remove next step by index
|
|
226
519
|
rm-blocker <name> <index> remove blocker by index
|
|
227
|
-
|
|
520
|
+
update-section <name> <section> "content" set an ISA body section
|
|
521
|
+
criteria <name> print the Criteria section
|
|
522
|
+
add-isc <name> "title" append a new open ISC to Criteria
|
|
523
|
+
check-isc <name> <id> mark ISC-N as done
|
|
524
|
+
list-isc <name> list all ISCs with open/done status
|
|
525
|
+
isa-init <name> mark project as ISA-initialized
|
|
526
|
+
scaffold-task-isa <title> create a one-shot task ISA in memory/work/
|
|
527
|
+
complete-task-isa <slug> mark a task ISA as complete
|
|
528
|
+
migrate migrate old JSON progress files → ISA.md
|
|
529
|
+
rm <name> delete the entire project
|
|
228
530
|
`);
|
|
229
531
|
}
|
|
230
532
|
|
|
231
|
-
|
|
533
|
+
function run(): void {
|
|
232
534
|
const [cmd, ...rest] = Bun.argv.slice(2);
|
|
233
535
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
234
536
|
help();
|
|
@@ -256,24 +558,10 @@ export function run(): void {
|
|
|
256
558
|
case "unpause":
|
|
257
559
|
setStatus(rest[0] ?? fail("Usage: unpause <name>"), "active");
|
|
258
560
|
return;
|
|
259
|
-
case "add-fact":
|
|
260
|
-
appendItem(
|
|
261
|
-
rest[0] ?? fail("Usage: add-fact <name> <text>"),
|
|
262
|
-
"facts",
|
|
263
|
-
rest.slice(1).join(" ")
|
|
264
|
-
);
|
|
265
|
-
return;
|
|
266
|
-
case "add-objective":
|
|
267
|
-
appendItem(
|
|
268
|
-
rest[0] ?? fail("Usage: add-objective <name> <text>"),
|
|
269
|
-
"objectives",
|
|
270
|
-
rest.slice(1).join(" ")
|
|
271
|
-
);
|
|
272
|
-
return;
|
|
273
561
|
case "add-next":
|
|
274
562
|
appendItem(
|
|
275
563
|
rest[0] ?? fail("Usage: add-next <name> <text>"),
|
|
276
|
-
"
|
|
564
|
+
"next",
|
|
277
565
|
rest.slice(1).join(" ")
|
|
278
566
|
);
|
|
279
567
|
return;
|
|
@@ -297,26 +585,8 @@ export function run(): void {
|
|
|
297
585
|
rest.slice(1).join(" ")
|
|
298
586
|
);
|
|
299
587
|
return;
|
|
300
|
-
case "rm-fact":
|
|
301
|
-
removeItem(
|
|
302
|
-
rest[0] ?? fail("Usage: rm-fact <name> <index>"),
|
|
303
|
-
"facts",
|
|
304
|
-
rest[1] ?? ""
|
|
305
|
-
);
|
|
306
|
-
return;
|
|
307
|
-
case "rm-objective":
|
|
308
|
-
removeItem(
|
|
309
|
-
rest[0] ?? fail("Usage: rm-objective <name> <index>"),
|
|
310
|
-
"objectives",
|
|
311
|
-
rest[1] ?? ""
|
|
312
|
-
);
|
|
313
|
-
return;
|
|
314
588
|
case "rm-next":
|
|
315
|
-
removeItem(
|
|
316
|
-
rest[0] ?? fail("Usage: rm-next <name> <index>"),
|
|
317
|
-
"next_steps",
|
|
318
|
-
rest[1] ?? ""
|
|
319
|
-
);
|
|
589
|
+
removeItem(rest[0] ?? fail("Usage: rm-next <name> <index>"), "next", rest[1] ?? "");
|
|
320
590
|
return;
|
|
321
591
|
case "rm-blocker":
|
|
322
592
|
removeItem(
|
|
@@ -325,6 +595,36 @@ export function run(): void {
|
|
|
325
595
|
rest[1] ?? ""
|
|
326
596
|
);
|
|
327
597
|
return;
|
|
598
|
+
case "update-section":
|
|
599
|
+
cmdUpdateSection(rest);
|
|
600
|
+
return;
|
|
601
|
+
case "criteria":
|
|
602
|
+
cmdCriteria(rest);
|
|
603
|
+
return;
|
|
604
|
+
case "add-isc":
|
|
605
|
+
cmdAddIsc(rest);
|
|
606
|
+
return;
|
|
607
|
+
case "check-isc":
|
|
608
|
+
cmdCheckIsc(rest);
|
|
609
|
+
return;
|
|
610
|
+
case "list-isc":
|
|
611
|
+
cmdListIsc(rest);
|
|
612
|
+
return;
|
|
613
|
+
case "isa-init":
|
|
614
|
+
cmdIsaInit(rest);
|
|
615
|
+
return;
|
|
616
|
+
case "scaffold-task-isa":
|
|
617
|
+
cmdScaffoldTaskIsa(rest);
|
|
618
|
+
return;
|
|
619
|
+
case "complete-task-isa":
|
|
620
|
+
cmdCompleteTaskIsa(rest);
|
|
621
|
+
return;
|
|
622
|
+
case "migrate":
|
|
623
|
+
cmdMigrate();
|
|
624
|
+
return;
|
|
625
|
+
case "set-path":
|
|
626
|
+
cmdSetPath(rest);
|
|
627
|
+
return;
|
|
328
628
|
case "rm":
|
|
329
629
|
cmdRm(rest);
|
|
330
630
|
return;
|
|
@@ -17,6 +17,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
17
17
|
import { resolve } from "node:path";
|
|
18
18
|
import { parseArgs } from "node:util";
|
|
19
19
|
import { ensureDir, paths } from "../../hooks/lib/paths";
|
|
20
|
+
import { readJsonl } from "../self-model";
|
|
20
21
|
|
|
21
22
|
// ── Config ──
|
|
22
23
|
|
|
@@ -27,7 +28,6 @@ const SYNTHESIS_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
27
28
|
interface SynthesisState {
|
|
28
29
|
timestamp: string;
|
|
29
30
|
days: number;
|
|
30
|
-
threads: { id: string; cwd?: string; title: string; context: string; opened: string }[];
|
|
31
31
|
sessions: { date: string; titles: string[] }[];
|
|
32
32
|
sessionCount: number;
|
|
33
33
|
ratings: {
|
|
@@ -74,14 +74,6 @@ function shouldRun(force: boolean): boolean {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function readJsonl<T>(path: string): T[] {
|
|
78
|
-
if (!existsSync(path)) return [];
|
|
79
|
-
return readFileSync(path, "utf-8")
|
|
80
|
-
.split("\n")
|
|
81
|
-
.filter((l) => l.trim())
|
|
82
|
-
.map((l) => JSON.parse(l) as T);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
77
|
function daysAgo(days: number): Date {
|
|
86
78
|
return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
87
79
|
}
|
|
@@ -100,28 +92,6 @@ function safeReaddir(dir: string): string[] {
|
|
|
100
92
|
|
|
101
93
|
// ── Data readers ──
|
|
102
94
|
|
|
103
|
-
interface Thread {
|
|
104
|
-
id: string;
|
|
105
|
-
cwd?: string;
|
|
106
|
-
title: string;
|
|
107
|
-
context: string;
|
|
108
|
-
status: string;
|
|
109
|
-
created: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getOpenThreads(): SynthesisState["threads"] {
|
|
113
|
-
const threads = readJsonl<Thread>(resolve(stateDir(), "threads.jsonl"));
|
|
114
|
-
return threads
|
|
115
|
-
.filter((t) => t.status === "open")
|
|
116
|
-
.map((t) => ({
|
|
117
|
-
id: t.id,
|
|
118
|
-
cwd: t.cwd,
|
|
119
|
-
title: t.title,
|
|
120
|
-
context: t.context,
|
|
121
|
-
opened: formatDate(t.created),
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
95
|
interface Reflection {
|
|
126
96
|
timestamp: string;
|
|
127
97
|
cwd?: string;
|
|
@@ -210,12 +180,9 @@ function getRatingStats(since: Date): SynthesisState["ratings"] {
|
|
|
210
180
|
const secondHalf = ratings.slice(mid);
|
|
211
181
|
const firstAvg = firstHalf.reduce((s, r) => s + r.rating, 0) / firstHalf.length;
|
|
212
182
|
const secondAvg = secondHalf.reduce((s, r) => s + r.rating, 0) / secondHalf.length;
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
: secondAvg - firstAvg < -0.5
|
|
217
|
-
? "declining"
|
|
218
|
-
: "stable";
|
|
183
|
+
const diff = secondAvg - firstAvg;
|
|
184
|
+
const decliningOrStable = diff < -0.5 ? "declining" : "stable";
|
|
185
|
+
const trend = diff > 0.5 ? "improving" : decliningOrStable;
|
|
219
186
|
|
|
220
187
|
return {
|
|
221
188
|
count: ratings.length,
|
|
@@ -251,8 +218,8 @@ function getRecentSessions(since: Date): {
|
|
|
251
218
|
|
|
252
219
|
const content = readFileSync(resolve(monthDir, file), "utf-8");
|
|
253
220
|
const titleMatch =
|
|
254
|
-
|
|
255
|
-
|
|
221
|
+
new RegExp(/^title:\s*"?(.+?)"?\s*$/m).exec(content) ??
|
|
222
|
+
new RegExp(/^\*\*Title:\*\*\s*(.+?)\s*$/m).exec(content);
|
|
256
223
|
const title = titleMatch?.[1] ?? file.replace(/\.md$/, "");
|
|
257
224
|
|
|
258
225
|
const existing = byDate.get(isoDate) ?? [];
|
|
@@ -280,7 +247,6 @@ export function writeSynthesis(state: SynthesisState): string {
|
|
|
280
247
|
|
|
281
248
|
export function synthesize(days: number): SynthesisState {
|
|
282
249
|
const since = daysAgo(days);
|
|
283
|
-
const threads = getOpenThreads();
|
|
284
250
|
const { sessions, count: sessionCount } = getRecentSessions(since);
|
|
285
251
|
const ratings = getRatingStats(since);
|
|
286
252
|
const algorithm = getAlgorithmStats(since);
|
|
@@ -288,7 +254,6 @@ export function synthesize(days: number): SynthesisState {
|
|
|
288
254
|
return {
|
|
289
255
|
timestamp: new Date().toISOString(),
|
|
290
256
|
days,
|
|
291
|
-
threads,
|
|
292
257
|
sessions,
|
|
293
258
|
sessionCount,
|
|
294
259
|
ratings,
|
|
@@ -346,7 +311,6 @@ Output: ~/.pal/memory/state/synthesis.json
|
|
|
346
311
|
{
|
|
347
312
|
success: true,
|
|
348
313
|
path: sp,
|
|
349
|
-
threads: state.threads.length,
|
|
350
314
|
sessions: state.sessionCount,
|
|
351
315
|
ratings: state.ratings.count,
|
|
352
316
|
reflections: state.algorithm.reflectionCount,
|