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,