learned

The review queue for <cfg>/learned: what Dhrona proposed waits on a branch until a person accepts or rejects it.

Dhrona’s nightly batch never changes the files Ramabana briefs from. It commits each candidate it keeps on its own pending/<id> branch of the learned folder’s git repository. The briefing and skill discovery read only the folder’s files, which are its HEAD, so a pending candidate has no effect until a person accepts it. --no-learned leaves out the folder whatever it holds.

pending lists what waits, diff shows one candidate’s change, accept moves it onto HEAD, and reject drops it. Each of diff, accept and reject returns a dict with ok and id, and error when ok is False, so a host such as Leela can show the result as it is. They take the lock a Dhrona batch holds, and refuse while a batch runs. accept also refuses a candidate that would take AGENTS.md past LEARNED_MAX, the 2,000 characters Dhrona allows one candidate, since accepted rounds add up.

compact asks Dhrona for a shorter AGENTS.md when accepted lessons crowd it. Dhrona’s editor model merges and shortens the paragraphs Dhrona proposed, keeps every paragraph a person wrote byte for byte, and commits the result as one more pending candidate, which diff, accept and reject act on like any other. compact(folder, id) also folds in pending candidate id, the case accept refuses for size: the refusal returns length and names /learned compact ID, and accepting that compaction accepts the candidate with it. compact returns {ok, id, before, after}, where before and after are the file’s length in characters, or {ok: False, error}. It needs the dhrona extra, and says so when Dhrona is missing. It runs one model turn, so it takes as long as that turn.


source

reject

def reject(
    folder, id
):

Drop pending candidate id; {ok, id}.


source

learned_command

def learned_command(
    folder, arg:str=''
):

/learned and ramabana-learned as text: list by default, compact, on, off or status, or diff, accept or reject with an id.


source

compact

def compact(
    folder, id:NoneType=None, chat:NoneType=None
):

Ask Dhrona for a shorter AGENTS.md as a pending candidate, folding in pending id if given: {ok, id, before, after} in characters, or {ok: False, error}.


source

accept

def accept(
    folder, id
):

Move pending candidate id onto HEAD, which the next briefing reads, then the other candidates onto it: {ok, id, commit, moved, stale}.


source

diff

def diff(
    folder, id
):

What accepting pending candidate id would change, as {ok, id, diff}.


source

pending

def pending(
    folder
):

Each candidate waiting for review, oldest first, as {id, at, subject, files, stale}; at is when it was proposed, in epoch seconds, and stale why it cannot be accepted or None.


source

restack

def restack(
    folder
):

Move each pending candidate proposed on an older HEAD onto HEAD where it picks cleanly; returns (moved, stale) ids.


source

learned_folder

def learned_folder(
    cfg:str='~/.config/ramabana'
):

<cfg>/learned, whether or not a session runs with it.


source

set_enabled

def set_enabled(
    cfg:str='~/.config/ramabana', on:bool=True
):

Turn learning on or off in this machine’s config.toml, its comments kept; cfg’s settings.json is never written. Raises ValueError for a config.toml that is not TOML.


source

enabled

def enabled(
    cfg:str='~/.config/ramabana'
):

Whether learning is on: learning in this machine’s config.toml, which a value only <cfg>/settings.json holds moves into the first time it is read. On unless set off.

A candidate is accepted in a throwaway worktree first, so a conflict never leaves markers in the files a running session briefs from. HEAD moves only when the pick is clean. After it moves, restack moves every other candidate proposed on an older HEAD onto the new one where it picks cleanly, and accept returns those ids as moved. A candidate that no longer picks cleanly, for example one that edits a line a compaction rewrote, is listed as stale, and pending gives the reason in its stale field, so a host can show it beside the candidate. Ids sort by when Dhrona proposed them, which moving does not change. A candidate whose change HEAD already holds is accepted with commit None. The commit keeps Dhrona as its author, and its commit time is the moment of acceptance, which is where Dhrona’s 30-day expiry clock starts.

import os, json, time
def git(d, *a): return subprocess.run(['git', '-C', str(d), *a], check=True, capture_output=True, text=True).stdout.strip()
def propose(folder, files, id):
    "What Dhrona's `keep` does: a commit on `pending/<id>` off HEAD, the folder's files untouched."
    wt = Path(tempfile.mkdtemp())/'wt'
    git(folder, 'worktree', 'add', '-q', '-b', PENDING + id, str(wt), 'HEAD')
    for rel, text in files.items(): (wt/rel).parent.mkdir(parents=True, exist_ok=True); (wt/rel).write_text(text)
    git(wt, 'add', '-A'); git(wt, '-c', 'user.name=Dhrona', '-c', 'user.email=d@x', 'commit', '-qm', f'Lesson from a replay: {id}')
    git(folder, 'worktree', 'remove', '--force', str(wt))

with tempfile.TemporaryDirectory() as d:
    f = Path(d)/'learned'; f.mkdir()
    (f/'AGENTS.md').write_text('A rule a person wrote.\n')
    git(f, 'init', '-q'); git(f, 'add', '-A'); git(f, '-c', 'user.name=t', '-c', 'user.email=t@x', 'commit', '-qm', 'first')
    a, b, c = '20261019-010101-aaaaaa', '20261019-020202-bbbbbb', '20261019-030303-cccccc'
    propose(f, {'skills/sizes/SKILL.md': '---\nname: sizes\ndescription: x\n---\nuse ls\n'}, a)
    propose(f, {'AGENTS.md': 'Something else entirely.\n'}, b)
    propose(f, {'AGENTS.md': 'A third view.\n'}, c)
    test_eq([(p['id'], p['files']) for p in pending(f)], [(a, ['skills/sizes/SKILL.md']), (b, ['AGENTS.md']), (c, ['AGENTS.md'])])
    test_eq((f/'AGENTS.md').read_text(), 'A rule a person wrote.\n')   # nothing pending reaches the briefing
    assert not (f/'skills').exists()
    assert '+use ls' in diff(f, a)['diff']
    r = accept(f, a)
    test_eq(r['moved'], [b, c])                            # the others now sit on the new HEAD
    assert r['ok'] and r['commit'] and (f/'skills'/'sizes'/'SKILL.md').exists() and a not in [p['id'] for p in pending(f)]
    test_eq(git(f, 'log', '-1', '--format=%an'), 'Dhrona')
    test_eq(accept(f, b)['ok'], True)
    test_eq([p['stale'] for p in pending(f)], [STALE])     # `c` rewrites the line `b` changed
    head, text = git(f, 'rev-parse', 'HEAD'), (f/'AGENTS.md').read_text()
    r = accept(f, c)                                       # it rewrote the line `b` changed
    assert not r['ok'] and 'no longer applies' in r['error'], r
    test_eq((git(f, 'rev-parse', 'HEAD'), (f/'AGENTS.md').read_text()), (head, text))   # HEAD and the files are as they were
    assert '<<<<' not in text and git(f, 'status', '--porcelain') == ''
    test_eq(reject(f, c), dict(ok=True, id=c))
    test_eq(pending(f), [])
    test_eq(accept(f, c)['ok'], False)                     # gone
    test_eq(accept(f, '--force')['ok'], False)             # not an id, never an option
    propose(f, {'AGENTS.md': 'Hand edit pending.\n'}, a)
    (f/'AGENTS.md').write_text('an uncommitted edit\n')
    assert 'uncommitted' in accept(f, a)['error']
    git(f, 'checkout', '-q', '--', '.')
    with _locked(f) as held:                                # a batch holds it
        assert held and 'batch is running' in accept(f, a)['error'] and 'batch is running' in reject(f, a)['error']
    test_eq(len(git(f, 'worktree', 'list').splitlines()), 1)   # no worktree left behind
    test_eq(pending(Path(d)/'none'), [])
    git(f, 'checkout', '-q', '--', '.'); reject(f, a)
    propose(f, {'AGENTS.md': 'A rule a person wrote.\n' + 'x' * LEARNED_MAX}, a)   # past Dhrona's limit
    head, r = git(f, 'rev-parse', 'HEAD'), accept(f, a)
    assert not r['ok'] and 'past the 2,000' in r['error'] and r['length'] > LEARNED_MAX and git(f, 'rev-parse', 'HEAD') == head, r
    assert a in learned_command(f) and 'past the 2,000' in learned_command(f, f'accept {a}')
    test_eq(learned_command(f, f'reject {a}'), f'rejected {a}')
    test_eq(learned_command(f), 'nothing is pending')
    assert 'say /learned' in learned_command(f, 'frobnicate')


parse files from cyclopts:   0%|          | 0/73 [00:00<?, ?it/s]

parse files from cyclopts:  23%|##3       | 17/73 [00:00<00:00, 166.92it/s]

parse files from cyclopts:  55%|#####4    | 40/73 [00:00<00:00, 199.34it/s]

parse files from cyclopts:  95%|#########4| 69/73 [00:00<00:00, 239.15it/s]
parse files from cyclopts: 100%|##########| 73/73 [00:00<00:00, 235.20it/s]
with tempfile.TemporaryDirectory() as d:
    f = Path(d)/'learned'; f.mkdir()
    (f/'AGENTS.md').write_text('A rule a person wrote.\n')
    git(f, 'init', '-q'); git(f, 'add', '-A'); git(f, '-c', 'user.name=t', '-c', 'user.email=t@x', 'commit', '-qm', 'first')
    a = '20261019-010101-aaaaaa'
    propose(f, {'AGENTS.md': 'A rule a person wrote.\n\n' + 'Read sizes with ls -l. ' * 40 + '\n'}, a); accept(f, a)
    r = compact(f, chat=lambda prompt: '[kept 1]\n\nRead sizes with ls -l.')
    assert r['ok'] and r['before'] > r['after'] and r['id'] in learned_command(f), r
    test_eq(git(f, 'show', f"{PENDING}{r['id']}:AGENTS.md"), 'A rule a person wrote.\n\nRead sizes with ls -l.')
    assert 'already pending' in learned_command(f, 'compact')
    import sys; mod = sys.modules.pop('dhrona.improve'); sys.modules['dhrona.improve'] = None   # dhrona not installed
    try: assert 'needs dhrona' in compact(f)['error']
    finally: sys.modules['dhrona.improve'] = mod
with tempfile.TemporaryDirectory() as d:
    f = Path(d)/'learned'; f.mkdir()
    (f/'AGENTS.md').write_text('A rule a person wrote.\n')
    git(f, 'init', '-q'); git(f, 'add', '-A'); git(f, '-c', 'user.name=t', '-c', 'user.email=t@x', 'commit', '-qm', 'first')
    a, big, x, y = '20261019-010101-aaaaaa', '20261019-020202-bbbbbb', '20261019-030303-cccccc', '20261019-040404-dddddd'
    lessons = 'A rule a person wrote.\n\n' + 'Read sizes with ls -l. ' * 40 + '\n'
    propose(f, {'AGENTS.md': lessons}, a); accept(f, a)
    propose(f, {'AGENTS.md': lessons + '\n' + 'Run pytest -q before a commit. ' * 40 + '\n'}, big)
    propose(f, {'skills/rg/SKILL.md': '---\nname: rg\ndescription: x\n---\nuse rg\n'}, x)
    propose(f, {'AGENTS.md': lessons.replace('Read sizes', 'Read file sizes', 1)}, y)
    r = accept(f, big)
    assert not r['ok'] and r['length'] > LEARNED_MAX and f'/learned compact {big}' in r['error'], r
    r = compact(f, big, chat=lambda prompt: '[kept 1]\n\nRead sizes with ls -l.\n\nRun pytest -q before a commit.')
    assert r['ok'] and r['before'] > LEARNED_MAX >= r['after'], r
    assert 'nothing is pending' in compact(f, '20990101-000000-abcdef')['error']
    r = accept(f, r['id'])                                  # the compaction carries `big` with it
    assert r['ok'] and (f/'AGENTS.md').read_text() == 'A rule a person wrote.\n\nRead sizes with ls -l.\n\nRun pytest -q before a commit.\n', r
    test_eq((r['moved'], r['stale']), ([x], [y]))           # `x` moved onto the compacted file; `y` edits a line it rewrote
    test_eq([(p['id'], p['stale']) for p in pending(f)], [(x, None), (y, STALE)])
    assert f'{y}  AGENTS.md' in learned_command(f) and '(stale: it no longer applies' in learned_command(f)
    test_eq(git(f, 'rev-parse', f'{PENDING}{x}^'), git(f, 'rev-parse', 'HEAD'))
    assert accept(f, x)['ok'] and accept(f, y)['error'] == STALE

learned_command is the text form a terminal shows. /learned in a session and ramabana-learned from a shell both use it: with no argument it lists what is pending, diff ID, accept ID and reject ID act on one, and compact proposes a shorter AGENTS.md.

Learning is one setting per machine, learning in config.toml (see setup), and it is on unless set off. set_enabled(cfg, False), /learned off in a session, or ramabana-learned off from a shell turn it off for every session on this machine, whatever its config folder. Off leaves the learned AGENTS.md and learned skills out of the briefing, drops the briefing’s rule to record lessons, keeps remember to plain notes, has Agent.lessons() return none, writes no start.json, and makes Dhrona’s nightly schedule return “learning is off” without touching the learned folder. Lessons already kept stay where they are, unread. The review queue still works, so a person can clear it. Agent(learned=False), the CLI’s --no-learned, turns learning off for one session only.

Ramabana 0.2.44 kept the switch in each config folder’s settings.json. When config.toml does not set learning, enabled(cfg) reads it from <cfg>/settings.json and writes it into config.toml, so a choice made there survives; the settings.json an embedding app shares is never written. A config.toml that is not valid TOML is never rewritten: set_enabled raises ValueError saying so, and /learned on returns that message:

with tempfile.TemporaryDirectory() as d:
    os.environ['RAMABANA_CONFIG'] = str(toml := Path(d)/'config.toml')
    try:
        assert enabled(d)
        test_eq(learned_command(learned_folder(d), 'off'), 'learning is off')
        test_eq((enabled(d), (Path(d)/SETTINGS).exists()), (False, False))
        assert '\nlearning = false\n' in toml.read_text()
        test_eq(set_enabled(d, True), True)
        toml.unlink(); (Path(d)/SETTINGS).write_text(json.dumps({'editor': {'tabSize': 2}, 'learning': 'off'}))
        test_eq(enabled(d), False)   # 0.2.44's setting moves to config.toml, and settings.json is left as it was
        assert 'learning = false' in toml.read_text() and json.loads((Path(d)/SETTINGS).read_text())['learning'] == 'off'
        toml.write_text('learning = [')
        assert 'not valid TOML' in learned_command(learned_folder(d), 'on') and toml.read_text() == 'learning = ['
    finally: os.environ.pop('RAMABANA_CONFIG')

source

learned_cli

def learned_cli(
    cmd:str='list', # list, diff, accept, reject, compact, or on, off or status for learning itself
    id:str='', # the pending id, for diff, accept and reject
    cfg:str='~/.config/ramabana', # the config folder whose `learned` folder to review
):

Review what Dhrona proposed for the learned folder: list it, diff, accept or reject one, or ask for a compaction.