portable-agent-layer 0.70.0 → 0.72.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 +5 -1
- package/assets/schema/pal-settings.schema.json +4 -0
- package/assets/skills/onboarding/SKILL.md +109 -0
- package/assets/skills/projects/SKILL.md +11 -2
- package/assets/templates/pal-settings.json +1 -0
- package/package.json +5 -1
- package/src/cli/index.ts +39 -12
- package/src/cli/migrate.ts +1 -1
- package/src/cli/personal-context.ts +67 -0
- package/src/cli/server.ts +13 -7
- package/src/cli/setup-identity.ts +13 -1
- package/src/cli/skill.ts +1 -1
- package/src/hooks/CompactRecover.ts +28 -86
- package/src/hooks/LedgerUnapplied.ts +3 -28
- package/src/hooks/LoadContext.ts +33 -60
- package/src/hooks/SecurityValidator.ts +16 -109
- package/src/hooks/handlers/agenda.ts +223 -0
- package/src/hooks/handlers/failure-principle.ts +19 -44
- package/src/hooks/handlers/inject-retrieval.ts +6 -2
- package/src/hooks/handlers/session-intelligence.ts +13 -70
- package/src/hooks/lib/agenda-store.ts +41 -0
- package/src/hooks/lib/capture-store.ts +103 -0
- package/src/hooks/lib/compact-recall.ts +89 -0
- package/src/hooks/lib/failure-principle.ts +98 -0
- package/src/hooks/lib/ledger-hook.ts +35 -0
- package/src/hooks/lib/ledger.ts +48 -1
- package/src/hooks/lib/paths.ts +0 -1
- package/src/hooks/lib/projects.ts +16 -1
- package/src/hooks/lib/security-gate.ts +159 -0
- package/src/hooks/lib/serves.ts +60 -0
- package/src/hooks/lib/session-context.ts +74 -0
- package/src/hooks/lib/stop.ts +14 -0
- package/src/hooks/lib/telos-goals.ts +144 -0
- package/src/hooks/lib/telos-topics.ts +68 -0
- package/src/hooks/lib/token-usage.ts +3 -1
- package/src/hooks/lib/wall-clock.ts +58 -0
- package/src/tools/agent/algorithm-reflect.ts +28 -97
- package/src/tools/agent/analyze.ts +19 -120
- package/src/tools/agent/handoff-note.ts +40 -70
- package/src/tools/agent/project.ts +47 -136
- package/src/tools/agent/relationship-note.ts +27 -46
- package/src/tools/agent/synthesize.ts +1 -1
- package/src/tools/agent/thread.ts +43 -123
- package/src/tools/control-room/data.ts +332 -0
- package/src/tools/control-room/matrix.ts +182 -0
- package/src/tools/control-room/server.ts +150 -0
- package/src/tools/control-room/ui/agenda.tsx +43 -0
- package/src/tools/control-room/ui/agents.tsx +67 -0
- package/src/tools/control-room/ui/app.css +857 -0
- package/src/tools/control-room/ui/app.tsx +74 -0
- package/src/tools/control-room/ui/board.tsx +82 -0
- package/src/tools/control-room/ui/format.ts +31 -0
- package/src/tools/control-room/ui/handoffs.tsx +37 -0
- package/src/tools/control-room/ui/index.html +19 -0
- package/src/tools/control-room/ui/ledger.tsx +137 -0
- package/src/tools/control-room/ui/matrix.tsx +117 -0
- package/src/tools/control-room/ui/panel.tsx +60 -0
- package/src/tools/control-room/ui/signal.tsx +161 -0
- package/src/tools/ledger/view.ts +3 -0
- package/src/tools/lib/algorithm-reflect.ts +84 -0
- package/src/tools/lib/analyze-report.ts +120 -0
- package/src/tools/lib/handoff-note.ts +88 -0
- package/src/tools/lib/note-flags.ts +59 -0
- package/src/tools/lib/project-isc.ts +151 -0
- package/src/tools/lib/relationship-reflect.ts +402 -0
- package/src/tools/lib/self-model.ts +499 -0
- package/src/tools/lib/session-usage.ts +216 -0
- package/src/tools/lib/skill-doctor.ts +457 -0
- package/src/tools/lib/thread.ts +119 -0
- package/src/tools/lib/token-report.ts +173 -0
- package/src/tools/lib/transcript-usage.ts +42 -0
- package/src/tools/lib/usage-buckets.ts +329 -0
- package/src/tools/relationship-reflect.ts +48 -412
- package/src/tools/self-model.ts +76 -558
- package/src/tools/session-summary.ts +8 -215
- package/src/tools/skill-doctor.ts +9 -444
- package/src/tools/token-cost.ts +18 -428
- package/assets/templates/ledger-page.html +0 -213
- package/src/cli/setup-telos.ts +0 -52
- package/src/hooks/lib/setup.ts +0 -60
- package/src/tools/ledger/server.ts +0 -111
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* bun ~/.pal/tools/project.ts list
|
|
10
|
-
* bun ~/.pal/tools/project.ts create [name] [--path PATH] [--objectives "..."]
|
|
10
|
+
* bun ~/.pal/tools/project.ts create [name] [--path PATH] [--objectives "..."] [--serves goal|revenue|fun]
|
|
11
|
+
* bun ~/.pal/tools/project.ts serves <name> <goal|revenue|fun> [note]
|
|
11
12
|
* bun ~/.pal/tools/project.ts resume <name>
|
|
12
13
|
* bun ~/.pal/tools/project.ts complete | archive | pause | unpause <name>
|
|
13
14
|
* bun ~/.pal/tools/project.ts add-next <name> "text"
|
|
@@ -38,6 +39,19 @@ import {
|
|
|
38
39
|
readProject,
|
|
39
40
|
writeProject,
|
|
40
41
|
} from "../../hooks/lib/projects";
|
|
42
|
+
import { isServesKind, SERVES_KINDS, setServes } from "../../hooks/lib/serves";
|
|
43
|
+
import {
|
|
44
|
+
archiveLine,
|
|
45
|
+
dropEmptyArchiveHeadings,
|
|
46
|
+
encodeIscText,
|
|
47
|
+
ISC_BOX,
|
|
48
|
+
iscTitle,
|
|
49
|
+
nextIscId,
|
|
50
|
+
parseIscs,
|
|
51
|
+
removeIscLine,
|
|
52
|
+
selectIscs,
|
|
53
|
+
taskSlug,
|
|
54
|
+
} from "../lib/project-isc";
|
|
41
55
|
|
|
42
56
|
function now(): string {
|
|
43
57
|
return new Date().toISOString();
|
|
@@ -83,6 +97,8 @@ function cmdCreate(args: string[]): void {
|
|
|
83
97
|
path: { type: "string" },
|
|
84
98
|
name: { type: "string" },
|
|
85
99
|
objectives: { type: "string" },
|
|
100
|
+
serves: { type: "string" },
|
|
101
|
+
"serves-note": { type: "string" },
|
|
86
102
|
},
|
|
87
103
|
allowPositionals: true,
|
|
88
104
|
});
|
|
@@ -111,6 +127,10 @@ function cmdCreate(args: string[]): void {
|
|
|
111
127
|
.join("\n")
|
|
112
128
|
: undefined;
|
|
113
129
|
|
|
130
|
+
if (values.serves !== undefined && !isServesKind(values.serves)) {
|
|
131
|
+
fail(`--serves must be one of: ${SERVES_KINDS.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
const project: ProjectProgress = {
|
|
115
135
|
name,
|
|
116
136
|
path,
|
|
@@ -118,11 +138,31 @@ function cmdCreate(args: string[]): void {
|
|
|
118
138
|
created: now(),
|
|
119
139
|
updated: now(),
|
|
120
140
|
...(goalLines ? { goal: goalLines } : {}),
|
|
141
|
+
...(isServesKind(values.serves)
|
|
142
|
+
? { serves: values.serves, serves_by: "user" as const }
|
|
143
|
+
: {}),
|
|
144
|
+
...(values["serves-note"] ? { serves_note: values["serves-note"] } : {}),
|
|
121
145
|
};
|
|
122
146
|
writeProject(project);
|
|
123
147
|
ok({ created: true, project });
|
|
124
148
|
}
|
|
125
149
|
|
|
150
|
+
/** The answer the scaffolder asks for, and the one place importance can be corrected. */
|
|
151
|
+
function cmdServes(args: string[]): void {
|
|
152
|
+
const [name, kind, ...note] = args;
|
|
153
|
+
if (!name || !kind) fail("Usage: serves <name> <goal|revenue|fun> [note]");
|
|
154
|
+
if (!isServesKind(kind)) fail(`serves must be one of: ${SERVES_KINDS.join(", ")}`);
|
|
155
|
+
|
|
156
|
+
const outcome = setServes({
|
|
157
|
+
name,
|
|
158
|
+
kind,
|
|
159
|
+
note: note.join(" ").trim() || undefined,
|
|
160
|
+
by: "user",
|
|
161
|
+
});
|
|
162
|
+
if (outcome === "missing") fail(`No project named "${name}".`);
|
|
163
|
+
ok({ project: name, serves: kind, by: "user" });
|
|
164
|
+
}
|
|
165
|
+
|
|
126
166
|
// ── resume ────────────────────────────────────────────────────────
|
|
127
167
|
|
|
128
168
|
// resume returns a lean orientation view: all narrative sections, but the
|
|
@@ -372,130 +412,13 @@ function cmdRm(args: string[]): void {
|
|
|
372
412
|
// Three states, not two: a retired ISC is one that stopped being valid, which the
|
|
373
413
|
// record must not report as completed work. The box character is the storage form
|
|
374
414
|
// and the id stays in it, so a retired line keeps reserving its id in nextIscId.
|
|
375
|
-
type IscStatus = "open" | "done" | "retired";
|
|
376
|
-
|
|
377
|
-
const ISC_BOX: Record<IscStatus, string> = {
|
|
378
|
-
open: "[ ]",
|
|
379
|
-
done: "[x]",
|
|
380
|
-
retired: "[~]",
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
function statusFromBox(box: string): IscStatus {
|
|
384
|
-
if (box.toLowerCase() === "x") return "done";
|
|
385
|
-
if (box === "~") return "retired";
|
|
386
|
-
return "open";
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
interface Isc {
|
|
390
|
-
id: number;
|
|
391
|
-
text: string;
|
|
392
|
-
status: IscStatus;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* An ISC is one markdown line, so a newline in its text would end the record
|
|
397
|
-
* and strand every paragraph after it as unparseable debris. Backslashes are
|
|
398
|
-
* escaped first so that decoding a literal "\n" in a regex cannot be mistaken
|
|
399
|
-
* for the separator.
|
|
400
|
-
*/
|
|
401
|
-
function encodeIscText(text: string): string {
|
|
402
|
-
return text
|
|
403
|
-
.replaceAll("\\", "\\\\")
|
|
404
|
-
.replaceAll("\r\n", "\n")
|
|
405
|
-
.replaceAll("\r", "\n")
|
|
406
|
-
.replaceAll("\n", "\\n");
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const ISC_UNESCAPE: Record<string, string> = { n: "\n", "\\": "\\" };
|
|
410
|
-
|
|
411
|
-
function decodeIscText(stored: string): string {
|
|
412
|
-
return stored.replaceAll(/\\(.)/g, (whole, ch) => ISC_UNESCAPE[ch] ?? whole);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function parseIscs(criteria: string): Isc[] {
|
|
416
|
-
const out: Isc[] = [];
|
|
417
|
-
for (const line of criteria.split("\n")) {
|
|
418
|
-
const m = new RegExp(/^-\s+\[( |x|~)\]\s+ISC-(\d+):\s+(.+)$/i).exec(line);
|
|
419
|
-
if (m)
|
|
420
|
-
out.push({
|
|
421
|
-
id: Number(m[2]),
|
|
422
|
-
text: decodeIscText(m[3].trim()),
|
|
423
|
-
status: statusFromBox(m[1]),
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
return out;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Collapse a full ISC line to a glanceable title for resume: cut at the first
|
|
430
|
-
// clause boundary, then hard-cap length. Full text stays reachable via show-isc.
|
|
431
|
-
function iscTitle(text: string): string {
|
|
432
|
-
const boundary = text.search(/; | — | \(|\. /);
|
|
433
|
-
const clause = (boundary > 0 ? text.slice(0, boundary) : text).trim();
|
|
434
|
-
return clause.length > 80 ? `${clause.slice(0, 79).trimEnd()}…` : clause;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Scans Criteria AND Changelog so an archived id can never be handed out again.
|
|
438
|
-
function nextIscId(p: ProjectProgress): number {
|
|
439
|
-
const ids = [...parseIscs(p.criteria ?? ""), ...parseIscs(p.changelog ?? "")].map(
|
|
440
|
-
(i) => i.id
|
|
441
|
-
);
|
|
442
|
-
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function removeIscLine(
|
|
446
|
-
section: string,
|
|
447
|
-
id: number
|
|
448
|
-
): { line: string | null; rest: string } {
|
|
449
|
-
const lines = section.split("\n");
|
|
450
|
-
const idx = lines.findIndex((l) =>
|
|
451
|
-
new RegExp(String.raw`^-\s+\[[ x~]\]\s+ISC-${id}:`).test(l)
|
|
452
|
-
);
|
|
453
|
-
if (idx === -1) return { line: null, rest: section };
|
|
454
|
-
const [line] = lines.splice(idx, 1);
|
|
455
|
-
return {
|
|
456
|
-
line,
|
|
457
|
-
rest: lines
|
|
458
|
-
.join("\n")
|
|
459
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
460
|
-
.trim(),
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Drops any "### Archived <date>" heading whose block has no content left —
|
|
465
|
-
// e.g. after every ISC filed under that date has been reopened.
|
|
466
|
-
function dropEmptyArchiveHeadings(changelog: string): string {
|
|
467
|
-
const lines = changelog.split("\n");
|
|
468
|
-
const blockHasContent = (headingIdx: number): boolean => {
|
|
469
|
-
for (let j = headingIdx + 1; j < lines.length && !lines[j].startsWith("### "); j++) {
|
|
470
|
-
if (lines[j].trim() !== "") return true;
|
|
471
|
-
}
|
|
472
|
-
return false;
|
|
473
|
-
};
|
|
474
|
-
return lines
|
|
475
|
-
.filter((l, i) => !(/^### Archived /.test(l) && !blockHasContent(i)))
|
|
476
|
-
.join("\n")
|
|
477
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
478
|
-
.trim();
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function archiveLine(
|
|
482
|
-
changelog: string | undefined,
|
|
483
|
-
doneLine: string,
|
|
484
|
-
kind: "Archived" | "Retired" = "Archived"
|
|
485
|
-
): string {
|
|
486
|
-
const heading = `### ${kind} ${new Date().toISOString().slice(0, 10)}`;
|
|
487
|
-
const base = (changelog ?? "").trim();
|
|
488
|
-
if (base.includes(heading)) return `${base}\n${doneLine}`;
|
|
489
|
-
return base ? `${base}\n\n${heading}\n${doneLine}` : `${heading}\n${doneLine}`;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
415
|
function cmdAddIsc(args: string[]): void {
|
|
493
416
|
const name = args[0] ?? fail("Usage: add-isc <name> <title>");
|
|
494
417
|
const title = args.slice(1).join(" ").trim();
|
|
495
418
|
if (!title) fail("Usage: add-isc <name> <title>");
|
|
496
419
|
const p = requireProject(name);
|
|
497
420
|
const current = p.criteria ?? "";
|
|
498
|
-
const id = nextIscId(p);
|
|
421
|
+
const id = nextIscId(current, p.changelog ?? "");
|
|
499
422
|
const newLine = `- [ ] ISC-${id}: ${encodeIscText(title)}`;
|
|
500
423
|
p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
|
|
501
424
|
p.updated = now();
|
|
@@ -556,13 +479,6 @@ function cmdReopenIsc(args: string[]): void {
|
|
|
556
479
|
ok({ checked: false, id });
|
|
557
480
|
}
|
|
558
481
|
|
|
559
|
-
function selectIscs(open: Isc[], done: Isc[], retired: Isc[], flags: Set<string>): Isc[] {
|
|
560
|
-
if (flags.has("--all")) return [...open, ...done, ...retired];
|
|
561
|
-
if (flags.has("--closed")) return done;
|
|
562
|
-
if (flags.has("--retired")) return retired;
|
|
563
|
-
return open;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
482
|
function cmdListIsc(args: string[]): void {
|
|
567
483
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
568
484
|
const name =
|
|
@@ -693,15 +609,6 @@ function cmdPruneIsc(args: string[]): void {
|
|
|
693
609
|
|
|
694
610
|
// ── Task ISA (work/) ──────────────────────────────────────────────
|
|
695
611
|
|
|
696
|
-
function taskSlug(title: string): string {
|
|
697
|
-
const sanitized = title
|
|
698
|
-
.toLowerCase()
|
|
699
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
700
|
-
.replace(/^-+|-+$/g, "")
|
|
701
|
-
.slice(0, 40);
|
|
702
|
-
return `${sanitized}-${Date.now().toString(36)}`;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
612
|
function taskIsaPath(slug: string): string {
|
|
706
613
|
const dir = resolve(paths.work(), slug);
|
|
707
614
|
mkdirSync(dir, { recursive: true });
|
|
@@ -753,7 +660,8 @@ function help(): void {
|
|
|
753
660
|
|
|
754
661
|
Commands:
|
|
755
662
|
list show all registered projects
|
|
756
|
-
create [name] [--path PATH] [--objectives X] register a project
|
|
663
|
+
create [name] [--path PATH] [--objectives X] [--serves KIND] register a project
|
|
664
|
+
serves <name> <goal|revenue|fun> [note] say what it is for — outranks PAL's guess
|
|
757
665
|
resume <name> print lean project view (open-ISC titles; full text via show-isc)
|
|
758
666
|
complete <name> mark complete
|
|
759
667
|
archive <name> mark archived
|
|
@@ -796,6 +704,9 @@ function run(): void {
|
|
|
796
704
|
case "create":
|
|
797
705
|
cmdCreate(rest);
|
|
798
706
|
return;
|
|
707
|
+
case "serves":
|
|
708
|
+
cmdServes(rest);
|
|
709
|
+
return;
|
|
799
710
|
case "resume":
|
|
800
711
|
cmdResume(rest);
|
|
801
712
|
return;
|
|
@@ -13,27 +13,17 @@
|
|
|
13
13
|
* Note types:
|
|
14
14
|
* --o Opinion/behavioral observation about the user (requires --confidence)
|
|
15
15
|
* --w World fact about the user's situation (objective, observable)
|
|
16
|
-
* --b Session diary — what
|
|
16
|
+
* --b Session diary — what the assistant did this session (first-person)
|
|
17
|
+
*
|
|
18
|
+
* Which flags make which notes is in lib/note-flags.ts.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import { parseArgs } from "node:util";
|
|
20
22
|
import { appendNotes } from "../../hooks/lib/relationship";
|
|
21
23
|
import { emit } from "../lib/emit";
|
|
24
|
+
import { notesFromFlags } from "../lib/note-flags";
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
const { values } = parseArgs({
|
|
25
|
-
args: Bun.argv.slice(2),
|
|
26
|
-
options: {
|
|
27
|
-
o: { type: "string", multiple: true },
|
|
28
|
-
w: { type: "string", multiple: true },
|
|
29
|
-
b: { type: "string" },
|
|
30
|
-
confidence: { type: "string" },
|
|
31
|
-
help: { type: "boolean", short: "h" },
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (values.help) {
|
|
36
|
-
console.log(`
|
|
26
|
+
const HELP = `
|
|
37
27
|
RelationshipNote — Append W/O/Session entries to today's relationship log
|
|
38
28
|
|
|
39
29
|
Usage:
|
|
@@ -45,45 +35,36 @@ Flags:
|
|
|
45
35
|
--o TEXT Opinion/behavioral observation about the user
|
|
46
36
|
--confidence N Confidence for --o (0.0–1.0, default 0.75)
|
|
47
37
|
--w TEXT World fact about the user's situation
|
|
48
|
-
--b TEXT Session diary — what
|
|
38
|
+
--b TEXT Session diary — what the assistant did (first-person)
|
|
49
39
|
|
|
50
40
|
Multiple flags may be combined in one call. At least one of --o, --w, --b is required.
|
|
51
41
|
|
|
52
42
|
Output: appends to memory/relationship/YYYY-MM/YYYY-MM-DD.md
|
|
53
|
-
|
|
54
|
-
process.exit(0);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!values.o && !values.w && !values.b) {
|
|
58
|
-
console.error("Required: at least one of --o, --w, --b");
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const notes = [];
|
|
43
|
+
`;
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const { values } = parseArgs({
|
|
47
|
+
args: Bun.argv.slice(2),
|
|
48
|
+
options: {
|
|
49
|
+
o: { type: "string", multiple: true },
|
|
50
|
+
w: { type: "string", multiple: true },
|
|
51
|
+
b: { type: "string" },
|
|
52
|
+
confidence: { type: "string" },
|
|
53
|
+
help: { type: "boolean", short: "h" },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
74
56
|
|
|
75
|
-
if (values.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
57
|
+
if (values.help) {
|
|
58
|
+
console.log(HELP);
|
|
59
|
+
process.exit(0);
|
|
79
60
|
}
|
|
80
61
|
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
const result = notesFromFlags(values);
|
|
63
|
+
if ("error" in result) {
|
|
64
|
+
console.error(result.error);
|
|
65
|
+
process.exit(1);
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
const { file, written } = appendNotes(notes);
|
|
86
|
-
emit.receipt(file, { written, deduped: notes.length - written });
|
|
68
|
+
const { file, written } = appendNotes(result.notes);
|
|
69
|
+
emit.receipt(file, { written, deduped: result.notes.length - written });
|
|
87
70
|
}
|
|
88
|
-
|
|
89
|
-
if (import.meta.main) run();
|
|
@@ -17,7 +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
|
+
import { readJsonl } from "../lib/self-model";
|
|
21
21
|
|
|
22
22
|
// ── Config ──
|
|
23
23
|
|
|
@@ -11,99 +11,54 @@
|
|
|
11
11
|
* bun ~/.pal/tools/thread.ts --list [--all]
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { resolve } from "node:path";
|
|
16
14
|
import { parseArgs } from "node:util";
|
|
17
|
-
import { currentAttribution, type RecordAttribution } from "../../hooks/lib/actor";
|
|
18
|
-
import { encodeAnchor } from "../../hooks/lib/anchor";
|
|
19
|
-
import { ensureDir, paths } from "../../hooks/lib/paths";
|
|
20
15
|
import { emit } from "../lib/emit";
|
|
16
|
+
import {
|
|
17
|
+
addThread,
|
|
18
|
+
readThreads,
|
|
19
|
+
resolveThreadIn,
|
|
20
|
+
threadsFile,
|
|
21
|
+
visibleThreads,
|
|
22
|
+
writeThreads,
|
|
23
|
+
} from "../lib/thread";
|
|
24
|
+
|
|
25
|
+
const HELP = `
|
|
26
|
+
Thread — Manage open threads across sessions
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
title: string;
|
|
28
|
-
context: string;
|
|
29
|
-
status: "open" | "resolved";
|
|
30
|
-
created: string;
|
|
31
|
-
resolved: string | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Storage ──
|
|
35
|
-
|
|
36
|
-
function threadsPath(): string {
|
|
37
|
-
return resolve(ensureDir(paths.state()), "threads.jsonl");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function generateId(): string {
|
|
41
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
42
|
-
}
|
|
28
|
+
Usage:
|
|
29
|
+
thread.ts --add --title "..." [--context "..."]
|
|
30
|
+
thread.ts --resolve --id <id>
|
|
31
|
+
thread.ts --list [--all]
|
|
32
|
+
`;
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return readFileSync(p, "utf-8")
|
|
49
|
-
.split("\n")
|
|
50
|
-
.filter((l) => l.trim())
|
|
51
|
-
.map((l) => JSON.parse(l) as Thread);
|
|
52
|
-
} catch {
|
|
53
|
-
return [];
|
|
34
|
+
function add(title: string | undefined, context: string | undefined) {
|
|
35
|
+
if (!title) {
|
|
36
|
+
console.error("--title required");
|
|
37
|
+
process.exit(1);
|
|
54
38
|
}
|
|
39
|
+
const thread = addThread(title, context ?? "");
|
|
40
|
+
emit.receipt(threadsFile(), {
|
|
41
|
+
id: thread.id,
|
|
42
|
+
title: thread.title,
|
|
43
|
+
status: thread.status,
|
|
44
|
+
});
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
id: generateId(),
|
|
71
|
-
cwd: encodeAnchor(process.cwd()),
|
|
72
|
-
...currentAttribution(),
|
|
73
|
-
title,
|
|
74
|
-
context,
|
|
75
|
-
status: "open",
|
|
76
|
-
created: new Date().toISOString(),
|
|
77
|
-
resolved: null,
|
|
78
|
-
};
|
|
79
|
-
appendFileSync(threadsPath(), `${JSON.stringify(thread)}\n`, "utf-8");
|
|
80
|
-
return thread;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveThread(id: string): {
|
|
84
|
-
success: boolean;
|
|
85
|
-
thread?: Thread;
|
|
86
|
-
message: string;
|
|
87
|
-
} {
|
|
88
|
-
const threads = readThreads();
|
|
89
|
-
const idx = threads.findIndex((t) => t.id === id);
|
|
90
|
-
if (idx === -1) return { success: false, message: `Thread not found: ${id}` };
|
|
91
|
-
threads[idx].status = "resolved";
|
|
92
|
-
threads[idx].resolved = new Date().toISOString();
|
|
93
|
-
writeThreads(threads);
|
|
94
|
-
return {
|
|
95
|
-
success: true,
|
|
96
|
-
thread: threads[idx],
|
|
97
|
-
message: `Resolved: ${threads[idx].title}`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function listThreads(all: boolean): Thread[] {
|
|
102
|
-
return all ? readThreads() : readThreads().filter((t) => t.status === "open");
|
|
47
|
+
function markResolved(id: string | undefined) {
|
|
48
|
+
if (!id) {
|
|
49
|
+
console.error("--id required");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const file = threadsFile();
|
|
53
|
+
const resolution = resolveThreadIn(readThreads(file), id, new Date());
|
|
54
|
+
if (!resolution) {
|
|
55
|
+
console.error(`Thread not found: ${id}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
writeThreads(resolution.threads, file);
|
|
59
|
+
emit.receipt(file, { id, status: "resolved", title: resolution.thread.title });
|
|
103
60
|
}
|
|
104
61
|
|
|
105
|
-
// ── CLI ──
|
|
106
|
-
|
|
107
62
|
function run() {
|
|
108
63
|
const { values } = parseArgs({
|
|
109
64
|
args: Bun.argv.slice(2),
|
|
@@ -125,49 +80,14 @@ function run() {
|
|
|
125
80
|
else if (values.list) cmd = "list";
|
|
126
81
|
|
|
127
82
|
if (values.help || !cmd) {
|
|
128
|
-
console.log(
|
|
129
|
-
Thread — Manage open threads across sessions
|
|
130
|
-
|
|
131
|
-
Usage:
|
|
132
|
-
thread.ts --add --title "..." [--context "..."]
|
|
133
|
-
thread.ts --resolve --id <id>
|
|
134
|
-
thread.ts --list [--all]
|
|
135
|
-
`);
|
|
83
|
+
console.log(HELP);
|
|
136
84
|
process.exit(cmd ? 0 : 1);
|
|
137
85
|
}
|
|
138
86
|
|
|
139
|
-
if (cmd === "add")
|
|
140
|
-
|
|
141
|
-
console.error("--title required");
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
const thread = addThread(values.title, values.context ?? "");
|
|
145
|
-
emit.receipt(threadsPath(), {
|
|
146
|
-
id: thread.id,
|
|
147
|
-
title: thread.title,
|
|
148
|
-
status: thread.status,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (cmd === "resolve") {
|
|
153
|
-
if (!values.id) {
|
|
154
|
-
console.error("--id required");
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
const resolved = resolveThread(values.id);
|
|
158
|
-
if (!resolved.success) {
|
|
159
|
-
console.error(resolved.message);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
emit.receipt(threadsPath(), {
|
|
163
|
-
id: values.id,
|
|
164
|
-
status: "resolved",
|
|
165
|
-
title: resolved.thread?.title,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
87
|
+
if (cmd === "add") add(values.title, values.context);
|
|
88
|
+
if (cmd === "resolve") markResolved(values.id);
|
|
169
89
|
if (cmd === "list") {
|
|
170
|
-
const threads =
|
|
90
|
+
const threads = visibleThreads(readThreads(), values.all ?? false);
|
|
171
91
|
emit.data(JSON.stringify({ count: threads.length, threads }, null, 2));
|
|
172
92
|
}
|
|
173
93
|
}
|