portable-agent-layer 0.34.0 → 0.36.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 -1
- package/assets/skills/presentation/SKILL.md +2 -0
- package/assets/skills/presentation/demo/slides/004-content.md +27 -1
- package/assets/skills/presentation/theme-base/base.css +206 -0
- package/assets/skills/presentation/theme-base/skeleton.html +49 -0
- package/assets/skills/presentation/tools/lib/lint-rules.ts +25 -0
- package/assets/skills/projects/SKILL.md +0 -1
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/templates/AGENTS.md.template +2 -1
- package/assets/templates/PAL/ALGORITHM.md +28 -3
- package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/STEERING_RULES.md +4 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
- package/assets/templates/PAL/WORK_TRACKING.md +1 -1
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +1 -1
- package/src/cli/setup-telos.ts +12 -79
- package/src/hooks/LoadContext.ts +22 -10
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/session-intelligence.ts +9 -86
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +57 -139
- package/src/hooks/lib/relationship.ts +3 -3
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/semi-static.ts +186 -0
- package/src/hooks/lib/setup.ts +0 -5
- package/src/hooks/lib/stop.ts +3 -0
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/copilot/install.ts +39 -8
- package/src/targets/copilot/uninstall.ts +58 -17
- package/src/targets/cursor/install.ts +8 -0
- package/src/targets/cursor/uninstall.ts +18 -1
- package/src/targets/lib.ts +26 -0
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +1 -1
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/relationship-reflect.ts +2 -2
- package/src/tools/self-model.ts +4 -4
- package/assets/templates/telos/PROJECTS.md +0 -7
|
@@ -233,6 +233,7 @@ Brief description.
|
|
|
233
233
|
│ │ - Work learning capture
|
|
234
234
|
│ │ - Failure logging
|
|
235
235
|
│ │ - Reflect trigger check
|
|
236
|
+
│ │ - Write context digests (semi-static sources)
|
|
236
237
|
│ │ - Auto-backup
|
|
237
238
|
│ │ - Count updates
|
|
238
239
|
│ │ - Tab reset
|
|
@@ -357,34 +358,44 @@ Relationship notes (O/B types)
|
|
|
357
358
|
|
|
358
359
|
## Context Loading Architecture
|
|
359
360
|
|
|
360
|
-
###
|
|
361
|
+
### Three-Tier Design
|
|
361
362
|
|
|
362
|
-
**
|
|
363
|
-
- CLAUDE.md — identity, modes, context routing table
|
|
364
|
-
- Loaded once
|
|
363
|
+
**Tier 1 — Operational** (loaded natively at agent startup):
|
|
364
|
+
- CLAUDE.md / AGENTS.md — identity, modes, context routing table
|
|
365
|
+
- Loaded once per session, always available, never re-fetched
|
|
365
366
|
|
|
366
|
-
**
|
|
367
|
-
-
|
|
368
|
-
-
|
|
367
|
+
**Tier 2 — Semi-static** (pre-compiled at previous session stop, loaded natively):
|
|
368
|
+
- Self-model, wisdom, opinions, synthesis, failures, steering rules
|
|
369
|
+
- Written to disk by `writeContextDigests()` at Stop time
|
|
370
|
+
- Loaded natively per-agent: `@imports` in CLAUDE.md (Claude Code), `instructions[]` in opencode config, `.mdc` rules in `~/.cursor/rules/` (Cursor), `.instructions.md` in `~/.copilot/instructions/` (Copilot)
|
|
371
|
+
- Content is global/user-level — safe to pre-compile (not project-scoped)
|
|
372
|
+
|
|
373
|
+
**Tier 3 — Dynamic** (injected fresh each session by LoadContext hook):
|
|
374
|
+
- Handoff notes, session intelligence, open threads, relationship notes, project history
|
|
375
|
+
- Changes per-session or is project-scoped — can't be pre-compiled
|
|
376
|
+
- Injected as `<system-reminder>` block via stdout
|
|
369
377
|
- Each section independently toggleable in `pal-settings.json → dynamicContext`
|
|
370
378
|
|
|
371
|
-
###
|
|
379
|
+
### Semi-Static Registry
|
|
380
|
+
|
|
381
|
+
All semi-static sources are defined in `src/hooks/lib/semi-static.ts` via `getSemiStaticSources()`. Adding one entry there propagates automatically to every consumer — no other files need to change.
|
|
382
|
+
|
|
383
|
+
### Dynamic Injection Order
|
|
372
384
|
|
|
373
385
|
```
|
|
374
386
|
LoadContext.ts
|
|
375
387
|
│
|
|
376
388
|
├─► Regenerate CLAUDE.md if template/telos changed
|
|
377
389
|
│
|
|
378
|
-
└─► Build system-reminder:
|
|
390
|
+
└─► Build system-reminder (dynamic sections only):
|
|
379
391
|
1. loadAtStartup files (user-configured)
|
|
380
|
-
2.
|
|
381
|
-
3.
|
|
382
|
-
4.
|
|
383
|
-
5.
|
|
384
|
-
6.
|
|
385
|
-
7.
|
|
386
|
-
8.
|
|
387
|
-
9. Active work summary (sessions + projects)
|
|
392
|
+
2. Handoff note (in-progress work from last session)
|
|
393
|
+
3. Session intelligence (rating trend, algorithm performance)
|
|
394
|
+
4. Open threads (current project only)
|
|
395
|
+
5. Recent interaction notes (last 2 days)
|
|
396
|
+
6. Active projects
|
|
397
|
+
7. Project session history (this project)
|
|
398
|
+
8. Signal trends (today/week/trend)
|
|
388
399
|
```
|
|
389
400
|
|
|
390
401
|
### On-Demand Context
|
|
@@ -436,6 +447,9 @@ src/targets/
|
|
|
436
447
|
├── cursor/ # Cursor specific
|
|
437
448
|
│ ├── install.ts # Register hooks + skills in ~/.cursor/
|
|
438
449
|
│ └── uninstall.ts
|
|
450
|
+
├── copilot/ # GitHub Copilot specific
|
|
451
|
+
│ ├── install.ts # Write instruction files + update VS Code settings
|
|
452
|
+
│ └── uninstall.ts
|
|
439
453
|
└── lib.ts # Shared: JSON read/write, settings merge, TELOS scaffold
|
|
440
454
|
```
|
|
441
455
|
|
|
@@ -452,6 +466,7 @@ All paths resolve through `src/hooks/lib/paths.ts`:
|
|
|
452
466
|
| Claude config | `~/.claude` | `PAL_CLAUDE_DIR` |
|
|
453
467
|
| opencode config | `~/.config/opencode` | `PAL_OPENCODE_DIR` |
|
|
454
468
|
| Cursor config | `~/.cursor` | `PAL_CURSOR_DIR` |
|
|
469
|
+
| Copilot config | `~/.copilot` | `PAL_COPILOT_DIR` |
|
|
455
470
|
| Codex config | `~/.codex` | `PAL_CODEX_DIR` |
|
|
456
471
|
| Agents dir | `~/.agents` | `PAL_AGENTS_DIR` |
|
|
457
472
|
|
|
@@ -4,4 +4,4 @@ PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captu
|
|
|
4
4
|
|
|
5
5
|
## Projects
|
|
6
6
|
|
|
7
|
-
Projects are managed in
|
|
7
|
+
Projects are managed via the `/projects` skill. State lives in `~/.pal/memory/state/progress/{slug}.json`, one file per project. Inspect with `bun ~/.pal/tools/project.ts list`; manage via `~/.pal/docs/PROJECT_LIFECYCLE.md`.
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"loadAtStartup": {
|
|
15
15
|
"_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
|
|
16
|
-
"files": [
|
|
17
|
-
"~/.pal/docs/STEERING_RULES.md"
|
|
18
|
-
]
|
|
16
|
+
"files": []
|
|
19
17
|
},
|
|
20
18
|
"dynamicContext": {
|
|
21
19
|
"_docs": "Dynamic context sections injected at session start. Set to false to disable.",
|
package/package.json
CHANGED
package/src/cli/setup-telos.ts
CHANGED
|
@@ -7,72 +7,9 @@
|
|
|
7
7
|
import { writeFileSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import * as clack from "@clack/prompts";
|
|
10
|
-
import { upsertProject } from "../../assets/skills/telos/tools/update-projects";
|
|
11
10
|
import { palHome } from "../hooks/lib/paths";
|
|
12
11
|
import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
|
|
13
12
|
|
|
14
|
-
function toKebabCase(name: string): string {
|
|
15
|
-
return name
|
|
16
|
-
.toLowerCase()
|
|
17
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
-
.replace(/^-|-$/g, "");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function promptProjectsLoop(): Promise<void> {
|
|
22
|
-
const addFirst = await clack.confirm({
|
|
23
|
-
message: "Do you want to add any projects now?",
|
|
24
|
-
initialValue: true,
|
|
25
|
-
});
|
|
26
|
-
if (clack.isCancel(addFirst) || !addFirst) return;
|
|
27
|
-
|
|
28
|
-
let addMore = true;
|
|
29
|
-
while (addMore) {
|
|
30
|
-
const name = await clack.text({
|
|
31
|
-
message: "Project name?",
|
|
32
|
-
placeholder: "e.g. PAL, My SaaS, Work Dashboard",
|
|
33
|
-
});
|
|
34
|
-
if (clack.isCancel(name)) return;
|
|
35
|
-
|
|
36
|
-
const status = await clack.select({
|
|
37
|
-
message: "Status?",
|
|
38
|
-
options: [
|
|
39
|
-
{ value: "Active", label: "Active" },
|
|
40
|
-
{ value: "Planning", label: "Planning" },
|
|
41
|
-
{ value: "Paused", label: "Paused" },
|
|
42
|
-
{ value: "Complete", label: "Complete" },
|
|
43
|
-
],
|
|
44
|
-
});
|
|
45
|
-
if (clack.isCancel(status)) return;
|
|
46
|
-
|
|
47
|
-
const priority = await clack.select({
|
|
48
|
-
message: "Priority?",
|
|
49
|
-
options: [
|
|
50
|
-
{ value: "High", label: "High" },
|
|
51
|
-
{ value: "Medium", label: "Medium" },
|
|
52
|
-
{ value: "Low", label: "Low" },
|
|
53
|
-
],
|
|
54
|
-
});
|
|
55
|
-
if (clack.isCancel(priority)) return;
|
|
56
|
-
|
|
57
|
-
const notes = await clack.text({
|
|
58
|
-
message: "Notes? (optional — leave blank to skip)",
|
|
59
|
-
placeholder: "e.g. Building the v2 API, blocked on design review",
|
|
60
|
-
});
|
|
61
|
-
if (clack.isCancel(notes)) return;
|
|
62
|
-
|
|
63
|
-
const id = toKebabCase(name as string);
|
|
64
|
-
const row = `| ${id} | ${name} | ${status} | ${priority} | ${notes || ""} |`;
|
|
65
|
-
upsertProject(id, row, `Added ${name} during PAL setup`);
|
|
66
|
-
clack.log.success(`Added: ${name}`);
|
|
67
|
-
|
|
68
|
-
const again = await clack.confirm({
|
|
69
|
-
message: "Add another project?",
|
|
70
|
-
initialValue: false,
|
|
71
|
-
});
|
|
72
|
-
if (clack.isCancel(again) || !again) addMore = false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
13
|
/** Prompt for missing TELOS context. Skips any step whose file already has real content. */
|
|
77
14
|
export async function promptTelos(): Promise<void> {
|
|
78
15
|
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
@@ -95,25 +32,21 @@ export async function promptTelos(): Promise<void> {
|
|
|
95
32
|
);
|
|
96
33
|
|
|
97
34
|
for (const key of pending) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
} else {
|
|
101
|
-
const step = SETUP_STEPS[key];
|
|
102
|
-
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
103
|
-
|
|
104
|
-
const answer = await clack.text({
|
|
105
|
-
message: step.question,
|
|
106
|
-
placeholder: step.hint,
|
|
107
|
-
});
|
|
35
|
+
const step = SETUP_STEPS[key];
|
|
36
|
+
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
108
37
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
38
|
+
const answer = await clack.text({
|
|
39
|
+
message: step.question,
|
|
40
|
+
placeholder: step.hint,
|
|
41
|
+
});
|
|
113
42
|
|
|
114
|
-
|
|
115
|
-
|
|
43
|
+
if (clack.isCancel(answer)) {
|
|
44
|
+
clack.cancel("Setup cancelled");
|
|
45
|
+
return;
|
|
116
46
|
}
|
|
47
|
+
|
|
48
|
+
const filePath = resolve(home, step.file);
|
|
49
|
+
writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
|
|
117
50
|
}
|
|
118
51
|
|
|
119
52
|
clack.outro("Personal context saved ✓");
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* context directly to ~/.copilot/copilot-instructions.md so it is picked up on load.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
14
|
import { buildClaudeMd, regenerateIfNeeded } from "./lib/claude-md";
|
|
15
|
-
import { buildSystemReminder } from "./lib/context";
|
|
15
|
+
import { type AgentTarget, buildSystemReminder } from "./lib/context";
|
|
16
16
|
import { logDebug, logError } from "./lib/log";
|
|
17
17
|
import { platform } from "./lib/paths";
|
|
18
18
|
|
|
@@ -36,21 +36,33 @@ try {
|
|
|
36
36
|
|
|
37
37
|
// --- Context to stdout (or file for Copilot) ---
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
// Determine agent target — controls which sections are skipped (loaded natively instead).
|
|
40
|
+
let agent: AgentTarget = "claude";
|
|
41
|
+
if (process.env.PAL_AGENT === "copilot") agent = "copilot";
|
|
42
|
+
else if (process.env.CURSOR_VERSION) agent = "cursor";
|
|
43
|
+
const reminder = buildSystemReminder({ agent });
|
|
40
44
|
if (!reminder) process.exit(0);
|
|
41
45
|
|
|
42
46
|
if (process.env.PAL_AGENT === "copilot") {
|
|
43
|
-
// Copilot:
|
|
44
|
-
|
|
47
|
+
// Copilot: semi-static in ~/.copilot/instructions/pal-*.instructions.md (written at stop).
|
|
48
|
+
// Write AGENTS.md + dynamic context to pal-session.instructions.md on each session start.
|
|
49
|
+
const instructionsDir = resolve(platform.copilotDir(), "instructions");
|
|
50
|
+
mkdirSync(instructionsDir, { recursive: true });
|
|
45
51
|
const agentsMd = buildClaudeMd();
|
|
46
52
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
47
|
-
if (
|
|
48
|
-
|
|
53
|
+
if (context) {
|
|
54
|
+
writeFileSync(
|
|
55
|
+
resolve(instructionsDir, "pal-session.instructions.md"),
|
|
56
|
+
`---\napplyTo: "**"\n---\n\n${context}`,
|
|
57
|
+
"utf-8"
|
|
58
|
+
);
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
logDebug(
|
|
61
|
+
"LoadContext",
|
|
62
|
+
`Copilot session instructions written: ${context.length} chars`
|
|
63
|
+
);
|
|
52
64
|
} else if (process.env.CURSOR_VERSION) {
|
|
53
|
-
// Cursor:
|
|
65
|
+
// Cursor: semi-static in ~/.cursor/rules/pal-context.mdc; inject AGENTS.md + dynamic here
|
|
54
66
|
const agentsMd = buildClaudeMd();
|
|
55
67
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
56
68
|
process.stdout.write(JSON.stringify({ additional_context: context }));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler: write pre-compiled context digest files for @import / instructions[].
|
|
3
|
+
*
|
|
4
|
+
* Runs at session stop so that CLAUDE.md can @import these files natively
|
|
5
|
+
* at the next session start, keeping hook stdout small.
|
|
6
|
+
*
|
|
7
|
+
* Sources are defined in src/hooks/lib/semi-static.ts — add one entry there
|
|
8
|
+
* to extend coverage to all consumers (CLAUDE.md, opencode, Cursor, Copilot).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
13
|
+
import { ensureDir, platform } from "../lib/paths";
|
|
14
|
+
import {
|
|
15
|
+
copilotFilename,
|
|
16
|
+
cursorFilename,
|
|
17
|
+
getSemiStaticSources,
|
|
18
|
+
} from "../lib/semi-static";
|
|
19
|
+
|
|
20
|
+
export function writeContextDigests(): void {
|
|
21
|
+
const sources = getSemiStaticSources();
|
|
22
|
+
|
|
23
|
+
// Resolve Cursor/Copilot destination dirs once (null if agent not installed)
|
|
24
|
+
let rulesDir: string | null = null;
|
|
25
|
+
let instructionsDir: string | null = null;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const cursorDir = platform.cursorDir();
|
|
29
|
+
if (existsSync(cursorDir)) {
|
|
30
|
+
rulesDir = ensureDir(resolve(cursorDir, "rules"));
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
/* non-fatal */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const copilotDir = platform.copilotDir();
|
|
38
|
+
if (existsSync(copilotDir)) {
|
|
39
|
+
instructionsDir = ensureDir(resolve(copilotDir, "instructions"));
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
/* non-fatal */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const src of sources) {
|
|
46
|
+
try {
|
|
47
|
+
const content = src.load();
|
|
48
|
+
if (!content) continue;
|
|
49
|
+
|
|
50
|
+
if (src.writesDigest) {
|
|
51
|
+
ensureDir(dirname(src.path));
|
|
52
|
+
writeFileSync(src.path, content, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (rulesDir) {
|
|
56
|
+
writeFileSync(
|
|
57
|
+
resolve(rulesDir, cursorFilename(src)),
|
|
58
|
+
`---\ndescription: ${src.description}\nalwaysApply: true\n---\n\n${content}`,
|
|
59
|
+
"utf-8"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (instructionsDir) {
|
|
64
|
+
writeFileSync(
|
|
65
|
+
resolve(instructionsDir, copilotFilename(src)),
|
|
66
|
+
`---\napplyTo: "**"\n---\n\n${content}`,
|
|
67
|
+
"utf-8"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* non-fatal */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stop handler: unified session intelligence capture.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Writes: session learning file, project history, relationship notes, last-handoff.
|
|
4
|
+
* Produces: title, summary, insights via Haiku.
|
|
5
|
+
* Writes: session learning file, project history.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Relationship notes → written in ALGORITHM LEARN phase via relationship-note.ts
|
|
8
|
+
* Handoff notes → written in ALGORITHM LEARN phase via handoff-note.ts
|
|
9
|
+
*
|
|
10
|
+
* Replaces: work-learning.ts (still exists but is bypassed).
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -15,7 +17,6 @@ import { inference } from "../lib/inference";
|
|
|
15
17
|
import { categorizeLearning } from "../lib/learning-category";
|
|
16
18
|
import { logDebug, logError } from "../lib/log";
|
|
17
19
|
import { ensureDir, paths } from "../lib/paths";
|
|
18
|
-
import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
|
|
19
20
|
import { fileTimestamp, monthPath } from "../lib/time";
|
|
20
21
|
import { logTokenUsage } from "../lib/token-usage";
|
|
21
22
|
import {
|
|
@@ -110,25 +111,8 @@ const INTELLIGENCE_SCHEMA = {
|
|
|
110
111
|
description:
|
|
111
112
|
"If status is in-progress: what remains to be done, key decisions made, blockers. If completed: empty string.",
|
|
112
113
|
},
|
|
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
114
|
},
|
|
131
|
-
required: ["title", "summary", "insights", "handoff"
|
|
115
|
+
required: ["title", "summary", "insights", "handoff"] as const,
|
|
132
116
|
};
|
|
133
117
|
|
|
134
118
|
interface IntelligenceOutput {
|
|
@@ -136,7 +120,6 @@ interface IntelligenceOutput {
|
|
|
136
120
|
summary: string;
|
|
137
121
|
insights: string;
|
|
138
122
|
handoff: string;
|
|
139
|
-
observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
|
|
140
123
|
}
|
|
141
124
|
|
|
142
125
|
// ── Main handler ──
|
|
@@ -160,9 +143,6 @@ export async function captureSessionIntelligence(
|
|
|
160
143
|
return;
|
|
161
144
|
}
|
|
162
145
|
|
|
163
|
-
// Relationship dedup — skip relationship capture if already done for this session
|
|
164
|
-
const skipRelationship = sessionId ? hasSessionNotes(sessionId) : false;
|
|
165
|
-
|
|
166
146
|
// Extract transcript windows
|
|
167
147
|
const userMessages = messages
|
|
168
148
|
.filter((m) => m.role === "user")
|
|
@@ -174,8 +154,7 @@ export async function captureSessionIntelligence(
|
|
|
174
154
|
const lastUser = extractLastUser(messages);
|
|
175
155
|
const status = detectStatus(lastAssistantText);
|
|
176
156
|
|
|
177
|
-
|
|
178
|
-
const userWindow = userMessages.slice(-15).map((t) => t.slice(0, 200));
|
|
157
|
+
const userWindow = userMessages.slice(-10).map((t) => t.slice(0, 200));
|
|
179
158
|
const assistantWindow = lastAssistantText.slice(0, 600);
|
|
180
159
|
|
|
181
160
|
if (userWindow.length < 3) return;
|
|
@@ -195,12 +174,9 @@ export async function captureSessionIntelligence(
|
|
|
195
174
|
status === "in-progress"
|
|
196
175
|
? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
|
|
197
176
|
: "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
177
|
].join("\n"),
|
|
202
178
|
user: `User messages:\n${userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n\nLast AI response:\n${assistantWindow}`,
|
|
203
|
-
maxTokens:
|
|
179
|
+
maxTokens: 350,
|
|
204
180
|
timeout: 15000,
|
|
205
181
|
jsonSchema: INTELLIGENCE_SCHEMA,
|
|
206
182
|
});
|
|
@@ -218,8 +194,6 @@ export async function captureSessionIntelligence(
|
|
|
218
194
|
const title = output?.title || extractContent(lastUser).slice(0, 80) || "session";
|
|
219
195
|
const summary = output?.summary || lastAssistantText.slice(0, 600);
|
|
220
196
|
const insights = output?.insights || "";
|
|
221
|
-
const handoff = output?.handoff || "";
|
|
222
|
-
|
|
223
197
|
// ── Write session learning file ──
|
|
224
198
|
|
|
225
199
|
const category = categorizeLearning(title, summary);
|
|
@@ -241,7 +215,6 @@ export async function captureSessionIntelligence(
|
|
|
241
215
|
"",
|
|
242
216
|
"## Insights",
|
|
243
217
|
insights || "*No insights captured.*",
|
|
244
|
-
...(handoff ? ["", "## Handoff", handoff] : []),
|
|
245
218
|
].join("\n");
|
|
246
219
|
|
|
247
220
|
const content = stringify(meta, body);
|
|
@@ -271,54 +244,4 @@ export async function captureSessionIntelligence(
|
|
|
271
244
|
|
|
272
245
|
if (sessionId) markCaptured(sessionId, filepath, messages.length);
|
|
273
246
|
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
247
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dynamic AGENTS.md generation.
|
|
2
|
+
* Dynamic AGENTS.md / CLAUDE.md generation.
|
|
3
3
|
*
|
|
4
|
-
* AGENTS.md is regenerated when setup.json or any
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* AGENTS.md (opencode, codex, copilot) is regenerated when setup.json or any
|
|
5
|
+
* telos file is newer. CLAUDE.md (Claude Code) is a real file — not a symlink —
|
|
6
|
+
* and prepends an @import for the self-model so that large static context loads
|
|
7
|
+
* natively rather than through the hook's stdout.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "node:fs";
|
|
20
21
|
import { dirname, relative, resolve } from "node:path";
|
|
21
22
|
import { assets, ensureDir, paths, platform } from "./paths";
|
|
23
|
+
import { getSemiStaticSources } from "./semi-static";
|
|
22
24
|
|
|
23
25
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
24
26
|
|
|
@@ -70,16 +72,12 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/** Ensure
|
|
75
|
+
/** Ensure codex symlink points to the canonical AGENTS.md.
|
|
76
|
+
* CLAUDE.md for Claude Code is a real file written by ensureClaudeCodeMd().
|
|
77
|
+
* Copilot uses ~/.copilot/instructions/*.instructions.md — no symlink needed. */
|
|
74
78
|
function ensureSymlinks(): void {
|
|
75
|
-
const { outputPath
|
|
76
|
-
ensureOneSymlink(symlinkPath, outputPath);
|
|
79
|
+
const { outputPath } = getOutputPaths();
|
|
77
80
|
ensureOneSymlink(resolve(platform.codexDir(), "AGENTS.md"), outputPath);
|
|
78
|
-
// Copilot instructions — only create if ~/.copilot/ already exists (i.e. Copilot is installed)
|
|
79
|
-
const copilotDir = platform.copilotDir();
|
|
80
|
-
if (existsSync(copilotDir)) {
|
|
81
|
-
ensureOneSymlink(resolve(copilotDir, "copilot-instructions.md"), outputPath);
|
|
82
|
-
}
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
/** Returns true if AGENTS.md needs to be regenerated */
|
|
@@ -89,11 +87,12 @@ export function needsRebuild(): boolean {
|
|
|
89
87
|
|
|
90
88
|
const outputMtime = statSync(outputPath).mtimeMs;
|
|
91
89
|
|
|
92
|
-
// Collect source files: template + setup.json + identity + PAL docs
|
|
90
|
+
// Collect source files: template + setup.json + identity + PAL docs + @import candidates
|
|
93
91
|
const sources: string[] = [
|
|
94
92
|
TEMPLATE_PATH,
|
|
95
93
|
resolve(paths.state(), "setup.json"),
|
|
96
94
|
resolve(paths.memory(), "pal-settings.json"),
|
|
95
|
+
...getSemiStaticSources().map((s) => s.path),
|
|
97
96
|
];
|
|
98
97
|
|
|
99
98
|
// Track PAL doc sources for rebuild detection
|
|
@@ -124,15 +123,71 @@ export function buildClaudeMd(): string {
|
|
|
124
123
|
.replaceAll("{{PRINCIPAL_NAME}}", id.principal.name);
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
/**
|
|
126
|
+
/** Build @import header lines for CLAUDE.md — one line per semi-static file that exists. */
|
|
127
|
+
function buildClaudeCodeImports(): string {
|
|
128
|
+
const claudeDir = platform.claudeDir();
|
|
129
|
+
|
|
130
|
+
const lines = getSemiStaticSources()
|
|
131
|
+
.map((s) => s.path)
|
|
132
|
+
.filter((p) => existsSync(p))
|
|
133
|
+
.map((p) => `@${relative(claudeDir, p).replaceAll("\\", "/")}`);
|
|
134
|
+
|
|
135
|
+
return lines.length > 0 ? `${lines.join("\n")}\n\n` : "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build CLAUDE.md content for Claude Code — prepends @import for self-model. */
|
|
139
|
+
export function buildClaudeCodeMd(): string {
|
|
140
|
+
return buildClaudeCodeImports() + buildClaudeMd();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Write ~/.claude/CLAUDE.md as a real file (upgrading from symlink if needed).
|
|
144
|
+
* Also rewrites if the @import header has changed (new digest files appeared). */
|
|
145
|
+
function ensureClaudeCodeMd(): void {
|
|
146
|
+
const claudeDir = platform.claudeDir();
|
|
147
|
+
if (!claudeDir) return;
|
|
148
|
+
const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
|
|
149
|
+
const expected = buildClaudeCodeMd();
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(claudeMdPath) && !lstatSync(claudeMdPath).isSymbolicLink()) {
|
|
152
|
+
const current = readFileSync(claudeMdPath, "utf-8");
|
|
153
|
+
if (current === expected) return; // no change needed
|
|
154
|
+
// @imports changed — rewrite
|
|
155
|
+
} else if (existsSync(claudeMdPath)) {
|
|
156
|
+
unlinkSync(claudeMdPath); // remove symlink
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* fall through */
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
ensureDir(claudeDir);
|
|
163
|
+
writeFileSync(claudeMdPath, expected, "utf-8");
|
|
164
|
+
} catch {
|
|
165
|
+
/* ignore write errors — non-fatal */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Regenerate AGENTS.md if any source file is newer, write real CLAUDE.md, ensure other symlinks. Returns true if rebuilt. */
|
|
128
170
|
export function regenerateIfNeeded(): boolean {
|
|
129
171
|
const { outputPath } = getOutputPaths();
|
|
130
172
|
if (!needsRebuild()) {
|
|
131
173
|
ensureSymlinks();
|
|
174
|
+
ensureClaudeCodeMd();
|
|
132
175
|
return false;
|
|
133
176
|
}
|
|
134
177
|
ensureDir(dirname(outputPath));
|
|
135
178
|
writeFileSync(outputPath, buildClaudeMd(), "utf-8");
|
|
179
|
+
// Write Claude Code's CLAUDE.md as a real file (removing any existing symlink)
|
|
180
|
+
const claudeDir = platform.claudeDir();
|
|
181
|
+
if (claudeDir) {
|
|
182
|
+
const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
|
|
183
|
+
try {
|
|
184
|
+
if (existsSync(claudeMdPath)) unlinkSync(claudeMdPath);
|
|
185
|
+
ensureDir(claudeDir);
|
|
186
|
+
writeFileSync(claudeMdPath, buildClaudeCodeMd(), "utf-8");
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore — CLAUDE.md write failure is non-fatal */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
136
191
|
ensureSymlinks();
|
|
137
192
|
return true;
|
|
138
193
|
}
|