Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion continual-learning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
69 changes: 64 additions & 5 deletions continual-learning/hooks/continual-learning-stop.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
/// <reference types="bun-types-no-globals/lib/index.d.ts" />

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;
Expand Down Expand Up @@ -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<T>(): Promise<T> {
const text = await stdin.text();
return JSON.parse(text) as T;
Expand Down Expand Up @@ -224,6 +278,11 @@ async function main(): Promise<number> {
state.lastTranscriptMtimeMs = transcriptMtimeMs;
saveState(state);

if (triggerAgentCli()) {
console.log(JSON.stringify({}));
return 0;
}

console.log(
JSON.stringify({
followup_message: FOLLOWUP_MESSAGE,
Expand Down