Agent Workflow Kit
Chapter 12
Technical engraving of transcripts feeding into a sorting hopper and indexed card drawers
Fig. 12Transcript memory sorted into drawers.

12 — Continual learning

When to read this: When you want the project to get smarter over time without manually retraining the agent. Covers an optional but recommended part of the kit.

The pattern#

CLAUDE.md is authored memory. You wrote it. The agent reads it every turn. It changes when you change it.

AGENTS.md is learned memory. It captures recurring user preferences and durable workspace facts that emerge from your actual usage: "the user always wants commit messages in imperative mood" or "this project's database is Postgres on Neon, accessed via Drizzle." It's updated automatically by the kit's continual-learning system.

The point: when you correct the agent for the seventh time about your preferred PR description format, that correction surfaces as an AGENTS.md bullet a few turns later. The next session reads it. The project gets smarter without retraining.

How AGENTS.md is structured#

The kit's bootstrapped AGENTS.md has just two sections:

## Learned User Preferences
- Prefers tracking work in the in-repo markdown backlog (docs/backlog.md inbox plus docs/backlog/phase-*.md) as the task system of record.
- Frontend UI work uses /craft-ui (visual review gate at 4 viewports); backend / infra work uses kickoff-spec (tests-pass gate).
- DESIGN.md at repo root is the source of truth for visual decisions. CLAUDE.md covers code; DESIGN.md covers pixels. Don't overload one with the other.
- ...
 
## Learned Workspace Facts
- {{STACK_FACT_ONE}}
- {{STACK_FACT_TWO}}

Two rules:

  • Bullets are short. Each one captures one durable preference or fact.
  • Each section is capped at 12 bullets. Older or less-load-bearing items get dropped as new ones are added.

Things that don't belong in AGENTS.md:

  • Secrets or private data.
  • One-off instructions ("for this turn only, do X").
  • Transient details (current task state, in-progress work).
  • Anything already in CLAUDE.md, DESIGN.md, or docs/PRD.md. Those are the source of truth. AGENTS.md is for learned deltas only.

The three pieces#

The continual-learning system has three components, all installed by agent-workflow:

PiecePathRole
Skill.claude/skills/continual-learning/SKILL.mdOrchestration. Delegates to the subagent and returns the result.
Subagent.claude/agents/agents-memory-updater.mdMines transcript deltas, updates AGENTS.md, refreshes the index. Only writes the ## Learned User Preferences and ## Learned Workspace Facts sections.
Stop hook.claude/hooks/continual-learning-stop.tsBun TypeScript. Tracks cadence; on eligible Stop events emits {"decision":"block","reason":"..."} to inject a follow-up that runs the skill.

The hook is wired into .claude/settings.json Stop hook block alongside check-tokens.sh. Hook state lives at .claude/hooks/state/ — gitignore that directory.

How a memory update happens#

The flow:

  1. Every Stop event (every time Claude Code finishes a turn), the Stop hook runs.
  2. The hook tracks cadence in .claude/hooks/state/continual-learning.json. It increments a turn counter, tracks time since last run, and tracks the transcript file's mtime.
  3. When cadence is met (default: ≥10 turns + ≥120 minutes since last run + transcript actually advanced), the hook emits {"decision":"block","reason":"<follow-up instruction>"}.
  4. Claude Code injects the follow-up. Instead of finishing the turn, the agent receives the follow-up: "Run the continual-learning skill now."
  5. The continual-learning skill delegates to the agents-memory-updater subagent.
  6. The subagent:
    • Reads existing AGENTS.md.
    • Loads the incremental index at .claude/hooks/state/continual-learning-index.json.
    • Resolves the Claude Code transcript directory for the workspace (encoded cwd form: /Users/me/Code/foo~/.claude/projects/-Users-me-Code-foo/).
    • Inspects only *.jsonl files there that are new or whose mtime is newer than the indexed mtime.
    • Pulls out durable, reusable items: recurring preferences/corrections and stable workspace facts.
    • Updates AGENTS.md carefully — updates matching bullets in place, adds only net-new bullets, deduplicates semantically similar bullets, caps each section at 12.
    • Refreshes the index for processed transcripts and removes entries for files that no longer exist.
  7. If no meaningful updates exist, the subagent responds exactly: No high-signal memory updates. and AGENTS.md is left unchanged. The index is still refreshed.

The whole process happens in-session. The memory update appears as a follow-up turn in the chat.

Default cadence#

The default thresholds are tuned for a normal workday. A memory update fires roughly every few hours of active work:

  • ≥ 10 turns since the last run.
  • ≥ 120 minutes since the last run.
  • Transcript mtime actually advanced (catches the case where Claude Code is open but you're not using it).

All three must be true. Prevents the hook from firing during quick interactions or while you're idle.

Why these defaults#

  • Lower turn counts (e.g., 5) produce too much memory churn. Every minor preference becomes a bullet.
  • Higher turn counts (e.g., 50) miss real signal because corrections get forgotten before they're mined.
  • Lower minute thresholds fire mid-flow and produce context noise.
  • Higher minute thresholds make the loop feel inert.

Ten turns + two hours is the calibration that's worked in practice. Tune below.

Trial mode#

If you've just installed the kit and you want to verify the loop works without waiting two hours, set:

export CONTINUAL_LEARNING_TRIAL_MODE=1

Trial mode uses looser thresholds:

  • ≥ 3 turns instead of 10.
  • ≥ 15 minutes instead of 120.
  • Expires after 24 hours (then reverts to defaults).

Trial mode is for verification, not normal operation. Once you've seen the loop fire successfully, unset the env var:

unset CONTINUAL_LEARNING_TRIAL_MODE

(Or remove it from your shell rc.)

Tuning the cadence for your team#

The cadence thresholds are read from environment variables. Override them in your shell rc:

export CONTINUAL_LEARNING_MIN_TURNS=15            # default 10
export CONTINUAL_LEARNING_MIN_MINUTES=180         # default 120
export CONTINUAL_LEARNING_TRIAL_MIN_TURNS=5       # default 3
export CONTINUAL_LEARNING_TRIAL_MIN_MINUTES=30    # default 15
export CONTINUAL_LEARNING_TRIAL_DURATION_MINUTES=2880  # default 1440 (24h)

For a team where multiple people work in the same project across the day, looser cadence (higher turn count, longer interval) keeps AGENTS.md updates from firing too often. For a solo workflow with intense bursts, tighter cadence keeps memory fresh.

Manually triggering a memory update#

You can bypass the cadence entirely by invoking the skill directly:

continual-learning

Or by invoking the subagent directly via the Agent tool:

have agents-memory-updater check the recent transcripts

Useful when:

  • You just had a long correction session and want the lessons captured immediately.
  • The Stop hook isn't firing for some reason and you want to verify the rest of the loop works.
  • You're cleaning up AGENTS.md by hand and want the subagent to deduplicate after.

The incremental index#

The index file at .claude/hooks/state/continual-learning-index.json is what makes the system fast. Without it, the subagent would re-read every transcript on every run.

Schema:

{
  "version": 1,
  "transcripts": {
    "/absolute/path/to/transcript-uuid.jsonl": { "mtimeMs": 1721234567890.123 }
  }
}

For each transcript, the index records the file path and its exact mtimeMs (floating-point milliseconds from fs.statSync). On the next run, the subagent compares each transcript's current mtime against the indexed one and only processes files that are new or modified.

After processing, the index is rebuilt: every existing parent transcript file is added or updated, and entries for deleted transcripts are removed. This keeps the index aligned with what's actually on disk.

Why exact mtime matters#

JavaScript's fs.statSync returns a floating-point millisecond value. If the index stores a rounded integer (1721234567890) and the actual mtime is 1721234567890.123, the comparison transcriptMtimeMs > indexedMtime is always true and the file gets reprocessed forever.

The kit's bootstrap copies a starter index (continual-learning/index.starter.json) which is just an empty {}. The bootstrap places copies at both .cursor/hooks/state/continual-learning-index.json (for Cursor's continual-learning plugin if you use Cursor) and .claude/hooks/state/continual-learning-index.json (for the kit's Claude Code system). Both are harmless if you only use one client.

Gitignoring the state directory#

.claude/hooks/state/ should be gitignored. Both the cadence state and the transcript index are local to the developer's machine. Add to .gitignore:

.claude/hooks/state/
.cursor/hooks/state/

The starter index ({}) is committed by bootstrap initially; subsequent updates land outside git.

Loop guard#

The Stop hook has a built-in loop guard. If a previous Stop hook already blocked the agent (stop_hook_active === true in the input JSON), the hook short-circuits and emits an empty {} instead of blocking again.

This prevents the failure mode where:

  1. Hook A blocks → agent runs continual-learning.
  2. continual-learning finishes → Stop event fires.
  3. Hook A blocks again → infinite loop.

With the guard, step 3 doesn't happen. The continual-learning run completes; the next Stop event resets the cadence; the loop is healthy.

Troubleshooting#

Continual learning never fires#

Three things to check:

  1. Bun is installed. which bun should return a path. The hook is a Bun TypeScript file; without Bun, it errors silently.
  2. The hook is wired in .claude/settings.json. Check the file. The hooks.Stop[].hooks array should include { "type": "command", "command": "bun run .claude/hooks/continual-learning-stop.ts" }. If not, copy the block from templates/claude-settings.example.json.
  3. You're past the cadence threshold. Default is ≥10 turns + ≥120 minutes. If you've been running for less than two hours, the loop won't fire. Use trial mode (CONTINUAL_LEARNING_TRIAL_MODE=1) to verify the rest of the loop works in a shorter window.

The state file is missing or corrupted#

Delete it:

rm .claude/hooks/state/continual-learning.json

The hook will recreate it on the next run with a fresh state. You'll lose the cadence count, but the index is separate and will still work.

The index is missing#

Same fix:

echo '{}' > .claude/hooks/state/continual-learning-index.json

Or copy from the kit:

cp ~/.local/share/agent-workflow-kit/continual-learning/index.starter.json \
   .claude/hooks/state/continual-learning-index.json

The next memory update will reprocess all transcripts (slow once, then incremental).

AGENTS.md is growing too long#

Each section caps at 12 bullets, but the cap only applies during memory updates. If you've been editing AGENTS.md by hand and added more than 12 bullets to a section, the subagent will trim on the next update. To force a trim now:

have agents-memory-updater clean up AGENTS.md

The subagent will deduplicate and cap the sections.

AGENTS.md has facts that don't apply anymore#

Edit AGENTS.md by hand. Delete the bullets that are wrong. The subagent respects your edits. It updates matching bullets and adds new ones. It doesn't fight your manual deletions. If a deleted bullet keeps reappearing because the signal is still in transcripts, the pattern is genuinely durable and you're going to keep hitting it. Either accept the bullet, or change the upstream behavior.

Cursor and Claude Code indexes diverge#

If you switch between Cursor and Claude Code on the same project, both clients have their own continual-learning state. The kit creates index files for both: .cursor/hooks/state/ and .claude/hooks/state/. The cursor-side index is consumed by Cursor's continual-learning plugin (if installed). The Claude-side index is consumed by the kit's hook.

Both systems update the same AGENTS.md (one file at the repo root), but they mine different transcripts. No conflict. Some duplication of work. If you primarily use one client, ignore the other's state directory.

"No high-signal memory updates" runs every time#

If the subagent never finds anything to add, it's mining transcripts but not finding durable patterns. Two possibilities:

  1. Your sessions are short or vary widely. Recurring patterns need at least a few sessions to be detectable. Be patient.
  2. The patterns are already in CLAUDE.md or DESIGN.md. The subagent skips anything covered by authoritative docs. If you're correcting the agent on points already in CLAUDE.md, the corrections aren't durable signal. They're a sign that CLAUDE.md isn't being read, or is itself wrong.

What this system isn't#

A few things continual learning doesn't do:

  • Doesn't replace CLAUDE.md. CLAUDE.md is authored constraints. AGENTS.md is learned deltas. Different purposes.
  • Doesn't auto-write code. The subagent only writes ## Learned User Preferences and ## Learned Workspace Facts sections of AGENTS.md. It doesn't touch your code or other docs.
  • Doesn't share across projects. Each project has its own AGENTS.md and its own index. For a preference that applies across all your projects, put it in your user-level memory (handled outside this kit) or copy it into each project's CLAUDE.md.
  • Doesn't run on Cursor turns. The Stop hook is Claude Code-specific. Cursor users get the same memory loop via Cursor's continual-learning plugin (the kit ports the same pattern across both clients), but the trigger mechanism is different.

Continue#

You've seen all the major systems the kit ships. Next: Chapter 13: Extending the kit covers customizing and extending it for your team.