reject
def reject(
folder, id
):Drop pending candidate id; {ok, id}.
<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.
/learned and ramabana-learned as text: list by default, compact, on, off or status, or diff, accept or reject with an id.
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}.
Move pending candidate id onto HEAD, which the next briefing reads, then the other candidates onto it: {ok, id, commit, moved, stale}.
What accepting pending candidate id would change, as {ok, id, diff}.
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.
Move each pending candidate proposed on an older HEAD onto HEAD where it picks cleanly; returns (moved, stale) ids.
<cfg>/learned, whether or not a session runs with it.
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.
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'] = modwith 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'] == STALElearned_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')Review what Dhrona proposed for the learned folder: list it, diff, accept or reject one, or ask for a compaction.