Skip to content

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-harness is 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.py to compare pyproject.toml and src/lazy_harness/__init__.py against each other (no hardcoded expected value).
  • [x] Resolve 23 preexisting ruff check src tests findings — auto-fix the fixable, justify or exclude the rest.
  • [x] Make the pre-commit gate green on main and 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.yml runs pytest, ruff check and ruff format --check across five OS/Python combinations and builds the docs with mkdocs build --strict in a docs job of its own — docs.yml publishes the site on push to main and 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.json and re-scanning only the delta on subsequent Stop hooks.
  • [x] Add a contract test that pins the exact marker characters the explanatory output 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 reconcile and lh memory decay. Without them the pipeline could only append: schema drift in decisions.jsonl went 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 doctor exposes a Features section (ADR-025) and lh config <feature> --init ships wizards for memory and knowledge (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.py holds the registry and plugins/builtins.py registers 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.py registers 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, ConfigArtifact and WriteOp in agents/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.signals had been inert for the deploy — lh doctor already read it: stop-verify-guard on 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 doctor reports the same signal gaps over the same profiles, from the one rule in hooks/signal_gaps.py, and tests/integration/test_deploy_signal_agreement.py asserts 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 CodexAdapter ran 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 real codex 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 by lh 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-guard writes 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-guard still 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 from list_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 let lh hooks run debug 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 a get_agent call rather than grepping for a spelling, because the grep missed the two sites written as a conditional expression. And lh 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_config call 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 removes hooks.json only when its managed groups are gone and no foreign group remains. CodexAdapter merges [mcp_servers.*] into config.toml with tomlkit, carrying [projects.*] trust and [hooks.state] approvals through untouched.
  • [x] System docs by role. Built — ADR-043 replaces AgentAdapter.system_doc_name() -> str with system_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 gave Capability a per_profile field reported through lh doctor. agents/launch.py:65 now calls agent_for_profile(cfg, resolution.name) instead of the global [agent].type, closing the concrete gap this item used to cite. cli/profile_cmd.py was reviewed in the same pass (PR #366): sync_profiles already resolved the adapter per profile through agent_for_profile(cfg, profile) whenever it was handed cfg (ADR-043) — the CLI's get_agent(cfg.agent.type) was only ever that function's fallback for a directory the config no longer names, the same shape as agent_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 read cfg.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, behind if not profile:; and monitoring/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:914 renders the report's top-line Agent: display, and the global [agent].type is precisely what that line reports — the per-profile loop starts separately below it; and cli/profile_cmd.py:326 passes the adapter to sync_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 explicit agent_type parameter or name claude-code / null directly. 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 throwaway CodexAdapter with the real one. Codex's edit dialect turned out to be two paths chosen non-deterministically — a Bash heredoc, and its own apply_patch tool carrying a raw patch blob under the same tool_input.command key Bash uses — and closing it took four fixes: mapping apply_patch to MODIFY_FILE, widening _shared.EDIT_TOOLS, parsing the patch blob into FileEdits, and fixing pre_tool_use_memory_size._projected_text's silent branch on tool name. lh doctor reports three trust verdicts — untrusted, trust unknown and trust stale (added by #367), plus orphaned entries — never trusted, because reading trusted_hash back only proves a hash was stored, not that it still matches the current content. Measured ceiling: the Bash edit path is structurally ungateable — its tool_input is 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 external hook 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 own hook_command rather than inventing a second substitution syntax. Shipping alongside it: shared roots keep every agent's export, lh doctor speaks JSON, and lh init asks 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 deploy now 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 Keychain mdat operator-only because an agent pane cannot enforce the Aqua boundary. Both preserve the current honest n/a / unknown outcomes 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 identity keys the source directory and the generated system docs, a profile declaring one is named {prefix}-{identity}[-{suffix}], lh run --agent / lh exec --agent narrow resolution to one agent before root matching, and lh metrics rename-profile rewrites 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 --profile values 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 run and lh exec all 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 serve HTTP 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 doctor prints horizon opens 2026-11-11 (derived design).

Theme 6 — Portable repository contract and honest cost comparison

  • [ ] Pilot the single-file AGENTS.md contract in lazy-harness, lazy-ai-tools and dotfiles; reject shadowing CLAUDE.md files 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 passes lh repo instructions and a sentinel probe from its root and one nested directory in both agents. Two changes in 0.77.0 made that true: #442 stopped lh deploy linking ~/.claude, whose CLAUDE.md had hidden every repository's AGENTS.md from Claude Code, and #444 gates the whole fleet, flags an ancestor CLAUDE.md in lh doctor, and measures AGENTS.md against 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ón section 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 doctor on a graph error in a repo present on the host, and report a configured repo absent from the host as skipped (ADR-071)

Closed themes

  • 2026-05 — ADR-018 feature discoverability. Closed by ADR-025 (lh doctor Features section) and ADR-026 (lh config <feature> --init wizards). Originally Theme 3, moved here as the implementation landed across the 0.10-0.20 release line.