Paste the relevant entry as the first message of a fresh Claude Code session in
the lopi repo. Newest first.
Sprint S13R connected lopi to kiban v1.8.0 (Phase A), re-ran the Phase 0 audit and
cleared its stop rule at 0 unmapped claims (Phase B), then resumed and completed the
original S13 Phases 1–4 (renamed C–F: determinism substrate, panic/resource surface,
error taxonomy, enforcement-from-first-prompt). Read first: CHANGELOG.md's [0.39.0]
entry, LEDGER.md's two S13R entries, and .konjo/killtests/S13/PHASE0-STOP-RULE.md's
2026-07-29 append.
What's already done and should not be re-derived: kiban pinned at v1.8.0
(.konjo/kiban.ref + KIBAN_REF in both CI jobs that reference it);
.konjo/profile.yml wired via a new konjo-gates CI job; CLAUDE.md converted to the
Phase-13 section contract; .claude/rules/security.md split into
security-invariants.md/security-sinks.md; rust-toolchain.toml pinned to 1.88.0
(real bisection, not guessed); [workspace.lints] + lints.workspace = true on all 18
crates + the root binary; overflow-checks = true in [profile.release] (KT-S13.2
verified) + [profile.bench] added; both production unbounded channels converted to
bounded; the indexing floor seeded at 211 (.konjo/indexing-floor.txt, re-measured —
does not match the brief's earlier "202", see LEDGER.md for why); the function-length
gate written and wired (.konjo/function-length-ceiling.txt, seeded at 74);
lopi-core's error taxonomy fully converted to thiserror; a SessionStart hook added;
the hardcoded /Users/wesleyscholl/lopi/ paths in .claude/settings.json and
post-edit.sh fixed; post-edit.sh extended to cover web/ TypeScript/Svelte edits.
First thing to do: confirm Cargo.toml's version (0.39.0) still matches
CHANGELOG.md's top entry and VERSION before touching anything.
Carried forward, explicitly not silently dropped — pick one and finish it rather than starting something new:
lopi-git's error taxonomy — onlydiff.rs(1 of 4anyhowfiles) converted.manager.rs(18 fallible fns),rebase.rs(4),worktree.rs(11) remain. Starting design sketch recorded inLEDGER.md'sError-Taxonomy-1: aGitManagerErrorwrappinggit2::Error+std::io::Error+ aCommandFailed { context, stderr }variant for theanyhow::bail!shell-out sites (git push, rebase abort). Convert one file at a time, running that file's test suite plus a fullcargo build --workspaceafter each — this crate is used everywhere (lopi-agent,lopi-orchestrator), so a rushed multi-file conversion in one pass is exactly the risk this note exists to head off.lopi-memory's error taxonomy — 0 of 30anyhowfiles converted. The largest remaining piece of Phase E by far. Needs its own dedicated session, not a squeeze-in at the end of another sprint. Look for a natural seam (thestore/module's sub-files likely share a shape) before picking a per-file or a unified-enum approach.- Doc-link debt (the rustdoc gate, kept soft this sprint with a named owner —
"whichever sprint next touches
lopi-agent/lopi-orchestrator/lopi-mcpdocs" — and a target of before Sprint S14 closes). Re-runRUSTDOCFLAGS="-D missing_docs -D rustdoc::broken_intra_doc_links" cargo doc --workspace --no-depsto get the current exact list (it grew past what this sprint's own re-measurement found —lopi-agent11,lopi-orchestrator8,lopi-mcp1 — by the time a future session gets to it) before starting; qualify each bare[Type]/[func]link with its full path, followinglopi-core's already-fixed precedent. gate_polarity's one filed real defect:eval_runner.rs:29'sevaluate_acceptance_gateproceeds when noAcceptanceis configured — the same shape as the already-fixedverifier_runner.rs/scorer.rssites, but deliberately not fixed this sprint since doing it properly means the same kind of explicit opt-in redesignverifier_error_proceeds(fail_open: bool)got, a real behavior decision (does an existing task with no acceptance start failing?), not a small patch.- Report the
pricing.rsfalse positive to kiban if a future session gets push access there:is_stale_given'sNone => trueis flagged bygate_polarityas a permissive default, buttruehere means "flag as stale" — the restrictive, cautious answer, not a permissive bypass. A real blind spot in the engine's bare-literal heuristic (it assumes any literaltrue/1.0/Ok(())is inherently the permissive end of range, which breaks for a boolean whose polarity runs the other way), worth a kill-test fixture in kiban itself.
Non-goals, correctly not attempted: squish/vectro's own CLAUDE.md conversions
(kiban's Lopi-Gate-Reconciliation-1 explicitly defers these, lopi was the named
pilot); cargo-semver-checks/cargo-udeps/flake budget/fuzz-in-CI (still S14 per the
original S13R brief); wiring the Konjo Verifier onto the sail path (still Wave 1/F1).
Sprint S13 (Quality Substrate and Continuous Enforcement) ran Phase 0 (the
honesty pass on quality claims) and then stopped, per the sprint's own
stop rule: Phase 0 found 5 self-claims with no genuine enforcing step
(2 dark .konjo/rubrics/*.toml files with zero code consumer, 3 of
CLAUDE.md's 8 "Additional Hard Rules" bullets that were actually soft
continue-on-error gates or had no mechanical check at all) against a
threshold of 3. Full audit: .konjo/killtests/S13/PHASE0-STOP-RULE.md.
One-way-door decisions: LEDGER.md's S13 Phase 0 entry.
What's already done and should not be re-derived: the two dark rubrics
(refactor_safety.toml, security_audit.toml) are deleted and every doc
that claimed "three canonical rubrics ship" (KONJO_VERIFIER.md, PLAN.md)
is corrected; the 3 false-hard CLAUDE.md bullets (coverage 80%/95%, doc
coverage, function length) now correctly describe their real enforcement
status, each cited to its exact konjo-gate.yml job:step; 4 dead path
globs in .claude/rules/benchmarking.md and .claude/rules/testing.md are
fixed and verified against the real repo layout; the sprint brief's baseline
evidence table is re-verified with two real drifts corrected (a second
production unbounded channel at src/repl/mod.rs:76 the original baseline
missed, and anyhow:: usage grown from 106 to 131 files).
First thing to do: before writing any Phase 1 code, re-verify Phase 0's
claim-mapping table is still accurate (nothing should have drifted since —
this repo's main doesn't move fast enough for that — but the brief's own
discipline is "verify, don't assume"). If it still holds at ≤3 unmapped
claims, proceed straight to Phase 1: rust-toolchain.toml, MSRV bisection
(record the bisection result, don't guess), the edition/unsafe-2024
question in crates/lopi-ui/src/client/auth.rs (resolve why CI is currently
green before touching it), [workspace.lints.rust]/[workspace.lints.clippy]
on the root Cargo.toml + lints.workspace = true on all 18 crates, and
overflow-checks = true in [profile.release] — gated by KT-S13.2
(add a temp #[test] in lopi-core doing u64::MAX + 1 via a runtime
value, confirm it panics under --release only after the profile change,
then delete the temp test). Phase 1 is the first phase to introduce a real
gate, so KT-S13.1 (a bad+good fixture pair under .konjo/fixtures/S13/
for every new gate, proven to reject/accept before it's wired into CI)
starts applying from here forward — it did not apply to Phase 0 since Phase
0 introduced no gates, only corrected/deleted existing claims.
Then Phase 2 (indexing floor ratchet — seed .konjo/indexing-floor.txt at
the current count, which needs re-measuring since Phase 0 found the
production-indexing-site count (202) unchanged from baseline but did not
re-verify it post-Phase-0-edits), Phase 3 (error taxonomy — lopi-core,
lopi-git, lopi-memory only, LEDGER.md-tracked), Phase 4 (the actual
enforcement-from-first-prompt wiring — CLAUDE.md security/resource lines,
security.md split into invariants/sinks, SessionStart hook, the
hardcoded macOS path in .claude/settings.json, post-edit.sh extension,
CI wiring for every new gate).
Sprint T0 (TUI Client Foundation & Domain Port) landed at 0.35.0. The TUI
now has a write-capable client (lopi_ui::client::TuiClient +
RemoteClient/LocalClient) and a single Rust port of the loop-stack
domain model (lopi_core::stack) — but no new widgets were built. T1 is
the first sprint that actually renders anything: an always-focused
InputBar widget plus a :-prefixed command palette with preset-alias
autocomplete off lopi_core::stack::preset_catalog(). Goal submission goes
through the TUI via TuiClient::create_task, using
lopi_ui::client::stack_payload::pane_submit_payload for the bare-prompt
case.
Read first: CHANGELOG.md's [0.35.0] entry and LEDGER.md's Sprint T0
section — both one-way doors (the lopi-core/lopi-ui split for the
domain port vs. wire-payload builders, and RemoteClient as authoritative
over LocalClient).
Carrying forward from T0, so T1 doesn't have to re-derive it:
LocalClient's chain methods (list_chains/get_chain/create_chain/enable_chain/disable_chain/run_chain_now) are stubbed to returnClientError::Unsupported—ChainScheduleManagerisn't reachable outside the axumAppStatetoday, and building a second one fromLocalClient's ownAgentPool/MemoryStorewould race the real one insidelopi sail. Not T1's problem (T1 doesn't touch chains at all), but T3 (Loop Stack Builder) will hit this directly and should re-verify the finding rather than assume it's still accurate.LocalClienthas no real caller yet —lopi watch --localstill constructs an empty, disconnectedEventBus. If T1 (or any later sprint) wants an embedded-TUI-inside-sailmode, that wiring doesn't exist yet and is new scope, not something T0 half-built.- KT-T0.3's fixture set (
crates/lopi-ui/src/client/stack_payload_tests.rs— three cases lifted fromstack.test.ts) is a permanent regression test, not a throwaway. T1 doesn't need to touch it (it doesn't add newcardToTaskPayloadfields), but T2 (Prompt Loop Builder) and T3 should extend it rather than write a parallel fixture set when they port more ofstack.ts. AppState::cognition(crates/lopi-ui/src/tui/cognition.rs) now retains the six previously-dropped event variants, but nothing renders it — T5 (Live Cognition Surface) is still the first sprint to build that panel, not T1.
Kill test for T1: typing :implement fix the thing in the TUI attaches
the same eval set the web composer attaches for the same input — i.e. the
resulting CreateTaskRequest.acceptance matches what
lopi_core::stack::preset_catalog()[&PresetKey::Implement].evals compiles
to via evals_to_acceptance.
The web composer's autonomy control is now wired end to end —
CreateTaskRequest.autonomy_level, resolved against the repo's
.lopi/loop.toml when unset (file = base, request = override — the
one-way-door precedent every future loop-config field must follow, see
LEDGER.md). no_progress_limit and isolation (branch/worktree) are new
per-task overrides on the same contract. Budget preset (quick|standard| deep|unlimited + USD) now has a real composer control wired to the
pre-existing budget_override field. Read first: CHANGELOG.md's
[0.32.0] entry, LEDGER.md's entry for this sprint (both one-way doors —
the precedence contract, and "no control renders unless it's wired or
correctly absent"), UI_PLAN.md's Backend Bindings table (corrected this
sprint — several rows it called "Missing"/"Partial" were already done by
earlier sprints; re-verify against types.rs/task.rs before trusting any
row in it, this is now the second time it was found stale).
What's still file-only, deliberately, not by oversight: vision_path,
trust_ceiling, self_prompt, skills_enabled, rules_enabled,
permission_allow — structural/rarely-per-run, per this sprint's own scope
boundary. None of them render as composer controls (correctly — "not shown"
is honest for a field that isn't wired, per Pillar 1). If a future sprint
wants any of these in the composer, it needs its own scoping pass; this one
deliberately didn't touch them.
What a future sprint should pick up:
- Verifier gate has a backend field but no composer control.
Task. verifier_required/verifier_model/verifier_effortare all onCreateTaskRequestalready (an earlier sprint), but nothing in the composer exposes them —UI_PLAN.md's Gap Map namedVerifierToggle.svelteand it still doesn't exist. Small, well-scoped, not touched this sprint (out of this sprint's stated scope). - Surfacing the resolved config, not just accepting an override. Phase 3's
honesty rule was satisfied by "wired or correctly absent," but the sprint
brief's stronger suggestion — showing the effective
.lopi/loop.tomlvalue next to anauto/inherit control (e.g. "Auto · from loop.toml (repo is actually verified_pr)") — wasn't built; it needs a live per-repo config fetch the composer doesn't currently make. The existing/loopLoop Engineering cockpit already displays a repo's effective config in full; consider reusing that fetch rather than inventing a second one. GuardrailsPopover's stack-scopebudgetrow was removed (Phase 3 honesty audit — it was inert, no backend or client consumer). If a genuine chain-level budget concept is ever wanted, it needs its own design (there's no server-side "whole chain" today — a chain is N independent task creations) before any control reappears at that scope; don't re-add the removed row without one.
Sprint F4 gave one CLI session per attempt (plan → implement → fix, not
one cold spawn per phase), with a silent cold-spawn fallback on any resume
failure, structural checker isolation, and task-level correlation. Read
first, in order: CLAUDE.md, CHANGELOG.md's [0.31.0] — Sprint F4 entry,
LEDGER.md's Sprint F4 entry in full (both one-way doors: the
per-attempt session lifecycle, and the checker-isolation guarantee made
structural), all five .konjo/killtests/F4/KT-4.*.md files, benchmarks/ results/<ts>_f4_session/summary.md, then this file's own words below.
Version note: F4 was developed against a HEAD where both F1
(0.29.0) and F3 (0.30.0) had already landed — the brief's own
§Ordering section anticipated this exact case ("F4 takes 0.30.0 if F1
lands first, 0.29.0 otherwise") but didn't anticipate F3 landing first
and also taking 0.30.0. F4 takes 0.31.0, the next free slot, rather
than either already-claimed number.
Sprint F5 (build cache) is next — the brief's own hand-off target. Four things carry forward:
- KT-4.4's cache-boundary number is F5's own input. F4 found the
claudeCLI defaults to Anthropic's 1-hour prompt-cache tier (ephemeral_1h_input_tokens), not 5 minutes — measured directly from the usage envelope across every kill-test and benchmark call this sprint made, not inferred. This is why F4 shippedimplement → fixcontinuity, not justplan → implement: the boundary turned out to be an order of magnitude wider than the brief's own worry assumed. F5 should read this as: your build-cache speedup widens F4's viable window further, it doesn't rescue a narrow one. If F5's own measurement ever shows a realcargo testphase taking close to an hour on a cold worktree, that is the point F4'simplement → fixcontinuity would start losing its cache benefit — check the actual gap F5 measures against KT-4.4's ~1-hour figure before assuming continuity still holds at whatever speed F5 lands at. - The Phase 5 measurement in this sprint is NOT the full 30-run
T01–T10 corpus the brief's own gate asked for.
benchmarks/corpus/ README.md's "Status (Sprint F0, 2026-07-26)" note is still accurate — that corpus run has never happened, and is now blocking F4's own headline hypothesis in addition to F0/F1's prior notes below. What F4 did run: a small (n=10), real (not synthetic — this session had liveclaudeCLI access, unlike prior sessions' recorded constraint) paired sample directly measuringplan → implementcost/cache-share under cold vs. resumed conditions on a scratch repo, not this repo's own corpus. Seebenchmarks/results/<ts>_f4_session/summary.mdfor the real numbers and the honest scope limit — do not read this as confirming the brief's own 30-run/T01–T10 merge gate; that gate is still open. Whoever eventually runs the T01–T10 corpus (now blocking F0's own Phase 3, F1's Phases 5-6, and F4's own merge gate at once) should treat F4's small-sample finding as a strong prior to check against, not a substitute for the real run. - The per-attempt session id (not the raw
TaskId) is now the shape any future correlation work should build on.tasks.cli_session_idholds the most recent attempt's session id, matchingtasks.branch/tasks.repo's existing "most recent attempt only" precedent — seeKT-4.2.mdfor why the rawTaskIddoesn't work here (it's stable across retries; CLI sessions are not). - F1's own checker-isolation guarantee is now structural, not just
conventional —
verifier_cli.rs/postmortem_cli.rspassSessionMode::Noneexplicitly throughapply_cli_caps's newsessionparameter, and both existing negative tests were extended to also assert no--session-id(not just no--resume). Any future sprint that adds a shared "give this spawn a session" helper must default these two call sites'SessionMode::Noneexplicitly, not inherit whatever a generic helper does by default.
Also worth knowing, not blocking: KT-4.1 found --permission-mode bypassPermissions itself fails in this sandboxed (root) container — the
same class of finding as F1's KT-1.3 on --bare. Every live call this
sprint substituted acceptEdits to verify the actual resume mechanics
instead. Whoever next has a real, non-root machine with a logged-in
claude CLI should re-run KT-4.1's specific bypassPermissions check.
Sprint F3 decoupled the event bridge's live broadcast from task_logs
persistence. Read first, in order: CLAUDE.md, CHANGELOG.md's
[0.30.0] — Sprint F3 entry, LEDGER.md's Sprint F3 entry (the
best-effort-persistence one-way door), all four
.konjo/killtests/F3/KT-3.*.md files, benchmarks/results/ 20260726T205826Z_f3_bridge/summary.md, then this file's own words below.
Version note: F3 was developed against 6688d7d (post-F2, 0.28.0),
before F1 had landed, and its own draft notes assumed it would take
0.29.0. F1 landed first instead while F3's PR was in review (see the
Sprint F1 entry immediately below this one) and took 0.29.0 — so F3
takes 0.30.0.
Sprint F4 (session continuity) is next. Three things carry forward, two from F3 and one inherited through F3 from F1's own handoff below:
KT-3.4's volume estimate was made pre-F1; F1 has since landed, but F3's measurement doesn't need a re-run — only a future live measurement does. F3's 30-run paired benchmark (benchmarks/results/20260726T205826Z_f3_bridge/) uses a synthetic-load harness (event_bridge_bench.rs::bridge_load_bench) that injectsAgentEvent::LogLinetraffic directly onto the bus at a documented, stated rate (~250 lines/sec/agent, 4 synthetic agents) — it never exercises F1's verifier/judge/post-mortem code, so F1 landing changes nothing about what that harness measures. What F1 does change is the real production event rate a live (non-synthetic, real-agent) re-measurement would see. That re-measurement was never attempted in F3 (out of scope, and this session's environment can't run one anyway — seeKT-3.1's own environment-substitution note) but is now actionable rather than merely estimated, since F1's verifier-session code exists to profile. Whoever eventually runs a real four-agent M3 measurement of the bridge should account for F1's added verifier-session traffic in the observed rate, rather than assuming F3's synthetic rate still applies.- The durability one-way door — log persistence is now best-effort under
pressure.
LEDGER.md's F3 entry: under sustained overload, the bridge's persistence channel dropstask_logsrows (counted, not silent —lopi_task_log_persist_dropped_totalat/metrics) rather than blocking the live stream or growing unbounded. This was confirmed safe byKT-3.3: nothing readstask_logsfor correctness, replay, or gating today — only display surfaces (web dashboard tail, Telegram/tail, MCPlopi_get_logs,lopi diagexport). If session continuity work (F4) or any later sprint starts treatingtask_logsas more than a display/inspection surface — e.g. reconstructing session state from logged lines, or feeding an evidence bundle that needs completeness guarantees — that is the trigger to reopen this door and add backpressure or durable buffering back explicitly, rather than assuming today's best-effort behavior already covers it. - F1's own item 3 below (F4 must not resume the checker session) still
applies and is worth reading in full — it's about a different
subsystem (
verifier_cli.rs/postmortem_cli.rs's session isolation) than F3 touched, but it's the same kind of constraint: a fresh-session guarantee that a future session-continuity helper could accidentally erase if it's applied blanket-wide instead of call-site-by-call-site.
Sprint F1 gave the verifier, the judge tier, and post-mortem a CLI backend
(claude -p on subscription auth), so all three actually run in every real
deployment instead of silently no-op'ing forever (with_api was never wired in
production — confirmed again by this sprint's own grep). Phases 1–4 shipped;
Phases 5 (two-tier checker) and 6 (--append-system-prompt on the worker) did not —
both are gated on a corpus measurement this session couldn't run, per the brief's own
"ship without it" fallback. Read first, in order: CHANGELOG.md's [0.29.0] — Sprint F1 entry, LEDGER.md's Sprint F1 entry, all four .konjo/killtests/F1/KT-1.*.md
files, then this file's own words below. F1 was this repo's Sprint F1; Sprint F2
(Correctness Holes) already landed independently before F1 did — see CHANGELOG.md's
[0.28.0] entry — so the two are not in the numeric order their briefs implied.
Four things carry forward:
- KT-1.3's
--barefinding needs re-verification on a real (non-sandboxed) target machine. In this session's own container,--barefailed CLI authentication 6/6 times, reproducibly —claude --helpdocuments it as skipping "keychain reads," and this container's credential wiring appears to depend on one. The checker ships without--bareas a result. This may be specific to this sandboxed session's credential proxying and not a general property of a real user's subscription login (which more commonly reads a plain on-disk token under~/.claude/, not a keychain). Whoever next has access to a real machine with a logged-inclaudeCLI should re-run KT-1.3's--barevs. non---barepaired comparison for real. If--bareturns out to work there, re-add it toverifier_cli.rs/postmortem_cli.rsfor the cost/latency win KT-1.2 documented (a non---barechecker session loads hooks/CLAUDE.md/skills and can burn real turns/spend on tool-discovery detours before concluding it's denied) — do not assume the auth failure generalizes without checking, but do not assume it's safe to add--bareback without checking either. - The T01–T10 corpus run is still outstanding — now blocking two sprints'
worth of work, not one. F0's Phase 3 flagged it as attended, hardware-required,
and not runnable in an unattended session; F1's Phase 5 (two-tier checker cost/
agreement measurement) and Phase 6 (worker system-prompt pass-rate measurement)
both needed it too and both went unshipped as a result, per each brief's own
"measure first, or don't ship" instruction. Whoever picks this up should treat one
corpus run as unblocking three sprints' pending measurements at once, not run it
three separate times. Follow
benchmarks/corpus/README.md§Measurement Protocol and.claude/rules/benchmarking.md. - F4 (session continuity, not yet started as of this writing) must not resume the
checker session. Phase 1's design constraint — "fresh session, never resumed" —
is what makes the checker a checker rather than a continuation of the maker's own
context.
verifier_cli.rs/postmortem_cli.rsnever pass--resume; a unit test in each (grade_via_cli_argv_never_includes_bare_or_resume,postmortem_cli_argv_never_includes_bare_or_resume) pins this. If F4 introduces a shared session-continuity helper for worker spawns, it must not be reused for these two call sites without deliberately re-deciding this constraint, not by accident. - F9 (Evidence and answerability) is now unblocked — F1's own brief named this as
the dependency ("An evidence bundle whose verifier verdict is always
trueis worse than no bundle"). The verifier verdict is no longer alwaystrue; F9 can proceed on that basis, though a real live confirmation (alopi sailrun, noANTHROPIC_API_KEY, a planted rubric violation actually rejected) is worth doing once more on whatever machine ends up running F9, since this sprint's own live confirmation ran in the same sandboxed container as KT-1.3's--barefinding.
Next Session — after Sprint F2 (Correctness Holes) — Sprint F3 decouples logs; two inherited items for F1/F5
Sprint F2 closed four defects (scorer stack detection, the unevaluated-repo
silent-pass, hardcoded pricing, stale model IDs), pinned --bare explicitly at
every spawn site, and relabeled the token-pressure gauge rather than replacing it
(no keyless Claude tokenizer exists for the job). Read first, in order: CLAUDE.md,
CHANGELOG.md's [0.28.0] — Sprint F2 entry, LEDGER.md's two Sprint F2 entries
(the unevaluated-repo one-way door, and the model-config/--bare one-way door), all
four .konjo/killtests/F2/KT-2.*.md files, then this file's own words below.
Sprint F3 (log decoupling — named in this sprint's own brief, not otherwise detailed here) is next. Three things carry forward, not F3's own scope but worth knowing before touching adjacent code:
- The scorer/nextest inconsistency, explicitly deferred to F5. Phase 1 added
pytest/Go/Gradle/Maven/pnpm/yarn detection to
crates/lopi-agent/src/ scorer_detect.rs, reusing lopi's own CI precedent where it made sense — but.github/workflows/konjo-gate.yml:170-175runscargo-nextestfor the Rust path while the scorer still runscargo test --quiet. F2's brief was explicit this alignment is F5's job, not F2's; this note is the handoff, not a new finding. Whoever picks up F5 should also check whether the newly-added pytest/Go/Gradle/Maven paths have their own CI-vs-scorer runner mismatches worth aligning in the same pass. - KT-2.4's tokenizer finding — no keyless Claude-accurate tokenizer exists for
a live, pre-send estimate.
crates/lopi-context/src/tokens.rs'sestimate_tokensstill usescl100k_base(OpenAI's GPT-4 BPE), now explicitly labeled as an estimate everywhere it's displayed (TurnMetrics.context_pressure's doc comment, the web dashboard's "Context pressure (est.)" gauge + tooltip,events.ts's turn-metrics log line). This is not fixed, only honestly labeled — if Anthropic ever publishes a keyless offline tokenizer for the current Claude generation, or lopi's own architecture changes so a post-hoc real count (already flowing throughTurnMetrics/UsageAccrualfrom the CLI's streamed usage) could feasibly substitute for the pre-send estimate, that's the trigger to revisit this — see.konjo/killtests/F2/KT-2.4.mdfor the full reasoning on why post-hoc real data can't serve the pre-send role today. - Phase 6's
--barepolicy, for F1 if F1 has not yet landed by the time you read this.apply_cli_caps(crates/lopi-agent/src/claude_support.rs) now takes an explicitbare: boolparameter; all three current spawn sites (all worker sessions) passfalse. If F1 adds a checker/post-mortem CLI spawn site, it should passbare: trueat that new site — per both sprints' coordination note, whichever sprint lands second inherits the other's--baredecision rather than re-deriving it. Checkgit log/CHANGELOG.mdfor whether F1 has landed before assuming this note is still live; if F1's own KT-1.3 already found the checker needs project context, that live finding wins over this default.
Also worth knowing, not urgent: Phase 4's model-ID config
(crates/lopi-agent/src/model_config.rs, models.toml) and Phase 3's pricing config
(crates/lopi-agent/src/pricing.rs, pricing.toml) both read their override file
once per process (cached via OnceLock) — a running lopi sail process needs a
restart to pick up an edited .lopi/models.toml/.lopi/pricing.toml, not just a
file save. If a future sprint wants live-reload, that's new scope, not a bug in this
one — the brief only asked for "no recompile," not "no restart."
Next Session — after Sprint F0 (Honesty Pass) — Sprint F1 wires the verifier; two attended actions still owed; one security gap found
Sprint F0 made every TOON/token performance claim trace to a committed measurement or
deleted it, confirmed whatsapp is unreachable (not deleted), corrected several
README/Safety claims that turned out inaccurate on read-through, stated the file-size
gate's scope explicitly, and added an advisory reachability check to G1. No runtime
behavior changed. Full detail in CHANGELOG.md's [0.27.1] — Sprint F0 entry and
LEDGER.md's Sprint F0 entry (the WhatsApp positioning call).
Two things F0 could not complete in an unattended agent session — do these first, attended, before trusting any pass-rate or benchmark number this repo publishes:
- KT-0.1's live component:
./benchmarks/run.sh --tasks T01against a real Claude subscription. The dry-run passed (harness not bit-rotted), but a live single-task run needs a human watching, per the sprint's own gate definition. Do this, confirm the harness still produces a real result end-to-end, before Phase 3. - Phase 3 — run the T01–T10 corpus and commit
benchmarks/results/<ts>_corpus/. Real money, real wall-clock, ten agent tasks. Followbenchmarks/corpus/README.md§Measurement Protocol and.claude/rules/ benchmarking.md(≥5 warmup runs, documented hardware, p50/p95/p99, a new timestamped directory, never overwrite). Once committed, rename "Expected Pass Rate" → "Measured Pass Rate" inbenchmarks/corpus/README.mdand remove the "Status (Sprint F0...)" note at the top of that file — staten=1explicitly unless more than one run was done. If a task the table predicted at 100% fails, record that as the finding; do not re-run silently until it passes.
Sprint F1 (named, not yet started) — wire the verifier/judge/post-mortem. F0's README
audit confirmed this precisely: Runner::with_api (crates/lopi-agent/src/runner/mod.rs)
is never called anywhere in the built binary — grep -rn "with_api" crates/ lopi-orchestrator/ and src/ both come back empty. That means the verifier
(runner/verifier_runner.rs), LLM-judge acceptance (runner/eval_runner.rs), and
post-mortem (runner/run_loop.rs, gated on adaptive_retry) all silently no-op on
every run today, regardless of CLI flags or config — a task can complete "successfully"
with none of them having run. --stability-gate is the one layer that does work,
because src/run_command.rs wires its own, separate AnthropicClient outside
with_api. F1's job: decide where with_api should be wired (the pool? lopi run
directly? both?), wire it, and update the README's "no separate API key required"
caveat (currently accurate as of F0, but only because these checkers are confirmed
dead — F1 changes that on purpose).
Also found during F0's audit, not fixed (out of scope for a measurement/docs sprint,
but real): allow_self_modify: false (crates/lopi-core/src/config.rs) — meant to
keep lopi from modifying its own src//crates/ — is enforced only on the lopi run
CLI path and the REPL (src/run_command.rs, src/repl/actions.rs). The lopi sail
web dashboard's task-creation API (crates/lopi-ui/src/web/handlers.rs::create_task)
and the lopi_submit_task MCP tool (src/mcp_commands/mod.rs) never check it — a task
submitted through either of those two paths, pointed at the lopi workspace itself, is
not blocked. This is a real containment gap on two of the four task-entry points the
README itself documents, not a documentation nuance. Whoever picks this up: add the
same check those two paths are missing, or centralize it somewhere all four entry
points share so it can't silently drift again.
Lower priority, tracked not blocking:
.konjo/scripts/reachability_check.py(new, advisory) currently flags ~20pub mods as unreferenced elsewhere in the workspace. Most are probably legitimate internal surface (tests, future library use); triage the list once, decide which (if any) should actually bepub(crate)or deleted, rather than leaving it as permanent noise.- Phase 5 (file-size gate) left
web//macos/unscoped by design —web/src/lib/ stores/stack.ts(~2,200 lines) would need to be split before the gate could safely extend there. Someone should decide whether that split, and the gate extension after it, is worth doing. LEDGER.md's Sprint F0 entry flags that whether WhatsApp gets wired up or deleted is unresolved — this sprint only established it's currently unreachable.
Next Session — after Sprint S9 (the recipe library) — three confirmed runner gaps, HV-4 portability, deferred CLI
Sprint S9 shipped recipes/ — six documented, live-run-measured .lopi/loop.toml
recipes plus the format contract (recipes/README.md) and an F0–F10 principle legend
this sprint introduced (no prior canonical numbering existed). CHANGELOG.md's
[Unreleased] — Sprint S9 entry has the full list; recipes/README.md is decays: state, re-verify it before trusting its cost/duration numbers. Four of six recipes
(fix-failing-test, lint-burndown, flaky-test-hunter, triage-issues) live-ran to a
clean, measured success. Two (dependency-bump, doc-drift-check) did not — not because
the recipes are wrong, but because live-running them surfaced real runner bugs, reported
honestly rather than retried into a fake clean number. Three things carry forward:
src/mcp_commands/mod.rs::submit_tasknever appliesRepoProfile.run_command.rs:177andsrc/repl/actions.rs:101both callRepoProfile::load_from_repo(&repo).apply(&mut task); the MCPlopi_submit_tasktool builds a bareTask::new(goal)and never does. Confirmed live:dependency-bumpanddoc-drift-checkeach correctly did the right work three times in a row (a realcargo update/doc rewrite, verified by hand on the resulting branch) and were hard-rolled-back every time byDiffChecker's defaultallowed_dirs(["src/", "tests/"]) because the.lopi.tomlallowed_dirsoverride meant to fix that never reached the task — reproduced twice, independently, same root cause both times. Fix: add the sameRepoProfile::load_from_repo(&repo).apply(&mut task)call tosubmit_taskthat the other two entry points already have. Real-world impact: any task submitted through the Claude Code/Desktop MCP plugin integration against a repo with a customized.lopi.tomlsilently ignores it today.LoopConfig::permission_allow/permission_deny(the flat, legacy fields) have no runtime effect. Traced bothcrates/lopi-orchestrator/src/pool/run_loop.rsandsrc/run_command.rs: both wire--allowedTools/--disallowedToolspurely fromLoopConfig::resolved_budget()(i.e.[budget]'s preset deny-list +budget.permission_allow), never from the flat fields. The flat fields' only live code path islopi loop show's display (src/loop_commands.rs). This means the field doc comment's claim ("genuinely enforced even alongside--dangerously-skip-permissions") is stale — true of the mechanism that predated[budget], not of what ships today.recipes/triage-issues/README.md's "Known containment gap" section is the full write-up; that recipe's safety story currently rests entirely onautonomy_level = "report_only", not onpermission_deny. Fix: either wire the flat fields intoresolved_budget()(extra denies beyond the preset's own list) or deprecate them outright so the doc comment stops overclaiming.run_guard_command(thegate/untilshell runner,crates/lopi-core/src/loop_config.rs) inherits the parent process's stdio. Harmless for the CLI (lopi run), where that's just extra visible terminal output. Corruptslopi mcp-serve's stdio JSON-RPC transport when a recipe'sgate/untilcommand produces its own stdout (e.g.dependency-bump'scargo test --workspacegate) — confirmed live, worked around in this sprint's own test harness by tolerating/skipping non-JSON lines, not fixed at the source. Fix: redirect the guard command's stdout/ stderr (Stdio::null()or captured, per the existing doc comment's stated intent that only the exit code matters).
Also carried forward, unrelated to the three bugs above:
- A recipe-authoring gotcha, not a bug:
Deliverable::infer_from_goal(crates/lopi-core/src/deliverable.rs) infers whether a zero-diff attempt is a legitimate success from whole-word verbs in the goal text —write/update/create/edit/etc. imply file changes are expected (zero diff = retry),summarize/review/analyze/etc. imply review-only (zero diff = success).triage-issues's goal originally said "write a one-paragraph summary" and failed three times despite explicitly saying "do not create files," becausewritematched the change-verb list first. Reworded to "summarize the report" and it succeeded in one attempt. Worth a lint or at least a callout inrecipes/README.md's format contract for anyone adding a report-only recipe later. - HV-4 (recipe marketplace/portability) — explicitly out of scope for S9 ("No recipe marketplace, registry, or sharing infrastructure"). The six recipes here are the proof-of-concept; HV-4 is packaging/distributing that pattern beyond this repo, sized as its own sprint.
lopi recipes list|show|applyCLI — deferred per the sprint brief's own instruction ("Only if the copy workflow is demonstrably awkward... Decide this from the Phase 2 experience, not in advance"). The copy workflow (mkdir .lopi && cp recipes/<name>/loop.toml .lopi/loop.toml) proved perfectly workable across all six live-run applications in this sprint — no CLI command was needed. Revisit only if a future session finds the plain-directory approach genuinely awkward in practice.
Next Session — after Sprint S4 (quality gate enforcement) — coverage climb, rustdoc cleanup, sqlx migration, kiban migration
Sprint S4 closed Problems 1 and 2 from the quality-gate brief — a hard,
never-regress coverage floor locked at 68.34% (.konjo/coverage-floor.txt +
.konjo/scripts/coverage_floor_check.py, kill-tested), a soft-gate convention lint
(.konjo/scripts/soft_gate_lint.py, kill-tested) that makes the KNOWN DEBT, verified <date> / ADVISORY BY DESIGN comment convention mechanical instead of hand-held, and
made cargo deny/cargo audit hard gates after triaging every finding (four crate
upgrades, one migrated config schema, four scoped license exceptions/allowances, five
scoped advisory ignores — see CHANGELOG.md's [0.26.0] entry for the full list and
LEDGER.md's Sprint S4 entry for the why behind the one-way-door coverage floor,
the lopi-local soft-gate lint, and the unmaintained/unsound schema migration).
Five things carry forward, not forgotten:
-
The coverage climb toward 80%, then 95%, is still owed — this sprint locked the floor at 68.34%, it did not raise it. The soft 80%/95% gate (
konjo-gate.yml, "Coverage gate" step) still has its ownKNOWN DEBT, verified 2026-07comment pointing at the lowest-coverage crates as the next step. Every future sprint that raises real coverage should also ratchet.konjo/coverage-floor.txtup in the same PR — the mechanism only protects a number that keeps moving. -
The rustdoc intra-doc-link cleanup (
lopi-agent'sStreamEventx4,lopi-orchestrator'sJobScheduler/TopologyHint/types) is still deferred. Explicitly out of scope for S4 (real doc-writing work, not config-scale). The "Documentation gate (rustdoc)" step inkonjo-gate.ymlstill carries its ownKNOWN DEBTcomment with the grep-and-qualify next step. -
The sqlx 0.7 -> 0.8+ major-version migration is now the single blocker on four RUSTSEC advisories (RUSTSEC-2026-0098/-0099/-0104 on
rustls-webpki, RUSTSEC-2024-0363 onsqlxitself — all pinned via sqlx 0.7.4's ownrustls 0.21dependency;cargo update -p sqlx/-p rustls-webpkiboth confirm no 0.7-compatible fix exists). This is real application-code migration work — sqlx's query/macro API changed across majors, andlopi-memoryis the crate map's whole SQLite persistence layer — sized as its own sprint, not a burn-down line item. Both.konjo/deny.tomland.cargo/audit.tomlcarry matching, reasoned[advisories.ignore]entries for all four IDs; keep the two lists in sync if either changes, and delete both sets of entries in the same PR that lands the sqlx bump. -
.konjo/scripts/coverage_floor_check.pyand.konjo/scripts/soft_gate_lint.pyare lopi-local by necessity, not by choice — pre-flight kill-test 3 confirmed kibanv1.4.0ships neither mechanism (konjo-gates-pyis Python/ML-repo-scoped;konjo-gates-rs, the Rust equivalent, is an explicit phase-1 stub whosemain.rsonly prints"konjo-gates-rs: phase 1"). Whoever picks upkonjo-gates-rs's real implementation should port both scripts' kill-tested behavior (.konjo/scripts/test_coverage_floor_killtest.sh,.konjo/scripts/test_soft_gate_lint_killtest.sh— 5 fixture cases each) into the crate rather than re-deriving the edge cases from scratch, then retire the lopi-local copies in favor of consuming kiban. -
cargo-deny'smultiple-versionsduplicate-crate warnings andlicense-not-encounteredwarnings are pre-existing, non-blocking noise —cargo deny checkreports them but they don't fail the gate (bans.multiple-versions = "warn", not"deny"). Not touched this sprint (out of scope: no new production code paths, config-scale only), but worth a look if a future sprint is already auditing the dependency graph — several duplicate versions (base64,hashbrown,itertoolsx3,windows-sysfamily) look collapsible.
Next Session — after Sprint S2′ (egress re-verify + provenance surfacing) — the deferred human gate, and the VPS-tied phases
Sprint S2′'s kill-test found its own brief's baseline stale — Sprint S2 (below)
had already shipped the deny-by-default Telegram egress allowlist this sprint was
asked to build, so no code changed there. What did land: GET /api/tasks and
GET /api/tasks/:id now surface a provenance field ("operator" / "untrusted"
/ "unknown"), derived from Task::source (already persisted, never previously
read back). Read docs/security/EGRESS_SURFACE.md first — the re-verification
kill-test, decays: state, re-run it before trusting it — and LEDGER.md's
Sprint S2′ entry for why a stale brief was treated as a finding rather than a
green light to rebuild. Two things carry forward:
- The human gate on egress that this marker is a foundation for is still
deferred, and still tied to the VPS/webhook path's return.
notify_loop(crates/lopi-remote/src/telegram/notify.rs) doesn't checkprovenance()at all today — it only checksegress_allowed_chat_ids. When the VPS returns and untrusted-origin (TaskSource::Webhook) notifications become a live concern again, the gate is now cheap to add: reuserequire_plan_approval's existing shape, but applied to the send, not just the plan — e.g. hold or flag-in-text anyReportReady/TaskCompletednotification whose task's provenance is"untrusted", the same way Sprint S2 Phase 5 already holds execution on those tasks pending plan approval. Don't build it against a loopback-only deployment with no reachable untrusted-webhook path — that was this sprint's own reason for stopping at "record and surface." - All four items from the Sprint S2 entry below are unchanged and still
open — S3 per-user identity, tool-execution sandboxing, the dormant
whatsapp::servepath's optional-by-default HMAC (still true: confirmed again inEGRESS_SURFACE.md§1 that no outbound WhatsApp sender exists to need the egress allowlist, but its inbound signature-verification gap is untouched), andWebConfig.host's dead configuration.
Sprint S2 closed F10's five phases — auth fail-closed (--insecure-no-auth,
refused on non-loopback), CORS allowlist, webhook secret (confirmed already fixed,
no change needed), a deny-by-default egress allowlist for lopi-remote's automated
Telegram sends, and a human-approval gate on any task seeded from TaskSource::Webhook
(reusing the pre-existing L2 plan-approval mechanism, not a new one). Read
docs/security/TRIFECTA_PATHS.md first — it's the full untrusted-input-to-side-effect
inventory, decays: state, re-verify it before trusting any of it — and
LEDGER.md's Sprint S2 entry for the why behind each breaking default and the
two scope decisions (Telegram excluded from the forced-approval gate; the dormant
WhatsApp path still got it). CHANGELOG.md's [0.25.0] entry has the full diff
summary. Four things carry forward, not forgotten:
-
S3 — per-user identity. Explicitly out of scope for S2 ("no auth provider integration... deliberately a separate sprint with a larger blast radius"). S2's single shared bearer token is a real improvement over no auth at all, but it's still one token for every caller — no per-user attribution, no per-user revocation. OAuth/SSO/identity-provider integration is S3's job, sized and staffed as its own sprint given the blast radius the brief already named.
-
Tool execution sandboxing / container isolation. Also explicitly out of scope ("real, valuable, much larger"). The agent runner has full code-exec, git, and PR capability with no sandbox boundary — S2's containment is entirely about when a run is allowed to happen (auth, provenance-gated approval) and where its output can go (CORS, egress allowlist), not about bounding what a running agent can touch on the host. A real fix here is process/container isolation for tool execution, scoped as its own sprint.
-
crates/lopi-remote/src/whatsapp.rs::serveis dead code in the built binary — confirmed viagrep -rn "whatsapp::serve" src/ crates/, matches only its own crate's tests. Its inbound/taskpath got S2'srequire_plan_approvalgate (so that containment travels with it), but its Twilio HMAC verification is still optional-by-default with no CLI wrapper enforcing a policy the waywebhook_commands.rsdoes for the GitHub webhook — because there's no CLI wrapper at all. Whoever wires this up next (alopi serve-whatsappcommand, presumably) should give it the same fail-closed treatmentenforce_webhook_secret_policygives the GitHub path, not inherit the "accepts unsigned when unset" library default. -
crates/lopi-core/src/config.rs'sWebConfig.hostis dead configuration — parsed fromlopi.tomlbut never read;Sail's actual bind host only ever comes from the CLI--hostflag. Noted in passing during S2's kill-test, not a security issue, just pre-existing drift — either wire it up (solopi.toml's[web].hostactually does something) or remove it so the config doesn't lie about what's configurable.
Doc-Integrity corrected docs/LOOP_ENGINEERING_ROADMAP.md's state table and
per-sprint status (9/18 sprints already shipped), banner-labeled 12 historical
audit docs, reconciled FEATURE_STATE_FINAL.md's F1–F8 findings (all 7 fixed
at the source level), and — once konjoai/kiban@v1.4.0 shipped the decays:
convention — pulled it in and wired konjo-doc-staleness into
.github/workflows/konjo-gate.yml as a hard gate (G0 · Doc Staleness).
Read CHANGELOG.md's Doc-Integrity Phase 4 entry and LEDGER.md's two
Doc-Integrity entries first (why the gate clones kiban instead of
pip installing it; why PLAN.md was deliberately left unstamped rather than
given a fabricated verified-against). Three things remain, not forgotten:
-
PLAN.mdis stale — frozen at v0.19.0 (2026-06-18) whilemainisv0.24.0— and needsdecays: statefront matter once it's caught up. Found while sourcing this sprint'sLEDGER.mdentry, flagged with a "known drift" note inPLAN.mditself rather than fixed (fixing it means re-auditing everything shipped across five versions — out of scope for a docs-correction sprint), and deliberately left unstamped rather than given a fabricatedverified-against(stamping it today would repeat the exact failure this sprint exists to catch, one file over). The next session that touchesPLAN.mdshould catch up its "Shipped" log and "Current Health" table tomainthe same way this sprint caught updocs/LOOP_ENGINEERING_ROADMAP.md, then adddecays: statefront matter in the same pass soG0 · Doc Stalenessstarts enforcing it immediately. -
A real live F1–F8 re-verification is still owed. This sprint's reconciliation (
docs/ops/FEATURE_STATE_RECONCILIATION_2026-07-24.md) is source-level only — it confirms the code changed in the right direction and each fix has a test, but it does not re-observe behavior in a runninglopi sailwith real auth and real spend the way the original Verify-1 audit did. F2 in particular has one open caveat: the store-layer wiring (runBarePane/launchBareCard) was confirmed, but theStackPane.sveltemarkup wasn't traced to confirm a visible run control actually invokes it. Schedule this as its own live-audit sprint, same discipline asFEATURE_STATE_FINAL.md(?demo=1forbidden, no CI sandbox, real subscription auth). -
docs/ui/UI-2-VV-report.mdWARNs on the new doc-staleness gate ("historical doc lacks a dated banner") because this repo's git history can't resolve a calendar date for its baseline (PR #64,55338d5) — left honest rather than guessed. If a real date turns up (GitHub's PR#64 merge timestamp, checked from outside this repo's local history), add it to the banner and the WARN clears.
Also worth a look, not urgent: this sprint's brief scoped an optional Phase 5
— a cargo xtask capability-matrix (or lopi state) command that mechanically
probes the four claims this sprint had to hand-verify (MCP call sites +
McpServe registration, worktree.rs existence + agent-path wiring,
lopi-skill's registry.rs/invocation.rs, VerifierAgent::new's isolation
default) and emits the table the roadmap embeds instead of hand-maintains.
Not built this sprint (scope was correction, not new tooling) — building it
would make the next drift mechanical to catch instead of requiring another
manual audit.
Next Session — after Constraint-Capture-2 (mine_patterns finally writes a constraint) — Phase 1 is now unblocked
Constraint-Capture-2 closed the gap where mine_patterns recorded stats but
never a constraint, and gated the newly-populated constraints behind a
promotion threshold. Its own Phase 1 (toolchain-scoped retrieval) was not
attempted during the sprint itself, because the stated dependency (Session
Prompt 1) hadn't landed yet — but it landed on main as Onboarding-Import-1
(below) while this PR was still open, and had to be merged in. Read first,
in order: CLAUDE.md, CHANGELOG.md's Constraint-Capture-2 entry (updated
at merge time), LEDGER.md's Constraint-Capture-2 entry in full (especially
the KT-C/KT-D findings, the promotion-gate numbers, and the merge-time
PatternExtra/upsert_pattern_row/COALESCE reconciliation), then this file's
own words below and Onboarding-Import-1's entry underneath it.
At sprint time, a full grep of schema.sql, CHANGELOG.md, and LEDGER.md
for toolchain/onboarding/detect_stack found nothing. That has changed:
Onboarding-Import-1 added patterns.toolchain (nullable, populated only by
the onboarding backfill path — still NULL for every live-mined
mine_patterns row) and patterns.source ('lopi_run' vs.
'onboarding_import'). Before starting Phase 1, re-confirm the current
state rather than trusting this note indefinitely — schemas can keep moving
between sessions, the same lesson this merge itself is proof of. Two real
open questions Phase 1 will hit immediately:
find_similar_patternshas no toolchain parameter yet — adding one is additive (default to unscoped, matching this sprint's Constraint-Capture-2 brief's own "backward-compatible: an unscoped call still works" framing).- Live-mined patterns (
mine_patterns, the path this sprint's own constraint capture feeds) still don't populatetoolchainat all — only the onboarding backfill path does. Toolchain-scoping a live task's retrieval would needmine_patternsto also callsrc/toolchain_detect.rs(or receive a toolchain hint from its caller), which today it does not. Decide whether Phase 1 should close that gap too, or scope toolchain retrieval to backfilled patterns only for a first cut.
MemoryStore::mine_patternsgained asuccess_constraint: Option<&str>parameter, writing it intopatterns.successful_constraintson both insert and update. All three real call sites (pool/run_loop.rs::run_one,src/run_command.rs::run_with_live_print,src/repl/actions.rs) now pass a real constraint on a clean success (matches!(outcome, TaskStatus::Success { .. })),Noneotherwise.patterns.occurrence_count— new column, incremented on everymine_patternsupdate.AgentRunner::success_constraint()(crates/lopi-agent/src/runner/ capture.rs, new) — derives a bounded constraint fromlast_plan, reusingreflection::summarize_attemptrather than duplicating it.seed_from_patterns's promotion gate (crates/lopi-agent/src/runner/ seed.rs::is_promotable):occurrence_count ≥ 2andsuccess_rate ≥ 0.5for mined patterns; postmortem-derived patterns exempt from both. SeeLEDGER.mdfor the full reasoning and the "how to apply" note on retuning these numbers later.- Phase 1 (toolchain-scoped retrieval) — not attempted this sprint, but
its precondition (Session Prompt 1 /
Onboarding-Import-1) landed at merge time — see above for what's now unblocked and what's still missing. - New tests across
crates/lopi-memory/src/store/tests.rs,crates/lopi-agent/src/runner/capture.rs, andcrates/lopi-agent/src/runner/seed.rs, including a live-verification test (live_check_backfilled_pattern_constraint_reaches_the_real_planning_prompt) that drives a file-backed store through the realmine_patterns→gather_seed→claude_support::build_plan_promptpipeline and asserts the backfilled constraint appears in the literal planning-prompt text. cargo build --workspace,cargo test --workspace(all crates),cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --check, andRUSTDOCFLAGS="-D missing_docs" cargo docall clean.VERSION(workspaceCargo.toml) bumped to0.24.0at merge time — this sprint andOnboarding-Import-1(below) both independently bumped to0.23.0, but that version had already shipped onmainwithout this sprint's changes by the time the two were reconciled, so this sprint's own bump moved to0.24.0rather than silently reusing an already-released version number.
No live Anthropic API session exists in this sandbox to run claude -p
itself, the same standing constraint recorded in every prior sprint's
LEDGER.md entry (Sprint Successor-1, MCPB-App-1/2). What was verified
instead: a real, file-backed MemoryStore, backfilled through the real
production write path, feeding the real gather_seed() → real
build_plan_prompt() — the literal string ClaudeCode hands to the claude
CLI subprocess for both its one-shot and streaming plan paths, confirmed (via
--nocapture) to contain the backfilled constraint. A session with real
claude -p access should, once Phase 4 (below) or any other planning-prompt
change lands: submit a real task in a repo with backfilled pattern history,
confirm the printed/logged planning prompt (or a debug log of it) contains a
non-empty constraint sourced from a prior pattern, and confirm the resulting
implementation actually reflects it (not just that the string was present in
the prompt).
- Phase 4 (stretch, explicitly deferred by this sprint's own brief) — a
promoted pattern as a live composer suggestion. Not started. The brief's
own kill-test for this phase (does
web/src/lib/components/Composer.sveltehave a hook point for this without disrupting the;-prefix verb grammar work) was never run. Scope as its own sprint if picked up. - Toolchain scoping (Phase 1) — no longer blocked on schema; see the new section above for what's ready and what still needs deciding (live-mined patterns don't carry a toolchain today, only backfilled ones do).
- The constraint-update policy is COALESCE (never overwrite once set), not
this sprint's originally-designed overwrite-latest — reconciled at merge
time against
Onboarding-Import-1's already-shippedupsert_pattern_rowsemantics (seeLEDGER.md). Revisit only with real evidence a stale constraint is stuck wrong on a row, not on intuition.
Onboarding-Import-1 shipped all five phases and both hard exit-gate
checks that don't require Wes's real machine — KT-A/KT-B (live,
~/.claude-access-dependent kill-tests) are correctly left open, not
assumed. Read first, in order: CLAUDE.md, CHANGELOG.md's
Onboarding-Import-1 entry, LEDGER.md's Onboarding-Import-1 entry in full
(the KT-C naming confirmation and the honest KT-A/KT-B scope limits), then
this file's own words below.
- Schema (Phase 0):
patterns.toolchain(nullable),patterns.source(DEFAULT 'lopi_run'), and a newonboarding_importsidempotency ledger table — all backward-compatibleALTER TABLE/CREATE TABLE IF NOT EXISTS. - Transcript reader (Phase 1):
crates/lopi-agent/src/transcript_import.rs— defensive NDJSON decoder for~/.claude/projects/**/*.jsonl, built against a real captured line from this very session's own transcript. - Toolchain detection (Phase 2):
src/toolchain_detect.rs— manifest-file based (Cargo.toml→rust,package.json→node,pyproject.toml/requirements.txt→python,go.mod→go,Gemfile→ruby), the first toolchain detection anywhere in lopi. - Backfill store path (Phase 3):
MemoryStore::backfill_onboarding_patternreuses a sharedupsert_pattern_rowhelper withmine_patterns— one write path, not two, per the brief's own hard reuse constraint. - Constraint extraction (Phase 4):
session_looks_successful()+extract_success_constraint()— a clean tool-result tail and explicit success language in the final assistant text, both required, documented in-code as a deliberately conservative heuristic (false positives here pollutesuccessful_constraintswith bad guidance). - CLI + idempotency (Phase 5):
lopi import [--dry-run] [--claude-dir], idempotent on the transcript's ownsessionId. - 33 new tests, 1620 workspace tests green, clippy clean,
-D missing_docsclean. Dry-run and a real (non-dry-run) round trip both verified against this sandbox's one real transcript — actual pasted output below, not a description of expected output:
$ lopi import --dry-run
🧭 lopi import — scanning /root/.claude (1 transcript file found)
[dry-run] would import 2afe0e65 · rust · "# Session Prompt 1 — Onboarding Import: Toolchain-Scoped Pat…"
🧭 would import 1 · 0 already imported · 0 with no human turn · 0 with an empty keyword fingerprint
$ lopi import
🧭 lopi import — scanning /root/.claude (1 transcript file found)
✅ 2afe0e65 · rust · "# Session Prompt 1 — Onboarding Import: Toolchain-Scoped Pat…" → pattern a711bd4e
🧭 imported 1 · 0 already imported · 0 with no human turn · 0 with an empty keyword fingerprint
$ lopi learn list
🧠 lopi learn — 1 pattern(s)
Id Keywords Avg Att. Success% Source
──────────────────────────────────────────────────────────────────────────────────────────
a711bd4e access across actual actually adds agai… 1.0 0% 📊 mined
$ lopi import --dry-run # re-run: idempotency check
🧭 lopi import — scanning /root/.claude (1 transcript file found)
🧭 would import 0 · 1 already imported · 0 with no human turn · 0 with an empty keyword fingerprint
(The 0% success rate is correct, not a bug: this session is still
in-progress, so session_looks_successful() rightly found no completion
signal in it yet — the heuristic didn't spuriously mark an unfinished session
as a clean success.)
KT-A (needs a real, multi-project ~/.claude/projects corpus). This
sandbox's ~/.claude/projects/ holds exactly one file — this session's own
in-progress transcript — not the 3+ files across separate projects
(lopi/squish/kiban) the original brief asked for. The one file available was
still genuinely useful: it directly confirmed the human-turn-vs-tool-result
schema ambiguity (see LEDGER.md) from real data, not a guess. What it
can't confirm: whether every real historical session across a full corpus
follows the same shape with zero exceptions, and whether any transcript ever
carries a type: "summary" entry (a possible richer goal source the brief
raised — none appeared here, so transcript_import.rs doesn't special-case
it). A session with real ~/.claude access needs to: capture 3+ real files
across genuinely different projects, diff them against
transcript_import.rs's assumptions, and either confirm no summary type
exists or add a case for it if one does.
KT-B (needs a real ~/.claude/settings.json). This container has no such
file at all (only launcher-settings.json, a different SDK-hook config, not
user retention prefs) — cleanupPeriodDays is simply unknown here, not
assumed to be the 30-day default or anything else. A session with real access
needs to check it and report how many days of real history exist — if
retention is near the default, that's a real finding about how much signal a
first lopi import run actually recovers, worth surfacing to Wes plainly.
- Continual recognition (this sprint's explicit non-goal, by design). The
toolchain/sourceschema groundwork this sprint laid is meant to be kept populated going forward by a live-capture sprint, not just this one-time backfill — that sprint still needs to be scoped and built. - Embedding-based clustering is out of scope unless a future kill-test
against a real multi-project corpus shows Jaccard/
keyword_fingerprintis inadequate at real scale — untested here, since this sandbox never had more than one transcript to test scale against. - claude.ai chat export ingestion (manual, email-delivered ZIP, no live API) — flagged as a possible future manual-import command, correctly not built this sprint.
MCPB-App-2 wired the stack-status widget's first click-driven write path —
a Cancel button per row, calling the already-existing lopi_cancel_task MCP
tool — but its own Phase 3 (live verification) is explicitly blocked, not
skipped: KT-B3, the widget's basic live render in a real Claude Desktop, still
has not been confirmed as of the most recent KT-B3-Live entries. Read
first, in order: CLAUDE.md, CHANGELOG.md's MCPB-App-2 entry, LEDGER.md's
MCPB-App-2 entry in full (the KT-1–KT-4 findings and the four "how to apply"
points), then this file's own words below.
- Pre-flight kill-tests KT-1 (tool-call symmetry — confirmed, no origin
branching in
crates/lopi-mcp/src/server.rs), KT-2 (callServerTool()vs.ontoolresult— confirmed distinct, cancel result wired through the former), KT-4 (no autonomy/plan-approval gate oncancel/delete_task— confirmed absent by reading the pool/store code directly). KT-3 (host-level approval UX) is unanswerable without a real host — correctly left open. src/mcp_ui/stack_status.html: a Cancel button on everyqueued/runningrow (isCancelable()), a confirm-then-two-click-fallback guard (requestCancel/doCancel), real-disabled-button double-submit prevention, inline.row-erroron failure, row replaced with a grayed "cancelled" line on success, and anapp.updateModelContext(...)call after a successful cancel..rowchanged from<button>to arole="button"div (nesting a real button inside it was invalid HTML — seeLEDGER.md) with a newroot.onkeydownrestoring keyboard activation.src/mcp_commands/server_wire_tests.rs— new. Two tests drivelopi_cancel_taskthrough the reallopi_mcp::serve()JSON-RPC loop with the realLopiToolHandler, not a mock — the surface the brief asked for, relocated from the (inaccessible)crates/lopi-mcplocation the brief named, since that crate has no dependency on lopi's actual tool implementations by design.mod_tests.rs'stest_state()is nowpub(super)so both test modules share it.- 1576 workspace tests green,
cargo clippy --workspace --all-targets -- -D warningsclean, widget's script body stillnode --checkclean,VERSIONbumped to0.22.0.
Phase 3 did not run at all. Nothing in this sprint's own testing exercises
whether the widget actually renders in a real host in the first place — that
question (KT-B3) predates this sprint and is still open per KT-B3-Live's
most recent entries (server spawns, MIME type and extension-negotiation fixes
landed, but the widget-render check itself was never observed against a real
Claude Desktop in any session so far). A session with real Claude Desktop
access needs to, in this order:
- Confirm KT-B3 itself first — install the current
.mcpb, submit a task, confirm the stack-status panel actually renders inline (not a text fallback / warning toast). If this still fails, that is this sprint's blocker, not this sprint's own code — stop and diagnose againstKT-B3-Live's three prior findings before touching anything here. - Only once KT-B3 passes: click Cancel on a real running/queued task,
observe whether
window.confirm()fires a native dialog or throws (this resolves KT-3 and the confirm-vs-two-click fallback split at once — ifconfirm()works, the two-click fallback code path can be considered dead code and reconsidered), confirm the task is actually cancelled and deleted (lopi_list_tasksor a direct DB check), confirm the row updates without a full widget refresh, confirm no console errors. - Click Cancel on a task that completes between page-load and click —
confirm the resulting "not found"
errorpayload renders as this sprint's inline.row-error, not a crash. - Rapid double-click on Cancel — confirm the real
disabledattribute actually prevents a secondcallServerTool()call (not just a UI-level debounce). - Capture a screenshot or recording of at least one successful cancel round trip as evidence, per this repo's own precedent for live-host checks — not just a text claim that it worked.
Whether a real MCP Apps host adds its own approval modal on top of a
widget-initiated tools/call (KT-3) is still genuinely unknown — the
widget's own confirm step does not assume either answer, and should not be
simplified away even if a host turns out to add a modal of its own; two
prompts (one host-level, one app-level) for a destructive action is not a
bug.
Sprint Successor-1 built the data model, lineage fields, and containment
gates for agent-authored successor tasks — no agent authoring yet. Read
first, in order: CLAUDE.md, CHANGELOG.md's [0.21.0] entry, LEDGER.md's
Sprint Successor-1 entry in full (the three one-way-door decisions:
SelfAuthored vs. SelfModify, the autonomy-ceiling clamp, and the
untrusted-source ratchet), then this file's own words below.
lopi-core::successor— theSuccessorproposal type (goal/when/rationale/allowed_dirs),SuccessorCondition, andSuccessor::validate().Taskgainedparent_task,chain_depth,successor_enabled,successor_fixture(all#[serde(default)]);TaskSource::SelfAuthored.TaskSourcemoved to its owntask_source.rs(file-size gate).derive_successor_task(parent, successor, max_depth)— the four containment gates (depth cap, autonomy ceiling, directory inheritance, untrusted-source lockdown), each with its own dedicated test.AgentEvent::TaskCompletedgainedsuccessor: Option<TaskId>.lopi-memory:tasks.parent_task/tasks.chain_depthcolumns +MemoryStore::lineage_chain(bounded ancestor walk, not a recursive tree).AgentRunner::derive_and_stash_successor(finalize.rs, besideemit_report) + pool-level enqueue via the realAgentPool::submit— gated onTask::successor_enabled, fed byTask::successor_fixtureonly (no parsing from agent output — that's this sprint's own hard boundary).- Pre-flight kill-tests KT-A/B/C all recorded (see
LEDGER.md); 1574 workspace tests green, clippy clean.
The Phase 4 integration test does not drive a real claude -p subprocess
through AgentRunner::run()'s full plan → implement → test → score loop.
That requires a live Anthropic API session, which this sandbox cannot reach
(no claude CLI session/network for that path). What was actually verified
instead, and why it's still meaningful:
crates/lopi-agent/src/runner/finalize.rs'sderive_and_stash_successor_*tests prove a passingfinalize()call really does invokederive_successor_taskand stash a gated child — the logic seam.crates/lopi-orchestrator/tests/successor_enqueue.rsproves the derived child really does land in the realTaskQueuevia the realAgentPool::submit(dedup/topology/audit intact) with lineage/depth/gates correct on the popped task — the plumbing seam.- Not yet verified: that a real end-to-end task run (real git repo, real
claude -psession, real diff, real commit) that reachesTaskStatus:: Successactually produces aTaskCompletedevent with a populatedsuccessorfield and a second row appearing in a livelopi saildashboard. This needs a session with real Claude Code CLI access: submit a task withsuccessor_enabled: trueand asuccessor_fixtureset (no API surface exists yet to set these from the CLI/REST layer — that's itself an open question below, KT-1) against a real repo, watch it run to completion, and confirm the successor task appears queued and eventually dispatched.
- KT-1 — no submission surface for
successor_enabled/successor_fixtureexists yet. Neitherlopi run's CLI flags, the RESTPOST /api/taskshandler, nor.lopi/loop.tomlexpose a way to set these fields today — this sprint only exercises them via directly-constructedTaskvalues in tests. Before Sprint Successor-2 adds parsing-from-agent-output, decide where a human-supplied fixture successor should be configurable from (a repo-level.lopi/loop.tomldefault? a per-task REST field? both?) — otherwise the only way to use this sprint's plumbing today is a hand-builtTask. - KT-2 —
DEFAULT_MAX_CHAIN_DEPTH = 3is a hardcoded constant, not a per-repo config.crates/lopi-core/src/successor.rsdocuments this as a deliberate scope cut (a natural.lopi/loop.tomlceiling once chains actually run unattended for a while), but it means every repo currently gets the same depth cap regardless of how much it trusts self-extending chains. Worth revisiting once Sprint Successor-2/3 make chains something that actually runs unattended rather than fixture-only. - Sprint Successor-2's own explicit scope (per the brief that ran this
sprint): parse a
Successorout of an agent's ownfinal_text, replacing thesuccessor_fixtureconfig-only path. Sprint Successor-3: branchadvance_to_next_step's static-goal chain scheduling on top of dynamic successors, plus web/macOS lineage rendering. Sprint Successor-4 (gated on a hardware kill-test not yet run):claude --resume/StreamEvent:: session_id()— explicitly out of scope until then.
The attended runbook (LOPI_KTB3_ATTENDED_RUNBOOK.md) ran for real for the
first time and did not reach the widget-render question — the server failed
to spawn. Two independent packaging bugs found and fixed this session, both
verified in one green run. Full detail in LEDGER.md's KT-B3-Live entry;
short version below.
Read first, in order: CLAUDE.md, CHANGELOG.md's KT-B3-Live entry,
LEDGER.md's KT-B3-Live entry in full, then LOPI_KTB3_ATTENDED_RUNBOOK.md
itself.
mcpb/manifest.jsonused${platform}, which is not a real MCPB substitution token — Claude Desktop's MCP log showed it passed through literally, soentry_point/mcp_config.commandresolved to a directory that never existed and the server hit "Failed to spawn process: No such file or directory" before tool discovery could even start. Fixed by hardcoding the literalserver/darwin-arm64/lopipath (the repo isdarwin-only percompatibility.platforms, so noplatform_overridesmechanism was needed).- This branch's
mcpb-release.ymlhad regressed totimeout 10(unavailable on macOS runners) — amain-merge timing gap, unrelated to Finding 1. Re-appliedperl -e 'alarm 10; exec @ARGV'directly. - Both verified together in run
29770853385(headSha467abb8), smoke-test included — green end to end, realinitialize/serverInforound trip.
- Discard the stale
.mcpbin the repo root (lopi-bfe4d7bb..., the artifact from the failed attempt) and pull the fresh one:lopi-467abb86e6e3408e73fefc7367db9e72d428587c-darwin-arm64.mcpbfrom run29770853385. - Re-run
LOPI_KTB3_ATTENDED_RUNBOOK.mdfrom step 1. This time the server should actually spawn — confirm that first (no repeat of the${platform}failure), then continue: tool list (all eight, includinglopi_get_stack_status), submit/check a real task, watch for an actual rendered panel vs. silent text fallback. - Given two packaging bugs slipped past the earlier
mcpb pack/unpackverification, don't trust that check alone again — it exercises the bundle mechanics, not the manifest's own command-resolution path a real host uses. If this session finds a third packaging issue, that's a sign the smoke-test step itself needs to install via a real (or real-ish) host path, not just unpack-and-invoke. - Write the
LEDGER.mdKT-B3 outcome entry per the runbook's own "either way" section — this will be the first time that section has real data to report instead of "not attempted."
The next step is the attended LOPI_KTB3_ATTENDED_RUNBOOK.md runbook —
not more Claude Code work. Everything automatable in LOPI_DISTRIBUTION_ PLAN.md's Track B is now built and packaged. Nothing about the actual
render has been verified — that's not an oversight, it's the correct
boundary a sandbox can't cross, per the runbook itself and the MCP-Serve-1
KT2 / MCP-App-1 KT-D2 precedent for this exact class of blocker.
Read first, in order: CLAUDE.md, CHANGELOG.md's [0.19.0] entry,
LEDGER.md's MCPB-App-1 entry in full (the branch-persistence decision,
the join fixture, and — importantly — the macOS-build toolchain finding are
all there), this file's own words below, then LOPI_KTB3_ATTENDED_RUNBOOK.md.
- Branch persistence —
tasks.branch, written byAgentRunner:: persist_branchthe momentTaskStartedfires. Real column, real store call, tested. lopi_get_stack_status— the eighth MCP tool. Joins the roster with per-task DAG stage and branch. Verified against a real two-task, two-stage concurrent fixture (KT-B2) — real field values, not just success/failure.- The
ui://lopi/stack-statuswidget —src/mcp_ui/stack_status.html, implements exactly the three lifecycle methods specified (ui/initialize/ui/notifications/initialized/ui/notifications/ tool-result), read-only, no interactivity. Bound via_meta.ui. resourceUri.lopi-mcpgained realresources/list/resources/readsupport to serve it, plusstructuredContenton every tool call. mcpb/manifest.json+.github/workflows/mcpb-release.yml—mcpb validate/pack/unpackmechanics verified for real (caught and fixed two schema errors in the process). The actual macOS arm64 binary does not exist yet — see below.
This is new — MCP-App-1 and the plan doc both assumed Deliverable 4 was
sandbox-safe ("nothing here needs nested-spawn access or a GUI host"). That
assumption held for KT-B1/KT-B2 and doesn't hold for a real target binary.
Checked two ways, not assumed (full detail in LEDGER.md):
- Plain
cargo build --target aarch64-apple-darwinfails immediately — this sandbox'sccis Linux GCC/Clang, incompatible withring's macOS-targeted build flags. cargo-zigbuildgets substantially further (pastring, pastopenssl-syswith vendored OpenSSL) but hits a hard wall atlibgit2-sys's ownbuild.rs, which unconditionally requires Apple's Security.framework/CoreFoundation.framework for anyappletarget — no feature flag exists upstream to avoid this. Proprietary Apple frameworks aren't obtainable in this sandbox, legitimately or otherwise.
What a session with real macOS access (attended, or a GitHub Actions run
on the new macos-14 workflow) needs to do:
- Trigger
.github/workflows/mcpb-release.ymlfor real (currentlyworkflow_dispatch-only, deliberately not wired to run automatically before its first real run is watched end to end) — or runcargo build --release --target aarch64-apple-darwin --bin lopiplusmcpb pack mcpbnatively on real Apple Silicon hardware. - Confirm the resulting
.mcpb's binary actually launchesmcp-servewhen invoked exactly asmcp_configspecifies — the workflow's own smoke-test step does this already; if run by hand, replicate it (drive a realinitializeover stdio, confirmserverInfocomes back). - Then, and only then, run
LOPI_KTB3_ATTENDED_RUNBOOK.mdagainst that real bundle: install in Claude Desktop, confirm the tools list shows all eight tools includinglopi_get_stack_status, submit a trivial task, and watch whether an actual rendered panel appears versus silent text-only fallback. Write theLEDGER.mdentry for whichever outcome happens — both are legitimate, complete results per the runbook's own framing.
Phase B2's remaining items (privacy policy doc, README quick-install
section, desktop-extension form submission) all wait behind KT-B3 clearing,
per the plan's own phasing — not attempted here. One consequence worth
knowing before it surprises anyone: mcpb/manifest.json's
privacy_policies array points at PRIVACY.md, which doesn't exist in the
repo yet — a 404 until Phase B2 writes it. Sideloading (this sprint's whole
distribution path) doesn't require the file to exist for install to work,
only directory listing does, so this doesn't block anything here — just
don't be surprised the link is dead if you follow it now.
LOPI_DISTRIBUTION_PLAN.md's repo copy is still the pre-Track-D-merge
draft (no Deliverables 1–2, no KT-B1/B2/B3, no widget mention in its Track
B section). This sprint, like MCP-App-1 before it, worked from a pasted
up-to-date version rather than the repo's own stale copy. Third time this
exact drift has been logged (LEDGER.md's MCP-App-1 and MCPB-App-1
entries both flag it) — genuinely overdue for a sync pass; a session that
trusts the repo's own file over a pasted one will miss the entire Track
B/D merge.
Read first, in order: CLAUDE.md, CHANGELOG.md's [0.18.0] entry,
LEDGER.md's MCP-App-1 entry in full, LOPI_DISTRIBUTION_PLAN.md's
Track D section — but read it with caution, see the drift note below.
MCP-App-1 attempted Track D (Loop Stacks inline MCP App dashboard). Its own
hard gate, KT-D2 ("does the MCP Apps ui/initialize handshake actually
complete in a real Claude Desktop install and a real claude.ai account"),
cannot be run in this sandboxed environment: headless Linux container, no
DISPLAY, no macOS/Windows, no authenticated claude.ai session anywhere on
disk. This was checked concretely (uname, $DISPLAY, /Applications,
credential paths, ps aux for any usable interactive claude session — see
LEDGER.md for the exact commands and output), not assumed. Per the sprint
brief's own instructions, that's a legitimate stop: no widget code, no
ui:// resource, no new tool implementation were written. This is the
correct outcome, not an incomplete one — don't treat "no widget shipped" as
a task left undone.
What was answered, since it doesn't need live hosts: KT-D3 (the
tool-binding decision). Full reasoning in LEDGER.md, short version: the
widget needs a new aggregating tool, not a rebind of
lopi_get_agent_dag. Neither existing tool covers Deliverable 4's fields
(task roster + branch + live stage-level TaskStatus) — tasks.status is
coarse ("running" for the entire execution, no stage detail), stage
detail only lives in agent_dag_nodes, and branch has no structured
durable source at all (only an in-memory event, a freeform log line, or
the terminal Success{branch} variant). That last point is a new
prerequisite MCP-App-1 found mid-research, not something the original plan
anticipated: persisting branch as a real column (or dedicated store call)
when TaskStarted fires needs to happen before the aggregating tool can be
built cleanly.
- KT-D2 itself. Build the trivial "hello from lopi"
ui://resource exactly as the original brief specified (a static HTML page, bound to any throwaway tool), and attempt the real round trip in a real Claude Desktop install and a real claude.ai account. If it renders cleanly, proceed to KT-D1. If the handshake fails silently (tool call succeeds, resource fetch succeeds, no iframe appears), log the exact protocol version / SDK version / host version / failure point and treat Track D as blocked pending an upstream fix — there's no client-side workaround for a host not completing its half of the handshake. - KT-D1, once KT-D2 clears: with the trivial resource attached, confirm a plain-text MCP-Serve-1 tool (not bound to the resource) still renders clean text in Claude Code, nothing broken by the resource's mere presence elsewhere in the server.
- The branch-persistence prerequisite this sprint found, before
building the new aggregating tool: decide how branch gets persisted
structurally (new
taskscolumn vs. a dedicated store call keyed onTaskStarted) — seeLEDGER.md'sMCP-App-1entry for the exact places branch currently does and doesn't appear. - Only then: Phase D1 (minimal widget against the new tool), D2 (real
structuredContent), D3 (cross-host verify: Desktop, claude.ai, Cowork if reachable; confirm Claude Code still degrades cleanly).
LOPI_DISTRIBUTION_PLAN.md in the repo is stale — it's the pre-MCP-Serve-1
draft (Track A still shown as unbuilt, no Track D section at all). This
sprint's brief pasted an up-to-date version (Track A marked shipped, Track D
added) directly into the session rather than pointing at the repo file,
which is the only reason this sprint had the real Track D spec to work
from. Not this sprint's job to fix (same call as the two-NEXT_SESSION_ PROMPT.md-files drift already flagged in the MCP-Serve-1 entry below), but
worth a sync pass — a session that trusts the repo's own copy over a pasted
one will miss Track D's existence entirely.
Read first, in order: CLAUDE.md, CHANGELOG.md's [0.17.0] entry,
LEDGER.md's MCP-Serve-1 entry, LOPI_DISTRIBUTION_PLAN.md Track A in full.
Confirm Cargo.toml's version matches CHANGELOG.md's top entry before doing
anything else.
lopi mcp-servesubcommand (src/mcp_commands.rs) — the curated seven-tool set from the plan's Track A 1.1 table, over stdio, reusinglopi_mcp::server::serve()unmodified.plugin/skills/lopi-cli/SKILL.md(see the layout note below for why it's underplugin/, not repo-rootskills/). Documents the CLI as it ships today.plugin/.claude-plugin/plugin.json+.claude-plugin/marketplace.json(repo root) +plugin/.mcp.json.- Local install verified live:
claude plugin marketplace add,claude plugin install,claude plugin details, and a reallopi_submit_task→lopi_get_taskround-trip through the actual installed/cached binary (not just the dev build).claude plugin validate --strictclean. - Stretch goal (submit to
anthropics/claude-plugins-community, announce publicly) — not done, correctly optional per the sprint brief, not attempted so as not to rush Phase 2/3.
The plan's package layout puts .claude-plugin/, .mcp.json, and skills/ at
the repo root. That fails claude plugin validate --strict live: it flags this
repo's own root CLAUDE.md as invalid "plugin root" content, and CLAUDE.md is
real contributor-facing content that shouldn't move or disappear to satisfy a
plugin validator. The actual layout shipped:
lopi/
├── .claude-plugin/marketplace.json # fixed discovery location — stays here
├── plugin/
│ ├── .claude-plugin/plugin.json # name: "lopi" — immutable, see LEDGER.md
│ ├── .mcp.json # ${CLAUDE_PLUGIN_ROOT}/bin/lopi mcp-serve
│ ├── bin/ # gitignored — built by scripts/build-plugin-bin.sh
│ └── skills/lopi-cli/SKILL.md
├── scripts/build-plugin-bin.sh
└── src/mcp_commands.rs
marketplace.json's one plugin entry has "source": "./plugin". If a future
session touches the manifest layout, re-run claude plugin validate --strict
against the actual repo (not a fixture) before trusting any restructure — this
exact failure mode is easy to reintroduce.
The Success Criteria's interactive checks — claude --plugin-dir <path> loading
the plugin, /reload-plugins picking it up, and the skill actually triggering
on a natural task-submission-shaped prompt — could not be driven end-to-end
from inside this session. This environment's permission classifier denies a
nested claude -p/interactive spawn from within an already-running Claude Code
session (confirmed live during KT2 — the attempt was blocked outright, not
merely slow). What was verified as a substitute, and is solid evidence but not
the same thing:
claude plugin validate --strictclean on the real plugin.claude plugin marketplace add+claude plugin install+claude plugin list+claude plugin details lopiall succeed and show the expected component inventory (1 skill, 1 MCP server, ~117 tok always-on cost).- The installed binary at its real cache path (
.../lopi/<version>/bin/lopi) round-trips a realinitialize→lopi_submit_task→lopi_get_taskMCP session correctly.
What a session with a real interactive claude — a human's local machine, or
an environment that doesn't block nested claude -p — needs to check:
claude --plugin-dir <path-to-lopi-repo> (or the marketplace-installed
version), then in a live session ask something task-submission-shaped ("submit
a lopi task to fix X") and confirm the lopi-cli skill actually fires and picks
the right MCP tool, not just that the skill would structurally load. Also
confirm /reload-plugins after a manifest edit actually picks up the change
without a full session restart.
LOPI_VS_OPENCLAW.md's feature table (row 2, "Agent Loop") cites an
AgentState enum with Planning → Implementing → Testing → Scoring → OpeningPr → RollingBack transitions. The real AgentState
(crates/lopi-core/src/agent.rs) has no OpeningPr/RollingBack variants and
is constructed nowhere in the codebase — dead scaffolding. TaskStatus
(crates/lopi-core/src/task.rs) is the live, CLI/API-surfaced type.
skills/lopi-cli/SKILL.md documents TaskStatus and calls out the drift
inline; LOPI_VS_OPENCLAW.md itself is still stale and worth a small fix
later — not this sprint's job, didn't touch it.
Track B (MCPB desktop extension) and Track C (Connectors Directory) — neither
touched. Track B reuses the exact same ToolHandler and state-sharing design
(see LEDGER.md); Track C needs its own re-derivation, not a copy-paste of
KT4's answer (a Streamable HTTP transport serving multiple concurrent clients
changes the dispatch-ownership calculus — see LEDGER.md's closing note).