Skip to content

feat(improve): healOnce, one full self-healing cycle - #994

Merged
LinuxDevil merged 3 commits into
mainfrom
s23-heal-once
Nov 8, 2026
Merged

LinuxDevil merged 3 commits into
mainfrom
s23-heal-once

Conversation

@LinuxDevil

@LinuxDevil LinuxDevil commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Why

S23 of the M4 plan (m3-notes/m4/parity.md, section 2a). lousho improve stopped at a printed card. This wires every step of the self-healing loop into one function, healOnce, that the CLI, the watcher (S24) and tests share: kill switch, lock, cooldown and daily budgets, detect, capture, propose, gate, replay canary, apply (auto) or queue (manual), corpus guard, reflection and library vote, audit, and a cycle record. It also adds the per-editor-session wall timeout (Harness² row 7) and the corroboration rule (B3).

R12: products W3.4 and WG.1–3 reuse this cycle through applyProposal/rollbackHarness; nothing product-specific here.

Changes

  • src/improve/heal.ts: healOnce(options): Promise<HealCycle>.
    • Kill switch first (.lousho/improve.stop, LOUSHO_IMPROVE=off, enabled: false) and again just before the write, so a pause that lands mid-cycle applies nothing.
    • One cycle per directory: .lousho/cycle.lock taken with timeoutMs: 0; a second caller gets busy. Cooldown and the daily caps are checked inside the lock, so two callers can't both spend the last slot.
    • Cooldown: any audited rollback within cooldownHours. Budgets: cycles/day and tokens/day from today's cycle records, automatic applies/day from the audit log (gate/canary actors only). The cycle cap is min(maxTokensPerDay − spent today, maxTokensPerCycle, maxTokens); protected-case admission and the canary run through a metered probe, improveAgent gets the remainder as maxTokens.
    • Detect: trigger failures needs minNewFailures failed runs under the current revision since the last cycle; score and manual skip that count.
    • Capture: only traces stamped with currentRevision(dir) (runsByRevision); recurring groups only when trigger.requireRecurring (auto default) unless the trigger is score. --protect passes are admitted 6/6 and join the corpus (origin: 'protected'); holdout = the corpus.
    • Canary: canaryReplay on training + corpus cases, firstRepeat: 100, runs: canary.replayRuns; audited canary-end. A failed canary applies nothing.
    • Auto: before the write the corpus guard's cost is estimated (corpus size × guard runs × the canary's tokens per run); if the budget left can't pay for it, nothing is applied (budget-exceeded). Then applyProposal(…, { mode: 'auto', by: 'canary' }); a stale refusal (field base) ends stale (not reflected on), any other refusal refused. Then guardCorpus(dir, { rollback: false, runs: guard.runs }), and on a regression, a guard that can't judge (inconclusive), or a guard that throws, the cycle rolls back to its own parent revision with expectedFrom (fail closed). It does not use rollback: true, because lastGoodRevision falls back to the oldest baseline when no guard-pass was ever audited (an empty corpus audits none), which would throw away earlier kept edits. A refused rollback keeps the edit and says so; the monitor (S10) still checks it. The post-apply guard is not cut off by the budget, but its tokens count.
    • Crash safety: the cycle record is checkpointed after each step (tokens included) and right after the apply. A cycle that stopped after its apply but before its guard is guarded at the start of the next cycle, before the budget check.
    • The kill switch is re-checked before the editors, before the canary and before the write; if improve.json switches to manual mid-cycle, the proposal is queued instead of applied.
    • Manual: savePending(…, { maxAlwaysOnChars }) → pending.
    • Gate rejection audited as reject; rejected and rolled-back cycles get one reflection; a reflection error is audited as the new reflect-fail kind; voteOnOutcome once on a guard rollback.
    • Every outcome is recorded in .lousho/cycles/<id>.json; the record is written as interrupted before work starts and replaced at the end.
  • src/improve/cycles.ts (new writer of .lousho/cycles/; add it to the R10 list): HealCycle, cycle records, usageToday, cooldownEnds, pausedBy.
  • casesFromFailures takes traceIds and recurring; ImproveOptions.editorTimeoutMs (default 10 min in healOnce) ends a session past it as an outage; audit action reflect-fail.
  • lousho improve: runs healOnce (trigger manual) on an agent directory; Ctrl-C stops it between steps. --dry-run keeps the old print-only behaviour exactly. An agent module, or an agent whose instructions are not in instructions.md (or whose skills differ from skills/*/SKILL.md), is forced to a dry run with a message. HELP and the docs/cli.md / docs/improve.md lines that said "applies nothing" are updated; the full docs are S27.

R10 writers: .lousho/cycles/ by cycles.ts; .lousho/cycle.lock by heal.ts; everything else through the existing writers.

Review (independent): no blockers; the should-fix findings (guard rollback target, inconclusive guard, crash after apply, token checkpoints, recurring/HELP, approval re-read, stale reflection) and nits are fixed in the second commit.

Testing

  • src/improve/heal.test.ts (mock editor model, temp agent directory, real observe db with revision-stamped traces): auto applies, guards and audits; manual queues and writes nothing; a canary failure applies nothing; a guard regression rolls back in the same cycle and then the cooldown blocks; stale base refused; failures from another revision ignored; idle below minNewFailures; recurring rule and the score trigger; stop file / env / enabled: false pause; a pause mid-cycle stops before the write; concurrent cycle is busy; a guard that throws or can't judge undoes the apply; a refused rollback keeps the edit; an interrupted apply is guarded at the next cycle; the kill switch flipping during admission stops before the editors; a switch to manual mid-cycle queues; a guard the budget can't pay for blocks the write; daily caps (cycles, applies, tokens); budget-exceeded; outage, no-failures, keep-base; gate rejection audited with a failed reflection audited; a re-proposed rejected proposal is refused; an aborted cycle is left interrupted; nothing written outside instructions.md, skills/*/SKILL.md and .lousho/. Every HealCycle['outcome'] is reached.
  • src/improve/cycles.test.ts, cases.test.ts (traceIds/recurring), improve.test.ts (editor wall timeout).
  • src/cli/improve.test.ts: existing print-only tests now pass --dry-run; new: auto applies, manual queues, --dry-run writes nothing, module target and model-specific instructions forced to dry run.
  • typecheck (src, tests, examples), lint, build, api:update + api:check, docs:llms + docs:llms:check, docs:verify-snippets -- --skip-build, docs:ratchet (1724), vitest src/improve src/cli src/observe, npx fallow audit --base origin/main after scoped coverage: clean.

🤖 Generated with Claude Code

LinuxDevil and others added 3 commits November 9, 2026 01:39
Wires detect, capture, propose, gate, canary, apply or queue, corpus guard,
reflection and audit into one function the CLI, the watcher and tests share.
lousho improve now runs one cycle on an agent directory; --dry-run keeps the
old print-only behaviour, and modules or non-instructions.md agents run dry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guard rollbacks go to the cycle's own parent; a guard that throws or can't
judge undoes the apply; an apply interrupted before its guard is guarded at
the next cycle; tokens are checkpointed per step and the guard is costed
before the write; the kill switch is rechecked before the editors and the
canary; a switch to manual mid-cycle queues instead of applying; stale is
not reflected on; inputs reach runCase as captured; the CLI stops on Ctrl-C
between steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil merged commit b32ffe7 into main Nov 8, 2026
6 checks passed
@LinuxDevil
LinuxDevil deleted the s23-heal-once branch November 8, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant