portable-agent-layer 0.21.0 → 0.23.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 +3 -2
- package/assets/agents/gemini-researcher.md +17 -3
- package/assets/agents/grok-researcher.md +19 -5
- package/assets/agents/multi-perspective-researcher.md +16 -2
- package/assets/agents/perplexity-researcher.md +17 -3
- package/assets/skills/analyze-pdf/SKILL.md +1 -1
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/extract-entities/SKILL.md +1 -1
- package/assets/skills/fyzz-chat-api/SKILL.md +6 -6
- package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
- package/assets/skills/reflect/SKILL.md +2 -2
- package/assets/skills/telos/SKILL.md +6 -6
- package/assets/templates/AGENTS.md.template +2 -2
- package/assets/templates/PAL/ALGORITHM.md +139 -13
- package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
- package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
- package/assets/templates/PAL/README.md +12 -9
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/PAL/WORK_TRACKING.md +2 -9
- package/assets/templates/pal-settings.json +6 -3
- package/assets/templates/settings.claude.json +2 -2
- package/package.json +3 -1
- package/src/cli/index.ts +7 -14
- package/src/hooks/handlers/rating.ts +1 -1
- package/src/hooks/handlers/relationship.ts +3 -3
- package/src/hooks/handlers/session-intelligence.ts +324 -0
- package/src/hooks/handlers/session-name.ts +3 -3
- package/src/hooks/handlers/synthesis.ts +36 -0
- package/src/hooks/handlers/update-check.ts +2 -2
- package/src/hooks/handlers/work-learning.ts +1 -1
- package/src/hooks/lib/context.ts +123 -41
- package/src/hooks/lib/graduation.ts +1 -1
- package/src/hooks/lib/inference.ts +1 -1
- package/src/hooks/lib/paths.ts +4 -12
- package/src/hooks/lib/readme-sync.ts +3 -3
- package/src/hooks/lib/security.ts +41 -27
- package/src/hooks/lib/stop.ts +6 -6
- package/src/hooks/lib/token-usage.ts +1 -0
- package/src/hooks/lib/work-tracking.ts +1 -51
- package/src/targets/claude/install.ts +3 -1
- package/src/targets/cursor/install.ts +9 -1
- package/src/targets/cursor/uninstall.ts +7 -0
- package/src/targets/lib.ts +214 -111
- package/src/targets/opencode/install.ts +6 -4
- package/src/tools/agent/algorithm-reflect.ts +122 -0
- package/src/tools/agent/synthesize.ts +361 -0
- package/src/tools/agent/thread.ts +162 -0
|
@@ -6,18 +6,21 @@ PAL is a persistent, cross-platform, cross-agent layer for portable AI workflows
|
|
|
6
6
|
|
|
7
7
|
**CLAUDE.md** (or the agent equivalent) is the entry point — generated from a template by the CLI installer. It defines execution modes, The Algorithm routing, and the context routing table. The agent loads it natively every session. A SessionStart hook keeps it fresh automatically.
|
|
8
8
|
|
|
9
|
-
**The PAL home directory (`~/.
|
|
9
|
+
**The PAL home directory (`~/.pal/`)** contains all system documentation, user context (TELOS), memory, and tools. The rest of the system lives in the PAL package (`src/`) and the agent's config directory (`~/.claude/`, `~/.config/opencode/`, `~/.cursor/`, or `~/.codex/`).
|
|
10
10
|
|
|
11
11
|
## Directory Structure
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
~/.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
~/.pal/ # PAL home
|
|
15
|
+
docs/ # System documentation (engine-managed)
|
|
16
|
+
ALGORITHM.md # The execution engine (4-phase)
|
|
17
|
+
CONTEXT_ROUTING.md # On-demand context routing table
|
|
18
|
+
MEMORY_SYSTEM.md # Memory guidelines
|
|
19
|
+
OPINION_TRACKING.md # Opinion system reference
|
|
20
|
+
STEERING_RULES.md # Behavioral rules
|
|
21
|
+
WORK_TRACKING.md # Work tracking reference
|
|
22
|
+
tools/ # Agent CLI tools (symlink → repo src/tools/agent/)
|
|
23
|
+
skills/ # Installed skills (symlinks → assets/skills/)
|
|
21
24
|
telos/ # User life context (TELOS)
|
|
22
25
|
MISSION.md, GOALS.md, PROJECTS.md, BELIEFS.md,
|
|
23
26
|
CHALLENGES.md, STRATEGIES.md, IDEAS.md, LEARNED.md,
|
|
@@ -124,5 +127,5 @@ PAL is designed to work identically across:
|
|
|
124
127
|
|
|
125
128
|
- **Add a skill:** Use the `create-skill` skill or manually create `assets/skills/<name>/SKILL.md`
|
|
126
129
|
- **Add startup files:** Append to `pal-settings.json → loadAtStartup.files`
|
|
127
|
-
- **Add user context:** Create files in `~/.
|
|
130
|
+
- **Add user context:** Create files in `~/.pal/telos/`
|
|
128
131
|
- **Toggle dynamic context:** Set keys in `pal-settings.json → dynamicContext` to `false`
|
|
@@ -447,7 +447,7 @@ All paths resolve through `src/hooks/lib/paths.ts`:
|
|
|
447
447
|
|
|
448
448
|
| Path | Default | Override |
|
|
449
449
|
|------|---------|----------|
|
|
450
|
-
| PAL home | `~/.
|
|
450
|
+
| PAL home | `~/.pal` | `PAL_HOME` |
|
|
451
451
|
| PAL package | Auto-detected from source | `PAL_PKG` |
|
|
452
452
|
| Claude config | `~/.claude` | `PAL_CLAUDE_DIR` |
|
|
453
453
|
| opencode config | `~/.config/opencode` | `PAL_OPENCODE_DIR` |
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
# Work Tracking
|
|
2
2
|
|
|
3
|
-
PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured)
|
|
3
|
+
PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured).
|
|
4
4
|
|
|
5
5
|
## Projects
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
7
|
+
Projects are managed in `telos/PROJECTS.md` and force-loaded at session startup via `pal-settings.json → loadAtStartup.files`.
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"loadAtStartup": {
|
|
15
15
|
"_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
|
|
16
16
|
"files": [
|
|
17
|
-
"~/.
|
|
18
|
-
"~/.
|
|
17
|
+
"~/.pal/docs/STEERING_RULES.md",
|
|
18
|
+
"~/.pal/telos/PROJECTS.md"
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
"dynamicContext": {
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"synthesis": true,
|
|
28
28
|
"signalTrends": true,
|
|
29
29
|
"failurePatterns": true,
|
|
30
|
-
"activeWork": true
|
|
30
|
+
"activeWork": true,
|
|
31
|
+
"projectHistory": true,
|
|
32
|
+
"sessionIntelligence": true,
|
|
33
|
+
"handoff": true
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"Bash(file //*)",
|
|
20
20
|
"Bash(stat //*)",
|
|
21
21
|
"Bash(readlink //*)",
|
|
22
|
-
"Bash(bun ~/.
|
|
23
|
-
"Bash(bun ~/.
|
|
22
|
+
"Bash(bun ~/.pal/skills/*/tools/*.ts *)",
|
|
23
|
+
"Bash(bun ~/.pal/tools/*.ts *)"
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"hooks": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"prepare": "husky",
|
|
45
45
|
"install:all": "bun run src/cli/index.ts cli install",
|
|
46
46
|
"uninstall": "bun run src/cli/index.ts cli uninstall",
|
|
47
|
+
"tool:synthesize": "bun run src/tools/agent/synthesize.ts",
|
|
48
|
+
"tool:thread": "bun run src/tools/agent/thread.ts",
|
|
47
49
|
"tool:analyze": "bun run src/tools/agent/analyze.ts",
|
|
48
50
|
"tool:wisdom-frame": "bun run src/tools/agent/wisdom-frame.ts",
|
|
49
51
|
"tool:reflect": "bun run src/tools/relationship-reflect.ts",
|
package/src/cli/index.ts
CHANGED
|
@@ -347,7 +347,6 @@ function doctor(silent = false): DoctorResult {
|
|
|
347
347
|
const hasAgent = claude.available || opencode.available || cursor.available;
|
|
348
348
|
|
|
349
349
|
const home = palHome();
|
|
350
|
-
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
351
350
|
const telosCount = (() => {
|
|
352
351
|
try {
|
|
353
352
|
return readdirSync(resolve(home, "telos")).filter((f) => f.endsWith(".md")).length;
|
|
@@ -373,13 +372,13 @@ function doctor(silent = false): DoctorResult {
|
|
|
373
372
|
cursor.available
|
|
374
373
|
? ok(`Cursor ${cursor.version || ""}`.trim())
|
|
375
374
|
: fail("Cursor — not found");
|
|
376
|
-
ok(`PAL home: ${home}
|
|
375
|
+
ok(`PAL home: ${home}`);
|
|
377
376
|
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
378
377
|
|
|
379
378
|
// API key checks
|
|
380
|
-
process.env.
|
|
381
|
-
? ok("
|
|
382
|
-
: fail("
|
|
379
|
+
process.env.PAL_ANTHROPIC_API_KEY
|
|
380
|
+
? ok("PAL_ANTHROPIC_API_KEY is set")
|
|
381
|
+
: fail("PAL_ANTHROPIC_API_KEY — not set (hooks need it for inference)");
|
|
383
382
|
process.env.PAL_GEMINI_API_KEY
|
|
384
383
|
? ok("PAL_GEMINI_API_KEY is set")
|
|
385
384
|
: warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
|
|
@@ -420,13 +419,9 @@ async function init(args: string[]) {
|
|
|
420
419
|
}
|
|
421
420
|
|
|
422
421
|
const home = palHome();
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
log.info(`Creating PAL home at ${home}`);
|
|
427
|
-
mkdirSync(resolve(home, "telos"), { recursive: true });
|
|
428
|
-
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
429
|
-
}
|
|
422
|
+
log.info(`Creating PAL home at ${home}`);
|
|
423
|
+
mkdirSync(resolve(home, "telos"), { recursive: true });
|
|
424
|
+
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
430
425
|
|
|
431
426
|
scaffoldTelos();
|
|
432
427
|
ensureSetupState();
|
|
@@ -670,13 +665,11 @@ async function update() {
|
|
|
670
665
|
async function status() {
|
|
671
666
|
const home = palHome();
|
|
672
667
|
const pkg = palPkg();
|
|
673
|
-
const isRepo = existsSync(resolve(pkg, ".palroot"));
|
|
674
668
|
|
|
675
669
|
const pkgJson = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8"));
|
|
676
670
|
|
|
677
671
|
console.log("");
|
|
678
672
|
log.info(`Version: ${pkgJson.version}`);
|
|
679
|
-
log.info(`Mode: ${isRepo ? "repo" : "package"}`);
|
|
680
673
|
log.info(`Package: ${pkg}`);
|
|
681
674
|
log.info(`Home: ${home}`);
|
|
682
675
|
console.log("");
|
|
@@ -358,6 +358,6 @@ export async function captureRating(message: string, sessionId?: string): Promis
|
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
// Path 2: Implicit sentiment (requires
|
|
361
|
+
// Path 2: Implicit sentiment (requires PAL_ANTHROPIC_API_KEY — inference silently no-ops without it)
|
|
362
362
|
await handleImplicitSentiment(cleaned, sessionId);
|
|
363
363
|
}
|
|
@@ -52,8 +52,8 @@ export async function captureRelationship(
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (!process.env.
|
|
56
|
-
logDebug("relationship", "Skipped: no
|
|
55
|
+
if (!process.env.PAL_ANTHROPIC_API_KEY) {
|
|
56
|
+
logDebug("relationship", "Skipped: no PAL_ANTHROPIC_API_KEY");
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -74,7 +74,7 @@ export async function captureRelationship(
|
|
|
74
74
|
logDebug("relationship", "Calling inference...");
|
|
75
75
|
const result = await inference({
|
|
76
76
|
system:
|
|
77
|
-
"You analyze messages from an AI
|
|
77
|
+
"You analyze messages from an AI assistant session to extract relationship observations. " +
|
|
78
78
|
"Types: O=opinions/preferences (how the user likes to work, what they want), " +
|
|
79
79
|
"B=biographical (what the AI accomplished this session, written in first-person), " +
|
|
80
80
|
"W=world facts (user's situation, projects, tools they use). " +
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop handler: unified session intelligence capture.
|
|
3
|
+
*
|
|
4
|
+
* Merges work-learning + relationship + handoff into a single Haiku call.
|
|
5
|
+
* Produces: title, summary, insights, handoff, relationship observations.
|
|
6
|
+
* Writes: session learning file, project history, relationship notes, last-handoff.
|
|
7
|
+
*
|
|
8
|
+
* Replaces: work-learning.ts + relationship.ts (both still exist but are bypassed).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { stringify } from "../lib/frontmatter";
|
|
14
|
+
import { inference } from "../lib/inference";
|
|
15
|
+
import { categorizeLearning } from "../lib/learning-category";
|
|
16
|
+
import { logDebug, logError } from "../lib/log";
|
|
17
|
+
import { ensureDir, paths } from "../lib/paths";
|
|
18
|
+
import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
|
|
19
|
+
import { fileTimestamp, monthPath } from "../lib/time";
|
|
20
|
+
import { logTokenUsage } from "../lib/token-usage";
|
|
21
|
+
import {
|
|
22
|
+
extractContent,
|
|
23
|
+
extractLastAssistant,
|
|
24
|
+
extractLastUser,
|
|
25
|
+
parseMessages,
|
|
26
|
+
} from "../lib/transcript";
|
|
27
|
+
import { appendProjectHistory, detectStatus } from "../lib/work-tracking";
|
|
28
|
+
|
|
29
|
+
// ── Dedup tracking (same as work-learning) ──
|
|
30
|
+
|
|
31
|
+
interface CaptureEntry {
|
|
32
|
+
filepath: string;
|
|
33
|
+
messageCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MIN_NEW_MESSAGES = 10;
|
|
37
|
+
|
|
38
|
+
function capturedPath(): string {
|
|
39
|
+
return resolve(paths.state(), "captured-learnings.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getPreviousCapture(sessionId: string): CaptureEntry | null {
|
|
43
|
+
const p = capturedPath();
|
|
44
|
+
if (!existsSync(p)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
47
|
+
if (Array.isArray(raw)) return null;
|
|
48
|
+
const entry = raw[sessionId];
|
|
49
|
+
if (!entry) return null;
|
|
50
|
+
if (typeof entry === "string") return { filepath: entry, messageCount: 0 };
|
|
51
|
+
return entry as CaptureEntry;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function markCaptured(sessionId: string, filepath: string, messageCount: number): void {
|
|
58
|
+
const p = capturedPath();
|
|
59
|
+
let data: Record<string, CaptureEntry> = {};
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(p)) {
|
|
62
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
63
|
+
if (!Array.isArray(raw) && typeof raw === "object") {
|
|
64
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
65
|
+
data[k] =
|
|
66
|
+
typeof v === "string"
|
|
67
|
+
? { filepath: v, messageCount: 0 }
|
|
68
|
+
: (v as CaptureEntry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
/* start fresh */
|
|
74
|
+
}
|
|
75
|
+
data[sessionId] = { filepath, messageCount };
|
|
76
|
+
const entries = Object.entries(data);
|
|
77
|
+
if (entries.length > 50) data = Object.fromEntries(entries.slice(-50));
|
|
78
|
+
writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function slugify(text: string): string {
|
|
82
|
+
return text
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
85
|
+
.trim()
|
|
86
|
+
.split(/\s+/)
|
|
87
|
+
.slice(0, 4)
|
|
88
|
+
.join("-");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── JSON schema for merged Haiku call ──
|
|
92
|
+
|
|
93
|
+
const INTELLIGENCE_SCHEMA = {
|
|
94
|
+
type: "object" as const,
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
title: { type: "string" as const, description: "Short session title, 5-10 words" },
|
|
98
|
+
summary: {
|
|
99
|
+
type: "string" as const,
|
|
100
|
+
description:
|
|
101
|
+
"What the AI did for the user, 2-4 sentences, AI perspective using 'we'",
|
|
102
|
+
},
|
|
103
|
+
insights: {
|
|
104
|
+
type: "string" as const,
|
|
105
|
+
description:
|
|
106
|
+
"What worked, what was surprising, what to do differently, 2-3 bullet points",
|
|
107
|
+
},
|
|
108
|
+
handoff: {
|
|
109
|
+
type: "string" as const,
|
|
110
|
+
description:
|
|
111
|
+
"If status is in-progress: what remains to be done, key decisions made, blockers. If completed: empty string.",
|
|
112
|
+
},
|
|
113
|
+
observations: {
|
|
114
|
+
type: "array" as const,
|
|
115
|
+
items: {
|
|
116
|
+
type: "object" as const,
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
properties: {
|
|
119
|
+
type: {
|
|
120
|
+
type: "string" as const,
|
|
121
|
+
enum: ["O", "W", "B"],
|
|
122
|
+
description: "O=preference, W=world fact, B=what AI did",
|
|
123
|
+
},
|
|
124
|
+
text: { type: "string" as const },
|
|
125
|
+
confidence: { type: "number" as const },
|
|
126
|
+
},
|
|
127
|
+
required: ["type", "text", "confidence"] as const,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["title", "summary", "insights", "handoff", "observations"] as const,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
interface IntelligenceOutput {
|
|
135
|
+
title: string;
|
|
136
|
+
summary: string;
|
|
137
|
+
insights: string;
|
|
138
|
+
handoff: string;
|
|
139
|
+
observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Main handler ──
|
|
143
|
+
|
|
144
|
+
export async function captureSessionIntelligence(
|
|
145
|
+
transcript: string,
|
|
146
|
+
sessionId?: string
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const messages = parseMessages(transcript);
|
|
149
|
+
if (messages.length < 6 || transcript.length < 2000) return;
|
|
150
|
+
|
|
151
|
+
// Dedup check
|
|
152
|
+
if (sessionId) {
|
|
153
|
+
const prev = getPreviousCapture(sessionId);
|
|
154
|
+
if (prev && messages.length - prev.messageCount < MIN_NEW_MESSAGES) return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip if no API key
|
|
158
|
+
if (!process.env.PAL_ANTHROPIC_API_KEY) {
|
|
159
|
+
logDebug("session-intelligence", "Skipped: no PAL_ANTHROPIC_API_KEY");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Relationship dedup — skip relationship capture if already done for this session
|
|
164
|
+
const skipRelationship = sessionId ? hasSessionNotes(sessionId) : false;
|
|
165
|
+
|
|
166
|
+
// Extract transcript windows
|
|
167
|
+
const userMessages = messages
|
|
168
|
+
.filter((m) => m.role === "user")
|
|
169
|
+
.map((m) => extractContent(m))
|
|
170
|
+
.filter((t) => t.length > 0);
|
|
171
|
+
|
|
172
|
+
const lastAssistant = extractLastAssistant(messages);
|
|
173
|
+
const lastAssistantText = extractContent(lastAssistant);
|
|
174
|
+
const lastUser = extractLastUser(messages);
|
|
175
|
+
const status = detectStatus(lastAssistantText);
|
|
176
|
+
|
|
177
|
+
// Wider window: 15 user msgs at 200 chars (relationship needs more context)
|
|
178
|
+
const userWindow = userMessages.slice(-15).map((t) => t.slice(0, 200));
|
|
179
|
+
const assistantWindow = lastAssistantText.slice(0, 600);
|
|
180
|
+
|
|
181
|
+
if (userWindow.length < 3) return;
|
|
182
|
+
|
|
183
|
+
// Single Haiku call
|
|
184
|
+
logDebug("session-intelligence", "Calling inference...");
|
|
185
|
+
let output: IntelligenceOutput | null = null;
|
|
186
|
+
try {
|
|
187
|
+
const result = await inference({
|
|
188
|
+
system: [
|
|
189
|
+
"You analyze a session between a human user and an AI assistant. Sessions may involve coding, research, writing, planning, analysis, or any other task.",
|
|
190
|
+
`Session status: ${status}.`,
|
|
191
|
+
"Produce ALL of the following:",
|
|
192
|
+
"1. title: short title (5-10 words) describing what was accomplished",
|
|
193
|
+
"2. summary: what the AI did for the user (2-4 sentences, AI perspective using 'we')",
|
|
194
|
+
"3. insights: what worked, what was surprising, what to do differently (2-3 points, no markdown)",
|
|
195
|
+
status === "in-progress"
|
|
196
|
+
? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
|
|
197
|
+
: "4. handoff: empty string (session completed)",
|
|
198
|
+
skipRelationship
|
|
199
|
+
? "5. observations: empty array (already captured)"
|
|
200
|
+
: "5. observations: 0-3 relationship observations. O=preference/opinion, W=world fact, B=what AI did this session (first-person). Be concise.",
|
|
201
|
+
].join("\n"),
|
|
202
|
+
user: `User messages:\n${userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n\nLast AI response:\n${assistantWindow}`,
|
|
203
|
+
maxTokens: 500,
|
|
204
|
+
timeout: 15000,
|
|
205
|
+
jsonSchema: INTELLIGENCE_SCHEMA,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (result.usage) logTokenUsage("session-intelligence", result.usage);
|
|
209
|
+
|
|
210
|
+
if (result.success && result.output) {
|
|
211
|
+
output = JSON.parse(result.output) as IntelligenceOutput;
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logError("session-intelligence", err);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fallbacks
|
|
218
|
+
const title = output?.title || extractContent(lastUser).slice(0, 80) || "session";
|
|
219
|
+
const summary = output?.summary || lastAssistantText.slice(0, 600);
|
|
220
|
+
const insights = output?.insights || "";
|
|
221
|
+
const handoff = output?.handoff || "";
|
|
222
|
+
|
|
223
|
+
// ── Write session learning file ──
|
|
224
|
+
|
|
225
|
+
const category = categorizeLearning(title, summary);
|
|
226
|
+
const slug = slugify(title);
|
|
227
|
+
const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
|
|
228
|
+
const filename = `${fileTimestamp()}_${category}_${slug}.md`;
|
|
229
|
+
|
|
230
|
+
const meta: Record<string, unknown> = {
|
|
231
|
+
title,
|
|
232
|
+
category,
|
|
233
|
+
date: new Date().toISOString().slice(0, 10),
|
|
234
|
+
cwd: process.cwd(),
|
|
235
|
+
};
|
|
236
|
+
if (sessionId) meta.session = sessionId;
|
|
237
|
+
|
|
238
|
+
const body = [
|
|
239
|
+
"## What Was Done",
|
|
240
|
+
summary,
|
|
241
|
+
"",
|
|
242
|
+
"## Insights",
|
|
243
|
+
insights || "*No insights captured.*",
|
|
244
|
+
...(handoff ? ["", "## Handoff", handoff] : []),
|
|
245
|
+
].join("\n");
|
|
246
|
+
|
|
247
|
+
const content = stringify(meta, body);
|
|
248
|
+
|
|
249
|
+
// Remove previous capture for this session
|
|
250
|
+
if (sessionId) {
|
|
251
|
+
const prev = getPreviousCapture(sessionId);
|
|
252
|
+
if (prev?.filepath && existsSync(prev.filepath)) {
|
|
253
|
+
try {
|
|
254
|
+
unlinkSync(prev.filepath);
|
|
255
|
+
} catch {
|
|
256
|
+
/* ignore */
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const filepath = resolve(dir, filename);
|
|
262
|
+
writeFileSync(filepath, content, "utf-8");
|
|
263
|
+
|
|
264
|
+
// Append to per-project history
|
|
265
|
+
appendProjectHistory(process.cwd(), {
|
|
266
|
+
date: new Date().toISOString().slice(0, 10),
|
|
267
|
+
title,
|
|
268
|
+
summary,
|
|
269
|
+
insights,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (sessionId) markCaptured(sessionId, filepath, messages.length);
|
|
273
|
+
logDebug("session-intelligence", `Learning captured: ${title}`);
|
|
274
|
+
|
|
275
|
+
// ── Write relationship notes ──
|
|
276
|
+
|
|
277
|
+
if (!skipRelationship && output?.observations && output.observations.length > 0) {
|
|
278
|
+
try {
|
|
279
|
+
const notes: RelationshipNote[] = output.observations.map((o) => ({
|
|
280
|
+
type: o.type,
|
|
281
|
+
text: o.text,
|
|
282
|
+
confidence: o.confidence,
|
|
283
|
+
}));
|
|
284
|
+
appendNotes(notes, sessionId);
|
|
285
|
+
logDebug(
|
|
286
|
+
"session-intelligence",
|
|
287
|
+
`${notes.length} relationship observations captured`
|
|
288
|
+
);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logError("session-intelligence:relationship", err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Write handoff state ──
|
|
295
|
+
|
|
296
|
+
if (handoff && status === "in-progress") {
|
|
297
|
+
try {
|
|
298
|
+
const handoffPath = resolve(ensureDir(paths.state()), "last-handoff.json");
|
|
299
|
+
let handoffs: Record<string, unknown> = {};
|
|
300
|
+
if (existsSync(handoffPath)) {
|
|
301
|
+
try {
|
|
302
|
+
handoffs = JSON.parse(readFileSync(handoffPath, "utf-8"));
|
|
303
|
+
} catch {
|
|
304
|
+
/* fresh */
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
handoffs[process.cwd()] = {
|
|
308
|
+
timestamp: new Date().toISOString(),
|
|
309
|
+
sessionId,
|
|
310
|
+
title,
|
|
311
|
+
status,
|
|
312
|
+
handoff,
|
|
313
|
+
artifacts: [],
|
|
314
|
+
};
|
|
315
|
+
// Keep last 20 projects
|
|
316
|
+
const entries = Object.entries(handoffs);
|
|
317
|
+
if (entries.length > 20) handoffs = Object.fromEntries(entries.slice(-20));
|
|
318
|
+
writeFileSync(handoffPath, JSON.stringify(handoffs, null, 2), "utf-8");
|
|
319
|
+
logDebug("session-intelligence", "Handoff state written");
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logError("session-intelligence:handoff", err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
import { logTokenUsage } from "../lib/token-usage";
|
|
21
21
|
|
|
22
22
|
const NAME_PROMPT =
|
|
23
|
-
"You generate concise 4-word session titles for AI
|
|
23
|
+
"You generate concise 4-word session titles for AI assistant sessions. " +
|
|
24
24
|
"Output EXACTLY 4 words in Title Case, no punctuation. Describe the specific task. " +
|
|
25
|
-
'Example: "Fix Session Name Generation", "
|
|
25
|
+
'Example: "Fix Session Name Generation", "Research Market Entry Strategy"';
|
|
26
26
|
|
|
27
27
|
export async function captureSessionName(
|
|
28
28
|
message: string,
|
|
@@ -42,7 +42,7 @@ export async function captureSessionName(
|
|
|
42
42
|
logDebug("session-name", `Named from prompt: "${name}"`);
|
|
43
43
|
|
|
44
44
|
// Spawn detached background process to upgrade with Haiku inference
|
|
45
|
-
if (!process.env.
|
|
45
|
+
if (!process.env.PAL_ANTHROPIC_API_KEY) return;
|
|
46
46
|
try {
|
|
47
47
|
const promptB64 = Buffer.from(message.slice(0, 800)).toString("base64");
|
|
48
48
|
const child = spawn(
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop handler: Run synthesis if 24h+ since last run.
|
|
3
|
+
* Imports synthesize logic directly — no subprocess needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { logDebug } from "../lib/log";
|
|
9
|
+
import { paths } from "../lib/paths";
|
|
10
|
+
|
|
11
|
+
const SYNTHESIS_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
export async function runSynthesis(): Promise<void> {
|
|
14
|
+
const statePath = resolve(paths.state(), "synthesis.json");
|
|
15
|
+
|
|
16
|
+
// Check 24h guard
|
|
17
|
+
if (existsSync(statePath)) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(readFileSync(statePath, "utf-8")) as { timestamp: string };
|
|
20
|
+
if (Date.now() - new Date(data.timestamp).getTime() < SYNTHESIS_TTL_MS) {
|
|
21
|
+
logDebug("synthesis", "Skipped — last synthesis < 24h ago");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Corrupted state — run anyway
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logDebug("synthesis", "Running synthesis...");
|
|
30
|
+
|
|
31
|
+
const { synthesize, writeSynthesis } = await import("../../tools/agent/synthesize");
|
|
32
|
+
const state = synthesize(7);
|
|
33
|
+
writeSynthesis(state);
|
|
34
|
+
|
|
35
|
+
logDebug("synthesis", "Synthesis complete");
|
|
36
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update checker — detects if a newer version of PAL is available.
|
|
3
3
|
*
|
|
4
|
-
* Repo mode (.
|
|
4
|
+
* Repo mode (.git exists next to package): git fetch + compare HEAD vs origin/main
|
|
5
5
|
* Package mode: fetch npm registry for latest version vs installed
|
|
6
6
|
*
|
|
7
7
|
* Caches result in state/update-available.json. Checked at most once per hour.
|
|
@@ -47,7 +47,7 @@ function writeCache(cache: UpdateCache): void {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function isRepoMode(): boolean {
|
|
50
|
-
return existsSync(resolve(palPkg(), ".
|
|
50
|
+
return existsSync(resolve(palPkg(), ".git"));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function getInstalledVersion(): string {
|
|
@@ -116,7 +116,7 @@ export async function captureWorkLearning(
|
|
|
116
116
|
.join("\n");
|
|
117
117
|
const result = await inference({
|
|
118
118
|
system:
|
|
119
|
-
"You summarize
|
|
119
|
+
"You summarize sessions between a human user and an AI assistant. Sessions may involve coding, research, writing, planning, analysis, or any other task. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown).",
|
|
120
120
|
user: `Human messages:\n${userMessages}\n\nAI response:\n${rawSummary.slice(0, 400)}`,
|
|
121
121
|
maxTokens: 300,
|
|
122
122
|
timeout: 15000,
|