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.
Files changed (81) hide show
  1. package/README.md +5 -1
  2. package/assets/schema/pal-settings.schema.json +4 -0
  3. package/assets/skills/onboarding/SKILL.md +109 -0
  4. package/assets/skills/projects/SKILL.md +11 -2
  5. package/assets/templates/pal-settings.json +1 -0
  6. package/package.json +5 -1
  7. package/src/cli/index.ts +39 -12
  8. package/src/cli/migrate.ts +1 -1
  9. package/src/cli/personal-context.ts +67 -0
  10. package/src/cli/server.ts +13 -7
  11. package/src/cli/setup-identity.ts +13 -1
  12. package/src/cli/skill.ts +1 -1
  13. package/src/hooks/CompactRecover.ts +28 -86
  14. package/src/hooks/LedgerUnapplied.ts +3 -28
  15. package/src/hooks/LoadContext.ts +33 -60
  16. package/src/hooks/SecurityValidator.ts +16 -109
  17. package/src/hooks/handlers/agenda.ts +223 -0
  18. package/src/hooks/handlers/failure-principle.ts +19 -44
  19. package/src/hooks/handlers/inject-retrieval.ts +6 -2
  20. package/src/hooks/handlers/session-intelligence.ts +13 -70
  21. package/src/hooks/lib/agenda-store.ts +41 -0
  22. package/src/hooks/lib/capture-store.ts +103 -0
  23. package/src/hooks/lib/compact-recall.ts +89 -0
  24. package/src/hooks/lib/failure-principle.ts +98 -0
  25. package/src/hooks/lib/ledger-hook.ts +35 -0
  26. package/src/hooks/lib/ledger.ts +48 -1
  27. package/src/hooks/lib/paths.ts +0 -1
  28. package/src/hooks/lib/projects.ts +16 -1
  29. package/src/hooks/lib/security-gate.ts +159 -0
  30. package/src/hooks/lib/serves.ts +60 -0
  31. package/src/hooks/lib/session-context.ts +74 -0
  32. package/src/hooks/lib/stop.ts +14 -0
  33. package/src/hooks/lib/telos-goals.ts +144 -0
  34. package/src/hooks/lib/telos-topics.ts +68 -0
  35. package/src/hooks/lib/token-usage.ts +3 -1
  36. package/src/hooks/lib/wall-clock.ts +58 -0
  37. package/src/tools/agent/algorithm-reflect.ts +28 -97
  38. package/src/tools/agent/analyze.ts +19 -120
  39. package/src/tools/agent/handoff-note.ts +40 -70
  40. package/src/tools/agent/project.ts +47 -136
  41. package/src/tools/agent/relationship-note.ts +27 -46
  42. package/src/tools/agent/synthesize.ts +1 -1
  43. package/src/tools/agent/thread.ts +43 -123
  44. package/src/tools/control-room/data.ts +332 -0
  45. package/src/tools/control-room/matrix.ts +182 -0
  46. package/src/tools/control-room/server.ts +150 -0
  47. package/src/tools/control-room/ui/agenda.tsx +43 -0
  48. package/src/tools/control-room/ui/agents.tsx +67 -0
  49. package/src/tools/control-room/ui/app.css +857 -0
  50. package/src/tools/control-room/ui/app.tsx +74 -0
  51. package/src/tools/control-room/ui/board.tsx +82 -0
  52. package/src/tools/control-room/ui/format.ts +31 -0
  53. package/src/tools/control-room/ui/handoffs.tsx +37 -0
  54. package/src/tools/control-room/ui/index.html +19 -0
  55. package/src/tools/control-room/ui/ledger.tsx +137 -0
  56. package/src/tools/control-room/ui/matrix.tsx +117 -0
  57. package/src/tools/control-room/ui/panel.tsx +60 -0
  58. package/src/tools/control-room/ui/signal.tsx +161 -0
  59. package/src/tools/ledger/view.ts +3 -0
  60. package/src/tools/lib/algorithm-reflect.ts +84 -0
  61. package/src/tools/lib/analyze-report.ts +120 -0
  62. package/src/tools/lib/handoff-note.ts +88 -0
  63. package/src/tools/lib/note-flags.ts +59 -0
  64. package/src/tools/lib/project-isc.ts +151 -0
  65. package/src/tools/lib/relationship-reflect.ts +402 -0
  66. package/src/tools/lib/self-model.ts +499 -0
  67. package/src/tools/lib/session-usage.ts +216 -0
  68. package/src/tools/lib/skill-doctor.ts +457 -0
  69. package/src/tools/lib/thread.ts +119 -0
  70. package/src/tools/lib/token-report.ts +173 -0
  71. package/src/tools/lib/transcript-usage.ts +42 -0
  72. package/src/tools/lib/usage-buckets.ts +329 -0
  73. package/src/tools/relationship-reflect.ts +48 -412
  74. package/src/tools/self-model.ts +76 -558
  75. package/src/tools/session-summary.ts +8 -215
  76. package/src/tools/skill-doctor.ts +9 -444
  77. package/src/tools/token-cost.ts +18 -428
  78. package/assets/templates/ledger-page.html +0 -213
  79. package/src/cli/setup-telos.ts +0 -52
  80. package/src/hooks/lib/setup.ts +0 -60
  81. package/src/tools/ledger/server.ts +0 -111
@@ -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,60 +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";
18
23
 
19
- interface HandoffEntry {
20
- timestamp: string;
21
- title: string;
22
- status: "in-progress" | "completed";
23
- handoff: string;
24
- artifacts: string[];
25
- source: "deliberate" | "auto";
26
- }
24
+ const HELP = `
25
+ HandoffNote — Write a handoff note for the current project
27
26
 
28
- function handoffPath(): string {
29
- return resolve(ensureDir(paths.state()), "last-handoff.json");
30
- }
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
31
30
 
32
- function readHandoffs(): Record<string, HandoffEntry> {
33
- const p = handoffPath();
34
- if (!existsSync(p)) return {};
35
- try {
36
- return JSON.parse(readFileSync(p, "utf-8"));
37
- } catch {
38
- return {};
39
- }
40
- }
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
41
36
 
42
- /** Returns how many entries survived the trim, which is what the receipt reports. */
43
- function writeHandoffs(handoffs: Record<string, HandoffEntry>): number {
44
- const entries = Object.entries(handoffs);
45
- const trimmed = entries.length > 20 ? Object.fromEntries(entries.slice(-20)) : handoffs;
46
- writeFileSync(handoffPath(), JSON.stringify(trimmed, null, 2), "utf-8");
47
- return Object.keys(trimmed).length;
48
- }
37
+ Output: writes to memory/state/last-handoff.json keyed by cwd
38
+ `;
49
39
 
50
- function writeHandoffNote(
51
- cwd: string,
52
- title: string,
53
- text: string,
54
- done: boolean
55
- ): { file: string; status: HandoffEntry["status"]; kept: number } {
56
- const handoffs = readHandoffs();
57
- handoffs[cwd] = {
58
- timestamp: new Date().toISOString(),
59
- title,
60
- status: done ? "completed" : "in-progress",
61
- handoff: text,
62
- artifacts: [],
63
- source: "deliberate",
64
- };
65
- const kept = writeHandoffs(handoffs);
66
- return { file: handoffPath(), status: handoffs[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 });
67
45
  }
68
46
 
69
47
  function run() {
@@ -72,37 +50,24 @@ function run() {
72
50
  options: {
73
51
  title: { type: "string" },
74
52
  text: { type: "string" },
53
+ waiting: { type: "string" },
75
54
  done: { type: "boolean" },
76
55
  help: { type: "boolean", short: "h" },
77
56
  },
78
57
  });
79
58
 
80
59
  if (values.help) {
81
- console.log(`
82
- HandoffNote — Write a handoff note for the current project
83
-
84
- Usage:
85
- bun ~/.pal/tools/handoff-note.ts --title "what we were doing" --text "what remains"
86
- bun ~/.pal/tools/handoff-note.ts --done # mark session completed
87
-
88
- Arguments:
89
- --title Brief title of what was being worked on (5-10 words)
90
- --text What remains unfinished — decisions made, next steps, blockers
91
- --done Mark as completed; suppresses "pick up where you left off" injection
92
-
93
- Output: writes to memory/state/last-handoff.json keyed by cwd
94
- `);
60
+ console.log(HELP);
95
61
  process.exit(0);
96
62
  }
97
63
 
98
64
  if (values.done) {
99
- const result = writeHandoffNote(
100
- process.cwd(),
101
- values.title || "session",
102
- values.text || "",
103
- true
104
- );
105
- emit.receipt(result.file, { status: result.status, entries: result.kept });
65
+ saveNote({
66
+ cwd: process.cwd(),
67
+ title: values.title || "session",
68
+ text: values.text || "",
69
+ done: true,
70
+ });
106
71
  process.exit(0);
107
72
  }
108
73
 
@@ -111,8 +76,13 @@ Output: writes to memory/state/last-handoff.json keyed by cwd
111
76
  process.exit(1);
112
77
  }
113
78
 
114
- const result = writeHandoffNote(process.cwd(), values.title, values.text, false);
115
- emit.receipt(result.file, { status: result.status, entries: result.kept });
79
+ saveNote({
80
+ cwd: process.cwd(),
81
+ title: values.title,
82
+ text: values.text,
83
+ done: false,
84
+ waitingOn: values.waiting,
85
+ });
116
86
  }
117
87
 
118
88
  if (import.meta.main) run();