portable-agent-layer 0.71.0 → 0.73.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.
Files changed (104) hide show
  1. package/package.json +10 -7
  2. package/src/cli/migrate.ts +1 -1
  3. package/src/cli/server.ts +7 -0
  4. package/src/cli/skill.ts +1 -1
  5. package/src/hooks/CompactRecover.ts +28 -86
  6. package/src/hooks/LedgerUnapplied.ts +3 -28
  7. package/src/hooks/LoadContext.ts +33 -60
  8. package/src/hooks/SecurityValidator.ts +16 -109
  9. package/src/hooks/handlers/agenda.ts +195 -7
  10. package/src/hooks/handlers/failure-principle.ts +19 -44
  11. package/src/hooks/handlers/session-intelligence.ts +13 -70
  12. package/src/hooks/lib/agenda-store.ts +2 -0
  13. package/src/hooks/lib/capture-store.ts +103 -0
  14. package/src/hooks/lib/compact-recall.ts +89 -0
  15. package/src/hooks/lib/failure-principle.ts +98 -0
  16. package/src/hooks/lib/goal-links.ts +83 -0
  17. package/src/hooks/lib/ledger-hook.ts +35 -0
  18. package/src/hooks/lib/ledger.ts +48 -1
  19. package/src/hooks/lib/models.ts +1 -1
  20. package/src/hooks/lib/projects.ts +5 -0
  21. package/src/hooks/lib/security-gate.ts +159 -0
  22. package/src/hooks/lib/session-context.ts +74 -0
  23. package/src/hooks/lib/settings.ts +2 -0
  24. package/src/hooks/lib/telos-goals.ts +8 -2
  25. package/src/hooks/lib/token-usage.ts +2 -0
  26. package/src/tools/agent/algorithm-reflect.ts +28 -97
  27. package/src/tools/agent/analyze.ts +19 -120
  28. package/src/tools/agent/handoff-note.ts +29 -77
  29. package/src/tools/agent/project.ts +34 -153
  30. package/src/tools/agent/relationship-note.ts +27 -46
  31. package/src/tools/agent/synthesize.ts +1 -1
  32. package/src/tools/agent/thread.ts +43 -123
  33. package/src/tools/control-room/attention.ts +146 -0
  34. package/src/tools/control-room/data.ts +78 -7
  35. package/src/tools/control-room/detail.ts +158 -0
  36. package/src/tools/control-room/matrix.ts +92 -20
  37. package/src/tools/control-room/prefs.ts +96 -0
  38. package/src/tools/control-room/server-config.ts +8 -0
  39. package/src/tools/control-room/server.ts +196 -11
  40. package/src/tools/control-room/snooze.ts +65 -0
  41. package/src/tools/control-room/static.ts +68 -0
  42. package/src/tools/control-room/ui/app.tsx +196 -50
  43. package/src/tools/control-room/ui/attention-bell.tsx +118 -0
  44. package/src/tools/control-room/ui/components/README.md +18 -0
  45. package/src/tools/control-room/ui/components/badge.tsx +38 -0
  46. package/src/tools/control-room/ui/components/button.tsx +43 -0
  47. package/src/tools/control-room/ui/components/card.tsx +45 -0
  48. package/src/tools/control-room/ui/components/input.tsx +24 -0
  49. package/src/tools/control-room/ui/components/label.tsx +18 -0
  50. package/src/tools/control-room/ui/components/popover.tsx +37 -0
  51. package/src/tools/control-room/ui/components/separator.tsx +25 -0
  52. package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
  53. package/src/tools/control-room/ui/components/switch.tsx +27 -0
  54. package/src/tools/control-room/ui/components/table.tsx +60 -0
  55. package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
  56. package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
  57. package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
  58. package/src/tools/control-room/ui/dist/index.html +19 -0
  59. package/src/tools/control-room/ui/format.ts +0 -12
  60. package/src/tools/control-room/ui/frame.tsx +125 -0
  61. package/src/tools/control-room/ui/index.html +2 -2
  62. package/src/tools/control-room/ui/lib/api.ts +27 -0
  63. package/src/tools/control-room/ui/lib/cn.ts +6 -0
  64. package/src/tools/control-room/ui/lib/write.ts +43 -0
  65. package/src/tools/control-room/ui/package.json +28 -0
  66. package/src/tools/control-room/ui/parts.tsx +235 -0
  67. package/src/tools/control-room/ui/screens/log.tsx +274 -0
  68. package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
  69. package/src/tools/control-room/ui/screens/projects.tsx +128 -0
  70. package/src/tools/control-room/ui/screens/settings.tsx +205 -0
  71. package/src/tools/control-room/ui/screens/today.tsx +391 -0
  72. package/src/tools/control-room/ui/theme.css +127 -0
  73. package/src/tools/control-room/ui/vite.config.ts +36 -0
  74. package/src/tools/control-room/writes.ts +106 -0
  75. package/src/tools/ledger/outcomes.ts +9 -0
  76. package/src/tools/ledger/query.ts +2 -0
  77. package/src/tools/ledger/view.ts +34 -10
  78. package/src/tools/lib/algorithm-reflect.ts +84 -0
  79. package/src/tools/lib/analyze-report.ts +120 -0
  80. package/src/tools/lib/handoff-note.ts +102 -0
  81. package/src/tools/lib/note-flags.ts +59 -0
  82. package/src/tools/lib/project-isc.ts +212 -0
  83. package/src/tools/lib/relationship-reflect.ts +402 -0
  84. package/src/tools/lib/self-model.ts +499 -0
  85. package/src/tools/lib/session-usage.ts +216 -0
  86. package/src/tools/lib/skill-doctor.ts +457 -0
  87. package/src/tools/lib/thread.ts +119 -0
  88. package/src/tools/lib/token-report.ts +173 -0
  89. package/src/tools/lib/transcript-usage.ts +42 -0
  90. package/src/tools/lib/usage-buckets.ts +329 -0
  91. package/src/tools/relationship-reflect.ts +48 -412
  92. package/src/tools/self-model.ts +76 -558
  93. package/src/tools/session-summary.ts +8 -215
  94. package/src/tools/skill-doctor.ts +9 -444
  95. package/src/tools/token-cost.ts +18 -428
  96. package/src/tools/control-room/ui/agenda.tsx +0 -43
  97. package/src/tools/control-room/ui/agents.tsx +0 -67
  98. package/src/tools/control-room/ui/app.css +0 -857
  99. package/src/tools/control-room/ui/board.tsx +0 -82
  100. package/src/tools/control-room/ui/handoffs.tsx +0 -37
  101. package/src/tools/control-room/ui/ledger.tsx +0 -136
  102. package/src/tools/control-room/ui/matrix.tsx +0 -117
  103. package/src/tools/control-room/ui/panel.tsx +0 -60
  104. package/src/tools/control-room/ui/signal.tsx +0 -161
@@ -82,6 +82,11 @@ function isScaffolding(line: string): boolean {
82
82
  return !l || l.startsWith("<!--") || l.startsWith("-->") || /^(-{3,}|_{3,})$/.test(l);
83
83
  }
84
84
 
85
+ /** A scaffold's empty bullet is punctuation, not a goal you stated. */
86
+ function hasWords(text: string): boolean {
87
+ return /[a-z0-9]/i.test(text);
88
+ }
89
+
85
90
  /**
86
91
  * Bullets are entries when a block has them; otherwise the paragraph is the
87
92
  * entry. Both shapes ship — the scaffold offers horizon headings with bullets,
@@ -90,10 +95,11 @@ function isScaffolding(line: string): boolean {
90
95
  function entriesIn(lines: string[]): string[] {
91
96
  const bullets = lines
92
97
  .filter((l) => /^\s*[-*+]\s+\S/.test(l))
93
- .map((l) => l.replace(/^\s*[-*+]\s+/, "").trim());
98
+ .map((l) => l.replace(/^\s*[-*+]\s+/, "").trim())
99
+ .filter(hasWords);
94
100
  if (bullets.length > 0) return bullets;
95
101
  const paragraph = lines.join(" ").trim();
96
- return paragraph ? [paragraph] : [];
102
+ return hasWords(paragraph) ? [paragraph] : [];
97
103
  }
98
104
 
99
105
  function goalsFrom(content: string): TelosGoal[] {
@@ -16,6 +16,8 @@ type TokenCaller =
16
16
  | "relationship"
17
17
  | "self-model"
18
18
  | "agenda-serves"
19
+ | "agenda-goal-links"
20
+ | "agenda-waiting-on"
19
21
  | "agenda-moves";
20
22
 
21
23
  interface TokenUsageEntry {
@@ -15,87 +15,36 @@
15
15
 
16
16
  import { appendFileSync } from "node:fs";
17
17
  import { parseArgs } from "node:util";
18
- import { currentAttribution, type RecordAttribution } from "../../hooks/lib/actor";
19
- import { encodeAnchor } from "../../hooks/lib/anchor";
20
18
  import { paths } from "../../hooks/lib/paths";
19
+ import { buildReflection, intOr, reflectionLine } from "../lib/algorithm-reflect";
21
20
  import { emit } from "../lib/emit";
22
21
 
23
- // ── Types ──
22
+ const HELP = `
23
+ AlgorithmReflect — Log algorithm performance after LEARN phase
24
24
 
25
- interface AlgorithmReflection extends RecordAttribution {
26
- timestamp: string;
27
- cwd: string;
28
- task: string;
29
- criteria_count: number;
30
- criteria_passed: number;
31
- criteria_failed: number;
32
- sentiment: number;
33
- q1: string;
34
- q2: string;
35
- q3: string;
36
- /** general = the improvement ideas generalize to the algorithm; task-specific = bound to this task. */
37
- scope: "general" | "task-specific";
38
- }
25
+ Usage:
26
+ bun run tool:algorithm-reflect --task "description" --criteria N --passed N --failed N --sentiment 1-10 \\
27
+ --q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
28
+
29
+ Arguments:
30
+ --task Brief task description
31
+ --criteria Total criteria count
32
+ --passed Criteria passed
33
+ --failed Criteria failed
34
+ --sentiment Implied satisfaction 1-10
35
+ --q1 Q1 — Self: what I'd do differently
36
+ --q2 Q2 — Algorithm: structural improvement
37
+ --q3 Q3 — AI: reasoning blind spot
38
+ --scope general (default) | task-specific — is the algorithm idea reusable or task-bound?
39
39
 
40
- // ── Core ──
40
+ Output: algorithm-reflections.jsonl in memory/learning/reflections/
41
+ `;
41
42
 
42
43
  function reflectionsPath(): string {
43
- paths.reflections(); // ensures the directory exists
44
+ paths.reflections();
44
45
  return paths.reflectionsFile();
45
46
  }
46
47
 
47
- /**
48
- * Assemble a reflection record from CLI-style input, stamping the current
49
- * cwd (anchored) and this machine's id. Exported so the stamping logic is
50
- * directly testable without going through argv parsing.
51
- */
52
- export function buildReflection(input: {
53
- task: string;
54
- q1: string;
55
- q2: string;
56
- q3: string;
57
- criteria_count?: number;
58
- criteria_passed?: number;
59
- criteria_failed?: number;
60
- sentiment?: number;
61
- scope?: string;
62
- }): AlgorithmReflection {
63
- return {
64
- timestamp: new Date().toISOString(),
65
- cwd: encodeAnchor(process.cwd()),
66
- ...currentAttribution(),
67
- task: input.task,
68
- criteria_count: input.criteria_count ?? 0,
69
- criteria_passed: input.criteria_passed ?? 0,
70
- criteria_failed: input.criteria_failed ?? 0,
71
- sentiment: Math.max(1, Math.min(10, input.sentiment ?? 5)),
72
- q1: input.q1,
73
- q2: input.q2,
74
- q3: input.q3,
75
- // Default to general (the ~94% case); only "task-specific" suppresses it
76
- // from algorithm-update clustering.
77
- scope: input.scope === "task-specific" ? "task-specific" : "general",
78
- };
79
- }
80
-
81
- function appendReflection(reflection: AlgorithmReflection): {
82
- success: boolean;
83
- message: string;
84
- path: string;
85
- } {
86
- const p = reflectionsPath();
87
- const line = `${JSON.stringify(reflection)}\n`;
88
- appendFileSync(p, line, "utf-8");
89
-
90
- return {
91
- success: true,
92
- message: `Reflection logged: ${reflection.criteria_passed}/${reflection.criteria_count} passed, sentiment ${reflection.sentiment}/10`,
93
- path: p,
94
- };
95
- }
96
-
97
- // ── CLI ──
98
-
99
48
  function run() {
100
49
  const { values } = parseArgs({
101
50
  args: Bun.argv.slice(2),
@@ -114,26 +63,7 @@ function run() {
114
63
  });
115
64
 
116
65
  if (values.help) {
117
- console.log(`
118
- AlgorithmReflect — Log algorithm performance after LEARN phase
119
-
120
- Usage:
121
- bun run tool:algorithm-reflect --task "description" --criteria N --passed N --failed N --sentiment 1-10 \\
122
- --q1 "self reflection" --q2 "algorithm reflection" --q3 "AI reflection"
123
-
124
- Arguments:
125
- --task Brief task description
126
- --criteria Total criteria count
127
- --passed Criteria passed
128
- --failed Criteria failed
129
- --sentiment Implied satisfaction 1-10
130
- --q1 Q1 — Self: what I'd do differently
131
- --q2 Q2 — Algorithm: structural improvement
132
- --q3 Q3 — AI: reasoning blind spot
133
- --scope general (default) | task-specific — is the algorithm idea reusable or task-bound?
134
-
135
- Output: algorithm-reflections.jsonl in memory/learning/reflections/
136
- `);
66
+ console.log(HELP);
137
67
  process.exit(0);
138
68
  }
139
69
 
@@ -147,15 +77,16 @@ Output: algorithm-reflections.jsonl in memory/learning/reflections/
147
77
  q1: values.q1,
148
78
  q2: values.q2,
149
79
  q3: values.q3,
150
- criteria_count: parseInt(values.criteria || "0", 10),
151
- criteria_passed: parseInt(values.passed || "0", 10),
152
- criteria_failed: parseInt(values.failed || "0", 10),
153
- sentiment: parseInt(values.sentiment || "5", 10),
80
+ criteria_count: intOr(values.criteria, 0),
81
+ criteria_passed: intOr(values.passed, 0),
82
+ criteria_failed: intOr(values.failed, 0),
83
+ sentiment: intOr(values.sentiment, 5),
154
84
  scope: values.scope,
155
85
  });
156
86
 
157
- const result = appendReflection(reflection);
158
- emit.receipt(result.path, {
87
+ const path = reflectionsPath();
88
+ appendFileSync(path, reflectionLine(reflection), "utf-8");
89
+ emit.receipt(path, {
159
90
  passed: reflection.criteria_passed,
160
91
  of: reflection.criteria_count,
161
92
  scope: reflection.scope,
@@ -5,130 +5,17 @@
5
5
  * Reads failures and session learnings, finds recurring patterns,
6
6
  * summarizes ratings, and generates recommendations.
7
7
  *
8
+ * What the report says is in lib/analyze-report.ts.
9
+ *
8
10
  * Usage: bun run tool:analyze
9
11
  */
10
12
 
11
13
  import { parseArgs } from "node:util";
12
14
  import { writeLastAnalyzeDate } from "../../hooks/lib/analyze-nudge";
13
- import { type AnalysisResult, analyze } from "../../hooks/lib/graduation";
14
-
15
- // ── ANSI Colors ──
16
-
17
- const c = {
18
- bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
19
- dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
20
- cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
21
- yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
22
- green: (s: string) => `\x1b[32m${s}\x1b[0m`,
23
- red: (s: string) => `\x1b[31m${s}\x1b[0m`,
24
- magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
25
- };
26
-
27
- function printReport(result: AnalysisResult): void {
28
- const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
29
- const hasRatings = result.ratings !== null;
30
-
31
- if (!hasPatterns && !hasRatings) {
32
- console.log("\n No patterns or ratings data found.\n");
33
- return;
34
- }
35
-
36
- if (result.ratings) {
37
- const r = result.ratings;
38
- const lowOrMid = r.average <= 4 ? c.red : c.yellow;
39
- const avgColor = r.average >= 7 ? c.green : lowOrMid;
40
- const ratingStr = `${r.average.toFixed(1)}/10`;
41
- const lowStr = `Low (≤4): ${r.low.count}`;
42
- const highStr = `High (≥7): ${r.high.count}`;
43
- console.log(
44
- `\n ${c.bold("Ratings:")} ${avgColor(ratingStr)} avg (${r.total} total)`
45
- );
46
- console.log(` ${c.red(lowStr)} | ${c.green(highStr)}`);
47
- }
48
-
49
- if (result.candidates.length > 0) {
50
- const graduationHeader = `Graduation Report — ${result.candidates.length} pattern(s) detected`;
51
- console.log(`\n ${c.bold(c.green(graduationHeader))}\n`);
52
- console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
53
-
54
- for (const candidate of result.candidates) {
55
- const domain = `[${candidate.domain}]`;
56
- const count = `${candidate.entries.length}x`;
57
- console.log(` ${c.cyan(domain)} ${c.bold(count)} occurrences`);
58
- console.log("");
59
-
60
- for (const entry of candidate.entries) {
61
- const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
62
- const tag =
63
- sourceType === "failure"
64
- ? c.red(`[${sourceType}]`)
65
- : c.yellow(`[${sourceType}]`);
66
- console.log(
67
- ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 100)}`
68
- );
69
- }
70
-
71
- console.log(`\n ${c.dim("Files:")}`);
72
- for (const entry of candidate.entries) {
73
- console.log(` ${c.dim(entry.path)}`);
74
- }
75
-
76
- console.log("");
77
- const framePath = `memory/wisdom/frames/${candidate.domain}.md`;
78
- console.log(` Target frame: ${c.magenta(framePath)}`);
79
- console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
80
- }
81
- }
82
-
83
- if (result.emerging.length > 0) {
84
- console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
85
- for (const group of result.emerging) {
86
- const domain = `[${group.domain}]`;
87
- const count = `${group.entries.length}x`;
88
- console.log(` ${c.cyan(domain)} ${c.bold(count)}`);
89
- for (const entry of group.entries) {
90
- const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
91
- const tag =
92
- sourceType === "failure"
93
- ? c.red(`[${sourceType}]`)
94
- : c.yellow(`[${sourceType}]`);
95
- console.log(
96
- ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 80)}`
97
- );
98
- }
99
- console.log(" Files:");
100
- for (const entry of group.entries) {
101
- console.log(` ${c.dim(entry.path)}`);
102
- }
103
- console.log("");
104
- }
105
- }
106
-
107
- if (result.recommendations.length > 0) {
108
- console.log(` ${c.bold("Recommendations:")}\n`);
109
- for (const rec of result.recommendations) {
110
- console.log(` ${rec}`);
111
- }
112
- console.log("");
113
- }
114
-
115
- if (result.candidates.length > 0) {
116
- console.log(` To crystallize: add a line to the wisdom frame file.`);
117
- console.log(` Format: ${c.green("- Your principle here [CRYSTAL: 85%]")}\n`);
118
- }
119
- }
15
+ import { analyze } from "../../hooks/lib/graduation";
16
+ import { reportLines } from "../lib/analyze-report";
120
17
 
121
- export async function run(argv: string[] = Bun.argv.slice(2)) {
122
- const { values } = parseArgs({
123
- args: argv,
124
- options: {
125
- help: { type: "boolean", short: "h" },
126
- actionable: { type: "boolean", short: "a" },
127
- },
128
- });
129
-
130
- if (values.help) {
131
- console.log(`
18
+ const HELP = `
132
19
  PAL Learning Analysis — unified graduation + ratings report
133
20
 
134
21
  Reads all captured failures (rating ≤3) and session learnings,
@@ -147,12 +34,24 @@ export async function run(argv: string[] = Bun.argv.slice(2)) {
147
34
  - Your principle here [CRYSTAL: 85%]
148
35
 
149
36
  Usage: pal cli analyze [--actionable]
150
- `);
37
+ `;
38
+
39
+ export async function run(argv: string[] = Bun.argv.slice(2)) {
40
+ const { values } = parseArgs({
41
+ args: argv,
42
+ options: {
43
+ help: { type: "boolean", short: "h" },
44
+ actionable: { type: "boolean", short: "a" },
45
+ },
46
+ });
47
+
48
+ if (values.help) {
49
+ console.log(HELP);
151
50
  process.exit(0);
152
51
  }
153
52
 
154
53
  const result = await analyze({ actionable: values.actionable });
155
- printReport(result);
54
+ for (const line of reportLines(result)) console.log(line);
156
55
  writeLastAnalyzeDate(new Date().toISOString());
157
56
  }
158
57
 
@@ -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 { existsSync, readFileSync, writeFileSync } from "node:fs";
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
- export interface HandoffEntry {
20
- timestamp: string;
21
- title: string;
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
- /** Returns how many entries survived the trim, which is what the receipt reports. */
45
- function writeHandoffs(handoffs: Record<string, HandoffEntry>): number {
46
- const entries = Object.entries(handoffs);
47
- const trimmed = entries.length > 20 ? Object.fromEntries(entries.slice(-20)) : handoffs;
48
- writeFileSync(handoffPath(), JSON.stringify(trimmed, null, 2), "utf-8");
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
- interface NoteInput {
53
- cwd: string;
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 writeHandoffNote(note: NoteInput): {
61
- file: string;
62
- status: HandoffEntry["status"];
63
- kept: number;
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
- const result = writeHandoffNote({
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
- const result = writeHandoffNote({
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();