portable-agent-layer 0.36.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 +1 -0
- 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 -20
- 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/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +27 -3
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- 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 +0 -1
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +14 -2
- 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/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 +9 -8
- 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/context.ts +45 -117
- 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 +3 -15
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +22 -18
- package/src/hooks/lib/semi-static.ts +4 -2
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -60
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +13 -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/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/lib.ts +140 -14
- package/src/targets/opencode/plugin.ts +22 -11
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +1 -1
- package/src/tools/agent/project.ts +375 -75
- 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 +13 -11
- package/src/tools/self-model.ts +20 -16
- 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/src/hooks/LoadContext.ts
CHANGED
|
@@ -39,7 +39,8 @@ try {
|
|
|
39
39
|
// Determine agent target — controls which sections are skipped (loaded natively instead).
|
|
40
40
|
let agent: AgentTarget = "claude";
|
|
41
41
|
if (process.env.PAL_AGENT === "copilot") agent = "copilot";
|
|
42
|
-
else if (process.env.
|
|
42
|
+
else if (process.env.PAL_AGENT === "cursor" || process.env.CURSOR_VERSION)
|
|
43
|
+
agent = "cursor";
|
|
43
44
|
const reminder = buildSystemReminder({ agent });
|
|
44
45
|
if (!reminder) process.exit(0);
|
|
45
46
|
|
|
@@ -61,12 +62,23 @@ try {
|
|
|
61
62
|
"LoadContext",
|
|
62
63
|
`Copilot session instructions written: ${context.length} chars`
|
|
63
64
|
);
|
|
64
|
-
} else if (process.env.CURSOR_VERSION) {
|
|
65
|
+
} else if (process.env.PAL_AGENT === "cursor" || process.env.CURSOR_VERSION) {
|
|
65
66
|
// Cursor: semi-static in ~/.cursor/rules/pal-context.mdc; inject AGENTS.md + dynamic here
|
|
66
67
|
const agentsMd = buildClaudeMd();
|
|
67
68
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
68
69
|
process.stdout.write(JSON.stringify({ additional_context: context }));
|
|
69
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`);
|
|
70
82
|
} else {
|
|
71
83
|
// Claude Code: raw text
|
|
72
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;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handler: persist the last user/assistant exchange on every Stop and PreCompact.
|
|
3
|
+
*
|
|
4
|
+
* Writes two outputs:
|
|
5
|
+
* 1. last-exchange/{sessionId}.json + last-exchange/latest.json
|
|
6
|
+
* → read by CompactRecover to re-inject after compaction
|
|
7
|
+
* 2. last-handoff.json keyed by cwd
|
|
8
|
+
* → read by loadHandoff() to surface "Pick Up Where You Left Off"
|
|
9
|
+
*
|
|
10
|
+
* Always overwrites — Stop is the source of truth for both. The LEARN-phase
|
|
11
|
+
* handoff-note.ts tool may also write to last-handoff.json; whichever runs last wins,
|
|
12
|
+
* but raw exchange is sufficient for continuity and costs nothing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { logDebug, logError } from "../lib/log";
|
|
18
|
+
import { ensureDir, paths } from "../lib/paths";
|
|
19
|
+
import { extractContent, extractLastAssistant, extractLastUser } from "../lib/transcript";
|
|
20
|
+
import { detectStatus } from "../lib/work-tracking";
|
|
21
|
+
|
|
22
|
+
type ParsedMessage = { role: string; content: unknown };
|
|
23
|
+
|
|
24
|
+
export function persistLastExchange(
|
|
25
|
+
messages: ParsedMessage[],
|
|
26
|
+
sessionId: string,
|
|
27
|
+
cwd: string = process.cwd()
|
|
28
|
+
): void {
|
|
29
|
+
try {
|
|
30
|
+
const lastUser = extractContent(extractLastUser(messages));
|
|
31
|
+
const lastAssistant = extractContent(extractLastAssistant(messages));
|
|
32
|
+
if (!lastUser && !lastAssistant) return;
|
|
33
|
+
|
|
34
|
+
// 1. Write last-exchange files for CompactRecover
|
|
35
|
+
const stateDir = ensureDir(resolve(paths.state(), "last-exchange"));
|
|
36
|
+
const payload = {
|
|
37
|
+
sessionId,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
trigger: null,
|
|
40
|
+
customInstructions: null,
|
|
41
|
+
userMessage: lastUser,
|
|
42
|
+
assistantMessage: lastAssistant,
|
|
43
|
+
};
|
|
44
|
+
const json = `${JSON.stringify(payload, null, 2)}\n`;
|
|
45
|
+
writeFileSync(resolve(stateDir, `${sessionId}.json`), json, "utf-8");
|
|
46
|
+
writeFileSync(resolve(stateDir, "latest.json"), json, "utf-8");
|
|
47
|
+
|
|
48
|
+
// 2. Write last-handoff.json for "Pick Up Where You Left Off"
|
|
49
|
+
const handoffPath = resolve(paths.state(), "last-handoff.json");
|
|
50
|
+
const existing: Record<string, unknown> = existsSync(handoffPath)
|
|
51
|
+
? JSON.parse(readFileSync(handoffPath, "utf-8"))
|
|
52
|
+
: {};
|
|
53
|
+
const title = (lastUser.slice(0, 80).replace(/\n/g, " ") || "Session").trim();
|
|
54
|
+
const handoff = [
|
|
55
|
+
lastUser ? `Last user message:\n${lastUser.slice(0, 500)}` : "",
|
|
56
|
+
lastAssistant ? `\nLast assistant response:\n${lastAssistant.slice(0, 500)}` : "",
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join("");
|
|
60
|
+
existing[cwd] = {
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
title,
|
|
63
|
+
status: detectStatus(lastAssistant),
|
|
64
|
+
handoff,
|
|
65
|
+
artifacts: [],
|
|
66
|
+
};
|
|
67
|
+
writeFileSync(handoffPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
68
|
+
|
|
69
|
+
logDebug(
|
|
70
|
+
"persist-last-exchange",
|
|
71
|
+
`Persisted exchange for session ${sessionId} (user=${lastUser.length}ch, assistant=${lastAssistant.length}ch)`
|
|
72
|
+
);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logError("persist-last-exchange", err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -48,7 +48,7 @@ export function parseExplicitRating(
|
|
|
48
48
|
prompt: string
|
|
49
49
|
): { rating: number; comment?: string } | null {
|
|
50
50
|
const trimmed = prompt.trim();
|
|
51
|
-
const match =
|
|
51
|
+
const match = new RegExp(/^(10|[1-9])(?:\s*[-:,]\s*|\s+)?(.*)$/).exec(trimmed);
|
|
52
52
|
if (!match) return null;
|
|
53
53
|
|
|
54
54
|
const rating = parseInt(match[1], 10);
|
|
@@ -271,6 +271,7 @@ function handleRating(
|
|
|
271
271
|
principle,
|
|
272
272
|
responsePreview,
|
|
273
273
|
userPreview,
|
|
274
|
+
cwd: process.cwd(),
|
|
274
275
|
ts: now(),
|
|
275
276
|
},
|
|
276
277
|
null,
|
|
@@ -33,7 +33,7 @@ function hasDocumentableChanges(): boolean {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
interface ReadmeSyncDecision {
|
|
37
37
|
decision?: "block";
|
|
38
38
|
reason?: string;
|
|
39
39
|
}
|
|
@@ -50,9 +50,10 @@ export function checkReadmeSync(): ReadmeSyncDecision {
|
|
|
50
50
|
|
|
51
51
|
if (!result.ok) {
|
|
52
52
|
logDebug("readme-sync", `README out of sync: ${result.issues.join("; ")}`);
|
|
53
|
+
const issueList = result.issues.map((i) => `- ${i}`).join("\n");
|
|
53
54
|
return {
|
|
54
55
|
decision: "block",
|
|
55
|
-
reason: `README.md is out of date. Please update it before finishing:\n${
|
|
56
|
+
reason: `README.md is out of date. Please update it before finishing:\n${issueList}`,
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* Relationship notes → written in ALGORITHM LEARN phase via relationship-note.ts
|
|
8
8
|
* Handoff notes → written in ALGORITHM LEARN phase via handoff-note.ts
|
|
9
9
|
*
|
|
10
|
-
* Replaces: work-learning.ts (still exists but is bypassed).
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
import { existsSync, readFileSync,
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { unlink, writeFile } from "node:fs/promises";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
15
|
import { stringify } from "../lib/frontmatter";
|
|
16
|
-
import { inference } from "../lib/inference";
|
|
16
|
+
import { hasApiKey, inference } from "../lib/inference";
|
|
17
17
|
import { categorizeLearning } from "../lib/learning-category";
|
|
18
18
|
import { logDebug, logError } from "../lib/log";
|
|
19
19
|
import { ensureDir, paths } from "../lib/paths";
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "../lib/transcript";
|
|
28
28
|
import { appendProjectHistory, detectStatus } from "../lib/work-tracking";
|
|
29
29
|
|
|
30
|
-
// ── Dedup tracking
|
|
30
|
+
// ── Dedup tracking ──
|
|
31
31
|
|
|
32
32
|
interface CaptureEntry {
|
|
33
33
|
filepath: string;
|
|
@@ -138,7 +138,7 @@ export async function captureSessionIntelligence(
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// Skip if no API key
|
|
141
|
-
if (!
|
|
141
|
+
if (!hasApiKey()) {
|
|
142
142
|
logDebug("session-intelligence", "Skipped: no PAL_ANTHROPIC_API_KEY");
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
@@ -161,6 +161,7 @@ export async function captureSessionIntelligence(
|
|
|
161
161
|
|
|
162
162
|
// Single Haiku call
|
|
163
163
|
logDebug("session-intelligence", "Calling inference...");
|
|
164
|
+
const numberedMessages = userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n");
|
|
164
165
|
let output: IntelligenceOutput | null = null;
|
|
165
166
|
try {
|
|
166
167
|
const result = await inference({
|
|
@@ -175,7 +176,7 @@ export async function captureSessionIntelligence(
|
|
|
175
176
|
? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
|
|
176
177
|
: "4. handoff: empty string (session completed)",
|
|
177
178
|
].join("\n"),
|
|
178
|
-
user: `User messages:\n${
|
|
179
|
+
user: `User messages:\n${numberedMessages}\n\nLast AI response:\n${assistantWindow}`,
|
|
179
180
|
maxTokens: 350,
|
|
180
181
|
timeout: 15000,
|
|
181
182
|
jsonSchema: INTELLIGENCE_SCHEMA,
|
|
@@ -224,7 +225,7 @@ export async function captureSessionIntelligence(
|
|
|
224
225
|
const prev = getPreviousCapture(sessionId);
|
|
225
226
|
if (prev?.filepath && existsSync(prev.filepath)) {
|
|
226
227
|
try {
|
|
227
|
-
|
|
228
|
+
await unlink(prev.filepath);
|
|
228
229
|
} catch {
|
|
229
230
|
/* ignore */
|
|
230
231
|
}
|
|
@@ -232,7 +233,7 @@ export async function captureSessionIntelligence(
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
const filepath = resolve(dir, filename);
|
|
235
|
-
|
|
236
|
+
await writeFile(filepath, content, "utf-8");
|
|
236
237
|
|
|
237
238
|
// Append to per-project history
|
|
238
239
|
appendProjectHistory(process.cwd(), {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
|
-
import { inference } from "../lib/inference";
|
|
13
|
+
import { hasApiKey, inference } from "../lib/inference";
|
|
14
14
|
import { logDebug, logError } from "../lib/log";
|
|
15
15
|
import {
|
|
16
16
|
extractFallbackName,
|
|
@@ -42,7 +42,7 @@ export async function captureSessionName(
|
|
|
42
42
|
logDebug("session-name", `Named from prompt: "${name}"`);
|
|
43
43
|
|
|
44
44
|
// Spawn detached background process to upgrade with Haiku inference
|
|
45
|
-
if (!
|
|
45
|
+
if (!hasApiKey()) return;
|
|
46
46
|
try {
|
|
47
47
|
const promptB64 = Buffer.from(message.slice(0, 800)).toString("base64");
|
|
48
48
|
const child = spawn(
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Imports synthesize logic directly — no subprocess needed.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
7
8
|
import { resolve } from "node:path";
|
|
8
9
|
import { logDebug } from "../lib/log";
|
|
9
10
|
import { paths } from "../lib/paths";
|
|
@@ -16,7 +17,9 @@ export async function runSynthesis(): Promise<void> {
|
|
|
16
17
|
// Check 24h guard
|
|
17
18
|
if (existsSync(statePath)) {
|
|
18
19
|
try {
|
|
19
|
-
const data = JSON.parse(
|
|
20
|
+
const data = JSON.parse(await readFile(statePath, "utf-8")) as {
|
|
21
|
+
timestamp: string;
|
|
22
|
+
};
|
|
20
23
|
if (Date.now() - new Date(data.timestamp).getTime() < SYNTHESIS_TTL_MS) {
|
|
21
24
|
logDebug("synthesis", "Skipped — last synthesis < 24h ago");
|
|
22
25
|
return;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* scanning directories and JSONL files.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, readdirSync, readFileSync
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { writeFile } from "node:fs/promises";
|
|
8
9
|
import { resolve } from "node:path";
|
|
9
10
|
import { assets, ensureDir, paths } from "../lib/paths";
|
|
10
11
|
|
|
@@ -147,5 +148,5 @@ function getCounts(): Counts {
|
|
|
147
148
|
export async function updateCounts(): Promise<void> {
|
|
148
149
|
const counts = getCounts();
|
|
149
150
|
const countsPath = resolve(ensureDir(paths.state()), "counts.json");
|
|
150
|
-
|
|
151
|
+
await writeFile(countsPath, JSON.stringify(counts, null, 2), "utf-8");
|
|
151
152
|
}
|