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
package/src/cli/setup-telos.ts
CHANGED
|
@@ -1,78 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive TELOS setup — prompts for personal context during `pal install`.
|
|
3
3
|
* Skips any step whose TELOS file already has real content.
|
|
4
|
-
* Projects use the upsertProject tool directly with a structured add-another loop.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { writeFileSync } from "node:fs";
|
|
8
7
|
import { resolve } from "node:path";
|
|
9
8
|
import * as clack from "@clack/prompts";
|
|
10
|
-
import { upsertProject } from "../../assets/skills/telos/tools/update-projects";
|
|
11
9
|
import { palHome } from "../hooks/lib/paths";
|
|
12
10
|
import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
|
|
13
11
|
|
|
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
12
|
/** Prompt for missing TELOS context. Skips any step whose file already has real content. */
|
|
77
13
|
export async function promptTelos(): Promise<void> {
|
|
78
14
|
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
@@ -95,25 +31,21 @@ export async function promptTelos(): Promise<void> {
|
|
|
95
31
|
);
|
|
96
32
|
|
|
97
33
|
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
|
-
});
|
|
34
|
+
const step = SETUP_STEPS[key];
|
|
35
|
+
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
108
36
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
37
|
+
const answer = await clack.text({
|
|
38
|
+
message: step.question,
|
|
39
|
+
placeholder: step.hint,
|
|
40
|
+
});
|
|
113
41
|
|
|
114
|
-
|
|
115
|
-
|
|
42
|
+
if (clack.isCancel(answer)) {
|
|
43
|
+
clack.cancel("Setup cancelled");
|
|
44
|
+
return;
|
|
116
45
|
}
|
|
46
|
+
|
|
47
|
+
const filePath = resolve(home, step.file);
|
|
48
|
+
writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
|
|
117
49
|
}
|
|
118
50
|
|
|
119
51
|
clack.outro("Personal context saved ✓");
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* Storage: ~/.pal/memory/state/last-exchange/{session_id}.json (with latest.json fallback).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
12
13
|
import { resolve } from "node:path";
|
|
14
|
+
import { isCursor } from "./lib/agent";
|
|
13
15
|
import { logDebug, logError } from "./lib/log";
|
|
14
16
|
import { paths } from "./lib/paths";
|
|
15
17
|
import { readStdinJSON } from "./lib/stdin";
|
|
@@ -61,7 +63,7 @@ const main = async () => {
|
|
|
61
63
|
process.exit(0);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const saved = JSON.parse(
|
|
66
|
+
const saved = JSON.parse(await readFile(file, "utf-8")) as SavedExchange;
|
|
65
67
|
const userBudget = Math.floor(MAX_OUTPUT * 0.4);
|
|
66
68
|
const assistantBudget = MAX_OUTPUT - userBudget - 300; // reserve for framing
|
|
67
69
|
|
|
@@ -81,7 +83,11 @@ const main = async () => {
|
|
|
81
83
|
"</system-reminder>",
|
|
82
84
|
].join("\n");
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
if (isCursor()) {
|
|
87
|
+
process.stdout.write(JSON.stringify({ additional_context: out }));
|
|
88
|
+
} else {
|
|
89
|
+
process.stdout.write(out);
|
|
90
|
+
}
|
|
85
91
|
logDebug("CompactRecover", `Re-injected ${out.length} chars from ${file}`);
|
|
86
92
|
|
|
87
93
|
// Consume-on-read: drop the session-keyed file after a successful injection so it
|
|
@@ -90,7 +96,7 @@ const main = async () => {
|
|
|
90
96
|
const sessionFile = sessionId ? resolve(stateDir, `${sessionId}.json`) : null;
|
|
91
97
|
if (sessionFile && file === sessionFile) {
|
|
92
98
|
try {
|
|
93
|
-
|
|
99
|
+
await unlink(sessionFile);
|
|
94
100
|
} catch (err) {
|
|
95
101
|
logError("CompactRecover:cleanup", err);
|
|
96
102
|
}
|
|
@@ -102,4 +108,4 @@ const main = async () => {
|
|
|
102
108
|
process.exit(0);
|
|
103
109
|
};
|
|
104
110
|
|
|
105
|
-
main();
|
|
111
|
+
await main();
|
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,25 +36,49 @@ 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.PAL_AGENT === "cursor" || process.env.CURSOR_VERSION)
|
|
43
|
+
agent = "cursor";
|
|
44
|
+
const reminder = buildSystemReminder({ agent });
|
|
40
45
|
if (!reminder) process.exit(0);
|
|
41
46
|
|
|
42
47
|
if (process.env.PAL_AGENT === "copilot") {
|
|
43
|
-
// Copilot:
|
|
44
|
-
|
|
48
|
+
// Copilot: semi-static in ~/.copilot/instructions/pal-*.instructions.md (written at stop).
|
|
49
|
+
// Write AGENTS.md + dynamic context to pal-session.instructions.md on each session start.
|
|
50
|
+
const instructionsDir = resolve(platform.copilotDir(), "instructions");
|
|
51
|
+
mkdirSync(instructionsDir, { recursive: true });
|
|
45
52
|
const agentsMd = buildClaudeMd();
|
|
46
53
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
47
|
-
if (
|
|
48
|
-
|
|
54
|
+
if (context) {
|
|
55
|
+
writeFileSync(
|
|
56
|
+
resolve(instructionsDir, "pal-session.instructions.md"),
|
|
57
|
+
`---\napplyTo: "**"\n---\n\n${context}`,
|
|
58
|
+
"utf-8"
|
|
59
|
+
);
|
|
49
60
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
logDebug(
|
|
62
|
+
"LoadContext",
|
|
63
|
+
`Copilot session instructions written: ${context.length} chars`
|
|
64
|
+
);
|
|
65
|
+
} else if (process.env.PAL_AGENT === "cursor" || process.env.CURSOR_VERSION) {
|
|
66
|
+
// Cursor: semi-static in ~/.cursor/rules/pal-context.mdc; inject AGENTS.md + dynamic here
|
|
54
67
|
const agentsMd = buildClaudeMd();
|
|
55
68
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
56
69
|
process.stdout.write(JSON.stringify({ additional_context: context }));
|
|
57
70
|
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
71
|
+
} else if (process.env.PAL_AGENT === "codex") {
|
|
72
|
+
// Codex: AGENTS.md already loaded via symlink; inject only dynamic context
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
hookSpecificOutput: {
|
|
76
|
+
hookEventName: "SessionStart",
|
|
77
|
+
additionalContext: reminder,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
logDebug("LoadContext", `Codex reminder injected: ${reminder.length} chars`);
|
|
58
82
|
} else {
|
|
59
83
|
// Claude Code: raw text
|
|
60
84
|
console.log(reminder);
|
|
@@ -11,17 +11,14 @@
|
|
|
11
11
|
* is currently a Claude Code event.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
15
16
|
import { resolve } from "node:path";
|
|
17
|
+
import { persistLastExchange } from "./handlers/persist-last-exchange";
|
|
16
18
|
import { logDebug, logError } from "./lib/log";
|
|
17
|
-
import {
|
|
19
|
+
import { paths } from "./lib/paths";
|
|
18
20
|
import { readStdinJSON } from "./lib/stdin";
|
|
19
|
-
import {
|
|
20
|
-
extractContent,
|
|
21
|
-
extractLastAssistant,
|
|
22
|
-
extractLastUser,
|
|
23
|
-
readTranscriptFile,
|
|
24
|
-
} from "./lib/transcript";
|
|
21
|
+
import { readTranscriptFile } from "./lib/transcript";
|
|
25
22
|
|
|
26
23
|
interface PreCompactInput {
|
|
27
24
|
session_id?: string;
|
|
@@ -45,36 +42,31 @@ const main = async () => {
|
|
|
45
42
|
logDebug("PreCompactPersist", "Transcript empty or unreadable");
|
|
46
43
|
process.exit(0);
|
|
47
44
|
}
|
|
45
|
+
const sessionId = input.session_id ?? "unknown";
|
|
46
|
+
const cwd = input.cwd ?? process.cwd();
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
// Stop fires after every response and is authoritative. Skip if it already
|
|
49
|
+
// wrote latest.json for this session — PreCompact is a safety net only.
|
|
50
|
+
const latestPath = resolve(paths.state(), "last-exchange", "latest.json");
|
|
51
|
+
if (existsSync(latestPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const latest = JSON.parse(await readFile(latestPath, "utf-8"));
|
|
54
|
+
if (latest?.sessionId === sessionId) {
|
|
55
|
+
logDebug(
|
|
56
|
+
"PreCompactPersist",
|
|
57
|
+
`Stop already persisted session ${sessionId} — skipping`
|
|
58
|
+
);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
/* unreadable — fall through and write */
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
const payload = {
|
|
59
|
-
sessionId,
|
|
60
|
-
timestamp: new Date().toISOString(),
|
|
61
|
-
trigger: input.trigger ?? null,
|
|
62
|
-
customInstructions: input.custom_instructions || null,
|
|
63
|
-
userMessage: lastUser,
|
|
64
|
-
assistantMessage: lastAssistant,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const stateDir = ensureDir(resolve(paths.state(), "last-exchange"));
|
|
68
|
-
const sessionFile = resolve(stateDir, `${sessionId}.json`);
|
|
69
|
-
const latestFile = resolve(stateDir, "latest.json");
|
|
70
|
-
const json = `${JSON.stringify(payload, null, 2)}\n`;
|
|
71
|
-
|
|
72
|
-
writeFileSync(sessionFile, json, "utf-8");
|
|
73
|
-
writeFileSync(latestFile, json, "utf-8");
|
|
74
|
-
|
|
66
|
+
persistLastExchange(messages, sessionId, cwd);
|
|
75
67
|
logDebug(
|
|
76
68
|
"PreCompactPersist",
|
|
77
|
-
`
|
|
69
|
+
`Persisted exchange before compaction for session ${sessionId}`
|
|
78
70
|
);
|
|
79
71
|
} catch (err) {
|
|
80
72
|
logError("PreCompactPersist", err);
|
|
@@ -83,4 +75,4 @@ const main = async () => {
|
|
|
83
75
|
process.exit(0);
|
|
84
76
|
};
|
|
85
77
|
|
|
86
|
-
main();
|
|
78
|
+
await main();
|
|
@@ -6,47 +6,69 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { blockResponse } from "./lib/agent";
|
|
9
|
-
import { checkBashCommand, checkFilePath
|
|
9
|
+
import { checkBashCommand, checkFilePath } from "./lib/security";
|
|
10
10
|
import { readStdinJSON } from "./lib/stdin";
|
|
11
11
|
|
|
12
|
+
// preToolUse shape (Claude Code + Cursor + Codex)
|
|
12
13
|
interface ToolUseInput {
|
|
13
14
|
tool_name: string;
|
|
15
|
+
hook_event_name?: string; // Codex includes this in all hook inputs
|
|
14
16
|
tool_input: {
|
|
15
17
|
command?: string;
|
|
16
18
|
file_path?: string;
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// beforeShellExecution shape (Cursor only) — flat, no tool_name wrapper
|
|
23
|
+
interface ShellExecInput {
|
|
24
|
+
command: string;
|
|
25
|
+
sandbox?: boolean;
|
|
26
|
+
}
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
type SecurityInput = ToolUseInput | ShellExecInput;
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
function isShellExec(input: SecurityInput): input is ShellExecInput {
|
|
31
|
+
return !("tool_name" in input) && "command" in input;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const input = await readStdinJSON<SecurityInput>();
|
|
36
|
+
if (!input) process.exit(0);
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const reason = checkBashCommand(
|
|
38
|
+
if (isShellExec(input)) {
|
|
39
|
+
// beforeShellExecution — command is always a shell command
|
|
40
|
+
const reason = checkBashCommand(input.command);
|
|
33
41
|
if (reason) {
|
|
34
42
|
process.stdout.write(blockResponse(`Blocked: ${reason}`));
|
|
35
|
-
process.exit(0);
|
|
36
43
|
}
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hookEventName = input.hook_event_name;
|
|
48
|
+
|
|
49
|
+
// preToolUse — Claude: "Bash", Cursor: "Shell", Codex: "shell"
|
|
50
|
+
const isBash =
|
|
51
|
+
input.tool_name === "Bash" ||
|
|
52
|
+
input.tool_name === "Shell" ||
|
|
53
|
+
input.tool_name === "shell";
|
|
54
|
+
const isFileWrite =
|
|
55
|
+
input.tool_name === "Write" ||
|
|
56
|
+
input.tool_name === "Edit" ||
|
|
57
|
+
input.tool_name === "write_file" ||
|
|
58
|
+
input.tool_name === "apply_patch";
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
60
|
+
if (isBash && input.tool_input.command) {
|
|
61
|
+
const reason = checkBashCommand(input.tool_input.command);
|
|
62
|
+
if (reason) {
|
|
63
|
+
process.stdout.write(blockResponse(`Blocked: ${reason}`, hookEventName));
|
|
64
|
+
process.exit(0);
|
|
42
65
|
}
|
|
43
66
|
}
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
process.stdout.write(blockResponse(fileReason));
|
|
68
|
+
if (isFileWrite && input.tool_input.file_path) {
|
|
69
|
+
const reason = checkFilePath(input.tool_input.file_path);
|
|
70
|
+
if (reason) {
|
|
71
|
+
process.stdout.write(blockResponse(reason, hookEventName));
|
|
50
72
|
process.exit(0);
|
|
51
73
|
}
|
|
52
74
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { checkReadmeSync } from "./handlers/readme-sync";
|
|
10
|
-
import { isCursor } from "./lib/agent";
|
|
10
|
+
import { isCodex, isCursor } from "./lib/agent";
|
|
11
11
|
import { logError } from "./lib/log";
|
|
12
12
|
import { readStdinJSON } from "./lib/stdin";
|
|
13
13
|
import { runStopHandlers } from "./lib/stop";
|
|
@@ -26,6 +26,9 @@ try {
|
|
|
26
26
|
if (isCursor()) {
|
|
27
27
|
// Cursor stop hook: followup_message auto-sends to the agent
|
|
28
28
|
process.stdout.write(JSON.stringify({ followup_message: decision.reason }));
|
|
29
|
+
} else if (isCodex()) {
|
|
30
|
+
// Codex stop hook: additionalContext re-queues as next prompt
|
|
31
|
+
process.stdout.write(JSON.stringify({ additionalContext: decision.reason }));
|
|
29
32
|
} else {
|
|
30
33
|
// Claude Code: block decision
|
|
31
34
|
process.stdout.write(JSON.stringify(decision));
|
|
@@ -16,15 +16,17 @@ import { readStdinJSON } from "./lib/stdin";
|
|
|
16
16
|
interface PromptSubmitInput {
|
|
17
17
|
prompt: string;
|
|
18
18
|
session_id?: string;
|
|
19
|
+
conversation_id?: string; // Cursor sends this instead of session_id
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const input = await readStdinJSON<PromptSubmitInput>();
|
|
22
23
|
logDebug("UserPromptOrchestrator", `Input: ${JSON.stringify(input).slice(0, 200)}`);
|
|
23
24
|
if (!input?.prompt) process.exit(0);
|
|
24
25
|
|
|
26
|
+
const sessionId = input.session_id ?? input.conversation_id;
|
|
25
27
|
const results = await Promise.allSettled([
|
|
26
|
-
captureRating(input.prompt,
|
|
27
|
-
captureSessionName(input.prompt,
|
|
28
|
+
captureRating(input.prompt, sessionId),
|
|
29
|
+
captureSessionName(input.prompt, sessionId ?? ""),
|
|
28
30
|
injectRetrieval(input.prompt),
|
|
29
31
|
]);
|
|
30
32
|
|
|
@@ -77,12 +77,12 @@ function alreadyPromoted(state: GraduationState, pattern: string): boolean {
|
|
|
77
77
|
return state.graduated.some((g) => g.pattern === pattern);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
interface AutoGraduateOptions {
|
|
81
81
|
/** Bypass the 24h TTL guard. State + content dedup still apply. */
|
|
82
82
|
force?: boolean;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
interface AutoGraduateResult {
|
|
86
86
|
ranAnalysis: boolean;
|
|
87
87
|
candidatesAtFloor: number;
|
|
88
88
|
promoted: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* is older than 7 days, or if no backup exists yet.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { readdir, stat } from "node:fs/promises";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { exportZip, timestamp } from "../lib/export";
|
|
10
10
|
import { logDebug } from "../lib/log";
|
|
@@ -16,14 +16,14 @@ export async function autoBackup(): Promise<void> {
|
|
|
16
16
|
const backupDir = paths.backups();
|
|
17
17
|
|
|
18
18
|
// Check most recent backup
|
|
19
|
-
const existing =
|
|
19
|
+
const existing = (await readdir(backupDir))
|
|
20
20
|
.filter((f) => f.startsWith("pal-backup-") && f.endsWith(".zip"))
|
|
21
21
|
.sort()
|
|
22
22
|
.reverse();
|
|
23
23
|
|
|
24
24
|
if (existing.length > 0) {
|
|
25
25
|
const latestPath = resolve(backupDir, existing[0]);
|
|
26
|
-
const latestMtime =
|
|
26
|
+
const latestMtime = (await stat(latestPath)).mtimeMs;
|
|
27
27
|
if (Date.now() - latestMtime < BACKUP_INTERVAL_MS) {
|
|
28
28
|
logDebug("backup", "Skipping — last backup is less than 7 days old");
|
|
29
29
|
return;
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Analysis is left to the human or the graduation pipeline, not auto-generated.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { writeFile } from "node:fs/promises";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { stringify } from "../lib/frontmatter";
|
|
13
13
|
import { ensureDir, paths } from "../lib/paths";
|
|
@@ -31,7 +31,8 @@ export async function captureFailure(
|
|
|
31
31
|
context: string,
|
|
32
32
|
transcript: string,
|
|
33
33
|
detailedContext?: string,
|
|
34
|
-
principle?: string
|
|
34
|
+
principle?: string,
|
|
35
|
+
cwd?: string
|
|
35
36
|
): Promise<void> {
|
|
36
37
|
const messages = parseMessages(transcript);
|
|
37
38
|
|
|
@@ -57,6 +58,7 @@ export async function captureFailure(
|
|
|
57
58
|
slug,
|
|
58
59
|
};
|
|
59
60
|
if (principle) meta.principle = principle;
|
|
61
|
+
if (cwd) meta.cwd = cwd;
|
|
60
62
|
|
|
61
63
|
const body = [
|
|
62
64
|
"## What Happened",
|
|
@@ -69,5 +71,5 @@ export async function captureFailure(
|
|
|
69
71
|
conversationSummary || "*(unavailable)*",
|
|
70
72
|
].join("\n");
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
await writeFile(resolve(dir, "capture.md"), stringify(meta, body), "utf-8");
|
|
73
75
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* timeout produces empty output, never blocks the prompt.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { isCodex, isCursor } from "../lib/agent";
|
|
10
11
|
import { logDebug, logError } from "../lib/log";
|
|
11
12
|
import { runRetrieval } from "../lib/retrieval";
|
|
12
13
|
import { ensureIndex } from "../lib/retrieval-index";
|
|
@@ -29,9 +30,10 @@ function withTimeout<T>(work: () => T, ms: number): Promise<T | null> {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!
|
|
33
|
+
/** Returns the retrieval reminder string, or null if nothing to inject. @lintignore dynamically imported by opencode plugin */
|
|
34
|
+
export async function getRetrievalReminder(prompt: string): Promise<string | null> {
|
|
35
|
+
if (!prompt?.trim()) return null;
|
|
36
|
+
if (!isEnabled("learningInjection")) return null;
|
|
35
37
|
|
|
36
38
|
const result = await withTimeout(() => {
|
|
37
39
|
const index = ensureIndex();
|
|
@@ -39,12 +41,33 @@ export async function injectRetrieval(prompt: string): Promise<void> {
|
|
|
39
41
|
return runRetrieval(prompt, index, process.cwd());
|
|
40
42
|
}, TIMEOUT_MS);
|
|
41
43
|
|
|
42
|
-
if (!result?.reminder) return;
|
|
44
|
+
if (!result?.reminder) return null;
|
|
43
45
|
|
|
44
46
|
logDebug(
|
|
45
47
|
"inject-retrieval",
|
|
46
|
-
|
|
48
|
+
`${result.matches.length} matches; top score=${result.matches[0]?.confidence.toFixed(3)}`
|
|
47
49
|
);
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
return result.reminder;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Write retrieval reminder to stdout in the correct format for the current agent.
|
|
55
|
+
* Claude Code: plain text. Cursor: { additional_context }. Codex: hookSpecificOutput JSON. */
|
|
56
|
+
export async function injectRetrieval(prompt: string): Promise<void> {
|
|
57
|
+
const reminder = await getRetrievalReminder(prompt);
|
|
58
|
+
if (!reminder) return;
|
|
59
|
+
if (isCursor()) {
|
|
60
|
+
process.stdout.write(JSON.stringify({ additional_context: reminder }));
|
|
61
|
+
} else if (isCodex()) {
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
hookSpecificOutput: {
|
|
65
|
+
hookEventName: "UserPromptSubmit",
|
|
66
|
+
additionalContext: reminder,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
process.stdout.write(`${reminder}\n`);
|
|
72
|
+
}
|
|
50
73
|
}
|