portable-agent-layer 0.71.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/package.json +1 -1
- package/src/cli/migrate.ts +1 -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/failure-principle.ts +19 -44
- package/src/hooks/handlers/session-intelligence.ts +13 -70
- 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/security-gate.ts +159 -0
- package/src/hooks/lib/session-context.ts +74 -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 +29 -77
- package/src/tools/agent/project.ts +13 -134
- 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 +2 -2
- package/src/tools/control-room/matrix.ts +1 -1
- package/src/tools/control-room/ui/ledger.tsx +2 -1
- 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
|
@@ -10,70 +10,38 @@
|
|
|
10
10
|
* bun ~/.pal/tools/handoff-note.ts --done # mark completed, suppress next-session injection
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import { resolve } from "node:path";
|
|
13
|
+
import { writeFileSync } from "node:fs";
|
|
15
14
|
import { parseArgs } from "node:util";
|
|
16
|
-
import { ensureDir, paths } from "../../hooks/lib/paths";
|
|
17
15
|
import { emit } from "../lib/emit";
|
|
16
|
+
import {
|
|
17
|
+
handoffFile,
|
|
18
|
+
type NoteInput,
|
|
19
|
+
readHandoffs,
|
|
20
|
+
recordNote,
|
|
21
|
+
statusOf,
|
|
22
|
+
} from "../lib/handoff-note";
|
|
23
|
+
|
|
24
|
+
const HELP = `
|
|
25
|
+
HandoffNote — Write a handoff note for the current project
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
status: "in-progress" | "completed";
|
|
23
|
-
handoff: string;
|
|
24
|
-
artifacts: string[];
|
|
25
|
-
source: "deliberate" | "auto";
|
|
26
|
-
/** What the work needs from the human before it can move — the one thing an agent cannot unblock. */
|
|
27
|
-
waitingOn?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function handoffPath(): string {
|
|
31
|
-
return resolve(ensureDir(paths.state()), "last-handoff.json");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function readHandoffs(): Record<string, HandoffEntry> {
|
|
35
|
-
const p = handoffPath();
|
|
36
|
-
if (!existsSync(p)) return {};
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
39
|
-
} catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
27
|
+
Usage:
|
|
28
|
+
bun ~/.pal/tools/handoff-note.ts --title "what we were doing" --text "what remains"
|
|
29
|
+
bun ~/.pal/tools/handoff-note.ts --done # mark session completed
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return Object.keys(trimmed).length;
|
|
50
|
-
}
|
|
31
|
+
Arguments:
|
|
32
|
+
--title Brief title of what was being worked on (5-10 words)
|
|
33
|
+
--text What remains unfinished — decisions made, next steps, blockers
|
|
34
|
+
--waiting What this needs from you before it can move (a decision, an answer, access)
|
|
35
|
+
--done Mark as completed; suppresses "pick up where you left off" injection
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
title: string;
|
|
55
|
-
text: string;
|
|
56
|
-
done: boolean;
|
|
57
|
-
waitingOn?: string;
|
|
58
|
-
}
|
|
37
|
+
Output: writes to memory/state/last-handoff.json keyed by cwd
|
|
38
|
+
`;
|
|
59
39
|
|
|
60
|
-
function
|
|
61
|
-
file
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const handoffs = readHandoffs();
|
|
66
|
-
handoffs[note.cwd] = {
|
|
67
|
-
timestamp: new Date().toISOString(),
|
|
68
|
-
title: note.title,
|
|
69
|
-
status: note.done ? "completed" : "in-progress",
|
|
70
|
-
handoff: note.text,
|
|
71
|
-
artifacts: [],
|
|
72
|
-
source: "deliberate",
|
|
73
|
-
...(note.waitingOn ? { waitingOn: note.waitingOn } : {}),
|
|
74
|
-
};
|
|
75
|
-
const kept = writeHandoffs(handoffs);
|
|
76
|
-
return { file: handoffPath(), status: handoffs[note.cwd].status, kept };
|
|
40
|
+
function saveNote(note: NoteInput): void {
|
|
41
|
+
const file = handoffFile();
|
|
42
|
+
const store = recordNote(readHandoffs(file), note, new Date());
|
|
43
|
+
writeFileSync(file, JSON.stringify(store, null, 2), "utf-8");
|
|
44
|
+
emit.receipt(file, { status: statusOf(note), entries: Object.keys(store).length });
|
|
77
45
|
}
|
|
78
46
|
|
|
79
47
|
function run() {
|
|
@@ -89,32 +57,17 @@ function run() {
|
|
|
89
57
|
});
|
|
90
58
|
|
|
91
59
|
if (values.help) {
|
|
92
|
-
console.log(
|
|
93
|
-
HandoffNote — Write a handoff note for the current project
|
|
94
|
-
|
|
95
|
-
Usage:
|
|
96
|
-
bun ~/.pal/tools/handoff-note.ts --title "what we were doing" --text "what remains"
|
|
97
|
-
bun ~/.pal/tools/handoff-note.ts --done # mark session completed
|
|
98
|
-
|
|
99
|
-
Arguments:
|
|
100
|
-
--title Brief title of what was being worked on (5-10 words)
|
|
101
|
-
--text What remains unfinished — decisions made, next steps, blockers
|
|
102
|
-
--waiting What this needs from you before it can move (a decision, an answer, access)
|
|
103
|
-
--done Mark as completed; suppresses "pick up where you left off" injection
|
|
104
|
-
|
|
105
|
-
Output: writes to memory/state/last-handoff.json keyed by cwd
|
|
106
|
-
`);
|
|
60
|
+
console.log(HELP);
|
|
107
61
|
process.exit(0);
|
|
108
62
|
}
|
|
109
63
|
|
|
110
64
|
if (values.done) {
|
|
111
|
-
|
|
65
|
+
saveNote({
|
|
112
66
|
cwd: process.cwd(),
|
|
113
67
|
title: values.title || "session",
|
|
114
68
|
text: values.text || "",
|
|
115
69
|
done: true,
|
|
116
70
|
});
|
|
117
|
-
emit.receipt(result.file, { status: result.status, entries: result.kept });
|
|
118
71
|
process.exit(0);
|
|
119
72
|
}
|
|
120
73
|
|
|
@@ -123,14 +76,13 @@ Output: writes to memory/state/last-handoff.json keyed by cwd
|
|
|
123
76
|
process.exit(1);
|
|
124
77
|
}
|
|
125
78
|
|
|
126
|
-
|
|
79
|
+
saveNote({
|
|
127
80
|
cwd: process.cwd(),
|
|
128
81
|
title: values.title,
|
|
129
82
|
text: values.text,
|
|
130
83
|
done: false,
|
|
131
84
|
waitingOn: values.waiting,
|
|
132
85
|
});
|
|
133
|
-
emit.receipt(result.file, { status: result.status, entries: result.kept });
|
|
134
86
|
}
|
|
135
87
|
|
|
136
88
|
if (import.meta.main) run();
|
|
@@ -40,6 +40,18 @@ import {
|
|
|
40
40
|
writeProject,
|
|
41
41
|
} from "../../hooks/lib/projects";
|
|
42
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";
|
|
43
55
|
|
|
44
56
|
function now(): string {
|
|
45
57
|
return new Date().toISOString();
|
|
@@ -400,130 +412,13 @@ function cmdRm(args: string[]): void {
|
|
|
400
412
|
// Three states, not two: a retired ISC is one that stopped being valid, which the
|
|
401
413
|
// record must not report as completed work. The box character is the storage form
|
|
402
414
|
// and the id stays in it, so a retired line keeps reserving its id in nextIscId.
|
|
403
|
-
type IscStatus = "open" | "done" | "retired";
|
|
404
|
-
|
|
405
|
-
const ISC_BOX: Record<IscStatus, string> = {
|
|
406
|
-
open: "[ ]",
|
|
407
|
-
done: "[x]",
|
|
408
|
-
retired: "[~]",
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
function statusFromBox(box: string): IscStatus {
|
|
412
|
-
if (box.toLowerCase() === "x") return "done";
|
|
413
|
-
if (box === "~") return "retired";
|
|
414
|
-
return "open";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export interface Isc {
|
|
418
|
-
id: number;
|
|
419
|
-
text: string;
|
|
420
|
-
status: IscStatus;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* An ISC is one markdown line, so a newline in its text would end the record
|
|
425
|
-
* and strand every paragraph after it as unparseable debris. Backslashes are
|
|
426
|
-
* escaped first so that decoding a literal "\n" in a regex cannot be mistaken
|
|
427
|
-
* for the separator.
|
|
428
|
-
*/
|
|
429
|
-
function encodeIscText(text: string): string {
|
|
430
|
-
return text
|
|
431
|
-
.replaceAll("\\", "\\\\")
|
|
432
|
-
.replaceAll("\r\n", "\n")
|
|
433
|
-
.replaceAll("\r", "\n")
|
|
434
|
-
.replaceAll("\n", "\\n");
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const ISC_UNESCAPE: Record<string, string> = { n: "\n", "\\": "\\" };
|
|
438
|
-
|
|
439
|
-
function decodeIscText(stored: string): string {
|
|
440
|
-
return stored.replaceAll(/\\(.)/g, (whole, ch) => ISC_UNESCAPE[ch] ?? whole);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function parseIscs(criteria: string): Isc[] {
|
|
444
|
-
const out: Isc[] = [];
|
|
445
|
-
for (const line of criteria.split("\n")) {
|
|
446
|
-
const m = new RegExp(/^-\s+\[( |x|~)\]\s+ISC-(\d+):\s+(.+)$/i).exec(line);
|
|
447
|
-
if (m)
|
|
448
|
-
out.push({
|
|
449
|
-
id: Number(m[2]),
|
|
450
|
-
text: decodeIscText(m[3].trim()),
|
|
451
|
-
status: statusFromBox(m[1]),
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return out;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Collapse a full ISC line to a glanceable title for resume: cut at the first
|
|
458
|
-
// clause boundary, then hard-cap length. Full text stays reachable via show-isc.
|
|
459
|
-
function iscTitle(text: string): string {
|
|
460
|
-
const boundary = text.search(/; | — | \(|\. /);
|
|
461
|
-
const clause = (boundary > 0 ? text.slice(0, boundary) : text).trim();
|
|
462
|
-
return clause.length > 80 ? `${clause.slice(0, 79).trimEnd()}…` : clause;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Scans Criteria AND Changelog so an archived id can never be handed out again.
|
|
466
|
-
function nextIscId(p: ProjectProgress): number {
|
|
467
|
-
const ids = [...parseIscs(p.criteria ?? ""), ...parseIscs(p.changelog ?? "")].map(
|
|
468
|
-
(i) => i.id
|
|
469
|
-
);
|
|
470
|
-
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function removeIscLine(
|
|
474
|
-
section: string,
|
|
475
|
-
id: number
|
|
476
|
-
): { line: string | null; rest: string } {
|
|
477
|
-
const lines = section.split("\n");
|
|
478
|
-
const idx = lines.findIndex((l) =>
|
|
479
|
-
new RegExp(String.raw`^-\s+\[[ x~]\]\s+ISC-${id}:`).test(l)
|
|
480
|
-
);
|
|
481
|
-
if (idx === -1) return { line: null, rest: section };
|
|
482
|
-
const [line] = lines.splice(idx, 1);
|
|
483
|
-
return {
|
|
484
|
-
line,
|
|
485
|
-
rest: lines
|
|
486
|
-
.join("\n")
|
|
487
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
488
|
-
.trim(),
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Drops any "### Archived <date>" heading whose block has no content left —
|
|
493
|
-
// e.g. after every ISC filed under that date has been reopened.
|
|
494
|
-
function dropEmptyArchiveHeadings(changelog: string): string {
|
|
495
|
-
const lines = changelog.split("\n");
|
|
496
|
-
const blockHasContent = (headingIdx: number): boolean => {
|
|
497
|
-
for (let j = headingIdx + 1; j < lines.length && !lines[j].startsWith("### "); j++) {
|
|
498
|
-
if (lines[j].trim() !== "") return true;
|
|
499
|
-
}
|
|
500
|
-
return false;
|
|
501
|
-
};
|
|
502
|
-
return lines
|
|
503
|
-
.filter((l, i) => !(/^### Archived /.test(l) && !blockHasContent(i)))
|
|
504
|
-
.join("\n")
|
|
505
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
506
|
-
.trim();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function archiveLine(
|
|
510
|
-
changelog: string | undefined,
|
|
511
|
-
doneLine: string,
|
|
512
|
-
kind: "Archived" | "Retired" = "Archived"
|
|
513
|
-
): string {
|
|
514
|
-
const heading = `### ${kind} ${new Date().toISOString().slice(0, 10)}`;
|
|
515
|
-
const base = (changelog ?? "").trim();
|
|
516
|
-
if (base.includes(heading)) return `${base}\n${doneLine}`;
|
|
517
|
-
return base ? `${base}\n\n${heading}\n${doneLine}` : `${heading}\n${doneLine}`;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
415
|
function cmdAddIsc(args: string[]): void {
|
|
521
416
|
const name = args[0] ?? fail("Usage: add-isc <name> <title>");
|
|
522
417
|
const title = args.slice(1).join(" ").trim();
|
|
523
418
|
if (!title) fail("Usage: add-isc <name> <title>");
|
|
524
419
|
const p = requireProject(name);
|
|
525
420
|
const current = p.criteria ?? "";
|
|
526
|
-
const id = nextIscId(p);
|
|
421
|
+
const id = nextIscId(current, p.changelog ?? "");
|
|
527
422
|
const newLine = `- [ ] ISC-${id}: ${encodeIscText(title)}`;
|
|
528
423
|
p.criteria = current ? `${current.trimEnd()}\n${newLine}` : newLine;
|
|
529
424
|
p.updated = now();
|
|
@@ -584,13 +479,6 @@ function cmdReopenIsc(args: string[]): void {
|
|
|
584
479
|
ok({ checked: false, id });
|
|
585
480
|
}
|
|
586
481
|
|
|
587
|
-
function selectIscs(open: Isc[], done: Isc[], retired: Isc[], flags: Set<string>): Isc[] {
|
|
588
|
-
if (flags.has("--all")) return [...open, ...done, ...retired];
|
|
589
|
-
if (flags.has("--closed")) return done;
|
|
590
|
-
if (flags.has("--retired")) return retired;
|
|
591
|
-
return open;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
482
|
function cmdListIsc(args: string[]): void {
|
|
595
483
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
596
484
|
const name =
|
|
@@ -721,15 +609,6 @@ function cmdPruneIsc(args: string[]): void {
|
|
|
721
609
|
|
|
722
610
|
// ── Task ISA (work/) ──────────────────────────────────────────────
|
|
723
611
|
|
|
724
|
-
function taskSlug(title: string): string {
|
|
725
|
-
const sanitized = title
|
|
726
|
-
.toLowerCase()
|
|
727
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
728
|
-
.replace(/^-+|-+$/g, "")
|
|
729
|
-
.slice(0, 40);
|
|
730
|
-
return `${sanitized}-${Date.now().toString(36)}`;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
612
|
function taskIsaPath(slug: string): string {
|
|
734
613
|
const dir = resolve(paths.work(), slug);
|
|
735
614
|
mkdirSync(dir, { recursive: true });
|
|
@@ -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
|
}
|
|
@@ -16,9 +16,9 @@ import { loadAnalyzeNudge } from "../../hooks/lib/analyze-nudge";
|
|
|
16
16
|
import { paths } from "../../hooks/lib/paths";
|
|
17
17
|
import { isStale, type ProjectProgress, readAllProjects } from "../../hooks/lib/projects";
|
|
18
18
|
import { readProjectHistory } from "../../hooks/lib/work-tracking";
|
|
19
|
-
import { type HandoffEntry, readHandoffs } from "../agent/handoff-note";
|
|
20
|
-
import { parseIscs } from "../agent/project";
|
|
21
19
|
import { anchorSlugOf, type LedgerFilter, queryLedger } from "../ledger/query";
|
|
20
|
+
import { type HandoffEntry, readHandoffs } from "../lib/handoff-note";
|
|
21
|
+
import { parseIscs } from "../lib/project-isc";
|
|
22
22
|
|
|
23
23
|
const DAY_MS = 86_400_000;
|
|
24
24
|
const HANDOFF_FRESH_DAYS = 7;
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "../../hooks/lib/projects";
|
|
21
21
|
import { isImportant, SERVES_MEANING } from "../../hooks/lib/serves";
|
|
22
22
|
import { dueFrom, readTelosGoals, type TelosGoal } from "../../hooks/lib/telos-goals";
|
|
23
|
-
import type { HandoffEntry } from "../
|
|
23
|
+
import type { HandoffEntry } from "../lib/handoff-note";
|
|
24
24
|
import { freshHandoffs } from "./data";
|
|
25
25
|
|
|
26
26
|
const URGENT_WITHIN_DAYS = 14;
|
|
@@ -28,8 +28,9 @@ function Row({ r }: { r: LedgerViewRow }) {
|
|
|
28
28
|
</span>
|
|
29
29
|
</td>
|
|
30
30
|
<td>{r.tool}</td>
|
|
31
|
-
<td className="target" title={r.target}>
|
|
31
|
+
<td className="target" title={r.command ?? r.target}>
|
|
32
32
|
<b>{slug}</b> {rest.join(" ")}
|
|
33
|
+
{r.command && <span className="reason">{r.command}</span>}
|
|
33
34
|
</td>
|
|
34
35
|
<td>{r.change}</td>
|
|
35
36
|
<td>
|
package/src/tools/ledger/view.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface LedgerViewRow {
|
|
|
40
40
|
change: string;
|
|
41
41
|
outcome: string;
|
|
42
42
|
reason?: string;
|
|
43
|
+
/** Present only on a blocked shell action, where the target is a directory. */
|
|
44
|
+
command?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export interface LedgerView {
|
|
@@ -100,6 +102,7 @@ function toRow(entry: LedgerEntry, registry: ActorRegistryEntry[]): LedgerViewRo
|
|
|
100
102
|
outcome: entry.outcome,
|
|
101
103
|
};
|
|
102
104
|
if (entry.reason) row.reason = entry.reason;
|
|
105
|
+
if (entry.command) row.command = entry.command;
|
|
103
106
|
return row;
|
|
104
107
|
}
|
|
105
108
|
|