Rename continuous-learning plugin to continual-learning

Apply a full path and identifier rename for the plugin (folder, hooks, skill path, marketplace source, and docs) so installation and references consistently use continual-learning.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ericzakariasson
2026-02-19 11:51:13 -05:00
parent 2cf89ae30b
commit 46c85c6e84
9 changed files with 46 additions and 32 deletions
@@ -0,0 +1,29 @@
{
"name": "continual-learning",
"displayName": "Continual Learning",
"version": "1.0.0",
"description": "Incrementally learns durable user preferences and workspace facts from transcript changes and keeps AGENTS.md up to date with plain bullet points.",
"author": {
"name": "Cursor",
"email": "plugins@cursor.com"
},
"homepage": "https://github.com/cursor/plugins",
"repository": "https://github.com/cursor/plugins",
"license": "MIT",
"logo": "assets/avatar.png",
"keywords": [
"continual-learning",
"agent-memory",
"transcripts",
"hooks",
"incremental"
],
"category": "developer-tools",
"tags": [
"automation",
"memory",
"transcripts"
],
"skills": "./skills/",
"hooks": "./hooks/hooks.json"
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Cursor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+68
View File
@@ -0,0 +1,68 @@
# Continual Learning
Automatically and incrementally keeps `AGENTS.md` up to date from transcript changes.
The plugin combines:
- A `stop` hook that decides when to trigger learning.
- A `continual-learning` skill that mines only high-signal transcript deltas.
It is designed to avoid noisy rewrites by:
- Reading existing `AGENTS.md` first and updating matching bullets in place.
- Processing only new or changed transcript files.
- Writing plain bullet points only (no evidence/confidence metadata).
## Installation
```bash
/add-plugin continual-learning
```
## How it works
On eligible `stop` events, the hook may emit a `followup_message` that asks the agent to run the `continual-learning` skill.
The hook keeps local runtime state in:
- `.cursor/hooks/state/continual-learning.json` (cadence state)
The skill uses an incremental transcript index at:
- `.cursor/hooks/state/continual-learning-index.json`
## Trigger cadence
Default cadence:
- minimum 10 completed turns
- minimum 120 minutes since the last run
- transcript mtime must advance since the previous run
Trial mode defaults (enabled in this plugin hook config):
- minimum 3 completed turns
- minimum 15 minutes
- automatically expires after 24 hours, then falls back to default cadence
## Optional env overrides
- `CONTINUAL_LEARNING_MIN_TURNS` (or legacy `CONTINUOUS_LEARNING_MIN_TURNS`)
- `CONTINUAL_LEARNING_MIN_MINUTES` (or legacy `CONTINUOUS_LEARNING_MIN_MINUTES`)
- `CONTINUAL_LEARNING_TRIAL_MODE` (or legacy `CONTINUOUS_LEARNING_TRIAL_MODE`)
- `CONTINUAL_LEARNING_TRIAL_MIN_TURNS` (or legacy `CONTINUOUS_LEARNING_TRIAL_MIN_TURNS`)
- `CONTINUAL_LEARNING_TRIAL_MIN_MINUTES` (or legacy `CONTINUOUS_LEARNING_TRIAL_MIN_MINUTES`)
- `CONTINUAL_LEARNING_TRIAL_DURATION_MINUTES` (or legacy `CONTINUOUS_LEARNING_TRIAL_DURATION_MINUTES`)
## Output format in AGENTS.md
The skill writes only:
- `## Learned User Preferences`
- `## Learned Workspace Facts`
Each item is a plain bullet point.
## License
MIT
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@@ -0,0 +1,247 @@
/// <reference types="bun-types-no-globals/lib/index.d.ts" />
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
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 DEFAULT_MIN_TURNS = 10;
const DEFAULT_MIN_MINUTES = 120;
const TRIAL_DEFAULT_MIN_TURNS = 3;
const TRIAL_DEFAULT_MIN_MINUTES = 15;
const TRIAL_DEFAULT_DURATION_MINUTES = 24 * 60;
const FOLLOWUP_MESSAGE =
`Run the \`continual-learning\` skill now. First read existing \`AGENTS.md\` and update existing entries in place (do not only append). Use incremental transcript processing with index file \`${INCREMENTAL_INDEX_PATH}\`: only read transcripts not in the index or transcripts whose mtime is newer than indexed mtime (re-read changed transcripts). After processing, write back the updated index mtimes and remove entries for deleted transcripts. Update \`AGENTS.md\` only for high-signal, repeated user-correction patterns or durable workspace facts. Exclude one-off/transient details and secrets. Keep each learned section to at most 12 bullets. Write plain bullet points only, with no evidence/confidence tags or other metadata annotations. If no meaningful updates exist, respond exactly: No high-signal memory updates.`;
interface StopHookInput {
conversation_id: string;
generation_id?: string;
status: "completed" | "aborted" | "error" | string;
loop_count: number;
transcript_path?: string | null;
}
interface ContinuousLearningState {
version: 1;
lastRunAtMs: number;
turnsSinceLastRun: number;
lastTranscriptMtimeMs: number | null;
lastProcessedGenerationId: string | null;
trialStartedAtMs: number | null;
}
function parsePositiveInt(value: string | undefined, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return fallback;
}
return parsed;
}
function parseBoolean(value: string | undefined): boolean {
if (!value) {
return false;
}
const normalized = value.trim().toLowerCase();
return (
normalized === "1" ||
normalized === "true" ||
normalized === "yes" ||
normalized === "on"
);
}
function readEnvValue(primary: string, legacy: string): string | undefined {
return process.env[primary] ?? process.env[legacy];
}
function loadState(): ContinuousLearningState {
const fallback: ContinuousLearningState = {
version: 1,
lastRunAtMs: 0,
turnsSinceLastRun: 0,
lastTranscriptMtimeMs: null,
lastProcessedGenerationId: null,
trialStartedAtMs: null,
};
if (!existsSync(STATE_PATH)) {
return fallback;
}
try {
const raw = readFileSync(STATE_PATH, "utf-8");
const parsed = JSON.parse(raw) as Partial<ContinuousLearningState>;
if (parsed.version !== 1) {
return fallback;
}
return {
version: 1,
lastRunAtMs:
typeof parsed.lastRunAtMs === "number" && Number.isFinite(parsed.lastRunAtMs)
? parsed.lastRunAtMs
: 0,
turnsSinceLastRun:
typeof parsed.turnsSinceLastRun === "number" &&
Number.isFinite(parsed.turnsSinceLastRun) &&
parsed.turnsSinceLastRun >= 0
? parsed.turnsSinceLastRun
: 0,
lastTranscriptMtimeMs:
typeof parsed.lastTranscriptMtimeMs === "number" &&
Number.isFinite(parsed.lastTranscriptMtimeMs)
? parsed.lastTranscriptMtimeMs
: null,
lastProcessedGenerationId:
typeof parsed.lastProcessedGenerationId === "string"
? parsed.lastProcessedGenerationId
: null,
trialStartedAtMs:
typeof parsed.trialStartedAtMs === "number" &&
Number.isFinite(parsed.trialStartedAtMs)
? parsed.trialStartedAtMs
: null,
};
} catch {
return fallback;
}
}
function saveState(state: ContinuousLearningState): void {
const directory = dirname(STATE_PATH);
if (!existsSync(directory)) {
mkdirSync(directory, { recursive: true });
}
writeFileSync(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
}
function getTranscriptMtimeMs(transcriptPath: string | null | undefined): number | null {
if (!transcriptPath) {
return null;
}
try {
return statSync(transcriptPath).mtimeMs;
} catch {
return null;
}
}
function shouldCountTurn(input: StopHookInput): boolean {
return input.status === "completed" && input.loop_count === 0;
}
async function parseHookInput<T>(): Promise<T> {
const text = await stdin.text();
return JSON.parse(text) as T;
}
async function main(args: string[]): Promise<number> {
try {
const input = await parseHookInput<StopHookInput>();
const state = loadState();
if (input.generation_id && input.generation_id === state.lastProcessedGenerationId) {
console.log(JSON.stringify({}));
return 0;
}
state.lastProcessedGenerationId = input.generation_id ?? null;
const countedTurn = shouldCountTurn(input);
const turnIncrement = countedTurn ? 1 : 0;
const turnsSinceLastRun = state.turnsSinceLastRun + turnIncrement;
const now = Date.now();
const trialEnabled =
args.includes("--trial") ||
parseBoolean(readEnvValue("CONTINUAL_LEARNING_TRIAL_MODE", "CONTINUOUS_LEARNING_TRIAL_MODE"));
if (trialEnabled && countedTurn && state.trialStartedAtMs === null) {
state.trialStartedAtMs = now;
}
const trialDurationMinutes = parsePositiveInt(
readEnvValue(
"CONTINUAL_LEARNING_TRIAL_DURATION_MINUTES",
"CONTINUOUS_LEARNING_TRIAL_DURATION_MINUTES"
),
TRIAL_DEFAULT_DURATION_MINUTES
);
const trialMinTurns = parsePositiveInt(
readEnvValue(
"CONTINUAL_LEARNING_TRIAL_MIN_TURNS",
"CONTINUOUS_LEARNING_TRIAL_MIN_TURNS"
),
TRIAL_DEFAULT_MIN_TURNS
);
const trialMinMinutes = parsePositiveInt(
readEnvValue(
"CONTINUAL_LEARNING_TRIAL_MIN_MINUTES",
"CONTINUOUS_LEARNING_TRIAL_MIN_MINUTES"
),
TRIAL_DEFAULT_MIN_MINUTES
);
const inTrialWindow =
trialEnabled &&
state.trialStartedAtMs !== null &&
now - state.trialStartedAtMs < trialDurationMinutes * 60_000;
const minTurns = parsePositiveInt(
readEnvValue("CONTINUAL_LEARNING_MIN_TURNS", "CONTINUOUS_LEARNING_MIN_TURNS"),
DEFAULT_MIN_TURNS
);
const minMinutes = parsePositiveInt(
readEnvValue("CONTINUAL_LEARNING_MIN_MINUTES", "CONTINUOUS_LEARNING_MIN_MINUTES"),
DEFAULT_MIN_MINUTES
);
const effectiveMinTurns = inTrialWindow ? trialMinTurns : minTurns;
const effectiveMinMinutes = inTrialWindow ? trialMinMinutes : minMinutes;
const minutesSinceLastRun =
state.lastRunAtMs > 0
? Math.floor((now - state.lastRunAtMs) / 60000)
: Number.POSITIVE_INFINITY;
const transcriptMtimeMs = getTranscriptMtimeMs(input.transcript_path);
const hasTranscriptAdvanced =
transcriptMtimeMs !== null &&
(state.lastTranscriptMtimeMs === null || transcriptMtimeMs > state.lastTranscriptMtimeMs);
const shouldTrigger =
countedTurn &&
turnsSinceLastRun >= effectiveMinTurns &&
minutesSinceLastRun >= effectiveMinMinutes &&
hasTranscriptAdvanced;
if (shouldTrigger) {
state.lastRunAtMs = now;
state.turnsSinceLastRun = 0;
state.lastTranscriptMtimeMs = transcriptMtimeMs;
saveState(state);
console.log(
JSON.stringify({
followup_message: FOLLOWUP_MESSAGE,
})
);
return 0;
}
state.turnsSinceLastRun = turnsSinceLastRun;
saveState(state);
console.log(JSON.stringify({}));
return 0;
} catch (error) {
console.error("[continual-learning-stop] failed", error);
console.log(JSON.stringify({}));
return 0;
}
}
const exitCode = await main(process.argv.slice(2));
process.exit(exitCode);
+10
View File
@@ -0,0 +1,10 @@
{
"version": 1,
"hooks": {
"stop": [
{
"command": "bun run ./hooks/continual-learning-stop.ts --trial"
}
]
}
}
@@ -0,0 +1,72 @@
---
name: continual-learning
description: Incrementally extract recurring user corrections and durable workspace facts from transcript changes, then update AGENTS.md with plain bullet points only. Use when the user asks to mine previous chats, maintain AGENTS.md memory, or build a self-learning preference loop.
---
# Continual Learning
Keep `AGENTS.md` current using transcript deltas instead of full rescans.
## Inputs
- Transcript root: `~/.cursor/projects/<workspace-slug>/agent-transcripts/`
- Existing memory file: `AGENTS.md`
- Incremental index: `.cursor/hooks/state/continual-learning-index.json`
## Workflow
1. Read existing `AGENTS.md` first.
2. Load incremental index if present.
3. Discover transcript files and process only:
- new files not in index, or
- files whose mtime is newer than indexed mtime.
4. Extract only high-signal, reusable information:
- recurring user corrections/preferences
- durable workspace facts
5. Merge with existing bullets in `AGENTS.md`:
- update matching bullets in place
- add only net-new bullets
- deduplicate semantically similar bullets
6. Write back the incremental index:
- store latest mtimes for processed files
- remove entries for files that no longer exist
## AGENTS.md Output Contract
- Keep only these sections:
- `## Learned User Preferences`
- `## Learned Workspace Facts`
- Use plain bullet points only.
- Do not write evidence/confidence tags.
- Do not write process instructions, rationale, or metadata blocks.
## Inclusion Bar
Keep an item only if all are true:
- actionable in future sessions
- stable across sessions
- repeated in multiple transcripts, or explicitly stated as a broad rule
- non-sensitive
## Exclusions
Never store:
- secrets, tokens, credentials, private personal data
- one-off task instructions
- transient details (branch names, commit hashes, temporary errors)
## Incremental Index Format
```json
{
"version": 1,
"transcripts": {
"/abs/path/to/file.jsonl": {
"mtimeMs": 1730000000000,
"lastProcessedAt": "2026-02-18T12:00:00.000Z"
}
}
}
```