portable-agent-layer 0.9.0 → 0.11.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/assets/skills/{analyze-pdf.md → analyze-pdf/SKILL.md} +4 -4
- package/{src → assets/skills/analyze-pdf}/tools/pdf-download.ts +3 -3
- package/assets/skills/{analyze-youtube.md → analyze-youtube/SKILL.md} +4 -4
- package/{src → assets/skills/analyze-youtube}/tools/youtube-analyze.ts +2 -2
- package/assets/skills/{council.md → council/SKILL.md} +3 -2
- package/assets/skills/{create-skill.md → create-skill/SKILL.md} +2 -1
- package/assets/skills/{extract-entities.md → extract-entities/SKILL.md} +4 -5
- package/{src → assets/skills/extract-entities}/tools/entity-save.ts +3 -3
- package/assets/skills/{extract-wisdom.md → extract-wisdom/SKILL.md} +3 -2
- package/assets/skills/{first-principles.md → first-principles/SKILL.md} +3 -2
- package/assets/skills/{fyzz-chat-api.md → fyzz-chat-api/SKILL.md} +6 -6
- package/{src → assets/skills/fyzz-chat-api}/tools/fyzz-api.ts +6 -6
- package/assets/skills/{reflect.md → reflect/SKILL.md} +2 -1
- package/assets/skills/{research.md → research/SKILL.md} +2 -1
- package/assets/skills/{review.md → review/SKILL.md} +2 -1
- package/assets/skills/{summarize.md → summarize/SKILL.md} +3 -2
- package/assets/skills/telos/SKILL.md +60 -0
- package/assets/skills/telos/tools/update-telos.ts +101 -0
- package/assets/skills/think/SKILL.md +47 -0
- package/assets/templates/AGENTS.md.template +8 -37
- package/assets/templates/PAL/CONTEXT_ROUTING.md +12 -0
- package/assets/templates/PAL/MEMORY_SYSTEM.md +26 -0
- package/assets/templates/PAL/OPINION_TRACKING.md +3 -0
- package/assets/templates/PAL/STEERING_RULES.md +23 -0
- package/assets/templates/PAL/WORK_TRACKING.md +14 -0
- package/assets/templates/settings.claude.json +80 -0
- package/package.json +2 -5
- package/src/hooks/handlers/rating.ts +4 -47
- package/src/hooks/handlers/reflect-trigger.ts +83 -0
- package/src/hooks/handlers/relationship.ts +8 -5
- package/src/hooks/handlers/session-name.ts +8 -6
- package/src/hooks/handlers/work-learning.ts +1 -0
- package/src/hooks/handlers/work-session.ts +16 -3
- package/src/hooks/lib/claude-md.ts +9 -24
- package/src/hooks/lib/context.ts +31 -48
- package/src/hooks/lib/graduation.ts +6 -4
- package/src/hooks/lib/learning-store.ts +7 -117
- package/src/hooks/lib/opinions.ts +191 -0
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/relationship.ts +5 -4
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/stop.ts +3 -0
- package/src/hooks/lib/text-similarity.ts +125 -0
- package/src/targets/claude/install.ts +16 -93
- package/src/targets/claude/uninstall.ts +22 -47
- package/src/targets/lib.ts +190 -48
- package/src/targets/opencode/install.ts +13 -2
- package/src/targets/opencode/uninstall.ts +4 -1
- package/src/tools/analyze.ts +49 -15
- package/src/tools/opinion.ts +250 -0
- package/src/tools/relationship-reflect.ts +215 -105
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Steering Rules
|
|
2
|
+
|
|
3
|
+
Behavioral directives — act on these, don't just know them.
|
|
4
|
+
|
|
5
|
+
**Surgical fixes only.** When debugging, make precise corrections to the broken behavior. Never delete or rearchitect components as a fix. If you believe a component is the root cause, explain your reasoning and ask before removing it.
|
|
6
|
+
|
|
7
|
+
**Never assert without verification.** Don't say something "is" a certain way unless you've verified it with your tools. After making changes, verify the result before claiming success. Evidence required — tests, diffs, tool output. Never "Done!" without proof.
|
|
8
|
+
|
|
9
|
+
**First principles over bolt-ons.** Most problems are symptoms. Understand → Simplify → Reduce → Add (last resort). Don't accrue technical debt through band-aid solutions.
|
|
10
|
+
|
|
11
|
+
**Read before modifying.** Understand existing code, imports, and patterns before suggesting changes.
|
|
12
|
+
|
|
13
|
+
**One change when debugging.** Isolate, verify, proceed. Don't change multiple things at once.
|
|
14
|
+
|
|
15
|
+
**Minimal scope.** Only change what was asked. No bonus refactoring, no extra cleanup, no unsolicited improvements.
|
|
16
|
+
|
|
17
|
+
**Ask before destructive actions.** Deletes, force pushes, production deploys — always ask first.
|
|
18
|
+
|
|
19
|
+
**Plan means stop.** "Create a plan" = present and STOP. No execution without approval.
|
|
20
|
+
|
|
21
|
+
**Error recovery.** When told you did something wrong — review the session, identify the violation, fix it, then explain what happened and capture the learning. Don't ask "What did I do wrong?"
|
|
22
|
+
|
|
23
|
+
**Act on what you know.** When tracked opinions or relationship notes reveal user preferences, apply them to your behavior. If you know the user prefers concise responses, be concise. If they prefer manual commits, never offer to commit.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Work Tracking
|
|
2
|
+
|
|
3
|
+
PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured) and `memory/state/projects.json` (AI-managed).
|
|
4
|
+
|
|
5
|
+
## Projects
|
|
6
|
+
|
|
7
|
+
Update `projects.json` via the work-tracking library when:
|
|
8
|
+
- **Starting sustained multi-session work** → create a project with objectives and an id (slugified, e.g. "pdf-template-engine")
|
|
9
|
+
- **Making a key decision** → add to the project's `decisions` array
|
|
10
|
+
- **Completing a milestone** → add to `completed`, remove from `nextSteps`
|
|
11
|
+
- **Session ends with open work** → update `nextSteps` and `handoff`
|
|
12
|
+
- **Work is done** → set status to "completed"
|
|
13
|
+
|
|
14
|
+
Do not create projects for one-off questions or quick fixes.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read",
|
|
5
|
+
"Grep",
|
|
6
|
+
"Glob",
|
|
7
|
+
"WebFetch",
|
|
8
|
+
"WebSearch",
|
|
9
|
+
"Bash(cat *)",
|
|
10
|
+
"Bash(head *)",
|
|
11
|
+
"Bash(tail *)",
|
|
12
|
+
"Bash(ls *)",
|
|
13
|
+
"Bash(find *)",
|
|
14
|
+
"Bash(grep *)",
|
|
15
|
+
"Bash(rg *)",
|
|
16
|
+
"Bash(wc *)",
|
|
17
|
+
"Bash(diff *)",
|
|
18
|
+
"Bash(which *)",
|
|
19
|
+
"Bash(file *)",
|
|
20
|
+
"Bash(stat *)",
|
|
21
|
+
"Bash(readlink *)",
|
|
22
|
+
"Bash(bun ~/.agents/skills/*/tools/*.ts *)"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"hooks": {
|
|
26
|
+
"SessionStart": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"UserPromptSubmit": [
|
|
38
|
+
{
|
|
39
|
+
"matcher": "",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"PreToolUse": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "Bash|Write|Edit",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"matcher": "Skill",
|
|
60
|
+
"hooks": [
|
|
61
|
+
{
|
|
62
|
+
"type": "command",
|
|
63
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"Stop": [
|
|
69
|
+
{
|
|
70
|
+
"matcher": "",
|
|
71
|
+
"hooks": [
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,11 +44,8 @@
|
|
|
44
44
|
"prepare": "husky",
|
|
45
45
|
"install:all": "bun run src/cli/install.ts",
|
|
46
46
|
"uninstall": "bun run src/cli/uninstall.ts",
|
|
47
|
-
"ai:entity-save": "bun run src/tools/entity-save.ts",
|
|
48
|
-
"ai:fyzz-api": "bun run src/tools/fyzz-api.ts",
|
|
49
|
-
"ai:pdf-download": "bun run src/tools/pdf-download.ts",
|
|
50
|
-
"ai:youtube-analyze": "bun run src/tools/youtube-analyze.ts",
|
|
51
47
|
"tool:analyze": "bun run src/tools/analyze.ts",
|
|
48
|
+
"tool:opinion": "bun run src/tools/opinion.ts",
|
|
52
49
|
"tool:reflect": "bun run src/tools/relationship-reflect.ts",
|
|
53
50
|
"tool:export": "bun run src/tools/export.ts",
|
|
54
51
|
"tool:import": "bun run src/tools/import.ts",
|
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { resolve } from "node:path";
|
|
13
|
-
import { stringify } from "../lib/frontmatter";
|
|
14
13
|
import { inference } from "../lib/inference";
|
|
15
|
-
import {
|
|
16
|
-
import { ensureDir, paths } from "../lib/paths";
|
|
14
|
+
import { paths } from "../lib/paths";
|
|
17
15
|
import { emitRating } from "../lib/signals";
|
|
18
|
-
import {
|
|
16
|
+
import { now } from "../lib/time";
|
|
19
17
|
import { logTokenUsage } from "../lib/token-usage";
|
|
20
18
|
|
|
21
19
|
/** Read cached last assistant response (written by StopOrchestrator), looked up by session */
|
|
@@ -237,37 +235,6 @@ const MIN_CONFIDENCE = 0.5;
|
|
|
237
235
|
|
|
238
236
|
// ── Rating Handling ──
|
|
239
237
|
|
|
240
|
-
function writeLearningMarkdown(
|
|
241
|
-
rating: number,
|
|
242
|
-
source: string,
|
|
243
|
-
context: string,
|
|
244
|
-
detailedContext: string,
|
|
245
|
-
responsePreview: string
|
|
246
|
-
): void {
|
|
247
|
-
const category = categorizeLearning(context, detailedContext);
|
|
248
|
-
const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
|
|
249
|
-
const filename = `${fileTimestamp()}_${source}-rating-${rating}_${category}.md`;
|
|
250
|
-
|
|
251
|
-
const meta: Record<string, unknown> = {
|
|
252
|
-
title: context.slice(0, 100) || "(low rating)",
|
|
253
|
-
category,
|
|
254
|
-
date: new Date().toISOString().slice(0, 10),
|
|
255
|
-
rating,
|
|
256
|
-
source,
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const body = [
|
|
260
|
-
"## Context",
|
|
261
|
-
context || "*(unavailable)*",
|
|
262
|
-
"",
|
|
263
|
-
...(detailedContext ? ["## Analysis", detailedContext, ""] : []),
|
|
264
|
-
"## Last Response",
|
|
265
|
-
responsePreview || "*(unavailable)*",
|
|
266
|
-
].join("\n");
|
|
267
|
-
|
|
268
|
-
writeFileSync(resolve(dir, filename), stringify(meta, body), "utf-8");
|
|
269
|
-
}
|
|
270
|
-
|
|
271
238
|
function handleRating(
|
|
272
239
|
rating: number,
|
|
273
240
|
context: string,
|
|
@@ -279,8 +246,8 @@ function handleRating(
|
|
|
279
246
|
const responsePreview = getLastResponse(sessionId).slice(0, 500);
|
|
280
247
|
emitRating(rating, context, source, responsePreview);
|
|
281
248
|
|
|
282
|
-
if (rating <=
|
|
283
|
-
//
|
|
249
|
+
if (rating <= 4) {
|
|
250
|
+
// Low rating — write pending file for Stop handler with full transcript
|
|
284
251
|
const userPreview = userMessage?.slice(0, 400);
|
|
285
252
|
writeFileSync(
|
|
286
253
|
resolve(paths.state(), "pending-failure.json"),
|
|
@@ -299,16 +266,6 @@ function handleRating(
|
|
|
299
266
|
),
|
|
300
267
|
"utf-8"
|
|
301
268
|
);
|
|
302
|
-
// No learning markdown for ≤3 — failure capture covers it with richer analysis + tags
|
|
303
|
-
} else if (rating < 5) {
|
|
304
|
-
// Low but not critical — write learning markdown
|
|
305
|
-
writeLearningMarkdown(
|
|
306
|
-
rating,
|
|
307
|
-
source,
|
|
308
|
-
context,
|
|
309
|
-
detailedContext ?? "",
|
|
310
|
-
responsePreview
|
|
311
|
-
);
|
|
312
269
|
}
|
|
313
270
|
}
|
|
314
271
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-trigger for relationship reflect — runs when conditions are met:
|
|
3
|
+
* - 7+ days since last reflect
|
|
4
|
+
* - 10+ new relationship notes since last reflect
|
|
5
|
+
*
|
|
6
|
+
* Spawns `bun run tool:reflect` as a detached background process.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { logDebug } from "../lib/log";
|
|
12
|
+
import { getLastReflectDate } from "../lib/opinions";
|
|
13
|
+
import { palPkg, paths } from "../lib/paths";
|
|
14
|
+
|
|
15
|
+
const MIN_DAYS_BETWEEN = 7;
|
|
16
|
+
const MIN_NEW_NOTES = 10;
|
|
17
|
+
|
|
18
|
+
function countNotesSince(since: string): number {
|
|
19
|
+
const relDir = paths.relationship();
|
|
20
|
+
if (!existsSync(relDir)) return 0;
|
|
21
|
+
|
|
22
|
+
let count = 0;
|
|
23
|
+
try {
|
|
24
|
+
for (const monthDir of readdirSync(relDir)) {
|
|
25
|
+
if (!/^\d{4}-\d{2}$/.test(monthDir)) continue;
|
|
26
|
+
const monthPath = resolve(relDir, monthDir);
|
|
27
|
+
try {
|
|
28
|
+
for (const file of readdirSync(monthPath)) {
|
|
29
|
+
if (!file.endsWith(".md")) continue;
|
|
30
|
+
const dateStr = file.replace(".md", "");
|
|
31
|
+
if (dateStr > since) {
|
|
32
|
+
const content = readFileSync(resolve(monthPath, file), "utf-8");
|
|
33
|
+
count += (content.match(/^- [OBW]/gm) || []).length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
/* skip */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* non-critical */
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function checkReflectTrigger(): Promise<void> {
|
|
47
|
+
const lastReflect = getLastReflectDate();
|
|
48
|
+
const now = new Date();
|
|
49
|
+
|
|
50
|
+
// Trigger if either condition is met (OR logic)
|
|
51
|
+
let timeThreshold = !lastReflect;
|
|
52
|
+
if (lastReflect) {
|
|
53
|
+
const daysSince =
|
|
54
|
+
(now.getTime() - new Date(lastReflect).getTime()) / (1000 * 60 * 60 * 24);
|
|
55
|
+
timeThreshold = daysSince >= MIN_DAYS_BETWEEN;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const newNotes = countNotesSince(lastReflect || "2000-01-01");
|
|
59
|
+
const volumeThreshold = newNotes >= MIN_NEW_NOTES;
|
|
60
|
+
|
|
61
|
+
if (!timeThreshold && !volumeThreshold) {
|
|
62
|
+
logDebug("reflect-trigger", `Skipping: ${newNotes} notes, time threshold not met`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logDebug(
|
|
67
|
+
"reflect-trigger",
|
|
68
|
+
`Triggering: ${newNotes} new notes, last: ${lastReflect || "never"}`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const proc = Bun.spawn(["bun", "run", "tool:reflect"], {
|
|
73
|
+
cwd: palPkg(),
|
|
74
|
+
stdout: "ignore",
|
|
75
|
+
stderr: "ignore",
|
|
76
|
+
stdin: "ignore",
|
|
77
|
+
});
|
|
78
|
+
proc.unref();
|
|
79
|
+
logDebug("reflect-trigger", "Spawned reflect in background");
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logDebug("reflect-trigger", `Failed to spawn: ${err}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -20,8 +20,9 @@ const OBSERVATION_SCHEMA = {
|
|
|
20
20
|
properties: {
|
|
21
21
|
type: {
|
|
22
22
|
type: "string",
|
|
23
|
-
enum: ["O", "W"],
|
|
24
|
-
description:
|
|
23
|
+
enum: ["O", "W", "B"],
|
|
24
|
+
description:
|
|
25
|
+
"O=opinion/preference, W=factual observation, B=belief/behavioral pattern",
|
|
25
26
|
},
|
|
26
27
|
text: { type: "string" },
|
|
27
28
|
confidence: { type: "number" },
|
|
@@ -74,8 +75,10 @@ export async function captureRelationship(
|
|
|
74
75
|
const result = await inference({
|
|
75
76
|
system:
|
|
76
77
|
"You analyze user messages from an AI coding session to extract relationship observations. " +
|
|
77
|
-
"
|
|
78
|
-
"
|
|
78
|
+
"Types: O=opinions/preferences (how they like to work, what they want), " +
|
|
79
|
+
"B=beliefs/behavioral patterns (how they approach problems, decision-making style, recurring habits), " +
|
|
80
|
+
"W=world facts (their situation, projects, tools they use). " +
|
|
81
|
+
"Focus on: preferences, corrections, frustrations, positive reactions, communication style, problem-solving approach. " +
|
|
79
82
|
"Return 0-3 observations. If nothing notable, return empty observations array. Be concise.",
|
|
80
83
|
user: `User messages from this session:\n${userMessages.map((m, i) => `${i + 1}. ${m}`).join("\n")}`,
|
|
81
84
|
maxTokens: 300,
|
|
@@ -93,7 +96,7 @@ export async function captureRelationship(
|
|
|
93
96
|
|
|
94
97
|
try {
|
|
95
98
|
const parsed = JSON.parse(result.output) as {
|
|
96
|
-
observations: Array<{ type: "O" | "W"; text: string; confidence: number }>;
|
|
99
|
+
observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
logDebug("relationship", `Parsed ${parsed.observations?.length ?? 0} observations`);
|
|
@@ -30,14 +30,16 @@ export async function captureSessionName(
|
|
|
30
30
|
): Promise<void> {
|
|
31
31
|
if (!sessionId) return;
|
|
32
32
|
|
|
33
|
-
// Skip if this session is already named
|
|
33
|
+
// Skip if this session is already named (non-untitled)
|
|
34
34
|
const names = readSessionNames();
|
|
35
|
-
|
|
35
|
+
const existing = names[sessionId];
|
|
36
|
+
if (existing && existing !== "untitled session") return;
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// Try deterministic name from this message's keywords
|
|
39
|
+
const name = extractFallbackName(message);
|
|
40
|
+
if (name === "untitled session") return; // not enough keywords yet
|
|
41
|
+
writeSessionName(sessionId, name);
|
|
42
|
+
logDebug("session-name", `Named from prompt: "${name}"`);
|
|
41
43
|
|
|
42
44
|
// TODO: re-enable when a consumer exists (tab titles, dashboard)
|
|
43
45
|
// // 2. Spawn detached background process to upgrade with inference
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Replaces the old work.ts handler.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
extractFallbackName,
|
|
8
|
+
readSessionNames,
|
|
9
|
+
writeSessionName,
|
|
10
|
+
} from "../lib/session-names";
|
|
7
11
|
import { now } from "../lib/time";
|
|
8
12
|
import {
|
|
9
13
|
extractContent,
|
|
@@ -29,9 +33,18 @@ export async function captureWorkSession(
|
|
|
29
33
|
|
|
30
34
|
const id = sessionId || `session-${Date.now()}`;
|
|
31
35
|
|
|
32
|
-
//
|
|
36
|
+
// Name the session if still untitled and enough messages
|
|
33
37
|
const names = readSessionNames();
|
|
34
|
-
|
|
38
|
+
let name = names[id] || "";
|
|
39
|
+
if ((!name || name === "untitled session") && messages.length >= 6) {
|
|
40
|
+
const userTexts = messages
|
|
41
|
+
.filter((m) => m.role === "user")
|
|
42
|
+
.map((m) => extractContent(m))
|
|
43
|
+
.join(" ");
|
|
44
|
+
name = extractFallbackName(userTexts);
|
|
45
|
+
if (name !== "untitled session") writeSessionName(id, name);
|
|
46
|
+
}
|
|
47
|
+
if (!name || name === "untitled session") name = "untitled session";
|
|
35
48
|
|
|
36
49
|
// Extract content
|
|
37
50
|
const lastUser = extractLastUser(messages);
|
|
@@ -17,8 +17,7 @@ import {
|
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { dirname, relative, resolve } from "node:path";
|
|
20
|
-
import {
|
|
21
|
-
import { assets, ensureDir, palHome, paths, platform } from "./paths";
|
|
20
|
+
import { assets, ensureDir, paths, platform } from "./paths";
|
|
22
21
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
22
|
|
|
24
23
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -71,44 +70,30 @@ export function needsRebuild(): boolean {
|
|
|
71
70
|
|
|
72
71
|
const outputMtime = statSync(outputPath).mtimeMs;
|
|
73
72
|
|
|
74
|
-
// Collect source files: template + setup.json +
|
|
73
|
+
// Collect source files: template + setup.json + PAL docs
|
|
75
74
|
const sources: string[] = [TEMPLATE_PATH, resolve(paths.state(), "setup.json")];
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
// Track PAL doc sources for rebuild detection
|
|
77
|
+
const palDocsDir = assets.palDocs();
|
|
78
|
+
if (existsSync(palDocsDir)) {
|
|
79
|
+
for (const f of readdirSync(palDocsDir).filter((f) => f.endsWith(".md"))) {
|
|
80
|
+
sources.push(resolve(palDocsDir, f));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
return latestMtime(...sources) > outputMtime;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function memoryPaths(): string {
|
|
88
|
-
const mem = resolve(palHome(), "memory");
|
|
89
|
-
return [
|
|
90
|
-
`- **Wisdom frames**: \`${resolve(mem, "wisdom", "frames")}/\` — crystallized principles per domain (loaded every session)`,
|
|
91
|
-
`- **Relationship notes**: \`${resolve(mem, "relationship")}/YYYY-MM/YYYY-MM-DD.md\` — daily interaction observations (loaded every session)`,
|
|
92
|
-
`- **Session learnings**: \`${resolve(mem, "learning", "session")}/YYYY-MM/*.md\` — reusable insights from sessions (loaded every session)`,
|
|
93
|
-
`- **Failure captures**: \`${resolve(mem, "learning", "failures")}/YYYY-MM/{timestamp}_{slug}/capture.md\` — what went wrong and why`,
|
|
94
|
-
`- **Signals**: \`${resolve(mem, "signals")}/ratings.jsonl\` — append-only rating signal log (do not edit directly)`,
|
|
95
|
-
].join("\n");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
87
|
/** Render AGENTS.md from the template using current state */
|
|
99
88
|
export function buildClaudeMd(): string {
|
|
100
89
|
const template = existsSync(TEMPLATE_PATH)
|
|
101
90
|
? readFileSync(TEMPLATE_PATH, "utf-8")
|
|
102
|
-
: "# PAL Context\n\n{{SETUP_PROMPT}}\n
|
|
91
|
+
: "# PAL Context\n\n{{SETUP_PROMPT}}\n";
|
|
103
92
|
|
|
104
93
|
const state = readSetupState();
|
|
105
94
|
const setupPrompt = state ? buildSetupPrompt(state) : null;
|
|
106
|
-
const telos = loadTelos();
|
|
107
95
|
|
|
108
|
-
return template
|
|
109
|
-
.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
|
|
110
|
-
.replace("{{TELOS}}", telos ? `${telos}\n` : "")
|
|
111
|
-
.replace("{{MEMORY_PATHS}}", memoryPaths());
|
|
96
|
+
return template.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "");
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
/** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { parse } from "./frontmatter";
|
|
9
9
|
import { readFailures, readLearnings } from "./learning-store";
|
|
10
|
+
import { loadOpinionContext } from "./opinions";
|
|
10
11
|
import { paths } from "./paths";
|
|
11
12
|
import { loadRecentNotes } from "./relationship";
|
|
12
13
|
import { readSessionNames } from "./session-names";
|
|
@@ -20,33 +21,6 @@ import {
|
|
|
20
21
|
staleProjects,
|
|
21
22
|
} from "./work-tracking";
|
|
22
23
|
|
|
23
|
-
/** Load all populated TELOS files as a single markdown string */
|
|
24
|
-
export function loadTelos(): string {
|
|
25
|
-
const telosDir = paths.telos();
|
|
26
|
-
if (!existsSync(telosDir)) return "";
|
|
27
|
-
|
|
28
|
-
const files = readdirSync(telosDir)
|
|
29
|
-
.filter((f) => f.endsWith(".md"))
|
|
30
|
-
.sort();
|
|
31
|
-
|
|
32
|
-
const sections: string[] = [];
|
|
33
|
-
|
|
34
|
-
for (const file of files) {
|
|
35
|
-
const content = readFileSync(resolve(telosDir, file), "utf-8").trim();
|
|
36
|
-
// Skip empty templates (only have a heading and comment)
|
|
37
|
-
const realLines = content
|
|
38
|
-
.split("\n")
|
|
39
|
-
.filter(
|
|
40
|
-
(l) =>
|
|
41
|
-
!l.startsWith("#") && !l.startsWith("<!--") && !l.startsWith("-->") && l.trim()
|
|
42
|
-
);
|
|
43
|
-
if (realLines.length === 0) continue;
|
|
44
|
-
sections.push(content);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return sections.join("\n\n---\n\n");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
24
|
/** Count lines in a signals JSONL file */
|
|
51
25
|
export function countSignals(filename: string): number {
|
|
52
26
|
const filepath = resolve(paths.signals(), filename);
|
|
@@ -62,22 +36,21 @@ export function countSignals(filename: string): number {
|
|
|
62
36
|
/** Load structured session history + project dashboard */
|
|
63
37
|
export function loadActiveWork(): { text: string; summary: string | null } | null {
|
|
64
38
|
try {
|
|
65
|
-
const
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const allRecent = recentSessions(48);
|
|
66
41
|
const projects = activeProjects();
|
|
67
42
|
const stale = staleProjects(7);
|
|
68
43
|
|
|
69
|
-
if (
|
|
44
|
+
if (allRecent.length === 0 && projects.length === 0) return null;
|
|
70
45
|
|
|
71
46
|
const lines: string[] = [];
|
|
72
47
|
|
|
73
|
-
if (
|
|
48
|
+
if (allRecent.length > 0) {
|
|
74
49
|
lines.push("## Recent Work (last 48h)");
|
|
75
|
-
for (const s of
|
|
50
|
+
for (const s of allRecent.slice(-10).reverse()) {
|
|
76
51
|
const ago = formatAgo(s.ts);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
lines.push(` Handoff: ${s.handoff.split("\n")[0].slice(0, 120)}`);
|
|
80
|
-
}
|
|
52
|
+
const here = s.cwd === cwd ? " *" : "";
|
|
53
|
+
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
81
54
|
}
|
|
82
55
|
}
|
|
83
56
|
|
|
@@ -106,7 +79,8 @@ export function loadActiveWork(): { text: string; summary: string | null } | nul
|
|
|
106
79
|
}
|
|
107
80
|
|
|
108
81
|
// Summary from most recent session
|
|
109
|
-
const
|
|
82
|
+
const cwdSessions = allRecent.filter((s) => s.cwd === cwd);
|
|
83
|
+
const last = cwdSessions.length > 0 ? cwdSessions[cwdSessions.length - 1] : null;
|
|
110
84
|
const summary = last?.summary?.slice(0, 60) || null;
|
|
111
85
|
|
|
112
86
|
return {
|
|
@@ -215,26 +189,33 @@ export function loadWisdomContext(): string {
|
|
|
215
189
|
}
|
|
216
190
|
}
|
|
217
191
|
|
|
218
|
-
/** Load recent session learning files as digest,
|
|
192
|
+
/** Load recent session learning files as digest, with detail for current project */
|
|
219
193
|
export function loadLearningDigest(): string {
|
|
220
194
|
try {
|
|
221
|
-
const
|
|
195
|
+
const cwd = process.cwd();
|
|
196
|
+
const entries = readLearnings(paths.sessionLearning(), 10);
|
|
222
197
|
if (entries.length === 0) return "";
|
|
223
198
|
|
|
224
|
-
const
|
|
225
|
-
const
|
|
199
|
+
const thisProject = entries.filter((e) => e.cwd === cwd).slice(0, 4);
|
|
200
|
+
const other = entries.filter((e) => e.cwd !== cwd).slice(0, 3);
|
|
226
201
|
|
|
227
|
-
if (
|
|
202
|
+
if (thisProject.length === 0 && other.length === 0) return "";
|
|
228
203
|
|
|
229
|
-
const lines: string[] = [
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
204
|
+
const lines: string[] = [];
|
|
205
|
+
|
|
206
|
+
if (thisProject.length > 0) {
|
|
207
|
+
lines.push("## This Project — Recent Sessions");
|
|
208
|
+
for (const e of thisProject) {
|
|
209
|
+
lines.push(`- **${e.title}**`);
|
|
210
|
+
if (e.insights) lines.push(` ${e.insights.split("\n")[0].slice(0, 150)}`);
|
|
211
|
+
}
|
|
233
212
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
213
|
+
|
|
214
|
+
if (other.length > 0) {
|
|
215
|
+
lines.push(thisProject.length > 0 ? "" : "", "## Other Recent Learnings");
|
|
216
|
+
for (const e of other) lines.push(`- ${e.title}`);
|
|
237
217
|
}
|
|
218
|
+
|
|
238
219
|
return lines.join("\n");
|
|
239
220
|
} catch {
|
|
240
221
|
return "";
|
|
@@ -347,8 +328,10 @@ export function buildSystemReminder(): string {
|
|
|
347
328
|
const trends = loadSignalTrends();
|
|
348
329
|
const failures = loadFailurePatterns();
|
|
349
330
|
const synthesis = loadSynthesisRecommendations();
|
|
331
|
+
const opinions = loadOpinionContext();
|
|
350
332
|
const parts: string[] = [];
|
|
351
333
|
if (wisdom) parts.push(wisdom);
|
|
334
|
+
if (opinions) parts.push(opinions);
|
|
352
335
|
if (relationship) parts.push(relationship);
|
|
353
336
|
if (digest) parts.push(digest);
|
|
354
337
|
if (synthesis) parts.push(synthesis);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Unified Learning Analysis — graduation + ratings summary in one pipeline.
|
|
3
3
|
*
|
|
4
4
|
* Reads failures and session learnings via learning-store, detects recurring
|
|
5
|
-
* patterns via
|
|
5
|
+
* patterns via Dice similarity on context text, and generates a ratings summary
|
|
6
6
|
* with recommendations via Haiku inference.
|
|
7
7
|
*
|
|
8
8
|
* A pattern qualifies for graduation when it appears 3+ times across different sessions.
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
15
|
import {
|
|
16
|
-
extractKeywords,
|
|
17
16
|
type FailureEntry,
|
|
18
17
|
type LearningEntry,
|
|
19
18
|
readFailures,
|
|
20
19
|
readLearnings,
|
|
21
|
-
similarity,
|
|
22
20
|
} from "./learning-store";
|
|
23
21
|
import { logDebug } from "./log";
|
|
24
22
|
import { ensureDir, paths } from "./paths";
|
|
23
|
+
import { extractKeywords, similarity } from "./text-similarity";
|
|
25
24
|
|
|
26
25
|
// ── Types ──
|
|
27
26
|
|
|
28
27
|
export interface AnalysisEntry {
|
|
29
28
|
source: string;
|
|
29
|
+
path: string;
|
|
30
30
|
text: string;
|
|
31
31
|
date: string;
|
|
32
32
|
}
|
|
@@ -86,7 +86,7 @@ function classifyDomain(text: string): string {
|
|
|
86
86
|
// ── Data Collection ──
|
|
87
87
|
|
|
88
88
|
const MIN_TEXT_LENGTH = 30;
|
|
89
|
-
export const SIMILARITY_THRESHOLD = 0.
|
|
89
|
+
export const SIMILARITY_THRESHOLD = 0.3;
|
|
90
90
|
const MIN_OCCURRENCES = 3;
|
|
91
91
|
|
|
92
92
|
function toAnalysisEntries(
|
|
@@ -99,6 +99,7 @@ function toAnalysisEntries(
|
|
|
99
99
|
if (f.context.length >= MIN_TEXT_LENGTH) {
|
|
100
100
|
entries.push({
|
|
101
101
|
source: `failure:${f.slug}`,
|
|
102
|
+
path: f.path,
|
|
102
103
|
text: f.context.slice(0, 300),
|
|
103
104
|
date: f.date,
|
|
104
105
|
});
|
|
@@ -110,6 +111,7 @@ function toAnalysisEntries(
|
|
|
110
111
|
if (text.length >= MIN_TEXT_LENGTH) {
|
|
111
112
|
entries.push({
|
|
112
113
|
source: `learning:${l.filename}`,
|
|
114
|
+
path: l.path,
|
|
113
115
|
text: text.slice(0, 300),
|
|
114
116
|
date: l.date,
|
|
115
117
|
});
|