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.mdis a one-line@AGENTS.mdinclude 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 (intask 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 theharness/spans.pyJSONL span sink. See the Evals & observability guide.tests/test_web.py— web tools (needs the optionalhttpx/trafilaturadeps).tests/test_pty.py— out-of-process PTY smoke (non-defaultptydep group; spawns a realkinunder a pseudo-terminal).tests/test_snapshot.py+snapshots/— hand-rolled snapshot guard (6 baselines, byte-stable). See the snapshot discipline insnapshots/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 viatask verify-perf(or--quickfor 200+500 only); OUT oftask verifybecause absolute numbers are machine-specific, the scaling factor is what to compare across machines. The numbers pick between Tier 1 (pre-bakedMarkdownstrips) / Tier 2 (Static(rich.console.Group)per msg) / Tier 3 (customTranscriptScroll) of the transcript-virtualization rock, or the Skip outcome. The current decision (Skip) is recorded inSTATUS.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 realKinAppdriven through the production@workturn worker. 11 scenarios (the v3 §1-3 + §4-7 surface plus cross-wire,/security-review, and a CI-safedry_run);--allwritessnapshots/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 pinnedsnapshots/live_baseline.json(--update-baselinere-pins;--passk Kreports the stochastic pass^k signal — see the Evals guide). Run viatask live-drive-all(the pre-promote check) ortask live-drive-dry(offline smoke). NOT intask verify(needs a reachable model endpoint). Per-scenario screenshots land atsnapshots/live_<scenario>_<ts>.svg(gitignored — per-run dev artifacts).
Gotchas and invariants¶
REFERENCE.md— every gotcha, footgun, security invariant, and postmortem. The tieredCLAUDE.mdfiles 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 isgit 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 runningkin;kin doctorverifies any install (three chunks exactly — install: uv/kin/git/PATH withtaskinfo-only; config: settings parse + a provider walk mirroringmake_backend's chains that names the winning endpoint/key source; network: GET{base_url}/modelswhere ANY HTTP response counts as reachable,--offlineskips), andkin --version(argparseaction="version"offkin.__version__) is the installer's success gate. Serving rides the outpost compose under apublishprofile (killed: the scp'd mini-compose; offered-not-chosen: Forgejo): twonginx:alpinestatics —get:7018 (page + scripts + wheel + sha256s fromcontainer/installer-www/, assembled byoutpost:_deployat deploy time, synced-by-construction) anddocs:7019 (the mkdocssite/— 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 becausekinra-secure-headers' nosniff + busybox's MIME table would block the docs CSS. Wheel quick-path: uv REJECTSkin @ …/kin-latest.whl(filename needs a python tag) so the deploy publishes the real PEP 427 wheel + a static PEP 503simple/kin/index — the version-stable one-liner isuv tool install kin --index https://get.kinra.ai/simple/(accepted: kin's built code is publicly downloadable from a private repo;kin-latest.whlstays as a curl/sha256 alias).install.sh=uv sync+uv tool install --editable, NOTtask 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,--checkdry-walk, main-at-EOF truncation guard, fresh-Opus adversarial pass pre-ship.outpost-install.shstages five empty 600-mode secret stubs and forks on the three REQUIRED oauth secrets (optional pair self-heals empty, matchingoutpost:_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 viastrip_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" viapaths_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_defaultstops the MRO walk'sTextArea._on_pastere-insert;stophalts the DOM bubble before the App'son_eventPaste-branch re-forwards the bubbled event back toself.focused); >10 files warns and inserts verbatim (silent partial attach is worse than a visible choice); oversize images (>5MB) auto-downscale via the macOSsips -Z 1568builtin (_decide_downscaleis the pure decision,run_sips_downscaleis the live subprocess,Noneon any failure — caller falls back to mentioning the original). Mention-tokenize viaat_mentions.mention_token; dedupe vsself.text; insert viaTextArea._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_pathtoken → warninglooks 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 viabool_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 — fullpaths_from_pastematrix + 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'serrors="replace"as a wall of replacement chars. Two thin extractors in_media.pymirroringextract_pdf_text(str | bytesin, normalizederror:out, caller owns the cap):extract_docx_textwalksiter_inner_content()(paragraphs + Markdown tables, document order; never touchesword/vbaProject.bin— the macro guarantee) andextract_xlsx_textrenders each sheet as a## title+ Markdown table (read_only/data_only, capped 10 sheets × 100 rows × 30 cols).read_file+at_mentions._read_mentiondispatch.docx/.docmand.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 guard —defusedxmllazy-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 (likepypdf), 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.markitdownrejected; 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 chokepointKinApp._request_quitthat 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 flipself._quitting = TrueSYNC (kills double-press race) and start@work(group="quit", exclusive=True)_run_quit. The worker cancelsagent_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), flipsstatus.busy_label = "curating memory… · ctrl+c to skip"(thebusy_labelreactive wins over a stale label from the cancelled turn), thenasyncio.wait_for(reflect_quietly(...), timeout=20.0).finally: self.exit()runs onCancelledErrortoo. Stomp-guard set keeps the cancelled turn from racing the curator:TurnLifecycleMixin._send/_end_turnandSlashCommandsMixin._run_compact's finally all readself._quitting;KinApp.action_interruptshort-circuits through_request_quiton 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 bynot self._quittingto prevent a double curation (everything else byte-identical). Blocking modals parked onpush_screen_waitduring the window are left alone (same disposition as today's/exit); non-send slash commands remain typable (accepted). 7 new tests intests/test_app/test_quit.py; 0 edits to the 51 pre-existingtest_unmount.py/test_slash.pytests (every existing one lands on the instant-exit branch —FakeHarness._turnsis 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_ONLYmemory_diruntouched.createstampsos.path.realpath(workdir)into aworkspace:frontmatter line onproject-factandepisodic(NOTpreference— always global by design; explicitworkspace: globalhonored verbatim; str_replace/insert deliberately do NOT re-normalize — explicit edit is intent). Read seams:MemoryIndex.recallgainsAND (f.workspace = '' OR f.workspace = ?), filter lands BEFORE the 20-candidate pool so the Python re-rank is untouched;store.index_blockpartitions 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_normalizecollapses missing/empty/globalto""). Schema:mem_files.workspace TEXT NOT NULL DEFAULT ''added on aSCHEMA_VERSION = 2migration (PRAGMA user_version != 2→ DROP + recreate + set version; NOT inmem_ftsso theai/ad/autriggers stay byte-identical and the stamp is never tokenized). Dashboard search (memory_api._searchworkdir=None) stays unfiltered — falsy workdir is the operator's deliberate escape hatch. 7 new tests intests/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-profileskillgrants (prompt-polish deferred follow-ups wave, Step 6). Shipped. The bundleddefaults/skills/catalog sits directly afterSYSTEM_PROMPTas 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 theskilltool itself (a read-only prompt loader, no side effects) to a closed allow-list. Two profiles earnskill:coder(open-ended domain work where build conventions matter mid-task) andresearcher(open-ended web research where research recipes matter mid-task). Five profiles explicitly DO NOT earnskill: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 withgeneral's "all tools available" framing. Frontmatter-only change:coder.md+researcher.mdgain- skillat the end oftools:;SUBAGENT_PREAMBLEuntouched; spawn pops (workflow/write_plan/ask) untouched. Accepted residual: a skill-less child sees the catalog and may attempt askillcall → one tool-not-found error turn (aKIN_STRICT_SKILLS=1-compatible cleanerror:reply the model can adapt to); revisit trigger = observed repeatedskill-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_profilepins 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 identicalweb_fetch → summarizeflow 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 atool_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-levelpropertieskeys (with(required)markers from the JSON-Schemarequiredarray). 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: emptyproperties, non-dict values, non-string names, non-listrequired— all degrade to "no note, prompt unchanged". 4 new tests intests/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— Liveplanningwalk-up (prompt-polish deferred follow-ups wave, Step 1). Shipped.Session.planningis now a root-resolving property — getter returns(self._blocker or self)._planning, setter routes to the root too;_blockeris pre-flattened to the root at construction (spawn_childpassesblocker=self._blocker or self), so the lookup is one hop, never a walk._planningreplacesplanninginSession.__init__; thechild.planning = self.planningline inspawn_childis DELETED (the snapshot was DR 0081's named revisit trigger — the mid-freeze bypass is now closed by construction).enter_planning/exit_planningbodies unchanged;exit_planningdocstring gained one sentence noting it lifts the ROOT's freeze. ~19 baresess.planning = True/Falsetest 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'stest_write_plan_root_walk_plan_pathhalf unchanged,planninghalf rewritten) + one new test (test_planning_live_root_propagationpins the five-part invariant: enter-mid-tree, exit-on-root, grandchild depth-2, setter routing, root-only session). Mandatory fresh-Opus adversarial review appended toresearch/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.runningis 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 readspollinginstead 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.2sstate-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 transcripteof(state badge/Finished/Stophidden-toggle/"report ready — reload to view"); coarse 45s rel-time re-tick + absolute-time hover everywhere;● N need you — Outpost/▸ running — Outpostbrowser-tab title consumes the SAME servertabspayload asapplyTabs(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_classin run_page.py ↔STATE_LABELS/STATE_CLASSin the transcript IIFE ↔jobStatusBadge/_dotClassin landing_js.py, all cross-cited); Graphite+ tokens single-sourced intemplates/theme.py(the run page's drifted.badgefork was the symptom); basebuttonis margin-neutral (spacing is a container concern — themargin-top: 0undo 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_errorsfor 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-onlyevents.text("system", …)notes — the model's transcript just ended; each site now setsSession._pending_stop_note(token_budget writes through the_blockerwalk-up), drained byloop._preprocess_root_turninto anenv.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 sentenceswrite_plan/present_plandescriptions 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_messagedrop the duplicated wire-internals paragraphs (schemas re-send every round);tasks→todosbackref;task.prompttells the dispatcher to name date/branch inline (children get no ambient env);workflowstates its caps by f-string interpolation ofWORKFLOW_MAX_PARALLEL/WORKFLOW_MAX_AGENTS. (4)todosreturns the authoritative rendered checklist ([x]/[>]/[ ], plain text) instead of a bare count — the tool normalizes silently, so a count alone hid drift; mirrorstasks's snapshot contract. (5) Kin-voiced summarizer —compaction.SUMMARY_SYSTEMis 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 addserror:). 4 new tests; the sentinel passed unedited.0081— Subagent dispatch contract (prompt-polish wave, Rock 1). Shipped. One sharedSUBAGENT_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 bySubagentOps.spawn_childBETWEEN the parent's composed system and the profile body, so the parent's cached prefix stays byte-identical (Anthropiccache_control+ vLLM radix prefix sharing survive; mixed-profile workflow fan-outs share strictly MORE prefix than before since divergence now starts at the persona).askis popped from EVERY spawned child, unconditionally, next to the existingworkflowpop — the pop makes the preamble's "no user to ask" true and closes the real footgun (a workflow-spawnedgeneralchild parking the whole run on a question modal untilWORKFLOW_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) andSession.from_profile(top-level launch) deliberately skip the preamble. Rejected: recursivetaskforplanner—child.planningpropagation is a spawn-time SNAPSHOT, not a live root walk-up, so a planner grandchild would inherit a staleFalseand edit freely mid-plan; revisit trigger:sess.planningbecoming a live walk-up property (REFERENCE.md § "Plan lifecycle & the planning freeze"). One authorized test edit (the empty-profile exact-equality assertion intest_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·,@ fileseverywhere — BANNER_SPEC mocks updated in lockstep);/helpdocuments only bindings that exist (the falseF1 / ?claim removed, double-esc rewind + ctrl+c + deny-with-note added;McpManagerModaladvertises its reale/xaliases); toast governance as a call-site convention (busy refusals = one template + warning/3s, echoes = default/2-3s, failures = error/5s,markup=Falsemandatory on dynamic messages) — deliberately NOT a wrapper helper; deadnoticeparam removed fromrender_banner/WelcomePanel.show.0079— Approval modal: native bodies + honest choice architecture + deny-with-note (polish wave, Rocks B1–B5).events.approval_requestgains additivekind/args/diff; the pre-approval diff is computed by pure helpers sharing the tools' own match path (edit_file._resolve_editfactored so preview and enforcement can never disagree;roundtrip._edit_previewfail-soft to ""). Per-kind bodies (edit = the real diff; shell =KinSyntaxStyle-highlighted command; mcp = server › tool + provenance note; network = labeled rows, warning only on truthyforce); 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_approvalreturns(allowed, deny_note). ConfirmModal focuses Cancel (destructive confirms are always an explicity).0078— Spinner vibes + "one heartbeat, colour elsewhere" (POLISH r11; polish wave, Rocks A1–A4). Curatedtheme.SPINNER_STYLESvibe set (bloom default / braille / arc / toggle / line; constant cell-width frames;resolve_spinnerhandles unknown names + the KIN_ASCII →lineoverride) + the pureshimmer.spinner_frame(elapsed-monotonic indexing — no per-style timers; reduced motion returns the literalG_RUNNINGrest). 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:spinnerkey (project-safe, NOT model-writable) /KIN_SPINNER//spinnerpicker with live preview; persistence via the/costhuman-typed shape. Same bar:format_durationunification (Reasoning tail aligned to ≥2s), dimesc interruptsat ≥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_providerlive cross-provider swap, per-wire effort vocab insession/controls.py) — but not built, because the preconditions honestly aren't met: (a) DR0069's guided-decoding probe found forcedtool_choicegrammar-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, DR0070'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./modelstays 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 DR0070'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 DR0072'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::Journalis 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 DR0072's own "DB polling from the child" alternative was rejected for. Prohibition #6/#7 framing per DR0070: 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(DR0074, sequential, one writer at a time) for continuing elsewhere,/export(DR0074) for a static keepsake, DR0028's read-only live-tail pane for watching remotely. The Outpost keeps its own sessions — a difference in location, not surface (DR0033). Named revisit triggers: kin distributed beyond single-operator (DR0070'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 — noTool, noCommandDef, noHANDLER_KINDSentry, noCOMMANDS.register(...)—src/kin/tui/commands/passthrough.pyis never imported by the tool registry, the harness command registry, or the TUI family-import list; the ONLY caller anywhere isKinApp._run_shell_passthrough, reached only from a newon_composer_submittedbranch (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_shellwith full user env, no scrub, no sandbox — the deliberate opposite ofask_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); reusesshell._drain_to_completion(a third caller of the SIGTERM→SIGKILL discipline) andshell._truncatefor one 16KiB cap (passthrough.MAX_OUTPUT) shared by all three sinks. Sinks: (1) transcript block viapassthrough.render_block— literalrich.text.Text, no markup; (2) new unconditional journal audit recordJournal.append_shell_passthrough({type: "shell_passthrough", command, exit, output, ts}— unrecognized type is a structural no-op toreplay.load'selif-with-no-elsechain and never reachesreplay_events, no version bump, DR 0067'ssnapshot/files_restoredprecedent); (3) next-turn<system-reminder>via newSession.inject_shell_passthrough_reminder, called fromloop._preprocess_root_turnbesideinject_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 throughenv._defangso a hostile command's OUTPUT can't forge a</system-reminder>close tag and smuggle instructions.Sessiongains two public methods;record_shell_passthroughmirrored additively ontoFakeHarness(planner-authorized),inject_shell_passthrough_reminderis not (only reachable via the realloop.run_turn). REFERENCE.md gains § "Shell passthrough" (the full re-verifiable invariant list);docs/guide/keybindings.mddocuments 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: newsrc/kin/harness/persistence/export.py::to_markdown(journal_path)— pure, noSessiondependency, builds onpersistence.replay.load(the meta header) + the EXISTING cross-providerreplay_eventsstream (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-.md in 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/saveNext-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,/resumegets back to it./editor: newsrc/kin/tui/external_editor.py—resolve_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. Newagents_md.nested_guidance(file_path, workdir): symlink-safe, workdir-contained, walks workdir(exclusive)→file-dir(inclusive) collecting each level'sAGENTS.md/CLAUDE.md-fallback (the same skip rulediscover()uses at the root, reapplied per level) plus.claude/rules/*.mdsorted by name, each capped at 50KiB via a shared_read_cappedhelper extracted from_read_section.loop._execute_onesurfaces it after a successful, non-error READ/EDIT-kind tool call with a stringpatharg 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 viaSession._agents_md_seen(reset inclear_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. Proposesscheduler._child_envgainingKIN_STEER_FILEat spawn (mirroring theKIN_COMPACT_THRESHOLD/KIN_JOB_IDprecedents 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 seamloop._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'sjobs.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 checksCONSTITUTION.mdprohibitions #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-onlyscheduler.cancel) is now the ONE process-kill path (§8.3's pid-safety invariant) for the v1 machine door, the dashboard's newPOST /api/jobs/{id}/cancel, and the Matrixstop <job_id>verb. Newv1_jobs.cancel_job_core(job) -> (outcome, cancelled_at)owns the marker-then-kill ordering + a v1-only gate on the durablev1_cancelled_atmarker/webhook (dashboard cron jobs andmatrix:-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'sconfirmModal→jpost→ toast pattern;fragments.py's_CONFIRM_JSsplit 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 onneeds_humanstaysenabledwith a real futurenext_run_at(only a one-shot self-disables), so the initial cut silently killed its schedule viacancel_row's not-live disable branch — a newskip_disablecarve-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 includestop— 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 editssrc/kin/harness/directly — the extension seam already IS the source (the mycelium principle). APreToolUse-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 → DR0068'sterm_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 policysettings.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.pyran ~10 trials of a nested, enum-and-required-heavy schema through kin's ownforce_tool_callon 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 namedtool_choiceis already grammar-constrained). Layeringresponse_format={"type":"json_schema",...}on top adds nothing (still 10/10); layeringextra_body.structured_outputson top hard-400s every trial ("You can only either use constraints for structured outputs or tools, not both") — reconfirmingresearch/probe_strict_decoding.py's 2026-07-03 raw-HTTP finding through the actual backend code this time. Result: zero change toforce_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. NewBackend.structure_via_prompt(messages, system, schema) -> str | None(backends/base.py, default-None extension point mirroringcount_tokens's pattern) — every backend but one falls through to the unchanged genericcreate_turnre-ask.OpenAIBackend.structure_via_promptis the sole override: whenself._base_urlis set (never the default api.openai.com path), attaches a per-request top-levelresponse_formatto a direct non-streaming call (the probe-validated, conflict-free shape);self._extra_bodyis read, never mutated; a 400 retries once with the param dropped.structured_outputsis used nowhere. G6.3 (unconditional):container/dashboard/scheduler.py::_child_envnowenv.setdefault("KIN_COMPACT_THRESHOLD", "0.85")besideTZ— 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;setdefaultso 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). Newsrc/kin/harness/term_notify.py(stdlib-only):enabled(workdir)mirrorsenv.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) whenTERM_PROGRAM∈{ghostty, iTerm.app, WezTerm}orTERM == xterm-kitty— all four web-verified OSC-9-capable 2026-07. Message bodies are STATIC STRINGS ONLY from a fixed 4-entry table (kindis an enum key, never free text — the hard invariant), belt-and-braces routed throughtitles._clean. TUI: newKinApp._notify_terminalmirrors_set_terminal_titleexactly (driver-None guard, bare try/except); firesturn_completeondoneonly when the turn ran>= 15s(a new_turn_started_atmonotonic stamp, single writerturn_lifecycle._sendbesideharness_busy = True), andneeds_inputon the five needs-input branches (approval_request/question/plan_ready/proposal_ready/sandbox_override_proposed);interrupteddeliberately fires nothing (the user is present). 10s debounce; newon_app_focus/on_app_blurhandlers set a_focus_capableLATCH (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_headlesswritesterm_notify.sequence(...)straight tosys.stderrat process completion whenenabled()ANDsys.stderr.isatty()— the one exception to the module's UI-free/print-free contract, and the only path that makeskin -pfrom another shell/tab meaningful.notifyadded tosettings/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 +/rewindfile restore (G2, Step 2 of the field-gap Wave A). Newsrc/kin/harness/snapshots.py(stdlib + git subprocess, git-workdir-only): a harness-side re-implementation of the Outpost's DR 0030 temp-indexgit commit-treerecipe underrefs/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_snapshothooks_execute_one: the first EDIT-kind call each turn captures a pre-write checkpoint (idempotent viasess._turn_snapshotted, reset per-session at every_run_turn_impl, not root-only); toggleKIN_SNAPSHOTS/snapshots(default ON). Journal gainssnapshot/files_restoredrecords (verifiedreplay.loadtolerates unknown types before landing — no version bump). NewHistoryOps.rewind_files/rewind_bothasync methods (existing syncrewinduntouched):filesrestores on-disk state to the checkpoint before the cut turn without touching the conversation;bothrestores 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/bothalways 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). Newsrc/kin/harness/tools/diagnostics.py(stdlib-only):write_file/edit_fileappend a lint pass's output to their result on a nonzero exit — v1 covers exactly.pyviaruff check --no-cache --output-format concise, plus an opt-intypass behind its own toggle (diagnostics_ty/KIN_DIAGNOSTICS_TY, default OFF). Subprocess posture mirrorstools/shell.py::_spawn's branch exactly (sandbox-wrapped exec under auto mode, unwrapped otherwise,start_new_session=True, reusesshell._drain_to_completionfor the 10s timeout + guaranteed reap) — running the linter looser thanshellwould weaken the sandbox posture. Every failure mode (toggle off viadiagnostics_after_edit/KIN_DIAGNOSTICSdefault ON, unsupported extension,shutil.whichmiss, 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: arun_reporttap lands on/runs/<job>/<run>(the server-rendered run-detail page;app._run_viewalready serves it with 303-to-/on any bad id, no server change); analerttap lands on/#card-jobs(the existing_applyHashJobs-view vocabulary). SW-side target grammar:targetFor(data)constructs paths from validated numeric fields ondataonly —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)._storePendingNavextends the persisted Cache-API shape to{kind, id, job, run, ts}so the iOS killed/suspend-race_navCheckresume 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 wherenotificationclickDOES fire). RE-1 (matrix-fabric) named as new lived input — a cleanrun_reportdeep-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 Matrixapprove/reject/reviseverbs), gated by the existing r19chat/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-actionininbox_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 invia="push-action"+push_approve/push_dismissaudit rows). CSRF hand-off: landing JS posts{type:"csrf", token}to the active SW afternavigator.serviceWorker.ready; the SW persists in the Cache API (kin-navunder/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_TIMEOUTcap (asyncio.wait_for(delivery.send, scheduler._ALERT_TIMEOUT)), closing the bare-delivery.sendanomaly 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). NewTranscriptModal(ModalScreen[None])insrc/kin/tui/modals/transcript.pymountsbuild_replay_widgets(REUSED, not forked — the only transcript builder, the same one/resume/--continue//rewinduse) into a freshVerticalScroll, pushed on TOP of theAgentPanel(3-deep modal stack: app →AgentPanel→TranscriptModal— theReadOnlyModalpush already establishes the modal-on-modal pattern). Modal-local_Shimexposes only_kin_labeled/_kin_lead(the entirebuild_replay_widgetsapp-surface contract, verified by reading the builder signature). AgentPanel key split:Enter(priority=True wins over OptionList'sselect) opens the rich view for agent rows;ostays the raw buffered output. Esc/qdismisses back to the panel. Read-only by design — no input path into the child (messaging is the MCA's, viaagent_message); the modal also does NOT live-tail (synchronous snapshot ofchild_session.messagesat open = consistent point-in-time view, works for running + idle + closed rows alike becausemessagessurvivesaclose). Empirical first-check PASSED: this is the first caller ofbuild_replay_widgetsoutput outside the live#transcriptcontainer — pilot test assertsUserMessage+AssistantMessage+ToolCallactually 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: boolplain__init__attr (default False;spawn_childsets it True alongsidechild.planningpropagation).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 (thehad_texthalf keeps it from re-streaming partial); a child'sheadless_retry=Trueopens the mid-stream retry path. Newevents.stream_retry(discard_chars=N)event: emitted BEFORE the retry'ssleep + 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_BackendError→done("error")path runs unchanged; with Steps 3 + 4 + 6 the child parks, the parent gets the structured partial + the resume hint, andagent_messageresumes the conversation. Steps 5 + 3 + 4 + 6 COMPOSE: retry → exhausted retries →done_reason="error"→ parked child → structured partial with resume hint → resume.0060—agent_message— coordinator-only resume on a parked subagent (Step 6 of the subagent-retention wave). Newagent_message(agent_id, message, run_in_background?)tool sitting alongsideagent_list/agent_output/agent_kill: send the next instruction to an IDLE child (a running agent cannot be messaged — wait withagent_output(wait=true)for it to finish). Foreground returns only the NEW turn's prose (via newForwardingEmit.result_since(mark)helper, the cap +truncatedflag mirror the existingresult(); the same_shape_child_resultshapes both paths); bg returnsresumed agent<N> (<profile>) in backgroundand produces a freshsubagent_completedreminder on the next depth-0 turn. The synchronous LATCH RESETS (status / finished_at / error / background /turns += 1, thenForwardingEmit._completion_fired/done_reason/_on_completionreset, thenseen_completion_ids.discard) land BEFORE any await — so two parallelagent_messagecalls on the same idle row can't both grab it (the second seesstatus=runningand refuses)._make_completion_hook(root, agent_id, profile)insession/subagents.pyis the bg completion-reminder hook EXTRACTED fromdispatch_subagent's inline closure so the bg RESUME path can reuse it (no fork-and-drift). Step 4's failure strings carry the newagent_message("<id>", ...)resume hint so a failed child hands the model a recovery path. Depth invariant: resume does NOT change the child'sdepth; a running resumed child refuses further messages (any await-chain target must be IDLE). v1 display gap (noted):prompt_tokens/completion_tokensshow 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).ForwardingEmitnow capturesdone_reasonfor fg + bg children (the fg path used to drop thedoneevent entirely, so a wire death mid-stream haddone_reason=Noneand the parent received the partial prose as the complete answer with no marker). New_shape_child_result(row, fwd, prose)insession/subagents.pybranches ondone_reason(stop/error/turn_cap/loop_detected/truncated) producing structured id-bearing failure strings. Theerror:prefix IS the cross-backendis_errormechanism (_finalizeauto-detects it; Anthropic wire carries it; OpenAI wire has only the text) — NO typed channel added.ForwardingEmit.truncated: boolreplaces string-sniffing for the 32KB cap (id-bearing hint appended by the shaper from the flag).run_subagent's rowstatusmirrors the bg path ondone_reason(a wire death isstatus=error, notok).0058— Subagent retention (Step 3 of the subagent-retention wave, the keystone). Every child gets a journal (spawn_childmintsparent_session_id+agent_profilemeta);_run_child_turnpersists turns; finished children PARK idle (success + crash branches no longeraclose);AgentRegistry.close_allREPLACESkill_all(cancels running rows THENacloses 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);dismisscloses too. THREE enumeration surfaces filter child journals (list_sessions/latest_session+ the dashboard_resolve_session_id_syncworkdir scan). Ghost-journal guard UNCHANGED (verified).0057— Unified agent registry (Step 1 of the subagent-retention wave).BgSubagents/BgAgent/sess.bg_subagents→AgentRegistry/AgentRecord/sess.agents; ONE id space shared by fg + bg children.spawn_childis the single construction seam (keyword-onlyon_completion);dispatch_subagent's wiring dup is gone.run_subagentregisters fg children + wraps the child turn in its ownasyncio.Tasksoagent_killcancels the child WITHOUT cancelling the parent (thet.cancelled()+current_task().cancelling()disambiguation). Kill-authority rule:killonly 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-levelharness_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— Thecode-auditgate (symbols-not-line-numbers + move-tombstone class check). Newtask shipgate (scripts/code_audit.py, stdlib-only) with two checks: (1) forbidfile:linecitations 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-createsNeedsHumanErrorin a sibling and the gate lights up naming both modules; spy test revertsreasoning.py's package-namespace import and the related test fails. One_EXEMPTentry (at_mentions.pyparser-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.pySDK cite restated from the wrong retry-logic to the real_clientauth-header props — a C2 claim-drift fix).testing.mdship-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