# learned


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

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`](https://vedicreader.github.io/ramabana/learned.html#pending)
lists what waits,
[`diff`](https://vedicreader.github.io/ramabana/learned.html#diff) shows
one candidate’s change,
[`accept`](https://vedicreader.github.io/ramabana/learned.html#accept)
moves it onto HEAD, and
[`reject`](https://vedicreader.github.io/ramabana/learned.html#reject)
drops it. Each of
[`diff`](https://vedicreader.github.io/ramabana/learned.html#diff),
[`accept`](https://vedicreader.github.io/ramabana/learned.html#accept)
and
[`reject`](https://vedicreader.github.io/ramabana/learned.html#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`](https://vedicreader.github.io/ramabana/learned.html#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`](https://vedicreader.github.io/ramabana/learned.html#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`](https://vedicreader.github.io/ramabana/learned.html#diff),
[`accept`](https://vedicreader.github.io/ramabana/learned.html#accept)
and
[`reject`](https://vedicreader.github.io/ramabana/learned.html#reject)
act on like any other. `compact(folder, id)` also folds in pending
candidate `id`, the case
[`accept`](https://vedicreader.github.io/ramabana/learned.html#accept)
refuses for size: the refusal returns `length` and names
`/learned compact ID`, and accepting that compaction accepts the
candidate with it.
[`compact`](https://vedicreader.github.io/ramabana/learned.html#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.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L168"
target="_blank" style="float:right; font-size:smaller">source</a>

### reject

``` python
def reject(
    folder, id
):
```

*Drop pending candidate `id`; `{ok, id}`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L145"
target="_blank" style="float:right; font-size:smaller">source</a>

### learned_command

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

*`/learned` and `ramabana-learned` as text: `list` by default,
[`compact`](https://vedicreader.github.io/ramabana/learned.html#compact),
`on`, `off` or `status`, or
[`diff`](https://vedicreader.github.io/ramabana/learned.html#diff),
[`accept`](https://vedicreader.github.io/ramabana/learned.html#accept)
or
[`reject`](https://vedicreader.github.io/ramabana/learned.html#reject)
with an id.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L137"
target="_blank" style="float:right; font-size:smaller">source</a>

### compact

``` python
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}`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L112"
target="_blank" style="float:right; font-size:smaller">source</a>

### accept

``` python
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}`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L107"
target="_blank" style="float:right; font-size:smaller">source</a>

### diff

``` python
def diff(
    folder, id
):
```

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

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L88"
target="_blank" style="float:right; font-size:smaller">source</a>

### pending

``` python
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.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L74"
target="_blank" style="float:right; font-size:smaller">source</a>

### restack

``` python
def restack(
    folder
):
```

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

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L46"
target="_blank" style="float:right; font-size:smaller">source</a>

### learned_folder

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

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

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L42"
target="_blank" style="float:right; font-size:smaller">source</a>

### set_enabled

``` python
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.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L34"
target="_blank" style="float:right; font-size:smaller">source</a>

### enabled

``` python
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`](https://vedicreader.github.io/ramabana/learned.html#restack)
moves every other candidate proposed on an older HEAD onto the new one
where it picks cleanly, and
[`accept`](https://vedicreader.github.io/ramabana/learned.html#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`](https://vedicreader.github.io/ramabana/learned.html#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.

``` python
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]

``` python
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
```

``` python
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`](https://vedicreader.github.io/ramabana/learned.html#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`](https://vedicreader.github.io/ramabana/learned.html#compact)
proposes a shorter `AGENTS.md`.

Learning is one setting per machine, `learning` in config.toml (see
[setup](19_setup.ipynb)), 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()`](https://vedicreader.github.io/ramabana/agent.html#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`](https://vedicreader.github.io/ramabana/learned.html#set_enabled)
raises `ValueError` saying so, and `/learned on` returns that message:

``` python
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')
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/learned.py#L178"
target="_blank" style="float:right; font-size:smaller">source</a>

### learned_cli

``` python
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.*
