mirror of
https://github.com/cursor/plugins.git
synced 2026-09-14 20:00:00 +08:00
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:
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user