Roadmap¶
Where lazy-harness is heading. This is a curated list of committed themes, not a wishlist. Items land here only when they are concrete enough to execute and the author intends to do them; exploratory ideas live in the internal backlog instead.
How to read this document¶
- Each theme groups related work that ships together or builds on a shared mechanism.
- Items are written as short, checkable deliverables. They get ticked off as PRs merge.
- The roadmap is revised whenever a theme closes or when priorities shift. There are no dates —
lazy-harnessis a single-maintainer project and ship-when-ready beats commit-to-a-date. - Finer-grained work (bugs, nice-to-haves, scratch notes) is not here. It lives in the internal backlog under
specs/backlog.md.
Theme 1 — Stability & quality gates¶
The pre-commit gate defined in AGENTS.md requires pytest, ruff check, ruff format --check, and mkdocs build --strict to all pass cleanly. That invariant is the floor every other change builds on.
- [x] Rewrite
tests/unit/test_version.pyto comparepyproject.tomlandsrc/lazy_harness/__init__.pyagainst each other (no hardcoded expected value). - [x] Resolve 23 preexisting
ruff check src testsfindings — auto-fix the fixable, justify or exclude the rest. - [x] Make the pre-commit gate green on
mainand keep it green. - [x] Add a GitHub Actions workflow that runs the gate on every PR and blocks merge on failure. All four checks are enforced:
tests.ymlrunspytest,ruff checkandruff format --checkacross five OS/Python combinations and builds the docs withmkdocs build --strictin adocsjob of its own —docs.ymlpublishes the site on push tomainand never sees a pull request, so the fourth check has to live here.
Theme 2 — Knowledge pipeline maturity¶
The compound-loop worker is the framework's memory engine. Two known issues today cause lost learnings — they share an underlying mechanism (per-session delta tracking) and should land together.
- [x] Implement
★ Insight ─block extraction as first-class compound-loop output. Spec:specs/designs/2026-04-13-compound-loop-insight-capture.md. - [x] Fix "learnings lost on long sessions" by storing the last processed message index per session in
insights/.cursor.jsonand re-scanning only the delta on subsequent Stop hooks. - [x] Add a contract test that pins the exact marker characters the
explanatoryoutput style emits, so a template change forces a visible failure. - [x] Close the two missing memory-pipeline stages. Built — ADR-040 adds reconcile and decay, shipped as
lh memory reconcileandlh memory decay. Without them the pipeline could only append: schema drift indecisions.jsonlwent unreported, and a learning nothing referenced stayed as current as one cited daily.
Theme 3 — Open architecture decisions¶
Decisions the audit surfaced that are waiting on real evidence before being promoted or rejected. Each one has a concrete trigger for revisiting.
- [x] Legacy ADR-010 Ollama backend. Promoted and superseded twice over. ADR-033 made inference provider-agnostic (implemented 2026-06-11); ADR-039 routes it per named role, so a local model can serve cheap work without taking over the calls where output quality is the point.
- [ ] Legacy ADR-013 Proactivity levels. Decide: promote as per-profile configuration, or reject and keep proactivity encoded as prose in each profile's
CLAUDE.md. Trigger: when a third identity is configured and the difference in autonomy between identities stops fitting in prose. Profiles of different agents that share one identity (ADR-068) do not count — they share its context by construction. - [x] ADR-018 implementation. Built —
lh doctorexposes a Features section (ADR-025) andlh config <feature> --initships wizards formemoryandknowledge(ADR-026).
Theme 4 — Framework extensibility¶
The plugin system (metrics sinks, ADR-004 agent adapters) is the framework's growth surface. The second extension point is chosen: it is not a new plugin kind but the unification of the five that already exist, specified in ADR-035. The concrete consumer justifying it is the configuration pane of the TUI, which without a registry would need six separate code paths.
- [x] Identify the second extension point. Chosen 2026-08-17 — the capability registry of ADR-035, driven by a real consumer rather than speculative design.
- [x] Ship the capability registry and migrate the five existing plugin kinds onto it. Landed 2026-08-17 —
plugins/capabilities.pyholds the registry andplugins/builtins.pyregisters all five kinds (tools, hooks, metrics sinks, agents, LLM backends). The sixth surface, the scheduler backend, is deliberately left out: its selection is a platform probe, not a user-facing activation. - [ ] Document the extension-point contract. The original trigger — "once two exist in code" — was met and passed:
plugins/builtins.pyregisters five kinds against one registry, so the data points the contract was waiting on are already there. What is left is the writing, and it is no longer gated on anything.
Theme 5 — More than one coding agent¶
The framework's hook, deploy and transcript layers were all written against a single agent's wire format. Supporting a second one is not a plugin: it is a contract — what a hook may decide, which of those decisions an agent actually honours, and which config documents the agent owns. The design is specs/designs/2026-09-13-multi-agent-harness-design.md; the decision is ADR-041, accepted since the freezing gate passed on 2026-09-15.
The contract was not frozen until a second, non-identity adapter had run against it — hardening an abstraction against one agent and only then meeting a second reproduces the mistake this theme exists to fix. That gate ran on 2026-09-15 and passed, so the contract is frozen and the items after it are unblocked.
The design and its derived blast-radius document are accepted as of 2026-09-18, on an end-to-end acceptance run against the second agent: 17 assertions, none failed (specs/designs/codex-evidence.md §6 records the run and the versions it was measured on). What acceptance does not close is the adoption criterion the theme is measured by — the horizon for that opens later, and below the threshold the adapters are removed rather than kept.
- [x] Identify harness-owned hooks by canonical name, not command text. Released in 0.60.0. The classifier matched a path the command generator had stopped emitting, so every harness hook was being read as another tool's; a redeploy after any change in command format would have installed each one twice.
- [x] Freeze the contract types. Released in 0.61.0 —
HookEvent,HookDecision,Verdict,HookOutput,Operation,Signal,ConfigArtifactandWriteOpinagents/base.py, with the Claude Code adapter implementing them. - [x] Make
lh hook <name> --profile <p>the runner. Released across 0.62.0 and 0.63.0 — three builtins migrated onto the event contract rather than all eighteen, plus a per-profile transcript reader so a hook asks the adapter for a signal instead of parsing a session file itself. - [x] Move config merging behind the adapter. Released in 0.65.0 — the adapter names the documents it owns and returns merged text; the deploy engine only discovers, reads, plans once per profile, and writes.
lh deploy --profile <name>lands with it, so a single profile can be deployed without touching the others. - [x] Skip — and name — hooks whose declared signals the profile's agent cannot deliver. Released in 0.67.0.
BuiltinHookSpec.signalshad been inert for the deploy —lh doctoralready read it:stop-verify-guardon an agent with no goal marker installed, ran, found nothing to verify and passed, green because it could not fail. The deploy now omits those entries, naming each one in its output before it is an absence in the artifact, and exits 0 rather than aborting.lh doctorreports the same signal gaps over the same profiles, from the one rule inhooks/signal_gaps.py, andtests/integration/test_deploy_signal_agreement.pyasserts the two agree in both directions. - [x] Run the contract gate against a throwaway second adapter. Closed 2026-09-15, across 0.66.0 to 0.67.1. A throwaway
CodexAdapterran the migrated builtins against a disposable Codex profile, in three runs, and the pass is composite rather than one green run. Run 2, on 0.67.0, settled the first three properties through a realcodex exec: a hook fires, a blocking hook actually refuses, and a hook whose required signal the second agent cannot supply is omitted from the artifact, named in the deploy output and named again bylh doctor. It failed on the property no run had looked at yet, and run 3 settled that one on the installed 0.67.1 binary rather than a worktree: a hook invoked with--profile <p>writes its log line under that profile's directory and nowhere else. Five defects came out of the runs, all of one shape — an answer derived from the global agent where the profile's agent is the source. Three limits are recorded rather than papered over: no single binary has passed all four properties in one run;stop-verify-guardwrites no log, so the isolation half asserts two builtins and not three; and of the fifteen unmigrated builtins the eight that write a log still leaked it, seven of them counted on every run as a known gap and the eighth never invoked by the gate at all, both tracked to the next item. That third limit closed on 2026-09-16 with the item below it: there are no unmigrated builtins left, so the gate has no known-gap set to derive and asserts coverage of the whole registry instead. The first limit stands as written. The second one's reason stands and its count does not:stop-verify-guardstill writes no log and the gate still skips it, but the isolation half asserts eleven builtins now rather than the two it could reach at 0.67.1, because the scope is derived fromlist_builtin_hooks()minus the skip list and the migration filled it. - [x] Migrate the remaining fifteen builtins onto the contract, each declaring the operations it covers and the signals it needs. Closed 2026-09-16. All eighteen now take
main(event: HookEvent) -> HookDecision, one commit per builtin, each carrying its own golden captured from the pre-migration hook so the evidence is a recording rather than a restatement of the new code. Three things fell out with them. The transitional field both entry points routed on is gone, so a builtin has one execution path instead of two — the second was an import-and-call that letlh hooks rundebug something the deployed command never did. The ten hardcoded global agent resolutions are gone, each removed by the migration that had the profile in scope; what holds the line is a test that walks every builtin's AST for aget_agentcall rather than grepping for a spelling, because the grep missed the two sites written as a conditional expression. Andlh deploy's warning about hooks that predate the runner is gone with the hooks it warned about. What did not close is per-profile agent selection outside the builtins — that is the item below, and the helper the builtins stopped using survives for its four remaining callers rather than being deleted with them. - [x] Multi-file config planning. Built — ADR-042 makes one
plan_configcall return every write and delete for a profile, and the engine refuses the whole plan — rather than locking a file — if any target's(mtime_ns, size)moved between read and apply. Deletion keys on proven ownership, not on the file existing: since ADR-042's 2026-09-19 evolution the adapter owns only the hook groups it can prove are lazy-harness builtins, and removeshooks.jsononly when its managed groups are gone and no foreign group remains.CodexAdaptermerges[mcp_servers.*]intoconfig.tomlwithtomlkit, carrying[projects.*]trust and[hooks.state]approvals through untouched. - [x] System docs by role. Built — ADR-043 replaces
AgentAdapter.system_doc_name() -> strwithsystem_docs() -> list[Path]: destinations the agent loads, not one recognised filename, so a nested or multi-file target becomes expressible and each is separately version-stamped. Segments lose their stem —head.md,_common/common.md,_common/<agent>.md,tail.md; the chezmoi source rename landed on 2026-09-18 and the temporary legacy reader was retired. - [x] Make agent selection per profile throughout. Closed 2026-09-18.
deploy/'s two readers, the launcher, and the hook/worker/CLI readers now resolve it per profile — landed together, which also gaveCapabilityaper_profilefield reported throughlh doctor.agents/launch.py:65now callsagent_for_profile(cfg, resolution.name)instead of the global[agent].type, closing the concrete gap this item used to cite.cli/profile_cmd.pywas reviewed in the same pass (PR #366):sync_profilesalready resolved the adapter per profile throughagent_for_profile(cfg, profile)whenever it was handedcfg(ADR-043) — the CLI'sget_agent(cfg.agent.type)was only ever that function's fallback for a directory the config no longer names, the same shape asagent_for_profile's own fallback for an undeclared profile, so no production change was needed there, only the missing CLI-level test. Re-measured 2026-09-18 (grep -rn "get_agent(" src/ | grep -v hooks/builtins): fifteen call sites, five of which readcfg.agent.type. Three are the documented fallback — "no profile resolved, fall back to the global agent" — and each is covered by a test from the change that introduced it:cli/memory_cmd.py:250, behind an early return taken whenever a profile is present;knowledge/compound_loop_worker.py:115, behindif not profile:; andmonitoring/statusline.py:44, whose enclosing function has no profile in scope to resolve from at all. The other two read the global agent to answer a question that is itself global, which is why this item closes rather than narrowing:cli/doctor_cmd.py:914renders the report's top-lineAgent:display, and the global[agent].typeis precisely what that line reports — the per-profile loop starts separately below it; andcli/profile_cmd.py:326passes the adapter tosync_profiles, which re-resolves per profile internally, so the global read is a default that never survives contact with a named profile. The remaining ten sites take an explicitagent_typeparameter or nameclaude-code/nulldirectly. No unconditional global read stands in for a per-profile one. - [x] Ship a real second adapter, replacing the throwaway, with its trust and capability state reported by
lh doctor. Built — ADR-044 replaces the step-4 throwawayCodexAdapterwith the real one. Codex's edit dialect turned out to be two paths chosen non-deterministically — aBashheredoc, and its ownapply_patchtool carrying a raw patch blob under the sametool_input.commandkeyBashuses — and closing it took four fixes: mappingapply_patchtoMODIFY_FILE, widening_shared.EDIT_TOOLS, parsing the patch blob intoFileEdits, and fixingpre_tool_use_memory_size._projected_text's silent branch on tool name.lh doctorreports three trust verdicts —untrusted,trust unknownandtrust stale(added by #367), plus orphaned entries — nevertrusted, because readingtrusted_hashback only proves a hash was stored, not that it still matches the current content. Measured ceiling: theBashedit path is structurally ungateable — itstool_inputis an arbitrary shell script with no path to extract — so a fully-fixed adapter gates only half of Codex's edits; ADR-044 records this as a consequence, not a TODO. - [x] A per-profile
externalhook command. Built — ADR-054 gives[hooks.*].external{profile}and{config_dir}placeholders, so one global list can express a command that differs per profile instead of forcing a second entry per profile. It reuses the shape of the runner's ownhook_commandrather than inventing a second substitution syntax. Shipping alongside it: shared roots keep every agent's export,lh doctorspeaks JSON, andlh initasks for the agent and the billing model. - [x] Close the system-doc migration window. Built — ADR-055 settles who renames the role-named segments and who owns the assembled document. Its kill criterion closed on 2026-09-18, once the deployed profiles and chezmoi source carried only role-named segments; the fallback then came out as specified.
- [x] Retire the legacy read fallback. Closed 2026-09-19 — the ADR-055 criterion was met, the reader was removed, and
lh deploynow assembles the documents so an upgrade restamps them. - [x] The second agent honours the harness's tool matchers. Built — ADR-056 records what an acceptance run measured rather than what the config implied: the deployed hook groups stay agent-agnostic, and the second agent matches them. The run that prompted it had reported every hook approved and still failed three assertions, which turned out to be the guard permitting a spelling the prompt left to the model — not a trust defect. The recursive-delete rule now denies on recursion alone, and the gate judges the agent against the guard's own answer for the command actually issued instead of against the fixture's fate — both are recorded in the ADR's Evolution section, which carries the probe run that produced them and points at the security-hooks cluster design for the reasoning.
- [x] Codex's persisted trust hash survives a release. Measured 2026-09-18 on 0.154.0 → 0.155.0 (evidence §4.3): the hashes one release stored were honoured by the next without re-approval. One pair; the item re-opens on the first upgrade that asks again for an unchanged
hooks.json. - [x] Credential freshness claims stop at the measured contract. ADR-057 rejects an invented liveness threshold over Codex's
last_refresh, and ADR-058 keeps Claude Code Keychainmdatoperator-only because an agent pane cannot enforce the Aqua boundary. Both preserve the current honestn/a/unknownoutcomes without new code. - [x] Project portable profile-owned skills. ADR-059 ships measured skill roots, whole-plan collision refusal, ownership-ledger cleanup and migration of the owned legacy Claude root. Commands and subagents remain native.
- [x] Profiles are identity × agent. Built — ADR-068 separates whose context a profile carries from which agent runs it: an optional
identitykeys the source directory and the generated system docs, a profile declaring one is named{prefix}-{identity}[-{suffix}],lh run --agent/lh exec --agentnarrow resolution to one agent before root matching, andlh metrics rename-profilerewrites local metrics history to a new name. - [x] Cut an existing installation over to identity-named profiles. Executed 2026-09-24, on both hosts, from the design's runbook (
specs/designs/2026-09-23-profile-identity-design.md): per-profile secrets files and local metrics history moved to the new names. A same-agent fallback for stale hook--profilevalues briefly relaxed the "every consumer moves in one window" framing (ADR-068, Evolution 2026-09-24) and was withdrawn the next day because it could cross identities: hooks,lh runandlh execall refuse an unknown profile again (ADR-068, Evolution 2026-09-25). - [ ] opencode session storage. Out of scope until an opencode adapter exists; the trigger is that adapter's lane, which must decide between the on-disk store and the
serveHTTP API before reading either. - [ ] Adoption kill criterion, horizon 2026-11-11. The check measures launches on a non-Claude profile over the trailing 28 days. Its threshold is
threshold = 5 * min(ratio over Claude profiles). If a profile falls below it when the horizon opens, the adapters are removed, not the runner. Until then,lh doctorprintshorizon opens 2026-11-11(derived design).
Theme 6 — Portable repository contract and honest cost comparison¶
- [ ] Pilot the single-file
AGENTS.mdcontract in lazy-harness, lazy-ai-tools and dotfiles; reject shadowingCLAUDE.mdfiles and gate root/nested discovery in both agents before migrating the remaining repositories (ADR-060). Wave 1 migrated on 2026-09-22, across five repositories rather than three (lazy-harness, lazy-ai-tools, dotfiles, lazy-ansible, lazy-desktop-manager); each passeslh repo instructionsand a sentinel probe from its root and one nested directory in both agents. Two changes in 0.77.0 made that true: #442 stoppedlh deploylinking~/.claude, whoseCLAUDE.mdhad hidden every repository'sAGENTS.mdfrom Claude Code, and #444 gates the whole fleet, flags an ancestorCLAUDE.mdinlh doctor, and measuresAGENTS.mdagainst the contract ceiling. The item stays open until the seven-working-day window that restarted on 2026-10-11 closes, around 2026-10-20, with the gate clean (it restarted because orphan generated system documents in a repository's source tree failed the gate during the earlier window); Wave 2 follows. - [x] Split actual billed cost from public API-equivalent cost in local storage, MetricEvent v4, response-level pricing and CLI views; keep unknown model/tier coverage visible (ADR-061).
- [x] Roll out the v4-tolerant receiver before producer emission, backfill and drain, then split the external Grafana panels into billed and API-equivalent measures (ADR-061).
- [x] ~~Publish a bounded, opt-in
Última sesiónsection from SessionEnd processing~~ (ADR-062). Shipped 2026-09-19 and removed 2026-10-26 without ever writing: no PRJ carried the section, and PRJ state is now written only by the projects skill at session close (ADR-072). The rollout step that would have opted the first project in is dropped with it. - [x] Price Claude Haiku 5.5 per request by prompt length and fix the Sonnet 5.5 cache-read rate (ADR-070)
- [x] Fail
lh doctoron a grapherrorin a repo present on the host, and report a configured repo absent from the host as skipped (ADR-071)