portable-agent-layer 0.51.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/assets/skills/analyze-pdf/SKILL.md +12 -0
- package/assets/skills/analyze-pdf/tools/pdf-read.ts +35 -0
- package/assets/skills/pal-analyze/SKILL.md +54 -0
- package/assets/skills/pal-reflect/SKILL.md +52 -0
- package/assets/statusline.ps1 +2 -2
- package/assets/statusline.sh +2 -2
- package/assets/templates/PAL/ALGORITHM.md +35 -6
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +2 -3
- package/assets/templates/pal-settings.json +0 -3
- package/package.json +7 -4
- package/src/cli/index.ts +6 -0
- package/src/hooks/LoadContext.ts +2 -1
- package/src/hooks/UserPromptOrchestrator.ts +5 -3
- package/src/hooks/handlers/failure-principle.ts +1 -7
- package/src/hooks/handlers/inject-retrieval.ts +5 -3
- package/src/hooks/handlers/rating.ts +14 -44
- package/src/hooks/handlers/reflect-trigger.ts +15 -0
- package/src/hooks/lib/analyze-nudge.ts +47 -0
- package/src/hooks/lib/context.ts +7 -18
- package/src/hooks/lib/log.ts +34 -2
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/projects.ts +0 -11
- package/src/hooks/lib/security.ts +1 -7
- package/src/hooks/lib/semi-static.ts +1 -62
- package/src/targets/lib.ts +41 -16
- package/src/targets/opencode/plugin.ts +7 -5
- package/src/tools/agent/analyze.ts +6 -4
- package/src/tools/agent/relationship-note.ts +56 -12
- package/src/tools/agent/synthesize.ts +40 -0
- package/src/tools/self-model.ts +18 -37
- package/src/hooks/lib/signal-trends.ts +0 -117
package/src/hooks/lib/context.ts
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { loadReflectNudge } from "../handlers/reflect-trigger";
|
|
9
10
|
import { loadAlgorithmReviewNudge } from "./algorithm-review";
|
|
11
|
+
import { loadAnalyzeNudge } from "./analyze-nudge";
|
|
10
12
|
import { readLearnings } from "./learning-store";
|
|
11
13
|
import { loadOpinionContext } from "./opinions";
|
|
12
14
|
import { paths } from "./paths";
|
|
13
15
|
import { loadActiveProjectsContext } from "./projects";
|
|
14
16
|
import { loadRecentNotes } from "./relationship";
|
|
15
|
-
import { loadFailurePatterns
|
|
17
|
+
import { loadFailurePatterns } from "./semi-static";
|
|
16
18
|
import * as settings from "./settings";
|
|
17
|
-
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
18
19
|
import { readFramePrinciples } from "./wisdom";
|
|
19
20
|
import { readProjectHistory } from "./work-tracking";
|
|
20
21
|
|
|
@@ -87,15 +88,6 @@ function loadSelfModel(): string {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/** Load signal trends as a formatted string */
|
|
91
|
-
export function loadSignalTrends(): string {
|
|
92
|
-
try {
|
|
93
|
-
return formatTrends(computeSignalTrends());
|
|
94
|
-
} catch {
|
|
95
|
-
return "";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
91
|
/** Load per-project session history for the current working directory */
|
|
100
92
|
function loadProjectHistoryContext(): string {
|
|
101
93
|
try {
|
|
@@ -298,13 +290,8 @@ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string
|
|
|
298
290
|
const activeProjects = settings.isEnabled("projects")
|
|
299
291
|
? loadActiveProjectsContext()
|
|
300
292
|
: "";
|
|
301
|
-
const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
|
|
302
293
|
const failures =
|
|
303
294
|
settings.isEnabled("failurePatterns") && !skipSemiStatic ? loadFailurePatterns() : "";
|
|
304
|
-
const synthesis =
|
|
305
|
-
settings.isEnabled("synthesis") && !skipSemiStatic
|
|
306
|
-
? loadSynthesisRecommendations()
|
|
307
|
-
: "";
|
|
308
295
|
const opinions =
|
|
309
296
|
!skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
310
297
|
const selfModel =
|
|
@@ -315,10 +302,14 @@ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string
|
|
|
315
302
|
const handoff = settings.isEnabled("handoff") ? loadHandoff() : "";
|
|
316
303
|
// Maintainer-only: self-gates to a repo checkout, "" for everyone else.
|
|
317
304
|
const algoReview = loadAlgorithmReviewNudge();
|
|
305
|
+
const reflectNudge = loadReflectNudge();
|
|
306
|
+
const analyzeNudge = loadAnalyzeNudge();
|
|
318
307
|
const parts: string[] = [];
|
|
319
308
|
if (startup) parts.push(startup);
|
|
320
309
|
if (handoff) parts.push(handoff);
|
|
321
310
|
if (algoReview) parts.push(algoReview);
|
|
311
|
+
if (reflectNudge) parts.push(reflectNudge);
|
|
312
|
+
if (analyzeNudge) parts.push(analyzeNudge);
|
|
322
313
|
if (selfModel) parts.push(selfModel);
|
|
323
314
|
if (wisdom) parts.push(wisdom);
|
|
324
315
|
if (opinions) parts.push(opinions);
|
|
@@ -327,8 +318,6 @@ export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string
|
|
|
327
318
|
if (activeProjects) parts.push(activeProjects);
|
|
328
319
|
if (projectHistory) parts.push(projectHistory);
|
|
329
320
|
if (digest) parts.push(digest);
|
|
330
|
-
if (synthesis) parts.push(synthesis);
|
|
331
|
-
if (trends) parts.push(trends);
|
|
332
321
|
if (failures) parts.push(failures);
|
|
333
322
|
if (parts.length === 0) return "";
|
|
334
323
|
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Only writes when debug is enabled (`pal cli debug on`) or when called via logError (always logged).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
appendFileSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
9
16
|
import { resolve } from "node:path";
|
|
10
17
|
import { palHome, paths } from "./paths";
|
|
11
18
|
|
|
@@ -14,7 +21,7 @@ const MAX_ROTATED = 5; // keep up to 5 rotated files (.1 newest → .5 oldest)
|
|
|
14
21
|
|
|
15
22
|
/** Resolved lazily so PAL_HOME overrides at runtime are honored. */
|
|
16
23
|
function logFile(): string {
|
|
17
|
-
return resolve(paths.
|
|
24
|
+
return resolve(paths.debug(), "debug.log");
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
function timestamp(): string {
|
|
@@ -90,6 +97,31 @@ export function logDebug(source: string, message: string): void {
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/** Write last user prompt + retrieval injection to last-prompt.md (only when debug is enabled) */
|
|
101
|
+
export function logPromptSnapshot(prompt: string, retrieval: string | null): void {
|
|
102
|
+
if (!isDebugEnabled()) return;
|
|
103
|
+
const path = resolve(paths.debug(), "last-prompt.md");
|
|
104
|
+
const content = retrieval
|
|
105
|
+
? `## Prompt\n\n${prompt}\n\n## Retrieval Injection\n\n${retrieval}`
|
|
106
|
+
: `## Prompt\n\n${prompt}`;
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(path, content, "utf-8");
|
|
109
|
+
} catch {
|
|
110
|
+
/* non-critical */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Write full context snapshot to context-snapshot.md (only when debug is enabled) */
|
|
115
|
+
export function logContextSnapshot(content: string): void {
|
|
116
|
+
if (!isDebugEnabled()) return;
|
|
117
|
+
const path = resolve(paths.debug(), "context-snapshot.md");
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(path, content, "utf-8");
|
|
120
|
+
} catch {
|
|
121
|
+
/* non-critical */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
/** Log an error (always written, regardless of debug mode) */
|
|
94
126
|
export function logError(source: string, error: unknown): void {
|
|
95
127
|
const path = logFile();
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -60,6 +60,7 @@ export const paths = {
|
|
|
60
60
|
synthesis: () => ensureDir(home("memory", "learning", "synthesis")),
|
|
61
61
|
work: () => ensureDir(home("memory", "work")),
|
|
62
62
|
backups: () => ensureDir(home("backups")),
|
|
63
|
+
debug: () => ensureDir(home("debug")),
|
|
63
64
|
} as const;
|
|
64
65
|
|
|
65
66
|
// Platform directories (env override or cross-platform defaults)
|
|
@@ -358,17 +358,6 @@ export function loadActiveProjectsContext(cwd: string = process.cwd()): string {
|
|
|
358
358
|
if (p.blockers?.length) {
|
|
359
359
|
lines.push(` Blockers: ${p.blockers.slice(0, MAX_INLINE_BULLETS).join("; ")}`);
|
|
360
360
|
}
|
|
361
|
-
if (p.criteria) {
|
|
362
|
-
const openIscs = p.criteria
|
|
363
|
-
.split("\n")
|
|
364
|
-
.filter((l) => /^-\s+\[ \]\s+ISC-\d+:/i.test(l))
|
|
365
|
-
.map((l) => l.replace(/^-\s+\[ \]\s+/, "").trim());
|
|
366
|
-
if (openIscs.length > 0) {
|
|
367
|
-
lines.push(
|
|
368
|
-
` Open ISCs (${openIscs.length}): ${openIscs.slice(0, MAX_INLINE_BULLETS).join("; ")}`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
361
|
} else {
|
|
373
362
|
const counts: string[] = [];
|
|
374
363
|
if (p.next?.length) counts.push(`${p.next.length} next`);
|
|
@@ -24,19 +24,12 @@ const HOOK_MANAGED_FILES = [
|
|
|
24
24
|
"captured-learnings.json",
|
|
25
25
|
"counts.json",
|
|
26
26
|
"session-names.json",
|
|
27
|
-
"debug.log",
|
|
28
27
|
"last-responses.json",
|
|
29
28
|
"signal-cache.json",
|
|
30
29
|
"pending-failure.json",
|
|
31
30
|
"token-usage.jsonl",
|
|
32
31
|
"graduated.json",
|
|
33
32
|
"update-available.json",
|
|
34
|
-
"debug.log.prev",
|
|
35
|
-
"debug.log.1",
|
|
36
|
-
"debug.log.2",
|
|
37
|
-
"debug.log.3",
|
|
38
|
-
"debug.log.4",
|
|
39
|
-
"debug.log.5",
|
|
40
33
|
"opinions.json",
|
|
41
34
|
"pal-settings.json",
|
|
42
35
|
"skill-index.json",
|
|
@@ -54,6 +47,7 @@ const HOOK_MANAGED_DIRS = [
|
|
|
54
47
|
"memory/wisdom/state",
|
|
55
48
|
"memory/projects",
|
|
56
49
|
"memory/state/progress",
|
|
50
|
+
"debug",
|
|
57
51
|
];
|
|
58
52
|
|
|
59
53
|
/** Escape a string for use in a RegExp */
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* and the session-stop digest writer.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync,
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import { parse } from "./frontmatter";
|
|
12
11
|
import { type FailureEntry, readFailures } from "./learning-store";
|
|
13
12
|
import { loadOpinionContext } from "./opinions";
|
|
14
13
|
import { palHome, paths } from "./paths";
|
|
@@ -47,59 +46,6 @@ function readFileSafe(path: string): string {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
/** Build recommendations from the most recent synthesis report. */
|
|
51
|
-
export function loadSynthesisRecommendations(): string {
|
|
52
|
-
try {
|
|
53
|
-
const synthDir = paths.synthesis();
|
|
54
|
-
if (!existsSync(synthDir)) return "";
|
|
55
|
-
|
|
56
|
-
const months = readdirSync(synthDir).sort().reverse();
|
|
57
|
-
for (const month of months) {
|
|
58
|
-
const monthDir = resolve(synthDir, month);
|
|
59
|
-
try {
|
|
60
|
-
const files = readdirSync(monthDir)
|
|
61
|
-
.filter((f) => f.endsWith(".md"))
|
|
62
|
-
.sort()
|
|
63
|
-
.reverse();
|
|
64
|
-
if (files.length === 0) continue;
|
|
65
|
-
|
|
66
|
-
const content = readFileSync(resolve(monthDir, files[0]), "utf-8");
|
|
67
|
-
|
|
68
|
-
const recMatch = new RegExp(
|
|
69
|
-
/## Recommendations\n\n([\s\S]*?)(?:\n##|\n$|$)/
|
|
70
|
-
).exec(content);
|
|
71
|
-
if (!recMatch?.[1]?.trim()) continue;
|
|
72
|
-
|
|
73
|
-
const recs = recMatch[1]
|
|
74
|
-
.trim()
|
|
75
|
-
.split("\n")
|
|
76
|
-
.filter((l) => l.trim())
|
|
77
|
-
.slice(0, 4);
|
|
78
|
-
|
|
79
|
-
if (recs.length === 0) continue;
|
|
80
|
-
|
|
81
|
-
const { meta } = parse<{ period?: string; average_rating?: string }>(content);
|
|
82
|
-
const period = meta.period ?? "";
|
|
83
|
-
const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
|
|
84
|
-
|
|
85
|
-
const header = [
|
|
86
|
-
"## Pattern Synthesis",
|
|
87
|
-
period ? `*${period} — ${avgRating}*` : "",
|
|
88
|
-
]
|
|
89
|
-
.filter(Boolean)
|
|
90
|
-
.join("\n");
|
|
91
|
-
|
|
92
|
-
return [header, ...recs].join("\n");
|
|
93
|
-
} catch {
|
|
94
|
-
/* try next month */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return "";
|
|
98
|
-
} catch {
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
49
|
/**
|
|
104
50
|
* Rank failures for injection: same-project (cwd match) first, then by recency.
|
|
105
51
|
* Project-relevant lessons are never crowded out by more-recent other-project
|
|
@@ -186,13 +132,6 @@ export function getSemiStaticSources(): SemiStaticSource[] {
|
|
|
186
132
|
slug: "opinions",
|
|
187
133
|
description: "PAL opinions",
|
|
188
134
|
},
|
|
189
|
-
{
|
|
190
|
-
path: resolve(memory, "learning", "synthesis-digest.md"),
|
|
191
|
-
writesDigest: true,
|
|
192
|
-
load: loadSynthesisRecommendations,
|
|
193
|
-
slug: "synthesis",
|
|
194
|
-
description: "PAL pattern synthesis",
|
|
195
|
-
},
|
|
196
135
|
{
|
|
197
136
|
path: resolve(memory, "learning", "failures-digest.md"),
|
|
198
137
|
writesDigest: true,
|
package/src/targets/lib.ts
CHANGED
|
@@ -98,17 +98,34 @@ export function loadSettingsTemplate(templatePath: string, pkgRoot: string): Set
|
|
|
98
98
|
export function mergeSettings(existing: Settings, template: Settings): Settings {
|
|
99
99
|
const result = { ...existing };
|
|
100
100
|
|
|
101
|
-
// Merge hooks
|
|
101
|
+
// Merge hooks — strip old-path PAL entries first, then insert current template entries.
|
|
102
|
+
// This handles reinstalling from a different path (repo vs global install) without
|
|
103
|
+
// leaving orphan hook entries that fire twice per event.
|
|
102
104
|
if (template.hooks) {
|
|
103
105
|
result.hooks ??= {};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
|
|
107
|
+
// Collect canonical forms of all template hook commands
|
|
108
|
+
const palCanonical = new Set<string>();
|
|
109
|
+
for (const entries of Object.values(template.hooks)) {
|
|
106
110
|
for (const entry of entries) {
|
|
107
111
|
const cmd = entry.hooks?.[0]?.command;
|
|
108
|
-
if (cmd
|
|
109
|
-
current.push(entry);
|
|
110
|
-
}
|
|
112
|
+
if (cmd) palCanonical.add(canonicalPalCmd(cmd));
|
|
111
113
|
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Strip existing PAL hooks that match canonically (removes old-path duplicates)
|
|
117
|
+
for (const [event, entries] of Object.entries(result.hooks)) {
|
|
118
|
+
result.hooks[event] = entries.filter((e) => {
|
|
119
|
+
const cmd = e.hooks?.[0]?.command;
|
|
120
|
+
return !cmd || !palCanonical.has(canonicalPalCmd(cmd));
|
|
121
|
+
});
|
|
122
|
+
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Insert template entries (old-path versions were just stripped)
|
|
126
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
127
|
+
const current = result.hooks[event] ?? [];
|
|
128
|
+
for (const entry of entries) current.push(entry);
|
|
112
129
|
result.hooks[event] = current;
|
|
113
130
|
}
|
|
114
131
|
}
|
|
@@ -195,20 +212,20 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
195
212
|
export function unmergeSettings(existing: Settings, template: Settings): Settings {
|
|
196
213
|
const result = { ...existing };
|
|
197
214
|
|
|
198
|
-
// Collect
|
|
215
|
+
// Collect canonical PAL hook commands from template (path-normalized)
|
|
199
216
|
if (template.hooks && result.hooks) {
|
|
200
|
-
const
|
|
217
|
+
const palCanonical = new Set<string>();
|
|
201
218
|
for (const entries of Object.values(template.hooks)) {
|
|
202
219
|
for (const entry of entries) {
|
|
203
220
|
const cmd = entry.hooks?.[0]?.command;
|
|
204
|
-
if (cmd)
|
|
221
|
+
if (cmd) palCanonical.add(canonicalPalCmd(cmd));
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
for (const [event, entries] of Object.entries(result.hooks)) {
|
|
209
226
|
result.hooks[event] = entries.filter((e) => {
|
|
210
227
|
const cmd = e.hooks?.[0]?.command;
|
|
211
|
-
return !cmd || !
|
|
228
|
+
return !cmd || !palCanonical.has(canonicalPalCmd(cmd));
|
|
212
229
|
});
|
|
213
230
|
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
214
231
|
}
|
|
@@ -377,9 +394,17 @@ type CodexHookCommand = { type: string; command: string; timeout?: number };
|
|
|
377
394
|
type CodexHookGroup = { matcher?: string; hooks: CodexHookCommand[] };
|
|
378
395
|
type CodexHooks = { hooks?: Record<string, CodexHookGroup[]> };
|
|
379
396
|
|
|
380
|
-
/**
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Normalize a PAL hook command for cross-path deduplication.
|
|
399
|
+
* Strips env-var prefix and normalizes the hook file path so reinstalling from
|
|
400
|
+
* a different location (repo vs global install) correctly strips old entries.
|
|
401
|
+
* "PAL_AGENT=x bun run /any/path/src/hooks/Foo.ts" → "bun run src/hooks/Foo.ts"
|
|
402
|
+
*/
|
|
403
|
+
function canonicalPalCmd(cmd: string): string {
|
|
404
|
+
const withoutEnv = cmd.replace(/^(?:\w+=\S+\s+)+/, "");
|
|
405
|
+
const hookMatch = /bun\s+run\s+.+\/src\/hooks\/(\S+)/.exec(withoutEnv);
|
|
406
|
+
if (hookMatch) return `bun run src/hooks/${hookMatch[1]}`;
|
|
407
|
+
return withoutEnv;
|
|
383
408
|
}
|
|
384
409
|
|
|
385
410
|
export function loadCodexHooksTemplate(
|
|
@@ -399,7 +424,7 @@ export function loadCodexHooksTemplate(
|
|
|
399
424
|
function collectPalCanonical(template: CodexHooks): Set<string> {
|
|
400
425
|
return new Set(
|
|
401
426
|
Object.values(template.hooks ?? {}).flatMap((groups) =>
|
|
402
|
-
groups.flatMap((g) => g.hooks.map((h) =>
|
|
427
|
+
groups.flatMap((g) => g.hooks.map((h) => canonicalPalCmd(h.command)))
|
|
403
428
|
)
|
|
404
429
|
);
|
|
405
430
|
}
|
|
@@ -413,11 +438,11 @@ function stripPalHooks(
|
|
|
413
438
|
hooks[event] = (hooks[event] ?? [])
|
|
414
439
|
.map((g) => {
|
|
415
440
|
const flat = g as unknown as CodexHookCommand;
|
|
416
|
-
if (!g.hooks && flat.command && palCanonical.has(
|
|
441
|
+
if (!g.hooks && flat.command && palCanonical.has(canonicalPalCmd(flat.command))) {
|
|
417
442
|
return null;
|
|
418
443
|
}
|
|
419
444
|
const filtered = (g.hooks ?? []).filter(
|
|
420
|
-
(h) => !palCanonical.has(
|
|
445
|
+
(h) => !palCanonical.has(canonicalPalCmd(h.command))
|
|
421
446
|
);
|
|
422
447
|
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
423
448
|
})
|
|
@@ -30,7 +30,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
30
30
|
await lib<typeof import("../../hooks/lib/context")>("context.ts");
|
|
31
31
|
const { checkBashCommand, checkFilePath } =
|
|
32
32
|
await lib<typeof import("../../hooks/lib/security")>("security.ts");
|
|
33
|
-
const { logDebug, logError } =
|
|
33
|
+
const { logDebug, logError, logPromptSnapshot } =
|
|
34
34
|
await lib<typeof import("../../hooks/lib/log")>("log.ts");
|
|
35
35
|
|
|
36
36
|
// Load shared handlers
|
|
@@ -139,19 +139,21 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
139
139
|
const text = partsToText(output.parts ?? []);
|
|
140
140
|
if (!text.trim()) return;
|
|
141
141
|
|
|
142
|
-
const
|
|
142
|
+
const retrieval = await getRetrievalReminder(text);
|
|
143
|
+
logPromptSnapshot(text, retrieval);
|
|
144
|
+
|
|
145
|
+
await Promise.allSettled([
|
|
143
146
|
captureRating(text, input.sessionID),
|
|
144
147
|
captureSessionName(text, input.sessionID),
|
|
145
|
-
getRetrievalReminder(text),
|
|
146
148
|
]);
|
|
147
149
|
|
|
148
|
-
if (
|
|
150
|
+
if (retrieval) {
|
|
149
151
|
const injected = {
|
|
150
152
|
id: `pal-retrieval-${Date.now()}`,
|
|
151
153
|
sessionID: input.sessionID,
|
|
152
154
|
messageID: input.messageID ?? `pal-msg-${Date.now()}`,
|
|
153
155
|
type: "text" as const,
|
|
154
|
-
text:
|
|
156
|
+
text: retrieval,
|
|
155
157
|
synthetic: true,
|
|
156
158
|
};
|
|
157
159
|
output.parts = [injected, ...(output.parts ?? [])];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
|
+
import { writeLastAnalyzeDate } from "../../hooks/lib/analyze-nudge";
|
|
12
13
|
import { type AnalysisResult, analyze } from "../../hooks/lib/graduation";
|
|
13
14
|
|
|
14
15
|
// ── ANSI Colors ──
|
|
@@ -23,7 +24,7 @@ const c = {
|
|
|
23
24
|
magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
function printReport(result: AnalysisResult): void {
|
|
27
28
|
const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
|
|
28
29
|
const hasRatings = result.ratings !== null;
|
|
29
30
|
|
|
@@ -117,9 +118,9 @@ export function printReport(result: AnalysisResult): void {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
async function run() {
|
|
121
|
+
export async function run(argv: string[] = Bun.argv.slice(2)) {
|
|
121
122
|
const { values } = parseArgs({
|
|
122
|
-
args:
|
|
123
|
+
args: argv,
|
|
123
124
|
options: {
|
|
124
125
|
help: { type: "boolean", short: "h" },
|
|
125
126
|
actionable: { type: "boolean", short: "a" },
|
|
@@ -145,13 +146,14 @@ async function run() {
|
|
|
145
146
|
To crystallize a graduated pattern, add it to the target wisdom frame:
|
|
146
147
|
- Your principle here [CRYSTAL: 85%]
|
|
147
148
|
|
|
148
|
-
Usage:
|
|
149
|
+
Usage: pal cli analyze [--actionable]
|
|
149
150
|
`);
|
|
150
151
|
process.exit(0);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
const result = await analyze({ actionable: values.actionable });
|
|
154
155
|
printReport(result);
|
|
156
|
+
writeLastAnalyzeDate(new Date().toISOString());
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
if (import.meta.main) await run();
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* RelationshipNote — Write
|
|
3
|
+
* RelationshipNote — Write W/O/Session entries to today's relationship log.
|
|
4
4
|
*
|
|
5
|
-
* Called in the ALGORITHM LEARN phase.
|
|
6
|
-
*
|
|
5
|
+
* Called in the ALGORITHM LEARN phase. Writes behavioral observations about
|
|
6
|
+
* the user (O, W) and session diary entries (--b).
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* bun ~/.pal/tools/relationship-note.ts --
|
|
9
|
+
* bun ~/.pal/tools/relationship-note.ts --o "Rico prefers X" --confidence 0.80
|
|
10
|
+
* bun ~/.pal/tools/relationship-note.ts --w "Rico is building X in TypeScript"
|
|
11
|
+
* bun ~/.pal/tools/relationship-note.ts --b "Debugged the cache split logic"
|
|
12
|
+
*
|
|
13
|
+
* Note types:
|
|
14
|
+
* --o Opinion/behavioral observation about the user (requires --confidence)
|
|
15
|
+
* --w World fact about the user's situation (objective, observable)
|
|
16
|
+
* --b Session diary — what Jarvis did this session (first-person, specific)
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import { parseArgs } from "node:util";
|
|
@@ -16,35 +23,72 @@ function run() {
|
|
|
16
23
|
const { values } = parseArgs({
|
|
17
24
|
args: Bun.argv.slice(2),
|
|
18
25
|
options: {
|
|
26
|
+
o: { type: "string", multiple: true },
|
|
27
|
+
w: { type: "string", multiple: true },
|
|
19
28
|
b: { type: "string" },
|
|
29
|
+
confidence: { type: "string" },
|
|
20
30
|
help: { type: "boolean", short: "h" },
|
|
21
31
|
},
|
|
22
32
|
});
|
|
23
33
|
|
|
24
34
|
if (values.help) {
|
|
25
35
|
console.log(`
|
|
26
|
-
RelationshipNote — Append
|
|
36
|
+
RelationshipNote — Append W/O/Session entries to today's relationship log
|
|
27
37
|
|
|
28
38
|
Usage:
|
|
29
|
-
bun ~/.pal/tools/relationship-note.ts --
|
|
39
|
+
bun ~/.pal/tools/relationship-note.ts --o "Rico prefers X" --confidence 0.80
|
|
40
|
+
bun ~/.pal/tools/relationship-note.ts --w "Rico is building X in TypeScript"
|
|
41
|
+
bun ~/.pal/tools/relationship-note.ts --b "Debugged the cache split logic"
|
|
42
|
+
|
|
43
|
+
Flags:
|
|
44
|
+
--o TEXT Opinion/behavioral observation about the user
|
|
45
|
+
--confidence N Confidence for --o (0.0–1.0, default 0.75)
|
|
46
|
+
--w TEXT World fact about the user's situation
|
|
47
|
+
--b TEXT Session diary — what Jarvis did (first-person, specific)
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
--b What happened this session (1-2 sentences, first-person, specific)
|
|
49
|
+
Multiple flags may be combined in one call. At least one of --o, --w, --b is required.
|
|
33
50
|
|
|
34
51
|
Output: appends to memory/relationship/YYYY-MM/YYYY-MM-DD.md
|
|
35
52
|
`);
|
|
36
53
|
process.exit(0);
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
if (!values.b) {
|
|
40
|
-
console.error("Required: --b");
|
|
56
|
+
if (!values.o && !values.w && !values.b) {
|
|
57
|
+
console.error("Required: at least one of --o, --w, --b");
|
|
41
58
|
process.exit(1);
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
const notes = [];
|
|
62
|
+
|
|
63
|
+
if (values.o && values.o.length > 0) {
|
|
64
|
+
const confidence = values.confidence ? Number.parseFloat(values.confidence) : 0.75;
|
|
65
|
+
if (Number.isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
66
|
+
console.error("--confidence must be a number between 0.0 and 1.0");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
for (const text of values.o) {
|
|
70
|
+
notes.push({ type: "O" as const, text, confidence });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (values.w && values.w.length > 0) {
|
|
75
|
+
for (const text of values.w) {
|
|
76
|
+
notes.push({ type: "W" as const, text });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (values.b) {
|
|
81
|
+
notes.push({ type: "Session" as const, text: values.b });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
appendNotes(notes);
|
|
45
85
|
|
|
46
86
|
console.log(
|
|
47
|
-
JSON.stringify(
|
|
87
|
+
JSON.stringify(
|
|
88
|
+
{ success: true, message: "Relationship note written", count: notes.length },
|
|
89
|
+
null,
|
|
90
|
+
2
|
|
91
|
+
)
|
|
48
92
|
);
|
|
49
93
|
}
|
|
50
94
|
|
|
@@ -242,9 +242,49 @@ function getRecentSessions(since: Date): {
|
|
|
242
242
|
export function writeSynthesis(state: SynthesisState): string {
|
|
243
243
|
const sp = synthesisPath();
|
|
244
244
|
writeFileSync(sp, JSON.stringify(state, null, 2), "utf-8");
|
|
245
|
+
writeSignalCache();
|
|
245
246
|
return sp;
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
function writeSignalCache(): void {
|
|
250
|
+
try {
|
|
251
|
+
const p = resolve(paths.signals(), "ratings.jsonl");
|
|
252
|
+
const all = readJsonl<Rating>(p);
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const todayStr = now.toISOString().slice(0, 10);
|
|
255
|
+
|
|
256
|
+
const avg = (rs: Rating[]) =>
|
|
257
|
+
rs.length === 0 ? null : round1(rs.reduce((s, r) => s + r.rating, 0) / rs.length);
|
|
258
|
+
|
|
259
|
+
const today = avg(all.filter((r) => r.ts.slice(0, 10) === todayStr));
|
|
260
|
+
const week = avg(all.filter((r) => new Date(r.ts) >= daysAgo(7)));
|
|
261
|
+
const month = avg(all.filter((r) => new Date(r.ts) >= daysAgo(30)));
|
|
262
|
+
|
|
263
|
+
const recent = all.slice(-20);
|
|
264
|
+
const mid = Math.floor(recent.length / 2);
|
|
265
|
+
let trend: "up" | "down" | "stable" = "stable";
|
|
266
|
+
if (mid >= 3) {
|
|
267
|
+
const a = avg(recent.slice(0, mid)) ?? 0;
|
|
268
|
+
const b = avg(recent.slice(mid)) ?? 0;
|
|
269
|
+
if (b - a > 0.5) trend = "up";
|
|
270
|
+
else if (a - b > 0.5) trend = "down";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cache = resolve(paths.state(), "signal-cache.json");
|
|
274
|
+
writeFileSync(
|
|
275
|
+
cache,
|
|
276
|
+
JSON.stringify(
|
|
277
|
+
{ computed_at: now.toISOString(), today, week, month, trend },
|
|
278
|
+
null,
|
|
279
|
+
2
|
|
280
|
+
),
|
|
281
|
+
"utf-8"
|
|
282
|
+
);
|
|
283
|
+
} catch {
|
|
284
|
+
/* non-critical */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
248
288
|
export function synthesize(days: number): SynthesisState {
|
|
249
289
|
const since = daysAgo(days);
|
|
250
290
|
const { sessions, count: sessionCount } = getRecentSessions(since);
|