diff --git a/continual-learning/README.md b/continual-learning/README.md index 410fe92..7cd1584 100644 --- a/continual-learning/README.md +++ b/continual-learning/README.md @@ -22,13 +22,14 @@ It is designed to avoid noisy rewrites by: ## How it works -On eligible `stop` events, the hook may emit a `followup_message` that asks the agent to run the `continual-learning` skill. +On eligible `stop` events, the hook prefers launching the `agent` CLI in print mode when `agent` is available on `PATH`. If the CLI is unavailable or cannot be started, the hook falls back to emitting a `followup_message` that asks the current session to run the `continual-learning` skill. The skill is marked `disable-model-invocation: true`, so it will not be auto-selected during normal model invocation. When it does run, it delegates the full memory update flow to `agents-memory-updater`. The hook keeps local runtime state in: - `.cursor/hooks/state/continual-learning.json` (cadence state) +- `.cursor/hooks/state/continual-learning-agent.log` (best-effort `agent` CLI output when launched from the hook) The updater uses an incremental transcript index at: diff --git a/continual-learning/hooks/continual-learning-stop.ts b/continual-learning/hooks/continual-learning-stop.ts index fa6f5d9..5c846b1 100644 --- a/continual-learning/hooks/continual-learning-stop.ts +++ b/continual-learning/hooks/continual-learning-stop.ts @@ -1,13 +1,23 @@ /// -import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { + closeSync, + existsSync, + mkdirSync, + openSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import { spawn, spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; import { stdin } from "bun"; -const STATE_PATH = resolve(".cursor/hooks/state/continual-learning.json"); -const INCREMENTAL_INDEX_PATH = resolve( - ".cursor/hooks/state/continual-learning-index.json" -); +const PROJECT_DIR = resolve(process.env.CURSOR_PROJECT_DIR ?? "."); +const STATE_DIR = resolve(PROJECT_DIR, ".cursor/hooks/state"); +const STATE_PATH = resolve(STATE_DIR, "continual-learning.json"); +const INCREMENTAL_INDEX_PATH = resolve(STATE_DIR, "continual-learning-index.json"); +const AGENT_LOG_PATH = resolve(STATE_DIR, "continual-learning-agent.log"); const DEFAULT_MIN_TURNS = 10; const DEFAULT_MIN_MINUTES = 120; const TRIAL_DEFAULT_MIN_TURNS = 3; @@ -138,6 +148,50 @@ function shouldCountTurn(input: StopHookInput): boolean { return input.status === "completed" && input.loop_count === 0; } +function canSpawnAgentCli(): boolean { + const result = spawnSync("agent", ["--version"], { + stdio: "ignore", + cwd: PROJECT_DIR, + env: process.env, + }); + return result.error === undefined; +} + +function triggerAgentCli(): boolean { + if (!canSpawnAgentCli()) { + return false; + } + + let logFd: number | null = null; + + try { + if (!existsSync(STATE_DIR)) { + mkdirSync(STATE_DIR, { recursive: true }); + } + + logFd = openSync(AGENT_LOG_PATH, "a"); + const child = spawn( + "agent", + ["-p", "--force", "--workspace", PROJECT_DIR, "--", FOLLOWUP_MESSAGE], + { + cwd: PROJECT_DIR, + detached: true, + stdio: ["ignore", logFd, logFd], + env: process.env, + } + ); + child.unref(); + return true; + } catch (error) { + console.error("[continual-learning-stop] failed to spawn agent CLI", error); + return false; + } finally { + if (logFd !== null) { + closeSync(logFd); + } + } +} + async function parseHookInput(): Promise { const text = await stdin.text(); return JSON.parse(text) as T; @@ -224,6 +278,11 @@ async function main(): Promise { state.lastTranscriptMtimeMs = transcriptMtimeMs; saveState(state); + if (triggerAgentCli()) { + console.log(JSON.stringify({})); + return 0; + } + console.log( JSON.stringify({ followup_message: FOLLOWUP_MESSAGE,