Skip to content

Internals & Contributing

Orientation for working on kin itself. This page links out to the repo's working documentation; it deliberately holds no content of its own, so the linked files stay the single source of truth.

These links point into the repository

The targets below live in the source tree, outside this site. They resolve for anyone with access to the repository.

This site vs the internal docs

This documentation site is the curated user guide — task-oriented pages for people running kin. The files linked below are the working documentation: the design notes, conventions, and gotcha logs written for the people (and AI peers) building kin. When the two disagree, the working docs and the source are authoritative; this site is the friendlier surface over them.

Architecture and conventions

The project conventions, the harness internals, and the UI internals — read these before changing the corresponding tier.

  • AGENTS.md — the canonical agent-guidance file (AAIF cross-tool standard): project-wide conventions, the stack, the run / verify commands, the pointer block, and the IPC seam. (CLAUDE.md is a one-line @AGENTS.md include shim for Claude Code.)
  • src/kin/harness/CLAUDE.md — the harness map: events, model backends, loop, session, modes, permissions, tools, and the tool-author guide.
  • src/kin/tui/CLAUDE.md — the Textual UI map: app, widgets, modals, theme, and the inbound / outbound / interrupt seam.

Testing & verification

Headless suites (the mechanical gate) and the perf benchmark that picks between virtualizing the transcript or skipping. The verify_*.py → pytest migration is complete (Phases 1-5, 2026-07-02) — task verify runs the full pytest collection; per-area targets (task verify-harness, task verify-app, etc.) invoke the matching subpackage or file.

  • tests/test_harness/ — ~50 submodules, the biggest split (loop, every tool, permissions, modes, headless, planning, subagents, browser, ssh egress, git network, etc.). The mechanical-gate heart of the suite. See Testing for the full layout + count.
  • tests/test_app/ — UI dispatch + modals + the slash command registry.
  • tests/test_settings.py · tests/test_banner.py · tests/test_integration.py · tests/test_workflow.py · tests/test_search.py · tests/test_memory.py · tests/test_provider_presets.py · tests/test_evals.py · tests/test_outpost/ (needs --extra outpost) — the rest of the headless suite. Stdlib + Textual alone (lazy SDK imports); no model, no terminal, no network.
  • tests/test_evals.py + snapshots/evals_baseline.json — the regression sentinel (in task verify): pinned FakeBackend scenarios fingerprinted (collapsed event sequence, tool-call names, terminal shape, span names) against the checked-in baseline; any drift is a named REGRESSION at commit time. Deterministic graders only; re-pin a reviewed contract change with --update-baseline. Also the headless coverage for the harness/spans.py JSONL span sink. See the Evals & observability guide.
  • tests/test_web.py — web tools (needs the optional httpx / trafilatura deps).
  • tests/test_pty.py — out-of-process PTY smoke (non-default pty dep group; spawns a real kin under a pseudo-terminal).
  • tests/test_snapshot.py + snapshots/ — hand-rolled snapshot guard (6 baselines, byte-stable). See the snapshot discipline in snapshots/README.md.
  • tests/test_perf.py — the synthetic transcript perf benchmark. Builds a fixed-seed journal at 200 / 500 / 1000 / 2000 messages and measures mount time, scroll-event-to-paint latency, and RSS delta. Run via task verify-perf (or --quick for 200+500 only); OUT of task verify because absolute numbers are machine-specific, the scaling factor is what to compare across machines. The numbers pick between Tier 1 (pre-baked Markdown strips) / Tier 2 (Static(rich.console.Group) per msg) / Tier 3 (custom TranscriptScroll) of the transcript-virtualization rock, or the Skip outcome. The current decision (Skip) is recorded in STATUS.md (Recently-landed, the transcript-virtualization Phase 0 entry).
  • scripts/live_drive_vllm.py — the live wire gate. The only test that pairs a real backend (make_backend) with the real KinApp driven through the production @work turn worker. 11 scenarios (the v3 §1-3 + §4-7 surface plus cross-wire, /security-review, and a CI-safe dry_run); --all writes snapshots/live_all_<ts>.json (schema_version=2) with per-scenario pass/fail + wall_s + git_sha + resolved model + wire + mode, then reports PASS/REGRESSION against the pinned snapshots/live_baseline.json (--update-baseline re-pins; --passk K reports the stochastic pass^k signal — see the Evals guide). Run via task live-drive-all (the pre-promote check) or task live-drive-dry (offline smoke). NOT in task verify (needs a reachable model endpoint). Per-scenario screenshots land at snapshots/live_<scenario>_<ts>.svg (gitignored — per-run dev artifacts).

Gotchas and invariants

  • REFERENCE.md — every gotcha, footgun, security invariant, and postmortem. The tiered CLAUDE.md files point here for the footguns.

Plans and backlog

  • STATUS.md — the agent-curated status board: Active / Next / Blocked / Recently-landed. The session-start read; the journal-of-record is git log.

Active design decisions

  • 0092 — get.kinra.ai installer + public docs.kinra.ai + kin doctor (get.kinra.ai installer wave). Shipped. One pasted command (bash <(curl -fsSL https://get.kinra.ai/install.sh)) takes a fresh Linux/macOS box to a running kin; kin doctor verifies any install (three chunks exactly — install: uv/kin/git/PATH with task info-only; config: settings parse + a provider walk mirroring make_backend's chains that names the winning endpoint/key source; network: GET {base_url}/models where ANY HTTP response counts as reachable, --offline skips), and kin --version (argparse action="version" off kin.__version__) is the installer's success gate. Serving rides the outpost compose under a publish profile (killed: the scp'd mini-compose; offered-not-chosen: Forgejo): two nginx:alpine statics — get :7018 (page + scripts + wheel + sha256s from container/installer-www/, assembled by outpost:_deploy at deploy time, synced-by-construction) and docs :7019 (the mkdocs site/fully public docs.kinra.ai, OVERRIDING the standing LOCAL-ONLY docs stance; accepted exposure: internals topology, assessed low-risk behind the wildcard-DNS'd VPS + WireGuard-only 10.0.0.x + Pocket ID on everything sensitive). nginx over busybox httpd because kinra-secure-headers' nosniff + busybox's MIME table would block the docs CSS. Wheel quick-path: uv REJECTS kin @ …/kin-latest.whl (filename needs a python tag) so the deploy publishes the real PEP 427 wheel + a static PEP 503 simple/kin/ index — the version-stable one-liner is uv tool install kin --index https://get.kinra.ai/simple/ (accepted: kin's built code is publicly downloadable from a private repo; kin-latest.whl stays as a curl/sha256 alias). install.sh = uv sync + uv tool install --editable, NOT task sync (Taskfile stays a gate runner); SSH key is the ONLY credential (GitHub probe: ssh exit 1 = success); both scripts bash-3.2, idempotent, sudo-free, --check dry-walk, main-at-EOF truncation guard, fresh-Opus adversarial pass pre-ship. outpost-install.sh stages five empty 600-mode secret stubs and forks on the three REQUIRED oauth secrets (optional pair self-heals empty, matching outpost:_deploy); "staged, awaiting secrets" exits 0 by design. Windows = WSL2 tab now, native-Windows rock queued; SHA256 not GPG.
  • 0091 — Composer paste interception — file drops normalize to @-mentions (drag-and-drop/file-paste wave, Rock 3). Shipped. Finder drag-drops and terminal pastes of absolute file paths reach the Composer via _on_paste, which the wave overrides with a 4-step body: sanitize via strip_paste_controls (C0/DEL/C1 stripped, bidi/ZWS left alone for prose — the title/toast sinks do their own cleaning); detect "the ENTIRE paste is a drop" via paths_from_paste (newline-split for iTerm, shlex.split(posix=True) for Ghostty shell-escapes, bare-spaced-path fallback, then ALL-tokens-must-be-absolute-and-exist discriminator — the drops-are-always-absolute prose-killer); on detection, event.prevent_default(); event.stop() (BOTH load-bearing — prevent_default stops the MRO walk's TextArea._on_paste re-insert; stop halts the DOM bubble before the App's on_event Paste-branch re-forwards the bubbled event back to self.focused); >10 files warns and inserts verbatim (silent partial attach is worse than a visible choice); oversize images (>5MB) auto-downscale via the macOS sips -Z 1568 builtin (_decide_downscale is the pure decision, run_sips_downscale is the live subprocess, None on any failure — caller falls back to mentioning the original). Mention-tokenize via at_mentions.mention_token; dedupe vs self.text; insert via TextArea._replace_via_keyboard (undo/selection semantics match a typed paste). Toasts (markup=False — display-sink discipline): attached N file(s) as @-mentions (+ · K already attached); is_secret_path token → warning looks like a credential file — it will not be attached (the mention still inserts; Rock 1 makes the mention inert visibly). paste_file_mentions(workdir=None) knob via bool_setting("KIN_PASTE_MENTIONS", "paste_file_mentions", True, …); PROJECT-SAFE (no egress/secret/containment surface — identical to a manual paste) and human-only (NOT model-writable — a UX toggle). 30 new tests (25 pure-function + 7 pilot — full paths_from_paste matrix + sanitize vectors + sips decision + each paste-handler branch + the App-re-forward smoke).
  • 0090 — read_file learns Word (.docx) + Excel (.xlsx) (docx/xlsx read_file wave). Shipped. Kills the silent-mojibake fallthrough: an Office file (zip of XML, not UTF-8) used to decode through _read_text's errors="replace" as a wall of replacement chars. Two thin extractors in _media.py mirroring extract_pdf_text (str | bytes in, normalized error: out, caller owns the cap): extract_docx_text walks iter_inner_content() (paragraphs + Markdown tables, document order; never touches word/vbaProject.bin — the macro guarantee) and extract_xlsx_text renders each sheet as a ## title + Markdown table (read_only/data_only, capped 10 sheets × 100 rows × 30 cols). read_file + at_mentions._read_mention dispatch .docx/.docm and .xlsx/.xlsm (macro variants routed deliberately; the VBA blob is never read). Two verified mitigations, not decorative: a declared-size zip-bomb cap (MAX_OFFICE_DECOMPRESSED_BYTES = 50 MB, summed from the zip central directory before any parse) and the XML entity-expansion guarddefusedxml lazy-imported FIRST and refused if absent (openpyxl auto-detects it; the explicit import is both load-bearing and the deptry DEP002 anchor). The docx side rides lxml's built-in limits (asymmetric, no gate). All three deps default (like pypdf), no extras group, no waivers. Four-pin security battery (test_office_security.py): DEFUSEDXML is True, zip-cap fires, macro variants never mojibake, corrupt-bytes → error: for all four extensions. markitdown rejected; pptx/rtf/odt/epub + web_fetch wiring deferred.
  • 0089 — TUI two-phase quit with visible memory curation + skip (Workstream B of the workspace-scoped memory + two-phase quit plan; Workstream A = workspace-scoped memory, DR 0088). Shipped. Every quit route (ctrl+c via priority binding, ctrl+q, command palette, /exit / /quit) funnels through one sync chokepoint KinApp._request_quit that gates on _reflection_warranted (client is None / _turns < 1 / memory.enabled(workdir) / settings.memory_reflect_enabled) and: (a) already-quitting (second ctrl+c / esc during curation) → cancel_group("quit") + self.exit() — instant skip; (b) not warranted (no turns, memory off) → self.exit() directly — same UX as the pre-Workstream-B behavior; (c) else flip self._quitting = True SYNC (kills double-press race) and start @work(group="quit", exclusive=True) _run_quit. The worker cancels agent_turn, signals MCP shutdown (teardown overlaps curation — preserves today's ≤20s worst-case quit latency because reflection uses a memory-only scoped registry, never MCP tools), flips status.busy_label = "curating memory… · ctrl+c to skip" (the busy_label reactive wins over a stale label from the cancelled turn), then asyncio.wait_for(reflect_quietly(...), timeout=20.0). finally: self.exit() runs on CancelledError too. Stomp-guard set keeps the cancelled turn from racing the curator: TurnLifecycleMixin._send / _end_turn and SlashCommandsMixin._run_compact's finally all read self._quitting; KinApp.action_interrupt short-circuits through _request_quit on esc during curation (prevents the double-esc rewind-picker-during-quit wart). on_unmount's reflection spawn is the ONLY change on the fallback path — gated by not self._quitting to prevent a double curation (everything else byte-identical). Blocking modals parked on push_screen_wait during the window are left alone (same disposition as today's /exit); non-send slash commands remain typable (accepted). 7 new tests in tests/test_app/test_quit.py; 0 edits to the 51 pre-existing test_unmount.py / test_slash.py tests (every existing one lands on the instant-exit branch — FakeHarness._turns is 0 by default).
  • 0088 — Workspace-scoped memory — auto-stamped frontmatter + read-seam filtering (Workstream A of the workspace-scoped memory + two-phase quit plan; Workstream B = TUI two-phase quit, separate DR). Shipped. Hybrid scoping — no per-workspace roots, no permission shape change, store.resolve_path / GLOBAL_ONLY memory_dir untouched. create stamps os.path.realpath(workdir) into a workspace: frontmatter line on project-fact and episodic (NOT preference — always global by design; explicit workspace: global honored verbatim; str_replace/insert deliberately do NOT re-normalize — explicit edit is intent). Read seams: MemoryIndex.recall gains AND (f.workspace = '' OR f.workspace = ?), filter lands BEFORE the 20-candidate pool so the Python re-rank is untouched; store.index_block partitions the listing into local + foreign (truthy workdir) and the foreign files collapse to ONE summary line — the reflection-safety seam, the only place a reflection pass sees that other workspaces even exist (as a count, never as titles or rel-paths). Legacy files = global (no migration; _ws_normalize collapses missing/empty/global to ""). Schema: mem_files.workspace TEXT NOT NULL DEFAULT '' added on a SCHEMA_VERSION = 2 migration (PRAGMA user_version != 2 → DROP + recreate + set version; NOT in mem_fts so the ai/ad/au triggers stay byte-identical and the stamp is never tokenized). Dashboard search (memory_api._search workdir=None) stays unfiltered — falsy workdir is the operator's deliberate escape hatch. 7 new tests in tests/test_memory.py; 0 edits to the 15 pre-existing tests (legacy-as-global guarantees the byte-identical round-trip). 0 edits to the reflection prompt's stop-reason / scope behavior — only one nudge line added.
  • 0087 — Skills catalog — per-profile skill grants (prompt-polish deferred follow-ups wave, Step 6). Shipped. The bundled defaults/skills/ catalog sits directly after SYSTEM_PROMPT as the cache anchor (DR 0081's "append-only" cache argument) — it cannot be stripped per-profile without busting parent→child cache sharing. So per-profile: grant the skill tool itself (a read-only prompt loader, no side effects) to a closed allow-list. Two profiles earn skill: coder (open-ended domain work where build conventions matter mid-task) and researcher (open-ended web research where research recipes matter mid-task). Five profiles explicitly DO NOT earn skill: explorer (narrow codebase-orientation rubric; skill load mid-orientation is noise), critic / critic-code / security-auditor (rubric-bound; skill load mid-critique is noise and may bias the review), planner (frozen by the planning freeze; no execution surface a skill would unlock). general (tools=None) inherits the FULL registry — Registry.scoped(None) is a no-op filter, consistent with general's "all tools available" framing. Frontmatter-only change: coder.md + researcher.md gain - skill at the end of tools:; SUBAGENT_PREAMBLE untouched; spawn pops (workflow / write_plan / ask) untouched. Accepted residual: a skill-less child sees the catalog and may attempt a skill call → one tool-not-found error turn (a KIN_STRICT_SKILLS=1-compatible clean error: reply the model can adapt to); revisit trigger = observed repeated skill-call misfires in child transcripts. One authorized test edit (test_skills_agents.py's researcher's exact-tools assertion gains "skill") + one new unit test (test_skill_grant_set_per_profile pins the closed grant set). No system-prompt edits, no preamble edits, no spawn-pop edits, no registry edits — the per-profile allow-list IS the gate.
  • 0086 — Subagent-result trust framing — third formal re-reject (prompt-polish deferred follow-ups wave, Step 4, doc-only). A subagent's returned prose enters the parent UNFRAMED, recorded a third time with the steelman explicitly named and rejected. Three-part reasoning: (1) principal parity — the parent + child are the SAME composed system (same tools/gates/egress/prompt-cache); framing only the child's identical web_fetch → summarize flow draws a trust boundary the architecture deliberately does NOT draw; (2) signal dilution[BEGIN UNTRUSTED …] markers mean "attacker-influenceable bytes crossed an external boundary" across 17 audited call sites today; wrapping first-person agent prose in the same markers weakens that meaning everywhere; (3) provenance-independent backstop — the permission gate on every consequential action (_decision_for's planning + mode + sandbox posture) is provenance-independent and applies identically to parent and child-influenced parent actions. The steelman (persuasion-shape argument: confident first-person report as a tool_result, materially different from raw bytes inside markers) is named AND rejected — the defense is the backstop, not the marker; deferral is cheap to reverse (the taint-bit sketch is ~15 lines, parked). Three named revisit triggers: (a) a demonstrated injection that traversed a child summary into a parent privileged action; (b) subagents ever gaining capabilities the parent's own gates don't cover; (c) any surface that auto-approves parent actions keyed on child output. REFERENCE.md § "Subagents" gains a one-line pointer citing this DR. Doc-only — zero code, zero test surface.
  • 0085 — Schema-up-front dispatch note (prompt-polish deferred follow-ups wave, Step 3). Shipped. run_subagent(..., schema=...) builds the child's prompt via a new _schema_note(prompt, schema) helper that appends a one-line [structured-output note] naming the schema's top-level properties keys (with (required) markers from the JSON-Schema required array). Names ONLY — never the schema JSON itself (Tier 1 + Tier 2 own the JSON emission; a second copy is drift bait); top-level keys only; nested schemas are NOT recursed. Appended to the USER prompt (NOT the system prompt — DR 0081 rejected schema foreshadowing in profile bodies and the system prompt is the cache anchor). The note NEVER instructs the child to emit JSON (that would regress the prose answer on the non-schema paths and confuse Tier-1 forced calls with prose-JSON hybrids). Defensive: empty properties, non-dict values, non-string names, non-list required — all degrade to "no note, prompt unchanged". 4 new tests in tests/test_harness/test_subagents.py (helper direct + nested-schema + junk-schemas + end-to-end dispatch); no pre-existing tests touched. Schema-omitted dispatch is byte-identical to pre-Step-3 behavior (regression pin).
  • 0084 — Live planning walk-up (prompt-polish deferred follow-ups wave, Step 1). Shipped. Session.planning is now a root-resolving property — getter returns (self._blocker or self)._planning, setter routes to the root too; _blocker is pre-flattened to the root at construction (spawn_child passes blocker=self._blocker or self), so the lookup is one hop, never a walk. _planning replaces planning in Session.__init__; the child.planning = self.planning line in spawn_child is DELETED (the snapshot was DR 0081's named revisit trigger — the mid-freeze bypass is now closed by construction). enter_planning/exit_planning bodies unchanged; exit_planning docstring gained one sentence noting it lifts the ROOT's freeze. ~19 bare sess.planning = True/False test pokes keep working through the setter untouched. Planner recursion stays off (the pre-freeze window is still open — Step 2 is a separate, decision-gated step); the DR records that the technical revisit trigger is satisfiable now and the policy question is independent. Two authorized test edits (test_subagents.py's spawn-propagation test rewritten to assert LIVE semantics; test_planning.py's test_write_plan_root_walk _plan_path half unchanged, planning half rewritten) + one new test (test_planning_live_root_propagation pins the five-part invariant: enter-mid-tree, exit-on-root, grandchild depth-2, setter routing, root-only session). Mandatory fresh-Opus adversarial review appended to research/2026-07-10-step1-adversarial-review.md.
  • 0083 — The liveliness doctrine (Outpost polish wave, 15 rocks). Shipped. One sentence governs all dashboard motion/freshness: motion is spent only on live work and open questions, only while true; settled = static; the UI never claims a liveness it doesn't have. Deliberately amends the 2026-07-07 pass's "no animations, CSS quiet by design" ruling for work-liveness surfaces ONLY (DR 0021's static tab notes and the config cards stay quiet; the header pill actually loses its pulse — connection health is not live work). Shipped corollaries: one running grammar (.dot.running is the sole motion carrier, now nested in the Jobs card's running badge too); the 3-state connection pill (live/polling/offline, honest about mechanism — a dead SSE channel now reads polling instead of a stale green "live"; hover = "as of HH:MM:SS"); elapsed work is the only per-second tick (running · 42s, Buildkite pattern; deliberately NO counter on needs-human rows — a clock on human latency reads as nagging); one-shot 1.2s state-flash (Yellow Fade) acknowledges autonomous state transitions calmly — no toasts fire from renders, push/Matrix stay the announcement channels; the run-detail header refreshes in place on transcript eof (state badge/Finished/Stop hidden-toggle/"report ready — reload to view"); coarse 45s rel-time re-tick + absolute-time hover everywhere; ● N need you — Outpost / ▸ running — Outpost browser-tab title consumes the SAME server tabs payload as applyTabs (closes DR 0021's R6); cancelled = neutral grey everywhere ("stopped by choice", never failure-red); raw enums never shown (needs_human → "needs human"). Consequences: a four-map lockstep contract for state vocabulary (_STATE_LABELS/state_class in run_page.py ↔ STATE_LABELS/STATE_CLASS in the transcript IIFE ↔ jobStatusBadge/_dotClass in landing_js.py, all cross-cited); Graphite+ tokens single-sourced in templates/theme.py (the run page's drifted .badge fork was the symptom); base button is margin-neutral (spacing is a container concern — the margin-top: 0 undo family is dead); focus grammar = halo(:focus)/double-ring(:focus-visible)/inset(full-bleed rows) with the resting-box-shadow-defeats-the-ring gotcha in REFERENCE.md; _shell's open disposition resolved — KEPT as the themed 404/500 page rendered by _catch_all_errors for HTML-accepting non-API GETs (EL-1 JSON contract byte-identical). Deferred with named triggers: dynamic favicon, per-row DOM diffing, modal consolidation, report fetch+inject, waiting-since counters. ≤5 animated elements at once by construction; reduced-motion = replace-not-delete (every motion is redundant signal over color+text+shape).
  • 0082 — Model-facing truth (prompt-polish wave, Rocks 2–4). Shipped. The model's context tells it the truth about its own harness, each fact once, at the cheapest altitude. (1) Stop-reason channel: turn_cap / token_budget / loop_detected were UI-only events.text("system", …) notes — the model's transcript just ended; each site now sets Session._pending_stop_note (token_budget writes through the _blocker walk-up), drained by loop._preprocess_root_turn into an env.stop_reason_reminder <system-reminder> on the next turn (bg-completion snapshot-and-clear shape, innermost prepend, transient/never persisted). (2) SYSTEM_PROMPT "Tool results" paragraph teaches the result grammar once in the cached prefix: untrusted framing generalized past its two rotted examples, error:/[refused: …] cross-backend conventions, the truncation-marker contract, parallel dispatch of independent same-turn calls; the planning paragraph drops the two sentences write_plan/present_plan descriptions already carry and gains the read-only-freeze consequence — all 7 sentinel pins preserved verbatim, net +19 words (588→607). (3) Description trims/caps: task/agent_message drop the duplicated wire-internals paragraphs (schemas re-send every round); taskstodos backref; task.prompt tells the dispatcher to name date/branch inline (children get no ambient env); workflow states its caps by f-string interpolation of WORKFLOW_MAX_PARALLEL/WORKFLOW_MAX_AGENTS. (4) todos returns the authoritative rendered checklist ([x]/[>]/[ ], plain text) instead of a bare count — the tool normalizes silently, so a count alone hid drift; mirrors tasks's snapshot contract. (5) Kin-voiced summarizercompaction.SUMMARY_SYSTEM is first-person Kin, so identity survives compaction. Verified-not-changed: web-tool cross-refs (finding was stale), reactive task caps, needs-human: prefixing (no gap — the generic exception catch adds error:). 4 new tests; the sentinel passed unedited.
  • 0081 — Subagent dispatch contract (prompt-polish wave, Rock 1). Shipped. One shared SUBAGENT_PREAMBLE (kin.harness.subagents) states the contract every child shares — dispatched for one focused task, no parent conversation, no user to ask (state assumptions and proceed), the FINAL message is the entire return value, batch independent tool calls — appended by SubagentOps.spawn_child BETWEEN the parent's composed system and the profile body, so the parent's cached prefix stays byte-identical (Anthropic cache_control + vLLM radix prefix sharing survive; mixed-profile workflow fan-outs share strictly MORE prefix than before since divergence now starts at the persona). ask is popped from EVERY spawned child, unconditionally, next to the existing workflow pop — the pop makes the preamble's "no user to ask" true and closes the real footgun (a workflow-spawned general child parking the whole run on a question modal until WORKFLOW_TIMEOUT; a bg child stacking a second blocking modal). Profile bodies trimmed to persona + rubric only — the duplicated isolation/return-contract boilerplate deleted from 6 profiles; critic/critic-code (which never had the contract) gain it via the preamble. inherit_system=False (artifact editor) and Session.from_profile (top-level launch) deliberately skip the preamble. Rejected: recursive task for plannerchild.planning propagation is a spawn-time SNAPSHOT, not a live root walk-up, so a planner grandchild would inherit a stale False and edit freely mid-plan; revisit trigger: sess.planning becoming a live walk-up property (REFERENCE.md § "Plan lifecycle & the planning freeze"). One authorized test edit (the empty-profile exact-equality assertion in test_subagent_system_prompt_inheritance).
  • 0080 — TUI voice conventions (polish wave, Rocks C1–C3): ONE hint voice across the composer placeholder / banner shortcut row / idle StatusBar hints (lowercase keys, present-tense verbs, single-space ·, @ files everywhere — BANNER_SPEC mocks updated in lockstep); /help documents only bindings that exist (the false F1 / ? claim removed, double-esc rewind + ctrl+c + deny-with-note added; McpManagerModal advertises its real e/x aliases); toast governance as a call-site convention (busy refusals = one template + warning/3s, echoes = default/2-3s, failures = error/5s, markup=False mandatory on dynamic messages) — deliberately NOT a wrapper helper; dead notice param removed from render_banner/WelcomePanel.show.
  • 0079 — Approval modal: native bodies + honest choice architecture + deny-with-note (polish wave, Rocks B1–B5). events.approval_request gains additive kind/args/diff; the pre-approval diff is computed by pure helpers sharing the tools' own match path (edit_file._resolve_edit factored so preview and enforcement can never disagree; roundtrip._edit_preview fail-soft to ""). Per-kind bodies (edit = the real diff; shell = KinSyntaxStyle-highlighted command; mcp = server › tool + provenance note; network = labeled rows, warning only on truthy force); amber title reserved for risky kinds (_CALM_KINDS), border stays primary for all. Always-scope inlined into the button label; Allow once is the focused default (bare Enter approves); un-scoped edit grants carry a "prefer allow once" breadth note. Deny-with-note: n → one-line input → ("deny", note)_approval_note-sanitized text rides the tool-result denial (The user says: "…"), so denial steers instead of dead-ending; request_approval returns (allowed, deny_note). ConfirmModal focuses Cancel (destructive confirms are always an explicit y).
  • 0078 — Spinner vibes + "one heartbeat, colour elsewhere" (POLISH r11; polish wave, Rocks A1–A4). Curated theme.SPINNER_STYLES vibe set (bloom default / braille / arc / toggle / line; constant cell-width frames; resolve_spinner handles unknown names + the KIN_ASCII → line override) + the pure shimmer.spinner_frame (elapsed-monotonic indexing — no per-style timers; reduced motion returns the literal G_RUNNING rest). Doctrine: exactly ONE frame-stepped spinner in the chrome (StatusBar busy glyph, riding the existing 1/12 tick) + one transcript mirror (running WorkflowCard at the style's own cadence — fixes the old 4fps drift); every other live surface is colour-only on LOCAL state; plain ToolCall rows / BgAgentsBar rows / TodoList stay static by decision. Selection: spinner key (project-safe, NOT model-writable) / KIN_SPINNER / /spinner picker with live preview; persistence via the /cost human-typed shape. Same bar: format_duration unification (Reasoning tail aligned to ≥2s), dim esc interrupts at ≥10s, idle key-hint line rotated only on the busy→idle edge (the decided no-Footer answer).
  • 0077 — Deferred: local-model escalation routing (G6.2, Step 8 of the field-gap Wave B). SLM-first auto-escalation is a field-validated 2025-26 pattern and kin has the plumbing (Session.set_provider live cross-provider swap, per-wire effort vocab in session/controls.py) — but not built, because the preconditions honestly aren't met: (a) DR 0069's guided-decoding probe found forced tool_choice grammar-constrained 10/10 on both wires, so the field's headline trigger (schema violation) is dead at Tier 1 on this fleet — the only remaining schema surface is the tool-less Tier-2 re-ask (session/subagents.py::_structure_via_prompt), per DR 0069's own "look at the tool-less Tier-2 floor, not Tier 1" invariant; (b) the designed anchor, KIN_AUTO_CLASSIFIER_MODEL (the planned CC-style Tier-3 classifier), does not exist anywhere in the tree — confirmed by grep, only named as a future lever in the gap analysis itself; (c) the local fleet serves one model at a time, so local escalation has no local target — the only escalation kin could build is local→paid cross-provider swap, a materially different cost/consent decision than the field's local-tiering pattern. Deferred (not Rejected, DR 0070's distinction) with three named revisit triggers: the auto-mode classifier lands, lived data names a real frequency (doom-loop-guard trips / Tier-2 re-ask failures worth automating), or a second model is served locally at once. /model stays the only routing surface; zero settings keys reserved. Doc-only — zero code.
  • 0076 — Rejected: session handoff (G4c, Step 7 of the field-gap Wave B). Mirrors DR 0070's refusal shape. The field ships live handoff (Cursor "Cloud Handoff") — a second live surface attached to the same running session; CONSTITUTION.md's "never two windows onto one Kin" is the governing line the field-gap analysis flagged this against. Reuses DR 0072's single-writer/single-reader test as the dispositive check: mid-run steering passed it (one message into an already-open window, prohibition #5 clear); live handoff fails it (a second window onto the same run, by construction). Also fails on the journal's design: persistence/journal.py::Journal is a single-writer append-only log every reader (replay.load, /resume, /rewind, the Outpost's read-only live-tail pane) assumes; attaching a second live writer/reader is a concurrency-model change, not an additive feature — the same layering objection DR 0072's own "DB polling from the child" alternative was rejected for. Prohibition #6/#7 framing per DR 0070: no lived need, so no breadth before the center and no ossification before discovery. Recommends the answer kin already ships in handoff's place: /fork + /resume (DR 0074, sequential, one writer at a time) for continuing elsewhere, /export (DR 0074) for a static keepsake, DR 0028's read-only live-tail pane for watching remotely. The Outpost keeps its own sessions — a difference in location, not surface (DR 0033). Named revisit triggers: kin distributed beyond single-operator (DR 0070's trigger 1), the journal growing a real merge/sync story, or a lived week where sequential fork/resume provably failed a real local↔Outpost need Blake actually hit. Doc-only — zero code.
  • 0075! shell passthrough (G8e, Step 6 of the field-gap Wave B; Opus-class implementer floor). Shipped. Composer-only entry point — no Tool, no CommandDef, no HANDLER_KINDS entry, no COMMANDS.register(...)src/kin/tui/commands/passthrough.py is never imported by the tool registry, the harness command registry, or the TUI family-import list; the ONLY caller anywhere is KinApp._run_shell_passthrough, reached only from a new on_composer_submitted branch (a human keystroke, never a model tool call). Idle-only (_turn_active/_compacting ⇒ notify + refuse, v1 keeps the interleaving problem off the table). Execution: asyncio.create_subprocess_shell with full user env, no scrub, no sandbox — the deliberate opposite of ask_sandbox_override's and the shell tool's scrubbed unsandboxed paths, because in THOSE the model chose the command (the threat scrubbing defends against) while here the human typed it directly (the same trust position as their own terminal); reuses shell._drain_to_completion (a third caller of the SIGTERM→SIGKILL discipline) and shell._truncate for one 16KiB cap (passthrough.MAX_OUTPUT) shared by all three sinks. Sinks: (1) transcript block via passthrough.render_block — literal rich.text.Text, no markup; (2) new unconditional journal audit record Journal.append_shell_passthrough ({type: "shell_passthrough", command, exit, output, ts} — unrecognized type is a structural no-op to replay.load's elif-with-no-else chain and never reaches replay_events, no version bump, DR 0067's snapshot/files_restored precedent); (3) next-turn <system-reminder> via new Session.inject_shell_passthrough_reminder, called from loop._preprocess_root_turn beside inject_completion_reminder — a NEW sibling queue (Session._pending_shell_passthroughs), not a repurposing of the bg-completion one (different formatter shapes), both command and output run through env._defang so a hostile command's OUTPUT can't forge a </system-reminder> close tag and smuggle instructions. Session gains two public methods; record_shell_passthrough mirrored additively onto FakeHarness (planner-authorized), inject_shell_passthrough_reminder is not (only reachable via the real loop.run_turn). REFERENCE.md gains § "Shell passthrough" (the full re-verifiable invariant list); docs/guide/keybindings.md documents the composer convention. Adversarial sub-pass findings appended to the DR post-review.
  • 0074 — TUI QoL batch: /export, /fork, /editor, Ctrl+R history search (G8, field-gap Wave B). One DR, four sequential landings (Steps 2–5) — batched because each is individually too small to carry its own record, the DR 0070/0072 precedent for a design-intent record spanning a partial build. All four steps shipped. /export: new src/kin/harness/persistence/export.py::to_markdown(journal_path) — pure, no Session dependency, builds on persistence.replay.load (the meta header) + the EXISTING cross-provider replay_events stream (never a second provider-native parser — reminder-stripping comes free); tool calls render as fenced ```<name> ` blocks, tool results elide to a one-line summary (export._elide) so a fullread_file/shell dump doesn't make the export unreadable. No sanitization on the output — it's a *file* sink the operator asked for, not a *display* sink (the DR argues the UI-phishing threat model display sinks defend against doesn't transfer to a markdown file opened later in the operator's own editor)./export [path]derives the journal path MODULE-LEVEL frompersistence.journal.project_dir(workdir)+app.client.session_id— no newSessionmethod, soFakeHarnessparity stays untouched; defaultkin-export-.mdin the workdir, explicit path wins, overwrite allowed with a notify (additive, no confirm, unlike/clear//rewind); busy-guard mirrors_cmd_clear. Supersedes STATUS.md's/save Next-up item (deleted same commit)./fork: new module-levelsrc/kin/harness/persistence/journal.py::fork_session(src_path) -> (new_path, new_id)— reads the source journal's raw lines, verifies line 1 is atype: metarecord, rewritessession_id/createdon a copy of that dict, and copies every subsequent line byte-verbatim; noJournalinstance touched./forkcomposes it with the EXISTING/resumeswitch path (app._resume(new_id)) rather than adding aSessionmethod — fork switches you to the copy, the source journal is left frozen at the fork point,/resume gets back to it./editor: newsrc/kin/tui/external_editor.pyresolve_editor()($VISUAL>$EDITOR>vi,shlex.split) +edit_text(app, initial)(temp file →with app.suspend(): subprocess.run(...)→ read-back →os.unlink, returningNone— never raising — on a non-zero exit or a caughtSuspendNotSupported);kin.tui.commands.info._editorseeds it withapp.composer.textand replaces the composer text on success, deliberately with NO busy-guard (editing a draft touches no journal/harness state, unlike/export//fork). First contact withApp.suspend()in the codebase surfaced a real gotcha:LinuxDriver.can_suspendis unconditionallyTrueeven underrun_test()'s headless driver, so calling the real thing in a test wouldSIGSTOPthe pytest process — the test suite stubsapp.suspendinstead. **Ctrl+R history search (Step 5):** newsrc/kin/tui/widgets/menus.py::HistoryMenu, a thirdOptionListsibling ofSlashMenu/AtMenu— no separate query field, the composer's own live text IS the search query while the overlay is open (ComposerAssistMixin._update_history_menure-populates on every composer edit);Optionids are index strings (not the history text —OptionListraisesDuplicateIDon a repeated id, and non-consecutive duplicate submissions are legal), with a parallel_rows: list[str]forcurrent_value(). Resolved the binding collision the Step 4 write-up flagged:ctrl+rwas alreadyaction_toggle_reasoning— history search KEEPSctrl+r(readline muscle memory is the point of the feature) and the reasoning-block toggle moves toctrl+n, the first genuinely free chord after checkingctrl+t/ctrl+g(both already claimed inKinApp.BINDINGS),ctrl+p/ctrl+q(Textual's own command-palette/quit), and TextArea's ownctrl+a/e/d/w/x/c/v/u/k/z/yclaims while the composer holds focus.Composer.on_keyroutesup/down/ctrl+r(cycle-to-next-older) while the overlay is open, mirroring the existing_at_open/_slash_openblocks;enter(on_composer_submitted_history_accept) andescape(close, no change) are wired the same way those menus special-case the two priority-bound keys. Mutual exclusion with the slash/at menus holds in both directions — opening history closes them, andon_text_area_changedskips the slash/atpopulatecalls entirely (not just "no match") while history is open, so a/-shaped search string can never reopen the slash menu underneath it.docs/guide/keybindings.md, the F1 help overlay, and bothdocs/getting-started/walkthroughs updated for thectrl+r/ctrl+nswap in the same commit. Step 2: 5 new tests (tests/test_harness/test_export.py) + 3 additive round-trip tests intests/test_app/test_slash.py. Step 3: 4 new tests (tests/test_harness/test_fork.py) + 2 additive round-trip tests intests/test_app/test_slash.py. Step 4: 9 new tests (tests/test_app/test_external_editor.py) + 2 additive round-trip tests intests/test_app/test_slash.py+ one authorized sentinel bump (test_commands_builtins, 39→40). Step 5: 12 new tests (tests/test_app/test_history_menu.py) — 0 pre-existing tests referencedctrl+r/toggle_reasoning` at all, so no key-literal edit was needed anywhere. 0 edits to any pre-existing test body across all four steps beyond the one Step 4 sentinel bump.
  • 0073 — Nested/lazy AGENTS.md discovery (G7, Step 1 of the field-gap Wave B). Shipped. New agents_md.nested_guidance(file_path, workdir): symlink-safe, workdir-contained, walks workdir(exclusive)→file-dir(inclusive) collecting each level's AGENTS.md/CLAUDE.md-fallback (the same skip rule discover() uses at the root, reapplied per level) plus .claude/rules/*.md sorted by name, each capped at 50KiB via a shared _read_capped helper extracted from _read_section. loop._execute_one surfaces it after a successful, non-error READ/EDIT-kind tool call with a string path arg by appending ONE <system-reminder> block to the tool result — a first (no prior code appended a reminder to a tool_result; existing reminder traffic rides the user-turn channel) — deduped per session via Session._agents_md_seen (reset in clear_history). Chooses lazy-load-on-read (Claude Code style) over Codex's always-concatenate, argued from the repo's own Anthropic prompt-cache discipline. @-import expansion is cut with a named revisit trigger (a real file needing it to stay under the cap); the compaction interaction is named as an accepted v1 gap, not fixed. discover() and its 16 tests are untouched.
  • 0072 — Mid-run steering — a steer-file channel across the subprocess boundary (G4b, Step 7 of the field-gap Wave A). Design doc, no build; Status: Proposed. Proposes scheduler._child_env gaining KIN_STEER_FILE at spawn (mirroring the KIN_COMPACT_THRESHOLD/KIN_JOB_ID precedents already there); the operator's steer text (a Matrix reply or a new dashboard run-page field) is appended as JSONL; the child drains it ONLY at the round-boundary seam loop._run_turn_impl's per-run token-budget check already uses — boundary-only, never mid-tool-call. Failure modes named: orphaned steer on a finished child, unbounded latency during a long tool call (documented, not fixed — a per-tool-batch drain is a future-wave knob), multiple queued steers delivered in append order, and cancel-wins when a steer races DR 0071's kill switch. Alternatives weighed: defer, refuse, schedule-only (the zero-new-machinery option, worth reconsidering as v1), a stdin pipe (rejected — no live handle to a daemonized child), DB polling from the child (rejected on layering — the harness must never read Outpost's jobs.db). Security posture argues MEDIUM over HIGH (the sender is the sole L1/L2-gated operator; injection can only redirect within already-granted tool permissions, never escalate) with a full audit trail (audit.py + a new journal record type) as the compensating control; explicitly checks CONSTITUTION.md prohibitions #5 (no collaboration machinery) and #8 (no speculative tail) — neither is violated, since there is exactly one writer identity and one reader. Recommendation: build in a future wave, gated on this DR's acceptance and on Stop (DR 0071) getting real operator use first.
  • 0071 — The kill switch: one cancel-semantics core behind three doors (G4a, Step 6 of the field-gap Wave A). scheduler.cancel_row(rowid) (extracted from the v1-only scheduler.cancel) is now the ONE process-kill path (§8.3's pid-safety invariant) for the v1 machine door, the dashboard's new POST /api/jobs/{id}/cancel, and the Matrix stop <job_id> verb. New v1_jobs.cancel_job_core(job) -> (outcome, cancelled_at) owns the marker-then-kill ordering + a v1-only gate on the durable v1_cancelled_at marker/webhook (dashboard cron jobs and matrix:-placeholder rows get the kill + run-status path only). Live/halted/pending classification deliberately excludes a healthy recurring cron job idle between fires from "pending" — Stop with nothing in flight 409s rather than silently disabling the schedule, preserving "pause disables the schedule; stop kills the run." v1_jobs._cancel's HTTP shell (scoping, 404/409/202, problem+json) is byte-compatible — the untouched v1 cancel test suite is the proof. Stop button on the run-detail page mirrors DR 0064's confirmModaljpost → toast pattern; fragments.py's _CONFIRM_JS split into a portable half + a new _HISTORY_MODAL_JS (landing-page-local) so the run page doesn't ship dead history-modal code. Post-implementation adversarial review found + fixed a MEDIUM gap: a RECURRING job halted on needs_human stays enabled with a real future next_run_at (only a one-shot self-disables), so the initial cut silently killed its schedule via cancel_row's not-live disable branch — a new skip_disable carve-out abandons the halted run without touching the schedule. One pre-existing sentinel test (test_all_verbs_constant_matches_grammar) extended by planner authorization to include stop — every other touched test file's diff is purely additive.
  • 0070 — Rejected: no user-configurable lifecycle hooks (G3, Step 5 of the field-gap Wave A). The field ships real hook surfaces (Claude Code's ~30-event matrix, OpenCode's arg-rewriting plugin intercepts, Codex's hooks config schema); kin's own absence of one was previously undocumented. The field's own justification doesn't transfer: hooks exist so users who can't edit the harness can extend it, but here the AI peer edits src/kin/harness/ directly — the extension seam already IS the source (the mycelium principle). A PreToolUse-style interceptor would also be a second, unaudited policy engine alongside the real one (modes + permissions.py + sandbox), and every hook event freezes the loop's internals as a compatibility contract for a single-operator system with no plugin authors to serve. The three concrete field use-cases are already covered natively: policy → modes/permissions/settings, alerting → DR 0068's term_notify.py (shipped this same wave, 04fa1ce), extension → skills/commands/agent profiles/MCP. Named, not open-ended, revisit triggers: kin distributed to users who don't edit the harness; a policy need that must survive upgrades without a repo edit; Outpost-side per-job policy settings.toml/the scheduler can't express. AGENTS.md's Conventions block carries a one-paragraph callout in the same commit so the absence reads as decision, not oversight.
  • 0069 — Guided-decoding probe + scheduler compact default (G6.1 + G6.3, Step 4 of the field-gap Wave A). scripts/probe_guided_decoding.py ran ~10 trials of a nested, enum-and-required-heavy schema through kin's own force_tool_call on BOTH wires against the live kin-one vLLM fleet: 10/10 first-try schema-valid on both the Anthropic bridge and the OpenAI wire (forced named tool_choice is already grammar-constrained). Layering response_format={"type":"json_schema",...} on top adds nothing (still 10/10); layering extra_body.structured_outputs on top hard-400s every trial ("You can only either use constraints for structured outputs or tools, not both") — reconfirming research/probe_strict_decoding.py's 2026-07-03 raw-HTTP finding through the actual backend code this time. Result: zero change to force_tool_call's happy path on either backend. The one real gap is Tier 2 — the tool-less prompt-JSON re-ask (session/subagents.py::_structure_via_prompt), the only place a schema violation can still occur. New Backend.structure_via_prompt(messages, system, schema) -> str | None (backends/base.py, default-None extension point mirroring count_tokens's pattern) — every backend but one falls through to the unchanged generic create_turn re-ask. OpenAIBackend.structure_via_prompt is the sole override: when self._base_url is set (never the default api.openai.com path), attaches a per-request top-level response_format to a direct non-streaming call (the probe-validated, conflict-free shape); self._extra_body is read, never mutated; a 400 retries once with the param dropped. structured_outputs is used nowhere. G6.3 (unconditional): container/dashboard/scheduler.py::_child_env now env.setdefault("KIN_COMPACT_THRESHOLD", "0.85") beside TZ — a scheduled child has no human to answer a context-overflow prompt, so it now auto-compacts at 85% instead of dying at the window ceiling; setdefault so an operator override always wins; interactive default unchanged. 4 new tests (3 backend, 1 scheduler), 0 edits to pre-existing tests.
  • 0068 — Terminal bell / OSC 9 walk-away notification (G5, Step 3 of the field-gap Wave A). New src/kin/harness/term_notify.py (stdlib-only): enabled(workdir) mirrors env.py::_disabled's layering (KIN_NOTIFY/notify, env presence wins, default ON); sequence(kind) composes a bare BEL always plus an OSC 9 payload (\x1b]9;<msg>\x07) when TERM_PROGRAM{ghostty, iTerm.app, WezTerm} or TERM == xterm-kitty — all four web-verified OSC-9-capable 2026-07. Message bodies are STATIC STRINGS ONLY from a fixed 4-entry table (kind is an enum key, never free text — the hard invariant), belt-and-braces routed through titles._clean. TUI: new KinApp._notify_terminal mirrors _set_terminal_title exactly (driver-None guard, bare try/except); fires turn_complete on done only when the turn ran >= 15s (a new _turn_started_at monotonic stamp, single writer turn_lifecycle._send beside harness_busy = True), and needs_input on the five needs-input branches (approval_request/question/plan_ready/proposal_ready/sandbox_override_proposed); interrupted deliberately fires nothing (the user is present). 10s debounce; new on_app_focus/on_app_blur handlers set a _focus_capable LATCH (flips true on the first focus event ever seen, never reset) — suppression only engages once the terminal has PROVEN it reports focus, so a terminal that never sends focus events can't silently swallow every notification forever. Headless: run_headless writes term_notify.sequence(...) straight to sys.stderr at process completion when enabled() AND sys.stderr.isatty() — the one exception to the module's UI-free/print-free contract, and the only path that makes kin -p from another shell/tab meaningful. notify added to settings/types.py::_KNOWN_TOP_LEVEL_KEYS (project-ok, not model-writable). 30 new tests, 0 edits to pre-existing tests.
  • 0067 — File snapshots-before-write + /rewind file restore (G2, Step 2 of the field-gap Wave A). New src/kin/harness/snapshots.py (stdlib + git subprocess, git-workdir-only): a harness-side re-implementation of the Outpost's DR 0030 temp-index git commit-tree recipe under refs/kin-snapshots/<session_id>/<ts_ns>-<hex> — never touches the real index/HEAD/worktree; FIFO-prunes to 20 refs/session, sorted by ref name (nanosecond-resolution) rather than git's 1s-resolution commit timestamps. loop._maybe_snapshot hooks _execute_one: the first EDIT-kind call each turn captures a pre-write checkpoint (idempotent via sess._turn_snapshotted, reset per-session at every _run_turn_impl, not root-only); toggle KIN_SNAPSHOTS/snapshots (default ON). Journal gains snapshot/files_restored records (verified replay.load tolerates unknown types before landing — no version bump). New HistoryOps.rewind_files/rewind_both async methods (existing sync rewind untouched): files restores on-disk state to the checkpoint before the cut turn without touching the conversation; both restores files FIRST and only truncates the conversation on success (files-first, abort-before-truncate atomicity). /rewind [n] [conversation|files|both] — bare /rewind//rewind <n> stays byte-identical (0 edits to existing rewind tests); files/both always confirm, even at n=1. run_code/shell writes remain uncovered (the same universal ceiling DR 0066 names); v1 is git-only, no tar fallback. 15 new tests, 0 edits to pre-existing tests.
  • 0066 — Diagnostics-after-edit (G1.1, Step 1 of the field-gap Wave A). New src/kin/harness/tools/diagnostics.py (stdlib-only): write_file/edit_file append a lint pass's output to their result on a nonzero exit — v1 covers exactly .py via ruff check --no-cache --output-format concise, plus an opt-in ty pass behind its own toggle (diagnostics_ty/KIN_DIAGNOSTICS_TY, default OFF). Subprocess posture mirrors tools/shell.py::_spawn's branch exactly (sandbox-wrapped exec under auto mode, unwrapped otherwise, start_new_session=True, reuses shell._drain_to_completion for the 10s timeout + guaranteed reap) — running the linter looser than shell would weaken the sandbox posture. Every failure mode (toggle off via diagnostics_after_edit/KIN_DIAGNOSTICS default ON, unsupported extension, shutil.which miss, spawn/timeout error) swallows to "" — diagnostics can never fail or delay-block a write. Output capped at 20 lines/2000 chars; a clean pass appends nothing. A subprocess pass, not an embedded LSP — the DR quotes OpenCode's own warning that LSP servers "get out of sync, use significant memory, slow down agent workflows." 4 new tests, 0 edits to pre-existing tests.
  • 0065 — Deep-link targets for run_report and alert taps (Step 4 of the Outpost mobile control-plane push-coverage wave). The push lane completes as a control plane: a run_report tap lands on /runs/<job>/<run> (the server-rendered run-detail page; app._run_view already serves it with 303-to-/ on any bad id, no server change); an alert tap lands on /#card-jobs (the existing _applyHash Jobs-view vocabulary). SW-side target grammar: targetFor(data) constructs paths from validated numeric fields on data only — inbox(id)/inbox/<id>, run_report(job,run)/runs/<job>/<run>, alert/#card-jobs, unknown/missing → /. Display-sink-adjacent invariant: wire-supplied URL strings are NEVER navigated to (both ends construct from ints, never a wire-string concatenation). New SW → page message shape: {type:"nav", kind, id, job, run} (the inbox {type:"inbox-nav", id} path stays byte-compatible with the deployed landing page during the deploy window). _storePendingNav extends the persisted Cache-API shape to {kind, id, job, run, ts} so the iOS killed/suspend-race _navCheck resume covers all three kinds. The accepted iOS killed-PWA tap-through limitation is NOT a target (the Cache-API machinery is reused for the new kinds because it is correct where notificationclick DOES fire). RE-1 (matrix-fabric) named as new lived input — a clean run_report deep-link is a data point for "does the re-confirm tap feel like safety or friction?"; the call is recorded in Step 5's close-out after the phone QA pass B.
  • 0064 — One-tap approve/dismiss for medium-risk inbox items (Step 2 of the Outpost mobile control-plane push-coverage wave). The push wire becomes a third one-tap answer surface (alongside the dashboard action bar and the Matrix approve/reject/revise verbs), gated by the existing r19 chat/gate.py::classify_risk — medium-risk items ({question} / {elicitation} kinds) carry Approve / Dismiss buttons on the lock-screen notification; high-risk items keep the r20 hold+confirm surface untouched. Server-side risk re-check (POST /api/inbox/{id}/push-action in inbox_api.py, registered beside _answer/_dismiss/_high_risk_confirm) is the truth — the envelope is just the wire-level gate. D8 canonical answer strings (the canonical "approved" lands on the row verbatim, provenance in via="push-action" + push_approve/push_dismiss audit rows). CSRF hand-off: landing JS posts {type:"csrf", token} to the active SW after navigator.serviceWorker.ready; the SW persists in the Cache API (kin-nav under /csrf-token). Equivalent exposure rationale (the token is already same-origin-script-readable via the meta tag; Cache API access requires the same capability) documented in DR + REFERENCE. SW fallback discipline: any failure (missing cached token, network error, non-2xx incl. 403/409/302) falls back to the existing deep-link path — no retry loop, no false success. _high_risk_confirm's notification path gets the _ALERT_TIMEOUT cap (asyncio.wait_for(delivery.send, scheduler._ALERT_TIMEOUT)), closing the bare-delivery.send anomaly Step 1 flagged. RE-3 named as new lived input (does the free-text grandfather still feel right now that one-tap approve exists? — the lived answer comes from the Step 5 phone QA pass A).
  • 0062 — Dip-in transcript modal (Step 7 of the subagent-retention wave; resolves the label collision on Step 7's commit message, which cites "DR 0061" — the retry DR, not this one). New TranscriptModal(ModalScreen[None]) in src/kin/tui/modals/transcript.py mounts build_replay_widgets (REUSED, not forked — the only transcript builder, the same one /resume / --continue / /rewind use) into a fresh VerticalScroll, pushed on TOP of the AgentPanel (3-deep modal stack: app → AgentPanelTranscriptModal — the ReadOnlyModal push already establishes the modal-on-modal pattern). Modal-local _Shim exposes only _kin_labeled / _kin_lead (the entire build_replay_widgets app-surface contract, verified by reading the builder signature). AgentPanel key split: Enter (priority=True wins over OptionList's select) opens the rich view for agent rows; o stays the raw buffered output. Esc/q dismisses back to the panel. Read-only by design — no input path into the child (messaging is the MCA's, via agent_message); the modal also does NOT live-tail (synchronous snapshot of child_session.messages at open = consistent point-in-time view, works for running + idle + closed rows alike because messages survives aclose). Empirical first-check PASSED: this is the first caller of build_replay_widgets output outside the live #transcript container — pilot test asserts UserMessage + AssistantMessage + ToolCall actually appear inside the modal's scroll (no blank / collapsed / overflow). v1 display gap, accepted: an open modal does not pause the live turn (the user can re-Open-Again to refresh).
  • 0061 — Headless retry posture (Step 5 of the subagent-retention wave). Session.headless_retry: bool plain __init__ attr (default False; spawn_child sets it True alongside child.planning propagation). loop._run_model's retry gate becomes (sess.headless_retry or not (had_text or had_reasoning)) and _is_transient(exc) and attempt < _MAX_RETRIES — the root's "never re-stream into the live UI" posture is LOCKED (the had_text half keeps it from re-streaming partial); a child's headless_retry=True opens the mid-stream retry path. New events.stream_retry(discard_chars=N) event: emitted BEFORE the retry's sleep + continue; ForwardingEmit.__call__ handles it by trimming N chars off the tail of its accumulated _text (join → slice → single-element list, the dedup primitive). Reasoning needs no handling (ForwardingEmit drops reasoning). On exhausted retries the existing _BackendErrordone("error") path runs unchanged; with Steps 3 + 4 + 6 the child parks, the parent gets the structured partial + the resume hint, and agent_message resumes the conversation. Steps 5 + 3 + 4 + 6 COMPOSE: retry → exhausted retries → done_reason="error" → parked child → structured partial with resume hint → resume.
  • 0060agent_message — coordinator-only resume on a parked subagent (Step 6 of the subagent-retention wave). New agent_message(agent_id, message, run_in_background?) tool sitting alongside agent_list / agent_output / agent_kill: send the next instruction to an IDLE child (a running agent cannot be messaged — wait with agent_output(wait=true) for it to finish). Foreground returns only the NEW turn's prose (via new ForwardingEmit.result_since(mark) helper, the cap + truncated flag mirror the existing result(); the same _shape_child_result shapes both paths); bg returns resumed agent<N> (<profile>) in background and produces a fresh subagent_completed reminder on the next depth-0 turn. The synchronous LATCH RESETS (status / finished_at / error / background / turns += 1, then ForwardingEmit._completion_fired/done_reason/_on_completion reset, then seen_completion_ids.discard) land BEFORE any await — so two parallel agent_message calls on the same idle row can't both grab it (the second sees status=running and refuses). _make_completion_hook(root, agent_id, profile) in session/subagents.py is the bg completion-reminder hook EXTRACTED from dispatch_subagent's inline closure so the bg RESUME path can reuse it (no fork-and-drift). Step 4's failure strings carry the new agent_message("<id>", ...) resume hint so a failed child hands the model a recovery path. Depth invariant: resume does NOT change the child's depth; a running resumed child refuses further messages (any await-chain target must be IDLE). v1 display gap (noted): prompt_tokens/completion_tokens show the most recent turn's usage, not a cumulative total (last-event-wins overwrite). Cross-restart resume (rehydrate from journal) deferred.
  • 0059 — Subagent failure semantics (Step 4 of the subagent-retention wave; kills the silent-partial family). ForwardingEmit now captures done_reason for fg + bg children (the fg path used to drop the done event entirely, so a wire death mid-stream had done_reason=None and the parent received the partial prose as the complete answer with no marker). New _shape_child_result(row, fwd, prose) in session/subagents.py branches on done_reason (stop / error / turn_cap / loop_detected / truncated) producing structured id-bearing failure strings. The error: prefix IS the cross-backend is_error mechanism (_finalize auto-detects it; Anthropic wire carries it; OpenAI wire has only the text) — NO typed channel added. ForwardingEmit.truncated: bool replaces string-sniffing for the 32KB cap (id-bearing hint appended by the shaper from the flag). run_subagent's row status mirrors the bg path on done_reason (a wire death is status=error, not ok).
  • 0058 — Subagent retention (Step 3 of the subagent-retention wave, the keystone). Every child gets a journal (spawn_child mints parent_session_id + agent_profile meta); _run_child_turn persists turns; finished children PARK idle (success + crash branches no longer aclose); AgentRegistry.close_all REPLACES kill_all (cancels running rows THEN acloses every retained-idle child — closing the pre-existing finished-bg leak where a parked child's lazy kernel/browser/bg-shells orphaned at app exit); dismiss closes too. THREE enumeration surfaces filter child journals (list_sessions/latest_session + the dashboard _resolve_session_id_sync workdir scan). Ghost-journal guard UNCHANGED (verified).
  • 0057 — Unified agent registry (Step 1 of the subagent-retention wave). BgSubagents/BgAgent/sess.bg_subagentsAgentRegistry/AgentRecord/sess.agents; ONE id space shared by fg + bg children. spawn_child is the single construction seam (keyword-only on_completion); dispatch_subagent's wiring dup is gone. run_subagent registers fg children + wraps the child turn in its own asyncio.Task so agent_kill cancels the child WITHOUT cancelling the parent (the t.cancelled() + current_task().cancelling() disambiguation). Kill-authority rule: kill only declares killed when the cancel actually landed.
  • 0056 — Remove the redundant TodoList header pulse (Step 0 of the subagent-retention wave). The TodoList pulse drove off app-level harness_busy (the one surface with no local liveness), making it a strict subset of the StatusBar shimmer; removed. Two pulse surfaces remain (ToolCall agent title, BgAgentsBar header); pulse_glyph + its package re-export stay.
  • 0055 — The code-audit gate (symbols-not-line-numbers + move-tombstone class check). New task ship gate (scripts/code_audit.py, stdlib-only) with two checks: (1) forbid file:line citations in non-test code comments/docstrings (the dominant rot vector in a repo whose main change type is the move), (2) flag duplicate module-level class definitions inside a package (the move-tombstone shape — a refactor leaves the class behind in two modules). Both guards mutation-proven: tombstone test re-creates NeedsHumanError in a sibling and the gate lights up naming both modules; spy test reverts reasoning.py's package-namespace import and the related test fails. One _EXEMPT entry (at_mentions.py parser-grammar example) — the only documented file-path-mention use case. The wave's 78 numbered citations across 23 code files were converted to symbol refs in the same commit (comment/docstring-only, 0 runtime change; factory.py SDK cite restated from the wrong retry-logic to the real _client auth-header props — a C2 claim-drift fix). testing.md ship-table repaired.
  • 0054 — Move landing composer to templates/landing.py (Step 6 of the 2026-07-08 dashboard mega-templates wave).
  • 0053 — Move run-detail page + _shell to templates/run_page.py (Step 5 of the 2026-07-08 dashboard mega-templates wave).
  • 0052 — Move landing