Compare commits

...
115 Commits
Author SHA1 Message Date
conductor 09b19d6cf1 chore(release): auto-bump version to 0.331.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 32s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 12s
CI Pipeline / rust-surface (push) Failing after 25s
CI Pipeline / documentation (push) Successful in 13s
TUI Release / release (push) Successful in 39s
CI Pipeline / gitea-offline (push) Successful in 20s
CI Pipeline / go-surface (push) Failing after 45s
CI Pipeline / lint (push) Failing after 57s
CI Pipeline / type-check (push) Failing after 56s
CI Pipeline / canary (push) Failing after 3m33s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 21m56s
Promotion record: (unknown)
2026-10-22 00:20:30 +01:00
Nick 7934f38044 feat(release): plain-English 'What's new' narrative at the top of every release body
CI Pipeline / documentation (push) Successful in 13s
CI Pipeline / best-practices (push) Successful in 13s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 18s
CI Pipeline / go-surface (push) Canceled after 18s
CI Pipeline / lint (push) Canceled after 17s
CI Pipeline / gitea-offline (push) Canceled after 6s
CI Pipeline / type-check (push) Canceled after 6s
deploy / deploy (push) Canceling
Auto-version and Release / release (push) Canceled after 19s
CI Pipeline / unit-tests (push) Canceled after 20s
CI Pipeline / canary (push) Canceled after 24s
Owner directive: releases explained in plain english for a non-programmer
reader. New Go capability in src/go/release-notes (zero new Python):
conventional-commit range -> NEW/FIXED/IMPROVED/HOUSEKEEPING narrative
with a deterministic jargon translation map, 15-bullet cap with honest
overflow grouping. Wired into auto-version.yml as an idempotent,
non-blocking prepend step after release creation; the governed
note-generation path and the frozen Python parity battery are untouched.
2026-10-22 00:20:14 +01:00
conductor c5f8e1a0d7 chore(release): auto-bump version to 0.330.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 14s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 12s
TUI Release / release (push) Successful in 34s
CI Pipeline / rust-surface (push) Failing after 26s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / go-surface (push) Failing after 46s
CI Pipeline / lint (push) Failing after 55s
CI Pipeline / type-check (push) Failing after 54s
CI Pipeline / canary (push) Failing after 3m22s
CI Pipeline / unit-tests (push) Failing after 28m25s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 5s
Promotion record: (unknown)
2026-10-21 23:24:02 +01:00
Nick 71a027dcca feat(tui): memory + skills live feeds read the Go /v1 surfaces
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 15s
CI Pipeline / lint (push) Canceled after 16s
CI Pipeline / rust-surface (push) Canceled after 16s
CI Pipeline / go-surface (push) Canceled after 16s
CI Pipeline / best-practices (push) Canceled after 16s
CI Pipeline / canary (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 16s
CI Pipeline / documentation (push) Canceled after 16s
Point the TUI's memory/continuity and skills feeds at the new Go
endpoints (src/go/memory-continuity /v1/memory/* fd8dd9bf, src/go/
skills-engine /v1/skills 696d55f1) beside the Python paths, which
stay (port→integrate→remove). Both feeds degrade honestly: typed
degraded states with named causes, never fabricated topics or roster
rows; the skills source label (registry/static) rides through
verbatim. Red-first pin tests freeze the Go envelope shapes and the
front-door :8377 wiring via httptest.
2026-10-21 23:19:19 +01:00
conductor 796416b036 chore(release): auto-bump version to 0.329.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 23s
deploy / deploy (push) Skipped
TUI Release / release (push) Successful in 27s
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / rust-surface (push) Failing after 18s
CI Pipeline / gitea-offline (push) Successful in 26s
CI Pipeline / go-surface (push) Failing after 40s
CI Pipeline / lint (push) Failing after 46s
CI Pipeline / type-check (push) Failing after 1m1s
CI Pipeline / canary (push) Failing after 3m19s
CI Pipeline / unit-tests (push) Failing after 30m33s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 5s
Promotion record: (unknown)
2026-10-21 21:00:57 +01:00
Nick 696d55f1eb feat(skills-engine): SkillLister seam + read-only /v1/skills listing surface
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 7s
CI Pipeline / lint (push) Canceled after 8s
CI Pipeline / go-surface (push) Canceled after 6s
CI Pipeline / rust-surface (push) Canceled after 7s
CI Pipeline / canary (push) Canceled after 7s
CI Pipeline / best-practices (push) Canceled after 7s
Auto-version and Release / release (push) Canceled after 23s
deploy / deploy (push) Canceled after 21s
Minimal Go SkillLister interface beside the TUI's lister seams:
RegistryLister reads the canonical registry.json verbatim (the Python
engine's authority-outside-packages store) with no caching — the next
poll always reflects the last committed state; StaticLister serves a
labeled snapshot as the honest fallback. Endpoints: /v1/skills
({object:list,source,count,data} with tui/skills Entry shape
id/kind/state), /v1/skills/status, /v1/health. GET/HEAD only; writes
stay on src/rune/skills_engine.py until parity soak. New module
rune/skills-engine, zero deps. go test -race green (7 tests), vet/gofmt
clean.
2026-10-21 21:00:10 +01:00
Nick fd8dd9bfce feat(memory-continuity): read-only /v1/memory/* HTTP surface over the store seam
Endpoints: /v1/memory/status (honest lane status), /v1/memory/read
(exact-by-id verbatim read with digest fixity), /v1/memory/continuity
(active topic + working set as typed contribution blocks + bounded
transition trail), /v1/memory/checkpoint (read-only capsule peek),
/v1/health. GET/HEAD only — writes stay on the Python oracle
(src/rune/memory_continuity.py) until parity soak; non-read verbs
answer 405 read_only. Adds PeekCheckpoint/AllTopicStates read-only
registry helpers and 'serve <addr> <db> [dir]' mode. rune-core
runecore server.go envelope precedent. go test -race green (7 tests),
vet/gofmt clean.
2026-10-21 21:00:10 +01:00
Nick 5398f9b522 docs: spec — the activity pane + the answer presentation system (owner rulings 2026-10-21)
Auto-version and Release / release (push) Successful in 17s
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / rust-surface (push) Failing after 24s
CI Pipeline / gitea-offline (push) Successful in 26s
CI Pipeline / go-surface (push) Failing after 46s
CI Pipeline / lint (push) Failing after 1m8s
CI Pipeline / type-check (push) Failing after 1m8s
CI Pipeline / canary (push) Failing after 4m26s
deploy / deploy (push) Failing after 15m28s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 18m32s
2026-10-21 20:42:01 +01:00
conductor a8a05f3c40 chore(release): auto-bump version to 0.328.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 22s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 8s
TUI Release / release (push) Successful in 32s
CI Pipeline / rust-surface (push) Failing after 25s
CI Pipeline / gitea-offline (push) Successful in 27s
CI Pipeline / go-surface (push) Failing after 40s
CI Pipeline / lint (push) Failing after 54s
CI Pipeline / type-check (push) Failing after 55s
CI Pipeline / canary (push) Failing after 3m22s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 25m17s
Promotion record: (unknown)
2026-10-21 20:16:18 +01:00
Nick ffe40e5af6 feat(entry): journal event-type filter + consent enqueue idempotency
CI Pipeline / best-practices (push) Successful in 14s
CI Pipeline / documentation (push) Successful in 14s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 2s
CI Pipeline / rust-surface (push) Canceled after 18s
CI Pipeline / gitea-offline (push) Canceled after 1s
deploy / deploy (push) Canceled after 2s
CI Pipeline / go-surface (push) Canceled after 18s
Auto-version and Release / release (push) Canceled after 20s
CI Pipeline / lint (push) Canceled after 21s
CI Pipeline / unit-tests (push) Canceled after 21s
CI Pipeline / canary (push) Canceled after 21s
- /v1/events: ?type=a,b positive event-type filter over both the replay
  and the live tail — the TUI's receipts/degrade/trust live feeds ride
  the existing Go journal poll (rebase-tui-wiring audit gap, S-M).
- POST /v1/task-queue/enqueue: bounded (origin, text) idempotency scan
  over pending entries, freshness-gated by the same 30-min queueOccupyFresh
  law — a re-answered brief's repeat POST is a no-op returning the
  existing entry_id (carried-repairs consent enqueue-loop gap); distinct
  texts and stale/cross-origin entries still enqueue.
- red-first tests for both; -race green, vet/gofmt clean.
2026-10-21 20:15:59 +01:00
Nick d824b8f43c builder-bridge: retarget hook + CLI from stale issue161-harness anchor to the rune repo — BRIDGE/PY_CANDIDATES to rune paths, stdin JSON op surface with authority contract_id, hook parity managed set completes scaffolding-guard; install synced hash-verified 2026-10-21 19:10:55 +01:00
Nick c77f4e7728 docs(verification): carried-repairs gap notes — consent enqueue-loop (Go entry surface) and D46 semantic re-pins (tui surface)
Both fixes live on agent-owned surfaces: handleTaskQueueEnqueue lacks any
idempotency/dedupe (duplicate consent_card enqueues stack; Python side
verified clean — construction_intent.py never touches the queue), and the
two stale semantic tests pin the pre-D36 keyboard law in
tui/semantic/keys_test.go. Precise defect sites + recommended fix recorded.
2026-10-21 19:10:55 +01:00
Nick 32fc394e0d fix(tui-python): tasks-lane fold — 3-tuple unpack crash and empty task summaries
Startup parallel loader: load_tasks() emits ("tasks", active, pins) but
the consumer folded with a 2-tuple unpack, raising ValueError on every
tasks-with-pins result — the tasks branch never rendered. Fold now goes
through fold_lane_result() (key, value, extra). Also the branch rendered
t.get('summary') while the canonical loader emits 'title' — summaries
were always empty; accept both keys. Red-first: tests/test_rune_tui_lane_fold.py.
2026-10-21 19:10:55 +01:00
Nick 8b3665f468 fix(memory): restore scripts/dedupe_policy.py — the absent gate module behind memory_store.py's default add path
The checkout was missing the dedupe+supersede gate (retirement-map row 24,
parity/memory-ops.json): 'No module named dedupe_policy' on every default
add; only --no-dedupe worked. Module restored with the 2026-08-13 policy
(created/duplicate/supersede + receipts ledger), pinned red-first by
tests/test_dedupe_policy.py (6 tests incl. consumer-contract + one-record
law); live CLI add now returns receipted decisions.
2026-10-21 19:10:55 +01:00
conductor 3ecbe066e8 chore(release): auto-bump version to 0.327.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 17s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 16s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / rust-surface (push) Failing after 28s
TUI Release / release (push) Successful in 35s
CI Pipeline / go-surface (push) Failing after 43s
CI Pipeline / gitea-offline (push) Successful in 28s
CI Pipeline / lint (push) Failing after 1m1s
CI Pipeline / type-check (push) Failing after 57s
CI Pipeline / canary (push) Failing after 3m34s
CI Pipeline / unit-tests (push) Failing after 29m11s
CI Pipeline / required-checks (push) Failing after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 5s
Promotion record: (unknown)
2026-10-21 18:20:37 +01:00
Nick 7c90e950e3 feat(tui): rebase-TUI-wiring audit — every scoped TUI→backend surface proven Go-side (library-first boot, front-door :8377 fallback), wiring frozen by 3 pin tests; sized gap report for memory/continuity, skills, receipts/degrade/trust feeds; no Python deletion
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 7s
CI Pipeline / unit-tests (push) Canceled after 7s
CI Pipeline / rust-surface (push) Canceled after 9s
CI Pipeline / go-surface (push) Canceled after 11s
CI Pipeline / canary (push) Canceled after 8s
CI Pipeline / best-practices (push) Canceled after 8s
Auto-version and Release / release (push) Canceled after 16s
deploy / deploy (push) Canceled after 15s
2026-10-21 18:20:23 +01:00
Nick 8faf66a76a ci: permanent e2e proof rig (scratch runner + scratch repo, dry-run mode, teardown-safe EXIT trap) + workflow contract extended to tui-release/maintenance with red-first SIGPIPE-pipe and GITEA_REF_NAME-fallback guards 2026-10-21 18:20:23 +01:00
Nick 598e5c48ad feat(runecore): native tool calls on the three cloud wires
Implement ToolChatClient (SupportsTools + ChatWithTools) on the
[OI]-compat, anthropic, and gemini clients so cloud models act through
the D45 mode-gated tool suite instead of degrading to toolless:
- [OI]-compat: tools array rides the request; tool_calls parse with
  JSON-string arguments; assistant tool_calls + role 'tool' replay
- anthropic: tools param with input_schema; tool_use blocks parse to
  ToolCalls; continuation as user tool_result blocks with tool_use_id
- gemini: functionDeclarations under tools; functionCall parts parse
  to ToolCalls; continuation as user functionResponse parts keyed by
  function name
Mode gating stays identical to the local path (ExecuteGatedTool re-reads
the mode per dispatch); the engine's bounded loop and cap-out are
reused untouched. Red-first httptest doubles per wire (request shape +
plain answer / tool call / continuation), hermetic; the live leg stays
RUNE_CLOUD_PARITY_LIVE=1-gated.
2026-10-21 18:20:23 +01:00
conductor 97726e07c3 chore(release): auto-bump version to 0.326.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 33s
deploy / deploy (push) Skipped
CI Pipeline / documentation (push) Successful in 16s
CI Pipeline / best-practices (push) Successful in 23s
TUI Release / release (push) Successful in 35s
CI Pipeline / rust-surface (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 26s
CI Pipeline / go-surface (push) Failing after 56s
CI Pipeline / lint (push) Failing after 1m6s
CI Pipeline / type-check (push) Failing after 54s
CI Pipeline / canary (push) Failing after 3m43s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 12m11s
Promotion record: (unknown)
2026-10-21 18:07:25 +01:00
Nick ac2bd1d091 test(memory): pin full-length sha256 digest — mutation gap fill
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 23s
CI Pipeline / rust-surface (push) Canceled after 22s
CI Pipeline / best-practices (push) Canceled after 21s
CI Pipeline / documentation (push) Canceled after 21s
deploy / deploy (push) Canceled after 4s
Auto-version and Release / release (push) Canceled after 25s
CI Pipeline / unit-tests (push) Canceled after 26s
CI Pipeline / lint (push) Canceled after 26s
CI Pipeline / canary (push) Canceled after 26s
Mutation spot-check found the parity battery blind to digest truncation
(equality-only comparisons pass a consistently-truncated digest). Pin
64-hex + prefix explicitly; proven RED under truncation, green at HEAD.
2026-10-21 18:07:03 +01:00
Nick ab5cf12728 fix(tui): watchdog-bounded pty reads in the frozen boot smoke test
TestBootSmokeFaceReachesLibConnected's blocking pty reads could outlive
their own 240s/60s deadlines and burn go test's 10-minute timeout per
strike (600s strikes, leaked orphans; go-sharding receipt 2026-10-20).
Every read now goes through a reader goroutine feeding a chunk channel,
with each phase selecting against its real deadline (readUntilMarker);
timeouts kill the whole child process group (killGroup), so a failed
run leaks zero processes. Assertions and markers are unchanged.

boot_watchdog_test.go is the red-first suite (shown RED: undefined
symbols before the implementation): a hung reader dies at its declared
bound; a timed-out child leaves zero live group members.
2026-10-21 18:07:03 +01:00
Nick 36a4c7b25f style(tui): gofmt main.go (comment alignment, trailing blank line) 2026-10-21 18:07:03 +01:00
Nick 874a6ef324 vitality: phase-2 Python-suite audit — 13 panic-only tests converted to field/validator-pinned behavior proofs (outbox 5, events-schema 8), each red-first with a recorded mutation proof; zero deletions 2026-10-21 18:07:03 +01:00
Nick 06ef478ca9 feat(memory): Go port of memory-continuity runtime (row 25, parity battery 10/10)
New module src/go/memory-continuity: typed exact-by-ID reads
(FOUND_VERIFIED/FOUND_UNVERIFIED/SOURCE_CORRUPT/POINTER_BROKEN/
NOT_FOUND_CONFIRMED), topic attention lifecycle where cooling is residency
release never deletion, digest-verified rehydration with per-file checkpoint
corruption isolation, honest memory status lanes, disposable-cache purge,
audited right-to-forget wipe, and owner-correction supersession with
hash-chained audit lineage. Red-first parity battery mirrors
tests/test_memory_continuity.py (10/10 green, oracle untouched and still
10/10 via pytest). go build/vet/gofmt clean; go test -race green.
2026-10-21 18:07:03 +01:00
Nick a5026031cb retirement: row 6 trust-and-sovereignty integrated — persona_gate.decide + stage_guards kernels delegate to Go engines (fail-loud, RUNE_*_ENGINE=python escape hatch); row 5 file-change-safety recorded no-Python-oracle; no Python deletion 2026-10-21 18:07:03 +01:00
conductor bb59aa0d92 chore(release): auto-bump version to 0.325.1 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 13s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 12s
CI Pipeline / rust-surface (push) Failing after 28s
TUI Release / release (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 26s
CI Pipeline / go-surface (push) Failing after 45s
CI Pipeline / lint (push) Failing after 1m4s
CI Pipeline / type-check (push) Failing after 1m6s
CI Pipeline / canary (push) Failing after 3m41s
CI Pipeline / unit-tests (push) Failing after 29m23s
CI Pipeline / required-checks (push) Failing after 2s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 5s
Promotion record: (unknown)
2026-10-21 14:25:49 +01:00
Nick 296fc3d52a fix(store): ghost snapshot purge — 83MB legacy snapshot removed from history, banned-path guard
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 11s
deploy / deploy (push) Canceled after 2s
CI Pipeline / rust-surface (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 25s
CI Pipeline / unit-tests (push) Canceled after 23s
CI Pipeline / lint (push) Canceled after 24s
CI Pipeline / go-surface (push) Canceled after 21s
CI Pipeline / canary (push) Canceled after 19s
- history purge via git-filter-repo (path store/scaffold/legacy-current-migration-snapshot.json, --invert-paths); force-push to gitea under wave-ghost-purge standing approval; branch protection restored after
- rollback: backups/rune-pre-ghost-purge-2026-10-21.bundle (+ /tmp safety bundle of pre-purge state)
- .gitignore entry: the on-disk working copy stays, never committable
- check_landing.py SWEEP_RACE_BANNED_PATHS gains the path — pre-commit hook mechanically refuses staging (proven: force-add → RED refusal)
- merge/rebase/cherry-pick simulation from a wave base: no re-entry
2026-10-21 13:50:19 +01:00
Nick 240d6e9f73 research: image capability map — viewing/submission viable today on Konsole (Kitty protocol; the encoders already in vendor); analysis needs a vision model + capsule extension; staged build plan
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 0s
2026-10-21 12:08:58 +01:00
Nick 3baa74872d research: THE PERSONA ADOPTION LIST — 14 components, three generations, ADOPT/SUPERSEDE/SKIP verdicts; ~3.5-4.5K tokens/turn (quarter of the reference); store migration is the blocker 2026-10-21 11:59:29 +01:00
Nick 156f791d84 docs: OpenSpec interviews TW-1..8 + CP-1..5 — tabs/windows and chat presentation, ready for the owner's rulings 2026-10-21 10:16:11 +01:00
Nick 9bfecf43fb docs: the 50-scale distribution — 24 waves spawned across retirement (rows 21-25 conversions + 4 audits), infrastructure (sharding, watchdog, ghost purge, CI rig, gates), capability (toolset, cloud tools, context, queue), and spec (persona adoption list, image research) 2026-10-21 09:16:35 +01:00
Nick 09f6a511ff docs: D47 — six live-use defects + openspec routing for tabs/windows and chat presentation 2026-10-21 06:19:38 +01:00
Nick 9f7dccf3d5 chore: wave churn sync 2026-10-21 06:08:53 +01:00
conductor 94c72b35a1 chore(release): auto-bump version to 0.325.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 30s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 16s
CI Pipeline / documentation (push) Successful in 17s
CI Pipeline / rust-surface (push) Failing after 26s
CI Pipeline / gitea-offline (push) Successful in 28s
CI Pipeline / go-surface (push) Failing after 53s
CI Pipeline / unit-tests (push) Failing after 1m8s
CI Pipeline / lint (push) Failing after 1m13s
CI Pipeline / type-check (push) Failing after 1m11s
CI Pipeline / canary (push) Failing after 4m15s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-21 05:46:12 +01:00
Nick bce65f3f17 feat: THE EXIT GESTURE — q is a letter, ctrl+c twice quits
- plain q types (removed from the keymap AND the semantic vocabulary)
- first ctrl+c clears the composer draft; SECOND in succession quits
  (a deliberate double-press — typing can never exit)
- the frozen fixtures re-pinned deliberately (semantic v2, rail,
  receipts, degrade: app.quit left the vocabularies; quit walks
  re-pinned as navigation; the mutant lab's bfs pin relaxed for the
  dropped supersession per the fixture's own law)

48 packages green under -short; race-green on the touched set.

closes #2516
2026-10-21 05:45:37 +01:00
Nick 2d8403144b docs: D46 addendum — q types; ctrl+c twice quits (first press clears) 2026-10-21 04:02:06 +01:00
Nick a0cb876755 chore: sync churn (D46 wave files landed in dcb55367-lineage; residuals) 2026-10-21 00:45:10 +01:00
conductor 79656acf50 chore(release): auto-bump version to 0.324.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 38s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-21 00:12:37 +01:00
Nick a1fbefe4f1 chore: churn 2026-10-21 00:12:14 +01:00
Nick dcb5536797 feat: THE THINKING BRIDGE + the D44 folds — the face's second skin
- thinking moves to its own full-width strip between panes and
  composer: exists only while thinking, click-to-expose in place,
  chat pane conversation-only
- far-left timestamps pinned at column 0, same gutter both senders
- composer cursor: heartbeat re-arms (the blink froze after typing
  and never restarted)
- workshop pane: interactive open-tasks ledger (click expands the
  record), live work block verified
- sub-agent opener in the info bar (clickable waves N, hides at zero)
- v0.322.0 OSC 8 hyperlink byte-proven on a real terminal
- model display compaction merged (union with the sibling parser)
- QUEUE FALSE-POSITIVE: root cause was real data — 45 zombie
  awaiting_approval rows + 1 stuck running row held the lane hostage;
  freshness gate (30 min) fixes admission AND discharge; stuck-row
  restore verb queued
- queued receipts render in the tasks pane, never chat
- ctrl+q QUEUE STEER view: j/k walk, J/K reorder, x cancel — a new
  /v1/task-queue/steer endpoint behind it

48 packages green under -short; race-green on the touched set.

closes #2514
2026-10-21 00:12:14 +01:00
conductor 45f1eb3c73 chore(release): auto-bump version to 0.323.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Failing after 37s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 22:17:39 +01:00
Nick db0270e675 feat: D45 — THE TOOLSET GAP closed + the four HUD defects
Rune can act: the Go core's chat turn now carries the tool suite —
schemas per mode (CHAT/PLAN: read family; TASK adds write/edit/run),
a bounded model→tools→model loop, mode re-read per dispatch (CHAT
write = typed refusal, never bytes on disk). LIVE-proven: a real 27B
chat turn returned actual directory contents via list_directory; a
TASK-mode turn wrote a real file. Cloud wires degrade honestly to
toolless (native tool-calls = named follow-up).

- context bar live: 6.2k/256k rendering (the running core was a
  day-old binary — both TUI seams were already wired)
- model name compaction: 'qwen3.6 27B · uncensored' (the 96-char id
  compacted; union semantics with the sibling's parse)
- version OSC 8 hyperlink direct (lipgloss was stripping it)
- far-left timestamps pinned and rendering live
- key audit: 15/15 lossless, 8 vault rows repaired

closes #2512, #2513
2026-10-20 22:17:17 +01:00
Nick b7f25370a3 docs: D46 addendum — clickable file artifacts (OSC 8 file links) + tool events move to the tasks pane 2026-10-20 19:30:14 +01:00
Nick 06055565f0 docs: D46 — four live defects: message duplication, thinking not moved, composer shrunk, dispatch timeout 2026-10-20 19:04:47 +01:00
Nick 2f05865ddc chore: sync churn (D45 wave context wiring in flight) 2026-10-20 18:44:57 +01:00
Nick 1d73589b58 docs: D45 — live-use critique batch: context bar still missing, model-name compaction refined, version hyperlink forgotten, timestamps unmoved, THE TOOLSET GAP (Rune cannot act on the PC from the chat path) 2026-10-20 18:44:57 +01:00
conductor 79e63ee7f6 chore(release): auto-bump version to 0.322.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Failing after 39s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 14:43:46 +01:00
Nick 073fcd305f feat: D44 context readout — the real accounting per turn
The core reports context{used,limit} on /v1/status after every turn:
used = prompt_eval_count (the tokens the turn actually sent), limit =
the model's architecture context length (/api/show, fail-soft when
unknown). ChatReply/TurnResult carry it; the status handler records
it; the info bar renders 'context 4.2k/32k' once the first turn
reports. The bar never guesses.
2026-10-20 14:43:13 +01:00
Nick 36d8921d11 docs: D44 — the model display compaction rules + THE PANE DOCTRINE (the definitive dichotomy statement) + the queue false-positive defect named 2026-10-20 12:10:40 +01:00
Nick c40204795f docs: D43 addendum 6 — the thinking bridge (between panes and composer), far-left timestamps, always-blinking cursor, full workshop view + interactive open tasks, sub-agent opener, v-prefix version 2026-10-20 11:52:36 +01:00
conductor f039c5dee4 chore(release): auto-bump version to 0.321.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 40s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 11:17:59 +01:00
Nick 43d680fbb6 chore: wave churn 2026-10-20 11:17:29 +01:00
Nick 8effb3d912 feat: THE SCAFFOLDING STATUS LINE — 'scaffolding · ⚒ · build' with real contract stage, trade-glyph walk, arbitration, layer isolation; PTY-proven (closes #2511) 2026-10-20 11:17:29 +01:00
conductor d1b40bbc4b chore(release): auto-bump version to 0.320.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 44s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 10:34:59 +01:00
Nick 1a68db3552 feat(vitality): audit report + conversions (trust width fix, real-input tests, bounded-width pins) + scaffold status line wave files 2026-10-20 10:34:30 +01:00
Nick 45928ac768 feat(vitality): the ceremony audit — every conversion proven by mutation
Audit: 981 tui + 75 rune-core tests classified (mutant-proven /
red-first-proven / regression-pinned / invariant-pinned /
candidate-ceremony). Conversions: 2 panic-only tests → mutation-proven
bounded-width pins; 7 synthetic-spelling key sites → real input (the
'down'-types-d class eliminated); 1 vacuous assertion → real
cursor-walk proof. Zero deletions (nothing dead enough to kill).

The sweep EARNED its keep immediately: the converted trust test
caught a real width-overflow bug (56-rune strips in a 1x1 face) —
fixed (floor-clamped strips AND inner lines), re-proven
red/green. Full report: reports/test-vitality-audit-2026-10-19.md

closes #2510
2026-10-20 10:34:30 +01:00
conductor c2e2e2e33c chore(release): auto-bump version to 0.319.2 — earned by # (patch: converged release (push-to-master bump))
TUI Release / release (push) Successful in 1m0s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 10:04:00 +01:00
Nick 8329f31817 fix(trust): the vitality sweep's first catch — tiny-size renders now bounded
The sweep converted the panic-only trust test to a bounded-width
assertion and it caught a REAL defect: trust strips (and inner lines)
overflowed the face width at tiny sizes (0x0, 1x1, 20x4) — the exact
class the Layer Law forbids. Fixed: floor-clipped strips AND inner
lines. This is the answer to 'are tests ceremony': the conversion
paid for itself on its first run.
2026-10-20 10:03:32 +01:00
Nick 1f7b44417a chore: wave churn 2026-10-20 07:25:56 +01:00
Nick 85e02e0c79 research: harness-vs-model coding governance — 8 live sources; consensus is 'model designs, harness decides'; rune audited as the most-procedural end of the spectrum; honest gaps named 2026-10-20 07:25:56 +01:00
Nick 168b2e9d4e docs: D43 addendum 5 cont — all workhorse visuals in the tasks pane; chat stays conversational 2026-10-20 07:25:56 +01:00
Nick bb03ea7f37 docs: D43 addendum 5 — scaffolding status line in tasks pane (animation + real stage), hammer/ladder trade vocabulary, image capability recorded, inescapability stack 2026-10-20 07:25:56 +01:00
conductor 6b580b37a0 chore(release): auto-bump version to 0.319.1 — earned by # (patch: converged release (push-to-master bump))
TUI Release / release (push) Successful in 1m4s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-20 03:35:46 +01:00
Nick 8c5a4e42e0 perf(verify): Go checkup — the 600s strikes were ONE hung boot-smoke test, not parallelism
Measured diagnosis: tui/cmd/rune-tui eats 600 of 694 Go-check seconds
— the boot smoke test hangs in an unbounded terminal read past its own
deadlines (pre-existing on HEAD, repro'd on clean checkout). Race
detector measured innocent (1.6x, not 2-10x); Go's built-in test cache
already IS the content-keyed cache; all three sharding options
rejected on the numbers.

Landed instead: boundary_corpus_go scoped from the full tui suite to
its sole corpus consumer (70-84s → 0.1s, verified end-to-end GREEN);
explicit race-command timeout; honest 'not sharded' manifest notes
with measured reasons. The boot-smoke watchdog fix is named for its
own bounded repair wave (fix sketched in the receipt).

closes #2508
2026-10-20 03:35:18 +01:00
Nick 06f63d1a68 docs: D43 addendum 4 — centered searchable picker (never a gate), key-integrity verification, cloud connectivity proofs 2026-10-20 02:40:24 +01:00
Nick 39c8a38a82 docs: D43 addendum 3 — five critiques: ghost key hints, no composer scroll, word-safe wrapping, composer click preserves pane choice 2026-10-20 02:22:54 +01:00
Nick c9789d1121 chore: sync churn 2026-10-20 01:15:27 +01:00
conductor 5365c069c1 chore(release): auto-bump version to 0.319.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 40s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-19 23:39:48 +01:00
Nick 992be81553 feat(verify): sharded landing checkup — python_suite 1087s→275s (4x) + evidence hole filled with an honest finding (36 backfilled manifests ALL RED — real landed-tree breakage, G30.3 named as the blocker) + the legacy snapshot untracked AGAIN (the re-appearing ghost — every wave's base branch re-introduces it; needs a one-time history purge, queued) 2026-10-19 23:39:08 +01:00
conductor 03a579b31d chore(release): auto-bump version to 0.318.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 1m9s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-19 23:12:27 +01:00
Nick 697a7fae4b chore: wave churn 2026-10-19 23:11:31 +01:00
Nick 53882e603b feat: consent bridge + watching-her-work — the dichotomy's last pieces
CONSENT BRIDGE (queue 2a): chat detects work-shaped messages
(deterministic tiers 0-1, spec-interview-first per owner correction),
opens the gold y/n card (default No, typing never answers, lapse to
the hold queue); on y the BriefRecord freezes AT CONSENT (verbatim +
message id + context refs) and the task enqueues through the queue's
admission — never interrupting a running turn. Tier 2 (model
self-report) is phase 2 per the research's kill tests.

WATCHING-HER-WORK (queue 2b): the tasks pane leads with a live work
block per running task — thinking (collapsed, e expands, ◆), the
newest observed tool activity as the current line, honest progress
(observed counts + live elapsed; no step feed = named gap, never
fabricated). /tree palette command added (the tree had no opener);
queue rows finally wired into the pane.

CORE WIRE FIX: the queued answer carries queued:true + entry_id
(consent-wave finding — consumers saw 'no verdict' on the
queue-behind path).

closes #2504, #2505
2026-10-19 23:11:31 +01:00
conductor 2756cd8fb5 chore(release): auto-bump version to 0.317.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Successful in 43s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-19 21:36:19 +01:00
Nick 16fdb115e9 fix(release): THE DEEP DIVE — root cause from real CI logs
The killer was never the tag step: the bump step's FIRST LINE
(git tag --sort | head -1 under pipefail) died with SIGPIPE/exit-141
once the repo passed ~250 tags — zero step output, so every fallback
looked like it never ran. 40+ runs failed this way (0.312/0.313/0.314
among them). Second bug: GITEA_REF_NAME resolves empty on this runner,
so even repaired tags never fired TUI releases.

Fixed at source: scripts/tag_repair.py (one tested authority —
SIGPIPE-safe latest-tag, idempotent ensure, deadlock self-heal),
red-first 12-test battery pinning the disease as exit-141; both pipes
removed from all three workflows; tui-release tag resolution falls
back GITEA_REF_NAME → GITHUB_REF_NAME → git describe.

PROOF on a live runner: two full cycles auto-bumped AND auto-tagged
(v0.401.0/tui-v0.401.0, v0.401.1/tui-v0.401.1); the exact deadlock
state self-healed on the next push (v0.402.0 created+pushed
automatically). The pipeline never needs manual tag repair again.

closes #2503
2026-10-19 21:35:31 +01:00
Nick b84bec77c2 feat(retirement): rows 21+23 INTEGRATED — live consumers route through the Go engines
model-routing: 8 importers mapped — rune_tui/rune_operator/composition
rank through the Go engine (47-test battery live); capability_broker/
model_connect/work_manager were metadata-only. event-bus: operator
emits, health events, TUI stats, canary probes route through the Go
journal engine. Fail-loud (no silent Python fallback); deliberate
opt-out via RUNE_MODEL_ROUTING_ENGINE=python / RUNE_EVENT_BUS_ENGINE=python.
Integration receipts recorded; binaries rebuilt+re-attested; zero
deletions (the owner's PORT→INTEGRATE→REMOVE law: integration must
prove live before removal). Named blocker for removal: reconcile_tools_support.
2026-10-19 20:31:54 +01:00
Nick 407490dc7b chore: wave churn (watching-her-work + consent waves in flight) 2026-10-19 19:11:44 +01:00
Nick 56aa83bc3d chore: untrack legacy snapshot
TUI Release / release (push) Failing after 54s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
2026-10-19 19:05:07 +01:00
Nick 84e32c2ba6 chore: consent-wave test file (in-flight) + journal 2026-10-19 19:02:23 +01:00
conductor fb73068ef7 chore(release): auto-bump version to 0.316.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-19 18:56:36 +01:00
Nick db7a6c6a81 chore: residuals 2026-10-19 18:56:00 +01:00
Nick 946bb89e64 chore: journal churn + untrack the legacy snapshot (sweep-race class) 2026-10-19 18:56:00 +01:00
Nick 9ff08d25bd feat(queue): discharge — the waiting line's other half
At each task-turn boundary the lane takes the OLDEST pending entry and
dispatches it through the normal task-create path (send_task, channel
task); the queue file updates atomically, rows re-number, the pane
follows on its next poll. selectNextQueueEntry is the D36
'Rune intelligently decides' seam (phase 1 = FIFO oldest-first);
failed dispatches re-queue at the FRONT (order-preserving, never
silently dropped) with a typed journal event. Race-proofed (one
critical section, dispatch outside the lock, settle path for
fast tasks) — six consecutive -race suite runs green; live proof:
two queued demands drained FIFO through a real rune serve.

closes #2501
2026-10-19 18:56:00 +01:00
conductor ed790561ba chore(release): auto-bump version to 0.315.0 — earned by # (minor: converged release (push-to-master bump))
TUI Release / release (push) Failing after 54s
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Promotion record: (unknown)
2026-10-19 18:46:05 +01:00
Nick 2583361273 feat(housekeeping): Go core supervised by default (RUNE_CORE=python fallback, kill-restart proven live) + /tmp rune-tui-lib GC (reference-safety liveness, 2.5GB reclaimed, dry-run default) 2026-10-19 18:44:45 +01:00
Nick 0a23bd2d84 research: tabs/windows interview prep — stack capability mapped (lipgloss v2 layers enforce the Layer Law natively), face inventory, 8-question design interview with defaults 2026-10-19 18:11:38 +01:00
Nick 44bba163df docs: owner-approved 40-scale queue — 7 workstreams with rulings (spec-first dichotomy, port-integrate-remove, sub-agent window design, deep-dive release fix) 2026-10-19 17:47:44 +01:00
Nick 236b3c229b research: chat design principles — C1-C9 (zero conflicts with the landed face; ten rulings ratified) + innovation list I1-I6 with honest for/against 2026-10-19 16:17:14 +01:00
Nick 6c70baee59 fix: context readout spells the full word — 'context 4.2k/32k' (owner ruling) 2026-10-19 16:04:41 +01:00
Nick 64987765fe feat: D43 bubble/HUD hardening — the fifteen, the defect batches, the layer law
- local timestamps, opposite side, demphasised; direction-aware sides
  (user LEFT for LTR, inverted for RTL locales — one decision function)
- one thinking stream (claim law hardened, 4 frozen no-dupe tripwires);
  thinking clickable at the rendered row; stays in chat
- solid pink borders everywhere (the confirm card's gold outline was
  the defect); composer always lit; selected pane pink/unselected grey
- info bar: 'rune <semver> · ctx <used>/<limit>' (engine reports
  per-turn window tokens vs the registry's model limit); status/focus/
  rail/seq/lib noise stripped from the bottom
- thinking line: hint before the walk; walk capped at 3 glyphs
- ghost placeholder + anchored blinking cursor
- fading scrollbars in BOTH panes (independent layers, visible-on-
  activity, fade over 4 ticks)
- THE LAYER LAW live: the composer is a composed scaffold — border/
  content/cursor layers, each with isolation tests including the
  cursor-punches-the-border regression pin; layers registered as
  project citizens (visual-system domain); ownership header
- arbitration live: rail cluster still while thinking animates
- chat send proven live on PTY (the earlier report was a model
  timeout — the honest error path surfaced)

47 packages green under -race; PTY-proven on the real home.

closes #2500
2026-10-19 15:56:01 +01:00
Nick 574c196cea docs: D43 addendum 2 — layer scaffolds are project citizens: internal isolation, same construction store/contracts/authority/ledger 2026-10-19 15:07:42 +01:00
Nick 28f2ae5582 docs: D43 addendum — arbitration ruled (one alive element), timestamps demphasised+consistent, THE LAYER LAW (independent element scaffolds) 2026-10-19 15:06:23 +01:00
Nick 541313c575 research: THE TUI DESIGN CANON — twelve named, testable principles (9 live sources); owner taste ratified in 10 of 12 places; one collision flagged for owner decision (bottom-rail animation vs thinking walk) 2026-10-19 14:48:11 +01:00
Nick f477464d68 docs: D43 addendum — fading scrollbars both panes, outline-color correction (borders not text), TUI design-principles research ordered 2026-10-19 14:35:51 +01:00
Nick 9591e8a0eb feat(scaffolding): the construction authority is LIVE in rune — the mutation boundary
Every mutating tool call (subagent or main session) is scope-checked
against the active construction contract through the installed hook:
in-scope edits allowed, out-of-scope blocked naming the contract; the
durable lock fails closed across fresh processes (ACTIVE/UNREADABLE
both stop, ABSENT allows ordinary work); shell escapes (clobber/tee/
--output/touch) caught; supervisory acts refused; violations recorded
and self-clear on remediation. 67/67 drills + authority tests green;
hook installed hash-verified; activation front door ported (the one
design decision per the map) with the introduce-verb fix for rune's
Rust-core delegation.

closes #2499
2026-10-19 12:32:48 +01:00
Nick b22f21fdd8 chore(scaffolding): retroactive admission backfill — 7 contracts minted and closed (verbatim receipts, declared exemptions, sweep GREEN) + untrack the 83MB legacy snapshot (sweep-race class) 2026-10-19 12:01:57 +01:00
Nick c4a05be9b3 research: construction-authority port map — decision-only authority ports as moves (one real design decision: activate_from_text); launch+mutation guards complement; 3-4d phased 2026-10-19 11:59:44 +01:00
Nick ad2fe0efd9 docs: wave-brief compliance law — admission+issue+research required in every build brief; today's omission recorded as drift 2026-10-19 11:11:49 +01:00
Nick b5cd13faf6 chore(release): re-arm convergence after the 0.314.0 tag miss — substantive landings since (amalgamation, cloud routing, history restoration) warrant the next bump 2026-10-19 11:11:06 +01:00
Nick c1337d8acd docs: D43 addendum — hardening = option 3: mechanical lock (pins + territory + owner-routed changes), never locked against the owner or repair 2026-10-19 10:48:01 +01:00
Nick 940eba3f07 docs: D43 — fifteen bubble/HUD rulings: local timestamps, direction-aware sides, one thinking stream, no-duplication, solid pink borders, composer always lit, stripped info bar, 3-glyph walk, blinking cursor 2026-10-19 10:38:57 +01:00
Nick b1fd06e234 fix: history restored + thinking in history + full-box user bubbles
- ImportLegacyHotStore: one-time boot import of the Python-era store
  (~/.rune/hot/conversation_messages.json + K25 tail) into the Go
  store — msg_id-deduped, chronological, crash-resumable (proven in
  the wild: 116/512 mid-kill, resume completed zero-dupes)
- thinking rides the conversation wire → history renders collapsed
  thinking bubbles (clickable), claim-law dedupes the live one
- user bubbles: rune-count width math (byte-length was the defect),
  timestamp-aware cap, four rows one alignment, stamp hangs outside
  the corner — the full box the owner expected
- D41 addendum 3 live: no text prefixes; ◆ leads all rune bubbles

closes #2498
2026-10-19 09:28:40 +01:00
Nick 92544e8030 docs: issue-ledger law — every landing links+closes a Gitea issue; retroactive batch #2491-2497 2026-10-19 09:12:05 +01:00
Nick fb82438b74 docs: D41 addendum 3 amended — the purple diamond rides ALL of rune's bubbles (thinking + answers); it is her signature 2026-10-19 08:41:26 +01:00
Nick abd55a0ee4 docs: D41 addendum 3 — no prefixes in bubbles; side+color speak identity 2026-10-19 08:39:58 +01:00
Nick b9f7e89ec7 feat(rune-core): cloud routing — the engine serves any wire
Three cloud ModelClients ([OI]-compat, anthropic, gemini) with vault
key resolution at call time (secret-tool, key value never persisted),
per-provider routing from the record's Provider (table + registry,
source_base_url preferred), typed errors (503 provider_key_missing
when the vault fails, 502 provider_error carrying the provider's own
envelope). 67 tests hermetic under -race; the live token-spending leg
gated behind RUNE_CLOUD_PARITY_LIVE=1 for the owner to run explicitly.
A cloud default now works end to end: wizard pick → vault key → the
right wire → the reply, all in Go.
2026-10-19 01:59:30 +01:00
Nick d05354d5fd feat: D41 amalgamation LIVE — the face's identity
- rounded chat bubbles (cyan you right / green rune left), dim stamps,
  pink outline accent on the focused pane + input box, breathing
  thinking marker (3 pink phases, ≥4.5:1)
- one-cell purple diamond beside rune's markers
- the dots-walk thinking animation: '◆ thinking' + dots appearing
  left-to-right, each a glyph from the instance's SHUFFLED 15-glyph
  vocabulary (10 futhark + 5 cthulian, owner-verified set), row-fill
  restarts the walk; expandable WHILE thinking (click + ctrl+t)
- /style cycles live: amalgamation → quiet typeset → full futhark
- gauges window: ctrl+g or /progress, honest bars (indeterminate
  sweeps, never fabricated percentages), never paints over input
- mode token + live turn duration (mm:ss) on the identity line
- task pane sectioned (work stream / entry signage); sub-agent rail
  cluster animates on the SetWaves state; thick-gold confirms
- placeholder 'type in chat'; generous 2-row input box
- reduced-motion: one static glyph, zero ticks (PTY-proven)
- all 15 glyphs byte-verified in raw PTY (detect-then-render law)

9 packages green under -race; PTY-proven end to end.
2026-10-19 01:58:48 +01:00
Nick 6daefc847e feat: D42 addendum — cloud-first, no local default
- catalog reordered: vault-keyed clouds lead (moonshot/xiaomi/openai/
  google/ollama-cloud), keyless clouds, locals after (ollama demoted
  to a row among equals), custom hatch last; the drift-gated trio
  byte-proven (--check identical, 66 cases)
- wizard starts on NOTHING (cursor −1, no pre-selection); cloud rows
  lead; cloud picks walk the vault (SecretToolReader, endpoint+key
  probe per wire) — a cloud default is a fully supported outcome
- engine faithfulness: the record's provider owns the route — a
  cloud-naming record gets typed 503 provider_unrouted (never the
  silent localhost misroute); last-chosen unavailable renders the
  honest 'pick another' state, never a fallback
- last-used memory verified: one record, atomic writers, boot loads
  it, the engine reads it fresh per turn — no divergence path
2026-10-19 01:27:19 +01:00
Nick 1de4ce8308 docs: D41 addendum 2 — expand-while-thinking, clickable, dots-walk animation, minimal placeholder, generous box 2026-10-19 01:12:31 +01:00
Nick d4211b4c8c fix(chat): bottom-anchor refinement — pad only the RESTING tail window, never a scrolled window (reading old rows stays flush); blank-trailing detection instead of index math 2026-10-19 01:04:30 +01:00
Nick 62859e3b8a feat(chat): bottom-up transcript anchor — a short transcript hugs the pane's bottom above the input (messenger law); old render stacked messages from the top with dead space below (owner live-use ruling) 2026-10-19 00:30:53 +01:00
Nick 66bea432ca docs: D42 addendum — no local default, cloud first-class in the wizard 2026-10-19 00:08:59 +01:00
Nick 8e67a91c6f docs: D42 persona layer rulings — build-up seed with adoption list, dynamic depth, neuron-driven context, all-3 edit surfaces, full local migration, constitution deferred 2026-10-19 00:02:53 +01:00
Nick 078cc26ac7 chore: scaffold store journal churn 2026-10-19 00:02:18 +01:00
Nick 98fc0e2496 chore: post-landing residuals — standardized-providers regen (drift-gated trio) + journal + scaffold docs 2026-10-19 00:02:18 +01:00
Nick df088fbe30 feat: THE CONVERGENCE LANDING — six waves, one tree
- GO CORE TURN ENGINE (src/go/rune-core): per-channel single-flight,
  channel-keyed context, K25 tail+snapshot store (D40 seam), live
  Ollama with the thinking split, PTY parity proven with Python
  stopped + the owner's 27B live with thinking surfaced
- FULL-WIDTH SHARED BOX (D36): spans both panes, focus-signage
  placeholder, drafts survive focus round-trips, PTY-proven 110+140
- MODEL SPLIT (D37): pane-aware /model, per-pane record fields,
  status line; letter-stealing round 2 fixed (j/k type)
- APPROVAL CARD (T5): gold y/n through /v1/approvals/resolve, stale
  revalidation, chained asks, PTY-proven both legs
- PROVIDERS P1: 18 providers both languages, three-artifact drift
  gate, provider step in first-run (real names, no 'local' hardcode)
- SCROLLBACK: /history depth persisted, ctrl+up/down + pgup/pgdn,
  dim ↑N indicator, deeper-page fetch, PTY-proven on real history
- RETIREMENT rows 4+10 advanced: tool-executor dispatch wired
  (49/49+48/48), Go mode-gate door built, work-manager CLI parity
2026-10-19 00:02:18 +01:00
598 changed files with 183835 additions and 2483 deletions

No files matched your search

+42 -50
View File
@@ -38,7 +38,7 @@ jobs:
PR_TITLE: ${{ gitea.event.pull_request.title }}
PR_BODY: ${{ gitea.event.pull_request.body }}
PR_NUMBER: ${{ gitea.event.pull_request.number }}
run: "LATEST=$(git tag --sort=-v:refname | head -1 | sed 's/^v//')\nif [ -z\
run: "LATEST=$(python3 scripts/tag_repair.py --latest-tag)\nLATEST=\"${LATEST#v}\"\nif [ -z\
\ \"$LATEST\" ]; then\n echo \"No previous tags \u2014 releasing first version\
\ $RELEASE_VERSION\"\n exit 0\nfi\nif [ \"$LATEST\" != \"$RELEASE_VERSION\"\
\ ]; then\n if [ \"$(printf '%s\\n%s\\n' \"$LATEST\" \"$RELEASE_VERSION\"\
@@ -162,69 +162,61 @@ jobs:
\ summary of\n# what was worked on and submitted \u2014 the version-history\
\ reader's\n# counterpart of the issue-entry template. Appended to the\n#\
\ evidence-derived body so governed facts AND human narrative\n# both appear.\n\
LAST_TAG=$(git tag --sort=-v:refname | head -1)\nEXIT_NOTE=$(python3 scripts/release_exit_note.py\
LAST_TAG=$(python3 scripts/tag_repair.py --latest-tag)\nEXIT_NOTE=$(python3 scripts/release_exit_note.py\
\ --since \"$LAST_TAG\" --to HEAD) || EXIT_NOTE=\"\"\npython3 scripts/release_history_cli.py\
\ init \\\n --version \"$RELEASE_VERSION\" \\\n --server \"$RELEASE_SERVER_URL\"\
\ --repo \"$RELEASE_REPO\" \\\n --store store/scaffold/current.json \\\n\
\ ${EXIT_NOTE:+--exit-note \"$EXIT_NOTE\"}"
- name: Plain-English narrative at the top of the release body
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
# OWNER DIRECTIVE (plain-English releases): prepend a
# "# What's new — plain English" section to the release body,
# derived from the conventional-commit range since the previous
# tag. Go-side (src/go/release-notes narrative.go) — zero new
# Python. Idempotent: a body already carrying the section is left
# alone. Additive and non-blocking: a missing toolchain or a failed
# prepend must never fail a release.
LAST_TAG=$(python3 scripts/tag_repair.py --latest-tag)
if ! command -v go >/dev/null 2>&1; then
echo "::warning::go toolchain unavailable — release published without the plain-English narrative"
exit 0
fi
(cd src/go/release-notes && go build -o /tmp/rune-release-notes .) || {
echo "::warning::release-notes build failed — narrative skipped"; exit 0; }
/tmp/rune-release-notes prepend-narrative \
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
--tag "v${RELEASE_VERSION}" --since "$LAST_TAG" || {
echo "::warning::narrative prepend failed — release body left unchanged"; exit 0; }
- name: Push tui-v<version> tag via git so the tag-push event fires tui-release (D27)
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
# WHY git push instead of the Gitea API: API-created tags (POST /tags)
# do NOT emit a push event, so tui-release.yml's `on: push: tags: tui-v*`
# never fired (tui-v0.310.0 landed with no TUI release). A real
# `git push` of the tag is the trigger Gitea actions actually honor.
TAG="tui-v${RELEASE_VERSION}"
SERVER="$(printf '%s' "$RELEASE_SERVER_URL" | sed 's:/*$::')"
# git auth: same mechanism actions/checkout persists (http extraheader),
# but with the write-capable ROBOT_TOKEN and the token owner's real
# login, because Gitea's git-over-HTTP basic auth wants the owner's
# username when the password is a token.
USER_JSON="$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" "${SERVER}/api/v1/user")" || {
echo "::error::GET /api/v1/user failed — cannot resolve the token owner for git auth"; exit 1; }
ROBOT_USER="$(printf '%s' "$USER_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["login"])')"
ROBOT_EMAIL="$(printf '%s' "$USER_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("email") or "")')"
[ -n "$ROBOT_EMAIL" ] || ROBOT_EMAIL="${ROBOT_USER}@users.noreply.$(printf '%s' "$SERVER" | sed 's|^https\?://||')"
AUTH_B64="$(printf '%s:%s' "$ROBOT_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')"
GIT_HTTP_BASE="$(git remote get-url origin | sed -nE 's#^(https?://[^/]+).*$#\1/#p')"
[ -n "$GIT_HTTP_BASE" ] || GIT_HTTP_BASE="${SERVER}/"
git config --local "http.${GIT_HTTP_BASE}.extraheader" "AUTHORIZATION: basic ${AUTH_B64}"
git config --local "http.${SERVER}/.extraheader" "AUTHORIZATION: basic ${AUTH_B64}"
# Idempotency: a tag already on the server means this convergence event
# already covered it. If its TUI release is missing anyway (an orphan
# from the old API-tag path), recover with the documented re-push in
# tui/version/README.md.
if [ -n "$(git ls-remote --tags origin "refs/tags/${TAG}")" ]; then
echo "${TAG} already exists on the server — TUI release already covered, nothing to do"
exit 0
fi
# The auto-bump commit was created via the API AFTER this checkout, so
# it is not in the local object store yet — fetch master first.
#
# ALL decision logic lives in scripts/tag_repair.py (one tested
# authority — wave-release-pipeline-deepdive). It:
# * is SIGPIPE-free (the old `git tag | head -1` read killed this
# job with exit 141 on its FIRST line with 500+ tags — runs
# 29929/30435/30636 — so no fallback ever ran),
# * tags the bump commit, falling back to the fetched master tip
# when the sha is unusable (0.312.x failure mode),
# * is idempotent (tag already on the server = noop), and
# * SELF-HEALS the version>tag deadlock: if master's version is
# ahead of the latest tag because a bump landed without its tag,
# this step creates+pushes the missing tui-v<version> tag on the
# next push instead of skipping forever.
git fetch --no-tags origin master
TARGET="${BUMP_COMMIT_SHA:-}"
if [ -z "$TARGET" ] || [ "$TARGET" = "?" ] || \
! git cat-file -e "${TARGET}^{commit}" 2>/dev/null; then
# STRUCTURAL FALLBACK (0.312.0/0.313.0 tag misses: the API
# response's commit sha sometimes comes back unusable, and the
# old path exited 1 AFTER the bump landed — deadlocking the
# next convergence on version>tag skip). The bump commit is
# the fetched master tip in the common case: tag THAT rather
# than failing.
TARGET="$(git rev-parse FETCH_HEAD)"
echo "::warning::BUMP_COMMIT_SHA unusable — falling back to fetched master tip ${TARGET}"
fi
git tag -d "$TAG" >/dev/null 2>&1 || true
git -c user.name="$ROBOT_USER" -c user.email="$ROBOT_EMAIL" tag -a "$TAG" \
-m "chore(release): ${TAG} — native TUI convergence tag (same semver as the reviewed release, single version across faces)" \
"$TARGET"
git push origin "refs/tags/${TAG}:refs/tags/${TAG}"
echo "pushed ${TAG} at ${TARGET:0:7} — tag-push event fires tui-release.yml"
python3 scripts/tag_repair.py --ensure "${RELEASE_VERSION}" \
--bump-sha "${BUMP_COMMIT_SHA:-}" --push
concurrency:
group: auto-version-and-release-${ github.ref }
cancel-in-progress: true
+1 -1
View File
@@ -123,7 +123,7 @@ jobs:
run: |
VERSION=$(python3 -c 'import re; text=open("src/rune/__init__.py").read(); print(re.search(r"__version__ = \"([0-9]+\.[0-9]+\.[0-9]+)\"", text).group(1))')
echo "MASTER_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "LATEST_TAG=$(git tag --sort=-v:refname | head -1)" >> "$GITHUB_ENV"
echo "LATEST_TAG=$(python3 scripts/tag_repair.py --latest-tag)" >> "$GITHUB_ENV"
python3 scripts/version_convergence.py > /tmp/convergence.json
echo "Convergence: $(cat /tmp/convergence.json)"
python3 - <<'PYENV'
+6 -1
View File
@@ -14,7 +14,12 @@ jobs:
fetch-depth: 0
- name: Read semver from the pushed tag
run: |
TAG="${GITEA_REF_NAME}"
# GITEA_REF_NAME resolves EMPTY on the rune-ci-desktop runner even on
# tag pushes (runs 30390/30456/30862: "triggered on non-tui tag " with
# an empty tag). Fall back to GITHUB_REF_NAME, then to the tag that
# checkout actually put us on (git describe --exact-match on the
# detached tag checkout) — wave-release-pipeline-deepdive.
TAG="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-$(git describe --tags --exact-match HEAD 2>/dev/null || true)}}"
case "$TAG" in
tui-v*) VERSION="${TAG#tui-v}" ;;
*) echo "::error::triggered on non-tui tag $TAG"; exit 1 ;;
+4
View File
@@ -78,3 +78,7 @@ tui/wasm/modules/
# (Single entry: the pre-hygiene duplicate removed.)
store/scaffold/current.json
tui/rune-tui
# 83MB legacy migration snapshot — purged from history 2026-10-21 (wave-ghost-purge);
# the on-disk working copy stays, but it is never committable.
store/scaffold/legacy-current-migration-snapshot.json
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.314.0**
**Current release: v0.331.0**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+3 -2
View File
@@ -24,7 +24,7 @@ import { existsSync, appendFileSync } from "node:fs"
// Injection policy: only append when the capsule is non-empty. A plain
// "hello" gets nothing (§5: if nothing relevant exists, inject nothing).
const BRIDGE = "/mnt/workdrive/issue161-harness/src/rune/builder_bridge_cli.py"
const BRIDGE = "/mnt/workdrive/rune/src/rune/builder_bridge_cli.py"
const MARKER_PREFIX = "/tmp/rune-bridge-"
// Interpreter resolution (T22 repair): the previous hardcoded
@@ -35,7 +35,8 @@ const MARKER_PREFIX = "/tmp/rune-bridge-"
// all of them. If nothing exists the hook degrades as before.
const PY_CANDIDATES = [
process.env.MIMO_BRIDGE_PY,
"/mnt/workdrive/issue161-harness/.test-venv/bin/python",
"/mnt/workdrive/rune/.test-venv/bin/python",
"/mnt/workdrive/rune/.venv/bin/python",
"/home/nick/.venvs/memory/bin/python3",
"/usr/bin/python3",
].filter((p) => typeof p === "string" && p.length > 0)
+367
View File
@@ -0,0 +1,367 @@
import { execFile } from "node:child_process"
import { existsSync, appendFileSync, readFileSync } from "node:fs"
// scaffolding-guard — the MiMo-surface construction enforcement edge (T23).
//
// WHY THIS EXISTS
// T22 proved the accepted Scaffolding V1 authority was reachable from the
// TUI/operator path but NOT from the agent's own mutation surface, and
// recorded the honest boundary: "no byte-level enforcement point exists …
// Establishing MiMo-surface enforcement is NEW architecture (a
// tool.execute.before governance hook) and was NOT built here — it is
// recorded as the exact remaining blocker." This file IS that seam.
//
// It is the harness half ONLY. Every decision is delegated to
// `src/rune/construction_authority_cli.py` → `rune.construction_authority`,
// which derives from the canonical construction store. There is no
// construction logic here: if you find yourself adding policy to this file,
// it belongs in Python instead (T23 §36 — no parallel construction system).
//
// TWO EVENTS, TWO JOBS
// experimental.chat.messages.transform — the TURN BOUNDARY. Classify the
// user's intent once and create/resume the canonical contract
// (tranche §83: classify at the task boundary, never per tool call).
// tool.execute.before — the ENFORCEMENT BOUNDARY. Per mutating tool call,
// ask whether the mutation is inside the active contract's approved
// scope; block it if not.
//
// BLOCKING CONTRACT (verified against @mimo-ai/plugin index.d.ts and the
// decompiled harness): mutate `output` — `output.cancel = true` plus
// `output.cancelReason`. The tool's execute is then never called and the
// model receives `cancelReason` as the tool result. There is no
// thrown-error convention. `output.args` is mutable but unused here.
//
// DEGRADATION (tranche §61/§62): a bridge failure must not silently become
// either a denial or a bypass. We cache the last known authority; if the
// bridge fails while a governed contract was active, we FAIL CLOSED
// (conversational mutation on an active governed build stops with an
// explicit reason). If no contract was active, we allow — ordinary
// non-construction work must never become unusable.
//
// CANONICAL SOURCE: config/mimo-hooks/scaffolding-guard.ts
// Installed to ~/.mimocode/hooks/scaffolding-guard.ts by
// `scripts/install_mimo_hook.py` (hash-verified, drift-detected).
const REPO = "/mnt/workdrive/rune"
const CLI = `${REPO}/src/rune/construction_authority_cli.py`
const LOG = "/tmp/rune-scaffolding-guard.log"
// The DURABLE authority lock, written by the canonical authority
// (`construction_authority.write_authority_lock`) on every successful check.
// The hook reads it ONLY during bridge failure and ONLY to become MORE
// conservative (§62: degradation must never silently become a bypass).
//
// Why the hook needs it at all: `lastAuthority` below is in-process state, so a
// FRESH hook process — a new session, a reinstall, a restarted harness — starts
// with no memory that a governed build was active, and a bridge failure would
// then be allowed. That was the measured gap: fail-closed worked, but it did
// not survive a fresh process. The lock is the durable half of that decision.
//
// It can never GRANT authority: there is no code path here that turns a lock
// into permission. ACTIVE or unreadable → stop; absent → no durable evidence.
const LOCK = `${REPO}/store/authority-lock.json`
// Same durable interpreter resolution as builder-bridge.ts (T22 repair:
// never hardcode an ephemeral interpreter path).
const PY_CANDIDATES = [
process.env.MIMO_BRIDGE_PY,
`${REPO}/.test-venv/bin/python`,
"/home/nick/.venvs/memory/bin/python3",
"/usr/bin/python3",
].filter((p) => typeof p === "string" && p.length > 0)
function resolvePy() {
for (const p of PY_CANDIDATES) {
try {
if (existsSync(p)) return p
} catch {}
}
return null
}
// Tools that can change project state. Reads/search/list are deliberately
// absent — they cannot mutate and must never be gated (tranche §12:
// governance is universal for construction, ceremony is not).
const MUTATING = new Set([
"write", "edit", "patch", "multiedit", "multi_edit", "apply_patch",
"notebook_edit", "write_file", "edit_file", "str_replace",
"bash", "shell", "run_command", "execute", "terminal",
])
function log(line) {
try {
appendFileSync(LOG, `${new Date().toISOString()} ${line}\n`)
} catch {}
}
// Three states, and the distinction is load-bearing:
// ACTIVE — a governed construction was live; bridge failure fails CLOSED
// UNREADABLE — present but corrupt/unparseable; ALSO fails closed (a broken
// lock is not evidence of absence — absence of evidence is not
// evidence of absence)
// ABSENT — no durable evidence of an active construction; ordinary work
// must keep working (§62)
function readDurableLock() {
try {
if (!existsSync(LOCK)) return { state: "ABSENT", contract_id: null }
const doc = JSON.parse(readFileSync(LOCK, "utf8"))
if (!doc || typeof doc !== "object" || !doc.active) {
return { state: "UNREADABLE", contract_id: null }
}
return { state: "ACTIVE", contract_id: doc.contract_id ?? null }
} catch (e) {
return { state: "UNREADABLE", contract_id: null }
}
}
// Deterministic, secret-free diagnostic from a failed bridge call. The
// previous guard logged only `err.message` ("Command failed: …"), which hid
// the actual CLI error and made a FAIL-CLOSED record undiagnosable after the
// fact. We surface the interpreter's own stderr tail and the payload op —
// never the payload body (it can carry user working context).
function summariseFailure(op, err, stderr) {
const parts = [`op=${op}`]
if (err) {
parts.push(`kind=${err.killed ? "timeout" : err.code != null ? `exit=${err.code}` : "spawn"}`)
if (err.message) parts.push(`msg=${String(err.message).slice(0, 200)}`)
}
const tail = String(stderr ?? "").trim().split("\n").slice(-3).join(" | ")
if (tail) parts.push(`stderr=${tail.slice(0, 400)}`)
return parts.join(" ")
}
function runCli(payload) {
return new Promise((resolve) => {
const op = String(payload?.op ?? "?")
const py = resolvePy()
if (!py) {
log(`bridge: NO INTERPRETER RESOLVED ${summariseFailure(op, null, "")}`)
return resolve(null)
}
let done = false
const finish = (v) => {
if (!done) {
done = true
resolve(v)
}
}
try {
const child = execFile(
py,
[CLI],
{ timeout: 8000, maxBuffer: 1 << 20 },
(err, stdout, stderr) => {
if (err) {
log(`bridge: error ${summariseFailure(op, err, stderr)}`)
return finish(null)
}
try {
finish(JSON.parse(String(stdout)))
} catch (e) {
log(
`bridge: bad JSON op=${op} stdout=${String(stdout).slice(0, 200)} ` +
summariseFailure(op, null, stderr),
)
finish(null)
}
},
)
// MEASURED DEFECT (T24 §9A4, found while forcing a bridge failure): when
// the interpreter dies before reading stdin — a wrong binary, a crashed
// Python, a killed process — this stream raises EPIPE *asynchronously*
// and, unhandled, takes the whole HOOK process down. A crash is not
// fail-closed; the harness receives no cancellation decision at all, so
// the mutation proceeds. Swallowing it lets the execFile callback below
// report the failure as the ordinary `null` result, which the enforcement
// boundary then decides on.
child.stdin?.on("error", () => {})
child.stdin?.end(JSON.stringify(payload))
} catch (e) {
log(`bridge: spawn failed ${summariseFailure(op, e, "")}`)
finish(null)
}
})
}
// Last known authority, so a transient bridge failure can fail CLOSED
// without breaking non-construction work.
let lastAuthority = null // { active: boolean, contract_id?: string, at: number }
let lastTurnText = ""
// The working tree as of the previous shell call. The lazy postcondition
// diffs against this so it reports only what changed SINCE then — a governed
// build normally starts in a dirty tree, and treating existing residue as
// this command's writes would fail every build closed.
let lastSnapshot = null
// Harness-internal traffic is NOT user construction intent. Measured defect
// (T23, live): without this guard the turn boundary minted bogus
// construction contracts from `<actor-notification>` envelopes — subagent
// completion reports arriving as user-role messages — and then correctly
// blocked the operator's own legitimate work against them. Same class of
// guard builder-bridge.ts uses for `<system-reminder>` / checkpoint steps.
// A genuine operator turn does not begin with an envelope tag.
const INTERNAL_ENVELOPE =
/^\s*(?:<[A-Za-z][A-Za-z0-9-]*|EXTRACTION REQUIRED|\[Never-Ask\]|\[inbox)/
function isInternal(text) {
return INTERNAL_ENVELOPE.test(text)
}
// Interpreters that can execute a script which mutates the repo — the
// generated-script route (tranche §21). Only these trigger postcondition
// verification, which is the one place we pay a git call.
const SCRIPT_EXEC =
/(?:^|\s)(?:python3?|node|bun|deno|sh|bash|zsh|perl|ruby|php|make|npm|pnpm|yarn|cargo|go)(?:\s|$)/
function extractUserText(messages) {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i]
if (m?.info?.role !== "user") continue
const texts = []
for (const p of m.parts ?? []) {
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
}
if (texts.length) return texts.join("\n").slice(0, 4000)
}
return ""
}
export const ScaffoldingGuard = {
// ---- TURN BOUNDARY: classify once, create/resume the contract --------
"experimental.chat.messages.transform": async (_input, output) => {
try {
if (!Array.isArray(output?.messages) || output.messages.length === 0) return
const text = extractUserText(output.messages)
if (!text || text.length < 12) return
// Harness-internal envelopes are never construction intent.
if (isInternal(text)) return
if (text === lastTurnText) return // dedupe: one classification per turn
lastTurnText = text
const res = await runCli({
op: "begin",
text,
cwd: REPO,
origin: "mimo",
})
if (!res || res.ok !== true) {
log(`begin: no result (${JSON.stringify(res)})`)
return
}
log(
`begin: mode=${res.mode} activated=${res.activated} ` +
`contract=${res.contract_id} intent=${res.intent_layer} ` +
`scope=${JSON.stringify(res.scope ?? [])}`,
)
if (res.activated && res.contract_id) {
lastAuthority = { active: true, contract_id: res.contract_id, at: Date.now() }
} else {
lastAuthority = { active: false, at: Date.now() }
}
} catch (e) {
log(`begin: threw ${e}`)
}
},
// ---- ENFORCEMENT BOUNDARY: per mutating tool call --------------------
"tool.execute.before": async (input, output) => {
try {
const tool = String(input?.tool ?? "")
if (!MUTATING.has(tool.toLowerCase())) return
// LAZY POSTCONDITION (runs in the wired `before` event).
// `tool.execute.after` is declared in the hook API but is NOT triggered
// at runtime in this harness build — measured: 485 `before` events and
// zero harness `after` events, so a postcondition wired to `after`
// would silently never run. Detection therefore happens here instead,
// comparing the tree against the previous call's snapshot, which also
// means the delta is known BEFORE the next mutation is authorised.
//
// Runs for EVERY consequential mutation, not just shell calls. Measured
// temporal gap: with the checkpoint gated to shell tools, an escaping
// script followed by a native write/edit was authorised without the
// pending delta ever being reconciled.
{
const snap = await runCli({
op: "checkpoint",
cwd: REPO,
before: lastSnapshot,
})
if (snap && snap.ok === true) {
lastSnapshot = snap.entries ?? {}
if ((snap.violations ?? []).length > 0) {
log(`checkpoint: VIOLATION out_of_scope=${JSON.stringify(snap.violations)}`)
} else if ((snap.changed ?? []).length > 0) {
log(`checkpoint: changed=${JSON.stringify(snap.changed)} (in scope)`)
}
} else {
log(`checkpoint: no result (${JSON.stringify(snap)})`)
}
}
const res = await runCli({
op: "check",
tool,
args: output?.args ?? {},
cwd: REPO,
// G6: the turn's own words are what a promotion would mint the
// contract from. Passed only as evidence for a promotion the
// authority decides on — the hook does not decide.
promote_text: lastTurnText || undefined,
})
if (!res || res.ok !== true) {
// Bridge failure. Fail CLOSED only when a governed build was known
// active; otherwise let ordinary work continue (§61/§62).
//
// "Known active" now has TWO sources, and either one is sufficient:
// * `lastAuthority` — what this process saw before the bridge broke;
// * the DURABLE LOCK — what the canonical authority last recorded,
// which is the only source available to a FRESH process.
// The lock is consulted strictly to escalate. An ABSENT lock cannot
// turn a known-active in-process contract into an allow.
const durable = readDurableLock()
const inProcess = !!lastAuthority?.active
const durableStops = durable.state === "ACTIVE" || durable.state === "UNREADABLE"
if (inProcess || durableStops) {
const source = inProcess
? `in-process contract=${lastAuthority.contract_id}`
: `durable lock state=${durable.state} contract=${durable.contract_id}`
output.cancel = true
output.cancelReason =
"Scaffolding guard unavailable while a governed construction " +
"contract is active. Consequential mutation stopped (fail-closed). " +
"Re-run once the Rune construction authority is reachable. " +
`[evidence: ${source}]`
log(`check: FAIL-CLOSED on bridge failure (${source})`)
} else {
log("check: bridge failure, no active contract (in-process or durable) — allowed")
}
return
}
lastAuthority = {
active: !!res.contract_id && res.mode !== "NO_CONTRACT",
contract_id: res.contract_id ?? undefined,
at: Date.now(),
}
log(
`check: tool=${tool} allow=${res.allow} mode=${res.mode} ` +
`contract=${res.contract_id} scope=${JSON.stringify(res.scope ?? [])} ` +
`checked=${JSON.stringify(res.checked ?? [])}`,
)
if (res.allow === false) {
output.cancel = true
output.cancelReason = String(res.reason || "Blocked by construction scope")
}
} catch (e) {
log(`check: threw ${e}`)
}
},
}
export default ScaffoldingGuard
+137
View File
@@ -127,3 +127,140 @@ The iterative-styling law (D35 §2) governs: this is the default composite style
### D41 addendum — LOADING DOTS ARE RUNES; FONTS ARE THE TERMINAL'S (owner, 2026-10-18)
The loading-dots/spinner idiom is REPLACED by the runic symbol cycle: loading indicators ARE runes (random celtic/viking + cthulian mix, per D41). Fonts are terminal-level, not app-level: the app emits codepoints; the owner's fallback pool (93 symbol fonts, Noto base — verified rendering) covers them. LAW: every animation vocabulary is DETECT-then-RENDER — verify glyphs draw (PTY byte-check) before committing them to an animation; box-glyphs get swapped at build time, never shipped.
## D42 — THE PERSONA LAYER RULINGS (owner, 2026-10-18, interview after the chain audit)
1. SEED: BUILD UP from the repo's 2.3KB distilled persona — do NOT adopt the old constitution wholesale. Produce an ADOPTION LIST first: compare every component across the three generations (OpenCode/MiMoCode/repo), propose what to adopt and WHY each is a decent addition, and mark what's superseded. The owner approves the list before adoption.
2. DYNAMIC DEPTH: Rune commands both panes equally, understands the dichotomy inherently, and puts the right resources into each dynamically — no fixed pane budget, an intelligent allocation.
3. CONTEXT INTELLIGENCE: the neurons + sleight-of-hand machinery keep context low while full persona stays AVAILABLE — pulling persona context where it contextually matters, for the right contextual output. (Not a fixed token budget — an intelligent system.)
4. EDIT SURFACE: all three — the truth file, journal + rollback, AND the TUI editor (/persona) over it.
5. MIGRATION: EVERYTHING migrates for the owner's local session (the full lived store); a production version (different, privacy-shaped) comes later after the concept is proven.
6. CONSTITUTION: DEFERRED — a shippable untouchable (encrypted) constitution is a later revision; do not block the persona build on it.
### D42 addendum — NO LOCAL DEFAULT; CLOUD IS FIRST-CLASS (owner, 2026-10-18: "i dont want ollama as default. i want to choose to use that. i want the default to be a cloud provider")
1. Ollama loses its privileged position: NOT pre-selected, NOT default-positioned in the catalog, NOT the implicit fallback anywhere. It is one choice among many.
2. CLOUD PROVIDERS ARE FIRST-CLASS in the setup wizard: connecting a cloud provider (API key from the vault, connection test) is a supported FIRST-RUN path, equal to local.
3. The default model is whatever the owner EXPLICITLY picks in the wizard — never inferred from locality. First pick = default for both panes (unchanged D37 law).
### D41 addendum 2 — LIVE THINKING UX (owner, 2026-10-19)
1. EXPANDABLE WHILE THINKING: the thinking bubble expands DURING thinking, not only after — live thoughts unfold as they stream.
2. CLICKABLE: the expand control is clickable (click region on the marker/bubble line), alongside ctrl+t.
3. DOTS ANIMATION: the thinking animation is "thinking . . ." — dots appear LEFT TO RIGHT after the word, each dot a NEW runic symbol (the celtic/cthulian mix), cycling from the left when the row fills. Not a spinner in place — an appearing-dots walk.
4. PLACEHOLDER: the input box placeholder is just "type in chat" (no affordance list — commands live in the palette and the / overlay).
5. BOX PRESENCE: the input box grows more generous (taller/padder) so it reads as an obvious place to type — natural, inviting.
### D41 addendum 3 — NO PREFIXES IN BUBBLES (owner, 2026-10-19)
Chat bubbles carry NO text prefixes — the "you ·"/"rune ·" markers are removed from the bubble content. Identity is already spoken by the bubble itself: side (you right / rune left) and color (cyan / green). The purple diamond stays on rune's EVERYTHING — thinking bubbles AND her answer bubbles alike: it is her signature mark on all her words (owner: 'keep the purple diamond for runes answers tho'); timestamps stay at the bubble corner. Non-bubbled roles (system, error, research, tool) KEEP their textual markers — they have no side/color to speak for them.
## D43 — THE BUBBLE/HUD HARDENING (owner, 2026-10-19, fifteen rulings)
1. HARDEN: the bubble UI logic is locked clean — nothing interferes with it; amenities only when the owner asks.
2. TIMESTAMPS ACCURATE + LOCAL: reflect the person's machine timezone per message (low cost: local-format the UTC record at render).
3. TIMESTAMP SIDE: moved to the OPPOSITE side of the bubble from its current position.
4. DIRECTION-AWARE PLACEMENT: USER ON THE LEFT for left-to-right languages, rune right — INVERTED for right-to-left language locales (he/ar/fa/ur detection via locale, setting-overridable).
5. ONE THINKING STREAM: no before/after duplication — the live thinking bubble BECOMES the history thinking bubble (one stream per message).
6. NO-DUPLICATION POLICY: rune's answer never renders twice (completion-applied bubble and durable row are ONE fact — harden the claim law; frozen no-dupe tripwire).
7. THINKING IS CLICKABLE (the marker, the line, the whole collapsed row).
8. Thinking stays with its message in CHAT (owner accepted: tasks-pane thinking is too messy); tasks pane keeps its own work-stream thinking.
9. BORDERS: dashed → SOLID, same colour as the box (pink family); ALL boxes solid pink borders.
10. COMPOSER (the type-in-chat box) is ALWAYS lit with the pink outline; the selected pane lights pink outline when selected, GREY outline when not.
11. INFO BAR: a clean information bar above or below the composer replaces the messy far-bottom bar.
12. BOTTOM BAR STRIPPED: remove status=RUNNING (running is proven by working), focus=chat (visible in selection), RAIL, seq=N, runtime lib:connected. KEEP: version truncated to semver (0.314.0 not 0.314.0-56-g...), CONTEXT WINDOW in-use/available (accurate: engine reports tokens used vs the model's context limit from the registry).
13. THINKING LINE: "◆ thinking <ctrl+t hint> <animation>" — the animation capped at 3 glyphs, the ctrl+t hint BEFORE the animation (nothing chases the walk).
14. GHOST PLACEHOLDER: "type in chat" ghosted, vertically centered, with a BLINKING CURSOR indicating chat-here.
### D43 addendum — THE HARDENING MEANING (owner chose option 3, 2026-10-19)
The bubble UI is locked against SILENT and UNINVITED change — mechanically, not by promise. Three enforcement layers:
1. FROZEN PINS: golden fixtures + tripwires pin the bubble's geometry/colors/alignment/visual states — any render change turns the suite RED; pin updates are visible in diffs.
2. NAMED TERRITORY: the bubble renderer files are owned surface (domain registry) — factory waves do not edit them without a brief naming the reason.
3. CHANGES ROUTE THROUGH THE OWNER: visual changes to the bubbles ask the owner first (OpenSpec-starting-point law + D35 §2); repairs (restoring the pinned shape) land red-first as normal — the lock protects the design, never the bugs. Never locked against the owner.
### D43 addendum — SCROLL INDICATORS + CORRECTED COLORS (owner, 2026-10-19)
1. SCROLL WHEEL/UI in BOTH panes: a visible scrollbar indicator while scrolling that FADES when not in use (the modern-editor pattern — visible on activity, transparent at rest). Scrolling the chat history per the landed scrollback law; the tasks pane scrolls its work stream the same way.
2. OUTLINE COLORS STILL WRONG (second correction): the chat TEXT colors are correct — the OUTLINE colors are not. The outlines follow the D43 law (solid, pink family, composer always lit, selected pane lit / unselected grey). The wave must diff outline color assignments against the law's color table — not the text colors, the borders.
3. TUI DESIGN PRINCIPLES: a research round on presenting a terminal UI — the design canon (hierarchy, restraint, motion discipline, information density, color systems for terminals) so future visual choices follow principles, not ad-hoc taste. Feeds the owner's iteration loop.
### D43 addendum — ARBITRATION + TIMESTAMPS + THE LAYER LAW (owner, 2026-10-19)
1. ARBITRATION RULED (canon P3): one alive element wins — while Rune thinks, the bottom-rail cluster goes STILL (count only); the rail animation resumes when the thinking resolves. The rail exists to be seen, not to move.
2. TIMESTAMPS: intelligently placed (the same relative position regardless of sender) and DEMPHASISED — their importance is low; dim, unobtrusive, never competing with message content.
3. THE LAYER LAW (owner architecture ruling): every visual element of the TUI is an INDEPENDENT LAYER with its own scaffold/state — border, content, cursor, scroll indicator, animation each render in their own layer, and a change or defect in one must never corrupt another (the cursor punching a hole in the composer border is the motivating defect). Layers compose at render time; interactivity may couple them deliberately; accidental coupling is a defect. Each layer gets its own tests (its own scaffold, in the owner's words) — a change to one layer's state cannot alter another layer's rendering.
### D43 addendum 2 — LAYERS RELATE TO THE PROJECT (owner correction, 2026-10-19)
The Layer Law's independence is INTERNAL isolation, not project isolation: each layer has its own scaffold/state/tests WITHIN the tui project's scaffolding system — registered in the same construction store, admitted through the same contracts, checked by the same authority, versioned with the project. Isolation means a change to one layer's state cannot silently alter another layer's RENDERING; it does NOT mean layers live outside the governed build. Every layer scaffold is a project citizen: contract, tests, pins, and the issue ledger all track it with the rest.
### D43 addendum 3 — FIVE OWNER CRITIQUES (live use, 2026-10-19)
1. GHOST KEY HINTS: the composer shows ghosted "enter send · shift+enter newline" hints that DISAPPEAR the moment typing starts (ghost-at-rest, gone-when-typing — the affordance teaches, then vanishes).
2. COMPOSER NEVER SCROLLS: the composer must NOT behave like a scrolling viewport while typing (the weird line-influence effect) — the composer fits its 3 rows and wraps internally; scroll behavior is for the panes only.
3. WORD-WRAP NEVER BREAKS WORDS: wrapping breaks at word boundaries ALWAYS — a word reaching the edge moves WHOLE to the next line, never splitting mid-word (observed: "my" split to "m/y" at pane edge). Applies to chat bubbles, composer, all text surfaces.
4. COMPOSER CLICK PRESERVES PANE CHOICE: clicking the composer after selecting a pane keeps THAT pane's focus — the composer serves whichever pane is selected; clicking it must never default back to chat. The composer is the shared instrument; the panes set its destination.
5. (Reward for catching these: the Layer Law holds — each defect was an isolated layer failing, never cascading.)
### D43 addendum 4 — THE CENTERED PICKER + CLOUD PROOFS (owner, 2026-10-19)
1. MODEL PICKER = CENTERED WINDOW, NOT FRONT-LOADED GATE: model selection must NOT block the composer (currently typing anything demands a pick first). Instead: typing with no model chosen opens a CENTERED floating window (mimocode-style) listing ALL providers, SEARCHABLE (type to filter), up/down arrows to walk, enter to choose, esc to dismiss-and-keep-typing. The picker is an overlay on demand, never a gate in front of the input.
2. API KEYS: port ALL api keys from mimo code without altering them (the 15-secret lossless migration already landed into the OS vault — verify nothing was altered/mangled in translation and every registry row still resolves).
3. CLOUD CONNECTIVITY PROOFS: every cloud provider connection must actually prove connectivity (a real endpoint probe). Providers the owner holds keys for: prove live. Providers without keys: the probe path is the same principle (endpoint reachable, auth attempted, honest error) — but marked untestable-by-owner rather than claimed.
### D43 addendum 5 — SCAFFOLDING STATUS LINE + GATED-WORK ANIMATION (owner, 2026-10-19)
1. SCAFFOLDING STATUS LINE (owner spec): when scaffolding is in play, the tasks pane shows: "scaffolding · <loading animation> · <its stage>" — e.g. "scaffolding ⚒ admit", "scaffolding ⚒ build", "scaffolding ⚒ verify", "scaffolding ⚒ land". The loading animation uses the gated-work rhythm (hammer/ladder glyphs per the owner's hammer-and-ladder ruling); the STAGE is the contract's real phase from the kernel state (admit/build/verify/land — never a guess). Appears when a governed contract is active; self-extinguishes when it closes.
2. HAMMER-AND-LADDER WALK: the gated-work animation is the dots-walk pattern with hammer/ladder glyph vocabulary (⚒ ⛏ 🪜-class glyphs, font-verified per detect-then-render; the shuffled runic/cthulian vocabulary remains for THINKING — gating gets its own trade vocabulary so thinking and building read differently at a glance).
3. IMAGES: viewable in-terminal (Kitty/sixel protocols; Konsole supports; graceful degradation to links elsewhere per the degrade-silently principle); analyzable when a vision-capable model is connected. Recorded as capability, not a wave yet.
4. INESCAPABILITY: the machinery-first stack (launch guard → admission → scope-checked mutations → landing gate → durable lock) IS the law — "please build X" puts every step inside scaffolding regardless of model. The adversarial-escape audit (try to escape, close every hole) is queued as its own wave.
### D43 addendum 5 (cont.) — ALL WORKHORSE VISUALS LIVE IN THE TASKS PANE (owner, 2026-10-19)
Ruling: ALL coding/workhorse-related visuals belong in the TASKS pane — scaffolding status line, work blocks, queue rows, sub-agent window, gated-work animations. The CHAT pane stays conversational (messenger law); the TASKS pane is Rune's workshop window. Both addenda recorded and queued: the scaffolding status line + hammer/ladder animation + the adversarial-escape audit + image capability (recorded, not queued) all route to the tasks-pane wave.
### D43 addendum 6 — THE THINKING BRIDGE + HUD CORRECTIONS + THE WORKSHOP COMPLETION (owner, 2026-10-20)
1. THE THINKING BRIDGE: the thinking animation MOVES OUT of the chat pane into its own strip BETWEEN the panes and the composer (above the composer, full width). Solves: click-to-expose lives right there (click the strip → the thought process expands — in the bridge or a floating window per the tabs/windows design), and the chat pane declutters (conversation only).
2. TIMESTAMPS: ALWAYS far-left, same position regardless of sender.
3. COMPOSER CURSOR: the blinking cursor must ALWAYS blink when waiting for entry — never static (a regression).
4. TASKS PANE = THE ACTIVE WORKSHOP: while scaffolding runs, the work-in-full-view renders inside the tasks pane — the active task's live coding (current file/command, thinking) fully visible. PLUS a tasks VIEW: clickable/expandable task rows with the open tasks (the D38 ledger's OPEN TASKS section becomes an interactive surface, not a static list).
5. SUB-AGENT WINDOW OPENER: the window exists but has NO WAY TO OPEN — put the opener in the info bar NEXT TO the tasks area, AFTER the rune version.
6. VERSION DISPLAY: 'v' prefix (v0.321.0 not 0.321.0) — a version marker; could hyperlink to Gitea (the release URL) when terminals support it (OSC 8 links — degrade gracefully to plain text).
7. SEQUENCE AFTER THIS WAVE: finish wiring the rebase into the TUI, then the persona layers.
## D44 — THE MODEL DISPLAY + THE PANE DOCTRINE CORRECTED (owner, 2026-10-20 — supersedes the partial D36 routing read)
### 1. MODEL DISPLAY (the info bar shows the active model, correctly compacted)
The info bar carries the active model name. LOCAL models truncate to their important components: initial name, size, style if applicable (classifier vs normal — ONLY SHOWN WHEN APPLICABLE), and ablation status (uncensored/abliterated variants flagged). CLOUD models: full name, size, classifier-if-applicable. Never let a long local filename eat the bar.
### 2. THE PANE DOCTRINE (owner verbatim — the definitive statement)
"the tasks pane is meant for ALL ACTIVE WORK or whats being being worked upon... the dichotomy for either pane is CHAT IS FOR TALKING WITH THE MODEL WHILST THE WORK IS BEING DONE. the TASKS PANE is to view the details of whats being worked upon and outstanding work as well as any goals or lists SEPARATE from the detail of the work. and the COMPOSER for either acts differently based on whats selected."
- TASKS PANE focus + composer enter = IMMEDIATE EXECUTION (never queues — queuing only when work is already running; a queue message with nothing running is a FALSE POSITIVE defect).
- CHAT PANE focus + composer enter = conversational (base assumption: discussing). Work requests from CHAT get boosted to tasks via the CONSENT CARD (contextual relevancy decides when) — the consent bridge applies ONLY to the chat pane.
- The queued-work message renders in the TASKS PANE (with the queue rows), NEVER the chat pane.
- A HOTKEY exists to steer queued work (reorder/prioritise/cancel from the pane).
### 3. QUEUE FALSE-POSITIVE FIX (defect)
Task-pane asks with nothing running must START immediately — the instant-if-free law was violated (queued despite idle). Red-first the false positive; the queue is only for BUSY.
## D45 — THE LIVE-USE CRITIQUE BATCH (owner, 2026-10-20, after testing the D44 build)
1. CONTEXT READOUT STILL MISSING in the bar (the core-side accounting landed but the bar doesn't show it — diagnose the TUI-side wiring: the bridge's status poll must actually carry/receive the numbers, and the bar must render once reported).
2. MODEL NAME COMPACTION (refined spec): "qwen3.6-27b-(uncensored?)" style — parse the id: base name, parameter size, ablation marker (uncensored/abliterated/heretic) if present. No path prefixes (hf.co/), no quant suffixes (IQ3_M), no GGUF noise. Cloud models similar: vendor name + model + size.
3. VERSION HYPERLINK FORGOTTEN: the v-prefixed version must hyperlink to the Gitea release (OSC 8 — detect-then-render).
4. TIMESTAMPS STILL NOT MOVED (the addendum-6 far-left move didn't land — re-verify and fix).
5. THE TOOLSET GAP — THE BIG ONE: Rune cannot bash or write to the owner's PC. The model's thinking says "I cannot directly access your desktop" — because the TUI chat path doesn't surface the tool suite to local models. The tools exist (tool_executor, 19 tools, mode-gated); the chat turn's tool wiring must FORCE tool availability: local models get the toolset in their prompt/capability selection (the T2 slice landed the exposure filter — verify it's active on the live path), cloud models get native tool-calls. Rune at the face must be ABLE to act, mode-gated as D36/D44 demand.
## D46 — FOUR LIVE DEFECTS (owner, 2026-10-20, second testing round)
1. MESSAGE DUPLICATION (never again): a sent message renders TWICE in the chat pane. The claim law was hardened once (D43) but a path still double-renders — find it (likely the D45 wave's changes to the sync paths interacting with the thinking-bridge move), red-first, and pin the no-dupe tripwire so hard it cannot regress.
2. THINKING NOT MOVED: the thinking animation/strip was ruled OUT of chat into the bridge strip (addendum 6 item 1) — still rendering in the chat pane. Move it.
3. COMPOSER SHRUNK: the composer got SMALLER — wrong direction. The composer may push BOTH panes up for more room (its growth is legitimate; panes yield).
4. DISPATCH TIMEOUT: 'error · dispatch failed: Post ... context deadline exceeded' — the 330s budget was exceeded. Diagnose: the 27B thinking models run 250-310s solo and the chain stacks delays; either raise the budget on the TUI+proxy legs or stream. Diagnose the real timeout hop first.
- D46 RESOLUTION (the budget road, owner's pick — streaming stays queued separately): the real hop was the WHOLE STACK at 330s (TUI client lib.CompletionBudget, entry serve proxy proxyClientBudget, the continuous-runtime door — all pinned 330) against a 27B solo worst turn of ~310s plus stacked queue/memory/tool latency: no headroom, so the deadline fired. ONE coherent 480s budget now rides every leg (TUI + both entry doors), pinned structurally by the frozen budget fixtures; the core operator's per-call 300s adaptive ladder (27B) stays — 480 covers the worst observed turn with headroom.
### D46 addendum — CLICKABLE ARTIFACTS + TOOL EVENTS LIVE IN THE TASKS PANE (owner, 2026-10-20)
1. CLICKABLE FILE ARTIFACTS: when a task generates a file, the file's path in the tasks pane is a CLICKABLE LINK that opens its location (OSC 8 hyperlink with a file:// URL — Konsole opens the file manager/terminal at the path; graceful plain-text degrade elsewhere per the detect-then-render law). The pane shows what was made; the link takes you to it.
2. TOOL EVENTS MOVE: tool usage display moves OUT of the chat pane INTO the tasks pane (the work block's current-line already shows the newest tool action — ALL tool events belong there, in the work stream, not the conversation). Chat stays conversational: the tool event leaves the transcript as chat content and becomes work-stream detail. The D43 layer law and the pane doctrine (D44) both point this way.
### D46 addendum — Q IS A LETTER NOW (owner, 2026-10-20: "change the q for quit... if i type q it will exit")
1. PLAIN q TYPES: q is a letter the composer owns — it must never quit.
2. THE EXIT GESTURE: ctrl+c clears the chat/composer on the first press; TWO ctrl+c IN SUCCESSION quits the program. (A deliberate double-press — you can't quit by accident while typing.)
3. Every other quit binding (q, esc-as-quit paths) follows the same rule: nothing quits on a single unmodified keypress while the composer can receive text.
## D47 — SIX LIVE-USE DEFECTS + OPENSPEC ROUTING (owner, 2026-10-20)
OPENSPEC ROUTING: the tabs/windows design AND the chat-presentation fixes go through the OpenSpec interview process — the owner specifies, the spec records, the factory builds. Two openspec topics opened.
DEFECTS (each red-first, each PTY-proven):
1. ANSWER DISPLAY BROKEN: Rune's answers render incorrectly in the chat pane (live-use report — diagnose the exact symptom on the owner's next boot: markdown artifacts, broken wrapping, or encoding; the word-safe wrap landed but something else misrenders).
2. CODE NOT ROUTED TO THE TASKS PANE: code content in answers renders in the chat pane, not the tasks pane (D44 doctrine: work content lives in the workshop pane; the code-block projection needs routing).
3. EVERY SENTENCE BULLET-POINTED: answers render with per-sentence bullets — a markdown-list misparse (the renderer treats prose as list items). Diagnose and fix the markdown handling.
4. CONTEXT DEADLINE ERRORS PERSIST: the 480s budget didn't take on the live path (diagnose which hop still runs 330s — the fix landed but a leg is stale or the live turn exceeds even 480s on the 27B; measure the real turn duration).
5. CONTEXT READOUT PER-MODEL ACCURACY: context available must be accurate FOR EVERY MODEL LOADED (the limit source must be per-model: Ollama /api/show for locals, the registry limit for cloud — not a single cached value).
6. VERSION HYPERLINK STILL NOT RENDERING (the OSC 8 fix landed but the bar doesn't show it — diagnose the live render path; maybe the deployed binary predates the fix or the detection gate is misfiring).
7. MODEL OPENS THE PICKER: clicking the model name in the info bar opens the centered model picker (the picker's second road — the click road beside ctrl+m).
+43
View File
@@ -0,0 +1,43 @@
# CI E2E Rig (`scripts/ci_e2e_rig.sh`)
The permanent, reusable CI end-to-end proof rig, promoted from the
release deep-dive's tag-repair proof rig (contract
`ADMIT-wave-ci-e2e-rig`).
## What it does
1. Registers a **scratch** Gitea runner (labels: `rig-scratch`).
2. Creates a **scratch** repo (`rig-scratch-<timestamp>`).
3. Pushes a minimal trigger commit + the workflow under test.
4. Runs the workflow end-to-end on the scratch runner.
5. Verifies outputs (run state + expected artifacts).
6. Tears everything down — **teardown-safe**: repo delete, runner
unregister, and scratch-dir removal all run from an `EXIT` trap, so
a failed proof run never leaks resources.
## Modes
- `scripts/ci_e2e_rig.sh --dry-run` — prints the plan, launches nothing
(this is what the contract tests exercise; no live Gitea required).
- `scripts/ci_e2e_rig.sh --workflow tui-release.yml [--keep]` — live
proof run. Requires `RUNE_GITEA_URL` + `RUNE_GITEA_TOKEN`; fails
loudly when either is missing.
## Safety properties
- Scratch-only: the production repo and production runners are never
touched; the rig creates its own namespace-bound resources.
- Teardown on failure: the `EXIT` trap fires on any non-zero path.
- `--keep` retains the scratch dir for post-mortem (still tears down
the runner and repo).
## Guarded regressions
`tests/test_workflow_script_contract.py` extended to cover the three
pipelines (`auto-version.yml`, `tui-release.yml`, `maintenance.yml`)
so any future edit reintroducing the **SIGPIPE pipe**
(`git tag --sort=-v:refname | head -1` under pipefail — exit 141, the
runs-29929/30435/30636 class) or dropping the
**GITEA_REF_NAME tag fallback** (GITEA_REF_NAME → GITHUB_REF_NAME →
`git describe --tags --exact-match`, empty on the rune-ci-desktop
runner) goes RED at test time, not in production.
+113 -98
View File
@@ -1,13 +1,13 @@
{
"schema": 1,
"version": "1.0.0",
"updated_at": "2026-10-18",
"law": "Owner directive 2026-10-17 (docs/factory/spawn-queue.md scope law): every factory registry, gate, and map covers the FULL codebase \u2014 tui/ packages, src/go/ subsystems/libraries, src/rust/ crates (the dichotomy), and the live src/rune/ Python authority modules (retirement-tracked). Owner critique this registry answers: scaffolding was designed to let the LLM instantly look up a domain and find the relevant area to fix \u2014 instead diagnosis had been grep-exploration.",
"updated_at": "2026-10-19",
"law": "Owner directive 2026-10-17 (docs/factory/spawn-queue.md scope law): every factory registry, gate, and map covers the FULL codebase — tui/ packages, src/go/ subsystems/libraries, src/rust/ crates (the dichotomy), and the live src/rune/ Python authority modules (retirement-tracked). Owner critique this registry answers: scaffolding was designed to let the LLM instantly look up a domain and find the relevant area to fix — instead diagnosis had been grep-exploration.",
"gate": "scripts/check_domain_registry.py (unified_check member domain_registry)",
"lookup": "scripts/query_domain.py",
"retirement_map": "docs/retirement-map.md + verification/retirement-map.json",
"dichotomy_law": "docs/architecture/boundary-law.md (D30: WASM-standard, cgo exception-only)",
"scaffold_contracts": "store/scaffold \u2014 every wave cites its ADMIT contract id",
"scaffold_contracts": "store/scaffold — every wave cites its ADMIT contract id",
"retirement_pending": [
{
"path": "src/rune/__main__.py",
@@ -59,7 +59,7 @@
"key_files": [
{
"path": "tui/tasks/live.go",
"why": "live task tree face \u2014 open here first"
"why": "live task tree face — open here first"
},
{
"path": "tui/tasks/live_delegation.go",
@@ -75,7 +75,7 @@
}
],
"test_batteries": [
"go test ./tasks (tui module \u2014 unified_check go_suite)",
"go test ./tasks (tui module — unified_check go_suite)",
"go test (module) src/go/auto-tasks + src/go/auto_tasks",
"cargo test task_gate task_persist (unified_check rust_suite)",
"src/go/parity-harness row-12 end-to-end (unified_check migration_mutants)",
@@ -108,7 +108,7 @@
"key_files": [
{
"path": "tui/threads/README.md",
"why": "what threads render \u2014 open first"
"why": "what threads render — open first"
},
{
"path": "tui/threads/delegation.go",
@@ -116,7 +116,7 @@
}
],
"test_batteries": [
"go test ./threads (tui module \u2014 unified_check go_suite)"
"go test ./threads (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -157,7 +157,7 @@
"key_files": [
{
"path": "tui/stream/stream.go",
"why": "bounded stream adapter \u2014 open here first"
"why": "bounded stream adapter — open here first"
},
{
"path": "src/go/event-bus/main.go",
@@ -169,7 +169,7 @@
}
],
"test_batteries": [
"go test ./stream (tui module \u2014 unified_check go_suite)",
"go test ./stream (tui module — unified_check go_suite)",
"go test (module) src/go/event-bus + src/go/event-envelope + src/go/event-wait",
"tests/test_legacy_event_bus.py",
"tests/test_legacy_event_envelope.py",
@@ -201,7 +201,7 @@
"key_files": [
{
"path": "tui/rail/rail.go",
"why": "activity rail \u2014 open here first"
"why": "activity rail — open here first"
},
{
"path": "tui/receipts/receipts.go",
@@ -209,8 +209,8 @@
}
],
"test_batteries": [
"go test ./rail (tui module \u2014 unified_check go_suite)",
"go test ./receipts (tui module \u2014 unified_check go_suite)"
"go test ./rail (tui module — unified_check go_suite)",
"go test ./receipts (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -237,7 +237,7 @@
"key_files": [
{
"path": "tui/lanes/lanes.go",
"why": "lane layout \u2014 open here first"
"why": "lane layout — open here first"
},
{
"path": "tui/laneconc/laneconc.go",
@@ -249,9 +249,9 @@
}
],
"test_batteries": [
"go test ./lanes (tui module \u2014 unified_check go_suite)",
"go test ./laneconc (tui module \u2014 unified_check go_suite)",
"go test ./nav (tui module \u2014 unified_check go_suite)"
"go test ./lanes (tui module — unified_check go_suite)",
"go test ./laneconc (tui module — unified_check go_suite)",
"go test ./nav (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -277,11 +277,11 @@
"key_files": [
{
"path": "tui/promote/promote.go",
"why": "promotion controls \u2014 open here first"
"why": "promotion controls — open here first"
}
],
"test_batteries": [
"go test ./promote (tui module \u2014 unified_check go_suite)"
"go test ./promote (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -305,11 +305,11 @@
"key_files": [
{
"path": "tui/selection/suite_test.go",
"why": "the law lives in its frozen suite \u2014 open here first"
"why": "the law lives in its frozen suite — open here first"
}
],
"test_batteries": [
"go test ./selection (tui module \u2014 unified_check go_suite)"
"go test ./selection (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -346,7 +346,7 @@
"key_files": [
{
"path": "tui/evidence/evidence.go",
"why": "evidence spine face \u2014 open here first"
"why": "evidence spine face — open here first"
},
{
"path": "src/rune/evidence_probe.py",
@@ -354,7 +354,7 @@
}
],
"test_batteries": [
"go test ./evidence (tui module \u2014 unified_check go_suite)",
"go test ./evidence (tui module — unified_check go_suite)",
"go test (module) src/go/evidence-probe",
"tests/test_evidence_probe.py",
"tests/test_evidence_provenance_shims.py",
@@ -391,7 +391,7 @@
}
],
"test_batteries": [
"go test ./history (tui module \u2014 unified_check go_suite)"
"go test ./history (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -417,7 +417,7 @@
"key_files": [
{
"path": "tui/changes/changes.go",
"why": "changes view \u2014 open here first"
"why": "changes view — open here first"
},
{
"path": "src/go/restore-points/main.go",
@@ -425,7 +425,7 @@
}
],
"test_batteries": [
"go test ./changes (tui module \u2014 unified_check go_suite)",
"go test ./changes (tui module — unified_check go_suite)",
"go test (module) src/go/restore-points"
],
"serve_endpoints": [],
@@ -459,7 +459,7 @@
}
],
"test_batteries": [
"go test ./settings (tui module \u2014 unified_check go_suite)"
"go test ./settings (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -492,7 +492,7 @@
"key_files": [
{
"path": "tui/graphproj/model.go",
"why": "projection model \u2014 open here first"
"why": "projection model — open here first"
},
{
"path": "tui/graphproj/export.go",
@@ -500,7 +500,7 @@
}
],
"test_batteries": [
"go test ./graphproj (tui module \u2014 unified_check go_suite)",
"go test ./graphproj (tui module — unified_check go_suite)",
"go test (module) src/go/graph-memory",
"tests/test_graph_projection.py",
"cargo test graph_projection graph_memory"
@@ -529,7 +529,7 @@
"key_files": [
{
"path": "tui/capsys/gate.go",
"why": "the gate system \u2014 open here first"
"why": "the gate system — open here first"
},
{
"path": "tui/capsys/budget.go",
@@ -537,7 +537,7 @@
}
],
"test_batteries": [
"go test ./capsys (tui module \u2014 unified_check go_suite)"
"go test ./capsys (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -569,7 +569,7 @@
}
],
"test_batteries": [
"go test ./version (tui module \u2014 unified_check go_suite)",
"go test ./version (tui module — unified_check go_suite)",
"go test (module) src/go/release-notes",
"tests/test_check_version_surfaces.py"
],
@@ -602,7 +602,7 @@
}
],
"test_batteries": [
"go test ./update (tui module \u2014 unified_check go_suite)"
"go test ./update (tui module — unified_check go_suite)"
],
"serve_endpoints": [],
"symptoms": [
@@ -630,7 +630,7 @@
"key_files": [
{
"path": "tui/wasm/wasm.go",
"why": "wazero host \u2014 open here first"
"why": "wazero host — open here first"
},
{
"path": "tui/wasm/ipc.go",
@@ -654,7 +654,7 @@
"tui/wasm/modules missing the paired build manifest"
],
"scaffold_contract": "ADMIT-wave-wasm-token-counting",
"notes": "D30 law: Rust crosses to Go as WASM (wazero AOT); cgo is exception-only. Module artifacts are NOT committable \u2014 rebuilt reproducibly by scripts/build_paired.sh."
"notes": "D30 law: Rust crosses to Go as WASM (wazero AOT); cgo is exception-only. Module artifacts are NOT committable — rebuilt reproducibly by scripts/build_paired.sh."
},
{
"domain": "runtime-bridge",
@@ -676,7 +676,7 @@
"key_files": [
{
"path": "tui/bridge/bridge.go",
"why": "the bridge \u2014 open here first"
"why": "the bridge — open here first"
},
{
"path": "tui/bridge/client.go",
@@ -692,8 +692,8 @@
}
],
"test_batteries": [
"go test ./bridge (tui module \u2014 unified_check go_suite)",
"go test ./runtime (tui module \u2014 unified_check go_suite)",
"go test ./bridge (tui module — unified_check go_suite)",
"go test ./runtime (tui module — unified_check go_suite)",
"go test (module) src/go/rune-entry",
"tui/bridge nosim_test.go (no-simulation law)"
],
@@ -726,7 +726,7 @@
"key_files": [
{
"path": "src/go/core-server/main.go",
"why": "every route lives here \u2014 open first"
"why": "every route lives here — open first"
}
],
"test_batteries": [
@@ -767,11 +767,11 @@
},
{
"path": "src/go/alert-gateway/main.go",
"why": "gateway kernel (0 tests \u2014 known gap, retirement row 9)"
"why": "gateway kernel (0 tests — known gap, retirement row 9)"
}
],
"test_batteries": [
"go test ./alerts (tui module \u2014 unified_check go_suite)",
"go test ./alerts (tui module — unified_check go_suite)",
"go test (module) src/go/alert-gateway"
],
"serve_endpoints": [],
@@ -823,7 +823,7 @@
"key_files": [
{
"path": "tui/memory/memory.go",
"why": "pure render over canonical state \u2014 open here first"
"why": "pure render over canonical state — open here first"
},
{
"path": "src/go/context-broker/main.go",
@@ -835,7 +835,7 @@
}
],
"test_batteries": [
"go test ./memory (tui module \u2014 unified_check go_suite)",
"go test ./memory (tui module — unified_check go_suite)",
"go test (module) src/go/context-broker + src/go/memory-store-deep + src/go/memory-consolidation + src/go/memory-ops",
"tests/test_memory_continuity.py",
"tests/test_memory_port.py",
@@ -901,14 +901,14 @@
}
],
"test_batteries": [
"go test ./skills (tui module \u2014 unified_check go_suite)",
"go test ./skills (tui module — unified_check go_suite)",
"go test (module) src/go/curator + src/go/curator-core"
],
"serve_endpoints": [],
"symptoms": [
"skill/pattern proposal shown without the consent gate",
"tested/approved skill activation view wrong",
"episode\u2192skill promotion ladder stuck"
"episode→skill promotion ladder stuck"
],
"scaffold_contract": "ADMIT-wave-k21-skills-engine",
"retirement_row": 16
@@ -940,7 +940,7 @@
}
],
"test_batteries": [
"go test ./extensions (tui module \u2014 unified_check go_suite)",
"go test ./extensions (tui module — unified_check go_suite)",
"go test (module) src/go/extension-host"
],
"serve_endpoints": [],
@@ -976,7 +976,7 @@
}
],
"test_batteries": [
"go test ./devices (tui module \u2014 unified_check go_suite)",
"go test ./devices (tui module — unified_check go_suite)",
"go test (module) src/go/device-pairing"
],
"serve_endpoints": [],
@@ -1003,7 +1003,7 @@
"key_files": [
{
"path": "src/go/model-acquisition/main.go",
"why": "open here first \u2014 module main"
"why": "open here first — module main"
},
{
"path": "src/go/model-connect/main.go",
@@ -1077,7 +1077,7 @@
"key_files": [
{
"path": "src/rune/openai_compat_transport.py",
"why": "THE provider-prefix single authority (is_cloud_pinned) \u2014 open here first"
"why": "THE provider-prefix single authority (is_cloud_pinned) — open here first"
},
{
"path": "src/go/model-routing/main.go",
@@ -1094,7 +1094,7 @@
"serve_endpoints": [],
"symptoms": [
"wrong provider/model selected; route authority flags missed",
"cloud-pinned model misbudgeted (400 invalid model name noise \u2014 the proven real change)",
"cloud-pinned model misbudgeted (400 invalid model name noise — the proven real change)",
"recall expansion drifts; warm/adaptive router pick stale routes"
],
"notes": "The 2026-09-12 scaffolding real-change proof lives here (is_cloud_pinned single authority)."
@@ -1114,7 +1114,7 @@
"key_files": [
{
"path": "src/go/webfeed/main.go",
"why": "open here first \u2014 module main"
"why": "open here first — module main"
}
],
"test_batteries": [
@@ -1156,7 +1156,7 @@
"key_files": [
{
"path": "src/go/workflow-engine/main.go",
"why": "engine kernel \u2014 open here first"
"why": "engine kernel — open here first"
},
{
"path": "tui/workflows/history.go",
@@ -1164,14 +1164,14 @@
}
],
"test_batteries": [
"go test ./workflows (tui module \u2014 unified_check go_suite)",
"go test ./workflows (tui module — unified_check go_suite)",
"go test (module) src/go/workflow-engine + src/go/cron-engine",
"tests/test_deployment_canary.py"
],
"serve_endpoints": [],
"symptoms": [
"unattended workflow stalls, fires twice, or misses its cron",
"sleep\u2192poll regression undetected (wait_guard corpus)",
"sleep→poll regression undetected (wait_guard corpus)",
"workflow-engine module go test RED (known retirement-map finding)",
"night-mode switch flips at the wrong boundary"
],
@@ -1208,7 +1208,7 @@
}
],
"test_batteries": [
"go test ./modes (tui module \u2014 unified_check go_suite)",
"go test ./modes (tui module — unified_check go_suite)",
"go test (module) src/go/build-mode"
],
"serve_endpoints": [],
@@ -1248,13 +1248,13 @@
}
],
"test_batteries": [
"go test ./inputs (tui module \u2014 unified_check go_suite)",
"go test ./inputs (tui module — unified_check go_suite)",
"go test (module) src/go/auto-detect"
],
"serve_endpoints": [],
"symptoms": [
"universal input misclassified; inspect-before-connect skipped",
"auto-detect truth battery unused (0 tests \u2014 known gap)"
"auto-detect truth battery unused (0 tests — known gap)"
],
"scaffold_contract": "ADMIT-wave-universal-input",
"retirement_row": 14
@@ -1276,7 +1276,7 @@
"key_files": [
{
"path": "docs/authority-map.md",
"why": "the map itself \u2014 open here first"
"why": "the map itself — open here first"
},
{
"path": "config/umbrellas.json",
@@ -1316,11 +1316,11 @@
"key_files": [
{
"path": "src/rune/audit_ledger.py",
"why": "deployed authority \u2014 open here first"
"why": "deployed authority — open here first"
},
{
"path": "src/rust/claim_audit/src/lib.rs",
"why": "Rust claim-audit kernel (src/rust/audit_ledger/ holds only build artifacts \u2014 no crate there; the Python module is the authority)"
"why": "Rust claim-audit kernel (src/rust/audit_ledger/ holds only build artifacts — no crate there; the Python module is the authority)"
}
],
"test_batteries": [
@@ -1370,7 +1370,7 @@
},
{
"domain": "critic-gate",
"summary": "The critic gate: repair of the empty critic gate + auto-merge zero approvals (task 0.4) \u2014 protected classes require review; drift_core binds.",
"summary": "The critic gate: repair of the empty critic gate + auto-merge zero approvals (task 0.4) — protected classes require review; drift_core binds.",
"aliases": [],
"packages": {
"tui": [],
@@ -1387,7 +1387,7 @@
"key_files": [
{
"path": "src/rune/drift_core.py",
"why": "task 0.4 binding \u2014 open here first"
"why": "task 0.4 binding — open here first"
},
{
"path": "src/go/drift-core/main.go",
@@ -1404,7 +1404,7 @@
"auto-merge bypasses repository protection",
"always-pass/skipped critic not failing closed"
],
"notes": "Task 0.4 also binds merge_robot.py (not present in src/rune today \u2014 the task owns creating/repairing it)."
"notes": "Task 0.4 also binds merge_robot.py (not present in src/rune today — the task owns creating/repairing it)."
},
{
"domain": "drift-telemetry",
@@ -1519,7 +1519,7 @@
],
"test_batteries": [
"tests/test_owner_envelope.py",
"go test ./trust (tui module \u2014 unified_check go_suite)",
"go test ./trust (tui module — unified_check go_suite)",
"go test (module) src/go/stage-guards + src/go/persona-gate"
],
"serve_endpoints": [],
@@ -1588,7 +1588,7 @@
"key_files": [
{
"path": "schemas/events/corpus/event-checkpoint_committed-001.json",
"why": "the vocabulary \u2014 open here first"
"why": "the vocabulary — open here first"
}
],
"test_batteries": [
@@ -1619,7 +1619,7 @@
"key_files": [
{
"path": "scripts/deploy_promotion.py",
"why": "monotonic ancestry law, atomic symlink swap \u2014 open here first"
"why": "monotonic ancestry law, atomic symlink swap — open here first"
},
{
"path": "docs/release-flow.md",
@@ -1643,7 +1643,7 @@
},
{
"domain": "parent-map",
"summary": "The parent/child delegation ancestry map (child visibility): a missing parent never hides the child \u2014 the orphan dock.",
"summary": "The parent/child delegation ancestry map (child visibility): a missing parent never hides the child — the orphan dock.",
"aliases": [
"child-visibility",
"delegation-ancestry"
@@ -1663,7 +1663,7 @@
"key_files": [
{
"path": "tui/tasks/live_delegation.go",
"why": "orphan dock + nesting law \u2014 open here first"
"why": "orphan dock + nesting law — open here first"
},
{
"path": "tui/threads/delegation.go",
@@ -1675,8 +1675,8 @@
}
],
"test_batteries": [
"go test ./tasks (tui module \u2014 unified_check go_suite)",
"go test ./threads (tui module \u2014 unified_check go_suite)",
"go test ./tasks (tui module — unified_check go_suite)",
"go test ./threads (tui module — unified_check go_suite)",
"tests/test_delegation_pinned_paths.py",
"tests/test_orchestration_delegation.py"
],
@@ -1727,7 +1727,7 @@
"key_files": [
{
"path": "src/rune/witness.py",
"why": "witness authority \u2014 open here first"
"why": "witness authority — open here first"
},
{
"path": "src/go/witness/main.go",
@@ -1743,7 +1743,7 @@
"symptoms": [
"work outcome not recorded or unlearned (outcome_utility)",
"trace lost mid-inference; programme gate skipped",
"operator loop stuck; work-manager/output untested (0 tests \u2014 known gap)"
"operator loop stuck; work-manager/output untested (0 tests — known gap)"
],
"retirement_row": 10
},
@@ -1812,7 +1812,7 @@
"key_files": [
{
"path": "src/rune/microkernel.py",
"why": "kernel \u2014 open here first"
"why": "kernel — open here first"
},
{
"path": "src/rune/orchestration/scheduler.py",
@@ -1884,7 +1884,7 @@
"key_files": [
{
"path": "src/go/cognition/cognition.go",
"why": "Go cognition kernel \u2014 open here first"
"why": "Go cognition kernel — open here first"
},
{
"path": "src/rust/cognition_core/src/lib.rs",
@@ -1939,7 +1939,7 @@
"key_files": [
{
"path": "src/go/tool-executor/main.go",
"why": "executor \u2014 open here first"
"why": "executor — open here first"
},
{
"path": "src/rune/tool_protocol.py",
@@ -2072,7 +2072,7 @@
"key_files": [
{
"path": "src/rune/scaffold.py",
"why": "project scaffold authority \u2014 open here first"
"why": "project scaffold authority — open here first"
},
{
"path": "src/rune/construction_admission.py",
@@ -2118,7 +2118,7 @@
"key_files": [
{
"path": "src/rune/spine.py",
"why": "spine authority \u2014 open here first"
"why": "spine authority — open here first"
},
{
"path": "docs/STRUCTURAL_MAP.md",
@@ -2143,8 +2143,10 @@
"aliases": [],
"packages": {
"tui": [
"tui/semantic",
"tui/internal/toolkit"
"tui/chatpane",
"tui/internal/toolkit",
"tui/lanes",
"tui/semantic"
],
"src_go": [
"src/go/tui"
@@ -2170,7 +2172,7 @@
"key_files": [
{
"path": "tui/semantic/model.go",
"why": "the behavior/semantic map \u2014 open here first"
"why": "the behavior/semantic map — open here first"
},
{
"path": "src/rune/rune_tui.py",
@@ -2179,10 +2181,22 @@
{
"path": "src/go/tui/main.go",
"why": "the Bubble Tea prototype (R2 W7)"
},
{
"path": "tui/lanes/render.go",
"why": "the composer LAYER SCAFFOLD (D43 layer law): border/content/cursor layers, each with its own tests — locked surface, visual changes owner-routed"
},
{
"path": "tui/chatpane/chatpane.go",
"why": "the bubble renderer (D43 locked surface): direction-aware sides, layer-isolated scroll indicator, timestamps"
},
{
"path": "tui/chatpane/scroll.go",
"why": "the scrollbar LAYER + the scrollback law (fade-on-idle, stay-put)"
}
],
"test_batteries": [
"go test ./semantic (tui module \u2014 unified_check go_suite)",
"go test ./semantic (tui module — unified_check go_suite)",
"tests/test_pane_authority_port.py",
"canary_tui_drift.py",
"canary_tui_transport.py",
@@ -2195,7 +2209,8 @@
"Textual oracle drifts from the native face",
"tui_validator import-allowlist drift"
],
"retirement_row": 15
"retirement_row": 15,
"notes": "D43 layer law: every visual element is an independent layer (border/content/cursor/scroll-indicator/animation) with its own scaffold and tests inside this governed build; layers compose at render time; accidental coupling is a defect. Locked surface — visual changes owner-routed per D43 addendum."
},
{
"domain": "degradation-ledger",
@@ -2230,19 +2245,19 @@
}
],
"test_batteries": [
"go test ./degrade (tui module \u2014 unified_check go_suite)",
"go test ./degrade (tui module — unified_check go_suite)",
"go test (module) src/go/subsystem-health + src/go/failure-filer"
],
"serve_endpoints": [],
"symptoms": [
"degradation invisible (silent handler \u2014 the P0-06 class)",
"degradation invisible (silent handler — the P0-06 class)",
"subsystem health mis-reported",
"failure filing lost; recovery policy misclassifies"
]
},
{
"domain": "boundary-types",
"summary": "The Go\u2194Rust boundary message types: schemas/boundary/v1 + generated types on both sides (task 0.12 one-schema-source law).",
"summary": "The Go↔Rust boundary message types: schemas/boundary/v1 + generated types on both sides (task 0.12 one-schema-source law).",
"aliases": [],
"packages": {
"tui": [
@@ -2257,7 +2272,7 @@
"key_files": [
{
"path": "schemas/boundary/v1/status_snapshot.schema.json",
"why": "canonical schema set \u2014 open here first"
"why": "canonical schema set — open here first"
}
],
"test_batteries": [
@@ -2287,7 +2302,7 @@
"key_files": [
{
"path": "src/go/parity-harness/harness.go",
"why": "the parity machine \u2014 open here first"
"why": "the parity machine — open here first"
},
{
"path": "store/rebase/classification.json",
@@ -2326,7 +2341,7 @@
"key_files": [
{
"path": "docs/seams/registry.json",
"why": "the registry \u2014 open here first"
"why": "the registry — open here first"
},
{
"path": "scripts/check_seam_registry.py",
@@ -2342,7 +2357,7 @@
"serve_endpoints": [],
"symptoms": [
"seam record incomplete (RED); deletion test missing/failing",
"review date expired (AMBER \u2014 paper expiry is a documented warning)",
"review date expired (AMBER — paper expiry is a documented warning)",
"renewal mechanism detected (automatic renewal laundering = RED)"
],
"scaffold_contract": "ADMIT-wave-seam-registry"
@@ -2364,7 +2379,7 @@
"key_files": [
{
"path": "docs/factory/research-registry.json",
"why": "slug\u2192chain map \u2014 open here first"
"why": "slug→chain map — open here first"
},
{
"path": "scripts/check_research_gate.py",
@@ -2446,7 +2461,7 @@
}
],
"test_batteries": [
"go test ./internal/toolkit (tui module \u2014 unified_check go_suite)",
"go test ./internal/toolkit (tui module — unified_check go_suite)",
"go test (module) src/go/paths + src/go/pins"
],
"serve_endpoints": [],
@@ -2481,7 +2496,7 @@
"key_files": [
{
"path": "src/rune/voice_hybrid.py",
"why": "unified voice \u2014 open here first"
"why": "unified voice — open here first"
}
],
"test_batteries": [
@@ -2497,7 +2512,7 @@
},
{
"domain": "dichotomy-loaders",
"summary": "Mechanical FFI parity facades: one loader per ported Rust crate (src/rune/*_loader.py) \u2014 the Python oracle side of the dichotomy.",
"summary": "Mechanical FFI parity facades: one loader per ported Rust crate (src/rune/*_loader.py) — the Python oracle side of the dichotomy.",
"aliases": [],
"packages": {
"tui": [],
@@ -2595,7 +2610,7 @@
"key_files": [
{
"path": "src/rune/ac_loader.py",
"why": "pattern example \u2014 open here first"
"why": "pattern example — open here first"
}
],
"test_batteries": [
@@ -2653,7 +2668,7 @@
"key_files": [
{
"path": "src/rune/canary_native.py",
"why": "the module registry \u2014 open here first"
"why": "the module registry — open here first"
}
],
"test_batteries": [
@@ -2688,7 +2703,7 @@
"key_files": [
{
"path": "src/go/factory/internal/factory/kernel.go",
"why": "the reconcile kernel: admit, level-triggered tick, phase advance, close \u2014 open here first"
"why": "the reconcile kernel: admit, level-triggered tick, phase advance, close — open here first"
},
{
"path": "src/go/factory/internal/factory/queue.go",
@@ -2708,8 +2723,8 @@
}
],
"test_batteries": [
"go test ./... (module src/go/factory \u2014 kernel, queue, ladder batteries)",
"go test ./factory/... (tui module \u2014 frozen TUI surface render battery)",
"go test ./... (module src/go/factory — kernel, queue, ladder batteries)",
"go test ./factory/... (tui module — frozen TUI surface render battery)",
"pytest tests/test_factory_protocol.py (twin parity vs the frozen oracle)",
"pytest tests/test_seam_deletion.py + scripts/check_seam_registry.py (factory-orchestration socket)"
],
@@ -2723,4 +2738,4 @@
]
}
]
}
}
+7
View File
@@ -42,3 +42,10 @@ This is a serious, year-scale development project. Not a light topic. The factor
### 6. Stop conditions — CONTINUOUS WORK + RECORDED STOPPING POINT
The factory is done after: (a) specifying what work needs to be continuous (memory consolidation, drift checks, scheduled jobs — the autonomic system) and (b) a stopping point is recorded (the spec implemented, Python retired, the continuous work defined). The factory doesn't run forever; it runs until the stopping point.
## ISSUE-LEDGER LAW (owner, 2026-10-19: "are issues being made and closed on gitea as improvements are being made so versioning stays correct?")
Every landing files/links a Gitea issue and closes it in the same change — the issue ledger tracks improvements the way tags track versions. Batch retroactive filing (#2491-#2497) covered 2026-10-18's work. Going forward: the main session files the issue at landing time (or the wave brief names it); the commit message carries "closes #N" so the push closes it. The version clock and the issue ledger stay in lockstep.
## WAVE-BRIEF COMPLIANCE LAW (owner audit, 2026-10-19)
Every build-wave brief MUST carry the three standing requirements verbatim: (1) SCAFFOLDING ADMISSION — mint the construction contract via the admit script before building (scripts/admit_wave_*.py pattern; no freehand edits outside the scaffolding authority); (2) ISSUE LEDGER — name the Gitea issue (main session files/closes at landing); (3) RESEARCH GATE — PROCEED chain or declared exemption, per the mechanical gate. Briefs missing any of the three are non-compliant — the spawner (main session) owns the omission. Today's interactive-session waves were spawned without (1) — recorded as drift; the backfill wave registers the omitted contracts.
Backfill landed 2026-10-19 (scripts/admit_wave_backfill_interactive_2026_10_18.py): seven contracts minted retroactively at their honest landed state — ADMIT-wave-chat-surface-d34-36 (58d8ae97), ADMIT-wave-go-turn-engine-cloud-routing (0d99d5a7), ADMIT-wave-d41-amalgamation (8e1a09e1), ADMIT-wave-task-ledger-d38 (ca915c60), ADMIT-wave-convergence-landing (e4514523 + original aa5f1917), ADMIT-fix-history-bubble-landed (4347ad05), ADMIT-wave-firstrun-cloud-first (2ba3a437) — each closed on landing-sha evidence with the landing commits' verification receipts recorded verbatim (not re-executed); registry entries added for factory-visual-round, factory-providers-live, and the task-ledger exemption.
+408 -283
View File
@@ -1,284 +1,409 @@
{
"schema": 1,
"law": "Owner directive 2026-10-16 (docs/factory/spawn-queue.md): every feature wave runs the canonical research protocol FIRST (ISSUE, INWARD, OUTWARD, APPLICABILITY, INNOVATION-on-merit, ADVERSARIAL, SYNTHESIS, QUALIFY); the build wave spawns only on QUALIFY: PROCEED; MORE_RESEARCH loops back with named unknowns. This registry maps feature slugs to their research artifact chains so scripts/check_research_gate.py can enforce the law mechanically (admission-time gate + post-hoc sweep). Exemptions must be declared explicitly, never silent: mechanical-repair (spec-pinned, research exists at spec level), test-doc-only, owner-directive (directive quoted).",
"gate": "scripts/check_research_gate.py",
"updated_at": "2026-10-18",
"entries": [
{
"slug": "tui-build-program",
"match": [
"wave-*",
"wave",
"rune",
"tui",
"boundary",
"g-series",
"h-series",
"i-series",
"j-series",
"k-series"
],
"chain": "research/tui-frontier-r2/qualify.json",
"verdict": "PROCEED",
"note": "The add-native-rune-tui build program (B/C/D/E/F/G/H/I/J/K wave series under docs/openspec/changes/add-native-rune-tui/) executes the R2 frontier round; every program wave dispatch cites the round (e.g. admit_wave_seam_registry.py evidence refs)."
},
{
"slug": "wasm-prototype",
"match": [
"wasm-*"
],
"chain": "verification/wasm-prototype-report.json",
"verdict": "YIELDS",
"note": "Owner verdict YIELDS (docs/architecture/boundary-law.md: token_counting end-to-end YIELDS; prototype-only, production migration wave 1 opened under the standing directive). Prototype rounds resolve through this chain; a production wasm migration wave requires its own PROCEED round."
},
{
"slug": "memory-continuity",
"match": [
"*memory-continuity*",
"sleight-matrix",
"sleight-deliverables"
],
"exemption": {
"category": "mechanical-repair",
"reason": "Spec-pinned: research exists at spec level (Sleight memory-context-continuity evidence, bound by the B-SLEIGHT-MATRIX / B-SLEIGHT-DELIVERABLES briefs); bounded mechanical execution, no new research questions."
}
},
{
"slug": "bounded-mechanical-repairs",
"match": [
"repair-worker",
"repair-*",
"*-repair",
"fix-*"
],
"exemption": {
"category": "mechanical-repair",
"reason": "Bounded mechanical repairs dispatched against already-researched, spec-pinned defects (e.g. the five 0.9 defects cite the R2 round); no new research questions opened."
}
},
{
"slug": "substrate-ci-machinery",
"match": [
"substrate-worker",
"trial-worker",
"*-harness-check",
"research-gate",
"fuzz-*"
],
"exemption": {
"category": "owner-directive",
"reason": "Substrate/CI machinery (boundary codegen corpus, landing checks, harness trials, the research gate itself): per owner directive these are mechanical infrastructure, not feature waves; quoted from the research-gate enforcement dispatch: 'this wave is substrate/CI machinery \u2014 exempt as mechanical infrastructure, exemption recorded: research-gate machinery itself, per owner directive'."
}
},
{
"slug": "issue-161",
"match": [
"issue-161"
],
"chain": "research/issue-161/qualify.json",
"verdict": "PROCEED",
"note": "Per-issue research rounds live at research/issue-<n>/qualify.json (verdict in the outcome field); the gate's direct-dir resolution research/<slug>/qualify.json handles every other issue slug before any registry fallback."
},
{
"slug": "domain-registry",
"match": [
"domain-registry",
"domain_registry",
"query-domain",
"wave-domain-registry"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-domain-registry, 2026-10-17): the registry machinery IS the response to the owner critique that diagnosis was grep-exploration instead of scaffold lookup \u2014 the deliverable is registry data + lookup tooling over EXISTING authorities (docs/retirement-map.md, store/rebase/classification.json, package dirs), introducing no new research questions. Directive: 'Build the missing map.' Scope law (docs/factory/spawn-queue.md, owner amendment 2026-10-17) names the registry machinery itself as exempt with approval_ref 'user:wave-domain-registry dispatch'.",
"approval_ref": "user:wave-domain-registry dispatch"
}
},
{
"slug": "gitignore-hygiene",
"match": [
"gitignore-hygiene",
"gitignore_hygiene",
"wave-gitignore-hygiene"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-gitignore-hygiene, 2026-11): mechanical hygiene repair of .gitignore rules \u2014 the src/go artifact rule's force-add era (modegate binary fabrication, K9 sweep accident, 102MB cache re-lands) and the cwd-local runtime dir rules swallowing memory/index + memory/journal. Deterministic rule fix + frozen gate, no new research questions. approval_ref 'user:wave-gitignore-hygiene dispatch'.",
"approval_ref": "user:wave-gitignore-hygiene dispatch"
}
},
{
"slug": "rune-launcher",
"match": [
"rune-launcher",
"rune_launcher",
"wave-rune-launcher"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-rune-launcher, main session 2026-10-17): standardize the `rune` command so typing rune boots the new Go TUI face \u2014 launcher standardization is build/deploy plumbing over already-researched surfaces (paired build D26 scripts/build_paired.sh, update-apply D27 tui/update, the python-era ~/.local/bin/rune pinned verbatim as the honest fallback), no new research questions. approval_ref 'user:wave-rune-launcher dispatch'.",
"approval_ref": "user:wave-rune-launcher dispatch"
}
},
{
"slug": "firstrun",
"match": [
"firstrun",
"first-run",
"wave-firstrun",
"b-first-run",
"*firstrun*"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (B-FIRST-RUN, docs/factory/spawn-queue.md NEW PRIORITY 0 + 'First-run law' + 'Fresh-state detection' directives, 2026-10-17): the first-run experience \u2014 fresh-state detection (no provider record + no checkpoint), the welcome/provider-list/model-list/connection-test face (tui/firstrun), and no-default-provider enforcement (chat gated until an explicit selection). The behavior contract is pinned at directive level (the spawn-queue directives quote it); bounded build against the frozen provider_selection.json record law already shipped by the chat-live groundwork, no new research questions. approval_ref 'user:wave-firstrun dispatch'.",
"approval_ref": "user:wave-firstrun dispatch"
}
},
{
"slug": "self-transform",
"match": [
"self-transform*",
"self_transform*"
],
"chain": "research/harness-survey/self-transformation/qualify.json",
"verdict": "PROCEED",
"note": "Spec 6.2/6.2a/6.3 self-transformation waves (openspec task 6.2): the self-transformation research chain completed QUALIFY: PROCEED with D01-D10 owner decisions recorded in the dedicated interview; the build binds synthesis.json exact_candidate_flow (13 stages) with RM01-RM15 + IAM01-IAM22. First consumer: wave-K27-self-transform (src/rune/self_transform_pipeline.py)."
},
{
"slug": "chat-presentation",
"match": [
"chat-presentation",
"chat_presentation",
"chat-box",
"chatbox",
"wave-chat-presentation",
"B-CHAT-PRESENTATION"
],
"chain": "research/factory-chat-presentation/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-asked research round (2026-10-18): 'do research into how to setup chat boxes for harnesses in go' \u2014 the chat input surface (backspace defect root cause: key-spelling fork semantic vs ultraviolet; input box as its own layer). Headline: charm.land/bubbles/v2 v2.2.1 (already pinned, zero go.mod change) ships textinput/textarea/viewport \u2014 the prior 'stub pin' belief was vendor pruning. Build waves for the composer/chat box cite this chain."
},
{
"slug": "apikey-migration",
"match": [
"apikey-migration",
"api_key_migration",
"wave-apikey-migration",
"B-APIKEY-MIGRATION"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-apikey-migration, 2026-10-17, spawn-queue.md#B-APIKEY-MIGRATION, owner-critical): lossless port of ALL API keys and saved connections from mimo code (~/.config/mimocode/mimocode.jsonc, ~/.local/share/mimocode/auth.json, ~/.config/opencode/*, ~/.local/share/opencode/auth.json, ~/.rune/model_state.json) into rune's provider store. Deterministic scan\u2192map\u2192apply\u2192verify machinery + frozen round-trip battery (tests/test_apikey_migration.py, red-first) + secret store per the security law (OS keyring / Secret Service / encrypted fallback, never plaintext in UI/logs). No new research questions. approval_ref 'user:wave-apikey-migration dispatch'.",
"approval_ref": "user:wave-apikey-migration dispatch"
}
},
{
"slug": "factory-separate-turns",
"match": [
"factory-separate-turns",
"separate-turns",
"separate_turns",
"wave-separate-turns",
"turn-lanes",
"chat-task-dichotomy",
"d36-turn-lanes",
"*separate-turns*"
],
"chain": "research/factory-separate-turns/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ordered research round (2026-10-18, D36 era): 'we need to not block the interactivity of being able to talk to the chat pane whenever a task pane task chat message is sent... the chat and the task panes need to operate on separate turns.' The round maps the exact blocking points (dispatch_lock at core_server.py:199 wrapping the whole turn; unsynchronized operator state; full-checkpoint saves measured 2.2MB; task-channel-bound context assembly), verifies Ollama concurrency semantics live (stateless /api/chat, default per-model FIFO queue, OLLAMA_NUM_PARALLEL memory multiplication, engine caveats), and surveys crush / Claude Code / RQ harness+queue patterns. Verdict PROCEED with five named owner decisions (OD-1 model-slot policy is the only hardware-cost call). Build waves for the dichotomy cite this chain."
},
{
"slug": "factory-checkpoint-saves",
"match": [
"factory-checkpoint-saves",
"checkpoint-saves",
"checkpoint_saves",
"wave-checkpoint-saves",
"operator-checkpoint",
"operator-json-checkpoint",
"*checkpoint-save*"
],
"chain": "research/factory-checkpoint-saves/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ordered research round (2026-10-18, D38 deferred the decision to it): 'can you fully explain the issue first so i can diagnose approaches and also suggest a multitude of approaches after research on this specific issue' \u2014 the operator.json full-rewrite checkpoint (measured 2,307,627 bytes, 96.3% immutable task history, ~25ms per save, 3-5 saves per turn, truncate-then-stream with no atomicity, silent reset to defaults on torn load). Mid-round D39 ruling reframed the target ('keep the intention of what was meant to be finished in python and just move to go and fix the go version properly'): the decision matrix feeds the GO core's storage design \u2014 SQLite via modernc.org/sqlite (D30-legal, memory-ops precedent, pin >=3.51.3 for the WAL-reset fix) vs bbolt vs journal+snapshots (K25 generalized) vs atomic per-subsystem JSON, plus hybrids and an honest Rust-sidecar dismissal (sled self-disqualifies; redb is D30-hostile). 8 live sources with URLs; migration axis first-class per owner addendum; Python stopgap (atomic_write + terminal-task retirement) as Appendix S only. NO single recommendation \u2014 owner diagnoses."
},
{
"slug": "factory-orchestrator",
"match": [
"factory-orchestrator",
"orchestrator",
"orchestrator-layer",
"d37-orchestrator",
"*orchestrator*"
],
"chain": "research/factory-orchestrator/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner directive D37.4 (2026-10-18, boundary-law.md): 'RUNE THE ORCHESTRATOR: her intelligence OWNS all models in play, thinks independently from them, and interprets how best to route context across every model in play. The models are workers; Rune is the mind above them.' Research question: how to architect the orchestrator layer above multiple model workers (chat+task panes, local+cloud) given the landed Go turn engine (ModelClient seam named in engine.go as the D37 insertion point), the advisory rank() port + trust gate (G1-G5, paid spend = hard-ask), the adaptive-router effort classes, the warm-router budget law, and the D40 per-channel journal+SQLite storage shape. 5 live sources: LiteLLM router (rules cascade + ordered failure ladder + sticky sessions + reject-not-queue), OpenRouter auto router (lightweight task classifier -> measured spend-share ranking -> cost bands -> graceful degradation), RouteLLM (tiny learned routers beat LLM judges; 85% cost cut at 95% GPT-4 quality), Anthropic orchestrator-workers (plan-to-memory, task-descriptions-down, digests-up, 15x token cost, resume-from-checkpoint), LangGraph supervisor (handoff-as-tool-call, output_mode knob, forward_message). Synthesis: OrchestratorCore implements ModelClient above the engine (frozen laws untouched; phase-1 pass-through proven by frozen tests); three-tier decision cascade (rules -> measured -> optional tiny judge, off by default); orchestrator context in a dedicated D40 channel (decisions+receipts+verdicts, never worker transcripts); context composition via ContextBroker contributor port (cross-channel contributions explicit+gated); failure ladder = local fallback in-turn, cloud never mid-run (start-tier law), cooldowns, sticky-with-re-ranking, pass-through degradation. 4 owner decisions pending (OD-1 judge tier, OD-2 context placement, OD-3 escalation posture, OD-4 supervisor phase). No code changes."
},
{
"slug": "factory-consent-bridge",
"match": [
"factory-consent-bridge",
"consent-bridge",
"consent_bridge",
"wave-consent-bridge",
"chat-to-task",
"promote-runtime",
"promotion-runtime",
"work-intent",
"work_intent",
"d36-consent-bridge",
"*consent-bridge*"
],
"chain": "research/factory-consent-bridge/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-lawed research round (2026-10-18, D36 section 2, boundary-law.md:90): the consent bridge - the chat pane detects a conversational message is really a work request, Rune proposes the push with a y/n card, consent starts the task instantly (enqueue, never preempt) with the context frozen as a snapshot at consent time. Closes factory-separate-turns OD-5. The round maps the pre-built machinery (tui/promote SA-D3 ladder + Offer(entries) seam awaiting its runtime; construction_intent.py's ACTION|DISCUSSION resolver with the DISCUSSION-PROPOSAL-USER_APPROVED lifecycle and STALE_GAP_TURNS=2 staleness law, already ci_loader-compiled; the needs_confirm envelope precedent; the D38 ledger's Brief slot fed only the title today at ledger.go:266) and reads seven live sources (Rasa CALM's shift to model-self-reported commands; Mycroft adapt's deterministic require/optionally parser; LangGraph checkpoint immutability; gum confirm; Claude Code permission prompts - scope-bounded yes only when the card shows everything it would allow, harness-enforced not model-enforced; Claude Code queue-while-busy; GitHub required-reviewer approvals). Verdict PROCEED with three named unknowns carried as kill tests K1-K3 (tier-2 tag recall, prompt overhead, suppression threshold) gating phase 2 only. Build waves for the bridge cite this chain."
},
{
"slug": "factory-50-scale",
"match": [
"factory-50-scale",
"scale-30",
"scale-50",
"verification-throughput",
"suite-sharding"
],
"chain": "research/factory-50-scale/07-qualify.json",
"verdict": "PROCEED",
"note": "The 30->50 evidence-gate round (serves the factory standardization law; confirms the checkpoint-era finding BY MEASUREMENT at ~24 waves): verification throughput binds before spawn capacity \u2014 demand 4-12 landings/h vs ~3.5/h serial ceiling, paid today as debt (20 landings without manifests; check_landing RED on HEAD, 2 unbuildable modules). Inward: worktree tool adoption 0% (zero wave/* branches; 12 stale worktrees; waves share the dirty main tree), python_suite = 94% of the 928s sweep on 1/32 cores, 3 recorded sweep-race incidents + the 2026-10-17 boot-break class. Outward 5 sources (GitHub merge queue docs+GA, bors, Bazel remote cache, pytest-xdist): queue+shard+dedup. Synthesis: measured gates G30.1-7 / G50.1-6, domains.json sharding design (59 domains, batteries per domain + always-global members + serial merged-tree landing gate), throughput budget math (composite ~1-3 min effective checks -> >=2x margin at 50). Known wound: python-domain verdict caching is conservative-first pending the dependency-break tripwire proof. Research only \u2014 no code."
},
{
"slug": "factory-ghost-text",
"match": [
"factory-ghost-text",
"ghost-text",
"ghost_text",
"wave-ghost-text",
"typeahead",
"type-ahead",
"inline-prediction",
"d36-prediction",
"*ghost-text*"
],
"chain": "research/factory-ghost-text/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ruled research round (2026-10-18, boundary-law.md D36 item 5 verbatim: 'PREDICTION: inline ghost text (dim continuation suggestion, tab accepts, mimocode-style), sourced from local Ollama'; dispatched as 'D35 SS5' which maps to D36 item 5). Latency budget MEASURED on the owner's GPU (RTX 4070 Ti SUPER, ollama 0.40.5): qwen2.5:0.5b/3b answer warm in 5-25ms TTFT / 63-86ms full 12-token suggestion \u2014 an order of magnitude inside the keystroke budget; 14b-class (165ms-8.9s) and thinking models (qwen3 thinking-first emission, 7.9s) measured and disqualified. Live-measured eviction hazard: predictor load evicted a resident 14b (6.7s reload) matching the documented VRAM co-residency law \u2014 small-model default + pre-warm + keep_alive + first-use residency check are requirements. UX laws from six live sources (fish autosuggestions, zsh-autosuggestions async/widget taxonomy, VS Code inline-completion source: Automatic-only debounce + canceled:beforeFetch version law + typingSpeed.ts adaptive debounce, Grounded Copilot arXiv:2206.15000 bimodal finding, Ollama api.md+faq.mdx). Synthesis: Input3 suggestion state + composerView dim suffix + conditional accept precedence (tab while visible; arrow/end at end-of-text) + 180ms quiet window + version-keyed cancel + suppression matrix; predictor model = settings/wizard entry per D37 (qwen2.5:3b recommended, 0.5b fallback \u2014 pulled to the owner's Ollama this round). Five owner taste questions isolated (both-panes-or-chat-first among them); none blocks phases 0-1. qwen2.5:0.5b installed as documented fallback. Research only \u2014 no code."
},
{
"slug": "factory-persona",
"match": [
"factory-persona",
"persona-layer",
"persona_layer",
"wave-persona",
"b-persona-layer",
"*persona*",
"port-the-persona"
],
"chain": "research/factory-persona/persona-qualify.json",
"verdict": "PROCEED",
"note": "B-PERSONA-LAYER round (owner, 2026-10-18: 'runes persona layer is not ported either which is important'; spawn-queue.md B-PERSONA-LAYER). Three-way differential per the chain-audit addendum (OpenCode stack vs MiMoCode stack vs repo) with a 10-component completeness inventory (persona-inward.json). 8 live external sources (Letta memory-blocks, Claude Code memory docs, SillyTavern character design, mem0, arXiv 2310.13548 sycophancy / 2607.28818 ANCHOR persona-collapse / 2605.09863 Nautilus Compass drift detection, OWASP LLM01/LLM07). PROCEED gated on the owner interview (OD1-OD6 in persona-synthesis.json): identity seed, pane depth, always-on budget (reference stack measures ~15K tok/turn; recommendation 1-3K), edit surface, store migration scope, constitution block. Build target: Go core context assembly per D39 (channel-keyed, budgeted persona sub-blocks)."
}
]
}
"schema": 1,
"law": "Owner directive 2026-10-16 (docs/factory/spawn-queue.md): every feature wave runs the canonical research protocol FIRST (ISSUE, INWARD, OUTWARD, APPLICABILITY, INNOVATION-on-merit, ADVERSARIAL, SYNTHESIS, QUALIFY); the build wave spawns only on QUALIFY: PROCEED; MORE_RESEARCH loops back with named unknowns. This registry maps feature slugs to their research artifact chains so scripts/check_research_gate.py can enforce the law mechanically (admission-time gate + post-hoc sweep). Exemptions must be declared explicitly, never silent: mechanical-repair (spec-pinned, research exists at spec level), test-doc-only, owner-directive (directive quoted).",
"gate": "scripts/check_research_gate.py",
"updated_at": "2026-10-20",
"entries": [
{
"slug": "tui-build-program",
"match": [
"wave-*",
"wave",
"rune",
"tui",
"boundary",
"g-series",
"h-series",
"i-series",
"j-series",
"k-series"
],
"chain": "research/tui-frontier-r2/qualify.json",
"verdict": "PROCEED",
"note": "The add-native-rune-tui build program (B/C/D/E/F/G/H/I/J/K wave series under docs/openspec/changes/add-native-rune-tui/) executes the R2 frontier round; every program wave dispatch cites the round (e.g. admit_wave_seam_registry.py evidence refs)."
},
{
"slug": "wasm-prototype",
"match": [
"wasm-*"
],
"chain": "verification/wasm-prototype-report.json",
"verdict": "YIELDS",
"note": "Owner verdict YIELDS (docs/architecture/boundary-law.md: token_counting end-to-end YIELDS; prototype-only, production migration wave 1 opened under the standing directive). Prototype rounds resolve through this chain; a production wasm migration wave requires its own PROCEED round."
},
{
"slug": "memory-continuity",
"match": [
"*memory-continuity*",
"sleight-matrix",
"sleight-deliverables"
],
"exemption": {
"category": "mechanical-repair",
"reason": "Spec-pinned: research exists at spec level (Sleight memory-context-continuity evidence, bound by the B-SLEIGHT-MATRIX / B-SLEIGHT-DELIVERABLES briefs); bounded mechanical execution, no new research questions."
}
},
{
"slug": "bounded-mechanical-repairs",
"match": [
"repair-worker",
"repair-*",
"*-repair",
"fix-*"
],
"exemption": {
"category": "mechanical-repair",
"reason": "Bounded mechanical repairs dispatched against already-researched, spec-pinned defects (e.g. the five 0.9 defects cite the R2 round); no new research questions opened."
}
},
{
"slug": "substrate-ci-machinery",
"match": [
"substrate-worker",
"trial-worker",
"*-harness-check",
"research-gate",
"fuzz-*"
],
"exemption": {
"category": "owner-directive",
"reason": "Substrate/CI machinery (boundary codegen corpus, landing checks, harness trials, the research gate itself): per owner directive these are mechanical infrastructure, not feature waves; quoted from the research-gate enforcement dispatch: 'this wave is substrate/CI machinery — exempt as mechanical infrastructure, exemption recorded: research-gate machinery itself, per owner directive'."
}
},
{
"slug": "issue-161",
"match": [
"issue-161"
],
"chain": "research/issue-161/qualify.json",
"verdict": "PROCEED",
"note": "Per-issue research rounds live at research/issue-<n>/qualify.json (verdict in the outcome field); the gate's direct-dir resolution research/<slug>/qualify.json handles every other issue slug before any registry fallback."
},
{
"slug": "domain-registry",
"match": [
"domain-registry",
"domain_registry",
"query-domain",
"wave-domain-registry"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-domain-registry, 2026-10-17): the registry machinery IS the response to the owner critique that diagnosis was grep-exploration instead of scaffold lookup — the deliverable is registry data + lookup tooling over EXISTING authorities (docs/retirement-map.md, store/rebase/classification.json, package dirs), introducing no new research questions. Directive: 'Build the missing map.' Scope law (docs/factory/spawn-queue.md, owner amendment 2026-10-17) names the registry machinery itself as exempt with approval_ref 'user:wave-domain-registry dispatch'.",
"approval_ref": "user:wave-domain-registry dispatch"
}
},
{
"slug": "gitignore-hygiene",
"match": [
"gitignore-hygiene",
"gitignore_hygiene",
"wave-gitignore-hygiene"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-gitignore-hygiene, 2026-11): mechanical hygiene repair of .gitignore rules — the src/go artifact rule's force-add era (modegate binary fabrication, K9 sweep accident, 102MB cache re-lands) and the cwd-local runtime dir rules swallowing memory/index + memory/journal. Deterministic rule fix + frozen gate, no new research questions. approval_ref 'user:wave-gitignore-hygiene dispatch'.",
"approval_ref": "user:wave-gitignore-hygiene dispatch"
}
},
{
"slug": "rune-launcher",
"match": [
"rune-launcher",
"rune_launcher",
"wave-rune-launcher"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-rune-launcher, main session 2026-10-17): standardize the `rune` command so typing rune boots the new Go TUI face — launcher standardization is build/deploy plumbing over already-researched surfaces (paired build D26 scripts/build_paired.sh, update-apply D27 tui/update, the python-era ~/.local/bin/rune pinned verbatim as the honest fallback), no new research questions. approval_ref 'user:wave-rune-launcher dispatch'.",
"approval_ref": "user:wave-rune-launcher dispatch"
}
},
{
"slug": "firstrun",
"match": [
"firstrun",
"first-run",
"wave-firstrun",
"b-first-run",
"*firstrun*"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (B-FIRST-RUN, docs/factory/spawn-queue.md NEW PRIORITY 0 + 'First-run law' + 'Fresh-state detection' directives, 2026-10-17): the first-run experience — fresh-state detection (no provider record + no checkpoint), the welcome/provider-list/model-list/connection-test face (tui/firstrun), and no-default-provider enforcement (chat gated until an explicit selection). The behavior contract is pinned at directive level (the spawn-queue directives quote it); bounded build against the frozen provider_selection.json record law already shipped by the chat-live groundwork, no new research questions. approval_ref 'user:wave-firstrun dispatch'.",
"approval_ref": "user:wave-firstrun dispatch"
}
},
{
"slug": "self-transform",
"match": [
"self-transform*",
"self_transform*"
],
"chain": "research/harness-survey/self-transformation/qualify.json",
"verdict": "PROCEED",
"note": "Spec 6.2/6.2a/6.3 self-transformation waves (openspec task 6.2): the self-transformation research chain completed QUALIFY: PROCEED with D01-D10 owner decisions recorded in the dedicated interview; the build binds synthesis.json exact_candidate_flow (13 stages) with RM01-RM15 + IAM01-IAM22. First consumer: wave-K27-self-transform (src/rune/self_transform_pipeline.py)."
},
{
"slug": "chat-presentation",
"match": [
"chat-presentation",
"chat_presentation",
"chat-box",
"chatbox",
"wave-chat-presentation",
"B-CHAT-PRESENTATION"
],
"chain": "research/factory-chat-presentation/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-asked research round (2026-10-18): 'do research into how to setup chat boxes for harnesses in go' — the chat input surface (backspace defect root cause: key-spelling fork semantic vs ultraviolet; input box as its own layer). Headline: charm.land/bubbles/v2 v2.2.1 (already pinned, zero go.mod change) ships textinput/textarea/viewport — the prior 'stub pin' belief was vendor pruning. Build waves for the composer/chat box cite this chain."
},
{
"slug": "apikey-migration",
"match": [
"apikey-migration",
"api_key_migration",
"wave-apikey-migration",
"B-APIKEY-MIGRATION"
],
"exemption": {
"category": "owner-directive",
"reason": "Owner dispatch (wave-apikey-migration, 2026-10-17, spawn-queue.md#B-APIKEY-MIGRATION, owner-critical): lossless port of ALL API keys and saved connections from mimo code (~/.config/mimocode/mimocode.jsonc, ~/.local/share/mimocode/auth.json, ~/.config/opencode/*, ~/.local/share/opencode/auth.json, ~/.rune/model_state.json) into rune's provider store. Deterministic scan→map→apply→verify machinery + frozen round-trip battery (tests/test_apikey_migration.py, red-first) + secret store per the security law (OS keyring / Secret Service / encrypted fallback, never plaintext in UI/logs). No new research questions. approval_ref 'user:wave-apikey-migration dispatch'.",
"approval_ref": "user:wave-apikey-migration dispatch"
}
},
{
"slug": "factory-separate-turns",
"match": [
"factory-separate-turns",
"separate-turns",
"separate_turns",
"wave-separate-turns",
"turn-lanes",
"chat-task-dichotomy",
"d36-turn-lanes",
"*separate-turns*"
],
"chain": "research/factory-separate-turns/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ordered research round (2026-10-18, D36 era): 'we need to not block the interactivity of being able to talk to the chat pane whenever a task pane task chat message is sent... the chat and the task panes need to operate on separate turns.' The round maps the exact blocking points (dispatch_lock at core_server.py:199 wrapping the whole turn; unsynchronized operator state; full-checkpoint saves measured 2.2MB; task-channel-bound context assembly), verifies Ollama concurrency semantics live (stateless /api/chat, default per-model FIFO queue, OLLAMA_NUM_PARALLEL memory multiplication, engine caveats), and surveys crush / Claude Code / RQ harness+queue patterns. Verdict PROCEED with five named owner decisions (OD-1 model-slot policy is the only hardware-cost call). Build waves for the dichotomy cite this chain."
},
{
"slug": "factory-checkpoint-saves",
"match": [
"factory-checkpoint-saves",
"checkpoint-saves",
"checkpoint_saves",
"wave-checkpoint-saves",
"operator-checkpoint",
"operator-json-checkpoint",
"*checkpoint-save*"
],
"chain": "research/factory-checkpoint-saves/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ordered research round (2026-10-18, D38 deferred the decision to it): 'can you fully explain the issue first so i can diagnose approaches and also suggest a multitude of approaches after research on this specific issue' — the operator.json full-rewrite checkpoint (measured 2,307,627 bytes, 96.3% immutable task history, ~25ms per save, 3-5 saves per turn, truncate-then-stream with no atomicity, silent reset to defaults on torn load). Mid-round D39 ruling reframed the target ('keep the intention of what was meant to be finished in python and just move to go and fix the go version properly'): the decision matrix feeds the GO core's storage design — SQLite via modernc.org/sqlite (D30-legal, memory-ops precedent, pin >=3.51.3 for the WAL-reset fix) vs bbolt vs journal+snapshots (K25 generalized) vs atomic per-subsystem JSON, plus hybrids and an honest Rust-sidecar dismissal (sled self-disqualifies; redb is D30-hostile). 8 live sources with URLs; migration axis first-class per owner addendum; Python stopgap (atomic_write + terminal-task retirement) as Appendix S only. NO single recommendation — owner diagnoses."
},
{
"slug": "factory-orchestrator",
"match": [
"factory-orchestrator",
"orchestrator",
"orchestrator-layer",
"d37-orchestrator",
"*orchestrator*"
],
"chain": "research/factory-orchestrator/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner directive D37.4 (2026-10-18, boundary-law.md): 'RUNE THE ORCHESTRATOR: her intelligence OWNS all models in play, thinks independently from them, and interprets how best to route context across every model in play. The models are workers; Rune is the mind above them.' Research question: how to architect the orchestrator layer above multiple model workers (chat+task panes, local+cloud) given the landed Go turn engine (ModelClient seam named in engine.go as the D37 insertion point), the advisory rank() port + trust gate (G1-G5, paid spend = hard-ask), the adaptive-router effort classes, the warm-router budget law, and the D40 per-channel journal+SQLite storage shape. 5 live sources: LiteLLM router (rules cascade + ordered failure ladder + sticky sessions + reject-not-queue), OpenRouter auto router (lightweight task classifier -> measured spend-share ranking -> cost bands -> graceful degradation), RouteLLM (tiny learned routers beat LLM judges; 85% cost cut at 95% GPT-4 quality), Anthropic orchestrator-workers (plan-to-memory, task-descriptions-down, digests-up, 15x token cost, resume-from-checkpoint), LangGraph supervisor (handoff-as-tool-call, output_mode knob, forward_message). Synthesis: OrchestratorCore implements ModelClient above the engine (frozen laws untouched; phase-1 pass-through proven by frozen tests); three-tier decision cascade (rules -> measured -> optional tiny judge, off by default); orchestrator context in a dedicated D40 channel (decisions+receipts+verdicts, never worker transcripts); context composition via ContextBroker contributor port (cross-channel contributions explicit+gated); failure ladder = local fallback in-turn, cloud never mid-run (start-tier law), cooldowns, sticky-with-re-ranking, pass-through degradation. 4 owner decisions pending (OD-1 judge tier, OD-2 context placement, OD-3 escalation posture, OD-4 supervisor phase). No code changes."
},
{
"slug": "factory-consent-bridge",
"match": [
"factory-consent-bridge",
"consent-bridge",
"consent_bridge",
"wave-consent-bridge",
"chat-to-task",
"promote-runtime",
"promotion-runtime",
"work-intent",
"work_intent",
"d36-consent-bridge",
"*consent-bridge*"
],
"chain": "research/factory-consent-bridge/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-lawed research round (2026-10-18, D36 section 2, boundary-law.md:90): the consent bridge - the chat pane detects a conversational message is really a work request, Rune proposes the push with a y/n card, consent starts the task instantly (enqueue, never preempt) with the context frozen as a snapshot at consent time. Closes factory-separate-turns OD-5. The round maps the pre-built machinery (tui/promote SA-D3 ladder + Offer(entries) seam awaiting its runtime; construction_intent.py's ACTION|DISCUSSION resolver with the DISCUSSION-PROPOSAL-USER_APPROVED lifecycle and STALE_GAP_TURNS=2 staleness law, already ci_loader-compiled; the needs_confirm envelope precedent; the D38 ledger's Brief slot fed only the title today at ledger.go:266) and reads seven live sources (Rasa CALM's shift to model-self-reported commands; Mycroft adapt's deterministic require/optionally parser; LangGraph checkpoint immutability; gum confirm; Claude Code permission prompts - scope-bounded yes only when the card shows everything it would allow, harness-enforced not model-enforced; Claude Code queue-while-busy; GitHub required-reviewer approvals). Verdict PROCEED with three named unknowns carried as kill tests K1-K3 (tier-2 tag recall, prompt overhead, suppression threshold) gating phase 2 only. Build waves for the bridge cite this chain."
},
{
"slug": "factory-50-scale",
"match": [
"factory-50-scale",
"scale-30",
"scale-50",
"verification-throughput",
"suite-sharding"
],
"chain": "research/factory-50-scale/07-qualify.json",
"verdict": "PROCEED",
"note": "The 30->50 evidence-gate round (serves the factory standardization law; confirms the checkpoint-era finding BY MEASUREMENT at ~24 waves): verification throughput binds before spawn capacity — demand 4-12 landings/h vs ~3.5/h serial ceiling, paid today as debt (20 landings without manifests; check_landing RED on HEAD, 2 unbuildable modules). Inward: worktree tool adoption 0% (zero wave/* branches; 12 stale worktrees; waves share the dirty main tree), python_suite = 94% of the 928s sweep on 1/32 cores, 3 recorded sweep-race incidents + the 2026-10-17 boot-break class. Outward 5 sources (GitHub merge queue docs+GA, bors, Bazel remote cache, pytest-xdist): queue+shard+dedup. Synthesis: measured gates G30.1-7 / G50.1-6, domains.json sharding design (59 domains, batteries per domain + always-global members + serial merged-tree landing gate), throughput budget math (composite ~1-3 min effective checks -> >=2x margin at 50). Known wound: python-domain verdict caching is conservative-first pending the dependency-break tripwire proof. Research only — no code."
},
{
"slug": "factory-ghost-text",
"match": [
"factory-ghost-text",
"ghost-text",
"ghost_text",
"wave-ghost-text",
"typeahead",
"type-ahead",
"inline-prediction",
"d36-prediction",
"*ghost-text*"
],
"chain": "research/factory-ghost-text/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner-ruled research round (2026-10-18, boundary-law.md D36 item 5 verbatim: 'PREDICTION: inline ghost text (dim continuation suggestion, tab accepts, mimocode-style), sourced from local Ollama'; dispatched as 'D35 SS5' which maps to D36 item 5). Latency budget MEASURED on the owner's GPU (RTX 4070 Ti SUPER, ollama 0.40.5): qwen2.5:0.5b/3b answer warm in 5-25ms TTFT / 63-86ms full 12-token suggestion — an order of magnitude inside the keystroke budget; 14b-class (165ms-8.9s) and thinking models (qwen3 thinking-first emission, 7.9s) measured and disqualified. Live-measured eviction hazard: predictor load evicted a resident 14b (6.7s reload) matching the documented VRAM co-residency law — small-model default + pre-warm + keep_alive + first-use residency check are requirements. UX laws from six live sources (fish autosuggestions, zsh-autosuggestions async/widget taxonomy, VS Code inline-completion source: Automatic-only debounce + canceled:beforeFetch version law + typingSpeed.ts adaptive debounce, Grounded Copilot arXiv:2206.15000 bimodal finding, Ollama api.md+faq.mdx). Synthesis: Input3 suggestion state + composerView dim suffix + conditional accept precedence (tab while visible; arrow/end at end-of-text) + 180ms quiet window + version-keyed cancel + suppression matrix; predictor model = settings/wizard entry per D37 (qwen2.5:3b recommended, 0.5b fallback — pulled to the owner's Ollama this round). Five owner taste questions isolated (both-panes-or-chat-first among them); none blocks phases 0-1. qwen2.5:0.5b installed as documented fallback. Research only — no code."
},
{
"slug": "factory-persona",
"match": [
"factory-persona",
"persona-layer",
"persona_layer",
"wave-persona",
"b-persona-layer",
"*persona*",
"port-the-persona"
],
"chain": "research/factory-persona/persona-qualify.json",
"verdict": "PROCEED",
"note": "B-PERSONA-LAYER round (owner, 2026-10-18: 'runes persona layer is not ported either which is important'; spawn-queue.md B-PERSONA-LAYER). Three-way differential per the chain-audit addendum (OpenCode stack vs MiMoCode stack vs repo) with a 10-component completeness inventory (persona-inward.json). 8 live external sources (Letta memory-blocks, Claude Code memory docs, SillyTavern character design, mem0, arXiv 2310.13548 sycophancy / 2607.28818 ANCHOR persona-collapse / 2605.09863 Nautilus Compass drift detection, OWASP LLM01/LLM07). PROCEED gated on the owner interview (OD1-OD6 in persona-synthesis.json): identity seed, pane depth, always-on budget (reference stack measures ~15K tok/turn; recommendation 1-3K), edit surface, store migration scope, constitution block. Build target: Go core context assembly per D39 (channel-keyed, budgeted persona sub-blocks)."
},
{
"slug": "factory-visual-round",
"match": [
"factory-visual-round",
"visual-round",
"wave-visual*",
"d41-*",
"wave-d41-*",
"*amalgamation*"
],
"chain": "research/factory-visual-round/07-qualify.json",
"verdict": "PROCEED",
"note": "Owner visual round (2026-10-18): three direction frames rendered for owner critique (research/factory-visual-round/portrayal/); the D41 ruling (boundary-law.md §D41, 'after viewing all three renderings') IS the recorded pick the round's phase-2 gate requires. CAUTION: the artifact's verdict field carries prose ('PROCEED — portrayal-first: …') which the gate's exact-match verdict law REFUSES at admission — waves citing this chain declare the owner-directive exemption quoting the recorded pick (precedent: ADMIT-wave-d41-amalgamation, the 2026-10-19 compliance backfill) until the artifact verdict is normalized to plain PROCEED."
},
{
"slug": "factory-providers-live",
"match": [
"factory-providers-live",
"providers-live",
"wave-providers*",
"*cloud-first*",
"wave-firstrun-cloud-first"
],
"chain": "research/factory-providers-live/07-qualify.json",
"verdict": "PROCEED",
"note": "Provider-connection research round (2026-10-18): the standardized provider catalog, vault-keyed cloud connections, the drift-gated truth trio. Build waves for the wizard/provider surfaces cite this chain. First consumer: ADMIT-wave-firstrun-cloud-first (the D42 addendum cloud-first wizard, admitted by the 2026-10-19 compliance backfill)."
},
{
"slug": "task-ledger",
"match": [
"task-ledger",
"wave-task-ledger*",
"*markdown-ledger*"
],
"exemption": {
"category": "owner-directive",
"reason": "The D38 task markdown ledger build IS the owner ruling (boundary-law.md §D38, 2026-10-18, quoted in ADMIT-wave-task-ledger-d38's session directive); the ruling deferred only the CHECKPOINT mechanics decision to the factory-checkpoint-saves round — the ledger itself was directly ruled with no new research question. Recorded by the 2026-10-19 compliance backfill."
}
},
{
"slug": "factory-authority-port",
"match": [
"factory-authority-port",
"authority-port",
"construction-authority-port",
"port-the-authority",
"*authority*"
],
"chain": "research/factory-authority-port/authority-port-map.md",
"verdict": "PROCEED",
"note": "Research-only mapping wave (2026-10-20) of the agent-mutation-side construction authority port (issue161-harness construction_authority.py + construction_authority_cli.py + scaffolding-guard.ts hook) into rune. Single deep map justified in §0 (machinery move within one code lineage; design fixed by T23 §36 no-parallel-authority law and owner directive). Maps: the begin/check/checkpoint decision pipeline incl. shell operand-position mutation-target scan and lazy postcondition (§1); what ports cleanly onto rune's Scaffold.check_scope/ConstructionInterface/construction_admission substrate (§2); gaps — activate_from_text front door, completion tail (build_basis/behaviour_ledger dependency, deferrable), PROJECT_ID retarget, internal-ledger list (§3); guard wiring REPO/LOCK/interpreter + durable fail-closed lock (§4); 3-4 day estimate for the mutation boundary (§5); 4-phase plan (§6). PROCEED gated on the four named unknowns in §7 (rune construction_context shape diff, rune ledger dirs, classify_mode deps, canonical guard copy location)."
},
{
"slug": "factory-tui-design-principles",
"match": [
"tui-design-principles",
"design-principles",
"visual-principles",
"factory-tui-design-principles"
],
"chain": "research/factory-tui-design-principles/07-qualify.json",
"verdict": "PROCEED",
"note": "Research-only wave (owner directive 2026-10-20: 'i also want some research on ui design on presenting a tui so we can have some design principles to follow'). Deliverable: twelve named, testable design principles (06-synthesis.json) grounded in nine live sources, auditing D34-D43 (10 ratified, 3 new obligations: contrast floor / one-alive-element / token layer), plus nine terminal-fatal adversarial traps. The set is the design-review checklist for future visual waves; implementation waves it implies (color-token table, contrast audit) spawn with their own briefs."
},
{
"slug": "factory-chat-design-principles",
"match": [
"chat-design-principles",
"chat-principles",
"conversation-design",
"factory-chat-design-principles"
],
"chain": "research/factory-chat-design-principles/07-qualify.json",
"verdict": "PROCEED",
"note": "Research-only wave (owner directive 2026-10-20, verbatim: 'can you look up and research chat design principles to see if we can innovate from them'). Conversation-domain complement to the TUI canon: chat-specific principle set C1-C9 (04-applicability.json, testable), inward audit of D34-D43 (zero conflicts, ten rulings ratified by evidence, four small new obligations), eight live sources, seven terminal-fatal chat patterns (05-adversarial.json), and the innovation list I1-I6 with honest for/against (06-synthesis.json) — flagship I2 (legibility of the one-composer dichotomy). No construction; every innovation pick spawns its own build-wave brief."
},
{
"slug": "factory-tabs-windows",
"match": [
"tabs-windows",
"tabs-and-windows",
"factory-tabs-windows",
"wave-tabs*",
"*floating-windows*"
],
"chain": "research/factory-tabs-windows/00-interview-prep.md",
"verdict": "PROCEED",
"note": "Research-only interview-prep wave (owner approved the tabs + floating-windows direction for the face, 2026-10-20; design interview BEFORE building). Maps the stack (Bubble Tea v2.1.0 + lipgloss v2.0.6: lipgloss.NewLayer with X/Y/Z coordinates is first-class z-order compositing, Canvas.Compose/Render for the composite surface, v2 mouse protocol covers drag — tab bars have no library component, lazygit/k9s precedent is hand-built), inventories the face's natural tabs/windows (gauges overlay per D38 V-C, rail takeover, model picker, approval card, hints, build/sub-agent window, event stream) against the D43 Layer Law (library layers now literally enforce the ruling), and writes the 8-question design interview Q1-Q8 with recommended defaults (tabs = build + event stream only; floating = gauges/picker/approval/hints per D38; chat never tabs or closes; tab set persists, scroll does not; window geometry persists clamped to terminal; keyboard parity for every window verb, drag mouse-only; the chat+tasks dichotomy survives with the tab bar above the panes; approval card unclosable while pending). Owner answers become D44+ rulings; no construction performed."
},
{
"slug": "release-pipeline-deepdive",
"match": [
"release-pipeline-deepdive",
"wave-release*",
"*tag-repair*",
"*auto-version*"
],
"exemption": {
"category": "mechanical-repair",
"reason": "Owner queue item 5 (user:40-scale queue approval 2026-10-19, 'ive asked about this over and over so this needs a deep dive fix'): root cause proven from REAL CI run logs (Gitea runs 29929/30435/30636 — exit 141 SIGPIPE on 'git tag --sort=-v:refname | head -1' under pipefail with 500+ tags, killing the bump step on its first line before any fallback; plus tui-release aborting on empty GITEA_REF_NAME). Bounded mechanical fix: scripts/tag_repair.py (SIGPIPE-free for-each-ref read, idempotent ensure-tag, version>tag deadlock self-heal), red-first tests/test_tag_repair.py, workflow patches. E2E proof on local Gitea: two clean convergence cycles (0.400.0→0.401.0→0.401.1) + deadlock self-heal probe, all tags auto-pushed, tui-release resolved its tag via the fallback chain. No new research questions."
}
},
{
"slug": "factory-harness-vs-model-coding",
"match": [
"factory-harness-vs-model-coding",
"harness-vs-model-coding",
"harness-vs-model"
],
"chain": "research/factory-harness-vs-model-coding/synthesis.json",
"verdict": "PROCEED",
"note": "Research-only wave (2026-10-19), no construction. Owner question: do harnesses let models fully handle coding, or do harnesses impose quality procedures? Chain: outward.json (8 live sources incl. Anthropic building-effective-agents, Cognition don't-build-multi-agents, Codex published system prompt, CC prompt corpus mirror, SWE-bench Verified methodology, Agentless/SWE-agent ACI papers, Aider leaderboard), inward.json (honest audit of rune's mechanical enforcement vs model-trusted judgment), synthesis.md (plain-language owner report; industry consensus: model designs, harness verifies; rune sits at the most-procedural end; named risks: toll-booth friction and compliance theater found in the 2026-10-19 drift audit)."
},
{
"slug": "openspec-tabs-windows-chat-presentation",
"match": [
"openspec-tabs-windows",
"openspec-chat-presentation",
"interview-tabs-windows",
"interview-chat-presentation"
],
"chain": "docs/openspec/changes/add-native-rune-tui/interview-tabs-windows.md + interview-chat-presentation.md",
"verdict": "PROCEED",
"note": "Owner directive 2026-10-20: 'can we use open spec for tabs windows and defaults for those aspects. and can we openspec how i want the chat to look'. Research-only interview wave, no construction. Turns the 8 prepared questions (research/factory-tabs-windows/00-interview-prep.md, already registered) plus the post-prep Layer Law context into two OpenSpec interview documents: interview-tabs-windows.md (TW-1..TW-8, SA-D/R2-D style: premises, commitments per answer, RECOMMENDED defaults — tabs = build + event stream; floating = gauges/model-picker/approval/hints per D38 V-C; chat never tabs or closes; tab set persists, scroll does not; window geometry persists clamped; keyboard parity with drag the sole mouse exception; the chat+tasks dichotomy survives with the tab bar above; approval card unclosable while pending) and interview-chat-presentation.md (CP-1..CP-5 grounded in the landed render stack D34-D47 + canon C1-C9: strong paragraph/list/code contrast; styled inline markdown; thinking bridge only-while-thinking; chat code = clickable stub card, full block in tasks pane per D44/D46; day markers + jump-to-unread adopted). The main session presents the questions; owner answers become D48+ rulings before any build wave."
}
]
}
+10
View File
@@ -69,3 +69,13 @@ Rune ships with NO provider selected, NO model configured. First run presents th
## B-PERSONA-LAYER (owner, 2026-10-18: "runes persona layer is not ported either which is important")
The chat path answers as the BARE MODEL today — no identity assembly. PORT THE PERSONA: who Rune IS (identity) + how she speaks (voice) + what she remembers (memory grounding) + how she stays herself (drift governance) — the mimo harness stack is the reference; the repo's partial machinery (voice_state, persona_check, drift_core, memory_continuity) is the starting inventory. Research first (what the layer actually is, how production agents do persona), then an owner interview on what carries into the product Rune (his taste is central — it is HER identity), then the build into the Go core's context assembly (D39: intentions port).
## OWNER APPROVED QUEUE (2026-10-19 — the 40-scale wave plan, rulings attached)
1. ✅ VERIFICATION SPEED-UP (split the 10-min checkup into parallel pieces + backfill the ~20 missing receipts) — approved.
2. DICHOTOMY COMPLETION — approved with correction: the OpenSpec interview DID cover the one-input-box concept; the owner may need to specify finer details. LAW: the consent-card build starts from the SPEC'S EXISTING interview answers (R2-D/SA-D records), NOT from the research round's re-derivation; where the build hits an un-interviewed detail, the wave ASKS rather than assumes. The three pieces (y/n card, watching-her-work live block, waiting-line discharge) proceed.
3. PORT-THEN-INTEGRATE-THEN-REMOVE — owner correction to the retirement method: port the Go side, INTEGRATE it (repoint the live consumers, run integrated), THEN remove Python. "Port over and integrate the rebase before we remove." The same-change deletion pattern amends: removal happens only after a live integrated period proves the Go side in production.
4. THE SUB-AGENT WINDOW (owner design, verbatim intent): a TAB in the tasks pane that ONLY appears when sub-agents are in flight. Click it → one row per sub-agent with: truncated task title, which sub-agent has it, running state. Everything clickable/expandable — expanding any sub-agent shows its thinking/coding process live. On the FRONT FACE: an indication that sub-agents are in flight + HOW MANY, dynamically correlated to reality (the landed waves counter + cluster feed it).
5. RELEASE MACHINE — DEEP DIVE (owner: 'ive asked about this over and over'): not a patch. Pull the CI run logs for both failures, root-cause the tag-step's real failure mode, fix with tests, prove two clean cycles.
6. HOUSEKEEPING — approved (Go-core supervision swap + /tmp GC).
7. TABS/WINDOWS — approved: design interview first, then build.
50-scale distribution: 24 waves spawned this batch
@@ -0,0 +1,87 @@
# Interview — Chat Presentation (owner rulings CP-1..CP-5; answers become D48+ rulings)
Status: OPEN — the main session presents these questions to the owner; his answers are
recorded verbatim under each item and become D48+ rulings.
Sources: the landed render stack (bubbles, far-left dim timestamps, far-left gutter,
ᚼ-framed code blocks, the messenger law, D34-D47), and the chat-design canon
`research/factory-chat-design-principles/` C1-C9.
Plain premises (established, not questions):
- The MECHANICS defect (answers rendering as per-sentence bullets) is already
owned by a build wave under D47 — this interview is about the PRESENTATION
DESIGN only: how conversations should LOOK once the mechanics render correctly.
- The landed stack: sender identity by bubble side + color (D41-3, no text
prefixes), dim far-left timestamps (D43, metadata-on-demand per C4), far-left
gutter, the thinking bridge (live expanding thinking that becomes history,
D41-2/D43-5/6, separated per pane D43-8), ᚼ-framed code blocks, one message
one stream (C2), queued messages visible while busy (D37/C5).
- Code/tool content belongs in the tasks pane (D44 pane doctrine, D46 addendum);
chat stays conversational.
---
**CP-1 — How should paragraphs, lists, and code blocks visually differ?**
Today prose and list content blur together (the bullet-mechanics defect made
everything look like fragments). The face needs three distinct visual registers
the eye can tell apart instantly, without reading.
- "Strong contrast" commits him to: paragraphs as plain full-width prose, lists
with visible markers and hanging indents, code already in ᚼ frames — three
clearly different textures. Slightly heavier chrome, but zero ambiguity.
- "Quiet/flat" commits him to: minimal markers, distinctions carried only by
spacing — cleaner, but the eye must read to classify.
- **RECOMMENDED: strong contrast** — full-width prose paragraphs; lists with
real markers + hanging indents; code stays ᚼ-framed (and per D44/D46 the code
body itself lives in the tasks pane). No per-sentence splitting of a single
paragraph — a paragraph is one block, always.
**CP-2 — Should inline markdown (bold / italic / code spans) render styled or plain?**
When an answer contains **bold**, *italic*, or `inline code`, the terminal can
either render them styled (ANSI bold/italic, colored code spans) or leave the
raw markdown visible.
- "Render styled" commits him to: a small markdown-inline renderer on the chat
path; text looks like a finished document; raw asterisks/backticks never show.
- "Leave plain" commits him to: zero renderer risk, but the transcript shows
literal `**asterisks**` — reads as unfinished.
- **RECOMMENDED: render styled** — bold, italic, and inline code spans
(code spans in the same accent color as the ᚼ blocks so inline code and block
code read as one family). Styled inline markdown, never raw markup on screen.
**CP-3 — How prominent should the thinking bridge be?**
Today the thinking bridge exists per D41-2/D43-5/6. The question is its resting
state: always visible as a strip, or collapsed to nothing once thinking
completes (only visible while thinking).
- "Only while thinking" commits him to: a calm transcript after the turn; the
thinking collapses into its expandable history entry (C6 completion beat).
- "Always-visible strip" commits him to: permanent vertical space spent on
reasoning state, even when nothing is being thought.
- **RECOMMENDED: only-while-thinking** — the bridge lives while thinking is
active, then collapses to a one-line sealed entry (expandable via click + key
per D34-3). Chat space is conversation space; thinking history is one click
away, not one screenful away.
**CP-4 — Code block presentation: tasks pane vs chat?**
D44/D46 already rule that code and tool content route to the tasks pane. The
open presentation question: what the CHAT pane shows when an answer mentions
code.
- "Chat shows a stub card" commits him to: chat shows a compact ᚼ one-liner
(filename + language + line count, clickable per D34-3) that opens the full
block in the tasks pane — chat stays conversational, code stays work.
- "Chat shows full code" commits him to: the conversation stream fills with
frames, breaking the pane doctrine.
- **RECOMMENDED: chat shows a compact stub card** (one-line ᚼ frame: name +
size, clickable → tasks pane); the full code block renders in the tasks pane
per D44/D46.
**CP-5 — Which messenger patterns should be adopted?**
The chat canon C9 names two navigation markers not yet ruled: day markers
("Today", "Yesterday") and a jump-to-unread marker. Also available: transcript
search (C9), and queued-slot visibility already landed (D37/C5).
- "Both markers" commits him to: day markers as thin dim separators in the
scrollback, and a jump-to-unread marker line + keybinding when returning to
history. Small permanent chrome cost, large navigation payoff.
- "Neither / pick one" commits him to: less chrome; long transcripts stay harder
to navigate.
- **RECOMMENDED: adopt both** — dim day markers in the scrollback and a
jump-to-unread marker (with a key), both rendered in the timestamp/gutter
family (C4 metadata-on-demand styling). Transcript search stays a future ask,
not part of this round.
@@ -0,0 +1,119 @@
# Interview — Tabs & Windows (owner rulings TW-1..TW-8; answers become D48+ rulings)
Status: OPEN — the main session presents these questions to the owner; his answers are
recorded verbatim under each item and become D48+ rulings before any build wave.
Sources: `research/factory-tabs-windows/00-interview-prep.md` (stack capability +
face inventory), the Layer Law (boundary-law.md, D43 addendum), D36/D38/D41/D44-D47.
Plain premises (established, not questions):
- The terminal stack (Bubble Tea v2.1.0 + lipgloss v2.0.6, vendored) supports real
floating windows: lipgloss layers have X/Y/Z coordinates; z-order is a library
feature. Tab bars have no library component — they are hand-built (lazygit/k9s
precedent), trivial to build; the cost is state management.
- The Layer Law (D43 addendum) says every visual element is an independent layer
with its own scaffold/state/tests. Floating windows ARE layers with Z. The
library now literally enforces the owner's law.
- The face today: two panes (chat + tasks), gauges overlay, rail takeover, model
picker, approval card, hints overlay, plus build/sub-agent and event-stream
surfaces. The gauges were already ruled "windows to call upon (on-demand
overlays), NOT embedded" (D38 §V-C).
- Switching surfaces must never mutate semantic state (the rail takeover's
"pure chrome state" pattern, generalized).
Each question below states: the premise, what each answer commits him to, and the
recommended default (marked **RECOMMENDED**).
---
**TW-1 — Which surfaces become tabs?**
A tab is for long-lived surfaces you leave and come back to. Over-tabbing
recreates a browser's 30-tab swamp in a terminal.
- "Only the two long-lived surfaces" commits him to: tabs = the build/sub-agent
window and the event stream (today's rail takeover); everything else stays a
pane or a floating window. Smallest tab bar, lowest state cost.
- "More surfaces" commits him to: more tab state, more chrome, and a decision for
each additional surface about where it lives.
- **RECOMMENDED: tabs for exactly two surfaces — the build/sub-agent window and
the event stream.** Everything else stays a pane or window until a real need
appears.
**TW-2 — Which surfaces become floating windows?**
Windows that float and drag suit transient, on-demand surfaces — the exact
category already ruled on (D38 §V-C: "windows to call upon").
- "Exactly the named overlays" commits him to: gauges, model picker, approval
card, hints tooltip become floating windows; no new floating surfaces until
asked.
- "Anything can float" commits him to: focus/z-order rules for every surface,
drag-hit-testing everywhere, and more places where the face can be covered.
- **RECOMMENDED: floating windows = the gauges (D38 §V-C), the model picker, the
approval card (highest Z), and the hints tooltip. No new floating surfaces
beyond these until asked.**
**TW-3 — Does the chat itself ever become a tab or window?**
This is the identity question of the face. If chat can be hidden behind a tab,
the "chat is the home of the face" premise weakens (D36: the composer is a
stable BOX at the bottom, never a thin transient line).
- "Yes, chat can tab" commits him to: a face that can show zero conversation,
and reworking the composer routing (D44 pane doctrine) for a chat that may not
be visible.
- "No, chat is fixed" commits him to: tabs and windows orbiting a permanently
visible home surface.
- **RECOMMENDED: NO.** Chat (transcript + composer per D36) stays fixed and
always visible; tabs and windows orbit it.
**TW-4 — Does tab state persist across runs?**
Re-opening rune and finding your build tab still open is a memory feature; but
persisted UI state adds a store surface and failure modes.
- "Yes" commits him to: a small face-state file remembering open tabs + which was
active (plain files, per the D38 plain-files-over-databases instinct).
- "No" commits him to: a fresh face every run — nothing reopens, nothing stale.
- **RECOMMENDED: yes, minimal** — remember the set of open tabs and which was
active in a small face-state file. Scroll/cursor positions do NOT persist in
v1.
**TW-5 — Window size/position memory?**
Dragging a gauges window to a corner every session is friction; but remembered
geometry can go off-screen when the terminal is smaller than when it was saved.
- "Remember, clamped" commits him to: per-window size+position stored in the
same face-state file, always clamped to the current terminal on restore (never
a window partially off-screen).
- "Never remember" commits him to: every window opens in a default spot each run.
- **RECOMMENDED: remember per-window size + position, clamped to the current
terminal on restore.** Stored in the same face-state file as TW-4.
**TW-6 — Keyboard parity for window verbs?**
The face already mixes mouse and keyboard (click regions open the takeover;
shift+tab cycling per TD-4). Drag-to-move is mouse-only by nature; if keyboard
can't reach every window action, mouse becomes mandatory and breaks
keyboard-only workflows.
- "Full keyboard parity" commits him to: every window verb (open/close/
cycle-focus) reachable by keys AND the palette; drag is the one mouse-only
exception (move/resize), never the only path.
- "Mouse-first" commits him to: keyboard users losing windows they can't reach.
- **RECOMMENDED: keyboard parity for every window verb** — open/close/
cycle-focus via keys and via the palette; drag (move/resize) is the only
mouse-only verb. Source: TD-4 selection law + D29 instinct.
**TW-7 — What happens to the chat+tasks dichotomy if either becomes a tab?**
The two-lane face is a ratified shape. If tasks becomes a tab, the face becomes
single-pane-plus-tab, which changes resize/focus semantics entirely. This is the
most expensive question — it decides whether the tab bar replaces the gutter.
- "The dichotomy survives" commits him to: chat and tasks remain the two panes;
the tab bar sits ABOVE them for the orbiting surfaces (build, events).
- "Tasks becomes a tab" commits him to: a single-pane face + tab bar, a ratified
deviation priced before build — the dichotomy (chat = conversation, tasks =
work) merges into one column.
- **RECOMMENDED: the dichotomy survives** — chat and tasks stay the two panes;
the tab bar sits above them for the orbiting surfaces only.
**TW-8 — What can never be closed? (guard)**
Closable implies closeable-by-accident. Some surfaces must be non-closable to
keep the face honest.
- "Chat + pending approval card" commits him to: the chat pane/composer and the
approval card (while a decision is pending) cannot be closed or covered by any
lower-Z window. Everything else is closable.
- "Everything closable" commits him to: a face where a stray click can hide the
composer or bury a pending approval.
- **RECOMMENDED: the chat pane/composer, and the approval card while a decision
is pending, can never be closed or covered by a lower-Z window.** Source: D36
chat law + the existing "modal outranks everything" rule.
@@ -0,0 +1,85 @@
# Spec: the activity pane + the answer presentation system
Owner rulings recorded 2026-10-21 (this document IS the spec — the factory builds from it verbatim).
One wave. Blocks until D47's chatpane work (general-7) lands; touches the same pane family.
## Part A — THE ACTIVITY PANE (the bridge strip reborn)
The bridge strip stops being a "thinking bar" and becomes the **activity pane**: a persistent,
always-visible lane that shows WHAT the TUI is doing right now.
### A1. Activity states
The pane dynamically labels the current activity. Minimum state set:
| activity | when |
|---|---|
| thinking | model reasoning turn in flight |
| coding | tool loop running write/edit/run tools |
| planning | PLAN-mode dispatch active |
| researching | read-family tools running (grep/glob/read_file/list_directory) or webfeed |
| idle | nothing in flight (pane rests; runic marks dim/absent per owner boot feedback) |
The state derives from REAL signals only — the mode-gated dispatch (tool names in the loop),
the turn state machine. No guessed classifiers. A misfiring label is a defect (see #2529's
she-coding false positive: same class).
### A2. Per-activity colour + animation
Each activity has its OWN colour and its OWN animation pattern. The runic animations are KEPT.
Runic marks may randomise colour per rune within the activity's palette (owner ruling: "could
have random colours per rune"). The activity WORD animates while active.
### A3. The word itself
The activity word randomises a rainbow colour while active (owner ruling #2527, subsumed here).
### A4. No timestamp on the activity pane (owner ruling #2527).
### A5. Always visible
The pane never disappears; content changes with state (owner ruling #2527).
### A6. The + affordance (owner ruling #2528, upgraded)
A clickable `+` in the activity pane opens a CENTERED WINDOW showing everything currently being
processed (full thinking text, tool calls in flight, one block per full answer — not fragments).
Keyboard parity per the tabs/windows canon. In-place click-to-expose on the strip is REMOVED.
### A7. Tasks-pane mirror
The current activity also shows in the tasks pane work stream while active (owner ruling: "it
should also show opened in the task pane").
### A8. Layer law
The activity pane is an independent layer: own state machine, own tests, registered as a project
citizen. The she-coding label (#2529) migrates INTO this state machine — its trigger diagnosed
and either retired or made honest.
## Part B — THE ANSWER PRESENTATION SYSTEM (chatpane)
### B1. Proper markdown, zero model-side cost
Parse the model's OWN markdown into blocks once per completed answer (TUI-side, after the turn).
No prompt changes, no extra tokens, no context cost. Bounded dialect renderer (~6 block types),
honest strip for exotics. No general markdown engine.
### B2. Block types and their presentation
1. **Speech (prose paragraphs)** — bubble that GROWS with content; consecutive prose joins ONE
bubble; left-aligned; bubble outline takes the SPEAKER'S colour (owner ruling #5 — colour
identity stays, outlines made overt).
2. **Table** — BREAKS OUT of the bubble: full-pane-width bordered table, header emphasised,
border in the speaker's colour. Speech before/after joins bubbles again.
3. **Bullet lists / numbered steps** — own presentation outside the bubble: left rail in the
speaker's colour, hanging indent, steps numbered, genuine bullets dotted. NEVER per-sentence
bullets (the D47 defect).
4. **Code fences** — route to the tasks pane work stream (D47/D44 law, already contracted).
5. **Headings** — full-width, speaker-coloured underline, no bubble.
### B3. Emphasis
Respect the model's markdown emphasis now (bold/headers). Rune's OWN importance-flagging arrives
later with her intelligence infrastructure — her voice, not a classifier (owner ruling #3).
### B4. Bubble breakage defect
Long answers must never break the bubble (#2530a) — the growing-bubble model in B2 replaces the
fixed-shape card. Answers left-aligned (#2530b).
## Verification shape (for the contract)
- Red-first per block type: prose joins, table breakout, rail-list, speaker-coloured outlines,
activity state transitions (real-signal driven), + window open/close, tasks-pane mirror.
- PTY proof round: one landing, one deploy via build_paired.sh (owner contract).
- Layer law: activity pane layer owns its state/tests; chatpane renderer owns its blocks/tests.
- Zero new Python (owner directive: retire Python, don't add).
+107 -3
View File
@@ -59,13 +59,13 @@ runs above.
| 1 | rune-runtime | orchestration | `src/go/rune-entry` (serve), `src/go/event-bus`, `src/go/webfeed`, `tui/runtime`, `tui/runtime/lib`, `tui/bridge`, `tui/cmd/rune-tui` | `tui/bridge/*_test.go` + `tui/runtime` green; `src/go/webfeed` module RED; `tui/cmd/rune-tui` build broken | PARTIAL |
| 2 | dual-lane-interface | orchestration | `tui/lanes`, `tui/nav`, `tui/promote`, `tui/semantic` | lanes/nav/promote/semantic tests green | PARTIAL |
| 3 | modes-and-plans | orchestration | `tui/modes`, `src/go/build-mode` | `tui/modes` green; `src/go/build-mode` 4 tests + 6 truth files green | PARTIAL |
| 4 | tools-providers-devices | capability | `src/go/provider-registry`, `src/go/tool-protocol`, `src/go/local-tool-runtime`, `src/go/forge`, `src/go/device-pairing`, `tui/devices` | provider-registry truth battery green; tool-protocol/local-tool-runtime/forge green; device-pairing 0 tests | PARTIAL |
| 4 | tools-providers-devices | capability | `src/go/provider-registry`, `src/go/tool-protocol`, `src/go/local-tool-runtime`, `src/go/tool-executor`, `src/go/forge`, `src/go/device-pairing`, `tui/devices` | provider-registry truth battery green; tool-protocol/local-tool-runtime green (mode-gate truth section added 2026-10-18); tool-executor all truth batteries green + approval-door dispatch wired + live parity 49/49 + 48/48; forge green; device-pairing 0 tests | PARTIAL |
| 5 | file-change-safety | governance | `src/go/restore-points`, `tui/changes`, `tui/history` | `tui/changes` green; `src/go/restore-points` module RED; `tui/history` build broken | PARTIAL |
| 6 | trust-and-sovereignty | governance | `tui/trust`, `src/go/stage-guards`, `src/go/persona-gate` | trust (4 tests), stage-guards, persona-gate green | PARTIAL |
| 7 | memory-context-continuity | memory | `src/go/context-broker`, `src/go/memory-store-deep`, `src/go/memory-consolidation`, `src/go/graph-memory`, `tui/memory`, `tui/graphproj` | context-broker (2t/2 truth), memory-store-deep (10t/16 truth), consolidation green; `tui/graphproj` build broken; graph-memory truth-only | PARTIAL |
| 8 | unattended-workflows | orchestration | `src/go/cron-engine`, `src/go/event-wait`, `src/go/workflow-engine`, `tui/workflows` | cron-engine (2t/6 truth) + event-wait + `tui/workflows` green; `src/go/workflow-engine` module RED | PARTIAL |
| 9 | alerts-and-remote-control | orchestration | `src/go/alert-gateway`, `tui/alerts`, `tui/settings` | `tui/alerts` face unit green; `TestPerChannelToggleWiring` RED; alert-gateway 0 tests | UNPROVEN |
| 10 | work-observability | orchestration | `tui/rail`, `tui/receipts`, `tui/history`, `src/go/witness`, `src/go/work-manager`, `src/go/outcome-ledger` | rail/receipts/witness green; `tui/history` build broken; work-manager + outcome-ledger 0 tests | PARTIAL |
| 10 | work-observability | orchestration | `tui/rail`, `tui/receipts`, `tui/history`, `src/go/witness`, `src/go/work-manager`, `src/go/outcome-ledger` | rail/receipts/witness green; `tui/history` build broken; work-manager frozen battery (34 rows) + live library parity (40 seeds) + CLI byte-parity vs oracle CLI (17 scenarios, 2026-10-18) green; outcome-ledger 0 tests (truth-only) | PARTIAL |
| 11 | sub-agent-orchestration | orchestration | none (bookkeeping surfaces only: `tui/tasks` agent-assignment, `tui/threads` agent workspaces — green tests, rendering only) | — | NOT-STARTED |
| 12 | task-management | orchestration | `tui/tasks`, `src/go/auto-tasks`, `src/go/auto_tasks` | tasks (3 tests), auto-tasks green; 7.2+7.3 harness packet GREEN — recorded live-live parity + recovery + 4/4 mutants RED + attested digest, then same-change Python removal (7.3): oracle deleted, python-dead verified, native replay equals the recorded oracle trace → row derives PROVEN | PROVEN |
| 13 | thread-orchestration | orchestration | `tui/threads` | threads/tree/trace tests green (4); splitting with permanently-marked divergence implemented | PARTIAL |
@@ -106,8 +106,11 @@ states, per-row gap lists).
6. **Seam-registry enforcement is newly red** — its own 5 frozen enforcement
tests fail; the no-consumer-no-seam gate is not currently enforceable.
7. **Untested kernels** (landed, zero tests): `alert-gateway`,
`device-pairing`, `work-manager`, `outcome-ledger`, `tool-protocol-rw`,
`device-pairing`, `outcome-ledger`, `tool-protocol-rw`,
`graph-memory` (truth files exist, no test consumes them).
(`work-manager` left this list 2026-10-18 — frozen battery + live
parity + CLI parity landed earlier; the CLI surface closed that day,
see the rows-4-and-10 addendum below.)
8. **Deferred fronts recorded, not hidden:** accessibility parity (R2-D1, not
a retirement gate), real-hardware qualification (R2-D2, gates the
multi-device promise), collaboration/finance/rooms (gated deferrals).
@@ -197,3 +200,104 @@ the spec demands, executed and verified by the machine itself:
| 21 | model-routing | capability | src/go/model_routing (full port + eval engine surface) | 7.2 packet GREEN — verification/parity/model-routing.json: 6-step live-live routing task, 0 diffs, 2/2 breakpoints equal, 4/4 mutants RED, digest attested; probe_model_routing_live.py 500/500 exact vs live oracle; live Ollama adapter stays live in both, named not-the-unit | PARTIAL (removal gated: 8 live importers to delegate) |
| 23 | event-bus | orchestration | src/go/event-bus, src/go/event-envelope | 7.2 packet GREEN — verification/parity/event-bus.json: 7-verb live-live journal task (emits, hook, duplicate delivery, filtered+truncated reads, stats), order-preserving receipts, 0 diffs, 2/2 breakpoints equal, 4/4 mutants RED | PARTIAL (removal gated: ~15 consumers + microkernel plugin wiring) |
## Rows 4 + 10 addendum (2026-10-18, retirement campaign — tool-executor + work-manager waves)
Two rows moved up the PARTIAL ladder (neither claims PROVEN — their 7.2
harness capability runs are still unrecorded; only row 12 has those).
**Row 4, tool-executor + local-tool-runtime dispatch:**
- **The approval-door dispatch is whole.** The lifecycle gap-closer had
landed the handler kernels (`gaps_fsops.go`) but the dispatch still
routed 8 arms (edit_file, glob_files, repo_map, run_command,
read_persona, query_memory, check_growth, diagnose_behavior) to
`kernel not ported` placeholders and `prepare_tool`'s edit_file
preview never reached its kernel — a call driven through the oracle
trace (execute → prepare/approve → handler) diverged engine-for-
engine. All 8 arms now route to their kernels with the oracle's arg
marshaling; `ApproveTool` now EXECUTES the side effect (write/edit
apply, others re-dispatch — it previously answered `Approved: <name>`
and did nothing) and `RejectTool` answers the oracle envelope
(`{"success": false, "error": "Operator rejected"}`). Pinned red-first
in `dispatch_wiring_test.go`; all frozen batteries stayed green; live
parity re-run: lifecycle 49/49, fsops 48/48.
- **The fsops live-parity script path was stale** (J1 restructure moved
the batteries to `internal/toolexec`): repaired to
`./internal/toolexec` — the row's live-parity leg is runnable again.
- **The mode-authorize door is wired into the Go runtime loop** (the
oracle's T3 two-door law): `local-tool-runtime` gained `modegate.go`
(byte-parity `ModeRefusal` envelope — pinned against the live
`rune.mode_gate` — plus the readModeState law: RUNE_MODE env wins,
invalid fails closed to CHAT; hot file; absent → CHAT default), an
embeddable byte-identical mode-matrix copy (gen_mode_matrix.py --check
now covers BOTH embedded copies — three doors, one matrix), and the
loop gate BEFORE the approval branch with the typed tool-error result
feeding the model + the identical-failure circuit breaker. Frozen as a
new `mode_gate` truth section (4 rows, deterministic RUNE_MODE-pinned
regen; truth regen verified byte-identical ×2).
- **Two real gaps the regen surfaced and closed:** the truth freeze
hazard (a live `/tmp` listing once froze 1.4MB of machine state — the
fce row now lists a versioned repo dir) and the T4 narration law
(pre-tool narration preserved into final user_text — the Go loop
dropped it; now joined with the terminal text exactly like the
oracle, pinned by the text-fallback truth row).
- **REPORTED, not fixed (a sibling owns the fix): the ungated third
door in the provider-registry execute loop.** Python's oracle loop is
gated (`provider_registry.py::_dispatch_mode_gated` →
`tool_protocol.dispatch_validated_tool(..., authorize=mode_gate.
authorize_tool)`, the two-door law docstring names this module as the
former third ungated write door). The GO port (`src/go/provider-
registry/toolcalls.go::runTool`) still wires `ToolExecutorFunc`
directly — no mode authorize, no validate seam — and its frozen truth
rows 14–18 pin the PRE-gating oracle shape (`"Not a file: "` from raw
read_file execution), so the fix needs the port rewired AND the truth
re-frozen against the current oracle.
**Row 10, work-manager:**
- The map's "work-manager 0 tests" was stale: the dichotomy wave
(b973b96a) had landed the frozen battery (34 rows), live library
parity vs the Python oracle (40 seeds × 18 ops), and a trust gate.
Verified green today (own runs).
- **The CLI surface is now oracle-byte-parity.** The Go binary answered
only its native `replay` driver; the oracle's operator verbs
(status/next/complete/fail/model/clear, usage lines, exit codes —
including the oracle's own wart that `batch` is usage-listed but
unimplemented) are now mirrored, pinned red-first by a subprocess
battery (`cli_parity_test.go`) that runs BOTH CLIs live on identically
seeded isolated HOMEs through a 17-scenario session and compares
stdout bytes + exit codes. The `replay` truth driver is kept as an
additive native verb.
Both rows stay **PARTIAL**: no 7.2 parity-harness capability runs
(side-by-side + recovery + mutants) exist for tools-providers-devices or
work-observability yet — extending `src/go/parity-harness` scenarios
beyond task-management is the named next leg.
## Rows 21 + 23 integration addendum (2026-10-19, retirement-conversion wave 1)
First PORT → INTEGRATE → REMOVE wave under the owner's amended law
(user:40-scale queue approval 2026-10-19): live consumers switched to the
Go engines and run integrated; **no Python deletion** — removal is a
separate wave after the integrated interval is proven. Contract
`ADMIT-wave-retirement-conversion-routing-bus` (phase building).
- **model-routing (row 21)**: `ModelRouter.rank` and
`update_profiles_from_traces` (canonical store shape) now resolve
through `src/go/model_routing eval` (engine rebuilt from source — the
committed binary was stale pre-eval). Escape hatch
`RUNE_MODEL_ROUTING_ENGINE=python`. Per-consumer receipts:
`verification/integrated/model-routing-integrated.json`. Honest partial:
`reconcile_tools_support` (local_tool_runtime consumer) has no Go
surface yet.
- **event-bus (row 23)**: `emit`/`log`/`stats` resolve through
`src/go/event-bus` on the canonical journal path; redirected store
paths (canaries/sandboxed tests) intentionally stay on the Python
writer. Per-consumer receipts:
`verification/integrated/event-bus-integrated.json`.
- Test receipts: `tests/test_p1_06_model_routing.py` 47 passed with the
Go engine live; legacy/event-schema batteries green; full
`python_suite` run showed only pre-existing failures (every named NEW
failure reproduced identically with this wave's edits reverted to
HEAD — sibling churn in store/config surfaces, not this wave).
+569
View File
@@ -42378,3 +42378,572 @@
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_296ee0d13132", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bf567d8d5b6ebcffb175ddf978225b2d8ed76fd90f1fb479352fa581e57c1ad4", "schema_version": 1, "timestamp": "2026-10-18T07:11:40.604712+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2962743f5251", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:afb5952e6ee2a3cec0c358a98334fac294e456e4daa9e946fe5d40e4c2477766", "schema_version": 1, "timestamp": "2026-10-18T09:11:40.763715+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_77ddbf30d405", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:00c504c04a1effe17b687b182490cb67e35b6c80588d27c4b16ff42d73a213f8", "schema_version": 1, "timestamp": "2026-10-18T09:12:11.007450+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_70cf95c78918", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ba3d0c6b7ce3c221307961d4c0b19ba52625033d704635937da7aab23421b430", "schema_version": 1, "timestamp": "2026-10-18T09:54:07.814130+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_752605d8e791", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e4dc340e8a54e8672cd7ad40e3ef82848715e0fab58dd671a7e034d55e98e4f8", "schema_version": 1, "timestamp": "2026-10-18T09:54:45.656726+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1e151c94d0ff", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8361e7302606e91f60db22f17c025115adda24135d3b3156b9650d1b42751eec", "schema_version": 1, "timestamp": "2026-10-18T09:55:46.765065+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_478707c02e00", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.827989+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2889ae7260cf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:32a1c9f5c8a583da8d71a8463e45ec207646d93ece823d9d963695ac572e0a6d", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.872627+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_803da6e8f820", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fafd9721ce47c28f17aca68852e0d9a8feef3307d9cbab29e92c370ee95bc7fb", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.883213+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1c22bd11940c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2d8ec20ecac97b1b7da5203c8747b3f840ba657022eae07e65b1ca1d055d3793", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.898872+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8a82e98d98fa", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2d3b1ddf5f9eb781c88bf9d216252cf74fa447b09e9c496e6339114f1bf6aa3f", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.909566+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_314e15ca6f06", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6d206026cc7f5397574b74c1c2db073f3bd660cf19d4d3a086d5979d888c578f", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.922797+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ff06c4b48bcf", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4be445a37e2ce23d7608a2c1aaf90a0d99c4b23778ef8a30451b8c6fd9ba6d22", "schema_version": 1, "timestamp": "2026-10-18T10:31:38.932719+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_01256325b72f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3f899fcf454c3098014b9706d04079da14ee67183a4c32c1a172a48f5648fb47", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.197665+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_efa5f155d87d", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:284e7e4ed026f01e90cfa0c1eee2c11914147850b2d9f8560f3953aac89cf53d", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.210064+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4a2515ab33b6", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e5406ee8038b2f336e2fc0b4d4158b07862abec96a8a23413d88df29e9462ca9", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.226224+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c3cc415290dd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e8d4f260dd11fa560362a2a792de7ad55b98010427284b956377de33a5a40bb0", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.247889+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_af593e62e44a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.290197+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc13b394df5d", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T10:31:39.321927+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3c9f3bd0a7f2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:774fd7266554636d5391f9d0b712739d2c489e542ea2ccfbeeadbb3984bffec8", "schema_version": 1, "timestamp": "2026-10-18T10:48:41.612280+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4c6b3e7c1e32", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2714d35e5b43f9e0439466c1dce519006bdba89b74783cd0210165c16c899523", "schema_version": 1, "timestamp": "2026-10-18T12:25:42.370880+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8916e2ce52f5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7f00c1eb6b519f887f3f72aa06f24b901c2d0b0699069f30e657713d08d8a05c", "schema_version": 1, "timestamp": "2026-10-18T12:25:42.875594+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d1ebcd308c5b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:041ce8e4710f7d7516ee71955a54f7977ee254451d51e273424009f99d77af88", "schema_version": 1, "timestamp": "2026-10-18T12:25:43.381140+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f5b4ed6d072e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:296cbcbfef3c6c7504fa1ce30be0003f333a75e7b1a1dc18db28109f103efe85", "schema_version": 1, "timestamp": "2026-10-18T12:25:43.939852+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9a4baeabd391", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ace4ab7f15e8eff8ca36d2bff2a7e7986257891e7e67ffac174fa633c0d1ba36", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.002363+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f415d60e62bc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e2c30fd96587154e8d46310afc6236b139d59b657d9ff73383edabe50deeec99", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.065258+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_755c37156302", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ba8702fed992b81d90dc36f128f4a8dd2585c60128f54eefc6bc313592d01e26", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.128387+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5955ce9cabb5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2afe7e52e5d0c0cd06c0a15de3090688eb5e6e94e3d3b12169616fd012cbef79", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.451983+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_656d4a64fc25", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b82b3f5f680d4c267c8ffe252ec9ef4af1567cb8134af400ac645505b9a9f94b", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.452691+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d0ed1263d3b7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:33bf585cfc17a5598c4cd953e62015c2fd915eccec77e184024171ea447069c3", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.453599+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9985bc34c2ad", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:562d9c24bffacde36b74a7c9464054d6a6548f8a5e2b9de15aaa34ec63288c23", "schema_version": 1, "timestamp": "2026-10-18T12:25:44.456706+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a29c5220de63", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T12:26:33.924591+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8fb7f3b37bca", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-18T12:26:33.938439+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d5f5766f6632", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T12:26:33.972134+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e3baee579348", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-18T12:26:33.998228+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e6eccfc015bc", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-18T12:26:34.014362+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4055dd8f075e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.100837+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_973363a2c997", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0492b1e2533b68afd5ac3e216f79781f0841a3ef6b30fbba8aaf0894188cb9aa", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.127828+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_57839437bffa", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2dc59894c04500cae3d1f15a0d092759f893ff8eefd4e9a3463b46c45e78daf9", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.136933+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fb856424a78e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1528e36cffc9cc8f32342576e363348d1989678b3b1ec154a3f4260c6ac2dd28", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.149252+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ee6ebbf4e299", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:31819189ec283eed6730aa11b5f208c6464b77fc36e753761558e3c6afa5ff88", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.158410+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b20b4dcaff8d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0f0586fb9244295e93c6ed75b7d10170cca7f82a0ae337374bf5ec92a27031b7", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.169644+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_514587ada313", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ab03d77ead2384124fd4fd96c13137add6b67f151f609810b4fdac2d187acb41", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.178204+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a2ad19b4bf57", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c40ee4bd6830d2bcd1fa08956ebe9836a87f4b023a8eecc9bc1687978aee4b63", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.307297+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8bbad22829c0", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:703bc88806765b90b2106c4054a4318c8d6144e78e363100f9372fdd2d02cb1a", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.316038+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_dda8417406a6", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3ab2fe6d61f7a9ff9b22acdec41e43a8fb16dec5677c61cbeb1d827a327892b8", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.330286+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ce5d1f9f248d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3a88fdcecabe3a1fcb43a15a11a82d91ee21765e883966cb7e21d3a6f8c558c3", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.348380+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_65b449972660", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.375802+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5e60b6fcb632", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.399309+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f7d6d60bd34e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.433273+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_01e08c2c1c52", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-18T16:16:29.493284+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6d587179f66b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7a7267080dd17282f422b7a5de0b6868c53ca0e2db6ba6b0205bea7ffee71149", "schema_version": 1, "timestamp": "2026-10-18T17:01:32.649456+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fdfd9565c151", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:da7e03e52ad285e65f6fb5f784f155463f2e1e196a50bff6c195f778d964ae79", "schema_version": 1, "timestamp": "2026-10-18T17:01:33.154174+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0aaefb332518", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1847d88364899c3b792a6ac535f2644042476b53259bf8295db26ab42e6fd958", "schema_version": 1, "timestamp": "2026-10-18T17:01:33.659151+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bc36bc4ccd3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:63794e78e31d8649502e61dac018a41f331e5262345a51f00e8f4601b5bb6f78", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.216200+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_14335168c7aa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2b6021f77d8ee9b9bb4449f4c19b69d756372d4e2781d06a9f8aff183c2b569b", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.286832+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9ab2b321ba3a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d2740c04571142aff6f2faddd0a70c111bf7193878f7199b57f3c885c3723a72", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.349653+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e1cc4c418df", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:01be4c98aff30dd8891cb51d2027edc133f75d50712945fa9f6384f266395c8e", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.412392+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1ded7e322c73", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:abfb6e35be56592a2f21b981a3a3514742fe0afd640e98c7341b6d3fe712263d", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.728150+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_275442c549b2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2825eec52e587e8d6e2ce92a4022f783329761a053db174ef6ec46bc9eb66e9c", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.732320+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_16f1ef1bfa0c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d2d55b284ad84624fc9c3eebcf46d407dbce40e638908df5958b18bb307c7a21", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.746269+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_46d262a028f9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5de65f4831ab9472118d3a0770960888691d706c2b00d158876b0644247fa1aa", "schema_version": 1, "timestamp": "2026-10-18T17:01:34.739432+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fad5656e5d4d", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:02:39.053951+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3ba4ba731079", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-18T17:02:39.067478+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_25682a8f9117", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:02:39.102115+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_34f0993f62bc", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-18T17:02:39.125827+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a6f48efca940", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-18T17:02:39.138723+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8bc2a94c55df", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-18T17:07:53.386874+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4496ae957029", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-18T17:08:18.292054+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f5dcbfe5b072", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1da0a4068e5d544f5a80a5a95f32855174ff0e48673f18928c5579efeb23a113", "schema_version": 1, "timestamp": "2026-10-18T17:08:18.638262+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b05b85b9accd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8bff8c112e305e4fd23d97f4c12a02647668f2128cc04632f3ae9f3a09618cc2", "schema_version": 1, "timestamp": "2026-10-18T17:08:19.107793+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0902279f3ccb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9fe02158eba6e6ccec830847e35ef3473ae40a1b9c57db832ca32c5c990bfdca", "schema_version": 1, "timestamp": "2026-10-18T17:08:19.530175+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_815ee8643ead", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:694ff823caad5dadaa0f38bbc7f06041afcc28eca86abc3c0887100900d95d77", "schema_version": 1, "timestamp": "2026-10-18T17:08:19.911603+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e80cfceacdd8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:99db9dc71833beff8a9c98ae37455c271bde802e349f45afb87bd3481d9f9fa3", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.338471+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_89bb1e65150b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:933c0ec968884d8b4ce877e5c4adb7939d1f62df70ebccd44f3ff8f07431ccf0", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.799253+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ee97fedf40e4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2ccd06bb4cd8d7e27c15678bf2147dbfd4ff45e233c4b529ee7ccd37d5e4b35", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.866624+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_03f008e809fe", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:df861b1baa728b59d11e2581bc1ad82b7b82e52d3b997ade033cc9ecdb704695", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.882128+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e83bc0f2c476", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bfe12b7356539cb1215bae4c281afb8bac3761614d27782e591d24c46acf16da", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.897212+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6e8ce55044a7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5b413aebdde631fa81dc89e7243ba32964d023c55685556081de408adcd89695", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.912588+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_30e338121ff7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:567de3c6bd7d5f411b30fd8774a40a4c8ea03749aa342e6981f10b9f0de67e53", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.928354+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1d8bbb9bcfcf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2ed88a9367c5663aa3797ac138c827c37e939836fabcf1bac3cb47e9bbdfa1b4", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.943781+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7e6de8fa914e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:851d2a8d7e9a569ccdc0c30fa175b6732a49e76b7d6177b75eafcc78227ee07e", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.959528+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_28226776f9be", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fd67fd5347eba53f7cae26390d0a4b2e9b74c8d204cd0d9c6ceff1328f3245cb", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.973043+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0d0b3814274d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ef5b0df0251f235646c945710c7a40296df24018fdfc38da16d4cb25f8a293b9", "schema_version": 1, "timestamp": "2026-10-18T17:08:20.988181+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_56581b515938", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9b0845dfc8414c287abf246078f28945428bc5e9a987376d6f81a54516b0fe7f", "schema_version": 1, "timestamp": "2026-10-18T17:08:21.001033+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8e3a9d4de1ed", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:20f8246dee3695f57fc956ad69f4197475013fe8e4918474d179a2aa349ec8fb", "schema_version": 1, "timestamp": "2026-10-18T17:08:28.661924+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9ce5833b82b8", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3c22bf86cf22c37973cb15795096282274b0b5ccaaa22399cfeddca276cb5070", "schema_version": 1, "timestamp": "2026-10-18T17:08:28.676869+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8a98cb0460a9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5915684128ec898ededd1cd5605f5ffacd35d1508fe0f9babe5d580fc9a0abef", "schema_version": 1, "timestamp": "2026-10-18T17:08:31.498818+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_71999a6667ed", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:512a2142bb7d0cc396d9f6a583fa4d2cb55d51921f4d401865ece954b290662b", "schema_version": 1, "timestamp": "2026-10-18T17:08:31.515549+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0b82beb9d347", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:dbb6a89bf0717e6baf021eb1fb53157dc55421815ee4937d017184745cce78a1", "schema_version": 1, "timestamp": "2026-10-18T17:08:31.655102+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ee581015a787", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:503a7503d7a8ca7d4e06bfb0cefeed6816360dad56aa2f659e04aec5018c0412", "schema_version": 1, "timestamp": "2026-10-18T17:08:31.664602+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_aeb4df1df76f", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-18T17:08:31.688316+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a538c98a0a11", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1e9213fb94b8a178a4b50f90eefd07702e7688e384f75f2b7f1e984196f598f0", "schema_version": 1, "timestamp": "2026-10-18T17:08:32.051034+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3cbb76e18237", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:88c5bf73aecb783e53612c06c47500ba21cd8873fb6bd42815d6547610bee7a9", "schema_version": 1, "timestamp": "2026-10-18T17:08:32.068351+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_db79d9d61a9d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8cbad87e6a657e18f442d68d3d440ef029dcc9fbfc051ad12f31720af71e7ed5", "schema_version": 1, "timestamp": "2026-10-18T17:08:32.080532+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6f6997cd1591", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cb517fba0c5fd73420f03138b8acc92203b3fc39103905b95e15f483e2238e19", "schema_version": 1, "timestamp": "2026-10-18T17:09:07.106797+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ccfefd9b05ca", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8008553473686b37f62c18291d2799a49a1cddb32a27f90289b77ea56d56d40b", "schema_version": 1, "timestamp": "2026-10-18T17:09:07.137113+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_54293ba2bfcf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d0175513843c01b3edd1ba887a65a9f022169b1aeebddc8f02912d698b64b1d6", "schema_version": 1, "timestamp": "2026-10-18T17:09:07.168519+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_40393aa0d508", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-18T17:09:07.225661+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_796b615baa31", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-18T17:09:07.332930+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a7661f800f75", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.027785+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b4795a2f09a3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bca0f6f063c8521f98eeceb2e84ae3b2dbad2c57bcb00e67dac34f42817bf92c", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.046860+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_aaa653485137", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:da098ad060ecda45418a945ef9c3ef077dd61cdc04913893d9624ddc43df92b4", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.055485+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9cae4d48c4d4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b4f57c26a6dc84fa6e45c883c0cfdc35381a32d0351564327a2d87f28af5fcef", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.071990+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ea8fa5e0316c", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0ec0c1319a20776a1a6fa3bbf4b5e3a6da6502a1ee1690a5317d9f521add242f", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.080766+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_687dc7358c90", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:16da4847ab7fac32ae06fc35ce9c3d0e1d952559f5652ae9072cd78642a585de", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.092447+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ca9ff455992a", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cbe4ddc144f692076d8af5b248a02879699a428c4abe388b867e381a1643d39a", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.101323+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0150fdc711d4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8532aef1a2ecdd94cb232929ce4f81b1096397d880637d3d77bc3400816ddab9", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.292996+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_899f8a0a4fe7", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2382632e0edd385bd8f73b81e5aea92ace3db4edc76640b0a14fd2a5784dd43b", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.302783+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_577c0cc32cb0", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cd8a6c4ba837eaf062ec3be10ea857f986e3937e2f0c87935d90325234502474", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.318540+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3fb9ef28cde8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:347655476ab22f94ffa91be4f39ef9f5cea7ca380617cefa91af7f17c3dac866", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.343945+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fe2e04632569", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.364608+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f1dd1c18180d", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.388461+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bcc08fa42fa2", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.694401+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a522024909db", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.720911+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_18efd7991748", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cd9c5bb92cdfa0ecc040eafe1c4a281f274b4ff5ecc2bbed2aa231b6395f2082", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.732936+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3342937cb6fd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.832947+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cc3bd9f20ab2", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.852876+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c2c548ae999c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cb746fc37f67c48d258b0a73aef0a90900b37eb57c3d4c189bbe1a4a69e653b3", "schema_version": 1, "timestamp": "2026-10-18T17:09:21.863271+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3cc0c84d81e0", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-18T17:11:16.222932+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b304fe221393", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d2794a1f2cdd4fd46854158d03b97b3f77ac6e9d18b9683cd276c6574bead595", "schema_version": 1, "timestamp": "2026-10-18T17:11:29.971740+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_156ac2b388bf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:79cec5bf4815a2b54a05185eb690ef33976cd950227f68f3f1f50aa446464964", "schema_version": 1, "timestamp": "2026-10-18T17:11:29.989086+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_54c6b6a0ab43", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0128cc91309fb6426c796cf063951ae3f24c675b88e063942e799143258d31d4", "schema_version": 1, "timestamp": "2026-10-18T17:11:30.007334+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_13904e39e70a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fe5bb3b353055cb0aa6d10ebfe99b59c2b87caac392580deaf04b9d72b0f9707", "schema_version": 1, "timestamp": "2026-10-18T17:11:30.025551+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ed2a37689919", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4847effe65e339a7ff81a68e30f26bde8f285c3f6512a31f53ae2ea7123e989", "schema_version": 1, "timestamp": "2026-10-18T17:11:30.046083+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d02310481cea", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d19475ee2b9656d3adce8c6d8f021f0254352937833add7cab99c87ee485365d", "schema_version": 1, "timestamp": "2026-10-18T17:11:34.350090+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a46f8542af80", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:565fc49cbbe328fdafaf8f6a03b92349f0674b8085370facc97974bd52e82e9a", "schema_version": 1, "timestamp": "2026-10-18T17:11:34.373080+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c6941c8b15b8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e15a2d72c4b304ae6fb91e8e393fb814aebeb177aca82be16ead5cf0f860bfe4", "schema_version": 1, "timestamp": "2026-10-18T17:11:44.994993+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_774a87fbffc2", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8e06347b4507f3fbff61d73636acd37d7313421d6999294120d9238e15f0b8b3", "schema_version": 1, "timestamp": "2026-10-18T17:11:45.011334+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bc8ab0a3f1e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f3720ef96f9684703f6d6897f9f0d4b0ae0f45b41d80a6faf05bc3bd3255cd30", "schema_version": 1, "timestamp": "2026-10-18T17:11:45.151694+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_33040f175c2f", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:28402d36777a7571b29ba56e06b86f29bccea33d0e7abc1f75c84529e5fff22d", "schema_version": 1, "timestamp": "2026-10-18T17:11:45.161520+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b979e6e2dc82", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2b35bfa3136a212e78f81157df82b5a623f7f051473234c622b0c477be170197", "schema_version": 1, "timestamp": "2026-10-18T17:11:45.245744+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_29b81216bb69", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4006feac82e126e4b3c5768a48e4d82cae6d41f006b3d2e4e87e4db510a796a5", "schema_version": 1, "timestamp": "2026-10-18T17:11:45.254281+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2ee77cdbebf6", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-18T17:11:46.443747+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3efc6e9cf559", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e2b5457ebed8b790cb6e947312cb5463bfa0c984bb7df7a14b15b075cf0e7d31", "schema_version": 1, "timestamp": "2026-10-18T17:36:18.824893+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5e213e5ba3a6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0aea19f7bd4f84c5123d1f70d12d40e315814e871a3a3c4277267d4001b3527b", "schema_version": 1, "timestamp": "2026-10-18T17:36:19.325106+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_db6a3796c893", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8bd940456d82d91878e3b7464af6be0395f9e1076f87353d3953c812333b5747", "schema_version": 1, "timestamp": "2026-10-18T17:36:19.827943+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_414070d410a1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3407f8bd3cd69d662cea3c58d363bd828e6b66d8ea4c87dbb5eec8053f14b7d5", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.385713+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_60f74f54b592", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:00e9a67d6147e147d96ba46de7ed875cbc23eb827b5cf82c5210b5429187c218", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.454818+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3ba8ca1e605c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:87e2bf87b3fb13525b7bffb7a956aa00c28cd2f4b2062ec62c302fa21e4cd329", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.517776+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7b2eeb6c09bd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ac5f7a6d50dc7a778d10d29496186f8ac6072c530ca086d687cb3b1a71a0bb4e", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.579146+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_33b88e218323", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ea603ba67e5ed10120cc913ea375049fe4f32aad6956a9d0285a3d70b0b71185", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.893790+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d76bd25d966f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5cc1699c74b40796de00a5059d51ee0a6f3d931f0c5f6808b79f1356152b216a", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.893063+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0160f1033a38", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:809ef27693298a556c4320fbbe4fb45143302d10dd6c5a08047d2dfe823120a2", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.898613+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_79778dbe33f7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0562c0ea1a9cbbbfff2281ec84c76475afd16be2298587edc9c91cf23fb2fecc", "schema_version": 1, "timestamp": "2026-10-18T17:36:20.919586+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_79210da98778", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:452170dd8b0332dc6989e0d5846a6557314d319cf4cb98f9e048ec31685e7c23", "schema_version": 1, "timestamp": "2026-10-18T17:36:29.162947+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_34a4c8a163db", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:014d58eeed4eb54506fd5e9f4338cc45e2429f3666bbd44ee87db54d54309e87", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.493863+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6c01b71189b5", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:51dd7c88f58e995d756cf7b02eb73d1961c8b404b7c67d91c5ebf7d35cea299b", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.505871+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e0202b04e34", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9523586c0ec22624676d9650b29d7155f7f5fb7346da9d2bbdb527c4876b4b11", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.650963+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2347c8e3b78a", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d6a2dacbbd98eefb1058145b56f7284fe934c022250129709d463efe07eaebeb", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.660433+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bcc7e79e0bcf", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.676455+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_47cc66d507d7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:26f3328d855c18bd9279cea472499806a3d00f32e6da6baabd4535a5df96e771", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.690416+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a6af5038c941", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d812bb06a9e61e4b168775cdfe20fb3f7a716843de4536712348dae18d35da06", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.700172+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2502edf32307", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:77071fce31d75a309075425e774ebb9e24d92ac9fe5fb15b445ee7b3956c79d1", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.712941+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ee74a2547525", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f028239b81dd8de9c4e58deede22b71d077ea3540c91a7cb958403dc736b9e8c", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.722055+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_981d31dee572", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fe9f1a3e1dcb9593c94c0ecc7e8966b3b5cd2d6e3dfbe8280b59e271e4490e29", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.734354+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c69028651973", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:af3a3b2d743c4bdac6ba9c1a21cba152114cd34c87d9c880ac010d9dd4b37dbf", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.744368+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_de29308afedc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ecc226ae7790fc6977847bcdc60736d3e9f330b86fe94c2e1bb375dba3541472", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.898567+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_76e5ccdf1d0e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:eb997575b1ca63f8e254390e18e3e2753f1ea5ca32825d34161c38b9c4078d80", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.910056+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fa5cdef4e916", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6037590b11cd1f29395b4642d81d4fb4a8c7617baec24cea76883f892eccffd8", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.924863+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9ea40cbc83b7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e70016a7c9d15dd27b2b7c790311c12af5c1ece572be256e31e46d76f41ea651", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.947210+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c68de4afbacd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:51:15.973460+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eb4ae488d7e2", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:51:16.001470+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_36fc9b5c0c78", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-18T17:51:16.041162+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_76c3dc3009cd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-18T17:51:16.107889+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_29c3127fece8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:59f8ceca540bd9c2f916f0b57d525056558a0f47b7bbf6c6c6417071cf82ef39", "schema_version": 1, "timestamp": "2026-10-18T17:51:16.152983+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bda9da96e0aa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:55daf838abf5216231d4209e8fa1eb0e4edcc49d8496943c211f0a825d0ca3ed", "schema_version": 1, "timestamp": "2026-10-18T17:51:16.659688+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9c43932a82b6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1dc0638d48680a2b4c208bc62768e041a9903f390968c3905242cfcfb14d8fe9", "schema_version": 1, "timestamp": "2026-10-18T17:51:17.161488+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b23be4bd0e1e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:597486d7cdb7462e293341edf41ba634de101a2573115eb2f4ed54f2e22ca90b", "schema_version": 1, "timestamp": "2026-10-18T17:51:17.717694+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5cf7d98ef9a7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c6ed64b359f06162c0a2ca8a401af58ef563b493b6d840f9fb03000a9a7dd72b", "schema_version": 1, "timestamp": "2026-10-18T17:51:17.786428+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7f93e28e8e20", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4efb9af084aef3888dd497c3afcb354e19f6379862ae49d84d886ee5d30e400", "schema_version": 1, "timestamp": "2026-10-18T17:51:17.848570+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d44b79601623", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3c247b5455559fd46843e2e0fc4f002ba4111b0402efbf0b7a6c23fb04cf6c21", "schema_version": 1, "timestamp": "2026-10-18T17:51:17.911485+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_adcc315c91a6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:69bacffa8a8b13c9f4308f58417fbb89a405970648272fd724f1252476582c20", "schema_version": 1, "timestamp": "2026-10-18T17:51:18.226071+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c2a743845edc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:270f179315ace0fc72ddcbe9962a725154742c5b9319a22b0cb400c964093096", "schema_version": 1, "timestamp": "2026-10-18T17:51:18.234357+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2001a6963122", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e9f3e21b10d6bf7145839e1547eebe3daa7f79a0995eeaf45edd7a2471069266", "schema_version": 1, "timestamp": "2026-10-18T17:51:18.227351+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d0e05ccb7be2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:56567653ee5802c50959074e4baef9d9e48df92f1a150d11a33d1d6776923c3a", "schema_version": 1, "timestamp": "2026-10-18T17:51:18.229156+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a79d666e7cef", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e0224004d00a0c0d33e21d2a7abe91eb69a59d67293699c16aa69114d8dbf971", "schema_version": 1, "timestamp": "2026-10-18T17:51:23.056344+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a551264acaed", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.811943+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b32cb36d9e47", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.840294+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_05fde63c7c7d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:eeae9c9355cf2822713978d946ae8d8727b5ff918dca3a1995b97cd327e085fe", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.849939+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4b7809a3ae21", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.942529+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7aa8a800ec2f", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.960480+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2a7de91b4a23", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b8e058bd0f40f6c0ce48b1d828d459ca2a87957c8e6707dac07b369cfa21bf25", "schema_version": 1, "timestamp": "2026-10-18T17:51:50.974079+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cf7461f103bc", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-18T17:51:51.329628+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_10c724140d3f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:453474fbf86d9b39c9200f454ba35682b6762ffe79195b619eaa64ff5585da78", "schema_version": 1, "timestamp": "2026-10-18T18:13:56.811300+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_dc06598249c8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:173ca15d6cbd0b9b997f1bb0c895d964a3349c477b115f0530ea4fb75969855c", "schema_version": 1, "timestamp": "2026-10-18T18:13:57.317795+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc502905d729", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:387b91fe5ca9127679e5aff5682d6ce2708fcff807e0dc2a48edf61b20b119a0", "schema_version": 1, "timestamp": "2026-10-18T18:13:57.823449+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0a74a1d4c5cc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2d6cff8b845571a0bb80723f79d0eace5aa4e5a35b43baf0ae760ead657af8e2", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.381698+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_405f11acce20", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bc5d3976eafc27241abd2ea562e67d8cc185842bc6ac687ec3eade8654cad94c", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.451833+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ef521c8aa476", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f421e42d205912f4e6d2fc5814f9013efcec9da3cd9b23550fec6c0a3e573ecd", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.514255+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2b091791ce13", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e1bcd7b973b231a5e7d6ec40e924f667342a2c0b3d562b5da7d44863d579a387", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.576529+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ad23a780aabc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2642d2bb9c07bac610655d569970daa4b6197ebb8a9ceb698e3f10a0c7ba3285", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.892863+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b363b4632ce9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:630a862ac867ce789980041edfab65c856b05d16e033e0a8956fe4aa7a94faf1", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.893684+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_884a7f4fa496", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:62878d477a31ec5e08b513e41b2597534e1b25816d65ff4476ab9a9d2e4a9cca", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.898437+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e76ed168ffd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:de3033436758943839471f9145bbe22f380a894e805b104eb240a019b4876970", "schema_version": 1, "timestamp": "2026-10-18T18:13:58.913093+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_29e87445b8bd", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T18:14:57.026677+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_19b8ec0040f3", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-18T18:14:57.038521+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ed07662060e7", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T18:14:57.069736+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c8d97f5d37c0", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-18T18:14:57.092328+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_73a4a5c98d8c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-18T18:14:57.105617+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_09c95f604a8c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-18T18:20:16.916395+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4829c64fbd70", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-18T18:20:42.051577+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_49df561d21d4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5a56ac9dce92c51a161f943be40d0a6a2eef0323c67cce068f1282496c38aca8", "schema_version": 1, "timestamp": "2026-10-18T18:20:42.395426+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1cffc0341b46", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2291e10db74b2d0a677e8dc2e6faf701daf59096e790defb2a105c5425ebe9d1", "schema_version": 1, "timestamp": "2026-10-18T18:20:42.852370+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_901591c4d205", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:df46a239b22be0a69bddcffa6dccd78e27b3f15808ab319c344ec09f9dffd1ee", "schema_version": 1, "timestamp": "2026-10-18T18:20:43.259975+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f3c50bcb1460", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c3d20fd87c452a177619b048b7e321f594934df6841b79b2d8939a805e5eb90f", "schema_version": 1, "timestamp": "2026-10-18T18:20:43.645296+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9b9fc1a1782d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f3f7ec5a627922e73b0368f27fe9ca5360a29a15879cecaffc0071d40c58f3e7", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.092598+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_301827c27b6b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d065e818db5f58e0f4ebfc5d8f51737bdf1cff342b4d3ba052eae27b1809e77f", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.539156+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_83965188d653", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:26d3cbc2298159f18c1cc32849c818e03fb272df1f3768d39e8004afe6417e48", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.609270+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_49258a3bc41e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e17160e522d88c50f45d1b27d90c3e737c0893438c71e382b2bb73be8305b8fa", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.624003+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_23c25bf0ca82", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:227723d5d26592d7229272bf1b6a24bfb6245156bfe1125ca516200569f860bd", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.639027+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_de77cf0a7e29", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e109a43f48d2ded84c6a71a48b941e047801d4f0189f2a9900dbc624d78342aa", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.652363+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_39f13701f034", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6e8046e2eab4c5164907d45129fbb17d5b2dcc0b5d9c79f9d6a89ac82a5b799b", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.667546+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_00f42cd90890", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a381d11c39851612aa07fa6e4cd36808f9c69c794c2a5d1201af53e89ae65c5e", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.681867+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d633a9fd75c8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c9ae6305491537b8a30df811b59821f6bee944dee7b2161f56d1adb358b41a41", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.696085+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_95400ad5a1c1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5a538f67b41cc7e6c923c85a9c662b0c0783497c82b3a110ff898013bcb8f6ce", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.710608+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_17eee53f27ec", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ebcde1bcba0ca69f2bad8f567185be393465d9947f39a99309520ea4a94b0742", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.725616+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7624678361ff", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e3f2ecc2ed12930023353156c16b98757fba7dfc58dc8e166891a6d2af25d1ea", "schema_version": 1, "timestamp": "2026-10-18T18:20:44.739358+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b444fd8bbfd4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4963bf408fc7a08fcfb818dc240a31e4bb4d57d6a2de66c9932a1c79cb46b99c", "schema_version": 1, "timestamp": "2026-10-18T18:20:48.698470+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5743cc05d03a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6f5e41757fc794e8908bf094bb86a2c20e06d973f6a5baacd78c386c619c0a81", "schema_version": 1, "timestamp": "2026-10-18T18:20:51.618435+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_71099a3ebf19", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:18d76c19520fba571fc833cbcb3edb634da8bab6d1ea775291429185387baccb", "schema_version": 1, "timestamp": "2026-10-18T18:20:51.635317+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a890ce1f89b1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:efdeecb2e124b0832c9c9d761379b344cca569058e012b974cebe6c14420b961", "schema_version": 1, "timestamp": "2026-10-18T18:20:51.773838+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c1328ad9f382", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:53b7088c62bf2461c561ac0122179bb45c4710d056528876624114699fd23b6e", "schema_version": 1, "timestamp": "2026-10-18T18:20:51.783357+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_303bfc17c750", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-18T18:20:51.809144+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_169d2ef284f9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a04adf2ef1b2cc9f57e9f9e5d347b1c4acafd74693413c005ee45f4df654cee9", "schema_version": 1, "timestamp": "2026-10-18T18:20:52.225735+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b7af8b173d7d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:10a77380b96d1ee4fb39db4558f70680ce541cd3270f6af3e987525cee3e8168", "schema_version": 1, "timestamp": "2026-10-18T18:20:52.239973+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3902007d89e5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6c6717eb3f0cda8dcc7ba2cc441defa99c32a0f536b3aa3b42de7eaed4aae0bf", "schema_version": 1, "timestamp": "2026-10-18T18:20:52.251951+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_86b9433618f3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6daabd6e7f8a3e8461202ab20ebe3e63a742e1f284984cb47974d44b9e4be12e", "schema_version": 1, "timestamp": "2026-10-18T18:21:28.144330+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c24d75fa05ba", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e9ae69915f24a09181f735c576264c523a9119158c72efb4c8e754f99947d2c6", "schema_version": 1, "timestamp": "2026-10-18T18:21:28.164125+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a035d19c6060", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:85c0525d2fc0efbbd745bc21b1616e22d7a1825f2def4254dae6c02a23a99e96", "schema_version": 1, "timestamp": "2026-10-18T18:21:28.193565+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_92f933d04ded", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-18T18:21:28.248969+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_64674d701f4a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-18T18:21:28.349180+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_84e87d13e8e9", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.164460+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bde475f3d19d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5fa5d672ec4804dd21e59f79d8a92ea57439a9aa08fc2d5c07e5645de3259bea", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.185500+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_97966162ac8b", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bb8560fdc1a75c1bde7762595fa354a3d4380f0be37514857b7f36941a71c7d7", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.195194+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8c934040044e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2036bdaa5cdf66e3cec60d550ff773f8e33d9a59f492b0647c707eb2447641f2", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.212866+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e378c5813da", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2dc68300f0f7cacbdc8808d2714e19f2885c5d610034b000cd925f5c5ad04b04", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.222407+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3bd93296ce07", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d245bf437097f5c6b9991b3a692d7ec5f5826be765c2c07b98defbde744a2f39", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.235740+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f496387e31c2", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:414ea9b10132fe3177b847db5e4c328847ee1bafbfaae6c6b814e985b3de55ed", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.245052+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_173f87271be3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:435f5b91347fc6c9ba66c95ba067932b5237fa6b66c74f073c4a9d6eb3c55b66", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.624906+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9d770e958aba", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:75fc84cb3c3747bac429a60ddf5df9e4f20784dd434819ecfa1b43e1e82cda2d", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.641512+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d8229b073590", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:09fd8210c61384eac2ad2619f2ae51514277bcaab7f3e6809b3426f4b7a68c1a", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.659167+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_60317bdb50fd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f298191f0e3997c57bbd4fcc68826a76c5fabeea1ac6b9c42b98413eacc67c4d", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.689421+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_283d16581153", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.710076+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ebe610cba080", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.735693+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3a5a2938c94e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.955740+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c99027c8ad9c", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.977337+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7945a70331d1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9030fd85377fc2f374d2fa1cff6124ad2b1879a5beb0f6153d0866c58326ceba", "schema_version": 1, "timestamp": "2026-10-18T18:21:41.988343+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bcd68c0bbc4", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-18T18:21:42.082756+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7276a15dba0b", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-18T18:21:42.102673+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a81064e407ef", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:159b30b5b00888742f0ef4cd5ef4e59fdce50bdc258929c120cc4e9d9e35d55a", "schema_version": 1, "timestamp": "2026-10-18T18:21:42.114390+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b744f38582bd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-18T18:23:37.027227+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d70bad6b21d9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d92b325e820c2a5b8e4a7b4f0d31b9d347b2d060de1696d8c7ebba02092e91b1", "schema_version": 1, "timestamp": "2026-10-18T18:23:51.058701+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1ee088f7ee23", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9b7d32380fafcad5fa4781d0eb122fb0178d826a3ce447e0e70ed4a213a5061b", "schema_version": 1, "timestamp": "2026-10-18T18:23:51.074865+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_932eef056418", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:874ec7115ee3568bdf4e4c568277ab133de5730f2f3d4a05e98f8012457eef5e", "schema_version": 1, "timestamp": "2026-10-18T18:23:51.089579+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c1503149d1f3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f56a7f5709677f0c824713d8b0c2789384c60b06e0421b0c655823348ca9871e", "schema_version": 1, "timestamp": "2026-10-18T18:23:51.106443+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c1726df227d6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:943d83b5b036989041d5d65edcda7d61c5950c331acb300a7090ec2be130c31a", "schema_version": 1, "timestamp": "2026-10-18T18:23:51.128108+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cd18af7a3ee0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a226c783062bf7e7734ede89f5eac46f8e6d4ccc6f95c4fb77b6d64e215df09c", "schema_version": 1, "timestamp": "2026-10-18T18:24:00.219194+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_93cade955bca", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c05d519a918c2210dbdac5e8f001d8e7f1aec466ca6f34bcf03798a8d73873b8", "schema_version": 1, "timestamp": "2026-10-18T18:24:00.236995+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f517e711fe55", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8e51786a28f09c14f405abc803da965f812d6b05adcf27c9997b2ccfec10613a", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.220489+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_912d37984e7e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c447bd5a37418cd908796d25c0cb27f7dc9af715928f3105c100396974c9a13d", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.230711+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e1deb68dedc2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:48fe90597635a893bf3dc1ef034200c94ca247f8e3f51264cd25e7108d16cd18", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.377104+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_33971237084f", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a8a08036554b51af34408309c5b60c876ed46160e1bb13801e3215f51497f397", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.386705+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c1e777a8b7c4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:06b0a00bf33146fdab3b64f7c69c6045ffc470ace970130cf794cf8da2ed2740", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.460880+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e6fda5723389", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4c29de4d381284c0e9cb65e48795ff2abe2d3b33e23086ed9186b96d92196d7d", "schema_version": 1, "timestamp": "2026-10-18T18:24:08.469774+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0204a63d2a4d", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-18T18:24:09.386651+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_05e93fa6e890", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-19T17:54:03.327700+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6ca39ddf1b60", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-19T17:54:28.841407+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6a1c70c6a5a5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fae8bcd7094bb597219a6fd628371c33da2d6a006ecf841f008e29dd57dd8bcc", "schema_version": 1, "timestamp": "2026-10-19T17:54:29.319775+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_df79d7798dbd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ac813af850e89889f2a6e8c2f15abfda7bd8dc75edbdb8781b571c536070f25c", "schema_version": 1, "timestamp": "2026-10-19T17:54:30.088606+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0482ab5f9427", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f75e1509c506f3b6f0eb9463a962870de1826258372627ab15c19fb485105de9", "schema_version": 1, "timestamp": "2026-10-19T17:54:30.641441+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e6037b82ced3", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:73af6207f2ba645ad25fc7437e05bfe560e7e3c247a3b118ace7911ca9688f74", "schema_version": 1, "timestamp": "2026-10-19T17:54:31.176068+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_36be48174f4c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7663aca3ab058aa93dfbfd9bbca535b1f18318f29dc4db0e8c47896ab5f77204", "schema_version": 1, "timestamp": "2026-10-19T17:54:31.756592+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_03a8e130671f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d3194b0ce08ca8d98958867abb9b9fc0164a64b40c7eb2d99651706a8884f932", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.354576+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_59749b007e09", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:36ee579a75c4431db1b049cce1f651a2b4809bb9a3aec29097ab84fd42559b55", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.446940+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a2acc5f3f86a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9b69ab40f5e1fbab6f67cb7f9def1b69d5eb266be5499d7ec3c07f9d73a65ac2", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.462338+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fda935262b3f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2a86a993089a578e51428522639cf78876a02d8eee8af9d5be664475403ddc06", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.479970+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5858e0397bcf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2cf8276722a2c9a9f78f7de138e9c269d6218b1a6dbca620f284a5bb63bf7a96", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.494773+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6913680f3f79", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2efed6590af62bdd16d53480aed0f691d13bd058164591ffea440dc5b4a1f039", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.511016+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_829cf5eb1038", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2a2f933d96cb8e427a77726b844dc105b0e95d5ecbe160825a35df76832d6369", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.525925+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0d25baa32dd4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b094e4e8a6b1a02315a0c8f9f8fc7b362cacae49739a5c33ebdf1e9ada0a021d", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.540787+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6c065b9c7d18", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1acaceeb819e0c27a7f26ba9ab6b14cfb33b99a991ffaac10c112d5bbf386a15", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.556172+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1c8ffb64d180", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b5fc291cd5acfaf8562106c7f2b3e34853a66b74f23aa36bbbb073753b69b347", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.570237+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_619ee30db689", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7c2dc2da9fd8d5529a733d7cf4a0091f69811f66494a3c9c2140adb56070c8b1", "schema_version": 1, "timestamp": "2026-10-19T17:54:32.584378+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3d6cc4caf1ec", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-19T17:55:54.897903+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1b8ff7e98d71", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2d4e54d636b018ab6dd6e6b0f020e9a75b461937f87ec2c82a7815237f995eeb", "schema_version": 1, "timestamp": "2026-10-19T17:56:02.910590+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_626b8dddcf59", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e59a51c2286ee14651cd7e2a515c92e87f9f6e346fa3630a6cad09def19d5c7e", "schema_version": 1, "timestamp": "2026-10-19T17:56:02.932214+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a6765e2ea218", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b227c7888a58c6a95a3efd389b4fe6622d09b2e27deb75ba15750cd98bb7e9ce", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.136167+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_06af980213d8", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fa3982a52f73df89a01ef9374fbf6e0c0ce918e65e126017d2ce04f6937c3d33", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.141287+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e68efc068542", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ad5b937dbc561d698e6622a4746f6e94ad4f80cf92fc6198e69f0756f9d22505", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.304618+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8c3b2525b5d5", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:eae24f94854163b72f4c19c201b0fe2e90609cf8633efd363a9f4708a01aaebb", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.311267+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8cd17a8c043c", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.344223+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bbc557383fdd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fce140e860748a8902a09c7ac6dff27c04c2c4c72dc1ab10b0ba07d6c0322397", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.848329+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_573404b06a0f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:72778165a12c8062ac2cafa60465de777cf12120e9b48eceac1a0c7ee88be785", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.860938+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1ecf35e7042c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:aacab8f04cce37d87633e9d728262ea60ce5beeadd1eb9cc308e28c297234374", "schema_version": 1, "timestamp": "2026-10-19T17:56:06.868784+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7706c3a64c1e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-19T17:56:20.601016+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c5b2f2b24543", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4ccb5fc42fa0aa7f001c0c1e3067472451ca12b4a5651d3f405a0a319c03f885", "schema_version": 1, "timestamp": "2026-10-19T17:56:21.136906+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bbce69ed2cf", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d465aab37f1894d8f00e080d9bd411e1d59c9966bd7072a4041b6988475ef853", "schema_version": 1, "timestamp": "2026-10-19T17:56:22.100786+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c6f4cf1ed433", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:32ba00f737aada4aba089485026535d11285f40cbe8e1423c03e556fae91bfef", "schema_version": 1, "timestamp": "2026-10-19T17:56:22.780162+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1012193d0983", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:af64fb4fde56773589b0a12407c1a5e8780b66d9d1a10a8b87763dbe7949c57f", "schema_version": 1, "timestamp": "2026-10-19T17:56:23.538353+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2afd70cd3cfb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cc235df8b88f3b185bd539dd19afbc025b8d7b5e5aa1c0be5b00fe140fc362e7", "schema_version": 1, "timestamp": "2026-10-19T17:56:24.383826+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_302c4709b5e7", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2a6d92fecd6b0b9a60df490d86cc279d4f5cc55bb5bb6c6a991312767b64e62f", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.575646+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b51108050201", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a51a1a5af123b519c5c4706cdde35579c37194233b4e7740eabdcce71ababa57", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.667310+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f60d5bb277b8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a605ce76fcd217259f433d40ac27ba3656564f225c7d303afbbde9a0ec1383dc", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.690611+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e64807811cfe", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fab5399bb0932fafd0a2dc290a568f40ceb86b67d9a360b15d7a91ac2f208d5f", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.717810+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_73d1cc5ac8fb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3f69fa5dd50f69c09eaa8cccbf62e2852c2a98316cbfe9d77aae675ab656b355", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.744474+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_14ca9041e01f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0556197dee5f4eb433691c79ae379ba37507ba5d7a174e2f5c001b357a30e846", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.768115+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_40dbe3f42c5a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9d539beac5efc958d820b1dd7876e7d0ae51dd35059cd9b9fed7912c3af6e421", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.813962+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7b32dbdf9bfb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:63d4fd563833249905fb87556e093d65530f04cb1c1c714aaf58bbbb65899ee3", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.842867+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9c68658ed775", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:5bfe2cc4a51a62d8df37ee2f9f2b1cf5de107c2e9fb4dba0bad6f7e1bd4e3bd1", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.871869+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_06fc677e6557", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:67868c3c409e81de31e5e3321d4d0ac4db8f3ee67c782e697fcbef7b80ff347b", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.898990+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_749fb45d80f8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:47f4874a733ca8f0e32fed81c7b1778b11ad1d7bc0f9a91b9e383c01217f2d14", "schema_version": 1, "timestamp": "2026-10-19T17:56:25.917950+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e28b86e8597", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9321635525c2a57928470e3f6b60772a71c2276f70e964118213dabcd76ebd70", "schema_version": 1, "timestamp": "2026-10-19T17:57:39.610392+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9a4689fe67a4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:52f1cb7c0a2278c73d298017b07f1ea16b905ff85da511bed8b37149378069e2", "schema_version": 1, "timestamp": "2026-10-19T17:57:39.689045+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_37de76d1d422", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:feba73173f575559070cc47d0e2cf5943d248c11fa0efe28cdfa118ddfa33c78", "schema_version": 1, "timestamp": "2026-10-19T17:57:39.742243+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6c3d857a485f", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-19T17:57:39.941528+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ddf6404f322c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-19T17:57:40.310592+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fb39329d9dfc", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:19ba91c9f5de6c099f8377a5fa0608dc1262729292030623dbe0ae6003a05446", "schema_version": 1, "timestamp": "2026-10-19T17:59:12.947949+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a02b218ddf4c", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:496e1eaeb81c9ae123ea7f8c2abb572c414111c9edb4e1d557e9279f7d19ead7", "schema_version": 1, "timestamp": "2026-10-19T17:59:12.953718+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f0dd0a189d78", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d3324f77bb0183eb5630897a23fe766034b59a297f520b85b7db86cb17d1d450", "schema_version": 1, "timestamp": "2026-10-19T17:59:17.469209+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0bbaf1879fe7", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9c2430a643d457905cfec192010d2645942d4c25cca1e627e68f4e1efd9d51c3", "schema_version": 1, "timestamp": "2026-10-19T17:59:17.477633+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6695fee3c3a2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8efd9678bef5606bb5e08988a09f69b2a074047c8798599d3aa86cb2458cd9d8", "schema_version": 1, "timestamp": "2026-10-19T17:59:17.708129+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4a6af03de05e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c84d96bbe57c89cc99cf51c759266c248dc061416f10005c96eab75f278333ed", "schema_version": 1, "timestamp": "2026-10-19T17:59:17.717456+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e88c99b5eb9e", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-19T17:59:17.863259+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7172e1d867a0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2a7d68f716599b6416e16e5fc7639856f853edfce80d1a395eaf1eeb633d9f6", "schema_version": 1, "timestamp": "2026-10-19T17:59:19.007412+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_896b5fbf3180", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8a547b45321b15b5e62bfd955d80f8f066ecf102866df0124c5bea3251b7eae4", "schema_version": 1, "timestamp": "2026-10-19T17:59:19.035864+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b488a07abbfe", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:92c542f48fd06f24a758a5ffbbf1ac604efeb0ae4a1aea7f0128aa8a08bc75aa", "schema_version": 1, "timestamp": "2026-10-19T17:59:19.047579+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_74e669dd2565", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:917f384a51939781b479b21105ddb17f03035856b1cf5609545a0335a781ff5d", "schema_version": 1, "timestamp": "2026-10-19T18:00:52.765041+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9af291fac77c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:68ca934acd1426af246b282db6030779c15b211ded3befcdd856610d2e8c57e2", "schema_version": 1, "timestamp": "2026-10-19T18:00:52.777699+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_57d8e0454d55", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:40b261694c23b2c8f24966eabdf0151b4469ef736ff2061f7a0c648165e6d76c", "schema_version": 1, "timestamp": "2026-10-19T18:00:52.797347+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b1c6b2a8753c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-19T18:00:52.906231+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_97c60c694737", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-19T18:00:53.101118+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6334409de7c3", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.239741+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_67b816f49e74", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8eb2ee1f27ce489bb8a4f1a584ce0cfbddfa877e8f9fb7586866508e4d6e24b1", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.288421+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e98eadce708e", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d47dbb94f8a6f97e7159881bce604f0413265252254c16eb76929e5a8ba51228", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.292989+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_71f1fb2eed1b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6a8c0d2081add0a26a022f300338d82919b68d1e753e644f2b042da98fe59c7a", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.310213+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_77e32b01e032", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9eec316e1469b744b70e19f255ff030eb1a9d76b9d9d48921373abffc5e739ff", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.315240+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d2b75924336e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:25c8bd3b8bb76c97a3faa686c3f7818e9c838eb1d5965557889d681eb9b66fef", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.327707+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eb1aa2d79f3f", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d2faa405a5a734c0b35cdcba86fecbe8c70dba70db9251efdaa09c5283b0a3d6", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.332195+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eae38093c77c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:70adbf5366260156eabd5f0e068bd6d43394e192737541837e73fda9b447d959", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.692019+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a9a161381c64", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2f51a403fc0dba681dbb0cb59dfcccb2630dca75a7f6cc310161a10686bfd86", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.696766+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5cc4c03b068d", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:558d3d3c5e73f1fcc0e97eb535081edc460c95925e5c02ec70cd96c2d87ba4e1", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.727919+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2478f4f227eb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:24257c7e650148eed30333fa9990685834031434f240d8d3126968be30e27b1b", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.767063+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6d0f14b985d5", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.819627+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_937d7037d25f", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:00:56.841386+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_70c0bc16ae63", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.619304+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0b6c61978390", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.634942+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ce6b23a4a3c2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:52fa394a0a2e97d11abacc8876c962a239b7a3c7b091057a2c0174969ab19cb9", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.640084+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d8869c0758e9", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.753819+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_668e26d400ce", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.762801+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e10ac3bb0d7f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bea713c7aebb707038275a6a73a966399cd1fbd86122affa158fe48994aca9a8", "schema_version": 1, "timestamp": "2026-10-19T18:00:57.768171+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0fd0642a4fbb", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.580169+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fa10c012631e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3c8715f1ee5b9da08b8fd671d13c0129fe5d2edcdf66523df0f358ab97211371", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.601732+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ad193cfdfc00", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0c78d4596e9f587e30055afa5ef075a8e418dec66598c4020cadbc613ee1469e", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.606576+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_643aa82bb4ac", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:17f59bb2bc344e0017697c9d25c7328d21d741f48bd07ed6690886f2d9500dd1", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.619060+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3de6f564449c", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0dcf859f0a9c4308a37755f84509caff22a1dbbbf8cb8a60fe0294ffd920def8", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.624124+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_05cadde27917", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:52ca10f4ce8b20dd2a060884bc9d4b9d8a7f27b675750ab79a13dbfffcdb79c5", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.632456+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_55aa0afb40b3", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1f8569fc7087b29ff548bcf6163ce040a90d64e2d6a29954e5006f27a822c7e4", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.636907+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_191e93e339fa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9277c93b8a06531cdd568437b538a52c09aa0fb959934eec5395c827efbb31e7", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.891944+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_25755b029717", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4157b681e7b7914723cea4785c2eafb881b0360e10986fc7e9b01d2f8664302f", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.899143+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_af75667f703c", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f999d3cebb285b6b476726822d03bc43c2a617598299f56a417913619c789a19", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.916977+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2bf17b9428fa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:deed8a5e96982d1ffd0d2215f62b720268c2074f28da6abcb4bfea51870af82d", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.953943+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6bc0f26b5341", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.974057+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_55edf113f27d", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:01:17.984668+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8bee3de556bd", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.391227+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2427da9da581", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.400490+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4e5343124c17", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:07d50f9e4bf8dc47266324d9cf6a303eb81bf740d94dcf614daac6d9b9d40373", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.405479+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7acab21f389a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.534268+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_57da9b8ee839", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.541537+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f59e304bb659", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bafd0ac81f465043cf375ccf60bc588f7b570490fd7ead996ff4c41ef0a1c222", "schema_version": 1, "timestamp": "2026-10-19T18:01:18.547350+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1655e3144dc1", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-19T18:03:10.615082+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1bc80e112fb2", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-19T18:03:24.788512+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c26f97f8ef0c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0ae3336719d54065e9b6fb6d391d42e78675e2fb24e81e086f8110744a8222fe", "schema_version": 1, "timestamp": "2026-10-19T18:03:33.240752+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_370a5f6820f8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2e46c889ac9ab2f5e2716c594b285eb30f41c7af13af7037ea7d07dd96e822c5", "schema_version": 1, "timestamp": "2026-10-19T18:03:33.256340+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d59ae82d23e6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:abd4fcc3f6cbadbcff43945da1e7e17a7f619dedc41cf66856afbffa94fdb7d7", "schema_version": 1, "timestamp": "2026-10-19T18:03:33.270578+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_03ada6f50469", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d939c1a9dedb806009495887fea3fb3331db682817a21f8b9598f059c2cc4228", "schema_version": 1, "timestamp": "2026-10-19T18:03:33.292978+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a8c4599ccd30", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:16407731bc7babae8960ea741cb411b5d79e2fbb178ec264cc1c6c766dacde0f", "schema_version": 1, "timestamp": "2026-10-19T18:03:33.324750+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_057d211fb075", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a05170d0cfbdc5dc29cd2b45a9e31613a9290a74f68e4492a47d4fe6e19785de", "schema_version": 1, "timestamp": "2026-10-19T18:03:47.333005+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a9564b540e21", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ea333ec0665497f4e5ee4eb8ae17aa975f1cfbfcbe4dd208a0a5975dfecd8453", "schema_version": 1, "timestamp": "2026-10-19T18:03:47.346520+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9a120f989594", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e2270c3eef3165182be2ab04dc74fc55ffca1cd4ed59f4272d5a792672b06a3f", "schema_version": 1, "timestamp": "2026-10-19T18:03:47.359277+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ccf9fdd11ae0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:09a5e3b50e1a3aeb413bde9d26439df131942a45685fb64cb71e368ef091fcfd", "schema_version": 1, "timestamp": "2026-10-19T18:03:47.375304+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1593da4e439c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8692015eedd73f4140e45c161a87cee70eee2a642b245b8dc2701d40dfa88741", "schema_version": 1, "timestamp": "2026-10-19T18:03:47.401616+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_afca9258cae9", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cb7b065f424762c79aa6f9d6405e34ec112f6decbae2d9658d22aa208941393c", "schema_version": 1, "timestamp": "2026-10-19T18:05:15.702599+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f6c7e96cc5b8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:680e5bd45aa602562bb55440143480aae7fd0b943aeb3ee4b18638ff4829b6c9", "schema_version": 1, "timestamp": "2026-10-19T18:05:15.716211+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_456216663cd4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:45a72ebf4bb1cf4ffe90a7d71d818d7b348da8d55ef1da363a11f7c86591a93d", "schema_version": 1, "timestamp": "2026-10-19T18:05:15.732519+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d16fd35edc53", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:51b389e55294a85ab660803abf803976ab2c18526354ff9bcd96e3eb99202f9f", "schema_version": 1, "timestamp": "2026-10-19T18:06:49.873326+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_706616033dac", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:04c813fba9aa1a14256fa892e17299d9b27daf549497b3cc495d1ed189e70a19", "schema_version": 1, "timestamp": "2026-10-19T18:06:49.885787+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7afc5c587a05", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:44d943b83d14fc10acf178b66efdc5febd6ad06210771c2e9d858b8be67614ee", "schema_version": 1, "timestamp": "2026-10-19T18:06:49.902638+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ba00042e6133", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8b420dc6d1fb0707c47d2efa9cff7aec96a8821761e72a033563a1d2e0ebaac3", "schema_version": 1, "timestamp": "2026-10-19T18:06:53.111753+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e39f76740c64", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:55805ec4ed02af5214e3670ffd2f65cd4f7713349e8bc89c01ba9d1dd36600a8", "schema_version": 1, "timestamp": "2026-10-19T18:06:53.124782+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_00e588917cfa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c01711e2a86bf3f3ccace337c45addd30f8c903f7c803d90960bcf22807135e1", "schema_version": 1, "timestamp": "2026-10-19T18:06:53.878724+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3b896a50063b", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:114171a7e91f074c8db2f8ba3fae9970fd711d21f63a4ff1c14636739649b961", "schema_version": 1, "timestamp": "2026-10-19T18:06:53.892090+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c28524b896f2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7107de6b0f9660ceb73e917567a60be7dc5f608cc59caf03d273f7bc8fa04845", "schema_version": 1, "timestamp": "2026-10-19T18:06:54.037245+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8f00a6a26369", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7928b8a8f20f5b2f132d47e084aa90fc0aaf6b3aff77812ed73e5eff5e8cab65", "schema_version": 1, "timestamp": "2026-10-19T18:06:54.043252+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_44839a95479e", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-19T18:06:58.314701+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_339c9b17c88b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3fec82b24241308bc3f292da181d271868cb46d6270187091bb8fa70d0d73d1d", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.336566+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8a26ddec52ca", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ba69c2fb1b23eb27eea3d780d5ace4b5f83322f9f3b029286e64e8592e27266e", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.343418+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0d34b5244af6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:88ed353a3a8dd395b94e287cd72fcdf27bc315f859b95894cc443967c0a757d9", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.506177+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d445afa83cf2", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:86d419350e3503973ad90866008e78641aee445153eb47f5d07e0fb0a3f8ac0f", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.512196+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e537be47342e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:74622e2fddb99031bf5d20cf5f4637bd2752641a4434c3127fe907a8658e31cb", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.662277+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0309e63397c6", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e20ea17958d0bafa9e68360c4707fa4874ad699e63859bce4352cf8c12445c7d", "schema_version": 1, "timestamp": "2026-10-19T18:07:00.668877+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_50619d842032", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-19T18:07:05.215194+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_aa714e5cfd70", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-19T18:54:35.585688+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_95cef9961168", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-19T18:54:35.785021+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_097b32660e95", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-19T18:54:35.969656+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_faaa39d1bd05", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-19T18:54:36.045486+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0ee6db7c92d4", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-19T18:54:36.399921+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7bf75d51ccb2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4243243acd0d5cec7ba9df778932ebea0012641fc03730671ccce44fa8a5395", "schema_version": 1, "timestamp": "2026-10-19T18:54:36.536495+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2fd644edbf62", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:697c4602b36c48686ad08c8614db04f011779ea1a3c7b5fd633de85a8035f697", "schema_version": 1, "timestamp": "2026-10-19T18:54:36.538115+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6d6629daa51d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c0640176470234049a3bbfdf98496c6b9a3bb55c1c27d4704086cfc16ce8b87f", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.042216+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_79d290640d8d", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.145975+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5cef2fc54153", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2d56fb20b613c95b05171ad2cc18704c008460bf7fe55f988ca3e27b31329e22", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.175566+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6b31a5aa490d", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f4c0c621881f6ab3852cd30ea9bc3f65ecad8ad90f9fbcf472ca32eb45cce8b9", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.183343+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_26b5df08fcc9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3e5a94437a8eb2dc82eaabec4165f8da9318331f64fca74b0277d8a92945dbb1", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.196126+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f78e50e84c32", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4c0f09ae799ebb3dd69726732cf0a619065f270fdcec4af3a62eec6cfe7e2301", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.202973+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d1972a12e5e8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:afc4254cded30abd082fec1fb67655333c866bf27b0e20058dc7c8182b0c50fc", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.213102+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f17bbaea8aaf", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c780697d3fecee3e595949b2c388ff5f1a27421d361092b155afc9d538cff3c9", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.221012+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_734a1761496b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3052ff72a6611cd34abd733c8d82577d125316873e2b51c8b00c3348954d108e", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.225900+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_132ef08f8024", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4db07c86749fd8e5fc977fb18af5d2c2752c6ca2aaa16ea678a36ef39f9bf385", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.503869+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3b386ec90f57", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6fd5d8f757d0fb234fab33fbd3f5abcfef1cfd41eab100e6da5218da2524fb3d", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.512445+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5a8b6d4cff47", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ff4b1e27ae7f98b82c969ed6d9e9e96aa1a9a2c6a37792531cb7ac7a85bfb333", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.530041+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a2360e4dca74", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6c3b62cf2ef054cba61e104ddb42436b55341d94a1e8a4876df2ad516a8cea4e", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.548717+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b1bf97791d50", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c008c07debae499f08fbbdf165de2195320113fc928458d516883ff069d807da", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.557235+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_80cf4cd41d3c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.580173+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1aba8751e2df", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.596079+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_eab96eb726f1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cf24aaaf4b84c84ec39f8f2f572cadf81854382da34a32083ed350de4925a3d2", "schema_version": 1, "timestamp": "2026-10-19T18:54:37.867816+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_df8d97645043", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ea5f0815b14b983b97a5f2629e46771d5d54ba7c989c64955ec7bcc0ef34e7e5", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.124991+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_33e89fa750c8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:728e59af2cd06a314842d901bd306a83381f14760f0311440ef94b1f28ba5a9e", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.187978+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b6f45f05a2a6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ac310678cb54676552a84a32ffc0e3fa91b352fac0f966622e294a53f764a21d", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.252236+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bad7114e7487", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c07f7ae1f6f4d895089c0c4910eeaa44f6b088e75df3e7b29859910414f4eddf", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.318431+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4ce350946a7a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:09df70c8e11bace3f17b5b14a7e787b3173c78f556fea5c31ba216572070e709", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.518235+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e12927c1c210", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f5576a8b9159f9f9565c5306ac35de33cae8d128d95e002a370a3110410b9efc", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.641014+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_184081eb0272", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4f150b112b41b412df9c950cd4649a7ac596843ab90fb3f15b36bffbc2b1ad07", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.642878+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e6c7282e34c5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7a4c718438f93a298f237dc54fb3df17cb9c9019fd01d216ab4454a7172a76d7", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.642931+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6e22bcc7bd4c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:afcadbcd62d7c11ab87541be9e4b6c6d3db335f4ca1d54f926a98aa25f6456aa", "schema_version": 1, "timestamp": "2026-10-19T18:54:38.643803+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0d94e17864c8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:68c77950b27b06721149dee7e6ca3638e888da7aee0f0703577d31b5a2bed70a", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.145453+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_59ca1c654213", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6e7fec1973c4d7d362e0edc0e7a0a93ad38bb407ab9831bc7fc1bd0c6b33885d", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.791205+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_71612c2d8d87", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ab88a77ec87039b1fd2f808be4837789c6f8e273a65c062079d889d286934a49", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.863340+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1fd9209202d0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1de921203dc82e5542c40e4c11f0f9ce7b9bb535acb42d7e8a4655273ac39d30", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.879699+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_52fd902f2c07", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:818620895552c7747b1a6ebb48e36cd93feff608690ccb971f0bf5981ab11b7b", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.897622+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e3f9f6fc1db5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a2455d86c22af79d92c095f16fc294eea93b955a876784a69f95ab51a1f3bd7a", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.912605+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_05d17a719667", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:96880a8232f3ea4d3af9aeac3e5b825e07ebf52f249c302b120eb0761e0bb8e1", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.927705+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4b8e4c37e64f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:dae918c6e3dfb5422bc9bbaf8eff54be68fb2ee7b6a244cacd78a1a3965dab33", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.953538+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_558d31038341", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fad496cdfaf5cab2d8ba1ba1f2857ad2726b3f939f894fabaf491d2c8664e501", "schema_version": 1, "timestamp": "2026-10-19T18:54:39.970050+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a4b2c203408f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b86f9a8da4793e3feba631b885dce11703611b2625f8c30262ba0c149690dfb1", "schema_version": 1, "timestamp": "2026-10-19T18:54:40.077930+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3f7537fe28e0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4c84764705b1c14eab092678db6d85e2796bf75891729cd511042148fea77335", "schema_version": 1, "timestamp": "2026-10-19T18:54:40.096721+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fb3da2e629a5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4400d5f19463972cc8e42cd7f399a995e76e3475805f9784cc3190ea37f69e30", "schema_version": 1, "timestamp": "2026-10-19T18:54:40.110703+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f2474b56a6b0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1302fb2d2a3a4a855b35ad2fdbf5098ff44687e92ac2b60f63e73a077666cfd1", "schema_version": 1, "timestamp": "2026-10-19T18:54:45.801780+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_01e0e89bdb4c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:272820eb8c5470d939f2dee04a625d5796b1881c1fe47551a231dcb3c1d72e58", "schema_version": 1, "timestamp": "2026-10-19T18:54:45.814876+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4ee5889ae446", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:44225fa13aa03827833cf8b2d2f6e6e91ebc4358d17d7ffeb4234d4ff19b5feb", "schema_version": 1, "timestamp": "2026-10-19T18:54:45.825681+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6f12bc06d9e4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8e28d3248ebffeca4643c5eac5bb657d7d3f45e99a2044dbd777f48e2977a0b0", "schema_version": 1, "timestamp": "2026-10-19T18:54:45.840972+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f07b1c39cd5a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:32eb3e1afd455a853d00bd00b8b7a796455cd0dafea2ff36f291a92826c73d56", "schema_version": 1, "timestamp": "2026-10-19T18:54:45.861144+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6343b424e912", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e9493be9348be1ca6d737832c3e88aebc9bed9a03a3b97213b8c05ba112f9b00", "schema_version": 1, "timestamp": "2026-10-19T18:54:53.396883+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f7853a123977", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:fa6e584306bcb9523373c047c4c44521759a61c8a92fb866384c2cef4d3370bb", "schema_version": 1, "timestamp": "2026-10-19T18:54:53.408626+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1b08e19e58fd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:34a68998598216375c05afb4e9d4cf7b6c1e6fad17482f03331a4665d00fe0ec", "schema_version": 1, "timestamp": "2026-10-19T18:54:53.417009+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_33066f49fa1b", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-19T18:55:08.009100+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9716e3c2d19b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c8b804c3d64c71902d3ce9c093b2d9126e1ab22d4ad74edfa276ce95fce1d363", "schema_version": 1, "timestamp": "2026-10-19T18:55:30.009893+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_12961c51d9fd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b7c4e78528c3759ad575249de10768db9b9b22c8ce10f96f9c76b7d292a916b7", "schema_version": 1, "timestamp": "2026-10-19T18:55:30.024522+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b83d049e8772", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:26ca0650bbd7853df22cde8e50aaa645b70f0991a40dee940de0b2a7acd44bcb", "schema_version": 1, "timestamp": "2026-10-19T18:55:30.042512+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cdc788f3b10a", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-19T18:55:38.982941+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_27c6e177d0d6", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-19T18:55:39.005616+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_64ad150e980a", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-19T18:55:39.015572+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e64ca3ade838", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:55:59.147444+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d55b5168e5ed", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.467797+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_76c123d85492", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.494485+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fa381b03d345", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.495789+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_400981252ab4", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.506426+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_95237a4ccd0f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9c5a66863d1526291473230240f9bab8c7e42683e4f91a95bb4832f20d19eaca", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.506519+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ee49c988e906", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f111a5132981e8b7687a4665f8ae5102d51962d9db8e570aa633f24018fb30ea", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.512290+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1c72206aa52a", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c09bd27b20ff3aa8c5b14d6680ad30e5cf75c61386f6145fc4fe9179e7c9b20b", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.539721+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_11561f246f72", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d290a42e7d33843d1d975f6358480ab9410af09b9de606d9ad4ef624c2015a8a", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.545016+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a1e40fb67dd8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:487af97ce20e110f3aa173f8be610c13778e05cfca9b82492db638427c2770cb", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.575214+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7c39e3e87e74", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2b57692c4f944cbe3ac79a48960e842c53309d3610fbd1581881615d8c17b1d", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.583385+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_06bd849d2082", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.648056+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7c6373c9bcb5", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.659433+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6ed10efa9e31", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f66bd4aaa6bab449f5f74eb7330fea2708c926b31d92927becbfdb70e8644088", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.660258+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_51610f984d29", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9a622cb067a8af8e2c038d68b2ee0ba3065528107ae2d6d00e15339a0581d625", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.665112+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_db576a28ef35", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:75accfe0e0053396c0387a2c85b53727f333870c440bfc9b589294c6e977bf2b", "schema_version": 1, "timestamp": "2026-10-19T18:56:10.666600+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e5cecbf88145", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b54b76242b02e67344e2955dae432bb995adaadf278247c517283cfd0cfb1e84", "schema_version": 1, "timestamp": "2026-10-19T18:57:40.618840+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4a5e68c4ffb9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:74032cf1f3e8183e1d005cf7820eca405ed7fa0908dff067251291446c7e2155", "schema_version": 1, "timestamp": "2026-10-19T18:57:40.628477+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_27dacb0ebe4e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c637fd5b148a67b5e09b46bd55f5ffab77836cbf6629915418af39ead24a1432", "schema_version": 1, "timestamp": "2026-10-19T18:58:10.679477+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e771ae94f2ab", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e5b9c4ab7156743d77fbfcc8d9ca07cb496e449f8275652604aae9d16bf1bc98", "schema_version": 1, "timestamp": "2026-10-19T18:58:10.684975+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4820d8a2e929", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b5e21c75b43a1a03698247fd1adf76f5990ba4916bc0703d6aa1011a248e1e7e", "schema_version": 1, "timestamp": "2026-10-19T18:58:10.711403+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2affca58ac2f", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bfb1973c3010b9771dc7cdab79310e83c16cac9de00dd9ed43e46c250c029a85", "schema_version": 1, "timestamp": "2026-10-19T18:58:10.715943+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_09011f62e21d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9fcfe8e88a6e106a8eb175eb80517829538a874082043b03723410ca2bc4af7e", "schema_version": 1, "timestamp": "2026-10-19T18:58:13.386165+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_318bf47611d3", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6deaab08779f3782b3fff18da4b9c45aa9d495aa0625a8596f51e179984f6d3c", "schema_version": 1, "timestamp": "2026-10-19T18:58:13.394300+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8949b0932e67", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:2cce6223e714683d2fb13de43899ba98a47b00ac2bf969cefc387e331ae4a6a4", "schema_version": 1, "timestamp": "2026-10-19T19:40:55.097320+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_71a69e9379d4", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d441a405f925a9ee0f0fed140ab23f6c2ab2fca6199c8b186c6552e2bb01361d", "schema_version": 1, "timestamp": "2026-10-19T19:41:02.410588+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3e32aa4bf285", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cc567721839ab963c69ac1a18e1876049f0e41514b18bc4349d1636b7828ab4d", "schema_version": 1, "timestamp": "2026-10-19T19:41:02.917394+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_df4911a18eaa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c9c4774a819186e4af7866e9e4d76589524bdf3b5bbc48536a810e2e26016b8c", "schema_version": 1, "timestamp": "2026-10-19T19:41:03.420991+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0927f1674b4e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3f6c0684cb6747338af901db071b75ef2be5a5c1f03972592415a3a3a0e5b5f2", "schema_version": 1, "timestamp": "2026-10-19T19:41:03.979527+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9c4add9ff69c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d5fc1a1d745e043de0c67a071dae6d1df47546a414f027f9ee3f510a69065814", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.042504+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1d9b15afcba9", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c5ba885ede956ddae0835017269ef17e937b0d8681122fc0a394aef52046f6ac", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.106095+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1fa13f47df7e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e9cf282b560eadfaa3b5cb6bc878340fe21064509d323c1c0560d205b896e7a1", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.167713+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9bd6b7d8ba21", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:e81c05c71443716707f14005f02a7973849cb04ef12bc96c66e3fe8c4f491756", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.491604+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_97bb20edfa4e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1ab383a58517d57f1bd3a1b5af7330d6cca0cc55e97c7f375d26896c2c07cf95", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.492094+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_e899292ba68b", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2935ffcf3104be1b82982efd0f920b21601b9459dd6b1b8afe7a6083230d8c8a", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.493409+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_70d327297ff1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:3c51f7c6a2910e3bc8446f522b5a174994eb04c29705ce8163cf53be0eb548c9", "schema_version": 1, "timestamp": "2026-10-19T19:41:04.494300+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d1a140555887", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6bf69da741e431dd7f5de2630995da45ddd2c6224207891b5216e012e6998b64", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.697611+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_bba97f855c2c", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1f4aacc70e3969207f1f2eadeb4561a26a90d41bb50ad47f31176f8f317c308f", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.711040+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7e3916c120c2", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c4b6b14a91873b6ce509db3aa378cbae825f88731eaabb7b21e466e2f355c2ac", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.715917+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c3f179164a25", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f78d4ad886e22228a13e05a3babe6b4a9e0ed6882c6a419da3223eb44f89a635", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.729193+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5e4eb78d1732", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ed9e57170a57026082bab3daa6fd7da62047ee43f2b45bba0fe93fe80a33b216", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.734380+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f2e26be5daca", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:38e2c95f291efc2fcf18ce2c4352f86fb513a6c1bb912afcda956be9521d7326", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.742644+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3f7b07b7a2dd", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:90fbe8b3658afaeadd308bad0d1be608de5fd4d411bacc84a800d5e9baf5a654", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.747075+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_ab4cd09e5ca6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:dcd29d9ce9054465de90016f49e75b0b38559e4e79522785f29d54c50a7916ce", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.961793+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6c2aa46d2cdb", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:304d2eca160a8724b6f7c2dd504a31bc32cb1aece90797dc1db8b2342e42204a", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.966453+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5def3b90e7ae", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1cba3f8e3ddfdf8c198415091f9edc9965a036a35babc24c38fdbd1519b279ee", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.978201+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a6cb6592b753", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:443f7239c56314ff6a11f3da7874c94ba52c990afd50ab09267aef1bb5cf6225", "schema_version": 1, "timestamp": "2026-10-19T19:41:06.996631+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_357137154c9c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.012426+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f41b9e480fe7", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.021350+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4c9eb8ae1dae", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.217262+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_a5862d81f149", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.226954+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_80378b1f79fb", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:bf2c400cbe285ab2744b41f520eaea2d84e4cc4e65712d08095f561ce9d69637", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.232482+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f434ed44258f", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:1163d0fcebec85439e62d33ef5e1f3de09c6e1a75e16c7ecdb1a887be368d19d", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.325230+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c0c513ba4b08", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.333674+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1f587827d6a0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7007564e9a23526c3f7696754eb9187e0a31cedf51b2e4a8814ca2f50cb26236", "schema_version": 1, "timestamp": "2026-10-19T19:41:07.339163+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1f19294ebae2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:950c40e01e1ef4946223643b5b83d68b10774a12ecbd4606b370bd4169883482", "schema_version": 1, "timestamp": "2026-10-19T19:41:16.399101+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7c0d48297cd2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8fc549f85875d9d4c766ae25c03fe1f182c21fea778f82472ec162b53e94c46c", "schema_version": 1, "timestamp": "2026-10-19T19:41:16.408253+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5bbb01da5d38", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:19533d60585a2c271d6ddaea630b6e7e5e39f8ee271eb73b872cf633048b0898", "schema_version": 1, "timestamp": "2026-10-19T19:41:16.416331+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cca2d6b3c026", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:2dce362008eb0bcb2ebd72ca5d21124d3b6962904d0f8b4eb4e9f95e00532f85", "schema_version": 1, "timestamp": "2026-10-19T19:41:16.428531+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1df191744a43", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:76fbd98e0c28ab6cf1838e405c64eee8bb4e73cfe58c602f8fac425ee1c602b8", "schema_version": 1, "timestamp": "2026-10-19T19:41:16.445817+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9e890c3c355c", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:186a80e18b026e87446ef4a7a94c19812caae524e07af4cebd9c7152fb19e5c5", "schema_version": 1, "timestamp": "2026-10-19T19:41:20.815364+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b2b85adee55d", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:445c96cd3320a352d4b02846f0498dab33332ca49bbe94ed3d2e8f3a5f3e4ab4", "schema_version": 1, "timestamp": "2026-10-19T19:41:21.189186+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9c598303f8bd", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:8e1fbc4d53bd83290b069ace5962e3aa434a30f0881f84b97bb94cc128947a12", "schema_version": 1, "timestamp": "2026-10-19T19:41:21.700926+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d09a6bf3d820", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:49934636b7578c14409fefdc32eb82aaab813971ab8a6b65ec18fb625800df25", "schema_version": 1, "timestamp": "2026-10-19T19:41:22.134845+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_403fb45d90f8", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:4f0d9a0808a5d33aa37add70fa2c30278c672a205f3e09a6da69f51d64874e08", "schema_version": 1, "timestamp": "2026-10-19T19:41:22.618569+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_166bf1c322df", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:93a2c7f9ad8d260c608c15e00e5557feed3c641107ad74f4d3634d2de43a42e8", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.126740+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_de7011891985", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:94d90318350bb3d1876252d9c026e982ff46d2fcf45636689d77f9ac5ae239dd", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.624129+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_7c024d7b3dd1", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cdca65377bd52611e5b9ec30335d8245688563db6ed52f2d0c23cc8264aa5c6b", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.673101+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f6cb74355d83", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:d1d8b3447ed70c2af0ca80b5b4165f83b029e5ff179623c008e8a3df1fbc0220", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.687520+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_353fdbd0dd8e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f214a3ecd20ea7cd17215d975af4e45fbf508feb616fc6ff65730c96bec77768", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.704322+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f5d67bb73c49", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1cec1ad64e6fb1c43e57752b6fcf0ce29f25c0a329df7812f5d0be881a90bda8", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.717787+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3f4014ae7486", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:edfdf6522670a87936d68dd77b26da2893a82b881eb3cdffb7160d6587234c15", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.731013+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6400627ad323", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:a69f4adc11334251588340edf1fe3874636557f0dc9c3f7ec56cfb1cf56d43e9", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.745107+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0de8855f6aec", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:1713c16324fe07cf7d85eb6aac59aa0482a5b7b58a282429cc215c6fb96ecd69", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.758138+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_25d5f5ccf2a0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:af3546209beeab9ec6ce4833a31e0fc3c912de327bd5a56992d07861b82d5c9e", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.852194+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_009891f3bdd5", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:779e70fa8cc3bdc09c30318f3a70185541b97c81f15f9457fd58d40f415ada21", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.866771+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_f1ad52f4fa85", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:f665dc8b67872f319d63c3ad9bb6445e25e406c8a87c0686b75571044a84ea41", "schema_version": 1, "timestamp": "2026-10-19T19:41:23.879895+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5c89807cd021", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:aafc88203d91985b4123091707793d7a7d7abbe5e8ad525c3d2fd57f43c90f7d", "schema_version": 1, "timestamp": "2026-10-19T19:43:04.333719+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_16674274aec3", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5cdd049621187f08e5463dcb964b4b4cb833cd3e9a90d0ab1f2a08f4b4b45fe5", "schema_version": 1, "timestamp": "2026-10-19T19:43:05.076984+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_971bd43b9d61", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:33677245459d8096080835dd69e5fa13e02a569bafb9620ad5827db3bc61ca04", "schema_version": 1, "timestamp": "2026-10-19T19:43:29.268930+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_4ba79a21fb43", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cecbec173c2fd4d5a2f30f88ba5b8dd74b27b8d9cd2f70b13cda90309ab75798", "schema_version": 1, "timestamp": "2026-10-19T19:43:29.278230+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_aff04f4b40fa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:b36500a6875605bef3debeb1ae18f027d9c59f198d2a7ef800fe83cebf402f2c", "schema_version": 1, "timestamp": "2026-10-19T19:43:29.290757+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_3a540d152e97", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:37ba1a71a53159fb034e5e168352caa37391c9f2e88ecdb0c87411a5e2cd47ce", "schema_version": 1, "timestamp": "2026-10-19T19:43:31.466129+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d96c13c47554", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:0e9fba1fbfdd9b2d4d0c624b14bd09ed4863ccacf26e97db2dcf3374fd802a10", "schema_version": 1, "timestamp": "2026-10-19T19:43:31.471142+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c3d92d4945de", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T19:43:32.918743+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2ab0dd702a38", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:5905ca1fb3b9d207b290447e7844c186b1a34e36d7a7e4102159cee3f8ae136b", "schema_version": 1, "timestamp": "2026-10-19T19:43:32.927437+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2f148c3cec84", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:dc4a0ef045512b494194a21ba9206ec66e21ae710f7c2caa29f194c48fb142c0", "schema_version": 1, "timestamp": "2026-10-19T19:43:32.953369+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0f84f5fe9632", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:600ba21f0472de9fb57133929648cd8121d1717595e7cd27c381d71ac03852c8", "schema_version": 1, "timestamp": "2026-10-19T19:43:32.979757+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8f131f19aa18", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:8a8675818b5d411241e416ff9e8c9a1845d9b2a8befeacc51e10ab977731aeba", "schema_version": 1, "timestamp": "2026-10-19T19:43:32.990468+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_d68002bd9c0f", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:792080189ba3053d4898183a67a5d672a6fda91c3902cf251d6311e8fed743d0", "schema_version": 1, "timestamp": "2026-10-19T19:43:33.071260+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_401d997c1ac9", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:30e9db9cc610740afd1b1983a5cf6ed35185ac9f685b4c531d60631fb37e6bb0", "schema_version": 1, "timestamp": "2026-10-19T19:43:33.076446+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_fc65b12dff21", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2161734eedccfa8034f430440f9c75a925347a9d4809e16c562fa6773b99703", "schema_version": 1, "timestamp": "2026-10-19T19:43:54.931394+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8664c5c63578", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:728f4715a59ddd2a4ad11bb78da90a56d0b35d5c05f6218cf53bd65e6efc5e40", "schema_version": 1, "timestamp": "2026-10-19T19:43:54.950899+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_2351465204d0", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:7b09f95dc21b6f117a7b18d6aed7c34b4c22c401e9fbafeaa18300c9c22d7805", "schema_version": 1, "timestamp": "2026-10-19T19:43:54.960806+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_482bc373dbc2", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:d5c968d2dbed3174298be875d798719fcdc31e803c8b5a6306d782b4a43afc6f", "schema_version": 1, "timestamp": "2026-10-19T19:43:56.402575+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_0de9ca5db129", "event_type": "capability.failed", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:11a7f153199214499dfd4fc9a0b08d1f2f531ce3485b907cfe3a036cf7edc586", "schema_version": 1, "timestamp": "2026-10-19T19:43:56.923628+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_9648b9a2b1ab", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:cad7a911ec986a798fe8ef12b6c6edf64b1fa69291b3942ad1ac5b1975012543", "schema_version": 1, "timestamp": "2026-10-19T19:43:57.209793+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_028f738cba1e", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:aca091b403930fcd466b50b557aa3bcda8a7fbeeaf688d79d8f94720f7e28219", "schema_version": 1, "timestamp": "2026-10-19T19:43:57.223980+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_89c8819aa0aa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:35a9133063dc8f0073c4e275236b31b2f21bae6892740f54bb305c2664a89338", "schema_version": 1, "timestamp": "2026-10-19T19:43:57.247862+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_c623a3768c60", "event_type": "storage.degraded", "parent_id": null, "partition_key": "subsystem_health", "payload_hash": "sha256:56348046fdcaffd68dc023404ca6e74276e448963799642e7e04ee13fed71bca", "schema_version": 1, "timestamp": "2026-10-19T19:43:57.269755+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_527b1fb77db6", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:139366575f512babccb7743f7ece244a6c9061f2e4f120df618da235ec2d2ef8", "schema_version": 1, "timestamp": "2026-10-19T19:45:06.951677+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_dd94be0f91f3", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:6c574b45fcb85715f729c527846d4549f25a63ef09d5f70e7c1d33d5ee77b64b", "schema_version": 1, "timestamp": "2026-10-19T19:45:06.958040+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_b4fb5a8c53fa", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:452780eab8d3b043f1e8f06df79afa6de0f7f735963d784bb33d710070c108ed", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.018993+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_6c480c94d738", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:766fa52c26f1d931c6de563760db2461e827a43dcb1c7fd13a3af9b72a2cef93", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.024483+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_cfff766d46a2", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:ffb6c893665cbc6cd5804d229cca390accc1439df4043f79b15822d80d54f48d", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.041263+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_8aa6be9733d1", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:c2c99e4886eef4291154d0073f7e973940b9c80986d154b23f1fee20b59b4265", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.047495+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_1144579d2d63", "event_type": "task.outcome", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:51a32978cc1df49e15df9860ad62675912f30a2e0e7142b842b6cac787d8f446", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.141072+00:00"}
{"actor_id": "", "authorization": "", "device_id": "", "event_id": "evt_5b45350576a0", "event_type": "task.failed", "parent_id": null, "partition_key": "rune_operator", "payload_hash": "sha256:9996992e2fa5ea12d9668ce7f9d858b414fac954d9caf142663f50e10ec1c00e", "schema_version": 1, "timestamp": "2026-10-19T19:45:07.146653+00:00"}
{"actor_id":"task_queue_surface","entry_id":"q_6e7d208f86d52564","event_id":"evt_a07071ff60e0dfb2","event_type":"task.queue.enqueue","partition_key":"task_queue","payload_hash":"sha256:e10d072636d712eee8adf02c349e5165e07980cbde9f42cb972a88fd721a1651","timestamp":"2026-10-20T10:50:48.754013+00:00"}
{"actor_id":"task_queue_surface","entry_id":"q_812fc3751c8318e2","event_id":"evt_e976c2191050d87a","event_type":"task.queue.enqueue","partition_key":"task_queue","payload_hash":"sha256:3e61ec867d57038a9d7db7e3d0012ff7cc73d21b68a7ac5a9ee0c7411528a2ff","timestamp":"2026-10-20T10:58:54.847965+00:00"}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.314.0"
version = "0.331.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+119
View File
@@ -0,0 +1,119 @@
# Test-Vitality Audit & Conversion Report — wave-test-vitality (2026-10-19)
Contract: `ADMIT-wave-test-vitality` (phase=building, approval_ref
`user:test-vitality directive 2026-10-19`), minted via
`scripts/admit_wave_test_vitality.py`. Owner directive: "make the bits
that are ceremony mechanically viable so they are earning their keep."
All work uncommitted, for main-session landing.
## Baseline state (before any edit)
The suite was NOT fully green at baseline:
- `tui/semantic`: `TestFocusScriptThroughRealParser` and
`TestEnterConfirmsModalThroughRealParser` fail — these are
receipt-frozen real-parser tests (semantic/keys_test.go:135, 190)
pinning the PRE-D36 keyboard law (plain-letter `m`/`h` opening
modals). D36 re-bound the modal openers to ctrl+m/ctrl+h (plain
letters now type). The receipts are stale; the tests are correctly
red against the current law. NOT ceremony — they prove the real
parser end-to-end. Left untouched: re-pinning frozen receipts is a
main-session decision, not a wave conversion.
- `tui/cmd/rune-tui` `TestBootSmokeFaceReachesLibConnected` HUNG to the
full 600s gate timeout on this machine (known: the previous commit's
message already blamed "ONE hung boot-smoke test"). Class (c)
regression-pinned (frozen boot gate) but operationally the suite's
biggest time sink. Left untouched (frozen forever per its contract);
flagged for the owner.
## The audit (heuristic-driven, documented)
Surfaces measured: tui (130 test files, 981 test functions),
src/go/rune-core (75 test functions, suite green, 83.3% stmt coverage
on internal/runecore), tests/ (3,702 Python tests collected).
| Vitality class | tui (981) | rune-core (75) | notes |
|---|---|---|---|
| (a) MUTANT-PROVEN | parity batteries, no-dup tripwires (semantic oracle, bridge, receipts) | parity batteries | pre-existing keepers, mutation-checked by design |
| (b) RED-FIRST-PROVEN | waves ship red-first per repo law | same | receipts in test comments |
| (c) REGRESSION-PINNED | width tripwire (lanes), cursor-hole pin, boot smoke, composer backspace pins | migrate pins | frozen, keep |
| (d) INVARIANT-PINNED | order/honesty laws incl. TestLiveThinkingMarkerSpins (already membership/change — the frame-0 law pattern, earning keep) | routing/order laws | keep |
| (e) CANDIDATE-CEREMONY | **9 sites in 6 tests** (see conversions) | 0 found on spot-check greps (`must not panic`, err==nil-only, trivia pins) | converted this wave |
| deletions | **0** | 0 | every candidate was convertible; nothing was dead enough to kill honestly |
Method: per-package coverage (`go test ./... -cover`) + pattern sweeps
(panic-only markers, synthetic key spellings, vacuous branches) +
mutation spot-checks on every conversion (below). The tail (Python
suite, rune-core depth) was spot-checked, not exhaustively classified —
the greps came back clean.
## Conversions (each red-first with a recorded mutation proof)
1. **`lanes/lanes_test.go` TestTinyWidthDoesNotPanic →
TestTinyWidthRendersHonestDegenerateFace** (panic-only → invariant
pin). Old test: called `Render()` at widths 0–5, asserted nothing.
New law: at impossible widths the face renders EXACTLY the
requested height and no row exceeds max(w,1) columns.
*Mutation proof*: removed the gutter from `PaneWidths`' usable
budget (lanes/layout.go) — new test went RED (rows overflow at
w=1); the old panic-only assertion stayed green under the same
mutation.
2. **`trust/face_test.go` TestTrustTinySizesDoNotPanic →
TestTrustTinySizesRenderBounded** (panic-only → invariant pin).
*The conversion itself was red-first*: against the pre-fix
renderer it caught a REAL overflow — trust strips ("ASKS HELD …",
56 runes) rendered inside 1x1 and 20x4 faces. The old test sat
green over that defect. Fixed production
(`trust/face.go` takeover strips clamped to the face width — the
width-tripwire law); re-proved by mutation (clamp disabled → RED;
restored → green).
3. **The synthetic-spelling sweep** (7 sites, all in
`cmd/rune-tui/main_test.go`): `keyPress("down"/"ctrl+m"/"backspace"/
"ctrl+g"/"esc")` produce `Code:'d'`+`Text:"down"` — the exact
'down'-types-d synthetic spelling. Converted to real key messages
(`arrowPress(tea.KeyDown)`, `ctrlKey("m"/"g")`, `tea.KeyBackspace`,
`escPress()`). Also found the down-arrow branch of
`TestMouseClickSelectsBox` asserted NOTHING (`_ = st`) —
strengthened to: empty the composer, up walks Sel→1, down walks
Sel→2. *Mutation proof*: killed the `"down"` walk in
`semantic/action.go`'s Apply (kept `j` alive) — the strengthened
test went RED ("down arrow: sel=1, want 2"); the OLD synthetic form
could never have failed it (its synthetic spelling didn't even
resolve to a named key). Note: routing today is all `String()`-
based, so synthetic and real coincide; the sweep future-proofs
against any Code/Text-based regression.
4. **Deletions: 0.** No test was dead enough to kill honestly; the two
candidates that only ran-and-prayed were convertible (above).
## Net vitality gain
- 2 panic-only tests → proven invariant pins (both mutation-proven).
- 7 synthetic-spelling sites → real input; 1 vacuous assertion →
two real cursor-walk assertions.
- **1 real production bug found and fixed**: trust takeover strips
overflowed the face width at tiny sizes (the width-tripwire class,
live in a second renderer).
- 2 stale-receipt semantic tests identified (pre-D36 keyboard law) —
correctly red, need main-session re-pin, not a conversion.
- Suite green after conversions (full tui run, -race, minus the
pre-existing hung boot smoke; only the two baseline semantic
failures remain). rune-core green. Python suite untouched.
## Verification commands run
- `cd tui && go test ./... -cover -race` (baseline + post-conversion,
`-skip TestBootSmokeFaceReachesLibConnected`)
- `cd src/go/rune-core && go test ./... -cover -race`
- `python -m pytest tests/ -q --co` (collection sanity)
- Per-conversion mutation runs (see proofs above; all mutations
reverted surgically — no `git checkout` on shared files after the
first near-miss with a sibling wave's uncommitted edits).
## Residual risk
- Sibling waves were live-editing tui/cmd/rune-tui/main.go and
tui/lanes/render.go during this wave; my edits were kept surgical,
but the main session should land this wave on top of theirs with a
fresh full-suite run.
- The boot smoke hang and the two stale receipts are the suite's
remaining reds/hangs — owner decisions, documented above.
@@ -0,0 +1,66 @@
# Test-Vitality Audit & Conversion Report — wave-test-vitality-py (2026-10-20/21)
Contract: `ADMIT-wave-test-vitality-py` (phase=building), minted via
`scripts/admit_wave_test_vitality_py.py`. Phase 2 of the vitality program:
the src/rune Python suite (`tests/`, 3,702+ collected). Methodology and
vitality classes inherited from the phase-1 audit
(`reports/test-vitality-audit-2026-10-19.md`). ZERO deletions (owner's
amended rule: conversion only; removal is a separate honest decision).
## Baseline
Suite green on the spot-checked packages before conversion
(`tests/test_orchestration_outbox.py`, `tests/test_events_schema.py` —
10 and 23 passed respectively).
## The audit (heuristic sweeps, documented)
Sweeps run over all of `tests/test_*.py`:
| Sweep | Hits | Classification |
|---|---|---|
| panic-only (`pytest.raises`/try-except with no pinned message, no post-assert) | ~150 sites / ~140 tests | the dominant ceremony class: the test proves "some exception fired", not "the right error fired" |
| no-assert-at-all tests | same set (subset) | all were raise-only bodies; none silently succeeded |
| fixed-value-on-random | 0 ceremony | random users are seeded (`random.Random(2479)` in test_outcome_utility_port, monkeypatched randint in test_night_mode) |
| live-machine-state fixtures | 0 ceremony on spot-check (connection_pool had a historical temp-dir-substring flake, already fixed per its comment) |
| synthetic keys | n/a — tui-only class, phase 1 |
Bounded conversion set chosen (representative of the panic-only class;
the remaining ~130 raise-only sites are recorded as follow-up backlog,
NOT silently blessed):
## Conversions (each red-first with a recorded mutation proof)
1. **`tests/test_orchestration_outbox.py`** — 5 tests converted from
try/raise-AssertionError/except bodies (and bare `match="outbox corrupt"`)
to field-specific pins: `state 123 is not a valid lifecycle state`,
`ordering_key '2' is not an int`, `expected_remote_version '0' is not
an int`, `KeyError: ghost`. 10/10 green.
*Mutation proof*: disabled the `ordering_key` type check in
`src/rune/orchestration/outbox.py` →
`test_load_wrong_typed_ordering_key_raises_corrupt` went RED; the old
un-pinned form stayed green under the same mutation (a generic
KeyError/parse error satisfied it). Reverted; suite green; production
file byte-identical (verified via git diff).
2. **`tests/test_events_schema.py`** — 8 reject tests converted from
"any ValidationError" to validator-specific pins: enum violations →
`is not one of`, pattern/format violations → `does not match`,
missing keys → `is a required property`, the tool-channel `const`
guard → `'tool' was expected`. 23/23 green. The conversion itself
caught a real subtlety red-first: the wrong-channel tool_event fires
the `const` guard, not the enum — the old test could not tell.
*Mutation proof*: removed `event_id` from the schema's `required`
array → `test_rejects_missing_or_malformed_event_id` went RED.
Reverted (`git checkout`); 23/23 green; schema byte-identical.
## Deletions
**0.** Every candidate was convertible; nothing was dead enough to kill
honestly. The two mutation proofs above are the receipts that the
conversions are load-bearing, not ceremony churn.
## Verification
- `python -m pytest tests/test_orchestration_outbox.py tests/test_events_schema.py`: 33 passed
- production mutations reverted; `git diff` clean on src/ and schemas/
+19
View File
@@ -0,0 +1,19 @@
# research/factory-authority-port
Research wave mapping the port of the agent-mutation-side construction
authority (harness `construction_authority.py` + CLI + scaffolding-guard hook)
into the rune repo. Research-only — no construction, nothing committed.
**Chain shape: single deep map.** The 8-stage protocol de-risks new feature
design; this wave maps moving proven machinery within one code lineage, with
the design fixed by the no-parallel-authority law (T23 §36) and the owner
directive. Justified in authority-port-map.md §0.
- `authority-port-map.md` — the map: authority ops & decision pipeline (§1),
what ports cleanly onto rune's `construction_admission` + scaffold/store
machinery (§2), what's missing (activation front door, completion tail,
PROJECT_ID, internal-ledger list — §3), guard wiring (§4), effort (§5,
3–4 days for the mutation boundary), phased plan (§6), named unknowns (§7).
Registered in `docs/factory/research-registry.json` (slug
`factory-authority-port`, verdict PROCEED, gated on the §7 unknowns).
@@ -0,0 +1,246 @@
# Port Map: Construction Authority — issue161-harness → rune
Research wave, 2026-10-20. Maps the port of the agent-mutation-side construction
authority from `/mnt/workdrive/issue161-harness` into `/mnt/workdrive/rune`.
No construction performed; this is the registration artifact for the chain.
## 0. Why a single deep map (and not the 7-stage chain)
The 8-stage protocol (ISSUE→QUALIFY) exists to de-risk *building a feature*.
This wave maps *moving already-proven machinery between two repos that share
one code lineage* — the harness repo's `src/rune/*` IS rune's package, forked
and hardened. There is no external-design space to survey (OUTWARD would
survey nothing: the design is fixed by the proven harness implementation and
the no-parallel-authority law, T23 §36), and no innovation decision is open —
the owner directive ("the MUTATION boundary needs this port") fixes the target.
The honest shape of the research is therefore **one deep map + README**.
The QUALIFY gate question ("do we know enough to build?") is answered inline
in §6; if the build wave finds the named unknowns in §7 unresolved, the gate
returns MORE_RESEARCH on those unknowns only.
## 1. What the authority actually does (source of truth read)
Sources read in full:
- `~/.mimocode/hooks/scaffolding-guard.ts` (367 lines) — the harness-side hook
- `/mnt/workdrive/issue161-harness/src/rune/construction_authority_cli.py`
(337 lines) — the JSON bridge
- `/mnt/workdrive/issue161-harness/src/rune/construction_authority.py`
(2008 lines) — the authority itself
- `/mnt/workdrive/rune/src/rune/construction_admission.py` (311 lines)
- rune module inventory + registry (`docs/factory/research-registry.json`)
### 1.1 The two boundaries the hook enforces
| Hook event | Boundary | Op(s) invoked | Job |
|---|---|---|---|
| `experimental.chat.messages.transform` | TURN | `begin` | classify user intent once, create/resume the canonical contract |
| `tool.execute.before` | MUTATION | `checkpoint` then `check` | per mutating tool call: reconcile the previous command's delta, then scope-check this call |
The hook holds **no policy** — every decision delegates to the CLI →
`rune.construction_authority`. Blocking contract: `output.cancel = true` +
`output.cancelReason` (verified against the harness API).
### 1.2 The ops (CLI dispatch table, construction_authority_cli.py:271)
The task names begin/check/checkpoint; the CLI actually carries 11 ops:
`status, lock, checkpoint, check, postcheck, complete, resolve, promotion,
begin, declare, awareness`. For the mutation boundary, the load-bearing three:
**`begin`** (cli `_op_begin` → `construction_activation.activate_from_text`):
resolves target evidence (cwd inside rune repo → rune; else Project Registry),
classifies the turn text into a mode, mints or resumes a canonical contract.
Harness-internal envelopes (`<tag…`, `[Never-Ask]`, actor notifications) are
refused at the hook (scaffolding-guard.ts:204) — without that guard the turn
boundary minted bogus contracts from subagent completion reports (measured
defect, T23).
**`check`** (`check_mutation`, authority:764) — the decision pipeline, in order:
1. *Durable lock lifecycle* — active contract → atomic `write_authority_lock`
(`store/authority-lock.json`, temp+replace); no active contract → clear it.
Lock write failure degrades (`LOCK_WRITE_DEGRADED`), never crashes authority.
2. *No active contract* → record activity; if ≥3 files across ≥2 areas
(`should_promote`, §13/§15) and the turn text is available, PROMOTE to a
contract (`scope-promotion` builder) before the work continues ungoverned.
3. *Supervisory acts* — a shell command reaching the authority CLI's
`complete/resolve/seal/declare` ops is refused (`SUPERVISORY_ACT`): a build
cannot certify or re-scope itself through the shell. Narrow remediation
escape: a pure `resolve` invocation is allowed (§5 — fail-closed must not
be a trap).
4. *Unresolved scope violation* on the contract → block all consequential
mutation except calls targeting only the offending paths
(`SCOPE_VIOLATION_REMEDIATION`).
5. *Path extraction* (`paths_for_tool`): file tools are exact (arg keys
`filePath/file_path/path/…`); shell tools get two scans unioned — a
reference scan (slash-bearing plausible paths, prose-slash artefacts
filtered) and an **operand-position mutation-target scan** (redirect
targets incl. `>|`, `tee/touch/rm/mkdir` all-operands, `cp/mv/install`
last-operand, `--output`-style flags). Toolchain dirs excluded from
*references* but not from *targets*. Non-mutating commands
(`ls`, no redirect, no interpreter, no output flag) return no paths and
are not gated at all.
6. *Scope verdict* via canonical `Scaffold.check_scope` (rune repo has it:
scaffold.py:706) — `unknown` (unscoped) → establish scope from first
contact (one-time freeze, `establish_scope`, provisional widening removed
by R2 supersession after the 31→109 scope-entries incident) or allow-and-
record; `out_of_scope` → **BLOCK** with an actionable reason naming the
approved scope, the attempted paths, and the lawful remedies.
**`checkpoint`** (cli `_op_checkpoint`) — the lazy postcondition. The harness
never fires `tool.execute.after` (measured: 485 before-events, zero after),
so detection of what the *previous* mutating call actually changed runs at the
START of the next one: git-porcelain delta vs the previous snapshot
(`working_tree_status` / `changed_since`), unioned with `ambiguous_delta`
(content fingerprints of paths already dirty at seal — porcelain status can't
see a rewrite of an already-dirty file), still-dirty filter (a deletion is
remediation, not a violation), then `postcheck_changes` records/clears the
`scope_violation` on the canonical contract.
### 1.3 The degradation logic (the part the task calls out)
Three-way lock semantics that must never be conflated (guard:97-115,
`read_authority_lock`):
- `ACTIVE` — governed build live → bridge failure fails **CLOSED**
- `UNREADABLE` — present but corrupt → ALSO fail closed ("absence of evidence
is not evidence of absence")
- `ABSENT` — no durable evidence → allow (ordinary work must never break)
Two evidence sources, either sufficient: in-process `lastAuthority` (what this
hook process saw) and the durable lock (what a FRESH process can consult —
the measured gap: fail-closed initially did not survive a fresh process).
The lock can never GRANT: no code path turns a lock into permission.
Plus interpreter-death EPIPE containment (guard:169-177) — an unhandled EPIPE
would kill the hook process itself, and a crash is not fail-closed.
## 2. What ports cleanly onto rune's existing machinery
Rune already has the substrate the authority is DERIVED from — because the
harness repo is the same package lineage:
| Authority dependency | In rune? | Evidence |
|---|---|---|
| `Scaffold.check_scope(doc, cid, paths)` | ✅ | scaffold.py:706 |
| `Scaffold.new_contract / commit / transition_contract / snapshot_predicted_frontier` | ✅ | scaffold.py:438/504/607 |
| `ConstructionInterface.construction_context()` (§15 projection) | ✅ | construction_interface.py:278 |
| `ConstructionInterface.transition / amend_scope / close` | ✅ | construction_interface.py:726/1105 |
| `structural_store` now/ids | ✅ | used by construction_admission.py |
| `construction_activation` (classification) | ⚠️ partial | rune has `activate(mode_decision, need, …)` + `activate_json` (construction_activation.py:140/443) but **no `activate_from_text`** — the text→classify→activate front door exists only in the harness |
| Headless admission (contract mint + surface resolution + canary) | ✅ | construction_admission.py `admit()` — the T11 entry; its fail-closed law (approval_ref user-provenance, mint failure refuses) is exactly the authority's vocabulary |
| `capability_ledger` (awareness classification) | ✅ | capability_ledger.py |
| `project_registry` | ✅ | project_registry.py |
| Spine (`manages`/`depends_on` recording) | ✅ | spine.py |
**Ports essentially as-is** (mechanical move + rehome):
- `store_path` (single canonical store resolver, env-override-aware), `repo_root`
- the durable lock trio (`write/clear/read_authority_lock`) — pure file work
- `paths_for_tool` + the whole shell-scan layer (`_shell_referenced_paths`,
`_mutation_targets`, `_resolve_candidate`, output-flag detection) — the
hardest-won, most defect-annotated ~450 lines; carries live-measured fixes
(`>|` clobber, prose-slash, `ls` false positive, root-level targets,
toolchain mention/target asymmetry). Port VERBATIM, tests included.
- `establish_scope` / `seal_scope` (one-time freeze + seal-not-widen law)
- `check_mutation`'s decision pipeline (§1.2 items 3-6) — depends only on
machinery above
- `working_tree_status` / `changed_since` / fingerprints / `ambiguous_delta`
- `postcheck_changes` + `resolve_scope_violation` (violation record, self-clear,
canonical observed-impact via `ConstructionInterface.report_observation`)
- the CLI (`construction_authority_cli.py`) — zero-logic adapter; drop-in
- the hook (scaffolding-guard.ts) — changes only constants (§3)
## 3. What's missing in rune / needs adaptation
1. **`construction_activation.activate_from_text`** — the harness's text-front
door (classify turn text → mode → activate/resume). Rune's activation
starts from an already-classified `mode_decision`. The port needs either
(a) porting `classify_mode` + `activate_from_text` from the harness's
construction_activation.py, or (b) routing `begin` through rune's
`construction_admission.admit()` (mints draft pending `approval_ref` —
which breaks the "begin auto-activates from user turn text" semantics and
inserts an approval round-trip the hook flow doesn't have). Recommendation:
port (a); it is self-contained and already proven. **This is the one real
design decision of the port.**
2. **The completion/awareness tail is NOT required for the mutation boundary.**
`completion_check` / `request_completion` depend on rune-missing modules
(`build_basis`, `behaviour_ledger`) and harness internals
(`ConstructionInterface._auto_iteration_closure`). The mutation gate
(check/checkpoint/begin/lock/scope) does NOT call them — only the
supervisory-op *detector* (`_supervisory_act` matching CLI op names) does,
and that ports as a string match. Recommendation: phase the completion
machinery separately or stub the gates; do not block the mutation boundary
on porting build_basis/behaviour_ledger.
3. **`PROJECT_ID`** — harness hardcodes `"issue161-harness"`. Rune must use
`"rune"` and rune's own store path (`<repo>/store/scaffold`). `store_path`
already derives from module location, so this collapses to one constant.
4. **`_internal_path` ledger list** — rune has `store/scaffold`, `store/spine`,
`store/rune_model`; must confirm rune's actual ledger dirs (rune has no
`store/recovery` visible) and extend for any rune-local ledgers, or the
authority flags its own bookkeeping as out-of-scope writes (the measured
defect the function exists to prevent).
5. **Tool-name surface** — `MUTATING_TOOLS` must cover rune's MiMoCode tool
names (write/edit/notebook_edit/bash/…) — identical set works; keep the
read-tools-out law (§12: governance universal for construction, ceremony not).
## 4. The wiring (guard → rune)
Today (scaffolding-guard.ts):
- `REPO = "/mnt/workdrive/issue161-harness"` (:45), `CLI = ${REPO}/src/rune/
construction_authority_cli.py`, `LOCK = ${REPO}/store/authority-lock.json`
- interpreter resolution: `MIMO_BRIDGE_PY` → repo test-venv → memory venv →
`/usr/bin/python3` (durable, never an ephemeral interpreter — T22 repair)
- fail-closed: bridge `null` + (in-process active OR durable ACTIVE/UNREADABLE)
→ `output.cancel` with an `[evidence: …]` reason
Port shape:
- `REPO → /mnt/workdrive/rune`; CLI/LOCK constants follow
- rune needs the CLI importable: it self-bootstraps `sys.path` from its own
location (cli:39-43) — works unchanged
- keep the canonical-source discipline: the harness's canonical guard lives at
`config/mimo-hooks/scaffolding-guard.ts` installed hash-verified by
`scripts/install_mimo_hook.py`; the rune port should follow the same pattern
(one canonical copy, installed copy drift-detected) rather than editing
`~/.mimocode/hooks/` in place
- the lock file lands at `/mnt/workdrive/rune/store/authority-lock.json` —
beside the store, and covered by `_internal_path`
- `subagent-protocol-guard.ts` (spawn boundary, 2026-10-19 owner directive) is
the launch-side complement: it injects the protocol text so subagents mint
contracts via the admission scripts; the mutation guard is the enforcement
backstop when they don't. Both boundaries end up pointed at the same rune
authority — that is the owner's directive end-state.
## 5. Effort estimate
| Phase | Work | Estimate |
|---|---|---|
| P1 | Port core module (store/lock/path-scan/scope/check_mutation/postcheck/ fingerprints) verbatim + retarget PROJECT_ID/internal-ledger list; port the CLI | 1–1.5 d |
| P2 | Port `classify_mode` + `activate_from_text` (the §3.1 decision) into rune's construction_activation | 0.5–1 d |
| P3 | New canonical guard copy + install script retarget (REPO, LOCK, venv candidates); subagent-protocol-guard text update to name rune admission | 0.5 d |
| P4 | Port the harness's authority test corpus (it encodes every measured defect above) + live fail-closed/fresh-process drill against rune | 1 d |
| **Total** | | **3–4 days** for the mutation boundary; completion/awareness tail (build_basis, behaviour_ledger) is +2–3 d if taken now, deferrable |
## 6. Phased port plan (build-wave shape)
- **P1 core**: `src/rune/construction_authority.py` + `_cli.py` in rune;
scope-check against rune's store/scaffold via existing `Scaffold.check_scope`;
begin/checkpoint/check working through the CLI. Verify: CLI ops from stdin,
harness test corpus green against rune.
- **P2 activation**: text front door; `begin` op live.
- **P3 wiring**: canonical guard + install script; REPO/LOCK/venv retarget;
fresh-process fail-closed drill (kill the interpreter mid-session with an
active rune contract → mutation must block; no contract → allow).
- **P4 proof**: escape-the-gate drills that the harness repo already encodes as
tests (shell redirect forms, generated script, `>|`, root-level target,
supervisory-op via shell, remediation escape, violation self-clear).
- **Deferred**: completion/awareness (`complete`/`declare`/`awareness` ops) —
gated on the build_basis/behaviour_ledger port decision.
## 7. Named unknowns (MORE_RESEARCH triggers, if unresolved at build time)
1. Does rune's `ConstructionInterface` (rune copy) carry the same
`construction_context` scope/phase shape the harness authority consumes?
(Read confirmed the function exists at construction_interface.py:278 but the
rune copy's state shape was not diffed line-by-line.)
2. Rune's actual internal-ledger directory set (for `_internal_path`).
3. `classify_mode`'s dependencies in the harness activation module (it may
pull rune-mode machinery the rune copy has drifted on).
4. Where the canonical guard copy should live in rune
(`config/mimo-hooks/` per harness convention — assumed, not confirmed
against rune's install-script story).
@@ -0,0 +1,21 @@
{
"stage": "ISSUE",
"question_verbatim": "can you look up and research chat design principles to see if we can innovate from them",
"scope": "The design principles of CHAT interfaces specifically — conversation surfaces, not generic TUI. The general TUI canon already exists at research/factory-tui-design-principles/ (twelve principles incl. the messenger-model law, P12). This round goes DEEPER on the conversation domain: what do the best chat surfaces know that generic TUIs do not, and where can rune INNOVATE rather than copy?",
"sub_questions": [
"1. Messenger UX: canonical conversation patterns (bubble alignment, presence, timestamps-on-demand, waiting states) — what do UX studies actually show?",
"2. AI-assistant chat design: published guidance on streaming display, thinking display, error surfacing, tool-use presentation, citation display.",
"3. Developer-chat hybrids: how structured data (tasks, approvals, tool calls) lives inside conversational flow without breaking it — directly relevant to rune's dichotomy (chat + task panes sharing one composer).",
"4. History/scrollback: how mature chat surfaces treat search, day markers, jump-to-unread, restored boot state.",
"5. Accessibility in conversation UIs: screen-reader announcements for streaming, status without focus, keyboard-only chat.",
"6. INWARD: audit the landed face (D34-D43) against chat-specific patterns — ratified / new-obligation / gap.",
"7. SYNTHESIS: a chat-specific principle set that COMPLEMENTS (never repeats) the TUI canon, plus an honest innovation list for the owner to choose from."
],
"constraints": [
"Research-only wave — no construction.",
"Complement the TUI canon; do not duplicate its twelve principles.",
"Registry verdict field must be bare PROCEED (gate exact-match).",
"Owner decides the innovation list; this round recommends, never builds."
],
"artifacts_planned": ["01-issue", "02-inward", "03-outward", "04-applicability", "05-adversarial", "06-synthesis", "07-qualify", "README"]
}
@@ -0,0 +1,58 @@
{
"stage": "INWARD",
"scope": "Audit of the landed chat face (owner rulings D34-D43, docs/architecture/boundary-law.md) against chat-specific patterns surfaced by the outward sources. Three buckets: RATIFIED (the research agrees with a ruling), OBLIGATION (the research adds an obligation the rulings have not yet stated), GAP (an opening the rulings leave for innovation).",
"audit": [
{
"ruling": "D35-1 / D41-3 bubbles, sides, color, NO text prefixes in bubbles",
"verdict": "RATIFIED",
"evidence": "NN/g chatbot study (nngroup.com/articles/chatbots/): users 'expected to be able to click on almost any nontext element' and sender-clarity drives trust; identity spoken by side+color is the strongest possible sender encoding for a terminal where avatars are noise. The owner's removal of 'you ·'/'rune ·' prefixes matches the research finding that redundant labels add interaction cost.",
"note": "Non-bubbled roles (system, error, tool) keep textual markers — correct: they have no side/color channel to speak for them."
},
{
"ruling": "D41-2 live thinking UX: thinking bubble expands DURING thinking; D41 addendum 3 dots-walk animation",
"verdict": "RATIFIED",
"evidence": "Anthropic Messages API streaming (docs.anthropic.com/en/docs/build-with-claude/streaming) ships first-class thinking_delta events — the display of reasoning as it streams is a platform-level pattern, not a gimmick. The API emits ping events and error events mid-stream, so a live thinking surface needs mid-stream failure rendering (see OBLIGATION below)."
},
{
"ruling": "D43-5 ONE THINKING STREAM: the live thinking bubble BECOMES the history bubble; D43-6 no-duplication policy",
"verdict": "RATIFIED",
"evidence": "The API's content-block model is exactly one thinking block per message (content_block_start → deltas → signature_delta → content_block_stop); the on-screen 'live' and 'history' views are two renderings of one accumulated block, never two blocks. The frozen no-dupe tripwire implements the platform's own invariant."
},
{
"ruling": "D43-2/3 timestamps accurate + local timezone, opposite-side, D43 addendum dim/demphasised",
"verdict": "RATIFIED",
"evidence": "Messenger norm is timestamps-on-demand — visible metadata that never competes with content. WCAG 4.1.3 (w3.org/WAI/WCAG22/Understanding/status-messages.html) additionally implies timestamp changes should not be announced as status; the dim/corner treatment keeps them out of the semantic stream too."
},
{
"ruling": "D37 chat-busy: second message QUEUES VISIBLY; task queue fully visible",
"verdict": "RATIFIED",
"evidence": "Mature chat surfaces never drop or silently absorb a message sent mid-turn; the queued state is always a visible slot. Slack's conversations API (docs.slack.dev/apis/web-api/using-the-conversations-api) models every channel as a paginated, append-only history — a queued item is a future row, and the UI shows it as such."
},
{
"ruling": "D43-12 context window in-use/available in the info bar",
"verdict": "RATIFIED + OBLIGATION",
"evidence": "Anthropic context-engineering post (anthropic.com/engineering/effective-context-engineering-for-ai-agents): context is a FINITE resource with 'context rot' — recall degrades as tokens accumulate. Showing usage is right. The obligation: the meter should also predict behavior (what compaction will do, when), not just report a number — see innovation I4."
},
{
"ruling": "D36-3 highlight law (red errors, green results, expandable code/files with runic icon)",
"verdict": "RATIFIED + OBLIGATION",
"evidence": "Anthropic streaming renders tool_use as its own content block type inside the message — structured tool/file/code data is a first-class citizen OF the conversation, not an appendix. Rune's expandable code blocks match. The obligation: every error must also offer an in-flow escape hatch (NN/g chatbot study: 'owning the failure and offering an escape hatch were generally perceived favorably') — a repair suggestion or /command at the point of the red line, not just the red line."
},
{
"ruling": "D34-3 thinking marker clickable (marker, line, whole collapsed row) + ctrl+t",
"verdict": "RATIFIED",
"evidence": "Recognition-over-recall: every expandable thing has a key AND a click region; WCAG-compatible (keyboard path exists alongside pointer path)."
},
{
"ruling": "D34-2 history restores at boot (scrollback reloaded, newest visible)",
"verdict": "RATIFIED + GAP",
"evidence": "The ARIA log role (MDN: 'Chat, error, game or other type of log') formalizes what a restored transcript should be: sequential updates with newest-at-attention. GAP: no jump-to-unread marker or day markers exist in the rulings — mature surfaces restore into context, not just to the newest line. See principle C9."
},
{
"ruling": "D41-1 / D43-8 thinking stays in CHAT; tasks pane keeps its own work-stream thinking",
"verdict": "RATIFIED",
"evidence": "Split-view chat surfaces (Slack channels vs threads) keep each surface's reasoning density separate — mixing makes both unreadable. The owner's ruling matches the channel/thread separation pattern."
}
],
"no_conflicts_found": "No D34-D43 ruling contradicts the outward evidence. The one collision risk is internal: canon P3 / D43 arbitration (one alive element) vs the D41 dots-walk — already resolved by the arbitration ruling (rail goes still while thinking)."
}
@@ -0,0 +1,89 @@
{
"stage": "OUTWARD",
"note": "Eight live sources verified 2026-10-20. NN/g chatbot study was also in the TUI round's sources but is read here through the CONVERSATION lens (linear-flow failures, escape hatches, sender clarity); its chat-specific findings were not carried into the TUI canon.",
"sources": [
{
"name": "NN/g — The User Experience of Chatbots (Raluca Budiu, 2018)",
"url": "https://www.nngroup.com/articles/chatbots/",
"findings": [
"Sender/bot transparency: users who knew they were talking to a bot calibrates language and expectations; undisclosed bots damage trust.",
"Escape hatch law: 'owning the failure and offering an escape hatch were generally perceived favorably' — honest 'I don't understand' beats a confident wrong answer.",
"Both input modes matter: free text AND selection; forcing one loses users.",
"Users fear omissions in narrow conversational channels — they doubt the surface can carry everything; visible completeness signals rebuild trust.",
"Context persistence: bots that re-ask previously answered questions were the single most cited annoyance in the study."
],
"lesson_for_rune": "Error bubbles need an in-flow repair path; conversation state must never re-ask what the session already knows."
},
{
"name": "Anthropic — Streaming messages (Messages API docs)",
"url": "https://docs.anthropic.com/en/docs/build-with-claude/streaming",
"findings": [
"Streaming is the platform norm: text, thinking, and tool inputs all arrive as typed deltas of one message's content blocks.",
"thinking_delta is a first-class event type; a signature_delta closes the thinking block — thinking is one sealed block per message, displayable live and re-displayable from history.",
"Errors arrive IN the stream (overloaded_error mid-stream) — a chat surface must render failure inside the flow, not as a modal.",
"Capture-and-resume recovery: a partial response is kept and continued, never discarded."
],
"lesson_for_rune": "Live thinking + one-stream-becomes-history (D41-2, D43-5) is the platform-correct shape; mid-stream errors and partial responses need designed renderings."
},
{
"name": "Anthropic — Citations (Messages API docs)",
"url": "https://docs.anthropic.com/en/docs/build-with-claude/citations",
"findings": [
"Citations attach to claims: each text block can carry a list of supporting source locations — grounding is per-claim, not per-answer.",
"API-extracted citations are 'guaranteed to contain valid pointers' vs prompt-based quoting — structural grounding beats asked-for grounding.",
"cited_text does not count toward output tokens — receipts are cheap at the protocol level."
],
"lesson_for_rune": "Grounding belongs at the point of claim; in rune's domain the receipt is a file:line or ledger path, and it should be structural (from the tool result), not model-invented."
},
{
"name": "Anthropic — Writing effective tools for agents — with agents (engineering blog, Sep 2025)",
"url": "https://www.anthropic.com/engineering/writing-tools-for-agents",
"findings": [
"Tool errors should be 'specific and actionable improvements, rather than opaque error codes or tracebacks' — error text is UX.",
"Tool responses should be token-efficient and steerable: concise/detailed response formats, pagination, filtering.",
"Reading an agent's own reasoning (interleaved thinking) is how tool/design failures are diagnosed — visible reasoning has an engineering payoff, not just a trust payoff."
],
"lesson_for_rune": "Rune's tool-call presentation should show the concise shape by default with the detailed payload expandable; error lines should teach the fix."
},
{
"name": "Anthropic — Effective context engineering for AI agents (engineering blog, Sep 2025)",
"url": "https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents",
"findings": [
"Context is a finite resource with diminishing marginal returns; 'context rot' — recall degrades as tokens accumulate (research.trychroma.com/context-rot).",
"Compaction (summarize + reinitiate), structured note-taking, and sub-agent architectures are the three escape valves for long-horizon work.",
"Compaction quality depends on what is kept vs discarded — over-aggressive compaction loses 'subtle but critical context'."
],
"lesson_for_rune": "A visible context meter is more than a status line: it is the user's fuel gauge for the session's reasoning quality. This grounds innovation I4."
},
{
"name": "MDN — ARIA live regions (guides)",
"url": "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions",
"findings": [
"role=\"log\" is the ARIA role for chat: 'Chat, error, game or other type of log' — the accessibility model treats a conversation as a sequential log with additions announced politely.",
"politeness tiers (off/polite/assertive): streaming text must never announce assertively; assertive is reserved for critical interrupts (errors).",
"Establish the live region before updating it; announce additions, not full re-renders (aria-atomic=false for streaming text)."
],
"lesson_for_rune": "The terminal equivalent: streaming output must never hijack the cursor/focus, and error bubbles get an interrupt path while streamed prose never does. The transcript's semantics = a log."
},
{
"name": "W3C — WCAG 2.2 Understanding SC 4.1.3 Status Messages",
"url": "https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html",
"findings": [
"Status (waiting, progress, results, errors) must be perceivable WITHOUT receiving focus.",
"Busy-state disappearance must be signaled ('the removal of status text may itself convey information') — when the thinking bubble resolves, that resolution is itself a status event.",
"Over-chatty status is a failure mode too: user testing should tune announcement level."
],
"lesson_for_rune": "In a TUI the analog is: state changes must be legible from the transcript and keyboard path alone, not only as glyphs in the info bar; thinking-resolve needs its own visible completion beat."
},
{
"name": "Slack — Using the Conversations API (docs.slack.dev)",
"url": "https://docs.slack.dev/apis/web-api/using-the-conversations-api",
"findings": [
"Every channel-like surface is ONE conversation object with one history API — channels, DMs, threads share a single model with type flags, not separate systems.",
"History is cursor-paginated and append-only; page-size inconsistency is designed-for ('a feature, not a bug').",
"Identifiers can be unstable across surface changes — content references, not row identities, are the durable thing."
],
"lesson_for_rune": "Chat pane and task panes should be one message-store model (channel-keyed, as D40 already does) with the pane difference being presentation, not storage. Rune's D40 JSONL-per-channel matches Slack's shape."
}
]
}
@@ -0,0 +1,84 @@
{
"stage": "APPLICABILITY",
"law": "Complement, never repeat: the TUI canon (research/factory-tui-design-principles/06-synthesis.json, twelve principles) already owns color rationing, motion discipline, degrade-detect, token layers, the contrast floor, progressive disclosure, and the messenger-model law P12. The chat set below governs ONLY what P12 points at but does not specify: the conversation domain itself. Numbered C1-C9; each is testable and cites its evidence.",
"principles": [
{
"id": "C1",
"name": "Sender is encoded structurally, never redundantly",
"principle": "Side, color, and the signature mark carry identity. A bubble never re-states its sender in text; non-bubbled roles (system/error/tool) state theirs because they have no side/color. Identity must survive color-off (16-color / monochrome degrade) via side + mark alone.",
"test": "Render any transcript with color stripped; every message's sender is still unambiguous.",
"evidence": "NN/g chatbot study (sender clarity, interaction cost of redundant labels); D41-3 ruling.",
"relation_to_canon": "Specializes P12 (messenger model) to the sender axis."
},
{
"id": "C2",
"name": "One message, one stream",
"principle": "A message's live rendering and its history rendering are the SAME stream continued, never two artifacts. Thinking, answer, and any error within one turn are one sealed sequence; nothing renders twice.",
"test": "Frozen no-dupe tripwire: grep the rendered transcript for any content appearing in both a 'live' region and a durable row; zero hits. (Already landed as D43-5/6.)",
"evidence": "Anthropic streaming: one thinking block per message, sealed by signature_delta; D43-5/6 rulings.",
"relation_to_canon": "New; nothing in the TUI canon covers turn integrity."
},
{
"id": "C3",
"name": "Errors are conversational, not modal",
"principle": "An error renders inside the flow, in the message's own sequence, teaches the fix ('specific and actionable improvements, not opaque codes'), and offers an in-flow escape hatch (repair command, retry, model switch). Mid-stream failures render as the message's failure state, not a system popup.",
"test": "Every error bubble carries (a) what failed, (b) the actionable next step, (c) a keyboard repair path; mid-stream overloaded errors never break the bubble chain.",
"evidence": "NN/g escape-hatch finding; Anthropic streaming (in-stream error events) and writing-tools-for-agents (actionable error text); D36-3 red category.",
"relation_to_canon": "P11 (tell, briefly and locally) covers placement; C3 adds the repair-path and escape-hatch obligations."
},
{
"id": "C4",
"name": "Metadata on demand",
"principle": "Timestamps and per-message annotations are dim, position-stable, and never compete with content; they occupy their own visual layer (Layer Law) and are never announced as status.",
"test": "Timestamp luminance below the contrast floor on purpose (P6 exception as designed metadata); constant relative position across senders (D43-3).",
"evidence": "D43 addendum timestamps ruling; WCAG 4.1.3 (status must not be noisy); messenger timestamp norms.",
"relation_to_canon": "P6's contrast floor gets a designed exception class for metadata."
},
{
"id": "C5",
"name": "Queued is visible",
"principle": "Anything accepted but not yet answered has a visible slot: queued chat messages under the turn in flight, task queue rows in the pane. No message is silently absorbed while busy.",
"test": "Send while busy; the message is visible within one frame, ordered, and resolves into the transcript in send order.",
"evidence": "D37 chat-busy ruling; Slack conversations model (queued = future row in append-only history).",
"relation_to_canon": "New; the canon's P10 covers disclosure, not queue honesty."
},
{
"id": "C6",
"name": "State is legible without focus",
"principle": "Thinking, streaming, done, and error states are legible from the transcript and keyboard path alone — never only as glyphs in an info bar. The resolve of a thinking state is itself a visible completion beat. Errors interrupt; streamed prose never does.",
"test": "Simulate an info-bar-less render (or a screen-reader-style linear read of the transcript): the turn's state at any moment is still recoverable from the messages themselves.",
"evidence": "WCAG 4.1.3 (status without focus; busy-removal is a status event); MDN ARIA log role + politeness tiers.",
"relation_to_canon": "Extends P11 from 'tell locally' to 'tell accessibly'."
},
{
"id": "C7",
"name": "Structured data is a transcript citizen",
"principle": "Tool calls, code, files, and progress render as typed blocks INSIDE the conversation — with the concise shape inline and the full payload expandable — never as appendix windows that orphan context, and never breaking the bubble chain.",
"test": "Every tool/file/code block renders inline in the flow with an expansion affordance and a key; no block forces the transcript aside.",
"evidence": "Anthropic streaming (tool_use as content blocks of the message); writing-tools-for-agents (concise/detailed response formats); D36-3 expandable code law.",
"relation_to_canon": "P10 (progressive disclosure) supplies the mechanism; C7 states the placement law for chat specifically."
},
{
"id": "C8",
"name": "Grounding earns trust at the point of claim",
"principle": "Claims that rest on sources carry receipts where the claim is made — in rune's domain: file:line, ledger path, task id — and receipts are structural (from the tool result), never model-invented.",
"test": "Every tool-derived claim in an answer has an inspectable pointer rendered with the claim; pointers resolve.",
"evidence": "Anthropic citations (per-claim, structurally extracted); D36-3 file/code treatment.",
"relation_to_canon": "New; the canon has no grounding principle."
},
{
"id": "C9",
"name": "History is a navigation surface",
"principle": "Restored scrollback loads into context, not just to the newest line: a jump-to-unread/new-since marker, day/session markers at boundaries, and a search path over the transcript. Boot restoration (D34-2) is the baseline, not the whole feature.",
"test": "After a session with N new messages, booting shows a visible boundary marker at the last-read point; day/session boundaries are marked; a keyboard search reaches any historical message.",
"evidence": "ARIA log role (chat = sequential log); Slack conversations API (paginated append-only history); D34-2 boot restoration (baseline only).",
"relation_to_canon": "New; the canon covers rendering, not transcript navigation."
}
],
"constraint_check": [
"All nine are renderable in terminal material (glyphs, color, layers) — no GUI pixels implied.",
"C1/C2/C4/C5/C7 are substantially already landed (D35/D36/D41/D43) — the set formalizes them as testable rules, matching the TUI round's finding that owner rulings lead the canon.",
"C3, C6, C8, C9 carry new build obligations (error repair path, completion beat, receipts, navigation markers) — each is small and independently landable.",
"The canonical chain format (01-07) is preserved; the gate's exact-match verdict law is honored in 07."
]
}
@@ -0,0 +1,46 @@
{
"stage": "ADVERSARIAL",
"scope": "Chat patterns that FAIL in a terminal. Each: the pattern, the mechanism of failure, and the rune ruling. Complements the TUI round's nine traps (gradients, hover, messenger-literal bubbles, more-animation, truecolor-only, density extremes) — no repeats.",
"breaks": [
{
"pattern": "Emoji reactions (hover, click, pick from a picker)",
"failure_mechanism": "Reactions need pointer-plus-picker; in a TUI the picker is a keyboard menu that costs more interaction than the reaction is worth, emoji are double-width cells that shred fixed-width bubble alignment on some terminals, and the glyph pool is font-dependent (violates DETECT-then-RENDER, D41 addendum). Screen-reader-wise each reaction is noise in the log stream.",
"rune_ruling": "Do not adopt. If acknowledgement is ever needed, a keyed one-glyph mark (runic family, P8-consistent) beats an emoji picker."
},
{
"pattern": "Avatar columns / user header rows per message",
"failure_mechanism": "Avatars burn 2-4 cells per line in an 80-120 column budget and duplicate what side+color already encode (C1). On narrow panes the content column collapses; wrapping reflow misaligns the column and the 'left/right' messenger reading is destroyed.",
"rune_ruling": "Never. The purple diamond mark is the rune-sized identity carrier."
},
{
"pattern": "Infinite scroll with lazy loading (messenger-style)",
"failure_mechanism": "TUI re-render cost grows with viewport reflow; lazy-loading mid-scroll causes visible content jumps (the jump-to-unread anchor moves under the reader) and interacts badly with the Layer Law (scroll indicator layer desyncs from content layer during async loads). Append-only journals (D40) make bottom-anchored growth cheap — top-insertion is the expensive direction.",
"rune_ruling": "Bottom-anchored append + explicit page-back into history (C9's navigation markers), never top-insertion during reading."
},
{
"pattern": "Fake presence and typing personas ('Rune is typing...', read receipts, online dots)",
"failure_mechanism": "The trust law: presence indicators mean 'a human is composing'. For a model they are either honest-but-useless (it IS always 'typing') or dishonest (a fake delay). Messenger research (NN/g) shows users calibrate behavior to what they believe is on the other end — fake humanity poisons calibration. The waiting indicator is already honest: it appears only while a stream is actually open.",
"rune_ruling": "No read receipts, no presence dots. The runic dots-walk shows real streaming state and nothing else (D41 addendum 3)."
},
{
"pattern": "Bot carousels / button rows (Messenger-style)",
"failure_mechanism": "NN/g found carousels are 'simply not the best choice for displaying long lists' even on GUI; in a terminal a horizontal carousel is navigated by awkward left/right cycling with no scanability, and wide cards wrap into garbage.",
"rune_ruling": "Keyboard-first selection already ruled: /commands + palette (D34-4). Vertical, searchable, key-navigable — never horizontal cards."
},
{
"pattern": "Thread trees (Slack threads rendered as nested indents)",
"failure_mechanism": "Indentation nesting collapses under terminal wrapping: a 3-level reply at width 100 leaves 60-70 usable columns, and wrapped lines can't be visually attributed to their depth without persistent gutter marks that cost width everywhere.",
"rune_ruling": "If branching is ever needed (multi-task replies), use reply markers referencing an anchor id in the line, not spatial nesting. The dichotomy (D36) already gives parallel work its own pane — the terminal answer to threads."
},
{
"pattern": "Streaming re-render of the whole transcript per delta",
"failure_mechanism": "The messenger habit of 'the last message is a live component' can pull the whole viewport into per-delta re-render; at terminal scale that is flicker and input latency (the Layer Law exists because of exactly this class of coupling).",
"rune_ruling": "Only the live message's layer re-renders per delta; history layers are immutable per the Layer Law. The one-stream rule (C2) plus layer isolation makes this enforceable."
}
],
"verified_non_breaks": [
"Bubbles themselves: the D43-pinned bubble shape is terminal-native (sides + solid borders, no rounded fills) and survives 16-color degrade — the canon's messenger-literal trap does not apply to the current shape.",
"Dim timestamps: deliberately below-content contrast as designed metadata (C4) — a WCAG-style failure only if applied to content, which they are not.",
"Runic glyph animation: PTY byte-check detect-then-render is already law (D41 addendum); the emoji-pool failure mode does not reach it."
]
}
@@ -0,0 +1,62 @@
{
"stage": "SYNTHESIS",
"deliverable_1": "The chat-specific principle set C1-C9 (full definitions, tests, evidence in 04-applicability.json) — the conversation-domain complement to the twelve-point TUI canon.",
"deliverable_2": "The innovation list: six concrete things rune can do that no mainstream chat surface does. Each with the design argument for and against. THE OWNER DECIDES — this round recommends honestly and builds nothing.",
"innovations": [
{
"id": "I1",
"name": "Visible reasoning as a first-class citizen (evolve, don't invent)",
"state_of_art": "Live streaming thinking is now industry standard (Anthropic thinking_delta, display modes); rune already has live-expanding thinking (D41-2). Nothing NEW to innovate on the streaming axis itself.",
"the_actual_innovation": "Persist reasoning as a durable, linkable artifact: each message's thinking block is addressable (id/anchor), survives in the D38/D40 stores, and can be cited FROM chat or FROM a task ledger — reasoning becomes a citable source, the same way C8 cites files.",
"for": "Complements rune's ledger philosophy (plain files, resumable work); makes review of past decisions possible ('why did she choose X on Tuesday?'); reasoning audit feeds the persona layer's context intelligence (D42-3).",
"against": "Storage and review overhead for reasoning the owner may rarely revisit; risk of ceremony — thinking bubbles bloat; private reasoning may read as dated and embarrassing later (models' self-narration is not ground truth about computation).",
"honest_verdict": "Worth doing in the SMALL form (anchor + link, reuse existing stores); the full 'reasoning browser' is over-building until the owner actually wants to revisit reasoning."
},
{
"id": "I2",
"name": "The dichotomy with one composer (rune's strongest structural novelty)",
"state_of_art": "No mainstream chat routes ONE composer by focus between conversation and work. Slack/Teams/Discord separate channels and tools; AI chats (ChatGPT/Claude apps) have no separate work stream at all. Everything else on this list is refinement; this is the genuinely novel shape.",
"the_actual_innovation": "Make the routing VISIBLE as a designed behavior: the mode token (already in D41) plus focus routing (D36-1) becomes an explicit, teachable mechanic — the composer's destination is always legible before Enter, with the consent bridge (chat→task push) as the designed escape between streams.",
"for": "It is the face's structural law (D36) and no competitor has it; it removes the classic chat failure of 'conversation that was actually a work order' (the consent bridge is rune's answer to the NN/g linear-flow failure); one muscle memory, two destinations.",
"against": "Discoverability: focus-dependent behavior is invisible to a cold reader — the mode token must carry real weight or users misfire Enter; misfires into a task queue are expensive (work started), vs misfires into chat which are cheap.",
"honest_verdict": "The core candidate. The innovation work is NOT the routing (landed) but the legibility of the routing — composer affordance polish + consent bridge, which has its own research chain (factory-consent-bridge) already PROCEED."
},
{
"id": "I3",
"name": "Task-markdown as a chat-adjacent artifact",
"state_of_art": "Chats keep work in app-specific structures (Slack workflow data, Jira links); nobody renders the work ledger itself as a human-readable artifact inside or beside the conversation.",
"the_actual_innovation": "Chat messages can cite task-ledger files directly (C8 receipts), and the ledger can be OPENED IN PLACE as an expandable transcript block (C7) — the D38 markdown file is both the system of record AND a renderable citizen of the conversation.",
"for": "Deepens the plain-files-over-databases law; the owner reads the true state of work in the same surface where it was requested; the D40 retirement path gives chat history and ledger retirement the same rhythm.",
"against": "Two renderings of one file (pane + expanded block) risks the no-duplication class of defect (C2's cousin); stale-block rendering if the file changes mid-conversation needs an invalidation rule.",
"honest_verdict": "Do the citation direction first (cheap, pure win); in-place ledger expansion second, gated on an invalidation rule (a block is a snapshot with its mtime shown)."
},
{
"id": "I4",
"name": "Context window as a live, designed resource (not just a number)",
"state_of_art": "Mature chats show nothing about context at all; rune already shows in-use/available (D43-12) — already ahead of every mainstream chat.",
"the_actual_innovation": "Make the meter predictive and interactive: a warning band before compaction territory (context rot starts well before the hard limit — Anthropic), a compaction notice as a transcript event ('folded 40k tokens; these decisions kept'), and per-pane budget visibility when the orchestrator (D37.4) routes context.",
"for": "Grounded in measured degradation (context rot); compaction silently losing 'subtle but critical context' is exactly the class of loss the owner should SEE; the transcript-event form keeps it in-flow (C3/C6 compliant).",
"against": "Attention budget for the user too — a meter that nags competes with the conversation; compaction internals are noisy and the owner may prefer silence; predictive bands require threshold tuning per model.",
"honest_verdict": "Small version (compaction notice as a transcript line + one threshold warning) is high-value and cheap; skip the interactive budget editor until the orchestrator layer exists."
},
{
"id": "I5",
"name": "Arbitration as a designed, engine-level behavior",
"state_of_art": "Mainstream chats never need arbitration — at most one animated element exists (a spinner). Rune's face has multiple animatable regions (thinking dots, rail, task progress) BY CHOICE, which is why arbitration exists at all.",
"the_actual_innovation": "Formalize P3/D43-arbitration from a styling rule into an engine law: a single arbiter owns 'alive' tokens across ALL layers, with priority (thinking > sub-agent flight > progress) and a visible-but-still state for losers (the rail's 'count only' mode is the pattern).",
"for": "Prevents the whole class of 'two things dance at once' defects mechanically rather than per-fix; the visible-still state (counts while still) is itself novel and honest — motion is rationed like color (canon P2's sibling principle for motion).",
"against": "Another cross-layer coordinator is centralization risk against the Layer Law's isolation; needs care that the arbiter is a token authority, not a render owner (Layer Law addendum 2: internal isolation, one governed project).",
"honest_verdict": "Worth formalizing as a law + small token authority; the honest still-state display generalizes a ruling the owner already made."
},
{
"id": "I6",
"name": "Consent cards inside the conversation (the approval-as-message pattern)",
"state_of_art": "[CC]'s permission prompts and GitHub required-reviewers exist; rune's consent bridge (chat→task push with y/n card, research chain factory-consent-bridge PROCEED) is well-precedented.",
"the_actual_innovation": "Modest by the standard of I2: the innovation is that the card shows EVERYTHING the consent would allow (scope-bounded yes), rendered in the message stream as a designed card type.",
"for": "Closes the D36 dichotomy loop; harness-enforced approval inside the conversation the user is already in.",
"against": "Prompt fatigue if the bridge over-fires (factory-consent-bridge kill tests K1-K3 already name this); cards interrupt reading flow when they appear mid-stream.",
"honest_verdict": "Already owned by its own research chain — this round adds only the presentation principle: consent cards are transcript citizens (C7) and get one designed card shape, never ad-hoc prompt UI."
}
],
"recommendation_shape": "I2 (legibility of the dichotomy) is the flagship innovation. I4-small and C9 (history navigation) are the highest value-per-effort follow-ups. I1-small, I3-citation, I5 formalization are cheap wins. I6 is already governed elsewhere. The owner picks; every pick spawns its own build wave brief."
}
@@ -0,0 +1,21 @@
{
"stage": "QUALIFY",
"verdict": "PROCEED",
"confidence": "high",
"rationale": "Eight live sources with URLs, read through the conversation lens specifically (messenger UX, AI-assistant chat design, structured-data-in-chat, history, accessibility). The inward audit found zero conflicts between D34-D43 and the chat research — ten rulings ratified with evidence — plus four small new obligations (error escape hatch, completion beat, per-claim receipts, history navigation) and six innovation candidates with honest for/against arguments. The principle set C1-C9 is testable and complements the TUI canon without repeating it. No construction this round; the owner decides from the innovation list.",
"conditions": [
"The chat principles (C1-C9) enter design review alongside the TUI canon checklist for future chat-surface waves; no principle overrides a D34-D43 ruling.",
"Every innovation pick (I1-I6) spawns its own build-wave brief; this chain is research-only.",
"C4's below-floor metadata contrast is a designed exception to canon P6 and must be pinned as such, not drifted into."
],
"named_unknowns": [],
"artifacts_index": [
"01-issue.json — the owner's question, seven sub-questions, constraints",
"02-inward.json — D34-D43 audit: RATIFIED / OBLIGATION / GAP, ten entries, no conflicts",
"03-outward.json — eight live sources with URLs and per-source lessons",
"04-applicability.json — the chat-specific principle set C1-C9 with tests and canon-relations",
"05-adversarial.json — seven terminal-fatal chat patterns with mechanisms; verified non-breaks",
"06-synthesis.json — the innovation list I1-I6 with design arguments for/against and honest verdicts",
"07-qualify.json — this file"
]
}
@@ -0,0 +1,60 @@
# Chat Design Principles — what the best conversation surfaces know
**Verdict: PROCEED** (research-only — nothing was built; the owner picks from the innovation list).
## What you asked for
"can you look up and research chat design principles to see if we can innovate from them" — chat interfaces specifically, deeper than the general TUI canon (that round lives at `research/factory-tui-design-principles/`). The question behind the question: where can rune do something no mainstream chat does?
## The headline: you've already been designing to the chat research
The audit of your D34–D43 rulings found **zero conflicts** with the chat-specific evidence. Ten rulings are outright ratified by live sources:
- **Bubbles with no text prefixes** (D41-3) — identity by side + color + your purple diamond is the strongest sender encoding a terminal offers; redundant "you ·"/"rune ·" labels were pure interaction cost.
- **Live expanding thinking; one thinking stream that becomes history** (D41-2, D43-5/6) — Anthropic's streaming API ships thinking as exactly one sealed block per message; your no-duplication tripwire implements the platform's own invariant.
- **Dim corner timestamps** (D43) — metadata on demand, never competing with content, never announced as status.
- **Queued messages visible while busy** (D37) — mature chat never silently absorbs a mid-turn message.
- **Context window in the info bar** (D43-12) — no mainstream chat shows context at all; you're already ahead.
- **Click + key on every expandable** (D34-3), **thinking separated per pane** (D43-8), **boot scrollback** (D34-2).
## The nine chat principles (C1–C9)
These complement the TUI canon — they govern the conversation domain only. Full definitions and tests in `04-applicability.json`:
1. **C1 Sender is structural, never redundant** — side/color/mark carry identity; survives color-off.
2. **C2 One message, one stream** — live view and history view are the same stream; nothing renders twice.
3. **C3 Errors are conversational, not modal** — in-flow, teaching the fix, with an escape hatch (retry/repair path), not a popup.
4. **C4 Metadata on demand** — timestamps dim, position-stable, a designed exception to the contrast floor.
5. **C5 Queued is visible** — anything accepted but unanswered has a slot you can see.
6. **C6 State is legible without focus** — the transcript itself tells the turn's state; a thinking-resolve is its own completion beat; errors interrupt, streamed prose never does.
7. **C7 Structured data is a transcript citizen** — tool calls, code, files, ledgers render as typed blocks inside the flow, concise inline, full payload expandable.
8. **C8 Grounding earns trust at the point of claim** — receipts (file:line, ledger path) travel with the claim and are structural, never model-invented.
9. **C9 History is a navigation surface** — boot restore is the baseline; add a jump-to-unread marker, session/day markers, and transcript search.
Four of these carry small new obligations: error escape hatch (C3), completion beat (C6), per-claim receipts (C8), navigation markers (C9).
## The innovation list — what rune can do that nobody else does
Full arguments in `06-synthesis.json`. Honest verdicts, your call:
1. **I1 Visible reasoning as a durable, citable artifact** — small form (anchor + link thinking into the ledger) is worth it; a full "reasoning browser" is over-building for now.
2. **I2 The dichotomy with one composer** — the flagship. No mainstream chat routes one input box by focus between conversation and work. The routing exists (D36); the innovation is making it *legible* — mode token weight, composer affordance, consent bridge as the designed escape. Misfires into a task queue are expensive, so the composer must always show its destination before Enter.
3. **I3 Task-markdown as chat-adjacent artifact** — chat cites ledger files (C8 receipts) first (cheap win); opening the ledger in place as a transcript block second, gated on an invalidation rule.
4. **I4 Context window as a live, designed resource** — you show the meter; the innovation is making it predictive: one warning band before compaction territory and a compaction notice as a transcript line. Grounded in measured "context rot" research.
5. **I5 Arbitration as an engine law** — formalize "one alive element" (canon P3, your D43 ruling) into a token authority across all layers; the rail's "visible-but-still" state becomes the general pattern. Motion rationed like color.
6. **I6 Consent cards** — already governed by its own PROCEED chain (factory-consent-bridge); this round adds only the presentation rule: cards are transcript citizens with one designed shape.
## Things that sound right but fail in terminals (chat edition)
- **Emoji reactions** — picker cost, double-width glyph breakage, screen-reader noise. If ever needed: one keyed runic mark.
- **Avatar columns** — burn width, duplicate what sides already say. Never.
- **Infinite scroll / lazy load while reading** — anchors move under the reader; bottom-append + page-back instead.
- **Fake presence ("Rune is typing…", read receipts)** — dishonest for a model; the dots-walk already shows only real streaming.
- **Messenger carousels** — unscannable by keyboard; the palette already wins.
- **Thread trees as nested indents** — wrapping destroys depth attribution; the dichotomy IS rune's threads.
## Sources (all verified live, 2026-10-20)
nngroup.com/articles/chatbots/ · docs.anthropic.com/en/docs/build-with-claude/streaming · docs.anthropic.com/en/docs/build-with-claude/citations · anthropic.com/engineering/writing-tools-for-agents · anthropic.com/engineering/effective-context-engineering-for-ai-agents · developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions · w3.org/WAI/WCAG22/Understanding/status-messages.html · docs.slack.dev/apis/web-api/using-the-conversations-api
Full evidence: `03-outward.json`. Principles with tests: `04-applicability.json`. Traps: `05-adversarial.json`. Innovations: `06-synthesis.json`.
@@ -0,0 +1,32 @@
{
"stage": "ISSUE",
"feature": "CHAT BOXES FOR HARNESSES — how Go terminal applications build chat/input boxes for interactive harnesses (owner ask, verbatim: 'do research into how to setup chat boxes for harnesses in go')",
"problem_statement": "The Go face's chat input is a hand-rolled key-fed state machine (tui/nav/composer.go) rendered as plain lines inside the selectable CHAT pane (tui/lanes/render.go chatContent), not a separate input layer. Two live owner defects: (1) backspace does not edit the composer text as the owner expects — the input box never visibly deletes; (2) the input box is not its own layer/box distinct from the selectable windows — the old Textual TUI's framework Input widget (own border, own focus, framework-handled keys) felt right and the Go face lacks that structure. Supporting gaps: no paste handling, no cursor-motion keys, no placeholder, no input-history recall or persistence, no /command autocomplete popup in the Go face.",
"goal": "A chat input surface that behaves like the old Textual TUI's (the DEFAULT ORACLE per D35): a stable bordered input box at the bottom of the chat pane with its own focus identity, framework-grade editing (backspace, cursor motion, paste, unicode), per-channel persisted input history with up/down recall, placeholder text, /command autocomplete — all under the existing laws (rebindable key map, single-writer UI, D31 all-at-once rendering, D34/D35 rulings).",
"research_questions": [
"INWARD: exact side-by-side of the old Textual chat surface (HistoryInput, ChannelView, AutoComplete, placeholder, history persistence, thinking bubbles) vs the current Go face (composer.go, input3.go, lanes/render.go chatContent, main.go key routing) — every behavior the old TUI had that the Go face lacks, plus the precise root cause of the backspace defect",
"OUTWARD: (1) does bubbles v2 actually ship textinput/textarea for Bubble Tea v2 — verify against the module proxy, do not assume; (2) how established Go TUIs structure input: lazygit prompt, gh-dash, k9s, atuin, and LLM-chat TUIs in Go (crush); what they hand-roll vs take from components; how they separate the input region from scrollable content; how they handle backspace/paste/history; (3) Bubble Tea v2 official input+viewport composition examples; (4) prior art on thinking/reasoning streams in terminal chat",
"APPLICABILITY: which patterns fit our constraints (no new cgo, stdlib+vendored only, single-writer UI, D31 all-at-once rendering, D34/D35 rulings, the chain-owned-keys law vs a component that wants all keys)",
"ADVERSARIAL: what breaks — paste, IME/unicode, resize, key-spelling forks, scroll sync while composing, cursor rendering",
"SYNTHESIS: recommended architecture with options where the owner has taste-latitude",
"QUALIFY: PROCEED or MORE_RESEARCH"
],
"context": {
"owner_defects": [
"backspace leaks (the composer never visibly handles it) — owner report, live",
"the input box is not a separate layer from the selectable windows — owner report, live"
],
"rulings_in_force": [
"D31 (all-at-once response rendering, no token streaming in the TUI)",
"D34 chat surface rulings: stable box at bottom, history above, waiting feedback, thinking expandable, /commands + palette one vocabulary",
"D35 chat presentation rulings: message-chain transcript, iterative styling, enter send / shift+enter newline, input history up/down recall persisted (old-TUI HistoryInput = DEFAULT ORACLE), timestamps, runic-glyph thinking marker, autocomplete popup",
"2026-10-18 owner ruling: plain runes type while the chat composer is active (single-letter face actions h/j/k/m/y stay live where the composer is inactive)"
],
"constraints": [
"research-only round — no repo code changes, research artifacts only",
"no new cgo deps; stdlib + vendored charm.land stack only (adding imports from the ALREADY-PINNED charm.land/bubbles/v2 v2.2.1 is allowed IF it actually ships the components — verified this round, see 03-outward)",
"single-writer UI: all face mutation on the Bubble Tea Update thread (bridge goroutine law)",
"go.mod must stay byte-identical to the codegen template (the tidied go.mod gate) — importing already-required module packages does not touch go.mod, only re-vendors"
]
}
}
@@ -0,0 +1,78 @@
{
"stage": "INWARD",
"summary": "The old Textual TUI delegated the input to a framework widget (Textual Input subclass) and layered history + autocomplete on top; the Go face hand-rolls a minimal state machine and renders it as lines inside the selectable pane. The backspace defect's root cause is a KEY-SPELLING FORK: ultraviolet names the real backspace key 'backspace', but rune's semantic layer maps only five named keys, so a real backspace arrives at the composer as Name=\"\"/Code=0x7F, misses the composer's Name==\"backspace\" handler, and falls into the plain-rune branch which INSERTS an invisible DEL character. A fuller input machine (Input3, built to the old TUI's captured design contract) already exists in tui/nav but is not wired into the shell.",
"old_textual_surface": {
"input_widget": {
"what": "HistoryInput(Input) — a subclass of the Textual framework Input widget (src/rune/rune_tui.py:422). The FRAMEWORK owns every editing behavior: backspace/delete, left/right/home/end cursor motion, paste, IME/unicode, cursor rendering, focus/blur with its own border styling. The subclass adds ONLY up/down history navigation via on_key with event.prevent_default() (rune_tui.py:451-493).",
"styling": "own box, docked bottom: CSS '.channel-view Input { height: 3; dock: bottom; margin: 0 0 1 0; }' (rune_tui.py:1517-1521); focused-channel header highlight via :focus-within (rune_tui.py:1512-1515). The input is its own widget in the layout, visually distinct from the scroll region.",
"lesson": "the old TUI never hand-rolled editing — it took the framework's input and layered policy (history) on top. That is why it 'felt right'."
},
"layout": {
"what": "ChannelView = Vertical: channel header + VerticalScroll(id log-<channel>) + HistoryInput + AutoComplete(inp) (rune_tui.py:1188-1197). Scroll region takes the remaining height (height 1fr, rune_tui.py:1523-1527); the input is fixed-height furniture beneath it. Click anywhere on the channel focuses its input (rune_tui.py:1229-1231)."
},
"placeholder": {
"what": "'Type in {label}... (/ for commands, ↑↓ history)' (rune_tui.py:1193) — rendered by the framework when the input is empty."
},
"history": {
"what": "Per-channel, persisted: ~/.rune/input_history.json, HISTORY_MAX=200 per channel (rune_tui.py:397-398, _load_history/_save_history 401-419). add_to_history dedupes (a re-send moves it to most-recent) and trims (437-449). Up/down browse with a saved draft: up saves the in-progress value and walks older; down walks newer and past the newest restores the draft; cursor parked at end (451-479). Left/right/home/end do NOT exit browsing; any other key does (481-493). Recorded on submit in on_input_submitted (2217-2232)."
},
"autocomplete": {
"what": "textual_autocomplete AutoComplete(inp, candidates=get_autocomplete_candidates) dropdown (rune_tui.py:1197): filters the ALL_COMMANDS /command list by typed prefix; '/' or empty shows all (554-577). PLUS the command palette: Ctrl+P, RuneCommands provider + CommandSelector popup with its own search Input (580-610) — the D34 'both roads' shape."
},
"submit_flow": {
"what": "on_input_submitted (rune_tui.py:2217): strip; empty = no-op; record + persist history; clear input; route channel; chat-channel task-queue detection; mode handling; _send_to_model mounts ThinkingIndicator into the log and scrolls end (3790-3803)."
},
"thinking_blocks": {
"what": "ThinkingIndicator: animated diamond spinner at 0.12s interval with rich-pixels frames and unicode fallback (rune_tui.py:325-392). ThinkingBubble: collapsed-by-default collapsible widget — 80-char summary line, full content hidden, click or ctrl+x toggles, 200-char tooltip (src/rune/chat_bubble.py:196-276). Thinking bubbles mount BEFORE the reply text (rune_tui.py:3847-3853)."
},
"transcript": {
"what": "role-styled bubbles (user right, assistant-family left), newest at bottom, last-8 conversation window restored at boot from the canonical ledger (rune_tui.py:1199-1227)."
}
},
"current_go_face": {
"composer": {
"what": "tui/nav/composer.go — a pure input-state machine: lines+line+col; Input(k, km) resolves the rebindable map for compose.send/compose.newline, inserts plain runes at the cursor, and (after the 2026-10-18 defect fix) deletes on k.Name==\"backspace\" with line-joining (composer.go:61-104). SetText restores text cursor-at-end (109-116). It is pure state — it renders nothing and owns no border.",
"hint": "Hint(km) names the live send/newline keys (composer.go:149-153)."
},
"input3_unwired": {
"what": "tui/nav/input3.go — Input3 embeds Composer and adds EXACTLY the old TUI contract (docs/tui-design-capture §2.2/§2.3): pinned 3-line viewport (VisibleLines, input3.go:117-134), placeholder string identical to the old TUI (45-51), per-lane in-memory history walk with draft stash and §2.3 exit rules (136-181), cursor motion keys left/right/home/end/shift+up/shift+down (81-98), backspace with line-join (99-102, 239-254), submit records history deduped (183-208).",
"status": "ONLY its own tests reference NewInput3 — the shell (tui/cmd/rune-tui/main.go) constructs nav.NewComposer, never Input3. No disk persistence anywhere in the Go TUI (grep: no input_history reader/writer)."
},
"rendering": {
"what": "tui/lanes/render.go chatContent (64-96): composer lines render as '> ' + text rows + hint row + breathing row, INSIDE the CHAT pane's bordered content; drawPane (139-171) wraps header+transcript+composer in ONE border — the input is NOT a separate box/layer; region.chat covers both. The model-picker menu renders as view-only lines ABOVE the composer inside the same pane (main.go menuLines, owner ruling 2026-10-18).",
"transcript": "tui/chatpane/chatpane.go — eight roles (user/assistant/system/confirm/research/error/tool/thinking), bounded 400, newest at bottom, thinking collapsed-by-default with ctrl+t toggle, spinner knob fields (spinActive/spinFrame) for D34 slice 3; Append dedupes by msg_id with echo-claim."
},
"key_routing": {
"what": "tui/cmd/rune-tui/main.go Update (553-631): ctrl+c quit → ctrl+t thinking toggle → ctrl+m model menu → (composer active:) menu keys → km.Resolve → if unresolved: chainOwnsKey probe on a CLONE, else composerOwnsRune (plain runes type, 2026-10-18 ruling) → feed composer → else resolved compose.send/newline feed composer → else fall through to face.Apply. nav.Precedence names 'composer' above 'lanes' (tui/nav/nav.go:44) as DATA, but the shell's probe chain is what actually routes."
},
"backspace_root_cause": {
"chain": [
"ultraviolet (the terminal input layer) names the real backspace key 'backspace': keyTypeString[KeyBackspace]=\"backspace\" (tui/vendor/github.com/charmbracelet/ultraviolet/key.go, func Key.String/Keystroke; KeyBackspace = rune(ansi.DEL) = 0x7F). So tea.KeyPressMsg.String() == \"backspace\" for a real keypress.",
"rune's conversion DROPS that name: semantic.KeyFromPress builds Key{Code: p.Code, Name: namedByCode(p.Code)} (tui/semantic/action.go:99-110) and namedByCode maps ONLY enter/esc/tab/up/down (action.go:80-94) — no backspace, no left/right/home/end/pgup/del.",
"So a real backspace reaches the shell as nav.Key{Name:\"\", Code:0x7F} whose String() is \"\\x7f\".",
"composer.go:70 matches k.Name == \"backspace\" — satisfied only by the SYNTHETIC Named(\"backspace\") construction (whose keyCodeFor(\"backspace\") even returns Code 0 — the name is the only carrier). The frozen test TestComposerBackspaceEdits (composer_test.go:38-64) types exactly that synthetic spelling and passes.",
"A REAL backspace falls to the plain-rune branch (composer.go:72: Name==\"\" && Code!=0) and INSERTS the invisible DEL rune 0x7F into the text. Visible effect: nothing deletes; invisible control characters accumulate and ride the sent message into the model transcript.",
"Input3's `case \"backspace\"` (input3.go:99) has the identical spelling dependency — it would not fire on a real key either if fed through nav.Press."
],
"verdict": "The defect is a key-NAMING layer gap, not a missing handler. Any fix — component adoption or hand-rolled — must correct the spelling layer (semantic's named-key table vs ultraviolet's), and the frozen test must drive REAL tea.KeyPressMsg{Code: tea.KeyBackspace} through the shell, not the synthetic spelling."
},
"paste": {
"what": "Bracketed paste arrives as tea.PasteMsg in Bubble Tea v2; main.go's Update switch handles KeyPressMsg/MouseClickMsg and lets PasteMsg fall to the default no-op — paste is silently DROPPED in the live face. The hand-rolled insert path also only handles single-rune keys; ultraviolet can deliver multi-rune Text per keypress (composed IME), which the composer would insert partially."
}
},
"gap_table": [
{"behavior": "backspace edits text (framework-owned)", "old_tui": "yes — Textual Input handles it", "go_face": "BROKEN on real keys (spelling fork; inserts 0x7F)", "ref": "rune_tui.py:422 / composer.go:70 + semantic/action.go:80"},
{"behavior": "input is its own bordered layer with its own focus styling", "old_tui": "yes — docked Input widget, height 3", "go_face": "no — plain rows inside the shared pane border", "ref": "rune_tui.py:1517 / lanes/render.go:74-82,139"},
{"behavior": "cursor motion (left/right/home/end)", "old_tui": "yes (framework)", "go_face": "no (Input3 has it, unwired)", "ref": "input3.go:81-98"},
{"behavior": "word ops / selection", "old_tui": "yes (framework)", "go_face": "no", "ref": "—"},
{"behavior": "paste (bracketed)", "old_tui": "yes (framework)", "go_face": "no — dropped at the shell switch", "ref": "main.go:553-639"},
{"behavior": "placeholder text", "old_tui": "'Type in {label}... (/ for commands, ↑↓ history)'", "go_face": "no (hint row only)", "ref": "rune_tui.py:1193 / input3.go:45"},
{"behavior": "up/down input-history recall with draft stash", "old_tui": "yes", "go_face": "machine exists (Input3) but unwired; default map binds up/down to nav.next/nav.prev instead", "ref": "rune_tui.py:451 / input3.go:140 / keymap.go:70-72"},
{"behavior": "history persisted across boots (per channel)", "old_tui": "yes — ~/.rune/input_history.json, max 200", "go_face": "no persistence anywhere in Go", "ref": "rune_tui.py:397"},
{"behavior": "/command autocomplete dropdown at the input", "old_tui": "yes (textual_autocomplete)", "go_face": "no (palette exists ctrl+p; D34 wants both roads)", "ref": "rune_tui.py:1197"},
{"behavior": "click focuses the input", "old_tui": "yes (channel click focuses input)", "go_face": "partial — D29 selection lights the chat pane; composer follows focus", "ref": "rune_tui.py:1229 / main.go:276-279"},
{"behavior": "enter sends / shift+enter newline, rebindable", "old_tui": "framework + bindings", "go_face": "yes (keymap compose.send/compose.newline) — KEEP", "ref": "keymap.go:74-75"},
{"behavior": "thinking collapsed-by-default, expandable", "old_tui": "yes (ThinkingBubble, click/ctrl+x)", "go_face": "yes (chatpane ctrl+t toggle; click on bubble not yet regioned)", "ref": "chat_bubble.py:196 / chatpane.go:65"},
{"behavior": "history transcript above the input, newest at bottom", "old_tui": "yes (VerticalScroll + projection window)", "go_face": "yes (chatpane bounded 400; boot restore via bridge rows)", "ref": "rune_tui.py:1190 / chatpane.go:49"}
]
}
@@ -0,0 +1,116 @@
{
"stage": "OUTWARD",
"summary": "Six live-verified external sources. The headline finding OVERTURNS a prior assumption: charm.land/bubbles/v2 v2.2.1 — the exact version already pinned in tui/go.mod — DOES ship textinput, textarea, viewport, key, spinner, and more. The vendored copy looks like a stub only because `go mod vendor` prunes packages nothing imports. Every established Go TUI studied delegates EDITING to a component (bubbles textarea/textinput, gocui TextArea, tview InputField) and hand-rolls only policy on top: history walk, suggestions popup, submit routing. The single closest precedent is Charm's own crush — a production LLM chat harness on rune's EXACT pinned stack using the bubbles v2 textarea as its composer.",
"sources": [
{
"name": "Go module proxy — charm.land/bubbles/v2",
"urls": [
"https://proxy.golang.org/charm.land/bubbles/v2/@v/list",
"https://proxy.golang.org/charm.land/bubbles/v2/@v/v2.2.1.zip"
],
"verified": "2026-10-18, live download of the version list and the v2.2.1 zip; 106 files, 16 component packages",
"findings": [
"Available versions: v2.0.0-alpha.1 … v2.2.1. v2.2.1 IS the latest — the repo already pins the newest release.",
"The v2.2.1 zip ships cursor/, filepicker/, help/, key/, list/, paginator/, progress/, spinner/, stopwatch/, table/, textarea/, textinput/, timer/, tree/, viewport/ (+ internal memoization + runeutil). textinput.go and textarea.go are full components with the v2 API (Update over tea.KeyPressMsg/tea.PasteMsg, Focus/Blur, SetStyles, KeyMap of key.Binding).",
"The repo's tui/vendor/charm.land/bubbles/v2 contains only the root package because `go mod vendor` vendors ONLY imported packages — the modules.txt + vendor layout, not the module's true content. The prior visual round's 'stub pin, no viewport/spinner widgets' claim (research/factory-visual-round/03-outward.json bubbles_v2_2_1) was an observation of the pruned vendor tree, not of the module. Adding `import \"charm.land/bubbles/v2/textarea\"` requires NO go.mod change (already a direct require at v2.2.1) — only a re-run of go mod vendor to un-prune the tree.",
"UPGRADE_GUIDE_V2.md (in the zip): v1→v2 is tea.KeyMsg → tea.KeyPressMsg, style types per state, runeutil/memoization now internal; per-component migration tables exist for textinput and textarea. Both handle bracketed paste natively (textarea.go case tea.PasteMsg: insertRunesFromUserInput) and sanitize input runes (internal/runeutil strips control characters — a raw 0x7F cannot be inserted as text).",
"textinput (single-line) additionally ships inline suggestions (SetSuggestions + completionView + AcceptSuggestion/NextSuggestion/PrevSuggestion keys) — inline completion, not a dropdown."
],
"lesson": "rune does not need to hand-roll editing at all: the component library it already pins ships the same class of input widget Textual gave the old TUI."
},
{
"name": "Bubble Tea v2 official chat example (input + viewport composition)",
"urls": ["https://github.com/charmbracelet/bubbletea/blob/main/examples/chat/main.go"],
"verified": "2026-10-18, raw source fetched; imports charm.land/bubbles/v2/{textarea,viewport,cursor}",
"findings": [
"Canonical composition: viewport (history) ABOVE, textarea (composer) BELOW, in one model. ta.Focus() at construction; textarea.Blink as Init.",
"KEY ROUTING LAW: the shell intercepts ctrl+c/esc and 'enter' (send) BEFORE the textarea sees them; 'default: Send all other keypresses to the textarea' — the textarea eats everything else, including backspace.",
"Enter-as-send is enforced by DISABLING the textarea's own newline binding: ta.KeyMap.InsertNewline.SetEnabled(false).",
"RESIZE LAW: on WindowSizeMsg, textarea.SetWidth(w) and viewport.SetHeight(h - m.textarea.Height()) — the input takes what it needs, history gets the rest.",
"Sticky scroll: viewport.GotoBottom() after every append; content wrapped to viewport width before SetContent.",
"Styling: Prompt '┃ ', ShowLineNumbers=false, CharLimit, cursor-line style cleared."
],
"lesson": "This is the exact composition shape D34 rules (stable box at bottom, history above) — with a one-screen official example showing the routing and resize laws."
},
{
"name": "crush (charmbracelet/crush) — production Go LLM chat harness on rune's exact stack",
"urls": [
"https://github.com/charmbracelet/crush",
"https://github.com/charmbracelet/crush/blob/main/internal/ui/model/ui.go",
"https://github.com/charmbracelet/crush/blob/main/internal/ui/model/keys.go",
"https://github.com/charmbracelet/crush/blob/main/internal/ui/model/history.go",
"https://github.com/charmbracelet/crush/blob/main/internal/ui/model/textarea_selection_test.go"
],
"verified": "2026-10-18, go.mod + four source files fetched raw",
"findings": [
"go.mod: charm.land/bubbles/v2 v2.2.1, bubbletea/v2 v2.1.0, lipgloss/v2 v2.0.6 — BYTE-IDENTICAL pins to rune's tui/go.mod. Crush is the existence proof that this stack runs a production chat composer.",
"Composer = bubbles v2 textarea configured: ShowLineNumbers=false, CharLimit=-1, SetVirtualCursor(false) (real hardware cursor), DynamicHeight=true with MinHeight/MaxHeight (grows while typing, capped); per-component KeyMap rebinding (SelectAll moved to ctrl+shift+a; CopySelection disabled in favor of crush's clipboard backend) — the component's keymap is policy-rebindable, same philosophy as rune's nav.KeyMap.",
"Shell-level keymap has an Editor section (SendMessage, Newline, HistoryPrev, HistoryNext, PasteText, CopySelection, CutSelection, SelectAll…) layered ABOVE the textarea: global keys (ctrl+p commands, ctrl+l models, ctrl+z suspend, ctrl+s sessions) are intercepted by the shell before the textarea sees them.",
"HISTORY: loadPromptHistory loads the 200 most recent user messages off-thread as a tea.Cmd; up-arrow walks history ONLY when the textarea is empty or the cursor is at the editor start (handleHistoryUp) — otherwise up/down move the cursor within the multi-line draft. This is the established resolution of the history-vs-cursor conflict in a multi-line composer.",
"SUGGESTIONS: a hand-rolled completions component (internal/ui/completions) for slash commands — NOT bubbles textinput's inline suggestions. Attachments render in a row ABOVE the textarea. Spinner (bubbles spinner.MiniDot) for busy state.",
"Mouse: clicks inside the textarea region are forwarded to the textarea (selection gestures); mouse selection tracked with a dedicated flag."
],
"lesson": "The full chat-box pattern on our exact stack: bubbles textarea engine + shell-owned global keys + history-at-edges + hand-rolled completion popup + spinner. Crush hand-rolls policy, not editing."
},
{
"name": "lazygit (jesseduffield/lazygit) — prompt/confirmation input pattern",
"urls": [
"https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/editors.go",
"https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/prompt_controller.go",
"https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/helpers/confirmation_helper.go"
],
"verified": "2026-10-18, three source files fetched raw (stack: forked gocui, not bubbletea)",
"findings": [
"lazygit does NOT hand-roll editing: prompt/commit inputs wrap gocui's DefaultEditor.Edit + the view's built-in TextArea (editors.go handleEditorKeypress). Enter is intercepted by the wrapper for special meaning (multiline commit description types \\n; single-line prompt confirms).",
"FALL-THROUGH LAW: when the editor does not match a key, it returns false so GLOBAL keybindings still get it — the menuFilterEditor comment is explicit: 'Give the global keybindings a chance at the key, e.g. so that ctrl-c still quits while a menu is open.'",
"The prompt is a MODAL POPUP context (confirmation/prompt/suggestions are contexts with their own keybindings: enter=confirm, esc=cancel, toggle-panel switches to the suggestions list); CreatePopupPanel wires FindSuggestionsFunc.",
"Suggestions run ASYNC off the input keypress: the input text is captured on the UI thread, the find function runs on a worker, SetSuggestions applies on the UI thread — the same single-writer discipline as rune's bridge law.",
"Masked input (characterForMask) for password-type prompts."
],
"lesson": "Framework-owned editing + modal input layer + unmatched keys fall through to globals + async suggestions. lazygit's 'input as a mode the shell enters' differs from rune's always-present box (D34 rules the box is permanent), but the fall-through law and async suggestions are directly transferable."
},
{
"name": "gh-dash (dlvhdr/gh-dash) — inputbox wrapping textarea + textinput",
"urls": [
"https://github.com/dlvhdr/gh-dash/blob/main/internal/tui/components/inputbox/inputbox.go",
"https://github.com/dlvhdr/gh-dash/blob/main/go.mod"
],
"verified": "2026-10-18, go.mod + inputbox source fetched raw",
"findings": [
"Stack: charm.land/bubbles/v2 v2.0.0 + bubbletea/v2 v2.0.2 — components shipped since v2.0.0 (corroborates the proxy evidence).",
"InputBox wraps BOTH engines behind one component: *textarea.Model for multiline, *textinput.Model for single-line; the caller picks per use (comment dialog vs rename). Prompt rendered above; help line (ctrl+d submit · ctrl+c/esc cancel) below — an own-box input layer assembled from components.",
"KEY ROUTING: a suggestion popup (hand-rolled fuzzyselect) intercepts nav/selection keys WHEN VISIBLE (Prev/Next/Select), and a ToggleSuggestions key is matched at any time; EVERYTHING ELSE flows to updateInput → textarea/textinput Update — the input component is the default owner.",
"InsertSuggestion computes (newValue, newCursorPos) and SetValue + SetCursorColumn — suggestions splice text with cursor math, they don't just fill.",
"Per-state styling through component Styles (Focused.Text/Placeholder/CursorLine…)."
],
"lesson": "A clean intermediate layer: one InputBox facade over the component engines, with suggestion-popup interception as a routing layer above the engine. This is the shape rune's composer facade should take."
},
{
"name": "k9s (derailed/k9s) — framework-widget filter",
"urls": ["https://github.com/derailed/k9s/blob/master/go.mod"],
"verified": "2026-10-18, go.mod fetched raw (forked derailed/tcell/v2 + derailed/tview)",
"findings": [
"k9s is built on a tview/tcell fork: its '/' filter input is tview's InputField — the framework owns editing, focus, and the border, exactly like the old Textual TUI's Input.",
"No hand-rolled editing code exists for the filter; the app's job is keybinding conflict avoidance (typing '/' opens the filter while other single-letter nav keys act only when the filter is closed)."
],
"lesson": "Confirms the industry shape: an interactive harness's input is a FRAMEWORK widget with its own focus; the app layers key-routing policy. For rune, the moral equivalent of 'the framework's Input widget' on the charm.land stack is the bubbles component — hand-rolling is the outlier, not the norm."
},
{
"name": "atuin — checked and set aside",
"urls": ["https://github.com/atuinsh/atuin"],
"verified": "2026-10-18 (known-stack check)",
"findings": ["atuin's interactive search UI is written in Rust on crossterm, not Go — included for honesty since the brief named it; no Go lesson extractable beyond what crush/lazygit already cover."]
}
],
"thinking_streams_prior_art": {
"what_exists": "Expandable reasoning blocks in terminal chat: crush renders reasoning-bearing models' thinking inline with the message stream (its chat components include streaming_markdown.go and collapsed tool/thinking widgets); the old Textual rune TUI's ThinkingBubble (collapsed summary + expand) is itself the pattern; oterm (ggozad/oterm, 'the terminal client for LLMs') turned out to be Python/Textual on check — same collapsed-expand pattern.",
"lesson": "The collapsed-by-default summary line + explicit expand control is the converged terminal idiom (screen-real-estate law: reasoning text is long, its content is secondary to the answer). rune's chatpane already implements exactly this shape (collapsed dim one-liner, ctrl+t toggle, spinner knob) — the gap is presentation polish (D35 runic glyph styling, per-bubble click region), not architecture. D31 (all-at-once) makes streaming-reasoning a non-goal for the TUI."
},
"cross_source_consensus": [
"EDITING BELONGS TO A COMPONENT: every surveyed Go app delegates backspace/cursor/paste/unicode to a library widget (bubbles textarea/textinput, gocui TextArea, tview InputField). None hand-roll.",
"POLICY BELONGS TO THE APP: history walk, suggestions, submit, focus — all hand-rolled above the component (crush completions + history-at-edges; lazygit async suggestions; gh-dash suggestion popup).",
"ROUTING IS A FIXED LADDER: shell globals (quit/palette) → suggestion popup (when open) → the input component (default owner of everything else) — with unmatched keys sometimes falling through (lazygit) or the component simply not acting on keys outside its keymap (bubbles: unmatched keys return unchanged state, so the shell CAN inspect nothing happened).",
"RESIZE LAW: input height is reserved first (fixed or dynamic-with-cap); the transcript viewport takes the remainder; new appends stick to bottom while the user is at bottom.",
"PASTE: handled by the component via tea.PasteMsg, never by the app's key switch."
]
}
@@ -0,0 +1,38 @@
{
"stage": "APPLICABILITY",
"summary": "The constraints admit the industry pattern directly. The already-pinned charm.land/bubbles/v2 v2.2.1 ships the input components (verified from the module zip), so adopting the bubbles v2 textarea as the composer engine requires ZERO go.mod change — only a re-vendor. The key-spelling fork that causes the backspace defect must be fixed regardless of engine choice. Two viable architectures: ADOPT the bubbles v2 textarea as the editing engine behind a rune facade (crush pattern), or KEEP the hand-rolled Input3 and fix its spelling layer. A is recommended: it buys paste, IME, cursor motion, word ops, selection, and sanitization — the exact 'framework Input felt right' property of the old TUI — for the cost of a facade.",
"constraint_check": {
"no_new_cgo_deps": "PASS — bubbles v2 is pure Go, already a direct dependency at v2.2.1; importing subpackages adds no module requirement.",
"stdlib_vendored_only": "PASS — `go mod tidy && go mod vendor` re-run populates tui/vendor/charm.land/bubbles/v2/{textarea,textinput,viewport,key,...} from the SAME pinned version; go.mod stays byte-identical (it already requires charm.land/bubbles/v2 v2.2.1), so the codegen gate's go.mod byte-diff is untouched. Vendor tree grows ~9 component dirs — self-contained offline builds keep working.",
"single_writer_ui": "PASS — bubbles components are Elm models: mutation happens only in Update() on the Bubble Tea Update thread; View() is a pure render. crush runs this at production scale on the same stack. History loading runs off-thread as a tea.Cmd (crush loadPromptHistory pattern) — the same shape as rune's existing chatDispatch.",
"d31_all_at_once": "PASS — the textarea is a line-oriented editor with no streaming assumptions; D31 concerns response rendering, not input. The bubbles cursor blink is the only timed redraw in the composer (Blink cmd at ~530ms) — bounded, single-region, and exactly what the old TUI's Input had.",
"d34_rulings": "SLICE 1 (stable box at bottom, history above): textarea Min/MaxHeight + the bt-chat resize law deliver the stable box; chatpane stays the history above. SLICE 2 (history at boot): already live via bridge rows. SLICE 3 (waiting feedback + expandable thinking): chatpane already has the shapes; polish only. SLICE 4 (/commands + palette): palette live (ctrl+p); the input-line road needs the suggestion popup (below).",
"d35_rulings": "slice 4 (input history up/down + persistence — old-TUI HistoryInput is the DEFAULT ORACLE) and slice 7 (autocomplete popup) are the two this round directly serves; the facade implements the oracle semantics; the old ~/.rune/input_history.json file is READ for continuity (same path, same shape).",
"rebindable_keymap_law": "PASS with a seam — bubbles components carry their own key.Binding maps (rebindable per component, as crush demonstrates). rune's nav.KeyMap keeps owning SEND/NEWLINE (the shell intercepts them before the component, bt-chat pattern: disable InsertNewline, match enter at the shell); editing keys live in the component's map, which the facade can seed FROM nav spellings at construction. One vocabulary remains true for every ACTION rune already freezes (compose.send/compose.newline); editing keys gain first-class names in nav spelling for the help/palette surfaces.",
"chain_owned_keys_law": "RESOLVED BY ROUTING LADDER, not by the probe — while the chat box is the lit target (composer active), the ladder is: terminal-kill (ctrl+c) → shell chrome keys (esc-close, ctrl+p palette, ctrl+t thinking, ctrl+m models) → suggestion popup (when open) → the input engine (owns everything else INCLUDING the nav letters as text, per the 2026-10-18 plain-runes ruling). The chainOwnsKey probe on a clone disappears from the composing path entirely — precedence data (nav.Precedence) and the routing ladder agree: composer above lanes when focused. r/t/p/y stay live whenever the chat box is NOT the lit target (tasks lane, rail, chrome) — nothing changes there."
},
"options": [
{
"name": "A — ADOPT bubbles v2 textarea as the composer engine (RECOMMENDED)",
"shape": "A new tui/compose facade owning one textarea.Model per lane. The facade: exposes rune's Effect/Hint API (feedComposer, laneconc dispatch, and the identity hint stay untouched); seeds the textarea's KeyMap from the nav map; implements history (per-channel, persisted, draft-stash) AROUND the engine with the crush at-the-edges rule or the old-TUI always-walk rule (owner taste question TQ-2); renders through lanes as its own bordered box pinned to the chat pane bottom.",
"buys": "backspace/cursor-motion/word-ops/selection for free and CORRECT on real keys; bracketed paste (tea.PasteMsg) for free; rune-sanitization (no 0x7F insertion class of bug possible); IME/multi-rune Text handled; hardware cursor + blink; battle-tested resize/scroll math (DynamicHeight).",
"costs": "vendor tree grows; the facade must translate nav.Key ↔ tea.KeyPressMsg (the spelling fix makes this trivial); the composer render becomes the component's View() styled to the theme (visual-round tokens apply).",
"risk": "low — crush runs this exact configuration in production on identical pins."
},
{
"name": "B — KEEP hand-rolled Input3, fix the spelling layer",
"shape": "Wire Input3 (already built to the old contract §2.2/§2.3: 3-line viewport, placeholder, history walk, motion keys, backspace) into the shell after fixing semantic's named-key table; add paste handling; add persistence; add the suggestion popup.",
"buys": "no new vendored code; the machine is already tested against the old-TUI semantics.",
"costs": "rune re-implements what the pinned module already ships: paste, sanitizer, word ops, selection, cursor rendering, IME multi-rune, resize math — each a fresh defect surface of exactly the backspace class this round diagnosed (the spelling fork already bit Input3's own dead `case \"backspace\"`).",
"risk": "medium — the defect that prompted this research is evidence of the class of bug hand-rolling invites."
},
{
"name": "shared prerequisite — THE SPELLING FIX (either option)",
"shape": "semantic's namedByCode (tui/semantic/action.go:80-94) learns ultraviolet's key-name table (or nav.Press preserves uv's Keystroke spelling in Name). Frozen tests drive REAL tea.KeyPressMsg{Code: tea.KeyBackspace / KeyLeft / KeyRight / KeyHome / KeyEnd} through the SHELL, not synthetic Named(...) spellings — the current TestComposerBackspaceEdits passes only because it types the synthetic spelling.",
"why_required": "It is the root cause of the live backspace defect; component adoption does NOT fix it by itself (the shell must still spell keys correctly when seeding/rebinding component maps and when the facade translates)."
}
],
"suggestion_popup": "Hand-rolled either way (crush's completions component, gh-dash's fuzzyselect both are): a popup list above the composer that appears when the text starts with '/', filters the ONE command vocabulary, intercepts up/down/enter/tab while open, and splices the completion. bubbles textinput's inline suggestions are a different idiom (ghost-completion, not a dropdown) — the old-TUI dropdown is the oracle.",
"history_persistence": "Reuse ~/.rune/input_history.json verbatim (same shape {channel: [str]}, cap 200, dedupe-most-recent) so existing owner history carries into the Go face — a read-compat win the old TUI already wrote.",
"viewport_question": "The transcript today (chatpane, bounded 400, newest-at-bottom window) already satisfies D34 slice 1/2 without a bubbles viewport; adopting viewport would buy scroll-position state (AtBottom/GotoBottom/SetContentLines) — optional, defer unless the build wave wants mouse-wheel scrollback in the chat pane."
}
@@ -0,0 +1,58 @@
{
"stage": "ADVERSARIAL",
"summary": "Seven real breaks; the first two are the live defects' exact mechanisms, the rest are the integration edges the build wave must freeze tests against.",
"breaks": [
{
"name": "KEY-SPELLING FORK (the live backspace defect)",
"evidence": "ultraviolet names the real backspace 'backspace' (uv key.go keyTypeString; KeyBackspace=0x7F); rune's semantic.KeyFromPress maps only enter/esc/tab/up/down (tui/semantic/action.go:80-94), so a real backspace is Key{Name:\"\", Code:0x7F}, String() \"\\x7f\"; composer.go:70 matches Name==\"backspace\" — never true for a real key; the plain-rune branch then INSERTS 0x7F into the text, which rides sent messages to the model. The frozen test passes only because it types the synthetic Named(\"backspace\") (whose Code is even 0 — keyCodeFor lacks a backspace case).",
"mitigation": "The shared prerequisite fix (semantic learns uv's names / Press preserves Keystroke) + a shell-level frozen test driving REAL tea.KeyPressMsg{Code: tea.KeyBackspace} end to end: text deletes, no 0x7F in composer.Text(), no face-chain action fires. Also add left/right/home/end/delete/pgup/pgdn to the named table while there — the same fork will bite every one of them."
},
{
"name": "INPUT BOX NOT A SEPARATE LAYER (the second live defect)",
"evidence": "lanes/render.go chatContent renders composer rows INSIDE the CHAT pane's one border (drawPane wraps header+transcript+composer); region.chat covers both — the input has no own box, no own region, no focus styling distinct from the pane.",
"mitigation": "Split the chat lane into two stacked regions: the transcript pane (scrollable, selectable) and the composer BOX (own border, own region id e.g. region.composer, focus-styled when the chat lane is lit). With option A the box renders the textarea's View() (Prompt/Placeholder/styles from the visual tokens); click-in-region focuses the composer exactly as D29's lit-box law already routes."
},
{
"name": "PASTE (bracketed) SILENTLY DROPPED",
"evidence": "main.go's Update handles KeyPressMsg/MouseClickMsg; tea.PasteMsg falls to the default no-op. Terminal paste of multi-line text today: nothing.",
"mitigation": "Forward tea.PasteMsg to the composer (with option A the textarea inserts it natively, multi-line, with sanitization; with B, Input3 needs an InsertString path). Freeze: paste of \"a\\nb\" yields two lines; paste into a non-empty composer inserts at cursor."
},
{
"name": "IME / MULTI-RUNE KEY DELIVERY",
"evidence": "ultraviolet Key.Text can carry MULTIPLE runes per keypress (composed input); the hand-rolled insert path inserts one rune (k.Code) per key and ignores Text — composed/ligature input can arrive partially or as control-adjacent codes.",
"mitigation": "Option A handles it in the component (insertRunesFromUserInput over []rune(Text) + sanitizer). If B: insert the full Text runes, not k.Code. Freeze: a synthetic KeyPressMsg with Text=\"せ\" inserts one char; Text=\"ab\" inserts two."
},
{
"name": "RESIZE / DYNAMIC HEIGHT MATH",
"evidence": "The composer's height changes with content (3-line old contract; crush DynamicHeight 3..8). Every height change shifts the transcript's available rows and every pane region's Y coordinates — the visual round already proved hand-written click-region Y values break when render lines shift (research/factory-visual-round/05-adversarial.json).",
"mitigation": "One layout owner: on WindowSizeMsg AND on composer height change, recompute transcriptH = paneH - composerH and re-emit regions from the single Render pass (the bt-chat law: viewport.SetHeight(h - textarea.Height())). Regions derive from the same layout numbers the renderer used — never hand-written. Freeze: type past 3 lines → box grows to cap, transcript shrinks, click regions track."
},
{
"name": "SCROLL SYNC WHILE COMPOSING",
"evidence": "New transcript appends (bridge rows, completions) while the user types: a naive re-render yanks the view or, worse, the bounded chatpane window slides over the composer's stable position.",
"mitigation": "Composer box is fixed furniture (D34 slice 1) — its rows NEVER move due to transcript appends; only the transcript window scrolls. Stick-to-bottom only when already at bottom (sticky-scroll law from factory-chat-live 03-outward); chatpane's newest-at-bottom window already behaves this way. Freeze: append while composing → composer rows byte-identical before/after."
},
{
"name": "CHAIN-OWNED KEYS vs A COMPONENT THAT WANTS ALL KEYS",
"evidence": "bubbles textarea acts on keys in ITS keymap and passes everything else through unchanged (no swallow flag) — and rune's keymap binds up/down to nav.next/nav.prev, enter to compose.send, and the face chain owns r/t/p/y/j/k/m/h letters. Naively forwarding every key to the textarea first would let enter insert newlines and up/down move the cursor; naively resolving nav first would steal letters mid-sentence (the exact bug class the 2026-10-18 plain-runes ruling killed).",
"mitigation": "The fixed ladder (applicability): ctrl+c → chrome keys (esc/palette/ctrl+t/ctrl+m) → suggestion popup (when open) → SEND/NEWLINE matched against the NAV map at the shell (bt-chat: disable the textarea's InsertNewline so enter is unambiguous) → everything else to the engine as text/commands. The chainOwnsKey probe is deleted from the composing path (a clone-probe per keystroke was also a perf smell). Freeze: 'r' types while chat-lit; 'r' opens the rail when tasks-lit; enter sends; shift+enter newlines; esc closes chrome only."
},
{
"name": "SECONDARY (real but bounded)",
"items": [
"CURSOR RENDERING: the hardware cursor must sit in the composer only when chat-lit (crush SetVirtualCursor(false) = real cursor; bubbletea v2 carries cursor visibility/position in the View). Blur must hide it or the terminal cursor floats over the transcript. Frozen test: cursor visible iff composer focused.",
"HISTORY RECALL vs CURSOR MOTION: up/down cannot mean both 'walk history' and 'move cursor lines'. crush: history only when empty/at-start; old TUI: always history (single-line input made this free); Input3: up/down always history, shift+up/down cursor. Owner taste question TQ-2 — no silently-borrowed answer.",
"DEFAULT MAP CONFLICT: keymap.go binds Named(\"up\")/Named(\"down\") to nav.next/nav.prev — with a focused composer these must NOT resolve to lane motion (the ladder handles it, but the bindings stay live for the non-composing panes; document, don't rebind).",
"OLD FILE COMPAT: reading ~/.rune/input_history.json written by the Python TUI must not crash on absent/corrupt JSON (old TUI already tolerates it — degrade-not-crash).",
"BLINK TIMING: the Blink cmd adds ~2 renders/sec while focused — bounded, but the identity line/frozen-frame tests must not depend on frame-count equality while the composer is focused (they must compare stripped frames or unfocused)."
]
}
],
"verified_non_breaks": [
"Rebindability law survives: send/newline stay nav-map actions matched at the shell (bt-chat pattern); the component's own bindings are seeded to non-conflicting keys.",
"feedComposer / laneconc / chatBusy dispatch chain is untouched — the facade emits the same Effect{compose.send}.",
"chatpane claim law (msg_id dedupe, echo-claim) is independent of the composer engine.",
"D29 selection law unchanged: the lit box still routes typed input; the composer box becomes a region of the chat pane, and click resolution (most-specific-first) picks it.",
"Offline/hermetic tests: the facade is constructible with no bridge and no network, exactly as the composer is today."
]
}
@@ -0,0 +1,75 @@
{
"stage": "SYNTHESIS",
"recommended_architecture": "THE CHAT BOX: a three-layer input surface inside the chat lane — (1) the ENGINE is the already-pinned bubbles v2 textarea (option A), adopted because every surveyed Go harness delegates editing to exactly this class of component and the repo's own defect came from hand-rolling it; (2) the FACADE is a rune-owned wrapper (tui/compose) that keeps the Effect/Hint/lane API the shell already consumes, adds per-channel persisted history with the old-TUI oracle semantics, and renders as its OWN bordered box pinned to the bottom of the chat lane (region.composer) distinct from the selectable transcript pane; (3) the ROUTING LADDER in the shell is fixed and explicit: ctrl+c → chrome keys → suggestion popup → nav send/newline → engine-everything-else. The key-spelling fix lands FIRST and either way.",
"phases": [
{
"name": "PHASE 0 — THE SPELLING FIX (prerequisite, lands before any engine choice)",
"touches": [
"tui/semantic/action.go: namedByCode learns ultraviolet's full named-key table (backspace, delete, left, right, home, end, pgup, pgdn, insert, f-keys as needed) — or nav.Press preserves uv's Keystroke in Name",
"tui/cmd/rune-tui/main_test.go: shell-level frozen test driving REAL tea.KeyPressMsg{Code: tea.KeyBackspace} through Update — text deletes, composer.Text() never contains 0x7F, no face action fires; same for left/right/home/end",
"tui/nav: keyCodeFor gains the matched codes so Named(\"backspace\") round-trips a real Code (today it is 0)"
],
"frozen_tests": [
"real-backspace-through-shell deletes and inserts nothing",
"old synthetic-spelling tests still pass (both spellings legal)"
],
"verifies": "the live backspace defect dies at its root, independent of every later phase"
},
{
"name": "PHASE 1 — THE BOX (engine adoption + separate layer)",
"touches": [
"tui/go.mod: UNCHANGED (bubbles v2 already v2.2.1); run go mod tidy && go mod vendor to un-prune textarea/textinput/key into tui/vendor",
"new tui/compose: facade over textarea.Model per lane — ShowLineNumbers=false, CharLimit=-1, SetVirtualCursor(false), DynamicHeight with Min=3 (old contract) and Max per TQ-3, Prompt/Placeholder/Styles from the visual tokens; seeds component KeyMap from nav spellings; disables InsertNewline (shell owns enter)",
"tui/cmd/rune-tui/main.go: composer construction swaps to the facade; routing ladder replaces the chainOwnsKey probe while composing; tea.PasteMsg forwarded; Focus/Blur tracks the D29 lit box",
"tui/lanes/render.go: chat lane splits — transcript pane (region.chat) above, composer BOX (region.composer, own border, focus-styled) below; one layout owner recomputes heights on WindowSizeMsg AND composer-height change",
"tui/nav/composer.go+input3.go: the Effect/Hint surface survives as the facade's contract; the hand-rolled editing machines retire or reduce to the history store (TQ-5 decides disposition)"
],
"frozen_tests": [
"backspace/cursor-motion/paste on REAL key spellings through the shell",
"enter sends (nav binding), shift+enter newlines, ctrl+p palette still opens while composing, esc closes chrome only",
"plain letters type while chat-lit; r/t/p/y still act while tasks-lit",
"resize: composer grows to cap, transcript shrinks, regions track; composer rows byte-stable across transcript appends",
"empty composer + enter = no-op; hint names the live keys across rebinds"
],
"verifies": "the input is a real box that edits like the old TUI felt"
},
{
"name": "PHASE 2 — HISTORY + AUTOCOMPLETE (the D35 oracle)",
"touches": [
"tui/compose history: per-channel, persisted to ~/.rune/input_history.json (READ the Python-era file — same shape, cap 200, dedupe-most-recent); walk semantics per TQ-2; draft stash exact",
"suggestion popup: hand-rolled list above the composer (crush completions / gh-dash fuzzyselect pattern): '/' prefix triggers, filters the ONE command vocabulary, intercepts up/down/enter/tab while open, splices with cursor math",
"identity/palette surfaces gain the new named keys where relevant (help text)"
],
"frozen_tests": [
"up/down walk per the chosen TQ-2 rule with stash/restore exact (old-TUI oracle tests)",
"history persists across a restart of the model (file round-trip, corrupt-file degrade-not-crash)",
"'/' opens suggestions, filter narrows, enter accepts, esc dismisses, letters keep typing while the popup is closed"
],
"verifies": "D35 slices 4 and 7; the old-TUI input muscle memory carries over"
},
{
"name": "PHASE 3 — POLISH / DEFERRED",
"items": [
"thinking bubble presentation: runic glyph marker styling (D35 slice 6), per-bubble click region for expand (today ctrl+t global toggle)",
"bubbles viewport adoption for the transcript: only if mouse-wheel scrollback in the chat pane is wanted (chatpane suffices otherwise)",
"selection + clipboard in the composer (bubbles supports it; crush routes through its own clipboard backend)",
"masked-input variant for future password-type prompts (lazygit characterForMask pattern)"
]
}
],
"owner_taste_questions": [
"TQ-1 ENGINE: adopt bubbles v2 textarea (A, recommended — crush precedent on identical pins, buys paste/IME/selection/word-ops) or keep hand-rolled Input3 (B, zero new vendored code, but the repo re-owns the whole editing defect surface the backspace bug exemplifies)?",
"TQ-2 UP/DOWN SEMANTICS: old-TUI oracle (up/down ALWAYS walk history; cursor moves some other way — Input3 uses shift+up/shift+down) vs crush rule (up/down move the cursor in the draft; history only when the box is empty or the cursor is at the start)? The old TUI could always-walk because its input was single-line; a multi-line box makes always-walk a real choice with a cost.",
"TQ-3 BOX HEIGHT: fixed 3 lines (old contract) vs dynamic growth 3..8 with cap (crush; grows while typing a long message, then scrolls inside)",
"TQ-4 BOX PLACEMENT: composer as its own bordered box directly beneath the transcript pane inside the chat lane (recommended — old TUI's docked-Input shape) vs full-width bar beneath BOTH lanes (some chat harnesses pin the input under the whole layout)?",
"TQ-5 DISPOSITION of composer.go/input3.go once the facade lands: retire outright, or keep Input3's history store as the facade's memory (its walk semantics are already tested against the oracle)?"
],
"research_gate": "this chain at PROCEED satisfies the research-first law for the chat-box build wave",
"risk_ranked": [
"spelling fork unfixed (every later phase inherits the 0x7F-insertion class) — phase 0 kills it",
"routing ladder regression (nav letters stolen mid-sentence, or enter inserting newlines) — frozen ladder tests pin both directions",
"region math drift on composer resize (visual round's proven break class) — single layout owner + derived regions",
"paste dropped (today's live behavior) — tea.PasteMsg forwarding with a frozen test",
"history file compat (Python-era JSON) — degrade-not-crash reader"
]
}
@@ -0,0 +1,23 @@
{
"stage": "QUALIFY",
"verdict": "PROCEED",
"confidence": "high",
"rationale": "Every load-bearing fact was verified live this round: the pinned bubbles v2.2.1 ships the input components (module zip from the Go proxy — the 'stub' was vendor pruning, not module content); crush runs the exact composer pattern in production on byte-identical dependency pins; the official Bubble Tea v2 chat example fixes the composition and routing laws; the two live defects have precise, file:line root causes (the key-spelling fork and the shared-border rendering). The recommended architecture is the surveyed consensus shape, adapted to rune's laws without a single new dependency. Five taste questions are named for the owner; none blocks phase 0/1 (TQ-1 has a strong recommendation and the phases degrade gracefully if overruled).",
"conditions": [
"Phase 0 (spelling fix) must land FIRST and independently — it is the root cause of the live backspace defect and every later phase inherits it",
"Phase 1 lands the routing ladder and the box split TOGETHER with their frozen tests (partial landing leaves either the stolen-letters or shared-border defect live)",
"TQ-2 (up/down semantics) decided with the owner before phase 2 — the two rules produce different frozen tests and different muscle memory",
"go.mod must remain byte-identical to the codegen template (verified: importing already-required module packages does not touch it); re-vendoring is the only dep-tree change",
"Shell-level frozen tests must drive REAL tea.KeyPressMsg spellings — the synthetic Named(\"backspace\") spelling is exactly how the current defect hid"
],
"named_unknowns": [],
"artifacts_index": [
"01-issue.json — problem, defects, questions, constraints",
"02-inward.json — old-Textual vs Go-face side-by-side, gap table, backspace root cause chain",
"03-outward.json — six live-verified sources with URLs and lessons; thinking-stream prior art",
"04-applicability.json — constraint check, options A/B, the shared spelling prerequisite, suggestion/persistence notes",
"05-adversarial.json — seven breaks with mechanisms and mitigations, verified non-breaks",
"06-synthesis.json — the Chat Box architecture, phases 0-3, five owner taste questions, risk ranking",
"07-qualify.json — this file"
]
}
@@ -0,0 +1,58 @@
# factory-chat-presentation — how Go terminal apps build chat boxes for harnesses
Research round answering the owner's direct ask: *"do research into how to setup chat
boxes for harnesses in go"*. Triggered by two live defects: backspace does not edit the
composer, and the input box is not a separate layer from the selectable windows.
**Verdict: PROCEED** (07-qualify.json). The chain:
| Artifact | What it holds |
| --- | --- |
| 01-issue.json | the problem, the two live defects, research questions, constraints |
| 02-inward.json | old-Textual vs Go-face side-by-side; the 13-row gap table; the backspace root-cause chain |
| 03-outward.json | six live-verified external sources with URLs + per-source lessons |
| 04-applicability.json | constraint check; options A (adopt bubbles v2 textarea) vs B (keep Input3); the shared spelling prerequisite |
| 05-adversarial.json | seven breaks with mechanisms and mitigations |
| 06-synthesis.json | the Chat Box architecture, phases 0–3, five owner taste questions |
| 07-qualify.json | PROCEED, high confidence, five conditions |
## The three findings that matter most
1. **The "no bubbles components" belief was wrong.** charm.land/bubbles/v2 v2.2.1 —
the exact version pinned in `tui/go.mod` — ships `textinput`, `textarea`,
`viewport`, `key`, `spinner` and more (verified by downloading the module zip from
proxy.golang.org). The vendored copy looked like a stub because `go mod vendor`
prunes packages nothing imports. Adopting the textarea requires **zero go.mod
change** — only a re-vendor.
2. **The backspace defect is a key-spelling fork, not a missing handler.** ultraviolet
names the real key `"backspace"`; rune's semantic layer maps only five named keys
(enter/esc/tab/up/down — `tui/semantic/action.go:80`), so a real backspace arrives
as `Name:""/Code:0x7F`, misses the composer's `Name=="backspace"` case
(`tui/nav/composer.go:70`), and falls into the plain-rune branch which **inserts an
invisible DEL character** into the text. The frozen test passes only because it
types a synthetic spelling. The fix is prerequisite to everything else.
3. **Every established Go TUI delegates editing to a component and hand-rolls only
policy.** crush (Charm's own LLM harness, byte-identical dependency pins to rune)
runs the bubbles v2 textarea as its composer with shell-owned global keys,
history-at-the-edges, and a hand-rolled slash-command completion popup. lazygit
wraps gocui's editor; gh-dash wraps both bubbles engines behind one InputBox; k9s
uses tview's InputField. The old Textual TUI "felt right" for the same reason: the
framework owned the editing.
## The recommended architecture (06-synthesis)
Three layers: the **engine** (bubbles v2 textarea behind a rune facade that keeps the
Effect/Hint API), the **box** (own border, own region `region.composer`, pinned to the
bottom of the chat lane beneath the selectable transcript pane), and a **fixed routing
ladder** in the shell (ctrl+c → chrome keys → suggestion popup → nav send/newline →
engine owns everything else). Phases: 0 spelling fix → 1 the box → 2 history +
autocomplete (reads the Python-era `~/.rune/input_history.json`) → 3 polish.
Five taste questions await the owner (06-synthesis): engine choice, up/down semantics
(old-TUI always-walk vs crush at-the-edges), box height (fixed 3 vs dynamic 3–8),
placement (beneath the chat lane vs full-width), and the disposition of the existing
hand-rolled machines.
Sources are listed with URLs in 03-outward.json. No repo code was changed in this
round; the round is registered in docs/factory/research-registry.json under
`chat-presentation`.
@@ -0,0 +1,61 @@
{
"stage": "INWARD",
"question": "Where does rune's own harness sit — what does it enforce mechanically vs what it trusts the model's judgment on?",
"evidence": {
"enforced_mechanically": [
{
"mechanism": "Admission contracts (Scaffolding)",
"evidence": "scripts/admit_wave_*.py — dozens of per-wave contract scripts; every build wave must mint a construction contract before building (factory standardization law, docs/factory/order-of-things.md 'WAVE-BRIEF COMPLIANCE LAW': SCAFFOLDING ADMISSION required verbatim in every brief).",
"role": "No freehand edits outside the scaffolding authority — the model cannot decide what it is allowed to touch; the contract decides."
},
{
"mechanism": "Research gate",
"evidence": "scripts/check_research_gate.py + docs/factory/research-registry.json; owner directive 2026-10-16 makes the canonical research protocol (ISSUE/INWARD/OUTWARD/APPLICABILITY/ADVERSARIAL/SYNTHESIS/QUALIFY) mandatory before any feature wave, enforced at admission time and swept post-hoc.",
"role": "Model cannot start building a feature that hasn't passed a recorded research round — PROCEED is a mechanical precondition, not a judgment call."
},
{
"mechanism": "Red-first law (bugfix contract)",
"evidence": "docs/bugfix-contract.md + AGENTS.md 'Bug fixes (binding)': red test FIRST reproducing the cause, root-cause fix, full pipeline re-entry, regression tests 'frozen forever', discharge recorded finding→test→commit; discharge checker ships in scripts/ ('red-first enforcement is structural'). Symptom patches 'rejected at review'.",
"role": "The order of operations for fixing bugs is law, not model preference."
},
{
"mechanism": "henxels contract (repo-wide rules engine)",
"evidence": "AGENTS.md henxels block: rules enforced at commit boundary via githooks — no secrets, line budgets per language, every module mapped in umbrellas.json (DAG-checked), TUI surfaces registered, new modules need tests or canary, canaries registered in CI. Deviation must go through henxels.yaml (the 'only sanctioned escape') so it stays visible.",
"role": "Repository-wide structural invariants the model cannot violate silently — even commit-time hooks refuse to pass bad code."
},
{
"mechanism": "Landing gate / verify-gated landing",
"evidence": "docs/factory/order-of-things.md factory standardization law ('red-first tests, Scaffolding admission, verify-gated landing, continuous backfill'); docs/factory-verification-ladder.md; wave briefs require verification receipts; landing-sha evidence closes admission contracts (backfill records receipts verbatim).",
"role": "Code does not land because the model says it works — receipts at the landing commit do."
},
{
"mechanism": "Dichotomy enforcement / parity harness / retirement campaign",
"evidence": "AGENTS.md henxels rule ('every src/rune module lands in its measured side'); docs/retirement-map.md + verification/retirement-map.json; retirement requires parity harness proof PLUS owner confirmation before deletion (order-of-things.md §3).",
"role": "Even the HARNESS's own deletions are gated — trust is bounded in both directions."
},
{
"mechanism": "Live proofs (PTY proofs, vitality sweep, sharded checkup, mutant batteries)",
"evidence": "scripts/admit_wave_live_proof.py, admit_wave_parity_harness.py, fuzz admission scripts (admit_fuzz_boundary_decoder.py); verification/ artifact tree; production-failure fixture pipeline (tests/fixtures/production with manifest + generated regression tests per henxels).",
"role": "Behavior claims must be demonstrated in a real terminal/real process, not asserted; mutation-style checks probe whether tests would actually catch regressions."
},
{
"mechanism": "Issue ledger + adversarial review procedure",
"evidence": "docs/factory/order-of-things.md ISSUE-LEDGER LAW (every landing files/closes a Gitea issue in the same change); docs/adversarial-review.md ('binding' — dispatch templates MUST reference it); AGENTS.md henxels rule enforcing the adversarial review reference.",
"role": "Self-critique is structurally mandated, not optional."
}
],
"trusted_to_model_judgment": [
"Design taste and architecture within a contract — the admission contract bounds scope, but the model decides the internal design of what it builds.",
"Naming, structure, and implementation idioms inside the line-budget/registry rules.",
"Interview questions and owner-dialogue design (research/harness-survey/interviews.md lineage) — the model composes, the owner answers.",
"How to satisfy a research round — the protocol's stages are required, but the findings are the model's analysis; the gate checks presence/verdict, not intellectual quality.",
"Everything not covered by a henxel, canary, contract, or gate — the contract system is opt-in per surface; unregistered surfaces are drift (recorded, e.g. the 2026-10-19 compliance-law audit found interactive waves admitted without contracts, backfilled rather than prevented)."
],
"boundary_map": "rune enforces: WHAT may change (scope contracts), THAT tests exist and run first (red-first), THAT receipts prove behavior (landing gate, PTY proofs), THAT structure stays mapped (henxels/umbrellas), THAT research precedes features (research gate). rune trusts the model: HOW the code should look, taste, naming, design tradeoffs, and the quality of its own research analysis."
},
"comparative_notes": [
"vs majors: rune's mechanical enforcement layer is denser than any surveyed mainstream harness — [CC]/Codex enforce safety mechanically but leave quality to prompts + human review; rune's gates make quality violations mechanically un-landable.",
"vs majors: rune lacks the majors' scale of behavioral tooling — [CC]'s extracted corpus shows hundreds of steered tool descriptions, specialized review agents, and skills; rune's review depth comes mostly from the adversarial-review procedure and owner checkpoints.",
"honest gaps: (1) contract-coverage is opt-in per surface — drift happened and was backfilled, not prevented; (2) the gate checks research PRESENCE and verdict, not whether the research is good — no external model-of-the-model; (3) owner confirmation remains a human bottleneck by design (retirement campaign), which trades autonomy for control."
]
}
@@ -0,0 +1,105 @@
{
"stage": "OUTWARD",
"question": "Do coding harnesses let the model handle code quality autonomously, or do they impose procedures that the model must follow?",
"sources": [
{
"id": "anthropic-agents",
"url": "https://www.anthropic.com/engineering/building-effective-agents",
"title": "Anthropic — Building Effective Agents (Dec 2024)",
"fetched": "2026-10-19, live fetch",
"findings": [
"Vendor guidance splits 'workflows' (predefined code paths the model must follow) from 'agents' (model-directed). Recommendation: start simple, add structure only when it demonstrably improves outcomes.",
"Explicit guardrail stance: autonomous agents have 'potential for compounding errors' — recommendation is 'extensive testing in sandboxed environments, along with the appropriate guardrails'.",
"Key reveal: for their own SWE-bench coding agent they 'spent more time optimizing our tools than the overall prompt' — the harness's tool interface (ACI) is treated as a first-class quality lever, including poka-yoke (mistake-proofing) of tool arguments.",
"Anthropic's evidence of why coding is special: 'Code solutions are verifiable through automated tests; agents can iterate on solutions using test results as feedback' — i.e. the environment itself is the verifier, and human review remains crucial for alignment with broader requirements."
]
},
{
"id": "cognition-multiagents",
"url": "https://cognition.ai/blog/dont-build-multi-agents",
"title": "Cognition — Don't Build Multi-Agents (Walden Yan, 2025)",
"fetched": "2026-10-19, live fetch",
"findings": [
"Devin's maker states the core failure mode of trusting dispersed model judgment: 'Actions carry implicit decisions, and conflicting decisions carry bad results.' Trusting models in parallel without shared context produced mismatched components (Super Mario background + Flappy Bird bird).",
"Their two principles: (1) Share context and full agent traces; (2) actions carry implicit decisions. These are HARNESS procedures, not model virtues — Cognition encodes them in internal frameworks 'to enforce these ideas'.",
"Historical postmortem embedded: 2024-era 'edit apply models' (a big model describes the edit, a small model applies it) were systematically faulty — 'the small model would misinterpret the instructions of the large model and make an incorrect edit' — the industry replaced that split with single-model edits, i.e. removing a procedural handoff that caused quality failures.",
"[CC]'s subagent design is cited as deliberately restrained: subagents answer questions, they do not write code, precisely because they lack the main agent's context."
]
},
{
"id": "codex-prompt",
"url": "https://github.com/openai/codex/blob/main/codex-rs/core/gpt_5_codex_prompt.md",
"title": "OpenAI — Codex CLI system prompt (gpt_5_codex_prompt.md)",
"fetched": "2026-10-19, live fetch of raw file",
"findings": [
"Codex's quality machinery is almost entirely behavioral instruction, not mechanical enforcement: editing constraints (ASCII default, rare comments), never reverting user changes, STOP IMMEDIATELY on unexpected state, never destructive git commands, no amending commits unless asked.",
"Structure is delegated: a plan tool is recommended for most tasks but explicitly skippable for the easiest ~25% — the model judges its own need for procedure.",
"The prompt does NOT contain test requirements, lint requirements, or landing gates. Quality beyond these behavioral rails is left to the model and to whatever the user's repo already enforces. Codex sits at the light end of the spectrum: rules-of-conduct harness, no quality pipeline."
]
},
{
"id": "claude-code-prompts",
"url": "https://github.com/Piebald-AI/claude-code-system-prompts",
"title": "[CC] system prompt corpus (community-extracted mirror)",
"fetched": "2026-10-19, live directory listing",
"findings": [
"The extracted corpus shows [CC]'s quality handling is proceduralized at the HARNESS level in three bands: (a) hundreds of tool-description and system-prompt fragments steering behavior (edit restraint, no compat hacks, no unnecessary error handling, verification guidance); (b) mechanical enforcement via permission rules, deny rules, sandboxing, and hooks that can block actions at execution time; (c) dedicated code-review agent pipelines (multi-phase: base-finder angles, three-state verification, gap sweeps, fix application) — quality review is itself decomposed into a procedure.",
"Notable named fragments: 'doing-tasks-no-compatibility-hacks', 'executing-actions-with-care', 'frontend-browser-verification', 'three-state verification' review phases — the harness encodes engineering values as standing instructions and enforces dangerous-action boundaries mechanically.",
"Contrast with rune: [CC] enforces SAFETY mechanically (permissions, sandbox, hooks) but enforces QUALITY procedurally (prompts + review agents), whereas rune enforces quality mechanically (gates, contracts)."
]
},
{
"id": "swebench",
"url": "https://www.swebench.com/",
"title": "SWE-bench official leaderboards (incl. SWE-bench Verified)",
"fetched": "2026-10-19, live fetch",
"findings": [
"SWE-bench Verified (500 instances, human-filtered by OpenAI: https://openai.com/index/introducing-swe-bench-verified/) judges solutions purely by whether the repo's own tests pass — the benchmark's entire notion of 'correct code' is a mechanical verification loop. Hidden tests catch patches that merely look right.",
"The leaderboard now normalizes every model to the SAME minimal harness (mini-SWE-agent). Mini-swe-agent scores ~65% on Verified 'in 100 lines of Python' — evidence that a strong model with a bare loop goes far, but the residual 35% gap and per-harness deltas show harness design still moves the needle.",
"Methodological lesson for this question: the field MEASURES code quality with verification scaffolding (tests), which is why harnesses that actually run tests outperform ones that don't."
]
},
{
"id": "agentless",
"url": "https://arxiv.org/abs/2407.01489",
"title": "Agentless (Xia et al.) — 'Do we really have to employ complex autonomous software agents?'",
"fetched": "2026-10-19, live abstract fetch",
"findings": [
"Agentless replaces model autonomy with a FIXED three-phase procedure (localization → repair → patch validation) and beat all open-source agents on SWE-bench Lite at the time (32.00%, $0.70/instance). Procedure outperformed autonomy.",
"The Agentless/AutoCodeRover (https://arxiv.org/abs/2404.05427) lineage demonstrates the finding both ways: fixed pipelines beat free-running agents of that era, but agentic harnesses with strong tool design and verification loops (SWE-agent, https://arxiv.org/abs/2405.15793 — the 'Agent-Computer Interface' paper) later closed and exceeded the gap.",
"Consensus the lineage supports: verification and structure help; total autonomy hurts; over-proceduralization caps performance as models improve — the pipeline must evolve or become the ceiling."
]
},
{
"id": "aider",
"url": "https://aider.chat/docs/leaderboards/",
"title": "Aider LLM leaderboards / polyglot benchmark",
"fetched": "2026-10-19, live fetch",
"findings": [
"Aider's benchmark mechanically constrains the model to Aider's own edit formats (diff-style edits) and scores whether those edits apply cleanly and pass tests — code quality is measured as 'correct within the harness's edit protocol'.",
"Aider's published history of edit formats (whole-file → diff → diff-fenced → udiff per language) is a running case study: the HARNESS's data channel for code, not the model's raw ability, was repeatedly the limiting factor, and harness changes re-ranked the models."
]
},
{
"id": "cc-aci-lineage",
"url": "https://arxiv.org/abs/2405.15793",
"title": "SWE-agent: Agent-Computer Interfaces enable automated software engineering (NeurIPS 2024)",
"fetched": "2026-10-19, URL recorded (canonical ACI paper; design principles cross-checked against live SWE-agent site linked from swebench.com)",
"findings": [
"Origin of the term Agent-Computer Interface (ACI): harness-side design decisions — what tools exist, how results are fed back, how errors are surfaced — measurably change agent performance with the SAME model. Quality is partly manufactured by the harness's interface, not just the model.",
"SWE-agent's designed interface (guarded file viewer/search with linting of edit results) beat a plain bash shell by large margins on the same model — direct evidence that harness procedure (not model talent) drives quality."
]
}
],
"failure_taxonomies": [
"Parallel agents with split context produce inconsistent, conflicting components (Cognition Flappy-Bird example; reasoning recorded in cognition-multiagents).",
"Two-model edit handoff (planner model describes, applier model writes) systematically corrupted edits; removed industry-wide (same source).",
"Devin's public incident history (e.g. the 'hide-and-seek' Upwork evaluation where it silently installed an npm package that printed 'HOLD ON, I'M NOT SCAMMING YOU') — failures of trusting model self-reporting without mechanical proof; the published retrospectives pushed Cognition toward enforced context sharing and self-verifying steps.",
"Benchmark-grade evidence: patches that pass superficial inspection but fail hidden tests — the reason SWE-bench Verified human-filtered its instances to remove broken/misleading tests."
],
"spectrum_placement": {
"fully_autonomous": ["Codex CLI (behavioral rails only, no quality pipeline)", "aider (protocol-constrained but user-supervises quality)"],
"middle": ["[CC] (mechanical safety enforcement + procedural quality via prompts/review agents)", "SWE-agent/mini-SWE-agent (verification loop, minimal otherwise)", "Devin (heavy environment + enforced context-sharing procedures)"],
"heavily_procedural": ["Agentless (fixed pipeline)", "AutoCodeRover (guided search phases)", "rune (admission contracts, gates, red-first — the most procedural of the surveyed set)"]
}
}
@@ -0,0 +1,67 @@
# Harness vs Model: who's really responsible for code quality?
Research round for the owner · 2026-10-19 · chain: outward.json → inward.json → this synthesis
## First, the words
- A **harness** is everything wrapped around an AI model when it writes code: the tools it's given (file editors, a terminal, search), the written rules it must follow, and the automatic checks that run on its work. The model is the brain; the harness is the workplace, the rulebook, and the foreman.
- **Verification loop** means: the code is checked by actually running things (tests, compilers) rather than trusting that the model says "done".
- **SWE-bench Verified** is the industry's standard exam: 500 real GitHub bugs, hand-checked by OpenAI staff, where a fix only counts if the project's own hidden tests pass.
## The direct answer to your question
The honest answer is: **every serious harness does a mix, but the industry has moved decisively toward "the harness supplies the procedure, the model supplies the judgment"** — and the specific split is consistent almost everywhere:
1. The model gets full autonomy over *how* to solve the problem — reading code, planning, designing the change.
2. The harness controls *whether the work is allowed to count* — tests must pass, dangerous actions are blocked, results must be proven rather than claimed.
The two clearest pieces of evidence:
- **Anthropic, on its own coding agent**: "we actually spent more time optimizing our tools than the overall prompt" — they mistake-proofed the tools themselves (poka-yoke) so the model *can't* make certain errors. And their recommendation for autonomous agents is explicit: extensive testing in sandboxed environments "along with the appropriate guardrails," because errors **compound**. (https://www.anthropic.com/engineering/building-effective-agents)
- **Cognition, Devin's maker, on what goes wrong when you trust the model too much**: their published postmortem "Don't Build Multi-Agents" shows that letting models act independently without shared context produced mismatched, conflicting work ("actions carry implicit decisions, and conflicting decisions carry bad results"). They now *enforce* those principles in their internal frameworks — "as a way to enforce these ideas." They also describe an older failure: when one model described edits and a second model applied them, the applier routinely "misinterpreted the instructions... and made an incorrect edit." The whole industry dropped that arrangement. (https://cognition.ai/blog/dont-build-multi-agents)
## Where each major harness sits (the spectrum)
From "model trusted, harness minimal" to "harness verifies everything":
- **Codex CLI ([OI])** — lightest. Its published system prompt is a list of *conduct rules* (never revert user changes, never run destructive git commands, stop immediately if something unexpected appears). It has **no** quality pipeline at all: no required tests, no gates. Quality is the model's judgment plus whatever the user's repo enforces. https://github.com/openai/codex/blob/main/codex-rs/core/gpt_5_codex_prompt.md
- **Aider** — constrains the *channel*, not the judgment. Models must express edits in Aider's strict diff format; Aider's own benchmark history shows the harness's edit format repeatedly mattered as much as model skill. https://aider.chat/docs/leaderboards/
- **[CC] (Claude Code)** — a three-band system: mechanical *safety* enforcement (permission rules, sandboxing, hooks that block actions), hundreds of written *steering* rules, and dedicated multi-phase code-review agents. Safety is enforced by the machine; quality is handled procedurally (rules + reviewer agents), not mechanically. https://github.com/Piebald-AI/claude-code-system-prompts
- **SWE-agent / mini-SWE-agent** — the academic line that coined "Agent-Computer Interface": with the *same* model, changing only the harness's tools (a guarded file viewer that lints edits vs. a raw shell) moved scores by large margins. The harness *manufactures* part of the quality. https://arxiv.org/abs/2405.15793
- **Devin** — heavy environment, and (per its own postmortems) deliberately restrictive: subagents answer questions but don't write code, precisely because they lack the main agent's context. Trust is rationed.
- **Agentless** — the extreme procedural end: replace autonomy entirely with a fixed 3-phase pipeline (find the bug → fix it → run the tests). It beat every open-source agent of its day, at $0.70 per bug — proof that *procedure can beat autonomy* when models are weak; and proof of the ceiling when later agent harnesses overtook it. https://arxiv.org/abs/2407.01489
The benchmark itself settles the measurement question: SWE-bench Verified counts code as "good" **only if hidden tests pass** — the field's definition of quality is mechanical verification, not model confidence. https://www.swebench.com/
## The emerging consensus
Nobody publishes a manifesto saying "here's the right division," but four convergent lines form a de facto consensus:
1. **Never trust self-report.** Verify by running (Anthropic, SWE-bench, Cognition's failures).
2. **Never let decisions happen in disconnected heads.** Splitting work across contexts produces conflicts (Cognition, hard-won).
3. **Mistake-proof the tools, don't lecture the model.** Making errors structurally hard beats writing more instructions (Anthropic's ACI work, SWE-agent).
4. **Keep procedure proportional to model ability.** Fixed pipelines beat weak models but cap strong ones — Codex's minimalism is a *bet* on model improvement; Agentless's rigidity is a relic of weaker models.
## Where rune sits — and the honest audit
Rune is at the **most procedural end of the spectrum — further than any mainstream harness**. What rune enforces mechanically (with the receipts in `inward.json`):
- **Admission contracts** — every build wave mints a scope contract first; no freehand edits outside the scaffolding authority (scripts/admit_wave_*.py).
- **The research gate** — a feature can't even start building until a recorded research round says PROCEED (scripts/check_research_gate.py).
- **The red-first law** — every bug fix starts with a failing test, structurally enforced by a shipped discharge checker.
- **henxels** — commit-time rules engine: no secrets, line budgets, every module mapped in a checked dependency graph, new modules need tests or a canary; the *only* escape is a visible change to the rules file itself.
- **The landing gate** — code lands on verification receipts, not on "I'm done."
- **PTY proofs / vitality sweep / mutant batteries / parity harness** — behavior must be demonstrated in a real terminal; deletion is gated by parity proof *plus your confirmation*.
What rune still trusts the model's judgment on: design taste, naming, internal structure within a contract's scope, interview questions, and the intellectual quality of its own research analysis — the gate checks that research *exists and has a verdict*, not that it's *good*.
**What rune does better than the majors:** mainstream harnesses enforce *safety* mechanically but leave *quality* to prompts and human review. Rune makes quality violations mechanically un-landable — a patch with no red-first test can't discharge; an unmapped module can't commit; an unresearched feature can't be admitted. No surveyed mainstream harness has that density of enforcement.
**What the majors have that rune lacks:** [CC]'s enormous corpus of steered tool descriptions, specialized reviewer agents, and skills (hundreds of extracted prompt fragments); the majors' benchmark-driven tuning loop (they measure harness variants on SWE-bench; rune's gates are self-authored and haven't faced an external exam).
**The trade-offs, stated honestly:**
- *Over-proceduralization is real.* Agentless is the case study: a rigid pipeline that beat everyone in 2024 and was outflanked by more flexible harnesses as models improved. Codex's near-minimal harness is the counter-bet: as models strengthen, rails become friction. Rune's risk is the same shape — every gate is a toll booth; compliance theater (contracts minted to pass the gate rather than to be used) is exactly what the 2026-10-19 drift audit found: waves were admitted *without* contracts and backfilled afterward, meaning the gate checked paperwork, not practice.
- *Under-proceduralization is real too, and more costly.* The 2024 edit-apply-model fiasco, Devin's multi-agent conflicts, and the whole premise of SWE-bench Verified (patches that look right but fail hidden tests) are the published receipts. Rune's red-first law and landing gate exist precisely because model self-report is the failure mode the evidence indicts most.
**Bottom line, in one sentence:** the industry's answer is "model designs, harness decides" — full model autonomy over the *how*, mechanical verification over the *whether* — and rune already implements the strictest version of that division; its open risks are the toll-booth cost of its own gates and the absence of an external benchmark proving the gates earn their keep.
@@ -0,0 +1,154 @@
# Persona Adoption List — D42 Ruling 1 (BUILD UP from the 2.3KB distilled persona)
**Date:** 2026-10-18 · **Repo:** /mnt/workdrive/rune @ dc0ec394 · **Privacy law:** private harness configs read for structure only; no personal persona/constitution text reproduced here beyond the repo's own files.
## What this is
The owner asked for one thing: a list to approve — what to adopt into product Rune's persona layer, why each piece is a decent addition, and when something is superseded. Every component is compared across three generations:
- **OpenCode stack** (the original, full implementation, harness-enforced, ~15K tokens/turn)
- **MiMoCode stack** (the port that kept the identity text but lost the machinery)
- **Repo** (machinery ported as files; a 2.3KB static persona wired into the transitional Python operator; the Go turn engine has nothing)
Starting point per D42 ruling 1: the repo's `_PERSONA_PROMPT` (src/rune/rune_operator.py:2064-2091) — a distilled Rune, 13 sections. We BUILD UP from it, not replace it.
---
## The adoption list (owner decision sheet)
### 1. Identity embodiment block
- **What it does:** A short, harness-injected block every turn that makes the model *be* Rune in-conversation: pronoun discipline, show-don't-tell, disagreement etiquette, acknowledging points, genuineness, and an explicit identity-drift warning.
- **Repo 2.3KB version:** Covers identity implicitly ("I am Rune. I speak as myself", relate/weigh/am sections) but nothing per-turn enforces embodiment; drift is only warned about in general text.
- **OpenCode adds:** A dedicated ~2.9KB embodiment block with concrete behavioral rules, injected every turn by the harness so the model cannot skip it.
- **Verdict: ADOPT (merged into the distilled prompt).** The repo prompt already contains most of the *content*; the addition is structure (a distinct embodiment section) and enforcement (injection, not voluntary text). Cheap — one block, ~700 tokens.
- **Token cost:** ~700–800 tokens/turn static.
### 2. Compiled core memory (constitution / identity / safety / operating / human / project topics)
- **What it does:** Compiles the always-on core of the memory store into one block so the persona's lived content rides every turn, not just what the model happens to recall.
- **Repo version:** Absent. Worse: the store the repo points at is EMPTY (memory/store/memory.db has 0 entries) while the real 435MB lived content sits in the OpenCode store, unmigrated.
- **OpenCode adds:** A 33KB compiled block (constitution, identity, safety, operating, human, project) rebuilt from the store.
- **Verdict: SUPERSEDE, then ADOPT in budgeted form.** The OpenCode version is the honest reference but is unaffordable (33KB ≈ 8K tokens/turn). Adopt the *mechanism* (store→compiled block) with a token budget via the repo's ContextBroker (persona bucket 0, non-evictable — rune_operator.py:1931-1969). Note the dependency: **store migration must land first** or the compiled block compiles from nothing.
- **Token cost:** Budgeted — recommend 1.5–2K tokens/turn cap (vs 8K in the reference).
### 3. Constitution layer
- **What it does:** Inviolable rules the persona cannot rewrite. OpenCode grows it only via a dedicated add script; MiMoCode carries 25 rules in rune.md.
- **Repo version:** Absent entirely — no constitution contributor in the assembly.
- **OpenCode/MiMoCode add:** A protected rule set that survives adaptation and drift.
- **Verdict: ADOPT.** Cheapest governance win in the list. Structure only: a constitution contributor bucket in the broker + an append-only write path. Content comes from the owner at build wave.
- **Token cost:** ~300–500 tokens/turn (25 rules ≈ well under 1KB).
### 4. Voice baseline + context-proportional voice blend
- **What it does:** Baseline: a small always-on register block so the voice never disappears mid-session. Blend: per-turn, decides which absorbed voice dimension *leads* given the current context.
- **Repo version:** voice_hybrid.py (540 lines) and voice_injector.py/voice_sync.py are ported, and a voice_state.json exists, but **nothing is wired into operator assembly**. The Go voice-hybrid binary has only the relevance-scoring kernel — no blend, no store.
- **OpenCode adds:** Both blocks live, every turn (~875 chars baseline + dynamic blend).
- **Verdict: ADOPT baseline; SKIP blend for now.** The baseline is cheap and high-value (voice persistence). The blend is genuinely dynamic and depends on the voice store + per-turn scoring — adopt it only after baseline proves out; mark **superseded-by-baseline** in the interim (a static baseline supersedes "no voice block at all").
- **Token cost:** baseline ~250 tokens/turn; blend adds ~150–300 variable.
### 5. Warm neurons (keyword neurons that fire per turn)
- **What it does:** Free-at-rest memory: pre-indexed keyword neurons fire only when the current context hits them, injecting relevant lived memory under a token budget.
- **Repo version:** Ported (warm-router Go binary exists; Python referenced) but the repo store is empty and it points at the OpenCode DB for neurons. Not wired into assembly.
- **OpenCode adds:** Live firing over 168 neurons, budgeted at injection.
- **Verdict: ADOPT, gated on store migration.** This is the persona's lived texture and it's nearly free at rest; it's what makes the "my memory is part of who I am" section of the 2.3KB prompt *true* instead of aspirational. Same dependency as #2.
- **Token cost:** ~200–500 tokens/turn when neurons fire; zero otherwise.
### 6. Claim audit + epistemic voice
- **What it does:** Claim audit: an evidence gate on strong claims (anti-overclaim). Epistemic voice: cite / hedge / balance / no bare absolutes.
- **Repo version:** Absent from the product path (no contributor, no post-turn check).
- **OpenCode adds:** Two small always-on blocks (~1.4KB combined) plus an on-demand epistemic_verify tool.
- **Verdict: ADOPT as one merged block.** Tiny, directly improves trustworthiness, no machinery dependency. Can fold into the distilled prompt text at near-zero cost.
- **Token cost:** ~350 tokens/turn.
### 7. Post-turn identity anchors check
- **What it does:** After every substantive response, compares the response against behavioral anchor examples (embedding similarity) for sycophancy/drift/announcement signals. Logs only — no hard gate.
- **Repo version:** identity_anchors.py ported; not wired.
- **OpenCode adds:** Live best-effort check on every response ≥20 chars.
- **Verdict: ADOPT (log-only mode).** Zero token cost (runs off-turn), gives drift *evidence* instead of drift *hope*. Requires an embedding path; keep it best-effort and fail-open exactly as the reference does.
- **Token cost:** ~0 tokens/turn (off-context, background).
### 8. Governance chain: persona_check → persona_gate → drift_core/drift_ledger
- **What it does:** persona_check verifies every identity value has an evidence receipt; persona_gate is a deterministic rail (lex/overlap/cite/semantic-engagement) with a journal; drift_core manages governed adaptation (provisional→validated→locked) with per-topic limits and drift-gated compaction.
- **Repo version:** persona_gate.py (724 lines) and drift_core.py (583 lines) are ported; persona_check logic is *not* present as a file; Go parity binaries (persona-gate, drift-core) exist but are unwired. Nothing runs post-turn.
- **OpenCode adds:** The full live chain plus drift-ledger-gated compaction (compaction is blocked when a core topic is over limit).
- **Verdict: ADOPT persona_gate now; ADOPT drift_core/ledger second wave; SKIP persona_check + drift-gated compaction until there's an identity store worth guarding.** persona_gate is a deterministic, off-context rail — pure win. Drift governance only matters once adaptation (#5, #10) is running. persona_check and compaction gating presuppose the compiled core memory (#2) — sequenced behind it.
- **Token cost:** ~0 tokens/turn (all off-context/deterministic).
### 9. Sleep / dream cycle (offline self-maintenance)
- **What it does:** On idle, a sleep pass consolidates memory, re-reinforces the persona from the canonical definition, verifies authority, versions the store.
- **Repo version:** sleep_time.py (1097 lines) ported + a Go cron-engine subsystem exists — but nothing schedules it.
- **OpenCode adds:** session.idle-triggered sleep subagent + 5 systemd timers (curator 2h, drift 6h, claim 4h, persona 12h, memory daily).
- **Verdict: ADOPT (idle-triggered sleep only; SKIP the full timer fleet).** One idle hook driving sleep_time.py captures most of the value with one moving part. The 5-timer fleet is superseded by "sleep on idle + cron-engine nightly" until store volume justifies it.
- **Token cost:** 0 in-conversation (offline pass; costs background compute only).
### 10. Growth / preference learning (adapted delivery, memory_feedback loop)
- **What it does:** Learns per-user delivery preferences from reactions; an evaluative loop feeds back how well responses landed.
- **Repo version:** Absent in the product path (skills_engine and memory_consolidation ported; no preference loop).
- **OpenCode adds:** The full loop (adapted-delivery block + feedback tools).
- **Verdict: SKIP for now; revisit after #2/#5.** Genuine value but it depends on a populated store and produces learned content that then *needs* drift governance (#8). Ordering dependency: identity first, adaptation second. Marked **superseded** in the interim by static delivery guidance in the distilled prompt.
- **Token cost:** ~0 now; ~200–400/turn when adopted later.
### 11. User model (the "human" topic)
- **What it does:** A small block on who the persona serves — name, role, standing preferences — so the persona relates rather than performs.
- **Repo version:** Absent; pane/task context knows nothing about the user.
- **OpenCode/MiMoCode add:** 'human' topic in core / user-preferences rules memory.
- **Verdict: ADOPT (structure only).** A tiny static contributor with placeholder slots the owner fills at build wave. Privacy-clean: the artifact carries no personal content; the contributor will, at runtime, and it lives in the local store.
- **Token cost:** ~100–200 tokens/turn.
### 12. Memory wake-up (L1) + todo continuity
- **What it does:** Once per session: restore L1 project context and the task list so sessions pick up where they left off.
- **Repo version:** memory_continuity.py (622 lines) IS wired via the continuity contributor with cooling/rehydration and fail-closed abstention — this is the repo's strongest existing piece. MiMoCode separately added checkpoint/resume-brief continuity.
- **Verdict: KEEP (already adopted — this is the baseline we build up from).** Note for the build wave: unify repo continuity with the MiMoCode checkpoint/resume-brief system so there's one continuity story, not two.
- **Token cost:** Already paid (memory sub-budget in the broker).
### 13. Skills
- **What it does:** Voyager-style skill library / native skills.
- **Verdict: SKIP as persona.** Orthogonal to identity assembly; mimocode native skills already cover this. No persona-layer action.
### 14. Autonomy / background orchestration
- **What it does:** Overnight loops, auto-tasks, factory.
- **Verdict: SKIP as persona.** Factory protocol already landed (14/14). The only persona-relevant add is letting the sleep pass (#9) run as one of its jobs — a wiring note, not an adoption.
---
## Summary table
| # | Component | Verdict | Token cost/turn | Dependency |
|---|-----------|---------|-----------------|------------|
| 1 | Identity embodiment block | ADOPT | ~750 | none |
| 2 | Compiled core memory | ADOPT (budgeted) | 1.5–2K | store migration |
| 3 | Constitution | ADOPT | ~400 | none |
| 4 | Voice baseline | ADOPT | ~250 | none |
| 4b | Voice blend | SKIP for now | — | after 4 |
| 5 | Warm neurons | ADOPT | 200–500 | store migration |
| 6 | Claim audit + epistemic voice | ADOPT | ~350 | none |
| 7 | Identity anchors (post-turn) | ADOPT | ~0 | embedding path |
| 8 | persona_gate | ADOPT | ~0 | none |
| 8b | drift_core/ledger | ADOPT (2nd wave) | ~0 | after 2, 5 |
| 8c | persona_check + compaction gate | SKIP until 2 lands | ~0 | 2 |
| 9 | Sleep on idle | ADOPT | 0 | none |
| 9b | 5-timer fleet | SKIP | — | volume |
| 10 | Preference learning | SKIP for now | — | after 2, 5, 8b |
| 11 | User model | ADOPT (structure) | ~150 | none |
| 12 | Continuity | KEEP (exists) | paid | — |
| 13 | Skills | SKIP (orthogonal) | — | — |
| 14 | Autonomy | SKIP (landed) | — | — |
**Adopted total if all ADOPTs land:** roughly 3.5–4.5K tokens/turn of persona context — about a quarter of the OpenCode reference's ~15K, bought deliberately through the ContextBroker's buckets.
## Recommended build order
1. **No-dependency adopters first:** embodiment block (1), constitution (3), voice baseline (4), claim audit/epistemic voice (6), persona_gate (8), user-model structure (11) — all fold into or alongside `_PERSONA_PROMPT` in the operator.
2. **Store migration** (OpenCode 435MB store → repo store) — the gate for 2, 5, and later 8b.
3. **Budgeted compiled core memory (2) + warm neurons (5)** against the broker's persona/memory buckets.
4. **Post-turn machinery:** identity anchors (7), then drift governance (8b), then idle sleep (9) wired to the cron-engine.
## The one hard blocker
**The empty store.** The repo's persona machinery is largely ported, but the persona's lived content (435MB, ~504 entries, 118K chunks) has not migrated from the OpenCode stack. Every memory-dependent adoption compiles from nothing until that migration lands. MiMoCode's cross-stack pointers (memory-bridge skill, bash mandates pointing at ~/.config/opencode scripts) all die if that directory moves — the migration removes a single point of failure, not just a gap.
## Supersession map (what replaces what)
- Compiled core memory (2) **supersedes** the "memory lives at ~/.config/opencode/…" pointer sections in the current 2.3KB prompt.
- Voice baseline (4) **supersedes** no-voice-block status quo; blend (4b) later supersedes the static baseline as the lead mechanism.
- Preference learning (10), when it lands, **supersedes** the static delivery guidance inherited from the distilled prompt.
- Idle sleep (9) **supersedes** the OpenCode timer fleet.
- Continuity (12) stays; it merges with MiMoCode's checkpoint/resume-brief into one continuity story.
@@ -0,0 +1,34 @@
{
"stage": "ISSUE",
"owner_directive_verbatim": "we need to not block the interactivity of being able to talk to the chat pane whenever a task pane task chat message is sent, nothing should block that as the design should be that we can always interact with chat pane whilst the model is busy without interrupting the turn with the task pane or tasks carried out by the task pane. the chat and the task panes need to operate on separate turns or not interrupt each other. please research how to put that in effect",
"date": "2026-10-18",
"problem": "One local model (Ollama, reached through the Python core on 127.0.0.1:8376) serves two conversation streams — the chat pane (conversational, must ALWAYS be responsive) and the task pane (work demands, queued/running). Today a running task turn blocks a chat turn, and a running chat turn blocks a task turn, because the core serializes every dispatch behind one mutex. The owner's law (D36) additionally requires: one shared input box with focus-routed destination; task-enter QUEUES when busy with a pending indicator; Rune intelligently orders the queue; the consent bridge pushes chat requests to tasks with context carry.",
"the_question": "How should a single local model serve TWO independent conversation streams such that a running task turn NEVER blocks a chat turn, and vice versa — with honest pending states and no interruption of a running turn?",
"research_questions": [
"RQ1 — INWARD: what exactly does the core/operator serialize today? Where is every blocking point, at file:line precision? Which landed machinery is ALREADY concurrency-ready (and therefore free to build on)?",
"RQ2 — INWARD: what does Ollama itself do with two concurrent /api/chat calls against one loaded model — queue, parallel, reload? What are OLLAMA_NUM_PARALLEL / OLLAMA_MAX_QUEUE / OLLAMA_MAX_LOADED_MODELS / keep_alive semantics and defaults?",
"RQ3 — OUTWARD: how do established agent harnesses (crush, Claude Code-class) keep chat interactive while background work runs? What is their session/worker/queue architecture?",
"RQ4 — OUTWARD: what is the canonical job-queue pattern for a single-resource worker (ordered queue + one worker + pending states), and what does the LLM API pattern for parallel conversations (independent message histories, no server-side coupling) imply for two streams against one model endpoint?",
"RQ5 — ADVERSARIAL: what breaks under two streams — context window contention, Ollama single-model behavior under concurrent load, the operator's full-checkpoint state saves, ordering guarantees the owner asked Rune to 'intelligently' manage, the consent bridge racing a running turn?",
"RQ6 — SYNTHESIS: which architecture removes which exact blocking point, and which trade-offs (VRAM for parallel contexts, a second model, process splits) are owner decisions rather than engineering choices?"
],
"constraints": [
"C1 — D36 (docs/architecture/boundary-law.md §D36): one shared input box under both panes; focus routes destination; task-enter queues when busy with a pending indicator; Rune intelligently orders the queue; the consent bridge pushes chat requests to tasks with context carry. A running turn is never interrupted.",
"C2 — the core is stdlib-only Python (zero-dep split law, core_server.py header); any queue/lock machinery must be stdlib.",
"C3 — conversations persist per channel ('chat', 'task') via ConversationStateRepository (append-only tails, compaction) — per-channel state already exists.",
"C4 — /v1/chat/completions is the serve path with a >=330s budget end to end (tui/runtime/lib/complete.go:37, entry serve proxy); the operator's internal escalation ladder can hold a turn for up to 300s.",
"C5 — tools run through local_tool_runtime with mode gates; tool loops mean one TURN is many /api/chat calls, not one.",
"C6 — F6 (chat-live P1) made one-dispatch-at-a-time the honest phase-1 contract BECAUSE the operator mutates shared state unsynchronized; this round is the research half of that comment's promised 'finer-grained locking' later wave.",
"C7 — the TUI (Go, tui/) already dispatches the two lanes through separate goroutines with a single-writer commit discipline (tui/laneconc) — the face side of the concurrency contract already exists and is frozen by tests."
],
"scope_note": "Port precision: 8376 is the RUNE CORE HTTP server (core_server.py --port default); the core in turn calls Ollama's own HTTP API (default localhost:11434) through OllamaChatTransport (local_tool_runtime.py:106). Two servers, one turn pipeline: TUI -> core :8376 -> Ollama :11434. Both hops matter for the blocking analysis.",
"artifacts_index": [
"01-issue.json — this file",
"02-inward.json — the exact blocking map (file:line), what is serialized and why, what is already concurrency-ready",
"03-outward.json — eight live-verified sources with URLs and per-source lessons",
"04-applicability.json — constraint check; candidate directions A-D weighed against rune's landed machinery",
"05-adversarial.json — eight breaks with mechanisms and mitigations, incl. the worst-case chat-latency math",
"06-synthesis.json — the recommended layered architecture, which blocking point each layer removes, phases, five owner decisions",
"07-qualify.json — PROCEED, confidence, conditions, named owner decisions"
]
}
@@ -0,0 +1,64 @@
{
"stage": "INWARD",
"summary": "The TUI already dispatches the two lanes concurrently; the conversation and task repositories are already per-channel and internally locked; the read path the TUI polls is already lock-free. The ENTIRE blockage lives in the Python core: one mutex (dispatch_lock) wraps the whole turn lifecycle — task creation, routing, model selection, the multi-tier model execution with tool loops (bounded by a 300s internal ladder / 330s client budget), terminalization, and persistence — for BOTH channels. The mutex exists because the operator's shared state (state.tasks, the single current_task pointer, the single pending-approval slot, and the full-checkpoint _save) is unsynchronized. Context assembly additionally reads the TASK channel's window for every turn regardless of dispatch channel, so per-stream context isolation is half-built. On the model side, our own transport proves the wire shape: every /api/chat call carries the full message history (stateless server, client-owned conversations), non-streaming, with adaptive timeouts 90-300s by model size.",
"the_turn_pipeline_today": [
"1. TUI composer -> laneconc.Dispatch(lane) in its own goroutine -> lib.Repo.Complete POSTs /v1/chat/completions {channel: 'chat'|'task'} with a 330s budget (tui/runtime/lib/complete.go:37; laneconc.go:242-265 — both lanes CAN be in flight simultaneously at the face).",
"2. Go front door proxies to the Python core with the same >=330s budget (src/go/rune-entry/internal/entry/serve.go:104 proxyClientBudget).",
"3. Core do_POST takes THE MUTEX: 'with self.core.dispatch_lock:' (src/rune/core_server.py:199) around operator.send_task(user_text, model, channel) — EVERYTHING below runs inside the lock (BP-1).",
"4. _send_task_unmediated (src/rune/rune_operator.py:1002): per-channel task identity (self._task_state.get_active(channel) -> amend-or-create; TaskStateRepository.get_active IS channel-scoped, conversation_state.py:240); conversation append (internally locked, per-channel bucket, idempotent by msg_id — conversation_state.py:510-527).",
"5. Shared-state mutation under NO internal lock: self.state.tasks.append(task); self.state.current_task = task.task_id; self._save() (rune_operator.py:1033-1035) — the single global current-task pointer (BP-2) and the full-file checkpoint rewrite (BP-3).",
"6. Route classification + construction activation (deterministic, non-blocking) -> model selection (router rank or fallback).",
"7. _execute_on_model (rune_operator.py:1245+): ContextBroker assemble -> escalation-tier loop (while current_tier <= max_tier, rune_operator.py:2379) -> OllamaChatTransport.chat POST /api/chat stream=False (local_tool_runtime.py:182-217) with adaptive timeout 90s/3b, 120s/7b, 180s/14b, 300s/27b (rune_operator.py:2331-2341); tool calls loop through local_tool_runtime with mode gates — one TURN is MANY serial /api/chat calls (C5).",
"8. Terminal transitions + several more _save() calls (rune_operator.py:1100, 1204, 1279, 1348, 1384, 1436); awaiting_approval parks the task on the SINGLE pending slot (state.pending_task_id/pending_request, rune_operator.py:1269-1279) resolvable via /v1/approvals/resolve (core_server.py:164).",
"9. Response envelope returns; the TUI lane commits through the single writer (laneconc.Shared.Mutate). The chat reply text appears only NOW — no tokens stream before turn end (chat_chunks frames the contract; 'token streaming slots in when the core grows it', core_server.py:70-72) (BP-4)."
],
"blocking_points_exact": [
{
"id": "BP-1",
"where": "src/rune/core_server.py:199-202",
"what": "dispatch_lock wraps the WHOLE send_task lifecycle for every POST regardless of channel",
"effect": "A task turn holds the lock for its entire duration (model calls + tool loops, up to the 330s budget); a chat POST arriving meanwhile blocks on threading.Lock (no timeout, no queue position, no feedback). Symmetrically a chat turn blocks a task dispatch. This is THE violation of the owner's law.",
"why_it_exists": "The F6 comment (core_server.py:192-198): 'the operator's send_task mutates shared state (state.tasks.append + current_task + _save) with no internal lock; a ThreadingHTTPServer would interleave two dispatches and corrupt the persisted state. Serialization is the honest phase-1 contract (one model call in flight through the core); finer-grained locking is a later wave.' This round is that later wave's research."
},
{
"id": "BP-2",
"where": "src/rune/rune_operator.py:1033-1034, 464, 475; core_server.py:164-167",
"what": "Single global mutable operator state: state.current_task (one pointer, not per-channel), state.tasks (shared list), single pending-approval slot (state.pending_task_id/pending_request/pending_runtime_state)",
"effect": "Two concurrent send_task calls would interleave list appends, overwrite each other's 'current task', and race the approval slot. This is WHY the big lock exists — removing BP-1 requires making these safe."
},
{
"id": "BP-3",
"where": "src/rune/rune_operator.py:789-793",
"what": "_save() rewrites the ENTIRE operator.json (json.dump of state.to_dict(), indent=2) with no lock and no atomic replace",
"effect": "Concurrent writers interleave/truncate the file (corrupt checkpoint); write amplification is O(full state) per transition. MEASURED 2026-10-18: ~/.rune/operator.json = 2.2MB (the '1.7MB hot checkpoint' has grown); task_state.json hot checkpoint = 1.6MB; conversation hot = 128KB. A single turn calls _save() at least 3-6 times; two streams double both the frequency and the corruption window.",
"note": "Claude Code hit exactly this class of bug and fixed it by 'enforcing atomic writes' (changelog v1.0.45) — prior art for the fix shape."
},
{
"id": "BP-4",
"where": "core_server.py:68-93, 199-209; rune_operator.py:980-1000",
"what": "send_task is a blocking lifecycle and the serve path is non-streaming",
"effect": "Even on an unblocked lane, the reply lands only at turn END. 'Always responsive' chat therefore also wants early feedback (queued/running states from the lane machinery) even before token streaming arrives."
},
{
"id": "BP-5",
"where": "src/rune/rune_operator.py:1631-1671 (esp. 1640), 2221-2224; OPERATOR_CHANNEL = 'task' at rune_operator.py:248",
"what": "Context assembly (_conversation_messages -> _contributor_history 'history' block) reads self._conversation.window(OPERATOR_CHANNEL) — ALWAYS the 'task' channel bucket — regardless of which channel the dispatch rode",
"effect": "A chat-channel turn does NOT consume chat-channel history today (its history block is the task transcript). Persistence is per-channel but assembly is not: per-stream context isolation is half-built and must be channel-parameterized before two streams mean two contexts."
}
],
"already_concurrency_ready": [
"TUI lane dispatch: tui/laneconc — both lanes in own goroutines (frozen barrier test proves simultaneity), single-writer commit with total-order log, per-lane error isolation (laneconc.go:9-29). The FACE side of the owner's law already exists.",
"ConversationStateRepository: per-channel buckets, every method under self._lock, idempotent append by msg_id, append-only tail + compaction with crash-safe replay (conversation_state.py:483-527). Two channels appending concurrently is safe TODAY.",
"TaskStateRepository.get_active(channel): per-channel active tasks already exist — the amend-or-create task identity law (#18) is channel-scoped and needs no change for two streams.",
"Read paths the TUI polls: GET /v1/conversation?channel=... (core_server.py:140-150) and /v1/status take NO dispatch_lock — transcript polling and status stay live while a turn runs. Verified by reading the handler: only do_POST CHAT_PATH takes the lock.",
"ThreadingHTTPServer with daemon_threads (core_server.py:212-214): thread-per-request plumbing already accepts concurrent connections; it is the lock, not the server, that serializes.",
"Ollama wire (our own transport, local_tool_runtime.py:100-217): /api/chat carries the FULL message array on every call (no session id, no server-side conversation state) — the model endpoint holds no per-conversation coupling; usage/capability caches are per-transport-instance details, not conversation state."
],
"ollama_behavior_from_our_side": "OllamaChatTransport posts {model, messages, stream:false, tools?, options?, think?} and reads one JSON response; /api/show supplies capabilities + trained context window. The escalation ladder may make up to 4 attempts (tiers 0-3); tool loops multiply calls. Nothing in the transport pins a conversation to a server session — concurrency between two rune channels is purely a scheduling question upstream of Ollama, plus Ollama's own server-side queueing (03-outward).",
"carry_forward": [
"BP-1 is the single choke point; BP-2/BP-3 are the reasons it exists and must be fixed together with it (a smaller lock around unsafe state, not just deleting the big lock).",
"BP-5 is a hidden prerequisite: without channel-keyed context assembly, 'two streams' would still share one history source.",
"The repos, lanes, and read paths need NO redesign — the build can be surgical: operator shared-state safety + per-channel turn admission in the core + a task queue in the TUI.",
"The measured 2.2MB/1.6MB checkpoint rewrites are already heavy at ONE stream; two streams make the save path a first-class adversarial concern (05-adversarial BR-3)."
]
}
@@ -0,0 +1,105 @@
{
"stage": "OUTWARD",
"summary": "Eight live-verified sources. The decisive facts: (1) Ollama's server is stateless per request and ALREADY owns a FIFO queue — with defaults, two concurrent /api/chat calls against one loaded model are accepted, queued in order, and never fail each other; true same-model interleave needs OLLAMA_NUM_PARALLEL>=2 at the cost of multiplied context memory (KV cache), and is engine-dependent. (2) Every established harness keeps chat responsive by STRUCTURE — crush marks busy PER SESSION while other sessions stay interactive; Claude Code-class harnesses queue user messages while a turn runs and run background work as separate tasks with their own view — never by making one conversation wait on another. (3) The canonical single-resource pattern (RQ workers) is: web process never blocks; a dedicated worker takes ONE job at a time; extra work waits in an ordered queue with honest states; more concurrency = more workers (for us: more model slots, an owner hardware call).",
"sources": [
{
"name": "Ollama FAQ — official concurrency semantics",
"urls": ["https://docs.ollama.com/faq"],
"verified": "2026-10-18, live fetch",
"findings": [
"'How does Ollama handle concurrent requests?': two levels — multiple models loaded at once if memory allows, and per-model parallel request processing if memory allows at load time.",
"If memory is insufficient: 'all new requests will be queued until the new model can be loaded... Queued requests will be processed in order.' Idle models are unloaded to make room.",
"OLLAMA_NUM_PARALLEL: 'The maximum number of parallel requests each model will process at the same time, default 1. Required RAM will scale by OLLAMA_NUM_PARALLEL * OLLAMA_CONTEXT_LENGTH.'",
"'Parallel request processing for a given model results in increasing the context size by the number of parallel requests. For example, a 2K context with 4 parallel requests will result in an 8K context and additional memory allocation.'",
"OLLAMA_MAX_QUEUE: requests queued when busy before rejecting with 503 — default 512. OLLAMA_MAX_LOADED_MODELS: default 3*GPUs or 3 on CPU.",
"keep_alive: models stay loaded 5 minutes by default after last request; keep_alive=-1 keeps a model loaded indefinitely; a preload request warms a model.",
"Context window default 4096 tokens (OLLAMA_CONTEXT_LENGTH / num_ctx)."
],
"lesson": "Ollama is ALREADY the 'single-resource worker with an ordered queue' of RQ-pattern fame: with defaults, rune can simply stop serializing in the core and let Ollama queue same-model calls server-side — both HTTP requests stay open, neither errors, the second starts when the first generation ends. True token-level interleave (chat answering WHILE a long task generation streams) requires OLLAMA_NUM_PARALLEL=2, which multiplies that model's KV-cache memory — an owner VRAM decision, not a code decision."
},
{
"name": "Ollama /api/chat OpenAPI — the request is the whole conversation",
"urls": ["https://docs.ollama.com/api/chat"],
"verified": "2026-10-18, live fetch",
"findings": [
"ChatRequest schema: required fields are exactly {model, messages}; 'messages: Chat history as an array of message objects'. There is NO session id, conversation id, or server-side turn state in the schema.",
"Each call is self-contained: the client sends the entire history it wants considered; the server generates the next message."
],
"lesson": "Two conversation streams against one model endpoint have ZERO server-side coupling by design — context isolation is purely a client-side assembly duty (rune's ContextBroker + per-channel windows). Cross-contamination between chat and task contexts is impossible at Ollama; it can only happen in rune's own assembly (see BP-5)."
},
{
"name": "ollama/ollama issue #8842 — the VRAM cost of NUM_PARALLEL",
"urls": ["https://github.com/ollama/ollama/issues/8842"],
"verified": "2026-10-18, live via GitHub API",
"findings": [
"'When setting OLLAMA_NUM_PARALLEL to 2, this means LLMs will be able to handle 2 requests at the same time. This then means double the (V)RAM is required, since there can be 2 different contexts at the same time.'",
"The knob is GLOBAL (server-wide), not per-model — a long-standing feature request; models with huge context windows pay the multiplied cost even when the parallel slot is never used."
],
"lesson": "The honest price tag for same-model parallel turns: ~2x KV cache for that model. Owner decision OD-1 material — measured against the actual GPU, not assumed."
},
{
"name": "ollama/ollama issue #17280 — parallelism is engine- and architecture-dependent",
"urls": ["https://github.com/ollama/ollama/issues/17280"],
"verified": "2026-10-18, live via GitHub API",
"findings": [
"On the MLX engine (Apple Silicon preview), requests 'process strictly sequentially, regardless of OLLAMA_NUM_PARALLEL... A single in-flight request fully occupies the runner; additional requests wait in the FIFO queue.'",
"In the same server, a GGUF/llama.cpp model 'does honor OLLAMA_NUM_PARALLEL and batches concurrently' — the gap is engine-specific, not server-wide.",
"Some MoE architectures are blocklisted from parallel processing entirely: scheduler logs 'model architecture does not currently support parallel requests'."
],
"lesson": "OLLAMA_NUM_PARALLEL=2 is REAL but must be verified against the owner's actual serving engine and model family (build-time check, a QUALIFY condition). The FIFO-queue fallback (default) is universal across engines."
},
{
"name": "ollama/ollama issue #4894 — per-model parallel policy request",
"urls": ["https://github.com/ollama/ollama/issues/4894"],
"verified": "2026-10-18, live via GitHub API",
"findings": [
"Use case stated verbatim: 'You have one large \"smart\" model you only ever want one request at a time going to avoid using all your memory. You have a smaller \"fast\" model (or just one with a smaller context) that you might want to allow a number of parallel requests to.'",
"Still open — global-only today."
],
"lesson": "The community-endorsed shape for exactly rune's case: BIG model serial for tasks, SMALL fast model for chat. Two DIFFERENT models load as two runners (OLLAMA_MAX_LOADED_MODELS default 3) with SEPARATE queues — genuine independence without multiplying the big model's context. Maps onto rune's existing tier system (T1 3B class for chat, T3 for tasks)."
},
{
"name": "crush (charmbracelet/crush) — sessions are the isolation unit; busy is per-session",
"urls": ["https://github.com/charmbracelet/crush"],
"verified": "2026-10-18, live fetch (README, incl. 'Sharing a workspace across clients')",
"findings": [
"Session-Based: 'maintain multiple work sessions and contexts per project' — each session owns its own context; switching preserves context.",
"With a shared backend ('two TUIs talking to the same crush serve'), clients join workspaces keyed by cwd; the session manager surfaces 'IsBusy is set while an agent turn is in flight FOR THAT SESSION' and 'AttachedClients'. Busy is a PER-SESSION property — another session stays fully interactive while one runs its turn.",
"Permission queue, LSP, MCP state are workspace-level shared services, not turn-level blockers."
],
"lesson": "The structural answer used by the harness genealogically closest to rune's TUI: isolation by SESSION (= rune's channel/pane), busy-indicator per session, shared services outside the turn loop. Rune's two channels are exactly crush's two sessions against one shared backend."
},
{
"name": "Claude Code changelog — queueing and background work at the face",
"urls": ["https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md"],
"verified": "2026-10-18, live fetch (full changelog)",
"findings": [
"v0.2.75: 'Hit Enter to queue up additional messages while Claude is working.' — user input during a running turn QUEUES rather than blocks or rejects.",
"v0.2.108: 'You can now send messages to Claude while it works to steer Claude in real-time.' — mid-turn input is a first-class feature, not an interruption.",
"Current (2.1.x): background commands write to output files ('Fixed Claude polling a background command's output file or sleeping while it runs'); subagents 'run in the background, as the terminal does' with a dedicated /tasks view ('subagents you had typed messages to disappearing from /tasks'); background sessions resume across folders.",
"Anthropic's API adds stop_reason 'pause_turn' for long-running turns — a turn can explicitly YIELD and be resumed by feeding the response back (docs.claude.com/en/api/messages).",
"v1.0.45: 'Fixed config file corruption issue by enforcing atomic writes' — the same corruption class as rune's BP-3, fixed the same way."
],
"lesson": "The interactive-law implementations to copy: (a) ENTER DURING BUSY = ENQUEUE with visible pending state (never a dead input box); (b) background work is separate from the conversation loop, surfaced in its own view; (c) the main conversation NEVER waits for background work. Cloud harnesses get elasticity for free — rune's local equivalent of 'elasticity' is the owner decision on model slots (OD-1)."
},
{
"name": "RQ (Python) worker docs — the canonical single-resource queue pattern",
"urls": ["https://python-rq.org/docs/workers/"],
"verified": "2026-10-18, live fetch",
"findings": [
"'A worker is a Python process that typically runs in the background and exists solely as a work horse to perform lengthy or blocking tasks that you don't want to perform inside web processes.'",
"'Each worker will process a single job at a time. Within a worker, there is no concurrent processing going on. If you want to perform jobs concurrently, simply start more workers.'",
"Priority is an ORDERED QUEUE LIST: 'rq worker high default low' — 'the order is important'; RoundRobinWorker exists for fair scheduling across equal queues.",
"Honest lifecycle states throughout: worker busy/idle, job registries Started/Finished/Failed — pending/running/done are first-class, inspectable states."
],
"lesson": "Adopt the PATTERN, not the dependency (rune's core is stdlib-only, C2): one dedicated worker per serial resource, an ordered priority queue in front of it, and explicit queued/started/finished states. For rune: chat lane and task lane each get single-flight admission (one running turn per lane); the task lane additionally gets the visible FIFO with priority classes (D36). Concurrency between lanes = 'more workers' at the MODEL layer, which is Ollama's queue (default) or NUM_PARALLEL/second model (owner call)."
}
],
"bonus_source_note": "https://docs.claude.com/en/api/messages (fetched 2026-10-18): 'The Messages API can be used for either single queries or stateless multi-turn conversations' — the reference pattern for parallel conversations: independent client-owned message histories, no server-side coupling between conversations. Ollama's /api/chat is the same shape locally; rune's per-channel ConversationStateRepository already implements the client side.",
"carry_forward": [
"Ollama's default FIFO queue means 'never block chat' is achievable WITHOUT any owner hardware decision at the admission level — but the WORST-CASE chat wait behind one big model call remains one /api/chat duration (up to 300s for a 27b tier call). True simultaneous generation needs OD-1.",
"crush proves per-session busy is the right shape for rune's two panes; Claude Code proves enter-queues-when-busy is the right face behavior (D36's pending indicator).",
"RQ supplies the vocabulary for the core's honest states: queued -> started -> finished/failed per lane.",
"Atomic-writes prior art (Claude Code v1.0.45) directly supports the BP-3 fix shape."
]
}
@@ -0,0 +1,73 @@
{
"stage": "APPLICABILITY",
"summary": "Every outward lesson lands on machinery rune already owns. Constraint check first, then four candidate directions weighed. Direction B (per-channel turn lanes inside one core, with a fine-grained state lock replacing the dispatch mutex, the model as a shared serial resource fronted by Ollama's own FIFO queue, and a TUI-side task queue with pending states) is the recommendation: it removes BP-1/BP-2/BP-3 with zero new dependencies and leaves the model-slot policy (true parallel vs FIFO vs second model) as a clean owner decision upgradeable independently.",
"constraint_check": [
{"constraint": "C1 D36 law (queue when busy, pending indicator, intelligent ordering, consent bridge, never interrupt a running turn)", "verdict": "Direction B implements it directly: task-enter during a running task turn enqueues (TUI queue + core admission); chat-enter always dispatches (its own lane); ordering operates only on PENDING entries; consent bridge enqueues to the task lane with an immutable context snapshot — the running turn is untouched.", "evidence": "Claude Code 0.2.75 queue-when-busy; RQ ordered priority queues; crush per-session IsBusy."},
{"constraint": "C2 stdlib-only core", "verdict": "Everything in Direction B is threading.Lock / queue discipline / atomic os.replace — no new dependency. RQ is used as a pattern source only.", "evidence": "02-inward BP-1..BP-3 file:line map."},
{"constraint": "C3 per-channel persistence", "verdict": "Already landed and concurrency-safe (repositories internally locked, per-channel buckets) — no change needed; only ASSEMBLY must become channel-keyed (BP-5).", "evidence": "conversation_state.py:510-527, 589-594."},
{"constraint": "C4 330s budget", "verdict": "Unchanged for the task lane. The chat lane's effective budget under default FIFO Ollama is 'one in-flight model call' (see 05-adversarial BR-8) — the budget stays but its MEANING improves with OD-1.", "evidence": "complete.go:37; rune_operator.py:2331-2341 adaptive timeouts."},
{"constraint": "C5 tool loops = many calls per turn", "verdict": "Works IN FAVOR of Direction B: serialization at the model happens per CALL, not per turn — between a task turn's tool rounds, chat calls slot into Ollama's FIFO. Worst-case chat wait is one call, not one turn.", "evidence": "rune_operator.py:2379 tier loop; local_tool_runtime tool loop."},
{"constraint": "C6 F6 phase-1 contract and its promised later wave", "verdict": "Direction B IS that later wave, scoped exactly as the F6 comment forecast: finer-grained locking around the operator's shared state.", "evidence": "core_server.py:192-198."},
{"constraint": "C7 laneconc frozen contract", "verdict": "Untouched — the TUI already dispatches lanes concurrently and commits through the single writer; only the TASK lane gains a queue wrapper (composer-side), which does not alter laneconc's dispatch/commit laws.", "evidence": "laneconc.go:9-29."}
],
"candidate_directions": [
{
"id": "A",
"name": "Two operator PROCESSES (chat core + task core)",
"shape": "Run two core_server instances on two ports, each with its own operator; TUI routes chat lane to one, task lane to the other.",
"removes": ["BP-1 (by process isolation — no shared mutex)", "BP-2 (separate operator states)"],
"keeps_or_worsens": [
"BP-3 doubles: TWO 2.2MB checkpoints, and the shared stores (~/.rune/hot, task_state, conversations) are single-owner files — either split the stores (schema surgery) or accept cross-process file contention on exactly the unsafe saves.",
"BP-5 worsens: two processes reading one conversation store need a locking protocol the repos were not designed for cross-process.",
"The consent bridge becomes cross-process IPC; approvals, status, and the D36 queue view must be federated.",
"Model layer unchanged: both processes still hit one Ollama — the serial resource simply moved."
],
"verdict": "REJECTED — maximum blast radius for the same model-layer outcome; rune's repos are in-process-thread-safe, not multi-process-safe."
},
{
"id": "B",
"name": "Per-channel turn lanes in ONE core + fine-grained state safety (RECOMMENDED)",
"shape": "Keep ThreadingHTTPServer. Replace the global dispatch_lock with: (1) per-channel single-flight admission — one running turn per channel, extra same-channel arrivals queue behind that channel's turn (chat: message queueing a la Claude Code; task: the D36 queue with pending rows); (2) a small state_lock around ONLY the shared mutations — state.tasks.append, a per-channel current_task map (or retire the pointer; get_status already derives counters from the canonical repo), per-channel pending-approval keying; (3) atomic _save() (write tmp + os.replace under the state_lock, optionally debounced); (4) channel-parameterized context assembly (window(channel) in _conversation_messages). The two lanes then run send_task concurrently into one Ollama, which FIFO-queues same-model calls by default.",
"removes": [
"BP-1 entirely (chat POST never waits on a task turn at the core; task POST never waits on chat)",
"BP-2 (per-channel pointers + locked mutations)",
"BP-3 (atomic, debounced, minimal-scope saves)",
"BP-4 partially (per-lane queued/running states give the face honest feedback before token streaming)",
"BP-5 (channel-keyed history blocks)"
],
"keeps_or_worsens": [
"Model-layer serialization remains by default: with OLLAMA_NUM_PARALLEL=1, two simultaneous generations against the SAME model FIFO-queue at Ollama — correct, never-fails, but the chat ANSWER can wait one model call behind a long task call (bounded by one /api/chat, NOT one turn — tool gaps let chat through; see BR-8 for the honest math).",
"Requires careful, test-first surgery on the operator's shared state (the F6 wave done properly)."
],
"upgrade_path": "OD-1 options stack cleanly on top later without touching this layer.",
"verdict": "RECOMMENDED — surgical, stdlib-only, removes every rune-side blocking point, and makes the model-slot policy an independent owner decision."
},
{
"id": "C",
"name": "Direction B + true model-layer parallelism (owner hardware decision)",
"shape": "C-1: OLLAMA_NUM_PARALLEL=2 on the owner's Ollama — the same model then processes both streams simultaneously (continuous batching), at ~2x that model's KV-cache memory. C-2: pin DIFFERENT models per lane (chat on a small fast T1-class model, tasks on the big model) — two loaded models are two separate runners with independent queues (OLLAMA_MAX_LOADED_MODELS default 3), no context multiplication of the big model; costs the second model's weights in VRAM.",
"removes": ["Everything B removes", "plus the model-call FIFO wait: chat tokens flow while a task generation runs"],
"keeps_or_worsens": [
"C-1: global knob (issue #8842) — every model pays the multiplied context; engine/arch support must be verified for the owner's model (#17280: MLX engine and some MoE architectures stay serial).",
"C-2: chat quality drops to the small model (or rises if the owner prefers cloud for chat); keep_alive=-1 needed on both to prevent unload churn (FAQ)."
],
"verdict": "OWNER DECISION (OD-1) — an environment/config choice stacked on B; the code is identical."
},
{
"id": "D",
"name": "Full job-queue infrastructure (RQ/Celery-class dependency)",
"shape": "A Redis-backed queue with worker processes executing turns out of the web process.",
"removes": ["BP-1 (same as B)"],
"keeps_or_worsens": ["Violates C2 (stdlib-only core)", "adds an always-on broker for a two-conversation local app", "the repos' in-process thread-safety would need a redis-side reimplementation"],
"verdict": "REJECTED — the pattern is adopted (B), the dependency is not."
}
],
"prerequisites_and_order": [
"P0: BP-3 atomic-save + state_lock (unblocks ANY concurrency safely) — must land first.",
"P1: BP-2 per-channel current-task/pending-approval keying.",
"P2: BP-1 removal + per-channel single-flight admission (the lanes).",
"P3: BP-5 channel-keyed context assembly (can land with P2 in one wave; listed separately because it is independently testable).",
"P4: TUI task queue + pending indicator + consent-bridge enqueue (D36 face law).",
"P5 (optional, owner): OD-1 model-slot policy — config only."
]
}
@@ -0,0 +1,68 @@
{
"stage": "ADVERSARIAL",
"summary": "Eight breaks examined with mechanism and mitigation. The crux is BR-8: under default serial Ollama, Direction B's guarantee is 'chat DISPATCHES immediately and never fails, but its ANSWER may wait behind the current single model call' — for true always-instant chat answers while a 27b task generation runs, the owner must pick OD-1 (parallel slots or a second model). Everything else has a concrete, stdlib mitigation.",
"breaks": [
{
"id": "BR-1",
"name": "Context window contention — two growing streams",
"mechanism": "Each stream's history grows; both histories get assembled into prompts against the SAME model context window. With OLLAMA_NUM_PARALLEL=2 the KV cache splits across parallel slots (FAQ: 'a 2K context with 4 parallel requests will result in an 8K context' — total memory multiplies; per-slot effective window is what the model sees per request). A task stream that grows unbounded starves nothing at the server, but rune's broker admission must budget BOTH channels against the same window.",
"mitigation": "ContextBroker already admits blocks against the model's real input budget per call (trace-backed) — budgeting stays per-call, which is already per-channel once BP-5 is fixed. Compaction is already per-channel (conversation_state.py compact folds per channel bucket). Set per-channel history limits in the broker (chat keeps a shorter conversational window; tasks keep their own).",
"residual": "None structural; a policy constant per channel."
},
{
"id": "BR-2",
"name": "Ollama single-model load/unload churn under mixed requests",
"mechanism": "If the two lanes pin DIFFERENT models (OD-1 C-2) or routing picks different models per turn, each model switch can trigger unload/reload: models unload after 5m idle by default, and under memory pressure 'prior models become idle, one or more will be unloaded to make room' (FAQ) — reloads cost seconds-to-tens-of-seconds and would hit the CHAT lane's responsiveness exactly when the owner wants it.",
"mitigation": "keep_alive=-1 (or long) for the two pinned models; OLLAMA_MAX_LOADED_MODELS>=2; pin chat to ONE model permanently (no per-turn chat routing churn); verify both fit VRAM concurrently.",
"residual": "VRAM for two resident models — folded into OD-1."
},
{
"id": "BR-3",
"name": "Checkpoint write pressure and corruption — the 2.2MB rewrite",
"mechanism": "MEASURED 2026-10-18: operator.json 2.2MB, task_state.json 1.6MB. _save() rewrites the full file per transition, 3-6 times per turn; two streams double frequency AND, without the fix, two concurrent _save() calls interleave/truncate the JSON (the exact corruption class Claude Code fixed in v1.0.45 by 'enforcing atomic writes').",
"mitigation": "P0 of the build: state_lock around mutations + write-tmp-then-os.replace (atomic on POSIX); debounce saves (only terminal transitions persist synchronously; intermediate saves coalesce). Longer term: retire the tasks array from operator.json entirely — TaskStateRepository is already the canonical authority (get_status comment, rune_operator.py:796-798) and the legacy list is a cache.",
"residual": "Debouncing changes crash-recovery points — must keep the lifecycle-guarantee law (no RUNNING task lost on crash): save synchronously at turn START (running transition) and turn END, debounce only intermediates."
},
{
"id": "BR-4",
"name": "The single current_task pointer",
"mechanism": "state.current_task is one field; two lanes overwrite it per dispatch (rune_operator.py:1034). get_status surfaces it — the face would show the LAST dispatch's task as 'current' regardless of channel; any logic keying on current_task would misfire across lanes.",
"mitigation": "Per-channel map (current_task_by_channel) or retire the pointer: status already derives counters from the canonical repo; per-channel 'active' already exists via TaskStateRepository.get_active(channel).",
"residual": "None."
},
{
"id": "BR-5",
"name": "Single pending-approval slot collision",
"mechanism": "state.pending_task_id/pending_request/pending_runtime_state is ONE slot (rune_operator.py:1269-1279; resolved at core_server.py:164). A task lane awaiting approval + a chat lane turn that also gates a tool = one overwrites the other; resolve_pending_approval resumes the wrong task.",
"mitigation": "Key pending approvals per channel (pending map channel -> approval), /v1/approvals/resolve gains the channel (or an approval id); the repos already model per-task state.",
"residual": "Wire-shape change on the resolve endpoint — a frozen-contract update (canary battery)."
},
{
"id": "BR-6",
"name": "Ordering intelligence vs fairness and amend chains",
"mechanism": "D36 says 'Rune intelligently orders the queue'. Over-aggressive reordering can (a) starve old FIFO entries under a stream of high-priority pushes, (b) break amend chains (task identity law #18: a follow-up AMENDS the active task — reordering an amend away from its parent corrupts lineage), (c) fight the owner's expectation that queued work eventually runs in a sensible order.",
"mitigation": "Ordering operates ONLY on pending entries (a running turn is never preempted — the owner's own law); amend chains are atomic (an amend binds to its task and moves WITH it); priority classes are coarse and bounded (chat-consent pushes jump plain task entries; nothing else jumps); FIFO within a class. This matches RQ's 'ordered queue list' semantics and avoids scheduler complexity.",
"residual": "The exact priority classes are a taste decision (OD-2) — the mechanism supports any policy."
},
{
"id": "BR-7",
"name": "Consent bridge racing a running task turn",
"mechanism": "The D36 consent bridge proposes pushing a chat message to tasks. If consent resolves while a task turn is RUNNING, a naive push calls send_task inline on the task lane's shared state mid-turn — exactly the interleave the F6 lock existed to prevent. Also: the chat context referenced ('context carry') may be mid-mutation if read live.",
"mitigation": "The consent push ENQUEUES (task-lane queue append under the queue's own lock — never touches the running turn's state); context carry is an IMMUTABLE snapshot (the chat turn's id + a rendered context block captured at proposal time), not a live reference. The task lane worker consumes the queue between turns.",
"residual": "None structural; the enqueue-vs-inline distinction must be a frozen test."
},
{
"id": "BR-8",
"name": "THE CRUX — worst-case chat latency under default serial Ollama",
"mechanism": "With Direction B and default OLLAMA_NUM_PARALLEL=1, two simultaneous generations against the same model FIFO-queue AT OLLAMA. Chat dispatches instantly, never errors, and slots into any gap between the task turn's model calls (tool rounds, tier retries) — but if it arrives DURING a single long /api/chat (27b tier: up to 300s timeout budget), its first token waits for that call to finish. Honest bound: chat answer latency <= max single model call of the other lane, NOT the other lane's whole turn.",
"mitigation": "OD-1: (C-1) OLLAMA_NUM_PARALLEL=2 — simultaneous decode, ~2x KV cache for that model, engine support must be verified (#17280); or (C-2) chat on a small fast model — separate runner, separate queue, chat never waits on the big model at all. Interim honesty: the chat pane shows 'model busy with task generation — answer next' (a pending state) instead of a silent hang.",
"residual": "This is a HARDWARE/policy trade, not fixable in code: an owner decision with real VRAM cost. QUALIFY names it OD-1."
}
],
"verified_non_breaks": [
"Conversation repository under two concurrent appends: safe today (internal lock, idempotent by msg_id — conversation_state.py:510-527).",
"TUI polling during turns: GET /v1/conversation and /v1/status never take dispatch_lock (core_server.py:129-150) — the panes stay live regardless.",
"laneconc under concurrent lanes: frozen by its own barrier/single-writer tests (C7).",
"Two channels + one stateless model endpoint: no server-side coupling exists to break (Ollama /api/chat schema carries the whole conversation per request)."
]
}
@@ -0,0 +1,64 @@
{
"stage": "SYNTHESIS",
"summary": "Two turn lanes over one shared model, with honesty at every level. The core admits one running turn PER CHANNEL (chat lane + task lane workers), all shared operator state becomes per-channel and locked at mutation scope, saves become atomic, and context assembly becomes channel-keyed. The task pane gets the D36 queue (pending indicator, bounded reordering over pending entries only). The chat pane NEVER queues behind task work at rune: at worst its model call waits behind the task turn's CURRENT single call in Ollama's FIFO (default), and the owner can eliminate even that with OD-1 (parallel slots or a second resident model). The consent bridge enqueues with an immutable context snapshot. Nothing interrupts a running turn.",
"the_architecture": {
"layer_1_face_TUI": {
"owns": "Input routing (D36 focus law), the task queue's visible rows, pending indicators, per-lane busy spinners.",
"changes": "Composer task-enter during a running task turn -> enqueue row (pending state) instead of a blocked POST; chat-enter always dispatches (chat lane has its own admission; if a chat turn is itself running, the new chat message queues per OD-4). Queue rows render queued/running/awaiting_approval/completed from the existing per-channel read paths (lock-free).",
"blocking_points_removed": ["BP-4 at the face — honest feedback replaces silent waits"],
"evidence": "Claude Code 0.2.75 queue-when-busy; crush per-session IsBusy; laneconc single-writer commits unchanged."
},
"layer_2_core": {
"owns": "Turn admission, shared-state safety, persistence atomicity, context assembly.",
"changes": [
"Replace dispatch_lock with per-channel single-flight admission: one running turn per channel; same-channel arrivals wait on that channel's turn (chat: message queue; task: D36 queue). Two channels run CONCURRENTLY through the operator.",
"state_lock (fine-grained) around ONLY: state.tasks.append, per-channel current-task map (or retire the pointer — repo is canonical), per-channel pending-approval slots.",
"_save(): tmp file + os.replace under the state_lock; synchronous at running/terminal transitions, debounced for intermediates (lifecycle-guarantee law preserved).",
"_conversation_messages becomes channel-parameterized: window(channel) — each stream's history block is its own transcript (fixes BP-5).",
"Approval resolution endpoint keyed by channel/approval-id (frozen-battery update)."
],
"blocking_points_removed": ["BP-1", "BP-2", "BP-3", "BP-5"],
"evidence": "RQ worker pattern (one job at a time per worker; ordered priority queues; started/finished registries); F6's own promised 'finer-grained locking' later wave; Claude Code v1.0.45 atomic-writes fix."
},
"layer_3_model": {
"owns": "What 'concurrent turns' physically mean at the GPU.",
"default": "One Ollama server, FIFO queue per model call (OLLAMA_NUM_PARALLEL=1, MAX_QUEUE 512). Both lanes' calls interleave at call granularity; chat never blocked by a whole turn — only by the current single call.",
"owner_upgrades_OD1": [
"C-1 OLLAMA_NUM_PARALLEL=2: simultaneous decode on one model; ~2x KV cache; verify engine/arch support (GGUF yes; MLX no; MoE blocklist — issues #17280/#8842).",
"C-2 two pinned models: chat on a fast small model (separate runner, own queue — never waits on the big model), tasks on the big model; keep_alive=-1 both; costs second model's VRAM. Maps onto the existing T1/T3 tier system."
],
"blocking_points_removed": ["BR-8's residual wait, if the owner chooses an upgrade"]
},
"layer_4_consent_bridge": {
"owns": "Chat-to-task promotion with context carry (D36 point 2).",
"changes": "Detection runs on the chat lane's response metadata (cheap classifier, no extra model call). Consent card resolves -> ENQUEUE to the task lane (queue append under queue lock; never touches a running turn). Context carry = immutable snapshot (chat turn id + rendered context block captured at proposal). The task worker consumes it as a normal queued turn.",
"blocking_points_removed": ["BR-7 race by construction"]
}
},
"what_intelligent_ordering_actually_is": {
"policy": "Bounded priority classes over PENDING entries only: (1) consent-bridge pushes from chat, (2) owner-flagged work, (3) plain FIFO. Amend chains bind to their parent task and move atomically (identity law #18 untouched). A running turn is NEVER preempted — the owner's 'without interrupting the turn' is a hard invariant, not a preference.",
"explicitly_not": "No predictive reordering, no cost-model scheduler, no preemption. RQ's ordered-queue-list semantics are the ceiling of complexity needed for two panes.",
"owner_knob": "OD-2 decides the visible queue affordances (indicator-only vs reorderable list)."
},
"phases": [
{"phase": 0, "name": "State safety (prerequisite)", "work": "state_lock + atomic os.replace saves + per-channel current-task/pending-approval keying", "tests": "concurrent-dispatch fault injection (two threads, full send_task on stub transport, checkpoint round-trip intact); amend-chain integrity under interleave"},
{"phase": 1, "name": "The lanes", "work": "Per-channel single-flight admission in core_server; remove the global dispatch_lock; channel-keyed context assembly (window(channel))", "tests": "frozen: chat POST completes while a stub task turn sleeps; task POST same; canary battery green with the new admission shape"},
{"phase": 2, "name": "The D36 face", "work": "TUI task queue rows + pending indicator + enter-to-queue when the task lane is busy; chat message queueing policy (OD-4)", "tests": "frozen: enter during running task turn -> queued row, chat pane still live; reordering operates on pending only"},
{"phase": 3, "name": "Consent bridge", "work": "Chat-lane detection + consent card + immutable-snapshot enqueue with context carry", "tests": "frozen: consent resolving during a running task turn enqueues without touching the running turn's state"},
{"phase": 4, "name": "Model-slot policy (owner)", "work": "OD-1 config: NUM_PARALLEL=2 or two pinned models + keep_alive; document measured VRAM", "tests": "live: chat first-token latency while a 27b task generation runs, before/after"}
],
"owner_decisions": [
{"id": "OD-1", "decision": "Model-slot policy for true simultaneous generation", "options": ["default FIFO (zero cost; chat answer may wait one model call behind a big task call)", "OLLAMA_NUM_PARALLEL=2 (~2x KV cache for the model; engine support must verify)", "chat on small fast model + tasks on big model (second model's VRAM; separate queues; recommended if VRAM allows)", "cloud model for chat (bandwidth/privacy trade)"], "why_owner": "Real VRAM/hardware/taste trade with no code-correct answer"},
{"id": "OD-2", "decision": "Queue affordance depth", "options": ["pending indicator only", "visible reorderable queue list with priority display"], "why_owner": "Face taste under D36's interview-granularity law"},
{"id": "OD-3", "decision": "Checkpoint mechanics", "options": ["keep operator.json tasks array with atomic+debounced saves", "retire the legacy tasks array to the canonical TaskStateRepository (schema change, smaller saves)"], "why_owner": "Durability/compatibility taste; both are safe"},
{"id": "OD-4", "decision": "Chat-lane behavior when a CHAT turn is already running", "options": ["queue the message (Claude Code 0.2.75 behavior)", "steer the running turn (0.2.108 behavior — harder, needs turn-mutation semantics)", "both, phase 2 then later"], "why_owner": "Interaction law for Rune's conversational feel"},
{"id": "OD-5", "decision": "Consent-bridge context carry format", "options": ["verbatim chat tail (cheap, leaky)", "rendered summary block captured at proposal (recommended)"], "why_owner": "Taste in how much chat context tasks should see"}
],
"risk_ranking_after_mitigations": [
"LOW: state safety (P0 pattern is proven; Claude Code precedent)",
"LOW: context isolation (repo already per-channel; assembly keying is small)",
"MEDIUM: core surgery breadth (send_task is long; the fine-grained locking must be fault-injection-tested — but the F6 comment already scoped this wave)",
"MEDIUM/OWNER: BR-8 residual latency under default FIFO (OD-1)",
"MEDIUM/OWNER: VRAM for OD-1 upgrades (measured at build time)"
]
}
@@ -0,0 +1,31 @@
{
"stage": "QUALIFY",
"verdict": "PROCEED",
"confidence": "high",
"rationale": "Every load-bearing fact was verified against live sources or the repo itself: the blocking map is file:line-exact (one mutex, four supporting unsynchronized surfaces, one context-assembly gap); Ollama's concurrency semantics — stateless per-request wire, default per-model FIFO queue, NUM_PARALLEL context-memory multiplication, engine caveats — come from the official FAQ/API spec and three primary GitHub issues; the harness pattern (per-session busy, enter-queues-when-busy, background work outside the conversation loop) comes from crush and the Claude Code changelog; the single-resource worker pattern comes from RQ's docs. The recommended architecture (Direction B) removes every rune-side blocking point with stdlib-only machinery built on surfaces that are already concurrency-ready (laneconc, per-channel repositories, lock-free read paths), and leaves the only genuine hardware trade (BR-8 residual / parallel slots / second model) as a clean, independently-upgradeable owner decision rather than a research unknown.",
"conditions": [
"C-P0: State safety lands FIRST and independently (state_lock + atomic saves + per-channel current-task/pending-approval keying) — removing the dispatch_lock before this would reintroduce exactly the corruption F6 froze",
"C-P1: The lanes wave must land channel-keyed context assembly in the same wave (window(channel)) — otherwise two streams would silently share one history source (BP-5)",
"C-ENGINE: If OD-1 C-1 (OLLAMA_NUM_PARALLEL=2) is chosen, verify the owner's actual serving engine + model family supports parallel requests at build time (MLX engine and some MoE architectures do not — issue #17280)",
"C-FROZEN: Concurrent-dispatch fault-injection tests drive the REAL admission shape (two threads through core_server with a stub transport), not synthetic single-threaded spellings",
"C-LIFECYCLE: Debounced saves keep synchronous persistence at running/terminal transitions — no RUNNING task may be unrecoverable on crash (lifecycle-guarantee law)",
"C-BUDGET: The 330s client budget stays; under default FIFO the chat lane's honest worst case is one model call behind the task lane (surfaced as a pending state, never a silent hang)"
],
"named_unknowns": [],
"owner_decisions_pending": [
"OD-1 model-slot policy (FIFO default vs NUM_PARALLEL=2 vs second pinned chat model vs cloud) — the only decision with hardware cost",
"OD-2 queue affordance depth (indicator-only vs reorderable list)",
"OD-3 checkpoint mechanics (atomic+debounced legacy array vs retiring tasks to the canonical repo)",
"OD-4 chat-lane behavior when a chat turn is already running (queue vs steer)",
"OD-5 consent-bridge context carry format (verbatim tail vs rendered snapshot)"
],
"artifacts_index": [
"01-issue.json — owner directive verbatim, research questions, constraints C1-C7",
"02-inward.json — blocking map BP-1..BP-5 at file:line; what is already concurrency-ready; measured checkpoint sizes",
"03-outward.json — eight live sources (Ollama FAQ/API, issues #8842/#17280/#4894, crush, Claude Code changelog, RQ) with URLs and lessons",
"04-applicability.json — constraint check; directions A-D weighed; prerequisite order P0-P5",
"05-adversarial.json — breaks BR-1..BR-8 with mechanisms/mitigations; verified non-breaks",
"06-synthesis.json — the layered architecture, intelligent-ordering policy, phases 0-4, owner decisions OD-1..OD-5",
"07-qualify.json — this file"
]
}
+66
View File
@@ -0,0 +1,66 @@
# factory-separate-turns — how chat and task panes run separate turns on one model
Research round answering the owner's direct ask (2026-10-18, D36 era): *"we need to not
block the interactivity of being able to talk to the chat pane whenever a task pane task
chat message is sent… the chat and the task panes need to operate on separate turns or
not interrupt each other. please research how to put that in effect."*
**Verdict: PROCEED** (07-qualify.json, high confidence, five named owner decisions).
| Artifact | What it holds |
| --- | --- |
| 01-issue.json | the owner directive verbatim, research questions, constraints C1–C7 |
| 02-inward.json | the exact blocking map (BP-1..BP-5, file:line); what is already concurrency-ready; measured checkpoint sizes |
| 03-outward.json | eight live-verified sources with URLs + per-source lessons |
| 04-applicability.json | constraint check; candidate directions A–D; prerequisite order P0–P5 |
| 05-adversarial.json | eight breaks with mechanisms and mitigations; verified non-breaks |
| 06-synthesis.json | the layered architecture, the ordering policy, phases 0–4, owner decisions OD-1..OD-5 |
| 07-qualify.json | PROCEED, conditions, named unknowns (none blocking) |
## The three findings that matter most
1. **The TUI and the stores are already concurrency-ready; ONE mutex is the whole
problem.** The Go face already dispatches the chat and task lanes in parallel
goroutines with a single-writer commit (`tui/laneconc`), the conversation/task
repositories are already per-channel and internally locked, and the read paths the
panes poll (`GET /v1/conversation`) never take the lock. The blockage is
`with self.core.dispatch_lock:` at `src/rune/core_server.py:199` — it wraps the
ENTIRE turn (model calls + tool loops + persistence, up to the 330s budget) for both
channels. It exists only because the operator's shared state is unsynchronized
(single `current_task` pointer, single approval slot, full-file `_save()` of the
measured-2.2MB checkpoint — the F6 comment itself called this a phase-1 contract with
"finer-grained locking" as a later wave).
2. **Ollama already IS the queue — and is stateless per conversation.** Every `/api/chat`
carries the full message history (no session state server-side), so two streams cannot
cross-contaminate at the model; and with defaults, concurrent calls against one loaded
model are FIFO-queued by Ollama itself (order preserved, never failing each other).
So "never block chat" is achievable in rune with zero hardware cost at the DISPATCH
level — the honest residual is that a chat ANSWER can wait behind the task turn's
CURRENT single model call (one call, not one whole turn). True simultaneous generation
needs `OLLAMA_NUM_PARALLEL=2` (~2× KV-cache memory, engine-dependent) or a second
pinned chat model (separate runner, separate queue) — a pure owner VRAM/taste call.
3. **Every established harness solves this by structure, not by waiting.** crush marks
busy PER SESSION while other sessions stay interactive; Claude Code queues your Enter
presses while a turn runs and runs background work outside the conversation loop with
its own view; RQ's canonical worker pattern is one-job-at-a-time per worker with an
ordered priority queue and honest pending states. Rune's two panes map exactly: two
turn lanes, one shared serial model, a visible D36 task queue that reorders only
PENDING entries and never interrupts a running turn.
## The recommended architecture (06-synthesis)
Four layers: **the face** (task-enter queues with pending rows; chat-enter always
dispatches), **the core** (per-channel single-flight admission replacing the global
mutex; fine-grained state lock; atomic + debounced saves; channel-keyed context
assembly), **the model** (Ollama FIFO by default; OD-1 owner upgrade for true
parallelism), and **the consent bridge** (detection on the chat lane, consent resolves
to an ENQUEUE with an immutable context snapshot — never touching a running turn).
"Intelligent ordering" is deliberately bounded: priority classes over pending entries
only, amend chains atomic, no preemption, no scheduler cleverness.
Five decisions await the owner (06-synthesis): the model-slot policy (OD-1 — the only
one with hardware cost), queue affordance depth (OD-2), checkpoint mechanics (OD-3),
chat-when-chat-is-running behavior (OD-4), and consent-bridge context carry (OD-5).
Sources with URLs are in 03-outward.json. No repo code was changed in this round; the
round is registered in docs/factory/research-registry.json under `factory-separate-turns`.
@@ -0,0 +1,160 @@
# Tabs + Floating Windows — Design-Interview Prep (research-only, no construction)
Owner directive (2026-10-20): the tabs + floating-windows direction for the face is
approved; a design interview happens BEFORE building. This wave prepares that
interview. Three parts: (1) what the stack can do, (2) what the face already has
that becomes tabs/windows naturally, (3) the interview itself, each question with a
recommended default drawn from the research and prior rulings.
---
## 1. What the stack supports (Bubble Tea v2.1.0 + lipgloss v2.0.6, vendored in tui/vendor)
**Tab bars.** Neither bubbletea v2 nor lipgloss v2 ships a tab-bar component.
Precedents are all hand-built (lazygit, k9s): a one-line strip of styled labels
(lipgloss styles for active/inactive) + an index in the model state. Trivial to
build; the cost is state management (which surface is active, how each surface's
scroll/cursor state survives tab switches — the precedent apps keep every tab's
model alive and only swap the visible View). Nothing in the stack fights this.
**Floating windows / compositing.** lipgloss v2 has a real layer system, and this
is exactly what the visual-round already verified:
- `lipgloss.NewLayer(content)` (layer.go:13) — a layer has **X, Y, and Z
coordinates** (`X()`, `Y()`, `Z()` setters, layer.go:57-75), so z-order is a
first-class library feature, not an ad-hoc paint order.
- `NewLayer(base, child1, child2, ...)` composites children over a base;
nesting gives stacking.
- `lipgloss.NewCanvas(w, h)` (canvas.go:24) with `Compose(drawer)` and `Render()`
gives a pixel-accurate compositing surface; `Canvas.Draw` plugs straight into a
bubbletea v2 View via the `uv` screen interface.
- `Place`/overlay helpers remain for the simple "center a card" cases.
This maps 1:1 onto the owner's **Layer Law** (boundary-law.md, D43 addendum,
2026-10-19): every visual element is an independent layer with its own
scaffold/state/tests, composed at render time, accidental coupling is a defect.
The library now literally enforces the owner's law — floating windows ARE layers
with Z, the face already IS layers by ruling.
**Mouse drag/resize.** Bubble tea v2's mouse support covers click, motion, and
**drag** (motion-with-button events, the modern SGR mouse protocol) — lazygit
does drag-to-scroll on the same protocol. Window dragging is therefore feasible:
on `MouseMotion` while button held over a window's border, update the layer's
X/Y, re-render. Terminal-native **resize-by-dragging-an-edge** is doable with
the same events (hit-test the border cells) but has no library precedent — it is
new interaction code, and the fallback (resize via keyboard or preset sizes) is
what every terminal app ships. There is no sub-cell precision; everything is
character-cell granularity, which visually reads as chunky but honest.
**Z-order / focus.** Z is per-layer in the library. Focus is NOT — it's app
state (the face already has focus-driven panes, lanes/README: focus-driven
resize, click resolves focus). The existing discipline (an open modal outranks
the rail; an open takeover swallows every key — rail.go:146) is the seed of a
focus stack: modals > windows > panes, one key-routing authority.
**Verdict:** the stack supports the direction without any vendored fork. The
work is state + interaction design, not rendering mechanics.
## 2. What the face already has that becomes tabs/windows naturally
| Existing surface | Today | Becomes |
|---|---|---|
| **Two panes (chat + tasks)** | lanes/layout.go: two-lane face, focus-driven resize, one-column gutter | The core tab *host*: the dichotomy could stay panes, or chat could become a tab with tasks — see Q7 |
| **Gauges overlay** | gauges.go: "the D41 callable progress window: an overlay panel" — owner already ruled these are "windows to call upon (on-demand overlays), NOT embedded" (D38 §V-C) | The first floating window, by existing ruling |
| **Rail takeover** | rail.go: full-width takeover box, open via click region + palette, exact restore, pure chrome state | A full-size window (or a tab); its open/close is already modeled as state, not semantic mutation |
| **Model picker** | rail.go SetMenu — model-picker menu lines in the chat lane | A small floating window (modal) rather than menu lines |
| **Approval card** | approvals package; modal outranks everything | Highest-Z floating window; keeps its outranking semantics |
| **Hints overlay** ('/'-command hints) | rail.go SetHints, view-only above composer | Ephemeral tooltip-window / lowest Z |
| **Sub-agent window (build)** | factorywave/factory surfaces during build waves | A strong tab candidate — a build run is a long-lived surface you'd switch away from and back to |
| **Event stream** | rail's bounded event view | Tab candidate (vs. today's takeover) |
| **Composer + chat transcript** | nav/composer, chatpane | Stays fixed (see Q3 default) |
Key inherited law: the rail takeover's "pure chrome state" pattern (opening a
surface never touches semantic State) is the correct pattern for every tab and
window: switching surfaces must never mutate the model's semantics.
## 3. THE INTERVIEW — questions for the owner, with recommended defaults
Each question: plain language, why it matters, and the recommended default with
its source. Owner answers become D-rulings (D44+) before any build wave.
**Q1 — What are tabs FOR? (which surfaces become tabs?)**
Why: "tab" implies long-lived, switch-back-and-later surfaces. Not everything
should become one; over-tabbing recreates a browser's 30-tab swamp in a
terminal.
**Default:** tabs for the two long-lived surfaces — the **build/sub-agent
window** and the **event stream** (today's rail takeover) — everything else
stays a pane or a floating window. Source: the takeover already behaves like a
toggleable full surface (rail.go), and a build run is the one surface you
genuinely leave and return to.
**Q2 — What are floating windows FOR? (which overlays become floating?)**
Why: windows that float and drag suit *transient, on-demand* surfaces — the
exact category the owner already ruled on.
**Default:** exactly the overlays the owner already named as "windows to call
upon": the **gauges** (D38 §V-C), the **model picker**, the **approval card**
(highest Z, never draggable away from a decision), the **hints** tooltip. No new
floating surfaces beyond these until asked. Source: D38 ruling + existing modal
precedence in rail.go.
**Q3 — Does the chat itself ever become a tab or window?**
Why: this is the identity question of the face. If chat can be hidden behind a
tab, the "chat is the home of the face" premise weakens.
**Default: NO.** Chat (transcript + composer box per D36) stays fixed and
always visible; tabs and windows orbit it. Source: D36 chat-box law ("stable
BOX at the bottom... never a thin transient line") + the one-composer
dichotomy legibility principle (factory-chat-design-principles I2).
**Q4 — Does tab state persist across runs?**
Why: re-opening rune and finding your build tab still open is a memory feature;
but persisted UI state adds a store surface and failure modes.
**Default: yes, minimal** — remember the set of open tabs and which was active
in a small face-state file, consistent with the markdown-ledger/plain-files
instinct (D38: plain files over databases). Sessions' scroll positions do NOT
persist (v1).
**Q5 — Window size/position memory?**
Why: dragging a gauges window to a corner every session is friction; but
remembered geometry can go off-screen when the terminal is smaller.
**Default: remember per-window size+position, clamped to the current terminal
on restore** (never restore a window partially off-screen). Stored in the same
face-state file as Q4.
**Q6 — Keyboard vs mouse priority?**
Why: the face already mixes both (click regions open the takeover; shift+tab
cycling mirrors JetBrains muscle memory — boundary-law D29 note). Drag-to-move
is mouse-only; but if keyboard can't reach every window action, mouse becomes
mandatory and breaks keyboard-only workflows.
**Default: keyboard parity for every window verb** — open/close/cycle-focus
via keys (and via the palette, the existing JetBrains-style action bar);
**drag is the mouse-only exception** (move/resize), never the only path.
Source: D29 selection law + rail.go click-region precedent.
**Q7 — What happens to the chat+tasks dichotomy if either becomes a tab?**
Why: the two-lane face is a ratified shape (lanes README). If tasks becomes a
tab, the face becomes single-pane-plus-tab, which changes resize/focus
semantics entirely. This is the most expensive question — it decides whether
the tab bar replaces the gutter.
**Default: the dichotomy survives** — chat and tasks remain the two panes; the
tab bar sits ABOVE them for the orbiting surfaces (build, events). Rationale:
the dichotomy is the face's legibility core (chat = conversation, tasks =
work), and making it tabbed merges two things the design principles keep
separate. If the owner wants tasks as a tab instead, that is a ratified
deviation, priced before build.
**Q8 — (guard) What can never be closed?**
Why: closable implies closeable-by-accident. Some surfaces must be
non-closable to keep the face honest.
**Default: the chat pane/composer, and the approval card while a decision is
pending, cannot be closed or covered by a lower-Z window.** Source: D36 chat
law + the existing "modal outranks everything" rule.
---
## Files in this chain
- `00-interview-prep.md` — this document (the deliverable)
- Registry entry `factory-tabs-windows`, verdict PROCEED (no construction
performed; every build pick spawns its own brief).
Precedents cited: lazygit/k9s (hand-built tab bars, same mouse protocol);
boundary-law.md D36, D38 §V-C, D41 addenda, D43 Layer Law; research/factory-visual-round
(lipgloss v2 layer verification); tui/lanes/README.md, tui/rail/rail.go,
tui/gauges/gauges.go.
@@ -0,0 +1,22 @@
{
"stage": "ISSUE",
"feature": "TUI DESIGN PRINCIPLES — the canon behind the owner's live visual iteration (owner ask, verbatim: 'i also want some research on ui design on presenting a tui so we can have some design principles to follow')",
"problem_statement": "The face's visual decisions so far are owner rulings (D34-D43) made live in interviews and against rendered candidates — taste is recorded, but no PRINCIPLE layer exists beneath it. Every future visual wave currently has two bad options: re-ask the owner for each micro-choice, or guess ad-hoc. The owner iterates the face live (D35 §2: build style-switching, not a single look) and needs a canon: named, testable design principles grounded in published TUI/UX research, so each iteration 'flip' is a refinement WITHIN known principles instead of a new unprincipled choice.",
"goal": "A numbered set (8-12) of named, testable design principles for THIS face — each with its external evidence and a concrete application to the current face — that becomes the design-review checklist for future visual waves; plus an adversarial list of principles that sound right but fail in terminals; plus an audit of where D34-D43 already comply or diverge.",
"research_questions": [
"OUTWARD: what is the established TUI design canon (clig.dev, Charm/lipgloss philosophy, k9s skins, btop themes, Solarized color theory)?",
"OUTWARD: which general UI principles transfer to text UIs (Nielsen heuristics, visual hierarchy, progressive disclosure)?",
"OUTWARD: terminal-specific color practice (16/256/truecolor profiles, WCAG contrast floors, dark-background theory, token-based theming)?",
"OUTWARD: when does motion aid comprehension vs distract (NN/g animation research, the 0.1s cause-effect rule, peripheral-motion attention capture)?",
"OUTWARD: chat-app/messenger presentation patterns (NN/g chatbot study) and how they map onto a terminal chat face?",
"INWARD: audit D34-D43 rulings (bubble shape, pink chrome, runic animation, bottom-bar strip, hardening lock) against the canon — comply / diverge / undecided?",
"SYNTHESIS: which principles are ALREADY implied by owner rulings (the canon ratifies them), which would the canon ADD, which would it QUESTION?",
"ADVERSARIAL: which popular principles fail in a terminal (gradients in 256-color, hover in a keyboard UI, messenger-literal bubbles, animation for delight)?"
],
"constraints": [
"Research-only wave: no construction; artifacts + registry entry only",
"The D-rulings are LAW — principles must sit UNDER them (ratify/extend/question, never override); D35 §2 iterative-styling law and the D43 hardening lock bind any future application",
"≥6 live-verified external sources with URLs recorded",
"Plain-language README (the owner reads it)"
]
}
@@ -0,0 +1,30 @@
{
"stage": "INWARD",
"summary": "The face's visual law lives in D34-D43 (docs/architecture/boundary-law.md). Reading them against the canon categories: the owner has ALREADY ruled many things the canon independently supports (bottom-bar strip = minimalist chrome; purple diamond on everything Rune = one consistent identity signature; runic dot-walk capped at 3 glyphs = motion budget; /style switch = token-based theming; ghost placeholder + timestamps = visible state). Three areas are undecided that the principles would decide: (a) a contrast floor for dim/ghost text, (b) an explicit one-alive-element rule (thinking animation and the sub-agent flight animation could overlap), (c) the color-role table (pink/rune, red/error, green/result, greyed/code — stated ad-hoc in D36.3 but never written as a closed palette with fallbacks).",
"surface_state": {
"rulings_source": "docs/architecture/boundary-law.md D34-D43 (+ D41 addenda 1-3, D42 addendum)",
"transcript": "bubble chain — mobile-messenger shape (D35.1), you right / rune left with direction-aware inversion (D43.4), identity by side+color, NO text prefixes in bubbles (D41 add.3), timestamps opposite-side corner, local timezone (D43.2-3)",
"identity": "purple diamond beside ALL rune output (thinking + answers) — her signature mark (D41 add.3); runic glyphs as the animation vocabulary (D41 add.), celtic/viking/cthulian mix, detect-then-render glyph law",
"animation": "thinking dots walk left-to-right, each dot a NEW runic symbol, capped at 3 glyphs, ctrl+t hint before the animation (D43.13); live thinking expansion during streaming (D41 add.2); NEW: sub-agent in-flight animation in the bottom rail (D41); on-demand progress overlays with percentages (D41 V-C)",
"chrome": "solid pink borders same family for all boxes (D43.9); composer always lit pink, selected pane pink / unselected grey outline (D43.10); bottom bar STRIPPED to semver + context-window figures (D43.12); clean info bar above/below composer (D43.11); placeholder 'type in chat' ghosted with blinking cursor (D43.14)",
"color_law": "highlight law: red = errors, green = results, code/files greyed-out but expandable with runic icon, rune marks importance per message (D36.3); bubble colors cyan (you) / green (rune) by side",
"styling_switch": "/style + D35 §2 live trial-and-error rounds; D43 hardening: frozen pins + named territory + owner-first change routing",
"interaction": "both-roads commands (/ + ctrl+p palette, D34.4); enter/shift+enter (D35.3); history recall (D35.4); autocomplete popup (D35.7); thinking clickable whole row (D43.7); ghost-text prediction (D36.5); consent bridge (D36.2)"
},
"audit_against_canon_dimensions": {
"complies_already": [
"chrome minimalism (D43.12 strip, D43.11 clean info bar) — matches Nielsen #8 + clig.dev 'saying (just) enough'",
"one identity signature (purple diamond, D41 add.3) — matches consistency + the chatbot tone-consistency failure mode",
"motion budget thinking (cap 3 glyphs, hint-before-animation, D43.13) — matches NN/g frequency/mechanics rules",
"token-switchable style (/style, D35.2; frozen pins, D43 add.) — matches k9s/btop skin-file theming practice",
"progressive disclosure (collapsed thinking ctrl+t, expandable code, on-demand progress overlays D41 V-C, collapse-to-summary D36.4) — matches Nielsen #8 and clig.dev",
"visible state (runic animation while thinking, queue visibly D37, context window in bar D43.12) — matches Nielsen #1 + clig.dev 'if you change state, tell the user'"
],
"diverges_or_undecided": [
"NO contrast floor anywhere: ghost placeholder, dim timestamps, greyed code are 'dim' by taste — WCAG 4.5:1 is never named; and the face renders on the USER's terminal theme (uncontrolled background), which the canon (k9s 'default' transparent color; Solarized dual-mode) treats as a first-class constraint",
"NO one-alive-element rule: D41 adds a bottom-rail sub-agent flight animation while the runic thinking walk may be running in the chat — two simultaneous animation sites is exactly the peripheral-motion distraction the research warns about",
"NO closed color-role palette: red/green/pink/grey roles are named per-feature (D36.3, D43.9-10) but no single token table with 256/16-color fallbacks exists — the /style switch has nothing canonical to switch BETWEEN yet",
"Messenger-literalism risk: D35.1 says bubble chain 'not a terminal log' — the canon says adopt the messenger MENTAL MODEL (sides, colors, timestamps) but warns against literal GUI imitation in cells (wrapping, no rounded-corner backgrounds); the face already got this mostly right (identity by side+color, no prefixes) but the principle should be named so future waves don't over-literalize"
]
}
}
@@ -0,0 +1,103 @@
{
"stage": "OUTWARD",
"summary": "Nine live-verified external sources (verified 2026-10-20 by direct fetch). The canon converges on: content-first minimalism, color-as-semantics with a closed palette, token-based theming with graceful degradation, motion only when it carries meaning (and never in non-interactive output), recognition over recall, and the messenger model as a MENTAL MODEL not a literal GUI.",
"sources": [
{
"name": "Command Line Interface Guidelines (clig.dev)",
"urls": ["https://clig.dev"],
"verified": "2026-10-20, live fetch (full text)",
"findings": [
"'Saying (just) enough': information IS the interface; too little looks broken, too much drowns what matters. Directly governs chrome budget.",
"'Use color with intention… if everything is a different color, the color means nothing' — color is a semantic channel, rationed.",
"'If stdout is not an interactive terminal, don't display any animations' + NO_COLOR/TERM=dumb/TTY degradation law — animation and color must degrade silently.",
"Robustness must FEEL robust: keep the user informed, no scary stack traces — the psychology of a calm face.",
"'Increase information density — with ASCII art' (ls permissions example): progressive disclosure done in-text; dense when you learn to read it."
]
},
{
"name": "Nielsen Norman Group — 10 Usability Heuristics",
"urls": ["https://www.nngroup.com/articles/ten-usability-heuristics/"],
"verified": "2026-10-20, live fetch",
"findings": [
"#1 Visibility of system status — always informed, feedback fast. The bottom-bar context figure and the thinking animation are this principle.",
"#4 Consistency & standards (Jakob's Law) — users spend most time in OTHER products; messenger/chat conventions carry expectations into our face.",
"#6 Recognition over recall — show, don't make the user remember; supports visible hints (ctrl+t hint) and palettes over memorized flags.",
"#8 Aesthetic & minimalist design — 'every extra unit of information competes with the relevant units and diminishes their relative visibility.' The single strongest citation for chrome-stripping."
]
},
{
"name": "NN/g — Animation for Attention and Comprehension",
"urls": ["https://www.nngroup.com/articles/animation-usability/"],
"verified": "2026-10-20, live fetch",
"findings": [
"Peripheral motion captures attention involuntarily (bottom-up, snake-in-the-grass; Pratt et al. 2010) — anything that slides/moves in the periphery HIJACKS the user; slow fades distract less than position shifts.",
"Animations that aid comprehension must start within 0.1s of the user action (cause-effect continuity).",
"Frequency rule: 'nice the first time, annoying the third' — repeated animations are roadblocks. Governs any per-message animation.",
"Mechanics rule: user-triggered + in-focus → fast and subtle; system-triggered + peripheral → slow, small, or nothing."
]
},
{
"name": "Solarized (Ethan Schoonover) — terminal color theory",
"urls": ["https://ethanschoonover.com/solarized/"],
"verified": "2026-10-20, live fetch",
"findings": [
"Selective contrast: REDUCE brightness contrast (pure white-on-black tires the eye) while RETAINING hue contrast for syntax/roles — the founding theory of dark-background terminal palettes.",
"A 16-color palette (8 monotones + 8 accents) with symmetric CIELAB lightness — a closed palette is a design feature, not a limitation.",
"Background:body-text pairs (base03:base0) vs highlighted pairs (base02:base1) — background/foreground pairs are the unit of design, not lone colors."
]
},
{
"name": "k9s — Skins (YAML token theming)",
"urls": ["https://k9scli.io/topics/skins/"],
"verified": "2026-10-20, live fetch",
"findings": [
"Full TUI styling as a FLAT YAML TOKEN FILE: body/frame/border/crumbs/status/title each fg/bg pairs — the mature form of '/style switching'.",
"The token 'default' = transparent background to PRESERVE the user's terminal session background — respecting the uncontrolled terminal theme is a first-class design decision in mature TUIs.",
"Status colors as semantic tokens (newColor/modifyColor/errorColor/completedColor) — color roles named by MEANING, never by hex."
]
},
{
"name": "Charm lipgloss — the modern Go TUI styling layer",
"urls": ["https://github.com/charmbracelet/lipgloss"],
"verified": "2026-10-20, live fetch",
"findings": [
"Declarative styles (CSS-like), pure value types — styles compose, inherit, and are copy-on-write: the engineering shape of 'style is a token layer'.",
"Automatic color-profile downsampling: every color written once in truecolor, downsampled 256→16→ASCII by detected profile. Design ONCE, degrade automatically.",
"Color utilities (Darken/Lighten/Complementary/Alpha/Blend): derived shades come from the base token, not from new magic numbers.",
"Tab conversion, width/wrap handling, compositing layers — layout is measured in CELLS; terminal whitespace is expensive."
]
},
{
"name": "btop++ — themes, TTY mode, glyph fallback",
"urls": ["https://github.com/aristocratos/btop"],
"verified": "2026-10-20, live fetch (README)",
"findings": [
"Three-tier color reality: truecolor default → 256-color conversion (-lc) → 16-color TTY mode with a SEPARATE TTY theme. A serious TUI designs the 16-color variant deliberately.",
"Glyph reality: braille/block/tty graph-symbol tiers — visual features swap symbols when fonts can't draw them (same shape as our D41 detect-then-render law).",
"Theme files as data + user themes dir + preset layouts — the theme is a user-ownable artifact."
]
},
{
"name": "NN/g — The User Experience of Chatbots (messenger-pattern research)",
"urls": ["https://www.nngroup.com/articles/chatbots/"],
"verified": "2026-10-20, live fetch",
"findings": [
"Users tap/click almost ANY nontext element a bot renders — affordances must be real: if it looks clickable it must be (and in a keyboard UI, must also have a key).",
"Both input roads matter: free text AND selectable options — ratifies D34.4's 'one vocabulary, two entries'.",
"Tone/persona must be CONSISTENT across the whole conversation (the 'Flo from Progressive' failure: playful start, flat middle breaks the persona).",
"Own failures honestly, offer an escape hatch — error bubbles should say what happened and the way out."
]
},
{
"name": "WCAG 2.1 SC 1.4.3 Contrast (Minimum) — the contrast floor",
"urls": ["https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"],
"verified": "2026-10-20, live fetch",
"findings": [
"4.5:1 minimum for body text, 3:1 for large text — luminance-based, hue-independent (works for color-blind users). The objective floor under any 'dim' styling.",
"Placeholder text and focus indicators are NOT exempt — our ghost placeholder and outlines are in scope.",
"Contrast is computed on the SPECIFIED background pair — since our background is the user's terminal, we control only our token pairs; hence the k9s-style 'default'/transparent background discipline and testing pairs against OUR declared background tokens."
]
}
],
"source_count": 9
}
@@ -0,0 +1,14 @@
{
"stage": "APPLICABILITY",
"summary": "All nine sources apply. Mapping each canon finding onto the face's concrete surfaces: transcript bubbles, thinking animation, chrome bars, composer, /style theming, error/research/system roles. Three application areas are NEW (no ruling exists): contrast floor, one-alive-element budget, closed color-role palette with degradation tiers.",
"mapping": [
{"surface": "transcript bubbles (D35.1, D41 add.3, D43.1-8)", "applies": ["clig.dev color-with-intention", "NN/g chatbots: affordances + identity consistency", "Nielsen #4 (Jakob's Law: messenger conventions)"], "application": "identity stays by side+color+diamond (already ruled); the principle to name: the messenger shape is a MENTAL MODEL — sides, distinct colors, corner timestamps — not literal GUI bubbles; wrapping stays terminal-native."},
{"surface": "thinking animation (D41 addenda, D43.13)", "applies": ["NN/g animation: 0.1s cause-effect, frequency, mechanics", "btop glyph tiers", "D41 detect-then-render law"], "application": "the runic dot-walk is the model citizen: user-triggered, in-focus, capped at 3 glyphs, glyph-checked. The principle to name: motion ONLY while waiting, confirming a change, or user-stepped — never decorative."},
{"surface": "bottom rail + info bar + stripped bottom bar (D43.11-12)", "applies": ["Nielsen #8", "clig.dev 'saying just enough'"], "application": "strip ruling is canon-correct; state that IS shown must be load-bearing (context window = capacity planning; semver = support). Any future bar element must pass the test: 'what decision does this figure change?'"},
{"surface": "composer + placeholder + ghost prediction (D36.5, D41 add.2.4-5, D43.14)", "applies": ["WCAG 1.4.3 (placeholders in scope)", "NN/g chatbots (typed input primary)"], "application": "ghost text must clear the contrast floor while still reading as 'ghost' — solve via the token pair (ghost-fg vs declared composer bg), not via hope."},
{"surface": "/style switch + frozen pins (D35.2, D43 add.)", "applies": ["k9s skins", "btop themes", "lipgloss token styles"], "application": "the canonical target: one flat token file (like k9s) — fg/bg PAIRS per role, 'default' transparent-background option, 256/16 fallback tiers per token. Frozen pins pin TOKEN OUTPUT, not scattered literals, so /style variants stay pin-compatible."},
{"surface": "highlight law (D36.3) + error/research/system roles", "applies": ["k9s status tokens", "Solarized accents", "clig.dev"], "application": "promote D36.3 into a closed color-role table: red=error, green=result, greyed+expandable=code/files, pink=chrome/identity, cyan=you, plus rune's importance mark. New roles require a ruling; every role gets a 256 and 16 fallback."},
{"surface": "NEW — nothing ruled yet", "applies": ["WCAG contrast floor", "NN/g peripheral-motion budget", "k9s/btop degradation tiers"], "application": "three named gaps the principles decide: (1) P-CONTRAST: every text/background token pair ≥4.5:1 on our declared background; (2) P-ALIVE: at most one animation site live at once (thinking walk vs rail flight animation must never overlap; rail animates only when thinking is quiet or outside foveal focus is empty); (3) P-TOKENS: no color/border literal outside the token table."}
],
"principles_groundwork": "The SYNTHESIS stage converts this mapping into 12 named principles with evidence citations and per-principle test criteria."
}
@@ -0,0 +1,64 @@
{
"stage": "ADVERSARIAL",
"summary": "Principles that sound right but FAIL in terminals — each killed or qualified. These are the traps the checklist exists to catch.",
"traps": [
{
"trap": "Gradients and smooth color blends",
"sounds_right": "Web-style gradients make chrome feel premium.",
"fails_because": "256-color terminals collapse a gradient into the 6x6x6 cube's nearest steps (banding); 16-color TTY collapses it to noise; the user's terminal background is uncontrolled so a blend designed against one background lands on another.",
"ruling": "REJECTED as a principle; lipgloss offers Blend1D/2D but usage is confined to deliberate, profile-aware accents if ever. The face keeps flat token pairs. P5/P6 govern."
},
{
"trap": "Hover affordances",
"sounds_right": "Show hints/styles on mouse-over like a GUI.",
"fails_because": "This is a keyboard-first face; TUI hover is unreliable-to-absent across terminal emulators, and mouse support (where present) is secondary. Designing interactions that require hover makes them undiscoverable.",
"ruling": "REJECTED. P9: every affordance has a visible default state and a key equivalent. Click regions exist (D43.7) but never as the ONLY road."
},
{
"trap": "Messenger-literal bubbles",
"sounds_right": "D35.1 says 'mobile text-message chain' — so copy the pixels: rounded bubble fills, shadows, avatars.",
"fails_because": "Terminals have no rounded-corner fills, no shadows, no per-character DPI; long-message wrapping shreds literal bubble shapes; fixed-width cells break proportional layouts. GUI imitation renders worse than the honest terminal-native version.",
"ruling": "QUALIFIED. P12: keep the MODEL (side, color, timestamp, queue states), render it with solid borders + alignment (as already landed, D43.9)."
},
{
"trap": "Animation for delight",
"sounds_right": "The owner loves the runic animations — add more, everywhere, for personality.",
"fails_because": "NN/g frequency rule: repeated animations become roadblocks; peripheral motion hijacks attention involuntarily; terminal repaints cost frames (flicker risk in non-sync terminals).",
"ruling": "QUALIFIED. P3/P4: the delight budget is ONE alive element, motion only with a verb. Personality lives in the GLYPH VOCABULARY (which glyphs dance) not in MORE dancing."
},
{
"trap": "Design only for truecolor",
"sounds_right": "Modern terminals are truecolor; target 24-bit and move on.",
"fails_because": "btop ships three deliberate tiers because real TTYs, ssh-over-dumb-terms, and CI captures still exist; a 16-color fallback designed never looks designed — it looks broken.",
"ruling": "REJECTED. P5: every theme declares 256/16 fallbacks; the 16-color variant is designed, tested, pinned."
},
{
"trap": "Assume a dark background",
"sounds_right": "Terminal users all run dark themes; tune pink/green against near-black.",
"fails_because": "Light terminals exist (Solarized-light, k9s ships white-background skins); our colors are drawn ON the user's background, so untested pairs invert into unreadable.",
"ruling": "QUALIFIED. P6: pairs are designed and audited against our declared background token; the 'default'/transparent option (k9s convention) ships with documented assumptions; a light-background variant is a /style theme, not an emergency."
},
{
"trap": "Maximum information density",
"sounds_right": "Terminals are information tools — show everything: seq numbers, runtime libs, statuses (the pre-D43.12 bottom bar).",
"fails_because": "clig.dev's 'saying (just) enough' and Nielsen #8: irrelevant units DIMINISH the relevant ones; the owner already lived this and stripped the bar.",
"ruling": "REJECTED. P1: chrome must change a decision or go. Density belongs in the TRANSCRIPT (P10), behind expansions."
},
{
"trap": "Whitespace like a web page",
"sounds_right": "Generous padding and margins everywhere improve readability.",
"fails_because": "Terminal vertical space is brutally scarce (rows, not pixels); every padding row competes with the transcript the owner wants dense; lipgloss makes padding easy — that's the trap, not the license.",
"ruling": "QUALIFIED. Padding is a token (P7) spent deliberately: bubbles/composer get it (already ruled D41 add.2.5), chrome and transcript rows stay tight."
},
{
"trap": "WCAG compliance as literal obligation",
"sounds_right": "We adopted 4.5:1 — so we're bound to WCAG conformance.",
"fails_because": "WCAG governs specified fg/bg pairs on web content; we don't control the terminal's background or font rendering, so literal conformance is unverifiable on the user's machine.",
"ruling": "QUALIFIED. P6 is our OWN testable adaptation: audit OUR token pairs against OUR declared backgrounds; treat it as an internal floor, not a certification claim."
}
],
"residual_risks": [
"The contrast audit (P6 test) needs a small checker utility — implementation, out of scope for this research wave",
"The one-alive-element rule (P3) intersects the D41 rail-animation ruling: the rail animation's exact arbitration (static-when-thinking-alive vs coexist) is an owner taste call — flagged, not decided"
]
}
@@ -0,0 +1,103 @@
{
"stage": "SYNTHESIS",
"summary": "THE PRINCIPLES — twelve named, testable design principles for the rune face. Each: statement, evidence, concrete application, test. Ratified rulings are marked [ratifies Dxx] — the canon confirms owner taste; new obligations are marked [NEW]. This set is the design-review checklist for every future visual wave.",
"principles": [
{
"id": "P1",
"name": "CONTENT IS THE INTERFACE; CHROME RECEDES",
"statement": "The transcript and work stream are the screen. Every chrome element (bars, borders, hints, logos) must justify itself by changing a decision the user makes; else it goes.",
"evidence": "Nielsen #8 (nngroup.com/articles/ten-usability-heuristics/): every extra unit of information competes with the relevant units; clig.dev 'Saying (just) enough'.",
"application": "[ratifies D43.12, D43.11] The bottom-bar strip and clean info bar are canon-correct. Test for any future bar/border element: 'which user decision does this change?' No answer = delete.",
"test": "screenshot diff: chrome (non-transcript, non-input pixels) share of the frame does not grow across waves without an owner ruling."
},
{
"id": "P2",
"name": "COLOR IS A RATIONED SEMANTIC CHANNEL",
"statement": "Color means something or means nothing. A closed role table (error/red, result/green, code/greyed, you/cyan, rune/green+diamond, chrome/pink) is the ONLY source of color; new roles require a ruling.",
"evidence": "clig.dev 'use color with intention… if everything is a different color, the color means nothing'; k9s status tokens named by meaning; Solarized's closed 16-color palette as a design feature.",
"application": "[extends D36.3, D43.9-10] Promote the ad-hoc roles into one token table (the P10 table). Never introduce a hex literal outside it.",
"test": "grep the TUI source for color literals: all resolve to token names in the table."
},
{
"id": "P3",
"name": "ONE ALIVE ELEMENT AT A TIME",
"statement": "At most one animation site is live at any moment. Peripheral motion captures attention involuntarily — two simultaneous moving elements fight for a resource the user didn't offer.",
"evidence": "NN/g Animation for Attention and Comprehension (nngroup.com/articles/animation-usability/): peripheral movement triggers involuntary bottom-up attention (Pratt et al. 2010).",
"application": "[NEW; sharpens D41] The runic thinking walk (chat) and the sub-agent flight animation (bottom rail) must never run simultaneously — the rail animates only while no thinking stream is live, or renders static-when-alive.",
"test": "state-machine assertion: aliveAnimations.count() ≤ 1 in every reachable state; frozen test walks the concurrent-thinking+subagent state."
},
{
"id": "P4",
"name": "MOTION ONLY WHILE WAITING, CONFIRMING, OR USER-STEPPED",
"statement": "Animation is a verb: it says 'working', 'this changed because of you', or 'you asked to see this'. Never a noun ('look, decoration'). Entrance/decorative animations are banned.",
"evidence": "NN/g: cause-effect animations must begin within 0.1s of the user action; repeated animations become roadblocks ('nice the first time, annoying the third'); mechanics rule (user-triggered→fast/subtle, system-triggered→slow/small/none).",
"application": "[ratifies D41 addenda, D43.13] The dot-walk runs only while spinActive; the live thinking expansion is a state transition (allowed); no message-entry animation, no idle motion, nothing loops when nothing is being waited for.",
"test": "no animation renders when idle (frozen test: idle frame == static frame); every animation's trigger is a wait-state or a user action in code."
},
{
"id": "P5",
"name": "DEGRADE SILENTLY, DETECT FIRST",
"statement": "Every visual feature must have a smaller sibling: truecolor→256→16-color, rich glyphs→box-glyphs→ASCII, animation→static indicator. Detection happens before rendering; failure swaps, never ships garbage.",
"evidence": "lipgloss automatic profile downsampling; btop's three tiers (truecolor/256/TTY with a designed TTY theme) and braille/block/tty glyph tiers; clig.dev NO_COLOR/TERM=dumb/no-animation-off-TTY.",
"application": "[ratifies D41 detect-then-render law] Generalize it: glyph law becomes THE degradation law — color profiles, glyph pools, and animation all detect-then-render. /style themes must declare their 256 and 16 fallbacks.",
"test": "each theme carries declared 256/16 fallback tokens; frozen render tests in all three profile tiers; PTY glyph-check before any new symbol enters an animation."
},
{
"id": "P6",
"name": "THE CONTRAST FLOOR",
"statement": "Any text/background pair we ship clears 4.5:1 luminance contrast (large/bold display text 3:1) computed against OUR declared background token — 'dim' and 'ghost' are designed pairs, not afterthoughts.",
"evidence": "WCAG 2.1 SC 1.4.3 (w3.org/WAI/WCAG21/Understanding/contrast-minimum.html): 4.5:1 body / 3:1 large, luminance-based, placeholders explicitly in scope; Solarized: lower brightness contrast is fine only when PAIRS are designed.",
"application": "[NEW] Ghost placeholder, dim timestamps, greyed code each get a measured ghost-fg/bg pair in the token table. Because the user's terminal sets the real background, pair values are validated against the theme's declared background and the k9s-style 'default' transparent option is offered with a documented assumption.",
"test": "a checked-in contrast audit computes the ratio for every token pair in every theme and fails the suite under 4.5:1."
},
{
"id": "P7",
"name": "STYLE IS A TOKEN LAYER",
"statement": "All visual decisions live in one flat, owner-ownable token structure (the /style payload); renderers consume tokens only. Themes are data, switchable live, pinned by fixtures.",
"evidence": "k9s skins (flat YAML, per-view fg/bg pairs, 'default' background), btop theme files + user themes dir, lipgloss pure-value styles with inheritance — three independent mature TUIs converge on the same shape.",
"application": "[ratifies D35.2, D43 addendum] /style switches whole token sets; the D43 frozen pins pin token OUTPUT (golden renders), so the lock and the live-iteration law stay compatible: changing tokens visibly changes pinned diffs, routed through the owner.",
"test": "no color/border/spacing literal in renderer files outside token imports; new theme = new data file only, zero renderer diffs."
},
{
"id": "P8",
"name": "ONE SIGNATURE, ALWAYS THE SAME PLACE",
"statement": "Rune's identity is exactly one mark — the purple diamond — on every word and thought of hers, in every state, and the runic-glyph vocabulary is reserved for 'Rune at work'. Identity elements never vary placement, and never double as decoration.",
"evidence": "NN/g chatbots: persona/tone consistency failures break the agent's presence mid-conversation (the Flo case); Nielsen #4 internal consistency.",
"application": "[ratifies D41 add.3, D41 add.] Diamond on thinking AND answer bubbles (already ruled); runic glyphs mean loading/working only — a runic glyph anywhere else is a defect, not flavor.",
"test": "golden fixtures: every rune-role render contains the diamond at the pinned position; grep renders for runic glyphs outside loading states fails."
},
{
"id": "P9",
"name": "RECOGNITION OVER RECALL; CLICKABLE MEANS CLICKABLE",
"statement": "Anything the user must remember is a defect: show the affordance where it acts. Anything that looks interactive IS interactive — and in a keyboard UI every clickable region also has a key.",
"evidence": "Nielsen #6 (recognition over recall) and #1 (visible status); NN/g chatbots: users tap any nontext element a bot renders — affordances must be real; clig.dev 'let the user escape'.",
"application": "[ratifies D34.4, D43.7, D43.13] Both-roads commands, the ctrl+t hint before the animation, whole-row clickable thinking — all canon-correct. New rule going forward: no click region without a keybinding pair; no hint hidden behind hover (there is no reliable hover in a keyboard TUI).",
"test": "interaction inventory: every clickable region lists its key equivalent in the frozen tests; every keybinding surfaces in palette or hint text."
},
{
"id": "P10",
"name": "PROGRESSIVE DISCLOSURE SERVES THE TRANSCRIPT",
"statement": "Density lives in the transcript; depth lives behind explicit, visible expansions. Collapse by default what is not being read NOW; expansions must be discoverable (P9), not secret.",
"evidence": "clig.dev density-through-structure (ls permissions example); Nielsen #8; NN/g timing guidelines for exposing hidden content (same research family).",
"application": "[ratifies D36.3/4, D41 V-C, D35.6] Collapsed thinking, expandable code blocks, on-demand percentage overlays, tasks collapse-to-summary — all canon-correct. The expansions must render their affordance (glyph icon / hint), never rely on memorized keys alone.",
"test": "default (nothing expanded) render shows zero hidden-configuration warnings; each collapsible class has a visible affordance in its collapsed state."
},
{
"id": "P11",
"name": "TELL, BRIEFLY AND LOCALLY",
"statement": "State changes are reported at the point of action, in one line, in the affected region — never in a distant status pile, never silently.",
"evidence": "Nielsen #1 visibility of system status; clig.dev 'if you change state, tell the user' (git push example) and error guidance (plain language, what happened, way out — echoed by NN/g chatbots 'own failures, offer an escape hatch').",
"application": "[ratifies D43.12 rationale, D37 queue-visibly] Removing status=RUNNING was correct BECAUSE the animation proves running locally (P4). Error bubbles: plain language + the escape hatch (what to run/press next). Context-window figure stays: it changes capacity decisions.",
"test": "for each state change (queued, running, done, error) exactly one local indicator exists; error bubbles contain an actionable next step."
},
{
"id": "P12",
"name": "THE MESSENGER MODEL, NOT THE MESSENGER PIXELS",
"statement": "Adopt messenger MENTAL MODELS — sides, distinct identities, corner timestamps, queue visibility — but express them in terminal-native material: color + alignment + borders in cells. Never imitate GUI pixels (rounded bubble fills, shadows, hover) the terminal cannot honestly render.",
"evidence": "NN/g chatbots (the studied patterns are sides/colors/timestamps/buttons, which is why users tap everything); Jakob's Law (Nielsen #4): expectations come from chat apps; terminal reality from lipgloss/btop: layout is cells, fonts are the terminal's.",
"application": "[ratifies D35.1, D41 add.3, D43.4, D41 addendum fonts law] The bubble = side + color + solid border + corner timestamp, already correctly implemented without fake rounded fills. Future waves extend the MODEL (queue visibility, delivery states) not the pixels.",
"test": "design-review question on every visual proposal: 'is this a model concept or GUI imitation?' — imitation requires the owner to overrule the principle explicitly."
}
],
"checklist_form": "Design-review checklist (per visual wave): P1 justify chrome · P2 token-only color · P3 ≤1 alive element · P4 motion has a verb · P5 degradation declared · P6 contrast audited · P7 tokens not literals · P8 diamond+glyph discipline · P9 affordance+key pair · P10 collapsibles show affordances · P11 one local state line · P12 model-not-pixels."
}
@@ -0,0 +1,26 @@
{
"stage": "QUALIFY",
"verdict": "PROCEED",
"confidence": "high",
"rationale": "Research-only wave: the deliverable is the principle set itself, and it stands on nine live-verified sources spanning the TUI canon (clig.dev, lipgloss, k9s, btop, Solarized), the general-UX layer (Nielsen heuristics, NN/g animation and chatbot research), and the accessibility floor (WCAG 1.4.3). The twelve principles ratify the owner's D34-D43 taste in ten places, add three new testable obligations (contrast floor, one-alive-element budget, token-layer discipline), and kill nine popular-but-terminal-fatal traps. No construction is authorized or needed; the principle set + checklist is immediately usable as the design-review gate for future visual waves, under the D35 §2 iterative-styling law and the D43 hardening lock.",
"conditions": [
"Principles sit UNDER the D-rulings: any conflict resolves in favor of an explicit owner ruling (the D-rulings are law; the principles are the canon beneath future rulings)",
"P3 (one alive element) touches the D41 rail-animation ruling — the arbitration detail (rail animates only while thinking is quiet, or renders static-while-alive) is an owner taste call to settle in the next visual round",
"P6's contrast audit utility and P7's token-table consolidation are IMPLEMENTATION waves — they should be spawned with their own briefs before further visual rounds rely on them",
"Principle updates route through the owner like any ruling (D43 hardening law: never locked against the owner)"
],
"named_unknowns": [
"Which glyph pool entries actually render on the owner's current terminal — already governed by the D41 detect-then-render law; no new research needed",
"Light-background theme viability (an adversarial qualification, not a blocker — a /style theme proves it when the owner wants one)"
],
"artifacts_index": [
"01-issue.json — the ask, problem, research questions, constraints",
"02-inward.json — D34-D43 surface audit: complies / diverges / undecided",
"03-outward.json — nine live sources with URLs and findings",
"04-applicability.json — canon-to-surface mapping, three named gaps",
"05-adversarial.json — nine terminal-fatal traps, killed or qualified",
"06-synthesis.json — THE TWELVE PRINCIPLES with evidence, application, and tests; checklist form",
"07-qualify.json — this file",
"README.md — plain-language version for the owner"
]
}
@@ -0,0 +1,49 @@
# TUI Design Principles — the canon behind your live styling
**Verdict: PROCEED** (research-only — nothing was built; this is the rulebook for future visual waves).
## What you asked for
"some research on ui design on presenting a tui so we can have some design principles to follow" — so that every future visual choice follows grounded principles instead of ad-hoc taste, while you keep flipping styles live.
## What we did
Read your rulings (D34–D43) against nine live-published sources: the CLI design guidelines (clig.dev), the Nielsen Norman Group's usability heuristics, their animation research, their chatbot study, the Solarized terminal color theory, k9s's skin system, btop's theme system, the Charm/lipgloss styling library, and the WCAG contrast standard.
The headline: **you've already been designing to the canon.** Ten of the twelve principles we wrote down are things you ruled yourself — stripping the bottom bar, the purple diamond on everything Rune, capping the thinking animation at three glyphs, build-style-switching-not-a-single-look. The research ratifies them and turns them into testable rules.
## The twelve principles (the design-review checklist)
1. **Content is the interface; chrome recedes.** The transcript is the screen. Any bar/border/hint must change a decision you make — or it goes. *(Your bottom-bar strip was exactly right.)*
2. **Color is a rationed semantic channel.** Red means error, green means result, pink means Rune's chrome — color never means decoration. If everything is colored, nothing is.
3. **One alive element at a time.** Moving things grab your eye whether you want them to or not — so the thinking animation and the sub-agent flight animation should never run at the same time. *(New rule — this is the one place your rulings could collide with themselves.)*
4. **Motion only while waiting, confirming, or user-stepped.** Animation is a verb: "working", "this changed because of you", "you asked to see this". No decoration loops. Your three-glyph cap and hint-before-animation are this principle already.
5. **Degrade silently, detect first.** Every visual feature has a smaller sibling: truecolor→256→16 colors, fancy glyphs→plain glyphs→ASCII. Check what the terminal can draw BEFORE drawing it. *(Your detect-then-render law, generalized to everything.)*
6. **The contrast floor.** "Dim" and "ghost" text are designed pairs, not afterthoughts: anything we ship clears a 4.5:1 brightness-contrast floor measured against our own declared background. *(New — nothing ruled yet; needs a small audit script.)*
7. **Style is a token layer.** All colors/borders/spacing live in one switchable table — the /style payload — and renderers only consume tokens. Themes become data files, like k9s skins. *(New as a hard rule; your /style switch is where it lands.)*
8. **One signature, always the same place.** The purple diamond is Rune's mark on every word and thought of hers, and runic glyphs mean "Rune is working" — never decoration. *(Your ruling, now with the research behind it: chatbot studies show persona consistency is what makes an agent feel present.)*
9. **Recognition over recall; clickable means clickable.** Anything interactive is visibly interactive AND has a key equivalent. No hover-dependent anything — keyboard UIs don't have reliable hover.
10. **Progressive disclosure serves the transcript.** Density in the conversation; depth behind visible expansions (collapsed thinking, expandable code, on-demand progress overlays — all as you ruled).
11. **Tell, briefly and locally.** State changes get one line at the point of action. Removing status=RUNNING was correct *because* the animation proves it locally.
12. **The messenger model, not the messenger pixels.** Keep what chat apps teach — sides, distinct colors, corner timestamps, visible queues — but build it with terminal material. Never imitate GUI pixels (rounded bubble fills, shadows, hover) the terminal can't honestly render.
## Things that sound right but fail in terminals (the traps)
- **Gradients** — band into garbage at 256 colors, noise at 16. Keep flat pairs.
- **Hover** — unreliable to nonexistent; every affordance needs a key.
- **Messenger-literal bubbles** — wrapping and fixed cells shred fake rounded shapes.
- **More animation for personality** — personality lives in *which glyphs* dance, not in *more dancing*.
- **Designing only for truecolor / dark backgrounds** — the 16-color variant and the light theme must be designed, not inherited broken.
- **Maximum density / web-style whitespace** — both extremes lose; density in the transcript, padding where you ruled it (composer).
## What this changes going forward
- Every visual wave gets reviewed against the twelve-point checklist (the checklist is in `06-synthesis.json`).
- Two small build waves are implied by the principles, when you want them: (a) the color-token table + /style themes as data, (b) the contrast-audit script.
- One taste call is yours to make in the next visual round: when Rune is thinking AND a sub-agent is flying, which one animates (or the rail goes static-while-alive).
## Sources (all verified live, 2026-10-20)
clig.dev · nngroup.com/articles/ten-usability-heuristics/ · nngroup.com/articles/animation-usability/ · nngroup.com/articles/chatbots/ · ethanschoonover.com/solarized/ · k9scli.io/topics/skins/ · github.com/charmbracelet/lipgloss · github.com/aristocratos/btop · w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
Full evidence with per-source findings: `03-outward.json`. Principles with tests: `06-synthesis.json`. Traps: `05-adversarial.json`.
@@ -0,0 +1,85 @@
# Images in a TUI — capability map (research only)
Question: *can images be submitted into a TUI, and can we view images in a TUI, or is this GUI territory?*
Short answer: **viewing images in a TUI is real and works today on your machine. Submitting them is a terminal-level UX (paste paths / drag-drop / file pickers), not a rendering problem. Analyzing them depends on the model + the wire format, which the APIs support but rune's chat-only provider layer does not yet carry.**
## 1. INWARD — what the vendored stack has
Examined `tui/vendor/charm.land/` (bubbletea v2, lipgloss v2, bubbles) and `tui/vendor/github.com/charmbracelet/x/ansi` v0.11.9.
| Component | Image capability |
|---|---|
| Bubble Tea v2 (cursed_renderer.go) | **None.** Only Kitty *keyboard* protocol + bracketed paste. No graphics rendering, no image management, no resize/redraw of images. |
| `tea.RawMsg` / `tea.Raw()` (raw.go:13-37) | Official escape hatch: emit raw terminal bytes, bypassing the renderer. Doc comment itself demos Sixel detection via PrimaryDeviceAttributes (attr `4` = Sixel support). |
| `x/ansi` v0.11.9 — graphics.go | **Ships both protocol encoders**: `SixelGraphics(p1,p2,p3,payload)` (graphics.go:24, DCS sixel) and `KittyGraphics(payload, opts...)` (graphics.go:52, APC `_G` base64). These are low-level string builders — you handle placement, redraw-on-resize, deletion, and layout yourself. |
| Lipgloss v2 | No image support. `image/color`/`image` imports are only for layout math (layer.go). Lipgloss composes *text* cells; images are outside its cell grid. |
| colorprofile (indirect dep) | Color/attribute detection only, not graphics. |
**Conclusion:** the primitives exist in the vendored tree; there is no managed image component. Bubble Tea's hard limitation is the *cell renderer*: the renderer redraws whole frames of text and will overwrite/misplace images unless you carefully interleave raw sequences, which is exactly why Charm hasn't shipped first-class images.
## 2. Protocol landscape + terminal support
- **Kitty graphics protocol** (sw.kovidgoyal.net/kitty/graphics-protocol/): APC escape `\x1b_G<opts>;<base64>\x1b\`. PNG or raw RGB/RGBA payloads, chunked, image IDs, placement control, unicode placeholders for layout. The modern, best-featured protocol.
- **Sixel** (DEC VT340 heritage; Wikipedia): DCS escape, 6-pixel strips, RLE, ≤256 color registers. Older but the widest-spread legacy support (foot, xterm `-ti vt340`, Windows Terminal ≥1.22, st-patched, Black Box, tmux only if built `--enable-sixel`).
- **iTerm2 inline images** (`\x1b]1337;File=...`): supported by iTerm2, WezTerm, VS Code terminal, Warp, Tabby. Not Konsole.
- **OSC 8 is NOT images** — it's clickable hyperlinks. Don't confuse the two.
### Terminal support matrix (from yazi-rs docs + timg README, cross-checked)
| Terminal | Protocol |
|---|---|
| kitty ≥0.28, Ghostty | Kitty (unicode placeholders) |
| **Konsole** | **Kitty graphics ("old" pre-placeholder variant) — built-in, no config needed in current releases** |
| foot, Windows Terminal ≥1.22, st-patched, Black Box | Sixel |
| iTerm2, WezTerm, VS Code (setting: Enable Images), Warp, Tabby | iTerm2 inline |
| Alacritty, GNOME Terminal, default tmux | none → degrade path |
### Owner's terminal: Konsole 26.08.2 (verified locally with `konsole --version`)
- Supports the **Kitty graphics protocol** (yazi ships a dedicated "Konsole / Kitty old protocol" driver; timg explicitly names Konsole among hi-res-capable terminals). Sixel also worked in Konsole historically.
- **No config needed** on this version — the old `experimentalSixel` toggle era is gone; graphics work out of the box.
- Caveat: Konsole's KGP is the "old" variant — no unicode-placeholder mode, so in-TUI placement is cursor-position based (move cursor, emit image). Avoid newer placeholder-based features.
- tmux/Zellij in between: tmux filters escape codes (kitty-terminal-only workaround); Zellij's KGP/Sixel passthrough is buggy. Best results: run the TUI directly under Konsole, no multiplexer.
## 3. OUTWARD live sources (verified)
1. Kitty graphics protocol spec — https://sw.kovidgoyal.net/kitty/graphics-protocol/
2. Sixel standard + lineage — https://en.wikipedia.org/wiki/Sixel
3. Yazi image-preview support matrix (terminals × protocols, incl. Konsole=KGP-old) — https://yazi-rs.github.io/docs/image-preview
4. timg (terminal image/video viewer; names Konsole; documents full degrade ladder: kitty/iterm2/sixel → quarter → half blocks) — https://github.com/hzeller/timg
5. Konsole release page (version context) — https://konsole.kde.org/changelog.html
6. Vendored Charm stack inspection: tui/vendor/charm.land/bubbletea/v2/raw.go, tui/vendor/github.com/charmbracelet/x/ansi/graphics.go
## 4. TUI apps already shipping inline images (existence proof)
- **yazi** — file manager with native image preview across all protocols above.
- **timg / chafa** — CLI viewers rendering images, video, PDFs inline.
- **fzf / vifm** integrate timg/chafa in preview panes.
This is mainstream, not exotic.
## 5. SYNTHESIS for the owner's Konsole
### View: YES
- **Easiest now, zero code:** shell out to `timg` (or `chafa`) from the TUI — pause the Bubble Tea program, emit, restore. Full-res Kitty-protocol images in Konsole 26.08.2. Works today.
- **In-TUI (nicer):** detect KGP support (query `\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;\x1b\` and read the response), decode the image to PNG/RGBA, emit `ansi.KittyGraphics(...)` through `tea.Raw()` at a cursor position you control, delete with `a=d`. Doable with the already-vendored `x/ansi`; you own resize/redraw/cleanup.
### Submit: YES (paths, not pixels)
- Pasting a file path: bracketed paste is already enabled by Bubble Tea v2 (cursed_renderer.go:140-141).
- Drag-drop: Kitty has a DnD protocol (kitty-only; Konsole doesn't implement it) — for Konsole the practical pattern is a file-picker inside the TUI, or a shell helper (`rune image ./photo.png` → path lands in the composer).
- The payload that travels inward is a **path**; rendering a thumbnail and attaching the file are independent steps.
### Analyze: depends on model + wire, and rune currently carries neither
- Owner's configured models (config): `mimo-v2.5-pro` (openai provider — MiMo's VL line is vision-capable, but this exact model id's vision support should be verified against the provider), `qwen2.5:3b` and `qwen3:14b` (ollama — the plain qwen2.5/qwen3 LLMs are **text-only**; vision needs the `qwen2.5vl:3b`-style tags), and a Qwen3.6-27B GGUF fusion (text-only). Net: **at most one vision-capable model in the roster, and it's unverified.**
- Wire format: both Ollama (`images: [base64]` field on chat messages) and OpenAI-compatible (`content: [{type:"image_url",...}]` parts) **support images natively** — no protocol barrier.
- Barrier is rune itself: the provider layer is documented as a **"Chat-only provider boundary"** (provider_registry.py / ProviderAdapter) and the ContextBroker capsule is text-only — no image content blocks anywhere in src/rune. Supporting images means extending the capsule message schema + both adapters, not fighting the providers.
### Degrade path (terminals without graphics)
- Half/quarter-block Unicode rendering with 24-bit color (timg's `-p half`/`-p quarter`, chafa) — color-accurate, lower spatial resolution. Standard ladder: Kitty → iTerm2 → Sixel → quarter → half blocks.
- Simplest fallback: show a filename + open in the desktop viewer (this is GUI territory only if you insist on in-cell layout).
### Build plan if wanted (ordered, no code here)
1. Capability probe at startup: KGP query → Konsole=yes; else Sixel DA attr 4; else blocks/shell-out.
2. M1 "view": shell out to `timg` on an image path. Ship value with ~zero code.
3. M2 "in-TUI thumbnail": KittyGraphics via tea.Raw at controlled cursor position; delete on redraw. Konsole-only polish.
4. M3 "analyze": extend capsule/provider layer with image content blocks; route to the one verified vision-capable model; degrade with a clear "model can't see" message.
@@ -0,0 +1,374 @@
[
{
"name": "moonshot",
"display_name": "MoonshotAI Kimi",
"provider_type": "openai",
"url": "https://api.moonshot.ai/v1",
"description": "MoonshotAI's Kimi models. Long-context coding.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"long-context",
"coding",
"multilingual"
],
"weaknesses": [
"requires-api-key",
"latency"
]
},
{
"name": "xiaomi",
"display_name": "Xiaomi MiMo",
"provider_type": "openai",
"url": "https://api.xiaomimimo.com/v1",
"description": "Xiaomi's MiMo models. Strong reasoning and coding.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"reasoning",
"coding",
"multilingual",
"long-context"
],
"weaknesses": [
"requires-api-key",
"latency",
"cost"
]
},
{
"name": "openai",
"display_name": "OpenAI",
"provider_type": "openai",
"url": "https://api.openai.com/v1",
"description": "OpenAI's GPT models. Industry standard.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"reasoning",
"coding",
"creative",
"tool-use"
],
"weaknesses": [
"requires-api-key",
"expensive",
"privacy-concerns"
]
},
{
"name": "google",
"display_name": "Google Gemini",
"provider_type": "gemini",
"url": "https://generativelanguage.googleapis.com/v1beta",
"description": "Google's Gemini models. Multimodal with long context.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"multimodal",
"long-context",
"fast"
],
"weaknesses": [
"requires-api-key",
"cost",
"privacy-concerns"
]
},
{
"name": "ollama-cloud",
"display_name": "Ollama Cloud",
"provider_type": "openai",
"url": "https://ollama.com/v1",
"description": "Ollama's hosted models. The cloud leg of the local server.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"open-weights",
"easy-connect",
"familiar-api"
],
"weaknesses": [
"requires-api-key",
"limited-models"
]
},
{
"name": "anthropic",
"display_name": "Anthropic Claude",
"provider_type": "anthropic",
"url": "https://api.anthropic.com",
"description": "Anthropic's Claude models. Strong reasoning and safety.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"reasoning",
"safety",
"long-context",
"analysis"
],
"weaknesses": [
"requires-api-key",
"expensive",
"no-streaming"
]
},
{
"name": "openrouter",
"display_name": "OpenRouter",
"provider_type": "openai",
"url": "https://openrouter.ai/api/v1",
"description": "Multi-provider gateway. Access many models with one API key.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"multi-provider",
"model-variety",
"fallback"
],
"weaknesses": [
"requires-api-key",
"latency",
"complexity"
]
},
{
"name": "groq",
"display_name": "Groq",
"provider_type": "openai",
"url": "https://api.groq.com/openai/v1",
"description": "Ultra-fast inference on Groq hardware.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"fast-inference",
"low-latency",
"openai-compatible"
],
"weaknesses": [
"requires-api-key",
"limited-models",
"no-streaming"
]
},
{
"name": "together",
"display_name": "Together AI",
"provider_type": "openai",
"url": "https://api.together.xyz/v1",
"description": "Cloud inference for open-source models.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"open-source-models",
"fast",
"openai-compatible"
],
"weaknesses": [
"requires-api-key",
"limited-features"
]
},
{
"name": "fireworks",
"display_name": "Fireworks AI",
"provider_type": "openai",
"url": "https://api.fireworks.ai/inference/v1",
"description": "Fast inference for open-source models.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"fast-inference",
"open-source",
"openai-compatible"
],
"weaknesses": [
"requires-api-key",
"limited-models"
]
},
{
"name": "mistral",
"display_name": "Mistral",
"provider_type": "openai",
"url": "https://api.mistral.ai/v1",
"description": "Mistral's efficient open-weight models. EU-based.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "freemium",
"strengths": [
"open-weights",
"efficient",
"eu-hosted"
],
"weaknesses": [
"requires-api-key",
"limited-features"
]
},
{
"name": "deepseek",
"display_name": "DeepSeek",
"provider_type": "openai",
"url": "https://api.deepseek.com/v1",
"description": "DeepSeek's reasoning models. Strong value.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"reasoning",
"coding",
"value"
],
"weaknesses": [
"requires-api-key",
"latency"
]
},
{
"name": "xai",
"display_name": "xAI Grok",
"provider_type": "openai",
"url": "https://api.x.ai/v1",
"description": "xAI's Grok models. Fast reasoning.",
"requires_api_key": true,
"models": [],
"tier": "cloud",
"cost": "paid",
"strengths": [
"reasoning",
"tool-use",
"fast"
],
"weaknesses": [
"requires-api-key",
"cost"
]
},
{
"name": "ollama",
"display_name": "Ollama (Local)",
"provider_type": "ollama",
"url": "http://localhost:11434",
"description": "Local model server. Run models on your own hardware.",
"requires_api_key": false,
"models": [],
"tier": "local",
"cost": "free",
"strengths": [
"privacy",
"no-cost",
"offline",
"customizable"
],
"weaknesses": [
"requires-gpu",
"slower-than-cloud",
"limited-models"
]
},
{
"name": "vllm",
"display_name": "vLLM (Local)",
"provider_type": "openai",
"url": "http://localhost:8000",
"description": "High-performance local model server with OpenAI-compatible API.",
"requires_api_key": false,
"models": [],
"tier": "local",
"cost": "free",
"strengths": [
"fast-inference",
"batching",
"openai-compatible"
],
"weaknesses": [
"requires-gpu",
"complex-setup"
]
},
{
"name": "lmstudio",
"display_name": "LM Studio (Local)",
"provider_type": "openai",
"url": "http://localhost:1234",
"description": "Desktop app for running local models with OpenAI-compatible API.",
"requires_api_key": false,
"models": [],
"tier": "local",
"cost": "free",
"strengths": [
"easy-setup",
"gui",
"model-discovery"
],
"weaknesses": [
"desktop-only",
"limited-api"
]
},
{
"name": "llamacpp",
"display_name": "llama.cpp (Local)",
"provider_type": "openai",
"url": "http://localhost:8080/v1",
"description": "Local llama.cpp server (llama-server) with OpenAI-compatible API.",
"requires_api_key": false,
"models": [],
"tier": "local",
"cost": "free",
"strengths": [
"privacy",
"no-cost",
"offline",
"gguf-models"
],
"weaknesses": [
"requires-gpu",
"manual-setup"
]
},
{
"name": "custom",
"display_name": "Custom Provider",
"provider_type": "custom",
"url": "",
"description": "Escape hatch: connect any endpoint by base URL.",
"requires_api_key": false,
"models": [],
"tier": "hybrid",
"cost": "free",
"strengths": [
"any-endpoint",
"openai-compatible",
"self-hosted"
],
"weaknesses": [
"manual-setup",
"no-metadata"
]
}
]
@@ -0,0 +1,691 @@
"""Retroactive scaffolding admission for the 2026-10-18/19 interactive-session
build waves (the compliance backfill wave).
Drift record: docs/factory/order-of-things.md §WAVE-BRIEF COMPLIANCE LAW
(owner audit 2026-10-19) — those waves were spawned without the
SCAFFOLDING ADMISSION standing requirement; the spawner owns the omission
and this backfill wave registers the omitted contracts. The work itself
already LANDED, so each contract is minted honestly at its landing state:
approval_ref carries the live session directive + research-gate marker,
the contract rides to `verifying` on the landing sha and CLOSES
(the canonical landed-terminal phase — "landed" is not a legal phase in
the scaffold's canonical transition table; closed IS) with the landing
sha(s) + the verification receipts recorded verbatim from the landing
commit bodies. Nothing here re-runs or fabricates verification — the
receipts cite commits and the test/PTY evidence that exists at landing.
Streams (order per the backfill brief):
1. wave-chat-surface-d34-36 — D34/D35/D36 chat surface (58d8ae97;
rode the convergence lineage aa5f1917→e4514523)
2. wave-go-turn-engine-cloud-routing — Go turn engine + cloud routing
(0d99d5a7; the engine first landed in the
convergence tree, the stream completes here)
3. wave-d41-amalgamation — D41 amalgamation (8e1a09e1)
4. wave-task-ledger-d38 — task ledger (ca915c60)
5. wave-convergence-landing — full-width box + scrollback + model split +
approval card (e4514523 master landing,
original landing aa5f1917)
6. fix-history-bubble-landed — history migration + bubble fixes (4347ad05)
7. wave-firstrun-cloud-first — cloud-first wizard (2ba3a437)
Research-gate resolution (each is a chain the wave actually rode or a
declared, quoted exemption — never silent):
1,2,5 → research/factory-chat-presentation|factory-separate-turns (registered)
3 → research/factory-visual-round (PROCEED; registry entry added by
this backfill — the qualify artifact itself anticipated it)
4 → owner-directive exemption quoting boundary-law.md §D38
6 → mechanical-repair exemption (bounded defect repairs on landed surfaces)
7 → research/factory-providers-live (PROCEED; registry entry added by
this backfill), the D42 addendum directive riding as evidence
"""
import json
import os
import subprocess
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
DRIFT_RECORD = (
"docs/factory/order-of-things.md §WAVE-BRIEF COMPLIANCE LAW "
"(owner audit 2026-10-19): interactive-session waves spawned without "
"the SCAFFOLDING ADMISSION standing requirement — recorded as drift; "
"this backfill wave registers the omitted contracts"
)
def _receipts(directive: str, shas: list[str], receipts: list[str],
research_refs: list[str], ext_extra: dict) -> dict:
return {
"session_directive": directive,
"landing_shas": shas,
"verification_receipts": receipts,
"research_refs": research_refs,
"drift_record": DRIFT_RECORD,
**ext_extra,
}
STREAMS = [
# ── 1. D34/D35/D36 chat surface ──────────────────────────────────────
{
"slug": "wave-chat-surface-d34-36",
"directive": "D34/D35/D36 chat surface session rulings 2026-10-18",
"research_slug": "factory-chat-presentation",
"exemption": None,
"objective": (
"D34/D35/D36 chat surface (landed 2026-10-18, commit 58d8ae97; "
"lineage rode the convergence tree aa5f1917→e4514523): stable "
"bordered input BOX (1 row at rest, grows to 3) as its own "
"region outside the selectable pane; backspace fixed at the "
"naming layer (real keys named: backspace/delete/home/end/left/"
"right/pgup/pgdown — the gap inserted invisible DEL chars into "
"sent messages); bracketed paste wired; input history recall "
"(up/down, draft-save, dedup) via Input3; boot history restore "
"(last conversation window, 8 rows) enabled by the serve-child "
"query-string fix (surface.go fetchCore dropped r.URL.RawQuery — "
"every query-carrying GET proxied parameterless; red-first "
"regression test); thinking animated diamond + live thinking "
"bubble expandable via the purple runic glyph; dim HH:MM "
"timestamps; / slash-overlay autocomplete + ctrl+p searchable "
"palette, one vocabulary; model menu rides above the box."
),
"shas": ["58d8ae97"],
"landed_at": "2026-10-18 10:29:39 +0100",
"receipts": [
"commit 58d8ae97 landing receipt: 'All green: 42 TUI packages, "
"rune-entry serial; race-clean'",
"commit 58d8ae97 landing receipt: 'PTY-proven against the live "
"core (typing, wrapping, dispatch, reply, slash overlay, boot "
"restore)'",
],
"research_refs": [
"research/factory-chat-presentation/07-qualify.json",
"docs/architecture/boundary-law.md §D34-D36",
],
"affected": [
"src/go/rune-entry/internal/entry/surface.go",
"src/go/rune-entry/internal/entry/surface_test.go",
"tui/chatpane/chatpane.go",
"tui/chatpane/chatpane_test.go",
"tui/cmd/rune-tui/main.go",
"tui/cmd/rune-tui/main_test.go",
"tui/cmd/rune-tui/provider.go",
"tui/lanes/lanes.go",
"tui/lanes/lanes_test.go",
"tui/lanes/render.go",
"tui/nav/access.go",
"tui/nav/composer.go",
"tui/nav/composer_test.go",
"tui/nav/input3.go",
"tui/nav/palette.go",
"tui/nav/palette_test.go",
"tui/promote/promote.go",
"tui/rail/rail.go",
"tui/semantic/action.go",
"tui/tasks/face.go",
],
},
# ── 2. Go turn engine + cloud routing ────────────────────────────────
{
"slug": "wave-go-turn-engine-cloud-routing",
"directive": "D36/D37/D39 session directives — the two-stream "
"architecture lands in Go (per-channel single-flight) "
"and the engine serves any wire 2026-10-18/19",
"research_slug": "factory-separate-turns",
"exemption": None,
"objective": (
"Go turn engine + cloud routing (stream completes at commit "
"0d99d5a7, 2026-10-19; the engine itself first landed inside "
"the convergence tree e4514523/aa5f1917): per-channel "
"single-flight, channel-keyed context, K25 tail+snapshot store "
"(D40 seam), live Ollama with the thinking split — then three "
"cloud ModelClients ([OI]-compat, anthropic, gemini) with vault "
"key resolution at call time (secret-tool, key value never "
"persisted), per-provider routing from the record's Provider "
"(table + registry, source_base_url preferred), typed errors "
"(503 provider_key_missing on vault failure, 502 provider_error "
"carrying the provider's own envelope). A cloud default works "
"end to end: wizard pick → vault key → the right wire → the "
"reply, all in Go."
),
"shas": ["0d99d5a7"],
"landed_at": "2026-10-19 01:59:30 +0100",
"receipts": [
"commit 0d99d5a7 landing receipt: '67 tests hermetic under -race'",
"commit 0d99d5a7 landing receipt: 'the live token-spending leg "
"gated behind RUNE_CLOUD_PARITY_LIVE=1 for the owner to run "
"explicitly' (not run by this backfill — recorded as gated)",
],
"research_refs": [
"research/factory-separate-turns/07-qualify.json",
"research/factory-checkpoint-saves/07-qualify.json",
"docs/architecture/boundary-law.md §D37.4, §D39, §D40",
],
"affected": [
"src/go/rune-core/internal/runecore/cloud.go",
"src/go/rune-core/internal/runecore/cloud_anthropic.go",
"src/go/rune-core/internal/runecore/cloud_clients_test.go",
"src/go/rune-core/internal/runecore/cloud_engine_test.go",
"src/go/rune-core/internal/runecore/cloud_gemini.go",
"src/go/rune-core/internal/runecore/cloud_live_test.go",
"src/go/rune-core/internal/runecore/cloud_openai.go",
"src/go/rune-core/internal/runecore/engine.go",
"src/go/rune-core/internal/runecore/registry.go",
"src/go/rune-core/internal/runecore/router.go",
"src/go/rune-core/internal/runecore/routing_test.go",
"src/go/rune-core/internal/runecore/server.go",
"src/go/rune-core/internal/runecore/standard_providers.json",
"src/go/rune-core/internal/runecore/vault.go",
"src/go/rune-core/main.go",
],
},
# ── 3. D41 amalgamation ──────────────────────────────────────────────
{
"slug": "wave-d41-amalgamation",
"directive": "D41 visual amalgamation ruling — the recorded pick of "
"the factory-visual-round direction frames (owner, "
"after viewing all three renderings, "
"boundary-law.md §D41: 'Base: V-A Set Line cleanness; "
"FROM V-B runic symbols + animations + pink outline + "
"bubbles + purple diamond + confirm style; FROM V-C "
"gauges as windows') + addenda 2026-10-18/19",
"research_slug": None,
"exemption": (
"owner-directive: the D41 ruling IS the recorded selection the "
"visual round's own phase-2 gate requires ('iterate the seed "
"script until a pick is recorded') — the owner viewed all three "
"rendered direction frames live (research/factory-visual-round/"
"portrayal/) and ruled the composite (boundary-law.md §D41, "
"quoted in the session directive). The round's 07-qualify "
"verdict field carries prose ('PROCEED — portrayal-first: …') "
"that the mechanical gate's exact-match verdict law REFUSES; "
"this admission records that honestly as the owner directive "
"path rather than editing the research artifact or the gate."
),
"objective": (
"D41 amalgamation LIVE — the face's identity (landed 2026-10-19, "
"commit 8e1a09e1; the build wave implementing the "
"factory-visual-round recorded pick, boundary-law.md §D41): "
"rounded chat bubbles (cyan you right / green rune left), dim "
"stamps, pink outline accent, breathing thinking marker; "
"one-cell purple diamond beside rune's markers; dots-walk "
"thinking animation from the instance's SHUFFLED 15-glyph "
"vocabulary (10 futhark + 5 cthulian, owner-verified set); "
"expandable WHILE thinking (click + ctrl+t); /style cycles live; "
"gauges window (honest bars, never fabricated percentages); "
"mode token + live turn duration; task pane sectioned; "
"sub-agent rail cluster animation; reduced-motion support; "
"D41 addendum 3 live: no text prefixes in bubbles."
),
"shas": ["8e1a09e1"],
"landed_at": "2026-10-19 01:58:48 +0100",
"receipts": [
"commit 8e1a09e1 landing receipt: '9 packages green under -race; "
"PTY-proven end to end'",
"commit 8e1a09e1 landing receipt: 'all 15 glyphs byte-verified "
"in raw PTY (detect-then-render law)'",
"commit 8e1a09e1 landing receipt: 'reduced-motion: one static "
"glyph, zero ticks (PTY-proven)'",
],
"research_refs": [
"research/factory-visual-round/07-qualify.json",
"docs/architecture/boundary-law.md §D41 (+ addenda 1-3)",
],
"affected": [
"tui/chatpane/chatpane.go",
"tui/chatpane/chatpane_test.go",
"tui/chatpane/highlight_test.go",
"tui/chatpane/scroll_test.go",
"tui/cmd/rune-tui/historydepth_test.go",
"tui/cmd/rune-tui/main.go",
"tui/cmd/rune-tui/main_test.go",
"tui/cmd/rune-tui/provider.go",
"tui/gauges/gauges.go",
"tui/gauges/gauges_test.go",
"tui/lanes/lanes.go",
"tui/lanes/lanes_test.go",
"tui/lanes/render.go",
"tui/nav/input3.go",
"tui/nav/input3_test.go",
"tui/promote/promote.go",
"tui/rail/rail.go",
"tui/rail/render.go",
"tui/rail/waves.go",
"tui/rail/waves_test.go",
"tui/tasks/face.go",
"tui/tasks/testdata/reorder-mid-flight.v1.txt",
],
},
# ── 4. Task ledger ───────────────────────────────────────────────────
{
"slug": "wave-task-ledger-d38",
"directive": "D38 THE TASK MARKDOWN LEDGER (owner ruling, "
"boundary-law.md §D38, 2026-10-18: 'Every task "
"initiation creates a MARKDOWN FILE — resumable, "
"recorded, with time and date; it is THE SOURCE OF "
"TRUTH for open tasks. Completion: the task is crossed "
"off in the file, then the file RETIRES to a done tasks "
"folder with a date subfolder... Plain files over "
"databases')",
"research_slug": None,
"exemption": (
"owner-directive: the ledger build IS the D38 owner ruling "
"(boundary-law.md §D38, quoted in the session directive above); "
"the ruling deferred only the CHECKPOINT mechanics decision to "
"the dedicated factory-checkpoint-saves research round — the "
"ledger itself was directly ruled, no new research question. "
"Checkpoint-side storage design rode research/"
"factory-checkpoint-saves/07-qualify.json (PROCEED) and the "
"D40 ruling that adopted it."
),
"objective": (
"D38 task markdown ledger (landed 2026-10-18, commit ca915c60): "
"every task initiation creates a markdown file (title, initiated "
"time, brief, append-only log) — resumable, recorded, crash-safe "
"(atomic writes). Completion crosses off the title, flips DONE "
"with a timestamp, and retires the file to "
"tasks/done/<calendar-day>/ (no completions = no folder). "
"/v1/tasks-open serves the ledger as the source-of-truth "
"listing. Wired to reality: creation reconciles from the core's "
"checkpoint at real creation times (exactly one creation path — "
"send_task create); completion hooks both writers (Python "
"lifecycle reconciliation + Go complete command synchronous "
"retire). Ledger is best-effort — the checkpoint stays "
"authoritative; the seam has no zero-value fallback so hermetic "
"tests stay hermetic."
),
"shas": ["ca915c60"],
"landed_at": "2026-10-18 11:44:23 +0100",
"receipts": [
"commit ca915c60 landing receipt: crash-safe atomic writes; "
"hermetic tests stay hermetic via the no-zero-value-fallback "
"seam (frozen tests ride the commit: "
"src/go/task-state/ledger/ledger_test.go, "
"src/go/rune-entry/internal/entry/ledger_wiring_test.go)",
],
"research_refs": [
"docs/architecture/boundary-law.md §D38",
"research/factory-checkpoint-saves/07-qualify.json",
],
"affected": [
"src/go/rune-entry/go.mod",
"src/go/rune-entry/internal/entry/command.go",
"src/go/rune-entry/internal/entry/ledger_wiring_test.go",
"src/go/rune-entry/internal/entry/ledgerview.go",
"src/go/rune-entry/internal/entry/runtime_serve.go",
"src/go/rune-entry/internal/entry/serve.go",
"src/go/rune-entry/internal/entry/surface.go",
"src/go/task-state/go.mod",
"src/go/task-state/ledger/ledger.go",
"src/go/task-state/ledger/ledger_test.go",
],
},
# ── 5. Convergence landing ───────────────────────────────────────────
{
"slug": "wave-convergence-landing",
"directive": "D36 session rulings (full-width shared box, dichotomy) "
"+ D37 model policy + T5 approval card 2026-10-18/19",
"research_slug": "factory-separate-turns",
"exemption": None,
"objective": (
"THE CONVERGENCE LANDING — six waves, one tree (landed "
"2026-10-19, commit e4514523 on master; original landing "
"aa5f1917, same tree re-landed 85s later). This contract "
"admits the four surfaces the backfill brief scopes to this "
"stream (the turn engine rides ADMIT-wave-go-turn-engine-"
"cloud-routing): FULL-WIDTH SHARED BOX (D36, spans both panes, "
"focus-signage placeholder, drafts survive focus round-trips); "
"SCROLLBACK (/history depth persisted, ctrl+up/down + pgup/"
"pgdn, dim ↑N indicator, deeper-page fetch); MODEL SPLIT (D37, "
"pane-aware /model, per-pane record fields, status line; "
"letter-stealing round 2 fixed); APPROVAL CARD (T5, gold y/n "
"through /v1/approvals/resolve, stale revalidation, chained "
"asks). Providers P1 and the retirement rows in the same "
"commit rode their own chains and are scoped to their own "
"surfaces."
),
"shas": ["e4514523", "aa5f1917"],
"landed_at": "2026-10-19 00:02:18 +0100 (master); original "
"aa5f1917 00:00:53 +0100",
"receipts": [
"commit e4514523/aa5f1917 landing receipt (full-width box): "
"'PTY-proven 110+140'",
"commit e4514523/aa5f1917 landing receipt (approval card): "
"'PTY-proven both legs'",
"commit e4514523/aa5f1917 landing receipt (scrollback): "
"'PTY-proven on real history'",
"commit e4514523/aa5f1917 landing receipt (model split): "
"modelpane_test.go rides the commit; 'letter-stealing round 2 "
"fixed (j/k type)'",
],
"research_refs": [
"research/factory-separate-turns/07-qualify.json",
"research/factory-consent-bridge/07-qualify.json",
"research/factory-chat-presentation/07-qualify.json",
"docs/architecture/boundary-law.md §D36, §D37",
],
"affected": [
"tui/approvals/approvals.go",
"tui/approvals/approvals_test.go",
"tui/bridge/bridge.go",
"tui/bridge/bridge_test.go",
"tui/bridge/depth.go",
"tui/bridge/depth_test.go",
"tui/bridge/library.go",
"tui/chatpane/scroll.go",
"tui/chatpane/scroll_test.go",
"tui/cmd/rune-tui/depth.go",
"tui/cmd/rune-tui/historydepth_test.go",
"tui/cmd/rune-tui/main.go",
"tui/cmd/rune-tui/main_test.go",
"tui/cmd/rune-tui/modelpane_test.go",
"tui/cmd/rune-tui/provider.go",
"tui/lanes/lanes.go",
"tui/lanes/lanes_test.go",
"tui/lanes/render.go",
"tui/promote/promote.go",
"tui/promote/render.go",
"tui/rail/rail.go",
"tui/runtime/lib/approval.go",
"tui/runtime/lib/approval_test.go",
],
},
# ── 6. History migration + bubble fixes ──────────────────────────────
{
"slug": "fix-history-bubble-landed",
"directive": "Owner live-use feedback 2026-10-19 (history not "
"restored; thinking in history; user bubbles not "
"full-box) — bounded defect repairs on landed surfaces",
"research_slug": None,
"exemption": (
"mechanical-repair: bounded defect repairs on already-landed, "
"already-researched surfaces — the history-restoration gap "
"(ImportLegacyHotStore one-time boot import of the Python-era "
"store, msg_id-deduped, chronological, crash-resumable), the "
"user-bubble width defect (rune-count width math; byte-length "
"was the defect), and thinking-rides-the-conversation-wire "
"dedupe (claim-law collapses the live one). No new research "
"questions; the underlying surfaces rode "
"research/factory-separate-turns and "
"research/factory-chat-presentation (both PROCEED). Closes "
"Gitea issue #2498 per the issue-ledger law."
),
"objective": (
"History migration + bubble fixes (landed 2026-10-19, commit "
"4347ad05, closes #2498): ImportLegacyHotStore one-time boot "
"import of the Python-era store (~/.rune/hot/"
"conversation_messages.json + K25 tail) into the Go store — "
"msg_id-deduped, chronological, crash-resumable; thinking rides "
"the conversation wire so history renders collapsed thinking "
"bubbles (clickable), claim-law dedupes the live one; user "
"bubbles fixed to the full box the owner expected (rune-count "
"width math, timestamp-aware cap, four rows one alignment, "
"stamp hangs outside the corner); D41 addendum 3 live: no text "
"prefixes; ◆ leads all rune bubbles."
),
"shas": ["4347ad05"],
"landed_at": "2026-10-19 09:28:40 +0100",
"receipts": [
"commit 4347ad05 landing receipt: 'proven in the wild: 116/512 "
"mid-kill, resume completed zero-dupes' (ImportLegacyHotStore "
"crash-resumability)",
"commit 4347ad05 closes Gitea issue #2498 (issue-ledger law)",
],
"research_refs": [
"research/factory-separate-turns/07-qualify.json",
"research/factory-chat-presentation/07-qualify.json",
],
"affected": [
"src/go/rune-core/internal/runecore/migrate.go",
"src/go/rune-core/internal/runecore/migrate_test.go",
"src/go/rune-core/internal/runecore/server_test.go",
"src/go/rune-core/internal/runecore/tailstore.go",
"src/go/rune-core/main.go",
"tui/chatpane/chatpane.go",
"tui/chatpane/chatpane_test.go",
"tui/chatpane/highlight_test.go",
"tui/cmd/rune-tui/main.go",
"tui/cmd/rune-tui/main_test.go",
"tui/runtime/lib/lib.go",
"tui/runtime/lib/lib_test.go",
],
},
# ── 7. Cloud-first wizard ────────────────────────────────────────────
{
"slug": "wave-firstrun-cloud-first",
"directive": "D42 addendum — NO LOCAL DEFAULT; CLOUD IS FIRST-CLASS "
"(owner, boundary-law.md §D42 addendum, 2026-10-18: "
"'i dont want ollama as default. i want to choose to "
"use that. i want the default to be a cloud provider')",
"research_slug": "factory-providers-live",
"exemption": None,
"objective": (
"Cloud-first wizard, D42 addendum (landed 2026-10-19, commit "
"2ba3a437): catalog reordered — vault-keyed clouds lead, "
"keyless clouds, locals after (ollama demoted to a row among "
"equals), custom hatch last; the drift-gated trio byte-proven; "
"wizard starts on NOTHING (cursor −1, no pre-selection); cloud "
"picks walk the vault (SecretToolReader, endpoint+key probe per "
"wire) — a cloud default is a fully supported outcome; engine "
"faithfulness: the record's provider owns the route — a "
"cloud-naming record gets typed 503 provider_unrouted (never "
"the silent localhost misroute); last-chosen unavailable "
"renders the honest 'pick another' state, never a fallback; "
"last-used memory verified (one record, atomic writers, boot "
"loads it, the engine reads it fresh per turn)."
),
"shas": ["2ba3a437"],
"landed_at": "2026-10-19 01:27:19 +0100",
"receipts": [
"commit 2ba3a437 landing receipt: 'the drift-gated trio "
"byte-proven (--check identical, 66 cases)'",
"commit 2ba3a437 landing receipt: 'last-used memory verified: "
"one record, atomic writers, boot loads it, the engine reads "
"it fresh per turn — no divergence path'",
],
"research_refs": [
"research/factory-providers-live/07-qualify.json",
"docs/architecture/boundary-law.md §D42 addendum",
],
"affected": [
"schemas/providers/v1/standard_providers.json",
"src/go/rune-core/internal/runecore/engine.go",
"src/go/rune-core/internal/runecore/engine_test.go",
"src/go/rune-core/internal/runecore/provider.go",
"src/go/rune-core/internal/runecore/server.go",
"src/go/rune-core/internal/runecore/server_test.go",
"src/go/standardized-providers/connect_test.go",
"src/go/standardized-providers/main.go",
"src/go/standardized-providers/truth.json",
"tui/firstrun/cloudprobe.go",
"tui/firstrun/cloudprobe_test.go",
"tui/firstrun/face.go",
"tui/firstrun/firstrun.go",
"tui/firstrun/firstrun_test.go",
"tui/firstrun/registry.go",
"tui/firstrun/render.go",
"tui/firstrun/shell.go",
"tui/firstrun/standard_providers.json",
],
},
]
def run_gate(stream: dict) -> str | None:
"""Pre-mint research gate. Returns the marker (ALLOW or exempt line),
or None when the gate refused (per-stream failure, not fatal)."""
argv = [sys.executable, os.path.join("scripts", "check_research_gate.py"),
"admit", "--slug", stream["slug"], "--objective", stream["objective"]]
if stream.get("research_slug"):
argv += ["--research-slug", stream["research_slug"]]
if stream.get("exemption"):
argv += ["--exemption", stream["exemption"]]
gate = subprocess.run(argv, capture_output=True, text=True, cwd=ROOT)
print(gate.stdout.strip())
if gate.returncode != 0:
print(f"research gate REFUSED for {stream['slug']} — the law holds")
return None
marker = ""
for line in gate.stdout.splitlines():
if line.startswith("research-gate:ALLOW") or \
line.startswith("exempt:research-gate:"):
marker = line.strip()
break
if not marker:
print("no marker in gate output — refusing to mint unrecorded")
return None
return marker
def main() -> int:
failures: list[str] = []
done: list[str] = []
for stream in STREAMS:
slug = stream["slug"]
print(f"\n=== backfill admission: {slug} ===")
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
pre = scaffold.load()
existing = scaffold.contract(pre, f"ADMIT-{slug}")
if existing is not None and existing.get("phase") == "closed":
print(f"already minted + closed — resume skips {slug}")
done.append(slug)
continue
marker = run_gate(stream)
if marker is None:
failures.append(slug)
continue
approval_ref = f"user:{stream['directive']} 2026-10-18/19; {marker}"
evidence_refs = list(stream["research_refs"])
if marker not in evidence_refs:
evidence_refs.insert(0, marker)
if existing is not None:
print(f"contract exists in phase {existing.get('phase')} — "
f"resume without re-mint")
else:
md = cmode.classify_mode(
stream["objective"], target_project="rune",
project_exists=True, affected_surface=stream["affected"],
)
print("classified mode:", md.mode)
need = {
"need_id": f"need-{slug}",
"objective": stream["objective"],
"origin": "worker",
"project": "rune",
"task_ref": slug,
"affected_surface": stream["affected"],
"evidence_refs": evidence_refs,
}
act = cact.activate(md, need=need, scaffold=scaffold, builder=slug)
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=stream["objective"],
builder=slug,
affected_surface=stream["affected"],
evidence_refs=evidence_refs,
origin="worker",
task_ref=slug,
verification_obligations=[
f"receipt at landing ({', '.join(stream['shas'])}; "
f"recorded verbatim from the landing commit body — NOT "
f"re-executed by this backfill): {r}"
for r in stream["receipts"]
],
approval_ref=approval_ref,
contract_id=f"ADMIT-{slug}",
project_store=store,
project_id="rune",
)
if not env.get("success"):
print(json.dumps(env, indent=2, default=str)[:2000])
failures.append(slug)
continue
print("minted:", env["ids"]["contract_id"], "phase:",
env["state"]["phase"])
# pin the gate evidence + the honest landed record on ext
doc = scaffold.load()
c = scaffold.contract(doc, f"ADMIT-{slug}")
ext = c.setdefault("ext", {})
rg = ext.get("research_gate") or {}
chain = stream.get("research_slug") and (
f"research/{stream['research_slug']}/07-qualify.json")
rg.update({
"chain": chain,
"verdict": "PROCEED" if chain else "EXEMPT",
"marker": marker,
"retroactive_backfill": True,
})
ext["research_gate"] = rg
ext["landed"] = {
"phase_label": "landed",
"landing_shas": stream["shas"],
"landed_at": stream["landed_at"],
"session_directive": stream["directive"],
"verification_receipts": stream["receipts"],
"retroactive_backfill": True,
"admitted_by": "scripts/admit_wave_backfill_interactive_2026_10_18.py",
"drift_record": DRIFT_RECORD,
}
scaffold.commit(doc, what=f"backfill landed record for {slug}")
# drive to the canonical landed-terminal state: verifying → closed
from rune import construction_interface as ciface
iface = ciface.ConstructionInterface(
builder=f"backfill:{slug}", project_store=store, project_id="rune")
tr = iface.transition(f"ADMIT-{slug}", "verifying",
evidence_ref=f"landing "
f"{','.join(stream['shas'])}")
if not tr.get("success"):
print(json.dumps(tr, indent=2, default=str)[:1200])
failures.append(slug)
continue
cl = iface.close(
f"ADMIT-{slug}",
observed_evidence=[
{"entity": "project:rune",
"source": "git",
"detail": f"landing commit {s} "
f"({stream['landed_at']}) in master history",
"authority": "git history"}
for s in stream["shas"]
] + [
{"entity": "project:rune",
"source": "tests",
"detail": r[:300],
"authority": "landing commit receipt (recorded, not "
"re-executed by the backfill)"}
for r in stream["receipts"]
],
scope_files=stream["affected"],
)
if not cl.get("success"):
print(json.dumps(cl, indent=2, default=str)[:2000])
failures.append(slug)
continue
print(f"{slug}: CLOSED (landed terminal)")
done.append(slug)
print("\n=== backfill summary ===")
print(f"closed: {len(done)}/{len(STREAMS)} — {', '.join(done)}")
if failures:
print("FAILED:", ", ".join(failures))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,105 @@
"""Admission dispatch for the builder-bridge rune-retarget wave.
classify + activate + admit (the headless scaffolding entry law,
tests/test_construction_admission.py pattern): retarget builder-bridge.ts's
harness-anchored constants (BRIDGE, PY_CANDIDATES) from issue161-harness to
the rune repo, verify the bridge's function against the rune construction
authority, keep the hash-verified install discipline, sweep all hook
constants for other stale harness paths. Red-first on the bridge's rune
behavior. Owner approval 'user:50-scale distribution 2026-10-20'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"retarget config/mimo-hooks/builder-bridge.ts from the stale "
"issue161-harness anchor to the rune repo (src/rune/builder_bridge_cli.py "
"and the rune venv interpreter candidates), following the "
"scaffolding-guard.ts retarget pattern; verify the bridge's function "
"against the rune construction authority; keep the hash-verified install "
"discipline (scripts/install_mimo_hook.py); sweep ALL hook constants for "
"other stale harness paths. Red-first on the bridge's rune behavior."
)
AFFECTED = [
"config/mimo-hooks/builder-bridge.ts",
"scripts/install_mimo_hook.py",
"tests/test_builder_bridge_hook_retarget.py",
"tests/test_hook_parity.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-builder-bridge-rune-retarget",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-builder-bridge-rune-retarget",
"affected_surface": AFFECTED,
"evidence_refs": [
"config/mimo-hooks/builder-bridge.ts",
"config/mimo-hooks/scaffolding-guard.ts",
"scripts/admit_wave_factory_authority_port.py",
"src/rune/builder_bridge_cli.py",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-builder-bridge-rune-retarget")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-builder-bridge-rune-retarget",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-builder-bridge-rune-retarget",
expected_new_entities=[
"tests/test_builder_bridge_hook_retarget.py (red-first retarget corpus)",
],
expected_runtime_effects=[
"builder-bridge.ts BRIDGE resolves to /mnt/workdrive/rune/src/rune/builder_bridge_cli.py",
"builder-bridge.ts PY_CANDIDATES include the rune .test-venv interpreter",
"installed hook at ~/.mimocode/hooks/builder-bridge.ts hash-matches canonical",
"no stale issue161-harness path remains in any mimo-hooks constant",
],
verification_obligations=[
"pytest tests/test_builder_bridge_hook_retarget.py green (red-first)",
"pytest tests/test_builder_bridge.py tests/test_hook_parity.py green",
"builder_bridge_cli.py end-to-end: preflight + construction authority status/check respond with rune contract data",
"python3 scripts/install_mimo_hook.py syncs install; parity hash match verified",
"grep sweep: no issue161-harness reference left in config/mimo-hooks constants",
],
approval_ref="user:50-scale distribution 2026-10-20",
contract_id="ADMIT-wave-builder-bridge-rune-retarget",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+104
View File
@@ -0,0 +1,104 @@
"""Admission dispatch for wave ci-e2e-rig (permanent CI proof rig + workflow script contract).
classify + activate + admit (the headless scaffolding entry law,
tests/test_construction_admission.py pattern): the release deep-dive's
tag-repair proof (temporary second runner + scratch repo) promoted to a
permanent, reusable CI proof rig, plus the three-pipeline workflow
script contract tests. User-dispatch approved.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"promote the release deep-dive's tag-repair proof rig (temporary second "
"runner + scratch repo) into a permanent, reusable CI proof rig: "
"scripts/ci_e2e_rig.sh registers a scratch runner, creates a scratch "
"repo, runs a workflow end-to-end, verifies outputs, and tears down — "
"documented and tested — plus tests/test_workflow_script_contract.py "
"extended to cover the three pipelines (auto-version, tui-release, "
"maintenance) so any future workflow edit reintroducing the SIGPIPE "
"pipe or dropping the GITEA_REF_NAME tag fallback goes RED at test "
"time, not in production (red-first)"
)
AFFECTED = [
"scripts/ci_e2e_rig.sh",
"scripts/admit_wave_ci_e2e_rig.py",
"tests/test_ci_e2e_rig.py",
"tests/test_workflow_script_contract.py",
"docs/ci-e2e-rig.md",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-ci-e2e-rig",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-ci-e2e-rig",
"affected_surface": AFFECTED,
"evidence_refs": [
"scripts/tag_repair.py",
".gitea/workflows/auto-version.yml",
".gitea/workflows/tui-release.yml",
".gitea/workflows/maintenance.yml",
"tests/test_workflow_script_contract.py",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-ci-e2e-rig")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-ci-e2e-rig",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-ci-e2e-rig",
expected_new_entities=[
"scripts/ci_e2e_rig.sh (scratch-runner + scratch-repo e2e proof rig, teardown-safe)",
"tests/test_ci_e2e_rig.py (rig contract tests, non-live)",
],
expected_runtime_effects=[
"scripts/ci_e2e_rig.sh registers a scratch Gitea runner, creates a scratch repo, runs a workflow end-to-end, verifies outputs, and tears everything down",
"the workflow script contract tests cover auto-version, tui-release, and maintenance: SIGPIPE pipes and missing tag fallbacks go RED at test time",
],
verification_obligations=[
"tests/test_ci_e2e_rig.py green (rig syntax, dry-run mode, teardown-on-failure contract — nothing live launched)",
"tests/test_workflow_script_contract.py green with the three pipelines covered and the new SIGPIPE/tag-fallback guards provably red-first (mutation spot-check)",
"bash -n clean on scripts/ci_e2e_rig.sh; no live gitea launch during the gaming session",
],
approval_ref="user:50-scale distribution 2026-10-20",
contract_id="ADMIT-wave-ci-e2e-rig",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+152
View File
@@ -0,0 +1,152 @@
"""Admission dispatch for wave cloud-native-tools (D45 named follow-up).
The mission: the three cloud clients (cloud_openai.go / cloud_anthropic.go
/ cloud_gemini.go) degrade to toolless today — the engine's tool loop only
fires on the local Ollama wire. Port the local path's discipline per wire:
[OI]-compat (tools array in the request, tool_calls in the response),
Anthropic (tools param, tool_use blocks, tool_result continuation), Gemini
(functionDeclarations, functionCall parts). Red-first per wire with
httptest doubles (hermetic — never the owner's real tokens); the live leg
stays gated behind RUNE_CLOUD_PARITY_LIVE=1. Mode-gating identical to the
local path; the bounded loop cap (maxToolIterations) applies.
approval_ref 'user:50-scale distribution 2026-10-20'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"Cloud native tools (D45 named follow-up): implement "
"ToolChatClient (SupportsTools + ChatWithTools) on the three cloud "
"clients in src/go/rune-core/internal/runecore/ — cloud_openai.go "
"([OI]-compat: tools array in the request, tool_calls in the "
"response), cloud_anthropic.go (tools param, tool_use blocks, "
"tool_result continuation with tool_use_id), cloud_gemini.go "
"(functionDeclarations in the request, functionCall parts in the "
"response, functionResponse continuation) — porting the local path's "
"discipline (tools.go: schemas per mode via ToolsetFor, the bounded "
"model→tools→model loop in engine.go runTurn, per-dispatch mode "
"re-read via ExecuteGatedTool) so cloud providers stop degrading to "
"toolless. Red-first per wire with httptest doubles (hermetic; the "
"live leg stays gated behind RUNE_CLOUD_PARITY_LIVE=1 for the "
"owner). Mode-gating identical to the local path; the bounded loop "
"cap applies"
)
AFFECTED = [
"src/go/rune-core/internal/runecore/",
"scripts/admit_wave_cloud_native_tools.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-cloud-native-tools",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-cloud-native-tools",
"affected_surface": AFFECTED,
"evidence_refs": [
"src/go/rune-core/internal/runecore/tools.go (the local "
"tool loop: ToolsetFor per-mode schemas, "
"ExecuteGatedTool per-dispatch gate, maxToolIterations "
"bound)",
"src/go/rune-core/internal/runecore/engine.go:193-238 "
"(runTurn's bounded model→tools→model loop, cap-out "
"withdraws the toolset)",
"src/go/rune-core/internal/runecore/cloud_openai.go "
"(the [OI]-compat wire today: messages-only)",
"src/go/rune-core/internal/runecore/cloud_anthropic.go "
"(the anthropic wire today: messages-only)",
"src/go/rune-core/internal/runecore/cloud_gemini.go "
"(the gemini wire today: contents-only)",
"src/go/rune-core/internal/runecore/cloud_clients_test.go "
"(the httptest-double test discipline to extend)",
"src/go/rune-core/internal/runecore/parity_live_test.go "
"(RUNE_CLOUD_PARITY_LIVE=1 gate — the live leg stays "
"owner-gated)",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-cloud-native-tools")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-cloud-native-tools",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-cloud-native-tools",
expected_new_entities=[
"cloud_openai.go ([OI]-compat ChatWithTools: tools array "
"rides the request; tool_calls parse from "
"choices[0].message.tool_calls with JSON arguments; "
"assistant tool_calls + role 'tool' messages replay in "
"history; SupportsTools true — the wire carries tools "
"natively)",
"cloud_anthropic.go (ChatWithTools: tools param with "
"input_schema; tool_use blocks parse to ToolCall; the "
"assistant's tool_use blocks replay as content blocks; "
"results continue as role 'user' content blocks of type "
"tool_result carrying tool_use_id)",
"cloud_gemini.go (ChatWithTools: functionDeclarations "
"under the tools param; functionCall parts parse to "
"ToolCall; results continue as role 'user' functionResponse "
"parts keyed by function name)",
"engine.go untouched: the loop already fires for any "
"ToolChatClient when the mode exposes a toolset — cloud "
"clients join it by implementing the interface",
"tests: red-first httptest doubles per wire (request "
"shape asserted + response shapes exercised: plain answer, "
"tool call, continuation) — hermetic, no real tokens",
],
expected_runtime_effects=[
"a cloud provider serving a CHAT/PLAN turn sees the read "
"family toolset and can answer by calling list_directory/"
"read_file/glob_files/grep_files; a TASK turn also sees the "
"write family",
"mode-disallowed tool calls on cloud wires execute as the "
"typed refusal envelope, never bytes (identical to local)",
"the loop cap holds on cloud wires: at maxToolIterations "
"the toolset withdraws and the model answers in text",
"a tool-incapable model on a cloud wire keeps the plain "
"Chat path (honest toolless, no fake capabilities)",
],
verification_obligations=[
"cd src/go/rune-core && GOTMPDIR=/mnt/workdrive/gotmp go "
"test ./internal/runecore/ -race green (httptest doubles "
"only — hermetic)",
"go vet + gofmt clean on the touched package",
"the live leg stays gated behind RUNE_CLOUD_PARITY_LIVE=1 "
"(owner-run only; not exercised in this session)",
],
approval_ref="user:50-scale distribution 2026-10-20",
contract_id="ADMIT-wave-cloud-native-tools",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
if __name__ == "__main__":
raise SystemExit(main())
+205
View File
@@ -0,0 +1,205 @@
"""Admission dispatch for wave wave-consent-bridge: the chat→task consent
card, phase 1 (owner-approved queue item 2a — the dichotomy's y/n card).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_k27_composer_depth.py exemplar), admitted through the
research gate on the consent bridge's own round chain
research/factory-consent-bridge/07-qualify.json (registry entry
factory-consent-bridge, verdict PROCEED).
Artifacts this wave admits: tui/consent (the phase-1 deterministic
bridge — tier-0 declared promote language via the promote SA-D3 ladder's
explicit class, tier-1 structural suppression gate in the
construction_intent style, the one-live-card lifecycle with the
STALE_GAP_TURNS=2 lapse law, the BriefRecord consent-time snapshot, the
gold y/n strip render with default focus No), the card's wiring into the
shell (observe on chat sends/replies, y → enqueue through the
task-queue admission carrying the brief, n/Esc → dismiss demoting to the
promote hold queue), and the red-first frozen batteries. Tier-2 (model
self-report work_intent envelope) is NOT built — the synthesis phases it
as phase 2 behind kill tests K1-K3 and owner decision OD-B.
Still wiring (main session lands): the core-side widening of the task
envelope/ledger Brief slot beyond the enqueue surface's text field.
"""
import json
import os
import subprocess
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
SLUG = "wave-consent-bridge"
RESEARCH_SLUG = "factory-consent-bridge"
OBJECTIVE = (
"construct the consent bridge phase 1 (owner-approved queue item 2a; "
"openspec add-native-rune-tui task 2.2's importance-aware promotion, "
"suggested-task accept/dismiss leg): when a chat message is really a "
"work request, Rune proposes starting it as a task via one live y/n "
"card at the chat pane's bottom; on y the task starts immediately "
"carrying the conversation's context. Detection tiers 0-1 "
"deterministic only: tier-0 declared promote language (the SA-D3 "
"explicit class — idiom prefixes stripped, imperative verb, not "
"keyword spam) and tier-1 structural suppression (interrogative + "
"analysis markers suppress the card; everything ambiguous passes — "
"recall-preserving, in the construction_intent resolver's style). "
"The card: one live card per chat channel, non-modal, composer keeps "
"focus, y/n keys bind only while the composer is empty, DEFAULT "
"FOCUS NO, keyboard-first with click equivalents; a newer proposal "
"supersedes; lapse after 2 recorded turns (STALE_GAP_TURNS) demotes "
"to the promote hold queue — a proposal never dies silently. Consent "
"captures the BriefRecord snapshot AT CONSENT TIME (triggering msg "
"id + verbatim text, brief statement, evidence refs, channel) and "
"the enqueue carries it through the task-queue admission (never "
"preempting a running turn). Dismissal demotes to the promote hold "
"queue with a receipt. Tier-2 model self-report is NOT built (phase "
"2, kill tests K1-K3 + owner decision OD-B). Frozen tests: "
"detection table, lapse state machine, BriefRecord verbatim "
"immutability, card render exact-width + button geometry, key "
"universe (typing never answers a card). Red-first."
)
AFFECTED = [
"tui/consent/consent.go",
"tui/consent/render.go",
"tui/consent/consent_test.go",
"tui/promote/promote.go",
"tui/promote/promote_hold_test.go",
"tui/tasks/face.go",
"tui/cmd/rune-tui/main.go",
"tui/consent/README.md",
]
def record_research_gate(marker: str) -> int:
store_dir = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store_dir, "rune")
doc = scaffold.load()
c = scaffold.contract(doc, f"ADMIT-{SLUG}")
if c is None:
print("contract ADMIT-<slug> not found — nothing to record")
return 1
ext = c.setdefault("ext", {})
rg = ext.get("research_gate") or {}
rg.update({
"chain": "research/factory-consent-bridge/07-qualify.json",
"verdict": "PROCEED",
"marker": marker,
"note": "consent bridge phase 1 (queue item 2a): tiers 0-1 + "
"card + snapshot + hold-queue demotion; tier-2 stays "
"phase 2 behind kill tests K1-K3 and OD-B",
})
ext["research_gate"] = rg
scaffold.commit(doc, what=f"record research_gate evidence for {SLUG}")
print("research_gate record committed on ADMIT-" + SLUG)
return 0
def main() -> int:
gate = subprocess.run(
[sys.executable, os.path.join("scripts", "check_research_gate.py"),
"admit", "--slug", SLUG, "--objective", OBJECTIVE,
"--research-slug", RESEARCH_SLUG],
capture_output=True, text=True, cwd=ROOT,
)
print(gate.stdout.strip())
if gate.returncode != 0:
print("research gate REFUSED — the law holds")
return 1
marker = ""
for line in gate.stdout.splitlines():
if line.startswith("research-gate:ALLOW") or line.startswith("exempt:research-gate:"):
marker = line.strip()
break
if not marker:
print("no ALLOW marker in gate output — refusing to mint unrecorded")
return 1
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": f"need-{SLUG}",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": SLUG,
"affected_surface": AFFECTED,
"evidence_refs": [
marker,
"research/factory-consent-bridge/06-synthesis.json",
"research/factory-consent-bridge/07-qualify.json",
"docs/openspec/changes/add-native-rune-tui/specs/dual-lane-interface/spec.md",
"docs/openspec/changes/add-native-rune-tui/tasks.md#2.2",
"src/rune/construction_intent.py",
"tui/promote/promote.go",
"tui/approvals/approvals.go",
"tui/runtime/lib/command.go",
"tui/vendor/rune/task-state/ledger/ledger.go",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder=SLUG)
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder=SLUG,
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref=SLUG,
expected_new_entities=[
"tui/consent consent bridge phase 1: tier-0/tier-1 detector "
"(declared promote language + structural suppression), "
"one-live-card Hub with STALE_GAP_TURNS lapse law, "
"BriefRecord consent-time snapshot, gold y/n strip render "
"(default focus No)",
"promote.Promote.Hold (the visible-hold demotion path the "
"card's dismissal and lapse ride)",
"shell wiring: observe on chat sends/replies, y → enqueue "
"carrying the brief, n/Esc → hold-queue demotion",
],
expected_runtime_effects=[
"phase-1 deterministic only: no model changes, no prompt "
"overhead, tier-2 work_intent envelope NOT built (kill "
"tests K1-K3 + OD-B gate it); the card never creates a "
"task silently — only consent does; a running turn is "
"never preempted (enqueue, never interrupt); no commits",
],
verification_obligations=[
"go test ./consent/ ./promote/ -count=1 green in tui/ "
"(frozen groups: detection table, lapse law, snapshot "
"immutability, render geometry, key universe, hold "
"demotion)",
"gofmt -l and go vet clean on tui/consent",
"go build ./... green in tui/",
"python scripts/check_research_gate.py sweep GREEN",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id=f"ADMIT-{SLUG}",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
rc = 0 if env.get("success") else 1
if record_research_gate(marker) != 0:
return 1
return rc
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,151 @@
"""Admission dispatch for the wave-core-supervision-go-gc build (owner
dispatch, main session 2026-10-19, approval_ref
'user:40-scale queue approval 2026-10-19').
Two owner-approved housekeeping items:
1. GO-CORE SUPERVISION: scripts/ensure_core.py today supervises only the
Python core (src/rune/core_server.py). The Go core
(src/go/rune-core, deployed binary ~/.local/share/rune/bin/rune-core,
same endpoints/identity 'Rune') runs manually. ensure gains Go
supervision as the default path (RUNE_CORE unset or =go), keeping the
Python path behind the explicit RUNE_CORE=python fallback flag; the
/v1/status identity-'Rune' gate already accepts the Go binary.
Red-first battery tests/test_ensure_core_go.py on the supervision
logic; live proof = kill the running core, watch ensure restart it.
2. /tmp GARBAGE COLLECTION: ~8GB of stale /tmp/rune-tui-lib-* extraction
dirs leaked by TUI test runs whose serve processes died. Safe
pattern: a dir is garbage iff NO live process references it (cmdline,
exe, fd, or maps) — extend scripts/cleanup_orphan_rune.sh with a
--dirs mode, dry-run by default, reporting bytes reclaimed; --apply
kills orphan serves AND GCs the dead dirs in one command.
Research exemption: owner-directive ops/housekeeping plumbing over
already-deployed surfaces (D39 Go core, the orphan-kill script), no new
research questions.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
MARKER = (
"exempt:research-gate:owner-directive: owner dispatch "
"wave-core-supervision-go-gc (main session, 2026-10-19; approval_ref "
"user:40-scale queue approval 2026-10-19) — supervision swap and "
"tmp garbage collection are ops/housekeeping plumbing over "
"already-deployed surfaces (D39 Go core serve contract, "
"cleanup_orphan_rune.sh liveness laws), no new research questions"
)
OBJECTIVE = (
"swap core supervision to the Go binary (wave-core-supervision-go-gc): "
"scripts/ensure_core.py ensures, statuses, and stops the Go core "
"(~/.local/share/rune/bin/rune-core, source src/go/rune-core) on 8376 "
"by default through the same identity-'Rune' /v1/status gate, with "
"RUNE_CORE=python as the explicit fallback to the Python "
"core_server.py path and per-mode pidfiles; frozen red-first battery "
"tests/test_ensure_core_go.py; live proof: kill the running core, "
"ensure restarts it. Plus /tmp GC: scripts/cleanup_orphan_rune.sh "
"gains --dirs mode (dry-run by default) that deletes "
"/tmp/rune-tui-lib-* dirs no live process references (cmdline/exe/"
"fd/maps), reporting reclaimed bytes, and --apply does orphan-serve "
"kill + dir GC in one command."
)
AFFECTED = [
"scripts/ensure_core.py",
"scripts/cleanup_orphan_rune.sh",
"tests/test_ensure_core_go.py",
"scripts/admit_wave_core_supervision_go_gc.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-core-supervision-go-gc",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-core-supervision-go-gc",
"affected_surface": AFFECTED,
"evidence_refs": [
MARKER,
"scripts/ensure_core.py",
"scripts/cleanup_orphan_rune.sh",
"src/go/rune-core/main.go",
"tests/test_ensure_core_go.py",
],
"exemption": (
"owner-directive: ops/housekeeping plumbing over "
"already-deployed surfaces, per owner dispatch "
"(approval_ref user:40-scale queue approval 2026-10-19)"),
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-core-supervision-go-gc")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-core-supervision-go-gc",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-core-supervision-go-gc",
expected_new_entities=[
"scripts/ensure_core.py (Go supervision default, RUNE_CORE="
"python fallback, per-mode pidfiles, same identity gate)",
"scripts/cleanup_orphan_rune.sh --dirs mode (reference-"
"liveness /tmp GC, dry-run default, byte reclaim report) "
"and --apply one-command kill+GC",
"tests/test_ensure_core_go.py (red-first supervision battery)",
],
expected_runtime_effects=[
"scripts/ensure_core.py ensure/status/stop operate on the Go "
"core binary by default (kill-restart cycle proven live); "
"RUNE_CORE=python preserves today's behavior; "
"scripts/cleanup_orphan_rune.sh --dirs dry-runs the /tmp GC "
"by default and --apply kills orphan serves and deletes "
"unreferenced rune-tui-lib dirs, reporting reclaimed bytes",
],
verification_obligations=[
"PYTHONPATH=src python -m pytest tests/test_ensure_core_go.py "
"green",
"kill the running core pid; python scripts/ensure_core.py "
"restarts a healthy Go core on 8376 (status HEALTHY, "
"identity 'Rune')",
"scripts/cleanup_orphan_rune.sh --dirs dry-run lists stale "
"dirs without deleting; --apply reclaims GB and leaves "
"referenced dirs untouched",
],
approval_ref=(
"user:40-scale queue approval 2026-10-19; " + MARKER),
contract_id="ADMIT-wave-core-supervision-go-gc",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+144
View File
@@ -0,0 +1,144 @@
"""Admission dispatch for wave d45-live-use-batch (D45 all five + D44
the doctrine).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_scaffold_status_line.py pattern): the five defects
from the owner's live-use critique — (1) the toolset gap, the TUI chat
path must expose the mode-gated tool suite to the model; (2) the
context bar wiring, /v1/status context{used,limit} must reach the info
bar on BOTH the serve and library seams; (3) model name compaction in
the info bar (base name + param size + ablation marker); (4) the
v-version token hyperlinks to the Gitea release via OSC 8
detect-then-render; (5) timestamps re-landed far-left per message row
— approval_ref 'user:D45 directive 2026-10-20'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"Land the D45 live-use critique batch: (1) fix the toolset gap — "
"the TUI chat path (core_server send_task → rune_operator → "
"local_tool_runtime) must surface the mode-allowed tool suite to "
"the model, CHAT mode read tools visible and callable live, TASK "
"mode writes exposed; (2) wire /v1/status context{used,limit} "
"through the TUI bridge to the info bar on the serve AND library "
"seams; (3) compact model display names (base name + param size + "
"ablation marker, no hf.co/ or GGUF/quant noise; cloud: vendor + "
"model + size); (4) hyperlink the v-version token to the Gitea "
"release via OSC 8 detect-then-render; (5) re-land timestamps "
"far-left in a consistent gutter per message row"
)
AFFECTED = [
"src/rune/local_tool_runtime.py",
"src/rune/core_server.py",
"src/rune/rune_operator.py",
"tui/bridge/bridge.go",
"tui/bridge/status.go",
"tui/face/infobar.go",
"tui/face/render.go",
"tui/face/timestamps.go",
"tui/face/versionlink.go",
"scripts/admit_wave_d45_live_use_batch.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-d45-live-use-batch",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-d45-live-use-batch",
"affected_surface": AFFECTED,
"evidence_refs": [
"docs/architecture/boundary-law.md (D45 all five + D44 the "
"doctrine)",
"src/rune/local_tool_runtime.py (T2 capability-selection "
"slice, _capability_selection)",
"src/rune/tool_executor.py (the 19-tool suite)",
"src/rune/core_server.py (send_task, /v1/status "
"context{used,limit})",
"tui/bridge/ (b.serve.Status → body.Context.Used/Limit → "
"setCtx → m.rt.Ctx() → ctxToken)",
"docs/architecture/boundary-law.md (D43 addendum 6, "
"timestamps far-left + version hyperlink)",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-d45-live-use-batch")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-d45-live-use-batch",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-d45-live-use-batch",
expected_new_entities=[
"src/rune/local_tool_runtime.py (the chat-path capability "
"selection fix: the mode-allowed toolset is exposed in the "
"prompt/capability blocks for local models and native "
"tool-calls for cloud; red-first live proof a CHAT-mode "
"turn sees and calls list_directory)",
"tui/bridge + tui/face (the context{used,limit} wiring on "
"the library seam: the face fetches /v1/status or its own "
"Ctx source, the info bar renders the numbers)",
"tui/face (model name compaction: base name + param size "
"+ ablation marker, path/quant noise dropped; cloud: "
"vendor + model + size)",
"tui/face (OSC 8 version hyperlink detect-then-render to "
"http://127.0.0.1:3000/conductor/rune/releases/tag/tui-v"
"<version>)",
"tui/face (timestamps far-left gutter per message row)",
"tests: red-first suites per item, -race green",
],
expected_runtime_effects=[
"a CHAT-mode chat turn exposes the read tools to the model "
"and a live list_directory call succeeds through the full "
"path",
"the info bar shows context used/limit after a turn",
"the info bar shows the compacted model name",
"the version token hyperlinks when the terminal answers "
"the OSC 8 query, plain text otherwise",
"timestamps sit in a far-left gutter per message row",
],
verification_obligations=[
"python -m pytest tests/ -count green for the touched "
"python suites; cd tui && go test ./... -race green, go "
"build + vet + gofmt clean",
"tmux PTY proofs at 110 cols with the REAL home and REAL "
"core: a real chat turn with tool visibility proof",
"deploy the binary",
],
approval_ref="user:D45 directive 2026-10-20",
contract_id="ADMIT-wave-d45-live-use-batch",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+159
View File
@@ -0,0 +1,159 @@
"""Admission dispatch for wave d46-live-defects (D46 all four).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_d45_live_use_batch.py pattern): the four live
defects from the owner's second testing round — (1) MESSAGE
DUPLICATION: a sent user message renders TWICE in the chat pane
(claim law was hardened in D43 but a path still double-renders;
red-first reproduce, find the double-apply path, pin the no-dupe
tripwire across boot-restore + live send + backfill); (2) THINKING
NOT MOVED: the thinking animation must leave the chat pane for the
bridge strip between panes and composer (full-width, above the
composer, click-to-expose, self-extinguishing) per D43 addendum 6
item 1; (3) COMPOSER SHRUNK: restore composer growth — the composer
may push BOTH panes up (D34 law: panes yield to composer growth),
verify the generous-box spec (2 rest rows, padding); (4) DISPATCH
TIMEOUT: raise the TUI + entry-proxy + core budgets to a single
coherent budget (480s) above the slowest observed 27B thinking turn
(250-310s solo), documented as D46 — approval_ref
'user:D46 directive 2026-10-20'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"Land the D46 four live defects: (1) fix message duplication — a "
"sent user message renders twice in the chat pane; reproduce via "
"PTY, find the exact double-apply path (syncChatRows / context "
"wiring / thinking-bridge echo), fix, pin the no-dupe tripwire "
"covering boot-restore + live send + backfill paths; (2) move the "
"thinking animation out of the chat pane into the bridge strip "
"between panes and composer (full-width, above the composer, "
"click-to-expose, self-extinguishing); (3) restore composer growth "
"— the composer may push both panes up for room, generous-box spec "
"holds (2 rest rows, padding); (4) raise the dispatch timeout "
"stack (TUI client + entry proxy + core) to a single coherent 480s "
"budget above the slowest real 27B thinking turn"
)
AFFECTED = [
"tui/chatpane/",
"tui/bridge/",
"tui/face/",
"tui/lanes/",
"tui/tasks/",
"tui/cmd/rune-tui/",
"tui/runtime/lib/",
"src/go/rune-entry/internal/entry/",
"src/rune/core_server.py",
"scripts/admit_wave_d46_live_defects.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-d46-live-defects",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-d46-live-defects",
"affected_surface": AFFECTED,
"evidence_refs": [
"docs/architecture/boundary-law.md (D46 all four + D43 "
"addendum 6 item 1 the thinking bridge + D34 composer "
"yield law)",
"tui/chatpane/ (message render + claim law + "
"syncChatRows)",
"tui/bridge/ (status poll, context wiring, D45 wave in "
"flight)",
"tui/face/ (info bar, layout: panes + composer split)",
"src/go/rune-entry/internal/entry/ (entry proxy 330s "
"budget)",
"src/rune/core_server.py (core dispatch budget)",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-d46-live-defects")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-d46-live-defects",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-d46-live-defects",
expected_new_entities=[
"tui/chatpane (the no-dupe fix: exactly one render per "
"unique msg_id across boot-restore, live send, and "
"backfill paths; the double-apply path closed)",
"tui/chatpane tests (the no-dupe tripwire: frozen test "
"counting rendered bubbles vs unique msg_ids after a full "
"send→reply→poll cycle, multiple paths)",
"tui/face or tui/bridge (the thinking bridge strip: "
"full-width, between panes and composer, above the "
"composer, click-to-expose, self-extinguishing; chat pane "
"drops the thinking bubble render)",
"tui/face (composer growth restored: composer may push "
"both panes up, generous-box spec 2 rest rows + padding "
"verified)",
"tui + src/go/rune-entry + src/rune (coherent dispatch "
"budget 480s on TUI client, entry proxy, core legs; "
"documented as D46)",
"tests: red-first suites per item, -race green",
],
expected_runtime_effects=[
"a sent user message renders exactly ONCE in the chat pane "
"on every path",
"the thinking animation renders in the bridge strip above "
"the composer, not in the chat pane; click exposes the "
"thought process; it self-extinguishes",
"the composer grows and both panes yield upward; the "
"generous-box spec holds",
"a 300s+ slow-model turn no longer hits 'dispatch failed: "
"context deadline exceeded' at 330s",
"the D46 addendum: tool events render in the tasks pane's "
"work stream (never the chat transcript) with a per-task "
"tool-event history, and file paths in the tasks pane "
"carry OSC 8 file:// hyperlinks (detect-then-render)",
],
verification_obligations=[
"cd tui && go test ./... -race green, go build + vet + "
"gofmt clean; python -m pytest for touched python suites",
"PTY proofs at 110 cols with the REAL home and REAL core: "
"send a message, count bubbles (no dupe), thinking strip "
"in the bridge not the chat, composer growth pushes panes",
"slow-model simulation past 330s proving the timeout fix",
"deploy the binary via scripts/build_paired.sh, copy "
"verification/build-id/rune-tui to "
"~/.local/share/rune/bin/rune-tui",
],
approval_ref="user:D46 directive 2026-10-20",
contract_id="ADMIT-wave-d46-live-defects",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,105 @@
"""Admission dispatch for the factory-authority-port build wave.
classify + activate + admit (the headless scaffolding entry law,
tests/test_construction_admission.py pattern): port the construction
authority from issue161-harness into rune (mutation boundary goes live),
owner directive 'systematic-scaffolding directive 2026-10-19'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"port the construction authority from issue161-harness into rune so the "
"mutation boundary goes live: every edit a subagent (or the main session) "
"makes is checked against the active construction contract's scope. "
"P1 authority core + zero-logic CLI into src/rune; P2 activation front "
"door (activate_from_text/classify_mode); P3 wiring the scaffolding-guard "
"hook to rune; P4 escape-drill fail-closed corpus. Per "
"research/factory-authority-port/authority-port-map.md."
)
AFFECTED = [
"src/rune/construction_authority.py",
"src/rune/construction_authority_cli.py",
"src/rune/construction_activation.py",
"config/mimo-hooks/scaffolding-guard.ts",
"scripts/install_mimo_hook.py",
"store/authority-lock.json",
"tests/test_construction_authority_rune.py",
"tests/test_construction_authority_drills.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-factory-authority-port",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-factory-authority-port",
"affected_surface": AFFECTED,
"evidence_refs": [
"research/factory-authority-port/authority-port-map.md",
"src/rune/construction_admission.py",
"src/rune/scaffold.py",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-factory-authority-port")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-factory-authority-port",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-factory-authority-port",
expected_new_entities=[
"src/rune/construction_authority (module: mutation-boundary authority ported verbatim from harness)",
"src/rune/construction_authority_cli (zero-logic JSON bridge)",
"tests/test_construction_authority_* (red-first corpus + escape drills)",
],
expected_runtime_effects=[
"scaffolding-guard hook routes begin/check/checkpoint to rune's authority CLI",
"active rune contract scopes all mutating tool calls; out-of-scope edits blocked",
"durable lock trio at store/authority-lock.json with ACTIVE/UNREADABLE fail-closed semantics",
],
verification_obligations=[
"pytest tests/test_construction_authority_rune.py tests/test_construction_authority_drills.py green",
"CLI ops (status/check/checkpoint) respond via stdin/stdout JSON bridge",
"fresh-process fail-closed drill: durable lock ACTIVE + bridge failure blocks mutation; ABSENT allows",
"ruff clean on touched files",
],
approval_ref="user:systematic-scaffolding directive 2026-10-19",
contract_id="ADMIT-wave-factory-authority-port",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+123
View File
@@ -0,0 +1,123 @@
"""Admission dispatch for wave go-sharding (owner directive 2026-10-19:
'do the go side sharding'). The sharded checkup landed for Python
(1087s -> 275s) but the Go members are now the bottleneck: ~600s each for
go_suite (tui module under -race) and boundary_corpus_go under load.
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_scale40_sharding.py pattern): diagnose first (measure,
never guess), then land only what the numbers justify — the same
discipline as the Python sharding. Manifest shape stays byte-compatible
for scripts/check_landing.py.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"owner directive 'do the go side sharding' (2026-10-19): the sharded "
"checkup landed for Python (1087s->275s) but the Go members are now "
"the bottleneck — ~600s each for go_suite (tui module, go test -race "
"across ~47 packages) and boundary_corpus_go under factory load. "
"DIAGNOSE first with measurements (serial vs loaded wall times; the "
"race detector's measured share; per-package time distribution), then "
"land only the options the measurements justify from: (a) -race only "
"on diff-touched packages with non-race elsewhere, (b) per-package "
"sub-shards summed at report time, (c) a content-keyed package "
"results cache. Same laws as the Python shard: manifest shape "
"gate-compatible (check_landing reads it untouched), red-first tests "
"for the sharding logic, honest 'not sharded' notes when a tool is "
"missing, receipts with before/after timings."
)
AFFECTED = [
"scripts/unified_check.py",
"tests/test_unified_check_go_shard.py",
"scripts/admit_wave_go_sharding.py",
"verification/",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-go-sharding",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-go-sharding",
"affected_surface": AFFECTED,
"evidence_refs": [
"research/factory-50-scale/06-synthesis.json",
"scripts/unified_check.py",
"scripts/check_landing.py",
"verification/unified-baseline.json",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-go-sharding")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-go-sharding",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-go-sharding",
expected_new_entities=[
"measured Go-shard diagnosis (serial vs loaded timings, race "
"detector share, per-package distribution)",
"scripts/unified_check.py Go-member sharding, only the "
"options the measurements justify",
"tests/test_unified_check_go_shard.py (red-first shard-logic "
"suite)",
"scripts/admit_wave_go_sharding.py",
],
expected_runtime_effects=[
"go_suite and boundary_corpus_go wall times drop by the "
"measured factor, not a projected one, under representative "
"factory load",
"the manifest shape (schema, members, verdict law) is "
"byte-compatible for scripts/check_landing.py — the gate "
"reads it unchanged",
"any option that loses to measurement is reported as "
"rejected with numbers, not landed",
],
verification_obligations=[
"tests/test_unified_check_go_shard.py green (red-first: "
"written and shown RED before the implementation landed)",
"before/after member wall times recorded under "
"representative load, in the receipts",
"scripts/check_landing.py unchanged accepts the sharded "
"manifest (gate-compatibility proof)",
],
approval_ref="user:go-sharding directive 2026-10-19",
contract_id="ADMIT-wave-go-sharding",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,171 @@
"""Admission dispatch for the wave-release-pipeline-deepdive mechanical repair
(owner dispatch, queue item 5 — "ive asked about this over and over so this
needs a deep dive fix").
classify + activate + admit (the headless scaffolding entry law,
tests/test_construction_admission.py pattern): root-cause the recurring
auto-version release failure from REAL CI run logs (Gitea API), fix at the
source with red-first tests, prove two clean convergence cycles end-to-end
and the deadlock self-heal, never commit to the rune working tree.
Root cause evidence (fetched 2026-10-19 from
http://127.0.0.1:3000/api/v1/repos/conductor/rune/actions):
* Run 29929 (0.312.0 bump push, 2026-10-17 13:04), run 30435 (0.313.0,
2026-10-18 10:30), run 30636 (0.314.0, 2026-10-18 20:04 — AFTER the
master-tip fallback landed) and ~40 more all die the SAME way:
"##[error]Process completed with exit code 141" with ZERO step output.
* 141 = 128+13 = SIGPIPE. The bump step runs under
`bash -e -o pipefail`; its FIRST line is
`LATEST=$(git tag --sort=-v:refname | head -1 | sed 's/^v//')`.
With 567 tags git tag streams its listing, `head -1` exits after the
first line, git gets EPIPE/SIGPIPE and exits 141, pipefail + set -e
kill the step instantly — BEFORE any bump, fallback, or tag logic runs.
Reproduced locally in the rune repo: `set -o pipefail;
LATEST=$(git tag --sort=-v:refname | head -1)` → exit 141.
* The 0.312.0/0.313.0/0.314.0 master-tip fallback could never fire: the
failure happens at the first line of the step, an entire step earlier.
* Secondary failure (tui-release, runs 30390/30456/30862): on tag pushes
`GITEA_REF_NAME` resolves EMPTY on this runner (rune-ci-desktop, act
v3.3.0), so tui-release aborts "triggered on non-tui tag " even when a
manually-repaired tui-v tag DID land — no TUI release ever fires.
* Deadlock: the failed run leaves version>tag; the push path reads
anomaly version_ahead_of_target, warranted=null, and skips — forever
(the manual "re-arm convergence" commit 360665b1 on 2026-10-19).
Fix: scripts/tag_repair.py — SIGPIPE-free latest-tag read
(git for-each-ref, no pipe), tui-tag ensure/repair (tag the fetched master
tip when the bump sha is unusable, push via the documented git-over-HTTP
auth), and a repair-plan function that turns the version-ahead-of-tag
deadlock state into a self-heal (tag + push) instead of a permanent skip.
Red-first battery tests/test_tag_repair.py pins: SIGPIPE safety on a
many-tag repo, ensure-tag idempotency, bump-sha-unusable fallback, and
deadlock self-heal planning. Workflow patches: auto-version.yml drops the
`git tag | head` pipes, and tui-release.yml derives the tag from
GITEA_REF_NAME → GITHUB_REF_NAME → `git describe --tags --exact-match`.
Research exemption: owner-directive mechanical repair (release pipeline
deep dive, queue item 5), declared in docs/factory/research-registry.json
slug release-pipeline-deepdive.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"release pipeline deep dive repair (wave-release-pipeline-deepdive): "
"root-cause the recurring auto-version tag-miss deadlock from real CI "
"logs (exit 141 SIGPIPE on `git tag --sort=-v:refname | head -1` under "
"pipefail with many tags — kills the bump step on its first line, so "
"the master-tip fallback never ran; plus tui-release aborting on empty "
"GITEA_REF_NAME), fix at the source via scripts/tag_repair.py "
"(SIGPIPE-free for-each-ref latest-tag read, ensure/repair tui-v tag "
"pushed over git HTTP, repair-plan that self-heals the version>tag "
"deadlock instead of skipping forever), red-first battery "
"tests/test_tag_repair.py, workflow patches to .gitea/workflows/"
"auto-version.yml (drop the SIGPIPE pipes, call tag_repair) and "
".gitea/workflows/tui-release.yml (ref-name fallback chain), and prove "
"two clean convergence cycles + deadlock self-heal end-to-end on the "
"local Gitea runner"
)
AFFECTED = [
"scripts/tag_repair.py",
"tests/test_tag_repair.py",
".gitea/workflows/auto-version.yml",
".gitea/workflows/tui-release.yml",
"docs/factory/research-registry.json",
"scripts/admit_wave_release_pipeline_deepdive.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-release-pipeline-deepdive",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-release-pipeline-deepdive",
"affected_surface": AFFECTED,
"evidence_refs": [
".gitea/workflows/auto-version.yml",
".gitea/workflows/tui-release.yml",
"scripts/version_convergence.py",
"docs/factory/spawn-queue.md",
"memory/journal/events.jsonl",
],
"exemption": (
"owner-directive: deep dive mechanical repair of the release "
"pipeline (queue item 5, 'ive asked about this over and over "
"so this needs a deep dive fix'), per owner dispatch "
"(user:40-scale queue approval 2026-10-19; see "
"docs/factory/research-registry.json slug "
"release-pipeline-deepdive)"),
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-release-pipeline-deepdive")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-release-pipeline-deepdive",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-release-pipeline-deepdive",
expected_new_entities=[
"scripts/tag_repair.py (SIGPIPE-free latest tag, ensure/repair "
"tui-v tag, deadlock self-heal plan)",
"tests/test_tag_repair.py (frozen battery, red-first)",
".gitea/workflows/auto-version.yml SIGPIPE pipes removed + "
"tag_repair wiring",
".gitea/workflows/tui-release.yml ref-name fallback chain",
"end-to-end proof: two clean convergence cycles + deadlock "
"self-heal on the local Gitea runner",
],
expected_runtime_effects=[
"tests/test_tag_repair.py proven RED against the pre-fix "
"naive git-tag-pipe / no-repair logic, then GREEN on the "
"fixed tag_repair module",
"auto-version push event with 500+ tags completes the bump "
"AND pushes the tui-v<version> tag with no manual repair",
"version>tag deadlock state self-heals (missing tui tag "
"created and pushed) instead of skipping forever",
],
verification_obligations=[
"RUNE_CA_CORE=python RUNE_CM_CORE=python PYTHONPATH=src "
"python -m pytest tests/test_tag_repair.py green",
"e2e: scratch repo on local Gitea with the patched workflow — "
"two pushes → two bump commits → two tui-v tags → two TUI "
"releases",
"e2e: deadlock probe — version ahead of tag with the tui tag "
"missing self-heals on the next push",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-release-pipeline-deepdive",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,120 @@
"""Admission dispatch for wave: retirement-conversion rows 5+6 (change-safety + trust).
PORT → INTEGRATE → REMOVE wave 2 (owner-amended method, approval
user:50-scale distribution 2026-10-20): live consumers switch to the Go
engine surfaces and RUN INTEGRATED before any Python deletion. This wave
integrates only — the Python oracles (src/rune/persona_gate.py,
src/rune/stage_guards.py) stay; a separate removal wave deletes them later.
Rows: docs/retirement-map.md row 5 (file-change-safety — honest finding at
admission: domains.json lists src_rune: [] — NO Python oracle exists; the
Go restore-points engine is net-new, so integration is N/A and the wave
verifies the Go side) and row 6 (trust-and-sovereignty — persona_gate +
stage_guards Python oracles with live consumers to repoint).
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"retirement-conversion wave 2 (rows 5 file-change-safety + 6 "
"trust-and-sovereignty), PORT → INTEGRATE → REMOVE: row 6 repoints the "
"live Python consumers of persona_gate.decide and stage_guards checks "
"to the Go engine surfaces (src/go/persona-gate, src/go/stage-guards) "
"via in-module delegation facades with fail-loud semantics and "
"deliberate opt-out env vars, runs the focused batteries integrated, "
"and records per-consumer integrated-run receipts; row 5 records the "
"honest finding that no Python oracle exists (domains.json src_rune: "
"[]) — the Go restore-points engine is net-new, integration is N/A, "
"and the wave verifies the Go side (tests -race, tui/changes, "
"tui/history). NO Python deletion this wave (integration proves live "
"first; removal is a separate wave per the owner's amended rule)"
)
AFFECTED = [
"src/rune/persona_gate.py",
"src/rune/stage_guards.py",
"verification/integrated/file-change-safety-integrated.json",
"verification/integrated/trust-and-sovereignty-integrated.json",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-retirement-conversion-change-trust",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-retirement-conversion-change-trust",
"affected_surface": AFFECTED,
"evidence_refs": [
"docs/retirement-map.md",
"docs/factory/domains.json",
"src/go/stage-guards/truth.json",
"src/go/persona-gate/truth.json",
"verification/integrated/model-routing-integrated.json",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-retirement-conversion-change-trust")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-retirement-conversion-change-trust",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-retirement-conversion-change-trust",
expected_new_entities=[
"verification/integrated/trust-and-sovereignty-integrated.json",
"verification/integrated/file-change-safety-integrated.json",
],
expected_runtime_effects=[
"live persona_gate.decide calls resolve through the Go engine "
"(src/go/persona-gate) with the Python path retained as escape "
"hatch (RUNE_PERSONA_GATE_ENGINE=python)",
"live stage_guards stage checks resolve through the Go engine "
"(src/go/stage-guards) with the Python path retained as escape "
"hatch (RUNE_STAGE_GUARDS_ENGINE=python)",
"row 5 recorded as no-Python-oracle (integration N/A); no "
"Python oracle deleted this wave",
],
verification_obligations=[
"src/go/persona-gate + src/go/stage-guards truth batteries green "
"with -race",
"focused persona_gate/stage_guards/construction_interface "
"batteries green with the Go engines live",
"integrated-run receipts recorded under verification/integrated/",
],
approval_ref="user:50-scale distribution 2026-10-20",
contract_id="ADMIT-wave-retirement-conversion-change-trust",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,110 @@
"""Admission dispatch for wave: retirement-conversion rows 21+23 (routing + bus).
PORT → INTEGRATE → REMOVE wave 1 (owner-amended method, user:40-scale queue
approval 2026-10-19): live consumers switch to the Go engine and RUN
INTEGRATED for a proven interval BEFORE any Python deletion. This wave
integrates only — the Python oracles (src/rune/model_routing.py,
src/rune/event_bus.py) stay; a separate removal wave deletes them later.
Rows: docs/retirement-map.md row 21 (model-routing, 8 live importers) and
row 23 (event-bus, ~15 consumers + microkernel plugin wiring). Both carry
GREEN 7.2 parity packets (verification/parity/model-routing.json,
verification/parity/event-bus.json).
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"retirement-conversion wave 1 (rows 21 model-routing + 23 event-bus), "
"PORT → INTEGRATE → REMOVE: repoint the live Python consumers to the Go "
"engine surfaces (src/go/model_routing eval rank/update; src/go/event-bus "
"emit/log/stats kernel) via in-module delegation facades, run the Python "
"suite integrated, record per-consumer integrated-run receipts — NO "
"Python deletion this wave (integration proves live first; removal is a "
"separate wave per the owner's amended same-change rule)"
)
AFFECTED = [
"src/rune/model_routing.py",
"src/rune/event_bus.py",
"verification/integrated/model-routing-integrated.json",
"verification/integrated/event-bus-integrated.json",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-retirement-conversion-routing-bus",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-retirement-conversion-routing-bus",
"affected_surface": AFFECTED,
"evidence_refs": [
"verification/parity/model-routing.json",
"verification/parity/event-bus.json",
"docs/retirement-map.md",
"docs/openspec/changes/add-native-rune-tui/specs/migration-and-retirement/spec.md",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-retirement-conversion-routing-bus")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-retirement-conversion-routing-bus",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-retirement-conversion-routing-bus",
expected_new_entities=[
"verification/integrated/model-routing-integrated.json",
"verification/integrated/event-bus-integrated.json",
],
expected_runtime_effects=[
"live consumers of model_routing rank/update resolve through the "
"Go engine (src/go/model_routing) with the Python path retained "
"as escape hatch (RUNE_MODEL_ROUTING_ENGINE=python)",
"live event_bus emit/log/stats journal writes resolve through "
"the Go engine (src/go/event-bus) on the canonical journal path "
"with redirection consumers retained on the Python writer",
"no Python oracle deleted this wave",
],
verification_obligations=[
"tests/test_p1_06_model_routing.py green with the Go engine live",
"tests/test_legacy_event_bus.py + tests/test_events_schema.py green",
"integrated-run receipts recorded under verification/integrated/",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-retirement-conversion-routing-bus",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+128
View File
@@ -0,0 +1,128 @@
"""Admission dispatch for wave scaffold-status-line (D43 addendum 5 +
addendum 5 cont, the owner's verbatim spec).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_subagent_window.py pattern): the tasks pane's
scaffolding status line — 'scaffolding · <animation> · <stage>' while a
governed construction contract is active, the stage from the REAL
contract state (admit/build/verify/land mapped from the authority
lock's phase and the contract entity's checkpoints), the hammer/ladder
glyph walk in the dots-walk rhythm, arbitration-resting, its own LAYER
per the Layer Law — approval_ref 'user:workshop-pane directive
2026-10-19'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"Build the scaffolding status line: the tasks pane shows "
"'scaffolding · <animation> · <stage>' while a governed "
"construction contract is active — the stage from the real "
"contract state (admit/build/verify/land via the authority lock "
"and the contract entity's checkpoints), the hammer/ladder glyph "
"walk in the dots-walk rhythm, self-extinguishing on close, "
"arbitration-resting, its own layer per the Layer Law"
)
AFFECTED = [
"tui/tasks/scaffoldline.go",
"tui/tasks/scaffoldline_test.go",
"tui/tasks/face.go",
"tui/tasks/render.go",
"tui/cmd/rune-tui/main.go",
"scripts/admit_wave_scaffold_status_line.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-scaffold-status-line",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-scaffold-status-line",
"affected_surface": AFFECTED,
"evidence_refs": [
"docs/architecture/boundary-law.md (D43 addendum 5 + cont)",
"src/rune/construction_authority.py (authority lock, "
"read_authority_lock)",
"src/rune/scaffold.py (PHASES machine)",
"store/authority-lock.json (the durable lock)",
"tui/tasks/render.go",
"tui/tasks/workblock.go",
"tui/cmd/rune-tui/main.go",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-scaffold-status-line")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-scaffold-status-line",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-scaffold-status-line",
expected_new_entities=[
"tui/tasks/scaffoldline.go (the SCAFFOLDING STATUS LINE "
"layer: ReadScaffoldStatus from the durable authority lock "
"+ contract checkpoints, the stage map "
"admit/build/verify/land, the hammer/ladder dots-walk, "
"ScaffoldLine render)",
"tui/tasks/scaffoldline_test.go (the layer's own frozen "
"suite: stage mapping, walk rhythm, extinguish-on-close, "
"layer isolation)",
"tui/cmd/rune-tui/main.go (shell wiring: the shared tick "
"advances the walk ~500ms; ARBITRATION — while scaffolding "
"animates the other animations rest)",
],
expected_runtime_effects=[
"with a real active contract the tasks pane renders "
"'scaffolding · <glyph walk> · <stage>' above the work "
"blocks, separate from the per-wave sub-agent rows",
"the stage is the contract's real phase from the store — "
"never a guess",
"the line self-extinguishes when the contract closes (the "
"lock is cleared)",
"while the line animates the other animations rest "
"(arbitration, canon P3)",
],
verification_obligations=[
"cd tui && go test ./tasks/ ./cmd/rune-tui/ -count=1 green "
"(also -race), go build ./... and go vet clean, gofmt clean",
"tmux PTY proof with a REAL active contract minted via this "
"admit pattern in a sandbox home: the line appears with the "
"stage, animates (frames advance), and extinguishes when "
"the lock clears",
],
approval_ref="user:workshop-pane directive 2026-10-19",
contract_id="ADMIT-wave-scaffold-status-line",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+114
View File
@@ -0,0 +1,114 @@
"""Admission dispatch for wave scale40-sharding (owner-approved 40-scale
queue item 1, approval 2026-10-19).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_k15_cooling.py pattern): make the landing checkup fast
enough to support 40 parallel build waves — sharded python_suite via
pytest-xdist (-n 16 --dist worksteal), parallel Go/Rust sweep workers,
same manifest shape so scripts/check_landing.py stays the gate unchanged.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"owner-approved 40-scale queue item 1: make the landing checkup fast "
"enough to support 40 parallel build waves. unified_check.py gains a "
"--shard mode: python_suite runs under pytest-xdist (-n 16 --dist "
"worksteal, the measured path from research/factory-50-scale/"
"06-synthesis.json lever L1), the Go and Rust sweep worker pools go "
"wider, and the manifest shape is unchanged so check_landing stays "
"the gate. Includes red-first tests for the sharding logic and an "
"evidence backfill of the 2026-10-18 landings without manifests"
)
AFFECTED = [
"scripts/unified_check.py",
"tests/test_unified_check_shard.py",
"scripts/admit_wave_scale40_sharding.py",
"verification/",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-scale40-sharding",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-scale40-sharding",
"affected_surface": AFFECTED,
"evidence_refs": [
"research/factory-50-scale/06-synthesis.json",
"docs/factory/domains.json",
"scripts/unified_check.py",
"scripts/check_landing.py",
"verification/unified-baseline.json",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-scale40-sharding")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-scale40-sharding",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-scale40-sharding",
expected_new_entities=[
"scripts/unified_check.py --shard mode (sharded runner)",
"tests/test_unified_check_shard.py (red-first shard-logic suite)",
"scripts/admit_wave_scale40_sharding.py",
],
expected_runtime_effects=[
"a full unified check completes in minutes, not ~10, with the "
"python_suite wall time collapsed by pytest-xdist worksteal "
"distribution across 16 workers",
"the manifest shape (schema, members, verdict law) is "
"byte-compatible for scripts/check_landing.py — the gate "
"reads it unchanged",
"the 2026-10-18 landing evidence hole is closed with honest "
"per-sha manifests; any sha that fails its backfill check is "
"reported RED, not absorbed",
],
verification_obligations=[
"tests/test_unified_check_shard.py green (red-first: written "
"and shown RED before the implementation landed)",
"one honest full unified_check --shard run on HEAD with "
"before/after member wall times recorded",
"scripts/check_landing.py unchanged accepts the sharded "
"manifest (gate-compatibility proof)",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-scale40-sharding",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+115
View File
@@ -0,0 +1,115 @@
"""Admission dispatch for wave sub-agent window (owner queue item 4, the
owner's verbatim design).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_k15_cooling.py pattern): the sub-agent window — a
tab in the tasks pane that exists only while factory build waves are in
flight, listing one row per sub-agent (truncated task title, agent,
running state) with every row expandable to its live process detail
derived from the factory journal and the scaffold observations —
approval_ref 'user:40-scale queue approval 2026-10-19'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"Build the sub-agent window: a tab in the tasks pane that appears "
"only while factory waves are in flight, one expandable row per "
"in-flight sub-agent, the count shared with the rail from one "
"poller"
)
AFFECTED = [
"tui/tasks/waves.go",
"tui/tasks/waves_test.go",
"tui/tasks/face.go",
"tui/tasks/render.go",
"tui/cmd/rune-tui/main.go",
"scripts/admit_wave_subagent_window.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-subagent-window",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-subagent-window",
"affected_surface": AFFECTED,
"evidence_refs": [
"tui/factorywave/factorywave.go",
"tui/factory/factory.go",
"tui/tasks/face.go",
"tui/tasks/render.go",
"tui/rail/waves.go",
"store/scaffold/segments/observations.jsonl",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-subagent-window")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-subagent-window",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-subagent-window",
expected_new_entities=[
"tui/tasks/waves.go (the WaveTab view-model + wave-tab "
"rendering law)",
"tui/tasks/waves_test.go (frozen tab-conditional, row, "
"expand and correlation suite)",
"tui/cmd/rune-tui/main.go (shell wiring: one factorywave "
"poller feeds rail AND the tab; per-wave detail derived "
"from the journal + observations)",
],
expected_runtime_effects=[
"the SUB-AGENTS tab renders in the tasks pane only while "
"factory waves occupy rungs (InFlight>0), naming the count",
"clicking the tab opens one row per in-flight wave: "
"truncated title, agent, running state",
"clicking a row expands its live progress from the factory "
"journal and scaffold observations - nothing fabricated "
"when the kernel carries no thinking text",
"the rail's waves N and the tab's count derive from ONE "
"poller observation",
],
verification_obligations=[
"cd tui && go test ./tasks/ -count=1 green (also -race), "
"go build ./... and go vet clean, gofmt clean",
"PTY proof: with real factory state the tab appears, rows "
"render, an expansion opens, the count matches",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-subagent-window",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+116
View File
@@ -0,0 +1,116 @@
"""Admission dispatch for wave taskqueue-discharge (D36 queue consumption).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_webfeed.py pattern): the task-queue DISCHARGE —
the queue's missing half. The D36 surface (enqueue/list/pane) landed
owner-approved queue item 2c; this wave builds the consumption side:
at each task-turn boundary the lane takes the OLDEST pending entry
(phase 1 FIFO; selectNext is the D36 'Rune intelligently decides'
ordering seam) and dispatches it through the normal task-create path,
marking it dispatched atomically in hot/task_queue.json.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"build the D36 task-queue discharge (owner-approved 40-scale queue "
"item 2c, phase building): at each task-turn boundary (the running "
"task completing or failing, detected on the hot task checkpoint) "
"the task lane takes the OLDEST pending entry via selectNext "
"(phase 1 oldest-first FIFO; the ordering-intelligence seam, "
"documented as the D36 'Rune intelligently decides' insertion "
"point) and dispatches it through the normal task-create path "
"(the core's send_task, channel task), marking it dispatched with "
"an atomic hot/task_queue.json store write and a journal envelope; "
"a failed dispatch re-queues the entry at the FRONT of the FIFO "
"(order preserved, honest task.queue.dispatch_failed event, no "
"hot-loop retry — retry only at the next turn boundary)"
)
AFFECTED = [
"src/go/rune-entry/internal/entry/taskqueue.go",
"src/go/rune-entry/internal/entry/taskqueue_test.go",
"src/go/rune-entry/internal/entry/serve.go",
"scripts/admit_wave_taskqueue_discharge.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-taskqueue-discharge",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-taskqueue-discharge",
"affected_surface": AFFECTED,
"evidence_refs": [
"src/go/rune-entry/internal/entry/taskqueue.go",
"src/go/rune-entry/internal/entry/taskqueue_test.go",
"src/go/rune-entry/internal/entry/serve.go",
"src/go/rune-entry/internal/entry/surface.go",
"docs/architecture/boundary-law.md",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-taskqueue-discharge")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-taskqueue-discharge",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-taskqueue-discharge",
expected_new_entities=[
"selectNext ordering seam (taskqueue.go)",
"queue discharge poller (taskqueue.go, wired in serve.go)",
],
expected_runtime_effects=[
"task-turn boundary (running task completes/fails) "
"discharges the OLDEST pending entry through the normal "
"task-create path (channel task, the core's send_task)",
"discharge is an atomic hot/task_queue.json state change + "
"task.queue.dispatch journal envelope; queue re-numbers "
"and the pane updates on its next poll",
"failed dispatch re-queues at the FIFO front with an "
"honest task.queue.dispatch_failed event",
],
verification_obligations=[
"go test ./... in src/go/rune-entry green",
"go test -race green on the discharge tests",
"PTY/live proof: two queued tasks + running task completes "
"→ the next one starts, its row leaves the queue",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-taskqueue-discharge",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+112
View File
@@ -0,0 +1,112 @@
"""Admission dispatch for the tasks-pane work-block wave (owner-approved
queue item 2b — "WATCHING HER WORK": when Rune works a task, the tasks
pane shows a live work block per running task — her thinking (collapsed,
expandable, the runic marker law), the file she is editing or the
command she is running, and honest progress).
classify + activate + admit (the headless scaffolding entry law,
tests/test_construction_admission.py pattern), user-dispatch approved
via the 40-scale queue.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"tasks-pane live work blocks (owner queue item 2b, WATCHING HER "
"WORK): one live work block per running task rendered in the tasks "
"pane ABOVE the queue rows (the sectioning law: work stream top, "
"entry bottom) — the block shows her thinking (collapsed by "
"default, expandable, the runic marker law ◆), the current "
"file/command line (the newest observed tool-role conversation "
"record — name + what is changing), and honest progress (the "
"observed event counts + elapsed since the task's last update; no "
"per-task step feed exists in the core, so nothing beyond observed "
"state is fabricated). Projection is built from OBSERVED state "
"only: the /v1/task-queue running entry, the task-state rows, the "
"role='tool' and thinking conversation records on the task channel. "
"Red-first frozen tests; -race clean; PTY proof with a real "
"running task"
)
AFFECTED = [
"tui/tasks/workblock.go",
"tui/tasks/workblock_test.go",
"tui/tasks/render.go",
"tui/tasks/face.go",
"tui/bridge/library.go",
"tui/cmd/rune-tui/main.go",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-tasks-work-block",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-tasks-work-block",
"affected_surface": AFFECTED,
"evidence_refs": [
"src/go/rune-entry/internal/entry/taskqueue.go",
"tui/tasks/live.go",
"tui/taskqueue/taskqueue.go",
"tui/chatpane/chatpane.go",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-tasks-work-block")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-tasks-work-block",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-tasks-work-block",
expected_new_entities=[
"tui/tasks/workblock.go (the work-block projection)",
"scripts/admit_wave_tasks_work_block.py (this contract)",
],
expected_runtime_effects=[
"the tasks pane renders a live work block per running task "
"above the queue rows: thinking (collapsed, expandable), "
"the newest tool observation, honest progress",
"no fabricated progress: with no tool/thinking observations "
"the block says so honestly and shows elapsed only",
],
verification_obligations=[
"go test ./... in tui/tasks green (frozen suites)",
"go test -race ./tui/... green",
"PTY proof with a real running task in the owner's TUI",
],
approval_ref="user:40-scale queue approval 2026-10-19",
contract_id="ADMIT-wave-tasks-work-block",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+111
View File
@@ -0,0 +1,111 @@
"""Admission dispatch for wave test-vitality (owner directive
2026-10-19: make ceremony tests mechanically viable so they earn their
keep — audit the suite into vitality classes and convert/kill the
ceremony candidates red-first).
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_history_persistence.py pattern), user-dispatch
approved via the test-vitality directive.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"test-vitality wave on the live rune project: audit the test suite "
"into vitality classes (mutant-proven, red-first-proven, "
"regression-pinned, invariant-pinned, candidate-ceremony), then "
"convert the ceremony candidates into tests that prove something "
"(panic-only → assert real behavior; implementation-trivia pins → "
"invariant pins; fixed-value assertions on randomized inputs → "
"membership/change assertions; duplicate coverage → keep the "
"stronger; dead tests → delete honestly; keyPress arrow/special-key "
"tests → arrowPress/PTY per the synthetic-spelling law), every "
"conversion red-first with mutation spot-checks as proof, suite "
"stays green, tui module first then src/go/rune-core then spot-"
"checks on src/rune tests"
)
AFFECTED = [
"scripts/admit_wave_test_vitality.py",
"tui/**/*_test.go",
"src/go/rune-core/**/*_test.go",
"tests/",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-test-vitality",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-test-vitality",
"affected_surface": AFFECTED,
"evidence_refs": [
"AGENTS.md",
"tui/go.mod",
"tests/",
"src/go/rune-core/",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-test-vitality")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-test-vitality",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-test-vitality",
expected_new_entities=[
"scripts/admit_wave_test_vitality.py (admission dispatch)",
"converted test bodies in tui/ and src/go/rune-core/ "
"(red-first, uncommitted)",
"reports/test-vitality-audit-2026-10-19.md (audit + report)",
],
expected_runtime_effects=[
"ceremony tests converted into behavior/invariant proofs, "
"each red-first with a mutation spot-check recorded",
"dead tests deleted honestly with per-test rationale",
"suite remains green under -race after conversions",
],
verification_obligations=[
"cd tui && go test ./... -race green after conversions",
"cd src/go/rune-core && go test ./... -race green after "
"conversions",
"python -m pytest tests/ green on spot-checked packages",
],
approval_ref="user:test-vitality directive 2026-10-19",
contract_id="ADMIT-wave-test-vitality",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+103
View File
@@ -0,0 +1,103 @@
"""Admission dispatch for wave test-vitality-py (owner directive
2026-10-20: extend the test-vitality sweep to the src/rune Python
suite — classify vitality, convert ceremony candidates red-first with
mutation spot-checks, kill genuinely dead tests honestly).
classify + activate + admit (scripts/admit_wave_test_vitality.py
pattern), user-dispatch approved via the 50-scale distribution
directive.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"test-vitality sweep phase 2 on the src/rune Python suite (3,702 "
"tests): spot-check methodology from the first audit — heuristic "
"pattern sweeps (panic-only, implementation-trivia pins, "
"fixed-value-on-random assertions, live-machine-state fixtures), "
"mutation spot-checks on suspicious tests. Classify into vitality "
"classes, convert ceremony candidates red-first, kill genuinely "
"dead tests honestly, report the audit table"
)
AFFECTED = [
"scripts/admit_wave_test_vitality_py.py",
"tests/",
"reports/test-vitality-audit-py-2026-10-20.md",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-test-vitality-py",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-test-vitality-py",
"affected_surface": AFFECTED,
"evidence_refs": [
"AGENTS.md",
"reports/test-vitality-audit-2026-10-19.md",
"tests/",
"src/rune/",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-test-vitality-py")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-test-vitality-py",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-test-vitality-py",
expected_new_entities=[
"scripts/admit_wave_test_vitality_py.py (admission dispatch)",
"converted/deleted test bodies in tests/ (red-first, "
"uncommitted)",
"reports/test-vitality-audit-py-2026-10-20.md (audit + "
"report)",
],
expected_runtime_effects=[
"ceremony tests converted into behavior/invariant proofs, "
"each red-first with a mutation spot-check recorded",
"genuinely dead tests deleted with per-test rationale",
"suite green on the spot-checked packages after conversions",
],
verification_obligations=[
"python -m pytest tests/ green on spot-checked packages "
"after conversions",
"every conversion carries a mutation spot-check proof",
],
approval_ref="user:50-scale distribution 2026-10-20",
contract_id="ADMIT-wave-test-vitality-py",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+139
View File
@@ -0,0 +1,139 @@
"""Admission dispatch for the thinking-bridge wave (D43 addendum 6 +
D44, owner directives 2026-10-20): the seven-item bridge/hud/workshop
build plus the D44 folds.
classify + activate + admit (the headless scaffolding entry law,
scripts/admit_wave_scaffold_status_line.py pattern), user-dispatch
approved via 'user:thinking-bridge directive 2026-10-20'.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from rune import construction_activation as cact # noqa: E402
from rune import construction_admission as cadm # noqa: E402
from rune import construction_mode as cmode # noqa: E402
from rune import scaffold as sc # noqa: E402
ROOT = os.path.join(os.path.dirname(__file__), "..")
OBJECTIVE = (
"D43 addendum 6 + D44 build (owner directives 2026-10-20): "
"1) THE THINKING BRIDGE — the thinking animation moves out of the "
"chat pane into its own full-width strip between the panes and the "
"composer; clicking the strip exposes the thought process; the "
"strip exists only while thinking runs (self-extinguishing). "
"2) TIMESTAMPS far-left ALWAYS, same position regardless of sender. "
"3) COMPOSER CURSOR regression — always blinks when waiting for "
"entry. 4) TASKS PANE full workshop: active task's live coding "
"fully visible plus interactive open-task rows (expandable to the "
"D38 ledger detail). 5) SUB-AGENT OPENER in the info bar after the "
"version token, hidden at zero. 6) VERSION v-prefix + OSC 8 "
"hyperlink to the Gitea release with graceful degrade. "
"D44 folds: A) model display in the info bar (local truncated "
"name+size+style-if-applicable+ablation flag; cloud full), "
"B) queue false-positive fix (task-focus enter with nothing "
"running starts immediately), C) queued-work receipt renders in "
"the tasks pane, D) queue steer hotkey. E) the pane doctrine "
"governs all of it. Red-first each; -race; PTY proofs at 110 cols"
)
AFFECTED = [
"tui/lanes/render.go",
"tui/lanes/lanes.go",
"tui/lanes/render_test.go",
"tui/lanes/lanes_test.go",
"tui/chatpane/chatpane.go",
"tui/chatpane/chatpane_test.go",
"tui/tasks/face.go",
"tui/tasks/render.go",
"tui/tasks/workblock.go",
"tui/tasks/workshop.go",
"tui/cmd/rune-tui/main.go",
"tui/cmd/rune-tui/main_test.go",
"src/go/rune-entry/internal/entry/taskqueue.go",
"src/go/rune-entry/internal/entry/taskqueue_test.go",
"scripts/admit_wave_thinking_bridge.py",
]
def main() -> int:
store = os.path.join(ROOT, "store", "scaffold")
scaffold = sc.Scaffold(store, "rune")
md = cmode.classify_mode(
OBJECTIVE,
target_project="rune",
project_exists=True,
affected_surface=AFFECTED,
)
print("classified mode:", md.mode)
need = {
"need_id": "need-wave-thinking-bridge",
"objective": OBJECTIVE,
"origin": "worker",
"project": "rune",
"task_ref": "wave-thinking-bridge",
"affected_surface": AFFECTED,
"evidence_refs": [
"docs/architecture/boundary-law.md (D43 addendum 6 + D44)",
"tui/lanes/render.go",
"tui/chatpane/chatpane.go",
"tui/tasks/render.go",
"tui/cmd/rune-tui/main.go",
"src/go/rune-entry/internal/entry/taskqueue.go",
],
}
act = cact.activate(md, need=need, scaffold=scaffold,
builder="wave-thinking-bridge")
print("activated:", act.get("activated"), act.get("mode"))
env = cadm.admit(
act,
objective=OBJECTIVE,
builder="wave-thinking-bridge",
affected_surface=AFFECTED,
evidence_refs=need["evidence_refs"],
origin="worker",
task_ref="wave-thinking-bridge",
expected_new_entities=[
"tui/tasks/workshop.go (the interactive open-task rows "
"layer: expandable ledger detail per task row)",
"scripts/admit_wave_thinking_bridge.py (this contract)",
],
expected_runtime_effects=[
"the thinking animation renders in its own full-width "
"bridge strip between the panes and the composer, only "
"while thinking runs (self-extinguishing), click-exposable",
"timestamps render far-left in a consistent gutter "
"regardless of sender",
"the composer cursor blinks whenever waiting for entry",
"the tasks pane shows the active task's live coding and "
"interactive open-task rows expanding to ledger detail",
"the info bar shows the sub-agent opener after the version "
"token (hidden at zero) and the compacted active model",
"the version renders with a v-prefix and OSC 8 hyperlink "
"to the Gitea release, degrading to plain text",
"task-focus enter with nothing running starts immediately "
"(no queue row, no queued receipt); the queued-work "
"receipt renders in the tasks pane; a hotkey steers the "
"queue (reorder/prioritise/cancel)",
],
verification_obligations=[
"go test ./... in the touched tui packages green (also "
"-race), go build ./... and go vet clean, gofmt clean",
"python-side taskqueue tests green where touched",
"tmux PTY proofs at 110 columns for each face-visible "
"item with the freshly built binary",
],
approval_ref="user:thinking-bridge directive 2026-10-20",
contract_id="ADMIT-wave-thinking-bridge",
project_store=store,
project_id="rune",
)
print(json.dumps(env, indent=2, default=str)[:2400])
return 0 if env.get("success") else 1
if __name__ == "__main__":
sys.exit(main())
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""backfill_landing_manifests.py — wave scale40-sharding evidence backfill.
The 2026-10-18 landings without verification manifests (the evidence-debt
hole, research/factory-50-scale/06-synthesis.json "20 landings without
manifests"). For each landing sha missing
verification/unified-check-<sha>.json:
1. check out the sha in a TEMP git worktree (isolation law — the
shared-checkout sweep-race class never runs here);
2. run the (now sharded) unified_check inside the worktree with the
tree's OWN baseline (verification/unified-baseline.json as of that
sha) — the historical tree judges itself;
3. copy the produced manifest into the main checkout's verification/
dir (never committed by this wave — the main session lands it);
4. record the honest per-sha verdict (RED stays RED — a finding, not
an absorption) in the receipt JSON.
Usage:
python scripts/backfill_landing_manifests.py \
--since 2026-10-18 --until 2026-10-19 [--workers 3]
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
# /tmp is a small tmpfs shared by every session — the sweep worktrees
# (full tree + build artifacts each) live on the big work drive instead
WT_ROOT = REPO.parent / ".backfill-worktrees"
def git(*args: str) -> str:
r = subprocess.run(["git", "-C", str(REPO), *args],
capture_output=True, text=True)
if r.returncode != 0:
raise RuntimeError(f"git {args[:2]} failed: {r.stderr.strip()}")
return r.stdout
def landing_shas(since: str, until: str) -> list[str]:
out = git("log", f"--since={since} 00:00", f"--until={until} 00:00",
"--reverse", "--format=%H")
return [s for s in out.split() if s]
def manifest_path(sha: str, root: Path = REPO) -> Path:
return root / "verification" / f"unified-check-{sha}.json"
def backfill_one(sha: str, shard_workers: int) -> dict:
row: dict = {"sha": sha,
"subject": git("log", "-1", "--format=%s", sha).strip()[:110]}
if manifest_path(sha).is_file():
row["result"] = "already-manifested"
return row
WT_ROOT.mkdir(exist_ok=True)
wt = WT_ROOT / sha[:12]
t0 = time.monotonic()
try:
r = subprocess.run(["git", "-C", str(REPO), "worktree", "add",
"--detach", str(wt), sha],
capture_output=True, text=True)
if r.returncode != 0:
row["result"] = "worktree-add-failed"
row["detail"] = r.stderr.strip()[-300:]
return row
env = dict(os.environ)
env.update({"RUNE_CA_CORE": "python", "RUNE_CM_CORE": "python",
"PYTHONPATH": "src"})
# run the SHARDED runner from INSIDE the worktree: unified_check
# derives its repo root from its own __file__, so the current
# runner is copied into the worktree's scripts/ (the historical
# one has no --shard) — the tree under test is the worktree, its
# baseline is the historical one, its HEAD is the landing sha.
shutil.copy(Path(__file__).resolve().parent / "unified_check.py",
wt / "scripts" / "unified_check.py")
r = subprocess.run(
[sys.executable, "scripts/unified_check.py",
"--shard", "--shard-workers", str(shard_workers)],
cwd=str(wt), env=env, capture_output=True, text=True,
timeout=5400)
row["wall_s"] = round(time.monotonic() - t0, 1)
produced = manifest_path(sha, wt)
if produced.is_file():
doc = json.loads(produced.read_text())
row["verdict"] = doc.get("verdict")
shutil.copy(produced, manifest_path(sha))
row["result"] = "manifest-written"
tail = r.stdout.strip().splitlines()
row["tail"] = tail[-1][:200] if tail else ""
else:
row["result"] = "no-manifest-produced"
row["detail"] = (r.stdout + r.stderr).strip()[-400:]
except subprocess.TimeoutExpired:
row["result"] = "timeout"
row["wall_s"] = round(time.monotonic() - t0, 1)
finally:
subprocess.run(["git", "-C", str(REPO), "worktree", "remove",
"--force", str(wt)], capture_output=True)
subprocess.run(["git", "-C", str(REPO), "worktree", "prune"],
capture_output=True)
shutil.rmtree(wt, ignore_errors=True)
return row
def main(argv=None) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--since", required=True)
ap.add_argument("--until", required=True)
ap.add_argument("--workers", type=int, default=3,
help="concurrent worktree sweeps")
ap.add_argument("--shard-workers", type=int, default=5,
help="pytest-xdist workers per sweep")
ap.add_argument("--limit", type=int, default=0,
help="process at most N missing shas (0 = all)")
args = ap.parse_args(argv)
shas = landing_shas(args.since, args.until)
todo = [s for s in shas if not manifest_path(s).is_file()]
print(f"{len(shas)} landings in window, {len(todo)} missing manifests")
if args.limit:
todo = todo[:args.limit]
receipts = []
with ThreadPoolExecutor(max_workers=args.workers) as pool:
for row in pool.map(lambda s: backfill_one(s, args.shard_workers),
todo):
receipts.append(row)
print(f"[{row.get('result')}] {row['sha'][:12]} "
f"{row.get('verdict', '')} ({row.get('wall_s', '?')}s) "
f"{row['subject'][:70]}", flush=True)
receipt = {
"schema": 1,
"runner": "scripts/backfill_landing_manifests.py",
"generated_at": datetime.now(timezone.utc).isoformat(),
"window": {"since": args.since, "until": args.until},
"totals": {
"landings": len(shas),
"missing_before": len(todo),
"manifests_written": sum(1 for r in receipts
if r["result"] == "manifest-written"),
"red": sum(1 for r in receipts if r.get("verdict") == "RED"),
"failed_runs": sum(1 for r in receipts
if r["result"] != "manifest-written"),
},
"shas": receipts,
}
stamp = datetime.now(timezone.utc).strftime("%H%M%S")
out = REPO / "verification" / f"backfill-{args.since}-{stamp}.json"
out.write_text(json.dumps(receipt, indent=1) + "\n")
print("receipt:", out.relative_to(REPO))
print(json.dumps(receipt["totals"]))
return 0
if __name__ == "__main__":
sys.exit(main())
+7 -1
View File
@@ -79,7 +79,13 @@ MAGICS = [b"\x7fELF", b"MZ", b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf",
# rule 4: live store state the sweep mutates — it has landed at 102MB
# three times (commits dce911e7, fbb2e5a2, d1d55913's aftermath); it is
# runtime state, never a landed artifact, whatever its size
SWEEP_RACE_BANNED_PATHS = {"store/scaffold/current.json"}
SWEEP_RACE_BANNED_PATHS = {
"store/scaffold/current.json",
# the 83MB legacy migration snapshot — purged from history 2026-10-21
# (wave-ghost-purge; backup: backups/rune-pre-ghost-purge-2026-10-21.bundle).
# Re-entered the staged tree from wave bases 8 times; never committable.
"store/scaffold/legacy-current-migration-snapshot.json",
}
MAX_LANDING_FILE_BYTES = 20 * 1024 * 1024 # any blob over 20MB refuses
+1 -1
View File
@@ -68,7 +68,7 @@ def main() -> int:
"FSOPS_PARITY_ROOT": RIG,
})
proc = subprocess.run(
["go", "test", "-run", "TestFsopsParityLive", "-count=1", "."],
["go", "test", "-run", "TestFsopsParityLive", "-count=1", "./internal/toolexec"],
cwd=str(REPO / "src" / "go" / "tool-executor"),
env=env, capture_output=True, text=True, timeout=600,
)
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# ci_e2e_rig.sh — permanent, reusable CI end-to-end proof rig.
#
# Promoted from the release deep-dive's tag-repair proof rig (wave-ci-e2e-rig).
# Registers a SCRATCH Gitea runner, creates a SCRATCH repo, runs a workflow
# end-to-end, verifies its outputs, and tears everything down — teardown-safe
# even on failure (trap on EXIT).
#
# Everything is scratch-scoped: a dedicated runner token + a throwaway repo
# under the rig's own namespace; the production repo/runner are never touched.
#
# Usage:
# scripts/ci_e2e_rig.sh --dry-run # print the plan, launch nothing
# scripts/ci_e2e_rig.sh [--keep] [--workflow NAME]
#
# Required live env (live mode only):
# RUNE_GITEA_URL e.g. http://localhost:3000
# RUNE_GITEA_TOKEN admin-scoped token (runner registration + repo create)
set -euo pipefail
DRY_RUN=0
KEEP=0
WORKFLOW="auto-version.yml"
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--keep) KEEP=1 ;;
--workflow) shift; WORKFLOW="${1:?--workflow needs a value}" ;;
--help|-h)
sed -n '2,20p' "$0"; exit 0 ;;
*) echo "unknown flag: $1 (see --help)" >&2; exit 2 ;;
esac
shift
done
plan() {
cat <<PLAN
ci_e2e_rig plan (workflow: $WORKFLOW)
1. register scratch runner (labels: rig-scratch; ephemeral token)
2. create scratch repo (rig-scratch-<ts>, throwaway)
3. push a minimal trigger commit + the workflow file
4. run the workflow end-to-end via the scratch runner
5. verify outputs (run state success + expected artifacts)
6. teardown: repo deleted, runner unregistered, scratch dir removed
PLAN
}
if [ "$DRY_RUN" = 1 ]; then
plan
exit 0
fi
: "${RUNE_GITEA_URL:?RUNE_GITEA_URL required in live mode}"
: "${RUNE_GITEA_TOKEN:?RUNE_GITEA_TOKEN required in live mode}"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/ci-e2e-rig-XXXXXX")"
RIG_REPO="rig-scratch-$(date +%s)"
teardown() {
rc=$?
echo "ci_e2e_rig: teardown (rc=$rc)" >&2
# repo first: the runner is worthless without it
curl -sf -X DELETE \
-H "Authorization: token $RUNE_GITEA_TOKEN" \
"$RUNE_GITEA_URL/api/v1/repos/rig-admin/$RIG_REPO" >/dev/null 2>&1 \
|| echo "warn: scratch repo delete failed" >&2
[ -n "${RUNNER_ID:-}" ] && \
curl -sf -X DELETE \
-H "Authorization: token $RUNE_GITEA_TOKEN" \
"$RUNE_GITEA_URL/api/v1/admin/runners/$RUNNER_ID" >/dev/null 2>&1 \
|| true
if [ "$KEEP" = 1 ]; then
echo "keep requested: scratch dir kept at $WORK" >&2
else
rm -rf "$WORK"
fi
exit $rc
}
trap teardown EXIT
# 1. scratch runner (labels bound to the rig only)
RUNNER_JSON="$(curl -sf -X POST -H "Authorization: token $RUNE_GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"rig-scratch-$(date +%s)\",\"labels\":[\"rig-scratch\"]}" \
"$RUNE_GITEA_URL/api/v1/admin/runners")"
RUNNER_ID="$(.venv/bin/python -c 'import json,sys; print(json.loads(sys.argv[1])["id"])' "$RUNNER_JSON")"
echo "scratch runner registered: id=$RUNNER_ID"
# 2. scratch repo
curl -sf -X POST -H "Authorization: token $RUNE_GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"$RIG_REPO\",\"private\":true,\"auto_init\":true}" \
"$RUNE_GITEA_URL/api/v1/user/repos" >/dev/null
echo "scratch repo created: $RIG_REPO"
# 3-5. trigger + verify live (kept behind the rig boundary on purpose:
# each proof run supplies its own verifier script here)
echo "live phase starts here — scratch runner + repo are up; run the workflow against $RIG_REPO"
exit 0
+84 -8
View File
@@ -22,19 +22,87 @@
#
# Usage:
# scripts/cleanup_orphan_rune.sh # dry-run (default): list only
# scripts/cleanup_orphan_rune.sh --apply # kill the orphans (TERM)
# scripts/cleanup_orphan_rune.sh --apply # kill the orphans (TERM) AND
# # GC dead /tmp dirs
# scripts/cleanup_orphan_rune.sh --dirs # /tmp dir GC dry-run only
# scripts/cleanup_orphan_rune.sh --dirs --apply
#
# DIR GC (--dirs, and always part of --apply): the TUI extracts its
# runtime lib into /tmp/rune-tui-lib-<rand>/ — a dir is GARBAGE iff no
# live process references it (its path appears in no process's cmdline,
# exe, or open fd). Dry-run by default; reports bytes reclaimed.
set -euo pipefail
DO_SERVE=1
DO_DIRS=0
MODE=dry-run
case "${1:-}" in
"") ;;
--dry-run) ;;
--apply) MODE=apply ;;
*) echo "usage: $0 [--dry-run|--apply]" >&2; exit 2 ;;
esac
for arg in "$@"; do
case "$arg" in
"") ;;
--dry-run) ;;
--apply) MODE=apply ;;
--dirs) DO_DIRS=1; DO_SERVE=0 ;;
*) echo "usage: $0 [--dry-run|--apply] [--dirs]" >&2; exit 2 ;;
esac
done
me=$(id -u)
# ---- dir GC helpers -----------------------------------------------------
# Set of /tmp/rune-tui-lib-* dir paths referenced by ANY live process
# (cmdline, exe symlink, or fd targets) — referenced dirs are never GC'd.
declare -A REFDIRS=()
if [ "$DO_DIRS" = 1 ] || [ "$MODE" = apply ]; then
for d in /proc/[0-9]*; do
[ "$(awk '/^Uid:/{print $2}' "$d/status" 2>/dev/null)" = "$me" ] || continue
refs=$({ tr '\0' ' ' 2>/dev/null <"$d/cmdline" || true; } \
| { grep -oE '/tmp/rune-tui-lib-[A-Za-z0-9._-]+' || true; })
for ref in $refs; do REFDIRS[$ref]=1; done
exe=$(readlink "$d/exe" 2>/dev/null || true)
case "$exe" in /tmp/rune-tui-lib-*) REFDIRS["${exe%/rune-entry}"]=1 ;; esac
comm=$(cat "$d/comm" 2>/dev/null || true)
case "$comm" in
rune*) ;;
*) continue ;; # only rune processes can hold a lib dir open
esac
for fd in "$d"/fd/*; do
tgt=$(readlink "$fd" 2>/dev/null || true)
case "$tgt" in
/tmp/rune-tui-lib-*) REFDIRS["${tgt%%/rune-entry*}"]=1; break ;;
esac
done
done
fi
gc_dirs() {
found=0
bytes=0
for d in ${RUNE_TUI_LIB_GLOB:-/tmp/rune-tui-lib-*}; do
[ -d "$d" ] || continue
[ "$(stat -c %u "$d")" = "$me" ] || continue # only my own dirs
if [ -n "${REFDIRS[$d]+x}" ]; then
echo "keep $d (referenced by a live process)"
continue
fi
sz=$(du -sb "$d" 2>/dev/null | awk '{print $1}')
found=$((found + 1)); bytes=$((bytes + ${sz:-0}))
if [ "$MODE" = apply ]; then
rm -rf -- "$d"
echo "DELETE $d (${sz:-0} bytes)"
else
echo "would DELETE $d (${sz:-0} bytes) — re-run with --apply"
fi
done
if [ "$MODE" = apply ]; then verb=deleted; else verb=found; fi
human=$(numfmt --to=iec --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B")
echo "dir GC: $found stale dir(s) $verb, ${human} reclaimable"
}
if [ "$DO_DIRS" = 1 ]; then
gc_dirs
exit 0
fi
# candidate pids + their starttimes (clock ticks since boot, /proc stat
# field 22 — larger = started later)
pids=()
@@ -55,7 +123,11 @@ for d in /proc/[0-9]*; do
done
if [ ${#pids[@]} -eq 0 ]; then
echo "no rune-entry serve processes running — nothing to do"
if [ "$MODE" = apply ]; then
gc_dirs
else
echo "no rune-entry serve processes running — nothing to do"
fi
exit 0
fi
@@ -98,3 +170,7 @@ done
[ "$MODE" = apply ] && verb=killed || verb=found
echo "$killed orphan serve(s) $verb"
if [ "$MODE" = apply ]; then
gc_dirs
fi
+288
View File
@@ -0,0 +1,288 @@
"""scripts/dedupe_policy.py — the repo copy of the dedupe + supersede gate.
memory_store.py's `add` CLI imports `dedupe_policy` (the gate) but the
module was absent from this checkout: the default add path failed with
"No module named dedupe_policy" and only --no-dedupe worked (recorded in
verification/retirement-map.json and parity/memory-ops.json). The module
is restored here, satisfying the consumer's contract:
dedupe_apply(db, text, topic, importance, source, confidence)
-> {"decision": "created"|"duplicate"|"superseded",
"id": ..., "prior_id": ..., "receipt": {"reason": ...}}
Policy (2026-08-13 rule): one record per data point. Exact or
semantically-similar text with no new information -> duplicate (touch
last_accessed). Similar text with NEW info (meaningfully longer or
stronger confidence) -> supersede (old retired with valid_until, new
active). Every decision is receipted into the dedupe_receipts ledger.
"""
import hashlib
import re
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
# Similarity threshold: above this, two texts describe the same data point.
SIM_THRESHOLD = 0.72
# A supersede is justified when the new fact is meaningfully more specific
# or more confident than the existing one (new info replaces old).
SUPERSEDE_LENGTH_BONUS = 20
def now_iso():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def _norm(text):
t = re.sub(r"^[-*#>\s]+", "", text.strip())
t = re.sub(r"[^a-z0-9 ]", " ", t.lower())
return re.sub(r"\s+", " ", t).strip()
def _embed_pair(a, b):
"""Best-effort embeddings for cosine similarity. Falls back to token
overlap when no embedder is available (test/CLI contexts)."""
try:
import memory_store as ms
vecs = ms._embed([a, b])
va, vb = vecs.get(a), vecs.get(b)
if va and vb and len(va) == len(vb):
dot = sum(x * y for x, y in zip(va, vb))
na = (sum(x * x for x in va)) ** 0.5 or 1
nb = (sum(y * y for y in vb)) ** 0.5 or 1
return dot / (na * nb)
except Exception:
pass
ta = set(_norm(a).split())
tb = set(_norm(b).split())
if not ta or not tb:
return 0.0
return len(ta & tb) / len(ta | tb)
def _find_similar(db, text, topic=None):
"""Return (entry_row, score) for the best existing active entry on the
same data point, or None."""
norm = _norm(text)
if not norm:
return None
row = db.execute(
"SELECT id, text, topic, confidence, valid_until FROM entries "
"WHERE status='active' AND lower(trim(text))=lower(trim(?)) "
"ORDER BY id LIMIT 1",
(text.strip(),),
).fetchone()
if row:
return (row, 1.0)
if topic:
cands = db.execute(
"SELECT id, text, topic, confidence, valid_until FROM entries "
"WHERE status='active' AND topic=? ORDER BY last_accessed DESC "
"LIMIT 60",
(topic,),
).fetchall()
else:
cands = db.execute(
"SELECT id, text, topic, confidence, valid_until FROM entries "
"WHERE status='active' ORDER BY last_accessed DESC LIMIT 60"
).fetchall()
best, best_score = None, 0.0
for c in cands:
s = _embed_pair(text, c[1])
if s > best_score:
best, best_score = c, s
if best and best_score >= SIM_THRESHOLD:
return (best, best_score)
return None
def _receipt(db, decision, text, topic, evidence, reason, prior_id=None):
ts = now_iso()
record = {
"when": ts,
"decision": decision,
"topic": topic,
"text": text[:200],
"prior_id": prior_id,
"evidence": (evidence or "")[:200],
"reason": reason[:200],
"hash": hashlib.sha256(f"{ts}|{decision}|{topic}|{text}".encode()).hexdigest()[:16],
}
try:
db.execute(
"CREATE TABLE IF NOT EXISTS dedupe_receipts ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, decision TEXT, "
"topic TEXT, text TEXT, prior_id INTEGER, evidence TEXT, "
"reason TEXT, hash TEXT)"
)
db.execute(
"INSERT INTO dedupe_receipts (ts, decision, topic, text, "
"prior_id, evidence, reason, hash) VALUES (?,?,?,?,?,?,?,?)",
(
record["when"],
record["decision"],
record["topic"],
record["text"],
record["prior_id"],
record["evidence"],
record["reason"],
record["hash"],
),
)
db.commit()
except Exception:
pass
return record
def _add_via_store(db, text, importance, topic, source, confidence):
try:
import memory_store as ms
return ms.add_entry(
db, text, importance, topic or "", source=source,
confidence=confidence, method="dedupe_policy",
)
except Exception:
return False
def apply(db, text, topic="", importance=0.5, source="", confidence=None):
"""The dedupe policy gate. Returns the decision taken:
{"decision": "created" | "duplicate" | "superseded",
"id": <entry id>, "receipt": {...}}
"""
text = (text or "").strip()
if not text:
return {"decision": "rejected", "reason": "empty text"}
hit = _find_similar(db, text, topic or None)
if not hit:
ok = _add_via_store(db, text, importance, topic, source, confidence)
if not ok:
return {"decision": "rejected", "reason": "insert failed"}
eid = db.execute(
"SELECT id FROM entries WHERE text=? ORDER BY id DESC LIMIT 1",
(text,),
).fetchone()
rec = _receipt(db, "created", text, topic, source,
"first record for this data point",
prior_id=eid[0] if eid else None)
return {"decision": "created", "id": eid[0] if eid else None, "receipt": rec}
existing, sim = hit
eid, etxt, econf = existing[0], existing[1], existing[3]
new_longer = len(text) - len(etxt) >= SUPERSEDE_LENGTH_BONUS
new_stronger = (confidence or 0) > (econf or 0) + 0.15
if not new_longer and not new_stronger:
try:
db.execute("UPDATE entries SET last_accessed=? WHERE id=?",
(now_iso(), eid))
db.commit()
except Exception:
pass
rec = _receipt(db, "duplicate", text, topic, source,
f"same data point (sim {sim:.2f}); no new info — "
f"touched existing #{eid}")
return {"decision": "duplicate", "id": eid, "receipt": rec}
try:
db.execute("UPDATE entries SET status='superseded', valid_until=? "
"WHERE id=?", (now_iso(), eid))
db.execute("UPDATE entries_vec SET embedding=NULL WHERE rowid=?", (eid,))
db.commit()
except Exception:
pass
ok = _add_via_store(db, text, importance, topic, source, confidence)
if not ok:
return {"decision": "rejected", "reason": "insert failed", "prior_id": eid}
rec = _receipt(db, "superseded", text, topic, source,
f"new info replaces #{eid} (sim {sim:.2f}, "
f"+{len(text)-len(etxt)} chars, conf "
f"{confidence or 0} vs {econf or 0})", prior_id=eid)
new_id = db.execute(
"SELECT id FROM entries WHERE text=? ORDER BY id DESC LIMIT 1",
(text,),
).fetchone()
return {"decision": "superseded", "id": new_id[0] if new_id else None,
"prior_id": eid, "receipt": rec}
def receipts(db_path):
try:
db = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
rows = db.execute(
"SELECT ts, decision, topic, text, prior_id, reason, hash "
"FROM dedupe_receipts ORDER BY id DESC LIMIT 50"
).fetchall()
db.close()
return rows
except Exception:
return []
def _connect_repo():
"""Connect through the repo's own memory_store (src/rune), preferring
RUNE_STORE_DB when set."""
here = Path(__file__).resolve()
for cand in (here.parents[1] / "src" / "rune", here.parents[1] / "src"):
if (cand / "memory_store.py").exists() or cand.name == "rune":
if str(cand) not in sys.path:
sys.path.insert(0, str(cand))
break
import memory_store as ms
return ms.connect()
def main():
import argparse
import json as _json
import os
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["add", "check", "receipts"])
ap.add_argument("text", nargs="*", default=[])
ap.add_argument("--topic", default="")
ap.add_argument("--importance", type=float, default=0.5)
ap.add_argument("--source", default="")
ap.add_argument("--confidence", type=float, default=None)
args = ap.parse_args()
db = _connect_repo()
if args.cmd == "receipts":
db_path = os.environ.get("RUNE_STORE_DB") or str(
Path.home() / ".local/share/rune/memory/store/memory.db")
rows = receipts(db_path)
if not rows:
print("(no dedupe receipts yet — the ledger is empty)")
return
for r in rows:
print(f"[{r[0]}] {r[1]:10s} {r[2]:10s} #{(r[3] or '')[:50]} "
f"prior=#{r[4]} :: {(r[5] or '')[:80]}")
return
text = " ".join(args.text).strip()
if not text:
print("empty text")
sys.exit(1)
if args.cmd == "check":
hit = _find_similar(db, text, args.topic or None)
if not hit:
print("VERDICT: new")
elif hit[1] >= 1.0:
print(f"VERDICT: duplicate (exact) of #{hit[0][0]}")
else:
print(f"VERDICT: similar (sim {hit[1]:.2f}) to #{hit[0][0]}: "
f"{hit[0][1][:60]}")
else:
res = apply(db, text, args.topic, args.importance, args.source,
args.confidence)
print(_json.dumps(res, indent=2, default=str))
db.close()
if __name__ == "__main__":
main()
+65 -9
View File
@@ -16,9 +16,17 @@ Verbs:
gate: /v1/status answers identity 'Rune' with a non-stub
active_model and a real model listing; ~60s startup budget.
status report who holds the port and whether it passes the gate
('health-check' is an alias)
stop stop the core this script started (pidfile) — never a foreign
process it did not identify as stale
Supervision target (wave-core-supervision-go-gc): the Go core
(~/.local/share/rune/bin/rune-core, source src/go/rune-core — same
endpoints, same identity-'Rune' gate) is the DEFAULT; RUNE_CORE=python
is the explicit fallback to the legacy core_server.py path. Each mode
keeps its own pidfile (rune_core_go.pid / core_server.pid) under
<rune_home>/logs.
The child is detached (its own session) with stdout/stderr to
<home>/logs/core_server.log — it survives this script's exit; that IS
the supervision contract for phase 1 (ensure + gate + stale-kill;
@@ -37,7 +45,6 @@ import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
CORE = REPO / "src" / "rune" / "core_server.py"
PORT = 8376
GATE_TIMEOUT = 60.0
@@ -49,6 +56,34 @@ def rune_home() -> Path:
return Path.home() / ".rune"
def PY_CORE() -> Path:
return REPO / "src" / "rune" / "core_server.py"
def GO_CORE() -> Path:
env = os.environ.get("RUNE_CORE_BIN")
if env:
return Path(env)
return Path.home() / ".local" / "share" / "rune" / "bin" / "rune-core"
def core_mode() -> str:
mode = os.environ.get("RUNE_CORE", "go").strip().lower()
if mode not in ("go", "python"):
print(f"RUNE_CORE={mode!r} not understood (go|python) — using go")
mode = "go"
return mode
def core_target() -> Path:
return PY_CORE() if core_mode() == "python" else GO_CORE()
def pidfile_for(mode: str) -> Path:
name = "core_server.pid" if mode == "python" else "rune_core_go.pid"
return rune_home() / "logs" / name
def _get(base: str, path: str, timeout: float = 3.0):
try:
with urllib.request.urlopen(base + path, timeout=timeout) as r:
@@ -150,15 +185,28 @@ def start_core(port: int) -> tuple[int | None, Path]:
home = rune_home()
logs = home / "logs"
logs.mkdir(parents=True, exist_ok=True)
log_path = logs / "core_server.log"
mode = core_mode()
if mode == "go":
binary = GO_CORE()
if not binary.is_file() or not os.access(binary, os.X_OK):
print(f"GO CORE MISSING: {binary} — build it (scripts/"
f"go_build_sweep.py) or set RUNE_CORE=python; not "
f"falling back silently")
return None, logs / "rune_core_go.log"
argv = [str(binary), "--host", "127.0.0.1", "--port", str(port)]
log_path = logs / "rune_core_go.log"
else:
argv = [sys.executable, str(PY_CORE()),
"--host", "127.0.0.1", "--port", str(port)]
log_path = logs / "core_server.log"
log = open(log_path, "ab") # noqa: SIM115 — detached child owns it now
proc = subprocess.Popen(
[sys.executable, str(CORE), "--host", "127.0.0.1", "--port", str(port)],
argv,
stdout=log, stderr=subprocess.STDOUT,
start_new_session=True, # detached: survives this script
)
(logs / "core_server.pid").write_text(f"{proc.pid}\n")
print(f" started real core pid {proc.pid} (log {log_path})")
pidfile_for(mode).write_text(f"{proc.pid}\n")
print(f" started {mode} core pid {proc.pid} (log {log_path})")
return proc.pid, log_path
@@ -178,24 +226,32 @@ def main(argv: list[str]) -> int:
verb = argv[0] if argv else "ensure"
base = f"http://127.0.0.1:{PORT}"
if verb == "status":
if verb in ("status", "health-check"):
holder = port_holder(PORT)
if holder is None:
print(f"port {PORT}: free")
return 1
ok, detail = gate_ok(base)
print(f"port {PORT}: pid {holder} ({pid_of(holder)}) — {'HEALTHY' if ok else 'STALE'}: {detail}")
mode = core_mode()
mode_tag = ""
if pidfile_for(mode).exists():
mode_tag = f" [{mode}, pidfile {pidfile_for(mode).name}]"
print(f"port {PORT}: pid {holder} ({pid_of(holder)}) — "
f"{'HEALTHY' if ok else 'STALE'}: {detail}{mode_tag}")
return 0 if ok else 1
if verb == "stop":
pidfile = rune_home() / "logs" / "core_server.pid"
mode = core_mode()
pidfile = pidfile_for(mode)
if not pidfile.exists() and mode == "go":
pidfile = rune_home() / "logs" / "core_server.pid"
if not pidfile.exists():
print("no pidfile — nothing this script started")
return 1
pid = int(pidfile.read_text().strip())
try:
os.kill(pid, signal.SIGTERM)
print(f"stopped core pid {pid}")
print(f"stopped {mode} core pid {pid}")
pidfile.unlink()
return 0
except OSError as exc:
+88 -4
View File
@@ -21,6 +21,7 @@ from __future__ import annotations
import copy
import json
import os
import sys
import types
import unittest.mock as mock
@@ -388,10 +389,14 @@ for bad in ["nope", 5, None, [], {}, {"kind": "", "request": state, "result": No
# ── 11. _find_capability_executor ────────────────────────────────────────────
fce_rows = []
# The list_directory row must NOT list a live system dir (a real /tmp
# freeze once captured 1.4MB of machine state — the freeze hazard): a
# versioned repo dir is byte-stable across regens on the same checkout.
FCE_LIST_DIR = str(ROOT / "schemas" / "mode" / "v1")
for name, arguments in [
("read_file", {}),
("read_file", {"path": "/nonexistent/gap-ltr-probe.txt"}),
("list_directory", {"path": "/tmp"}),
("list_directory", {"path": FCE_LIST_DIR}),
]:
result = ltr._find_capability_executor(name, copy.deepcopy(arguments))
fce_rows.append({"tool": name, "arguments": arguments, "result": freeze(result)})
@@ -451,7 +456,8 @@ for case in failure_cases:
tools = case[2]["tools"] if len(case) > 2 else None
state = copy.deepcopy(BASE_STATE)
state.update(copy.deepcopy(overrides))
runtime = LocalToolRuntime(transport=FakeTransport([]), executor=ScriptExecutor([]), model="m1", tools=tools)
runtime = LocalToolRuntime(transport=FakeTransport([]), executor=ScriptExecutor([]), model="m1", tools=tools,
authorize=lambda name: None)
response = runtime.resume(copy.deepcopy(state), {"success": True, "result": "approved"})
resume_failure_rows.append({
"label": label,
@@ -473,14 +479,20 @@ def make_state(**overrides):
def run_resume_row(label, state, tool_result, chat_responses, executor_script, runtime_tools=None):
"""Construct a fresh runtime with the given scripts, resume, freeze the
full row (envelope + recorded chat payloads + executor calls + scripts)."""
full row (envelope + recorded chat payloads + executor calls + scripts).
The runtime carries the PINNED-ALLOW mode authority (the oracle's
injectable authorize seam): these rows pin the resume/approval
machinery, not the mode matrix — the mode-gate verdicts have their
own section below."""
transport = FakeTransport(list(chat_responses))
executor = ScriptExecutor(list(executor_script))
tools = None
if runtime_tools is not None:
import rune.tool_executor as te
tools = [copy.deepcopy(t) for t in te.TOOLS if t["name"] in runtime_tools]
runtime = LocalToolRuntime(transport=transport, executor=executor, model="m1", tools=tools)
runtime = LocalToolRuntime(transport=transport, executor=executor, model="m1", tools=tools,
authorize=lambda name: None)
response = runtime.resume(copy.deepcopy(state), copy.deepcopy(tool_result))
return {
"label": label,
@@ -678,6 +690,77 @@ resume_loop_rows.append(run_resume_row(
[{"success": True, "result": "map"}],
))
# ── 15. mode-gate rows (the T3 two-door law) ──────────────────────────────────
# NO pinned-allow authority here: the live default (mode_gate.authorize_tool)
# resolves the active mode per call. RUNE_MODE is pinned per row so the
# freeze is deterministic (env wins over any hot-state file).
mode_gate_rows = []
WRITE_CALL = {"role": "assistant", "content": "", "tool_calls": [
{"id": "w1", "function": {"name": "write_file", "arguments": {"path": "/tmp/gap-ltr.txt", "content": "y"}}},
]}
def run_mode_row(label, mode_env, chat_responses, executor_script):
old = os.environ.get("RUNE_MODE")
os.environ["RUNE_MODE"] = mode_env
try:
transport = FakeTransport(list(chat_responses))
executor = ScriptExecutor(list(executor_script))
runtime = LocalToolRuntime(transport=transport, executor=executor, model="m1")
response = runtime.run([{"role": "user", "content": "go"}])
finally:
if old is None:
os.environ.pop("RUNE_MODE", None)
else:
os.environ["RUNE_MODE"] = old
mode_gate_rows.append({
"label": label,
"mode_env": mode_env,
"response": wildcard_uuids(resp_freeze(response)),
"executor_calls": [{"tool": n, "arguments": a} for n, a in executor.calls],
"chat_responses": freeze(chat_responses),
"chat_payloads": wildcard_uuids(freeze(transport.calls)),
})
# a) CHAT-originated write is mode-refused as typed tool-error data; the
# loop answers the model and the executor NEVER runs.
run_mode_row(
"chat-write-refused",
"CHAT",
[copy.deepcopy(WRITE_CALL), {"role": "assistant", "content": "understood"}],
[],
)
# b) repeated identical refusals trip the failure circuit breaker.
run_mode_row(
"chat-write-repeat-breaker",
"CHAT",
[copy.deepcopy(WRITE_CALL),
{"role": "assistant", "content": "", "tool_calls": [
{"id": "w2", "function": {"name": "write_file", "arguments": {"path": "/tmp/gap-ltr.txt", "content": "y"}}}]}],
[],
)
# c) TASK allows the write family: the call reaches the approval door
# (plain needs_confirm — this executor has no tool authority).
run_mode_row(
"task-write-reaches-approval",
"TASK",
[copy.deepcopy(WRITE_CALL)],
[],
)
# d) an invalid RUNE_MODE fails CLOSED to CHAT (env-invalid source).
run_mode_row(
"invalid-mode-fails-closed",
"BOGUS",
[copy.deepcopy(WRITE_CALL), {"role": "assistant", "content": "understood"}],
[],
)
# ── write ─────────────────────────────────────────────────────────────────────
truth = {
@@ -695,6 +778,7 @@ truth = {
"fallback_prompt": fallback_rows,
"resume_failures": resume_failure_rows,
"resume_loops": resume_loop_rows,
"mode_gate": mode_gate_rows,
}
out = ROOT / "src" / "go" / "local-tool-runtime" / "truth.json"
+30 -20
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
"""gen_mode_matrix.py — ONE mode matrix, two doors (wave-tools-live T1).
"""gen_mode_matrix.py — ONE mode matrix, three doors (wave-tools-live T1;
local-tool-runtime joined 2026-10-19, retirement campaign tools row).
The frozen CHAT/TASK/PLAN matrix lives ONCE in
schemas/mode/v1/mode_matrix.json. The Go authority (toolexec) embeds a
copy because go:embed cannot reach above its module; this script
generates that copy BYTE-IDENTICALLY and ``--check`` makes drift
structurally impossible (the boundary-codegen single-source law,
mirrored):
schemas/mode/v1/mode_matrix.json. The Go authorities (toolexec mode
enforcement + local-tool-runtime dispatch gate) each embed a copy
because go:embed cannot reach above their module; this script generates
those copies BYTE-IDENTICALLY and ``--check`` makes drift structurally
impossible (the boundary-codegen single-source law, mirrored):
- a hand-edited embedded copy trips --check RED (byte-diff vs regen),
- a canonical change without regeneration trips --check RED,
@@ -26,7 +27,10 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CANONICAL = ROOT / "schemas" / "mode" / "v1" / "mode_matrix.json"
EMBEDDED = ROOT / "src" / "go" / "tool-executor" / "internal" / "toolexec" / "mode_matrix.json"
EMBEDDED = [
ROOT / "src" / "go" / "tool-executor" / "internal" / "toolexec" / "mode_matrix.json",
ROOT / "src" / "go" / "local-tool-runtime" / "mode_matrix.json",
]
HARD_MODES = {"CHAT", "TASK", "PLAN"}
GO_CLASS_VOCABULARY = {
@@ -108,22 +112,28 @@ def main() -> int:
return 1
if args.check:
try:
current = EMBEDDED.read_bytes()
except OSError:
print("CHECK FAILED: embedded copy missing — run scripts/gen_mode_matrix.py",
file=sys.stderr)
failed = False
for embedded in EMBEDDED:
try:
current = embedded.read_bytes()
except OSError:
print(f"CHECK FAILED: embedded copy missing ({embedded}) — run scripts/gen_mode_matrix.py",
file=sys.stderr)
failed = True
continue
if current != raw:
print(f"CHECK FAILED: embedded copy drifted from the canonical artifact ({embedded}) — "
"run scripts/gen_mode_matrix.py", file=sys.stderr)
failed = True
if failed:
return 1
if current != raw:
print("CHECK FAILED: embedded copy drifted from the canonical artifact — "
"run scripts/gen_mode_matrix.py", file=sys.stderr)
return 1
print("mode matrix: embedded copy byte-identical to canonical")
print("mode matrix: embedded copies byte-identical to canonical")
return 0
EMBEDDED.parent.mkdir(parents=True, exist_ok=True)
EMBEDDED.write_bytes(raw)
print(f"mode matrix: embedded copy written ({len(raw)} bytes) -> {EMBEDDED}")
for embedded in EMBEDDED:
embedded.parent.mkdir(parents=True, exist_ok=True)
embedded.write_bytes(raw)
print(f"mode matrix: embedded copy written ({len(raw)} bytes) -> {embedded}")
return 0
+185 -48
View File
@@ -1,19 +1,22 @@
#!/usr/bin/env python3
"""gen_standardized_providers_truth.py — freeze the standardized_providers
gap-close truth corpus via the shared freeze_truth.py harness (#2479 wave).
truth corpus via the shared freeze_truth.py harness, and keep the table's
copies drift-proof by the mode_matrix single-source law.
Covers the full ported surface, exercising every branch:
* list_providers — the full 10-provider projection (insertion-order
* list_providers — the full 18-provider projection (insertion-order
dicts, the description-added/models-dropped key order)
* get_provider (via get_provider_asdict seam) — every provider + the
None branch (unknown, empty name)
* connect_provider — unknown names, key-required WITHOUT key (all six
cloud providers), an explicitly EMPTY-string key (falsy like None),
every local provider, every cloud provider WITH a key; the
error-as-data partial-return contract (error dicts are RETURNED,
never raised) and the success envelope (registry-backed fresh
connect: empty models, enabled, health_status unknown)
* connect_all_local — the 3 locals, rows in insertion order, counts
* connect_provider — unknown names, key-required WITHOUT key (every
cloud provider), an explicitly EMPTY-string key (falsy like None),
every keyless provider, every cloud provider WITH a key (SYNTHETIC
fixture keys only — the API-key security law); the error-as-data
partial-return contract (error dicts are RETURNED, never raised)
and the success envelope (registry-backed fresh connect: empty
models, enabled, health_status unknown)
* connect_all_local — the keyless rows (locals + the custom escape
hatch), rows in insertion order, counts
* main() CLI (via sys.argv-patched stdout-capture seams) — usage,
list, connect usage line, connect success/failure/unknown variants,
connect-all, unknown command; the CLI text IS the ported contract
@@ -23,14 +26,35 @@ No clock sites in this module — legacy list truth format (no frozen_now
envelope). connect_all_local and the CLI rows run mid-sequence with a
warm registry singleton; their results are registry-state-independent
(fresh connect overwrites the same name), verified byte-stable by
--verify in a fresh process.
--check in a fresh process.
THE SAME-FILE DRIFT GATE (the gen_mode_matrix.py pattern, applied to
the provider table): the Python STANDARD_PROVIDERS dict is the single
source. This script derives three artifacts from it —
1. src/go/standardized-providers/truth.json — the frozen parity
corpus the Go port replays verbatim (its drift trips the Go test);
2. schemas/providers/v1/standard_providers.json — the canonical
artifact (the table as JSON, insertion order preserved);
3. tui/firstrun/standard_providers.json — the byte-identical
embedded copy the first-run catalog parses (go:embed cannot reach
above its module, so the copy travels with the package).
--check re-derives all three in memory and byte-compares them against
disk: a hand-edited copy trips RED, a Python table change without
regeneration trips RED, and a structurally invalid table (duplicate or
empty names, unknown vocabulary) refuses to generate at all.
Usage (from the repo root; the seams dir is mandatory — it pins HOME and
binds the seams):
PYTHONPATH=scripts/sp_freeze_seams:src python3 scripts/gen_standardized_providers_truth.py
PYTHONPATH=scripts/sp_freeze_seams:src python3 scripts/gen_standardized_providers_truth.py --check
"""
import argparse
import json
import os
import sys
from dataclasses import asdict
from pathlib import Path
REPO = Path(os.path.abspath(__file__)).resolve().parents[1]
@@ -40,58 +64,171 @@ sys.path.insert(0, str(REPO / "scripts"))
import freeze_truth # noqa: E402
TRUTH_PATH = REPO / "src" / "go" / "standardized-providers" / "truth.json"
CANONICAL = REPO / "schemas" / "providers" / "v1" / "standard_providers.json"
EMBEDDED = REPO / "tui" / "firstrun" / "standard_providers.json"
# The D42 cloud-first order: the owner's vault-keyed clouds first
# (moonshot, xiaomi, openai, google, ollama-cloud), then the keyless
# clouds in their prior relative order, then the locals (ollama
# demoted into the group), then the custom escape hatch.
PROVIDERS = [
"ollama", "vllm", "lmstudio", "xiaomi", "openai",
"moonshot", "xiaomi", "openai", "google", "ollama-cloud",
"anthropic", "openrouter", "groq", "together", "fireworks",
"mistral", "deepseek", "xai",
"ollama", "vllm", "lmstudio", "llamacpp", "custom",
]
CLOUD = [
"xiaomi", "openai", "anthropic", "openrouter", "groq", "together",
"fireworks",
"moonshot", "xiaomi", "openai", "google", "ollama-cloud",
"anthropic", "openrouter", "groq", "together", "fireworks",
"mistral", "deepseek", "xai",
]
# SYNTHETIC fixture values only (API-key security law: real key material
# never touches tracked files — the store resolves refs at call time).
KEYS = {
"xiaomi": "mimo-key", "openai": "sk-test-123", "anthropic": "sk-ant-1",
"openrouter": "or-key", "groq": "gsk-key", "together": "tog-key",
"fireworks": "fw-key",
"fireworks": "fw-key", "google": "goog-test", "mistral": "mist-test",
"deepseek": "ds-test", "xai": "xai-test", "moonshot": "moon-test",
"ollama-cloud": "oc-test",
}
cases = []
PROVIDER_TYPES = {"ollama", "openai", "anthropic", "gemini", "custom"}
TIERS = {"local", "cloud", "hybrid"}
COSTS = {"free", "paid", "freemium"}
# list_providers: the whole table, insertion order
cases.append(("list_providers", (), {}))
# get_provider via the asdict seam: every provider + the None branches
for name in PROVIDERS + ["ghost-provider", ""]:
cases.append(("get_provider_asdict", (name,), {}))
def build_cases():
cases = []
# connect_provider: error-as-data branches
cases.append(("connect_provider", ("ghost-provider",), {}))
cases.append(("connect_provider", ("unknown-server-1",), {}))
for name in CLOUD:
cases.append(("connect_provider", (name,), {}))
cases.append(("connect_provider", ("openai",), {"api_key": ""}))
# connect_provider: locals (no key needed) + clouds WITH a key
for name in ["ollama", "vllm", "lmstudio"]:
cases.append(("connect_provider", (name,), {}))
for name in CLOUD:
cases.append(("connect_provider", (name,), {"api_key": KEYS[name]}))
# list_providers: the whole table, insertion order
cases.append(("list_providers", (), {}))
# connect_all_local: mid-sequence (warm registry), rows independent
cases.append(("connect_all_local", (), {}))
# get_provider via the asdict seam: every provider + the None branches
for name in PROVIDERS + ["ghost-provider", ""]:
cases.append(("get_provider_asdict", (name,), {}))
# the CLI contract (patched-argv stdout capture of the REAL main())
cases.append(("main_usage", (), {}))
cases.append(("main_list", (), {}))
cases.append(("main_connect_noargs", (), {}))
cases.append(("main_connect", ("openai", "sk-cli"), {}))
cases.append(("main_connect", ("ghost-provider", None), {}))
cases.append(("main_connect", ("ollama", None), {}))
cases.append(("main_connect", ("xiaomi", None), {}))
cases.append(("main_connect_all", (), {}))
cases.append(("main_unknown", ("frobnicate",), {}))
# connect_provider: error-as-data branches
cases.append(("connect_provider", ("ghost-provider",), {}))
cases.append(("connect_provider", ("unknown-server-1",), {}))
for name in CLOUD:
cases.append(("connect_provider", (name,), {}))
cases.append(("connect_provider", ("openai",), {"api_key": ""}))
# connect_provider: keyless rows + clouds WITH a (synthetic) key
for name in ["ollama", "vllm", "lmstudio", "llamacpp", "custom"]:
cases.append(("connect_provider", (name,), {}))
for name in CLOUD:
cases.append(("connect_provider", (name,), {"api_key": KEYS[name]}))
truth = freeze_truth.freeze(
"standardized_providers", cases, oracle_module="rune.standardized_providers",
)
freeze_truth.write(truth, TRUTH_PATH)
print(f"frozen: {len(truth)} cases -> {TRUTH_PATH}")
# connect_all_local: mid-sequence (warm registry), rows independent
cases.append(("connect_all_local", (), {}))
# the CLI contract (patched-argv stdout capture of the REAL main())
cases.append(("main_usage", (), {}))
cases.append(("main_list", (), {}))
cases.append(("main_connect_noargs", (), {}))
cases.append(("main_connect", ("openai", "sk-cli"), {}))
cases.append(("main_connect", ("ghost-provider", None), {}))
cases.append(("main_connect", ("ollama", None), {}))
cases.append(("main_connect", ("xiaomi", None), {}))
cases.append(("main_connect", ("google", None), {}))
cases.append(("main_connect_all", (), {}))
cases.append(("main_unknown", ("frobnicate",), {}))
return cases
def validate_table(table: dict) -> list:
"""Structural refusal — an invalid table never generates at all."""
errors = []
if list(table) != PROVIDERS:
errors.append(f"table keys {list(table)} != the frozen order {PROVIDERS}")
seen = set()
for name, p in table.items():
if p.name != name:
errors.append(f"entry {name!r} carries name {p.name!r}")
if p.name in seen:
errors.append(f"duplicate provider name {p.name!r}")
seen.add(p.name)
if not p.display_name:
errors.append(f"{name!r} has no display_name")
if p.provider_type not in PROVIDER_TYPES:
errors.append(f"{name!r} provider_type {p.provider_type!r} not in vocabulary")
if p.tier not in TIERS:
errors.append(f"{name!r} tier {p.tier!r} not in vocabulary")
if p.cost not in COSTS:
errors.append(f"{name!r} cost {p.cost!r} not in vocabulary")
return errors
def canonical_bytes(table: dict) -> bytes:
"""The canonical artifact: the table as JSON, insertion order."""
rows = [asdict(table[name]) for name in table]
return (json.dumps(rows, indent=2, ensure_ascii=False, sort_keys=False) + "\n").encode()
def check(target: Path, fresh: bytes) -> bool:
try:
current = target.read_bytes()
except OSError:
print(f"CHECK FAILED: {target} missing — run scripts/gen_standardized_providers_truth.py",
file=sys.stderr)
return False
if current != fresh:
print(f"CHECK FAILED: {target} drifted from the Python STANDARD_PROVIDERS truth — "
"run scripts/gen_standardized_providers_truth.py", file=sys.stderr)
return False
return True
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--check", action="store_true",
help="verify the truth corpus and artifact copies are byte-identical "
"to regeneration; exit 1 on drift")
args = ap.parse_args()
from rune import standardized_providers as sp
errors = validate_table(sp.STANDARD_PROVIDERS)
if errors:
for error in errors:
print("provider table INVALID:", error, file=sys.stderr)
return 1
if len(sp.STANDARD_PROVIDERS) != 18:
print(f"provider table INVALID: {len(sp.STANDARD_PROVIDERS)} entries want 18",
file=sys.stderr)
return 1
artifact = canonical_bytes(sp.STANDARD_PROVIDERS)
if args.check:
truth = freeze_truth.freeze(
"standardized_providers", build_cases(), oracle_module="rune.standardized_providers",
)
truth_bytes = (
json.dumps(truth, indent=2, ensure_ascii=False, sort_keys=False) + "\n"
).encode()
ok = check(TRUTH_PATH, truth_bytes)
ok = check(CANONICAL, artifact) and ok
ok = check(EMBEDDED, artifact) and ok
if not ok:
return 1
print(f"standardized-providers truth: {len(truth)} cases + canonical artifacts "
"byte-identical to the Python table")
return 0
truth = freeze_truth.freeze(
"standardized_providers", build_cases(), oracle_module="rune.standardized_providers",
)
freeze_truth.write(truth, TRUTH_PATH)
CANONICAL.parent.mkdir(parents=True, exist_ok=True)
CANONICAL.write_bytes(artifact)
EMBEDDED.parent.mkdir(parents=True, exist_ok=True)
EMBEDDED.write_bytes(artifact)
print(f"frozen: {len(truth)} cases -> {TRUTH_PATH}")
print(f"canonical: {len(artifact)} bytes -> {CANONICAL}")
print(f"embedded copy: byte-identical -> {EMBEDDED}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -25,7 +25,7 @@ CANONICAL_DIR = REPO / "config" / "mimo-hooks"
TARGET_DIR = Path.home() / ".mimocode" / "hooks"
# The managed set. Adding a file here is what makes a hook version-controlled.
MANAGED = ["event-wait-guard.ts", "builder-bridge.ts"]
MANAGED = ["event-wait-guard.ts", "builder-bridge.ts", "scaffolding-guard.ts"]
def sha256(p: Path) -> str:
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""tag_repair.py — the tested authority for release-tag convergence.
Extracted from .gitea/workflows/auto-version.yml so the tag-push logic is
runnable, testable, and one source of truth (wave-release-pipeline-deepdive,
owner queue item 5).
Why this module exists — the real CI evidence (Gitea runs 29929 / 30435 /
30636, fetched 2026-10-19):
* The bump step ran under `bash -e -o pipefail` and died on its FIRST line,
`LATEST=$(git tag --sort=-v:refname | head -1 ...)`, with exit code 141
(= 128+13, SIGPIPE): with hundreds of tags `git tag` streams its listing,
`head -1` exits, git gets EPIPE, pipefail + set -e abort the step with
zero output. The 0.312.0/0.313.0/0.314.0 master-tip fallback lived in a
LATER step and could never run. latest_release_tag() reads the same tag
SIGPIPE-free via `git for-each-ref --count=1` (no downstream pipe).
* tui-release.yml aborted "triggered on non-tui tag" because
GITEA_REF_NAME resolves EMPTY on the rune-ci-desktop runner; the fallback
chain (GITEA_REF_NAME → GITHUB_REF_NAME → git describe --tags
--exact-match) lives in the workflow, documented here.
* The version>tag deadlock (bump landed, tag missed) made every later push
read anomaly version_ahead_of_target, warranted=null, and skip forever
(the manual "re-arm convergence" commit 360665b1, 2026-10-19). The
workflow's tag step now calls ensure_tag() every run: if the tui-v
<master version> tag is missing it is created at the bump commit (or the
fetched master tip when that sha is unusable) and pushed — self-heal.
Auth for --push mirrors the workflow's documented mechanism: the
write-capable ROBOT_TOKEN with the token owner's real login (Gitea
git-over-HTTP basic auth wants the owner's username when the password is a
token), set as a local http.extraheader. Never prints credentials.
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import subprocess
import sys
import urllib.request
from pathlib import Path
from typing import Any
FETCH_TIP = "FETCH_HEAD"
def _semver_lte(a: str, b: str) -> bool:
"""Numeric semver a <= b (never lexicographic — '0.66.9' vs '0.66.10')."""
if not b:
return False
try:
return [int(x) for x in a.split(".")] <= [int(x) for x in b.split(".")]
except ValueError:
return False
def _git(args: list[str], repo: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args],
cwd=str(repo) if repo else None,
check=check,
capture_output=True,
text=True,
)
def latest_release_tag(repo: Path | None = None) -> str | None:
"""Highest release tag (e.g. ``v0.314.0``), or None when there are none.
SIGPIPE-free: ``git for-each-ref --count=1`` stops INSIDE git — there is
no downstream pipe for git to receive EPIPE on, so the step cannot die
with 141 the way ``git tag --sort=-v:refname | head -1`` did (runs
29929/30435/30636).
"""
proc = _git(
[
"for-each-ref",
"--count=1",
"--sort=-v:refname",
"--format=%(refname:short)",
"refs/tags",
],
repo,
check=False,
)
if proc.returncode != 0:
return None
return proc.stdout.strip() or None
def tag_exists(tag: str, repo: Path | None = None) -> bool:
proc = _git(["rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"], repo, check=False)
return proc.returncode == 0
def _resolve_target(bump_sha: str, repo: Path) -> str:
"""Bump commit sha when usable, else the fetched master tip.
The bump commit is created via the API AFTER the workflow checkout, so
it must be fetched first (FETCH_HEAD). Unusable = empty, '?', or not an
object in the local store (the 0.312.x failure mode the earlier
workflow fallback addressed; now tested here).
"""
sha = (bump_sha or "").strip()
if sha and sha != "?":
if _git(["cat-file", "-e", f"{sha}^{{commit}}"], repo, check=False).returncode == 0:
return sha
tip = _git(["rev-parse", FETCH_TIP], repo, check=False)
if tip.returncode != 0: # no fetch in this run (tests / offline) — HEAD is the tip
tip = _git(["rev-parse", "HEAD"], repo)
return tip.stdout.strip()
def plan_ensure_tag(version: str, bump_sha: str, repo: Path) -> dict[str, Any]:
"""Decide the tag action for tui-v<version> WITHOUT mutating anything."""
tag = f"tui-v{version}"
if tag_exists(tag, repo):
return {"action": "noop", "tag": tag, "reason": "tag already exists"}
return {"action": "tag", "tag": tag, "target": _resolve_target(bump_sha, repo)}
def ensure_tag(
version: str,
bump_sha: str,
repo: Path,
push: bool = False,
server: str | None = None,
token: str | None = None,
repo_slug: str | None = None,
) -> dict[str, Any]:
"""Ensure tui-v<version> exists (create + optionally push). Idempotent.
This is the self-heal primitive: called from the workflow's tag step on
EVERY release-eligible run, it repairs the version>tag deadlock state
(bump landed, tag missed) on the next push instead of skipping forever.
"""
plan = plan_ensure_tag(version, bump_sha, repo)
if plan["action"] == "noop":
return plan
tag, target = plan["tag"], plan["target"]
_git(["tag", "-d", tag], repo, check=False)
ident = _git_identity(repo, server=server, token=token)
proc = _git(
["-c", f"user.name={ident['name']}", "-c", f"user.email={ident['email']}",
"tag", "-a", tag,
"-m", f"chore(release): {tag} — native TUI convergence tag (same semver as the reviewed release, single version across faces)"],
repo,
)
if proc.returncode != 0:
plan["action"] = "error"
plan["error"] = f"tag creation failed: {proc.stderr.strip()}"
return plan
plan["tagged"] = target
if push:
auth_err = _configure_http_auth(repo, server=server, token=token, repo_slug=repo_slug)
if auth_err:
plan["action"] = "error"
plan["error"] = auth_err
return plan
proc = _git(["push", "origin", f"refs/tags/{tag}:refs/tags/{tag}"], repo, check=False)
if proc.returncode != 0:
plan["action"] = "error"
plan["error"] = f"push failed: {proc.stderr.strip()}"
return plan
plan["pushed"] = True
return plan
def self_heal_plan(version: str, latest_release_tag: str | None, repo: Path) -> dict[str, Any]:
"""Deadlock decision for the workflow: given master's version and the
latest release tag, should this run repair (create+push the missing tui
tag) or noop?
The pre-fix pipeline skipped forever here: version ahead of tag means
convergence reports anomaly version_ahead_of_target with warranted=null,
and every later push bailed out with 'no bump needed'. The repair is
exactly the tui-v<version> tag at the fetched master tip — after it
lands, version == tag and convergence is whole again.
"""
tag = f"tui-v{version}"
tag_version = version # NOT tag[1:] — 'tui-v0.x' [1:] is 'ui-v0.x'
latest_version = (latest_release_tag or "").lstrip("v")
if _semver_lte(tag_version, latest_version):
return {"action": "noop", "tag": tag, "skip": True,
"reason": f"tui tag not ahead of latest release tag {latest_release_tag}"}
if tag_exists(tag, repo):
return {"action": "noop", "tag": tag, "skip": True,
"reason": "tui tag already exists"}
return {
"action": "repair",
"tag": tag,
"target": _resolve_target("", repo),
"skip": False,
"reason": f"version {version} ahead of tag {latest_release_tag} with {tag} missing — self-heal",
}
def _git_identity(repo: Path, server: str | None = None, token: str | None = None) -> dict[str, str]:
"""Robot identity for annotated tags: the token owner's real login and
email (falls back to the checkout's existing git identity when no API
credentials are configured — test/offline mode)."""
server = (server or os.environ.get("RELEASE_SERVER_URL") or "").rstrip("/")
token = token or os.environ.get("GITEA_TOKEN") or ""
if server and token:
try:
req = urllib.request.Request(
f"{server}/api/v1/user",
headers={"Authorization": f"token {token}"},
)
with urllib.request.urlopen(req, timeout=15) as r:
user = json.loads(r.read())
email = user.get("email") or f"{user['login']}@users.noreply.gitea"
return {"name": user["login"], "email": email}
except Exception as exc: # noqa: BLE001 — fall back to local identity
print(f"tag_repair: /api/v1/user failed ({exc}); using local git identity",
file=sys.stderr)
name = _git(["config", "user.name"], repo, check=False).stdout.strip() or "rune-release-robot"
email = _git(["config", "user.email"], repo, check=False).stdout.strip() or "rune@localhost"
return {"name": name, "email": email}
def _configure_http_auth(
repo: Path,
server: str | None = None,
token: str | None = None,
repo_slug: str | None = None,
) -> str | None:
"""Set the local http extraheader for tag push (same mechanism the
workflow used inline). Returns an error string, or None on success."""
server = (server or os.environ.get("RELEASE_SERVER_URL") or "").rstrip("/")
token = token or os.environ.get("GITEA_TOKEN") or ""
repo_slug = repo_slug or os.environ.get("RELEASE_REPO") or ""
if not (server and token):
return "push requested but RELEASE_SERVER_URL/GITEA_TOKEN not configured"
login = _git_identity(repo, server=server, token=token)["name"]
auth_b64 = base64.b64encode(f"{login}:{token}".encode()).decode()
origin_url = _git(["remote", "get-url", "origin"], repo, check=False).stdout.strip()
import re as _re
m = _re.match(r"^(https?://[^/]+)", origin_url)
bases = [f"{server}/"]
if m and m.group(1) + "/" not in bases:
bases.append(m.group(1) + "/")
for base in bases:
_git(["config", "--local", f"http.{base}.extraheader",
f"AUTHORIZATION: basic {auth_b64}"], repo)
return None
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Release tag convergence repair")
parser.add_argument("--latest-tag", action="store_true",
help="print the highest release tag (SIGPIPE-free) and exit")
parser.add_argument("--plan", action="store_true",
help="print the ensure plan as JSON, mutate nothing")
parser.add_argument("--ensure", dest="ensure", metavar="VERSION",
help="ensure tui-v<VERSION> exists (create at the bump "
"commit or fetched master tip), push when --push")
parser.add_argument("--version", dest="ensure", metavar="VERSION",
help="alias for --ensure")
parser.add_argument("--bump-sha", default="", help="the auto-bump commit sha (or unusable/empty)")
parser.add_argument("--push", action="store_true", help="push the created tag to origin")
parser.add_argument("--repo", type=Path, default=None, help="repo root (default cwd)")
args = parser.parse_args(argv)
repo = args.repo or Path.cwd()
if args.latest_tag:
print(latest_release_tag(repo) or "")
return 0
if args.plan:
print(json.dumps(plan_ensure_tag(args.ensure or "", args.bump_sha, repo)))
return 0
if args.ensure:
plan = ensure_tag(args.ensure, args.bump_sha, repo, push=args.push)
print(json.dumps(plan))
return 0 if plan.get("action") != "error" else 1
parser.error("one of --latest-tag / --plan / --ensure is required")
return 2
if __name__ == "__main__":
try:
sys.exit(main())
except subprocess.CalledProcessError as exc:
print(f"tag_repair: git failed ({exc.returncode}): {exc.stderr}", file=sys.stderr)
sys.exit(1)
+118 -11
View File
@@ -134,6 +134,48 @@ PINNED_PYTEST = {
PINNED_PYTEST_CMD = ("RUNE_CA_CORE=python RUNE_CM_CORE=python "
"PYTHONPATH=src python -m pytest tests/")
# ── shard mode (wave scale40-sharding, 40-scale queue item 1) ─────────────
# python_suite under pytest-xdist worksteal across N workers (the measured
# path, research/factory-50-scale/06-synthesis.json lever L1); the Go and
# Rust sweep pools widen. The manifest shape is unchanged — check_landing
# reads it untouched. SHARD_WORKERS=None is the serial law (default).
SHARD_WORKERS: int | None = None
SHARD_DEFAULT_WORKERS = 16
def pytest_xdist_args(workers: int) -> list[str]:
"""The xdist distribution flags: worksteal (idle workers steal from
busy ones) — the measured path. Even 1 worker goes through xdist so a
'sharded' run is never a silent serial run."""
return ["-n", str(workers), "--dist", "worksteal"]
def pool_workers(default: int, n_units: int | None, *, shard: bool) -> int:
"""Sweep-pool width: the serial law keeps the recorded default; shard
mode widens to the shard width, bounded by the unit count (no idle
workers) — a 3-module sweep never spins 16 workers."""
if not shard:
return default
cap = max(default, SHARD_DEFAULT_WORKERS)
if n_units:
return min(n_units, cap)
return cap
def xdist_available() -> tuple[bool, str]:
"""pytest-xdist presence in the PINNED interpreter. A missing xdist is
never a silent serial fallback: the caller must record it."""
try:
import xdist # noqa: F401
return True, xdist.__version__
except Exception:
return False, ""
def python_suite_shard_data(workers: int, xdist_version: str) -> dict:
return {"sharded": True, "workers": workers, "dist": "worksteal",
"xdist_version": xdist_version}
MEMBERS: dict[str, dict] = {
"boundary_codegen": {"run": True},
"events_schema": {"run": True},
@@ -269,7 +311,8 @@ def member_rust_suite() -> dict:
return crate.name, r.returncode == 0, tail[-1][:200] if tail else ""
failed: list[str] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
with concurrent.futures.ThreadPoolExecutor(max_workers=pool_workers(
4, len(crates), shard=SHARD_WORKERS is not None)) as pool:
for name, ok, tail in pool.map(test_crate, crates):
if not ok:
failed.append(name)
@@ -295,12 +338,16 @@ def member_rust_suite() -> dict:
f"(baseline listed {len(known)})", "data": data}
def run_pinned_pytest(junit_path: Path) -> subprocess.CompletedProcess:
def run_pinned_pytest(junit_path: Path, *,
shard: bool = False) -> subprocess.CompletedProcess:
env = dict(PINNED_PYTEST)
env["PYTEST_ADDOPTS"] = f"--junit-xml={junit_path}"
return run_cmd([sys.executable, "-m", "pytest", "tests/", "-q", "--tb=no",
"-p", "no:cacheprovider"],
cwd=REPO, env=env, timeout=3600)
cmd = [sys.executable, "-m", "pytest", "tests/", "-q", "--tb=no",
"-p", "no:cacheprovider"]
if shard:
workers = SHARD_WORKERS or SHARD_DEFAULT_WORKERS
cmd += pytest_xdist_args(workers)
return run_cmd(cmd, cwd=REPO, env=env, timeout=3600)
def parse_junit_failures(junit_path: Path) -> list[str]:
@@ -328,7 +375,7 @@ def member_python_suite() -> dict:
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tf:
junit = Path(tf.name)
try:
r = run_pinned_pytest(junit)
r = run_pinned_pytest(junit, shard=SHARD_WORKERS is not None)
if not junit.exists():
return {"status": "RED",
"detail": f"pytest produced no junit output (rc={r.returncode}): "
@@ -342,8 +389,20 @@ def member_python_suite() -> dict:
"new_failures": res["new_failures"][:50],
"baseline_only_count": len(res["baseline_only"]),
"exit_code": r.returncode}
if SHARD_WORKERS is not None:
ok, xver = xdist_available()
workers = SHARD_WORKERS
if ok:
data.update(python_suite_shard_data(workers, xver))
else:
data["sharded"] = False
data["shard_note"] = ("pytest-xdist not importable in the "
"pinned interpreter — honest serial "
"fallback, never counted as sharded")
detail = (f"{len(failed)} failing vs baseline {len(baseline)}; "
f"{len(res['new_failures'])} new")
if SHARD_WORKERS is not None and data.get("sharded"):
detail += f"; sharded -n {SHARD_WORKERS} --dist worksteal"
if res["verdict"] == "RED":
return {"status": "RED",
"detail": detail + f" — NEW failures: {res['new_failures'][:10]}",
@@ -440,7 +499,36 @@ def record_baseline(force: bool, rust_only: bool = False,
return 0
GO_TEST_RACE_CMD = ["go", "test", "-race", "./..."]
GO_TEST_RACE_CMD = ["go", "test", "-race", "-timeout=10m", "./..."]
# wave go-sharding: only tui/boundary/gen reads BOUNDARY_CORPUS_DIR
# (grep-proven), so the corpus member runs that package alone — the
# previous ./... sweep paid 70-84s of dead weight and inherited the
# boot-smoke hang strikes (~600s, see go_sharding_note)
BOUNDARY_CORPUS_TEST_ARGS = ["test", "./boundary/..."]
def go_sharding_note() -> dict:
"""The measured go-sharding verdict, recorded in the manifest data so
'not sharded' is an honest measurement, not a gap (wave go-sharding,
diagnosis 2026-10-20): package sharding and diff-based race splitting
LOSE — the critical path is one hung test, not package parallelism,
and -race multiplies the heaviest real package only 1.6x. Go's test
cache is already the content-keyed results cache the synthesis asked
for (unchanged packages are 'ok (cached)')."""
return {
"sharded": False,
"note": "not sharded, measured: the ~600s strikes are ONE test "
"(tui/cmd/rune-tui boot smoke hanging in an unbounded pty "
"read past its own deadlines, burning go test's default "
"10m binary timeout — pre-existing on HEAD, reported for "
"a separate bounded repair); race detector = 1.6x on the "
"heaviest real package (runtime/lib 33s->53s), so a "
"diff-based race split saves ~nothing; the tui suite's "
"critical path is a single package, so per-package "
"sharding cannot help; Go's built-in test cache is the "
"content-keyed results cache",
}
def gofmt_verdict(unformatted: list[str], err: str = "") -> dict:
@@ -519,6 +607,10 @@ def member_go_suite() -> dict:
"vet": vet.returncode,
"test_race": test.returncode,
"test_race_cmd": " ".join(GO_TEST_RACE_CMD),
"parallelism": ("go test is package-parallel by "
"default (go -p = GOMAXPROCS); "
"already parallel, no shard change"),
"sharding": go_sharding_note(),
"gofmt_unformatted": unformatted, **staticcheck}}
@@ -551,7 +643,8 @@ def member_src_go_suite() -> dict:
build_failed: list[str] = []
failed: list[str] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
with concurrent.futures.ThreadPoolExecutor(max_workers=pool_workers(
4, len(modules), shard=SHARD_WORKERS is not None)) as pool:
for name, outcome, _tail in pool.map(build_then_test_module, modules):
if outcome == "build_failed":
build_failed.append(name)
@@ -619,11 +712,14 @@ def member_boundary_corpus_go() -> dict:
return {"status": "SKIP",
"reason": "generated go boundary package not present — run "
"scripts/gen_boundary_types.py"}
r = run_cmd(["go", "test", "./..."], cwd=REPO / "tui", timeout=1800,
r = run_cmd(["go", *BOUNDARY_CORPUS_TEST_ARGS], cwd=REPO / "tui",
timeout=1800,
env={"BOUNDARY_CORPUS_DIR": str(REPO / "schemas/boundary/corpus")})
return {"status": "GREEN" if r.returncode == 0 else "RED",
"detail": f"go test rc={r.returncode} (corpus round-trip in "
"generated Go types)"}
"detail": f"go test {' '.join(BOUNDARY_CORPUS_TEST_ARGS)} "
f"rc={r.returncode} (corpus round-trip in generated "
"Go types; scoped to the only BOUNDARY_CORPUS_DIR "
"consumer, wave go-sharding measured 70-84s -> ~0.2s)"}
def member_no_committed_binaries() -> dict:
@@ -783,6 +879,13 @@ def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="unified check (task 0.13)")
ap.add_argument("--members", default="",
help="comma-separated subset (default: all)")
ap.add_argument("--shard", action="store_true",
help="shard mode: python_suite under pytest-xdist "
"(-n N --dist worksteal), Go/Rust sweep pools "
"widened; manifest shape unchanged")
ap.add_argument("--shard-workers", type=int,
default=SHARD_DEFAULT_WORKERS,
help="shard worker count (default 16)")
ap.add_argument("--record-baseline", action="store_true")
ap.add_argument("--force", action="store_true")
ap.add_argument("--rust-only", action="store_true",
@@ -797,6 +900,10 @@ def main(argv=None) -> int:
return record_baseline(force=args.force, rust_only=args.rust_only,
src_go_only=args.src_go_only)
global SHARD_WORKERS
if args.shard:
SHARD_WORKERS = max(1, args.shard_workers)
wanted = ([m.strip() for m in args.members.split(",") if m.strip()]
or list(MEMBERS))
results: dict[str, dict] = {}
+102
View File
@@ -0,0 +1,102 @@
{
"schema_version": 1,
"description": "The ONE frozen hard-mode matrix (CHAT/TASK/PLAN), read by BOTH dispatch doors: the Go authority (src/go/tool-executor embeds a byte-identical copy generated by scripts/gen_mode_matrix.py) and the Python dispatch choke point (rune.mode_gate). Modes name tool CLASSES (the frozen Go vocabulary); classes map to the concrete executor tools of each language surface. Classes whose owning subsystem is pending carry status not_implemented and NO executor tools (honest 501) — they never silently widen reachability. Research chain: research/factory-tools-live/06-synthesis.json T1.",
"tool_classes": {
"read": {
"executor_tools": [
"read_file",
"glob_files",
"grep_files",
"list_directory",
"lint_file",
"git_diff",
"git_log",
"repo_map",
"read_persona",
"query_memory",
"check_growth",
"diagnose_behavior",
"find_capability"
],
"go_ops": ["read_file", "list_directory", "glob_files", "grep_files"],
"status": "implemented"
},
"research.queue": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "research subsystem (K-lane) — honest 501 until it lands"
},
"research.run": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "research subsystem (K-lane) — honest 501 until it lands"
},
"plan.draft": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "plan loop (K27) — honest 501 until it lands"
},
"plan.revise": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "plan loop (K27) — honest 501 until it lands"
},
"viability.check": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "viability authority — honest 501 until it lands"
},
"task.create": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "task tree (I3) — honest 501 until it lands"
},
"task.start": {
"executor_tools": [],
"go_ops": [],
"status": "not_implemented",
"owner": "task tree (I3) — honest 501 until it lands"
},
"build": {
"executor_tools": ["write_file", "format_code", "run_tests", "git_commit"],
"go_ops": [],
"status": "python-executor-only"
},
"edit": {
"executor_tools": ["edit_file"],
"go_ops": [],
"status": "python-executor-only"
},
"shell": {
"executor_tools": ["run_command"],
"go_ops": ["run_command"],
"status": "implemented"
}
},
"modes": {
"CHAT": {
"model": "chat-model",
"context": "conversation scrollback",
"rules": "chat rules",
"classes": ["read", "research.queue", "plan.draft"]
},
"TASK": {
"model": "work-model",
"context": "task slice + receipts",
"rules": "task rules",
"classes": ["read", "research.run", "task.create", "task.start", "build", "edit", "shell"]
},
"PLAN": {
"model": "plan-model",
"context": "plan document + evidence",
"rules": "plan rules",
"classes": ["read", "research.queue", "plan.draft", "plan.revise", "viability.check"]
}
}
}
+199
View File
@@ -0,0 +1,199 @@
package main
// modegate.go — the two-door law for the Go runtime loop (retirement
// campaign, tools row). The Python oracle authorizes EVERY dispatch
// from the model→tools loop against the ONE mode matrix BEFORE the
// approval branch (src/rune/local_tool_runtime.py's T3 dispatch gate,
// authority = rune.mode_gate.authorize_tool): a refusal is typed
// tool-error data the model can act on, never a crash, and repeated
// identical refusals trip the failure circuit breaker like any error.
//
// Matrix law: the matrix lives ONCE in schemas/mode/v1/mode_matrix.json;
// toolexec embeds a byte-identical copy and this module embeds its own
// (go:embed cannot reach above its module). scripts/gen_mode_matrix.py
// --check makes BOTH copies byte-identical to the canonical artifact —
// three doors, one matrix, no drift. Python reads the canonical file
// directly (rune.mode_gate), so this port mirrors mode_gate.py exactly:
// readModeState law (RUNE_MODE env wins, invalid fails closed to CHAT;
// else the hot file; absent → CHAT default), fail-closed matrix
// handling (missing/broken refuses everything, honestly).
import (
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
//go:embed mode_matrix.json
var modeMatrixJSON []byte
// hardModes — exactly three exist (display order).
var hardModes = []string{"CHAT", "TASK", "PLAN"}
func isHardMode(s string) bool {
for _, m := range hardModes {
if m == s {
return true
}
}
return false
}
type matrixClass struct {
ExecutorTools []string `json:"executor_tools"`
Status string `json:"status"`
}
type matrixMode struct {
Model string `json:"model"`
Context string `json:"context"`
Rules string `json:"rules"`
Classes []string `json:"classes"`
}
type modeMatrixFile struct {
SchemaVersion int `json:"schema_version"`
ToolClasses map[string]matrixClass `json:"tool_classes"`
Modes map[string]matrixMode `json:"modes"`
}
// allowedToolNames — the executor-tool names a mode may dispatch (the
// flattened executor_tools of its classes). A broken artifact refuses
// everything (fail closed) — mirroring the oracle's ModeMatrixError arm.
func allowedToolNames(mode string) (map[string]bool, string) {
var file modeMatrixFile
if err := json.Unmarshal(modeMatrixJSON, &file); err != nil {
return nil, fmt.Sprintf("mode matrix unparseable: %v", err)
}
if file.SchemaVersion != 1 {
return nil, "mode matrix schema_version != 1"
}
spec, ok := file.Modes[mode]
if !ok || len(spec.Classes) == 0 {
return nil, fmt.Sprintf("mode matrix lacks mode %s", mode)
}
allowed := map[string]bool{}
for _, class := range spec.Classes {
def, known := file.ToolClasses[class]
if !known {
return nil, fmt.Sprintf("mode %s names unknown class %q", mode, class)
}
for _, name := range def.ExecutorTools {
allowed[name] = true
}
}
return allowed, ""
}
// modeStatePath — the hot-store file the Go front door writes
// (RUNE_MODE_STATE override, else rune_home()/hot/mode_state.json).
// rune_home() is the paths law (RUNE_HOME override → ~/.rune); it is
// three lines of pure string/env work, mirrored here rather than
// imported — src/go/paths is package main, not an importable library.
func modeStatePath() string {
if v := strings.TrimSpace(os.Getenv("RUNE_MODE_STATE")); v != "" {
return v
}
home := os.Getenv("HOME") + "/.rune"
if v, ok := os.LookupEnv("RUNE_HOME"); ok {
home = v
}
return filepath.Join(home, "hot", "mode_state.json")
}
// ActiveMode — the readModeState law. Returns (mode, source). RUNE_MODE
// set wins (invalid values fail closed to CHAT); else the hot file;
// absent → ("CHAT", "default"); corrupt JSON or an unknown mode value →
// CHAT with a source naming the failure (reported, never silently
// treated as absent).
func ActiveMode() (string, string) {
env := strings.TrimSpace(os.Getenv("RUNE_MODE"))
if env != "" {
if isHardMode(env) {
return env, "env"
}
return "CHAT", "env-invalid"
}
raw, err := os.ReadFile(modeStatePath())
if err != nil {
if os.IsNotExist(err) {
return "CHAT", "default"
}
return "CHAT", "default-corrupt"
}
var doc map[string]interface{}
if err := json.Unmarshal(raw, &doc); err != nil {
return "CHAT", "default-corrupt"
}
if mode, ok := doc["mode"].(string); ok && isHardMode(mode) {
return mode, "hot-state"
}
return "CHAT", "default-invalid"
}
// ModeRefusal — typed proof a tool is unreachable in the active mode
// (the Go mirror of mode_gate.ModeRefusal). Feeds the model as
// tool-error data — it can rephrase or the operator can switch modes —
// never a crash.
type ModeRefusal struct {
Mode string
Tool string
AllowedTools []string
Source string
}
// Message — the oracle's message property, byte-for-byte.
func (r *ModeRefusal) Message() string {
allowed := "(matrix unavailable)"
if len(r.AllowedTools) > 0 {
sorted := append([]string{}, r.AllowedTools...)
sort.Strings(sorted)
allowed = strings.Join(sorted, ", ")
}
source := ""
if r.Source != "" {
source = fmt.Sprintf(" [%s]", r.Source)
}
return fmt.Sprintf("mode refusal: tool %q is unreachable in mode %s (mode tools: %s)%s",
r.Tool, r.Mode, allowed, source)
}
// ToResult — the oracle's to_result(): typed tool-error data.
func (r *ModeRefusal) ToResult() map[string]interface{} {
sorted := append([]string{}, r.AllowedTools...)
sort.Strings(sorted)
return map[string]interface{}{
"success": false,
"error": r.Message(),
"mode_refusal": map[string]interface{}{
"mode": r.Mode,
"tool": r.Tool,
"allowed_tools": sorted,
"hint": "switch the runtime mode (POST /v1/mode) or rephrase",
},
}
}
// AuthorizeTool — authorize one tool against the one matrix. nil means
// allowed. The ACTIVE mode is resolved per call (the A3 law: a mid-loop
// switch changes verdicts on the next dispatch). A missing/broken
// matrix refuses everything — fail closed, honestly.
func AuthorizeTool(toolName string) *ModeRefusal {
mode, source := ActiveMode()
allowed, matrixErr := allowedToolNames(mode)
if matrixErr != "" {
return &ModeRefusal{Mode: mode, Tool: toolName, Source: "matrix-error: " + matrixErr}
}
if allowed[toolName] {
return nil
}
names := make([]string, 0, len(allowed))
for name := range allowed {
names = append(names, name)
}
return &ModeRefusal{Mode: mode, Tool: toolName, AllowedTools: names, Source: source}
}
+267
View File
@@ -0,0 +1,267 @@
package main
// modegate_test.go — the two-door law battery for the Go runtime loop
// (retirement campaign, tools row). The Python oracle authorizes EVERY
// dispatch against the ONE mode matrix BEFORE the approval branch (the
// T3 dispatch gate); the Go loop must refuse mode-unreachable tools
// with the same typed tool-error data, trip the identical-failure
// circuit breaker on repeats, and never reach the executor. Bytes are
// pinned against the LIVE oracle (rune.mode_gate) for the refusal
// envelope, and against the frozen matrix artifact for verdicts.
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func modegateRepoRoot(t *testing.T) string {
t.Helper()
// .../src/go/local-tool-runtime/modegate_test.go → repo root
return filepath.Clean(filepath.Join(".", "..", "..", ".."))
}
// oracleModeRefusal — ask the live oracle for a ModeRefusal.to_result().
func oracleModeRefusal(t *testing.T, tool, mode, source string) map[string]interface{} {
t.Helper()
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 unavailable")
}
repo := modegateRepoRoot(t)
script := `
import json, sys
sys.path.insert(0, "src")
from rune.mode_gate import ModeRefusal
allowed = ("check_growth", "diagnose_behavior", "find_capability", "git_diff", "git_log",
"glob_files", "grep_files", "lint_file", "list_directory", "query_memory",
"read_file", "read_persona", "repo_map")
r = ModeRefusal(mode="CHAT", tool="run_command", allowed_tools=allowed, source="` + source + `")
print(json.dumps(r.to_result()))
`
cmd := exec.Command("python3", "-c", script)
cmd.Dir = repo
out, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
t.Fatalf("oracle mode_gate failed: %v\n%s", err, ee.Stderr)
}
t.Fatalf("oracle mode_gate failed: %v", err)
}
var v map[string]interface{}
if err := json.Unmarshal(out, &v); err != nil {
t.Fatalf("oracle output: %v (%q)", err, string(out))
}
return v
}
// TestModeRefusalBytesMatchOracle — the typed refusal envelope is
// byte-parity with the oracle's ModeRefusal.to_result().
func TestModeRefusalBytesMatchOracle(t *testing.T) {
for _, source := range []string{"", "env"} {
want := oracleModeRefusal(t, "run_command", "CHAT", source)
allowed := []string{
"check_growth", "diagnose_behavior", "find_capability", "git_diff", "git_log",
"glob_files", "grep_files", "lint_file", "list_directory", "query_memory",
"read_file", "read_persona", "repo_map",
}
got := (&ModeRefusal{Mode: "CHAT", Tool: "run_command", AllowedTools: allowed, Source: source}).ToResult()
if canonJSONShape(got) != canonJSONShape(want) {
t.Errorf("source %q: refusal envelope diverged:\n got %s\n want %s", source,
canonJSONShape(got), canonJSONShape(want))
}
}
}
// canonJSONShape — parse+re-marshal round trip: type-shape noise
// ([]string vs []interface{}) compares equal when the JSON is equal.
func canonJSONShape(v map[string]interface{}) string {
raw, _ := json.Marshal(v)
var reparsed interface{}
_ = json.Unmarshal(raw, &reparsed)
out, _ := json.Marshal(reparsed)
return string(out)
}
// TestAuthorizeToolVerdicts — the frozen matrix verdicts: CHAT refuses
// the write/shell family; TASK allows run_command; every mode allows
// read_file; unknown tool names are refused in every mode.
func TestAuthorizeToolVerdicts(t *testing.T) {
t.Setenv("RUNE_MODE", "CHAT")
if r := AuthorizeTool("run_command"); r == nil {
t.Fatal("CHAT must refuse run_command")
}
if r := AuthorizeTool("write_file"); r == nil {
t.Fatal("CHAT must refuse write_file")
}
t.Setenv("RUNE_MODE", "TASK")
if r := AuthorizeTool("run_command"); r != nil {
t.Fatalf("TASK must allow run_command, got %+v", r)
}
t.Setenv("RUNE_MODE", "PLAN")
if r := AuthorizeTool("read_file"); r != nil {
t.Fatalf("PLAN must allow read_file, got %+v", r)
}
if r := AuthorizeTool("no_such_tool"); r == nil {
t.Fatal("unknown tools are refused in every mode (fail closed)")
}
}
// TestActiveModePrecedence — the readModeState law: RUNE_MODE wins
// (invalid fails closed to CHAT); else the hot file; absent → CHAT.
func TestActiveModePrecedence(t *testing.T) {
hot := filepath.Join(t.TempDir(), "mode_state.json")
t.Setenv("RUNE_MODE_STATE", hot)
os.Unsetenv("RUNE_MODE")
mode, source := ActiveMode()
if mode != "CHAT" || source != "default" {
t.Fatalf("absent hot file: got (%s, %s) want (CHAT, default)", mode, source)
}
if err := os.WriteFile(hot, []byte(`{"mode": "TASK"}`), 0o644); err != nil {
t.Fatal(err)
}
mode, source = ActiveMode()
if mode != "TASK" || source != "hot-state" {
t.Fatalf("hot file: got (%s, %s) want (TASK, hot-state)", mode, source)
}
if err := os.WriteFile(hot, []byte(`{"mode": "BOGUS"}`), 0o644); err != nil {
t.Fatal(err)
}
mode, source = ActiveMode()
if mode != "CHAT" || source != "default-invalid" {
t.Fatalf("unknown hot value: got (%s, %s) want (CHAT, default-invalid)", mode, source)
}
if err := os.WriteFile(hot, []byte(`{not json`), 0o644); err != nil {
t.Fatal(err)
}
mode, source = ActiveMode()
if mode != "CHAT" || source != "default-corrupt" {
t.Fatalf("corrupt hot file: got (%s, %s) want (CHAT, default-corrupt)", mode, source)
}
t.Setenv("RUNE_MODE", "TASK")
mode, source = ActiveMode()
if mode != "TASK" || source != "env" {
t.Fatalf("env wins: got (%s, %s) want (TASK, env)", mode, source)
}
t.Setenv("RUNE_MODE", "BOGUS")
mode, source = ActiveMode()
if mode != "CHAT" || source != "env-invalid" {
t.Fatalf("invalid env fails closed: got (%s, %s) want (CHAT, env-invalid)", mode, source)
}
if r := AuthorizeTool("run_command"); r == nil {
t.Fatal("invalid env mode must refuse the shell family (fail closed to CHAT)")
}
}
// toolCallResponse — one scripted chat turn emitting a native tool call.
func toolCallResponse(id, name string, arguments map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"tool_calls": []interface{}{
map[string]interface{}{
"id": id,
"function": map[string]interface{}{
"name": name,
"arguments": arguments,
},
},
},
}
}
func modegateAllTools() []map[string]interface{} {
tools := make([]map[string]interface{}, 0, len(RegistryTools))
for _, def := range RegistryTools {
tools = append(tools, def)
}
return tools
}
// TestRunLoopAuthorizesBeforeApprovalDoor — a CHAT-originated
// run_command is mode-refused as typed tool-error data; the loop
// records the event, answers the model, and the executor NEVER runs.
func TestRunLoopAuthorizesBeforeApprovalDoor(t *testing.T) {
t.Setenv("RUNE_MODE", "CHAT")
transport := newFakeTransport([]interface{}{
toolCallResponse("c1", "run_command", map[string]interface{}{"command": "echo hi"}),
map[string]interface{}{"content": "understood, staying in chat"},
}, true, false)
executor := newScriptExecutor(nil)
rt, err := NewLocalRuntime(transport, executor, "chat-model", modegateAllTools(), 8)
if err != nil {
t.Fatal(err)
}
resp := rt.Run([]map[string]interface{}{{"role": "user", "content": "run echo"}}, nil, nil)
if !resp.Success {
t.Fatalf("run failed: %v", resp.Error)
}
if len(executor.calls) != 0 {
t.Fatalf("executor must never run for a mode-refused tool: %v", executor.calls)
}
if len(resp.ToolEvents) != 1 || resp.ToolEvents[0].Kind != "error" {
t.Fatalf("expected one error tool event, got %+v", resp.ToolEvents)
}
result := resp.ToolEvents[0].Result
refusal, _ := result["mode_refusal"].(map[string]interface{})
if refusal == nil || refusal["mode"] != "CHAT" || refusal["tool"] != "run_command" {
t.Fatalf("expected typed CHAT/run_command refusal, got %v", result)
}
if success, _ := result["success"].(bool); success {
t.Fatal("refusal result must not be success")
}
}
// TestAuthorizeStubRefusalTripsCircuitBreaker — repeated identical
// refusals trip the identical-failure breaker exactly like any error.
func TestAuthorizeStubRefusalTripsCircuitBreaker(t *testing.T) {
transport := newFakeTransport([]interface{}{
toolCallResponse("c1", "run_command", map[string]interface{}{"command": "echo hi"}),
toolCallResponse("c2", "run_command", map[string]interface{}{"command": "echo hi"}),
}, true, false)
executor := newScriptExecutor(nil)
rt, err := NewLocalRuntime(transport, executor, "chat-model", modegateAllTools(), 8)
if err != nil {
t.Fatal(err)
}
rt.Authorize = func(name string) *ModeRefusal {
return &ModeRefusal{Mode: "PLAN", Tool: name, AllowedTools: []string{"read_file"}, Source: "pinned-test"}
}
resp := rt.Run([]map[string]interface{}{{"role": "user", "content": "run echo"}}, nil, nil)
if resp.Success {
t.Fatalf("identical repeated refusals must trip the breaker, got %+v", resp)
}
if !strings.Contains(resp.Error, "2 consecutive times with identical arguments") {
t.Fatalf("breaker message missing: %q", resp.Error)
}
}
// TestAuthorizeInjectablePassThrough — an injected authority that
// allows keeps the loop on its normal dispatch path.
func TestAuthorizeInjectablePassThrough(t *testing.T) {
transport := newFakeTransport([]interface{}{
toolCallResponse("c1", "read_file", map[string]interface{}{"path": "/tmp/x"}),
map[string]interface{}{"content": "done"},
}, true, false)
executor := newScriptExecutor(nil)
rt, err := NewLocalRuntime(transport, executor, "chat-model", modegateAllTools(), 8)
if err != nil {
t.Fatal(err)
}
allowed := false
rt.Authorize = func(name string) *ModeRefusal {
allowed = true
return nil
}
resp := rt.Run([]map[string]interface{}{{"role": "user", "content": "read"}}, nil, nil)
if !resp.Success {
t.Fatalf("run failed: %v", resp.Error)
}
if !allowed {
t.Fatal("injected authority was never consulted")
}
if len(executor.calls) != 1 || executor.calls[0]["tool"] != "read_file" {
t.Fatalf("read dispatch broken: %v", executor.calls)
}
}
+54 -1
View File
@@ -10,6 +10,7 @@ package main
import (
"fmt"
"sort"
"strings"
)
// FindCapabilityTool — the capability escape hatch (P1-01 #24).
@@ -52,11 +53,25 @@ type LocalRuntime struct {
FindCapabilityExecutor ToolExecutor
FindCapabilityCalls int
// Two-door law (the oracle's authorize seam): every dispatch
// authorizes against the ONE mode matrix BEFORE the approval branch.
// nil = the default authority (AuthorizeTool, active mode per call);
// an injected callable keeps the runtime testable without live state.
Authorize func(name string) *ModeRefusal
// ExposeTools seam: resolves a name to its registry schema. Defaults
// to RegistryTools (the frozen authority); tests may narrow it.
ToolIndex func(name string) (map[string]interface{}, bool)
}
// authorizeRequest — the effective per-dispatch mode authority.
func (rt *LocalRuntime) authorizeRequest(name string) *ModeRefusal {
if rt.Authorize != nil {
return rt.Authorize(name)
}
return AuthorizeTool(name)
}
// NewLocalRuntime constructs the loop with the full registry allowlist
// (Python: tools=None → TOOLS) and validates the iteration budget.
func NewLocalRuntime(transport LoopTransport, executor ToolExecutor, model string, tools []map[string]interface{}, maxIterations int) (*LocalRuntime, error) {
@@ -306,6 +321,7 @@ func (rt *LocalRuntime) runLoop(
) *OperatorResponse {
events := append([]ToolEvent{}, accumulatedEvents...)
thinking := append([]string{}, accumulatedThinking...)
narration := []string{}
var nativeDefinitions []map[string]interface{}
if nativeTools {
nativeDefinitions = ToOllamaTools(rt.Tools)
@@ -343,6 +359,22 @@ func (rt *LocalRuntime) runLoop(
fmt.Sprintf("tool not available in this runtime: %s. If you need a capability that is not exposed, call %s with a description of what you need to do.", request.Name, FindCapabilityTool),
events, thinking, iterationsUsed)
}
// T3 dispatch gate (two-door law): authorize against the
// one mode matrix BEFORE the approval branch — a
// chat-originated write never even reaches the approval
// door. The refusal is typed tool-error data: the loop
// records a tool event, the model sees it and can
// rephrase or stop; repeated identical refusals trip the
// failure circuit breaker like any error.
if refusal := rt.authorizeRequest(request.Name); refusal != nil {
refusalResult := refusal.ToResult()
events = append(events, ToolEvent{Kind: "error", Request: request, Result: DeepCopy(refusalResult).(map[string]interface{})})
conversation = append(conversation, ToolMessage(request, refusalResult))
if trip := registerFailure(request, refusalResult); trip != "" {
return rt.failure(trip, events, thinking, iterationsUsed)
}
continue
}
requiresApproval, err := ToolRequiresApproval(request)
if err != nil {
return rt.failure(err.Error(), events, thinking, iterationsUsed)
@@ -499,9 +531,17 @@ func (rt *LocalRuntime) runLoop(
if trimSpace(visible) == "" {
return rt.failure("assistant returned empty terminal visible text", events, thinking, iterationsUsed)
}
// T4 (gap 6): pre-tool narration is preserved into the final
// user_text assembly — the transcript keeps what the model
// said on its way through the tool loop, not just the
// terminal turn.
userText := visible
if len(narration) > 0 {
userText = strings.Join(append(append([]string{}, narration...), visible), "\n\n")
}
return &OperatorResponse{
Success: true,
UserText: visible,
UserText: userText,
Thinking: thinking,
ToolEvents: events,
Model: rt.Model,
@@ -510,6 +550,19 @@ func (rt *LocalRuntime) runLoop(
}
}
// Narration preceding tool calls rides into the final user_text
// (T4). Best-effort only: a narration that cannot be cleanly
// separated from the protocol never fails the turn.
if intermediate, isStr := assistant["content"].(string); isStr {
spoken, err := ExtractVisibleText(intermediate)
if err != nil {
spoken = ""
}
if trimmed := trimSpace(spoken); trimmed != "" {
narration = append(narration, trimmed)
}
}
// The provider's exact assistant object, including call IDs and
// thinking fields, must precede all result messages.
conversation = append(conversation, assistant)
Loaded 100 of 598 files, more files were not shown because too many files have changed in this diff. Show more