atl learnings
Inspect and drain the durable learning queue — the substrate the self-driving learning loop runs on.
Markers captured in conversation (the inline <!-- learning ... --> notes Claude drops mid-session) are transferred into the queue exactly once, deduped by content hash. The /drain skill folds each pending item into the knowledge base (wiki / journal / agent KB), then acks it — so a processed item is deleted and can never be re-reported. That processed-then-deleted design is what structurally kills v1's long-session re-report bug class: reports come from the queue, never from re-scanning an ever-growing transcript.
The queue is one embedded bbolt file at ~/.atl/queue.db — no server, no daemon. Every project's queue lives in that one file, isolated into per-project buckets keyed by the working directory. All of these subcommands operate on the current project (the directory you run them in).
When to use it
You will rarely run these by hand — the loop drives them automatically. Reach for them to:
status— glance at how much is waiting to be folded into your knowledge base (this is the same count theSessionStarthook surfaces).peek— see the actual pending items, or feed the machine-readable list to a script. This is the deterministic read surface the/drainskill consumes.ack— manually mark an item processed (delete it) if you want to skip something the loop would otherwise fold in.transcript— print the conversation flow (prose only). This is the read surface a drain's mining step uses to recover learnings the agent forgot to mark; with--channelit becomes that channel's resumable forward sweep.
Usage
atl learnings status # pending counts per channel
atl learnings status --json # pending counts as JSON (channel→count)
atl learnings peek # list pending items (human-readable)
atl learnings peek --json # full machine-readable list
atl learnings peek --channel learning # filter to one channel
atl learnings ack <id> # mark an item processed (delete it)
atl learnings transcript # recent conversation flow (a plain read)
atl learnings transcript --json # the same flow as role/text records
atl learnings transcript --channel learning # sweep forward, advancing that channel's cursor
atl learnings recover # list items stranded in deleted projects (dry run)
atl learnings recover --apply # move them into this project, where a drain can reach themSubcommands
atl learnings status
Prints the pending item count for each channel, read straight from the queue (correct by construction, never inferred). The channels are core's own learning plus any channel an installed team declares — profile-team's profile-fact is the shipped example (see declaring a capture channel). When nothing is queued it prints:
learning queue: empty (nothing pending)Otherwise:
learning queue — pending by channel:
learning 3
profile-fact 1With --json it emits the same counts as a stable JSON object (channel→count) instead — the lightweight machine-readable view its siblings peek and transcript already expose. The keys are sorted, and an empty queue is {} (not null), so the output is stable for scripts.
| Flag | Type | Default | What it does |
|---|---|---|---|
--json | bool | false | Emit the pending counts as a JSON object (channel→count); {} when empty. |
$ atl learnings status --json
{"learning":3,"profile-fact":1}atl learnings peek
Lists the pending items the /drain skill works through — id, channel, and the first line of the payload. With nothing queued it prints no pending items.
| Flag | Type | Default | What it does |
|---|---|---|---|
--channel <name> | string | (all) | Filter to one channel (e.g. learning). A channel no installed team declares is rejected, and the error lists the ones actually active — so a typo fails loudly instead of quietly matching nothing. |
--json | bool | false | Emit the full pending list as JSON (id, channel, payload, enqueued_at) — the form the /drain skill drives off of. |
Human-readable output shows a truncated 12-character id, the channel, and the payload's first line:
a1b2c3d4e5f6 learning BSD sed requires escaped pipes for alternation …atl learnings ack <id>
Deletes a processed item from the queue — processed-then-deleted, so it can never resurface. Takes exactly one id — the full id, or any unambiguous prefix of it, including the 12-char form peek prints (it resolves git-short-SHA style; an unknown or ambiguous prefix errors instead of guessing). An id that matches no pending item — a typo, or one you already acked — is rejected with an error rather than silently succeeding, so a wrong id fails loudly instead of pretending to work. The /drain skill acks each item exactly once after integrating it.
acked a1b2c3d4e5f6...atl learnings transcript
Prints the user + assistant conversation flow for the current project — prose only; tool calls and tool results are stripped as noise. This is the read surface a drain's mining step works from: it scans the flow for corrections, reverts, and durable facts the agent never marked, then enqueues each (deduped by the queue's content hash, so re-reading is always safe).
It has two modes, and the difference is whether it keeps a cursor.
| Flag | Type | Default | What it does |
|---|---|---|---|
--channel <name> | string | (none) | Sweep forward for this capture channel, advancing its cursor. Must be an active channel. |
--limit <n> | int | 2 | Read the most recent N transcripts. Applies to the cursorless read only. |
--json | bool | false | Emit the turns as JSON (role, text) instead of [role] text lines. |
Bare — a plain read. It emits the most recent 256 KB of prose from the most recent --limit transcripts and records nothing, so running it to have a look can never consume a drain's unmined material. When older turns are cut, a note says so (on stderr, so --json stays a parseable array).
--channel <name> — that channel's sweep. It resumes from where the channel last mined, emits the next 256 KB of prose going forward, advances the cursor, and reports what is still pending:
atl: 12.4 MB of transcript still unmined for channel "learning" — sweep again to continueSuccessive sweeps therefore cover a session completely, instead of re-reading its tail while whatever arrived between two runs is read by neither. The budget is per invocation and deliberately sized to a mining subagent's context, so a large backlog drains across several runs rather than in one. --limit does not apply: a transcript with unmined turns is never skipped for being old.
The cursor is kept per channel (at ~/.atl/mine-cursor/) because the mine has more than one consumer — /drain sweeps for learning, profile-team's /profile-drain for profile-fact, and both can run off the same turn. A single shared position would let whichever ran first consume the window the other still needed. A channel's first sweep has no position to resume from, so it reads the recent tail and baselines every existing transcript at its current end — adopting the cursor does not replay every session the project has ever had.
Either mode reports any transcript record skipped for being over-long, so turns missing from the flow are never silent.
Human-readable output is one line per turn:
[user] no, use refresh tokens not sessions
[assistant] Good call — switching to refresh tokens.atl learnings recover
Moves pending items out of buckets whose project directory no longer exists and into the current project's, where a drain can reach them.
The queue partitions by project, and every read surface is project-scoped — so a bucket keyed to a deleted directory is invisible from everywhere, indistinguishable from no bucket at all. atl work dispatch cuts one git worktree per unit and deletes it on completion, so before the key became the repository root, an autonomous worker's markers were queued under a path that then ceased to exist. Measured 2026-08-08: 13 items across 6 vanished buckets, seven of them real learnings captured by delivery workers three weeks earlier. The payloads were intact the whole time; only their address was gone.
Keying by the repository root stops new losses. It cannot surface what is already stranded — after a re-key, nothing looks for the old addresses — which is why this exists beside it.
Dry run by default. --apply performs the move. Payload, id and the original capture time are preserved, so a drain sees a three-week-old rescue rather than something captured today; an id the target already holds is left alone, so recovering twice never duplicates. No tombstone is written for the source: a tombstone means processed, and a stranded item never was.
atl doctor reports the same condition as queue-stranded.
Examples
Check what's waiting, then look at it:
atl learnings status
atl learnings peekDrive the queue from a script — read the JSON, integrate each item, ack it:
atl learnings peek --channel learning --json
# ... process each item ...
atl learnings ack <id>Related
/drain— the skill that readspeek, folds each item into the knowledge base, andacks it. The everyday way the queue is drained; thelearningssubcommands are its deterministic plumbing.atl setup-hooks— wires theSessionStarthook that surfaces the pending count and transfers captured markers into the queue.