portable-agent-layer 0.35.0 → 0.37.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 +2 -1
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -21
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +54 -5
- 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/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +12 -80
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +35 -11
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +17 -93
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +92 -246
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +4 -16
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +24 -18
- package/src/hooks/lib/semi-static.ts +188 -0
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -65
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +16 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- 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 +166 -14
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +23 -12
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +15 -13
- package/src/tools/self-model.ts +23 -19
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
- package/assets/templates/telos/PROJECTS.md +0 -7
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semi-static context source registry.
|
|
3
|
+
*
|
|
4
|
+
* One entry here = the only change needed to add a new source across all consumers:
|
|
5
|
+
* CLAUDE.md @imports, opencode instructions[], Cursor .mdc, Copilot .instructions.md,
|
|
6
|
+
* and the session-stop digest writer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { parse } from "./frontmatter";
|
|
12
|
+
import { readFailures } from "./learning-store";
|
|
13
|
+
import { loadOpinionContext } from "./opinions";
|
|
14
|
+
import { palHome, paths } from "./paths";
|
|
15
|
+
import { readFramePrinciples } from "./wisdom";
|
|
16
|
+
|
|
17
|
+
/** A single semi-static context source — built at session stop, loaded natively at session start. */
|
|
18
|
+
interface SemiStaticSource {
|
|
19
|
+
/** Absolute path used in @imports (CLAUDE.md), instructions[] (opencode), and digest writes. */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
/** When true, session-stop handler writes build result to path. */
|
|
22
|
+
readonly writesDigest: boolean;
|
|
23
|
+
/** Returns current content — builds fresh when writesDigest is true, reads the file otherwise. */
|
|
24
|
+
load(): string;
|
|
25
|
+
/** Slug for ~/.cursor/rules/pal-${slug}.mdc and ~/.copilot/instructions/pal-${slug}.instructions.md */
|
|
26
|
+
readonly slug: string;
|
|
27
|
+
/** Human-readable description for Cursor .mdc frontmatter. */
|
|
28
|
+
readonly description: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns the Cursor rules filename for a source. */
|
|
32
|
+
export function cursorFilename(src: SemiStaticSource): string {
|
|
33
|
+
return `pal-${src.slug}.mdc`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Returns the Copilot instructions filename for a source. */
|
|
37
|
+
export function copilotFilename(src: SemiStaticSource): string {
|
|
38
|
+
return `pal-${src.slug}.instructions.md`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readFileSafe(path: string): string {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(path)) return "";
|
|
44
|
+
return readFileSync(path, "utf-8").trim();
|
|
45
|
+
} catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
/** Build the 5 most recent failure lessons as an avoid-list. */
|
|
104
|
+
export function loadFailurePatterns(): string {
|
|
105
|
+
try {
|
|
106
|
+
const entries = readFailures(paths.failures(), 5);
|
|
107
|
+
if (entries.length === 0) return "";
|
|
108
|
+
|
|
109
|
+
const lines = entries.map((e) => {
|
|
110
|
+
const label = e.rating ? `[${e.rating}/10]` : "";
|
|
111
|
+
const text = e.principle || e.context;
|
|
112
|
+
return `- ${label} ${text}`.trim();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
|
|
116
|
+
} catch {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* All semi-static context sources in load order.
|
|
123
|
+
* Adding one entry here is the only change needed to extend coverage to all consumers.
|
|
124
|
+
*/
|
|
125
|
+
export function getSemiStaticSources(): SemiStaticSource[] {
|
|
126
|
+
const memory = paths.memory();
|
|
127
|
+
const home = palHome();
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
path: resolve(memory, "self-model", "current.md"),
|
|
131
|
+
writesDigest: false,
|
|
132
|
+
load: () => readFileSafe(resolve(memory, "self-model", "current.md")),
|
|
133
|
+
slug: "self-model",
|
|
134
|
+
description: "PAL self-model",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
path: resolve(memory, "wisdom", "context.md"),
|
|
138
|
+
writesDigest: true,
|
|
139
|
+
load: () => {
|
|
140
|
+
try {
|
|
141
|
+
const principles = readFramePrinciples();
|
|
142
|
+
if (principles.length === 0) return "";
|
|
143
|
+
return ["## Crystallized Principles", ...principles.map((p) => `- ${p}`)].join(
|
|
144
|
+
"\n"
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
slug: "wisdom",
|
|
151
|
+
description: "PAL wisdom",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: resolve(memory, "relationship", "opinions-context.md"),
|
|
155
|
+
writesDigest: true,
|
|
156
|
+
load: () => {
|
|
157
|
+
try {
|
|
158
|
+
return loadOpinionContext();
|
|
159
|
+
} catch {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
slug: "opinions",
|
|
164
|
+
description: "PAL opinions",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
path: resolve(memory, "learning", "synthesis-digest.md"),
|
|
168
|
+
writesDigest: true,
|
|
169
|
+
load: loadSynthesisRecommendations,
|
|
170
|
+
slug: "synthesis",
|
|
171
|
+
description: "PAL pattern synthesis",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
path: resolve(memory, "learning", "failures-digest.md"),
|
|
175
|
+
writesDigest: true,
|
|
176
|
+
load: loadFailurePatterns,
|
|
177
|
+
slug: "failures",
|
|
178
|
+
description: "PAL recent failure lessons",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
path: resolve(home, "docs", "STEERING_RULES.md"),
|
|
182
|
+
writesDigest: false,
|
|
183
|
+
load: () => readFileSafe(resolve(home, "docs", "STEERING_RULES.md")),
|
|
184
|
+
slug: "steering",
|
|
185
|
+
description: "PAL steering rules",
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
}
|
package/src/hooks/lib/setup.ts
CHANGED
|
@@ -5,23 +5,15 @@
|
|
|
5
5
|
* The AI is instructed to mark steps done after writing each file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
-
import { ensureDir, palHome, paths } from "./paths";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
interface SetupStep {
|
|
13
11
|
done: boolean;
|
|
14
12
|
file: string;
|
|
15
13
|
question: string;
|
|
16
14
|
hint: string;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
export interface SetupState {
|
|
20
|
-
version: number;
|
|
21
|
-
completed: boolean;
|
|
22
|
-
steps: Record<string, SetupStep>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
/** Ordered setup steps — defines the wizard flow */
|
|
26
18
|
export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
27
19
|
mission: {
|
|
@@ -36,11 +28,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
|
36
28
|
"What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
|
|
37
29
|
hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
|
|
38
30
|
},
|
|
39
|
-
projects: {
|
|
40
|
-
file: "telos/PROJECTS.md",
|
|
41
|
-
question: "What projects are you currently working on? (~/.pal/telos/PROJECTS.md)",
|
|
42
|
-
hint: "e.g. PAL (active, high priority), personal blog (paused), side SaaS (early stage)",
|
|
43
|
-
},
|
|
44
31
|
beliefs: {
|
|
45
32
|
file: "telos/BELIEFS.md",
|
|
46
33
|
question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
|
|
@@ -55,10 +42,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
|
55
42
|
|
|
56
43
|
export const STEP_ORDER = Object.keys(SETUP_STEPS);
|
|
57
44
|
|
|
58
|
-
function setupPath(): string {
|
|
59
|
-
return resolve(ensureDir(paths.state()), "setup.json");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
45
|
/** Check if a TELOS file has real content (not just template scaffolding) */
|
|
63
46
|
export function hasRealContent(filePath: string): boolean {
|
|
64
47
|
if (!existsSync(filePath)) return false;
|
|
@@ -75,49 +58,3 @@ export function hasRealContent(filePath: string): boolean {
|
|
|
75
58
|
return false;
|
|
76
59
|
}
|
|
77
60
|
}
|
|
78
|
-
|
|
79
|
-
/** Create initial setup state, auto-detecting already-populated TELOS files */
|
|
80
|
-
export function createInitialState(): SetupState {
|
|
81
|
-
const steps: Record<string, SetupStep> = {};
|
|
82
|
-
for (const [key, def] of Object.entries(SETUP_STEPS)) {
|
|
83
|
-
const populated = hasRealContent(resolve(palHome(), def.file));
|
|
84
|
-
steps[key] = { done: populated, ...def };
|
|
85
|
-
}
|
|
86
|
-
const allDone = Object.values(steps).every((s) => s.done);
|
|
87
|
-
return { version: 1, completed: allDone, steps };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Read setup state, or return null if no setup.json exists */
|
|
91
|
-
export function readSetupState(): SetupState | null {
|
|
92
|
-
const p = setupPath();
|
|
93
|
-
if (!existsSync(p)) return null;
|
|
94
|
-
try {
|
|
95
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Write setup state to disk */
|
|
102
|
-
export function writeSetupState(state: SetupState): void {
|
|
103
|
-
writeFileSync(setupPath(), `${JSON.stringify(state, null, 2)}\n`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Seed setup.json if it doesn't exist yet. Returns the state. */
|
|
107
|
-
export function ensureSetupState(): SetupState {
|
|
108
|
-
const existing = readSetupState();
|
|
109
|
-
if (existing) return existing;
|
|
110
|
-
const fresh = createInitialState();
|
|
111
|
-
writeSetupState(fresh);
|
|
112
|
-
return fresh;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Get the list of remaining (not done) step keys, in order */
|
|
116
|
-
export function remainingSteps(state: SetupState): string[] {
|
|
117
|
-
return STEP_ORDER.filter((k) => !state.steps[k]?.done);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Check if setup is fully completed */
|
|
121
|
-
export function isSetupComplete(state: SetupState): boolean {
|
|
122
|
-
return state.completed;
|
|
123
|
-
}
|
package/src/hooks/lib/signals.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { paths } from "./paths";
|
|
4
4
|
import { now } from "./time";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface Signal {
|
|
7
7
|
ts: string;
|
|
8
8
|
type: string;
|
|
9
9
|
[key: string]: unknown;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Append a signal to a JSONL file in the signals directory */
|
|
13
|
-
|
|
13
|
+
function emitSignal(
|
|
14
14
|
filename: string,
|
|
15
15
|
data: { type: string; [key: string]: unknown }
|
|
16
16
|
): void {
|
package/src/hooks/lib/stdin.ts
CHANGED
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Used by StopOrchestrator.ts (Claude Code) and opencode plugin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync,
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
7
8
|
import { resolve } from "node:path";
|
|
8
9
|
import { autoGraduate } from "../handlers/auto-graduate";
|
|
9
10
|
import { autoBackup } from "../handlers/backup";
|
|
11
|
+
import { writeContextDigests } from "../handlers/context-digests";
|
|
10
12
|
import { notifyDesktop } from "../handlers/desktop-notify";
|
|
11
13
|
import { captureFailure } from "../handlers/failure";
|
|
14
|
+
import { persistLastExchange } from "../handlers/persist-last-exchange";
|
|
12
15
|
import { projectTouch } from "../handlers/project-touch";
|
|
13
16
|
import { checkReflectTrigger } from "../handlers/reflect-trigger";
|
|
14
17
|
import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
|
|
@@ -22,7 +25,7 @@ import { logDebug, logError } from "./log";
|
|
|
22
25
|
import { ensureDir, paths } from "./paths";
|
|
23
26
|
import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
interface RunStopHandlersOptions {
|
|
26
29
|
lastAssistantMessage?: string;
|
|
27
30
|
sessionId?: string;
|
|
28
31
|
}
|
|
@@ -40,6 +43,9 @@ export async function runStopHandlers(
|
|
|
40
43
|
// Cache last assistant response (session-scoped)
|
|
41
44
|
cacheLastResponse(messages, options.lastAssistantMessage, options.sessionId);
|
|
42
45
|
|
|
46
|
+
// Always persist last exchange — drives CompactRecover + "Pick Up Where You Left Off"
|
|
47
|
+
if (options.sessionId) persistLastExchange(messages, options.sessionId);
|
|
48
|
+
|
|
43
49
|
// Run all handlers concurrently. Auto-graduate is idempotent (24h TTL +
|
|
44
50
|
// state-dedup + content-dedup) so it's safe to fire on every Stop.
|
|
45
51
|
// project-touch only fires when cwd resolves to an active registered project.
|
|
@@ -56,6 +62,7 @@ export async function runStopHandlers(
|
|
|
56
62
|
autoGraduate(),
|
|
57
63
|
projectTouch(options.lastAssistantMessage),
|
|
58
64
|
notifyDesktop(options.sessionId),
|
|
65
|
+
Promise.resolve(writeContextDigests()),
|
|
59
66
|
]);
|
|
60
67
|
|
|
61
68
|
const handlerNames = [
|
|
@@ -71,6 +78,7 @@ export async function runStopHandlers(
|
|
|
71
78
|
"auto-graduate",
|
|
72
79
|
"project-touch",
|
|
73
80
|
"desktop-notify",
|
|
81
|
+
"context-digests",
|
|
74
82
|
];
|
|
75
83
|
for (let i = 0; i < results.length; i++) {
|
|
76
84
|
const r = results[i];
|
|
@@ -148,15 +156,16 @@ async function checkPendingFailure(transcript: string): Promise<void> {
|
|
|
148
156
|
if (!existsSync(pendingPath)) return;
|
|
149
157
|
|
|
150
158
|
try {
|
|
151
|
-
const pending = JSON.parse(
|
|
159
|
+
const pending = JSON.parse(await readFile(pendingPath, "utf-8")) as {
|
|
152
160
|
rating: number;
|
|
153
161
|
context: string;
|
|
154
162
|
detailedContext?: string;
|
|
155
163
|
principle?: string;
|
|
156
164
|
responsePreview?: string;
|
|
157
165
|
userPreview?: string;
|
|
166
|
+
cwd?: string;
|
|
158
167
|
};
|
|
159
|
-
|
|
168
|
+
await unlink(pendingPath);
|
|
160
169
|
|
|
161
170
|
// Extract principle from full transcript if not already present
|
|
162
171
|
let { principle, detailedContext } = pending;
|
|
@@ -196,7 +205,7 @@ Return JSON:
|
|
|
196
205
|
detailed_context?: string;
|
|
197
206
|
};
|
|
198
207
|
principle = parsed.principle || undefined;
|
|
199
|
-
|
|
208
|
+
detailedContext ??= parsed.detailed_context || undefined;
|
|
200
209
|
}
|
|
201
210
|
} catch {
|
|
202
211
|
/* graceful fallback — capture without principle */
|
|
@@ -208,7 +217,8 @@ Return JSON:
|
|
|
208
217
|
pending.context,
|
|
209
218
|
transcript,
|
|
210
219
|
detailedContext,
|
|
211
|
-
principle
|
|
220
|
+
principle,
|
|
221
|
+
pending.cwd
|
|
212
222
|
);
|
|
213
223
|
} catch {
|
|
214
224
|
// Non-critical
|
|
@@ -8,10 +8,9 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { HAIKU_MODEL } from "./models";
|
|
9
9
|
import { ensureDir, paths } from "./paths";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type TokenCaller =
|
|
12
12
|
| "rating"
|
|
13
13
|
| "failure"
|
|
14
|
-
| "work-learning"
|
|
15
14
|
| "session-name"
|
|
16
15
|
| "session-intelligence"
|
|
17
16
|
| "relationship"
|
package/src/hooks/lib/wisdom.ts
CHANGED
|
@@ -63,13 +63,13 @@ function existingCrystalPrinciples(content: string): string[] {
|
|
|
63
63
|
if (m[1]) out.push(m[1].trim());
|
|
64
64
|
}
|
|
65
65
|
for (const line of content.split("\n")) {
|
|
66
|
-
const m =
|
|
66
|
+
const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/).exec(line);
|
|
67
67
|
if (m?.[1]) out.push(m[1].trim());
|
|
68
68
|
}
|
|
69
69
|
return out;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
interface PromoteCrystalResult {
|
|
73
73
|
domain: string;
|
|
74
74
|
principle: string;
|
|
75
75
|
confidence: number;
|
|
@@ -123,7 +123,7 @@ export function promoteCrystal(
|
|
|
123
123
|
return { domain, principle, confidence, framePath, skipped: null };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
interface FrameDoc {
|
|
127
127
|
domain: string;
|
|
128
128
|
principle: string;
|
|
129
129
|
body: string;
|
|
@@ -153,7 +153,7 @@ export function readFramesForRetrieval(): FrameDoc[] {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
for (const line of content.split("\n")) {
|
|
156
|
-
const m =
|
|
156
|
+
const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
|
|
157
157
|
if (!m) continue;
|
|
158
158
|
const name = m[1]?.trim();
|
|
159
159
|
const pct = parseInt(m[2] ?? "", 10);
|
|
@@ -188,7 +188,7 @@ export function readFramePrinciples(): string[] {
|
|
|
188
188
|
|
|
189
189
|
// legacy fallback: bullet lines "- X [CRYSTAL: N%]"
|
|
190
190
|
for (const line of content.split("\n")) {
|
|
191
|
-
const match =
|
|
191
|
+
const match = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
|
|
192
192
|
if (!match) continue;
|
|
193
193
|
const name = match[1]?.trim();
|
|
194
194
|
const pct = parseInt(match[2] ?? "", 10);
|
|
@@ -29,7 +29,7 @@ function sessionsPath(): string {
|
|
|
29
29
|
return resolve(ensureDir(paths.state()), "sessions.json");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function readSessions(): SessionRecord[] {
|
|
33
33
|
const p = sessionsPath();
|
|
34
34
|
if (!existsSync(p)) return [];
|
|
35
35
|
try {
|
|
@@ -54,12 +54,6 @@ export function writeSession(record: SessionRecord): void {
|
|
|
54
54
|
writeFileSync(sessionsPath(), JSON.stringify(pruned, null, 2), "utf-8");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** Filter sessions within the last N hours */
|
|
58
|
-
export function recentSessions(hours: number): SessionRecord[] {
|
|
59
|
-
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
60
|
-
return readSessions().filter((s) => new Date(s.ts).getTime() > cutoff);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
57
|
/** Detect session completion status from last assistant message */
|
|
64
58
|
export function detectStatus(lastAssistant: string): SessionRecord["status"] {
|
|
65
59
|
const completionSignals =
|
|
@@ -118,15 +112,15 @@ function cleanForHandoff(text: string): string {
|
|
|
118
112
|
/** Extract handoff notes from last assistant message */
|
|
119
113
|
export function extractHandoff(lastAssistant: string): string {
|
|
120
114
|
// Look for explicit next-steps / TODO / remaining sections
|
|
121
|
-
const sectionMatch =
|
|
115
|
+
const sectionMatch = new RegExp(
|
|
122
116
|
/(?:next steps?|todo|remaining|what's left|still need|want me to)[:\s]*\n([\s\S]{10,300}?)(?:\n\n|\n(?=[A-Z#]))/i
|
|
123
|
-
);
|
|
117
|
+
).exec(lastAssistant);
|
|
124
118
|
if (sectionMatch) return cleanForHandoff(sectionMatch[1]);
|
|
125
119
|
|
|
126
120
|
// Look for closing question/offer (common assistant pattern)
|
|
127
|
-
const closingMatch =
|
|
121
|
+
const closingMatch = new RegExp(
|
|
128
122
|
/(?:want (?:me to|to)|shall I|should I|ready to|anything else|let me know)[^\n]*$/im
|
|
129
|
-
);
|
|
123
|
+
).exec(lastAssistant);
|
|
130
124
|
|
|
131
125
|
const cleaned = cleanForHandoff(lastAssistant);
|
|
132
126
|
|
|
@@ -144,7 +138,7 @@ export function extractHandoff(lastAssistant: string): string {
|
|
|
144
138
|
|
|
145
139
|
// ── Per-Project History ──────────────────────────────────────────
|
|
146
140
|
|
|
147
|
-
|
|
141
|
+
interface ProjectHistoryEntry {
|
|
148
142
|
date: string;
|
|
149
143
|
title: string;
|
|
150
144
|
summary: string;
|
|
@@ -152,8 +146,8 @@ export interface ProjectHistoryEntry {
|
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
/** Convert a cwd path to a filesystem-safe slug (last directory segment) */
|
|
155
|
-
|
|
156
|
-
const normalized = cwd.
|
|
149
|
+
function cwdToSlug(cwd: string): string {
|
|
150
|
+
const normalized = cwd.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
157
151
|
return normalized.split("/").pop() || "unknown";
|
|
158
152
|
}
|
|
159
153
|
|
|
@@ -57,7 +57,7 @@ if (removedAgents.length > 0) {
|
|
|
57
57
|
// --- Remove PAL system docs ---
|
|
58
58
|
removePalDocs();
|
|
59
59
|
|
|
60
|
-
// --- Remove AGENTS.md and CLAUDE.md
|
|
60
|
+
// --- Remove AGENTS.md and CLAUDE.md ---
|
|
61
61
|
const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
|
|
62
62
|
const claudeMd = resolve(CLAUDE_DIR, "CLAUDE.md");
|
|
63
63
|
try {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Codex target installer
|
|
3
|
+
* Merges PAL hooks into ~/.codex/hooks.json (never overwrites user hooks).
|
|
4
|
+
* Symlinks skills. Ensures AGENTS.md symlink via regenerateIfNeeded().
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
16
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
17
|
+
import {
|
|
18
|
+
copySkills,
|
|
19
|
+
countSkills,
|
|
20
|
+
generateSkillIndex,
|
|
21
|
+
loadCodexHooksTemplate,
|
|
22
|
+
log,
|
|
23
|
+
mergeCodexHooks,
|
|
24
|
+
readJson,
|
|
25
|
+
scaffoldPalSettings,
|
|
26
|
+
writeJson,
|
|
27
|
+
} from "../lib";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensure `features.hooks = true` in config.toml without touching other content.
|
|
31
|
+
* Appends the setting if missing; skips if already present.
|
|
32
|
+
*/
|
|
33
|
+
function enableCodexHooks(configPath: string): void {
|
|
34
|
+
let content = "";
|
|
35
|
+
if (existsSync(configPath)) {
|
|
36
|
+
content = readFileSync(configPath, "utf-8");
|
|
37
|
+
// Already enabled — nothing to do
|
|
38
|
+
if (/^\s*hooks\s*=\s*true/m.test(content)) {
|
|
39
|
+
log.info("Codex hooks already enabled in config.toml");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// [features] section exists but no hooks line — insert after the header
|
|
43
|
+
if (/^\[features\]/m.test(content)) {
|
|
44
|
+
content = content.replace(/(\[features\][^\n]*\n)/, "$1hooks = true\n");
|
|
45
|
+
writeFileSync(configPath, content, "utf-8");
|
|
46
|
+
log.success("Added hooks = true to existing [features] section in config.toml");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// No config.toml, or no [features] section — append the block
|
|
51
|
+
const block = `${content.endsWith("\n") || content === "" ? "" : "\n"}\n[features]\nhooks = true\n`;
|
|
52
|
+
writeFileSync(configPath, content + block, "utf-8");
|
|
53
|
+
log.success("Enabled hooks = true in ~/.codex/config.toml");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
57
|
+
const CODEX_DIR = platform.codexDir();
|
|
58
|
+
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
59
|
+
|
|
60
|
+
// --- Ensure ~/.codex/ exists ---
|
|
61
|
+
mkdirSync(CODEX_DIR, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// --- Merge hooks ---
|
|
64
|
+
if (existsSync(HOOKS_FILE)) {
|
|
65
|
+
copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
|
|
66
|
+
log.info("Backed up hooks.json");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
|
|
70
|
+
const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
|
|
71
|
+
const merged = mergeCodexHooks(existing, template);
|
|
72
|
+
|
|
73
|
+
writeJson(HOOKS_FILE, merged);
|
|
74
|
+
log.success("Merged PAL hooks into ~/.codex/hooks.json");
|
|
75
|
+
|
|
76
|
+
// --- Symlink skills to ~/.codex/skills/ ---
|
|
77
|
+
const codexSkillsDir = resolve(CODEX_DIR, "skills");
|
|
78
|
+
copySkills(codexSkillsDir);
|
|
79
|
+
generateSkillIndex();
|
|
80
|
+
|
|
81
|
+
// --- Scaffold PAL settings ---
|
|
82
|
+
scaffoldPalSettings();
|
|
83
|
+
|
|
84
|
+
// --- Generate / verify AGENTS.md symlink ---
|
|
85
|
+
regenerateIfNeeded();
|
|
86
|
+
log.success("Ensured AGENTS.md symlink at ~/.codex/AGENTS.md");
|
|
87
|
+
|
|
88
|
+
// --- Enable hooks in config.toml ---
|
|
89
|
+
const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
|
|
90
|
+
enableCodexHooks(CONFIG_FILE);
|
|
91
|
+
|
|
92
|
+
log.success("Codex installation complete");
|
|
93
|
+
console.log("");
|
|
94
|
+
log.info(`Skills: ${countSkills()}`);
|
|
95
|
+
log.info(`Hooks: ${HOOKS_FILE}`);
|