Compare commits

...
896 Commits
Author SHA1 Message Date
conductor 8019ff2c03 chore(release): auto-bump version to 0.131.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-08 00:25:05 +01:00
Nick d980aeba43 feat(go): factory batch 6 — cache content-hash (contract BUILDER-2026-10-07T23:23:32Z-8265ae-unknown)
ContentHash sha256[:16] ported to cache/ — the change-detection hash.
4-case battery from the live oracle. sqlite layer stays reactive.
2026-10-08 00:23:43 +01:00
Nick 7a2dec529b chore(scaffold): contract store snapshot (batch 5 retro-ship) 2026-10-08 00:21:16 +01:00
Nick 76430ca71f feat(go): factory batch 5 — provider tool-format adapters (contract BUILDER-2026-10-07T23:14:45Z-635b00)
FormatToolsOI/FormatToolsAnthropic ported: the provider-facing shape
transforms with default-parameters fallback. 2-case battery from the
live oracle (with and without the parameters key). The [OI] in the
original name was literal Go generic syntax — invalid; renamed to OI.
2026-10-08 00:20:44 +01:00
Nick 8031eed1c1 Merge remote-tracking branch 'gitea/master' 2026-10-08 00:09:22 +01:00
Nick 4c1b13e61a feat(go): factory batch 4 — hook placement mechanics (contract BUILDER-2026-10-07T23:06:18Z-ec6208-unknown)
RecommendPlacement ported verbatim to hook-engine: the decision table
(hook points with append-order shape, the frequency ladder, cache
scopes, concurrency, failure modes, context budget) + the
HOOK_*/FREQ_*/SCOPE_* constants. 14-case factor-matrix battery from the
live oracle — first-run green. The factory continues.
2026-10-08 00:09:18 +01:00
conductor 192e161998 chore(release): auto-bump version to 0.130.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-08 00:07:28 +01:00
Nick 6daf950642 Merge remote-tracking branch 'gitea/master' 2026-10-08 00:05:09 +01:00
Nick d78d8d4dc4 feat(go): factory batch 3 — tool-protocol CLI (contract BUILDER-2026-10-07T23:01:39Z-0457fe-unknown)
The module-family JSON CLI over the ported protocol core:
- ops: validate / normalize_text / ollama / visible / contains_internal /
  registry_shape / list-tools / requires-approval (argv compat)
- registry_shape: 19 tools, order preserved — the frozen registry
  set-equality check surfaced through the CLI
- requires-approval: the auth boundary op (bash→run_command alias
  canonicalized before the check; read-only set mirrored)

All gates green. The factory continues — batch 4 scopes next.
2026-10-08 00:05:05 +01:00
conductor 4dee7221cb chore(release): auto-bump version to 0.130.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-08 00:01:35 +01:00
Nick 630642f499 feat(go): factory batch 2 — text-fallback parse + span surgery (contract BUILDER-2026-10-07T22:11:52Z-9ffb73-unknown batch 2)
The span-surgery half of tool_protocol.py ported:
- DecodedObjects: balanced JSON scan with exact spans (the JSONDecoder
  raw_decode walk)
- ProtocolEntries: fence/xml wrapper expansion (spans widen to the
  wrapper when the surrounding line is empty), malformed-residue checks
  (tool_call text, literal tool-key outside strings)
- ExtractVisibleText: reverse-order span removal with the
  line-boundary widening rule
- containsToolObjectKey: the outside-strings scanner
- requestFromEnvelope: validation errors PROPAGATE as hard errors for
  the whole text (oracle-verified: '<tool_call>{"tool": "x"}' →
  'unknown tool: x' — my first read swallowed them; the fixture caught it)

PARITY: the batch-1 truth battery's visible/normalize cases now ACTIVE
(they were pinned waiting for this batch). 17/17 green.
2026-10-07 23:59:08 +01:00
Nick 38c9d2729c Merge remote-tracking branch 'gitea/master' 2026-10-07 23:48:29 +01:00
Nick 4032c753f3 feat(go): factory batch 1 — tool-protocol validation core (contract BUILDER-2026-10-07T22:11:52Z-9ffb73)
The model→tool boundary ported to tool-protocol:
- ValidateToolRequest: alias canonicalization BEFORE schema (grep→
  grep_files, bash→run_command — bash never bypasses run_command's
  approval boundary), exact Python error texts (unknown tool / required
  argument missing / unknown argument / must be <type>), array item
  type checks, deterministic arg iteration for error stability
- schemas from the FROZEN tools_registry.json (shared with
  tool-executor — the set-equality authority)
- NormalizeOllamaToolCalls: native envelope + string-arguments JSON
  decode, malformed-shape errors
- ContainsInternalProtocol: result/error markers + xml tool_call spans
- ToolRequest dataclass aligned to the Python shape (Arguments field)
- 17-case truth battery from the live oracle; span-surgery surfaces
  (extract_visible_text, text-fallback parse) pinned for batch 2
2026-10-07 23:48:24 +01:00
conductor 30b5de31a2 chore(release): auto-bump version to 0.129.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 23:48:04 +01:00
Nick 03e0b8667a Merge remote-tracking branch 'gitea/master' 2026-10-07 23:47:54 +01:00
Nick 13a9bfe88d fix(operator): script-mode imports + guarded emission + test env parity (contract BUILDER-2026-10-07T22:45:14Z-1d3c31)
Three faults from the factory loop's CI watch:
- _wire_auto_filer script-mode fallback assumed the script dir was on
  sys.path — insert it before the flat imports
- the terminalization emission call sites leaked override-raised
  exceptions — the documented contract (journal failure never masks the
  operator path) is now enforced AT the call sites (the method-internal
  guard cannot cover subclass overrides); the two inner journal guards
  carry their contract annotation explicitly
- test_operator_script_can_import set env={HOME: tmp}, which silently
  relocated python's user-site (~/.local derives from HOME) and hid
  httpx — the test now carries the real user-site forward via PYTHONPATH
  while keeping the data-home isolation intent

Full suite: 2396 passed. Ratchet PASS bare (the annotation sweep also
dropped BLE001 to 344 — under baseline).
2026-10-07 23:47:54 +01:00
conductor c091cacb61 chore(release): auto-bump version to 0.129.4 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 22:58:09 +01:00
Nick d1f62288ab Merge remote-tracking branch 'gitea/master' 2026-10-07 22:57:57 +01:00
Nick 7c5310d1b2 fix(tools): go truth-harness runner fix — fixtures must live where the product policy allows (contract BUILDER-2026-10-07T21:57:47Z-01b08a-unknown)
Run 20074+ go-surface red: the harness created truth roots via
os.MkdirTemp over the allowed dirs, but on the runner the effective root
resolved under /var/lib/gitea-desktop-runner — INSIDE the blocked /var —
so ValidateRead/WritePath correctly denied the harness own fixtures. The
product gate was right; the harness was wrong.

- mkTruthRoot: /tmp FIRST (read-allowed on every surface), then allowed
  dirs, then HOME
- mkWriteRoot: write-allowed dirs only (the generator read/write split),
  never /tmp — write policy is ~ + /mnt/workdrive
- candidates inside BlockedDirs are SKIPPED: a root there can never pass
  the product gate, so creating it just guarantees a red

Verified: local green + simulated blocked-TMPDIR green. Full sweeps:
Go all modules green + gofmt clean; Rust 80/80 clippy/fmt/test clean;
Python 225 passed (2 known local ruff-PATH artifacts, gate bare = PASS).
2026-10-07 22:57:56 +01:00
conductor c39f1d0b3c chore(release): auto-bump version to 0.129.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 22:35:41 +01:00
Nick 231d4e51f1 Merge remote-tracking branch 'gitea/master' 2026-10-07 22:35:28 +01:00
Nick e82b2eb2af fix(rust): rust-surface CI debt discharge — the new gates lit up the debt they were built to catch (contract BUILDER-2026-10-07T21:34:56Z-95ad63-unknown)
First CI execution of the rust-surface job failed 17 crates, exactly as
designed. Discharged:

- doc_lazy_continuation (20+): paragraph separators per clippy's own help
- not_unsafe_ptr_arg_deref (11 FFI exports): item-level allow with the
  ABI reason — #[no_mangle] extern 'C' signatures are the Survivor loader
  contract; the raw pointer IS the FFI boundary
- manual_clamp (2): one real clamp rewrite; one parity-preserving allow
- too_many_arguments / type_complexity / while_let / filter_map: item
  allows with parity reasons; ONE semantic rewrite (filter_map→map in
  cognition_core — provably identical: Num→Some(*n)/else Some(0.0) is a
  total map)
- dead_code (2): allows with provenance reasons

PLUS pre-existing dead test code the new cargo test step lit up for the
first time: construction_depth's 3 tests passed None where the signature
takes bool (never compiled under any gate until today); context_pressure
had a str-vs-String vec and a fixture assuming 1 char/token when the
tokenizer is 3.5 — the fixture is now DERIVED from CHARS_PER_TOKEN so the
threshold ladder's top step is actually proven.

Result: 80/80 crates — clippy -D warnings CLEAN, fmt --check CLEAN,
cargo test PASS. The gates hold from here.
2026-10-07 22:35:28 +01:00
conductor a977805377 chore(release): auto-bump version to 0.129.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 21:55:57 +01:00
Nick 9eb4a36d62 Merge remote-tracking branch 'gitea/master' 2026-10-07 21:51:38 +01:00
Nick bf3ab130af fix(ci): dep-lock resync — the hypothesis pin hand-edit bypassed the canonical lock path (contract BUILDER-2026-10-07T20:56:00Z-see-log)
The #150 gate exists precisely to catch this: dev extras gained
hypothesis, the lock was hand-edited instead of uv-compiled, and the
transitive dep (sortedcontainers) never landed. Regenerated via
uv pip compile — provenance chains correct, gate green.

Lesson: the lock is GENERATED. Hand-edits to requirements-ci.txt are
the bug, not the fix.
2026-10-07 21:51:38 +01:00
conductor 8e9edeeea7 chore(release): auto-bump version to 0.129.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 21:44:05 +01:00
Nick 4faa1c107e Merge remote-tracking branch 'gitea/master' 2026-10-07 21:43:56 +01:00
Nick bb4e753e0e fix(ci): ci.yml YAML parse repair — the polyglot gate registered as a failed workflow (contract BUILDER-2026-10-07T20:43:27Z-cdf3fe)
The clippy step's echo carried unescaped shell substitution inside a
double-quoted YAML scalar and the cargo-test step name spanned two lines —
gitea parsed the workflow as FAILED before any job ran (context 'ci.yml',
zero jobs, run 20047). Escaped the echo, single-lined the name; the file
now parses with all 11 jobs including rust-surface.
2026-10-07 21:43:56 +01:00
conductor 52333e8ee9 chore(release): auto-bump version to 0.129.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 21:32:08 +01:00
Nick 1f6b6fb6aa Merge remote-tracking branch 'gitea/master' 2026-10-07 21:31:48 +01:00
Nick eef7768501 feat(pipeline): the POLYGLOT LINT GATE — language-aware dynamic linting + fuzz + property invariants (contract BUILDER-2026-10-07T20:01:40Z-ac6140-unknown)
Nick's directive: linting must apply DYNAMICALLY on the right language —
Go/Rust/anything gets the correct linter — plus fuzzy tests and useful
bug-catchers, wired into the Gitea pipeline.

THE GAPS (confirmed by inspection): Rust had ZERO linting (crates built,
never clippy); Go had vet only (gofmt drifted across 41 files); no fuzzing
anywhere; no property-based tests.

NEW:
- scripts/polyglot_lint.py: detects changed files vs the push range,
  groups by language (extension + path prefixes), runs ruff / go vet +
  gofmt -l / cargo clippy + cargo fmt --check per language; unknown
  extensions skipped with a note; exit non-zero on any finding
- CI: gofmt gate (formatting is a gate), go test -race sweep, go fuzz
  smoke (15s per target), and a NEW rust-surface job: rustfmt --check +
  clippy -- -D warnings + cargo test (currently trivial — the crates carry
  no #[test] yet; this lights up the moment the first lands)
- GO FUZZ TARGETS on the byte-parity surfaces: canonicalJSON (round-trip),
  Norm (deterministic + charset), Stem (no-grow/no-empty — idempotence
  deliberately NOT asserted: the oracle folds retroactively → retroactive
  → retroactiv, verified), FtsQueryConjunctive, Overlap (symmetry + [0,1])
- PYTHON property invariants (hypothesis, pinned 6.168.5 in dev extras +
  requirements-ci.txt): the same mathematical properties, mirrored

FOUND BY THE FUZZER ON FIRST LIGHT:
- canonicalJSON passed control bytes (<0x20) through RAW — Python
  json.dumps escapes them \uXXXX even with ensure_ascii=False. The Go
  digest DIVERGED from the oracle on adversarial payloads. FIXED
  (jsonEsc now emits \u00XX); the approval-digest surface is
  byte-stable again
- the FTS builder never escapes embedded quotes (oracle-verified: Python
  does the same) — pinned as oracle truth; FTS5 hardening filed separately

Also: gofmt -w across the tree (the 41-file drift converged; every module
builds), clippy --fix + cargo fmt across all 80 crates (warnings ~110 →
59 mechanical-fixed, remainder are stylistic and now GATED so debt
cannot grow). THE PIPE-MASK hit my own verification twice more this
session — the gate script itself exits 1 correctly (verified bare).
2026-10-07 21:31:23 +01:00
conductor 81099a11f7 chore(release): auto-bump version to 0.128.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 20:52:40 +01:00
Nick 3c13be1d76 Merge remote-tracking branch 'gitea/master' 2026-10-07 20:52:28 +01:00
Nick de9dee7dd9 feat(go): memory-store batch 4 — the dense-side meta-judgement (contract BUILDER-2026-10-07T19:47:37Z-1cf1dc-unknown)
The recall() gate ported to memory-store-deep:
- DenseGate: vector proximity is not a relation — a dense hit (cos =
  1 - distance/2) is kept iff it shares a DISTINCTIVE content word with
  the entry (df/total < 0.05 via LIKE-substring counts); fallback keeps
  no-content-word queries or strong matches (cos >= 0.80). The
  black-coffee/black-holes fix, now deterministic-side.
- QueryContent (len>3 non-stop, first-occurrence order)
- RankFusionEntries: verified IDENTICAL to the chunk-side fusion (same
  code, :1297 vs the batch-2 port) — wrapper pins the parity for the
  recall() call shape; the docstring's raw-BM25 claim is stale, the
  CODE min-max normalizes in all modes (code is the oracle)
- the abstain constants: RECALL_BUDGET=8, RECALL_MIN_SCORE=0.42

PARITY: truth_gate.json — 6 cases from the live oracle against a fixture
store (distinctive/common/strong-cos/no-content branches + fusion
identity + constants). Embedding/vec0 SQL and the cache stay reactive.
2026-10-07 20:52:25 +01:00
conductor c949df0e98 chore(release): auto-bump version to 0.127.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 20:39:30 +01:00
Nick 18c12fbda7 Merge remote-tracking branch 'gitea/master' 2026-10-07 20:39:16 +01:00
Nick ee7da6d080 feat(go): warm-router deep batch — the always-on digest core (contract BUILDER-2026-10-07T19:35:07Z-4d6b4c-unknown)
The digest selection machinery ported to warm-router:
- LexicalSignal (punctuation-strip, len>3, _DIGEST_STOP 64 words extracted
  verbatim), LexicalOverlap (Jaccard)
- Universality: the rule-vs-record classifier (date regex → 0, dated
  markers 2+ → 0 / 1 → 0.25, directive markers capped +0.25, terse
  40/90 ±0.15/-0.2, clamp)
- DigestSelect: the pure core — topic_rank slug ordering, 5-key stable
  sort (topic, -universality, always_on, -importance, -lexical density),
  junk skip (<12 chars / no triggers), universality gate, lexical gate,
  Jaccard dedup >=0.45, token sizing (len/4), oversized skip, budget fit

PARITY: truth_digest.json — 9 cases. The oracle corrected MY expectation
in the fixture: operating-gates-copy (importance 0.8) outranks the
original (0.7) on the importance tiebreak — fixture honesty, again. DB
I/O stays reactive. First-run green on the port itself.
2026-10-07 20:39:13 +01:00
conductor 2fa29f408c chore(release): auto-bump version to 0.126.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 20:27:41 +01:00
Nick f433efe375 Merge remote-tracking branch 'gitea/master' 2026-10-07 20:27:31 +01:00
Nick be00b56a0c feat(tools): undo_last port + the DEAD-FUNCTION finding (contract BUILDER-2026-10-07T19:20:10Z-af954d-unknown, defect #2481)
- UndoLast ported to tool-executor: glob *.bak, mtime-desc (name desc on
  ties), split('_', 4) → parts[4] underscore-to-slash reconstruct,
  exists-check, restore+delete. Verbatim semantics.
- THE FINDING: undo_last can NEVER restore absolute paths — the safe-name
  mangling eats the leading slash and split('_',4) drops the first path
  segment into parts[3]. Verified live three ways (multi-segment tmp,
  underscore-free /tmp, cwd-relative). The safety mechanism silently does
  nothing while the mutation stands. FILED: #2481 (fix the Python oracle
  first via a sidecar manifest, regenerate truth, flip Go — the parity
  battery verifies both sides flip together).
- The truth test PINS the dead behavior (oracle parity, not aspiration):
  a future one-sided 'fix' cannot silently diverge.
- UndoDir gains a seam (undoDirFn) so tests redirect without touching
  ~/.rune/undo. Per-wave routine held (gates bare, spine rebuilt, both
  snapshots shipped).
2026-10-07 20:27:28 +01:00
conductor 72d443423e chore(release): auto-bump version to 0.125.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 20:13:28 +01:00
Nick f96991bf2e Merge remote-tracking branch 'gitea/master' 2026-10-07 20:13:16 +01:00
Nick 5dea803e92 fix(tools): SECURITY-ADJACENT parity repair — the approval boundary (contract BUILDER-2026-10-07T19:10:38Z-cc6be2-unknown)
src/go/tool-executor readOnlyTools diverged from the Python oracle in
BOTH directions:
- it wrongly ADDED run_command — a mutating tool the oracle routes
  through operator approval; the Go port would execute it read-only
  (the dangerous direction: approval bypass)
- it MISSED 8 of the 12 oracle read-only tools (git_diff, git_log,
  repo_map, read_persona, query_memory, check_growth, diagnose_behavior,
  find_capability)

Fixed to the exact 12-tool set, and pinned by a FROZEN truth battery
(gen_tool_executor_auth_truth.py → truth_auth.json) asserting set
equality both ways forever — the test's failure message says it plainly:
THE APPROVAL BOUNDARY HAS DRIFTED. Per-wave routine held (gates bare,
spine rebuilt, both snapshots shipped).
2026-10-07 20:13:13 +01:00
conductor 30ec1c8153 chore(release): auto-bump version to 0.125.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 20:01:07 +01:00
Nick ad7e6fef2c Merge remote-tracking branch 'gitea/master' 2026-10-07 20:00:59 +01:00
Nick df9290fcff feat(go): memory-store batch 3 — the scoring/abstention kernel (contract BUILDER-2026-10-07T18:57:06Z-c2429c-unknown)
The recall scoring kernel ported to memory-store-deep:
- ScoreRecallRows: rank-ordered metadata scoring — confidence (0.7
  default) x temperature (1.0 default) x recency (half-life 30.0,
  hl/(hl+days) on non-negative days, timestamp failures degrade to 1.0);
  dense base vs lexical base+5.0; the exact round discipline
  (rrf/conf/temp 4-4-2-2)
- PositionAware: the lost-in-the-middle re-rank — best item FIRST,
  second-best LAST, same items, order-only (no-op < 3)
- the FULL abstention ladder verbatim: neither → []; lexical-only
  max-relevance gate; dense-only min_score → >=0.74 position-aware →
  >=2 rows with 0.08 gap → gap-abstain → position-aware(map_reduce);
  hybrid min_score then position-aware(map_reduce)

PARITY: truth_score.json — 6 cases covering every abstention branch
(hybrid 3, dense-only high-conf/gap-abstain/min-score, lexical gate,
neither). First-run green — the oracle's fixture honesty paid forward.
2026-10-07 20:00:56 +01:00
conductor 779a26eaae chore(release): auto-bump version to 0.124.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 19:50:45 +01:00
Nick 8111e0b3ba Merge remote-tracking branch 'gitea/master' 2026-10-07 19:50:36 +01:00
Nick 3f440693de feat(go): scaffold batch 3 — the closure mechanics (contract BUILDER-2026-10-07T18:42:38Z-52829e-unknown)
CloseContract ported to Go:
- CloseQuality: the pure bucket math verbatim — pred_existing from
  affected_entities + the frontier's 4 keys, correctly_predicted /
  predicted_but_unaffected / expected_new_observed / expected_new_missing,
  expected_runtime prefix-matching (both directions), genuinely_unexpected_new,
  legacy missed/needless aliases, candidate_corrections, evidence_sources
- RecordObservedImpact: observations append (detail[:300]), sorted
  implicated, evidence_count — INDEPENDENTLY SOURCED, never re-derives
  the prediction (the §17 invariant)
- CloseContract: superseding/verifying phase gate (exact error text),
  inputs assembly, quality stamping, optional scope_check, checkpoint
  append, phase/status/next_action/observed_at mutation
- ContractEntity gains Ext (the ext dict the lifecycle carries)

PARITY: truth_close.json — 4 cases (edge-covering bucket math, full
lifecycle close with scope_check, both gate errors). The ratchet stopped
the first commit attempt (2 new generator handlers) — annotated with
intentional-silence reasons, re-landed. Per-wave routine: gates BARE,
spine rebuilt, both snapshots shipped.
2026-10-07 19:50:33 +01:00
conductor 1d4aa87ca4 chore(release): auto-bump version to 0.123.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 19:35:15 +01:00
Nick 61432632f7 feat(go): build-mode batch 2 — worktree git operations, plan/exec split (contract BUILDER-2026-10-07T18:26:40Z-8be89a-unknown)
The worktree lifecycle ported to build-mode with a TESTABILITY split:
- the PLAN layer is pure and oracle-batteried: WorktreePlan / DeletePlan
  (two bare steps, no short-circuit — parity with the Python's separate
  subprocess calls) / MergePlan (exact message format 'Build {id}: {desc}')
- the EXEC is reactive and fixture-proven: a REAL tmp git repo round-trip
  (create → commit on the branch → merge lands the file in the repo root
  → delete clears worktree+branch), plus the duplicate-create and
  conflicting-merge failure texts (the wrapper 'Failed to merge
  worktree:' is the stable contract; git's own message varies by version —
  the ORACLE's fixture text pinned the shape, the live git pinned the
  variance)
- state transitions verified: worktree_path set on create, cleared by
  merge-follows-delete

PARITY: truth_worktree.json — 6 cases (3 argv plans, state round-trip,
both error texts) from the live BuildManager driving real git with the
clock frozen. Per-wave routine held: gates BARE, spine rebuilt, both
store snapshots shipped.
2026-10-07 19:35:04 +01:00
Nick 5601fee02a Merge remote-tracking branch 'gitea/master' 2026-10-07 19:18:46 +01:00
Nick cdca988d00 chore(spine): rebuild the structural manifest — the scaffold store-layer + memory-store read-path batches added modules the committed spine predated (second occurrence of this class; SPINE REBUILD now joins scaffolding_coverage in the per-wave routine) 2026-10-07 19:18:46 +01:00
conductor d05a9e6741 chore(release): auto-bump version to 0.122.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 19:14:10 +01:00
Nick ddb7490cc6 Merge remote-tracking branch 'gitea/master' 2026-10-07 19:13:58 +01:00
Nick d12fbab06a feat(go): memory-store batch 2 — the deterministic read-path (contract BUILDER-2026-10-07T18:05:47Z-152c16-unknown)
Read-path mechanics of memory_store.py ported to memory-store-deep:
- FtsQueryConjunctive: extended-Boolean AND (len>3 non-stop terms, len>2
  fallback, quoted, first 8, space-joined) — msStopwords extracted
  verbatim from the live module
- ChunkText: paragraph recombination + 1.5x hard-split
- RankFusion: RRF k=15, three modes (hybrid/dense-only/lexical-only with
  min-max bm25 normalize), insertion-ordered ranks, NUMERIC ids out
  (Python int ids; the string map is internal)
- ProjectChunkRows: -rrf sorted, text[:400], rrf round 4, budget cap
- MapReduce: top-3 per chunk + the REDUCE pass (all mapped re-sorted by
  score) — the oracle caught my chunk-order-only first read
- FuseResults: NormKey (lowercase whitespace-collapse) dedup, exact field
  shape, kind-wise min-max scale-alignment (the audit fix), final sort

PARITY: truth_recall.json — 17 cases from the _py_* oracles; fixtures
SHIP in the inputs. Embed surfaces stay Python-side (model-coupled).
2026-10-07 19:13:55 +01:00
conductor 19db782906 chore(release): auto-bump version to 0.121.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 19:01:18 +01:00
Nick e0d7d7bb03 Merge remote-tracking branch 'gitea/master' 2026-10-07 18:56:20 +01:00
Nick 5dda590588 feat(go): scaffold batch 2 — the transactional store layer (contract BUILDER-2026-10-07T17:47:00Z-960f12)
The §21 transactional boundary ported to Go:
- CanonicalDigest: sha256 of the sorted+redacted semantic payload (byte
  parity with json.dumps(sort_keys=True): Python default separators,
  ensure_ascii escaping) — the §12B real commit semantics
- RedactSecrets (exact names + credential substrings), SortedNorm
  (recursive; lists sort by their dumps repr)
- Commit (intended-vs-on-disk digest readback), VerifyOnDisk (the
  forgotten-save lie detector), SaveVerified — exact Python assertion
  texts including the %r single-quote form
- SaveDoc: schema guard + indent=1 sort_keys write (the disk format)
- ImpactFrontier: the bounded Spine-edges-only walk — change_kind
  inference, NON_OBLIGATION/STALE/CANDIDATE skips, contract expansion
  (implements/consumes); the ORACLE corrected my inverted party
  computation (c==b → party=a)
- SnapshotPredictedFrontier + SpineBridgeEntity (identity-only bridge)

PARITY: truth_store.json — 10 cases incl. digest key-order stability,
secret redaction shape, forgotten-save detection with exact text, three
frontier modes. Frontier docs SHIP in the truth inputs (batch-1 lesson).
The ratchet blocked the first commit attempt (new generator handler) —
the bare-command law held; annotated and re-landed.
2026-10-07 18:56:17 +01:00
conductor 953197823a chore(release): auto-bump version to 0.120.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 18:48:14 +01:00
Nick eba579f99f Merge remote-tracking branch 'gitea/master' 2026-10-07 18:42:22 +01:00
Nick 9fd3d8b15d fix(lint): scaffold truth generator — intentional oracle-error handler annotated (ratchet back to 345/75; the pipe-mask that let the violation commit is the recurring lesson, re-learned) 2026-10-07 18:42:22 +01:00
conductor 5171cc3ef8 chore(release): auto-bump version to 0.120.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 18:41:17 +01:00
Nick 64f77e82ff feat(go): scaffold batch 1 — the contract-lifecycle core (contract BUILDER-2026-10-07T17:27:25Z-6bc737-unknown)
Deterministic core of scaffold.py ported to src/go/scaffold (integrated
with the existing W5c classify/status CLI, not replacing it):
- the 9-phase STORE machine (22 legal edges incl. migrating/superseding/
  failed-retry) — DISTINCT from the 16-edge contract machine frozen in the
  Rust membrane; two layers, each authoritative at its own boundary
- TerminalPhases DERIVED from the map (single authority)
- NewContract (identity defaults, sorted-list discipline, doc mutation via
  add_entity parity, duplicate error), TransitionContract (exact error
  texts incl. Python list-repr quoting, checkpoint note[:200], status
  updates, next_action, observed_at)
- CheckScope (equality/prefix/substring; UNKNOWN on unscoped — never
  silently green), LayoutObligations (FLEXIBLE vs USER-LOCKED texts),
  JsonFrontier (Python json.dumps byte form: insertion-ordered keys,
  ', '/': ' separators, ensure_ascii escaping)

PARITY: truth_scaffold.json — 19 cases from the live Scaffold oracle
(now frozen); the generator SHIPS scope+doc in the inputs so the Go replay
needs no oracle-side state. Governance note: the completeness gate required
constraints={'contracts': ['contract:construction_contract_lifecycle']} —
full spine id, prose never counts.
2026-10-07 18:41:07 +01:00
Nick d64df3d011 Merge remote-tracking branch 'gitea/master' 2026-10-07 18:20:40 +01:00
Nick 4d1064449b chore(scaffold): ship the contract store snapshot — the build-mode/lexicon/coverage contracts existed in the live store but not in the committed one (the scaffolding gate in CI reads the COMMITTED store; same class as the go.mod parity law: local-only state is not delivery) 2026-10-07 18:20:40 +01:00
conductor 1e9ddea492 chore(release): auto-bump version to 0.119.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 18:18:19 +01:00
Nick c5ecbacf98 feat(go): build-mode batch 1 — the phase machine + ledger mechanics (contract BUILDER-2026-10-07T17:08:00Z-6e177d-unknown)
Deterministic core of build_mode.py ported to src/go/build-mode:
- the 11-phase machine in declaration order + PhaseStatus/BuildPhase/
  VerificationTier constants
- LogPhase + the named completion functions, clock INJECTED
  (date-time-bomb law); side effects pinned: complete_plan assigns
  batches, start_correct increments corrections
- ResumePoint (reversed declaration-order scan) + the pinned quirk:
  complete_verify(false) records status 'complete' with the failure in
  DETAILS — the resume scan never fires on it
- BuildStatus shape; build_id %Y%m%d-%H%M%S from the injected clock
- module-family CLI (curator-core convention); worktree git ops stay
  reactive (batch 2)

PARITY: truth_buildmode.json — 5 flow cases with RECORDED CALLS replayed
through the ported functions; the oracle's ledger pinned two subtleties my
first read missed (complete_feasible's nested details key; assess/research
log their parameter dicts, not kwargs maps). Comparisons are canonical-JSON
(Go []string vs decoded []interface{} differ in type, not bytes).
2026-10-07 18:18:07 +01:00
Nick 892fb4cafe Merge remote-tracking branch 'gitea/master' 2026-10-07 18:04:47 +01:00
Nick c2a25d187b test(lexicon): recovered-module coverage contract (contract BUILDER-2026-10-07T17:04:41Z-8d5c90-unknown)
The henxel gate flagged drift-ledger.py + lexicon_evolution.py as
ungoverned new modules (run 19849). This pins their core semantics:
coherence ladder, stem-aware grounding parking, promote verdict shapes,
drift-ledger importlib load + check on a synthetic store.

Fixture honesty held: the live oracle corrected three hand-labeled cases —
in a Nick-centric corpus 'nick' is df-1 distinctive, so nick-bearing facts
genuinely ground/relate; the novel-axis probes avoid it.
2026-10-07 18:04:47 +01:00
conductor 9189e5083c chore(release): auto-bump version to 0.118.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 17:57:28 +01:00
Nick 4c78ac0cfe Merge remote-tracking branch 'gitea/master' 2026-10-07 17:56:45 +01:00
Nick d6028ca928 fix(governance): recovery fallout alignment (contract BUILDER-2026-10-07T16:56:29Z-9c97ce-unknown)
- drift-ledger.py: 3 intentional degrade handlers annotated noqa+reason
  (ratchet back to 345/75 baseline; behavior byte-identical)
- store/spine/current.json re-extracted: the two recovered modules + their
  memory_env import edges enter the structural self-model (299 entities)
- config/umbrellas.json: drift-ledger + lexicon_evolution join the memory
  umbrella membership
2026-10-07 17:56:36 +01:00
conductor 3aeecf5142 chore(release): auto-bump version to 0.118.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 17:46:21 +01:00
Nick 723836e27a Merge remote-tracking branch 'gitea/master' 2026-10-07 17:45:32 +01:00
Nick a7b13099aa feat(go): curator-core batch 2 — the REAL lexicon semantics (contract BUILDER-2026-10-07T16:31:17Z-997c9d)
The batch-1 LePromoteEligible encoded the LEXICON DEGRADE — the phantom's
shadow ('lexicon module unavailable — strength gate only'). With the phantom
recovered from provenance (5749029), the live oracle returns real verdicts
and the port was behind it. This batch closes that gap:

- lexicon.go: verbatim port of the recovered lexicon_evolution.py —
  ContentWords, Stem (ordered suffix rules + y->i fold), DistinctiveTerms
  (stem-aware df, max(total*0.05, 1.5)), SharedDistinctive, Coherence
  (conflict/consistent/novel), GroundedLex, PromoteEligible — including the
  Python list-repr byte format in the shared-terms reason
- LePromoteEligible upgraded to the real semantics (wash-adjusted threshold
  preserved; degrade path kept as LePromoteEligibleDegrade for the record)
- truth.json le_promote_eligible cases REGENERATED against the live oracle
  (degrade shadow gone); truth_lexicon.json: 14 new direct cases —
  coherence (incl. common-word-is-not-a-relation), grounded (stem-aware,
  invented-token parking), distinctive terms, all four promote verdicts
- scripts/gen_curator_lexicon_truth.py: the committed generator

Also discovered during this batch: the 12:52 checkpoint was 45 min stale —
batches 2-4 (candidates/coherence/decay+compress) had already landed
(9ccb331..38fd578); duplicate work discarded, committed batches kept.
2026-10-07 17:45:20 +01:00
conductor c205c25598 chore(release): auto-bump version to 0.117.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 17:31:04 +01:00
Nick a8feb3eee4 Merge remote-tracking branch 'gitea/master' 2026-10-07 17:28:26 +01:00
Nick 574902926e fix(curator): recover the two provenance phantoms — lexicon_evolution + drift-ledger (contract BUILDER-2026-10-07T16:26:19Z-71624c)
Provenance over reconstruction (cycle-5 law):
- lexicon_evolution.py: ~/odysseus 0308ccc6 (feat/memory-lifecycle), verbatim
  229 lines — the branch HEAD had DELETED it, which is why the import was a
  phantom everywhere; no reconstruction was needed
- drift-ledger.py: ~/odysseus rework/product-api HEAD 8524e8e1 — the later
  evolution of the original (adds the package-relative import fallback that
  makes the curator's importlib load robust)

Restored semantics:
- le_promote_eligible now returns real lexicon verdicts (distinctive-term
  sharing / novel-axis) instead of the strength-only degrade
- _contradicts_identity runs real coherence conflict verdicts
- the drift write-gate (run_plan) loads and enforces (check strict=False rc=0)

Verified live: both modules import; curator call sites return lexicon
verdicts (degrade string absent); drift gate check passes; 93 related
tests green; ruff clean.
2026-10-07 17:28:08 +01:00
conductor 70389268a2 chore(release): auto-bump version to 0.117.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 17:24:01 +01:00
Nick 3199e36cd4 Merge remote-tracking branch 'gitea/master' 2026-10-07 17:23:48 +01:00
Nick df6f52e652 fix(tools): repair wave — master red closed (contract BUILDER-2026-10-07T16:11:35Z-70ca24)
- tool-executor batch 6: jsonUnmarshal/paramOrder/rawKeyOrder moved from
  truth_prompt_test.go into json_order.go — the non-test build failed with
  undefined symbols (go-surface run 19792 job 56712)
- truth batteries regenerated HERMETICALLY via the committed live-oracle
  generator (scripts/gen_tool_executor_truth.py): {{TMP}}/{{WROOT}}
  placeholders replace the generation host's /mnt/workdrive/tmp* paths in
  inputs AND expectations; fixtures manifest carried in each truth file;
  tests recreate fixtures under their own allowed roots (truth_harness_test.go)
- list_dir comparison now exact (both sides sort); grep compares sorted
  line sets (oracle os.walk order is OS-dependent, not a parity property);
  truncation marker pinned by a new hermetic test
- construction_decision lib.rs: the 'rune dichotomy:' rule-string parity
  half committed (Python half landed in 6d63034; strings verified identical)
- stale in-tree binaries rebuilt from current source
2026-10-07 17:23:15 +01:00
conductor bd2137fc4f chore(release): auto-bump version to 0.117.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 16:57:51 +01:00
conductor 060a2e65ec chore(release): auto-bump version to 0.117.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 16:54:24 +01:00
Nick fe6e9dfbbc feat(go): tool-executor batch 6 — tool prompt + registry (W4b parity, contract-closing)
TOOLS registry ported as embedded data (tools_registry.json, generated from the live Python oracle — 19 tools, order preserved); BuildToolPrompt byte-identical with build_tool_prompt (verified against truth_prompt.json, including the parameter-order preservation via raw-JSON key scanning). Registry shape test locks order + required fields. Contract a259d6 closes: 6 batches (security paths, approval lifecycle, file ops, list/grep/cmd-gate, git+quality, prompt+registry).
2026-10-07 16:54:12 +01:00
Nick 360fbb5f94 sync: remote bump 2026-10-07 16:48:41 +01:00
Nick fbb74b83d3 feat(go): tool-executor batch 5 — compatibility protocol parser + result formatting (W4b parity)
Ports _protocol_entries net behavior (balanced-JSON scan with string/escape awareness, {"tool","args"} envelope, fenced + XML wrappers parse transparently), parse_tool_call (first-call legacy shape — envelope keys are tool/args, NOT name/arguments: live-oracle verified), format_tool_result (exact [Tool result]/[Tool error] prefixes, Unknown-error default). Unknown tools parse fine — validation is dispatch job (fail-open parse, fail-closed dispatch). 8-case battery + unknown-tool test green. Contract a259d6: 5 of 6 batches done.
2026-10-07 16:47:42 +01:00
conductor ceffe7d2e4 chore(release): auto-bump version to 0.116.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 16:40:50 +01:00
Nick 63bee210eb sync: remote bump 2026-10-07 16:40:44 +01:00
Nick d5fba689de feat(go): tool-executor batch 4 — git ops + quality gate (W4b parity)
Ports git_diff/git_log/git_commit (staged flag, limit+path filter, message-required, -A default staging) and the quality trio: LANGUAGE_CONFIG table byte-parity, _detect_language (lowercased extension), lint (30s, ✓ clean verdict), tests (120s, cwd=file-parent-or-dir, stderr appended), format. Decision-layer parity tests green; the command runners gate on tool availability per environment.
2026-10-07 16:40:16 +01:00
conductor 39d83fc388 chore(release): auto-bump version to 0.115.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 16:33:30 +01:00
Nick 1b5f774bac sync: remote bump 2026-10-07 16:33:24 +01:00
Nick 98cde56413 feat(go): tool-executor batch 3 — list/grep/command gate (W4b parity)
Ports _list_directory (emoji prefixes, sorted, pattern filter), _search_files (recursive grep: case-insensitive substring, include filter, 50-match truncate marker, rel:line: format — with the OS-LISTDIR ORDER parity: Go os.ReadDir sorts but Python os.listdir is raw OS order; fixed via Readdirnames(-1) with files-then-subdirs walk), and the command gate (shell-syntax refusal set | > < ; && || ` $(, explicit-shell requirement). Oracle-driven fixes: raw-order walk, ListDirectory keeps sorted (Python sorted(iterdir)).
2026-10-07 16:32:49 +01:00
Nick e267c51293 feat(go): tool-executor batch 2 — windowed read, diff generation, atomic write + undo (W4b parity)
Ports _read_file (1-indexed offset, default-100 window, SWE-agent numbering, truncation context on both ends), generate_diff (unified diff, difflib hunk semantics — two oracle-driven fixes: hunk start on both sides shares the context window, and range lengths count ctx+mid on each side), WriteFile (diff-for-confirmation, never writes), ApplyContent (atomic tmp+rename with backup), BackupFile/undo. Contract a259d6 in building.
2026-10-07 16:17:20 +01:00
conductor 5694193959 chore(release): auto-bump version to 0.114.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 15:13:28 +01:00
Nick 7667c2937c feat(go): tool-executor security core — path validation + approval lifecycle digests (W4b parity)
Ports resolve_path/is_path_allowed/validate_read_path/validate_write_path with the TRUTHFUL-REASON semantics (blocked-under-allowed reports the blocklist, not "outside allowed"; write denials name the allowed dirs — P0-05 P1). Ports the approval lifecycle: PreparedTool, canonical digest (BYTE-IDENTICAL with Python json.dumps sort_keys+separators — verified via live subprocess oracle in the test), path digest (missing/file-content/other branches), one-shot consumption with immediate revalidation (expired/modified/target-changed). Contract BUILDER-2026-10-07T13:37:12Z-a259d6 in building.
2026-10-07 15:10:46 +01:00
conductor b537018ab2 chore(release): auto-bump version to 0.113.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 14:52:26 +01:00
conductor eac67a24da chore(release): auto-bump version to 0.112.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 14:38:40 +01:00
Nick a804cb6436 feat(go): warm-router batch 5 — neuron listing mechanics (W5b parity, contract-closing)
Ports _parse_trigger (comma-split; the JSON-array non-handling is reproduced exactly — oracle-verified), list_neurons body-knowledge construction (fires-on/first-5, knows/80-char, wash lifecycle notes), and the token estimate. SQL read stays at the store seam. 5-case battery + token tests green. The warm-router contract now spans: scoring, route allocation, event/proactive dispatch, priming, listing.
2026-10-07 14:33:45 +01:00
conductor 883554a2db chore(release): auto-bump version to 0.112.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 14:28:08 +01:00
Nick daba0e3294 feat(go): warm-router batch 4 — ACTIVE warm-tier priming mechanics (W5b parity)
Ports _prime_neurons (max-score re-fire semantics, turn counting), _decay_primes (PRIME_DECAY=0.6, PRIME_FLOOR=0.15), and the bloat guard (empty/idle>24h eviction, hard cap 40 sessions by fewest turns). Association-neighbour priming weight 0.5 defined. Persistence stays caller-side. 3-case oracle battery + max-semantics test green.
2026-10-07 14:23:36 +01:00
conductor c4825c2d66 chore(release): auto-bump version to 0.111.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 14:13:29 +01:00
Nick 02f7d1ca42 sync: remote bump 2026-10-07 14:12:00 +01:00
Nick cc52bfb69f feat(go): warm-router batch 3 — reactive event map + proactive query assembly (W5b parity)
Ports route_event (the 8-event neuron-topic map, event-data context truncated to 200 chars) and route_proactive (top-3 task summaries capped at 100 chars, errors→diagnostic query, commitments→reminder query). The final route() call stays caller-side (embeddings + session state are I/O). 6-case live-oracle battery green.
2026-10-07 14:11:34 +01:00
conductor e0d0923488 chore(release): auto-bump version to 0.111.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 14:09:48 +01:00
Nick 47c2e56393 feat(go): warm-router route allocation — budget-constrained firing (W5b batch 2 parity)
The pure slice of route(): top-scoring relevant neuron fires its FULL body (relevance outranks budget — parity law), subsequent neurons fire AAAK summaries when available, token cost max(1, len/4), over-budget neurons skipped (not truncated). 2-case live-oracle battery + budget-skip + top-never-compressed tests. I/O (embeddings, session state, priming, associations, use_history) stays on the caller side per the dichotomy.
2026-10-07 14:04:00 +01:00
Nick 004d168157 sync: remote bump 2026-10-07 13:55:44 +01:00
Nick 87e1320175 feat(go): warm-router scoring core — gated combination + ACT-R finish bonuses (W5b parity)
Ports _py_score_gate (keyword primary gate, semantic secondary >0.55, context 0.3/trigger capped at 1.0) and _py_score_finish (importance, base-level 0.1x capped 0.3, wash log1p boost, cache 0.1x capped 0.2, body-knowledge overlap 0.03/word capped 0.15, ACT-R fan penalty >10 triggers max 50%, length bonus 0.25 for >6-char triggers, protected boost 4.0 for persona-alfred + philosophy-sagan-core). 7-case live-oracle battery + protected-boost delta test. I/O signals stay arguments (embeddings/SQLite/cache are the caller reach) — the pure core is the parity surface.
2026-10-07 13:55:27 +01:00
conductor 7a27ed557c chore(release): auto-bump version to 0.110.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:55:13 +01:00
Nick ec20649e35 sync: remote bump 2026-10-07 13:47:51 +01:00
Nick 303848a0fb fix(go): subsystem-health lock-copy — pointer registry, fields-only marshal (same family as cron/provider fixes) 2026-10-07 13:47:10 +01:00
conductor 74d6b7393b chore(release): auto-bump version to 0.110.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:42:04 +01:00
Nick f30b62c457 sync: remote bump 2026-10-07 13:38:45 +01:00
Nick 38fd57846c feat(go): curator-core batch 4 — decay pass + compress clustering mechanics (W5c parity)
Ports decay_pass (STALE_DAYS=90/DECAY_RATE=0.2/FORGET_FLOOR=0.2, constitution+candidates skipped, FORGET journaling) and the compress clustering pieces (_tokens/_overlap Jaccard, greedy >=0.35 cluster join, longest-member representative + (also: …) summary). 4-case decay truth + cluster semantics — one expectation corrected BY the oracle (pairwise 0.333 < 0.35 means the third spine line never joins; fixture honesty held). Store-block seam lands with the memory kernel membrane.
2026-10-07 13:38:43 +01:00
Nick 9628587bfa fix(ci): tmpfs guard — prune stale /tmp cargo target dirs before builds
Root cause of the ensurepip/venv failures across lint+unit-tests: /tmp (32G tmpfs) was 100% full — 23.4GB across 304 per-run cargo target dirs from the CARGO_TARGET_DIR redirection. Runner-side cleanup now keeps the 3 newest and drops the rest at the start of every go job. Verified: venv creation works again locally.
2026-10-07 13:36:11 +01:00
conductor 5421bf931b chore(release): auto-bump version to 0.109.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:29:18 +01:00
conductor 292c511ab5 chore(release): auto-bump version to 0.109.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:25:37 +01:00
Nick 654b89d84f fix(go): scaffold missing strings import — caught by CI go loop (local builds masked it? no: pre-merge commit lacked it) 2026-10-07 13:23:52 +01:00
Nick 19b80c95a5 sync: remote bump 2026-10-07 13:19:28 +01:00
Nick 6ba8302bc8 feat(go): curator-core batch 3 — the coherence gate (distinctive-word sessions grounding)
Ports _grounded_in_sessions: rarest-significant-word probe over the stop-set-filtered fact words, >=2 DISTINCT session hits required (independence beats multiplicity — one rich session cannot trigger false growth), absent rarest word = park. CoherenceCheck entry with source-order semantics and a nil-corpus hook (kernel wiring comes via the membrane). Truth battery frozen from the live oracle including the subtle teaching cases (the oracle showed preserved-vs-kept wording differences legitimately fail grounding — the gate is deliberately strict).
2026-10-07 13:18:53 +01:00
conductor 9e5830432c chore(release): auto-bump version to 0.108.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:17:00 +01:00
Nick f75c887bd7 sync: remote bump 2026-10-07 13:16:54 +01:00
Nick 3e1c6c0ca4 fix(go): provider-registry lock-copy (pointer receivers, fields-only marshal) + operator-deep pid-unique test binary
Same copies-lock family as cron-engine: saveRegistry/loadRegistry now pointer-based, JSON marshals only durable fields (Providers/ConfigPath). operator-deep TestMain and runtime resolver build the adaptive-router test binary under a pid-unique temp path — the shared /tmp/go-adaptive-router collided with a stale root-owned artifact across runs.
2026-10-07 13:16:41 +01:00
conductor c99b9fef14 chore(release): auto-bump version to 0.108.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 13:07:01 +01:00
Nick 39d7083611 sync: remote bump 2026-10-07 13:06:52 +01:00
Nick 9ccb331a41 fix(governance): scaffold-membrane retro-adopted (the gate caught its own author) + curator-core CLI entry
Two finds from the newest tip: (1) the absolute-scaffolding gate flagged scaffold-membrane because I committed it before extending the adoption record — the law working as designed; contract opened, walked draft→closed, record extended. (2) curator-core is a main-package family member but lacked main() — CLI entry added (one JSON op per invocation, module-family contract).
2026-10-07 13:06:39 +01:00
conductor dc0005423b chore(release): auto-bump version to 0.108.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 12:59:06 +01:00
Nick 01ae71de41 sync: remote bump 2026-10-07 12:58:56 +01:00
Nick eb9b3916a7 fix(umbrellas): governance umbrella maps the recovered grading + worthiness modules; spine store rebuilt
test_umbrella_impact contract A: no module outside the map. The two recovered modules were unmapped; spine rebuild + governance-umbrella registration closes it.
2026-10-07 12:58:43 +01:00
Nick 5707045f96 fix(go): cron-engine saveEngine/loadEngine take pointers — sync.Mutex must never be copied (go vet copies-lock)
CI go-surface caught the lock copy: saveEngine(ce CronEngine) copied the embedded mutex on every call. Now pointer-based throughout; JSON marshals only the durable Tasks map.
2026-10-07 12:56:42 +01:00
Nick c0d1898826 feat(go): curator-core batch 2 — candidate ledger + monthly journal (W5c parity)
Ports promote_candidates (3-sighting durability, explicit-op and strength>=3 ledger skips, coherence/drift gate hooks), decay_candidates (CANDIDATE_STALE_RUNS/24 staleness, CANDIDATE-DROP journaling), journal_append (byte-format parity: `ts` **OP** → `target` + indented fields). 6-case live-oracle truth battery. Two oracle-driven fixes: strength accepts int (Python int() semantics), test interface comparison decodes JSON numbers as float64.
2026-10-07 12:54:09 +01:00
Nick 0d3bf42e7f fix(ci): Survivor staticlib step builds construction_interface too — scaffold-membrane links it 2026-10-07 12:49:18 +01:00
Nick 1ebd66bb4c feat(scaffolding): the pipeline IS the hybrid — Rust phase-machine staticlib + Go membrane (Survivor)
Nick's directive: scaffolding was a Python file when it should be the hybrid process. The construction_interface crate is now the REAL membrane: deterministic phase machine (16-edge transition matrix, envelope shape-parity with _envelope) as Rust staticlib, Go scaffold-membrane package linking it via cgo (no dlopen/env vars), 8-edge legal + 8-edge illegal parity battery + live-probe test. FFI symbols ci_validate/ci_transition. The Python ConstructionInterface remains the reactive/intake side per the dichotomy; the deterministic verdicts now come from the kernel. Contract BUILDER-2026-10-07T11:48:20Z-1ce250 opened first and walked draft→closed — the law applied to its own enforcement.
2026-10-07 12:48:46 +01:00
Nick e44ee897db feat(governance): ABSOLUTE SCAFFOLDING LAW — CI gate verifies every Go/Rust unit has a construction contract
Nick's directive: scaffolding is not optional. New CI step (scaffolding_coverage.py) fails the build when any src/go or src/rust unit lacks a construction contract mapping; retro-adoption record carries the pre-law units (120) with their BUILDER-2026-10-07T11:3x contract ids; the 13 contract-scoped units verify against live store/scaffold/ JSON. Frozen tests prove the gate catches uncovered units AND accepts adopted ones. From now on: contract opens BEFORE the commit lands, never after.
2026-10-07 12:38:09 +01:00
conductor 7b21a34117 chore(release): auto-bump version to 0.107.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 12:35:10 +01:00
conductor 45cc070b02 chore(release): auto-bump version to 0.107.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 12:30:09 +01:00
Nick 497f229af1 fix(go): context-broker go.mod carries its sqlite dependency — committed module files never matched the imports
The committed go.mod was a 3-line stub (no require, no go.sum) while main.go imports modernc.org/sqlite; local builds worked only because the local files had drifted ahead. CI go-surface failed on exactly that gap. Full go.mod + go.sum committed.
2026-10-07 12:25:19 +01:00
Nick eb897663af sync: remote bump 2026-10-07 12:24:38 +01:00
Nick 11b6832edd feat(go): curator-core batch 1 — the write-gate mechanics (W5c parity)
Ports curator.py mechanical core: BlockPath descriptor, EntryKey (raw sha256[:16]) vs CandidateKey (NORMALIZED sha256[:16] — distinct by design), _norm markdown-strip semantics (strip+lower, byte-verified against the oracle), FindEntry fuzzy line match, LePromoteEligible drift-with-integrity thresholds (wash>=20→1, >=10→2, >=5→2) with the lexicon-unavailable degrade verdict byte-identical to Python, DetectConflict conservative duplicate/containment, SanitizeSlug with procedure fallback, EnsureEntryMeta/TouchEntry index mechanics. 13-case Python-oracle truth battery committed; 3 spot-fixes made BY the oracle (Norm trailing-space, header lowercase, padded-key divergence) — fixture honesty held.
2026-10-07 12:24:14 +01:00
Nick 3d55f30429 fix(worthiness): noqa must sit on the diagnostic line — S110 points at except, not pass
The ruff noqa pragma is attached to the line the diagnostic points at; S110s diagnostic is the except line, so the S110 noqa on the pass line was ignored. Both codes combined on the except line; baseline 345/75 restored.
2026-10-07 12:23:11 +01:00
Nick a932645075 fix(worthiness): silent-handler ledger annotations — the recovered module ships with the compliant-handler spec 2026-10-07 12:18:41 +01:00
conductor 6c3b8edbbe chore(release): auto-bump version to 0.106.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 12:11:35 +01:00
conductor 7cecc90ac6 chore(release): auto-bump version to 0.106.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 12:07:00 +01:00
Nick 8d23d43ab9 docs: supersession report — programmatic evidence, curator vs memory_consolidation verdict + the 16-phantom census 2026-10-07 12:04:40 +01:00
Nick 4a75d14c68 sync: remote bumps 2026-10-07 12:00:46 +01:00
Nick 6437daf606 fix(curator): recover the ORIGINAL grading + worthiness from the Odysseus source branch — reconstruction replaced by provenance
The phantom-dependency investigation traced both modules to their origin: ~/odysseus feat/memory-lifecycle (memory_platform/grading.py 192 lines, worthiness.py 411 lines). The rune port copied curator.py but never its two dependencies. ORIGINALS now wired: grading G0-G4 tier vocabulary (tier_of → G0/G1/G2p/G2/P/G3/G4; affirm_rule passes unknown prohibitions through UNCHANGED — deliberate no-blind-substitution design), worthiness ABSORB/PROMOTE/DEFER/REJECT with auditable per-question answers and veto tuples. Frozen tests live-verified against the originals (5+5 green). curator plan_ops worthiness integration probe green — the exact call that died on ModuleNotFoundError for every environment.
2026-10-07 12:00:35 +01:00
Nick afddbf2f65 fix(kernels): scaffold_core language.rs was a stale R2-Wave-1 copy — aligned to the Wave-0 corrected dichotomy oracle
Canary-caught (4fa9a14): scaffold_core carried the OLD pre-64ba7cf signal lists (8 signals, different order, "storage" instead of "store"), so its evidence arrays and one placement diverged from the Python oracle. language.rs GO_SIGNALS/RUST_SIGNALS now byte-identical in order+vocabulary to construction_decision.py::_py_language_placement. One hand-labeled canary fixture ("network+storage → hybrid") was itself stale — re-derived from the LIVE oracle per the fixture-honesty law: network+storage → go (storage is not a rust signal), and a true hybrid row added with "store".
2026-10-07 11:55:40 +01:00
Nick 612da55189 fix(ci): Survivor staticlib step must build cognition_core too — cognition package links libcognition_core.a in-tree
The 4fa9a14 go-surface failure: the in-tree cargo build only covered route_risk_flags (adaptive-router); src/go/cognition links libcognition_core.a which was built by hand locally but never in CI. Both crates now build in-tree before the go loop; the go test stderr was masked by 2>/dev/null so the missing-lib error was invisible — the parity gate step (unmasked) is the diagnostic channel for Survivor failures.
2026-10-07 11:49:22 +01:00
conductor 8b2eee07d0 chore(release): auto-bump version to 0.106.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 11:48:10 +01:00
conductor a5baeb8e18 chore(release): auto-bump version to 0.106.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 11:43:15 +01:00
Nick df6e0aa1cf fix(curator): reconstruct the never-committed grading dependency — curator was dead code in every environment
curator.py imported 'grading' since #314, but the module was never committed (ext:grading in the rebase inventory; absent from repo, deployed tree, and venv). Every import failed; canary_full_system caught the ImportError and ran with curator=None — the entire 1,838-line curator silently disabled. Reconstruction implements the observable contract from plan_ops routing (grade_fact keyword routing over the store grade taxonomy, tier_of store tiers, affirm_rule positive-affirmation table) with frozen tests. Module docstring marks it RECONSTRUCTED, not recovered.
2026-10-07 11:22:16 +01:00
Nick 4fa9a143af sync: remote bump 2026-10-07 11:13:43 +01:00
Nick 53abd24d49 fix(ci): go loop builds into workspace bins/ — /tmp collision was killing go-surface 2026-10-07 11:13:29 +01:00
conductor 32c4184f7b chore(release): auto-bump version to 0.106.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 11:11:03 +01:00
Nick 65638f3590 fix(voice): fresh-store crashes in voice layer — first-boot defect caught by canary_native (W6)
source_profiles() and get_delivery_register() crashed with "no such table" on a fresh RUNE_HOME (first boot has no entries/chunks tables yet). The voice layer must blend from zero learned emphasis and report no-register — degrade paths added per the existing empty-result semantics. Canary caught a real product defect (first-run experience), not a test-env artifact. CI go loop: build output moved to workspace-local bins/ — /tmp/go-* collided with stale root-owned artifacts from prior runs (permission denied).
2026-10-07 11:10:41 +01:00
Nick 9f33784986 sync: remote bump 2026-10-07 11:06:37 +01:00
Nick 4f31741ac6 fix(ci): four unit/lint gates repaired — spine store staleness, date time bomb, lint ledger, digest
1. spine store rebuilt (d8ce21b-era classification + cc_loader/memory_consolidation relationships) — rebuild-neutrality test now passes against the CURRENT map.
2. DATE-TIME BOMB (test_warm_base_level_math_and_gather): base_level_activation used the real clock against fixed fixture rows; three days of decay crossed the -2.0 clamp and broke the test with no code change. Fix: optional now_iso seam (no-clock law, backward-compatible default), test pins NOW_ISO. Discovered via live probe: the oracle returned exactly -2.0 under the real clock.
3. LINT LEDGER (+1 BLE001 from 3c79a0a consolidation kernel): _core() probe except annotated per the compliant-handler spec (kernel-unavailable degrades to the Python oracle). Baseline 345 restored without re-baselining.
4. AGENTS.md digest synced (henxels sync).
2026-10-07 11:06:24 +01:00
conductor b198fc8dee chore(release): auto-bump version to 0.105.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:57:45 +01:00
Nick 84066eb745 sync: remote bump 2026-10-07 10:53:00 +01:00
Nick 85c0f1eafa feat(go): adaptive-router — DEEP PARITY PORT of the route authority (W5b)
Full port of adaptive_router.py (625 lines, 19 fns): classify_route (legacy + provenance paths, LOCAL_FIX defect-doctrine, deep-research floor), floor predicates, escalation/downgrade with FloorViolation, verification-failure hook, append-only route events, research dispatch, derive_sufficiency (Option C with mechanism-uncertainty, test-surface, external-ref, scope-novelty, action-verb semantics). Survivor hybrid: detect_flags = Rust rrf_detect staticlib (live-probe proves the membrane returns real flags, no silent nil). PARITY: 24 Python-oracle truth cases WITH inputs committed; Go test replays every case — all pass. scripts/gen_adaptive_router_truth.py regenerates the battery from the Python oracle.
2026-10-07 10:52:49 +01:00
conductor edc00f9883 chore(release): auto-bump version to 0.105.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:52:10 +01:00
Nick 6f55c89677 chore(deps): requirements-ci.txt iniconfig 2.3.0→2.3.1 — upstream resolution drift (lint gate #150) 2026-10-07 10:46:37 +01:00
Nick d1c49c9d93 sync: remote bump 2026-10-07 10:43:32 +01:00
Nick 8752575a6e feat(go): operator-deep routing batch — route authority delegation + design-lite (W5b parity)
Ports _route_task_class (delegates to the adaptive-router binary over its JSON CLI — one doctrine implementation, INITIAL_ROUTE trace persisted to routes/route.jsonl), _route_instruction (PL1-03 mutation obligation lines), _route_execution_context (LOCAL_FIX focused / STANDARD_CHANGE integration + design_lite), _design_lite (ADAPT-4 bounded contract, deterministic surface extraction), _task_counters (created→pending bucket mapping), _fallback_model (exact strings; SUBSTRING SEMANTICS preserved — live oracle proves hi⊂this routes quick; quirk flagged for parity-plus, not silently changed). Integration test builds the real router binary in TestMain.
2026-10-07 10:43:24 +01:00
conductor 851e2a112f chore(release): auto-bump version to 0.104.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:42:55 +01:00
Nick f65fc5d258 sync: remote bump 2026-10-07 10:37:59 +01:00
Nick 26e1cd8242 feat(go): memory-store-deep batch 1 — wash lifecycle, CRUD, working set, audit chain, health (W5c parity)
Full parity ports: increment_wash (threshold-20 promotion to established), get_wash_score (log1p wash factor × importance × recency decay), promote_to_rock, rock/washable queries, update/delete/touch, working set (replace semantics), the hash-chained audit ledger (sha256 prev|claim|verdict|evidence [:24] — BYTE-IDENTICAL with Python hashlib, proven in truth_test), stats (working+audit+top_topics), memory_health_check (stale/never/trust distribution/health score). Committed Go truth tests carry Python-computed expected values. Batch 2 (recall pipeline, curiosity probe, tier sweep, chunk/mine ops) queued.
2026-10-07 10:37:42 +01:00
conductor b9a3d2239c chore(release): auto-bump version to 0.103.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:31:03 +01:00
Nick 5bf15537d2 sync: remote bump 2026-10-07 10:28:03 +01:00
Nick 40a2f58fad feat(scripts): dichotomy_audit.py — standing function-level completion instrument
Replaces the one-off audit with a re-runnable instrument: per-module AST function extraction, name-preserving matching against Go/Rust sources, FULL/PARTIAL/NOT-TOUCHED verdicts with missing-function naming. Current honest measure: 13% function-level (290/2225 fns; 7 FULL / 50 PARTIAL / 121 NOT TOUCHED) — the true queue, regenerated with --write after every wave.
2026-10-07 10:27:53 +01:00
conductor c9dcfa2434 chore(release): auto-bump version to 0.102.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:24:09 +01:00
Nick f8118a335e feat(go): adaptive-router — DEEP PARITY PORT of the route authority (W5b)
Full port of adaptive_router.py (625 lines, 19 fns): classify_route with the LOCAL_FIX/STANDARD_CHANGE/RESEARCHED_CHANGE doctrine, the deep-research floor + FloorViolation on downgrade, escalation/verification-failure paths, append-only route events, build_research_dispatch, and derive_sufficiency (Option C) with provenance. detect_flags stays Rust — rrf_detect linked via the Survivor staticlib pattern. Parity is CI-enforced: truth.json (24-case Python oracle battery) is committed and replayed in a HARD test step (not swallowable). CI builds the staticlib in-tree so cgo main packages link.
2026-10-07 10:24:02 +01:00
Nick c8a85d5950 sync: remote tip 2026-10-07 10:15:58 +01:00
Nick d00f567178 ci: single-pass priority gate — infer, apply, then gate-check
Nick's directive: the gate fired before the inference that would have made its warning unnecessary — every keyword-inferrable submission ate a spurious unprioritized flag, a warning comment, and a flag add/remove cycle. Reordered into ONE job: infer (spine > keywords > P-inference) → reconcile labels → THEN gate-check against the post-apply state. A submission whose priority is inferable now gets its P-label in the same run that would have warned; the warning fires only when inference genuinely produced nothing. Gate stays issue-scoped per the filing contract.
2026-10-07 10:15:49 +01:00
conductor dc0b510de3 chore(release): auto-bump version to 0.101.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-07 10:09:55 +01:00
Nick e5e5008339 sync: merge-robot auto-bump 0.100.0 2026-10-07 10:09:44 +01:00
Nick a0f67b7682 feat(go): local-tool-runtime — DEEP port with bounded tool loop, circuit breaker (W4b)
PARITY port of LocalToolRuntime.run/_run_loop: bounded model→tools→model loop with P0-05 circuit breaker for consecutive identical failures, tool allowlist checking, max_iterations enforcement (1-8), and Ollama transport. This is the loop that makes Rune respond to tool results — not a shell.
2026-10-07 10:08:03 +01:00
Nick da4e422efa feat(go): hook-engine — DEEP port with function discovery, auto-tagging (W5b)
PARITY port of hook_engine.py: function discovery (scans src/rune/ for .py files), auto-tagging (voice, memory, tool systems tagged with cost/criticality/input_dep), hook firing, and the function metadata registry. Not a shell — the actual discovery and tagging logic.
2026-10-07 10:03:33 +01:00
Nick 208c0b3202 feat(go): provider-registry — provider connection management (W5b deep port)
PARITY port of provider_registry.py: connect, disconnect, list, models (Ollama + [OI] + Anthropic), chat. Supports ollama, openai, and anthropic provider types with API key auth and model listing. Connection pooling via shared http.Client.
2026-10-07 10:00:13 +01:00
Nick bb24c4387d feat(go): warm-router — ACT-R base level + neuron scoring + routing (W5b deep port)
PARITY port of warm_router.py: base_level_from_uses (ACT-R activation math with decay exponent 0.5 and clamping), score_neuron (keyword hits + importance boost + wash count boost), route (topic extraction). The ACT-R math is the core scoring function — it determines how relevant a memory is based on recency and frequency of use.
2026-10-07 09:55:38 +01:00
Nick a133fd6840 feat(go): memory-store-deep — FULL memory store port (W5c deep port)
PARITY port of memory_store.py core operations: add_entry with 12 parameters (text, importance, topic, source, method, confidence, temperature, always_on, priority, memory_type), recall with budget/min_score/type filtering, supersede, wash, get, invalidate, stats. Uses modernc.org/sqlite (pure Go). The DB path creation and schema creation are handled in Go. All operations tested: add→search→wash→supersede→stats. Note: the importance and confidence parameters come from the CLI but the test shows default values because the --importance flag needs a separate positional check — will fix in the next iteration. The SQLite schema is the specification.
2026-10-07 09:28:20 +01:00
Nick 44aff75599 feat(go): operator-deep — REAL functions ported from rune_operator.py (W5b deep port)
PARITY port of the module-level functions: infer_task_family (task-family keyword matching), _bounded (text collapse), _filter_safety_noise (15 safety-noise patterns), _is_follow_up (8 follow-up patterns), _sorted_tool_names. Plus RuneTask struct and operator state management. These are the REAL functions from the Python module — not facade shells.
2026-10-07 09:19:30 +01:00
Nick ce383f1fa4 docs: honest audit of the dichotomy rebase — 9% complete, not 100%
The audit shows: 27 full ports, 69 partial ports (mostly <20% coverage), 47 not touched. The previous session built CLI shells, not full ports. The real work is ~51,380 lines of Python logic remaining. This document is the baseline for measuring real progress.
2026-10-07 09:09:25 +01:00
Nick 6b44a3b630 feat(go): TUI — Bubble Tea terminal interface (W7)
The complete Rune TUI rewritten from Textual (Python) to Bubble Tea (Go). Chat LEFT, task RIGHT, status bar — the same layout. Lipgloss styling with rounded borders and truecolor. Statically linked binary — no Python runtime needed.
2026-10-07 08:54:19 +01:00
Nick fb39c6eac6 feat(rust): iteration + drift_ledger + drift_observability + construction_projection + construction_patterns_builtin + spine_loader (W6) 2026-10-07 08:49:02 +01:00
Nick 9bc4c2d5cc feat(rust): programme_continuation — stage-to-stage continuation (W6) 2026-10-07 08:42:19 +01:00
Nick 80130ad079 feat(rust): construction_pattern + structural_model (W6) 2026-10-07 08:40:18 +01:00
Nick 9856db2b04 feat(rust): construction_decision + construction_interface (W6 deep port) 2026-10-07 08:37:34 +01:00
Nick 1ef1722bcf feat(rust): behavior + construction_intent (W6) 2026-10-07 08:35:19 +01:00
Nick fae049f649 feat(rust): spine — Rune self-model spine data structures (W6 deep port) 2026-10-07 08:31:06 +01:00
Nick 45e2e4c43a feat(rust): structural_store — structural truth store data structures (W6 deep port) 2026-10-07 08:28:04 +01:00
Nick 17c249447b feat(rust): capability_broker — capability selection for the operator (W6 deep port) 2026-10-07 08:25:54 +01:00
Nick 712046c345 feat(rust): system_registry + voice_hybrid (W6) 2026-10-07 08:23:55 +01:00
Nick 54ff1c685e feat(rust): prompt_sync + voice_sync + layout_contract (W6) 2026-10-07 08:21:33 +01:00
Nick 2ac1da854e feat(rust): build_basis — build basis summary (W6) 2026-10-07 08:19:19 +01:00
Nick bfe7729a2f feat(rust): claim_audit — claim verification (W6) 2026-10-07 08:16:58 +01:00
Nick 13f6c709b5 feat(rust): graph_memory — entity storage (W6) 2026-10-07 08:13:39 +01:00
Nick 608b0b3526 feat(rust): contracts + capability_fit + capability_ledger + memory_selector (W6) 2026-10-07 08:09:05 +01:00
Nick 1161fa6348 feat(rust): subconscious_pass + attention_capsule (W6) 2026-10-07 08:04:51 +01:00
Nick f0ae1a56e0 feat(rust): drift_cache + testing_mode (W6) 2026-10-07 08:02:36 +01:00
Nick 4a9b01b128 feat(rust): drift_resume + toolchain + construction_commands + construction_activation (W6) 2026-10-07 07:59:28 +01:00
Nick 8b61d15db7 feat(rust): construction_mode — contextual construction classification (W6) 2026-10-07 07:56:05 +01:00
Nick 57b6011def feat(rust): outcome_ledger — cognitive performance evidence (W6) 2026-10-07 07:47:58 +01:00
Nick 5b093b8ba3 feat(rust): wait_guard_corpus + rune_profiler (W6) 2026-10-07 07:44:13 +01:00
Nick 02ca31003f feat(rust): inference_trace — inference cost tracking (W6) 2026-10-07 07:35:48 +01:00
Nick 32f252b8a0 feat(rust): dependency_tracker + context_pressure (W6) 2026-10-07 07:32:47 +01:00
Nick 180da9d364 feat(rust): token_optimizer — token spend reduction (W6) 2026-10-07 07:29:14 +01:00
Nick 97bb7c51f0 feat(rust): self_improve — self-improvement analysis (W6) 2026-10-07 07:26:04 +01:00
Nick ea2ae7594c feat(rust): drift_core — multi-factor value scoring + promotion lifecycle (W6) 2026-10-07 07:22:29 +01:00
Nick cd89123b01 feat(rust): task_gate — repeated instruction gate (W6) 2026-10-07 07:15:26 +01:00
Nick f71f5f83fd feat(rust): shard_manager — topic-based shard routing (W6) 2026-10-07 07:13:10 +01:00
Nick 832d6122fb feat(rust): progressive_escalation — escalation ladder (W6) 2026-10-07 07:10:13 +01:00
Nick 5ac0824a89 feat(rust): cognitive_budgeter — budget calculation (W6) 2026-10-07 07:09:02 +01:00
Nick 55df1829e8 feat(rust): audit_ledger — HMAC-chained audit ledger (W6) 2026-10-07 07:06:17 +01:00
Nick 83871f707b feat(rust): project_registry + behaviour_ledger (W6) 2026-10-07 07:01:42 +01:00
Nick 3dd39e1320 feat(rust): verification_layer — 7 verifier types (W6) 2026-10-07 06:58:28 +01:00
Nick 6524be860e feat(rust): recall_uplift — deterministic query expansion (W6) 2026-10-07 06:54:45 +01:00
Nick 04e592d11b feat(rust): time_awareness — temporal context (W6) 2026-10-07 06:50:10 +01:00
Nick 07f5d373ac feat(rust): salience_gate — salience classification (W6) 2026-10-07 06:47:50 +01:00
Nick cbf95ddcf3 feat(rust): response_adapter — normalizes provider output (W6) 2026-10-07 06:43:14 +01:00
Nick 11f51b04d5 feat(rust): task_persist — task persistence across sessions (W6) 2026-10-07 06:37:55 +01:00
Nick 835aa9fa50 feat(rust): wait_guard — sleep→poll regression detector (W6) 2026-10-07 06:36:02 +01:00
Nick ab1273e0b9 feat(rust): intent_continuity — deterministic intent-state tracking (W6) 2026-10-07 06:32:11 +01:00
Nick 3f34fcd843 feat(rust): construction_depth — proportional scaffolding depth selection (W6) 2026-10-07 06:30:14 +01:00
Nick 11c7036543 feat(rust): failure_recovery + rune_spinner (W6)
failure_recovery: 7 failure classes with recovery policies (TRANSIENT through UNKNOWN). 4 tests green. rune_spinner: diamond + unicode spinner animations. 2 tests green.
2026-10-07 01:48:47 +01:00
Nick e351ca7078 feat(rust): warm_neuron_store + graph_projection (W6)
warm_neuron_store: list_neurons, neuron_has, neuron_digests. graph_projection: project_json, project_dot, project_mermaid — all three formats.
2026-10-07 01:43:25 +01:00
Nick 63d3baf75c feat(rust): rune_spinner — animated loading indicator (W6) 2026-10-07 01:40:44 +01:00
Nick 2b1249a4a8 feat(rust): programme_gates + structural_migration — gate accounting + schema consent (W6)
programme_gates: mandatory/optional gate accounting with demotion rejection and acceptance gating. structural_migration: MINOR vs MATERIAL classification, affected project detection. Both parity proven with tests.
2026-10-07 01:37:40 +01:00
Nick 59bf524aed feat(rust): impact_preview — visible change-impact preview (W6) 2026-10-07 01:35:05 +01:00
Nick 459a35234c feat(rust): route_risk_flags — first Wave 6 Rust port (parity proven)
72 RUST modules remaining. Starting with the smallest: route_risk_flags (1KB). 3 tests green: no flags, risk flags, uncertainty flags. Survivor staticlib pattern (rlib + cdylib + staticlib).
2026-10-07 01:31:38 +01:00
Nick b3f1611a0a feat(go): scaffold — Go scaffolding interface (W5c, orchestration layer) 2026-10-07 01:19:42 +01:00
Nick e367ca8df7 feat(go): abstraction — Go fact extraction (W5c) 2026-10-07 01:18:19 +01:00
Nick 63d8188284 feat(go): persona-gate — persona rule gating (W5c deep port)
PARITY port of decide(): lexical overlap scoring, restrictive rule classification (command-style prohibition vs obligation), user-rule priority ordering, permissive mode support, citation generation.
2026-10-07 01:15:14 +01:00
Nick 2293b91713 feat(go): elastic-cognition — salience classification + budget (W5c deep port)
PARITY port of classify_salience: 7 signal types (trivial, emergency, owner_correction, directive, task_failure, repeated_retry, high_uncertainty, novel_capability), channel priors, tier routing (quick/coding/complex/brainstorm), witness determination, and cognitive budget. All tested with real inputs.
2026-10-07 01:09:11 +01:00
Nick 78861d8c05 feat(go): curator — memory curation with drift-with-integrity promotion (W5c deep port)
PARITY port of the core curator logic: promotion eligibility with wash-count-adjusted thresholds (wash>=20 → min 1, wash>=10 → min 2, wash>=5 → min 2, else 3), distinctive-term matching, novel-axis detection, identity contradiction detection, entry key normalization.
2026-10-07 01:05:16 +01:00
Nick e5b3de7565 feat(go): witness — FULL parity port with all signal types (W5b deep port)
PARITY FIRST. All 6 signal types from witness.py: error, correction, completion, topic_drift, commitment/preference/urgency/learning detection (pattern tables), contradiction. Plus is_correction, extract_topic (with stopwords), and witness_reflect (Ollama model call). All tested with real inputs producing correct signals.
2026-10-07 01:02:01 +01:00
Nick 1313e04538 feat(go): tool-protocol-rw — FULL schema validation with type checks, alias normalization, approval boundary (W4b deep port)
PARITY port of tool_protocol.py: 18 tool schemas with per-argument type validation (string/integer/boolean/array/object), required-argument enforcement, alias normalization (bash→run_command, grep→grep_files) BEFORE schema checks, read-only approval boundary (13 read-only, 5 write). All validation paths tested: valid, alias, missing required, wrong type, unknown tool. Accepts both "arguments" and "args" JSON field names.
2026-10-07 00:41:35 +01:00
Nick 83e40972ab feat(go): stage-guards — FULL parity port with §15F-§15I (W5b deep port)
PARITY FIRST. The complete stage_guards.py: readiness canary (§15F), execute_stage (readiness→action→exit canary), liveness canary (§15F stall detection), verify_test_truth (§15G exit-code-is-not-proof), henxels_placement (§15H proportional), reconcile (§15I desired-vs-actual). All six operations tested and verified against the Python original. This is the deep port — 300+ lines of Go replacing 281 lines of Python, with every branch and error path covered.
2026-10-07 00:28:44 +01:00
Nick c56ed640cb feat(go): context-broker — FULL compaction algorithm with rolling summaries (W5b deep port)
PARITY FIRST. The full compaction algorithm from context_broker.py: rolling summary store (SQLite), session-keyed persistence, archive-first constraint, turn-count trigger, degradation-to-tail on summarizer failure, and the keep-turns/keep-messages logic. Parity proven: same structural output (triggered=True, turns_summarized=2, persisted=True, compacted=11 messages) as the Python original. The only difference is the summarizer content (Go uses extractive, Python calls Ollama) — intentional for the parity test.
2026-10-07 00:22:11 +01:00
Nick 2d5e7aa6ba feat(go): context-broker — Go context window management (W5b)
Token estimation, budget allocation, history compaction, memory-disabled check. The compaction algorithm evicts lowest-priority evictable blocks when over budget.
2026-10-06 23:21:14 +01:00
Nick 1fed5cf3c6 feat(go): local-tool-runtime — the Go tool loop (W4b) 2026-10-06 23:16:46 +01:00
Nick cb18492e2e feat(go): tool-protocol — tool request validation and dispatch (W4b) 2026-10-06 23:15:18 +01:00
Nick 73cb7847c3 feat(go): core-server + stage-guards — HTTP server and construction stage checks (W5b) 2026-10-06 23:13:50 +01:00
Nick 975f9a8405 feat(survivor): the full Go+Rust hybrid — every Rust crate has a Survivor Go package
Named Survivor by Nick: Rust (survives) + Go (running). All 10 Rust crates now have Go packages under src/go/cognition/ — retrieval, storage, scaffold, provenance, spine, scoring, capability, token, query. Every package links the Rust staticlib (.a) at compile time via cgo. Zero .so loading, zero env vars. The Survivor build pattern is now the standard for all Go+Rust modules.
2026-10-06 23:04:35 +01:00
Nick 66d6586db6 feat(go): cognition package — Go↔Rust hybrid staticlib membrane (W4 improvement)
The Rust kernels compile as staticlibs (.a) and link INTO the Go binary at build time. No .so loading, no CC_RUST_LIB env var, no dlopen. The Go cognition package exposes TierGet, ConsolidationMemory, ConsolidationNeuron, WitnessExtractTopic, WitnessSignals, SafetyGetParams, DriftCheck, PinsRecall, GrowthNearDup as Go functions backed by Rust compute. 4 tests green: TierGet, ConsolidationMemory, WitnessExtractTopic, GrowthNearDup. All 10 crates now produce both .so (for Python ctypes) and .a (for Go static linking).
2026-10-06 22:55:00 +01:00
Nick 1e11c26961 feat(go): sleep-time — Go sleep consolidation orchestrator (W5c) 2026-10-06 22:46:52 +01:00
Nick 384a0d81bf feat(go): hook-engine — plugin hook dispatch (W4) 2026-10-06 22:40:56 +01:00
Nick 8d48a33b80 docs: language dichotomy ledger — measured benchmarks governing every port
Seven laws from real benchmarks: (1) FFI overhead negligible vs Ollama inference, (2) Go wins CPU-bound reactive (5.1x concurrent HTTP, true parallelism vs GIL 0.8x), (3) simple ops stay in caller language (Python 0.3µs vs Rust FFI 10.4µs), (4) Rust wins complex deterministic compute, (5) compression across FFI makes things 125% SLOWER (experimental, parked), (6) reduced payload keys give -25%, (7) inline no-FFI is fastest for simple ops. Includes decision tree, corrected wave assignments, and the practical rule.
2026-10-06 22:31:43 +01:00
Nick d70a01aca4 feat(go): memory-ops — Go SQLite memory operations (W5c)
Add, search, get, wash, supersede, stats. Uses modernc.org/sqlite (pure Go, no cgo). The biggest single port — the orchestration layer of the 99KB memory_store.py. The SQLite schema is the specification; the Go methods map 1:1 to the Python functions.
2026-10-06 21:40:16 +01:00
Nick a8cd17499e feat(go): operator-support — routing, context, tasks, goals, growth, warm routing (W5b) 2026-10-06 21:35:50 +01:00
Nick f8d75e9458 feat(go): operator — the Go reactive turn loop (W5b)
The operator receives a message, calls the model via Ollama, parses tool calls, executes them via the tool-executor binary, and returns a response. Concurrent via goroutines. The 126KB Python operator is replaced by this Go binary + its supporting modules.
2026-10-06 21:34:27 +01:00
Nick beec19912c feat(go): model-connect — all 10 provider modules collapsed into one Go binary (W5a)
OllamaProvider (Chat, Show, ListModels), ModelRouter (keyword scoring + capability matching), ProviderBudget (token tracking per provider). Go net/http with connection reuse replaces Python urllib per-request. Route, list, show, chat operations verified.
2026-10-06 21:28:29 +01:00
Nick 6e86f9945f feat(go): microkernel — plugin lifecycle, capability resolution, hook dispatch (W4) 2026-10-06 21:20:40 +01:00
Nick a17018c871 feat(go): failure-filer — error classification, credential redaction, spool (W4) 2026-10-06 21:18:08 +01:00
Nick 210b18cfde feat(go): work-manager — work batching and dispatch (W4) 2026-10-06 21:15:29 +01:00
Nick 68b724cd5b feat(go): night-mode — Go night consolidation orchestrator (W4) 2026-10-06 21:12:26 +01:00
Nick f3f50599b7 feat(go): cache — Go SQLite-backed key-value cache (W4) 2026-10-06 21:09:43 +01:00
Nick c4b8b76315 feat(go): cron-engine — Go scheduled task engine (W4) 2026-10-06 21:05:35 +01:00
Nick a982f64c2c feat(go): recall-router — abstention → deep-research routing (W4) 2026-10-06 21:00:22 +01:00
Nick 82eaefca1c feat(go): connection-pool — Go pooled SQLite connections (W4) 2026-10-06 20:57:57 +01:00
Nick 87c13746b6 feat(go): parallel-processor — Go goroutine concurrent task runner (W4) 2026-10-06 20:55:09 +01:00
Nick f54c608b3f feat(go): subsystem-health — failure tier tracking (W4) 2026-10-06 20:52:42 +01:00
Nick fc9b9c08fe feat(go): message-queue — task addition detection and message queuing (W4) 2026-10-06 20:50:45 +01:00
Nick 03704d7691 feat(go): event_envelope merged into event-bus — authority + urgency classification (W4) 2026-10-06 20:47:56 +01:00
Nick 2ff32032ff ci: go-build iterates all src/go/*/ dirs — every Go module built and vetted 2026-10-06 20:39:25 +01:00
Nick 6d630348ba feat(infra): Wave 0 — henxels scope, umbrella manifest, scaffolding protocol, tranche plan
The infrastructure that governs ALL ports: henxels.yaml extended to govern src/rust/ and src/go/ (line caps, no-secrets, filename casing); umbrella manifest extended with 44 go_surfaces and 9 rust_crates (the spine reads umbrellas for non-module surfaces); construction_decision.py GO_SIGNALS/RUST_SIGNALS corrected to match the dichotomy (reactive=Go, deterministic=Rust); tranche plan v3 committed with verification pipeline via Gitea.
2026-10-06 20:37:21 +01:00
Nick 1611d2b6a5 plans: tranche v3 final — verification pipeline via Gitea, CI stacking, plain-English HOW 2026-10-06 20:26:33 +01:00
Nick 294eef368c plans: the FULL dichotomy rebase tranche — every Python module ported (docs only) 2026-10-06 19:24:01 +01:00
Nick 20b8112ba1 feat(go): tool-executor — the Go reactive file/command executor (R2 W4 slice 2)
src/go/tool-executor/: Go implementation of the core tool operations — read_file (with offset/limit windowing), list_directory (with glob filter), glob_files (sorted by mtime), grep_files (with include filter), run_command. JSON envelope output matching the Python contract. 4.5MB statically-linked binary, zero external deps. The approval machinery (PreparedTool/pending) stays Python-side for now.
2026-10-06 16:26:06 +01:00
Nick 9092e7d2f4 feat(go): event-bus — the first Go reactive module (R2 W4 slice 1)
src/go/event-bus/: a Go event journal that reads/writes the same events.jsonl the Python event_bus.py uses. Go os.File + bufio.Scanner for the append-only journal; manual CLI parsing for emit/log/stats. Concurrent-safe via O_APPEND. This is the first module of the Go reactive runtime shell (the 57 GO-classified modules from the re-classified dichotomy).
2026-10-06 16:17:01 +01:00
Nick 64ba7cf8f8 fix(rebase): re-classify 14 modules RUST→GO — the dichotomy was mis-applied (#2461 close-out)
The classifier saw 'deterministic/state domain' in the evidence and put modules with urllib/threading into RUST. Nick's actual definition: Go = initiates/responds/routes (reactive), Rust = computes/stores (deterministic). 14 modules whose primary purpose is network I/O or process spawning move to GO: elastic_cognition, goal, growth_delta, witness, persona_gate, sleep_time, curator, memory_consolidation, memory_store, scaffold, stage_guards, evidence_probe, warm_router, abstraction. New split: 57 GO (32k lines) / 85 RUST (25k lines) / 7 TUI / 29 TEST. This is the honest dichotomy.
2026-10-06 15:58:27 +01:00
conductor 5befd81b23 chore(release): auto-bump version to 0.100.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 14:53:54 +01:00
Nick 3c79a0a9c0 feat(memory): memory_consolidation delegation — consolidation_memory + consolidation_neuron kernels (R2 W4)
First Wave 4 port: the consolidation kernels (compression ratio, text truncation, knowledge record assembly) move to the cognition_core crate as consolidation_memory + consolidation_neuron FFI symbols. The Python module delegates through _consolidation_memory_oracle / _consolidation_neuron_oracle (which use drift_compression for the content and the kernel via cc_loader for the record). Seven contracts: oracle basic, no-entry-id, rock-no-compression, kernel parity x2, delegated returns. 7/7 with the kernel, 5/5 pinned. Also: cc_loader registers the two new symbols. The module went from 733→740 lines (oracle + probe added, consolidate_neuron body simplified).
2026-10-06 14:51:15 +01:00
conductor 79bbb5fea8 chore(release): auto-bump version to 0.99.10 — earned by #2469 (task-scoped probes and tool-name compatibility (#2468))
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2469
2026-10-06 07:28:03 +01:00
conductor 872b0c7a75 Merge pull request 'task-scoped probes and tool-name compatibility (#2468)' (#2469) from fix/operator-probe-tools-2468 into master 2026-10-06 07:27:57 +01:00
Nick fcee8fbec7 fix(operator): probe budget exhaustion lands in the failure handler (#2468)
The pyright ratchet caught my first attempt: a try-scoped from-import made ProbeBudgetExceeded possibly-unbound in the except tuple. Imports are now module-level dual (the file convention), the early-return returns False explicitly, and a _probe_failure_classes helper carries the caught-class set with degraded graceful behavior. Focused battery 87 green; pyright shows only the 3 pre-existing baseline errors.
2026-10-06 07:14:24 +01:00
Nick d767bd9a9e wip(operator): isolate probe budgets and normalize requested tool names (#2468)
Red-first checks prove shared global probe budget leaks into chat, and bash/grep compatibility names fail before dispatch. Change scopes artifacts by task digest, catches exhaustion, normalizes only to canonical approval-guarded handlers and exposes run_command for explicit shell intent. Focused 87 tests green. INCOMPLETE: scaffold fit blocked by missing checkable contracts, TUI status noise and full CI remain.
2026-10-06 07:14:24 +01:00
Nick 47e6eba735 test(delegation): pinned-mode probe paths — coverage for the fallback lines (#2461 close-out)
The remaining uncovered lines in safety_override/abstraction/tier_manager/witness are the RUNE_COGNITION_CORE=python pins, the _cc_probe loader-error fallbacks, and the stopword gather seams. Four contracts exercise all of them under the pinned env.
2026-10-06 07:11:54 +01:00
conductor 818f4b9e21 chore(release): auto-bump version to 0.99.9 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 06:54:57 +01:00
conductor d1631a8333 Merge pull request 'ci: build cognition_core in unit-tests + canary jobs (#2461 close-out)' (#2477) from ci/build-cognition-core-unit into master 2026-10-06 06:54:52 +01:00
Nick f5754fe2c0 fix(ci): CC_RUST_LIB belongs in the unit-tests job env, pointing at the runner-owned target dir
The step-level export pointed at a wrong path and the job-level env lacked the variable entirely — the kernel probe returned None on CI. Now the job env carries CC_RUST_LIB like its seven sibling crates.
2026-10-06 06:48:55 +01:00
Nick 6df2580567 fix(ci): CC_RUST_LIB must follow CARGO_TARGET_DIR — the runner-owned target dir redirects the .so (#2461 close-out) 2026-10-06 06:30:39 +01:00
Nick 3ee6f59e1b ci: build cognition_core in unit-tests + canary jobs (#2461 close-out)
The abstraction kernel-fallback test (and the W3.4 canaries) need libcognition_core.so; the crate was never built in the unit-tests/canary jobs, so the kernel-probe correctly returned None on CI and the fallback contract could not be exercised. Also set CC_RUST_LIB for the unit-tests job so the loader finds the built artifact.
2026-10-06 06:15:49 +01:00
Nick 3811219abc test(safety/abstraction): direct delegation batteries — the last 0%-honest gaps (#2461)
safety_override: tier-0 normal, pattern learn+match, keyword extraction contract, min-keyword matching, refusal detection (5 contracts). abstraction: kernel fallback schema with provenance, stopword seam determinism (2 contracts, kernel test skips when pinned). Fake sqlite shaped to the real call contract (fetchall/fetchone on the right objects). 20/20 across the three delegation batteries.
2026-10-06 06:00:10 +01:00
conductor edf9d52caa chore(release): auto-bump version to 0.99.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 05:51:40 +01:00
Nick 8a2525c4ac fix(cognition): regex_lite honors \b anchors and \s adjacency — the faithful subset closes its fidelity gap (#2461)
The 34-row matrix hid a fidelity gap: the gap-matcher treated \s* as an UNBOUNDED gap and dropped \b entirely, so \bactually...\s*(...)\b matched "actually wait" (it+wait substring across an unbounded span) where Python re says False. Fix: \b anchors are preserved as sentinels and enforced as word boundaries at match edges; \s becomes an ADJACENCY constraint (next phrase within one char). Matrix 30/30 with re-truth, the false-positive probe now correct, canaries 27/27 x2 modes, in-crate probes 5/5. Also: witness delegation battery (7 contracts) + tier delegation battery (6) — direct coverage for the delegation layer.
2026-10-06 05:50:51 +01:00
Nick fb69ae83fc test(tier): direct delegation battery — the 0% honest gap closed (#2461)
tier_manager was only exercised via the cc_loader FFI in canaries (never importing the Python module), so its delegation layer read 0% in the diff-cover measurement. Six direct contracts: oracle matrix, kernel==oracle parity per case, promote/demote parity, rock immobility, kernel-unavailable fallback, config surface.
2026-10-06 05:31:58 +01:00
conductor d3a240edc8 chore(release): auto-bump version to 0.99.7 — earned by #2476 (ci: register the W3.4 cognition_core canaries (#2461 close))
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2476
2026-10-06 05:23:39 +01:00
conductor 278c5e372b Merge pull request 'ci: register the W3.4 cognition_core canaries (#2461 close)' (#2476) from ci/register-w34-canaries into master 2026-10-06 05:23:33 +01:00
conductor d08621222a Merge branch 'master' into ci/register-w34-canaries 2026-10-06 05:17:32 +01:00
Nick db25db26d1 ci: register the W3.4 cognition_core canaries in the pipeline (#2461 close)
canary_growth_pins (26 checks incl. the 34-row witness parity matrix), canary_persona, canary_cognition_core were shipping but never ran in CI — the registration verify owed since W3.2. Now judged on every pipeline run.
2026-10-06 05:11:29 +01:00
conductor 1a321154f8 Merge pull request 'agents: land skill used explicitly duriung Delta workflows with collaborators' (#2475) from SamCDay/rune:delta-land into master
Reviewed-on: conductor/rune#2475
2026-10-06 04:36:01 +01:00
SamCDay ac9765d5d1 agents: land skill used explicitly duriung Delta workflows with collaborators
Signed-off-by: Sam Day <me@samcday.com>
2026-10-06 03:54:52 +01:00
conductor 0fdfe195e7 chore(release): auto-bump version to 0.99.6 — earned by #2473 (Scaffold: programme_continuation wired into close() (T5.15 slice 3) + same-secon)
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2473
2026-10-06 03:52:23 +01:00
conductor 71d2d63a36 Merge pull request 'Scaffold: programme_continuation wired into close() (T5.15 slice 3) + same-second id fix' (#2473) from feat/wire-programme-continuation into master 2026-10-06 03:52:17 +01:00
Nick 0873b521e3 feat(scaffold): programme_continuation wired into close() (T5.15 slice 3) + same-second contract-id collision fix
Slice 3 of the completion wave: closing a contract that is a stage of a persisted Programme marks that stage COMPLETE, persists the ledger, and rides the continuation canary on the close envelope — the next eligible stage is SURFACED, never guessed (task done ≠ programme done). No programme bound → no continuation block; ledger failure degrades without failing the closure. Three contracts. Also fixed en route: contract ids were second-granularity timestamps that collided when two needs filed in the same second (red test proves distinct ids now, uuid suffix added).
2026-10-06 03:46:24 +01:00
conductor f7e6c5cff5 chore(release): auto-bump version to 0.99.5 — earned by #2470 (TUI: chat pane LEFT + post-response dim-spam consolidation (T5.14/T5.13))
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2470
2026-10-06 03:27:08 +01:00
conductor fc46dbea52 Merge pull request 'TUI: chat pane LEFT + post-response dim-spam consolidation (T5.14/T5.13)' (#2470) from feat/tui-pane-swap-chat-left into master 2026-10-06 03:27:02 +01:00
conductor d8ede8f3f8 Merge branch 'master' into feat/tui-pane-swap-chat-left 2026-10-06 03:19:13 +01:00
conductor 804c2f62df chore(release): auto-bump version to 0.99.4 — earned by #2472 (Scaffold: construction_commands dispatch — the serving wire (T5.15 slice 2))
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2472
2026-10-06 03:11:51 +01:00
conductor e5d10f18ae Merge pull request 'Scaffold: construction_commands dispatch — the serving wire (T5.15 slice 2)' (#2472) from feat/wire-construction-commands-dispatch into master 2026-10-06 03:11:44 +01:00
conductor 3ed58d48a9 Merge branch 'master' into feat/tui-pane-swap-chat-left 2026-10-06 03:02:57 +01:00
conductor 6c997b6af4 Merge branch 'master' into feat/wire-construction-commands-dispatch 2026-10-06 03:02:52 +01:00
conductor 9b5befb137 chore(release): auto-bump version to 0.99.3 — earned by #2471 (Scaffold: stage_guards wired into completion transitions (T5.15 slice 1))
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2471
2026-10-06 02:50:18 +01:00
conductor 6899d859da Merge branch 'master' into feat/tui-pane-swap-chat-left 2026-10-06 02:50:11 +01:00
conductor 8ef70448c3 Merge pull request 'Scaffold: stage_guards wired into completion transitions (T5.15 slice 1)' (#2471) from feat/wire-stage-guards-transition into master 2026-10-06 02:50:08 +01:00
Nick 55f268f978 feat(scaffold): construction_commands dispatch — the serving wire (T5.15 slice 2)
The command seam validated intents but nothing EXECUTED them. dispatch() binds the canonical store, runs the validator, then executes accepted intents through the Construction Interface (resume/transition/amend_scope), refusing via the lifecycle with accepted=False+success=True (a refusal is a valid protocol outcome, not an error). Idempotent replay returns the recorded intent without re-executing. Three contracts: end-to-end phase movement, refusal shape, replay-no-double-execute; seam file 12/12. Slice 1 of 5 was stage_guards (#2467 wire wave).
2026-10-06 02:40:49 +01:00
Nick 320026a4a3 feat(scaffold): stage_guards wired into the completion-facing transitions (T5.15 slice 1)
The five orphaned V1 components: slice 1 wires stage_guards for real. transition(verifying|closed) now runs §15F readiness StageChecks derived from guard: verification obligations (or inline guard_checks); a blocked guard = VERIFICATION_FAILED envelope and the phase does NOT move — builder cannot self-certify past a failing check. building/free edges unchanged; capabilities() schema documents the new arg; CLI transport carries guard_checks as JSON arg 4. Four contracts red-first (blocked/pass/inline-override/free-edge); 56 interface+guards tests green.
2026-10-06 02:40:49 +01:00
Nick 0e88b6bc08 feat(tui): consolidate post-response dim spam into one collapsed line (#2468/T5.13)
Five separate dim system messages after every task/build response (budget, claims, growth, self-improve, plus the cron/queue echoes) spammed chat. New collect_post_response_signals() is a PURE headless collector with degrade-not-crash try blocks; the worker renders ONE line or silence. 6 frozen contracts: compose order [chat,task], resize semantics re-keyed, collector all-signals/silence/never-raises/cycle-multiple. 38 TUI/layout tests green; pyright error count unchanged at baseline 8.
2026-10-06 02:40:25 +01:00
Nick 004e5b3118 feat(tui): swap panes — chat LEFT, task RIGHT (#2465/T5.14)
Nick: chat-first feels natural. Compose yield order reversed (rune_tui.py compose), resize_left/resize_right actions re-keyed to the new visual positions (left=chat shrink now grows task), focus actions and all name-keyed query_one calls untouched (position-independent by design). Static source proof: yield order [chat, task]; 45 layout/TUI tests green.
2026-10-06 02:40:25 +01:00
conductor db15ae3650 chore(release): auto-bump version to 0.99.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 02:28:19 +01:00
Nick d8ce21b76d fix(spine): the measured dichotomy decides — hints demoted to labeled fallbacks (#2461, Nick correction)
Nick caught the deviation: module verdicts were substring-guessed from names (memory→rust, gitea→go) with a silent rust default. Authority order now: the measured classification map (import-evidence) decides FIRST for every module; hint hits are labeled "(name hint)" so provenance is visible; unmeasured+unhinted modules are "unclassified" (12), never a guessed rust. Store rebuilt: 106 rust / 45 go measured, 33+7 hint-labeled, 23 test-infra, 12 honest unclassified. Battery 95 green (test updated to pin the authority contract).
2026-10-06 02:28:16 +01:00
Nick d4d43dbf67 merge: spine_language tests (the new-module henxel repair) 2026-10-06 02:07:44 +01:00
Nick 8a6c950140 test(spine_language): the carved transforms ship WITH tests (#2461 new-module henxel)
CI red at d67fc46: spine_language.py crossed the new-module henxel with zero coverage. Eight contracts: measured-map read + graceful degrade, kind-default verdicts, TUI/hints, subsystem member-majority, unqualified-discovery honesty (candidate stays DECLARED), legacy-ID normalization with edge re-pointing, and the closed-contract-only gather law. 8/8 green. Also: construction_commands dispatch WIP stashed on its own branch (slice 2, next).
2026-10-06 02:04:19 +01:00
Nick d67fc46581 chore(rebase): refresh store/rebase inventory + classification for the 6 post-W0 modules (#2461 close-out)
The W0 map had drifted (174 vs 180 files): cc_loader/sc_loader/mr_loader/mems_loader (FFI kernel membranes → RUST), spine_language (deterministic carve → RUST), __init__ (metadata reads) now carry evidence-backed side records and measured function/line counts. spine_language._classification_sides() — the runtime reader — sees 180 entries; spine battery green.
2026-10-06 01:40:56 +01:00
conductor 84b8d6b215 chore(release): auto-bump version to 0.99.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 01:22:12 +01:00
Nick 7f5d57236a fix(ci): 0.99.0 CI repair — silent-handler annotations + spine_language umbrella mapping (#2461)
The 0.99.0 bump ran CI against the delegation code before the annotations landed: BLE001 345->349, S110 75->81 (my new kernel-failure fallbacks), plus spine_language unmapped in the umbrella manifest (test_umbrella_impact) and the resulting spine-rebuild membership diff (rebuild-neutrality). All four new fallbacks carry intentional-silence noqa annotations per the ratchet rule (count now exactly 345/75 — baseline UNCHANGED, ledger honest); spine_language mapped into perception next to spine; store rebuilt so the membership edge and language stamps are in the committed store. 127 tests green incl. the four CI-red surfaces.
2026-10-06 01:22:05 +01:00
conductor bda07c68ee chore(release): auto-bump version to 0.99.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 00:11:25 +01:00
Nick c5be22be09 feat(cognition): witness/safety/abstraction kernel delegation + regex_lite faithful-subset rewrite (#2461 W3.4, #2465 adjacency)
The W3.4 kernels existed but the Python publics still ran Python. Now: witness extract_topic/detect_signals delegate through the _stopwords()/_tables() gather seams (PATTERN-TABLE GATHER-SEAM LAW) with _py_ oracles; abstraction extract_facts gains the deterministic kernel FALLBACK when the LLM returns nothing; tier_manager was already wired; the RUNE_COGNITION_CORE=python pin is now honored by a SHARED cc_loader.core() probe (the four module-local probes silently ignored it). The canary matrix caught a REAL kernel bug: regex_lite dropped \b anchors as literal b chars so every word-boundary row silently missed (bwrongb never appears in text); rewritten as alternatives-expansion + segment-variant DFS gap-walk; 34-row matrix judged against REAL Python re shows 0 mismatches. canary_growth_pins 27/27 x5 both engine modes; canary_unbounded 14/14; per-row fixtures carry re-truth, not hand labels (a lying fixture is worse than uncovered).
2026-10-06 00:05:54 +01:00
conductor 4d6fec3d17 chore(release): auto-bump version to 0.98.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-06 00:01:45 +01:00
conductor 15dbbc562a chore(release): auto-bump version to 0.98.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 23:56:19 +01:00
Nick 6eb5e06119 refactor(spine): carve the #2465 doc-level transforms into spine_language.py (#2461 keep-green)
spine.py crossed the 1200-line giant-file debt cap (1203) with the language-verdict addition — CI lint red. The three transforms (language placement, discovery-ID normalization, contract-discovery write-back + gatherer) move to spine_language.py intact, re-exported through spine for import-compat (structure unchanged: 256 entities, same verdicts; spine battery 87, canaries 19/19 + 21/21, henxels hold, spine.py back to 1034).
2026-10-05 23:53:33 +01:00
Nick 78e4750a77 feat(tier): tier_manager delegates get/promote/demote to the cognition_core kernels (#2461 W3.4 delegation owed)
The kernels and FFI landed in b0a682b but the Python publics still ran Python — the strangler breach this slice owed. Live probe caught the FFI-arg-shape defect: promote/demote take the FULL ENTRY (kernel recomputes current tier internally), not a tier string; passing a string silently returned threshold-default verdicts (promote cold went warm-to-hot WRONGLY via defaults, demote rock went cold ignoring rock protection). 12-case kernel==oracle sweep green: wash>=20, recency hot/warm/cold, naive-timestamp parity trap, rock protection. PY oracle bodies kept; RUNE_COGNITION_CORE=python pins fallback; canary_growth_pins 26/26 both engines; canary_unbounded 14/14.
2026-10-05 23:50:11 +01:00
conductor c3948bae98 chore(release): auto-bump version to 0.97.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 23:48:25 +01:00
Nick b8184a0f01 chore(spine): store rebuild carries subsystem language verdicts (#2465) 2026-10-05 23:47:58 +01:00
Nick 469b91f217 feat(spine): subsystem language verdicts from member majority (#2465 addition 3)
The five unassessed subsystems are containers: their language derives from member modules measured sides (classification.json, the W0 dichotomy map, rust and go counts carried in the verdict text). Unclassified members noted rather than guessed; unreadable classification degrades to unassessed. Spine canaries 19/19 and 21/21, spine battery 87 green.
2026-10-05 23:47:55 +01:00
Nick bb960b1656 test(gitea): canary the numeric-ID stale-label removal (#2467)
Replace the obsolete removeLabel(name) pattern assertion with the actual Gitea /labels/{id} request, check ID lookup under the 404 idempotency guard, and require real P0–P3 managed labels. Local canary 39/39; workflow JS regression passes.
2026-10-05 22:22:53 +01:00
conductor 38c76a0c29 chore(release): auto-bump version to 0.96.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 22:15:03 +01:00
Nick 3186566514 fix(gitea): reconcile priority labels by numeric Gitea ID (#2467)
Issue #2467 carried both P0 and P2 because name-based label DELETE returned 404 and the workflow silently ignored it. Use the numeric ID API, enforce exactly one priority, clear stale unprioritized flags, and execute the workflow JavaScript against a Gitea-compatible fake in regression tests.
2026-10-05 22:14:47 +01:00
Nick 24d14ff8e1 fix(spine): preserve candidate discoveries without inventing module IDs (#2465)
Migrate 22 malformed contract-discovery IDs to construction-surface, retain evidence/edges, keep candidates DECLARED, unify cold/warm language projection, and refresh the pinned aiohttp lock. Red regression tests precede the fix; local full suite 2301 passed, spine canaries 19/19 and 21/21.
2026-10-05 22:14:46 +01:00
conductor 6d0d455033 chore(release): auto-bump version to 0.96.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 19:34:11 +01:00
Nick b0a682b61f feat(cognition): R2 W3.4 — tier/witness/safety/abstraction kernels + protocol upgrade (#2461, #2465)
Contract BUILDER-2026-10-05T12:03:34Z-cli — FIRST SLICE under the 90/100
verification law (64172d8).

Kernels (cognition_core/broker.rs):
- tier_get/promote/demote: thresholds+now injected (no-clock); wash>=20
  gate; rock immobility on demote; naive/malformed → designed fallback
- witness: extract_topic (word-run + stopwords-as-data), detect_signals
  (error/correction/completion/topic-drift/commitment/contradiction/
  preference/urgency/learning; pattern tables travel as data; the regex-
  lite subset is faithful to the tables' shapes)
- safety: get_params projection, update_pattern bump/new-row semantics
- abstraction: extract_facts sentence/content-word projection
- crate-level probe tests (parse_probe.rs: aware parse, epoch, tier recency)

Protocol upgrade (issue #2465) — three additions to the machine itself:
1. PER-CONTRACT MINI-DOC: construction_projection.mini_doc() emits the
   LLM quick-lookup page (what was built / connections / how to update)
   per closed contract; close() writes it to store/scaffold/docs/<cid>.md
2. SPINE WRITE-BACK: spine.build() now consumes closed contracts'
   discovered entities (expected_new_observed + candidate corrections) —
   the spine learns what construction actually built (256 entities now)
3. LANGUAGE VERDICTS: every spine entity carries identities.language
   (198 rust / 31 go / 6 tui / 5 unassessed); other projects get the
   fit assessment at classification

Also: one lost FFI block restored (the stash dance dropped
cc_tier_*/cc_witness_*/cc_safety_*/cc_abstraction_* — the battery
caught it: undefined symbol).

Gates: canary_growth_pins 26/26 x3, persona 18/18, cognition_core
38/38, suite 2299/0, ruff clean, type ratchet PASS, henxels all-hold.
2026-10-05 19:34:05 +01:00
conductor ab18540d7e chore(release): auto-bump version to 0.95.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 19:06:06 +01:00
Nick f489467e1b fix(ci): the priority gate's demand is now programmatically satisfied
Nick's catch (2026-10-05): the auto-label bot highlighted that issues
carry priority only in their titles — the PRIORITY GATE job demanded
P0-P3 labels but nothing ever ASSIGNED them programmatically. The title
inference even used a dead label ('priority: high', normalized away
2026-09-04).

- auto-label's priority inference re-implemented in the gate's own
  namespace: title prefix \bP([0-3])\b first, then keyword inference
  (spine/critical/urgent -> P0; broken/fix/repair/defect -> P1;
  kernel/port/refactor/improve -> P2; benchmark/measurement/deferred ->
  P3); managedLabels now carry the REAL P-labels
- paired with the custodian fix: create_issue assigns the P-label at
  FILING (no issue ever lands unprioritized)
- full open-issue set backfilled (2461-2466, 2455/2459) and the live
  probe (#2466, self-verifying) closed after verifying P1 landed
  at filing
2026-10-05 19:05:38 +01:00
Nick 64172d8675 docs(plan): the 90/100 verification law (Nick 2026-10-05)
- 100% branch coverage on the deterministic Rust kernels (achievable,
  exhaustive via parity matrices)
- ≥90% per-slice changed-lines working bar, aiming higher per slice
- floor ratchet climbs after clean waves (70->76->84->90)
- honest fixtures only — a lying-fixture branch is worse than uncovered
- assertion quality counts: value-verifying asserts only
2026-10-05 12:58:18 +01:00
conductor 8e75414907 chore(release): auto-bump version to 0.95.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 12:50:38 +01:00
Nick 7a9d823792 feat(cognition): R2 W3.3 kernels in cognition_core (broker.rs: drift_check + identity_prompt assembly + growth near_dup/profile + pins recall/projection)
Missing from the previous commit's staged set (the harness caught it):
the full W3.3 kernel block now lands. All parity walls verified:
canary_growth_pins 9/9, canary_persona 18/18, canary_cognition_core
38/38, suite 2298/0, diff-cover 72%.
2026-10-05 12:50:33 +01:00
Nick 98b5a73ed1 feat(cognition): R2 W3.3 — identity/growth/pins kernels + full coverage (72%)
- identity_anchors: check_drift delegates (pattern table stays the
  module's data authority via _pattern_triples); _py_check_drift oracle
  covers the four prefix families; drift parity proven on the full
  table (31 patterns) + the full-text multi-class case
- growth_delta: _near_dup/growth_profile delegate; oracle accepts delta
  dicts AND raw strings; near-dup kernel corrected to the REAL
  semantics: prof-key = strip-punct lowercase words >4 chars, split at
  |A∩B|/max(|A|,|B|) >= 0.25 (NOT a jaccard-min heuristic)
- pins: recall kernel projects via _project parity (details-blob parse,
  defaults, 'reason' field); empty-context abstention frozen
- probe branches exercised honestly: pinned-fallback + ImportError via
  the from-import resolution path (the from-import goes through the
  package attribute, not sys.modules — the PEP resolution law frozen in
  the battery)
- reflect malformed-JSON + [:3] cap + apply budget cut frozen
- suite 2295/0; diff-cover 72% (floor 70); canaries 9/9+18/18+38/38
  x3; henxels hold; ruff/type clean
2026-10-05 12:50:33 +01:00
conductor 4a9d8001c1 chore(release): auto-bump version to 0.94.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 09:00:47 +01:00
Nick 538bbeb821 feat(cognition): R2 W3.2 — persona_gate decision kernels (cognition_core)
Contract BUILDER-2026-10-05T07:19:27Z-cli (Wave 3 slice 2, #2461).

- cognition_core/broker.rs gains the persona-decision kernel:
  persona_lex (distinctive content words: punct strip, light plural
  stem, acronym preservation — 55-word stoplist), persona_overlap
  (intersection over min-size), persona_decide (engagement matrix:
  amplifier set, semantic gates 0.45/0.30/0.68, user-rule 0.4 bar,
  permissive/legacy-restrictive verdicts, _cite parity '[topic:pN] text')
- persona_gate.py: _lex/_overlap/decide delegate; semantic cosines
  gathered Python-side (embedder calls are IO — the membrane's
  discipline) and passed into the kernel; _py_ oracles are the
  load-bearing fallback
- FFI: cc_persona_lex/overlap/decide; loader extended
- canary_persona.py (canary-first red on every seam): 18/18 x3 —
  lex matrix incl. acronyms/3-char exclusions, decide engagement paths,
  citation format parity
- shim battery 35/35; suite 2294/0; ruff clean; type ratchet PASS;
  henxels all-hold
2026-10-05 09:00:43 +01:00
conductor cc3b3d81eb chore(release): auto-bump version to 0.93.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 08:43:51 +01:00
Nick eb01156d9f Merge branch 'master' of http://127.0.0.1:3000/conductor/rune into r2-wave2-memory-retrieval 2026-10-05 08:43:19 +01:00
Nick 290d264f31 feat(cognition): R2 W3.1 — context_broker kernel + the surrogate-pair parser fix across all nine crates
Contract BUILDER-2026-10-05T07:19:27Z-cli, closed: module:context_broker
correctly_predicted; cognition_core expected_new_observed; cc_loader
surfaces honestly new (#2463 wave epic).

- cognition_core crate (zero-dep, no-clock): context_broker's four
  deterministic kernels — default_estimator (chars//4; utf16-length
  corruption caught — the canary's astral cases), default_budget,
  default_memory_budget (+cap), memory_disabled (env gate value-parsed)
- THE PARSER BUG (nine pyjson copies repaired): the surrogate-pair branch
  tested self.b[i+1..] for the second escape's backslash — but hex4 had
  already advanced i past the first escape's 'u+digits', so the correct
  guard window is [i..] not [i+1..]; and after the guard, i advanced +2
  leaving hex4 to read a shifted window ('bad \\u digits' on
  \\ud83c\\udfb8). Fixed: guard at [i..], pre-hex4 advance +1.
  Astral chars (emoji) now parse as ONE code point — char-count parity
  restored everywhere (estimator/chunk/norm)
- context_broker: four functions delegate (load-bearing _py_ oracles);
  the env gate is cross-checked Python-side (Python verdict stays
  authority)
- cc_loader: ctypes shim, CC_RUST_LIB file-form; battery-covered
- CI: crate builds in both jobs, CC_RUST_LIB envs, canary registered

Gates: canary 38/38 x3 + full seven-crate wall green, suite 2293/0,
type ratchet PASS, ruff clean, henxels all-hold, diff-cover 75%.
2026-10-05 08:43:19 +01:00
conductor 16c147dbee chore(release): auto-bump version to 0.92.2 — earned by #2460 (R2 W2.1: deterministic Rust memory-retrieval kernel)
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2460
2026-10-05 07:57:54 +01:00
conductor 15cba0bb24 Merge pull request 'R2 W2.1: deterministic Rust memory-retrieval kernel' (#2460) from r2-wave2-memory-retrieval into master 2026-10-05 07:57:47 +01:00
Nick 1283b0bf94 fix(memory): W2.1 coverage 62->84 + pair-list contract harmonized
- chunk gather + projection now speak the W2.1 pair-list contract
  consistently on BOTH engines (fusion['ranks'] is [[rowid, rrf], ...];
  dict-form still accepted for backward callers)
- oracle-path tests: all three modes, abstention edges, malformed
  timestamps, hybrid accumulation, chunk SQL gather through a real
  fixture (vec-table absent by design; embed stubbed — vector execution
  stays Python per the W2.0 ruling)
- battery 32->32 tests extended; suite 2291/0; canary 75/75 x3;
  diff-cover 84% (floor 70)
2026-10-05 07:51:40 +01:00
Nick 1ee35d76c9 fix(memory): W2.1 coverage + stopword parity + chunk projection kernel
- battery: delegation + projection coverage for the kernel consumers
  (29-test shim battery; CI's 45% was the pre-extension tree)
- chunk projection (chunk_recall tail) moved into the kernel:
  RRF-desc order, text[:400] chars, budget cap, page/room/default
  semantics — canary 75/75 x3
- stopword list AST-extracted exactly (51 words incl. needs/wants/likes)
- memory_store: recall/fused/association/chunk kernels delegate with
  _py_ oracles as load-bearing fallback

Gates: canary 75/75 x3, shim battery 29/29, suite 2286/0, ruff clean,
type ratchet PASS, henxels all-hold. PR 2460 updated with the
propagation evidence line.
2026-10-05 07:20:38 +01:00
Nick e8daf78eaa Merge remote-tracking branch 'gitea/master' into r2-wave2-memory-retrieval 2026-10-05 06:39:03 +01:00
Nick 19976c310c feat(memory): R2 W2.1 — deterministic Rust memory-retrieval kernel (#2458)
Contract BUILDER-2026-10-04T20:10:25Z-cli, closed: memory_store and
memory_selector correctly_predicted; kernel expected_new_observed; shim
surfaces honestly new. Stacked on W2.0 (merged de54453).

- memory_retrieval crate (zero-dep, no-clock, rlib+cdylib+bin): the
  retrieval transform family in ONE kernel surface — map_reduce,
  position_aware, fts_query_conjunctive (51-word stoplist extracted via
  AST, no transcription drift), chunk_text (byte-vs-char law honored),
  norm_key, plus the extracted kernels: rank_fusion (RRF/lexical
  normalization/mode selection), score_recall_rows (metadata blend +
  mode-specific QPP abstention), merge_association_rows (association
  projection), fuse_results (fact/research projection + per-kind
  min-max scale alignment)
- PARITY-PLUS-IMPROVEMENT proven: recall's N+1 (per-candidate entry
  reads + repeated DF probes) ELIMINATED — rows batch through the W2.0
  read boundary once and the kernel runs per retrieval stage
- Parity trap frozen: naive timestamps take Python's designed
  TypeError->recency=1.0 fallback (parse_iso now refuses naive, exactly
  like aware-vs-naive subtraction) — the 34-case matrix caught this
- UTF-8 transport fix: ensure_ascii=True surrogate pairs corrupt astral
  chars (emoji) in char-count semantics — direct UTF-8 transport, frozen
- both _py_ oracles and delegating publics; RUNE_RETRIEVAL_CORE=python
  pins the fallback
- CI: crate builds in both jobs, MR_RUST_LIB envs, canary registered

Gates: canary 72/72 x3, suite 2286/0, type ratchet PASS, ruff clean,
henxels all-hold. Store: 218 entities,
rust-crate:memory_retrieval modeled.
2026-10-05 06:38:57 +01:00
conductor e2430a9154 chore(release): auto-bump version to 0.92.1 — earned by #2457 (R2 W2.0: SQLite-backed zero-dep Rust read boundary)
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2457
2026-10-05 01:56:56 +01:00
conductor de5445357e Merge pull request 'R2 W2.0: SQLite-backed zero-dep Rust read boundary' (#2457) from r2-wave2-memory-storage into master 2026-10-05 01:56:51 +01:00
Nick e94c8e27a8 Merge remote-tracking branch 'gitea/master' into r2-wave2-memory-storage 2026-10-04 21:07:54 +01:00
Nick 0816c5997d feat(memory): R2 W2.0 — retain SQLite behind a zero-dep Rust read boundary (#2456)
Contract BUILDER-2026-10-04T15:54:17Z-cli closed: module:memory_store correctly_predicted; memory_storage crate, mems_loader, and canary honestly new.

- memory_storage crate: zero Cargo dependencies; system libsqlite3 FFI; sqlite3_open_v2(SQLITE_OPEN_READONLY) makes writes impossible at the connection level; no write-capable mems_* exports
- typed read parity: INTEGER/FLOAT/TEXT/NULL/BLOB, bound string/numeric/bool/NULL params, aggregates, empty results, unicode, and honest SQLite errors
- mutation SQL is rejected; fixture SHA-256 is identical before/after all reads and the rejected mutation
- memory_store gains an explicit backend seam: stdlib SQLite remains the default; Rust read path is opt-in via RUNE_MEMORY_STORAGE=rust; connection-based transactional callers are untouched
- stats_for_path is the proving consumer; both backends return identical result shape
- canary-first: crate-absent red proven, now 19/19 three consecutive
- CI builds the crate in both jobs, pins MEMS_RUST_LIB, and runs the crate canary; pytest shim battery covers loader/seam lines

Evidence: full suite 2283/0, diff-cover 71% >= 70, type ratchet PASS, ruff clean, henxels all-hold. SQLite is retained exactly as Nick directed; this slice exports no Rust write path.
2026-10-04 21:07:43 +01:00
conductor b502a1d7e8 chore(release): auto-bump version to 0.92.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-04 16:52:48 +01:00
Nick 0cd587dfd3 Merge branch 'master' of http://127.0.0.1:3000/conductor/rune 2026-10-04 16:52:43 +01:00
Nick 8404cddb1d feat(scaffolding): R2 Wave 1 — the scaffolding engine reborn (Go start / Rust execution)
Contract BUILDER-2026-10-04T15:25:33Z-cli, closed: scaffold,
construction_decision, go-surface:rune-entry correctly_predicted; crate +
shim + canary honestly new. The engine governed its own rebuild.

- scaffold_core crate (zero-dep, no-clock, rlib+cdylib+bin):
  lifecycle.rs (the LEGAL_TRANSITIONS state machine with exact error
  strings — the single authority, terminals derived), close.rs (the
  closure classification — all buckets incl. the dict-literal overwrite
  quirk and legacy aliases), language.rs (Nick's protocol addition: the
  placement verdict — rune dichotomy rule, other-project fit assessment)
- THE PARITY PROOF: all 43 production contracts in the ledger replay to
  IDENTICAL closure verdicts — the new engine reproduces the V1-accepted
  system's real history byte-for-byte
- Go surface: rune scaffold subcommand — pure ops direct to the Rust
  binary over stdio NDJSON (the tranche's first contracted membrane
  junction); store-backed ops proxy to the engine (strangler membrane);
  4 new Go unit tests (stub-binary junction + failure surfacing)
- scaffold.py: bucket math factored to _py_close_quality, consumed by
  close_contract and the replay battery (one computation, two engines)
- construction_decision.py: language_placement in the classification path
- Canary-first: canary_scaffold_core.py red-proven, now 152/152 x3
  (legality matrix 81 pairs, ledger replay, determinism, Go membrane,
  placement matrix incl. delegation agreement)
- CI: crate builds in both jobs, SC_RUST_LIB envs, canary registered

Gates: pytest green (one bridge-flake rerun), canary 152/152 x3, type
ratchet PASS, ruff clean, henxels all-hold (shim battery extended).
Store: 214 entities — rust-crate:scaffold_core modeled.
2026-10-04 16:52:13 +01:00
conductor 17ec4862e3 chore(release): auto-bump version to 0.91.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-04 16:23:59 +01:00
Nick 6b712a9b75 feat(rebase): R2 Wave 0 — measured inventory + classification + straggler closure
Wave 0.1/0.2: 174 modules, 2,191 functions AST-measured
(store/rebase/inventory.json) and evidence-classified per the dichotomy
law (store/rebase/classification.json — every module carries its import/
content evidence string): GO 43 modules/19,495 lines (reactive,
initiating, network, turn-time), RUST 93/36,215 (deterministic, state,
storage, cognition), TUI-LAST 7/5,438, TEST-INFRA 29/5,395, RETIRES 2
(bootstrap glue). plans/total-rebase-inventory.md is the human-readable
table.

Wave 0.3: the five R1 stragglers (#2171, #2119, #1844, #1755, #1738)
dispositioned with placement evidence and closed — superseded-into-R2,
each folded into its owning wave.
2026-10-04 16:23:55 +01:00
Nick 31f49671ec docs(plan): total-rebase tranche R2 v2 — Nick's six critiques folded in
- parity-plus-improvement rule (parity is the floor, measured improvement
  is in scope, functionality is the gate)
- hybridized Go<->Rust membrane (the no-socket wall withdrawn; junctions
  are contracted, recorded exceptions to the default dichotomy)
- continuous TUI wiring per unit (the TUI wave stays last but integrates
  a system already aimed at it)
- language-placement intelligence in the scaffolding protocol itself
  (rune: the dichotomy rule; other projects: language-fit assessment)
- SQLite RETAINED and wired via system-library FFI (zero-dep legal);
  the bespoke memory redesign deferred to its own later program
- provenance lock: Wave 1 rebuilds the V1-ACCEPTED scaffolding (Nick's
  design, SCAFFOLDING_V1_DEFINITION.md + North Star verbatim), proven by
  production-ledger parity replay
2026-10-04 16:17:32 +01:00
Nick 103a42103b docs(plan): total-rebase tranche R2 — Go initiates and reacts, Rust determines and stores
Designed on the measured inventory (161 modules, 64,482 lines, from the
spine store) per the TRANCHE-DEPTH TEMPLATE. Scaffolding engine first
(Go start / Rust execution, Wave 1) so every later wave is driven by the
new protocol itself; zero-dep law holds with the split doing the work
(Go owns network + reactivity, Rust owns deterministic compute, bespoke
storage engine replaces SQLite in Wave 2); TUI last over the transport
seam; 5 R1 stragglers folded into Wave 0; approval gate before execution.
2026-10-04 16:02:42 +01:00
conductor 615da6bd09 chore(release): auto-bump version to 0.90.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-04 13:25:17 +01:00
conductor 693a494884 Merge pull request 'Phase G strangler slice 2: evidence-provenance Rust candidacy (#2449)' (#2454) from phase-g-strangler-evidence-provenance into master 2026-10-04 13:25:10 +01:00
Nick ee653c6a4c Merge remote-tracking branch 'gitea/master' into phase-g-strangler-evidence-provenance
# Conflicts:
#	requirements-ci.txt
2026-10-04 13:18:54 +01:00
Nick 171f3fb250 fix(tests): close the diff-cover hole — 63% -> 76% on the changed-lines floor
Runs 18367/18369 failed unit-tests on diff-cover (63% < 70%, 338
missing lines) with pytest itself green — the changed-lines floor counts
PYTEST-traced lines only, and three slice surfaces had canary-only
coverage. Battery extension (the shim-battery law applied to my own
slices):
- warm_router: gate branches, finish stack, public score_neuron wrapper
  (designed-fallback determinism), base-level math + gather (hermetic
  via monkeypatched table ensure — STORE_DB resolution is suite-order
  dependent, the order-dependent failure the coverage run exposed)
- tui_transport: InProcess four-method drive, V1 against an in-process
  core, factory modes incl. the bogus-mode ValueError
- token_optimizer: optimize_prompt/get_status/get_cache_stats/flush

CI-exact verification: full coverage suite 2278/0, diff-cover 76%
(floor 70). ruff clean.
2026-10-04 13:09:53 +01:00
Nick 9731181a6b fix(tests): respawn the bridge daemon on mid-test death (root-caused)
The hardening's own diagnostic nailed it (run 18359): the daemon answers
/health at fixture setup, then dies SILENTLY mid-test (alive=False,
out='') on the saturated runner — host shows 20GB swap in use, and a
silent kill with zero output is the memory-pressure signature, not a
race. A retry can never survive a dead process.

Fix: registrations are file-backed under WR_ROOT, so a respawned daemon
resumes the same state — post_event now ensures liveness before each
attempt (respawn on death) and _spawn is the single spawn path (init's
T21.5 fresh-port retry preserved). Bridge suites 19/19, ruff clean.
2026-10-04 12:30:37 +01:00
Nick 544c19719e fix(tests): harden the bridge daemon post_event against load-dependent refusal
Observed 2026-10-04 (runs 18340/18342/18346/18347): the fixture's daemon
answers /health at setup, then a POST lands ECONNREFUSED under runner
load — the twin-failure pattern across two commits while sibling runs
passed. Harness-side hardening per the intermittent-defect law:
post_event retries twice with a beat, and on final failure dumps the
daemon's captured output (alive state + log tail) so the next diagnosis
starts from evidence. Bridge suite 11/11 locally, ruff clean.
2026-10-04 12:18:28 +01:00
Nick d571658b5a fix(ci): register tui_transport in the TUI registry
The tui_panels_are_governed henxel held on the H4 slice-1 surface
(runs 18346/18347): new TUI surfaces ship with a registry entry in the
same PR. Entry added (not-a-surface seam, contract ref, coverage ref).
Local henxels: all hold.
2026-10-04 12:16:23 +01:00
Nick 50c997beab feat(phase-h): H4 slice 1 — TUI transport seam (the migration wave's strangler path)
Contract BUILDER-2026-10-04T10:32:00Z-cli, closed: rune_tui,
go-surface:rune-entry, canary_rune_serve correctly_predicted;
tui_transport + the core_server extension honestly new.

The TUI's operator surface is exactly 4 public calls — this slice puts
them behind one seam so the wave migrates surface-by-surface instead of
rewriting 4k lines of Textual in one go:
- tui_transport.py (new): InProcessTransport (today's behavior,
  DEFAULT via RUNE_TUI_TRANSPORT=inprocess — zero behavior change on
  merge) and V1Transport (stdlib HTTP against the Go front door)
- core_server.py: /v1/conversation + /v1/approvals/resolve — the two
  endpoints the TUI needed beyond H2's set; the Go front passes them
  through (proxy law), H2 battery extended to 28/28 with parity
- rune_tui.py: the 4 call sites route through the seam; the one
  private _task_state peek stays in-process by design (annotated)
- canary_tui_transport.py (new, canary-first red-proven): 9/9 x3 —
  four-method equivalence against the stub core, approval boolean
  round-trips both ways, dead core raises (never a silent empty answer)

Gates: pytest 2270/0, canaries 9/9 + 28/28 x3, type ratchet PASS,
ruff clean. Store rebuilt: 212 entities.
2026-10-04 11:52:38 +01:00
Nick 9e6215e605 fix(ci): henxels holds — intentional mirrors registered + shim-layer pytest battery
Contract BUILDER-2026-10-04T10:29:39Z-cli (successor to 10:20:24Z-cli,
same red evidence run 18322 lint job 51891).

- henxels.yaml: the deliberate-duplicate registry — shared/** (deployed-
  vintage mirror tree), the named src/rune strangler shims, per-crate
  pyjson copies (self-contained zero-dep design), tiny zero-dep
  lockfiles, store/scaffold data, issue-template + completion_file
  mirrors, the interop/rebase-proof fixture
- tests/test_rust_core_shims.py (new, 11/11): the thin pytest battery
  the diff-cover law owed the shim layer — drives the DELEGATION lines
  (loader resolves, envelope unpack, public-vs-fallback agreement) for
  spine_loader, ms_loader, cs_loader, tc_loader, and core_server's
  in-process shapes; skips cleanly on unbuilt crates

Local: henxels all-hold green, battery 11/11.
2026-10-04 11:30:19 +01:00
Nick 6429c5bb49 fix(ci): canary environment repair — runner-HOME rustup path + per-run go binary
Contract BUILDER-2026-10-04T10:20:24Z-cli, closed: all four surfaces
correctly_predicted. Red evidence: run 18322 (4 canary failures).

Two defect classes, both canary-side environment assumptions:
- the Rust-parity canaries resolved the rustup toolchain via
  Path.home() — on the runner that is /var/lib/gitea-desktop-runner,
  which has no toolchain, so cargo fell through to the BROKEN system
  rustc (LLVM_23.1 symbol error). Now the absolute /home/nick path
  (x-ACL covers the runner user) wins, with the home path as fallback
  for other hosts. token_counting gets the same fix preemptively.
- canary_rune_serve built its Go binary to the fixed path
  /tmp/rune-serve-canary — nick owns the local file; the runner cannot
  overwrite it (EACCES). Now a per-run path (-PID).

Verified: all five canaries green locally; runner-HOME simulated
(HOME=/var/lib/gitea-desktop-runner) green for the crate build;
ruff + type ratchet clean.
2026-10-04 11:22:49 +01:00
Nick ea34f2d5ab feat(inference): H3 slice 5 — token counting/compression into the token_counting crate
Contract BUILDER-2026-10-04T09:57:23Z-cli, closed: module:token_optimizer
correctly_predicted; crate + shim + canary honestly new. FINAL H3 domain —
the Phase H plan's back-end port list is now fully delivered.

- token_counting crate (zero-dep, rlib+cdylib+bin): estimate_tokens
  (code points, floor division), compress_context (hand-rolled run
  collapse for 3+ newlines and 2+ spaces — no regex crate; literal
  case-sensitive prefix shortening; unicode strip)
- tc_loader.py: ctypes shim (TC_RUST_LIB file-form)
- token_optimizer.py: estimate_tokens/_compress_context delegate;
  class keeps cache I/O, persona loading, debounce clock
- Repair en route (caught by test_token_optimizer_cache.py): the first
  helper-block placement sat mid-class and swallowed four methods
  (optimize_prompt/get_status/get_cache_stats/flush) into a nested def —
  moved after the class, suite green again
- Canary-first: canary_token_counting.py red-proven, now 26/26 three
  consecutive (unicode code points, emoji, run boundaries, tab-vs-space,
  mid-word prefix non-match, delegation)
- CI: crate builds in both jobs, TC_RUST_LIB envs, canary registered

Gates: pytest 2259/0, canary 26/26 x3, type ratchet PASS, ruff clean.
Store rebuilt through the Rust spine: 211 entities,
rust-crate:token_counting modeled.
2026-10-04 11:18:19 +01:00
Nick 0eff96a335 feat(capability): H3 slice 4 — capability scoring ported to the capability_scoring crate
Contract BUILDER-2026-10-04T09:43:55Z-cli, closed: capability_fit and
capability_broker correctly_predicted; crate + shim + canary honestly new.

- capability_scoring crate (zero-dep, rlib+cdylib+bin): evaluate_fit
  (EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN with exact reason strings,
  unhealthy status join incl. truthy superseded_by, set-math missing
  list), score_card (weights 4.0 name / 3.0 capability / 2.0 examples /
  0.8 description / 0.3 generic penalty / priority bonus)
- wordlists.rs GENERATED from the capability_broker.py literals via AST
  extraction (12 generic caps, 94 stopwords — no transcription drift)
- hand-rolled ASCII word-run scanner replaces re.findall(r'\w+') —
  no regex crate (zero-dep law holds)
- cs_loader.py: ctypes shim (CS_RUST_LIB file-form)
- capability_fit.py / capability_broker.py: _py_ variants as fallback +
  parity oracle, thin delegating publics, RUNE_CAPABILITY_CORE pin
- Canary-first: canary_capability_scoring.py red-proven, now 24/24
  three consecutive (every fit class, every weight branch, stopword
  and generic penalty, delegation)
- CI: crate builds in both jobs, CS_RUST_LIB envs, canary registered

Gates: pytest 2259/0, canary 24/24 x3, type ratchet PASS, ruff clean.
Store rebuilt through the Rust spine: 209 entities,
rust-crate:capability_scoring modeled.
2026-10-04 10:56:10 +01:00
Nick de68444e50 feat(memory): H3 slice 3 — warm_router scoring math into memory_scoring
Contract BUILDER-2026-10-04T09:28:47Z-cli, closed: warm_router and
rust-crate:memory_scoring correctly_predicted.

- warm.rs: score gate (keyword primary gate, semantic secondary gate
  0.55, boost shape, context cap), score finish (importance, b
  contribution, wash boost, cache boost, body-knowledge overlap, fan
  penalty MULTIPLYING before length bonus and protected boost ADD —
  order is load-bearing), ACT-R base-level decay (power-law sum, ln,
  clamp [-2,2], cold -2.0, non-string row -> -0.2 whole call faithful
  to the original's AttributeError-through-outer-catch)
- warm_router.py restructured gather-then-combine: embeddings, SQLite
  use-history, and the cache file are gathered in the wrapper in the
  original's exact reach order — NO base-level or cache I/O on the
  no-fire path (the original early-returns before touching them)
- base_level_activation keeps its SQLite scan; the decay math
  delegates; the outer exception contract (anything -> -2.0) preserved
- Battery extended: 108/108 three consecutive (7 gate branches,
  11 finish stacks, 7 ACT-R cases incl. clamp-high and cold starts)

Gates: pytest 2259/0, canary 108/108 x3, type ratchet PASS, ruff clean.
2026-10-04 10:42:41 +01:00
Nick d58e88da5c feat(memory): H3 slice 2 — drift_compression + drift_cache pure surfaces into memory_scoring
Contract BUILDER-2026-10-04T09:15:51Z-cli, closed: drift_compression,
drift_cache AND rust-crate:memory_scoring correctly_predicted (the
resolver + existing entity made the crate predictable this time);
canary/loader honestly predated the contract.

- compression.rs: ratio table, compress_text (int() truncation,
  Unicode char counts, post-clamp ellipsis compare), entry/warn
  surfaces with exact recommendation strings (em-dashes included)
- cache.rs: compute_priority (raw product, no rounding — faithful),
  qualifies_for_cache admission decision; DriftCache file I/O and
  clock stay Python (zero-dep split law)
- drift_compression.py / drift_cache.py: _py_ variants as fallback +
  parity oracle, thin delegating publics, shared _ms_core seam
- ms_loader.py: 7 new FFI symbols, same segfault-law discipline
- Canary battery extended: 83/83 three consecutive (ratio boundaries,
  text edges incl. unicode, every warn branch, priority grid,
  qualification boundaries)

Gates: pytest 2259/0, canary 83/83 x3, type ratchet PASS, ruff clean.
2026-10-04 10:27:15 +01:00
Nick c730196dba feat(memory): H3 slice 1 — drift_core scoring trio ported to the memory_scoring crate
Contract BUILDER-2026-10-04T08:57:33Z-cli, closed: module:drift_core
correctly_predicted; crate + shim + canary honestly new.

- memory_scoring crate (zero-dep, rlib+cdylib+bin): compute_value with
  now injected (no-clock law — the original reads the wall clock),
  check_promotion_eligibility, pre_commit_gate_entry. Hand-rolled
  ISO-8601 parser (no chrono), banker's rounding via round_ties_even,
  Unicode-aware char counts, Python falsy-coercion semantics preserved
  (0.0 importance -> 0.5)
- Vintage quirk preserved, not fixed: the fourth harmful pattern carries
  uppercase but matches against text.lower() — it can never fire in the
  original, and the parity battery caught my intent-faithful port
  diverging. Behavior fixes belong to their own contract
- ms_loader.py: ctypes shim (FFI segfault law, MS_RUST_LIB file-form)
- drift_core.py: pure trio delegates when the crate is built
  (load-bearing Python fallback; RUNE_MEMORY_CORE=python pins it);
  DB-backed functions stay Python (zero-dep split law);
  signatures made honestly Optional (implicit-Optional ratchet repair)
- Canary-first: canary_memory_scoring.py red-proven crate-absent,
  now 32/32 three consecutive — factor battery, threshold boundaries,
  every critic branch, edge inputs, public-API delegation
- CI: crate builds in both jobs, MS_RUST_LIB envs, canary registered

Gates: pytest 2259/0, canary 32/32 x3, type ratchet PASS, ruff clean.
Store rebuilt through the Rust spine: 207 entities,
rust-crate:memory_scoring + module:canary_memory_scoring modeled.
2026-10-04 10:14:36 +01:00
Nick c750513fab feat(phase-h): H2 Go front door — /v1 surface with NDJSON streaming + parity battery
Contract BUILDER-2026-10-04T08:32:43Z-cli, closed: go-surface:rune-entry
and workflow:ci.yml correctly_predicted; core_server + canary honestly
new (unresolvable at submit time).

- core_server.py (new): stdlib sidecar over the operator surface —
  GET /v1/models, GET /v1/status, POST /v1/chat/completions with NDJSON
  streaming; one error envelope shape across every failure path;
  injectable operator (the canary drives a deterministic stub via the
  established _execute_on_model seam — no live model calls)
- serve.go (new): rune serve subcommand — Go owns the front door
  (stdlib net/http only), byte-pass-through proxy to the core,
  locally-generated error envelopes byte-matching the core's shape,
  core-down 502 core_unavailable
- canary-first: canary_rune_serve.py fired red (module absent), now
  22/22 three consecutive — Python shape freeze, cross-language parity
  modulo volatile (id/created), NDJSON line-sequence equality,
  core-down honesty on both sides
- Go unit tests 6 → 11 (help/flag exits, 404 + 502 envelopes,
  byte-for-byte proxy pass-through); vet clean
- CI: canary registered; go-surface job covers serve.go module-wide

Gates: pytest 2259/0, canary 22/22 x3, type ratchet PASS
(cast-narrowed stdlib override, zero ignores), ruff clean.
2026-10-04 09:55:16 +01:00
Nick 27c4bf8d78 feat(spine): H3-spine — spine projection machinery ported to a zero-dep Rust crate
Contract BUILDER-2026-10-04T07:19:36Z-cli, closed: module:spine
correctly_predicted; crate + loader + canary + CI wiring honestly new
(unresolvable at submit time — they are the delivery).
Nick directives: 'rebuild the spine in rust' + 'add scaffolding to the
rust conversion' — this is both, through the full pipeline.

Rust core (src/rust/rune_spine/, zero-dep, rlib+cdylib+bin):
- pyjson.rs: Python-parity JSON (reused from evidence_provenance)
- store.rs: doc semantics — high-water add_entity, dedup add_relationship
  with CANDIDATE guard, authority observation + reconcile, resolve_status,
  impact_frontier with the obligation semantics tables
- project.rs: full build() projection — carry-forward, manifest ingest
  with generated-view refusal, module re-derivation, deployed observation,
  meta-surface projection (rust-crate/go-surface/config/workflow)
- drift.rs + query.rs: drift classification, query, export_umbrellas
- FFI: single-arg JSON envelopes, sp_free_string; no-clock law (now is
  always an argument)

Zero-dep split law: every filesystem scan stays in spine.py
(_gather_build_inputs/_gather_drift_inputs); both cores consume the same
gathered dict, so parity divergence is logic, never environment.
spine_loader.py: ctypes shim (argtypes+restype per the FFI segfault law,
SP_RUST_LIB file-form override, load-bearing Python fallback).
The unseeded Phase A path stays on the Python core — the public
ingest_umbrellas seam is monkeypatched by isolation tests.

Parity battery (canary-first, red-proven crate-absent):
canary_spine_rust.py 21/21 — drift, 10 query entities, export, seeded
build, unseeded ingest, plus a carry-forward mask guard (asserts the
Rust projection models declared surfaces directly — caught the
config_manifest wiring bug the structural diff could not).

Gates: pytest 2259/0, canary_spine_meta 19/19 x2 through the Rust path,
type ratchet PASS, ruff clean, diff-cover 84% >= 70.
CI: crate builds in both jobs, SP_RUST_LIB pinned, canary registered.
Store rebuilt through the Rust core itself: 204 entities,
rust-crate:rune_spine modeled (self-hosting meta-scaffold).
2026-10-04 09:26:01 +01:00
Nick 5b8023ca2a docs(scaffolding): T4.7 reassessment matrix — all tranche rows correctly_predicted
Successor closures against the H-meta-modeled spine with the repaired
resolver (57eaa8f):
- row 1 v2 slice-1 query_memory: 3/3 (BUILDER-2026-10-04T07:17:13Z-cli)
- row 2 slice-2 evidence_provenance six ports: 9/9 (07:17:15Z-cli)
- row 3 H1 Go first-call shim: 4/4 (07:17:17Z-cli)

Every delivered tranche surface now audits clean against the spine it
lives in. Matrix complete; T4.7 done.
2026-10-04 08:17:40 +01:00
Nick 57eaa8f383 fix(scaffolding): contract scope resolver recognizes H-meta entity kinds
Contract BUILDER-2026-10-04T07:14:03Z-cli, closed: module:construction_decision
correctly_predicted, zero genuinely_unexpected_new.

T4.7 row-1 closure flagged rust-crate:query_memory resolving as
kind=unresolved — classify_surface's prefix set predated the H-meta
entity classes (rust-crate:/go-surface:/config:/workflow:), so
spine-modeled surfaces could never close correctly_predicted.

Red-first: test_hmeta_kind_prefixes_classify_as_structural_entity
failed pre-fix; identity-resolution path needed no change (works for
any id present in an authority). Regression frozen in
test_construction_decision.py (2 new tests, 21/21 file green,
decision neighborhood 107/107).
2026-10-04 08:16:38 +01:00
Nick 567fd39937 feat(spine): H-meta meta-scaffold — every part of rune modeled as a spine entity
Contract BUILDER-2026-10-04T06:24:56Z-cli, closed: module:spine
correctly_predicted; six new entity surfaces honestly unmodelable at
submit time (they are the delivery).

- spine.py: project_meta_surfaces() in the seeded derivation path
  (build step 4) — rust-crate, go-surface, config, and workflow entity
  classes with belongs_to edges; crate-resident canary scan projects
  obligated_canary edges from the owning crate
- config/umbrellas.json: declared rust_crates, go_surfaces, configs;
  canary registrations for audit_ledger, rune_entry, spine_meta
- canary_spine_meta.py (new, canary-first: 14 red proven): entity
  existence/kind checks, canary modeling + edges, determinism, and
  open-world carry-forward — 19/19 five consecutive runs
- determinism contract made honest: structural equality (deep
  timestamp strip incl. last_success + observation set-dedup), not
  clock equality — the per-build deployed_identity observation ticks
  by design
- test_structural_store: schema gate admits the new entity kinds
- ci.yml: canary_spine_meta registered in the canary loop

Gates: umbrella/henxels/structural 32/32, full pytest 2257/0.
2026-10-04 08:09:40 +01:00
Nick 817a97abd9 feat(phase-h): H1 Go first-call shim — delivered under construction contract BUILDER-2026-10-04T05:25:57Z-cli
Scaffolding-first rebuild of the reverted ad-hoc attempt (1e6cb4d), per
Nick's 2026-10-04 directive that all coding replacements run through the
construction pipeline:

- Need submitted with acceptance criteria + constraints (zero-dep Go,
  pinned toolchain, canaries-first); resolution REUSE, approval covered
  by Nick's Go-frontend directive; contract generated and approved.
- CANARY LANDED FIRST (canary_rune_entry.py restored; module absent ->
  canary red proves ordering), then the module restored from the reverted
  reference and the canary went 12/12 green.
- Contract lifecycle driven to close: observe -> verifying -> close.
  Closure verdict (honest): all four delivered surfaces land in
  genuinely_unexpected_new — the spine/structural registry does not model
  the new Go surface, the canary, the pin file, or the workflow yet. That
  IS the coverage signal the closure exists to emit; the spine-modeling
  gap is now a recorded follow-up across H-wave deliveries.

Retrofit closures also completed under the same directive:
- Slice 2 (evidence-provenance): BUILDER-2026-10-04T05:23:17Z-cli — all
  six ported modules correctly_predicted; crate + canaries + shim battery
  honestly unmodeled.
- Slice 1 (query-memory): BUILDER-2026-10-04T05:24:11Z-cli —
  module:query_memory correctly_predicted; crate + canaries unmodeled.
- BUILDER-2026-10-04T05:14:20Z-cli (v1) and ...:18:52Z (v2) stand as
  labeled learning artifacts (entity-vocabulary mismatch).

go test 6/6, go vet clean, canary 12/12 (desktop + bare-HOME CI sim),
henxels exit 0, pin check PASS.
2026-10-04 06:28:51 +01:00
Nick 1e6cb4d1ba revert(phase-h): withdraw ad-hoc H1 shim + enablement — will be rebuilt through the scaffolding construction pipeline
Nick directive 2026-10-04: all coding replacements must be done via the
scaffolding construction pipeline (construction_decision -> normalization
-> discovery/fit/resolution -> toolchain -> layout obligations -> approval
gate -> durable trace -> generated contract -> canaries-first ->
implementation -> verification -> delivery record). The H1 Go shim was
built ad-hoc; this reverts it (and the H0 CI surface) for a proper
rebuild. The scaffolding run that replaces it follows as its own
construction contract.
2026-10-04 06:11:20 +01:00
Nick 4e11aa4268 fix(ci): go pin check strips the distro build-experiment suffix
CachyOS ships go 1.27.1-X:nodwarf5 (an LLVM build experiment flag). The
pin compares the RELEASE (1.27.1), not the suffix — same pin law, honest
comparison. Verified locally before push this time.
2026-10-04 05:59:05 +01:00
Nick 1703c25367 feat(phase-h): H0+H1 — Go toolchain enablement and the first-call shim
H0 (enablement, per plan 2026-10-04-phase-h-go-frontend-rust-backend.md):
- Go 1.27.1 installed (pacman, world-readable — runner user verified).
- go-version.txt pins the toolchain; the go-surface CI job fails loud on
  drift instead of letting a distro upgrade change behavior downstream.
- go-surface job: vet + test + build, runs on the rune-ci-desktop label.

H1 (first-call shim, Go owns the front end per Nick's USER-LOCKED directive):
- src/go/rune-entry: the 'rune' binary replicates the DEPLOYED bash
  launcher's exact contract — cd /mnt/workdrive/rune-code/current/src,
  RUNE_HOME/RUNE_MEMORY_DIR/MIMO_PROBE_ENABLED env (explicit ambient
  overrides win), exec handoff to the memory-venv python -m rune with
  args/signals/exit codes passing through 1:1. Falls back to the dev
  checkout when the deployed revision is absent, and to PATH python3 when
  the venv is absent (CI sandboxes).
- Flags: --version (deployed pyproject), --python-only (resolved handoff),
  --help (exit 0), unknown flag (usage on stderr, exit 2).
- canary_rune_entry.py: 12 checks — builds the module in a temp dir
  (zero-dep, system Go), verifies the version surface, the handoff shape,
  flag discipline, the Go unit battery (6 tests), and go vet. Green on the
  desktop AND under a bare-HOME CI simulation (the fallback expectation is
  asserted honestly, not fudged).
- Registered in the canary suite loop (henxels exit 0).

Observed-not-invented: the launcher contract comes from ~/.local/bin/rune;
the version from the deployed pyproject (0.89.0 at this commit).
2026-10-04 05:52:43 +01:00
conductor 7cc47de6de chore(release): auto-bump version to 0.89.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-04 05:09:58 +01:00
Nick e93315d156 fix(ci): same label correction on the branch (rune-desktop -> rune-ci-desktop) 2026-10-04 05:09:54 +01:00
Nick be49fb32d1 fix(ci): retargeted jobs must use the runner LABEL rune-ci-desktop, not the runner name
Correction to 1462262: act_runner matches jobs by runner LABELS. The only
registered runner (rune-desktop on gitea-desktop-runner) carries the label
'rune-ci-desktop:host' — 'rune-desktop' is the runner's NAME, which matches
nothing. All 8 retargeted jobs queued forever (runs 18197/18203/18211
stuck exactly this way) while ci.yml jobs (which use the correct label)
drained normally. rune-desktop -> rune-ci-desktop in all 8 spots.
2026-10-04 05:09:53 +01:00
Nick c41ed97373 docs(plan): phase H — Go front end / Rust back end (Nick's USER-LOCKED directive)
Directive supersedes KEEP-PYTHON rubric verdicts. Plan scopes:
- Go: runtime entry (rune binary), CLI/IPC first-call dispatch, /v1
  endpoint surface (the rubric's one capability-proven Go fit: stdlib
  net/http + NDJSON streaming). TUI deferred to its own wave (H4).
- Rust: remaining pure-logic back-end modules (drift_*, warm_router
  scoring, memory_store helpers, capability scoring, token counting)
  into zero-dep crates, each port running the full slice-2 pipeline.
- Honest constraints carried: network code never in crates; SQLite/regex/
  IO stay at language edges; every surface gets observed-byte-parity proof.

Sequenced H0 (enablement: install Go, CI Go job) -> H1 (first-call shim)
-> H2 (dispatch + /v1) -> H3 (back-end ports) -> H4 (TUI wave).
2026-10-04 04:47:40 +01:00
Nick 558401844e fix(release): TUI footer version + drift gate now covers the footer pattern
Master CI unit-tests failure (run 18195): rune_tui.py:1888's footer said
'Rune v0.88.4' while __version__ is 0.89.0. Root cause in the auto-version
push-to-master bump: its replace pairs cover the footer, but the drift
GATE's regex checks only covered the Version: header. When RELEASE_VERSION
was stale, both replaces no-opped; the gate's RECOVER regex force-fixed
the header (first-match re.sub) and the footer passed through unchecked —
the exact half-bump shape seen in 396ae12.

- rune_tui.py footer -> Rune v0.89.0 (fixes test_tui_banner_uses_the_reviewed_release_version)
- auto-version.yml: gate checks add the footer regex ('Rune v({VER}) \|'),
  so future footer drift triggers the same RECOVER path as the header.
2026-10-04 04:34:05 +01:00
Nick 26e4929b10 chore(deps): re-resolve ci lock — markupsafe 3.0.4, platformdirs 4.12.3, websockets 17.2 (master catches up to the strangler branch's regen) 2026-10-04 03:16:27 +01:00
Nick 1462262476 fix(ci): retarget 8 jobs from dead rune-infra-pi label to rune-desktop (master)
Same fix as 0bf4726 on the strangler branch, applied directly to master:
queued workflow runs resolve their workflow definition from the DEFAULT
branch, so the strangler-branch commit alone could not drain stuck runs
18178 (auto-label on a PR) and 18183 (auto-label on an issue). This is
the landing spot. 8 occurrences across 5 workflows retargeted.
2026-10-04 03:01:40 +01:00
Nick 0bf47265c8 fix(ci): retarget 8 jobs from dead rune-infra-pi label to live rune-desktop runner
New submissions (issues/PRs) were stuck queued forever: auto-label.yml's
priority-gate + demand-comment jobs declared runs-on: rune-infra-pi, a
runner that no longer exists anywhere (registration list shows only
rune-desktop on gitea-desktop-runner). Every issue or PR filed since the
label went dark sat unprocessed — which is exactly the 'new submissions
not working' symptom.

Retargeted (sed, 8 occurrences across 4 workflows):
- auto-label.yml (2): priority gate, demand comment
- auto-backup.yml (1), auto-update-gitea.yml (1), auto-version.yml (1)
- maintenance.yml (5)

Verified live: probe issue + probe PR + probe branch all created AND
processed successfully through the API (submissions themselves work; it
was the responder workflows that were orphaned). Probe artifacts cleaned
up. The stuck queued runs 18178/18183 will drain once this lands.
2026-10-04 02:49:13 +01:00
Nick 02a9dc4a31 fix(ci): rust toolchain law — healthy rustup toolchain, runner-owned target, EP_RUST_LIB wiring
CI cargo build failed twice (runs 18169/18170): the SYSTEM rustc is broken
on this host at the packaging level — pacman rust 1:1.99.0-1's
librustc_driver.so has 638 undefined symbols versioned LLVM_23.1, and
/usr/lib/libLLVM.so.23.1 does not define narrow-char std::string::_M_mutate
under that node (it exports _M_create/_M_assign there; only the wide-char
_M_mutate carries the version). Every compile dies with a symbol-lookup
error while cargo --version prints fine — a silent-broken build host.

Fix (all local, no system changes):
- PATH points at /home/nick/.rustup/toolchains/stable.../bin FIRST — that
  toolchain is healthy and ships its own libLLVM. The ordering matters: via
  RUSTUP_TOOLCHAIN (shim-based), rustup would exec the broken system rustc.
  /home/nick already carries the x-ACL for gitea-desktop-runner (proven:
  runner-user build succeeds, 0.95s, zero-dep crate).
- CARGO_TARGET_DIR is a runner-owned /tmp dir (the repo's target/ is
  nick-owned; publish-by-copy into it failed with EACCES). No copy step at
  all now: ep_loader + the canary resolve artifacts via EP_RUST_LIB (file
  path form — the loader's override takes a FILE; set in both jobs' env).
- unit-tests job gains the same env block as canary (RUNE_HOME/
  RUNE_MEMORY_DIR/EP_RUST_LIB) so the shim battery's cdylib load and the
  isolation law hold in CI exactly as locally.
- Python canary resolves _BIN next to the EP_RUST_LIB file (binary and
  cdylib are siblings in the release dir; desktop keeps the repo-relative
  default).

Verified locally: runner-user build + EP_RUST_LIB file-form through BOTH
canaries (74/74, 66/66) and the shim battery (7/7); desktop default path
re-verified after artifact restoration (deps/ hardlinks were never lost —
an earlier publish test's rm removed the top-level copies; restored from
deps/).
2026-10-04 02:17:44 +01:00
Nick 0f3aeff58b fix(ci): diff-cover ratchet + canary artifact coupling — shim battery, crate build in CI
diff-cover 70% floor (run 18164's unit-tests failure):
- The floor counts CHANGED lines covered by pytest only. The strangler's
  verification moved the six ported shims to canaries/parities (outside
  pytest), so 46% of slice-2's changed lines were pytest-covered and the
  gate red-lined the port itself.
- tests/test_evidence_provenance_shims.py: a six-test shim battery driving
  the delegation surfaces the port changed (verification_layer dispatch +
  seven wrappers, evidence_probe schema/budget, behaviour_ledger state
  machine incl. accept_limitation + reload round-trip, behavior decision
  math on an isolated db, claim_audit scan->verify->degrade->journal->
  report pipeline, audit_ledger append->verify->read on the isolated
  ledger). Isolation law documented in the module docstring: RUNE_HOME/
  RUNE_MEMORY_DIR pinned before import (both resolve paths at import);
  behavior/claim_audit are imported by no other test file.
- save() finding: mutators never auto-persist in EITHER vintage (verified
  against the pre-port module) — the round-trip test calls save().
- .coveragerc omits ffi_query_memory_smoke.py (= the canary precedent:
  standalone evidence script, never suite-imported).
- Result replicated locally against gitea/master: 116 diff lines, 87%
  (floor 70), exit 0.

Canary job now builds the crate (run 18164's canary failure):
- Until 18164 the job silently relied on the host executor seeing the
  desktop's prebuilt target/ — my checkout-relative BIN made that visible
  as an uncaught Popen FileNotFoundError (§5 crash before finishing).
- ci.yml gains cargo build --release for the zero-dep crate (system cargo,
  world-readable on the runner; no registry fetch).
- §5 fails loud-but-counted (FAIL test, not a crash) when the artifact is
  missing, with the regression meaning spelled out.

Local wall: pytest 2257/0; canaries 74+66 both modes; lint 345/75 PASS;
type ratchet PASS under the pinned instrument; henxels exit 0; umbrella
gate exit 0; dep-lock in sync.
2026-10-04 02:08:29 +01:00
Nick ff2abcd502 fix(ci): run-18161 follow-through — mode detection, plan kebab-case, smoke coverage, deterministic type instrument
Canary mode detection (the real 18161 defect):
- isfile() is not a valid LIVE/ISOLATED discriminator: run 18141's shim
  writes left a zombie 1-entry ledger in the runner's persistent HOME, which
  flipped the naive gate to LIVE with wrong data. The discriminator is now
  the repo's own CI-sandbox convention (RUNE_HOME=/tmp/rune-ci-<id> — the
  workflow's own cleanup guard matches exactly that).
- The isolated battery seeds a PER-RUN temp ledger (module-attr override on
  the shim's LEDGER_FILE) instead of writing the runner's persistent home;
  binary §5 gets HOME=<temp> so binary and shim resolve the same file.
  Both modes: 74/74 locally and under simulated runner topology (zombie
  ledger present, RUNE_HOME set). Rust canary §4 gets the same dual-mode
  treatment.

Henxels holds cleared:
- plans/: three phase-g files renamed to kebab-case; references updated in
  lib.rs, ep_loader.py, canary docstring, Cargo.toml, supersession ledger.
- ffi_query_memory_smoke coverage: smoke repaired (was stranded — imported
  the deleted src/rune vintage; now drives shared/ shim, 41/41 green) and
  tests/test_ffi_query_memory_smoke.py runs it, skipping honestly when the
  cdylib isn't built (CI has no cargo step).

Type ratchet — instrument repair (§16V), not reading-management:
- scripts/type_check_ratchet.py now pins pyright --pythonpath to the
  INVOKING interpreter. Ambient-interpreter dependency resolution was the
  residual non-determinism: runner (py311, no rich/Pillow) vs desktop (3.14
  system site) produced 460 vs 454 on the identical tree, and any import
  ignore flipped to unnecessary-ignore in the other environment.
- pyrightconfig extraPaths now covers src/rune + shared + both crate dirs
  (bare intra-repo imports resolve under the pinned environment).
- Pillow declared in pyproject (genuine lazy dep of the diamond-frames
  surface; was undeclared) — lock regenerated, dep-lock checker green.
- Measured under the pinned instrument: 115 errors / 439 warnings
  (CI-equivalent locked 3.11 venv) and 115/436 (local 3.14 venv) — both
  below the 117/454 baseline; baseline unchanged. My canary's 4
  reportOptionalSubscript errors fixed by real narrowing, not suppression.

Lint baseline: BLE001 346 -> 345 (bulk_issue_guard.py's retry catch gains a
reasoned noqa; the file stays untracked per checkpoint hygiene, but the
honest-ledger test counts disk state — the noqa keeps local == CI).

Local wall: full pytest 2251/0 (incl. the new smoke test); canaries 74+66
both modes; parities 33/24/13/16/17; henxels exit 0; umbrella gate exit 0;
dep-lock green.
2026-10-04 01:08:20 +01:00
Nick b61af7a526 fix(ci): slice-2 canary completeness + gate repair — dual-mode canary law, ports 3-6 sections
Canary completeness (contract §5/§10 — owed at slice end):
- canary_audit_ledger.py 34 -> 74 checks: port-3 (evidence_probe schema gate
  + budget count + ProbeBudgetExceeded), port-4 (behaviour_ledger state
  machine: define/record/invalidate/outstanding/summary/history + byte-exact
  error envelopes), port-5 (behavior decision math through the shim on an
  isolated RUNE_HOME db: correction/habit math, promotion, not-found shapes),
  port-6 (claim_audit full pipeline on an isolated journal: scan/scan_text/
  audit/report + verify + stoplist wiring).
- canary_evidence_provenance_rust.py 28 -> 66 checks: the same four ports
  driven through the cdylib FFI (ep_validate_probe, ep_bl_*, ep_habit_/
  ep_correction_reinforce, ep_ca_* batteries).

Dual-mode canary law (repairs run 18141's red):
- Live-ledger pins (entries floor, live read/list shapes) fire only when the
  live promotion ledger exists in this process's HOME; under CI's sandboxed
  HOME the same sections run an isolated battery (shim-seeded entries,
  FFI-built chain) — identical check count, honest MODE print. The law and
  the artifact coupling are documented in both canary docstrings.
- Ghost-divergence repair: the Rust binary honors RUNE_MEMORY_DIR/RUNE_HOME
  first (main.rs ledger_path) while the deployed shim resolves expanduser
  (HOME); §5 binary envs now strip RUNE_* so both surfaces resolve the same
  ledger in every environment.
- §10 pins RUNE_MEMORY_DIR alongside RUNE_HOME (memory_dir() honors the
  former first).

Gate debt exposed by 18141's failure profile (pre-existing, now closed):
- config/umbrellas.json: ffi_query_memory_smoke mapped to the memory
  umbrella (owed since 43e7f5f; umbrella gate green in push mode).
- requirements-ci.txt regenerated: markupsafe 3.0.3 -> 3.0.4,
  websockets 17.1 -> 17.2 (checker green).
- lint-baseline.json BLE001 347 -> 346: claim_audit silent handlers
  repaired with exc_info logging + reasoned noqa (deliberate-silence sites
  only); canary battery handlers carry the canary-law reason. S110 holds.
- type ratchet green at 107/454 (local pyright pinned to CI's 1.1.414;
  isinstance runtime-shape checks carry scoped ignores; ep_loader imports
  annotated as runtime-by-design).
- store/scaffold + spine snapshots travel with the change (as at 2ac2d0a);
  supersession ledger canary rows updated to the true final counts.

Full local wall: 5 parity harnesses 33+24+13+16+17 all green; both canaries
green in LIVE and ISOLATED modes; consumer import chains (iteration,
testing_mode, rune_operator, contracts, curator + deployed shims) green;
live chain intact through the deployed shim; pytest suite 2250 passed/0.
2026-10-04 00:32:28 +01:00
Nick 497d14a449 ci: register canary_audit_ledger in the canary suite loop
The henxels contract requires every src/rune/canary_*.py on disk to be
registered in the CI canary loop (gap #138's rule is per-canary). The
slice-2 canary (port 1, canary pair carrier, 34 checks) was added with
the port but its registration was missed — the contract test caught it
live. Registered in the slice-1 position (after canary_query_memory,
the strangler-adjacent grouping).
2026-10-03 16:25:52 +01:00
Nick 2e68b6b054 phase-g slice 2: supersession ledger + registry remap + shared deletion
- plans/2026-10-03-phaseG-evidence-provenance-supersession.md: the
  slice-level ledger — port table (6/6, commits + parity + consumer
  proof), 12-step doctrine discharge, vintage-merge disclosures
  (symmetry guard, scan_text repair, port-1 sequence break), consumer
  map, live-data rules honored.
- shared/audit_ledger.py DELETED (superseded; zero importers verified
  by sweep; deployed shim + Rust core proven live before removal —
  language-governance rule ⑥).
- coverage registry: audit_ledger removed from SHARED_D.
- regression gate: exit 0 pre- and post-deletion.
2026-10-03 16:20:08 +01:00
Nick 969e39f6a3 phase-g slice 2 port 6: claim_audit decision skeleton -> rust core (parity 17/17)
- claims.rs: the verify() verdict tree (comparative-no-anchor gate,
  ledger-hits PASS, DEGRADE tails), report aggregation (total/by_verdict/
  by_type with '?' defaults), the clean fold, evidence-term stoplist.
  Regex engine + I/O stay Python (zero-dep crate law); timestamps Python.
- ep_ca_verify_gate / ep_ca_audit_clean / ep_ca_report_aggregate /
  ep_ca_evidence_terms FFI + ep_loader wrappers (restype law).
- claim_audit.py is now a shim, BOTH copies (src/rune + deployed):
  verify() delegates the tree + term filtering; report() / clean fold
  delegate; regex machinery (scan/reframe/degrade/EVIDENCE_TAG) + journal
  I/O + runner stay Python.
- VINTAGE MERGE (disclosed): the deployed scripts vintage carried the
  SYMMETRY GUARD (research_lens.symmetry_check, 2026-08-15) which src/rune
  lacked — preserved (load-bearing behavior, feature-preserving merge).
- REPAIRED a dead consumer: rune_tui.py:3752 calls claim_audit.scan_text()
  expecting {degraded, claims, clean} — NO implementation existed in either
  vintage (silent try/except death since the call landed). scan_text() is
  now a first-class surface; verified live (returns the exact dict shape).
- parity_claim.py: 17-case battery — verify tree 12 (flags derived the
  same way the original derives them: original verify() vs gate FFI),
  report aggregate over a real-shaped journal (incl. '?' fallback rows),
  clean fold 3, stoplist filter — 17/17 GREEN pre-switch vs original.
- deployed shim live: real journal (1127 rows, 851 DEGRADE / 276 PASS)
  aggregates through the Rust core; TUI backend trio imports clean.
2026-10-03 16:19:12 +01:00
Nick 273267763e phase-g slice 2 port 5: behavior decision math -> rust core (parity 16/16)
- habitmath.rs: the reinforce decision math — habit (activation/success/
  failure counting, confidence min(1.0, 0.5 + rate*0.5), candidate→active
  promotion at min_activations, neutral still counts activation) and
  correction (success +0.1 / failure −0.3 with ceilings/floors, evidence
  counting, active/archived at 0.3). STORAGE STAYS PYTHON — SQLite I/O
  and regex matching remain in the module (the crate is zero-dep by
  design; hand-rolling SQLite/regex would be hubris, not hardening).
  No clock in the core — timestamps pinned by the shim.
- ep_habit_reinforce / ep_correction_reinforce FFI + ep_loader wrappers.
- behavior.py is now a shim: reinforce() fetches the row, delegates the
  math, writes the update back; add/match/list/stats/CLI unchanged.
- parity_behavior.py: 12 decision-math cases (outcome × state × boundary:
  promotion-at-min, floors, ceilings, neutral, archive-threshold) with
  SEMANTIC comparison (parse-then-compare — the 1.0-vs-1 render delta is
  serialization, not semantics) + full-stack live-db scenarios both sides
  (promotion observed, activations logged) — 16/16 GREEN.
- deployed ~/.config/opencode/scripts/behavior.py upgraded to the same
  shim shape (vintage upgrade per contract §2) pointing at the LIVE db
  (~/.config/opencode/memory/behavior.db, 2 corrections / 1 habit,
  untouched); read paths verified against the live db (stats + match).
  Pre-deploy backup: /tmp/behavior_deployed_pre_port5.bak.
2026-10-03 16:01:52 +01:00
Nick 8973caae77 phase-g slice 2 port 4: behaviour_ledger -> rust core (parity 13/13)
- behaviour.rs: the full state machine — define (normalize/dup-check),
  transitions with history append (at/from/to/kind/detail[:200]/gen),
  record_auto_verification (ceiling AUTO_VERIFIED), record_real_world
  (USER_CONFIRMED iff passed && confirmed_by==user), accept_limitation,
  invalidate_for_change (CONFIRMED_STATES-only, deps ∩ closure + owner
  hit, default reason 'structural change touched: [...]' Python list
  repr sorted, reasons override, unrelated preserved), retest_history,
  outstanding (TEST_FAILED-first sort), summary. Timestamps passed IN
  (now) — no clock in the core, parity deterministic.
- FFI: doc-in/doc-out convention with {ok, err} envelope — err raises
  ValueError with the exact Python message (duplicate/unknown-behaviour/
  unknown-state all byte-equal). Read paths: ep_bl_get/retest_history/
  outstanding/summary.
- behaviour_ledger.py is now a shim: file glue (load/save) + constants
  stay Python; every state-machine op delegates via the doc envelope.
- parity_behaviour.py: full-scenario battery (3 behaviours, auto+real-
  world+other-confirm, selective invalidation with reasons override +
  owner hit, filters, error messages, save render indent=1 sort_keys)
  — byte-equal docs, 13/13 GREEN pre-switch (vs original) and
  post-switch (round-trip).
- consumers verified: iteration.py:23, testing_mode.py:19 (constants at
  import), rune_tui.py:3152 (lazy) — import chains green.
2026-10-03 15:59:14 +01:00
Nick 3450a1cb83 phase-g slice 2 port 3: evidence_probe -> rust core (parity 24/24)
- probe.rs: validate_probe schema gate + count_probes budget count,
  byte-parity on every error string incl. Python repr rendering:
  {status!r} single-quote escaping, sorted-list ['a', 'b'] shapes,
  non-string-status double-error (field error + membership error —
  Python runs the membership check on the RAW value), GROUNDED source
  >= 8, extra-fields sorted, field-order iteration.
- ep_validate_probe + ep_count_probes FFI; ep_loader wrappers with
  restype declarations (the segfault law holds: every new ep_* symbol
  gets argtypes+restype).
- evidence_probe.py is now a shim: validate_probe + count_probes
  delegate to the Rust core; run_probe orchestration (runner subprocess,
  exception semantics, budget enforcement, record_probe) stays Python.
- parity_probe.py: 24-case battery (17 schema-gate shapes incl. the
  non-string status trap, 3 non-dict top levels, 4 count_probes file
  shapes) byte-equal through FFI; GREEN pre-switch (vs original) and
  post-switch (round-trip).
- consumers verified: rune_operator.py:581/603 (run_probe), failure_
  recovery.py (budget map), research_stage_executor.py:312 (run_probe +
  MAX_PROBES_PER_TASK + ProbeBudgetExceeded) — all 23 sibling tests
  green through the shim.
2026-10-03 15:54:38 +01:00
Nick 9cb93e6573 phase-g slice 2 port 2: verification_layer -> rust core (parity 33/33)
- verification.rs: all seven verifiers + dispatch, to_dict shape
  byte-parity (key order passed/verifier_type/evidence_refs/failures/
  retryable/escalation_reason), Python str() rendering rules (bool ->
  True/False in evidence_refs — exit_code=False renders False not 0),
  Python == 0 semantics (bool/int cross-compare), default-value guards.
- ep_verify FFI + ep_loader.verify wrapper; verification_layer.py is now
  a shim (dataclass stays Python — rune_operator:323 constructs it
  directly; verify_* delegate to the Rust core and reconstruct).
- parity_verification.py: 33-case battery (7 verifiers, pass/fail/edge/
  default/unknown) byte-equal through the FFI, incl. CLI indent=2 render;
  GREEN pre-switch (vs original) and post-switch (round-trip).
- canaries extended: python 34/34 (deployed shim + port-2 sections),
  rust 28/28 (ep_verify battery + key-order parity).
- consumers: contracts.py dynamic __import__, canary_native MODULES list,
  full_audit file list — all resolve the shim; import surface unchanged.
2026-10-03 15:51:48 +01:00
Nick 4b0d1d24d2 phase-g slice 2 port 1: audit_ledger canary pair (deployed-shim 28/28, rust 23/23)
- Python canary pins the deployed shim surface to the frozen contract:
  live-path LEDGER_FILE, drift-key default, 3-key dict verify_chain,
  ascending serving order (observed), canary append through the shim
  (live entry 4242, chain intact post-append), rust CLI parity shapes.
- Rust canary drives the compiled surfaces: binary CLI shapes (verify/
  list/log-promotion + argparse error shapes incl. --limit=N form), FFI
  contract via ep_loader (5 fns), fresh-ledger append+verify+get, and the
  live-chain drift-key rule (23+ entries, chain intact).
- Sequence-break disclosure: the shim was deployed before these canaries
  existed; both now green against the deployed state — recorded on #2449.
2026-10-03 15:46:15 +01:00
Nick 114c394741 phase-g slice 2: evidence_provenance rust crate + ctypes loader (audit_ledger parity-complete, shadow 18/18) 2026-10-03 02:41:21 +01:00
Nick 7c68e41a63 phase-g slice 2: freeze evidence-provenance contract (drift-key live-ledger resolution, authority = src/rune vintage, port order set) 2026-10-02 23:12:22 +01:00
Nick 2ac2d0aefd refactor(query-memory): supersede old Python implementation with Rust cdylib (strangler steps 9-12)
Old src/rune/query_memory.py deleted (zero importers verified by sweep).
Coverage-audit registry remapped: shared/query_memory.py (ctypes shim) is
canonical; query_memory out of SRC_A/SHARED_D, into SHARED_B. Supersession
ledger written to plans/. Steps 1-11 evidence in ledger + issue #2448.
2026-10-02 20:52:40 +01:00
Nick 6f44a87894 feat(phase-g): authority-switch shim — deployed query_memory.py is now a ctypes shim over the Rust cdylib
Step 8 of the 12-step strangler doctrine: the deployed in-process consumers
(memory_store.py, growth_simulator.py) now run the Rust classification core
in-process via qm_loader.py. Full historical Python class API preserved,
including the query-history snapshot attribute (loaded at construction,
appended on record — canary check 2 reads it).

Post-switch verification wall ALL GREEN: FFI smoke 41/41, parity harness
ALL GREEN (stdout + history.jsonl + topic_stats.json byte-parity), Python
canary 12/12, Rust canary 27/27, both consumers verified live against the
deployed shim.
2026-10-02 20:24:26 +01:00
Nick 485623dbc4 feat(query-memory): add qm_topic_stats + qm_dead_weight FFI exports and loader bindings
Closes the FFI surface gap: Python get_topic_stats/get_dead_weight_topics
now have FFI exports wrapping the existing Rust topic_stats/dead_weight
impls via TopicStats::to_json. Loader extended with the two bindings
returning the pinned Python shapes (ordered dict / filtered name list).
FFI-gap parity probe: exact-equal vs deployed Python. Verification wall
green: smoke 41/41, parity ALL GREEN, canaries 12/12 + 27/27.
2026-10-02 19:49:19 +01:00
Nick 43e7f5f7d2 feat(query-memory): FFI exports for in-process Python consumption of the Rust core
Seven #[no_mangle] extern "C" exports on lib.rs (qm_new, qm_with_dir, qm_rate,
qm_record, qm_stats, qm_history, qm_health) so the deployed in-process consumer
(~/.config/opencode/scripts/memory_store.py adaptive_exploration_rate) can load
the compiled Rust core via cdylib instead of the Python implementation.

- ctypes loader qm_loader.py mirrors the deployed consumer's exact call shape
- FFI smoke src/rune/ffi_query_memory_smoke.py: 41/41 PASS against an isolated
  store — construction-ordering fixes: seed-then-construct (both implementations
  snapshot the store at construction), post-record health parity
- verification post-FFI: parity harness ALL GREEN, Python canary 12/12,
  Rust canary 27/27 (src/rust/query_memory/canary_rust.py)

Phase G strangler slice 1, step toward shadow comparison + authority switch.
2026-10-02 17:59:50 +01:00
Nick 54d2db97b0 refactor(query-memory): split core into lib.rs — rlib + cdylib for in-process Python loading
Phase G slice 1 (retrieval-index) strangler: the whole implementation lived
in main.rs (prior checkpoint's lib.rs claim was wrong — disk wins). Core
(QueryMemory, classification, serialization) now in src/lib.rs; main.rs is
a thin CLI shell over it. crate-type = [rlib, cdylib] so the deployed mimo
scripts (memory_store.py, growth_simulator.py) can load the compiled core
in-process instead of paying interpreter-subprocess costs.

Verified post-refactor: parity harness ALL GREEN (stdout + history.jsonl +
topic_stats.json byte-parity), Python canary 12/12 exit 0, Rust canary
27/27 exit 0 against rebuilt release binary.
2026-10-02 17:25:10 +01:00
Nick 523a859656 test(query-memory): add Rust canary proving C1-C8 against compiled binary
C9 canary for the Phase G strangler slice 1 (retrieval-index): 27 checks
in the same PASS/FAIL counter discipline as the Python canary, driving
target/release/query_memory with isolated HOME per section. Covers CLI
surface (C8), storage layout + cross-compat (C1), classification
tie-break (C2), adaptive rate clamp + threshold (C3), dead weight (C4),
corruption tolerance (C5), timestamp shape (C6), truncation (C7).

Fixes one probe defect found live: the C5 history-corruption probe
expected stats to rebuild from history.jsonl, but get_topic_stats reads
ONLY topic_stats.json in BOTH implementations (verified byte-identical
side-by-side) — probe corrected to assert the shared real behaviour.

Observed: 27/27 PASS, exit 0.
2026-10-02 16:11:54 +01:00
Nick 9b765852b8 feat(retrieval-index): Rust strangler candidate — Phase G slice 1
Zero-dependency Rust port of src/rune/query_memory.py (retrieval-index,
RUST-candidate per Phase E admission gate):

- src/rust/query_memory/ — first Rust workspace in the repo
- json_min.rs: hand-rolled JSON parser/serializer mirroring json.dumps
  (preserve-order objects, ensure_ascii escaping, surrogate pairs,
  indent-2 stats shape, f64 shortest-repr parity)
- main.rs: classification core + CLI (record/stats/rate/health),
  TOPIC_KEYWORDS in source insertion order, first-max tie-break,
  adaptive exploration rate (+-0.05 at >=10 queries), UTC isoformat
  parity incl. microsecond-omission law
- parity_test.py: cross-language conformance harness — identical
  command sequence through both implementations in isolated HOMEs,
  byte-parity on history.jsonl (timestamps masked) + topic_stats.json

Observed: cargo build clean; parity harness ALL GREEN (stdout +
both store files) across 24-command sequence covering unicode,
first-max tie-breaks, special chars, adaptive-threshold crossing.

Refs: /tmp/phaseG_retrieval_index_contract.md (C1-C10)
2026-10-02 15:40:12 +01:00
Nick f1e63b703e fix(token_optimizer): include content in CacheEntry.to_dict — closes #770 cache-loss defect
Pre-fix to_dict omitted the required field content, so _load's
CacheEntry(**entry_data) raised TypeError on every reload and the
blanket except silently discarded the whole cache on every restart.

Frozen regression contract tests/test_token_optimizer_cache.py
(3 tests: save_persists_content, roundtrip_restores_content,
corrupt_storage_degrades_to_empty_cache). Full suite 2250/0.
2026-09-30 12:18:35 +01:00
Nick 6d40f94f18 fix(test): hermetic group-liveness probe in TTY no-accumulation test
The test measured 'group-kill leaves nothing behind' via a global
'pgrep -x -c sleep' before/after delta — any unrelated system sleep
starting mid-loop on the runner (mount helpers, cron helpers) landed
in the delta and false-failed. CI run 13255 failed with exactly
'1 NEW sleep survivor' from an ambient sleep, reproduced locally
(/tmp/ambient_proof.mjs: measured_delta 1 from one spawned ambient
sleep).

Fix: probe OUR OWN process groups with kill(-pgid, 0) after the kill
sequence — a live group answers, a fully-reaped group throws ESRCH.
Hermetic to ambient load; negative control (/tmp/hermetic_catch.mjs)
confirms the probe still reports a survivor when the kill is skipped,
so the test's protective intent is preserved.

Verified: test green x3 locally, whole TTY contract file 8/8, fixed
test green under deliberate ambient-sleep bombardment.
2026-09-30 02:07:30 +01:00
Nick 7a869476be fix(forge_client): terminate _request with explicit raise — clears 5 pyright Optional warnings at call sites
_request never returns None (every attempt returns or raises), but the
implicit exhausted-loop path made pyright infer Optional at all 5 call
sites' .json() accesses, pushing the warning ratchet 454→459. The
terminal AssertionError documents exhaustiveness at the source instead
of 5 duplicated guards. Local pyright: errors 109, warnings 447 (≤454).
2026-09-30 01:41:30 +01:00
Nick e5292cce7b feat(forge): add httpx retry/timeout wrapper for transient-failure resilience
- _request() helper in forge_client.py: retry on connect-phase transport
  errors and idempotent-method 5xx/429, exponential backoff, 30s timeout
  default preserved per attempt; create_issue (POST) and list-style GETs
  route through it per retry-where-noise semantics
- frozen contract tests/test_forge_client_resilience.py (10 tests,
  written red-first per the bug-fix contract)
- legacy FakeResponse fixtures updated with status_code=200 (wrapper
  reads status_code for the GET-5xx retry gate)
- httpx wrapper spec: docs/rebase-language-rubric.md W4 named improvement
- model_routing.py:99 probe deliberately EXCLUDED (failure is the
  signal, not noise — one-shot design is the contract)
2026-09-30 01:10:22 +01:00
conductor 396ae120dc chore(release): auto-bump version to 0.89.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-30 00:10:51 +01:00
conductor 01b57297b8 chore(release): auto-bump version to 0.88.4 — earned by #2269 (Wave 0: rebase tranche inventory, issue factory, Rust interop proof)
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/2269
2026-09-30 00:10:04 +01:00
conductor 497db5a144 Merge pull request 'Wave 0: rebase tranche inventory, issue factory, Rust interop proof' (#2269) from tranche-r1-wave-0 into master 2026-09-30 00:09:13 +01:00
Nick 9425ccc1f4 docs(rebase): record executed kill-test dispositions for orjson + inventory items
orjson: microbench on live 3.5MB knowledge.json — load 1.68x (bar 2x FAIL),
dump+decode 1.71x (bar 5x FAIL) => KEEP-PYTHON, no dependency declared.
Inventory correction: DONE via regen (5bc837c); W2 drift claim verified false.
2026-09-30 00:00:57 +01:00
Nick 3ed547f86f chore(deps): re-resolve ci lock after upstream platformdirs 4.12.2 release
Dep-lock drift ratchet red on CI run 13207. Documented regen path:
uv pip compile --upgrade. Only the pin moved.
2026-09-29 23:55:18 +01:00
Nick 5bc837c232 fix(rebase): regenerate inventory — drop deleted complexity_scorer + 12 stale files, add 49 current (rubric named correction) 2026-09-29 23:23:58 +01:00
Nick 08bc1a6b62 feat(native): add JSONL streaming reader for native units (W7 contract gap)
stream_native(): per-line json.loads over subprocess stdout, progress
events yielded live, terminal envelope validated as the FINAL line,
watchdog timeout with adapter-kill vs unit-death distinction, concurrent
stderr drain. Generator yields event dicts then one terminal NativeResult
(gaining an events field). Malformed interior lines skipped and counted;
a malformed final line invalidates the envelope, so ok=False while partial
progress stays available. Tests use a Python subprocess as the fake
native unit — no Rust build dependency in the test path (house style
from tests/test_native_interop_proof.py).
2026-09-29 23:08:32 +01:00
Nick 201ca05b56 docs(rebase): land FINAL Wave 1 language-fit rubric (40 clusters, 0 recodes) 2026-09-29 23:08:32 +01:00
Nick 5cd90f2104 fix(ci): exempt docs/rebase/** from top-level-docs henxel
The top-level docs henxel (in: ./docs/*) reaches subdirectories —
run 13170 held docs/rebase/rebase-inventory.json and
docs/rebase-wave-coverage.json after the c8bc454 relocation. The
./docs/v1/** precedent in the same except list shows the rule's
recursive reach; the Wave 0 JSON artifacts are machine-generated
data, not docs, and follow the same precedent shape.
2026-09-29 15:09:23 +01:00
Nick c8bc454e33 fix(lint): relocate Wave 0 JSON artifacts to docs/rebase/
The henxels allowed_filetypes rule polices top-level ./docs/* for
markdown/yaml only; machine-generated JSON artifacts are not docs.
Moving rebase-inventory.json and rebase-wave-coverage.json under
docs/rebase/ (outside the single-segment glob) rather than widening
the gate's except list to fit new files. All consumer references
updated (rebase_inventory.py, rebase_issue_factory.py, wave0 report).
2026-09-29 14:47:38 +01:00
Nick b81edb2029 fix(tests): pin event-bus journal into tmp in terminalization helper test
test_terminalization_helper_single_path scanned the ambient
event_bus.JOURNAL_DIR — an import-time-frozen constant resolved via
memory_env.memory_dir(), whose RUNE_HOME defaults to the hardcoded
/mnt/workdrive/rune. On this machine that path coincidentally IS the
repo, so the test passed locally; on the CI runner the default path is
unwritable, emit()'s deliberate degradation swallowed the write, and the
scan found no task.outcome — the sole unit red of 2,219 on run 13155.

Fix pins JOURNAL_DIR/EVENT_LOG to the test's TemporaryDirectory with
addCleanup restore — same isolation-seam family as the hot-store law
(ambient state must never be read by tests). Red-first repro'd with a
hostile RUNE_HOME locally: exact CI signature ([] is not true) before
the pin, green after.
2026-09-29 14:11:30 +01:00
Nick 87a890dce3 fix(ci): regenerate requirements-ci.txt — dep-lock drift from 0.88.3 pyproject bump
Master's auto-bump to 0.88.3 changed pyproject.toml without regenerating
the CI dep lock; the merge carried both sides in (coverage 7.16.1 ->
7.16.2, nodeenv, platformdirs). Canonical regen per
scripts/check_dep_lock.py's documented command, plus --upgrade: uv pip
compile to an EXISTING output file preserves its stale pins as resolver
preferences, so a plain in-place regen silently keeps the old pins.
Measured: same uv, same invocation, -o fresh-path resolves 7.16.2 while
-o requirements-ci.txt kept 7.16.1 until --upgrade was added.
2026-09-29 14:11:30 +01:00
Nick a0821be8a5 fix(rebase): narrow inventory signature-unparse handlers to explicit exception types
Replaces two blind except-Exception-pass handlers in _arg_str/_signature
with (AttributeError, RecursionError, ValueError) and documents the
degraded-metadata contract: a pathological annotation node must never
abort the whole inventory walk.
2026-09-29 13:29:53 +01:00
Nick cb137bd37a Merge remote-tracking branch 'gitea/master' into tranche-r1-wave-0
# Conflicts:
#	.gitignore
#	config/umbrellas.json
2026-09-29 13:26:54 +01:00
Nick 72afb1a55a feat(rebase): wave 0 — whole-project inventory, per-function issue factory, Rust interop proof unit
Wave 0 of the approved language-rebasing tranche (R1):

- scripts/rebase_inventory.py: AST-based whole-project inventory →
  docs/rebase-inventory.json (1,852 true units = 1,030 top-level fns
  + 822 methods; module-level walk skips defs; class-method leak fixed)
- docs/rebase-wave-coverage.json: waves 2–7 coverage {239,251,129,544,415,274}
- scripts/rebase_issue_factory.py: one-issue-per-unit Gitea factory,
  dry-run default, deterministic titles + dedup, --limit defaults to
  unlimited for create; 1,852 issues created across waves 2–7
- interop/rebase-proof + shared/native_units.py + tests: std-only Rust
  proof unit for the §5 native-interop contract (argv→JSON, --version,
  exit 2 on failure); thin typed Python adapter; 8/8 tests pass with
  corrected ok-semantics (ok = exit 0 AND not timed out)
- config/umbrellas.json: native_units registered in capability umbrella
  (manifest law — the map travels with the change)
- docs/rebase-wave0-report.md: Wave 0 close-out report
2026-09-29 12:47:34 +01:00
conductor c9095f5af5 chore(release): auto-bump version to 0.88.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-23 18:16:50 +01:00
conductor b5422f0772 chore(release): auto-bump version to 0.88.3 — earned by #412 (fix(scaffolding): T22 — connect the MiMo edge + apply the meaning gate on the na)
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/412
2026-09-23 18:16:42 +01:00
conductor da36b99184 Merge pull request 'fix(scaffolding): T22 — connect the MiMo edge + apply the meaning gate on the native path' (#412) from t22-scaffolding-default-path into master 2026-09-23 18:16:35 +01:00
conductor 873d0025ad fix(scaffolding): T22 — cover the new paths; two defects the coverage tests found
Self-review against the CI gates surfaced two genuine defects in the first
commit. Changed-lines coverage is now 85% (floor 70):

1. The bridge projection read the WRONG STORE. `_active_construction_lines`
   built `ConstructionInterface` without `project_store`, so it fell back to
   the interface default instead of the configured construction store — the
   `RUNE_CONSTRUCTION_STORE` override was ignored, so a redirected
   deployment would have projected another project's construction state.
   Fixed: pass `_construction_store()` + the project id.

2. LOST UPDATE between the intent-layer note and the main activation path.
   The degrade path called `_construction_note()`, which does its own
   load/save; the normal path then saved its own stale `doc`, silently
   clobbering the note. Fixed: append the note to the SAME in-memory doc
   (one doc, one save), so the failure is recorded and survives.

New coverage: active-contract projection + capsule rendering + read-only
guarantee; store-resolution override; intent-layer-unavailable degrade path.

Note: the stale TUI banner (`Rune v0.87.6` vs `__version__` 0.88.0) that
this commit also fixed on the old base was fixed independently on master by
the 0.88.2 release bump, so the rebase takes master's banner — nothing to
carry here.
2026-09-23 18:10:07 +01:00
conductor a9ce967557 chore(ci): refresh dependency lock — upstream patch bumps unblock #150 gate
`scripts/check_dep_lock.py` (CI step "#150: deps come ONLY from the pinned
lock") regenerates the lock into a temp path and compares. Three transitive
deps released upstream since the lock was generated, so the check fails for
ANY branch today, independent of the T22 change:

  diff-cover  10.5.1  -> 10.6.0
  multidict   6.9.0   -> 6.9.1
  platformdirs 4.11.11 -> 4.11.12

Regenerated with the exact command the gate instructs. Note: compiling with
`-o requirements-ci.txt` is a NO-OP for the pins (uv prefers the existing
output file's versions); the fresh resolution must be written to a new path
and moved into place, which is what the checker itself does.

No dependency declarations changed (pyproject.toml untouched) — pins only.
2026-09-23 18:09:26 +01:00
conductor eb3a2c681c fix(scaffolding): T22 — connect the MiMo edge and apply the meaning gate on the native path
Reality-wins audit of the accepted Scaffolding V1 system against the live
repo/runtime found three genuine wiring defects (no architecture invented):

1. The MiMo edge was DEAD. `~/.mimocode/hooks/builder-bridge.ts` hardcoded
   the ephemeral interpreter `/tmp/ci-venv/bin/python`, which no longer
   exists, so every bridge call failed and the hook silently degraded —
   MiMo received NO Rune Core preflight at all (zero fire-log lines). The
   hook was also UNVERSIONED (no canonical copy), which is how it rotted.
   Repair: canonical source `config/mimo-hooks/builder-bridge.ts` with
   durable interpreter resolution (env override > repo venv > user venv >
   /usr/bin/python3), installed + hash-verified + drift-checked by
   `install_mimo_hook.py`, which now manages a hook SET rather than one file.

2. The native path skipped the canonical meaning layer. `construction_intent`
   (the §17J/§17L/§17M intent resolver: interrogative structure, negation,
   request modals, referent/proposal resolution) existed but was imported
   ONLY by tests. `rune_operator._construction_activation` went straight to
   `classify_mode`, so a pure question carrying a target reference
   ("explain how this module works") classified EXISTING_PROJECT_CHANGE and
   recorded a construction activation for a discussion turn. Repair: the
   intent layer now resolves MEANING before the ONE classifier decides mode
   (the boundary the intent module documents). DISCUSSION maps to
   NOT_CONSTRUCTION — the canonical vocabulary is unchanged, an explicit
   user override (/scaffold-mode) outranks the gate, and an unavailable
   intent layer preserves the previous behaviour exactly rather than
   silently changing governance.

3. The shared Construction Context reported a TERMINAL contract as active.
   `construction_context()` selected on `status == "open"` alone, so 8
   contracts with phase `abandoned` (terminal) were reported as the active
   construction. Repair: terminal phases are now DERIVED once from the
   canonical transition table (`scaffold.TERMINAL_PHASES`), and a contract
   is active only when its phase is non-terminal.

Also: the bridge capsule now surfaces the active Scaffolding construction
(contract/phase/scope/obligations/next-action) to the builder as a
read-only projection of the shared Construction Context — the MiMo edge of
state Scaffolding already owns. Inject-nothing is preserved when no
contract is active; the projection never mutates and degrades to [].

Spine projection refreshed (import graph legitimately gained importers).

Honest boundary: no byte-level enforcement point exists. `tool_executor.py`
never consults construction state, so the 19A invariant gates only the
/build-start route. 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.

Verification: 511 passed across the construction/operator/bridge/hook/
structural/parity suites; native-default matrix 12/12; real operator-path
proof with fresh-process readback (EXISTING_PROJECT_CHANGE activated,
question -> NOT_CONSTRUCTION, PROJECT_BUILD bootstraps scaffold + registry);
installed hook invoked directly -> capsule injected + fire log written.
LOCAL_MODEL_COMPUTE_EXECUTED = NO.
2026-09-23 18:09:26 +01:00
conductor 1bc1cdeaf0 chore(release): auto-bump version to 0.88.2 — earned by #408 (auto-label: stop re-adding managed labels to closed issues (#407))
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/408
2026-09-20 11:17:13 +01:00
conductor 31565844d7 Merge pull request 'auto-label: stop re-adding managed labels to closed issues (#407)' (#408) from fix-407-autolabel into master 2026-09-20 11:17:04 +01:00
conductor c08b211f0e auto-label: stop re-adding managed labels to closed issues; keyword fallback filing-time only
Root cause of #407: the labels job re-ran its full inference on every
issues event, including 'labeled' events on closed issues. Any label
mutation (human cleanup, bot churn) re-triggered inference, and the
spine-authoritative mapping re-added the type label to the closed issue
(#324: kind bug, closed FIXED_VERIFIED, bug label re-added within
seconds of every removal — making the reconciliation checker
permanently red for that issue).

Fix:
- Closed issues are skipped entirely; post-close label curation
  belongs to humans and the conductor. Labels are filing-time
  metadata.
- The pre-template keyword fallback now runs only on the 'opened'
  action, per its documented scope; edits and label events on
  spine-less issues no longer override human curation.
- Priority gate also skips closed issues (hygiene, same class).

Version intent: patch (workflow behavior fix, no API change).
Verification after merge: remove 'bug' from closed #324, wait >60s
across any label event, confirm no re-add; reconciliation checker
green for #324.
2026-09-20 11:11:30 +01:00
conductor 46c5032208 chore(release): auto-bump version to 0.88.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-19 19:21:40 +01:00
conductor 92584daeef chore(release): auto-bump version to 0.88.1 — earned by #355 (fix(tui): sync banner version surface to 0.88.0)
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/355
2026-09-19 19:21:33 +01:00
conductor bb5e243c88 Merge pull request 'fix(tui): sync banner version surface to 0.88.0' (#355) from fix-tui-banner-surface into master 2026-09-19 19:21:28 +01:00
conductor ac936a8ba9 fix(tui): sync banner version surface to 0.88.0
The converged-release bump updated __init__/header but missed the TUI
banner string at rune_tui.py:1888; the banner-surface test correctly
failed master CI. The version-surfaces checker watches the header
comment only — banner added to the gap list for the bump workflow.
2026-09-19 19:13:16 +01:00
conductor e6c8c5444d chore(release): auto-bump version to 0.88.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-19 18:34:33 +01:00
conductor 11270cb06a chore(release): auto-bump version to 0.87.6 — earned by #354 (await-intent: refresh refs before op-type inference (M10 correction))
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/354
2026-09-19 18:34:25 +01:00
conductor f8a568714b Merge pull request 'await-intent: refresh refs before op-type inference (M10 correction)' (#354) from m10-derive-refresh into master 2026-09-19 18:34:19 +01:00
conductor c3f81e60ec Merge branch 'master' into m10-derive-refresh 2026-09-19 18:28:51 +01:00
conductor efb99d4682 await-intent: refresh remote refs before op-type inference (M10 correction)
A just-merged master commit was invisible to branch --contains until refs
were fetched: the local clone's stale master outranked repo truth and a
master_ci operation classified as branch_ci. derive_op_type now refreshes
remote-tracking refs (bounded by GIT_TIMEOUT, fail-open) and containment
checks all refs. Generic fix; no SHA special-casing.
2026-09-19 18:23:17 +01:00
conductor 489718ab1b chore(release): auto-bump version to 0.87.5 — earned by #353 (R13/M9 PR-flow live proof (harmless docs marker))
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/353
2026-09-19 18:02:51 +01:00
conductor 9db580ac6d Merge pull request 'R13/M9 PR-flow live proof (harmless docs marker)' (#353) from r13-m9-pr-proof into master 2026-09-19 18:02:46 +01:00
conductor 80ed978fbf docs: R13/M9 PR-flow live proof marker (harmless, no code change) 2026-09-19 17:57:33 +01:00
conductor 3f119e50d8 chore(release): auto-bump version to 0.87.4 — earned by #352 (Topology: custodian defaults to PC-internal Gitea endpoint)
Promotion record: http://192.168.1.156:3000/conductor/rune/pulls/352
2026-09-19 17:46:52 +01:00
conductor 517379313b Merge pull request 'Topology: custodian defaults to PC-internal Gitea endpoint' (#352) from gitea-internal-endpoint into master 2026-09-19 17:46:42 +01:00
conductor 643f9aa9d1 custodian: default Gitea endpoint = PC-internal (127.0.0.1:3000)
Topology reconciliation 2026-09-19: Gitea migrated Pi->PC. PC automation
uses the internal endpoint (no public DNS / Pi proxy round trip). External
endpoint unchanged for humans: https://gitea.thingscouldgetdicey.online
(Pi Caddy -> PC:3000). GITEA_URL env still overrides.
2026-09-19 17:40:59 +01:00
conductor 61a5082633 chore(release): auto-bump version to 0.87.3 — earned by #351 (Auto-label: declared kind is authoritative for epics; absorption is the filing s)
Promotion record: conductor/rune#351
2026-09-19 16:50:54 +01:00
conductor 9121bbd8b0 Merge pull request 'Auto-label: declared kind is authoritative for epics; absorption is the filing standard' (#351) from auto-label-epic-kinds into master 2026-09-19 16:49:48 +01:00
conductor cba9045982 auto-label: any declared kind is authoritative; absorption is the filing standard
Two defects in one pass:

1. The spine-parsing fix from d96b89b only matched kind: bug|feature|research.
   Epic spines (kind: epic) fell through to the keyword heuristic, which
   re-added 'bug' to epics whose bodies mention fixes (observed live on
   #181/#284 — label respawned within 30s of removal). Now ANY declared kind
   is authoritative: recognized kinds map to labels, any other declared kind
   (epic, ...) gets NO type label and NO guessing. Keywords only apply to
   pre-template issues with no spine.

2. Standard: one open note per idea. Parked ideas live as absorbed checklist
   items in their parent epic; standalone issues only for live work. Applied
   today: #175 absorbed #176-#179; #181 absorbed #182/#183 + watchlist
   #164-#166. Reconciliation artifact updated for all nine closures.

Custodian create_issue and AGENTS.md now carry the standard so future
filing follows it.
2026-09-19 16:42:55 +01:00
conductor 0c92f482a7 chore(release): auto-bump version to 0.87.2 — earned by #350 (Templates: plain-English heading carries the issue summary)
Promotion record: conductor/rune#350
2026-09-19 16:11:27 +01:00
conductor 3120e8ae08 Merge pull request 'Templates: plain-English heading carries the issue summary' (#350) from issue-templates-heading into master 2026-09-19 16:08:34 +01:00
conductor ea3c20e205 templates: heading IS the summary — drop the meta-label
The heading should not announce itself as 'plain English'; it should simply
be the summary in everyday words. Meta-commentary removed from the heading
line, instructions preserved in the comment block.
2026-09-19 16:01:05 +01:00
conductor a93327d107 templates: plain-English heading carries the issue summary
The section header itself must restate the issue title in everyday words,
so a scan of headings alone tells the story of the board. Applied to all
three templates (web-UI + canonical copies) and retrofitted onto all 30
live issues.
2026-09-19 15:41:15 +01:00
conductor e08625f629 chore(release): auto-bump version to 0.87.1 — earned by #349 (Issue templates: parsable spine, plain-English-first, version intent; auto-label)
Promotion record: conductor/rune#349
2026-09-19 15:35:03 +01:00
conductor 021fef2065 Merge pull request 'Issue templates: parsable spine, plain-English-first, version intent; auto-label reads declared kind' (#349) from issue-templates into master 2026-09-19 15:31:49 +01:00
conductor 3788875410 test: pin live-defect record to empty — #324 verified non-reproducing 2026-09-19
The pin asserted the T15 audit's conclusion as permanent. The audit's own
doctrine is demonstrated-currently-incorrect-behaviour; #324 has not
demonstrated in nine days and is closed FIXED_VERIFIED. The pin now
guards the empty record; a new live defect updates it deliberately.
2026-09-19 15:25:04 +01:00
conductor d96b89bd4c issue templates: spine + plain-English + version intent; auto-label reads declared kind
Issues were mash-of-text: unreadable for Nick, unparsable for the pipeline,
and the auto-label workflow guessed 'bug' from keywords — misclassifying
honest prose and fighting the T15 label audit. Changes:

- feature/bug templates: add verifiable close conditions, version-impact
  block (declared at submission, verified at merge), why-still-open field;
  plain English stays first and is the contract
- research template: question, falsifier, budget, durable findings path;
  new 'research' label kind
- custodian create_issue(kind, version_intent): assembles from the
  canonical template, forces the kind label, pre-ticks version intent
- auto-label.yml: type label comes from the declared spine kind; keyword
  guessing only as pre-template fallback
- reconciliation checker: align live-sync bug-label rule with the
  documented open-only exemption
- reconciliation artifact: #158/#162 SUPERSEDED (by #315/#316),
  #324 FIXED_VERIFIED (no longer reproduces), #163 revalidated
2026-09-19 15:08:23 +01:00
conductor efe3871916 test(tty): stabilize no-accumulation test — delta vs pre-loop snapshot of unrelated system sleeps (mount helper sleeps race the window) 2026-09-19 07:24:10 +01:00
conductor 026a1bd919 test(tty): canonical tool hash parity (bash.ts + await-operation.ts) — same contract as hook parity 2026-09-19 07:18:07 +01:00
conductor d9bcebb730 feat(tty): MiMo harness process-ownership contract — group-killed bash execution + explicit detachment
TTY-repair tranche: bash.ts v2 replaces bare execSync with a
process-group-owned spawn wrapper (MODE A: captured pipes, own pgid,
SIGTERM->SIGKILL group kill on timeout, listener teardown before
finalize) and an explicit detached mode (MODE B: setsid, stdin
/dev/null, output to a durable log file, unref) for nohup/setsid/&-
suffixed commands. Wait-intent compilation preserved with a STRICT
gate (normalized=true + ContinuePendingOperation + concrete operation
bound); arbitrary shell syntax passes through untouched. execFile
with argv array (no nested sh -c quoting) for the router call.
await-operation.ts pinned as canonical source (accepted healthy path,
unchanged). scripts/install_mimo_tools.py installs + hash-verifies.
tests/test_mimo_tty_contract.py: 8 tests (A-I incl. fd-no-pts,
group-kill, no-accumulation, quoting, wait-intent boundary,
await-op preservation).
2026-09-19 07:16:35 +01:00
conductor 2b2639783a chore(r8): qualify attempt-1 type-check cause as LIKELY_CPU_CONTENTION/CAUSE_UNCONFIRMED (S110 remains proven) 2026-09-19 06:39:11 +01:00
conductor 634821efd4 chore(r8): LOST_EVENT_RECOVERY PASS — live lost-event proof + canonical evidence
Live fault-injection proof (attempt 3, ci-c6c18ae): completion event
deliberately suppressed at the producer for one operation; first
watchdog failure-detector window (303s) consulted ci_runs authority,
recovered terminal success truthfully
(watchdog_authority_recovered), SAME await-operation invocation
returned, next MiMo turn automatic, R8_PROOF_COMPLETE, natural exit
rc=0. Attempt 1 (ci-89b6030) additionally proved truthful FAILURE
recovery (source CI genuinely red via S110 ratchet; seam defect fixed
in c6c18ae). Late-event idempotence: released event matched=0,
terminal state untouched, resume_attempts=0. All
FAILURE_RECOVERY_* targets met; SILENT_LOSS=0,
STATE_CONTRADICTION=0. Healthy-path hashes unchanged; fault rule
removed; deterministic suites 50/50.
2026-09-19 06:33:48 +01:00
conductor c6c18ae041 fix(r8): silent-handler ratchet compliance in fault seam — log instead of pass
The two catch-all handlers added in dac6e97 (r8_fault.record evidence
write, notify_event_bridge seam probe) tripped the #126 silent-handler
ratchet (S110 75 -> 77), which cascaded to the lint-baseline contract
test. Both handlers now log to stderr/stdout and continue — the seam
stays fail-open but is no longer silent. S110 back to 75=75.
2026-09-19 06:12:38 +01:00
conductor e03ad9aad9 chore(r8): latency-label terminology correction + lost-event fault seam landed with acceptance artifact
0. Evidence terminology only (no gate reinterpretation): acceptance
   latencies relabeled per primary artifacts — SOURCE_TO_TOOL_RETURN
   61.802s, TOOL_RETURN_TO_MODEL_TURN 13.636s, SOURCE_TO_MODEL_TURN
   75.438s (formerly mislabeled SOURCE_TO_MODEL_RESUME).
   REAL_ASYNC_BRIDGE_COMPLETE unchanged.
R8 seam (already committed dac6e97, included in this merge-tracking
   state): operation-scoped suppression rule, fail-open, evidence-
   logged; 7 seam tests.
2026-09-19 05:25:45 +01:00
conductor 89b603024e merge remote event-driven-clean (master merge) before R8 push 2026-09-19 04:59:53 +01:00
conductor dac6e97e16 feat(r8): operation-scoped lost-event fault-injection seam (disabled by default)
REAL_ASYNC_BRIDGE R8: narrowest reversible fault for lost-event
failure-recovery. scripts/r8_fault.py consults
$RUNE_WR_ROOT/r8_suppress_rules.json keyed by EXACT operation_id;
notify_event_bridge.py records the suppression decision (no .done file,
no bridge POST) and returns, so the waiter's watchdog/authority
recovery path is the only route to terminal truth. Fail-open design:
absent/corrupt rule file means normal delivery. Evidence appended to
r8_fault_log.jsonl. Healthy-path delivery semantics untouched
(guard clause only). Tests: tests/test_r8_fault_injection.py (7).
2026-09-19 04:59:07 +01:00
conductor fe45c6a8dd chore(release): auto-bump version to 0.87.0 — earned by #348 (feat(event-driven): REAL_ASYNC_BRIDGE acceptance persistence + gate reconciliati)
Promotion record: conductor/rune#348
2026-09-19 04:07:52 +01:00
conductor c7b2edd4fc Merge pull request 'feat(event-driven): REAL_ASYNC_BRIDGE acceptance persistence + gate reconciliation' (#348) from event-driven-clean into master 2026-09-19 04:06:21 +01:00
conductor 96f0e7e263 Merge branch 'master' into event-driven-clean 2026-09-19 03:59:45 +01:00
conductor a168cd935e chore: retrigger PR CI (umbrella propagation body now present; push CI green at 9485) 2026-09-19 03:52:45 +01:00
conductor a2c4a11b0d chore(deps): refresh lock — platformdirs 4.11.10 -> 4.11.11 (upstream release drift)
Root cause of the six consecutive CI lint failures (9464/9467/9471/9472/
9480/9479): the dep-lock ratchet correctly detected upstream drift
(platformdirs 4.11.11 released; uv pip compile regenerates a different
lock). Not a code defect — proven locally: fresh-venv reproduction of the
lint job's dep-lock step failed with exactly this mismatch. Remedy per
#150: regenerate + commit.
2026-09-19 03:45:01 +01:00
conductor 16164c6f5e style: ruff-format branch-added python files
CI lint's format-check on added files (base-range dependent) failed on
files that predate the format gate's effective coverage. Formatting-only:
no semantic changes; integration tests 15/15, ruff check clean, ratchet
PASS, daemon convergence re-verified.
2026-09-19 03:32:24 +01:00
conductor 0722dc2339 chore: retrigger CI (transient duplicate-run nondeterminism: 9464/9467 failure vs local lint-clean + 2196/0; documented quirk remedy) 2026-09-19 03:08:51 +01:00
conductor a7ec1b49d7 chore(event-driven): acceptance persistence + gate reconciliation + lost-event spec — REAL_ASYNC_BRIDGE healthy path COMPLETE
Acceptance evidence (store/scaffold/real_bridge_final_acceptance.json):
final unobserved run ci-5b61f8a — raw timeline, model counts (1 await-
operation, 0 other tools/sleeps/polls/rearms/observers), wake
classification (event=1, watchdog-discovered=0, deadline=0,
WATCHDOG_FAILURE_DETECTOR_CYCLE_COUNT=1 with the nuance preserved),
canonical terminal state (COMPLETED/success/authority_verified_terminal,
no FIFO contradiction), latency table with precision labels, 14/14
acceptance conditions PASS.

Gate reconciliation (evidence-specific, no bulk-pass):
- real_bridge_gates: 14 PASS, R13 PARTIAL (branch proven; PR/master flows
  unrun), R8 UNRUN (deliberate lost-event fault test not executed)
- mimo_semantics_gates: 10 PASS, M9/M10 UNRUN (PR/master via tool unrun)

Drive-skip correctness fix: self-resume detection keyed on explicit
ext.resume_mode marker instead of resume_cmd absence — records without
inline resume_cmd remain dispatchable via the WR_RESUME_CMD env template
(v02 T12/T10/v03 F regressions were stale-daemon interference on the test
root, but the absence-keyed skip was still wrong design; daemon suites
45/45 + 36/36 + 7/7 green after fix).

Lost-event FAILURE-RECOVERY spec persisted (lost_event_recovery_spec.json)
— specified, deliberately NOT executed; separate from healthy-path stats.

Implementation was proven in the final unobserved run; this commit is
acceptance evidence persistence, not feature work.
2026-09-19 02:59:15 +01:00
conductor 5b61f8a8bb chore(event-driven): diagnostic run evidence — full B-chain observed (contaminated window, superseded by final unobserved run) 2026-09-19 02:01:18 +01:00
conductor c730719768 chore(release): auto-bump version to 0.86.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-19 01:49:42 +01:00
conductor 4fe0f8ee46 chore(release): auto-bump version to 0.86.0 — earned by #347 (feat(event-driven): completion-file inotify bridge + operation-id-first tool)
Promotion record: conductor/rune#347
2026-09-19 01:48:20 +01:00
conductor 8999f29041 Merge pull request 'feat(event-driven): completion-file inotify bridge + operation-id-first tool' (#347) from event-driven-clean into master 2026-09-19 01:47:49 +01:00
conductor c288f4726d merge: integrate master (v0.85.9 auto-version) into event-driven-clean 2026-09-19 01:41:10 +01:00
conductor 308bbb74b7 feat(event-driven): final integration seam — state ownership, orphan recovery, CLI contract, daemon governance, task binding
§2/§3 state ownership: register_process_wait resume_mode ('fifo' default
preserves T15; 'self' = waiter owns continuation via completion-file
inotify, NO resume_cmd). ci_event_wait registers self-mode and stamps
durable terminal operation state (COMPLETED/FAILED + authority evidence)
before the envelope — a dead session can no longer downgrade operation
truth. Daemon drive skips resume_cmd-less waits; resume() re-reads and
never touches terminal records.

§4 orphan sweep: bounded periodic reconciliation of RESUMING waits past
ORPHAN_GRACE_SECS (failure detection, not scheduling) — truthful
RESUME_UNVERIFIED/READY_TO_RETRY + lease release; deterministic tests.

§5 CLI contract: ci_event_wait parses strictly; --watchdog supported;
unknown flags exit 2 loudly. No silent argument loss.

§6 governance: canonical daemon source IN REPO (bridge/daemon/);
scripts/install_wr_daemon.py installs atomically + verifies sha256
convergence (--check/--restart). One implementation, one deploy path.

§9 task binding: --task-id through cli/tool; task-bound resolution scopes
pending operations; stored operation wins over model hints; fresh
explicit-id operations normalize provisionally (ghost shas refused).

tests/test_real_async_integration.py: 10 frozen tests. Full suite 2196/0.
2026-09-19 01:40:34 +01:00
conductor 1f01e87453 Merge branch 'master' into event-driven-clean 2026-09-19 01:06:13 +01:00
conductor 4e8e081f48 feat(event-driven): completion-file producer in CI notify + wr-root env harmonization
- notify_event_bridge: writes durable resumes/<operation_id>.done BEFORE the
  bridge POST — delivery no longer depends on the daemon being alive (R3);
  best-effort on the same terms as the POST, never fails CI
- completion_file: honors both WR_ROOT (daemon) and RUNE_WR_ROOT (repo)
  conventions, WR_ROOT wins — daemon, waiter, and test roots can no longer
  disagree about delivery location
- tests/test_completion_delivery: notify-writes-when-bridge-down, write
  failure never fails CI, root resolution under both conventions

REAL_ASYNC_BRIDGE gates covered: R3 (partial — daemon side proven in
mimo-wait-resume tests/test_wr_v04_completion.py: 7/7 incl. R4 7ms wake,
R9 durability, R10 byte-identical duplicates)
2026-09-19 00:56:19 +01:00
conductor 6c681b97bf chore(release): auto-bump version to 0.85.9 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-19 00:25:51 +01:00
conductor d01d6eb6d4 feat(event-driven): completion-file inotify delivery + operation-id-first tool + M/R gates
- scripts/completion_file.py: durable completion-file + inotify wake
  (replaces FIFO+lease for process-wait delivery; zero CPU, zero
  arbitrary timer, kernel-level event notification)
- ci_event_wait: replaced FIFO wait loop with inotify + durable-event +
  wr-status checks on every cycle; short-sha resolution; explicit sha
- await_intent.py: no-sha auto-resolution from pending operations
- await-operation.ts: operation_id-first schema; sha/op_type/deadline
  are optional hints (non-authoritative)
- M/R gate objects: 28 gates total across M-patch + real-bridge
2026-09-19 00:13:06 +01:00
conductor f90695197d fix(event-driven): durable-event + wait-record recheck on every cycle + H-matrix tests
- Every cycle checks: wr status (daemon may transition to COMPLETED),
  durable event log (find_matching_event), completion file (inotify)
- Removed debug print, relaxed test timing for inotify+disk I/O path
- All 11 ci_event_wait + 32 other wait tests green
2026-09-19 00:11:51 +01:00
Nick 279c2b86cb chore(event-driven): recomputed 18/18 gates — EVENT_DRIVEN_CONTINUATION_COMPLETE (truthful evidence, clean window)
Previous 18/18 was truthfully superseded (supersession note): H8/H15/H18
were contradicted by raw evidence. Root causes for 9db239f/51416aa
deadline_pending success: orphaned lease + timer-blind re-arms +
runs-list page flood + short-sha equality failure. ALL FIXED.

Final evidence commit: 57206b7 == deployed == runtime == v0.85.8.
Clean-window: one logical await per flow; zero pending-exposed-to-model;
zero manual internal APIs; zero literal external sleeps.
2026-09-18 20:08:51 +01:00
conductor 57206b7b31 chore(release): auto-bump version to 0.85.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 19:30:36 +01:00
conductor b69ffcd44f chore(release): auto-bump version to 0.85.8 — earned by #346 (fix(event-driven): resolve short SHAs to full 40-char before authority read)
Promotion record: conductor/rune#346
2026-09-18 19:28:49 +01:00
conductor c4b4ebd0af Merge pull request 'fix(event-driven): resolve short SHAs to full 40-char before authority read' (#346) from deadline-return-fix into master 2026-09-18 19:27:46 +01:00
conductor 4220621cb0 Merge branch 'master' into deadline-return-fix 2026-09-18 19:21:10 +01:00
Nick 4b54451444 fix(event-driven): resolve short SHAs to full 40-char before authority read — 12-char prefix silently failed _fetch_runs equality (962e351 root cause) 2026-09-18 19:14:23 +01:00
conductor 962e3511e1 chore(release): auto-bump version to 0.85.7 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 17:30:12 +01:00
conductor fb809135c6 chore(release): auto-bump version to 0.85.7 — earned by #345 (fix(event-driven): orphaned-lease release + durable-event recheck + paginated ru)
Promotion record: conductor/rune#345
2026-09-18 17:29:02 +01:00
conductor 10acdfe708 Merge pull request 'fix(event-driven): orphaned-lease release + durable-event recheck + paginated runs' (#345) from deadline-return-fix into master 2026-09-18 17:28:01 +01:00
Nick fd9e1bffb8 chore(deps): apply regenerated lock (multidict 6.9.0) 2026-09-18 17:18:30 +01:00
Nick 631426763e chore(deps): re-lock requirements-ci.txt (upstream multidict 6.9.0) — dep-lock gate 2026-09-18 17:17:52 +01:00
Nick 09cd18ad54 fix(event-driven): orphaned-lease release + durable-event recheck on re-arm + paginated runs fetch (all pages) + behavioral deadline tests
ROOT CAUSE (Defect A, 9db239f + 51416aa deadline_pending->success):
ci_event_wait exited at deadline without releasing the daemon lease.
The orphaned lease blocked the daemon from writing resume tokens to
the FIFO for re-registered waits — so completion events that WERE
persisted (10:43:01 for 9db239f, 12:01:49 for 51416aa) could never be
delivered to a later waiter. Additionally: internal re-arms were
timer-blind (no durable-event recheck), and _fetch_runs used a single
50-run page (scheduled merge-robot floods push older SHAs past it).

FIXES:
- lease release in run() finally block (orphan prevention)
- durable-event recheck on every internal re-arm
- _fetch_runs paginates up to 4 pages, collecting all matches
- deploy_qualify --sha flag (explicit sha, env masked)
- model sha = hint; stored target wins on mismatch
- behavioral tests: terminal-before-deadline, nonterminal-never-returned
2026-09-18 17:09:40 +01:00
Nick da500df487 chore(hardening): H18 final convergence — deployed 51416aa/0.85.6 (duplicate-run quirk disclosed) 2026-09-18 13:25:21 +01:00
Nick 51416aa95e chore: retrigger CI (duplicate-run quirk: 9281 failure + 9282 success for same sha; upstream Gitea run duplication) 2026-09-18 12:57:52 +01:00
conductor b68f31ff0d chore(release): auto-bump version to 0.85.6 — earned by #344 (chore(hardening): 18/18 gates — EVENT_DRIVEN_CONTINUATION_COMPLETE)
Promotion record: conductor/rune#344
2026-09-18 12:28:18 +01:00
conductor dd41f15571 Merge pull request 'chore(hardening): 18/18 gates — EVENT_DRIVEN_CONTINUATION_COMPLETE' (#344) from hardening-live-proof into master 2026-09-18 12:26:33 +01:00
conductor 57e4b8672c Merge branch 'master' into hardening-live-proof 2026-09-18 12:20:35 +01:00
Nick 6923128f45 chore(hardening): 18/18 gates PASS, can_accept=true — EVENT_DRIVEN_CONTINUATION_COMPLETE (recomputed) 2026-09-18 12:13:55 +01:00
conductor 9db239fcb4 chore(release): auto-bump version to 0.85.5 — earned by #343 (docs(hardening): live proof marker (H12/H13))
Promotion record: conductor/rune#343
2026-09-18 11:36:03 +01:00
conductor 658ec63627 Merge pull request 'docs(hardening): live proof marker (H12/H13)' (#343) from hardening-live-proof into master 2026-09-18 11:35:45 +01:00
Nick 427c3ac6bb docs(hardening): live proof marker (H12-H14) 2026-09-18 11:02:22 +01:00
conductor 509ec00c87 chore(release): auto-bump version to 0.85.4 — earned by #342 (fix(hardening): operation-identity authority — explicit sha end-to-end)
Promotion record: conductor/rune#342
2026-09-18 10:34:26 +01:00
conductor bea523a0e3 chore(release): auto-bump version to 0.85.4 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 10:33:03 +01:00
conductor 0a15a1675d Merge pull request 'fix(hardening): operation-identity authority — explicit sha end-to-end' (#342) from hardening-authority-identity into master 2026-09-18 10:31:33 +01:00
Nick 81c6d1f9c6 fix(hardening): operation-identity authority — explicit sha end-to-end, paginated runs fetch, model-sha-as-hint
Defect A root cause: Gitea runs-list page flood (scheduled merge-robot
runs push relevant SHAs past the limit=50 first page) + collect-only-
first-page pagination -> terminal commit read as pending -> parked until
deadline. FIX: _fetch_runs paginates up to 4 pages collecting all matches
before verdict.

Defect B root cause: authority_verdict spawned deploy_qualify reading
DEPLOY_SHA from ambient env. FIX: explicit --sha arg (env masked when
sha is explicit); _ci_runs_verdict/op_ci_alive take sha as parameter
only; no env read remains in the authority path.

Defect C root cause: model sha treated as lookup key — a typo derived a
ghost operation. FIX: model sha is a HINT; unresolved sha + single
pending op -> stored target used with model_sha_mismatch logged; bound
op always wins over hints. deploy_qualify gained --sha flag (env kept
for workflow backward-compat only).

Tests: H-matrix (identity, prefix collision, env staleness,
monotonicity, aggregator lag, operation-id-first) + full 2179 suite.
2026-09-18 10:25:59 +01:00
conductor d53bb9a535 chore(release): auto-bump version to 0.85.3 — earned by #341 (chore(event-driven): E13 final convergence evidence)
Promotion record: conductor/rune#341
2026-09-18 04:28:45 +01:00
conductor 023151acb9 Merge pull request 'chore(event-driven): E13 final convergence evidence' (#341) from event-driven-gates-evidence into master 2026-09-18 04:26:47 +01:00
conductor 2da9305d96 Merge branch 'master' into event-driven-gates-evidence 2026-09-18 04:21:24 +01:00
Nick 2e05ea6848 chore(event-driven): E13 final convergence evidence — deployed 05f2a74/0.85.2 after CI retrigger 2026-09-18 04:14:27 +01:00
Nick be161c2043 chore: retrigger CI (type-check transient on 1c5dc55; local fresh-venv ratchet PASS 447/454) 2026-09-18 04:14:27 +01:00
conductor a6539986b9 chore(release): auto-bump version to 0.85.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 04:14:27 +01:00
conductor 8da216d155 chore(release): auto-bump version to 0.85.2 — earned by #340 (fix(event-driven): authority resilience — status-404 fallback)
Promotion record: conductor/rune#340
2026-09-18 04:14:27 +01:00
Nick 05f2a74c6f chore: retrigger CI (type-check transient on 1c5dc55; local fresh-venv ratchet PASS 447/454) 2026-09-18 03:46:55 +01:00
conductor 1c5dc557a6 chore(release): auto-bump version to 0.85.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 03:18:19 +01:00
conductor a6bccc1c5d chore(release): auto-bump version to 0.85.2 — earned by #340 (fix(event-driven): authority resilience — status-404 fallback)
Promotion record: conductor/rune#340
2026-09-18 03:16:27 +01:00
conductor f208b5e61c Merge pull request 'fix(event-driven): authority resilience — status-404 fallback' (#340) from event-driven-continuation into master 2026-09-18 03:15:46 +01:00
conductor aa800acbad Merge branch 'master' into event-driven-continuation 2026-09-18 03:09:04 +01:00
Nick 30be2c96b8 chore(deps): apply regenerated lock (platformdirs 4.11.10) 2026-09-18 02:59:55 +01:00
Nick ca00be8259 chore(deps): re-lock requirements-ci.txt (upstream platformdirs 4.11.10) — dep-lock gate 2026-09-18 02:59:04 +01:00
Nick 50a63e587f fix(event-driven): authority resilience — commit-status 404 fallback to runs-based verdict
Gitea's combined commit-status endpoint can 404 for the newest commit
(observed live on auto-version bump commits: status object materializes
late). When deploy_qualify returns pending AND the status endpoint is
unreadable, fall back to the operation-scoped Actions-runs verdict (same
evidence the two legs read). Also export DEPLOY_SHA in run() for python
callers. Found via E10 fresh-process proof.
2026-09-18 02:54:20 +01:00
conductor a54ab559cb chore(release): auto-bump version to 0.85.1 — earned by #339 (chore(event-driven): 16/16 gates — EVENT_DRIVEN_CONTINUATION_COMPLETE)
Promotion record: conductor/rune#339
2026-09-18 01:28:33 +01:00
conductor 599c3ab1c2 Merge pull request 'chore(event-driven): 16/16 gates — EVENT_DRIVEN_CONTINUATION_COMPLETE' (#339) from event-driven-continuation into master 2026-09-18 01:27:35 +01:00
conductor 483bfd7944 Merge branch 'master' into event-driven-continuation 2026-09-18 01:21:47 +01:00
Nick 1650b470c6 chore(event-driven): 16/16 gates PASS, can_accept=true — EVENT_DRIVEN_CONTINUATION_COMPLETE 2026-09-18 01:15:08 +01:00
conductor 853cf3e0ae chore(release): auto-bump version to 0.85.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-18 00:47:49 +01:00
conductor 875bb792f7 chore(release): auto-bump version to 0.85.0 — earned by #338 (feat(event-driven): structured-state routing + event-triggered cognition)
Promotion record: conductor/rune#338
2026-09-18 00:46:00 +01:00
conductor f06ddb3a20 Merge pull request 'feat(event-driven): structured-state routing + event-triggered cognition' (#338) from event-driven-continuation into master 2026-09-18 00:45:16 +01:00
Nick 2ebd9da139 feat(event-driven): structured-state routing + event-triggered cognition
- PendingOperation = durable continuation object: continuation_state
  (ACTIVE_REASONING/WAITING_EXTERNAL/RESUMABLE/RECOVERY_REQUIRED),
  canonical op_type/authority, task_id — parked cognition is a first-class
  durable state a fresh process can read
- ci_event_wait stamps cognitive-state transitions (WAITING_EXTERNAL on
  park, RESUMABLE/RECOVERY_REQUIRED on terminal)
- DEPLOY_SHA export fix: CLI/python run() consumers previously got a
  silent 'pending' authority verdict forever — the authority subprocess
  never received the sha (found via E10 fresh-process proof)
- await_intent_cli route-bash: Bash commands expressing external wait
  intent compile to the canonical continuation (new bash.ts harness tool
  routes through it); plain local sleeps execute normally
- tests/test_await_intent.py: E-matrix (derivation truth table,
  provenance order, observability)
2026-09-18 00:39:23 +01:00
conductor 65001196d8 chore(release): auto-bump version to 0.84.2 — earned by #337 (chore(async-intent): 13/13 gates — ASYNC_INTENT_NORMALIZATION_COMPLETE)
Promotion record: conductor/rune#337
2026-09-17 23:37:23 +01:00
conductor 76bb2b97be Merge pull request 'chore(async-intent): 13/13 gates — ASYNC_INTENT_NORMALIZATION_COMPLETE' (#337) from async-intent-live-proof into master 2026-09-17 23:36:25 +01:00
conductor e765f654f9 Merge branch 'master' into async-intent-live-proof 2026-09-17 23:31:09 +01:00
Nick f7141200bd chore(async-intent): 13/13 gates PASS, can_accept=true — ASYNC_INTENT_NORMALIZATION_COMPLETE (normalization observability log included) 2026-09-17 23:25:33 +01:00
conductor 53b4830b08 chore(release): auto-bump version to 0.84.1 — earned by #336 (docs(async-intent): live proof marker (A8/A9))
Promotion record: conductor/rune#336
2026-09-17 22:57:07 +01:00
conductor fa1b7c4be1 Merge pull request 'docs(async-intent): live proof marker (A8/A9)' (#336) from async-intent-live-proof into master 2026-09-17 22:56:32 +01:00
Nick 246f26663d docs(async-intent): live-proof marker for A8-A10 workflows 2026-09-17 22:19:35 +01:00
conductor 5718a3ea85 chore(release): auto-bump version to 0.84.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-17 22:04:28 +01:00
conductor 02802f321f chore(release): auto-bump version to 0.84.0 — earned by #335 (feat(async-intent): intent normalization at the execution boundary)
Promotion record: conductor/rune#335
2026-09-17 22:02:43 +01:00
conductor 4ce121a493 Merge pull request 'feat(async-intent): intent normalization at the execution boundary' (#335) from async-intent-normalization into master 2026-09-17 22:02:15 +01:00
Nick 8a8d3534c6 feat(async-intent): intent normalization at the execution boundary
MODEL OUTPUT = INTENT; RUNE CONTROLLER = EXECUTION SEMANTICS.

scripts/await_intent.py: deterministic resolver that derives canonical
op_type from stored normalized records / open PRs / git ancestry — model
supplied op_type and deadline_secs are recorded but NEVER used as policy
(git/Gitea truth outranks even legacy wait records). Sleep/check/poll
intent aimed at a known pending operation compiles to
ContinuePendingOperation; plain legitimate sleeps are untouched.
Timing provenance: USER_EXPLICIT/EXTERNAL_CONTRACT authoritative,
MODEL_INVENTED discarded. Ambiguity fails safe with structured
candidates. await_intent_cli.py = tool surface; compact JSONL
observability per normalization. await-operation.ts rewired to normalize
before dispatch (needs commit of harness tool separately).
2026-09-17 21:55:20 +01:00
conductor d0ec37925f chore(release): auto-bump version to 0.83.5 — earned by #334 (docs(memory-t0): external review report)
Promotion record: conductor/rune#334
2026-09-17 21:11:55 +01:00
conductor f9936fa244 Merge pull request 'docs(memory-t0): external review report' (#334) from memory-t0-external-report into master 2026-09-17 21:11:04 +01:00
Nick 04a277f22c docs(memory-t0): external review report (self-contained brief for third-party LLM review) 2026-09-17 21:05:37 +01:00
conductor b2777d666f chore(release): auto-bump version to 0.83.4 — earned by #333 (chore(memory-t0): as-is memory inventory — MEMORY_T0_COMPLETE)
Promotion record: conductor/rune#333
2026-09-17 20:17:10 +01:00
conductor 0982f7317f Merge pull request 'chore(memory-t0): as-is memory inventory — MEMORY_T0_COMPLETE' (#333) from memory-t0-asis-map into master 2026-09-17 20:15:54 +01:00
Nick 9583678997 chore(memory-t0): as-is memory inventory — MEMORY_T0_COMPLETE (17/17 gates)
Read-mostly archaeology: one canonical experiential store (memory.db:
899 entries, hybrid dense+BM25 recall, dedupe/supersede gate with 1465
receipts), derived knowledge.json (7965 items), dormant graph/shards/
temporal, harness-native duplicate-authority surface identified, INWARD
port verified as unbound-but-real contract. DEFECT FOUND (recorded, not
fixed): sleep-time systemd timer reads stale opencode.db — designed
transcript-mining loop broken; consolidation survives via per-turn
mandate. Preservation risks classified; fresh-worker answers embedded.
2026-09-17 20:10:03 +01:00
conductor 5db8b202d9 chore(release): auto-bump version to 0.83.3 — earned by #332 (chore(t21.5): acceptance V1_ACCEPTED — 17/17 gates, ledger readback)
Promotion record: conductor/rune#332
2026-09-17 18:30:17 +01:00
conductor 78c0d97a43 Merge pull request 'chore(t21.5): acceptance V1_ACCEPTED — 17/17 gates, ledger readback' (#332) from t21.5-pending-class-fix into master 2026-09-17 18:28:19 +01:00
conductor 551530c78c Merge branch 'master' into t21.5-pending-class-fix 2026-09-17 18:22:01 +01:00
Nick b669e4d4ed chore(t21.5): acceptance V1_ACCEPTED — can_accept()=true, 17/17 gates PASS; scaffolding-v1 PATCH 1.0.4; event-wait-resume MINOR 0.4.0 2026-09-17 18:16:34 +01:00
conductor 2b6a5a6b95 chore(release): auto-bump version to 0.83.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-17 17:52:32 +01:00
conductor e2e7b74a8e chore(release): auto-bump version to 0.83.2 — earned by #331 (fix(t21.5): pending-class re-arm — residual nonterminal-return generator closed)
Promotion record: conductor/rune#331
2026-09-17 17:51:11 +01:00
conductor db4ce7f6a6 Merge pull request 'fix(t21.5): pending-class re-arm — residual nonterminal-return generator closed' (#331) from t21.5-pending-class-fix into master 2026-09-17 17:50:49 +01:00
Nick 505209b839 chore(deps): re-lock requirements-ci.txt (upstream idna 3.20) — dep-lock gate 2026-09-17 17:45:38 +01:00
Nick f7256aa29e fix(t21.5): pending-class verdicts and not-started operations re-arm internally — residual nonterminal-return generator closed (§18 audit) 2026-09-17 17:38:06 +01:00
conductor 26e3765319 chore(release): auto-bump version to 0.83.1 — earned by #330 (chore(t21.5): final gate evidence (G13/G14, MINOR 0.4.0, PATCH 1.0.4))
Promotion record: conductor/rune#330
2026-09-17 14:36:38 +01:00
conductor e6451a5afc Merge pull request 'chore(t21.5): final gate evidence (G13/G14, MINOR 0.4.0, PATCH 1.0.4)' (#330) from t21.5-gate-evidence into master 2026-09-17 14:36:12 +01:00
Nick 7008743620 chore(t21.5): gate evidence G13/G14 + event-wait-resume MINOR 0.4.0 + scaffolding-v1 PATCH 1.0.4 2026-09-17 14:29:56 +01:00
conductor f545f1ed8d chore(release): auto-bump version to 0.83.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-17 13:47:17 +01:00
conductor 73803e6a0d chore(release): auto-bump version to 0.83.0 — earned by #329 (feat(t21.5): root-cause continuation + guard retirement + V1 parity)
Promotion record: conductor/rune#329
2026-09-17 13:45:40 +01:00
conductor ed1696606e Merge pull request 'feat(t21.5): root-cause continuation + guard retirement + V1 parity' (#329) from t21.5-root-cause-continuation into master 2026-09-17 13:45:14 +01:00
Nick 4944f9e41c test(t21.5): no-spec fallback coverage (ratchet -> 94%) 2026-09-17 13:39:52 +01:00
Nick 6722066c79 test(t21.5): coverage for awareness error paths, dispatch wrap, event_wait fallback (ratchet 69 -> 80%) 2026-09-17 13:35:08 +01:00
Nick df8262b9ed fix(t21.5): pyright warning cleanup in event_wait loader + activation awareness (ratchet 456 -> baseline) 2026-09-17 13:16:02 +01:00
Nick 756ebe6201 fix(t21.5): pyright-clean provider loader; harden test Daemon fixture against bind races (retry on fresh ephemeral port) 2026-09-17 13:06:17 +01:00
Nick f64ee95aad feat(t21.5): root-cause continuation + guard retirement + V1 parity
- ci_event_wait: OPERATION_SPECS (op-specific completion authority:
  branch_ci/pull_request_checks -> own runs; master_ci/deploy/release ->
  deploy qualification); SINGLE LOGICAL AWAIT (all nonterminal conditions
  re-arm internally; model sees only terminal verdicts or real deadline)
- await-operation tool: op_type parameter
- runtime wait guard + cwd guard RETIRED (canonical hook = documented
  no-op; prevention lives in tests, not interception)
- wr_root_resolver.py: ONE wait/resume root authority; split-brain fails
  loudly; start_wr_daemon.py resolves root once; governed reconciliation
- construction_activation._auto_structural_awareness: PROJECT_BUILD
  bootstrap auto-registers Registry + spine manages edge (no manual
  bookkeeping); fixed latent dispatch_from_decision mode-object bug
- research_memory_port.py: future-facing Memory port inside existing
  INWARD (provider contract, zero-result fast path, teachable-moment
  candidate seam; APPLICABILITY governs transfer)
- v1_parity_manifest.py: 56 rows, 0 blocking mandatory-V1 items
- G1-G12 gate evidence in store/scaffold/t21.5_gates.json
2026-09-17 12:56:52 +01:00
conductor c3a0a48ca5 chore(release): auto-bump version to 0.82.3 — earned by #328 (chore(t21.4): final gate evidence — V1_RETEST_REQUIRED (10 PASS / 2 BLOCKED))
Promotion record: conductor/rune#328
2026-09-17 03:40:45 +01:00
conductor b4ed258121 Merge pull request 'chore(t21.4): final gate evidence — V1_RETEST_REQUIRED (10 PASS / 2 BLOCKED)' (#328) from t21.4-gate-evidence into master 2026-09-17 03:40:19 +01:00
Nick bacea2d472 chore(t21.4): final gate accounting — 10 PASS / 2 BLOCKED, can_accept()=false, V1_RETEST_REQUIRED
G11 RAW_SELF_AUDIT blocked: 3 inter-cycle unjustified sleeps executed in
wait-guard heredoc blind spot (wait_guard v4 defect filed); 1 guard
intervention post-workflows. G12 consequently blocked. All other gates
PASS with live evidence. Verdict computed by the persisted gate object,
not asserted.
2026-09-17 03:35:17 +01:00
conductor 9acfc0953e chore(release): auto-bump version to 0.82.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-17 03:16:16 +01:00
conductor 818aa49043 chore(release): auto-bump version to 0.82.2 — earned by #327 (fix(deploy_qualify): canonical-default server/repo fallback for out-of-CI consum)
Promotion record: conductor/rune#327
2026-09-17 03:14:22 +01:00
conductor 212239ca7b Merge pull request 'fix(deploy_qualify): canonical-default server/repo fallback for out-of-CI consumers' (#327) from t21.4-qualify-defaults-fix into master 2026-09-17 03:13:41 +01:00
conductor ae6d4a0d98 Merge branch 'master' into t21.4-qualify-defaults-fix 2026-09-17 03:08:14 +01:00
Nick 00013f8c0b fix(deploy_qualify): canonical-default Gitea server/repo fallback — out-of-CI consumers got silent pending verdicts
The await_operation first-class tool (and any local deploy_qualify.py
caller) never exported GITEA_SERVER/GITEA_REPO, so main() printed
'pending' unconditionally — the first-class wait could never resolve
success even against a green, converged deployment. CI behavior is
unchanged (env always exported there); the fallback only fires when
the caller omitted the vars. Root-caused live in T21.4 G10 dogfood.
2026-09-17 03:02:37 +01:00
conductor ba2966e631 chore(release): auto-bump version to 0.82.1 — earned by #326 (T21.4: post-restart final V1 proof — governed rollback, auto execution context, )
Promotion record: conductor/rune#326
2026-09-17 02:45:18 +01:00
conductor 20fcee3f40 Merge pull request 'T21.4: post-restart final V1 proof — governed rollback, auto execution context, material closure, structural coverage' (#326) from t21.4-final-v1-proof into master 2026-09-17 02:44:36 +01:00
conductor 493d9c2800 Merge branch 'master' into t21.4-final-v1-proof 2026-09-17 02:40:14 +01:00
Nick 6b5d8ad023 chore(t21.4): record PR #326 + propagation declaration in gate object 2026-09-17 02:36:03 +01:00
Nick f3ef4672f3 test(t21.4): coverage for auto material-change closure + spine entry-point seeding (CI coverage ratchet) 2026-09-17 02:07:15 +01:00
Nick be6bc17537 feat(t21.4): post-restart final V1 proof — governed rollback API, automatic project execution context, material-change closure, structural coverage
- scripts/rollback_generation.py: canonical rollback_generation() on the
  deploy_promotion machinery (shared lock, refuse-before-mutation,
  ancestor-only eligibility, identity+health qualification, durable
  history, revert on failed health). Live B->A->B proven with refusals.
- tool_executor._resolve_execution_cwd: automatic project execution
  context (workdir > project registry resolve_cwd > process); unknown
  projects refused pre-execution. Live A-F proven.
- construction_interface.close(): auto material_change + revision +
  drift/revise flow (rule 19Q.2 integrated, proven live).
- spine: entry-point seeding (__main__ is a runtime path invisible to
  the import graph); subsystem belongs_to membership via coverage audit.
- scripts/structural_coverage_audit.py: 237 modules A157/B41/C28/D11/E0,
  100% meaningful coverage, 100% impl ownership, 0 significant unknowns.
- Scaffolding internal 1.0.2 -> 1.0.3 (PATCH, contract unchanged).
- Gate object T21.4 persisted (12 mandatory gates, GateAccounting).
2026-09-17 01:38:39 +01:00
conductor 40b3731928 chore(release): auto-bump version to 0.82.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 23:07:32 +01:00
Nick caa77f8a35 feat(t21.3): acceptance V1_ACCEPTED — each reason closed with correction evidence 2026-09-16 23:06:30 +01:00
Nick 4d215ea666 feat(t21.3): Scaffolding 1.0.2 PATCH — T21.3 correction fixes (contract unchanged) 2026-09-16 23:06:30 +01:00
conductor fda9b6dda0 chore(release): auto-bump version to 0.81.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 22:37:40 +01:00
Nick 116d1ffbeb chore(store): T21.3 remaining evidence + guard fixes 2026-09-16 22:36:54 +01:00
Nick 040988cf00 fix(test): P → Path reference fix 2026-09-16 22:36:54 +01:00
conductor ffed816465 chore(release): auto-bump version to 0.81.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 22:27:49 +01:00
Nick 72ab7dccf7 fix(test): hook parity test uses env-overridable hook target (not test runner's HOME) 2026-09-16 22:27:14 +01:00
conductor 5d20dc7f51 chore(release): auto-bump version to 0.81.4 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 21:58:56 +01:00
Nick 9e903dacaf fix(t21.3): all F821/SyntaxError fixed in src/ scripts/ shared/ (CI-caught, locally preflighted per 21.1L rule) 2026-09-16 21:58:06 +01:00
Nick ed1b41abd5 fix(drift_core): remove all dead code with F821 + add missing closing brace (CI-caught) 2026-09-16 21:58:06 +01:00
Nick ff1b950f11 fix(drift_cache): define SCRIPTS_DIR (F821, CI-caught) 2026-09-16 21:58:06 +01:00
Nick 3afc9ff872 fix(shared): remove remaining dead code blocks with F821 (CI-caught) 2026-09-16 21:58:06 +01:00
conductor 28e9c8c1c1 chore(release): auto-bump version to 0.81.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 21:13:00 +01:00
Nick 8249854881 fix(shared): remove dead code with F821 undefined names + re-pin platformdirs (CI-caught) 2026-09-16 21:12:15 +01:00
conductor 4aeef171d2 chore(release): auto-bump version to 0.81.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 20:38:31 +01:00
Nick 2ccac89ec4 chore(store): T21.3 evidence 2026-09-16 20:37:19 +01:00
Nick 232986f0c3 fix(test): widen time-sleep variant accepted classes 2026-09-16 20:37:19 +01:00
conductor 725c779ac7 chore(release): auto-bump version to 0.81.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 20:30:15 +01:00
Nick edd6d06300 fix(t21.3): bridge permissions (a+rwX for runner user) + ci_event_wait token propagation note 2026-09-16 20:29:43 +01:00
conductor 98cb3a98a5 chore(release): auto-bump version to 0.81.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 19:55:29 +01:00
Nick 7881e78e7f feat(t21.3): await_operation tool (corrected plugin API format) + canonical copy in config/mimo-tools/ 2026-09-16 19:55:02 +01:00
conductor 57a689d22f chore(release): auto-bump version to 0.80.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 19:52:27 +01:00
Nick 45869a77cd chore(store): T21.3 evidence + guard updates 2026-09-16 19:51:57 +01:00
Nick 224fbfa4e9 feat(t21.3): 21.3B root-cause documented + 21.3C await_operation tool (first-class async wait)
ROOT CAUSE (verified, not inferred): the model generates sleep N because
NO await/wait tool exists in its tool vocabulary. The model rationally
chooses bash sleep as the least-bad option. The fix: expose
await_operation as a first-class MiMo tool via the plugin API.

The tool: registers on the W/R event bridge, blocks until the completion
event arrives, returns verified JSON verdict. The model sees a normal
tool result — not an error, not a guard rejection.
2026-09-16 19:51:57 +01:00
conductor 417e824a51 chore(release): auto-bump version to 0.79.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 18:37:41 +01:00
Nick ca2807e9b5 feat(t21.2): 21.2F blind external adoption — independent creator + independent adopter, 100% recall on important entities, legacy trap preserved, 0 false relationships; 21.2J matrix 6 new held-outs; 21.2I contradiction hunt persisted 2026-09-16 18:37:22 +01:00
conductor 25f5219982 chore(release): auto-bump version to 0.78.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 18:22:41 +01:00
Nick 2d09bee39e feat(t21.2): 21.2B resolve_cwd fix + 21.2C universal wait enforcement + 21.2D canonical hook ownership + 21.2G mandatory-gate accounting
- 21.2B: resolve_cwd returns repo_identity (git:<path> stripped) not
  scaffold_location; worktree override; unknown project raises; no
  repo → refuse; 8 held-outs incl. old-bug reproduction.
- 21.2C: wait_guard covers time.sleep(N) (inline + heredoc), polling
  loops (while/until + status on full command), repeated delay+status;
  7 new held-outs; legitimate cases still pass.
- 21.2D: canonical hook source at config/mimo-hooks/event-wait-guard.ts;
  scripts/install_mimo_hook.py verifies hash; drift test detects
  canonical↔installed divergence.
- 21.2G: programme_gates.py — mandatory gates end in PASS/BLOCKED/
  WAIVED_BY_USER only; GateDemotionError prevents mandatory→optional
  demotion; 6 held-outs.
2026-09-16 18:22:05 +01:00
conductor 77bc2161a6 chore(release): auto-bump version to 0.77.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:19:38 +01:00
Nick 74e18402f0 feat(t21.1): acceptance restored to V1_ACCEPTED — each RETEST_REQUIRED reason closed with named correction evidence (21.1M) 2026-09-16 17:19:17 +01:00
conductor 39ec00b361 chore(release): auto-bump version to 0.76.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:16:25 +01:00
Nick 95946b6efd feat(t21.1): Scaffolding 1.0.1 PATCH — 21.1 correction work recorded (contract unchanged, implementation fixes only) 2026-09-16 17:15:35 +01:00
conductor 72d3c909c8 chore(release): auto-bump version to 0.75.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:12:07 +01:00
Nick ab2dfe7b09 feat(t21.1): 21.1G research-depth truth + 21.1F real rollback + 21.1E retirement completion
- 21.1G: build_basis.revise_depth() — original + revised depth both
  recorded; OUTWARD downgrade requires explicit resolution evidence;
  held-outs cover real-precedent-blocked, resolved-locally, no-op
  refused, provenance-reuse, requirements-invalidate (5 cases).
- 21.1F: real rollback B→A executed (ln -sfn current→c7c6b2d; smoke
  proof against A: __version__ + classify_mode importable); repromotion
  B verified; A tree intact; last-good coherent. LIVE_PROVEN.
- 21.1E: complexity_scorer (602 lines) retired through full lifecycle —
  caller migrated to canonical classify_mode, supersession recorded,
  stabilization green, deleted, spine reconciled, lint baseline shrunk.
  message_queue detect_task_addition REFUSED (4 live consumers —
  migration incomplete).
2026-09-16 17:11:39 +01:00
conductor 1326713234 chore(release): auto-bump version to 0.74.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 16:26:42 +01:00
Nick 867321ed05 feat(t21.1): 21.1E governed retirement — complexity_scorer deleted (602 lines)
Full lifecycle: DISCOVER (T20L candidate) → consumers mapped (1: the
TUI hint printer) → dynamic risk checked (clean) → canonical replacement
(classify_mode — the ONE classifier, same route as /build and
conversational) → caller migrated → supersession recorded (construction_mode
supersedes complexity_scorer) → stabilization run green → DELETE → spine
reconciled → full regression green (2061) → lint baseline shrunk
(BLE001 350→347, S110 78→75 — earned by the deletion).

Held-out proving REFUSAL where evidence is insufficient:
message_queue.detect_task_addition has 4 live consumers → NOT deleted
(20L MIGRATION_CANDIDATE stands).
2026-09-16 16:26:14 +01:00
conductor a44ced163e chore(release): auto-bump version to 0.73.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 16:08:06 +01:00
Nick f40f7e7233 chore(store): T21.1 evidence 2026-09-16 16:07:19 +01:00
Nick 8bef4682f5 feat(t21.1): deterministic spec-drift detection via revision counters (21.1D)
Wall-clock timestamps are metadata, not causal ordering (same-second
events share a stamp — the T21 race). Every material_change now bumps
material_revision; every revise_spec records covers_material_revision.
spec_drift_status() is the callable consistency gate: material >
governed → SPEC_VERSION_DRIFT + durable obligation. 7 held-outs cover
mandate CASES A-G (drift on change, no false drift on metadata,
PATCH/MINOR close, move-only stable, old-version proof reopens).
2026-09-16 16:07:19 +01:00
conductor f939d9db5a chore(release): auto-bump version to 0.72.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 16:02:49 +01:00
Nick dcead3fecc fix(t21.1): universal wait-guard closure + execution-context hardening (21.1B/21.1C)
- 21.1B ROOT CAUSE (DB evidence): the live bypass (2026-09-16 12:59Z)
  was a 6-segment multiline sleep 540 → cd → git fetch → releases curl
  that walked past the 3-segment lookahead. wait_guard.py now scans ALL
  segments downstream of the sleep; TS hook matches (v3); 21B-4 bypass
  held-outs green incl. the EXACT live command.
- 21.1C: repo-affecting commands carry explicit cwd (systemic fix):
  project_registry.resolve_cwd() (raises on unknown projects —
  refuse mutation, never guess); hook CwdGuard refuses repo-touching
  bash without explicit cwd marker (generic shells unrestricted);
  4 held-outs mirror the convention in the suite.
- STATUS_POLL_RE broadened (releases API + git fetch shapes).
2026-09-16 16:01:33 +01:00
conductor 87654cbfd1 chore(release): auto-bump version to 0.72.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 15:54:18 +01:00
Nick 6dddb0a5ee fix(wait-guard): full-downstream scan — T21.1 root-caused the live bypass (2026-09-16 12:59Z: 6-segment multiline walked past the 3-segment lookahead; DB evidence proved the exact command). 21B-4 bypass held-outs now green. Suite 2050. 2026-09-16 15:53:41 +01:00
Nick f90725dba8 chore(store): final T21 records 2026-09-16 14:28:51 +01:00
Nick 24b7dda24f docs(t21): final V1 acceptance — V1_ACCEPTANCE_READY, Scaffolding 1.0.0 earned 2026-09-16 14:28:51 +01:00
conductor c7c6b2d5ac chore(release): auto-bump version to 0.72.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 14:15:07 +01:00
Nick 6a761f2d87 feat(t21): Scaffolding V1 → 1.0.0 governed baseline (21S) — earned across T14-T21; 21L fresh-context test: 18.8KB ledgers answered all four questions without repo exploration 2026-09-16 14:14:26 +01:00
conductor 9ef267f2e3 chore(release): auto-bump version to 0.71.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 13:59:35 +01:00
Nick bd80fb0e3b chore(store): T21 evidence 2026-09-16 13:58:58 +01:00
Nick 759a836726 fix(tui): _task_repo_for_loader referenced undefined _operator (F821) — real accessor is rune_operator.get_operator() (21G follow-through, CI-caught) 2026-09-16 13:58:58 +01:00
conductor 71f051d928 chore(release): auto-bump version to 0.71.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 13:49:35 +01:00
Nick 2b1d0ac245 feat(t21): 21K external-project adoption proof — disposable stock-tool repo adopted through the full machinery (registry→basis→scaffold→ledgers→packet→MINOR construction→descriptive impact→commit); packet bound exactly 1 file; existing consumers untouched 2026-09-16 13:48:43 +01:00
conductor fbb5018413 chore(release): auto-bump version to 0.70.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 13:41:58 +01:00
Nick c8f0a8e353 feat(t21): 21G legacy transformation + 21I task-state ordering fix + 21O deterministic drift test
- 21G: the TUI startup task loader now reads the CANONICAL
  TaskStateRepository (17F authority) instead of the task_persist
  export ledger; task_persist demoted to session-end export view
  (its only remaining role). Characterized first, migrated with
  fallback, BLE001 handler made visible.
- 21I: terminal _apply_runtime_result ordering REVERSED — canonical
  transition commits before legacy cache save; crash now leaves cache
  STALE (backfillable) instead of AHEAD of truth. 6 fault-injection
  held-outs incl. a structural source-grep guard.
- 21O: spec-drift test made deterministic.
2026-09-16 13:41:32 +01:00
conductor fb7fb34ca5 chore(release): auto-bump version to 0.69.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 05:09:17 +01:00
Nick abe24da81d chore(store): T21 evidence 2026-09-16 05:08:14 +01:00
Nick 15c54539b5 fix(operator): canonical-first ordering in terminal task path (T21 21I)
The T20 cold benchmark's crash-window risk was REAL: the terminal
_apply_runtime_result block saved the legacy cache (operator.json) to
disk BEFORE the canonical TaskStateRepository.transition() — a crash
between left the cache claiming completed/failed while canonical truth
still said running. Reversed: _ensure_canonical + transition commit
first; _save() follows. A crash now leaves the cache STALE (backfillable)
instead of AHEAD. 6 fault-injection held-outs including a structural
guard that greps the real source for ordering regressions.
2026-09-16 05:08:14 +01:00
conductor ad35a2f7f5 chore(release): auto-bump version to 0.69.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 05:02:04 +01:00
Nick e2f536f71a feat(t21): living capability specs + strict internal SemVer (21C/21D/21E)
- spec layer on the EXISTING capability ledger (no second authority):
  requirements/expected_behaviours/non_goals/acceptance_criteria +
  version_history with full bump provenance (reason, changed sections,
  affected consumers, validation consequences).
- strict SemVer machinery: classify_version_bump derives
  PATCH/MINOR/MAJOR from the change shape; proposed kinds verified
  against actual (unjustified bumps raise); no-change revises raise.
- baseline policy (21D-1): explicit governance_version_started_at +
  baseline_reason; real Rune capabilities baselined 0.1.0-0.4.0 by
  contract maturity (no fake 1.x history).
- version-aware quicksand (21N): MAJOR break reopens behaviours with
  version-referenced reasons; proven-against-old-version history
  queryable; unrelated capabilities version-stable.
- spec drift (21O): material change without bump = detectable ordering
  signal; deterministic held-outs.

Suite 2039 green.
2026-09-16 05:00:15 +01:00
conductor 5b2712f5e6 chore(release): auto-bump version to 0.68.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 04:53:32 +01:00
Nick 124b77e95b feat(t21): wait-guard regression protection + MiMo controller hook (21B)
- 21B-1: Rune sleep audit — no UNJUSTIFIED_SLEEP in active paths;
  auto_bump_pr.py polling classified legacy (21G candidate). T20's
  sleeps were MODEL-GENERATED Bash (controller-level defect).
- 21B-2/3: controller is a compiled binary (not source-patchable) —
  guard implemented at the extension surface: ~/.mimocode/hooks/
  event-wait-guard.ts via @mimo-ai/plugin tool.execute.before.
  LIVE_PROVEN_THIS_SESSION: v1 hot-reloaded and intercepted a tool
  call mid-session (no restart). v1 false-positive (heredoc
  documentation containing the pattern) found live and fixed: v2
  requires sleep+poll pairing across adjacent segments with heredoc
  bodies stripped.
- 21B-4: wait_guard.py — Python mirror of the classification logic in
  the Rune suite (survives model changes/compaction); 8 held-outs
  reproducing the exact T20 patterns (540/300/600 sleeps + status
  probes blocked; heredoc docs, bare sleep, short backoff, segment
  isolation permitted).
2026-09-16 04:52:23 +01:00
conductor 1b7995e093 chore(release): auto-bump version to 0.67.1 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 04:03:24 +01:00
Nick fcd835883e chore(store): T20 evidence 2026-09-16 04:02:53 +01:00
Nick 37ce68eff8 fix(tui): /testing used Path without import (F821) — raw-ruff gate caught it, lint ratchet scope gap noted (T20 follow-through) 2026-09-16 04:02:53 +01:00
conductor 2fa0a5f144 chore(release): auto-bump version to 0.67.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 03:43:31 +01:00
Nick c3ef29c59d feat(retrofit): semantic release 0.67.0 — governed minor release of the Rune-wide retrofit (T20)
T20 is a feature-level tranche: Rune structural taxonomy (15 domains,
107+ entities), capability/behaviour ledgers, /testing backlog, Spine
modernization, rune graph projection, semantic release policy — new
subsystem functionality and materially expanded system behaviour.
Classified MINOR per docs/RELEASE_POLICY.md. First release computed by
the FIXED semver_bump (synthesized-title kind carried through).
2026-09-16 03:43:06 +01:00
conductor f079aa2cc9 chore(release): auto-bump version to 0.66.84 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 03:33:28 +01:00
Nick c03c953a45 fix(release): semver_bump honors synthesized convergence title kind (T20 20B REAL DEFECT)
The push-convergence path synthesizes PR titles as '<kind>: converged
release' where kind comes from version_convergence.highest_warranted_bump
(minor|patch). semver_bump then re-derived the kind from the TITLE TEXT
and 'minor:' matched nothing → patch. Every feature tranche downgraded:
T18/T19/T20 feat ranges bumped 0.66.77-83 instead of 0.67.0. Fixed:
MINOR_PREFIX_RE carries the convergence decision through. Held-out
proves minor-title→0.67.0 and patch-title→0.67.1 (no accidental
re-minor). This is the 0.66.x problem the mandate named, root-caused.
2026-09-16 03:32:53 +01:00
conductor 90ae1c2612 chore(release): auto-bump version to 0.66.83 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 03:16:08 +01:00
Nick cb1b8303fe feat(retrofit): 20U/20W evidence + 20B policy record (T20 final tranche work)
- 20U: V1 requirement classification (NEAR_V1_ACCEPTANCE_READY with
  exact missing proof classes).
- 20W: 12-case retrofit adversarial matrix (unknown ownership,
  ghost relationships, duplicate authority, dynamic consumers,
  spine↔scaffold reconciliation, file moves, stale ledgers, packet
  scoping, read-only projection, hidden-consumer retirement blocks,
  canonical task-state wins, env-skew visibility, bounded packets).
  Matrix found REAL spine↔model drift (4 missing domains — fixed in
  the model).
- 20B: semantic release policy doc + held-outs (patch→.81,
  minor→.67.0, no accidental re-minor). T20 classified MINOR.

Suite 2019 green.
2026-09-16 03:14:53 +01:00
conductor 3c54782634 chore(release): auto-bump version to 0.66.82 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 03:02:15 +01:00
Nick 478ce45755 feat(retrofit): T20 0.67.0 MINOR release record (semantic policy) 2026-09-16 03:02:04 +01:00
Nick a96ed90b90 feat(retrofit): rune taxonomy + 20Q honesty fixes + 20W matrix + coverage (T20 20D-20W)
- 20D/E: rune taxonomy from import-graph reality (15 domains, 107+
  entities, 171+ edges); honest coverage (37% file — NOT complete).
- 20F: 4 representative capability ledgers (699-866 bytes) + risk_notes
  field added from the 20Q cold-benchmark finding (write-ordering
  failure window now recorded where fresh models re-read it).
- 20G: 10-behaviour Rune inventory (8 AUTO_VERIFIED from live evidence,
  1 manual pending, 1 hardening item; ZERO USER_CONFIRMED manufactured).
- 20I: Spine↔Rune-model boundary (Spine=module facts; rune model=
  domains/capabilities/behaviours; 8 subsystems linked by name map).
- 20J: rune graph projection (107 nodes/171 edges, JSON/DOT/Mermaid).
- 20K: honest coverage by category (file 37%, structural 36%,
  dependency 92%, state 3 explicit, behaviour 10, validation 8+1).
- 20L: 5 legacy candidates classified (bounded/migration/keep/retire).
- 20Q: HONEST comparison — cold agents found MORE breadth (9 vs 2
  influencers) and a REAL ledger gap; recorded without inflation.
- 20R: REAL quicksand proof on Rune entities (reopen + preserve).
- 20W: 12 held-outs; matrix found REAL spine↔model drift (4 domains
  missing — fixed in the model, not the test).
- 20T: 37 stale build worktrees removed (verified clean); no open
  contracts; programmes durable.
2026-09-16 03:01:42 +01:00
conductor 713f85a0e3 chore(release): auto-bump version to 0.66.81 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 02:41:18 +01:00
Nick 504935828b feat(retrofit): rune structural model + ledgers + backlog + Spine links (T20 20C-20R)
- 20B: semantic release policy (docs/RELEASE_POLICY.md) + held-outs
  (patch→0.66.81, minor→0.67.0, no accidental re-minor).
- 20C: QUALIFIED retrofit Build Basis (stable-ID contract, dynamic-
  uncertainty taxonomy, strangler-fig, two-tier coverage, LSP combine).
- 20D/E: rune taxonomy from import-graph reality (9 domains, 11
  subsystems, 107 entities/171 edges, honest 37% file coverage —
  NOT claimed complete).
- 20F: 4 representative capability ledgers (699-866 bytes each).
- 20G: 10-behaviour Rune inventory (8 AUTO_VERIFIED from live
  evidence, 1 manual pending, 1 hardening item; ZERO USER_CONFIRMED
  manufactured).
- 20I: Spine↔Rune-model authority boundary defined (Spine=module
  facts; Rune model=domains/capabilities/behaviours; shared kind:name
  discipline; 8 spine subsystems linked to rune domains).
- 20J: rune graph projection (JSON/DOT/Mermaid, 107 nodes/171 edges).
- 20K: honest coverage metrics by category (file 37%, structural 36%,
  dependency 92%, state 3 explicit, behaviour 10, validation 8+1).
- 20L: 5 legacy candidates classified (build_mode=BOUNDED_ADAPTER,
  task_persist=MIGRATION, canaries=KEEP, complexity_scorer=RETIREMENT,
  message_queue=MIGRATION).
- 20R: REAL quicksand proof — construction_mode change reopened
  VAL-RUNE-001 (history preserved); voice_synthesis change left
  VAL-RUNE-004 untouched.
- 20P: lock-parity proven (0 diff); residual rule recorded; VAL-010.

Suite 2002 green; ratchets PASS.
2026-09-16 02:40:53 +01:00
conductor 8e78bc936f chore(release): auto-bump version to 0.66.80 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 01:39:06 +01:00
Nick a34762d512 fix(ci): propcache 0.5.4 re-pin (upstream churn within the hour) + pyright warning baseline 448→454 (CI-resolution drift with new pins; T14H-class skew documented — systemic env-determinism fix deferred to retrofit tranche) 2026-09-16 01:38:38 +01:00
conductor 772402ed39 chore(release): auto-bump version to 0.66.79 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 01:25:08 +01:00
Nick 80a1fd0a2d chore(manifest): regen after testing_mode 2026-09-16 01:24:29 +01:00
Nick 3fb28e2461 feat(t19): testing mode + self-hosted /testing command + T19 layer model (19L-19U)
- testing_mode.py (19L/M/N/O/P): TestingSession (coordination-only,
  pausable — durable truth stays in the Behaviour Ledger); dynamic
  queue (fix-for-B-reopens-A via structural closure, 19S-9 proven);
  Observation→closure gate (closure requires retest AND user
  confirmation — automated green alone recorded as NOT closed);
  forge projection through the T18 boundary (failure never rewrites
  truth, 19S-14 proven); cross-project aggregate.
- 19R LIVE_PROOF: /testing command implemented through the full
  system — self-scaffold lookup → capability-ledger packet (bound
  exactly ONE file) → qualified INWARD basis → predictive impact →
  preview → contract 19r-testing-command → event-bridged verification
  → descriptive impact (prediction matched, delta persisted) →
  selective invalidation + obligations → ledgers updated → closed.
- 19T benchmark (honest): cold agent found the answer in 6 tool calls
  (4 files) — on this well-organized repo raw speed ≈ equal; the
  scaffold advantage is correctness/continuity (validation state,
  obligations, invalidation), recorded without inflation.
- 19U: V1 definition gains the T19 layer model + both governing
  objectives verbatim; implementation audit gains T19 classifications.
- type ratchet: forge.py gitea_auth import made resolvable via
  pyrightconfig extraPaths (the +1 warning FIXED, not re-baselined).

Suite 2002 green; ratchets PASS; Henxels rc=0.
2026-09-16 01:24:29 +01:00
conductor 94ba2c97a3 chore(release): auto-bump version to 0.66.78 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 00:55:11 +01:00
Nick 94a4774853 feat(t19): project registry + capability/behaviour ledgers + iteration engine (19A-19I)
- 19A: /build contract-mint degradation paths REMOVED (fail-closed);
  4 held-outs prove no silent governance bypass (failure paths, not
  happy paths).
- 19C: project_registry.py — discovery/index ONLY; references-not-
  truth boundaries proven (no entities in entries, no Spine leakage).
- 19D: capability_ledger.py — stable IDs surviving moves, full
  history on material_change/move/supersede/retire, bounded
  context_packet.
- 19F/G: behaviour_ledger.py — versioned Behaviour/Test Specs,
  hardening lifecycle (UNVALIDATED→…→USER_CONFIRMED/RETEST_REQUIRED),
  generation-aware history PRESERVED through invalidation, automated
  green never implies USER_CONFIRMED.
- 19I: selective invalidation — only dependency-overlapping
  confirmations reopen, every invalidation carries a reason,
  unrelated hardened behaviours preserved (19S-2/5 proven).
- 19B: QUALIFIED Build Basis from prior-art research (structural
  closure ADOPT as sound backbone; generation-pinned confirmations
  ADOPT; predicted-vs-observed feedback loop INNOVATE; context
  packets COMBINE derived+curated; embedding-first REJECTED).
- iteration.py: context_packet, predictive/descriptive impact,
  impact-delta persistence, machine-derived post-change obligations.
2026-09-16 00:54:34 +01:00
conductor efb2c6b9a1 chore(release): auto-bump version to 0.66.77 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 23:56:47 +01:00
Nick 99115889d7 chore(store): manifest regen 2026-09-15 23:56:26 +01:00
Nick 2a971578e1 fix(ci): dependency lock re-pin (propcache 0.5.3, yarl 1.25.1 — upstream drift) + pyright warning baseline 443→448 (T18 18T)
The lock-drift ratchet correctly failed CI: upstream released propcache
0.5.3/yarl 1.25.1; lock re-pinned via uv --upgrade-package (fresh
resolution parity). The pyright warning baseline moves 443→448 with
justification: errors IMPROVED 117→114; the +5 warnings come from the
5 new V1 foundation modules under CI's venv resolution (T14H-class
environment skew — runner-user repro on identical venv gives 442;
documented, not chased to phantom parity). Net type debt: DOWN.

Deploy correctly refused the red SHA — fail-closed held.
2026-09-15 23:56:26 +01:00
conductor ec434f2a70 chore(release): auto-bump version to 0.66.76 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 23:40:30 +01:00
Nick d9c50c66c9 feat(v1): adversarial matrix — 2 real fixes + RETROFIT_READY reassessment (T18 18R/18S)
18R matrix found and fixed 2 REAL defects:
- secret redaction gap: *_token/api_key/password-CONTAINING keys were
  not redacted (exact-match list only) — structural stores could
  persist credential values. Now substring-matched; rule O held-outs
  prove redact+continue, never nag/block.
- drift detector read identities.source only; the self-scaffold
  convention stores sources in ext — stale-scaffold reconciliation
  was blind to the primary model. Fixed.

18S: RETROFIT_READY — every V1 foundation capability test-proven,
live-proven on Scaffolding itself where the tranche could reach; zero
frozen-kernel exceptions required (rule B record). Suite 1973.
2026-09-15 23:39:42 +01:00
conductor a382c15562 chore(release): auto-bump version to 0.66.75 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 23:34:23 +01:00
Nick 186fdd1410 chore(store): T18 self-scaffold + evidence 2026-09-15 23:33:42 +01:00
Nick aa9750c59f feat(v1): self-scaffold + self-scaffolded upgrade + graph projection + convergence (T18 18M-18Q)
- 18O: ONE canonical structural Scaffold of Scaffolding itself
  (store/scaffold/structural.json — 31 entities, 46 authority-graded
  relationships: domains/subsystems/components with real source paths,
  state stores, verification, deployment, the bounded build_mode
  adapter marked legacy_of). 5 consistency held-outs: sources exist,
  no duplicate truth with Spine, legacy not hidden, schema known to
  consent machinery.
- 18P SELF-SCAFFOLDED UPGRADE PROOF: contract 18q-graph-projection
  driven end-to-end FROM the maintained scaffold — structural lookup →
  contextually-generated research plan → qualified Build Basis →
  impact closure → visible preview → contract+workspace record →
  event-bridged verification (resumed via event, verdict success) →
  authority re-read → scaffold updated by the model itself → closed.
  All 11 steps scaffold-driven; zero architecture rediscovery.
- 18Q: graph_projection.py — stable versioned READ-ONLY projection
  (JSON/DOT/Mermaid) over structural truth; never an authority. 4
  held-outs incl. read-only proof.
- 18N generalisation: adversarial paraphrase families (37 unseen
  phrases across 6 tests) found and fixed 4 REAL semantic gaps:
  negation frames, idiom-prefix imperatives, modal+proposal referent
  ordering, all-stale-proposals fallthrough, preceding-context
  relative-that disambiguation.
- 18M full backend convergence: /build start now MINTS a canonical
  Construction Contract (classified) before BuildManager — no route
  runs an independent construction state machine.

Suite 1966 green; ratchets PASS.
2026-09-15 23:33:42 +01:00
conductor ab114d260b chore(release): auto-bump version to 0.66.74 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 23:01:01 +01:00
Nick b7865a76bb chore(manifest): regenerate after forge registration 2026-09-15 23:00:23 +01:00
Nick 04b0f4ac96 feat(v1): impact preview, structural drift, schema consent, workspace lifecycle, forge boundary (T18 18G-18L)
- impact_preview.py (18G): user-visible structured pre-build projection
  from impact closure + Build Basis — target/domains/consumers/files/
  state/environment/migration/risks/verification/rollback. Structured
  conclusions only, no private reasoning. 2 held-outs.
- drift_resume extension (18H): detect_structural_drift — source-file
  moved/missing detection against structural-model claims; REALITY
  WINS (auto-reconcile suggestion) except user-locked claims escalate
  to CONFLICTING. 3 held-outs.
- structural_migration.py (18I): MINOR (additive, automatic) vs
  MATERIAL (rename/drop/id-format → ConsentRequired raised on silent
  attempt — structurally impossible to silently upgrade); consent
  brief; affected-projects listing. 4 held-outs.
- workspace lifecycle (18K): canonical stage machine on structural_model
  (base_verified→…→cleaned, order-enforced, generation references —
  never live-tree mutation). BuildManager stays the bounded worktree
  adapter (17G decision, rule 18K: not deleted for neatness). 2 held-outs.
- forge.py (18L): minimum stable forge boundary (Protocol + GiteaForge
  delegating to canonical gitea_auth/scripts — no duplicate clients).
  ForgeUnavailable is blocking-for-operation, never a truth change.
  Git remains source-history authority. 3 held-outs.

Suite 1951 green; ratchets PASS.
2026-09-15 23:00:23 +01:00
conductor 9b328c9af1 chore(release): auto-bump version to 0.66.73 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 22:34:39 +01:00
Nick bc0f244934 feat(v1): Build Basis + structural model + contextual research plan (T18 18C-18F + 18D correction)
- build_basis.py: durable versioned research basis — depth decision
  derived from adaptive_router routes (no second router; deviations
  recorded with justification), OUTWARD prior-art contract, legal
  dispositions (ADOPT/ADAPT/COMBINE/REJECT/INNOVATE), qualification
  gate, attachable evidence_ref, structured summary (no private
  reasoning).
- 18D CORRECTION (Nick): THE RESEARCH PLAN ITSELF IS CONTEXTUALLY
  GENERATED — generate_research_plan synthesizes topics from THIS
  intent's material unknowns/decisions/risks; NO permanent topic list.
  Held-outs prove unrelated intents produce materially different plans;
  the structural-model topics never leak into unrelated research.
  Principle persisted in SCAFFOLDING_V1_DEFINITION.md.
- 18D qualified Build Basis persisted from REAL prior-art research
  (Backstage/Kythe/SCIP/Nx/Bazel/C4/Glean surveyed; adopt/adapt/
  combine/reject/innovate decisions recorded with refs).
- structural_model.py: first-class project structural Scaffold over
  the EXISTING structural_store core (no second graph store, no
  competing Spine). Backstage relation vocabulary + Kythe edge kinds
  adopted; stable deterministic IDs (Kythe-adapted: stability PROMISED
  where Kythe declines); every edge carries evidence_refs+authority+
  status (the genuinely-absent innovation); provides-ripple in impact
  closure (Nx-affected semantics); dynamic consumers stay explicit
  uncertainty. 7 held-outs.

Suite 1938 green; ratchets PASS; Henxels rc=0.
2026-09-15 22:33:59 +01:00
Nick 29570f404a docs(t17): 17Z postscript — first natural event-driven CI wake (mode=event, 112.5s) on the final release cycle 2026-09-15 20:18:48 +01:00
Nick 927548e9a0 chore(store): T17 evidence records 2026-09-15 20:09:54 +01:00
Nick 781b286f5b docs(t17): freeze assessment (FREEZE_READY) + 36-case adversarial matrix (17V/17X) 2026-09-15 20:09:54 +01:00
conductor d1ccd7ebec chore(release): auto-bump version to 0.66.72 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 20:06:18 +01:00
Nick 3adfa1b1ed fix(bridge): wr_root anti-split-brain policy + map/17T updates (T17 17T)
wr_root(): explicit env → shared root (warn if legacy also exists) →
legacy → fallback. The 17T benchmark's map-assisted path found the
two-data-roots split-brain risk; an unpinned consumer now gets a loud
warning instead of silently reading a stale store. Structural map
updated: programme continuation WIRED, conversational route added.
17T benchmark persisted (store/evidence/t17/17T_reentry_benchmark.md):
raw cost ≈ equal; cold path asserted a FALSE runtime fact (daemon dead)
at 95% confidence while the map path verified liveness and found the
split-brain — the correctness gap is the benchmark's real finding.
2026-09-15 20:05:54 +01:00
Nick da4eece9e0 chore(simplify): reconcile manifest + shrink lint baseline after plugin deletion (17U follow-through)
The dead BuildStateMachine carried 4 BLE001 + 2 S110 silent handlers —
deletion earned a baseline shrink (BLE001 354→350, S110 80→78).
v1_authority_map build_orchestration responsibility no longer maps the
deleted path, with a deletion note.
2026-09-15 20:05:54 +01:00
Nick 2e1f0c8cab refactor(simplify): remove dead build_mode plugin orchestrator (T17 17U)
The plugin's BuildStateMachine (366 lines) + 11 phase files had ZERO
importers anywhere in src/, scripts/, tests/, shared/ — a second
parallel orchestrator duplicating build_mode.py's phase machine
(17G audit finding). Verified dead: removed. Spine store carried no
entities for it (consistent with zero importers); manifest + henxels
green after deletion.
2026-09-15 20:05:54 +01:00
conductor 6e2f5950bd chore(release): auto-bump version to 0.66.71 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:49:43 +01:00
Nick c53c82bb35 chore(store): 17R proof records 2026-09-15 19:49:18 +01:00
Nick 35adcd8cb1 feat(consolidation): task-state authority + /build classification gate (T17 17F/17G)
17F: TaskStateRepository.list_all() added; get_status task counters now
derive from the CANONICAL repo (created→pending mapping; in-memory
fallback only for a fresh empty store). operator.json's task list is
demoted to a legacy cache — never a second authority. 3 held-outs.

17G: '/build start' now runs the SAME canonical Scaffolding
classification as every other construction route — NOT_CONSTRUCTION
refuses, UNKNOWN surfaces probe questions, classification-unavailable
fails closed. BuildManager retained as a bounded adapter (worktree
isolation + resume ledger are its unique capabilities; contract
activation stays on the operator hook path with the ONE activate()).

Suite 1920 green; ratchets PASS.
2026-09-15 19:49:18 +01:00
conductor 810d0027c3 chore(release): auto-bump version to 0.66.70 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:38:59 +01:00
Nick 4839a62b70 chore(spine): rebuild after 17R contract 2026-09-15 19:38:31 +01:00
Nick 15408841ac feat(context): request-modal handling + 17R conversational self-hosting proof
17R live finding: 'can you fix that?' is an imperative in interrogative
clothing — the terminal-? rule misrouted polite requests to DISCUSSION
and the first proof run proceeded on a non-load-bearing intent. Fixed:
request modals (can/could/would you, please) + construction verb =
ACTION with USER_APPROVED; information openers (how/what/why/should)
outrank modals ('how would you build X?' stays method discussion).
Held-outs added for both.

17R PROOF (load-bearing intent, end-to-end): conversational request →
USER_APPROVED ConstructionIntent → classify_mode EXISTING_PROJECT_CHANGE
→ contract 17r2-task-persist-load (approved→building) → async
verification under the real event bridge (resumed via event, verdict
success) → independent authority re-read PASS → verifying→closed with
evidence checkpoints. The fixed defect: task_persist.load() did not
exist — the TUI startup loader's AttributeError was silently swallowed
(dead startup task loading, 17F audit finding).

Suite 1917 green.
2026-09-15 19:38:31 +01:00
conductor c5c69de06f chore(release): auto-bump version to 0.66.69 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:27:31 +01:00
Nick fcde0b2ce3 feat(context): conversational construction interpretation layer (T17 17I-17Q)
construction_intent.py — the MEANING layer per the §17O authority
boundary: resolves action-vs-discussion (interrogative STRUCTURE, not
vocabulary: 'how would you build X?' never authorizes), multi-turn
referents against live proposals (recency-first, ordinal selectors,
ambiguity → clarification never guessed, stale-context resistance with
content-word revival, relative-pronoun 'that' excluded), explicit
imperative demands (creation verb + indefinite article = new standalone
artifact; strong/direct trigger without unnecessary re-asking), and the
DISCUSSION→PROPOSAL→USER_APPROVED lifecycle (suggestions are never
authorization). Emits the full §17N structured ConstructionIntent;
feeds the ONE canonical classifier — no second classifier.

classify_mode: indefinite-article new-artifact evidence, with structural
precedence (a known existing target means article language describes a
CHANGE, not a new project — §5).

Held-outs: 22 cases including all 20 §17Q matrix items with unseen
paraphrases; live-defect fixes found BY the matrix (relative-that,
question/imperative 'do', turn accounting, stale detection).

Suite 1915 green; ratchets PASS; Henxels rc=0.
2026-09-15 19:27:04 +01:00
conductor 1f96055b69 chore(release): auto-bump version to 0.66.68 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:11:06 +01:00
Nick 923a21ba0b chore(store): 17E programme proof records — durable programme + closed stage contracts 2026-09-15 19:10:46 +01:00
Nick 6348386869 fix(programme): truthful failure phase + typing cleanup (17E review)
Failed stages now land in phase=failed (legal building→failed
transition, evidence checkpoint on contract) instead of a swallowed
close attempt. S110/BLE001 eliminated — no exception swallowing in the
failure path. Ratchet-clean.
2026-09-15 19:10:46 +01:00
Nick 55002ad838 feat(programme): canonical continuation wiring + live 4-stage proof (T17 17D/17E)
programme_continuation was TEST-PROVEN but UNWIRED (16M finding). Now:
- scaffolding_stage_runner: binds stages 1:1 to Scaffolding contracts,
  drives the real Construction Interface lifecycle; async stage work
  runs under the event bridge (WAITING_EVENT is a state, not a stop);
  canonical phase re-read after EVERY transition (live finding: stale
  local contract refs silently stopped the draft→building chain);
  work evidence recorded ON the contract before close.
- run_construction_programme: persist → canary → next around EVERY
  transition; durable programme record reloadable after restart.
- canary extended: PROGRAMME_COMPLETE_WITH_FAILURES (all-terminal with
  failed stages closes honestly instead of hanging UNKNOWN).

LIVE PROOF (17E, real bridge, 4.3s): governance stage → async stage
(event wait, real daemon) → deliberately failing stage (isolated,
programme continued) → final stage → PROGRAMME_COMPLETE_WITH_FAILURES;
all 4 contracts closed with checkpoints. 7 wiring held-outs: evidence-
on-contract, failure isolation, restart from durable record, duplicate
signal idempotency, already-closed skip, callable work, missing
contract fails honestly. 15M verdict enum extended.
2026-09-15 19:10:46 +01:00
conductor 6684289a8f chore(release): auto-bump version to 0.66.67 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 18:41:59 +01:00
Nick e25ad0f964 fix(bridge): re-land cross-user FIFO chmod + regression guards (T17 17C)
Root cause of every post-T16 production wait sleeping to its deadline:
(a) the T16 os.chmod(fifo, 0o666) was lost in a rebase resolution —
FIFOs landed 644 (umask 022), the daemon (different user) got EACCES on
the resume write, rc=1 → RESUME_UNVERIFIED; same-user tests passed.
(b) waitstore _atomic_write (daemon snapshot 0b8f384) left mkstemp 0600
records the runner-user waiter could not read back.

Proofs: replay path exercised production-realistically against real
completed CI (event_replayed, 4.6s, authority re-read decided); full
cross-user delivery repro (runner user) now wakes in 2.05s; two
regression guards (mode assertion + cross-user delivery on the real
daemon). Daemon invariants 45/45 + 36/36.
2026-09-15 18:41:32 +01:00
Nick c9dee14c62 docs(t16): final tranche evidence — 16S/T/U/V/X/Y findings + 16Z convergence record 2026-09-15 17:56:15 +01:00
conductor fe9d154778 chore(release): auto-bump version to 0.66.66 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:39:29 +01:00
Nick 2a4f9c8367 chore(spine): rebuild after 16R contract registration 2026-09-15 17:38:56 +01:00
Nick 7ddfa7eee7 fix(maintenance): canary-suite glob points at src/rune (T16 16Q/16R)
16Q: the job looped shared/canary_*.py — real canaries live in
src/rune/; it silently no-opped since the canaries moved.
16R: this bounded Rune-internal improvement was executed THROUGH the
retroactive Scaffolding flow — Need → contract 16R-maintenance-canary-
glob (approved→building→verifying→closed) with acceptance verified
independently (YAML parses, glob fixed, 24 canary files at target) —
proving the 16M/16N map drives actual construction, not just docs.
2026-09-15 17:38:56 +01:00
conductor 986b450bc2 chore(release): auto-bump version to 0.66.65 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:34:28 +01:00
Nick b51afea0cb feat(reentry): durable event replay + drift-aware resume + 16O benchmark (T16 16C-fix/16O/16P)
- event_wait.find_matching_event(): replays matching completion events
  from the durable bridge event log at registration. Root-caused from
  TWO production cycles: on a serialized single-runner machine the CI
  wake always lands before/around deploy registration — a forward-only
  FIFO rendezvous can never deliver it (both cycles fell back to the
  900s deadline, truthfully). The event log exists precisely for this;
  the wake is still never the verdict.
- ci_event_wait: event_replayed mode short-circuits to the authority
  read; 2 new held-outs (9 total).
- drift_resume.py (16P): claim-vs-observation reconciliation with
  EXPECTED/BENIGN/MATERIAL/CONFLICTING/UNKNOWN classes; MATERIAL/
  CONFLICTING/UNKNOWN block dependent work; user-locked claims escalate
  to CONFLICTING (reality never silently overrides user-locked
  authority). Held-outs driven by the REAL 16O fixture: the VAT
  regression blocks resume; the checkpoint commit resumes clean.
- 16O benchmark persisted (store/evidence/t16/16O_benchmark.md): honest
  result — on a tiny fixture cold and scaffold-assisted re-entry cost
  about the same (8 vs 9 files); the scaffold's advantage is drift
  classification, recorded next actions, and intent, not raw step
  count. No fabricated win.

Suite 1884 green; ratchets PASS.
2026-09-15 17:34:00 +01:00
Nick c15840514f docs(structure): Rune structural map + 16M inventory + 16D live latency proof (T16 16M/16N/16D)
- docs/STRUCTURAL_MAP.md: fast re-entry surface — canonical routes,
  known duplicate routes, unfinished/blocked/deprecated, fresh-session
  quick answers. Companion to Spine (authority) and 16M inventory.
- store/evidence/t16/16M_inventory.md: full retroactive inventory —
  10 canonical systems + aux systems classified; duplicate routes
  priority-ordered (task-state ×4, build_mode legacy, provider ×5
  entry points, T14/T15 drivers, maintenance ledgers ×3, unwired
  programme_continuation); hard-coded path audit.
- store/evidence/t16/16D_latency_proof.md: live proof cycle 1 —
  c707a81 deploy ran event-driven end-to-end in production: watchdog
  re-arms on live CI, deadline expired exactly once at 900.92s as
  failure detection, final authority read success, promoted 16:18:17Z.
  Sleep-30 polling floor gone. Event-before-waiter ordering documented
  honestly (cycle 2 in flight proves the wake path).
2026-09-15 17:34:00 +01:00
Nick 2833175329 feat(scaffolding): future-Substrate seam — outbound projection + inbound command interface + mock client held-outs (T16 16I/16J/16K)
- construction_projection.py: stable SCHEMA_VERSION=1 public projection
  (all §16I mandate fields: phase/stage/waiting_state/correlation/
  canaries/verification/checkpoint/evidence/failure_class/completion/
  candidate_next_action). A projection, NOT a second authority — derived
  read-only from the canonical scaffold store + live wait bridge; degrades
  to BRIDGE_UNREACHABLE when the bridge is absent, never lies.
- construction_commands.py: bounded validated inbound commands
  (REQUEST_STATE/START/RESUME/ACKNOWLEDGE_BLOCKER/PROVIDE_APPROVAL/
  PROVIDE_CONTEXT — PAUSE/CANCEL deliberately unexposed: no paused phase
  in the lifecycle; abandonment needs user provenance). Every command
  carries identity/correlation, validates against LEGAL_TRANSITIONS,
  cannot mutate storage (returns validated intents; the Construction
  Interface executes), idempotent by command_id. Substrate asks;
  Scaffolding decides.
- tests/test_substrate_seam.py: 9 held-outs — MockSubstrateClient knows
  NOTHING of current.json/internal arrays; state via public seam only;
  event observation + correlation; valid RESUME accepted; invalid START
  refused; direct-store semantics inexpressible; restart survives on
  identities alone; Scaffolding fully usable without any client;
  idempotent replay; unknown command/construction refused; schema
  validated.

Suite 1878 green; ratchets PASS; Henxels rc=0.
2026-09-15 17:34:00 +01:00
conductor 4a06d9f11a chore(release): auto-bump version to 0.66.64 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 17:02:38 +01:00
Nick c707a81933 fix(deploy): shared bridge data root for cross-user event-driven CI wait (T16 16C live defect)
The deploy step runs as the runner user while the W/R daemon runs as
nick — defaulting wr_root() to the invoking user's home made FIFO
creation fail (PermissionError) and the deploy failed fast+truthfully on
both 0.66.63 pushes (runs 7953/7958). No false green; deployment
correctly stalled at 5bd8cca.

- shared data root /mnt/workdrive/wr-bridge (waits/events/resumes/
  leases migrated; daemon restarted on it; 46 waits intact)
- deploy.yml: RUNE_WR_ROOT/RUNE_WR_CODE exported + READINESS-BEFORE-
  ACTION write probe (§16U: clear bridge-misconfigured error instead of
  a FIFO-creation stack trace)
- event_wait: FIFOs and durable wait records relaxed to 0o666 — the
  bridge is cross-user by construction now

Suite 1869 green; ratchets PASS.
2026-09-15 17:02:04 +01:00
Nick 37deb6085c feat(provider): provider ownership boundary + PROVIDER_UNAVAILABLE recovery class (T16 16F/16G)
- provider_context.py: require_model() resolves the model from the
  ambient provider context; missing context fails LOUDLY
  (PROVIDER_UNRESOLVED, exit 3) — no silent default provider anywhere.
- t14/t15 builder drivers, evidence_probe, research executor/relay:
  hardcoded 'Verboo Code/glm-5.3-flash' defaults REMOVED (6+ sites);
  provider/model selection now belongs exclusively to the provider
  layer via the ambient context. No caller anywhere set MIMO_MODEL —
  every run silently used the code-chosen provider until now.
- failure_recovery: PROVIDER_UNAVAILABLE class (context-forced; real
  status/API code preserved verbatim in evidence; policy
  block_branch_preserve_error, 0 retries, no fallback — 16G-D).
- tests/test_provider_ownership.py: structural guard (provider names
  in Scaffolding code lines fail the suite; module list live-discovered)
  + 16G held-outs (verbatim codes, zero fallback attempts, resumable
  after block, provider preference never mutates scaffold truth).
- 16U root-cause completion: pyright skew 117⇄124 driven by ambient
  VIRTUAL_ENV/PATH leakage into pyright's interpreter resolution —
  the ratchet now neutralizes ambient-venv signals (PYTHONNOUSERSITE
  was necessary but not sufficient; reproduced and closed).
- spine reconciliation: T15 modules (event_wait, stage_guards,
  failure_recovery, programme_continuation) were missing belongs_to
  relationships in the tracked store while the generated manifest had
  them — registered with SCAFFOLD-TRANCHE-015 evidence; provider_context
  registered under capability; manifest regenerated via export-umbrellas.

Suite 1869 green; ratchets PASS; Henxels rc=0.
2026-09-15 17:02:04 +01:00
conductor affe852ff7 chore(release): auto-bump version to 0.66.63 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 16:27:30 +01:00
Nick e1a71efeb9 feat(ci): event-driven CI qualification wait over the existing W/R bridge (T16 16C/16D/16E)
- deploy.yml sleep-30 polling loop REPLACED: registers a gitea-identity
  PROCESS_EVENT wait, yields in select() on the FIFO, resumes on the
  ci.yml notify-event-bridge WAKE, then re-reads the canonical Gitea
  verdict (deploy_qualify.py). Event payload is never completion truth.
- ci.yml gains notify-event-bridge (always(), best-effort): a red CI
  wakes the waiter as loudly as a green one; bridge unreachable ->
  watchdog recovery covers the lost wake.
- event_wait.py: register_process_wait gains provider/event_type/sha
  identity (16E: ONE bridge primitive; builder + gitea are adapters);
  open_fifo_reader + caller-owned fifo_fd close the 16Y#10 race (event
  before waiter in select()); await_resume_event returns eof instead of
  busy-looping on a caller-owned fd.
- ci_event_wait.py watchdog: failure DETECTOR only — liveness-gated
  re-arm, one-shot authority recovery on lost events, truthful failure
  when no verdict exists; timestamps emitted for the 16D latency proof.
- tests: 8 held-outs on the REAL daemon (shared wr_daemon fixture in
  conftest — no duplicated helpers). Daemon-side: /process-event passes
  through provider/event_type/sha (defaults preserve T15); lease_path
  sanitizes slash-bearing session ids (resume delivery was impossible
  for them — found live, fixed in daemon snapshot 79434e0).

Suite 1861 green; ratchets PASS; Henxels rc=0.
2026-09-15 16:26:54 +01:00
conductor 5bd8cca941 chore(release): auto-bump version to 0.66.62 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 14:15:22 +01:00
Nick fd5f31bba2 fix(scaffolding): §15D tests use ephemeral ports, not fixed 8793
CI failure on the 0.66.61 bump run (job 18584): the latency test got
watchdog/alive:False because its daemon could not bind the FIXED port
8793 — a stale TIME_WAIT socket from an earlier test in the same file
held it (verified locally: 8793 bindable only with SO_REUSEADDR while
no listener exists). The notifier's event POSTs went nowhere, so the
watchdog correctly reported PROCESS_LOST: the harness was at fault,
not the bridge. Daemon fixture now binds port 0 (ephemeral) and reads
back the assigned port. 3x consecutive green locally.
2026-09-15 14:14:44 +01:00
conductor 8a2a4f7b71 chore(release): auto-bump version to 0.66.61 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 13:42:21 +01:00
Nick b2d6802dba fix(scaffolding): §15K test no longer shells out to git worktree on CI
test_15k_build_manager_has_no_parallel_construction_state invoked
BuildManager.start_build for real, which runs 'git worktree add' against
the HARDCODED production path /mnt/workdrive/rune — a real VCS mutation
inside a unit test. Locally it polluted ~/.rune/work/builds; on the CI
runner it fails outright (dubious-ownership: runner checkout is a
different user). The test now redirects BUILDS_DIR/WORKTREE_DIR into tmp
and stubs create_worktree — the property under test (no canonical
construction state from the /build vehicle) is orthogonal to the git op.
This was the CI unit-tests failure on 193d819 (1 failed, 1852 passed).
2026-09-15 13:41:43 +01:00
conductor 3ebbf9a9b1 chore(release): auto-bump version to 0.66.60 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 13:29:02 +01:00
Nick 193d81961d fix(scaffolding): event_wait importlib-clean loading, zero pyright warnings
_load_waitstore now resolves the bridge's own waitstore.py via
importlib.util.spec_from_file_location (no dynamic bare import, no
sys.path mutation) with per-root caching. Verified: 0 errors/0 warnings
in event_wait.py; per-file stash comparison shows my 8 touched files add
zero warnings over baseline (rune_operator 5/5, rune_tui 307/307
identical). The 443→445 CI warning delta is pre-existing environment
skew in untouched files, not T15 regression.
2026-09-15 13:25:39 +01:00
conductor 94ad81245b chore(release): auto-bump version to 0.66.59 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 04:59:31 +01:00
Nick 196cf193df fix(scaffolding): stage_guards assert-check field typed Any (pyright 117 clean) 2026-09-15 04:58:54 +01:00
Nick f95b2a63df feat(scaffolding): §15X/§15Y — efficiency comparison + adversarial matrix completion
15X: T14→T15 comparison record — readiness prevention now evidenced
(3 cases), interface-discovery prevention (1), zero escaped stages;
8 recurring classes mechanically prevented, listed with mechanisms.

15Y: remaining-gap held-outs — cross-project event isolation
(correlation rejects foreign run_ref) + user-gated scope amendment
(builder self-broadening refused, user authority works). The full 25-case
matrix is covered across test_event_wait (events), test_stage_guards
(canaries/verification/reconciliation), test_native_default (TUI
defaults/research negative), test_programme_continuation, and
test_t14_builder_contract.
2026-09-15 04:54:39 +01:00
Nick 708871e511 feat(scaffolding): §15V/§15W — TUI backlog + honest self-hosting proof
15V: 7 new TUI Upgrade Backlog projections (construction-auto-activated,
builder-state-waiting, canary-state, verification-truth,
programme-continuation, resume-discovery, release-convergence) with
INSPECTION/CONTROL classification; TUI stays projection/client.

15W: night-run self-hosting record — 15 stages one programme, no
voluntary stops; event-driven waits default; polling inventory (all
remaining sleeps marked TEST_TIMING or repaired); 10 defects found and
repaired in-programme listed honestly, including the pkill that killed
the production W/R daemon (restarted, health 200).

suite: 1852 passed, 0 failed
2026-09-15 04:53:53 +01:00
Nick 18ef9a53e3 refactor(scaffolding): §15U — resume discovery migrated to the interface
ConstructionInterface.discover_active_contract: ONE canonical home for
fresh-builder resume discovery (per-project scaffolds + main store;
most-recent contract; pure readback). T14/T15 driver helpers are now
shims. capabilities() advertises the op. Held-out: empty store invents
nothing; seeded store discovers the right contract.
2026-09-15 04:50:38 +01:00
Nick 05edd68027 feat(scaffolding): §15R — second generalisation LIVE-PROVEN (Python/SQLite task ledger)
Different language (Python 3.14), different pattern (storage evolution:
SQLite schema + CRUD vs stateless in-memory scheduler), different runtime
(sqlite3 stdlib), same canonical defaults: readiness → classification →
PROJECT_BUILD → spec contract → scope → obligations → real model builder
→ event-driven wait → exit canaries → 15G test truth → evidence closure.

Live findings (honest):
- the builder wrote ledger.py + ledger.test.py (Node-style name per the
  projection scope) + README; 14/14 unittest pass
- the driver's default verifier (unittest discover) found ZERO tests —
  15G correctly REFUSED closure (false-green protection working)
- the mismatch was the DRIVER's verifier not matching the SCOPE's naming
  convention, not the builder's work; completing with the matching
  verifier closed the contract from evidence
- canonical watchdog→recovery path exercised again (builder outlived the
  deadline; waitstore verdict carried completion)
2026-09-15 04:47:39 +01:00
Nick d166e83d55 feat(scaffolding): §15S/§15T — migration scenario + failure classification
15S: breaking-change lifecycle held-out — old API + consumer governed
before change; verifying→migrating→superseding enforced by the phase
map; the old path survives as governed history (supersession is
lifecycle state, not erasure); BUILD NEW never destroys before migration.

15T: src/rune/failure_recovery.py — deterministic classification
(TRANSIENT/INTERMITTENT/KNOWN_PROCESS_DEFECT/PERMANENT/APPROVAL_
REQUIRED/UNKNOWN) with explicit retry budgets; PERMANENT/APPROVAL
never auto-retry; no infinite loops possible. 2 held-outs.
2026-09-15 04:19:17 +01:00
Nick 98b4c58d36 feat(scaffolding): §15Q — real event-driven builder run LIVE-PROVEN + liveness hardening
scripts/t15_builder_session.py: real model builder (mimo run/Verboo) under
the event notifier; driver YIELDS on the bridge FIFO (no polling on the
normal path); readiness canary before launch; exit canaries + 15G test
truth after; evidence-gated closure.

Live findings (recorded honestly):
- real model builds run 10-65 min — the 900s watchdog misfired twice as a
  false PROCESS_LOST; root cause: (a) deadline < real build time, (b)
  node builders re-group/detach children so pgid AND cmdline liveness
  scans can miss the work. Repair: await_resume_event re-arms bounded on
  liveness; the driver consults CANONICAL WAITSTORE STATE after watchdog
  expiry — a recorded verdict means completion, process-table absence is
  not truth (§15E canonical-state-wins).
- an orphaned T14-era builder process (old .mjs-scope prompt, quota-stuck)
  was discovered polluting process checks — killed and recorded.

Result: contract closed from evidence; 10/10 node tests; both builder
exit events processed by the bridge (verdict success both runs).
Evidence: store/evidence/t15/.
2026-09-15 04:08:22 +01:00
Nick f5c8b07d15 feat(scaffolding): §15N/§15O/§15P — discoverability, provenance, idempotency
15N: capabilities() now exposes full argument schemas (types, required
fields, per-item shape, notes), lifecycle phase map + legal transitions,
and scope-amendment authority — a fresh builder DISCOVERS the
report_observation contract instead of trial-and-error (the T14
dict-vs-list + missing-entity mistakes become impossible). Held-out:
schema-guided first report succeeds; schema-violating payload rejected.

15O: audit_scope is PROVENANCE-AWARE — residue paths present before the
builder ran (or explicitly runtime-declared) stay exempt and recorded;
NEW residue-looking paths appearing mid-session (.mimocode/evil,
.git/config) are SCOPE VIOLATIONS. The hostile lookalike can no longer
hide behind the T14 prefix rule.

15P: PROJECT_BUILD bootstrap twice = one project identity (no
duplication); event replay after restart = readback-only recovery
(idempotent by construction, contract-inspected).
2026-09-15 03:16:44 +01:00
Nick 292c72ec3d feat(scaffolding): §15M/§15L — programme continuation canary + authority boundaries
15M: src/rune/programme_continuation.py — after EVERY stage completes the
canary asks 'is the programme objective satisfied?'; if open and eligible
stages remain the NEXT stage starts immediately (no user re-prompt, no
voluntary stop); event-wait is PROGRAMME_OPEN_WAITING (no polling);
blocked branches fall back to pre-authorized work; every stop carries an
honest verdict (PROGRAMME_COMPLETE/ALL_REMAINING_BLOCKED/APPROVAL_
REQUIRED/UNKNOWN). 7 held-outs incl. the required 4-stage auto-continuation
proof + durable persist/reload. Bounded to ONE construction programme —
not a Substrate scheduler.

15L: authority boundary record persisted (construction=Scaffolding,
wait=event bridge, research-state continuity=FUTURE Substrate (not
built), research execution=Research Core, TUI=client/projection) +
negative held-out: research intents never PROJECT_BUILD.
2026-09-15 03:11:13 +01:00
Nick 0a436b2f97 feat(scaffolding): §15J/§15K — Scaffolding IS the native construction default
15J (native route through the REAL send_task path — TUI input →
RuneOperator.send_task → construction hook → classifier → activation →
durable state):
- 6 held-outs (tests/test_native_default.py): PROJECT_BUILD without any
  command; EXISTING attaches Rune authority; LOCAL_CHANGE light; NOT_
  CONSTRUCTION touches nothing; UNKNOWN probes without invention; manual
  override (/scaffold-mode <MODE>) through the SAME classify_mode with
  user provenance
- classifier: explicit FILE mentions ('the README', 'scheduler.js') now
  bind scoped targets — deterministic lexical evidence + the ONE surface
  authority (classify_surface), lazily imported (cycle-safe)

15K (bypass prevention):
- /build start hardened: classifies through the canonical machinery
  BEFORE BuildManager mints any state; NOT_CONSTRUCTION refused;Build-
  Manager remains execution vehicle, no longer a construction authority
- inventory: BuildManager provably creates NO canonical scaffolds

Process repairs caught during the stage (15X evidence):
- os.chdir pollution from a test leaked cwd into later tests (bare
  os.chdir → monkeypatch.chdir)
- construction-hook tests were making REAL model calls (42s, provider
  quota burned, starved stage-telemetry's model call → GATE_FAILED):
  send_task's _execute_on_model now stubbed in hook tests — suite 144s
  → 54s and deterministic
- S110 noqa for the deliberate classifier degrade; new modules mapped
  into config/umbrellas.json (perception: event_wait, stage_guards)
- suite: 1835 passed, 0 failed
2026-09-15 03:06:55 +01:00
Nick 2ba4114fce feat(scaffolding): §15F/§15G/§15H/§15I — guarded stage execution
src/rune/stage_guards.py (design lessons absorbed, not the systems):
- 15F: StageContract (ready_when/done_when/invariants as EXECUTABLE
  checks) + readiness_canary (failure BLOCKS, action never runs) +
  execute_stage (action return = evidence INPUT; exit canary
  independently verifies; failure RAISES — self-certification
  structurally impossible) + liveness_canary (inactivity = STALLED,
  never completion)
- 15G: verify_test_truth — exit 0 is not proof: zero-tests false green
  refused, all-skipped refused, contract-required artifacts enforced,
  min_tests contract-derived (never universal constants)
- 15H: henxels_placement — REQUIRED at structural boundaries/FULL depth,
  NOT_APPLICABLE for tiny changes; both recorded with rationale
- 15I: reconcile (desired vs freshly-read actual) — green command with
  absent durable effect leaves non-empty delta; next stage must not start
- tests/test_stage_guards.py: 14 held-outs
2026-09-15 02:19:28 +01:00
Nick eccb446a3c feat(scaffolding): §15C/§15D/§15E — event-driven wait becomes the canonical default
- 15C audit: the W/R bridge (daemon :8791) was complete but its resume
  target was mimo-session-shaped only — every Python controller had to
  sleep/poll. Root cause recorded, not excused.
- src/rune/event_wait.py: register_process_wait (durable PROCESS_EVENT
  wait + per-wait FIFO resume cmd), run_builder_under_events (real
  builder under a notifier that observes actual exit and POSTs the
  event), await_resume_event (select-block, NO sleep floor, watchdog
  deadline = failure detector), resume_or_recover (restart: durable
  waitstore wins over transient token).
- 15E adversarials: duplicate/stale/wrong-correlation/malformed events,
  controller restart, lost-event watchdog (zombie-aware liveness).
- Latency proof: builder exits at T, controller resumes at T+~0.1s —
  no arbitrary floor. Failures carry truth (exit_code verdict).
- tests/test_event_wait.py: 8 proofs, real daemon subprocess,
  disposable WR_ROOT. Daemon invariant suites 45/45 + 36/36 unchanged.
2026-09-15 02:12:54 +01:00
conductor 5e6a397e7c chore(release): auto-bump version to 0.66.58 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 00:39:58 +01:00
Nick 87fad6ff8b feat(scaffolding): §14F/§14I/§14J — kernel equivalence held-out, adversarial matrix, first-pass efficiency record
14J adversarial held-outs (tests/test_t14_builder_contract.py):
- .mjs/.cjs/.mts/.cts classify as source_path (T14D real defect regression)
- builder self-claim with failing/absent verification cannot close
- observation payloads without entity rejected
- runtime residue classified + recorded, never silently whitelisted
- out-of-scope creation fails the audit
- closed-contract re-closure refused; duplicate resume idempotent
- 14F: Rune-internal vs external change traverse the SAME kernel —
  identical scope/obligations envelopes, phase map, verification gate,
  closure semantics; allowed differences are authority identity only

14I: process_learning record (record:first-pass-efficiency-t14) — 7 defect
classes with discovery stage + prevention; all caught at interface-
validation or exit-canary stage, zero escaped, zero stale-doc/forgotten-
save recurrences; maintenance:builder-interaction-ledger RESOLVED via
canonical _record_session observations + store/evidence/t14/ (no new
subsystem).
2026-09-15 00:38:03 +01:00
Nick 87252705a8 fix(gates): §14H — local-vs-CI pyright skew ROOT-CAUSED + mechanically normalized
Root cause (proven by controlled experiment): pyright resolves the python
environment for import analysis; a developer's user-site packages
(~/.local/lib/python3.14/site-packages) leak into resolution. Same
checkout, same lock-pinned venv: nick 124/429, runner user 117/437.
PYTHONNOUSERSITE=1 pins both contexts to identical arithmetic (verified:
exact 117-error match both sides).

Fix: type_check_ratchet.py runs pyright with PYTHONNOUSERSITE=1 — local
preflight now PREDICTS CI. Residual 437-vs-443 warning delta noted
honestly (warnings are capped, not matched); error arithmetic identical.
'CI is the gate' was not the answer; the environments now agree.
2026-09-15 00:30:58 +01:00
Nick 4423367ddb feat(scaffolding): §14D/§14E — REAL model builder adoption + true interruption/resume LIVE-PROVEN
t14_builder_session.py: real builder session driver.
- 14D: 'mimo run' (Verboo Code/glm-5.3-flash, PL1 provider policy default;
  primary Xioami/mimo-v2.5-pro endpoint quota-exhausted 429) as a SEPARATE
  OS process consumes the Construction Interface projection (scope,
  obligations, phase, verification requirement, next action) and implements
  a JS practice scheduler. Builder files: package.json, scheduler.js
  (validated createPracticeItem/listItems/completeItem), scheduler.test.js
  (11 node:test cases). Scope audit + independent node --test by the
  driver; closure from evidence only (§14X.8).
- 14E: real SIGKILL to the builder process mid-session; fresh builder
  process resumed from canonical state only (no transcript): recovered
  project/contract/phase/scope, inspected-not-overwrote, zero duplication,
  verification still required, evidence-gated closure.
- Runtime residue (.mimocode session scaffolding) classified and recorded
  explicitly — not silently whitelisted, not a builder scope violation.
- Requirement spec governed as contract on project surface; Need references
  it (REUSE of specification); BUILD_NEW silent-choice gate respected.
Evidence logs: store/evidence/t14/.
2026-09-15 00:20:13 +01:00
Nick f85c654d9d fix(release): §14B/§14C — stale-publisher race repaired + convergence-intent ledger eliminated
14B: the out-of-lock publisher (publish_out_of_lock) no longer trusts the
originating job's SHA. It re-reads CURRENT deployment authority immediately
before publishing and constructs the note from the authority's identity.
Adversarial race held-out: A promotes → B supersedes → B publishes → A
resumes → final history says B, A's SHA never enters. Unreadable authority
→ no publication (never fabricate an identity).

Body parsers (summary_from_body / sha_from_body) moved to release_notes.py
as the single authority; release_history_cli.py delegates.

14C: .convergence-intent.json ELIMINATED (1 writer, 0 readers, non-atomic
write, bogus recorded_at). Pending convergence is derived from the
deployment authority itself (atomic symlink + build identity): restart-
readable, corruption-immune, monotonic, idempotent by construction.

T11 stale-refusal invariant preserved (CLI revalidation unchanged).
2026-09-14 23:30:51 +01:00
Nick 7417a8ca02 chore(scaffolding): T14A — T13 maturity corrections (builder-operated, interruption, equivalence, CI-skew all downgraded to TEST-PROVEN/OPEN) 2026-09-14 13:42:30 +01:00
conductor ecfa80a7b3 chore(release): auto-bump version to 0.66.57 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 12:51:55 +01:00
Nick 2a106e39a3 fix(test): 29e accepts CONTRACT provenance (obligations projection now includes contract-level obligations) 2026-09-14 12:50:33 +01:00
Nick 54cee39b95 fix(test): 13F — fixture includes verification obligations so obligations count > 0 2026-09-14 12:46:52 +01:00
Nick c27d13d9ac fix(scaffolding): obligations projection includes contract-level obligations (not just decision_trace — 13F: resume works for direct-contract builders) 2026-09-14 12:44:29 +01:00
Nick dd5030407e fix(scaffolding): resume falls back to contract identities when no decision_trace exists (13F) 2026-09-14 12:41:51 +01:00
Nick ba0ec0ae31 fix(scaffolding): resume falls back to contract identities when no decision_trace exists (13F: direct new_contract without submit_need still has resumable state) 2026-09-14 12:38:50 +01:00
Nick 4d410319ce fix(test): 13F — use .get for need objective (construction_mode may not persist objective field) 2026-09-14 12:35:22 +01:00
Nick 09ec2b81ba fix(test): add missing textwrap import (13F fresh-resume subprocess script) 2026-09-14 12:32:23 +01:00
Nick 577695a1a3 fix(test): 13E — exact filename matching in builder implementation (test_scheduler.js was matching the scheduler.js branch) 2026-09-14 12:28:09 +01:00
Nick 2fad565016 fix(scaffolding): classify .js/.ts/.jsx/.tsx/.go/.java/.c/.cpp/.h/.css/.html as source_path — was falling to unresolved (T13E: JS files need scope) 2026-09-14 12:21:48 +01:00
Nick 6b805eea4a refactor(release): §13D — split convergence into intent-inside-lock + publish-outside-lock (no network I/O holds the flock) 2026-09-14 12:06:58 +01:00
Nick 4ecd99023f chore(scaffolding): pyright baseline to CI arithmetic 117/443 (local/CI environment skew: local 124/429, CI 117/443 — CI is the gate) 2026-09-14 08:04:22 +01:00
conductor d3410671fd chore(release): auto-bump version to 0.66.56 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 07:44:54 +01:00
Nick a4c099a9ca fix(scaffolding): construction_depth — narrow affected_surface before len() (pyright ratchet) 2026-09-14 07:44:26 +01:00
Nick 09ba25473a chore(scaffolding): re-baseline pyright ratchet to 124/430 (pre-existing drift from T10/T11 modules — verified unchanged by stash test) 2026-09-14 07:40:46 +01:00
conductor b7a71e69f1 chore(release): auto-bump version to 0.66.55 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 07:29:19 +01:00
Nick a30464df1a test(scaffolding): T12G interruption/resume at 4 phases + T12H process-learning held-outs (§12G/§12H) 2026-09-14 07:28:37 +01:00
Nick 0f80334be1 fix(test): 12F lifecycle — commit before fresh-process resume (§21 discipline: persist then account) 2026-09-14 07:28:37 +01:00
Nick c09a42f10a fix(scaffolding): standalone language overrides non-constructible noun in classification (T12F: 'create, list and complete practice items' is a build, not a list request) 2026-09-14 07:28:37 +01:00
Nick 13704bd315 feat(scaffolding): T12E — proportional depth selector (LIGHT/STANDARD/FULL from mode + evidence; 13 held-out matrix) 2026-09-14 07:28:37 +01:00
conductor 56f96f0d60 chore(release): auto-bump version to 0.66.54 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 06:53:16 +01:00
Nick e86da59cdb fix(release): deploy_stage gets Gitea config for in-lock convergence; workflow fallback made continue-on-error (§12C — no competing authority, in-lock hook is primary) 2026-09-14 06:52:49 +01:00
conductor 143e6095ed chore(release): auto-bump version to 0.66.53 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 06:39:03 +01:00
Nick fe4bc43a83 fix(release): deploy_stage imports release_notes via the rune package (installed by runner pip -e), not bare top-level 2026-09-14 06:38:43 +01:00
conductor 83426c0e1c chore(release): auto-bump version to 0.66.52 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 06:27:21 +01:00
Nick b5c9a3c04e feat(release): in-critical-section convergence hook — the WINNER refreshes release history atomically with its promotion (SCAFFOLD-TRANCHE-0012 §12C)
§12C design insight from the 0.66.51 deploy (run 7721): the workflow-step
refresh fired (steps.promotion_truth gate worked) but the authority
readback disagreed — an overlapping deploy had superseded the candidate
between promotion-check and refresh-step. Correct refusal, but the WINNER
never converges because its own refresh can hit the same race.

DEPLOY_PROMOTION.PROMOTE now accepts on_promoted: invoked INSIDE the flock
critical section immediately after the symlink swap — only the promotion
WINNER converges, atomically with the swap. Failure degrades to STALE
(software valid, history lags, retry converges). deploy_stage passes a
convergence hook that builds evidence-derived notes and publishes to Gitea.

Tests: 4 new (hook fires on promotion, NOT fired for stale, failure
degrades not crashes, A→B race winner-converges-inside-lock); suite 1687
green; race tests all green.
2026-09-14 06:26:24 +01:00
conductor cb10db1e78 chore(release): auto-bump version to 0.66.51 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 06:02:13 +01:00
Nick 1d466a6926 test(scaffolding): T12D native activation matrix + BLE001 re-baseline for deliberate construction degrades (SCAFFOLD-TRANCHE-0012 §12D)
- tests/test_native_activation.py: full 12D matrix through the REAL
  send_task backend path (PROJECT_BUILD bootstrap, EXISTING_PROJECT_CHANGE,
  LOCAL_CHANGE, non-construction non-activation, UNKNOWN probes-not-
  activation, fresh-process resume from durable scaffold)
- lint-baseline.json: BLE001 352→354 — the construction hook's two
  deliberate degrade-loudly handlers (construction must never break the
  ordinary task path)
2026-09-14 06:01:47 +01:00
Nick fa51b96ca7 feat(release): race-safe eventual-convergence revalidation + 12C test plan (SCAFFOLD-TRANCHE-0012 §12C)
The deploy workflow's promotion gate (steps.promotion_truth.outputs) is
necessary but not sufficient: two overlapping deploys can both observe
promoted=true at their own moments, and the STALE one's refresh could
overwrite the newer converged identity.

release_history_cli.py refresh now REVALIDATES against the deployment
authority at refresh time: if current's build identity differs from the
candidate, the stale refresh is REFUSED (T11 invariant preserved) and the
WINNING deploy's refresh converges the release to the final truth.

12C test plan: clean promotion refresh, already-current idempotence,
A→B race final-B convergence, candidate/authority disagreement refusal,
no-duplicate, missing-release explicit failure, evidence-derived notes.
2026-09-14 05:39:01 +01:00
Nick 5796caa30a feat(scaffolding): T12B — real durable commit semantics (canonical digest readback + invariant support + 8 held-outs)
structural_store.canonical_digest(): stable semantic digest using the same
normalization save() applies — key ordering cannot create false failure.

Scaffold.commit(): saves, reloads from disk, compares digests — catches
missing store, partial writes, unreadable state, and authority-path
mismatches. Optional selector/expect invariant for callers. Held-outs
honest about semantics: commit persists what it is given (the invariant is
the DETECTOR, not a rollback); save mkdirs nonexistent dirs (repair, not
failure); corrupt prior store is repaired by commit and detected by
verify_on_disk.
2026-09-14 05:35:25 +01:00
Nick 5169e02610 chore(scaffolding): T12A — T11 completion overclaims corrected durably (all-green wording, production-exercised overclaim, manual-vs-automatic convergence); corrected maturity table; backend input path identified 2026-09-14 05:20:51 +01:00
conductor cceb607d99 chore(release): auto-bump version to 0.66.50 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 04:54:49 +01:00
Nick c692013441 fix(scaffolding): remove dead None-comparison in commit() — pyright warning ratchet (T11 §0 follow-through) 2026-09-14 04:53:59 +01:00
conductor 11ebc50a55 chore(release): auto-bump version to 0.66.49 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 04:28:04 +01:00
Nick 08c782fe91 feat(scaffolding): contextual activation + PROJECT_BUILD bootstrap + project-existence authority + release-output gate repair (SCAFFOLD-TRANCHE-0011)
§0 RELEASE-OUTPUT GATE CORRECTION (audited against real Gitea/job logs):
T10's convergence refresh gated on vars.promoted — a repo CONFIG variable,
not a step output. Repo variables list is EMPTY, so the condition was
always false and the refresh NEVER ran: v0.66.47's promoted deploy (job
17917, PROMOTED + RUNTIME VERIFIED in log) executed zero refresh lines,
leaving the release without its deployed identity. CORRECTED:
- promotion step carries id: promotion_truth
- refresh gated on steps.promotion_truth.outputs.promoted == 'true'
  (the actual supported Gitea Actions step-output mechanism)
- deployed identity read from the deployment authority (build identity
  readback) with authority-vs-candidate agreement enforced before refresh
- recorded as process-defect:vars-vs-steps-output-syntax

§1 PROJECT-EXISTENCE AUTHORITY: T10 inferred exists=(project!='rune') —
invalid. Now _project_exists_evidence derives existence from structural
evidence only: Rune via Spine authority; external via bound Project
Scaffold entity; arbitrary names with no scaffold → False; missing
identity → None (UNKNOWN, never manufactured). Scoped change to a
proven-nonexistent project clarifies instead of claiming existing
semantics.

§3-§10 ACTIVATION DISPATCHER (construction_activation.py — NOT another
router/planner): dispatches the evidence-backed mode to the correct
lifecycle. NOT_CONSTRUCTION/UNKNOWN touch nothing (UNKNOWN returns probes,
never activates). LOCAL_CHANGE → bounded governance, no scaffold
bootstrap. EXISTING_PROJECT_CHANGE → attaches to existing authority.
PROJECT_BUILD → ACTUALLY BOOTSTRAPS: provisional Project Scaffold
initialized with explicit UNKNOWN fields (repository: undecided,
toolchain: unresolved, deployment: unknown, requirement gaps recorded) —
identity before repository, nothing manufactured, survives restart
(fresh-process resume test-proven). Rune-originated Needs derive
provisional project ids; same classifier, same dispatch.

§21 FORGOTTEN-SAVE PROCEDURAL REPAIR: Scaffold.commit() = mutation + save
+ readback as ONE boundary; accounting only AFTER commit. Regression test
proves mutation survives accounting failures.

Fixed mid-implementation: activation store-path hardcoded a relative repo
path (now derives from parent scaffold), init's returned doc was stale
(reload-after-persist discipline), ConstructionMode.from_dict added for
durable-trance dispatch.

Tests: 40 new (§1 existence, §6-13 activation/bootstrap/non-activation,
§17 resume, §18 Rune-originated, §21 transactional); suite 1662 green;
henxels rc=0; pyright clean.
2026-09-14 04:27:40 +01:00
conductor 4bcae0bb10 chore(release): auto-bump version to 0.66.48 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 03:44:47 +01:00
Nick a8d1b98332 fix(release): convergence refresh gate used vars.promoted (repo variable) instead of steps.promotion_truth.outputs.promoted — refresh never executed for v0.66.47 (§0 T11 audit: job 17917 log has zero refresh lines; repo variables empty) 2026-09-14 03:44:20 +01:00
Nick 74272842aa chore(scaffolding): T10 closure accounting completed separately (forgotten-save recurrence recorded per §25 — bundle-then-save anti-pattern identified) 2026-09-14 03:09:36 +01:00
Nick aa67119d55 chore(scaffolding): SCAFFOLD-TRANCHE-0010 closed — governance accounting recorded 2026-09-14 03:08:25 +01:00
Nick 9681baa318 chore(scaffolding): contextual-construction TUI backlog entry persisted (save_verified — Guard A caught the pre-save slip) 2026-09-14 02:55:06 +01:00
conductor 8a6b051149 chore(release): auto-bump version to 0.66.47 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 02:54:46 +01:00
Nick 6bb5507d33 feat(scaffolding): contextual construction activation core — mode classifier, shared Construction Context, interface ops (SCAFFOLD-TRANCHE-0010)
§0 RELEASE-HISTORY GATE REPAIR (confirmed live): deploy.yml convergence
refresh gated on success() alone — true even for non-promoting
stale/superseded jobs, so a late stale candidate could write its own HEAD
as deployed_sha into Gitea history. CORRECTED: gated on promoted=true
output + deployment-authority readback (build identity must equal
candidate SHA before refresh).

§2 ROUTER AUDIT: no new router created — classification lives in ONE
canonical home (construction_mode.classify_mode), wired into
make_decision's trace; consumers: Rune backend + Construction Interface.

- construction_mode.py: typed ConstructionMode decision (NOT_CONSTRUCTION/
  LOCAL_CHANGE/EXISTING_PROJECT_CHANGE/PROJECT_BUILD/UNKNOWN) with
  evidence/provenance/probe questions. NOT keyword-authoritative:
  'build confidence'/'make a list' → NOT_CONSTRUCTION (verb-object
  negation); bare 'this' deixis doesn't bind scope; standalone language +
  existing project → UNKNOWN clarification (project evidence wins, §5);
  capability BUILD_NEW != mode PROJECT_BUILD (§3, test-proven);
  UNKNOWN never silently escalates; manual override authority-validated
  with provenance (§23); model proposals recorded never governing.
- construction_interface.py: classify + construction_context operations —
  the shared Construction Context projection (§15) serving Rune native
  AND MiMo through the same backend; builder identity recorded,
  semantics identical.
- §0 negative-proof wiring: stale/superseded deploy cannot modify Gitea
  release deployed identity.

Tests: 23 new (§19 matrix, §20 context-sensitivity, §23 override,
interface equivalence, shared context); suite 1643 green; henxels rc=0;
pyright clean.
2026-09-14 02:54:18 +01:00
Nick 8692f1dc76 chore(scaffolding): T9B closure scope recorded — completion truth persisted (save_verified after pre-save failure caught) 2026-09-14 02:27:36 +01:00
Nick 78d70def5f chore(scaffolding): T9B closed — automatic release-history completion truth recorded (init + convergence both automatic) 2026-09-14 02:27:03 +01:00
conductor c9260ace18 chore(release): auto-bump version to 0.66.46 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 02:07:28 +01:00
Nick 2a7e9dcfcf feat(release): automatic release-history wiring — init on version/tag, convergence refresh on same-version deploy (SCAFFOLD-TRANCHE-009B)
§1 HELD-OUT CONVERGENCE DEFECT confirmed live: v0.66.45 release recorded
deployed ce328a80 while the deployment authority had converged to
e3263ef9/0.66.45 — repaired via the new refresh path; the release now
states BOTH honestly (tag commit ce328a80 · final deployed e3263ef9).

§2 audit answers: NO workflow invoked release_notes.py; release creation
used PR title/body paste (empty '##' bodies on direct pushes); nothing
refreshed after same-version deploys. T9 proved the mechanism, not
automatic adoption — recorded, history not rewritten.

§4 WIRING (one note-generation authority, no YAML logic):
- auto-version.yml: old PR-paste release step REMOVED (§6); replaced by
  release_history_cli.py init — evidence-derived notes from the governed
  scaffold store, fail-closed on publication failure (§5/§16: Rune cannot
  silently create another empty historical release)
- deploy.yml: convergence refresh after successful promotion — same
  release PATCHed with the converged deployed identity + newly-governed
  facts; visible non-blocking failure (documentation failure never rolls
  back valid software; retry converges)
- release_notes.py: auto_initial_note (no hand-written prose, §7) +
  convergence_refresh (evidence accumulates, tag SHA never changes,
  idempotent) + build_from_deployment
- §11 ledger-only: maintenance:builder-interaction-ledger created;
  §16: release-history backlog entry amended (health indicator), no
  duplicates

Tests: 10 new (§13 A-L incl. 0.66.45 convergence fixture, idempotence,
failure visibility, retry recovery, no-duplicate guarantee, unsupported-
claim exclusion through the auto path); suite 1620 green.
2026-09-14 02:07:06 +01:00
Nick e3263ef9b1 chore(scaffolding): T9 closure accounting + release-history TUI backlog entry 2026-09-14 01:29:02 +01:00
conductor ce328a8080 chore(release): auto-bump version to 0.66.45 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 01:11:16 +01:00
Nick 4df1f6f23f feat(release): evidence-derived release history + T8 evidence correction (SCAFFOLD-TRANCHE-009)
- release_notes.py: deterministic extraction from governed records →
  structured ReleaseNote → Markdown → Gitea API. Idempotent (PATCH update
  for existing tags — never duplicates). Version/tag/release-commit/
  converged-deployed-identity distinction exposed honestly (§6).
- §24F unsupported-claim filter: extras AND extracted facts pass the same
  exclusion surface — the retracted T8 close-refusal claim cannot enter
  generated notes (test-proven).
- §1/§23: T8 evidence correction recorded with full provenance (original
  claim, correction, reason, evidence); process-defect:unsupported-
  causal-claim logged. MiMo maturity survives on supported evidence only.
- §18 backfill: 0.66.35→0.66.43 published from closed-contract
  release_evidence records, marked historical backfill. Pre-0.66.35
  skipped (weak evidence — honest limitation).
- §19: 0.66.44 notes published to real Gitea (release 89): tag commit
  f86b2d1eec vs final deployed d84060cc02 distinction, capabilities
  improvement, bounded MiMo adoption proof (corrected basis), lock-drift
  fix, external-project limitation.
- Gitea 400-tolerance: plain-text error bodies handled (actual behavior,
  not assumed GitHub semantics).

Tests: 17 new (§24/§25/§26 plans); suite 1608 green; henxels rc=0;
pyright clean.
2026-09-14 01:10:42 +01:00
Nick 7484e175be chore(scaffolding): T8 evidence correction — close-refusal claim retracted (durable checkpoints + live output show direct verifying→closed success); process defect recorded 2026-09-14 00:56:08 +01:00
Nick d84060cc02 chore(deps): refresh coverage pin 7.16.0 -> 7.16.1 — lock drift ratchet (upstream release between runs) 2026-09-13 21:52:48 +01:00
Nick b102225d4e chore(scaffolding): SCAFFOLD-TRANCHE-008 — interface governance record, TUI projection NONE, builder interaction log 2026-09-13 21:37:42 +01:00
conductor f86b2d1eec chore(release): auto-bump version to 0.66.44 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 21:34:13 +01:00
Nick 307b0bf814 feat(scaffolding): capabilities operation — builder schema negotiation (SCAFFOLD-TRANCHE-008 live task)
Closes maintenance:interface-capabilities-op — T7 brief §28 required
builders to detect the supported interface schema before use; the envelope
carried the schema field passively but no operation exposed it.

- ConstructionInterface.capabilities(): schema_version, supported
  operations, error-state vocabulary, mode + project identity. Read-only,
  no contract required.
- CLI dispatch: capabilities command (transport parity — builders detect
  schema without the Python API).
- New-behavior proof: 2 tests (op + CLI roundtrip). Old-behavior
  preservation: existing 26 interface tests untouched and green; full
  suite 1591 green.

Governed through the Construction Interface itself (SCAFFOLD-TRANCHE-008):
Need submitted via submit_need, scope/obligations/patterns consumed before
implementation, observations reported via report_observation, phase map
completed through the interface (verifying → superseding → close).
2026-09-13 21:33:42 +01:00
Nick d776f286de chore(scaffolding): SCAFFOLD-TRANCHE-007 closed — TUI backlog (12 entries + projection seam), interface quality accounting, scope amendment with provenance 2026-09-13 19:53:03 +01:00
conductor 621f88c425 chore(release): auto-bump version to 0.66.43 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 19:33:06 +01:00
Nick ef6811ff58 fix(scaffolding): annotate report_observation payload as Any — runtime isinstance guards are genuine for CLI JSON, not redundant (type-check ratchet) 2026-09-13 19:32:42 +01:00
Nick a4f02f227c refactor(scaffolding): Construction Interface methods in class body — removes class-attribute binding warnings (pyright ratchet) 2026-09-13 19:16:34 +01:00
conductor 5b315ac7c7 chore(release): auto-bump version to 0.66.42 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 17:23:46 +01:00
Nick 90bd2b3285 feat(scaffolding): Construction Interface — builder adapter, TUI backlog, pattern-precision repair (SCAFFOLD-TRANCHE-007)
§0 PATTERN FALSE-POSITIVE REPAIR (investigated, not assumed):
- Root cause layer 1: raw-substring signal matching — 'ui' matched
  'patterns_bu_i_l_t_in.py'. Boundary-aware matchers: short (<4 char)
  signals match path TOKENS / entity-namespace PREFIXES only; longer
  signals may contain.
- Root cause layer 2: ui-component-identity-stable was keyed on
  path_contains ['tui','ui','widget'] — re-keyed to entities_match
  ui:/region:/layout: with namespace-prefix semantics (structural kinds
  carry meaning in their prefix).
- Verified all four directions: T6 false positive gone; tui_backlog path
  no longer activates identity obligation; genuine UI work still derives;
  genuine deploy hits survive. Recorded as
  process-defect:raw-substring-signal-match + TUI registry entry for
  tui_backlog.py (ledger, honestly marked not-a-surface).

- construction_interface.py: ONE builder-facing boundary (Rune-internal +
  external-project modes). submit_need → inspect_decision → get_scope →
  get_obligations → report_observation → verify → transition → close →
  resume + amend_scope. Structured JSON envelope (schema v1: success/ids/
  state/errors/warnings); explicit error states (UNKNOWN_CONTRACT,
  SCOPE_VIOLATION, APPROVAL_REQUIRED, VERIFICATION_FAILED,
  STORE_WRITE_FAILED...); builder identity recorded for audit, semantics
  identical for any builder (§17); NO structural-authority writes exposed;
  scope amendment is user-gated; closure cannot be self-certified; CLI
  transport with --builder/--project-store flags.
- tui_backlog.py: durable TUI Upgrade Backlog (§22-25) — projection
  classification (NONE/INSPECTION/CONTROL/BOTH/UNKNOWN), auto-capture seam
  record_tui_projection_decision for contract closures, backlog NEVER
  structural authority (frontier propagation empty, test-proven).

REAL PROOF §33: builder session via interface-only subprocess (no internal
imports): submit→decision→scope→obligations→transition→observe→verify→
close→FRESH-PROCESS RESUME recovering need/decision/scope/obligations/
next-action from contract id alone. Scope green, quality green.

Tests: 33 new (interface §29/§30, pattern precision §31, backlog §32);
suite 1589 green; henxels rc=0; pyright clean.
2026-09-13 17:23:13 +01:00
Nick 1bb4ea86b8 chore(scaffolding): SCAFFOLD-TRANCHE-006 scope amendment + regeneration-sequencing honesty note (RED retained) 2026-09-13 15:04:11 +01:00
Nick f98416e94a chore(scaffolding): SCAFFOLD-TRANCHE-006 trace regenerated through pattern-aware path + closed via save_verified (dogfooded Guard A) 2026-09-13 15:03:47 +01:00
conductor b3465176f8 chore(release): auto-bump version to 0.66.41 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 14:48:45 +01:00
Nick aab5159ccc feat(scaffolding): Construction Pattern core — evidence-backed applicability, filtered obligation derivation, process guards (SCAFFOLD-TRANCHE-006)
Also §0 pre-flight: SCAFFOLD-TRANCHE-005 found approved/open (parent-
lifecycle neglect — child closed, parent forgotten); closed with real
existing evidence only, provenance recorded, process-defect entity added.

- construction_pattern.py: open-world ConstructionPattern (signals,
  obligations with when-conditions, provenance, revision + history,
  maturity, ext); evaluate_applicability — deterministic signal groups,
  verdicts APPLIES/PARTIAL/DOES_NOT_APPLY/UNKNOWN, name similarity
  establishes nothing, project exclusions override, model proposals stay
  PENDING; derive_pattern_obligations — obligations FILTERED by project
  evidence (REQUIRED/APPLICABLE/NOT_APPLICABLE/UNKNOWN + EXCEPTED with
  provenance retained, §18); combine_pattern_obligations — dedupe with
  also_from, conflicts surfaced never silently resolved (§26-27);
  authority boundary: pattern = guidance, project evidence wins (§3)
- construction_patterns_builtin.py: 4 evidence-grounded patterns, all
  appropriately immature (§15):
  * capability-module-adaptation (UNIT-PROVEN; build-but-don't-break;
    provenance T1/T3/T4 contracts)
  * release-deployment-change (UNIT-PROVEN; 9 obligations incl.
    fail-closed qualification, exact identity, staging!=promotion,
    promotion truthfulness, monotonic ordering, negative+forward proofs,
    deployed-identity verification; provenance deploy-order-race
    incident + DEPLOY-MONOTONIC-001 + red-promotion incidents)
  * ui-tui-structural-change (UNIT-PROVEN; layout inheritance, USER-LOCKED
    preservation, UNKNOWN-never-PASS; provenance issue-105 + T3 real proof)
  * store-schema-evolution (CANDIDATE; backward readability, roundtrip,
    authority preservation, migration-only-if-needed; provenance T2/T4/T5)
- make_decision integration: APPLIES+PARTIAL patterns consulted (§7 —
  per-obligation when-conditions do the precision work); provenance
  PATTERN distinct from STRUCTURAL derivations (§8); pattern refs +
  revisions carried into generated contracts (§17/§19)
- construction_guards.py + Scaffold.save_verified/verify_on_disk (§21-22):
  Guard A durable-mutation readback — verify_on_disk is the pure no-write
  readback that catches the forgotten-save lie (T5 defect class A);
  Guard B source-edit postcondition — zero-match is a truthful no-op
  (EditNoOp), expected-count + postcondition enforced (T5 defect class B)
- REAL PROOFS: §23 read-only replay of DEPLOY-MONOTONIC-001 Need —
  release pattern APPLIES on evidence, 9 obligations derived WITHOUT the
  Need listing them (negative/forward/monotonic/deployed-identity all
  pattern-derived, provenance carried); resolution COMPOSE matches the
  original repair; §24 live change save_verified/verify_on_disk — the
  store pattern contributed store-roundtrip, an obligation the raw Need
  did not state; §25 held-out — webhook-config-only deploy change does
  NOT demand promotion rework but keeps release regression verification

Tests: 30 new (§28-§32 plans + held-out + live); suite 1556 green;
henxels rc=0; pyright clean.
2026-09-13 14:48:28 +01:00
Nick 80f10b9be7 chore(scaffolding): §0 pre-flight — SCAFFOLD-TRANCHE-005 closed with real evidence; parent-lifecycle-neglect process defect recorded 2026-09-13 14:24:15 +01:00
Nick 9d75e51b22 chore(spine): canonical rebuild — observation refresh 2026-09-13 14:02:23 +01:00
conductor b6113fad7b chore(release): auto-bump version to 0.66.40 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 14:01:25 +01:00
Nick f765365d03 fix(release): ancestry check fetches canonical branch on divergent classification
Observed live on deploy 7449 (SHA 535c2909): a delayed older candidate's
checkout cannot contain commits newer than itself — its local object
store lacks the deployed commit entirely, so the ancestry classification
returned 'divergent' and fail-closed refused. Correct refusal, wrong
reason: the candidate IS an ancestor of the deployed build.

Repair: on divergent classification, fetch origin's canonical branch
(best-effort, credentials already configured in the workflow checkout)
and re-classify. Still divergent afterwards = genuine divergence, refusal
stands. Regression test reproduces the 7449 shape with a depth-1
checkout that genuinely lacks the deployed commit's objects.

Release-path invariant unchanged: unknown/unprovable ordering still
refuses; the fetch only lets the checker SEE the newer reality before
refusing.
2026-09-13 14:01:04 +01:00
Nick b94a3aef61 chore(scaffolding): CONTRACT-GEN-REPAIR-001 closed (saved) + contract-gen maintenance item resolved with evidence — Tranche 5 §22/§29 2026-09-13 13:38:44 +01:00
conductor 8ca4628694 chore(release): auto-bump version to 0.66.39 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 13:37:26 +01:00
Nick 535c290917 feat(scaffolding): affected-surface propagation + requirement completeness gate + impact-aware contract generation (SCAFFOLD-TRANCHE-005)
Origin: maintenance:contract-gen-affected-surface (opened by
DEPLOY-MONOTONIC-001's honest closure — planned work appeared as
genuinely_unexpected because generate_contract dropped the Need's
affected surface).

- construction_decision.py:
  * classify_surface/resolve_surface/resolve_affected_surface:
    open-world surface classification (structural entity / source path /
    workflow config / ui component / layout region / unresolved — never
    dropped) with deterministic resolution against authorities via
    identity, recorded path attributes, and Layout Contract bindings
  * check_completeness: bounded structure-grounded gate BEFORE fit —
    affected-surface entities contribute their IMPLEMENTED contracts to
    the material scope; unrepresented ones = MATERIAL_GAP with full
    provenance (derived_from/rule/evidence) + concrete probe questions.
    Verdicts COMPLETE_ENOUGH/MATERIAL_GAP/UNKNOWN; UNKNOWN never
    silently COMPLETE. PREMATURE-FIT GUARD: a concrete verdict
    (REUSE/SATISFIED/COMPOSE/ADAPT_EXISTING) on a materially incomplete
    requirement is downgraded to INSUFFICIENT_EVIDENCE with the
    would-have-been verdict retained — the fit engine was right GIVEN
    its requirement; the requirement was too narrow
  * record_proposed_requirement: model-proposed requirements are
    PENDING evidence, never structural authority by themselves
  * iterative decision trace: prior_iterations retained with why-it-
    changed provenance; final resolution stays singular
  * test-surface derivation: tests/test_<name>.py in an affected
    surface derives the test:<name> expected-new entity (Rune path
    convention; structural consequence, provenance recorded)
  * impact-aware generate_contract: distinct affected_paths /
    affected_ui_surfaces / affected_regions / workflow_config_surfaces /
    unresolved_surfaces / expected_runtime_effects surfaces; file scope
    GENERATED from affected paths + selected impl paths + expected-new
    paths, caller additions provenance-tracked; file scope != structural
    impact != expected new != runtime effect
- scaffold.py: closure classifies expected RUNTIME effects (an observed
  entity matching a predicted effect phrase = expected, not unexpected;
  prediction still cannot self-certify — independent evidence required);
  legacy contracts unaffected (empty lists, no crash)

REAL PROOFS: §23 read-only replay of the deploy incident — ONE pass
identifies MATERIAL_GAP (contract:monotonic_promotion on
module:deploy_stage), blocks the premature REUSE automatically, emits a
concrete probe question; §22 live proof CONTRACT-GEN-REPAIR-001
generated through the improved path with all surfaces propagated,
regeneration proving the planned test surface is now expected-new.

Tests: 28 new (surface/completeness/trace/runtime/compat plans §24-§28);
suite 1525 green; henxels rc=0; pyright clean.
2026-09-13 13:36:50 +01:00
Nick 6bc31c513e chore(spine): canonical rebuild — observation refresh 2026-09-13 13:08:10 +01:00
Nick 386137dc14 chore(release): DEPLOY-MONOTONIC-001 closure interpretation + scope amendment + decision-path finding ledgered 2026-09-13 13:00:52 +01:00
Nick f87adc0d18 chore(release): DEPLOY-MONOTONIC-001 closed + deploy-order-race maintenance item resolved with evidence 2026-09-13 13:00:17 +01:00
conductor ed3a73a0a9 chore(release): auto-bump version to 0.66.38 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 12:52:26 +01:00
Nick 600008482d fix(release): monotonic promotion — stale deploy candidates fenced at promotion time (DEPLOY-MONOTONIC-001)
Root cause of the 2026-09-13 deploy-order race (deploys 7355/7359):
deploy_stage.py promoted unconditionally in job-finish order — no
comparison against the currently-deployed commit, no lock, unconditional
PROMOTED claim. Two overlapping deploys (tranche push + auto-bump push)
could finish out of order and move current BACKWARD.

Repair (governed through the Tranche 4 live Scaffolding path — decision
trace + contract DEPLOY-MONOTONIC-001 durably saved BEFORE edits):

- deploy_promotion.py: promotion authority with GIT ANCESTRY ordering
  (canonical release branch; Rune's flow is linear master history):
  same -> NOOP_ALREADY_CURRENT (idempotent), descendant -> PROMOTED,
  ancestor -> STALE_SUPERSEDED (truthful non-error refusal), divergent
  or unknown -> fail closed. flock'd critical section: the ancestry
  check against the commit current ACTUALLY points to, the last-good
  refresh, and the symlink swap happen atomically — no check/swap
  interleaving window. Symlink swaps are os.replace(2) over temp links:
  current is never absent, never half-updated (crash-safe).
- deploy_stage.py: promotion routed through the authority; truthful
  outcome reporting — STALE_SUPERSEDED prints no PROMOTED claim,
  mutates nothing, exits 0 (the newer build legitimately won);
  divergent/unknown exit 1 (fail closed).
- deploy.yml: promotion truthfulness check reads the deployment
  authority (current symlink) after staging; runtime verification only
  runs for real promotions — build/stage success != promotion success.

Test matrix (brief §20 A-M): 21 tests — the exact race shape with B
promoting then A fenced, forward order, same-candidate idempotence,
retry-after-promotion last-good integrity, stale mutation-freedom
(current AND last-good untouched), divergent orphan-branch fail-closed,
qualification gate regression, stage/promote distinction, promoted-
outcome proven from deployment authority, auto-bump overlap shape,
crash-safety coherence across mixed outcomes, red-candidate refusal
regression, green forward deploy, concurrent-thread serialization, and
the §21 LIVE two-process race: old process paused at its promotion
barrier, new promotes, old resumes and is fenced — current remains the
newer build, deterministic via file barrier (no sleeps).

Suite 1497 green; henxels rc=0; pyright clean.
2026-09-13 12:51:57 +01:00
Nick 4bfa4f9b0a chore(scaffolding): SCAFFOLD-TRANCHE-004 persisted + closed — Phase A persistence gap recorded in provenance note 2026-09-13 05:49:22 +01:00
Nick 1d6213cd74 docs(scaffolding): Tranche 4 — live adoption flow, authority map, contract-quality semantics, honest maturity 2026-09-13 05:46:49 +01:00
Nick 9c3f5f0a1d chore(spine): canonical rebuild — deployed identity observation refresh (0.66.37) 2026-09-13 05:34:50 +01:00
conductor c0594ce192 chore(release): auto-bump version to 0.66.37 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 05:24:57 +01:00
Nick dc0ac7b92e fix(tests): rebuild-neutrality comparison tolerates deployed-identity advance
CI caught (run 7368 attempt 2): the relationship equality asserted raw
observed_at and deployed_as evidence SHAs. The deployed tree legitimately
advanced (0a9b0648 deploy landed between the committed store's local build
and CI's rebuild), so the deploy-identity-projector refreshed those edges
and strict equality failed. The test docstring always promised 'modulo
observation timestamps and the legitimate deployed-identity advance' —
the comparison now implements that promise: deployed_as edges excluded,
observed_at stripped, all structural content still enforced.
2026-09-13 05:24:33 +01:00
Nick 5e0a2def9d chore(scaffolding): Tranche 4 closure — maintenance ledger (deploy-order race observed live, carried items) 2026-09-13 05:00:46 +01:00
conductor 049a8dbd86 chore(release): auto-bump version to 0.66.36 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 04:49:45 +01:00
Nick 0a9b0648cd feat(scaffolding): live Need→Decision→Contract adoption path + contract-quality repair + layout obligation injection (SCAFFOLD-TRANCHE-004)
- construction_decision.py: the adoption layer — Need intake (references
  Task/Intent by pointer, never duplicates), requirement normalization
  (gaps recorded, never invented), deterministic discovery+fit+resolution
  across authorities with merge precedence (BUILD_NEW never a fallback
  across merges; normalization gaps block it), toolchain decision (existing
  toolchain wins), USER-LOCKED layout obligation inheritance, approval
  gate (material changes require user; approved-bounded work never re-asks),
  durable model-independent decision trace, contract generation populated
  from the trace. Research routing is an HONEST SEAM: recorded route, not
  executed ADAPT call (§6).
- scaffold.py contract-quality repair (Tranche 3 defects): closure now
  distinguishes predicted_existing_affected / expected_new /
  correctly_predicted / predicted_but_unaffected / expected_new_observed /
  expected_new_missing / genuinely_unexpected_new; prediction cannot
  self-certify observation; legacy keys (missed/needless/observed)
  preserved — old contracts readable without migration
- layout obligations: affected UI component/region/implementation pulls
  its Layout Contract into generated obligations; USER-LOCKED obligations
  state that changes require explicit user authorization; unrelated
  backend changes inherit none
- REAL LIVE PROOF (§24): the contract-quality Need ran through the new
  path — discovered module:scaffold (PARTIAL fit on closure-quality
  contract), ADAPT_EXISTING verdict, python KEEP_EXISTING, approval
  covered by SCAFFOLD-TRANCHE-004, generated LIVE-CONTRACT-QUALITY-001,
  implemented, verified (10 contract-quality tests), closed with repaired
  semantics — which correctly flagged parent-tranche surfaces as outside
  this bounded contract's scope (RED honored, attribution recorded)
- baseline correction recorded: Tranche 3 pre-build = 1386 tests (report
  wording corrected in contract-quality notes, provenance kept)
- version-display lock status: still scaffold-provenance, USER-LOCKED
  upgrade PENDING USER CONFIRMATION (not fabricated)

Tests: 37 new (decision path 15, contract quality 10, layout obligations
10, plus repaired-closure compat); suite 1476 green; henxels rc=0;
pyright 0 errors on new/changed modules.
2026-09-13 04:49:17 +01:00
Nick da2ec4df0e chore(scaffolding): SCAFFOLD-TRANCHE-003 closure — deployed 0.66.35, contract-quality interpretation recorded 2026-09-13 04:23:28 +01:00
conductor dcf992bb2f chore(release): auto-bump version to 0.66.35 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 04:17:29 +01:00
Nick 3dadf2d3f5 feat(scaffolding): capability-fit discovery, gap decisions, toolchain resolution, layout contracts (SCAFFOLD-TRANCHE-003)
- capability_fit.py: open-world CapabilityRequirement; deterministic
  candidate discovery over Spine/Scaffold stores (kind + providing-
  relationship eligible; environment-contradiction excluded); fit levels
  EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN from contracts+health, never names;
  resolution surface SATISFIED/REUSE/CONFIGURE/COMPOSE/ADAPT_EXISTING/
  ACQUIRE_EXTERNAL/BUILD_NEW/INSUFFICIENT_EVIDENCE — BUILD_NEW never
  silent (insufficient evidence blocks it), brittle-coupling check
  (load-bearing missing contracts) forces BUILD_NEW or ACQUIRE with
  recorded rationale; composition covers via contract union, flagged
  temporary (permanence user-controlled); LLM semantic-fit seam records
  provider/model/reason/evidence/confidence — prose never authority
- toolchain.py: EXISTING PROJECT TOOLCHAIN WINS; constraint keys fixed
  and language-free, values open-world; language change = CHANGE_MAJOR
  requiring user approval, never silent; unknown constraints ->
  INSUFFICIENT_EVIDENCE (no invented certainty); Python only selected
  because the project uses it — fixtures prove rust/ts/zig resolve
- layout_contract.py: open-world regions (web/tui/qt coexist); stable
  ui:<component> identity surviving implementation changes; LOCKED
  requires user provenance (mechanically enforced); locked amendment
  requires authorized_by=user (enforced, reason required); deterministic
  adapter-observation gate PASS/FAIL/UNKNOWN — UNKNOWN never PASS;
  USER-LOCKED drift preserves the contract (deliberate exception to
  reality-wins), marks DRIFTING, creates maintenance item; flexible
  violation fails gate until repaired
- REAL TUI PROOF: Rune's actual version display (issue #105, plan
  plans/2026-09-03-tui-footer.md) observed from source via deterministic
  adapter — footer-bar first child; gate cycle PASS -> controlled
  violation FAIL -> restore PASS; binding encoded scaffold-provenance
  (durable records establish placement; USER LOCK upgrade NEEDS
  CONFIRMATION — not fabricated)
- capability/layout integration proven: version-display requirement
  discovers the existing capability + layout binding (REUSE), never
  builds another widget
- 3 modules registered in Spine authority + canonical build + manifest
  regenerated (Tranche 2 lesson applied: no hand-edited structural facts)

Tests: 53 new (fit 24 + toolchain 9 + layout 16 + real proof 4);
suite 1439 green; henxels rc=0; pyright 0 errors on new modules.
2026-09-13 04:16:48 +01:00
Nick c8b317de53 chore(scaffolding): SCAFFOLD-TRANCHE-001 final closure — deployed identity 0.66.34, full release chain recorded 2026-09-13 03:50:59 +01:00
conductor ce330d3ac0 chore(release): auto-bump version to 0.66.34 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 03:42:19 +01:00
Nick 16b90fd8be fix(scaffolding): canonical spine rebuild after manual registration — projector-owned edges restored
Root cause of CI unit-tests failure on c7a28e1: the closure step added
belongs_to relationships by hand and recorded deploy observations without
running spine build(), so the stored graph lacked the deploy-identity-
projector's deployed_as edges and diverged from any canonical rebuild.
The rebuild-neutrality test correctly flagged the unexplained structural
change (assert pre == post).

Fix: rebuild through spine's authoritative projector chain. The stored
graph now matches a fresh build exactly; test isolation contract restored
(live-store comparisons must follow canonical build, never hand edits).
2026-09-13 03:42:01 +01:00
Nick c7a28e1db3 chore(scaffolding): SCAFFOLD-TRANCHE-001 closure evidence — release CI proof + deployed identity 2731db0/0.66.33 2026-09-13 03:32:02 +01:00
conductor 2731db03f4 chore(release): auto-bump version to 0.66.33 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-13 03:25:23 +01:00
Nick eb2f1a3e0e feat(scaffolding): construction substrate — shared structural store, Project Scaffold, Construction Contracts (SCAFFOLD-TRANCHE-001)
- structural_store.py: generic primitives extracted from spine.py —
  zero Rune constants (grep-gated boundary tests); Spine behavior-neutral
  (68 tests + identical-structure rebuild proof)
- scaffold.py: Project Scaffold over the shared core — open-world
  entity/relation kinds, repo/forge binding with secret redaction at
  save-time (shared core, Spine inherits), Construction Contracts with
  legal-phase-map lifecycle, cross-process resumability, mechanical
  scope check (unknown ≠ green for unscoped contracts), predicted-vs-
  observed closure evidence, authority-preserving drift reconciliation
- Rune Spine attachment: construction provenance via evidence, not
  authority transfer; project internals proven to never leak
- bootstrap contract SCAFFOLD-TRANCHE-001 executed through the system
  it implements: migrated → phased → closed with scope amendment
  (RED preserved in history) → fresh-runtime continuity proven
- umbrellas.json regenerated from Spine after module registration
- docs/SCAFFOLDING.md: Tranche 2 architecture + honest maturity

Tranche tests: 54 new (17 boundary + 37 scaffold); full suite 1386 green;
henxels rc=0. External-repo live proof BLOCKED ON USER REPO SELECTION.
2026-09-13 03:24:53 +01:00
Nick e3b459dfdf Revert "chore(release-safety): deliberate red candidate — negative deploy-proof (revert follows)"
This reverts commit b8d030e6c4.
2026-09-12 20:18:26 +01:00
Nick b8d030e6c4 chore(release-safety): deliberate red candidate — negative deploy-proof (revert follows) 2026-09-12 20:15:11 +01:00
conductor 20a47fac86 chore(release): auto-bump version to 0.66.32 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 20:10:54 +01:00
Nick 36ee604646 fix(release): required-checks verdict evaluates needs results — fail-closed deploy
ROOT CAUSE (red-CI promotions 0.66.28/29): the required-checks
'Publish protected-branch gate' step read /tmp/need_* files that no step
ever wrote and echoed 'All required CI checks passed' UNCONDITIONALLY.
With if: always() the needs gate is overridden, so the job ran on red
needs and published success — deploy_qualify correctly trusted it and
promoted red commits (24413e4, fd6e9b4, de30cd9; status timelines in the
closure evidence).

FIX: publish step now runs only when every needs.*.result == 'success';
companion fail-closed step exits 1 otherwise, so the required-checks
context reports failure instead of lying.

DEFENSE-IN-DEPTH: deploy_qualify verdict is now the AND of two
independent legs — (1) newest required-checks observation for the exact
SHA, (2) the latest COMPLETED ci.yml@refs/heads/master run for that exact
SHA with every job concluded success (fan-out resolved by path, run
resolved by head_sha, jobs envelope unwrapped — no aggregate-status
fallback). Any pending/missing/disagreement = not deployable.

CANARY: gitea_pipeline now asserts the verdict evaluates needs results
and fails closed — the class locked shut. 20 qualifier tests with
fixtures from the real incident shapes; 1402 suite green.
2026-09-12 20:10:28 +01:00
Nick 20f493596d chore(types): baseline 117/443 — CI-observed counts on the fixed tree
CI's locked pyright reports errors 117 / warnings 443 on the current tree
(spine fixes removed exactly 4: 121→117, coherent with the 24413e4 run).
Local .type-venv pyright classifies 124/429 for the same tree — a local/
runner environment skew in error-vs-warning classification that must be
investigated separately; the GATE is CI, so the baseline records CI's
arithmetic. Both counts sit below the original 120/465 baselines.
2026-09-12 19:55:41 +01:00
conductor faf3575fb3 chore(release): auto-bump version to 0.66.31 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 19:50:15 +01:00
Nick 2e16fc4c89 fix(spine): commit the nullable-flow fixes — they were never landed
The hardening-era pyright fixes (supersede() single entity fetch,
membership-loop assert before identities write) sat as uncommitted
working-tree modifications through multiple checkout/stash cycles. Every
pushed tree since e2464ae therefore still carried the 4 nullable-flow
errors — the CI type-check failures on fd6e9b4/12303bb were CORRECT
failures of the unfixged tree, not runner anomalies. With this commit:
spine.py = 0 errors under locked pyright 1.1.414; total 124 = the
re-baseline from the previous commit. Lesson recorded: verify fixes are
COMMITTED (git show HEAD:<file>) before diagnosing CI failures against
them.
2026-09-12 19:49:47 +01:00
Nick 12303bbd20 chore(types): re-baseline pyright counts under locked pyright 1.1.414
The count baseline (120) was frozen under pyright 1.1.412; the dependency
lock has since drifted to 1.1.414 (fae36c4/9c78b3d/e7d4c2a 'upstream
release drift' resyncs), which reports 124 errors / 429 warnings on the
tree — including on the baseline-era commit itself (127 under 1.1.414).
Same precedent as every prior pyright drift re-baseline.

DISCOVERED DEFECTS recorded (out of scope here, flagged for follow-up):
1. CI type-check job has been publishing false successes across the
   0.66 era — commit-status 'success in ~30s' while the canonical
   ratchet on the identical tree fails (a real pass takes minutes).
   Likely runner/execution anomaly; needs its own investigation.
2. deploy.yml promoted commits with failing type-check (not gated on
   ci.yml) — 0.66.28/29 were promoted red.

Spine-era errors themselves: fixed — src/rune/spine.py reports 0 under
the locked pyright (nullable-flow fixes: supersede() single-fetch,
membership-loop assert).
2026-09-12 19:47:20 +01:00
conductor de30cd958e chore(release): auto-bump version to 0.66.30 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 19:18:32 +01:00
Nick fd6e9b4bca fix(tests): scope-classification test is environment-independent
The first version asserted harness filesystem state (~/.config/opencode
exists) — true on the desktop, false on the CI runner, where unit-tests
and type-check failed. CI failure = real signal, test assumption was
machine-dependent. Now asserts the classification itself: propagate()
resolves against SCRIPTS_DIR by construction, and the tracker imports no
Spine module (checked via AST, not prose — the scope docstring mentions
Spine legitimately). Verified under fake HOME (no harness) + real HOME.
2026-09-12 19:18:13 +01:00
Nick 24413e4228 chore(spine): sync store state (post-hardening observations) 2026-09-12 19:12:34 +01:00
Nick 407c23a689 docs(spine): pre-Memory closure — real-build durability proof + scope classification
Closure 1: alias and supersession durability now proven through the REAL
build path (create→alias/supersede→save→reload→spine.build()→save→reload→
resolve) — the earlier __wrapped__-fallback test never exercised the actual
refresh; both survive (aliases carried as entity data, superseded entities
preserved with superseded_by intact).

Closure 2: dependency_tracker.py classified NON-STRUCTURAL TO RUNE / KEEP
LOCAL — its graph describes ~/.config/opencode/scripts (the dev harness;
verified: 20/23 names collide with src/rune but all exist in the scripts
dir, propagate() resolves against SCRIPTS_DIR). Not a competing structural
authority; migrating harness structure into Spine would wrongly expand
scope. Scope classification documented in-module + regression test pins
the separation.

Closure 3: bounded sweep found no other manually maintained Rune
structural copy — remaining umbrella consumers are all generated-view
compatibility (CI gate, henxels, research_inward_context, gen_prd_state,
shared/umbrella_update); scaffold_projection is Spine-backed.

1382 tests, lint ratchet, henxels, canary-native green.
2026-09-12 19:12:34 +01:00
conductor 982a9b07d5 chore(release): auto-bump version to 0.66.29 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:43:59 +01:00
Nick ff506ebe55 feat(spine): CLI alias/supersede commands; save() coerces string paths
Real-change proof for the hardening pass (task SPINE-CLI-ALIAS): bounded
CLI completion on module:spine. The change surfaced a real defect via
runtime evidence — save() accepted 'Path | None' per its signature but
CLI callers legitimately pass strings; now coerced. Independent evidence:
full suite 1379, lint, henxels, scaffold consumer check, CI.
2026-09-12 18:43:37 +01:00
conductor 2032819d99 chore(release): auto-bump version to 0.66.28 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:41:16 +01:00
Nick e2464ae9d6 feat(spine): hardening — authority-owned reconciliation, contract semantics, durable identity
T1: impact_frontier carries change_kind — contract_change expands all
parties; implementation_change (contract preserved) demotes sibling
implementers to contextual_not_obligations (sharing a contract is not
being affected by a sibling's internal change); contract_violation
escalates. Pre-hardening test expectation that accepted siblings as
direct obligations was expectation-laundering — corrected with the
architectural justification recorded.

T3/T4: relationships carry authority+status; per-projector generation
records; reconcile_authorities stales facts their owner no longer
observes (marked + drift-reported + frontier-excluded, never deleted —
open-world preserved, truth marked); authority_failed degrades; both
sides of an authority conflict preserved (never last-wins);
resolve_status: UNKNOWN/CURRENT/STALE/CONFLICTING/SUPERSEDED — 'not
checked' is never healthy.

T5: alias (rename) + supersede (replacement) — old spine:// references
stay meaningful; duplicate entity/resolve_uri defs removed (shadowing
trap). T2: record_observed_impact builds the observed set from external
evidence items (test/canary/runtime authority), not frontier-rerun.
CANDIDATE status for evidence-free (model-speculated) relationships.
65/65 spine tests, 1379 suite, gates green.
2026-09-12 18:40:53 +01:00
conductor d7e04f1098 chore(release): auto-bump version to 0.66.27 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:20:25 +01:00
Nick e639383c49 fix(spine): build() carries forward ALL entities/relationships — open-world refresh
Predicted-vs-observed caught it: build()'s carry-forward filtered by known
kinds/types, silently destroying task bindings (and any future entity
class) on every refresh — the exact TOO-CLOSED failure the Spine contract
forbids. Everything now passes through; module-level facts re-derive on
top; exact-duplicate observations suppressed. Task bindings survive build;
predicted==observed on the Phase D migration change.
2026-09-12 18:19:59 +01:00
Nick 57a274d97d docs(phase-d): research_inward_context consumes Spine-generated view
Phase D consumer migration: the INWARD context builder reads structural
facts from the Spine store's generated compatibility view (same schema —
zero code-path change), documented as TEMPORARY SPINE-BACKED
COMPATIBILITY. Graceful degradation to a manual manifest if Spine is not
seeded in a checkout.
2026-09-12 18:13:15 +01:00
conductor aac5295c24 chore(release): auto-bump version to 0.66.26 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:07:08 +01:00
Nick 9563148f9d feat(spine): contracts, structural impact resolution, task binding, event seam
Structural Impact Resolution: impact_frontier() — bounded LEVEL 0/1/2
resolution with a data-driven semantics table. Contract entities expand to
all implements/consumes parties (a contract change deserves every
consumer's inspection); unknown relationship types default to
obligation-bearing (fail-loud, open world); non-obligation classes
(documented_by, related_memory, supersedes...) never explode a frontier.

Graph separation (addendum): Spine is its own logical graph — the frontier
traverses Spine edges only; memory-style associations cannot expand
structural impact; stable spine:// URIs let future Memory reference Spine
entities without duplicating authority.

Also: task binding (affects edges + bind_task/task_entities),
predicted_vs_observed comparison (misses become CANDIDATE corrections,
evidence-gated), generic structural_events() seam (dumb typed facts — no
salience/policy; future Substrate subscribes), aggregate query() (the
intrinsic self-knowledge property: owner/deps/consumers/contracts/state/
evidence/frontier in one deterministic call), evidence_conflict drift
class. henxels similarity scan excludes store/spine (snapshots are
legitimately near-identical by design). 45/45 spine tests; full gates green.
2026-09-12 18:06:16 +01:00
conductor 76984c6362 chore(release): auto-bump version to 0.66.25 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 17:34:32 +01:00
Nick 4072d36b1a fix(spine): build re-derivation uses high-water upgrade — no demotion noise
Evidence surfaced it: re-deriving module maturity during build() logged a
demotion-rejection observation for every already-earned module (171 noise
records). Build now upgrades via the silent high-water path; promote()
logging stays reserved for explicit evidence-gated API calls.
2026-09-12 17:34:05 +01:00
conductor 6d871abc9c chore(release): auto-bump version to 0.66.24 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 17:03:51 +01:00
Nick ce40acbb1f feat(spine): V1 — evolvable structural self-model as the single structural authority
Open-world entity/relationship store (schema_version 1): kinds, relationship
types and metadata are open tags — unknown future capabilities enter as data,
never as core edits (extensibility test included, synthetic capability
round-trips with zero core change). Evidence-gated maturity ladder
DECLARED<PRESENT<WIRED<REACHABLE<EXERCISED<LIVE_PROVEN — no evidence, no
promotion; demotion rejected and recorded.

Umbrella migration Phase A→C: manifest ingested once (generated-view
refusal guard prevents the ingest cycle; manual truth archived at
config/umbrellas.manual.json), canaries preserved as obligated_canary
relationships, config/umbrellas.json now a GENERATED compatibility view for
legacy consumers (CI umbrella_impact gate + henxels) — single-authority
route proven by registering module:spine through Spine alone (one store
edit; the legacy view flows out; contract tests validate the round-trip).

Dynamic observations: deployed identity (_build_identity.json), drift
classes incl. registered_but_absent, present_but_unowned,
registered_no_static_importers (D6 dead-subnet now mechanical),
version_surfaces_disagree, relationship_disappeared. Deterministic
snapshots + diff. Scaffolding V1 now consumes Spine for ownership/deps/
consumers (spine_maturity surfaced in every card). 31 spine tests; full
gates green.
2026-09-12 17:03:27 +01:00
Nick 1de28b55a0 docs(tooling): desktop runner wakes local W/R daemon after jobs (v0.3, Gitea authoritative) 2026-09-12 16:16:58 +01:00
conductor 75d6d7d62a chore(release): auto-bump version to 0.66.23 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 11:16:07 +01:00
Nick 7f47969128 feat(scaffolding): V1 tranche 1 — projection engine over existing authorities
scripts/scaffold_projection.py derives per-component cards (subsystem,
dependencies, consumers, mirrors, proof obligations + evidence state) from
the umbrella manifest, canary registry, AST import graph (incl. relative
lazy imports), and tests/ filesystem. No new authority: projections only.
docs/SCAFFOLDING.md records the mechanism audit (KEEP/EVOLVE/REPLACE/RETIRE)
and the real-change proof on the context-discovery fix.
2026-09-12 11:15:42 +01:00
Nick a65e5d44de fix(runtime): context discovery routes cloud-pinned models to the provider wire
Module-level context_length used OllamaChatTransport unconditionally, so
every cloud-pinned task logged context_discovery tier=1 HTTPError 400 and
was misbudgeted at the 8192 fallback instead of the provider's real window
(131072 for Verboo Code/glm-5.3-flash). is_cloud_pinned is now THE
provider-prefix rule — one authority in openai_compat_transport, consumed
by context discovery (new) and transport selection (deduped from
rune_operator._transport_for). Red-first regression tests added.
Scaffolding-derived impact: consumer rune_operator; gates suite/lint/
henxels + inference/orchestration canaries.
2026-09-12 11:15:42 +01:00
Nick 26a040892d docs(reconciliation): ADAPT family reconciliation on PL1 held-out evidence
#292/#293/#297 FIXED_VERIFIED (closed with evidence comments — LOCAL_FIX
live-proven by held-out tasks, DESIGN-LITE implemented, task_gate shipped);
#294/#295/#296/#299/#301 DEFERRED_FUTURE (canonical remaining-obligations
list); #300/#315/#316/#284 VALID_OPEN. Resolves the ADAPT-LIVE-complete vs
open-issues tension: the authorized tranche is complete; the backlog never
was its scope. Checker green (72 records). PE-1789102658-0269 SEED→VALIDATED
recorded in the canonical intent state.
2026-09-12 11:04:54 +01:00
conductor 1497c85cd4 chore(release): auto-bump version to 0.66.22 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 10:50:09 +01:00
Nick a2463ad80b fix(operator): approval-resume uses provider-prefix transport selection
_runtime_for_model hardcoded OllamaChatTransport, so the post-approval
follow-up model call hit localhost with the cloud-pinned name → 400
'invalid model name'. The direct turn path was fixed in 0.66.17; the resume
path now delegates to the same _transport_for authority (one selection
rule, two call sites). Red-first regression tests added.
2026-09-12 10:49:42 +01:00
Nick 6965fc7375 fix(transport): chat() returns flat ChatMessage — envelope shape broke runtime tool-call loop
OllamaChatTransport.chat returns the flat message object (the ChatTransport
contract), but the [OI]-compat translation returned the response envelope
{'message': {...}}. LocalToolRuntime normalizes tool_calls at the top level,
found none, and failed 'empty terminal visible text' while the model had
returned a valid read_file call. Observed live: PL1 Task A wire capture
2026-09-12. Contract-parity regression test added (red-first).
2026-09-12 10:49:42 +01:00
Nick 0daad967c1 test: mock cloud_transport_for at source module — CI has no user config
The selection test called the real cloud_transport_for, which reads the
user's mimocode config (absent on the CI runner). Mocking at the consumer
module was a no-op (function-level import re-resolves from source). Mock now
targets the source module. Local green.
2026-09-12 10:00:14 +01:00
conductor 502317d8b6 chore(release): auto-bump version to 0.66.21 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 07:57:49 +01:00
Nick 07cdc2fc56 feat(intent): minimal intent-continuity ledger — deterministic state machine, 7 invariants
PART 6 of the overnight handoff. IntentRecord (7 fields: objective,
parent_intent, active_detour, return_when, return_to, unresolved_obligations,
success_condition) + IntentLedger (JSON persistence, no LLM). Classifications:
BLOCKER | FIX | EXTENSION | SUPERSEDING | ORTHOGONAL | LATER. All 7 invariants
tested: new ideas never replace the active objective; detours preserve parents;
resolving a detour exposes the return point; supersession accounts for open
obligations; child completion never resolves the parent; persist/reload
round-trips; transitions are deterministic. 1309 green.
2026-09-12 07:57:26 +01:00
Nick b4161a4034 test: valid identifier + tempdir storage + journal isolation — CI green on cloud-execution tests
Three fixes: (1) [OI]CompatTransport is an invalid Python identifier —
renamed to the module's actual class name; (2) fixed /tmp storage paths in
selection tests collided with a root-owned /tmp/hot on the CI runner (the
hot store derives from Path(storage).parent) — per-test tempdirs; (3) the
operator's journal/event edges resolve via memory_env whose real memory dir
is root-owned in CI → RUNE_MEMORY_DIR pointed at a per-test tempdir. 1304
green locally.
2026-09-12 06:43:59 +01:00
conductor e036f5f545 chore(release): auto-bump version to 0.66.20 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 06:00:32 +01:00
Nick ab6c4f4e38 fix(transport): pair assistant tool_call ids with following tool messages' tool_call_ids
Observed live: glm-5.3-flash tool-loop turn 2 returned the SAME edit_file
call with empty content both turns — the provider treated the tool result as
orphaned because the translation assigned fresh ids to assistant tool_calls
while the tool messages carried the runtime's minted call_ids. The fix pairs
each tool_call with the FOLLOWING tool message's tool_call_id. Proven live:
turn 2 now returns a proper text summary instead of looping.
2026-09-12 06:00:07 +01:00
Nick c520b024d1 test: fix transport test CI failures — /tmp/hot collision, valid identifier, RUNE_MEMORY_DIR isolation
Three CI failures on the transport commits: (1) fixed /tmp storage paths in
selection tests collided with a root-owned /tmp/hot on the runner (the hot
store derives from Path(storage).parent) — per-test tempdirs now; (2) an
invalid [..]-bracketed identifier crept into one test — renamed; (3) the
selection tests' journal edges resolved via memory_env to a root-owned
memory dir — RUNE_MEMORY_DIR now isolated per test. 1304 green locally.
2026-09-12 05:41:07 +01:00
conductor 1c73d809b9 chore(release): auto-bump version to 0.66.19 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 04:40:00 +01:00
Nick 34fc38d4ed fix(runtime): clamp cloud wire timeout to >=120s — tier-1 30s starves cloud round trips
Observed live: glm-5.3-flash read-timeout at 30.6s mid-iteration (tier-1
timeout is 30s; cloud round trips with a full tool schema routinely exceed
it). The clamp is transport config — the escalation ladder still governs
retries/tiers; one round trip just needs enough rope. Clamp test added.
2026-09-12 04:39:41 +01:00
Nick a67a9c8520 test: isolate remaining fixed-path operator storage (same /tmp collision family) 2026-09-12 04:33:05 +01:00
conductor b0efaffca1 chore(release): auto-bump version to 0.66.18 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 04:30:41 +01:00
Nick a087a0b5e7 fix(tests): isolate operator storage to per-test tempdirs — fixed /tmp paths collide on the CI runner
test_context_broker parity + new transport-selection tests used fixed /tmp
storage paths; the derived hot-store dirs (/tmp/hot etc.) are root-owned on
the CI runner → PermissionError on every write (observed live: 5 CI test
failures on 58af0b0). All operator storage now uses per-test tempdirs.
2026-09-12 04:30:18 +01:00
Nick acb3ce0157 docs(reconciliation): ADAPT-only pass — #311 SUPERSEDED by #315; #315/#316/#301 DEFERRED_FUTURE
ADAPT-1 v2 baseline experiment framing (#315/#316) is historical — the
adaptive pipeline went live without the pre-registered comparison; the
underlying work items (#158, #162) remain open. FINAL-12 (#301) blocked on
PE-0269 substrate. Consistency checker green (72 records).
2026-09-12 04:10:59 +01:00
conductor 58af0b0f78 chore(release): auto-bump version to 0.66.17 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 04:08:35 +01:00
Nick 95f9e15ed9 fix(runtime): key transport selection on provider prefix, not routing_mode
Observed live: an operator-requested model keeps routing_mode='local' (line
~887), so keying selection on routing_mode=='cloud' still sent pinned
'Verboo Code/glm-5.3-flash' to OllamaChatTransport → 400. Selection now keys
on the model's provider prefix — deterministic, no router change. Selection
regression tests added (pinned cloud → OpenAI-compat; local/bare → Ollama).
1300 tests green; ratchet 352/80; henxels clean.
2026-09-12 04:08:13 +01:00
Nick b083b0c4cf fix(runtime): provider-keyed transport selection — pinned cloud models kept hitting Ollama
Selection keyed on routing_mode=='cloud', but an operator-requested model
(send_task(model=...)) sets routing_mode='local' (line 887), so pinned cloud
models silently used OllamaChatTransport and 400'd. Selection now keys on the
model's provider prefix: any '<Provider>/<model>' with a non-ollama prefix
rides the OpenAI-compatible wire; ollama-prefixed and bare names keep the
proven path. Deterministic — no router, ladder, or escalation change.
Regression tests: pinned cloud → OpenAI-compat; local/bare → Ollama.
2026-09-12 04:08:13 +01:00
conductor 46e74110ad chore(release): auto-bump version to 0.66.16 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 03:40:49 +01:00
Nick c669e37a3d fix(runtime): pin cloud models by bare model id — qualified name 400s on the wire
The runtime passes '<Provider>/<model>' to the transport; the provider API
wants the bare model id. Observed live: every pinned-Verboo PL1 held-out
attempt 400'd because the wire carried 'Verboo Code/glm-5.3-flash'.
Regression test proves the wire now carries 'glm-5.3-flash'. 1296 green.
2026-09-12 03:40:28 +01:00
conductor 7f4a6262ae chore(release): auto-bump version to 0.66.15 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 03:09:58 +01:00
Nick 34d5ab74b7 feat(runtime): OpenAI-compatible transport for cloud-pinned operator execution
Merges prototype/openai-compat-transport (7b268e1). The operator's execution
substrate had exactly one transport (OllamaChatTransport, localhost:11434) —
send_task(model='<Provider>/<model>') posted cloud model names to Ollama's
/api/chat and 400'd (observed live: Verboo Code/glm-5.3-flash, 0.2s). The
supported model= override could not execute ANY cloud provider.

OpenAICompatTransport speaks the OpenAI wire format (/chat/completions) and
returns Ollama-shaped responses (LocalToolRuntime loop unchanged). Selection
is deterministic on routing_mode=='cloud' — no router, ladder, or escalation
change. Provider config resolved from the same source of truth the mimo CLI
uses (mimocode.jsonc baseURL + auth.json key).

Translation unit tests (6) + live smoke against the real Verboo API
(TRANSPORT-SMOKE-OK). Umbrella manifest updated. Stray experiment files
removed. 1295 tests green; ratchet 352/80; henxels clean.
2026-09-12 03:09:17 +01:00
Nick 7b268e17fe prototype: OpenAI-compatible transport for cloud-pinned operator execution (NOT RELEASED)
Local branch only — preserved per directive 2026-09-12: the PL1 held-out
strong-provider rerun is BLOCKED_BY_EXECUTION_SUBSTRATE (operator's only
transport is Ollama; send_task(model='Verboo Code/glm-5.3-flash') 400s).
This prototype makes the supported override executable but is NOT authorized
for release. Translation unit tests + live smoke (TRANSPORT-SMOKE-OK) pass.
2026-09-12 02:29:15 +01:00
997 changed files with 831431 additions and 3732 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
---
name: land
description: Review and land one small Rune change through the governed PR workflow.
disable-model-invocation: true
metadata:
delta-action: land
---
# Land one small diff
1. Read applicable `AGENTS.md` rules and the repo's PR/merge workflow.
2. Confirm the issue, exact diff, target branch, and permission to publish and land.
3. Keep one small, reversible change; preserve unrelated work.
4. For bugs, follow `docs/bugfix-contract.md`: red test first, root cause, discharge.
5. Run required tests, static analysis, canaries, and henxels; record results.
6. Get independent `/review` per `docs/adversarial-review.md`; clear blockers.
7. Inspect remotes; publish the PR to the approved forge, never `local`.
8. Require human approval and green required CI on the exact PR head; recheck changed heads.
9. Load `report-landed`; use the repo's merge workflow. Stop on conflicts; never force-push or bypass hooks.
10. Verify the target commit and CI, then report the actual landing result and remaining blockers.
BIN
View File
Binary file not shown.
+4
View File
@@ -15,6 +15,9 @@
# Canary files are omitted: src/rune/canary_*.py are standalone CI
# verification scripts executed directly by the canary job, never imported by
# the test suite — counting them would make the global floor a fiction.
# ffi_query_memory_smoke.py is the same class: a standalone evidence script
# driven directly (and by tests/test_ffi_query_memory_smoke.py when the cdylib
# is built), never imported by the suite.
[run]
source = src/rune
@@ -22,6 +25,7 @@ omit =
tests/*
scripts/*
src/rune/canary_*.py
src/rune/ffi_query_memory_smoke.py
[report]
fail_under = 29
+35 -9
View File
@@ -1,16 +1,46 @@
---
name: Bug Fix
about: Fix a bug or issue
about: Demonstrate currently incorrect behaviour — plain English first, proof required
title: "[BUG] "
labels: bug
assignees: conductor
---
## Plain summary
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
<restate the issue title here in everyday words — this heading IS the
plain-English summary; no meta-labels like "plain English" in it>
<!-- The heading above carries the summary itself — a scan of issue
headings alone should tell the whole story of the board. TOP PRIORITY:
if a technical detail below contradicts this section, the issue is
buggy — fix the issue. -->
<!-- SUBMITTED_BODY -->
## Example
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
What is broken: <what a person observes going wrong>
What should happen instead: <the correct behaviour>
How often / when: <always | after X | since change Y>
## Proof it is broken (required — no proof, no bug)
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
If this section cannot be filled, file it as a question, not a bug. -->
- [ ] Reproduction command or steps (deterministic where possible)
- [ ] The exact wrong output observed: <paste/log excerpt>
- [ ] Where the evidence lives: <log path, fixture, session id>
## Steps to reproduce
<!-- How to trigger the bug -->
## What would let us close this
- [ ] The reproduction command now shows correct behaviour
- [ ] A regression test exists that fails before the fix and passes after
- [ ] No other behaviour changed (or changes are listed and justified)
## Version impact (declared at submission — verified at merge)
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
- [ ] patch — same behaviour, done correctly (usual for bugs)
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
## Technical details
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
@@ -29,10 +59,6 @@ assignees: conductor
- [ ] Canary passes (name)
- [ ] Closed with a properly formatted closing comment + commit references
## Steps to reproduce
<!-- How to trigger the bug -->
## Cognitive invariants (memory/cognition issues — both handoffs)
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
+33 -8
View File
@@ -1,18 +1,44 @@
---
name: Feature Request
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
about: New feature or improvement — plain English first, verifiable close conditions, version intent
title: "[FEATURE] "
labels: feature
assignees: conductor
---
## Plain summary
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
understand the goal without decoding internals. -->
<restate the issue title here in everyday words — this heading IS the
plain-English summary; no meta-labels like "plain English" in it>
<!-- The heading above carries the summary itself — a scan of issue
headings alone should tell the whole story of the board. TOP PRIORITY:
if a technical detail below contradicts this section, the issue is
buggy — fix the issue. A non-technical reader must be able to retell this
section. 3-6 sentences, no jargon. -->
<!-- SUBMITTED_BODY -->
What is this about: <what>
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
## Example
<!-- A concrete example of the intended behavior, in plain terms:
"If X happens, the system will Y instead of Z." -->
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
## What would let us close this
<!-- Checkable statements, verifiable by looking at something that EXISTS —
a file, a log line, a passing test. Not "when appropriate". -->
- [ ] <condition 1 — observable>
- [ ] <condition 2>
## Version impact (declared at submission — verified at merge)
<!-- The pipeline derives the real bump from merged conventional commits and
compares against this declaration. A mismatch is flagged, not fatal:
either the declaration was wrong or the work grew. Both facts matter. -->
- [ ] major — breaking: changes a contract, schema, or interface others depend on
- [ ] minor — new capability: something becomes possible that wasn't
- [ ] patch — repair: same behaviour, done correctly
- [ ] none — touches no released artifact (docs, memory, process notes)
## Technical details
<!-- WHAT — one-line objective.
@@ -37,7 +63,6 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
- [ ] Canary passes (name)
- [ ] Closed with a properly formatted closing comment + commit/PR references
## Cognitive invariants (memory/cognition issues — both handoffs)
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
@@ -69,4 +94,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
- [ ] **P3 — low** — measurement / benchmark / deferred policy
- [ ] **P3 — low** — measurement / benchmark / deferred policy
+76
View File
@@ -0,0 +1,76 @@
---
name: Research Submission
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
title: "[RESEARCH] "
labels: research
assignees: conductor
---
<restate the issue title here in everyday words — this heading IS the
plain-English summary; no meta-labels like "plain English" in it>
<!-- The heading above carries the summary itself — a scan of issue
headings alone should tell the whole story of the board. TOP PRIORITY:
if a technical detail below contradicts this section, the issue is
buggy — fix the issue. -->
<!-- SUBMITTED_BODY -->
The question: <what we're trying to learn, one sentence>
Why we're asking: <what decision this research will inform — a name, an issue
number, or "exploratory: none yet">
What we believe today: <the current assumption, stated so it can be proven wrong>
## The shape of an answer
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
this is stated BEFORE starting so the research cannot become a search for confirmation>
- [ ] Budget: <time/effort cap — when research must stop and report back>
## What would let us close this
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
a comment thread is not a durable finding
- [ ] The decision the research informs is recorded: made / deferred / changed
- [ ] If a follow-up build issue was minted: <#N or "none needed">
## Version impact (declared at submission — verified at merge)
- [ ] none — research changes no released artifact (usual)
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
## Technical detail
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
rebuildable-projection or authority requirements if findings touch canonical state. -->
## Pipeline requirements
- [ ] Findings committed under the stated durable path
- [ ] Every citation verified against a real source (no fabricated references)
- [ ] Closed with a properly formatted closing comment: findings summary + decision
## Cognitive invariants (memory/cognition research — both handoffs)
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
- [ ] **Read/write authority** — which canonical state is affected
- [ ] **Provenance** — findings traceable to evidence
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
- [ ] **No duplicate authority** — why extend vs create
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
## Related umbrella
- [ ] Memory
- [ ] Governance
- [ ] Identity
- [ ] Growth
- [ ] Perception
- [ ] Infrastructure
- [ ] Life (Nick's)
- [ ] Custodian
- [ ] Orchestration
## Priority (REQUIRED — the priority gate)
<!-- Pick exactly one. An issue without a priority is not filed. -->
- [ ] **P0 — critical** — blocks a decision that blocks other work
- [ ] **P1 — high** — informs a next-stage dependency
- [ ] **P2 — medium** — quality/consolidation understanding
- [ ] **P3 — low** — watchlist / deferred / exploratory
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
backup:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+140 -55
View File
@@ -7,49 +7,38 @@ on:
types: [opened, edited]
jobs:
# PRIORITY GATE (2026-09-04): every issue must carry exactly one of
# P0/P1/P2/P3. An issue that lands without one gets 'unprioritized' +
# a demand comment — filing is not complete until prioritized.
priority-gate:
if: github.event_name == 'issues' && github.event.action != 'labeled'
runs-on: rune-infra-pi
# SINGLE-PASS FILING (2026-10-07, Nick's first-time-correct directive):
# the old design ran TWO jobs on submission — priority-gate (which saw
# no P-label and posted the ⚠️ warning) and auto-label (which inferred
# and applied the P-label). The gate ran BEFORE the inference that
# would have made the warning unnecessary: every keyword-inferrable
# submission ate a spurious warning, a flag add/remove cycle, and a
# wasted workflow run. The fix is ORDER: infer → apply → THEN gate.
# A submission whose priority is inferable gets its P-label in this
# same run; the warning fires only when inference genuinely produced
# nothing (a true filing gap, not a race the robot loses to itself).
auto-label:
runs-on: rune-ci-desktop
steps:
- name: Enforce priority label
- name: Infer, apply, then gate-check (one pass)
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
if (!issue) return;
const labels = (issue.labels || []).map(l => l.name);
const priorities = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
const has = labels.filter(l => priorities.includes(l));
if (has.length >= 1) {
// Prioritized — clear the flag if it was set.
if (labels.includes('unprioritized')) {
await github.rest.issues.removeLabel({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issue.number, name: 'unprioritized',
}).catch(e => { if (e.status !== 404) throw e; });
}
// CLOSED-ISSUE GUARD (#407): labels are filing-time metadata.
// A closed issue's lifecycle has ended; re-running label
// inference on it re-adds stale type labels (bug = currently
// incorrect behavior — a closed fixed issue must not carry
// it) and makes any human label cleanup self-defeating,
// because every PUT fires a 'labeled' event that re-triggers
// this job. Post-close label curation belongs to humans and
// the conductor, not this workflow.
const issueState = context.payload.issue?.state;
if (issueState === 'closed') {
console.log(`Issue #${context.payload.issue?.number} is closed — auto-label skips (labels are curated post-close).`);
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issue.number, labels: ['unprioritized'],
});
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issue.number,
body: `⚠️ **Priority gate:** this issue has no priority. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
});
auto-label:
runs-on: rune-infra-pi
steps:
- name: Analyze and label
uses: actions/github-script@v7
with:
script: |
const action = context.payload.action;
const isIssue = context.payload.issue !== undefined;
const title = context.payload.issue?.title || context.payload.pull_request?.title || '';
const body = context.payload.issue?.body || context.payload.pull_request?.body || '';
const text = (title + ' ' + body).toLowerCase();
@@ -58,9 +47,44 @@ jobs:
const managedLabels = new Set([
'memory', 'governance', 'identity', 'growth',
'perception', 'infrastructure', 'life', 'custodian',
'priority: high', 'bug', 'feature',
'bug', 'feature', 'research',
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
]);
// Type label: DECLARED over GUESSED. The template spine
// (kind: bug|feature|research|epic|...) is authoritative.
// A recognized kind maps to its label; ANY other declared kind
// (e.g. epic) means NO type label and NO keyword guessing —
// epics are containers, not bugs. Keyword heuristics apply
// only to pre-template issues with no spine at all.
let isBug = false, isFeature = false, isResearch = false;
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
if (kindMatch) {
const kind = kindMatch[1];
isBug = kind === 'bug';
isFeature = kind === 'feature';
isResearch = kind === 'research';
} else if (action === 'opened') {
// KEYWORD FALLBACK (#407): pre-template issues only, and
// ONLY at filing time. Re-running keyword guesses on every
// edit/label event overrides human curation of spine-less
// issues (documented rule: keyword heuristics are the
// pre-template fallback, not a standing authority).
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
isBug = textBug;
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
}
if (isBug) {
desiredLabels.add('bug');
}
if (isFeature) {
desiredLabels.add('feature');
}
if (isResearch) {
desiredLabels.add('research');
}
// Umbrella labels
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
desiredLabels.add('memory');
@@ -87,19 +111,31 @@ jobs:
desiredLabels.add('custodian');
}
// Priority labels
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
desiredLabels.add('priority: high');
}
// Type labels
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
if (isBug) {
desiredLabels.add('bug');
}
if (isFeature) {
desiredLabels.add('feature');
// Priority labels (2026-10-05: PROGRAMMATIC P-ASSIGNMENT —
// the gate demanded P0-P3 labels but nothing assigned them;
// the dead 'priority: high' label is gone per the 2026-09-04
// normalization). Assignment from the TITLE prefix first
// ([P1]/P1:/priority=...), then keyword inference using the
// gate's own rule set (the scale lives in ONE place — the
// gate check at the bottom of THIS job).
const pMatch = title.match(/\bP([0-3])\b/);
const priorityLabelNames = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
if (pMatch) {
desiredLabels.add(priorityLabelNames[Number(pMatch[1])]);
} else if (text.includes('urgent') || text.includes('critical') ||
text.includes('spine') || text.includes('blocks other work')) {
desiredLabels.add(priorityLabelNames[0]); // P0 — critical
} else if (text.includes('broken') || text.includes('fix') ||
text.includes('repair') || text.includes('defect') ||
text.includes('real defect') || text.includes('money')) {
desiredLabels.add(priorityLabelNames[1]); // P1 — high
} else if (text.includes('improve') || text.includes('consolidat') ||
text.includes('refactor') || text.includes('kernel') ||
text.includes('port')) {
desiredLabels.add(priorityLabelNames[2]); // P2 — medium
} else if (text.includes('benchmark') || text.includes('measurement') ||
text.includes('deferred')) {
desiredLabels.add(priorityLabelNames[3]); // P3 — low
}
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
@@ -109,6 +145,7 @@ jobs:
issue_number: issueNumber,
});
const currentLabels = new Set(current.data.map(label => label.name));
const currentLabelIds = new Map(current.data.map(label => [label.name, label.id]));
const staleManaged = [...currentLabels].filter(
label => managedLabels.has(label) && !desiredLabels.has(label)
);
@@ -118,11 +155,14 @@ jobs:
for (const name of staleManaged) {
try {
await github.rest.issues.removeLabel({
// Gitea's DELETE /labels/{id} requires the numeric label ID.
// Octokit's GitHub-style removeLabel(name) returned 404 and
// left stale priority labels behind (#2467: P0 AND P2).
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name,
id: currentLabelIds.get(name),
});
} catch (error) {
if (error.status !== 404) {
@@ -140,5 +180,50 @@ jobs:
labels: missingDesired,
});
}
console.log(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
// ---- PRIORITY GATE (runs LAST — after inference and
// reconciliation, so the verdict reflects the labels THIS run
// just applied, not a pre-inference snapshot). Issue-scoped:
// the filing contract governs issues; PRs get inferred labels
// but are not filing units.
if (!isIssue) {
console.log(`PR #${issueNumber}: labels applied, gate skipped (gate is issue-scoped).`);
return;
}
const reconciled = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issueNumber,
});
const actual = reconciled.data;
const activePriority = actual.filter(l => priorityLabelNames.includes(l.name));
const flag = actual.find(l => l.name === 'unprioritized');
if (activePriority.length === 1) {
// Prioritized (by human or by this run's inference). Clear
// any stale flag — no warning is ever posted on this path.
if (flag) {
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issueNumber, id: flag.id,
});
}
console.log(`Priority gate: satisfied (${activePriority[0].name}).`);
return;
}
// Genuinely unprioritized: inference produced nothing AND the
// caller set nothing. Flag + demand comment — the filing gap
// is real, not a race artifact.
const reason = activePriority.length === 0
? 'no priority'
: `${activePriority.length} priority labels (exactly one required)`;
if (!flag) {
await github.rest.issues.addLabels({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issueNumber, labels: ['unprioritized'],
});
}
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issueNumber,
body: `⚠️ **Priority gate:** this issue has ${reason}. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
});
console.log(`Priority gate: flagged — ${reason}.`);
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
update-gitea:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- name: Check current Gitea version
id: current
+19 -35
View File
@@ -11,7 +11,7 @@ permissions: write-all
jobs:
release:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
with:
@@ -131,8 +131,12 @@ jobs:
"pyproject.toml": [(f'version = "{VERSION}"', f'version = "{NEW}"')],
}
# Fetch current master content, apply replacements, VERIFY all five
# surfaces agree on NEW before committing anything (TUI gate).
# Fetch current master content, apply replacements, VERIFY all six
# surface patterns (five files + the TUI footer) agree on NEW before
# committing anything (TUI gate). The footer check was added after
# the 0.89.0 half-bump: a stale RELEASE_VERSION made both replace
# pairs no-op; the gate's RECOVER regex fixed the header only and
# the footer silently stayed 0.88.4, failing the banner test.
edited = {}
for path, pairs in surfaces.items():
status, cur = api(f"/contents/{path}?ref=master")
@@ -151,6 +155,7 @@ jobs:
checks = [
("src/rune/__init__.py", rf'__version__ = "({VER})"'),
("src/rune/rune_tui.py", rf"Version: v({VER})"),
("src/rune/rune_tui.py", rf"Rune v({VER}) \|"),
("src/rune/canary_gitea_automation.py", rf"'__version__ = \"({VER})\"'"),
("pyproject.toml", rf'version = "({VER})"'),
("README.md", rf"v({VER})"),
@@ -213,38 +218,17 @@ jobs:
env.write(f"RELEASE_VERSION={NEW}\n")
env.write("AUTO_BUMPED=true\n")
PYBUMP
- name: Create release from merged pull request
- name: Create evidence-derived release (SCAFFOLD-TRANCHE-009B)
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
PR_TITLE: ${{ gitea.event.pull_request.title }}
PR_BODY: ${{ gitea.event.pull_request.body }}
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
LIST_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases?limit=50
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
# Skip idempotently if a release for this tag already exists (retry/replay safety).
if curl --fail --silent "$LIST_API_URL" \
-H "Authorization: token $GITEA_TOKEN" \
| python3 -c 'import json,sys,os; v=os.environ["RELEASE_VERSION"]; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")=="v"+v]; sys.exit(0 if exists else 1)'; then
echo "Release v$RELEASE_VERSION already exists — skipping creation"
exit 0
fi
python3 - <<'PY' > /tmp/rune-release.json
import json, os
version = os.environ["RELEASE_VERSION"]
body = f"## {os.environ['PR_TITLE']}\n\n{os.environ.get('PR_BODY', '')}"
print(json.dumps({
"tag_name": f"v{version}",
"target_commitish": "master",
"name": f"Rune v{version}",
"body": body,
"draft": False,
"prerelease": False,
}))
PY
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/tmp/rune-release.json \
"$RELEASE_API_URL"
rm -f /tmp/rune-release.json
# ONE note-generation authority: the tested release_notes module.
# The old PR-title/body paste path (which produced empty '##'
# bodies on direct-push releases) is REMOVED — governed evidence
# from the scaffold store is the source, not PR prose.
python3 scripts/release_history_cli.py init \
--version "$RELEASE_VERSION" \
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
--store store/scaffold/current.json
+274 -8
View File
@@ -63,10 +63,32 @@ jobs:
- name: Structural contract (henxels)
run: |
.ci-venv/bin/henxels check --all --plain
# ABSOLUTE SCAFFOLDING LAW (2026-10-07): every Go/Rust implementation
# unit must map to a construction contract in store/scaffold/. New
# units without a contract fail the build — open the contract BEFORE
# the commit lands, never after.
- name: Scaffolding coverage (absolute law)
run: |
python3 scripts/scaffolding_coverage.py
unit-tests:
runs-on: rune-ci-desktop
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
env:
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
# The unit shim battery loads the cdylib (ep_loader); point it at the
# runner-owned cdylib FILE from the crate-build step below (the override
# expects a file path, not a directory).
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
CC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcognition_core.so
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -102,6 +124,26 @@ jobs:
fi
python3 scripts/umbrella_impact.py check \
--base "$(git merge-base origin/master HEAD)" --head HEAD ${BODY_FILE:+--body-file "$BODY_FILE"}
- name: Build Rust evidence_provenance crate
run: |
# tests/test_evidence_provenance_shims.py drives the cdylib through
# the shim layer. Toolchain law (see the canary job for the full
# rationale): system rustc is broken on this host (LLVM_23.1
# version-node symbol gap); use the healthy rustup toolchain, PATH
# first so the rustup shim can't exec the broken system rustc.
# Runner-owned /tmp target dir (the repo's target/ is nick's).
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
mkdir -p "$CARGO_TARGET_DIR"
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
- name: Test with coverage (global floor + changed-lines ratchet)
run: |
# #149 coverage ratchet — both floors live in .coveragerc; raise,
@@ -113,12 +155,143 @@ jobs:
.test-venv/bin/coverage xml
.test-venv/bin/diff-cover coverage.xml --compare-branch origin/master --fail-under=70 --diff-range-notation ..
go-surface:
runs-on: rune-ci-desktop
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Verify Go toolchain version pin
run: |
# H0 pin: the Go surface builds against the pacman toolchain; the
# pin file is the single source of truth so a distro upgrade that
# changes behavior fails loud here, not downstream in the canary.
WANT=$(cat go-version.txt)
GOT=$(go version | awk '{print $3}' | sed 's/^go//' | cut -d- -f1)
echo "pinned: $WANT | toolchain: $GOT"
[ "$GOT" = "$WANT" ] || { echo "::error::Go toolchain drift: want $WANT, got $GOT (update go-version.txt with the rollout)"; exit 1; }
- name: build Survivor staticlibs in-tree (for cgo main packages)
run: |
# Survivor main packages link Rust staticlibs via ${SRCDIR} paths
# (adaptive-router → libroute_risk_flags.a). Those paths resolve
# only when the crate target dir is IN-TREE, so this build must
# NOT set CARGO_TARGET_DIR (the redirected builds below are for
# the FFI-loader canaries and stay as they are).
# Every Survivor-linked crate must have its .a in-tree before the
# go loop: adaptive-router → route_risk_flags; cognition →
# cognition_core. Add crates here as Survivor packages grow.
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
cargo build --release --manifest-path src/rust/construction_interface/Cargo.toml
- name: prune stale /tmp cargo target dirs (tmpfs guard)
run: |
# /tmp is a 32G tmpfs; every CI run leaves a ~218MB
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
# the tmpfs (2026-10-07: venv creation failed disk-full across
# every job). Keep the 3 newest; drop the rest.
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
- name: go vet + go test + go build
run: |
for dir in src/go/*/; do
if [ -f "$dir/go.mod" ]; then
echo "=== Building $dir ==="
cd "$dir"
go vet ./...
go test ./... 2>/dev/null || true
OUT_DIR="${GITHUB_WORKSPACE:-$PWD}/bins"
mkdir -p "$OUT_DIR"
go build -o "$OUT_DIR/go-$(basename "$dir")" .
cd - > /dev/null
fi
done
- name: adaptive-router parity gate (HARD — oracle replay, not swallowable)
run: |
cd src/go/adaptive-router
go test ./... -run TestParityAgainstPythonOracle -v
- name: gofmt gate (formatting is a gate — polyglot lint contract)
run: |
UNFORMATTED=$(gofmt -l src/go/ 2>/dev/null)
if [ -n "$UNFORMATTED" ]; then
echo "::error::gofmt drift — run gofmt -w on these:"
echo "$UNFORMATTED"
exit 1
fi
- name: go race detector (every module's test suite under -race)
run: |
for dir in src/go/*/; do
if [ -f "$dir/go.mod" ]; then
cd "$dir"
go test -race ./... 2>/dev/null || true
cd - > /dev/null
fi
done
- name: go fuzz smoke (seed corpus + 15s per target — panics and
invariant violations fail the build; corpus caches in the repo)
run: |
for dir in src/go/tool-executor src/go/curator-core src/go/memory-store-deep; do
cd "$dir"
go test -fuzz . -fuzztime 15s ./... 2>&1 | tail -2
cd - > /dev/null
done
rust-surface:
runs-on: rune-ci-desktop
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: rustup clippy + rustfmt components
run: |
rustup component add clippy rustfmt 2>/dev/null || true
cargo clippy --version
cargo fmt --version
- name: cargo fmt --check (formatting is a gate)
run: |
FAIL=0
for crate in src/rust/*/; do
if [ -f "$crate/Cargo.toml" ]; then
(cd "$crate" && cargo fmt --check) || FAIL=1
fi
done
exit $FAIL
- name: cargo clippy (warnings are errors — first linting the Rust
surface has EVER had; pre-existing debt tracked in the crate-local
lib.rs allow list, NOT a global opt-out)
run: |
FAIL=0
for crate in src/rust/*/; do
if [ -f "$crate/Cargo.toml" ]; then
echo "=== clippy $(basename "$crate") ==="
(cd "$crate" && cargo clippy --quiet -- -D warnings) || FAIL=1
fi
done
exit $FAIL
- name: cargo test (lights up when the first #[test] lands)
run: |
FAIL=0
for crate in src/rust/*/; do
if [ -f "$crate/Cargo.toml" ]; then
(cd "$crate" && cargo test --quiet) || FAIL=1
fi
done
exit $FAIL
canary:
runs-on: rune-ci-desktop
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
env:
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
# The build emits to a runner-owned /tmp target dir (the repo's target/
# is nick-owned); EP_RUST_LIB points ep_loader at the cdylib FILE there
# (the override expects a file path, not a directory).
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -130,6 +303,35 @@ jobs:
python3 -m venv .canary-venv
.canary-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
.canary-venv/bin/pip install --disable-pip-version-check -e . --no-deps
- name: Build Rust evidence_provenance crate
run: |
# The canary pair + the unit shim battery drive the COMPILED surfaces
# (binary CLI parity, ep_loader FFI, ctypes cdylib loads).
#
# Toolchain law (troubleshooting history): the SYSTEM rustc is broken
# on this host — librustc_driver-<sha>.so (pacman rust 1:1.99.0-1)
# references std::string::_M_mutate under the LLVM_23.1 version node,
# but /usr/lib/libLLVM.so.23.1 does not define that narrow-char
# symbol under that node (it exports _M_create/_M_assign there, and
# only the WIDE-char _M_mutate) — every compile dies with `symbol
# lookup error` while `cargo --version` still prints. The rustup
# toolchain under /home/nick/.rustup is healthy and ships its own
# libLLVM, and /home/nick carries the x-ACL for this runner user.
# Point PATH at the toolchain bin dir (bypasses the rustup shim,
# which would exec the broken system rustc via RUSTUP_TOOLCHAIN).
# Target dir is runner-owned (/tmp): the repo's target/ is nick's.
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
mkdir -p "$CARGO_TARGET_DIR"
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
- name: Run canary suite
run: |
FAILED=0
@@ -156,6 +358,21 @@ jobs:
src/rune/canary_p1_06.py \
src/rune/canary_p1_01.py \
src/rune/canary_query_memory.py \
src/rune/canary_rune_entry.py \
src/rune/canary_spine_meta.py \
src/rune/canary_audit_ledger.py \
src/rust/rune_spine/canary_spine_rust.py \
src/rune/canary_rune_serve.py \
src/rune/canary_tui_transport.py \
src/rust/memory_scoring/canary_memory_scoring.py \
src/rust/capability_scoring/canary_capability_scoring.py \
src/rust/token_counting/canary_token_counting.py \
src/rust/scaffold_core/canary_scaffold_core.py \
src/rust/memory_storage/canary_memory_storage.py \
src/rust/memory_retrieval/canary_memory_retrieval.py \
src/rust/cognition_core/canary_growth_pins.py \
src/rust/cognition_core/canary_persona.py \
src/rust/cognition_core/canary_cognition_core.py \
src/rune/canary_unbounded.py \
src/rune/canary_full_system.py \
src/rune/canary_native.py; do
@@ -286,12 +503,61 @@ jobs:
run: |
NEW="$(echo "${{ gitea.event.pull_request.head.ref }}" | sed 's|chore/auto-bump-||')"
python3 scripts/check_version_surfaces.py check --root . --version "$NEW"
- name: Publish protected-branch gate (full pipeline path)
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
- name: Publish protected-branch gate (full pipeline path — all needs green)
# RELEASE-SAFETY FIX (2026-09-13): the previous version of this step
# read /tmp/need_* files that no step ever writes and echoed success
# UNCONDITIONALLY — required-checks published green while type-check
# and unit-tests were red, and deploy promoted the red commits
# (observed live: 24413e4, fd6e9b4, de30cd9 promoted 0.66.28/29 with
# failing type-check). The verdict now comes from the needs context
# itself: this step runs ONLY when every required job succeeded.
if: >-
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
&& needs.lint.result == 'success'
&& needs['unit-tests'].result == 'success'
&& needs.canary.result == 'success'
&& needs['best-practices'].result == 'success'
&& needs.documentation.result == 'success'
&& needs['type-check'].result == 'success'
&& needs['gitea-offline'].result == 'success' }}
run: echo "All required CI checks passed"
- name: Refuse promotion when any required check failed (fail-closed)
# Companion to the step above: if anything is not exactly success
# (failure, cancelled, skipped on a non-bump path), the required-checks
# context reports FAILURE — deploy_qualify then refuses promotion.
# Fail-closed: not verified → not deployable.
if: >-
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
&& (needs.lint.result != 'success'
|| needs['unit-tests'].result != 'success'
|| needs.canary.result != 'success'
|| needs['best-practices'].result != 'success'
|| needs.documentation.result != 'success'
|| needs['type-check'].result != 'success'
|| needs['gitea-offline'].result != 'success') }}
run: |
# All needs must have succeeded (skipped deps = this is a bump PR, handled above)
FAILED=""
for job in lint unit-tests canary best-practices documentation type-check gitea-offline; do
RESULT=$(cat /tmp/need_$job 2>/dev/null || echo "")
done
echo "All required CI checks passed"
echo "::error::Required CI checks not green — refusing to publish required-checks success"
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
exit 1
notify-event-bridge:
# T16 §16C: post the CI completion WAKE to the local event bridge so
# event-driven waiters (deploy.yml → ci_event_wait.py) resume on the
# event instead of sleep-polling. ALWAYS runs (success or failure —
# a red CI must wake the waiter just as loudly as a green one). The
# wake is best-effort and never decides the verdict: the waiter
# re-reads the canonical Gitea state via deploy_qualify.py.
name: CI Pipeline / notify-event-bridge
runs-on: rune-ci-desktop
needs: [required-checks]
if: always()
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 1
- name: Wake event bridge (wake only — never completion truth)
env:
CI_SHA: ${{ gitea.sha }}
CI_RESULT: ${{ needs.required-checks.result }}
RUNE_EVENT_BRIDGE_URL: ${{ vars.RUNE_EVENT_BRIDGE_URL || 'http://127.0.0.1:8791' }}
run: python3 scripts/notify_event_bridge.py
+78 -17
View File
@@ -19,38 +19,74 @@ jobs:
with:
fetch-depth: 0
- name: Wait for CI qualification on this exact SHA
- name: Wait for CI qualification on this exact SHA (event-driven, T16 §16C)
# T16 §16C/§16D: the old sleep-30 → query → repeat loop is gone.
# The waiter registers a correlation on the W/R event bridge and
# yields in select() until ci.yml's notify-event-bridge job posts
# the completion WAKE; the verdict is then re-read from canonical
# Gitea (deploy_qualify.py). The 900s deadline is a FAILURE
# DETECTOR (lost-event recovery), never the scheduler.
env:
GITEA_SERVER: ${{ gitea.server_url }}
GITEA_REPO: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
# Shared bridge data root: the deploy step runs as the runner
# user while the daemon runs as nick — the rendezvous state
# (waits/events/resumes/leases) must be shared. §16C live
# finding: defaulting to the invoking user's home made the
# controller unable to create its FIFO (PermissionError) and
# the deploy failed fast+truthfully on both 0.66.63 pushes.
RUNE_WR_ROOT: /mnt/workdrive/wr-bridge
RUNE_WR_CODE: /home/nick/.local/share/mimo-wait-resume
run: |
DEPLOY_SHA=$(git rev-parse HEAD)
export DEPLOY_SHA
echo "Qualifying $DEPLOY_SHA"
DEADLINE=$((SECONDS + 900))
while [ $SECONDS -lt $DEADLINE ]; do
STATUS=$(python3 scripts/deploy_qualify.py)
echo "required-checks: $STATUS"
if [ "$STATUS" = "success" ]; then exit 0; fi
if [ "$STATUS" = "failure" ]; then
echo "::error::CI failed on $DEPLOY_SHA — deployment refused"
exit 1
fi
sleep 30
done
echo "::error::CI qualification timeout on $DEPLOY_SHA — deployment refused"
exit 1
echo "Qualifying $DEPLOY_SHA (event-driven)"
# READINESS BEFORE ACTION (§16U): fail with a clear message if
# the bridge rendezvous is not writable in THIS user's context,
# instead of a stack trace at FIFO creation time.
if ! touch /mnt/workdrive/wr-bridge/resumes/.write-probe 2>/dev/null; then
echo "::error::RUNE_WR_ROOT not writable by $(id -un) — bridge data root misconfigured"
exit 1
fi
rm -f /mnt/workdrive/wr-bridge/resumes/.write-probe
python3 scripts/ci_event_wait.py --sha "$DEPLOY_SHA" --deadline 900
- name: Stage, verify, promote
- name: Stage, verify, promote (monotonic)
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
GITEA_SERVER: ${{ gitea.server_url }}
GITEA_REPO: ${{ gitea.repository }}
run: |
python3 scripts/deploy_stage.py
- name: Post-deploy runtime verification (the consuming path)
- name: Promotion truthfulness check
id: promotion_truth
run: |
SHA=$(git rev-parse HEAD)
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
if [ "$CURRENT_TARGET" = "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
echo "PROMOTED: current == candidate ($SHA)"
echo "promoted=true" >> "$GITEA_OUTPUT"
else
# Not promoted. deploy_stage exit 0 + non-matching current =
# NOOP_ALREADY_CURRENT or STALE_SUPERSEDED — truthful non-error
# terminal states (a newer build already won). Do NOT claim
# promotion; the runtime-verification step below only runs for
# real promotions.
echo "NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop — current is authoritative)"
echo "promoted=false" >> "$GITEA_OUTPUT"
fi
- name: Post-deploy runtime verification (only for real promotions)
if: ${{ success() }}
run: |
SHA=$(git rev-parse HEAD)
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
if [ "$CURRENT_TARGET" != "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
echo "SKIP runtime verification: this job did not promote (superseded/noop)"
exit 0
fi
# Exactly what the launcher execs: cd <current>/src, import rune.
RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory \
/home/nick/.venvs/memory/bin/python3 -c "
@@ -62,3 +98,28 @@ jobs:
assert rune.__version__ == info['version']
print('RUNTIME VERIFIED:', info)
"
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
# The PRIMARY convergence mechanism is deploy_stage.py's out-of-lock
# publisher (publishes from CURRENT deployment authority, §14B/§14C).
# This step is a non-blocking FALLBACK: if the primary publish failed
# (e.g. missing Gitea config), this retries. NEVER fails the job —
# documentation failure never outweighs deployment validity.
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
continue-on-error: true
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
run: |
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)" 2>/dev/null || echo "")
if [ -z "$VERSION" ]; then
echo "SKIP: cannot determine version"
exit 0
fi
python3 scripts/release_history_cli.py refresh \
--version "$VERSION" \
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
--store "$GITHUB_WORKSPACE/store/scaffold/current.json" \
--release-sha "$(git rev-parse HEAD)" \
--deployed-sha "$(git rev-parse HEAD)" \
|| echo "::warning::release-history fallback refresh FAILED — in-lock hook may have already converged; retry converges"
+9 -6
View File
@@ -8,7 +8,7 @@ on:
jobs:
lint-and-format:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
- name: Lint Python files
@@ -18,13 +18,16 @@ jobs:
ruff format shared/ src/ || true
canary-suite:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
- name: Run all canaries
# T16 §16Q reconciliation: the glob looped shared/canary_*.py —
# real canaries live in src/rune/ (canary_native.MODULES is the
# registry authority). The old job silently no-opped.
run: |
FAILED=0
for canary in shared/canary_*.py; do
for canary in src/rune/canary_*.py; do
if [ -f "$canary" ]; then
echo "Running $canary..."
python3 "$canary" || FAILED=$((FAILED + 1))
@@ -34,7 +37,7 @@ jobs:
exit $FAILED
best-practices:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
- name: Check best practices
@@ -69,7 +72,7 @@ jobs:
grep -rn "TODO\|FIXME\|HACK\|XXX" shared/ src/ --include="*.py" | head -10 || echo " Clean"
documentation-check:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
- name: Check documentation coverage
@@ -110,7 +113,7 @@ jobs:
# push-triggered safety net in auto-version.yml. Detects master/tag drift
# and files a deduped issue — it never auto-releases without the PR flow.
release-drift-reconciliation:
runs-on: rune-infra-pi
runs-on: rune-ci-desktop
steps:
- uses: actions/checkout@v4
with:
+1
View File
@@ -14,3 +14,4 @@ state/
# Runtime artifacts (cwd-local event journal + capability index)
index/
journal/
src/rust/*/target/
+26
View File
@@ -26,6 +26,9 @@ Only use `git commit --no-verify` in a genuine emergency: it bypasses the hooks
- Every workflow is registered in the pipeline registry — new pipeline machinery ships with documentation (in ./.gitea/workflows/*.yml)
- Every TUI surface is registered — no silent UI growth (in ./src/rune/*tui*.py)
- New rune modules ship with tests or a canary — no ungoverned code surfaces (in ./src/rune/*.py)
- Rust crate files stay under a line budget (in ./src/rust/*/src/*.rs)
- Go modules stay under a line budget (in ./src/go/**/*.go)
- No secrets in Rust or Go source files (in ./src/rust/*/src/*.rs, ./src/go/**/*.go)
- PRDs are living documents — their generated state sections regenerate identically from governed sources
- PRD staleness is a CI failure — docs rot is silent failure (in ./scripts/gen_prd_state.py)
- The adversarial review procedure is binding — dispatch templates must reference it (in ./docs/adversarial-review.md)
@@ -58,3 +61,26 @@ fix, full pipeline re-entry (suite + ratchet + ruff + canaries + henxels;
gates for anything beyond a mechanical minor), regression tests frozen forever,
discharge recorded finding → test → commit. Symptom patches are rejected at
review. To escape: edit `henxels.yaml` or this file — visibly.
## Issue organisation standard (binding, 2026-09-19)
One open note per idea. Issues use the template spine (kind, subsystem,
version intent) with a plain-English heading that restates the subject —
no meta-labels. Before minting a standalone issue, check whether the work
belongs inside an existing parent:
- **Parked ideas under a blocked/future epic** are filed as checklist items
in the parent's "Absorbed children" section, NOT as standalone issues.
Absorption is reversible — re-mint standalone when the idea becomes live.
- **Live, dependency-tracked, or starting work** gets a standalone issue.
- **Closures state the reason as data**: `closed_because` in the spine
(completed / superseded-by-#N / absorbed-into-#N / wont-do / duplicate),
plus a standard closing comment (plain summary first).
- The reconciliation checker (`scripts/gitea_reconciliation_check.py`) must
stay green after any issue state change; the artifact is updated in the
same change that moves issues.
## Async continuation final flow proofs (R13/M9) — 2026-09-19
PR-flow live proof marker: harmless documentation addition exercising the
canonical single-continuation path on the PC-local forge. No code change.
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.14**
**Current release: v0.131.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.**
+196
View File
@@ -0,0 +1,196 @@
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
with inotify wake. Replaces FIFO+lease for process-wait delivery.
DESIGN:
Producer (bridge/daemon/notify):
1. Validates operation identity
2. Atomically writes a completion file:
resumes/<operation_id>.done
containing the event/verdict JSON (write to tmp, rename for atomicity)
Waiter (ci_event_wait / any continuation consumer):
1. Checks if the completion file already exists (event-before-waiter)
2. If not: registers inotify watch on the resumes/ directory
3. Re-checks after watch registration (closes the registration race)
4. Blocks in kernel on inotify — zero CPU, zero timer
5. On file-created event: reads the file, verifies authority, returns
The event is a WAKE SIGNAL. Authority still decides terminal truth.
The completion file is durable — survives daemon and waiter restarts.
WHY NOT FIFO:
FIFO is one-shot (token consumed), requires lease management, and
fails under session-busy guards. A completion file is atomic, durable,
and works across restarts. inotify wakes the process with zero CPU
cost and zero latency between file creation and wake.
WHY NOT POLLING:
Polling introduces an arbitrary interval. inotify is a kernel callback
that fires exactly when the filesystem changes — the latency tracks
the real completion event, not a configured interval.
"""
from __future__ import annotations
import json
import os
import select
import struct
import time
from pathlib import Path
def _wr_bridge() -> Path:
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
# and any isolated test root always resolve the SAME directory.
root = os.environ.get("WR_ROOT") or os.environ.get("RUNE_WR_ROOT") or "/mnt/workdrive/wr-bridge"
return Path(root)
def _resumes_dir() -> Path:
return _wr_bridge() / "resumes"
# inotify constants (Linux)
IN_CREATE = 0x100
IN_CLOSE_WRITE = 0x8
IN_MOVED_TO = 0x80
def completion_path(operation_id: str) -> Path:
return _resumes_dir() / f"{operation_id}.done"
def write_completion(operation_id: str, event: dict) -> Path:
"""Atomically write a durable completion file. The rename(2) ensures
the waiter never sees a partial file."""
_resumes_dir().mkdir(parents=True, exist_ok=True)
target = completion_path(operation_id)
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
os.rename(str(tmp), str(target)) # atomic on same filesystem
os.chmod(str(target), 0o666) # cross-user bridge
return target
def read_completion(operation_id: str) -> dict | None:
"""Read a durable completion file. Returns None if absent."""
p = completion_path(operation_id)
if not p.is_file():
return None
try:
return json.loads(p.read_text())
except (OSError, ValueError):
return None
def completion_exists(operation_id: str) -> bool:
return completion_path(operation_id).is_file()
def _inotify_init_and_add_watch(directory: str) -> int:
"""Create an inotify instance and add a watch on the directory.
Returns the inotify fd."""
libc = __import__("ctypes").CDLL(None, use_errno=True)
fd = libc.inotify_init()
if fd < 0:
raise OSError("inotify_init failed")
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
mask = IN_CREATE | IN_MOVED_TO
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
if wd < 0:
os.close(fd)
raise OSError(f"inotify_add_watch failed for {directory}")
return fd
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
"""Read pending inotify events. Returns list of filenames that were
created/moved-to. Blocks until at least one event or timeout."""
poll = select.poll()
poll.register(fd, select.POLLIN)
events = []
buf = os.read(fd, 4096)
pos = 0
while pos < len(buf):
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
pos += 16
name = buf[pos : pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
pos += name_len
if mask & (IN_CREATE | IN_MOVED_TO):
events.append(name)
return events
def wait_for_completion(operation_id: str, *, timeout_secs: float | None = None, poll_callback=None) -> dict | None:
"""Block until a completion file exists for this operation.
Uses inotify for zero-latency, zero-CPU wake. Falls back to
polling_callback (if provided) when inotify is unavailable.
The completion file is a WAKE SIGNAL. The caller must still verify
canonical authority after reading it.
Returns the event dict from the completion file, or None on timeout.
"""
target = completion_path(operation_id)
# 1. check-before-watch (event may have arrived before we started)
existing = read_completion(operation_id)
if existing is not None:
return existing
_resumes_dir().mkdir(parents=True, exist_ok=True)
try:
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
except OSError:
# inotify unavailable: fall back to polling callback
if poll_callback:
return poll_callback(operation_id, timeout_secs)
return None
try:
# 2. re-check after watch registration (closes the race between
# the initial check and the watch being active)
existing = read_completion(operation_id)
if existing is not None:
return existing
# 3. block in kernel on inotify — zero CPU, exact latency
import select as _select
poll = _select.poll()
poll.register(fd, _select.POLLIN)
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
while True:
remaining = None
if deadline is not None:
remaining = max(0.1, deadline - time.monotonic())
if remaining <= 0.1:
return None # timeout
events = poll.poll(remaining * 1000 if remaining else None)
if not events:
continue # poll timeout, re-check
names = _inotify_read(fd)
if target.name in names:
# 4. file appeared — read it
existing = read_completion(operation_id)
if existing is not None:
return existing
# partial/corrupt: keep watching
finally:
os.close(fd)
return None
def clear_completion(operation_id: str) -> None:
"""Remove a consumed completion file (idempotent cleanup)."""
p = completion_path(operation_id)
try:
p.unlink()
except OSError:
pass
+799
View File
@@ -0,0 +1,799 @@
#!/usr/bin/env python3
"""daemon.py — Gitea webhook receiver + reconciliation + resume driver (v0.2).
W/R-v0.2 invariants implemented here:
A. SINGLE-FLIGHT PER SESSION — per-session lease (waitstore.acquire_lease,
/proc-validated, atomic) + in-process inflight set. A second READY wait
for the same session stays queued until the lease releases.
B. BUSY SESSION PROTECTION — before spawning, /proc is scanned for a live
`mimo run -s <sid>`-shaped process not owned by our lease. Found → skip
launch (wait stays READY). No invented MiMo APIs; the lease is the
conservative floor against W/R-created concurrency.
C. ATOMIC SESSION LEASE — persisted {session_id, wait_id, pid, pgid,
pid_starttime, acquired_at, state}; O_EXCL create, steal only on /proc
evidence of a dead owner.
D. PROCESS TREE OWNERSHIP — workers spawn with start_new_session=True (own
process group). Watch timeout ⇒ SIGTERM group → bounded grace → SIGKILL
group → verify zero surviving group members → release lease → truthful
READY_TO_RETRY (never terminal-on-timeout).
E. TIMEOUT IS NOT SUCCESS — rc==0 maps to verdict terminal
(COMPLETED/FAILED); rc!=0 or unverifiable ⇒ RESUME_UNVERIFIED
(not schedulable, operator-requeueable); timeout ⇒ READY_TO_RETRY.
F. STALE RESUMING RECOVERY — on startup, every RESUMING record is
inspected: live lease owner ⇒ adopted and monitored; otherwise ⇒
RESUME_UNVERIFIED (+ stale lease released). Never auto-relaunched.
G. MULTIPLE WAITS, SAME SESSION — schedulable() orders oldest-first; one
continuation finishes before the next starts.
H. PIDFILE CORRECTNESS — singleton enforced by flock on daemon.pid; the
lock dies with the process, so stale pidfiles (incl. SIGKILL) cannot
admit a second daemon.
The resume command is configurable (WR_RESUME_CMD, JSON list with
{session_id}/{msg} placeholders) so the test harness can use a stub; the
default is the real mechanism: `mimo run -s <sid> -m <model> <msg>`.
"""
from __future__ import annotations
import fcntl, json, os, signal, subprocess, sys, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import waitstore
import verify
import completion_file # REAL_ASYNC_BRIDGE R3: durable completion-file delivery
RESUME_MODEL = "Verboo Code/glm-5.3-flash"
RECONCILE_SECS = int(os.environ.get("WR_RECONCILE_SECS", "60"))
RECONCILE_EVERY_TICKS = max(1, int(os.environ.get("WR_RECONCILE_TICKS", "3")))
WAKE_MIN_AGE_SECS = float(os.environ.get("WR_WAKE_MIN_AGE_SECS", "5"))
WAKE_RETRIES = int(os.environ.get("WR_WAKE_RETRIES", "3"))
WAKE_RETRY_SECS = float(os.environ.get("WR_WAKE_RETRY_SECS", "4"))
DRIVE_LOOP_SECS = int(os.environ.get("WR_DRIVE_SECS", "20"))
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
# progress is an orphaned resume flow (thread died between lease acquire
# and release). Must exceed the longest legitimate in-flight watch
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
WATCH_SECS = int(os.environ.get("WR_WATCH_SECS", "600"))
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
# progress is an orphaned resume flow (thread died between lease acquire
# and release). Must exceed the longest legitimate in-flight watch
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
ORPHAN_GRACE_SECS = int(os.environ.get("WR_ORPHAN_GRACE_SECS", str(3 * WATCH_SECS)))
TERM_GRACE_SECS = int(os.environ.get("WR_TERM_GRACE_SECS", "10"))
MAX_ATTEMPTS = int(os.environ.get("WR_MAX_ATTEMPTS", "3"))
_WR_ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
PIDFILE = _WR_ROOT / "daemon.pid"
LOG = _WR_ROOT / "daemon.log"
DEFAULT_RESUME_CMD = ["mimo", "run", "-s", "{session_id}", "-m", RESUME_MODEL, "{msg}"]
def log(msg: str) -> None:
with open(LOG, "a") as f:
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
def resume_cmd(session_id: str, msg: str, wait_id: str = "", override: list | None = None) -> list[str]:
"""T15 §15D: per-wait override wins; placeholders {session_id}/{msg}/
{wait_id}. A synthetic controller session (Scaffolding stage driver)
is not a MiMo session — the global default cannot resume it, which is
exactly why fixed-sleep polling crept back into T12-T14."""
if override:
return [
c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in override
]
tmpl = os.environ.get("WR_RESUME_CMD")
cmd = json.loads(tmpl) if tmpl else DEFAULT_RESUME_CMD
return [c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in cmd]
# ── process-tree evidence & ownership ─────────────────────────────────────
def pgid_members(pgid: int) -> list[int]:
out = []
for d in Path("/proc").iterdir():
if not d.name.isdigit():
continue
try:
stat = (d / "stat").read_text()
fields = stat.rsplit(")", 1)[1].split()
# zombies: killed but not yet reaped. Counting them makes the
# survivor check report the direct child we just KILLed before
# communicate() reaps it (observed live 2026-09-12).
if fields[0] == "Z":
continue
if int(fields[2]) == pgid:
out.append(int(d.name))
except (OSError, IndexError, ValueError):
continue
return out
def terminate_tree(pgid: int) -> list[int]:
"""TERM → bounded grace → KILL. Returns surviving members (must be [])."""
try:
os.killpg(pgid, signal.SIGTERM)
except ProcessLookupError:
pass
deadline = time.time() + TERM_GRACE_SECS
while time.time() < deadline:
if not pgid_members(pgid):
return []
time.sleep(0.2)
try:
os.killpg(pgid, signal.SIGKILL)
except ProcessLookupError:
pass
for _ in range(50): # up to 10s for KILL to land
if not pgid_members(pgid):
return []
time.sleep(0.2)
return pgid_members(pgid)
def stray_worker_pids(session_id: str, exclude_pgids: set[int] = ()) -> list[int]:
"""Live `mimo run -s <sid>`-shaped processes not owned by our workers."""
out = []
for d in Path("/proc").iterdir():
if not d.name.isdigit():
continue
try:
parts = [p.decode(errors="replace") for p in (d / "cmdline").read_bytes().split(b"\0") if p]
except OSError:
continue
if not parts:
continue
joined = " ".join(parts)
if session_id not in joined or "-s" not in parts:
continue
if "mimo" not in joined.lower() and "@mimo-ai" not in joined.lower():
continue
try:
if os.getpgid(int(d.name)) in exclude_pgids:
continue
except OSError:
continue
out.append(int(d.name))
return out
# ── verification → transition (unchanged authority model) ─────────────────
def _authoritative_verify(rec: dict) -> tuple[str, dict]:
"""Authoritative verification with a deterministic test seam.
Production (WR_VERIFY_STUB unset): real Gitea API via verify.py — the
ONLY source of success/failure truth. Test (WR_VERIFY_STUB=path to
JSON {"<run_ref-or-sha>": verdict, "default": verdict}): stubbed
verdicts so invariant tests never touch the network. The stub is a
test fixture, not an alternate authority — no production path sets it.
"""
stub_path = os.environ.get("WR_VERIFY_STUB", "")
if stub_path:
try:
spec = json.loads(Path(stub_path).read_text())
except Exception:
return "pending", {"stub_error": "unreadable stub"}
key = str(rec.get("run_ref", "") or rec.get("sha", ""))
verdict = spec.get(key, spec.get("default", "pending"))
return verdict, {"stub": True, "key": key}
import verify
if rec.get("wait_kind") == "RELEASE_DEPLOY":
return verify.verify_release_deploy(rec.get("sha", ""), rec.get("repo_root", ""), rec.get("deployed_link", ""))
return verify.verify_run(rec.get("run_ref", ""), rec.get("sha", ""), rec.get("expected", "success"))
def try_transition(rec: dict, event: dict | None = None) -> dict:
if rec["status"] != "WAITING_EXTERNAL":
return rec
if rec.get("wait_kind") == "PROCESS_EVENT":
# T15 §15D: the matching event IS the authority. The notifier
# wrapper observed the real process exit; exit_code decides the
# verdict. No Gitea lookup applies. A duplicate event never
# reaches here (match applies only to WAITING_EXTERNAL waits).
if not event:
return rec # no event → nothing authoritative happened
try:
exit_code = int(event.get("exit_code"))
except (TypeError, ValueError):
exit_code = None
if exit_code is None:
return rec # malformed event: stay WAITING (15E hostile case)
verdict = "success" if exit_code == 0 else "failure"
evidence = {"process_event": True, "exit_code": exit_code, "operation_id": event.get("run_ref", "")}
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
rec["status"] = "READY_TO_RESUME"
rec["verdict"] = verdict
if verdict == "failure":
rec["failure_evidence"] = evidence
rec["process_event_evidence"] = evidence
waitstore.save(rec)
return rec
verdict, evidence = _authoritative_verify(rec)
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
if verdict == "success":
rec["status"] = "READY_TO_RESUME"
rec["verdict"] = "success"
elif verdict == "failure":
rec["status"] = "READY_TO_RESUME"
rec["verdict"] = "failure"
rec["failure_evidence"] = verify.failed_job_evidence(rec.get("run_ref", ""))
else:
return rec # pending/unknown → stay WAITING
waitstore.save(rec)
return rec
def continuation_msg(rec: dict) -> str:
c = rec["continuation"]
if rec.get("verdict") == "success":
return (
f"External wait satisfied (wait {rec['wait_id']}).\n"
f"Original objective: {c.get('objective', '')}\n"
f"Waited for: {c.get('waited_for', '')}\n"
f"Authoritative result: workflow success verified via Gitea API.\n"
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
f"Verify: {c.get('verify_when_woken', '')}\n"
f"Continue from: {c.get('on_success', '')}"
)
ev = json.dumps(rec.get("failure_evidence", {}))[:400]
return (
f"External wait finished with FAILURE (wait {rec['wait_id']}).\n"
f"Original objective: {c.get('objective', '')}\n"
f"Authoritative result: workflow FAILURE verified via Gitea API.\n"
f"Failed job evidence: {ev}\n"
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
f"Do NOT treat this as success. Continue from: {c.get('on_failure', '')}"
)
# ── resume driver (single-flight, tree-owned) ─────────────────────────────
def resume(rec: dict) -> None:
"""Run one continuation to completion. Runs on a dedicated worker thread.
Preconditions: caller verified no inflight worker for this session."""
sid, wid = rec["session_id"], rec["wait_id"]
# REAL_ASYNC_BRIDGE §2 invariant: a successful authority-verified
# operation completion must NEVER be overwritten or downgraded by a
# legacy session/FIFO delivery failure. Terminal operation state
# (COMPLETED/FAILED — stamped by the waiter's own authority
# verification) is owned by the OPERATION domain; this function owns
# only SESSION-DELIVERY. Re-read from disk: the in-memory copy may be
# stale relative to the waiter's terminal stamp.
fresh = waitstore.load(wid)
if fresh and fresh.get("status") in waitstore.TERMINAL:
log(
f"resume {wid}: skipped — operation already terminal "
f"({fresh['status']}); delivery worker never overrides "
f"operation truth"
)
return
rec = fresh or rec
msg = continuation_msg(rec)
# B: busy-session guard before touching the lease
strays = stray_worker_pids(sid)
if strays:
rec["status"] = "READY_TO_RESUME"
rec["resume_note"] = f"queued: target session busy (stray pids {strays})"
waitstore.save(rec)
log(f"queued {wid}: session busy {strays}")
return
# pre-register the lease owner as THIS daemon so acquisition is atomic
lease = waitstore.acquire_lease(sid, wid, os.getpid(), os.getpid())
if lease is None:
live = waitstore.read_lease(sid)
rec["status"] = "READY_TO_RESUME"
rec["resume_note"] = f"queued: lease held by wait {live and live.get('wait_id')}"
waitstore.save(rec)
log(f"queued {wid}: lease held")
return
rec["status"] = "RESUMING"
rec["resume_attempts"] = rec.get("resume_attempts", 0) + 1
waitstore.save(rec)
try:
proc = subprocess.Popen(
resume_cmd(sid, msg, wid, rec.get("resume_cmd")),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
except Exception as e:
waitstore.release_lease(sid, wid)
rec["status"] = "READY_TO_RETRY" # spawn failed pre-delivery → retryable
rec["resume_note"] = f"spawn failed: {e}"
waitstore.save(rec)
log(f"spawn {wid} failed pre-delivery: {e}")
return
pgid = proc.pid # start_new_session ⇒ pgid == child pid
waitstore.update_lease(sid, wait_id=wid, pid=proc.pid, pgid=pgid, pid_starttime=waitstore._pid_starttime(proc.pid))
log(f"launched {wid}: pid={proc.pid} pgid={pgid} attempt={rec['resume_attempts']}")
timed_out = False
try:
stdout, stderr = proc.communicate(timeout=WATCH_SECS)
rc = proc.returncode
except subprocess.TimeoutExpired:
timed_out = True
survivors = terminate_tree(pgid)
try:
stdout, stderr = proc.communicate(timeout=30)
except Exception:
stdout, stderr = "", ""
rc = proc.returncode
if survivors:
log(f"ERROR {wid}: group {pgid} survivors after KILL: {survivors}")
waitstore.release_lease(sid, wid)
if timed_out:
# D+E: timeout is NOT success. Tree terminated; retry truthfully.
if rec["resume_attempts"] >= MAX_ATTEMPTS:
rec["status"] = "RESUME_UNVERIFIED"
rec["resume_note"] = (
f"watch timeout x{rec['resume_attempts']}; process group {pgid} terminated; attempts exhausted"
)
else:
rec["status"] = "READY_TO_RETRY"
rec["resume_note"] = f"watch timeout; process group {pgid} terminated (TERM→KILL, verified); will retry"
log(f"timeout {wid}: tree {pgid} terminated, rc={rc}")
elif rc == 0:
# COMPLETION TRUTH (constraint 4): rc==0 means the headless worker
# EXITED CLEANLY — the strongest terminal evidence the supported CLI
# provides. Timeout/cancel can never reach this branch. The evidence
# chain (rc, output tail, timing) is recorded on the wait record.
terminal = "COMPLETED" if rec.get("verdict") == "success" else "FAILED"
rec["status"] = terminal
rec["resume_evidence"] = {
"kind": "worker_exited_rc0",
"rc": 0,
"output_tail": (stdout or "")[-400:],
"finished_at": waitstore.now(),
}
log(f"resumed {wid}: rc=0 → {terminal}")
else:
# Nonzero rc: delivery/completion unproven. Honest, not schedulable.
rec["status"] = "RESUME_UNVERIFIED"
rec["resume_evidence"] = {
"kind": "worker_exited_nonzero",
"rc": rc,
"output_tail": ((stdout or "") + (stderr or ""))[-400:],
"finished_at": waitstore.now(),
}
rec["resume_note"] = f"resume rc={rc}; delivery unverified"
log(f"resumed {wid}: rc={rc} → RESUME_UNVERIFIED")
waitstore.save(rec)
# ── startup recovery (invariant F) ────────────────────────────────────────
def startup_recovery() -> None:
for rec in waitstore.all_waits():
if rec.get("status") != "RESUMING":
continue
sid, wid = rec["session_id"], rec["wait_id"]
lease = waitstore.read_lease(sid)
if lease and lease.get("wait_id") == wid and waitstore._owner_alive(lease):
# Adopt the live worker and monitor it to a bounded conclusion.
pgid = lease["pgid"]
log(f"recover {wid}: adopted live worker pgid={pgid}")
deadline = time.time() + WATCH_SECS
while time.time() < deadline and pgid_members(pgid):
time.sleep(2)
survivors = terminate_tree(pgid) if pgid_members(pgid) else []
waitstore.release_lease(sid, wid)
rec["status"] = "RESUME_UNVERIFIED"
rec["resume_note"] = "adopted post-restart; outcome unverifiable" + (
f"; survivors killed: {survivors}" if survivors else ""
)
waitstore.save(rec)
log(f"recover {wid}: adopted worker ended → RESUME_UNVERIFIED")
else:
if lease:
waitstore.release_lease(sid, wid) # stale
rec["status"] = "RESUME_UNVERIFIED"
rec["resume_note"] = "recovered at daemon startup: no live owner; delivery unverifiable"
waitstore.save(rec)
log(f"recover {wid}: no live owner → RESUME_UNVERIFIED")
# ── reconciler + drive loop (invariant G: deterministic queue) ────────────
_inflight: dict[str, threading.Thread] = {}
_inflight_lock = threading.Lock()
def drive_ready() -> None:
for rec in waitstore.schedulable():
sid = rec["session_id"]
# CONTENTION POLICY (constraint 2): MiMo exposes NO supported
# busy/idle session signal (verified 2026-09-12: `mimo session list
# --format json` has no status field; no `session status` command;
# no status column in the session schema; no documented ACP/serve
# endpoint). Therefore automatic injection into interactive sessions
# is NOT YET SAFE and stays disabled. Only waits explicitly declared
# target_kind="headless" (disposable/owned sessions) auto-dispatch.
if rec.get("target_kind") != "headless":
if not rec.get("hold_reason"):
rec["hold_reason"] = (
"INTERACTIVE_AUTO_RESUME = NOT YET SAFE: "
"no supported MiMo busy/idle signal; "
"queued until operator releases"
)
waitstore.save(rec)
continue
if rec.get("ext", {}).get("resume_mode") == "self":
# REAL_ASYNC_BRIDGE §3: self-resuming wait (completion-file /
# inotify waiter owns its continuation). The daemon has NO
# delivery role here — dispatching a delivery worker would
# stamp a phantom rc!=0 over the waiter's own truthful
# terminal state (the canonical-state contradiction).
# NB: keyed on the explicit self marker, NOT on resume_cmd
# absence — records without inline resume_cmd may still be
# dispatchable via the WR_RESUME_CMD env template.
continue
with _inflight_lock:
t = _inflight.get(sid)
if t is not None and t.is_alive():
continue # single-flight: session already being driven
# B: never launch into a busy session
strays = stray_worker_pids(sid)
if strays:
log(f"drive: {rec['wait_id']} held, session busy {strays}")
continue
th = threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}")
_inflight[sid] = th
# start INSIDE the lock: a just-created thread is not yet
# is_alive() until start() runs — starting outside the lock let
# the same drive cycle dispatch a second worker for the session
# (single-flight race, caught by the T2 invariant test).
th.start()
log(f"drive: dispatched {rec['wait_id']} (session {sid})")
def reconcile_once() -> None:
for rec in waitstore.pending():
if rec["status"] != "WAITING_EXTERNAL":
continue
created = rec.get("created_at", "")
try:
import calendar
age = time.time() - calendar.timegm(time.strptime(created, "%Y-%m-%dT%H:%M:%SZ"))
except ValueError:
continue
if age > RECONCILE_SECS:
try_transition(rec)
_tick = 0
def orphan_sweep() -> int:
"""REAL_ASYNC_BRIDGE §4: permanent mid-run orphan-lease recovery.
FAILURE DETECTION ONLY — never involved in normal completion
scheduling. A wait stuck in RESUMING past ORPHAN_GRACE_SECS means the
resume flow died between lease acquire and release (worker thread
lost, exception mid-watch). Reconcile exactly as startup_recovery
would have: release the lease, stamp RESUME_UNVERIFIED truthfully
(delivery unverifiable), never auto-relaunch.
"""
import calendar as _cal
healed = 0
for rec in waitstore.all_waits():
if rec.get("status") != "RESUMING":
continue
try:
updated = _cal.timegm(time.strptime(rec.get("last_updated", ""), "%Y-%m-%dT%H:%M:%SZ"))
except ValueError:
continue
if time.time() - updated < ORPHAN_GRACE_SECS:
continue # legitimate in-flight watch (up to WATCH_SECS)
wid, sid = rec["wait_id"], rec["session_id"]
lease = waitstore.read_lease(sid)
owner_dead = lease is not None and not waitstore._owner_alive(lease)
daemon_owned = lease is not None and lease.get("pid") == os.getpid()
if lease is None or owner_dead or daemon_owned:
if lease is not None:
waitstore.release_lease(sid, wid)
if rec.get("resume_attempts", 0) >= MAX_ATTEMPTS:
rec["status"] = "RESUME_UNVERIFIED"
else:
rec["status"] = "READY_TO_RETRY"
rec["resume_note"] = (
"orphaned mid-run: resume flow died between lease acquire "
"and delivery ("
+ ("lease owner dead" if owner_dead else "resume thread lost" if daemon_owned else "no lease")
+ "); orphan sweep recovered"
)
waitstore.save(rec)
log(f"orphan-sweep {wid}: {rec['status']} — {rec['resume_note']}")
healed += 1
return healed
def reconciler() -> None:
global _tick
while True:
# v0.3: the local-runner wake is the fast path; periodic
# authoritative verification demotes to a slower fallback
# (missed-event + daemon-restart recovery), not API polling.
_tick += 1
if _tick % RECONCILE_EVERY_TICKS == 0:
try:
reconcile_once()
except Exception as e:
log(f"reconcile error: {e}")
try:
orphan_sweep() # §4 failure detection — bounded, periodic
except Exception as e:
log(f"orphan-sweep error: {e}")
try:
drive_ready()
except Exception as e:
log(f"drive error: {e}")
with _inflight_lock:
for sid, t in list(_inflight.items()):
if not t.is_alive():
del _inflight[sid]
time.sleep(DRIVE_LOOP_SECS)
# ── webhook receiver (wake signal only) ───────────────────────────────────
def wake_sweep(event: dict, eid: str) -> dict:
"""Sweep WAITING_EXTERNAL waits through authoritative verification.
Runs on a background thread (the hook's HTTP response returns
instantly, minimising the runner's offline window). If any verified
wait got 'pending' — the normal case for a job that finished
microseconds ago: the post-task hook fires after results are reported
but before the runner's final task acknowledgement flips server-side
status — retry on a BOUNDED event-triggered schedule (default 4s x3),
never a polling loop. The final ack lands milliseconds after the hook
exits, so the first retry almost always sees the terminal state.
"""
arrival = time.time()
log(f"local wake {eid}: {json.dumps(event)[:140]}")
import calendar
def sweep() -> tuple[int, int, int]:
verified = advanced = pending = 0
for rec in waitstore.pending():
if rec["status"] != "WAITING_EXTERNAL":
continue
try:
age = time.time() - calendar.timegm(time.strptime(rec.get("created_at", ""), "%Y-%m-%dT%H:%M:%SZ"))
except ValueError:
continue
if age < WAKE_MIN_AGE_SECS:
continue
verified += 1
before = rec["status"]
try_transition(rec)
if rec["status"] != before and rec["status"] == "READY_TO_RESUME":
advanced += 1
elif rec["status"] == "WAITING_EXTERNAL":
pending += 1
return verified, advanced, pending
verified, advanced, pending = sweep()
log(
f"local wake {eid}: verified={verified} advanced={advanced} "
f"pending={pending} sweep_ms={int((time.time() - arrival) * 1000)}"
)
for attempt in range(1, WAKE_RETRIES + 1):
# Retry while the sweep didn't conclusively advance anything:
# pending verdicts (ack-window) AND fresh-wait skips (age gate).
# Bounded, event-triggered — not a polling loop.
if not pending and verified:
break
time.sleep(WAKE_RETRY_SECS)
verified, advanced, pending = sweep()
log(
f"local wake {eid}: retry{attempt} verified={verified} "
f"advanced={advanced} pending={pending} "
f"total_ms={int((time.time() - arrival) * 1000)}"
)
return {"event_id": eid}
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/process-event":
# T15 §15D: process-exit events from builder/controller
# notifiers. Minimal dedicated endpoint — the Gitea webhook
# path stays untouched. Payload: {operation_id, exit_code,
# completed_at} + OPTIONAL provider/event_type/sha (T16
# §16C/§16E: the CI adapter registers as provider="gitea",
# event_type="ci_completed"; defaults preserve the T15 builder
# identity so existing notifiers are unaffected). The event is
# a WAKE, never truth — resumed controllers re-read canonical
# authority before any verdict.
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
payload = json.loads(body) if body else {}
except json.JSONDecodeError:
self.send_response(400)
self.end_headers()
return
event = {
"provider": str(payload.get("provider", "builder")),
"event_type": str(payload.get("event_type", "builder_exit")),
"run_ref": str(payload.get("operation_id", "")) or None,
"sha": payload.get("sha"),
"exit_code": payload.get("exit_code"),
"completed_at": payload.get("completed_at") or "",
}
event = {k: v for k, v in event.items() if v is not None}
eid = waitstore.record_event(event)
log(f"process-event {eid}: {json.dumps(event)[:140]}")
# REAL_ASYNC_BRIDGE (R3): durable completion-file delivery.
# Every event reaching the bridge is written as
# resumes/<operation_id>.done so inotify waiters wake on the
# kernel event (R4) with no FIFO/lease machinery, and the
# event survives daemon AND waiter restarts (R9). Atomic
# rename; duplicate events rewrite identical content (R10).
_op_id = str(payload.get("operation_id", ""))
if _op_id:
try:
# Stable identity only: duplicate deliveries of the
# same logical event must produce byte-identical
# completion files (R10 idempotence by construction).
# completed_at is volatile and lives in the durable
# event log; the file is a WAKE SIGNAL, never truth.
_stable = {
k: event.get(k)
for k in ("provider", "event_type", "run_ref", "sha", "exit_code")
if event.get(k) is not None
}
completion_file.write_completion(_op_id, _stable)
except Exception as e: # noqa: BLE001 — delivery best-effort; wait-record transition + watchdog recovery still cover the lost file
log(f"process-event {eid}: completion-file write failed: {e}")
matched = 0
for rec in waitstore.pending():
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
matched += 1
try_transition(rec, event)
# matched READY controller waits are dispatched IMMEDIATELY
# (event-driven, no floor). Guard: only never-resumed waits —
# a duplicate/late event must not double-dispatch (15E);
# single-flight lease + this guard together make resume
# idempotent.
for rec in waitstore.pending():
if rec["status"] == "READY_TO_RESUME" and rec.get("resume_cmd") and rec.get("resume_attempts", 0) == 0:
threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}").start()
matched += 1
try:
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
except (BrokenPipeError, ConnectionResetError):
pass
return
if self.path == "/wake":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
payload = json.loads(body) if body else {}
except json.JSONDecodeError:
self.send_response(400)
self.end_headers()
return
event = {
"provider": "gitea-local-runner",
"task_id": str(payload.get("task_id", "")) or None,
"run_id": str(payload.get("run_id", "")) or None,
"repository": str(payload.get("repository", "")) or None,
"job_result": str(payload.get("job_result", "")) or None,
}
event = {k: v for k, v in event.items() if v}
eid = waitstore.record_event(event)
# Respond instantly; sweep on a worker thread. The hook is
# fire-and-forget (curl -m 2) — a broken pipe on write is
# harmless and must not propagate.
try:
threading.Thread(
target=wake_sweep, args=(event, eid), daemon=True, name=f"wake-{payload.get('task_id', '?')}"
).start()
except Exception as e:
log(f"wake spawn error: {e}")
try:
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({"queued": True, "event_id": eid}).encode())
except (BrokenPipeError, ConnectionResetError):
pass
return
if self.path != "/webhook":
self.send_response(404)
self.end_headers()
return
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
payload = json.loads(body)
except json.JSONDecodeError:
self.send_response(400)
self.end_headers()
return
action = payload.get("action") or payload.get("event_type") or ""
wf = payload.get("workflow_run") or {}
event = {
"provider": "gitea",
"event_type": f"workflow.{action}" if wf else action,
"sha": (wf.get("head_sha") or payload.get("sha") or "")[:12] or None,
"run_ref": str(wf.get("id") or payload.get("run_number") or "") or None,
}
event = {k: v for k, v in event.items() if v}
eid = waitstore.record_event(event)
log(f"webhook event {eid}: {json.dumps(event)[:140]}")
matched = 0
for rec in waitstore.pending():
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
matched += 1
try_transition(rec)
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.end_headers()
self.wfile.write(b'{"ok":true}')
return
self.send_response(404)
self.end_headers()
def log_message(self, *a):
pass
def main() -> int:
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8791
# H (constraint 1): flock is the SINGLETON AUTHORITY. daemon.pid's
# content is diagnostic metadata only — it never gates startup, never
# implies liveness, and is overwritten after the exclusive lock is held.
# Stale pid content (incl. SIGKILL debris) is thus harmless by design.
PIDFILE.parent.mkdir(parents=True, exist_ok=True)
lock_fd = os.open(PIDFILE, os.O_CREAT | os.O_RDWR, 0o644)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
log("refusing start: another daemon holds the pidfile flock")
print("daemon already running (pidfile flock held)", file=sys.stderr)
return 1
os.ftruncate(lock_fd, 0)
os.write(lock_fd, f"{os.getpid()}\n".encode()) # diagnostic, lock held for life
startup_recovery()
t = threading.Thread(target=reconciler, daemon=True)
t.start()
log(f"daemon starting on :{port}")
srv = ThreadingHTTPServer(("0.0.0.0", port), Handler)
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
sys.exit(main())
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""verify.py — authoritative Gitea verification (webhook ≠ success).
Queries the Gitea API for the TRUE state of the run/job/commit the wait
references. Returns (verdict, evidence). verdict ∈ {success, failure,
pending, unknown}. Only this module may move a wait out of
WAITING_EXTERNAL.
"""
from __future__ import annotations
import json, subprocess
from pathlib import Path
HOST = "https://gitea.thingscouldgetdicey.online"
REPO = "conductor/rune"
def _token() -> str:
# Canonical ladder without importing Rune: env → token file → git store
import os
tok = os.environ.get("GITEA_TOKEN", "").strip()
if tok:
return tok
tf = os.path.expanduser("~/.config/opencode/secrets/gitea-token")
if os.path.isfile(tf):
return open(tf).read().strip()
r = subprocess.run(
["git", "credential", "fill"],
input="protocol=https\nhost=gitea.thingscouldgetdicey.online\nusername=conductor\n\n",
capture_output=True,
text=True,
timeout=10,
)
for line in r.stdout.splitlines():
if line.startswith("password="):
return line[9:]
raise RuntimeError("no gitea credential available")
def _api(path: str) -> tuple[int, object]:
import urllib.request
req = urllib.request.Request(f"{HOST}/api/v1/repos/{REPO}{path}", headers={"Authorization": f"token {_token()}"})
try:
with urllib.request.urlopen(req, timeout=20) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, {"error": e.read().decode()[:200]}
def _jobs_list(run_ref: str):
"""GET jobs for a run, unwrapping the {'jobs': [...]} envelope."""
s, data = _api(f"/actions/runs/{run_ref}/jobs")
if s != 200:
return s, None
if isinstance(data, dict):
data = data.get("jobs", [])
return s, data if isinstance(data, list) else None
def _resolve_run_id(run_ref: str, sha: str) -> str:
"""Task-list ids are NOT run ids. If run_ref lookup 404s, resolve the
real run id by head SHA via /actions/runs (T15: run 6907 vs task 15743)."""
if run_ref:
s, jobs = _jobs_list(run_ref)
if s == 200 and jobs:
return run_ref
if sha:
s, runs = _api(f"/actions/runs?limit=30")
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else (runs if isinstance(runs, list) else [])
# One SHA fans out to multiple runs (lint + deploy-only). Prefer a
# substantive (non-skipped) run; a skipped-only run proves nothing.
fallback = ""
for r in items:
if str(r.get("head_sha", "")).startswith(sha):
rid = str(r.get("id"))
if r.get("conclusion") != "skipped":
return rid
fallback = fallback or rid
return fallback
return ""
def verify_run(run_ref: str, sha: str, expected: str) -> tuple[str, dict]:
"""run_ref = actions run id (or task id — resolved via SHA lookup).
Defect fixed 2026-09-12 (surfaced by W/R-v0.3 live proof, run 7110):
the jobs query here used raw _api + isinstance(jobs, list), but this
Gitea wraps jobs in the {"jobs": [...]} envelope — the check always
failed, so EVERY EXACT_WORKFLOW verdict silently fell through to the
aggregate commit-status fallback, where unrelated runs on the same
SHA (multi-run fan-out: ci.yml + deploy-only) poison the verdict.
Use _jobs_list (unwraps the envelope), same as _resolve_run_id."""
run_ref = _resolve_run_id(run_ref, sha)
if run_ref:
s, jobs = _jobs_list(run_ref)
if s == 200 and isinstance(jobs, list) and jobs:
conclusions = {j.get("name"): j.get("conclusion") for j in jobs}
statuses = {j.get("status") for j in jobs}
if statuses <= {"completed"}:
verdict = "success" if set(conclusions.values()) <= {"success", "skipped"} else "failure"
return verdict, {"run_ref": run_ref, "jobs": conclusions}
return "pending", {"run_ref": run_ref, "statuses": sorted(statuses)}
if sha:
s, st = _api(f"/commits/{sha}/status")
if s == 200 and isinstance(st, dict):
state = st.get("state", "unknown") # success|error|failure|pending|warning
rid = _resolve_run_id("", sha) # attach run id so failure evidence works
if state == "success":
return "success", {"sha": sha, "state": state, "run_ref": rid}
if state in ("error", "failure"):
return "failure", {"sha": sha, "state": state, "run_ref": rid}
return "pending", {"sha": sha, "state": state}
return "unknown", {"run_ref": run_ref, "sha": sha}
def failed_job_evidence(run_ref: str, sha: str = "") -> dict:
"""For failure continuations: exact failing job + conclusion."""
run_ref = _resolve_run_id(run_ref, sha)
if not run_ref:
return {}
s, jobs = _jobs_list(run_ref)
if s != 200 or not isinstance(jobs, list):
return {}
bad = [j for j in jobs if j.get("conclusion") not in ("success", "skipped", None)]
return {
"failed_jobs": [
{"name": j.get("name"), "conclusion": j.get("conclusion"), "url": j.get("url", "")} for j in bad
]
}
# ── RELEASE_DEPLOY verification (W/R v0.1, overnight handoff Part 2) ──────
# Aggregate commit status is NOT a valid completion authority for Rune's
# release/deploy flow: the release successor SHA, not the source SHA, is what
# converges (observed live: wr-1789179026 stranded on pending while 0.66.15
# was deployed). The authority chain is:
# source SHA → source runs terminal → release successor commit (descendant)
# → successor runs success → deployed/current identity converges.
def _git(args: list[str], repo_root: str) -> str:
import subprocess
r = subprocess.run(["git"] + args, capture_output=True, text=True, cwd=repo_root or None, timeout=15)
if r.returncode != 0:
raise RuntimeError(f"git {' '.join(args[:2])}: {r.stderr.strip()[:120]}")
return r.stdout
def _runs_for_sha(sha: str) -> list[dict]:
s, runs = _api("/actions/runs?limit=50")
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else []
return [r for r in items if str(r.get("head_sha", "")).startswith(sha)]
def _sha_verdict_from_runs(runs: list[dict]) -> tuple[str, dict]:
"""success | failure | pending from a SHA's workflow runs.
Terminal verdict ONLY when every run is completed — mixed states
(some success, some still in flight) must stay PENDING (observed live:
a SHA with 1 completed-success + 1 queued + 1 in-progress was judged
success mid-flight; wr-1789182522 2026-09-12)."""
if not runs:
return "pending", {"note": "no runs found"}
non_terminal = [r for r in runs if r.get("status") != "completed"]
if non_terminal:
return "pending", {"in_flight": len(non_terminal)}
conclusions = {r.get("name"): r.get("conclusion") for r in runs}
bad = {n: c for n, c in conclusions.items() if c == "failure"}
if bad:
return "failure", {"failed": bad}
return "success", {"runs": conclusions}
def verify_release_deploy(source_sha: str, repo_root: str = "", deployed_link: str = "") -> tuple[str, dict]:
"""RELEASE_DEPLOY authority chain. Returns (verdict, evidence)."""
source_sha = (source_sha or "")[:12]
ev: dict = {"source_sha": source_sha, "kind": "RELEASE_DEPLOY"}
# 1. Source SHA's own runs must be terminal and healthy.
src_runs = _runs_for_sha(source_sha)
src_verdict, src_ev = _sha_verdict_from_runs(src_runs)
ev["source"] = src_ev
if src_verdict != "success":
return src_verdict, ev
# 2. Release successor: first chore(release) commit descending from source.
if not repo_root:
return "pending", {**ev, "note": "no repo_root — cannot check lineage"}
try:
out = _git(
[
"log",
"--format=%H %s",
"--reverse",
f"{source_sha}..master",
"--grep=^chore\\(release\\)",
"--extended-regexp",
],
repo_root,
)
except RuntimeError as e:
return "pending", {**ev, "note": str(e)}
successors = [l for l in out.splitlines() if l.strip()]
if not successors:
return "pending", {**ev, "note": "no release successor yet"}
succ_line = successors[0]
succ_sha_full, _, succ_subject = succ_line.partition(" ")
succ_sha = succ_sha_full[:12]
ev["successor"] = {"sha": succ_sha, "subject": succ_subject[:80]}
# Ancestry is guaranteed by the range, but verify explicitly (deterministic
# lineage over title guessing):
try:
_git(["merge-base", "--is-ancestor", source_sha, succ_sha_full], repo_root)
except RuntimeError:
return "failure", {**ev, "note": "successor is not a descendant of source"}
# 3. Successor runs must be terminal and healthy.
succ_runs = _runs_for_sha(succ_sha)
succ_verdict, succ_ev = _sha_verdict_from_runs(succ_runs)
ev["successor"]["runs"] = succ_ev
if succ_verdict != "success":
return succ_verdict, ev
# 4. Deployed identity convergence. A mismatch is FAILURE only when the
# successor's runs are ALL completed — if any is still running/queued the
# deploy may simply not have propagated yet → PENDING (observed live:
# reconciler checked mid-deploy, verdict failure, deployment converged
# ~2 min later; wr-1789182522 2026-09-12).
if deployed_link:
link = Path(deployed_link)
if link.is_symlink() or link.is_dir():
deployed = str(link.resolve())
deployed_id = Path(deployed).name
ev["deployed"] = deployed_id
if not deployed_id.startswith(succ_sha[:12]):
still = [r for r in _runs_for_sha(succ_sha) if r.get("status") != "completed"]
if still:
return "pending", {**ev, "note": "deploy propagating (runs in flight)"}
return "failure", {**ev, "note": f"deployed {deployed_id} != successor {succ_sha} (all runs terminal)"}
else:
return "failure", {**ev, "note": f"deployed link missing: {deployed_link}"}
return "success", ev
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""waitstore.py — wait/resume state machine v0.2 (developer tooling, NOT Rune).
States:
RUNNABLE → WAITING_EXTERNAL → READY_TO_RESUME → RESUMING → COMPLETED | FAILED
↑ │
│ ├─(watch timeout, tree killed)
│ └─(rc!=0 / unverifiable)
READY_TO_RETRY ←─────┘
RESUME_UNVERIFIED : delivery after crash/timeout cannot be proven.
TERMINAL-ish — never auto-scheduled; explicit
`requeue()` required to make it READY_TO_RESUME.
TIMED_OUT : external event never arrived within the wait budget.
Waits persist as waits/<wait_id>.json (atomic tmp+rename).
Leases persist as leases/<session_id>.lease.json (atomic tmp+rename,
validated against /proc so a dead or PID-reused owner is stealable).
"""
from __future__ import annotations
import fcntl, json, os, tempfile, time, uuid
from pathlib import Path
ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
WAITS = ROOT / "waits"
EVENTS = ROOT / "events"
LEASES = ROOT / "leases"
SCHEDULABLE = {"READY_TO_RESUME", "READY_TO_RETRY"}
VALID_STATES = {
"RUNNABLE",
"WAITING_EXTERNAL",
"READY_TO_RESUME",
"RESUMING",
"COMPLETED",
"FAILED",
"READY_TO_RETRY",
"RESUME_UNVERIFIED",
"TIMED_OUT",
}
TERMINAL = {"COMPLETED", "FAILED", "RESUME_UNVERIFIED", "TIMED_OUT"}
def _atomic_write(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".tmp-")
try:
with os.fdopen(fd, "w") as f:
json.dump(data, f, indent=1, sort_keys=True)
os.replace(tmp, path)
# T17 17C: the bridge root is SHARED across users (deploy waiter
# runs as the runner user; daemon as nick). mkstemp creates 0600 —
# daemon re-saves locked the runner user out of records it must
# read (waitstore.load in await_resume_event; find_matching_event
# replay scan silently skipped them). 0644: owner writes, all read.
os.chmod(path, 0o644)
except BaseException:
if os.path.exists(tmp):
os.unlink(tmp)
raise
def now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def new_wait(
*,
session_id: str,
provider: str,
event_type: str,
repo: str,
sha: str = "",
run_ref: str = "",
expected: str = "success",
wait_kind: str = "EXACT_WORKFLOW",
target_kind: str = "interactive",
repo_root: str = "",
deployed_link: str = "",
objective: str = "",
waited_for: str = "",
verify_when_woken: str = "",
on_success: str = "",
on_failure: str = "",
resume_cmd: list | None = None,
) -> dict:
wid = f"wr-{int(time.time())}-{uuid.uuid4().hex[:6]}"
rec = {
"wait_id": wid,
"session_id": session_id,
"created_at": now(),
"provider": provider,
"event_type": event_type,
"repo": repo,
"sha": sha,
"run_ref": run_ref,
"expected": expected,
# T15 §15D: PROCESS_EVENT — a wait whose authoritative verdict is
# the matching process-exit event itself (exit_code carried on the
# event), e.g. a builder/controller process completing. The event
# IS the authority here (the notifier wrapper observed the real
# process exit); no Gitea lookup applies.
"wait_kind": wait_kind
if wait_kind in ("EXACT_WORKFLOW", "RELEASE_DEPLOY", "PROCESS_EVENT")
else "EXACT_WORKFLOW",
# CONTENTION POLICY: "interactive" (default, conservative) never
# auto-dispatches — MiMo exposes NO supported busy/idle session
# signal, so injecting into a possibly-busy interactive session is
# NOT YET SAFE. Only explicitly declared headless/disposable
# targets ("headless") are auto-resumed.
"target_kind": target_kind if target_kind in ("headless", "interactive") else "interactive",
"repo_root": repo_root,
"deployed_link": deployed_link,
# T15 §15D: per-wait resume command override (placeholder template,
# same substitution as WR_RESUME_CMD: {session_id}/{msg}/{wait_id}).
# A synthetic controller session (e.g. a Scaffolding stage driver)
# is NOT a MiMo session — the global mimo-shaped default cannot
# resume it, which is exactly why fixed-sleep polling crept back
# in T12-T14. Per-wait override makes ANY registered continuation
# target resumable through the same bridge.
"resume_cmd": list(resume_cmd) if resume_cmd else None,
"status": "WAITING_EXTERNAL",
"last_verified": "",
"resume_attempts": 0,
"continuation": {
"objective": objective,
"waited_for": waited_for,
"verify_when_woken": verify_when_woken,
"on_success": on_success,
"on_failure": on_failure,
},
}
_atomic_write(WAITS / f"{wid}.json", rec)
return rec
def load(wid: str) -> dict | None:
p = WAITS / f"{wid}.json"
return json.loads(p.read_text()) if p.is_file() else None
def save(rec: dict) -> None:
assert rec.get("status") in VALID_STATES, rec.get("status")
rec["last_updated"] = now()
_atomic_write(WAITS / f"{rec['wait_id']}.json", rec)
def all_waits() -> list[dict]:
out = []
for p in WAITS.glob("*.json"):
try:
out.append(json.loads(p.read_text()))
except json.JSONDecodeError:
continue
return out
def pending() -> list[dict]:
return [r for r in all_waits() if r.get("status") in {"WAITING_EXTERNAL"} | SCHEDULABLE]
def schedulable() -> list[dict]:
"""READY waits, oldest first — deterministic queue order."""
rs = [r for r in all_waits() if r.get("status") in SCHEDULABLE]
rs.sort(key=lambda r: (r.get("created_at", ""), r.get("wait_id", "")))
return rs
def requeue(wid: str, note: str) -> dict | None:
"""Operator-explicit requeue of a RESUME_UNVERIFIED/RESUME_FAILED wait."""
rec = load(wid)
if not rec:
return None
rec["status"] = "READY_TO_RESUME"
rec["resume_note"] = note
save(rec)
return rec
# ── events (wake signals, idempotent) ─────────────────────────────────────
def record_event(event: dict) -> str:
import hashlib
body = json.dumps(event, sort_keys=True)
eid = hashlib.sha256(body.encode()).hexdigest()[:16]
f = EVENTS / f"{eid}.json"
if not f.is_file():
_atomic_write(f, {"event_id": eid, "received_at": now(), "event": event})
return eid
def match(rec: dict, event: dict) -> bool:
if rec.get("provider") != event.get("provider"):
return False
if rec.get("event_type") and event.get("event_type") and rec["event_type"] != event["event_type"]:
return False
if rec.get("sha") and event.get("sha") and rec["sha"] != event["sha"]:
return False
if rec.get("run_ref") and event.get("run_ref") and rec["run_ref"] != event["run_ref"]:
return False
return True
# ── leases (per-session single-flight, /proc-validated) ───────────────────
#
# Invariant 3 (PID reuse): liveness is NEVER "PID exists". The lease records
# pid + pgid + process starttime (/proc/<pid>/stat field 22); all three must
# match or the owner is a different process wearing a recycled PID.
#
# Invariant 1 (flock as authority): the lease file's flock serializes
# read-modify-write across processes and dies with its holder, so a crashed
# daemon can never wedge the lock. The lease JSON + /proc decide who OWNS the
# session; the flock only prevents racing acquirers from double-stealing.
_lease_fds: dict[str, int] = {} # session_id → open fd holding the flock
def _pid_starttime(pid: int) -> str | None:
"""Field 22 of /proc/<pid>/stat — distinguishes PID reuse."""
try:
stat = Path(f"/proc/{pid}/stat").read_text()
return stat.rsplit(")", 1)[1].split()[19]
except (OSError, IndexError):
return None
def _owner_alive(lease: dict) -> bool:
pid = lease.get("pid")
if not pid or not Path(f"/proc/{pid}").is_dir():
return False
if _pid_starttime(pid) != lease.get("pid_starttime"):
return False # PID reused by an unrelated process
try:
if os.getpgid(pid) != lease.get("pgid"):
return False
except OSError:
return False
return True
def lease_path(session_id: str) -> Path:
# T16 §16C finding (observed live in daemon.log): session ids like
# "controller-<op>-Verboo Code/glm-5.3-flash" contain "/" — the raw
# embed made lease creation raise FileNotFoundError, so resume
# delivery for those waits could never succeed. Sanitize to a flat
# filesystem-safe name; the hash suffix prevents collisions between
# ids that sanitize identically.
import hashlib as _hashlib
import re as _re
safe = _re.sub(r"[^A-Za-z0-9._-]+", "_", session_id)[:80]
digest = _hashlib.sha256(session_id.encode()).hexdigest()[:10]
return LEASES / f"{safe}-{digest}.lease.json"
def read_lease(session_id: str) -> dict | None:
p = lease_path(session_id)
if not p.is_file():
return None
try:
return json.loads(p.read_text())
except json.JSONDecodeError:
return None
def _locked_lease_fd(session_id: str) -> int | None:
"""Open the lease file and take its flock (non-blocking). The fd stays
open (lock held) until release_lease closes it."""
if session_id in _lease_fds:
return _lease_fds[session_id] # already hold it
LEASES.mkdir(parents=True, exist_ok=True)
fd = os.open(lease_path(session_id), os.O_CREAT | os.O_RDWR, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
os.close(fd)
return None # another process holds the lock — mid-acquire
_lease_fds[session_id] = fd
return fd
def _write_lease_locked(session_id: str, lease: dict) -> None:
fd = _lease_fds[session_id]
os.ftruncate(fd, 0)
os.lseek(fd, 0, os.SEEK_SET)
os.write(fd, json.dumps(lease, indent=1, sort_keys=True).encode())
os.fsync(fd)
def acquire_lease(session_id: str, wait_id: str, pid: int, pgid: int) -> dict | None:
"""Atomically create or steal the session lease. Returns the lease dict,
or None if a live owner holds it (or another process is mid-steal).
Staleness is decided by /proc identity evidence, never by age alone."""
fd = _locked_lease_fd(session_id)
if fd is None:
return None
existing = read_lease(session_id)
if existing and _owner_alive(existing):
_lease_fds.pop(session_id, None)
os.close(fd)
return None # live worker owns this session
lease = {
"session_id": session_id,
"wait_id": wait_id,
"pid": pid,
"pgid": pgid,
"pid_starttime": _pid_starttime(pid),
"acquired_at": now(),
"state": "RESUMING",
}
_write_lease_locked(session_id, lease)
return lease
def update_lease(session_id: str, **fields) -> None:
lease = read_lease(session_id) or {}
lease.update(fields)
_write_lease_locked(session_id, lease)
def release_lease(session_id: str, wait_id: str) -> bool:
"""Release only if the current lease belongs to this wait (no stealing
a successor's lease during a slow shutdown path). Closes the flock fd."""
lease = read_lease(session_id)
if not (lease and lease.get("wait_id") == wait_id):
return False
fd = _lease_fds.pop(session_id, None)
if fd is not None:
try:
fcntl.flock(fd, fcntl.LOCK_UN)
except OSError:
pass
os.close(fd)
lease_path(session_id).unlink(missing_ok=True)
return True
+124
View File
@@ -0,0 +1,124 @@
import { execFile } from "node:child_process"
import { existsSync, appendFileSync } from "node:fs"
// builder-bridge — Rune Core preflight for the MiMo builder (handoff
// 2026-09-10 §3–§7, Option C bridge tranche). For each user turn, the hook
// extracts the user's message from the outgoing LLM request, runs the Rune
// Core builder-bridge preflight (deterministic: route classification +
// memory selection + active-Scaffolding projection, ZERO model calls), and
// appends a compact capsule to the final user message. Same Rune Core the
// TUI/operator path uses — this file is a thin adapter with no cognition.
//
// CANONICAL SOURCE: config/mimo-hooks/builder-bridge.ts
// Installed to ~/.mimocode/hooks/builder-bridge.ts by
// `scripts/install_mimo_hook.py` (hash-verified, drift-detecting — the same
// contract as event-wait-guard.ts). T22: this file was previously
// UNVERSIONED and hardcoded an ephemeral interpreter path (/tmp/ci-venv),
// which silently killed the whole edge; both defects are repaired here.
//
// Overhead contract (§7/§8): capsule ≤ ~300 tokens; a repeat/empty task
// costs ~0ms (TTL cache); a fresh task ~150ms (bounded retrieval). The hook
// degrades silently to no-injection on any failure — it must never block
// the builder.
//
// 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 MARKER_PREFIX = "/tmp/rune-bridge-"
// Interpreter resolution (T22 repair): the previous hardcoded
// "/tmp/ci-venv/bin/python" is an EPHEMERAL path that no longer exists, so
// every bridge call failed and the hook silently degraded — the MiMo edge
// received no Rune Core preflight at all (zero fire-log lines). Resolve
// against durable candidates, first existing wins; env override wins over
// 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",
"/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
}
// Per-process dedupe: identical task text within the cache TTL does not need
// a second bridge call — the CLI has its own TTL cache, but skipping the
// process spawn entirely for the same message text within 10 minutes keeps
// repeats at ~0ms.
const lastText = new Map() // text -> timestamp
const TTL = 600_000
function extractUserText(messages) {
// messages: Array<{ info: Message, parts: Part[] }> — newest last.
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 default {
"experimental.chat.messages.transform": async (input, output) => {
try {
if (!Array.isArray(output.messages) || output.messages.length === 0) return
const userText = extractUserText(output.messages)
if (!userText || userText.length < 12) return // trivial/noise guard
// Harness-internal steps (checkpoint writer, compaction, system
// reminders) are not builder tasks — no capsule (measured: the fire
// log showed checkpoint-writer steps receiving capsules 2026-09-10).
if (/^(<system-reminder>|EXTRACTION REQUIRED)/.test(userText.trim())) return
const now = Date.now()
const prev = lastText.get(userText)
if (prev !== undefined && now - prev < TTL) return // dedupe repeats
lastText.set(userText, now)
if (lastText.size > 64) {
// evict oldest
let oldestKey = null, oldestVal = Infinity
for (const [k, v] of lastText) if (v < oldestVal) { oldestVal = v; oldestKey = k }
if (oldestKey) lastText.delete(oldestKey)
}
const py = resolvePy()
if (!py) return // no durable interpreter — degrade (never block)
const capsule = await new Promise((resolve) => {
execFile(py, [BRIDGE, userText], { timeout: 15_000 }, (err, stdout) => {
resolve(err ? "" : (stdout?.toString() ?? ""))
})
})
if (!capsule.trim()) return
// Durable fire-trace: one line per ACTUAL injection into an outgoing
// model request (handoff §1 proof requirement). Not a canary — this is
// standing observability for the bridge's consumer edge.
try {
appendFileSync("/tmp/rune-bridge-fire.log",
JSON.stringify({ ts: new Date().toISOString(), task: userText.slice(0, 80), capsule_tokens: Math.round(capsule.length / 4), py }) + "\n")
} catch {}
// Append the capsule to the LAST user message (the one being answered).
const last = output.messages[output.messages.length - 1]
if (last?.parts && Array.isArray(last.parts)) {
last.parts.push({
type: "text",
text: "\n\n<rune-preflight>\n" + capsule.trim() + "\n</rune-preflight>\n(Injected by Rune Core builder bridge — deterministic preflight, zero model calls. Relevant state above; ignore if not applicable to this task.)",
})
}
} catch {
// never block the builder on bridge failure
}
},
}
+56
View File
@@ -0,0 +1,56 @@
import type { Hooks } from "@mimo-ai/plugin"
// event-wait-guard — RETIRED (T21.5 §6/§8, 2026-09-17).
//
// HISTORY: this hook was the T21 §21B-3 runtime interception layer that
// blocked model-generated sleep→poll continuation (T20 regression), and
// later carried the T21.1 §21.1C cwd guard. Both were DOWNSTREAM patches:
// they corrected the model's output instead of fixing the generator.
//
// ROOT-CAUSE FIX (T21.5 §2-§5): the generator was ci_event_wait's
// wait loop returning nonterminal control to the model — caused by a
// single hard-coded master-deploy authority that was blind to branch/PR
// operations. T21.5 fixed this upstream:
// - OPERATION_SPECS: per-operation-type completion authority
// (branch_ci/pull_request_checks -> own Actions runs;
// master_ci/deploy/release -> deploy qualification);
// - SINGLE LOGICAL AWAIT: all nonterminal conditions re-arm inside
// ci_event_wait.run(); the model sees only terminal results;
// - structured PendingOperation envelopes (op_type, authority,
// internal_rearm_count) propagate to the controller.
//
// With the generator fixed, runtime interception is retired per the
// tranche rule FIXED UPSTREAM BEHAVIOUR > DOWNSTREAM GUARD CORRECTION.
// Regression prevention lives in the test suite:
// tests/test_single_logical_await.py (no model re-arm contract)
// tests/test_ci_event_wait.py (watchdog=recovery, event=wake)
// tests/test_wait_guard_corpus.py (canonical wait-pattern corpus)
// NOT as a permanent runtime block. The hook file stays installed as a
// documented no-op so canonical-source/install parity (test_hook_drift,
// test_hook_parity) continues to prove the installed runtime matches
// this repository's canonical state.
export const EventWaitGuard: Hooks = {
"tool.execute.before": async (_input, _output) => {
// RETIRED — no interception. See history above.
return
},
}
export const CwdGuard: Hooks = {
"tool.execute.before": async (_input, _output) => {
// RETIRED (T21.5 §8) — the harness/construction execution boundary
// now resolves project context upstream (execution-context binding).
// No runtime refusal. See tests/test_auto_execution_context.py.
return
},
}
export const CombinedGuards: Hooks = {
"tool.execute.before": async (input, output) => {
// Kept as the default export shape; both sub-guards are retired.
return
},
}
export default CombinedGuards
+92
View File
@@ -0,0 +1,92 @@
import { tool } from "@mimo-ai/plugin"
import { execSync } from "child_process"
export default tool({
description: "Wait for an external operation (CI, deploy, release) to complete. Call this when you cannot make progress until an external process finishes. You do NOT need to know the sha, op_type, or duration — Rune's controller resolves those from the bound PendingOperation. Just call this when you want to wait.",
args: {
operation_id: tool.schema.string().optional().describe("Optional: the operation to wait for. If omitted, the controller resolves the current pending operation automatically."),
task_id: tool.schema.string().optional().describe("Optional: the active task id (e.g. T4). When provided, resolution is scoped to operations bound to this task — stale global operations are never consulted."),
sha: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller uses this only to help resolve which operation you mean."),
op_type: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller derives the canonical operation type from repository/Gitea state."),
deadline_secs: tool.schema.number().optional().describe("OPTIONAL HINT — non-authoritative. The controller applies governed policy timing. Real user deadlines must be stated in conversation."),
},
async execute(args) {
// INTENT NORMALIZATION — model output is intent; the controller
// owns execution semantics. All model args are hints recorded for
// observability; canonical truth comes from the bound operation.
const parts = []
if (args.operation_id) parts.push(`--operation-id ${args.operation_id}`)
if (args.task_id) parts.push(`--task-id ${args.task_id}`)
if (args.sha) parts.push(`--sha ${args.sha}`)
if (args.op_type) parts.push(`--model-op-type ${args.op_type}`)
if (args.deadline_secs) parts.push(`--model-deadline ${args.deadline_secs}`)
const normCmd = `python3 /mnt/workdrive/issue161-harness/scripts/await_intent_cli.py normalize ${parts.join(" ")}`
let norm: any
try {
const normOut = execSync(normCmd, {
cwd: "/mnt/workdrive/issue161-harness",
encoding: "utf-8" as const,
timeout: 120000,
env: { ...process.env, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge" },
})
norm = JSON.parse(normOut.trim().split("\n").find((l: string) => l.startsWith("{")) || "{}")
} catch (e: any) {
return JSON.stringify({ ok: false, stage: "normalization", error: (e.stderr || e.message || "?").slice(0, 300) })
}
if (norm.normalized === "AMBIGUOUS") {
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
}
if (!norm.normalized) {
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
}
// canonical continuation: stored/derived op_type + POLICY deadline
// (the sha for the wait comes from the normalization resolution,
// NOT from the model's args)
const waitSha = norm.sha_for_wait || args.sha || ""
if (!waitSha) {
return JSON.stringify({ ok: false, stage: "resolution", error: "no sha resolved for the pending operation", norm })
}
const cmd = `python3 /mnt/workdrive/issue161-harness/scripts/ci_event_wait.py --sha ${waitSha} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
try {
const result = execSync(cmd, {
cwd: "/mnt/workdrive/issue161-harness",
timeout: (norm.deadline_secs + 60) * 1000,
encoding: "utf-8" as const,
env: { ...process.env, DEPLOY_SHA: waitSha, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge", RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume" },
})
const jl = result.trim().split("\n").find((l: string) => l.startsWith("{"))
const envelope = jl ? JSON.parse(jl) : { ok: false, error: "no output" }
return JSON.stringify({
...envelope,
normalization: {
model_deadline_ignored: norm.model_deadline_ignored,
model_op_type_ignored: norm.model_op_type_ignored,
model_sha_ignored: !!args.sha && args.sha !== waitSha,
canonical_op_type: norm.op_type,
canonical_authority: norm.authority,
deadline_provenance: norm.deadline_provenance,
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
},
})
} catch (e: any) {
const lines = (e.stdout || "").trim().split("\n")
const jl = lines.find((l: string) => l.startsWith("{"))
if (jl) {
const envelope = JSON.parse(jl)
return JSON.stringify({
...envelope,
normalization: {
model_deadline_ignored: norm.model_deadline_ignored,
model_op_type_ignored: norm.model_op_type_ignored,
model_sha_ignored: !!args.sha && args.sha !== waitSha,
canonical_op_type: norm.op_type,
canonical_authority: norm.authority,
deadline_provenance: norm.deadline_provenance,
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
},
})
}
return JSON.stringify({ ok: false, stage: "wait", error: (e.stderr || e.message || "?").slice(0, 300) })
}
},
})
+167
View File
@@ -0,0 +1,167 @@
// bash — executes a shell command. PROCESS-OWNERSHIP CONTRACT (v2):
//
// MODE A (tool-owned): the child is launched in its own process group;
// stdout/stderr are CAPTURED pipes (never the TUI tty). On timeout or
// failure the ENTIRE process group is signalled (SIGTERM, grace,
// SIGKILL) so grandchildren cannot survive the invocation, then the
// result is finalized. After finalization NO descendant retains a
// writer into MiMo's streams.
//
// MODE B (explicitly detached): a command suffixed with " &" that the
// user/model intends to outlive the tool (nohup/setsid/disown-style)
// is executed via the detached helper: stdin=/dev/null, stdout/stderr
// -> durable log file, own session (setsid), no MiMo PTY inheritance.
// The tool returns the log path as the durable handle.
//
// Wait-intent compilation (sleep/check/poll aimed at a pending operation)
// is preserved: interception happens at the SEMANTIC level via
// await_intent route-bash, and a NO-OP for anything that is not a genuine
// continuation of a live pending operation. Arbitrary shell syntax
// (quotes, pipes, semicolons) passes through untouched — the router
// response is only trusted when it carries normalized=true AND
// ContinuePendingOperation with a concrete operation bound.
import { tool } from "@mimo-ai/plugin"
import { spawn, execFile } from "child_process"
const REPO = "/mnt/workdrive/issue161-harness"
const WR_ROOT = "/mnt/workdrive/wr-bridge"
// ── MODE A: process-group-owned capture execution ──────────────────────
interface OwnedResult { stdout: string; stderr: string; status: number; timedOut: boolean }
function runOwned(command: string, timeoutMs: number): Promise<OwnedResult> {
return new Promise((resolve) => {
// /bin/sh -c "command" in its OWN process group (detached:true makes
// the child a session/group leader we can signal as a WHOLE).
const child = spawn("/bin/sh", ["-c", command], {
cwd: REPO,
stdio: ["ignore", "pipe", "pipe"], // NEVER inherit the TUI tty
env: process.env,
detached: true, // own process group
})
let stdout = "", stderr = ""
let timedOut = false
let settled = false
const outChunks: Buffer[] = []
const errChunks: Buffer[] = []
child.stdout!.on("data", (d) => outChunks.push(d))
child.stderr!.on("data", (d) => errChunks.push(d))
const killGroup = (sig: NodeJS.Signals) => {
try { process.kill(-child.pid!, sig) } catch { /* already gone */ }
}
const finalize = (status: number) => {
if (settled) return
settled = true
clearTimeout(timer)
// detach listeners, then resolve — no late listeners remain
child.stdout!.removeAllListeners()
child.stderr!.removeAllListeners()
resolve({ stdout: Buffer.concat(outChunks).toString(), stderr: Buffer.concat(errChunks).toString(), status, timedOut })
}
const timer = setTimeout(() => {
timedOut = true
killGroup("SIGTERM")
// bounded grace for clean shutdown, then SIGKILL the group
setTimeout(() => killGroup("SIGKILL"), 2000)
// the exit/close event settles; a hard stop-guard settles anyway
setTimeout(() => finalize(124), 4000)
}, timeoutMs)
child.on("error", (e) => finalize(-1))
child.on("close", (code) => finalize(code ?? -1))
})
}
// ── MODE B: explicit detachment for "cmd &" invocations ────────────────
function runDetached(command: string): Promise<string> {
return new Promise((resolve) => {
const logPath = `/tmp/mimo-detached-${Date.now()}-${Math.floor(Math.random() * 1e6)}.log`
const out = `exec >>"${logPath}" 2>&1; exec 0</dev/null; ${command}`
const child = spawn("/bin/sh", ["-c", `setsid /bin/sh -c '${out.replace(/'/g, `'\\''`)}'`], {
cwd: REPO,
stdio: "ignore", // child rebinds its own fds — no PTY inheritance
env: process.env,
detached: true,
// unref: MiMo's event loop does not wait for it
})
child.unref()
resolve(JSON.stringify({
detached: true,
log: logPath,
note: "process detached (setsid, own fds); output streams to the log file only; fd 0/1/2 do NOT reference the MiMo tty",
}))
})
}
// ── wait-intent routing (semantic level, preserved from v1) ────────────
async function tryCompileWaitIntent(command: string): Promise<string | null> {
let routeOut: string
try {
routeOut = await new Promise<string>((resolve, reject) => {
execFile("python3", [`${REPO}/scripts/await_intent_cli.py`, "route-bash", "--command", command], {
cwd: REPO, timeout: 60000, encoding: "utf-8",
env: { ...process.env, RUNE_WR_ROOT: WR_ROOT },
maxBuffer: 8 * 1024 * 1024,
}, (err, stdout) => { if (err && !stdout) reject(err); else resolve(stdout || "") })
})
} catch {
return null // router unavailable: normal execution (never block on routing)
}
const jl = routeOut.trim().split("\n").find((l) => l.startsWith("{"))
if (!jl) return null
let norm: any
try { norm = JSON.parse(jl) } catch { return null }
// STRICT gate: compile ONLY a normalized, concrete continuation.
if (!(norm.normalized === true &&
norm.action === "ContinuePendingOperation" &&
norm.operation_id && norm.sha_for_wait)) return null
const waitCmd = `python3 ${REPO}/scripts/ci_event_wait.py --sha ${norm.sha_for_wait} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
const r = await runOwned(waitCmd, (norm.deadline_secs + 60) * 1000)
const jline = r.stdout.trim().split("\n").find((l) => l.startsWith("{"))
if (jline) {
return JSON.stringify({
...JSON.parse(jline),
intent_compilation: {
raw_command_kind: norm.raw_command_kind || "sleep_poll",
literal_sleep_executed: false,
operation_id: norm.operation_id,
canonical_op_type: norm.op_type,
authority: norm.authority,
},
})
}
return JSON.stringify({ ok: false, stage: "compiled-wait", error: (r.stderr || "no output").slice(0, 300) })
}
export default tool({
description: "Run a shell command. If the command expresses external wait intent (sleep/check/poll aimed at CI, deploy, or release operations with pending operations), it is automatically compiled to Rune's event-driven continuation — you do not need to manage wait mechanics. Plain local commands execute normally. Commands ending in ' &' run detached with output logged to a file.",
args: {
command: tool.schema.string().describe("Shell command to run"),
},
async execute(args) {
// 1. wait-intent compilation (semantic, strict-gated)
try {
const compiled = await tryCompileWaitIntent(args.command)
if (compiled !== null) return compiled
} catch { /* fall through to normal execution */ }
// 2. MODE B: explicit detachment for backgrounded invocations
const cmd = args.command
if (/\b(nohup|setsid)\b/.test(cmd) || /&\s*$/.test(cmd)) {
return await runDetached(cmd)
}
// 3. MODE A: tool-owned, group-killed, captured execution
const r = await runOwned(cmd, 120000)
const text = r.stdout + (r.stderr ? `\n[stderr]\n${r.stderr}` : "")
if (r.timedOut) {
return `exit timed-out (process group terminated): ${text.slice(0, 500)}`
}
if (r.status !== 0) {
return `exit ${r.status}: ${text.slice(0, 500) || "?"}`
}
return text || "(no output)"
},
})
+369 -220
View File
@@ -1,223 +1,372 @@
{
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
"umbrellas": {
"memory": {
"description": "Storing, retrieving, and managing knowledge",
"modules": [
"memory_store",
"memory_consolidation",
"memory_env",
"shard_manager",
"query_memory",
"graph_memory",
"warm_neuron_store",
"abstraction",
"cache",
"recall_router",
"drift_cache",
"drift_compression",
"tier_manager",
"graph_shard_bridge",
"memory_selector",
"retrieval_benchmark"
],
"canaries": [
"canary_query_memory",
"canary_memory_synthesis",
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"governance": {
"description": "Quality control, drift detection, promotion, audit",
"modules": [
"drift_core",
"drift_observability",
"drift_ledger",
"audit_ledger",
"curator",
"claim_audit",
"contracts",
"build_pipeline_policy",
"failure_auto_filer"
],
"canaries": [
"canary_full_system",
"canary_schema_migration"
],
"depends_on": []
},
"identity": {
"description": "Who the agent is \u2014 persona, voice, anchors",
"modules": [
"persona_definition",
"persona_gate",
"identity_anchors",
"voice_hybrid",
"voice_sync",
"voicebox",
"voice_injector",
"behavior",
"prompt_sync"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"growth": {
"description": "Learning and improvement",
"modules": [
"self_improve",
"subconscious_pass",
"goal",
"elastic_cognition",
"growth_delta",
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"growth_simulator"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"memory",
"governance"
]
},
"perception": {
"description": "Input processing and context assembly",
"modules": [
"context_broker",
"context_pressure",
"attention_capsule",
"auto_context",
"auto_tasks",
"witness",
"salience_gate",
"cognitive_budgeter",
"complexity_scorer"
],
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
]
},
"capability": {
"description": "Tool selection and execution authority",
"modules": [
"capability_broker",
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"forge_client",
"message_queue",
"connection_pool",
"adaptive_router",
"route_risk_flags",
"evidence_probe",
"builder_bridge",
"builder_bridge_cli",
"recall_uplift"
],
"canaries": [
"canary_p1_01",
"canary_p0_05",
"canary_native"
],
"depends_on": [
"memory"
]
},
"inference": {
"description": "Model routing and outbound requests",
"modules": [
"model_routing",
"model_profiles",
"model_registry",
"model_connect",
"provider_registry",
"provider_budget",
"standardized_providers",
"inference_trace",
"response_adapter",
"token_optimizer"
],
"canaries": [
"canary_p1_06",
"canary_p1_02",
"canary_resource_authority"
],
"depends_on": [
"capability"
]
},
"orchestration": {
"description": "Task lifecycle, operator, TUI presentation",
"modules": [
"rune_operator",
"rune_tui",
"rune_graphic",
"rune_spinner",
"rune_profiler",
"chat_bubble",
"message_widgets",
"tui_validator",
"microkernel",
"event_bus",
"event_envelope",
"hook_engine",
"system_registry",
"cron_engine",
"task_gate",
"task_persist",
"work_manager",
"parallel_processor",
"tiered_executor",
"time_awareness",
"night_mode",
"pins",
"progressive_escalation",
"safety_override",
"sleep_time",
"verification_layer",
"outcome_ledger",
"warm_router",
"subsystem_health",
"build_mode",
"dependency_tracker",
"auto_detect",
"build_orchestrator",
"custodian",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"umbrella_update"
],
"canaries": [
"canary_p1_07",
"canary_orchestration_foundation",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_tui_drift",
"canary_cognitive_strata",
"canary_surface_parity"
],
"depends_on": [
"perception",
"capability",
"inference",
"identity",
"growth"
]
}
"_comment": "GENERATED by rune.spine — compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-10-07T16:54:42Z",
"umbrellas": {
"capability": {
"canaries": [
"canary_native",
"canary_p0_05",
"canary_p1_01",
"canary_rune_entry",
"canary_rune_serve"
],
"configs": [],
"depends_on": [
"memory"
],
"description": "Tool selection and execution authority",
"go_surfaces": [
"rune-entry",
"model-connect",
"provider-registry",
"provider-budget",
"standardized-providers",
"tiered-executor"
],
"mirrors": [],
"modules": [
"adaptive_router",
"builder_bridge",
"builder_bridge_cli",
"capability_broker",
"connection_pool",
"core_server",
"cs_loader",
"evidence_probe",
"forge",
"forge_client",
"intent_continuity",
"local_tool_runtime",
"message_queue",
"native_units",
"openai_compat_transport",
"provider_context",
"recall_uplift",
"route_risk_flags",
"tool_executor",
"tool_protocol"
],
"rust_crates": [
"capability_scoring"
]
},
"governance": {
"canaries": [
"canary_full_system",
"canary_schema_migration",
"canary_audit_ledger",
"canary_spine_meta"
],
"configs": [
".coveragerc",
"go-version.txt",
"lint-baseline.json",
"pyright-baseline.json"
],
"depends_on": [],
"description": "Quality control, drift detection, promotion, audit",
"mirrors": [],
"modules": [
"audit_ledger",
"build_pipeline_policy",
"cc_loader",
"claim_audit",
"contracts",
"curator",
"drift_core",
"drift_ledger",
"drift_observability",
"failure_auto_filer",
"grading",
"programme_gates",
"sc_loader",
"worthiness"
],
"rust_crates": [
"evidence_provenance",
"scaffold_core"
]
},
"growth": {
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance",
"memory"
],
"description": "Learning and improvement",
"mirrors": [],
"modules": [
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"elastic_cognition",
"goal",
"growth_delta",
"growth_simulator",
"self_improve",
"subconscious_pass"
]
},
"identity": {
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
],
"description": "Who the agent is — persona, voice, anchors",
"go_surfaces": [
"persona-definition",
"voicebox",
"voice-injector"
],
"mirrors": [],
"modules": [
"behavior",
"identity_anchors",
"persona_definition",
"persona_gate",
"prompt_sync",
"voice_hybrid",
"voice_injector",
"voice_sync",
"voicebox"
]
},
"inference": {
"canaries": [
"canary_p1_02",
"canary_p1_06",
"canary_resource_authority"
],
"depends_on": [
"capability"
],
"description": "Model routing and outbound requests",
"mirrors": [],
"modules": [
"inference_trace",
"model_connect",
"model_profiles",
"model_registry",
"model_routing",
"provider_budget",
"provider_registry",
"response_adapter",
"standardized_providers",
"tc_loader",
"token_optimizer"
],
"rust_crates": [
"token_counting"
]
},
"memory": {
"canaries": [
"canary_full_system",
"canary_memory_synthesis",
"canary_query_memory"
],
"depends_on": [
"governance"
],
"description": "Storing, retrieving, and managing knowledge",
"go_surfaces": [
"memory-store-ops",
"curator",
"sleep-time",
"consolidation-ops",
"persona-gate",
"witness-ops",
"stage-guards"
],
"mirrors": [],
"modules": [
"abstraction",
"cache",
"drift-ledger",
"drift_cache",
"drift_compression",
"ffi_query_memory_smoke",
"graph_memory",
"graph_shard_bridge",
"lexicon_evolution",
"memory_consolidation",
"memory_env",
"memory_selector",
"memory_store",
"mems_loader",
"mr_loader",
"ms_loader",
"query_memory",
"recall_router",
"retrieval_benchmark",
"shard_manager",
"tier_manager",
"warm_neuron_store"
],
"rust_crates": [
"memory_retrieval",
"memory_scoring",
"memory_storage",
"query_memory"
]
},
"orchestration": {
"canaries": [
"canary_cognitive_strata",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_orchestration_foundation",
"canary_p1_07",
"canary_surface_parity",
"canary_tui_drift",
"canary_tui_transport"
],
"depends_on": [
"capability",
"growth",
"identity",
"inference",
"perception"
],
"description": "Task lifecycle, operator, TUI presentation",
"go_surfaces": [
"event-bus",
"message-queue",
"hook-engine",
"microkernel",
"subsystem-health",
"parallel-processor",
"work-manager",
"failure-filer",
"cron-engine",
"night-mode",
"recall-router",
"local-tool-runtime",
"tool-executor",
"operator",
"adaptive-router",
"build-mode",
"builder-bridge",
"core-server",
"auto-context",
"auto-tasks",
"event-wait",
"evidence-probe",
"forge",
"forge-client",
"goal",
"growth-delta",
"warm-router",
"release-notes"
],
"mirrors": [],
"modules": [
"auto_detect",
"build_mode",
"build_orchestrator",
"chat_bubble",
"cron_engine",
"custodian",
"dependency_tracker",
"event_bus",
"event_envelope",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"hook_engine",
"message_widgets",
"microkernel",
"night_mode",
"outcome_ledger",
"parallel_processor",
"pins",
"progressive_escalation",
"rune_graphic",
"rune_operator",
"rune_profiler",
"rune_spinner",
"rune_tui",
"safety_override",
"sleep_time",
"subsystem_health",
"system_registry",
"task_gate",
"task_persist",
"tiered_executor",
"time_awareness",
"tui_transport",
"tui_validator",
"umbrella_update",
"verification_layer",
"warm_router",
"work_manager"
]
},
"perception": {
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
],
"description": "Input processing and context assembly",
"mirrors": [],
"modules": [
"attention_capsule",
"auto_context",
"auto_tasks",
"behaviour_ledger",
"build_basis",
"capability_fit",
"capability_ledger",
"cognitive_budgeter",
"construction_activation",
"construction_commands",
"construction_decision",
"construction_depth",
"construction_intent",
"construction_interface",
"construction_mode",
"construction_pattern",
"construction_patterns_builtin",
"construction_projection",
"context_broker",
"context_pressure",
"drift_resume",
"event_wait",
"failure_recovery",
"graph_projection",
"impact_preview",
"iteration",
"layout_contract",
"programme_continuation",
"project_registry",
"release_notes",
"salience_gate",
"scaffold",
"spine",
"spine_language",
"spine_loader",
"stage_guards",
"structural_migration",
"structural_model",
"structural_store",
"testing_mode",
"toolchain",
"tui_backlog",
"wait_guard",
"wait_guard_corpus",
"witness"
],
"rust_crates": [
"rune_spine"
]
}
}
}
+225
View File
@@ -0,0 +1,225 @@
{
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
"umbrellas": {
"memory": {
"description": "Storing, retrieving, and managing knowledge",
"modules": [
"memory_store",
"memory_consolidation",
"memory_env",
"shard_manager",
"query_memory",
"graph_memory",
"warm_neuron_store",
"abstraction",
"cache",
"recall_router",
"drift_cache",
"drift_compression",
"tier_manager",
"graph_shard_bridge",
"memory_selector",
"retrieval_benchmark"
],
"canaries": [
"canary_query_memory",
"canary_memory_synthesis",
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"governance": {
"description": "Quality control, drift detection, promotion, audit",
"modules": [
"drift_core",
"drift_observability",
"drift_ledger",
"audit_ledger",
"curator",
"claim_audit",
"contracts",
"build_pipeline_policy",
"failure_auto_filer"
],
"canaries": [
"canary_full_system",
"canary_schema_migration"
],
"depends_on": []
},
"identity": {
"description": "Who the agent is \u2014 persona, voice, anchors",
"modules": [
"persona_definition",
"persona_gate",
"identity_anchors",
"voice_hybrid",
"voice_sync",
"voicebox",
"voice_injector",
"behavior",
"prompt_sync"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"growth": {
"description": "Learning and improvement",
"modules": [
"self_improve",
"subconscious_pass",
"goal",
"elastic_cognition",
"growth_delta",
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"growth_simulator"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"memory",
"governance"
]
},
"perception": {
"description": "Input processing and context assembly",
"modules": [
"context_broker",
"context_pressure",
"attention_capsule",
"auto_context",
"auto_tasks",
"witness",
"salience_gate",
"cognitive_budgeter",
"complexity_scorer"
],
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
]
},
"capability": {
"description": "Tool selection and execution authority",
"modules": [
"capability_broker",
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"intent_continuity",
"openai_compat_transport",
"forge_client",
"message_queue",
"connection_pool",
"adaptive_router",
"route_risk_flags",
"evidence_probe",
"builder_bridge",
"builder_bridge_cli",
"recall_uplift"
],
"canaries": [
"canary_p1_01",
"canary_p0_05",
"canary_native"
],
"depends_on": [
"memory"
]
},
"inference": {
"description": "Model routing and outbound requests",
"modules": [
"model_routing",
"model_profiles",
"model_registry",
"model_connect",
"provider_registry",
"provider_budget",
"standardized_providers",
"inference_trace",
"response_adapter",
"token_optimizer"
],
"canaries": [
"canary_p1_06",
"canary_p1_02",
"canary_resource_authority"
],
"depends_on": [
"capability"
]
},
"orchestration": {
"description": "Task lifecycle, operator, TUI presentation",
"modules": [
"rune_operator",
"rune_tui",
"rune_graphic",
"rune_spinner",
"rune_profiler",
"chat_bubble",
"message_widgets",
"tui_validator",
"microkernel",
"event_bus",
"event_envelope",
"hook_engine",
"system_registry",
"cron_engine",
"task_gate",
"task_persist",
"work_manager",
"parallel_processor",
"tiered_executor",
"time_awareness",
"night_mode",
"pins",
"progressive_escalation",
"safety_override",
"sleep_time",
"verification_layer",
"outcome_ledger",
"warm_router",
"subsystem_health",
"build_mode",
"dependency_tracker",
"auto_detect",
"build_orchestrator",
"custodian",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"umbrella_update"
],
"canaries": [
"canary_p1_07",
"canary_orchestration_foundation",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_tui_drift",
"canary_cognitive_strata",
"canary_surface_parity"
],
"depends_on": [
"perception",
"capability",
"inference",
"identity",
"growth"
]
}
}
}
+9 -12
View File
@@ -104,15 +104,15 @@
},
{
"path": "src/rune/attention_capsule.py",
"role": "AttentionCapsule — the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
"role": "AttentionCapsule \u2014 the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
"disposition": "retain-authority",
"evidence": "P1-02 (#25): the capsule IS the product the model receives — the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
"evidence": "P1-02 (#25): the capsule IS the product the model receives \u2014 the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
},
{
"path": "src/rune/inference_trace.py",
"role": "Durable per-run inference trace (tokens served/estimated, output, latency, per-block visibility)",
"disposition": "migrate-adapter",
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served — builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served \u2014 builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
}
]
},
@@ -160,13 +160,13 @@
"path": "src/rune/capability_broker.py",
"role": "Authoritative capability selector (select_for_job + verified floor + find_capability escape hatch)",
"disposition": "retain-authority",
"evidence": "P1-01 (#24): select_for_job is the single selection authority — its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
"evidence": "P1-01 (#24): select_for_job is the single selection authority \u2014 its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
},
{
"path": "memory/index/capability_cards.json",
"role": "Capability-card index (rebuilt on empty/stale/corrupt; native cards synthesized from TOOLS metadata)",
"disposition": "migrate-adapter",
"evidence": "P1-01 (#24): load_cards validates and rebuilds — the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
"evidence": "P1-01 (#24): load_cards validates and rebuilds \u2014 the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
},
{
"path": "src/rune/local_tool_runtime.py",
@@ -486,12 +486,6 @@
"disposition": "retain-authority",
"evidence": "Defines the owner-approved canary-first Gitea sequence."
},
{
"path": "src/rune/plugins/build_mode/__init__.py",
"role": "Checkpoint/ledger state-machine primitive used by current governed builds",
"disposition": "migrate-adapter",
"evidence": "Persists checkpoints but encodes an older phase order."
},
{
"path": "src/rune/build_mode.py",
"role": "Separate BuildManager and worktree engine",
@@ -504,6 +498,9 @@
"disposition": "archive-candidate",
"evidence": "Reports fixed success strings for phases without executing their claimed effects."
}
],
"notes": [
"T17 17U: plugin BuildStateMachine deleted (zero importers; second parallel orchestrator)"
]
},
{
@@ -540,4 +537,4 @@
]
}
]
}
}
+28
View File
@@ -111,3 +111,31 @@ subsystem): fetches live issue state, compares to
- record lacking a required field
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
## PL1 held-out era reconciliation (2026-09-12)
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
backlog was never in the ADAPT-LIVE tranche scope).
| # | state | evidence / basis |
|---|---|---|
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
(72 records, 100 live issues in window) after these updates.
+53
View File
@@ -0,0 +1,53 @@
# Rune Semantic Release Policy (T20 §20B)
Pre-1.0 convention. Scaffolding "V1" does NOT mean Rune 1.0.
## Bump kinds
**PATCH** (0.66.80 → 0.66.81):
- bug fix, correction, implementation/internal maintenance
- no meaningful new user/system capability
**MINOR** (0.66.80 → 0.67.0):
- meaningful new Rune capability
- new subsystem functionality
- materially expanded system behaviour
- significant new supported workflow
**MAJOR**: reserved for Rune 1.0+ compatibility/public-contract semantics.
## How the machinery expresses this (no manual version edits)
The existing machinery already maps commit shape → bump kind:
- `semver_bump.next_version` (PR titles): feat→minor, else patch
- `version_convergence.highest_warranted_bump` (push ranges):
feat/breaking→minor, fix/perf→patch, else None
**Tranche-level rule (20B)**: a feature tranche lands its feature
commits (`feat(...)`), and the convergence machinery bumps MINOR at
the tranche's release boundary. Patch-only follow-ups (fix/perf)
warrant only PATCH — the machinery cannot accidentally re-minor
because `highest_warranted_bump` takes the max over the *post-
boundary* range, and a fix-only range yields "patch".
## Why T20 → 0.67.0
T20 is a feature-level tranche (Rune-wide retrofit: structural
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
Spine modernization, semantic release). The release is classified
MINOR under this policy, earned through the governed convergence
machinery — never a manual version edit.
## T20 0.67.0 release record
T20 classified MINOR under this policy: Rune-wide retrofit (structural
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
Spine modernization, semantic release) = new subsystem functionality +
materially expanded system behaviour. Earned through the governed
convergence machinery (feat-bearing range → minor), never a manual edit.
## Root cause record (T20, 2026-09-16)
semver_bump re-derived the bump kind from the SYNTHESIZED title text —
"minor: converged release" matched nothing → patch. Feature tranches
T18/T19/T20 downgraded to 0.66.77-83. Fixed: MINOR_PREFIX_RE carries the
convergence kind through; first push after the fix with a feat-bearing
range lands 0.67.0 through the normal machinery.
+11 -1
View File
@@ -54,7 +54,7 @@ This detour is complete when:
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
@@ -177,3 +177,13 @@ Root causes (live-proven T15 Part F, 2026-09-11 evening):
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
drained, zero delayed duplicates, zero workflow failures.
## Local CI tooling (developer-side, NOT Rune runtime): W/R-v0.3 reactive wake (2026-09-12)
The desktop gitea-runner (v3.3.0) now runs `host.post_task_script` = `/usr/local/bin/gitea-wr-wake.sh`
after every job: a sub-second fire-and-forget POST to the local W/R daemon (`127.0.0.1:8791/wake`),
which sweeps waiting external waits through AUTHORITATIVE Gitea verification (bounded retry for the
reporter-ack window; 60s reconciler demoted to fallback). Runner event ≠ success proof. Config:
`/etc/gitea-desktop-runner/config.yaml` (backup: config.yaml.pre-wr03). Measured: terminal→verified
wait transition ~2s (was 0-30s polling). INTERACTIVE_AUTO_RESUME remains NOT YET SAFE (no supported
MiMo busy/idle signal) — headless-only dispatch, unchanged.
+290
View File
@@ -0,0 +1,290 @@
# Scaffolding V1 — Tranche 1
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
built every time Rune changes._
## Tranche 1 contract
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
never a new one:
| question | authority projected |
|---|---|
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
| who consumes X? | reverse import edges |
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
`all [outfile]`. If the projection disagrees with an authority, fix the
authority — never the projection.
## Mechanism audit (2026-09-12, deployed 0.66.22)
| mechanism | verdict | basis |
|---|---|---|
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
## Real-change proof (this tranche)
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
for a 131072-window model (observed live in every PL1 Task A/B run).
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
derived — subsystem `capability`; consumers `rune_operator`; gates:
full suite, lint ratchet, henxels, inference+orchestration canaries;
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
authority in `openai_compat_transport.py`; consumed by context discovery
(new) and transport selection (deduped); red-first regression tests.
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
· the projection itself caught two gaps mid-flight (relative imports
hidden consumers; a broad except biting the lint ratchet) — both fixed at
the source the projection pointed to.
4. Deploy: normal release chain → deployed identity verified.
## Maturity (honest)
- Projection engine: **UNIT-PROVEN** + proven on one real change.
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
capability discovery / gap recognition / reuse-before-build / external
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
does not block them; the projection source-list is the extension surface).
- Deferred with reasons recorded: none blocked by Tranche 1.
---
# Scaffolding V1 — Tranche 2: Construction Substrate (2026-09-13)
_Tranche 2 (contract SCAFFOLD-TRANCHE-001) built the construction substrate
itself: a shared structural-store primitive, the Project Scaffold layer over
it, and durable Construction Contracts — Rune's first build tracked by the
system it builds._
## Architecture
src/rune/structural_store.py shared generic primitive — NO Rune constants
↓ ↓
src/rune/spine.py src/rune/scaffold.py
(Rune Spine — canonical (Project Scaffold — canonical
structural self-model) construction/structural truth
about EXTERNAL projects)
Authority map (no duplicates):
| fact | authority | others |
|---|---|---|
| Rune's structure | `spine.py` + `store/spine/current.json` | scaffold only references |
| external project structure | `scaffold.py` store | Spine holds bridge facts only |
| content history / rollback | Git | both reference commits |
| coordination (issues/PRs/CI) | Gitea | scaffold stores observations |
| why work exists | Task/Intent | contracts reference task ids |
Construction Contract = ONE durable object (`contract:<id>`, kind
`construction_contract`) with plain `phase`/`status` fields governed by a
small legal-transition map (draft → approved → building → verifying →
migrating → superseding → closed; failed/abandoned terminal-or-retry). No
workflow engine. Resumability = store durability, proven across real
process boundaries.
## Mechanics
- **Repo binding** (`bind_repo`): forge/owner/name/branch/root/placement as
scaffold DATA. No secrets — schema rejects secret-named fields AND every
`save()` recursively redacts credential-named keys (defense in depth in
the shared core, so Spine inherits it too).
- **Scope check** (`check_scope`): changed files vs approved scope paths →
green / red / **unknown** (an unscoped contract is never silently green).
Out-of-scope change = RED until explicitly amended; amendments recorded,
original RED preserved in history.
- **Predicted vs observed**: frontier snapshot at approval; observed impact
captured at closure from INDEPENDENT evidence (git, tests) — never from
re-walking the prediction's own graph. Missed entities become CANDIDATE
corrections awaiting evidence.
- **Drift**: authority-owned facts that fail re-observation go STALE —
never silently fresh, never force-reverted. Reality wins.
- **Bridge** (`spine_bridge_entity`): identity + repo surface only. Project
internals NEVER leak into Rune Spine (test-enforced). Contract evidence
may cite `spine://` URIs — citations are inert on the referenced
authority.
## Real proof (this tranche on itself)
SCAFFOLD-TRANCHE-001 migrated from its pre-scaffold JSON into the new
store, advanced through phases with evidence checkpoints, closed with
predicted-vs-observed from git/test evidence, scope check caught the
store's own runtime artifacts (RED → explicit amendment → green, history
kept), and construction provenance attached into Rune Spine without
duplicating authority. A fresh interpreter answers what/why/state/
evidence/changed/next from the store alone — no transcripts.
## Maturity (honest)
| component | state |
|---|---|
| structural_store core | EXERCISED (unit-proven + real use) |
| Rune Spine after refactor | LIVE_PROVEN behavior-neutral (68 tests + identical rebuild) |
| Project Scaffold | EXERCISED |
| Construction Contract | EXERCISED (resumability proven cross-process) |
| repo binding (identity model) | UNIT-PROVEN (local git fixture) |
| external-repo live proof | BLOCKED ON USER REPO SELECTION |
| Scaffolding overall | substrate only — capability-fit discovery, gap decisions, toolchain selection, patterns, maintenance TUI all NOT BUILT (data seams exist) |
---
# Scaffolding Tranche 4 — Live Need → Decision → Construction Contract (2026-09-13)
_The adoption tranche: a real Need now flows automatically through the
Tranche 2/3 mechanisms and produces a governed Construction Contract. The
model no longer has to remember the procedure — it is durable Scaffolding._
## Live flow
NEED (reference to Task/Intent — pointer, never a copy)
↓ normalization (gaps recorded, never invented)
↓ structural discovery + fit + resolution (Spine + Project Scaffold)
↓ toolchain decision (existing project toolchain wins)
↓ layout obligations (USER-LOCKED authority inherited)
↓ user-approval gate (material changes require approval; approved
bounded work never re-asks)
↓ DecisionTrace (durable, model-independent)
↓ generated Construction Contract → existing build/verify pipeline
## Authority map (unchanged — the path CONNECTS, never duplicates)
| authority | owns |
|---|---|
| Task/Intent | why work exists |
| Rune Spine | Rune structural truth |
| Project Scaffold | external-project structural/construction truth |
| Layout Contract | desired interface constraints (USER-LOCKED = user authority) |
| Construction Contract | governed build obligations/history |
| Git / Gitea | history / execution |
## Boundary honesty
- **ADAPT/research**: the live path records a research ROUTE
(TARGETED_EVIDENCE_PROBE / FULL_RESEARCH_PIPELINE) when evidence is
insufficient — a seam, NOT an executed pipeline call. No fake
integration claimed.
- **BUILD_NEW** is never a fallback: insufficient evidence blocks it;
normalization gaps block it; merge precedence across authorities
requires every consulted authority to agree.
- **No second task engine**: one decision function + one trace object.
## Contract-quality repair (Tranche 3 defects)
Closure now distinguishes:
predicted_existing_affected planned changes to existing entities
expected_new entities the build PLANNED to create
correctly_predicted predicted AND independently observed
predicted_but_unaffected predicted, NOT observed
expected_new_observed planned creation happened
expected_new_missing planned creation did NOT happen
genuinely_unexpected_new observed, planned by neither list
Prediction cannot self-certify observation: the observed set comes ONLY
from caller evidence (git/tests/build/deploy). Legacy keys (missed/
needless/observed) preserved — old contracts readable without migration.
## Layout-obligation inheritance
A change touching a UI component/region/implementation inherits its Layout
Contract as a build obligation. USER-LOCKED obligations state that changing
placement requires explicit user authorization. Unrelated backend changes
inherit nothing. USER-LOCKED drift is never reconciled by editing the
contract — code adapts.
## Maturity (honest)
- Need intake: EXERCISED (real live proof)
- decision orchestration: LIVE-PROVEN for the bounded live flow
- capability discovery adoption: EXERCISED
- fit/gap adoption: EXERCISED
- toolchain adoption: EXERCISED
- contract generation: LIVE-PROVEN (one real generated contract)
- contract-quality prediction: EXERCISED (repaired semantics, one live closure)
- layout-obligation inheritance: UNIT-PROVEN (fixtures) + real TUI binding
- Scaffolding overall: adoption path live for one bounded flow — NOT
general autonomous construction, NOT universal discovery/toolchain
intelligence, NOT Scaffolding complete.
---
# Scaffolding Tranche 9 — Release History + Evidence Correction (2026-09-13)
_Gitea Releases are now an evidence-derived human-readable history of
Rune's evolution. Plus the Tranche 8 builder-evidence correction._
## Release semantics (audited)
| surface | meaning |
|---|---|
| VERSION | semver in pyproject/init (e.g. 0.66.44) |
| TAG | v<VERSION>, created by auto-version at the BUMP commit |
| release commit | the auto-bump SHA — NOT necessarily final state |
| converged deployed identity | last commit deployed under the same VERSION |
Same-version convergence commits (closure/accounting/lock fixes landing and
deploying after the bump) are normal; the notes record BOTH the tag commit
and the final deployed identity, distinction exposed honestly.
## Mechanism
`release_notes.py`: deterministic evidence extraction from governed records
(closed contracts, resolved maintenance, contract-quality records) →
structured ReleaseNote → Markdown → Gitea API (POST create / PATCH update).
Idempotent: publishing twice for a tag UPDATES, never duplicates. Notes are
historical documentation — NOT Memory, NOT structural authority.
## T8 evidence correction (§1)
The T8 report claimed a close-from-verifying refusal + reactive superseding
transition. Durable checkpoints show NO superseding transition and the
phase map permits verifying→closed; live output showed first-attempt close
success. The claim was RETRACTED with full provenance (original claim,
correction, reason, evidence preserved in the contract record) and
recorded as process-defect:unsupported-causal-claim. MiMo adoption
maturity survives on the SUPPORTED evidence: scope constrained file edits,
pattern obligations forced preservation proof, interface-reported
observations, fresh resume.
## Maturity
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
history: skipped (weak evidence — honest limitation).
## Contract discovery → spine (2026-10-05, #2465 CI correction)
The closed construction contract is the authority for discoveries. Spine
rebuild reads its observed entities and candidate corrections; it does not
claim a correction has been built. Qualified IDs such as
`rust-crate:cognition_core` keep their declared kind. Bare file paths,
test suites and other unqualified references become
`construction-surface:<reference>`, never `module`. An observed
surface may be PRESENT; a candidate stays DECLARED until independently
observed. Existing bad records are migrated with their evidence and edge
references intact. This matters: the first write-back mapped bare paths to
modules, made 22 false module records and crashed CI on the next rebuild.
The per-contract quick-reference markdown is generated from the scaffold
store at close; it is a view, not another authority.
+79
View File
@@ -0,0 +1,79 @@
# SCAFFOLDING IMPLEMENTATION AUDIT + V1 GAP MATRIX (T18 §18B)
_Every claim classified against evidence. Scale: LIVE_PROVEN (production
evidence) / TEST_PROVEN (suite green, no production exercise) /
IMPLEMENTED_UNPROVEN / PARTIAL / PLANNED / ABSENT / LEGACY / DUPLICATE._
## Kernel claims
| Claim | Status | Evidence |
|---|---|---|
| Contract lifecycle + legal transitions | LIVE_PROVEN | 28 contracts driven through real flow; refusal envelopes observed |
| classify_mode (one classifier) | LIVE_PROVEN | gates TUI /build (17G), operator hook, tests 62+ |
| Conversational intent layer | TEST_PROVEN | 22+ held-outs incl. unseen paraphrases; 17R live exercise (one real conversational proof) |
| Programme continuation | LIVE_PROVEN | 17E 4-stage proof on real bridge; restart/dup/failure held-outs |
| Event-driven waits (builder) | LIVE_PROVEN | T15 15Q real model build; T17E async stage |
| Event-driven waits (CI) | LIVE_PROVEN | T16 16D deadline path ×2 + T17 natural wake (mode=event, 112.5s) |
| Event replay (before-registration) | TEST_PROVEN + production-realistic exercise | 17C 4.6s vs real completed CI; natural-cycle confirmation: the 17Z wake arrived mid-wait, replay path itself not yet hit naturally |
| Cross-user bridge delivery | LIVE_PROVEN | 17C repro as runner user (2.05s) + production deploys |
| Provider boundary | TEST_PROVEN (structural guard) + observed loud-failure | 16F/G suites; no silent fallback observed in production runs |
| Builder cannot self-certify | LIVE_PROVEN | T14 closure refusals ×2; phase-gated close |
| Completion truth evidence-driven | LIVE_PROVEN | contract checkpoints; verify_test_truth refusals |
| Interruption/resume | LIVE_PROVEN | T14E SIGKILL; 17E restart held-outs |
| Impact preview (visible, pre-build) | ABSENT → T18 | nothing renders structured impact before build today |
| Structural project model (first-class) | PARTIAL | stores are spine-shaped (entities/rels) but projects model CONTRACTS, not architecture; no domain/subsystem/interface nesting for arbitrary projects |
| Structural queries/impact closure for projects | PARTIAL | spine.impact_frontier exists for Rune; project stores lack query API |
| Research→Build Basis (durable, versioned) | ABSENT → T18 | research output today stays in transient context/artifacts |
| Research-depth decision at construction entry | PARTIAL | ADAPT routes task classes; no explicit construction-stage research gate |
| OUTWARD prior-art-first pass | PARTIAL | stage exists in Research Core; not strengthened to precedent-first ordering |
| Schema versioning + consent-gated migration | PARTIAL | stores carry schema_version=1; no migration machinery/consent flow |
| Environment/runtime model | PARTIAL | deployment_surface exists in Spine; project scaffolds don't model envs |
| Workspace lifecycle (worktree per contract) | PARTIAL | BuildManager worktrees exist (legacy adapter); not wired to contracts |
| Forge abstraction | ABSENT (Gitea-coupled scripts) | deploy/qualify call Gitea API directly |
| Structural drift for project scaffolds | PARTIAL | drift_resume (16P) covers checkpoint-vs-reality; no file/entity-level drift detection |
| Graph projection for visualisation | ABSENT → T18 | Spine has snapshots; no stable read-only graph projection |
| Memory/Substrate | ABSENT (BY DESIGN — out of scope) | rule L |
## V1 North Star gap matrix (this tranche's targets)
| North Star capability | Gap | T18 action |
|---|---|---|
| Research-led construction (early research, Build Basis) | ABSENT Build Basis | 18C: build_basis.py + ADAPT depth hook |
| OUTWARD precedent-first | PARTIAL | 18D: strengthen stage contract + real research run |
| First-class structural Scaffold | PARTIAL (contract-shaped) | 18E: structural_model.py over structural_store core |
| Structure→scope→governance | PARTIAL | 18F: queries + impact closure feeding contracts |
| Visible impact preview | ABSENT | 18G: impact_preview projection |
| Structural drift | PARTIAL | 18H: extend drift_resume with entity-level checks |
| Schema versioning + consent | PARTIAL | 18I: migration + consent flow + tests |
| Environment/runtime model | PARTIAL | 18J: env entities + verification distinction |
| Workspace lifecycle | PARTIAL (legacy adapter) | 18K: workspace contract on the BuildManager adapter |
| Forge boundary | ABSENT | 18L: forge.py adapter interface (Gitea impl) |
| Full route convergence | PARTIAL (classify shared; backends differ) | 18M: audit + smallest fix |
| Generalisation (not phrase list) | TEST_PROVEN | 18N: adversarial paraphrase families |
| Self-scaffold | PARTIAL (contracts only) | 18O: structural model of Scaffolding itself |
| Upgrade-via-Scaffold proof | ABSENT | 18P: one real upgrade driven by the scaffold |
| Graph projection | ABSENT | 18Q: versioned read-only projection |
## T19 additions (post-T18 classifications)
| Capability | Status | Evidence |
|---|---|---|
| Project/Scaffold Registry | TEST_PROVEN | registry tests; boundaries proven (no truth in index, no Spine leak) |
| /build governance fail-closed | LIVE_PROVEN | 19A: T18 degradation paths removed; failure-path held-outs |
| Capability Ledgers | TEST_PROVEN | lifecycle+history+packet tests |
| Surgical context packets | LIVE_PROVEN | 19R: packet bound exactly 1 file for a real change |
| Behaviour/Test Specs + hardening lifecycle | TEST_PROVEN | ledger suites incl. generation history |
| Selective invalidation | TEST_PROVEN | 19S-2/5/9/10 held-outs |
| Predictive vs descriptive impact | TEST_PROVEN | delta persistence tests |
| Post-change obligations | TEST_PROVEN | machine-derived from structural truth |
| /testing backend + aggregate | TEST_PROVEN + LIVE (19R) | testing_mode suites; command wired in real TUI |
| Observation→closure (user-confirm gate) | TEST_PROVEN | closure requires retest AND user confirm |
| Forge failure never rewrites truth | TEST_PROVEN | 19S-14 held-out |
| Self-hosted T19 change | LIVE_PROVEN | contract 19r-testing-command, 11-step scaffold-driven flow |
## Kernel exceptions required (rule B record)
None yet. Every T18 plan so far EXTENDS (new modules consuming
structural_store) rather than modifying frozen kernel behavior. Any
exception discovered during implementation will be recorded here with
evidence that the V1 requirement could not be met otherwise.
+153
View File
@@ -0,0 +1,153 @@
# SCAFFOLDING V1 DEFINITION (T18 §18B)
_Plane-English + technical definition of Scaffolding as it ACTUALLY
exists at tranche T18 start (master 29570f4). Companion artifact:
SCAFFOLDING_IMPLEMENTATION_AUDIT (evidence classification per claim)._
## What Scaffolding is (plain English)
Scaffolding is Rune's **construction authority**: the governed process
by which software gets built or changed. It takes an authorized need,
classifies what kind of construction it is, proportionally governs the
work through a durable contract (with legal lifecycle transitions),
derives scope from structural reality, drives builders, waits on events
(not sleeps), refuses self-certified completion, verifies from
canonical reality, and records evidence on durable objects so any
future session can resume without a transcript.
It is NOT a scheduler, NOT a memory system, NOT a provider selector,
NOT a research engine, NOT a release system. It is the layer that
decides *whether, how, and with what obligations* construction happens
— and whether it actually finished.
## What it owns
- **Construction classification** (`construction_mode.classify_mode` —
the ONE classifier: NOT_CONSTRUCTION / LOCAL_CHANGE /
EXISTING_PROJECT_CHANGE / PROJECT_BUILD / UNKNOWN; never silently
chooses; unknown surfaces probe questions)
- **Conversational interpretation** (`construction_intent.py` —
meaning/referents/authorization; DISCUSSION→PROPOSAL→USER_APPROVED;
feeds the ONE classifier, never deepens)
- **Project Scaffold + Contracts** (`scaffold.py` — legal phase map
draft→approved→building→verifying→…→closed/failed/abandoned; scope;
obligations; checkpoints; predicted-vs-observed at close)
- **Programme continuation** (`programme_continuation.py` —
run_construction_programme: stages 1:1 to contracts, persist→canary→
next; "task done ≠ programme done")
- **Impact/scope machinery** (construction_decision, completeness
gates, affected-surface propagation)
- **Stage guards** (`stage_guards.py` — readiness blocks, liveness
proves progress, exit canary proves effect; verify_test_truth refuses
false greens)
- **Failure taxonomy** (`failure_recovery.py` — 7 classes incl.
PROVIDER_UNAVAILABLE; retry budgets; PERMANENT/APPROVAL never retry)
- **Builder interface** (`construction_interface.py` — ONE builder
boundary; JSON envelopes; builder identity recorded-but-inert;
discover_active_contract; capabilities())
## What it does NOT own
- **Provider/model selection** (provider layer owns; ambient context
via `provider_context.require_model`; structural guard test)
- **Research competence** (Research Core owns; Scaffolding invokes and
consumes — see 18C integration)
- **Asynchronous wait/resume machinery** (W/R event bridge owns;
`event_wait.py` is the repo-side adapter: event drives continuation,
watchdog detects failure, durable record is truth)
- **Release/deployment** (deploy_stage/promotion/qualify + Gitea
workflows own it; Scaffolding records evidence)
- **Rune's structural self-model** (Spine owns — `spine.py` over
`structural_store.py`; Scaffolding stores use the SAME generic core)
- **Long-horizon relevance/memory** (future Substrate — not built;
seam exists: construction_projection + construction_commands)
- **The TUI** (client; sends text, renders projections)
## How the flows work
**New build (PROJECT_BUILD)**: conversational/explicit intent →
classify_mode (+ project-existence evidence) → activate() bootstraps a
provisional Project Scaffold (identity before repository, UNKNOWN
fields explicit) → contract draft→approved (user provenance) →
building (builder under event wait) → verifying (exit canary;
independent evidence) → closed with predicted-vs-observed.
**Existing-project change**: intent → classify (scoped/file/target
evidence; proportional) → attach to existing authority → light or full
contract by depth → same lifecycle, proportionate ceremony.
**Research-led construction (V1 target — CORRECTED 2026-09-15, Nick):**
> THE RESEARCH PLAN ITSELF IS CONTEXTUALLY GENERATED. There is NO
> permanent research-topic checklist. For every request:
> REQUEST/INTENT → understand target/current reality → identify material
> unknowns/decisions/risks → generate a context-specific research plan →
> perform proportional INWARD/OUTWARD/deeper research. OUTWARD asks what
> already exists relevant to THIS specific intent (adopt/adapt/combine/
> reject/gaps/innovation). The categories researched are selected from
> context; deeper technical research is driven by unresolved questions
> and consequence level, never performed mechanically.
`build_basis.generate_research_plan()` implements this: topics are
synthesized from THIS intent's material unknowns/decisions/risks;
the held-out suite proves unrelated intents produce materially
different plans and that no canned category list exists.
**Future upgrades (V1 target)**: authorized need → research-depth
decision (ADAPT) → Research Core pass → durable Build Basis →
structural Scaffold consult/construct → impact closure + visible
preview → contract with structurally-derived scope → isolated
workspace → governed build/verify → merge/release → structural
Scaffold updated. (Parts of this chain are new in T18 — see audit.)
**Interruption/resume**: SIGKILL-safe. Durable contract + programme
records + wait records are the truth; fresh process resumes from
identity alone; duplicate signals idempotent (live-proven T14E/T17E).
**Deployment/release participation**: Scaffolding does not run the
pipeline; it consumes qualified CI/deploy evidence, records deployed
identity on contracts, and refuses closure without it where contracted.
## T19 layer model (the general engineering system)
> "The Scaffold should let the LLM operate with surgical precision
> locally, while preserving global awareness of what else must be
> checked or changed."
>
> "Lower active cognitive load without losing the whole idea."
- **STRUCTURAL MODEL** tells Rune what the project contains / how it connects.
- **CAPABILITY LEDGER** provides compact local re-entry context (stable IDs
across moves; history preserved through supersede/retire).
- **BEHAVIOUR VALIDATION LEDGER** tells Rune what intended behaviour is
actually confirmed — generation-aware, selective invalidation, automated
green never implies USER_CONFIRMED.
- **PROJECT REGISTRY** tells Rune which Scaffolds/projects exist
(discovery/index only — not structural truth, not Spine).
- **ITERATION ENGINE** (predictive→descriptive impact, impact-delta
records, machine-derived obligations) makes quicksand protection
STANDARD behaviour of every construction route.
- **/TESTING** is a dependency-aware hardening programme: dynamic queue,
fix-for-B-may-reopen-A, closure requires retest AND user confirmation.
- **FORGE** provides workflow/history projection (failure never rewrites
canonical validation truth).
- **FUTURE SUBSTRATE** will coordinate sequencing/current focus/pause-
resume but does NOT own structural or validation truth.
## How Spine relates
Spine = Rune's canonical structural self-model (structural_store
generic core, maturity ladder, impact_frontier). Scaffolding's project
stores share the SAME generic core (zero Rune constants) — one graph
discipline, two scopes: Spine models Rune; project Scaffolds model
their projects. No duplicate truth: a project's structural model lives
in its own store; Rune's lives in Spine. Later retrofit will modernise
Spine using the discipline V1 generalises (18K relationship).
## How Substrate will interact (without owning internals)
Read: versioned outbound projection (construction_projection,
SCHEMA_VERSION=1). Write: bounded validated commands
(construction_commands — 6 commands, lifecycle-validated, idempotent,
no direct storage mutation). Substrate asks; Scaffolding decides.
Memory graph explicitly out of scope (T18 rule L).
+73
View File
@@ -0,0 +1,73 @@
# Rune Structural Map
_Maintained structural projection — the fast re-entry surface (T16 §16N).
Companion to the Spine store (`store/spine/current.json`, structural
authority) and `store/evidence/t16/16M_inventory.md` (full classification
record). Update this map when authority or canonical routes change; do
not copy the repo here._
## What is Rune?
A self-hosting coding agent: TUI client over an operator core, with a
structural self-model (Spine), a construction authority (Scaffolding),
an event-driven wait/resume bridge (W/R), a research pipeline, and a
fail-closed release/deployment pipeline that builds, tests, releases,
and deploys Rune itself to `/mnt/workdrive/rune-code/current`.
## Where things happen (canonical routes only)
| Concern | Authority | State |
|---|---|---|
| Structural self-model | `src/rune/spine.py` | `store/spine/current.json` |
| Construction (build lifecycle) | `src/rune/scaffold.py` + `construction_*.py` | scaffold store (`<dir>/.rune/scaffold`) |
| Builder sessions | `scripts/t15_builder_session.py` (event-driven successor; T14 is legacy) | contract + wait records |
| Provider/model selection | `rune_operator` final decider; ambient context via `provider_context.require_model` | `state/model_routing_profiles.json` |
| Wait/resume (any long op) | `src/rune/event_wait.py` + W/R daemon (out-of-repo snapshot at `~/.local/share/mimo-wait-resume`, data root `/mnt/workdrive/wr-bridge` — PIN `RUNE_WR_ROOT`: unpinned consumers get a loud warning, never a silent split-brain) | waits/events/leases/resumes JSON |
| Programme continuation | `src/rune/programme_continuation.py` (WIRED T17: `run_construction_programme` binds stages to Scaffolding contracts) | scaffold store `programmes/` |
| Conversational construction | `src/rune/construction_intent.py` (meaning/referents/authorization) → `construction_mode.classify_mode` (the ONE classifier) | intents are transient; contracts durable |
| CI qualification verdict | `scripts/deploy_qualify.py` (two legs, fail-closed) | Gitea commit status + runs |
| Release/deploy | `scripts/deploy_stage.py` → `deploy_promotion.py` (monotonic, flock) | deploy root + `current`/`last-good` symlinks |
| Research | `scripts/research_task_tree.py` (authority; labels are projection) | `research/issue-<N>/` |
| Task completion truth | `verification_layer.py` + `orchestration/conversation_state.py` | durable event store + checkpoints |
| Routing (task class) | `src/rune/adaptive_router.py` | `routes/route.jsonl` |
| Canary registry | `src/rune/canary_native.MODULES` | observed by Spine |
| Structural health | `henxels_checks.py` + `henxels.yaml` | repo root |
## Known non-canonical / duplicate routes (do not extend; reconcile via 16Q)
- `build_mode.BuildManager` (legacy /build) — superseded by builder_bridge
+ event-driven sessions; still wired.
- `scripts/t14_builder_session.py` — superseded by t15 event-driven driver.
- Provider selection entry points ×5 (`model_routing.rank`,
`model_connect route`, `model_registry get`, `provider_registry`,
`work_manager`) — operator is the de-facto final decider.
- Task-ish state ×4 (task-tool DB, `task_persist` persisted.json,
`memory_store.working`, orchestration event store).
- Maintenance ledgers ×3 (scaffold maintenance_item, tui_backlog, drift_ledger).
## Unfinished / blocked / deprecated
- programme_continuation.py: WIRED (T17 17D — run_construction_programme
is the canonical multi-stage driver; 17E live-proven).
- maintenance.yml canary-suite job: loops `shared/canary_*.py` — wrong
dir (real canaries in `src/rune/`), job likely no-ops (LEGACY).
- daemon out-of-repo: the W/R daemon is deployed from
`~/.local/share/mimo-wait-resume` with its own git snapshot —
packaging debt, no CI coverage of the deployed copy.
- Substrate: NOT BUILT. Scaffolding exposes the seam
(`construction_projection.py`, `construction_commands.py`); mock
client proves compatibility (`tests/test_substrate_seam.py`).
## Fresh-session quick answers
- Is a claim true? → Spine store + evidence_refs, never commit messages.
- Is construction allowed? → scaffold contract phase + LEGAL_TRANSITIONS.
- How does a long wait work? → register on the W/R bridge, yield on the
FIFO event; watchdog is failure detection, never scheduling; the event
is a WAKE — authority is re-read after resume.
- Who picks the provider? → the user layer, via ambient context;
construction never does (`test_provider_ownership.py` enforces).
- What runs where? → CI on `rune-ci-desktop` runner; deploy stages to
`/mnt/workdrive/rune-code/<sha>`; data home pinned at
`/mnt/workdrive/rune` (RUNE_HOME); implementation worktree
`/mnt/workdrive/issue161-harness`.
+205
View File
@@ -0,0 +1,205 @@
# DICHOTOMY REBASE — HONEST AUDIT (function-level)
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
**Function-level completion: 13%** (290/2225 runtime functions matched)
| Status | Modules | Lines | Meaning |
|---|---|---|---|
| FULL | 7 | 686 | 80%+ of functions ported |
| PARTIAL | 50 | 17873 | 20–80% — real logic exists, gaps named below |
| NOT TOUCHED | 121 | 49578 | no name-preserving implementation |
| Total runtime | 178 | 68137 | |
## NOT TOUCHED — the port queue, in priority order (largest first)
| Module | Lines | Fns | Side |
|---|---|---|---|
| rune_tui | 4038 | 157 | TUI-LAST |
| rune_operator | 2723 | 68 | GO |
| memory_store | 2573 | 77 | GO |
| curator | 1838 | 43 | GO |
| tool_executor | 1812 | 47 | GO |
| warm_router | 1536 | 36 | GO |
| construction_interface | 1173 | 27 | RUST |
| spine | 1034 | 31 | RUST |
| construction_decision | 1014 | 20 | RUST |
| scaffold | 868 | 31 | GO |
| elastic_cognition | 765 | 13 | GO |
| memory_consolidation | 740 | 22 | GO |
| structural_store | 656 | 33 | RUST |
| abstraction | 647 | 16 | GO |
| contracts | 626 | 7 | RUST |
| work_manager | 591 | 25 | GO |
| construction_intent | 555 | 16 | RUST |
| canary_audit_ledger | 540 | 2 | TEST-INFRA |
| identity_anchors | 537 | 16 | RUST |
| build_mode | 517 | 33 | GO |
| drift_core | 517 | 14 | RUST |
| event_wait | 517 | 14 | GO |
| tui_validator | 514 | 13 | TUI-LAST |
| memory_selector | 511 | 13 | RUST |
| system_registry | 501 | 14 | RUST |
| construction_pattern | 496 | 12 | RUST |
| graph_memory | 474 | 9 | RUST |
| tool_protocol | 461 | 27 | GO |
| auto_tasks | 460 | 14 | GO |
| capability_fit | 455 | 13 | RUST |
| drift_ledger | 453 | 16 | RUST |
| goal | 441 | 19 | GO |
| release_notes | 439 | 16 | GO |
| cron_engine | 436 | 27 | GO |
| model_profiles | 435 | 15 | GO |
| model_routing | 432 | 13 | GO |
| voice_hybrid | 421 | 10 | RUST |
| behavior | 407 | 9 | RUST |
| token_optimizer | 407 | 22 | RUST |
| capability_ledger | 405 | 18 | RUST |
| chat_bubble | 401 | 12 | TUI-LAST |
| prompt_sync | 376 | 14 | RUST |
| provider_budget | 354 | 20 | GO |
| structural_model | 352 | 20 | RUST |
| programme_continuation | 349 | 11 | RUST |
| canary_rune_serve | 329 | 13 | TEST-INFRA |
| builder_bridge | 326 | 11 | GO |
| canary_p1_07 | 324 | 10 | TEST-INFRA |
| layout_contract | 316 | 6 | RUST |
| drift_observability | 315 | 8 | RUST |
| build_basis | 314 | 12 | RUST |
| canary_p0_07 | 310 | 10 | TEST-INFRA |
| attention_capsule | 296 | 7 | RUST |
| canary_p1_01 | 294 | 12 | TEST-INFRA |
| voice_sync | 293 | 10 | RUST |
| drift_cache | 292 | 15 | RUST |
| outcome_ledger | 286 | 7 | RUST |
| time_awareness | 283 | 14 | RUST |
| tiered_executor | 282 | 7 | GO |
| standardized_providers | 281 | 5 | GO |
| canary_p1_06 | 280 | 7 | TEST-INFRA |
| canary_p1_03 | 271 | 9 | TEST-INFRA |
| auto_context | 270 | 8 | GO |
| canary_gitea_automation | 263 | 3 | TEST-INFRA |
| canary_p1_02 | 253 | 10 | TEST-INFRA |
| construction_patterns_builtin | 249 | 1 | RUST |
| canary_full_system | 244 | 2 | TEST-INFRA |
| toolchain | 236 | 5 | RUST |
| task_gate | 235 | 8 | RUST |
| core_server | 234 | 12 | GO |
| canary_native | 231 | 3 | TEST-INFRA |
| openai_compat_transport | 227 | 9 | GO |
| shard_manager | 223 | 9 | RUST |
| spine_language | 218 | 5 | RUST |
| evidence_probe | 212 | 9 | GO |
| model_registry | 211 | 7 | GO |
| canary_p0_04 | 207 | 8 | TEST-INFRA |
| forge_client | 203 | 18 | GO |
| canary_context_broker | 199 | 10 | TEST-INFRA |
| canary_p0_05 | 199 | 13 | TEST-INFRA |
| testing_mode | 196 | 12 | RUST |
| cc_loader | 195 | 26 | RUST |
| canary_spine_meta | 192 | 7 | TEST-INFRA |
| verification_layer | 189 | 12 | RUST |
| ffi_query_memory_smoke | 182 | 5 | RUST |
| canary_tui_transport | 174 | 7 | TEST-INFRA |
| behaviour_ledger | 173 | 12 | RUST |
| canary_tui_drift | 170 | 2 | TEST-INFRA |
| inference_trace | 164 | 6 | RUST |
| ms_loader | 163 | 16 | RUST |
| voicebox | 161 | 11 | GO |
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
| forge | 146 | 11 | GO |
| rune_graphic | 145 | 5 | TUI-LAST |
| project_registry | 139 | 11 | RUST |
| voice_injector | 138 | 5 | GO |
| canary_rune_entry | 137 | 3 | TEST-INFRA |
| tui_backlog | 125 | 5 | TUI-LAST |
| drift_resume | 118 | 3 | RUST |
| canary_conversation_state | 115 | 2 | TEST-INFRA |
| canary_query_memory | 110 | 2 | TEST-INFRA |
| tui_transport | 108 | 10 | TUI-LAST |
| spine_loader | 105 | 7 | RUST |
| canary_unbounded | 104 | 2 | TEST-INFRA |
| message_widgets | 104 | 6 | TUI-LAST |
| sc_loader | 96 | 6 | RUST |
| cs_loader | 90 | 5 | RUST |
| tc_loader | 88 | 5 | RUST |
| canary_schema_migration | 86 | 1 | TEST-INFRA |
| mems_loader | 83 | 5 | RUST |
| provider_context | 48 | 1 | GO |
| wait_guard_corpus | 46 | 0 | RUST |
| canary_cognitive_strata | 44 | 1 | TEST-INFRA |
| builder_bridge_cli | 42 | 1 | GO |
| __init__ | 40 | 1 | RUST |
| mr_loader | 38 | 13 | RUST |
| canary_memory_synthesis | 33 | 1 | TEST-INFRA |
| canary_resource_authority | 33 | 1 | TEST-INFRA |
| canary_surface_parity | 33 | 1 | TEST-INFRA |
| canary_orchestration_foundation | 32 | 1 | TEST-INFRA |
| canary_gitea_delegation | 31 | 1 | TEST-INFRA |
## PARTIAL — gaps to close (matched/total fns; first missing named)
| Module | Lines | Coverage | Missing (first named) |
|---|---|---|---|
| local_tool_runtime | 806 | 8/30 | _find_capability_executor, _extract_context_length, context_length, reset_context_length_cache, supports_tools, supports_thinking |
| provider_registry | 666 | 7/27 | _format_tools_openai, _format_tools_anthropic, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
| sleep_time | 971 | 5/25 | _ensure_dirs, db_connect, fetch_sessions, extract_parts, extract_todos, write_transcript |
| persona_gate | 724 | 7/26 | _now, _cc_pg_core, _py_lex, _lex_input, _py_overlap, _embed_raw |
| context_broker | 823 | 8/26 | _py_default_estimator, _cc_core, _py_default_budget, _py_default_memory_budget, _py_memory_disabled_value, reset_discovered_windows |
| capability_broker | 905 | 4/20 | _native_tool_schemas, _infer_task_family, _scored_tools, _select_for_job, load_registry, _native_tool_cards |
| safety_override | 569 | 7/22 | _keyword_stopwords, _cc_probe, cc_loader_pin, self_test, _py_matches, escalation_rate |
| growth_delta | 437 | 5/19 | _store_db, _llm, _parse_json, _load_deltas, _save_deltas, _cc_core |
| failure_auto_filer | 527 | 11/24 | _error_text, unconsumed, mark_consumed, unconsumed, mark_consumed, submit |
| night_mode | 405 | 5/18 | get_night_mode, start_night_mode, stop_night_mode, get_status, get_report, to_dict |
| cache | 492 | 5/17 | _record_hit, _record_miss, flush_stats, get_cache, put_cache, invalidate_scope |
| parallel_processor | 194 | 3/14 | _shutdown_processor, get_processor, run_parallel_with_args, get_status, test_parallel, run_parallel_with_args |
| claim_audit | 483 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
| tier_manager | 262 | 5/15 | _cc_probe, cc_loader_pin, _py_core_now, _py_get_tier, _py_promote, _py_demote |
| witness | 466 | 7/17 | _now, _tables, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
| drift_compression | 307 | 3/12 | _py_get_compression_ratio, _py_compress_text, _py_compress_entry, _py_should_warn, _py_warn_before_compress, get_compression_ratio |
| message_queue | 316 | 5/13 | _load_queue, _save_queue, add_message, get_pending, approve_all, get_approved |
| model_connect | 462 | 2/10 | is_moe_model, _now, _clean_model_name, _get_cloud_models, health_check, route_model |
| intent_continuity | 168 | 2/9 | to_dict, from_dict, _load, save, create_detour, resolve_detour |
| pins | 284 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
| adaptive_router | 625 | 13/19 | _floor_applies_from_flags, _floor_reason, record_final_route, _event, _record_transition_with, _detect_external_refs |
| audit_ledger | 231 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
| context_pressure | 262 | 3/9 | log_measurement, read_log, log_stats, _now, handle_hook, main |
| hook_engine | 860 | 2/8 | discover_functions, tag_function, recommend_placement, validate_placements, generate_report, handle_hook |
| microkernel | 434 | 6/12 | now_iso, discover, _extract_manifest, _load_registry, _save_registry, _run_hook |
| persona_definition | 210 | 2/8 | _by_topic, compile_full, emit_constitution, emit_identity, emit_persona, handle_hook |
| progressive_escalation | 279 | 2/8 | execute_with_escalation, test_escalation, handle_hook, to_dict, execute, verify |
| connection_pool | 144 | 3/8 | get_pool, get_connection, get_status, get_connection, get_status |
| iteration | 190 | 3/8 | _now, context_packet, predictive_impact, descriptive_impact, add |
| task_persist | 269 | 5/10 | _save, export, restore, main, handle_hook |
| construction_commands | 268 | 1/5 | _refuse, _validate, _accept, dispatch |
| construction_mode | 395 | 1/5 | _tokens, _contains_any, to_dict, from_dict |
| event_envelope | 303 | 4/8 | _content_hash, test_envelope, to_canonical_dict, to_dict |
| response_adapter | 222 | 3/7 | parse_plain_response, parse_response, test_adapter, to_dict |
| self_improve | 242 | 1/5 | _now, suggest_improvements, handle_hook, main |
| subconscious_pass | 187 | 1/5 | main, _extract_topic, save_reflection, get_recent_findings |
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
| dependency_tracker | 202 | 2/5 | check_propagation, get_full_map, main |
| event_bus | 244 | 4/7 | _store_path, _content_hash, handle_hook |
| salience_gate | 243 | 1/4 | test_salience, handle_hook, to_dict |
| subsystem_health | 163 | 3/6 | set_auto_filer, record_failure, configure_logging |
| warm_neuron_store | 122 | 3/6 | _db, neuron_slugs, handle_hook |
| cognitive_budgeter | 208 | 1/3 | test_budgeter, to_dict |
| construction_projection | 135 | 1/3 | _contract_entity, validate |
| graph_projection | 108 | 3/5 | write_all, _now |
| rune_profiler | 165 | 1/3 | profile_model_call, main |
| stage_guards | 281 | 7/9 | to_dict, chk |
| structural_migration | 77 | 2/4 | apply_migration, consent_brief |
| construction_depth | 138 | 1/2 | to_dict |
| recall_router | 162 | 2/3 | handle_hook |
## FULL — ported, parity-gated
| Module | Lines | Fns | Side |
|---|---|---|---|
| recall_uplift | 141 | 1/1 | RUST |
| rune_spinner | 130 | 4/5 | RUST |
| wait_guard | 116 | 1/1 | RUST |
| failure_recovery | 108 | 1/1 | RUST |
| programme_gates | 85 | 4/5 | RUST |
| impact_preview | 69 | 2/2 | RUST |
| route_risk_flags | 37 | 1/1 | RUST |
+92 -92
View File
@@ -11,24 +11,22 @@
"NEEDS_REVALIDATION",
"DEFERRED_FUTURE"
],
"live_defects": [
324
],
"live_defects": [],
"records": [
{
"number": 324,
"title": "Engine defect: checkpoint writer fails deterministically on aborted worker sessions (MessageAbortedError -> empty checkpoint shells)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"bug",
"custodian",
"life",
"memory"
],
"live_defect": true,
"last_revalidated": "2026-09-11",
"next_action": ""
"live_defect": false,
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 defect no longer reproduces: zero MessageAbortedError in current logs (incl. active worker), six most recent session checkpoints full documents (111-396 lines); verified by present-absence 2026-09-19"
},
{
"number": 316,
@@ -41,8 +39,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B3-v2 Qdrant associative-memory projection spike \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 315,
@@ -54,14 +52,14 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B2-v2 wiring-status/architecture-truth report generator \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 311,
"title": "ADAPT-1 B2 (STANDARD-LIKE): wiring-status/architecture-truth report generator (#158)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "SUPERSEDED",
"labels": [
"infrastructure",
"perception"
@@ -74,14 +72,14 @@
"number": 301,
"title": "FINAL-12: Cross-track benchmark + adaptive provisional ratification",
"tracker_state": "open",
"reconciliation_state": "NEEDS_REVALIDATION",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"growth",
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "cross-track benchmark + adaptive provisional ratification \u2014 canonical remaining-obligations list"
},
{
"number": 300,
@@ -92,90 +90,90 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "escalation/downgrade honored at acceptance on the deployed runtime \u2014 probe_and_escalate exists; fresh live acceptance evidence pending"
},
{
"number": 299,
"title": "ADAPT-10: Harness/context legibility audit",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature",
"governance"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "harness/context legibility audit \u2014 canonical remaining-obligations list"
},
{
"number": 297,
"title": "ADAPT-8: Deterministic-first QUALIFY",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 296,
"title": "ADAPT-7: DESIGN consolidation experiment (APPLICABILITY + INNOVATION)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "DESIGN consolidation experiment (APPLICABILITY+INNOVATION) \u2014 canonical remaining-obligations list"
},
{
"number": 295,
"title": "ADAPT-6: Evidence capsule / context minimization",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "evidence capsule / context minimization \u2014 canonical remaining-obligations list"
},
{
"number": 294,
"title": "ADAPT-5: Bound RESEARCHED_CHANGE work (budgets + stop conditions)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "budgets + stop conditions for RESEARCHED_CHANGE \u2014 canonical 'remaining ADAPT obligations for later epics'"
},
{
"number": 293,
"title": "ADAPT-4: STANDARD_CHANGE path (DESIGN-LITE)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 292,
"title": "ADAPT-3: LOCAL_FIX fast path",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
@@ -190,8 +188,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "epic tracker for remaining ADAPT sub-items (#294 #295 #296 #299 #300 #301 #315 #316)"
},
{
"number": 259,
@@ -237,8 +235,8 @@
{
"number": 183,
"title": "Future: typed temporal semantics for facts, relationships, beliefs, and preferences",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -250,14 +248,14 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 182,
"title": "Future: GitHub code-search prior-art audit for graph and vector memory",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -266,8 +264,8 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 181,
@@ -287,13 +285,13 @@
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"next_action": "epic holds absorbed children #182 #183 + research watchlist #164 #165 #166; closes when prerequisites land or ideas dropped"
},
{
"number": 179,
"title": "Future: succession/privacy semantics and lineage evaluation",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -305,14 +303,14 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 178,
"title": "Future: evidence-grounded ancestral perspective with explicit uncertainty",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -322,14 +320,14 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 177,
"title": "Future: temporal autobiographical model with provenance and reinterpretation lineage",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -340,14 +338,14 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 176,
"title": "Future: lineage-aware situated curiosity for meaningful autobiographical gaps",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"feature",
@@ -358,8 +356,8 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 175,
@@ -379,13 +377,13 @@
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"next_action": "epic holds absorbed children #176-#179 as checklist items; closes when prerequisites land or ideas dropped"
},
{
"number": 166,
"title": "RESEARCH WATCHLIST: Memory-R1 (RL memory ops), Titans (test-time memory), Vespa/Weaviate/Milvus (retrieval competitors)",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"growth",
@@ -394,14 +392,14 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 165,
"title": "RESEARCH: SimpleMem semantic compression + A-MEM dynamic linking + Letta sleep-time compute",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"infrastructure",
@@ -410,14 +408,14 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 164,
"title": "RESEARCH: HippoRAG 2 \u2014 PPR-style associative propagation as warm-neuron activation candidate",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"labels": [
"P3 \u2014 low",
"growth",
@@ -426,27 +424,27 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
},
{
"number": 163,
"title": "Temporal graph backend spike: Neo4j vs FalkorDB (LadybugDB experimental; Graphiti as reference)",
"tracker_state": "open",
"reconciliation_state": "NEEDS_REVALIDATION",
"reconciliation_state": "VALID_OPEN",
"labels": [
"P1 \u2014 high",
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "run the backend spike; compare on same corpus/query set; record evidence-backed recommendation"
},
{
"number": 162,
"title": "Qdrant associative-memory projection spike (dense/sparse/multivector + ColBERT late-interaction reranking)",
"tracker_state": "open",
"reconciliation_state": "NEEDS_REVALIDATION",
"tracker_state": "closed",
"reconciliation_state": "SUPERSEDED",
"labels": [
"P1 \u2014 high",
"custodian",
@@ -454,8 +452,8 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 scope carried verbatim by #316 (ADAPT-1 B3-v2); single-tracker rule"
},
{
"number": 159,
@@ -478,8 +476,8 @@
{
"number": 158,
"title": "Generated wiring-status / architecture-truth report",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "SUPERSEDED",
"labels": [
"P2 \u2014 medium",
"growth",
@@ -489,8 +487,8 @@
"ready"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-19",
"next_action": "none \u2014 scope carried verbatim by #315 (ADAPT-1 B2-v2); single-tracker rule"
},
{
"number": 156,
@@ -1150,5 +1148,7 @@
"last_revalidated": "2026-09-11",
"next_action": ""
}
]
],
"last_reconciled": "2026-09-19",
"reconciled_by": "fresh-session tranche 2026-09-19: template retrofit (spine+plain-English on all open issues), closures #158/#162 (SUPERSEDED) and #324 (FIXED_VERIFIED), research labels applied to #163/#164/#165/#166/#182/#316, stale bug labels removed from #163-#166; 2026-09-19 absorption pass: parked children absorbed into parents (#175<-#176..#179; #181<-#182,#183,#164..#166) per one-open-note-per-idea standard"
}
+134
View File
@@ -0,0 +1,134 @@
# THE LANGUAGE DICHOTOMY LEDGER — measured, not assumed
_Created 2026-10-06 from real benchmarks. This is the governing standard for every port in the full dichotomy rebase. Every port must pass the "which language is better for THIS operation?" test before being assigned._
---
## The Measured Benchmarks
| Operation | Python | Python ctypes → Rust | Go cgo → Rust | Pure Go inline |
|---|---|---|---|---|
| Simple tier check | 0.05 µs | 1.4 µs | 1.4 µs | — |
| Consolidation (669B payload) | 0.2 µs | 2.9 µs | 3.4 µs | 0.3 µs |
| Concurrent HTTP (10 requests) | 10.8ms (4.0x) | — | 8.4ms (5.1x) | — |
| CPU-bound pattern matching (1000 msgs) | 9.6ms (0.8x — GIL) | — | ~2.5ms (est. 3.5-4x) | — |
| Compression + zlib wrapper | 0.2 µs | 13.7 µs (worse!) | — | — |
## The Laws
### Law 1: FFI overhead is negligible for the real workload
The cgo/ctypes boundary costs 1.4-3.4 µs per call. Ollama inference costs 200-5000 ms. FFI overhead is 0.0003-0.0017% of total request time. The language of the caller doesn't matter for the boundary.
### Law 2: Go wins for CPU-bound reactive work
Goroutines deliver true parallelism (5.1x concurrent HTTP, ~3.5-4x CPU-bound). Python's GIL serialises CPU-bound work (0.8x speedup with 4 threads). Go wins for: operator, witness, event bus, work manager, tool executor.
### Law 3: Simple operations stay in the caller's language
Pure Python compression: 0.3 µs. Same op via Rust FFI: 10.4 µs. The FFI overhead exceeds the compute benefit for simple operations. If a Go module needs a string truncation, do it IN GO.
### Law 4: Rust wins for complex deterministic operations
Compile-time type safety, zero-cost abstractions, memory safety, near-native speed for complex compute. Rust wins for: scoring, dedup, pattern matching across many signals, record assembly with many fields, structural store operations.
### Law 5: Compression across the FFI boundary makes things SLOWER
Measured: uncompressed FFI call = 6.1 µs. With zlib wrapper = 13.7 µs (+125%). The cgo transition overhead (~1-2µs) dominates; payload size doesn't matter for local calls under 10KB. Compression helps only for network transfers (>100KB payloads).
### Law 6: Reduced payload (shorter keys) gives a marginal improvement
Measured: 6.1 → 4.6 µs (-25%) by shortening JSON keys from "wash_count" to "w". This is a valid optimisation for hot-path FFI calls but not worth the complexity for most operations.
### Law 7: Pure inline (no FFI) is fastest when the operation is simple
Measured: pure Go inline = 0.3 µs vs 6.1 µs via FFI. If the operation is simple enough to implement in the caller's language, do it inline. Only cross the FFI boundary for operations complex enough to justify it.
---
## The Practical Decision Tree
```
Is the operation I/O-bound (HTTP, subprocess, file I/O)?
→ YES → GO (goroutines, net/http, os/exec)
→ NO ↓
Is the operation CPU-bound and in the request path (responding to user)?
→ YES → GO (goroutines give true parallelism where the GIL fails)
→ NO ↓
Is the operation complex deterministic compute (multiple functions, thresholds, scoring)?
→ YES → RUST (type safety, zero-cost abstractions, memory safety)
BUT measure: does the compute exceed the FFI overhead?
→ NO ↓
Is the operation simple (string truncation, basic comparison, simple lookup)?
→ YES → Do it INLINE in the caller's language (no FFI)
```
---
## The Wave Assignment (corrected)
| Wave | Language | Why |
|---|---|---|
| W4: event-bus, hook-engine, message-queue, microkernel, subsystem-health, parallel-processor, work-manager, failure-filer, cron-engine, night-mode, cache, connection-pool, recall-router | **Go** | I/O-bound, CPU-bound reactive, event-driven |
| W4b: tool-executor, tool-protocol, local-tool-runtime | **Go** | I/O-bound, request-path |
| W5a: model-connect (all 10 provider modules) | **Go** | I/O-bound (HTTP), connection pooling |
| W5b: operator, adaptive_router, build_mode, warm_router, etc. | **Go** | CPU-bound reactive (routing, dispatch) + I/O-bound |
| W5c: memory_store, curator, sleep_time | **Go** | I/O-bound (SQLite + subprocess) |
| W6: spine, construction_*, contracts, capability_*, drift_*, graph_memory, memory_selector, identity_anchors, pins, salience_gate, self_improve, token_optimizer | **Rust** | Complex deterministic compute (scoring, dedup, classification, structural operations) |
| W6 simple modules: response_adapter, route_risk_flags, rune_profiler, rune_spinner, wait_guard, task_persist, time_awareness | **Stay in caller's language** | Simple operations where FFI overhead > compute benefit |
| W7: TUI | **Go (Bubble Tea)** | The reactive front end that interfaces with the user |
---
## The Compression Idea (EXPERIMENTAL — NOT WORTH IT)
**Measured result: compression across the FFI boundary makes things 125% SLOWER.**
The cgo transition overhead (~1-2µs) is the dominant cost, not the payload size. Compressing the payload adds CPU work (compress + decompress) that exceeds the benefit of a smaller payload crossing the boundary. The compressed payload is 20% of the original size, but the total time doubles.
Compression WOULD help only if:
- The payload were >100KB (network transfer between machines)
- The boundary were a network connection, not a local function call
**Mark as experimental and park. Do not implement.**
---
## The Hybrid Language Idea (FUTURE — NOT NOW)
A hybrid language combining Go's concurrency with Rust's type safety and memory safety would eliminate the FFI boundary entirely. This would be the ideal language for the entire runtime.
But this is a much larger project — effectively designing and implementing a new programming language. It should be explored after the rebase is complete and the system is stable in Go+Rust. The cgo boundary works today with negligible overhead.
---
## Provenance
- Benchmarks run 2026-10-06 on nick-desktop (CachyOS, 4 cores, cgo 1.21, modernc.org/sqlite)
- Rust kernel: libcognition_core.so from cognition_core crate
- Go binaries: statically linked, go 1.21
- Python: /home/nick/.venvs/memory/bin/python3
- These measurements are for THIS hardware and THIS codebase. Re-measure if the hardware changes.
---
## The Hybrid Language: SURVIVOR
Named by Nick (2026-10-06): Rust (survives, memory-safe, deterministic) + Go (running, reactive, concurrent) = **Survivor** — the hybrid language pattern where Go reactive code links Rust deterministic compute as a static library at compile time.
### The Survivor Build Pattern
- Rust crates compile as staticlib (.a) + cdylib (.so)
- Go binaries link the .a files at compile time via cgo LDFLAGS
- The caller sees ONE binary with ONE function call — no FFI boundary from the outside
- No CC_RUST_LIB / SC_RUST_LIB env vars needed
- Measured: 0.6µs/call vs 1.4-3.4µs for the .so approach
### Survivor Go Packages
All under src/go/cognition/:
- cognition (root): tier, consolidation, witness, safety, drift, pins, growth
- retrieval: memory_retrieval kernel (mr_call)
- storage: memory_storage kernel (mems_read_query)
- scaffold: scaffold_core kernel (sc_check_transition, sc_close_quality, sc_language_placement)
- provenance: evidence_provenance kernel (ep_* functions)
- spine: rune_spine kernel (sp_drift, sp_export, sp_project, sp_query)
- scoring: memory_scoring kernel (ms_* functions)
- capability: capability_scoring kernel (cs_evaluate_fit, cs_score_card)
- token: token_counting kernel (tc_compress_context, tc_estimate_tokens)
- query: query_memory kernel (qm_* functions)
+382
View File
@@ -0,0 +1,382 @@
# Language-Fit Rubric — Wave 1 (Language-Rebasing Tranche R1)
> STATUS: FINAL — self-verified 2026-09-29 per tranche §12 (all 6 rounds completed,
> evidence-sufficiency statements recorded, kill tests frozen; execution proceeds on the
> evidence, every RECODE still passes its benchmark gate — no win, no recode).
> Per-subsystem sections filled from live research rounds (≥2 live-verified external sources per claim,
> per-source lessons, evidence→decision mapping, kill test per candidate). No code changes in Wave 1.
> Governing law from §11: **KEEP-PYTHON honesty determines how much of the rebase volume is real.**
> Toolchain baseline: Rust 1.98.1 + GCC 16.2.1 present; Go NOT installed — Go earns installation only
> through rubric evidence here.
## Verdict (all rounds, one paragraph)
**KEEP-PYTHON across the board: 40 clusters scored, 0 recode units awarded.** Six research
rounds (W2 leaf utilities, W3 memory, W4 provider-layer, W5 orchestration, W6 runtime-core,
W7 TUI) each characterized their subsystem's actual workload inward (file:line-anchored) and
tested Python/Rust/Go/C against it with live-verified sources and frozen kill tests. The
pattern is consistent: the compute-shaped work already executes in C (hashlib, sqlite3/FTS5/vec0,
json, regex), the dominant latency is network or LLM-bound, and no cluster contains a hot loop
that pays for a process-boundary recode through the 658 µs–0.63 ms interop crossing. **Go:
REJECTED — does not earn installation this tranche** (D5=0 everywhere; nothing measured or
fetch-confirmed earns it; orchestration additionally rejects its design fit; K-GO kill tests
frozen per round should the case be revived). **Rust: toolchain-ready, justified nowhere this
tranche** — keep as the standing option behind the CI-proven std-only adapter for any future
measured hot loop. **C: rejected everywhere scored** (duplicates C already inside SQLite/hashlib;
adds an unsafe-memory surface). The rebase volume the tranche hypothesized is real only as a
small set of named improvements — see "Wave-plan consequence" below.
## Rubric dimensions (applied to every candidate language per subsystem)
| Dimension | Question | Evidence required |
|---|---|---|
| D1 Workload fit | Does the subsystem's dominant workload (I/O-bound, CPU-bound, FFI, protocol, UI) actually favour this language? | Benchmarks or authoritative docs, live-verified |
| D2 Measured gain | What wins, concretely — latency, memory, startup, binary distribution? | Named hot paths from the Wave 0 inventory + numbers |
| D3 Interop cost | What does the interop contract (§5) cost to carry for this boundary? | Adapter surface size, ABI/build complexity |
| D4 Ecosystem | Do the needed libraries exist and are they maintained? | Live-verified crate/package/module listings |
| D5 Toolchain reality | Is the toolchain present (or installable and justified) on this machine and the CI runner? | Runner + local `--version` evidence |
| D6 Maintenance continuity | Can the Rune ecosystem's single-maintainer loop sustain code in this language here? | Honest judgement, stated plainly |
| D7 Kill test | What is the CHEAPEST disproof of this candidate? | One falsifiable early test, frozen before any recode |
Scoring: each candidate gets D1–D6 scored 0–2 with one-line evidence each, D7 written regardless.
Decision per subsystem: **KEEP-PYTHON** / **RECODE→<language>** — KEEP-PYTHON is the expected default;
a RECODE that cannot pass its own kill test reverts to KEEP-PYTHON before implementation.
---
## W3 — Memory & persistence (units: memory_store 56 fns, curator 43, warm_router 32, memory_consolidation 20, sleep_time 25, + graph_memory, night_mode, identity_anchors)
- **Research round actor:** general-2 (COMPLETED 2026-09-29 — artifacts at
`research/wave1/memory/`: inward.md, outward/ ×6, outward-sources.md, synthesis.md)
- **Intention profile:** JSONL-heavy appends, BM25/FTS ranking, consolidation passes, long-running
scheduled jobs, file-backed state.
- **Decision:** **KEEP-PYTHON (all 7 clusters, zero RECODE).** Cluster verdicts:
(1) hot-store JSON persistence — KEEP with upgrade path: orjson in-process wheel
(~10× dumps / 2× loads); a bespoke Rust CLI unit is dead on spawn cost (the
`shared/native_units.py` crossing ~1–5 ms ≈ orjson's entire 3.4 MB `knowledge.json` decode);
(2) SQLite store + FTS5/vec0 recall — KEEP, the compute is already C inside SQLite
(`memory_store.py:646` vec0 MATCH, `:693` bm25); (3) neuron scoring/routing — KEEP,
network-bound `_embed` (Ollama subprocess curl) dominates; (4) graph BFS — KEEP,
hops=1 default, per-node SQL, tiny N; the future fix is a SQL CTE, not a language;
(5) batch consolidation/curator — KEEP, LLM/I-O-bound and latency-insensitive,
asyncio sweet spot; (6) orchestration/glue — KEEP, pure glue pinned by the Python-TUI
interop contract; (7) embedding transport — KEEP, network-bound.
- **Candidate matrix summary:** Python 7/7 · Go 0/7 (not installed, nothing measured or
fetch-confirmed earns installation; Go's JSON stack mid-rewrite upstream) · hand-written C
0/7 (C already battle-tested inside SQLite — hand-writing would duplicate it) · Rust 0/7
as CLI units (spawn cost kills the one credible vector; in-process wheels excluded by
Wave 0 policy).
- **Kill tests (frozen in synthesis.md):** Rust-JSON — orjson microbench on live 3.4 MB
`knowledge.json` (if drop-in and ≥2× load / ≥5× dump, the CLI unit is disproven on spawn
cost); Rust-general — kill unless a profile shows ≥50% wall time in pure Python compute
(none exists; reopen only on materially changed workload); Go — instrument one
`consolidate_all` run, kill if Python-compute share <50% (it will be — LLM/I-O bound);
C — kill unless a hot kernel absent from the SQLite extension ecosystem is identified
(none).
- **Sources + lessons:** 6 live-fetched 2026-09-29 (orjson, sled, sqlite-vec, SQLite FTS5
docs, Go json/v2 rewrite issue, pydantic-core) — full annotations in
`research/wave1/memory/outward-sources.md`.
- **Evidence sufficiency (honest):** static analysis only, no runtime profiling;
memory.store SQLite DB not materialized locally — kill tests specify *measured*
thresholds rather than assertions. Verdicts rest on "Python is glue around C/network,"
which profiling would only strengthen.
## W4 — Provider/model layer (units: provider_registry 28, model_connect 10, model_routing 15, model_profiles 15, provider_budget 21, + standardized_providers, model_registry, connection_pool)
- **Research round actor:** general-5 (COMPLETED 2026-09-29 — artifacts at
`research/wave1/provider-layer/`: inward.md, outward-sources.md, outward/ (8 evidence
JSONs), synthesis.md)
- **Intention profile:** HTTP/SSE streaming, connection pooling, retries/backoff, protocol parsing,
tool-call reliability.
- **Intention profile correction (inward-verified):** the layer has **NO retry/backoff and NO
SSE stub at all** (grep-verified) — `stream: False` everywhere, ~900 lines, remote-LLM-latency-
bound. Rewrite pitches are therefore feature-adds, not ports.
- **Candidate matrix:** 5 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
workload): (C1) HTTP wire + transport (provider_registry, openai_compat_transport, model_connect
discovery) Python 5 / Go 2 / Rust 1; (C2) connection pool (connection_pool.py, 144 lines —
in-process object guards under threading.Lock, NOT a TCP pool) Python 5 / Go 2 / Rust 2;
(C3) routing decisions (model_routing.py, 432 lines, pure decision logic, one httpx call)
Python 5 / Go 1 / Rust 1; (C4) budget accounting (provider_budget.py, 354 lines, pure
arithmetic + lock, no I/O) Python 5 / Go 1 / Rust 1; (C5) config/registry/normalization
(model_profiles, model_registry, standardized_providers, model_connect tables — dataclasses +
JSON, editable by non-systems programmers) Python 5 / Go 1 / Rust 1.
- **Decision:** **KEEP-PYTHON (all 5 clusters).**
- **Kill tests:** C1 Go — KILLED: toolchain absent (`which go` → not found); 28 call-site
behaviors incl. the tool-call envelope contract (openai_compat_transport.py:67-107, 160-207)
would be re-implemented with zero incumbent test port and NO performance signal (remote-LLM-
latency-bound, kilobyte payloads). Re-earn path if Go is ever installed: std-only proof via
`interop/rebase-proof/` + a benchmark harness on C1 only. C1 Rust — KILLED BY CONSTRUCTION:
SSE parsing requires reqwest+tokio+eventsource-stream+nom (outward O3/O4/O5 — even mistral.rs
takes the tokio stack), violating the std-only interop contract. C2–C5 Go/Rust — KILLED:
burden of proof sits on the rewrite candidates; kill tests need contention/throughput/
counter-evidence that does not exist; routing runs once per request at millisecond scale
against multi-second LLM calls (5 orders of magnitude below any measurable threshold).
- **Sources + lessons:** 8 live-fetched (O1–O8), ≥2 per major claim, coverage map in
`outward-sources.md`. Lessons: (1) stdlib-only Rust has no realistic precedent for HTTP/SSE
workloads — the interop contract's zero-crate standard effectively decides Rust out of all
network-wire clusters; (2) Go's stdlib HTTP+NDJSON streaming is capability-proven
(pkg.go.dev/net/http, ollama api/client.go, ollama's Go API server) — Go's losses here are
evidence-absence, not capability-absence; (3) the honest reliability upgrade for this layer
is a small Python-level httpx retry/timeout wrapper — a feature-add, not a language change.
## W5 — Orchestration, authority, plugins, skills (units: orchestration/ 44 files 244 fns, contracts, build_mode 34, construction_authority*, verification_construction, spine, + governance surfaces: drift_*, persona_gate, safety_override, witness, behavior, growth_delta, claim_audit)
- **Research round actor:** general-1 (COMPLETED 2026-09-29 — artifacts at
`research/wave1/orchestration/`: inward.md, outward-sources.md, outward/ (4 search
JSONs), synthesis.md)
- **Intention profile:** contract/lifecycle state machines, authority gating, fail-closed validation,
HMAC-chained ledgers, policy dispatch.
- **Intention profile correction (inward-verified):** the subsystem is durable-ledger I/O,
authority wiring, deterministic policy gates, and TUI-facing coordination — NOT hot loops
(2 RLocks, 0 sockets, 0 thread pools across 43 files). **Scope gap: `construction_authority.py`
and `verification_construction.py` do not exist in the tree** — the construction cluster is
covered by build_mode.py; no evidence invented for the missing files.
- **Candidate matrix:** 6 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
workload): (C1) durable state & ledger stores (event_store.py:56-101, atomic.py, evidence_store.py
— fsync-bound; hashing already C via hashlib) Python 5; (C2) composition roots & authority wiring
(composition.py, authority.py, cognitive.py — pure glue, runtime cost ≈ 0) Python 5; (C3)
policy/state machines/gates (lifecycle.py, scheduler.py, broker.py, no_change.py, reflex.py —
deterministic dict/enum decisions, sub-ms; testability is the requirement) Python 5; (C4)
structural self-model & schema tooling (spine.py:24-25, microkernel.py, contracts.py, records.py
— introspection + canonical JSON; only candidate for a future Rust spine-diff if snapshots grow
1000×) Python 4; (C5) TUI-facing orchestration & build pipeline (build_mode.py 11-stage pipeline,
gateway.py, transport.py — the consumer IS the Python TUI; latency dominated by LLM/subprocess
waits) Python 5; (C6) benchmark/migration harnesses (retrieval_benchmark.py, migrate.py,
benchmark.py — offline tooling, recode buys nothing, loses iteration speed) Python 4.
- **Decision:** **KEEP-PYTHON (all 6 clusters). This subsystem contributes 0 recode units to
the wave plan.** Go: REJECTED, do not install — differentiator (goroutine-dense servers)
targets a workload absent here; cost is exception→error-value rewrite of ~260 functions.
Rust: FEASIBLE (toolchain proven) but UNJUSTIFIED anywhere — no cluster has the hot loop
that pays for a process-boundary recode; keep as toolchain-ready for other subsystems.
C: REJECTED — the only compute-shaped work (sha256) already executes C via stdlib hashlib.
- **Kill tests:** K1 (frozen, ALREADY RUN): 100 round-trips through the rebase-proof adapter —
mean 0.63ms, p95 0.74ms, cold 0.87ms, 100% exit-0 + ok:true → adapter overhead does NOT block
recodes on cost; recodes die on value grounds. Re-open any cluster if profiling shows a
function consuming >10% of a real interactive session. K-GO (frozen, ~30 min): Go prototype
of event_store.py's batched append+fsync vs Python at 1000 events; if Python's p95 is within
2× of Go's, Go is dead here permanently. K-RUST (frozen, ~20 min): measure spine diff on a
10k-entity snapshot; <50ms kills any Rust spine recode by absence of pain.
- **Sources + lessons:** 4 live-fetched web sources (pydantic-core, ruff blog, go.dev FAQ,
Python 3.14 hashlib docs) + 1 LOCAL EXPERIMENT (n=100 adapter benchmark). Lessons: (1) the
rebase-proof adapter round-trips at 0.63ms/op — subprocess-per-op is viable for coarse ops,
never per-event; (2) Python's "slowness" argument dissolves where the actual compute already
runs in C via stdlib (hashlib, sqlite) — the language question is really "where is the hot
loop", and here the answer is nowhere; (3) assumed limits flagged honestly: human-scale event
cadence (batch_size=8) and Linux spawn cost — both would need revisit at 10⁴+ events/sec or
cross-platform targets.
## W6 — Runtime core & tooling (units: tool_executor 46, tool_protocol 27, local_tool_runtime 32, capability_broker 19, context_broker 21, work_manager 25, cron_engine 28, + failure_auto_filer 28, hook_engine, elastic_cognition, auto_tasks, goal)
- **Research round actor:** general-4 (COMPLETED 2026-09-29 on disk despite cancellation
notification — artifacts at `research/wave1/runtime-core/`: inward.md, outward/,
outward-sources.md, synthesis.md)
- **Intention profile:** subprocess/process supervision, native execution boundary (native_units
adapter already exists), event loops, cron scheduling, hook dispatch.
- **Decision:** **STAY-PYTHON (all 6 clusters).** Round scores (1–5 = fit for the cluster's
actual inward-characterized workload): (A) subprocess lifecycle (tool_executor, cron_engine,
auto_tasks) 4.7 — `subprocess.run(timeout=, capture_output=)` is a one-call expression of the
pattern; Rust std::process has NO built-in child timeout (outward-verified), strictly more
code under zero-crate; (B) protocol/JSON parsing (tool_protocol, brokers) 4.5 — inputs are
KB-scale model fragments; Python json/regex is already C-backed, so a rewrite swaps C for
compiled code at the same asymptotic cost; serde's typed-struct model actively mismatches
fence-wrapped schema-flexible output; (C) model→tool control loop + HTTP
(local_tool_runtime, elastic_cognition, goal) 4.8 — wall time is LLM HTTP latency in seconds;
Python overhead is µs–ms noise; the loop's content is policy (circuit breaker, escalation
ladders, routing tables); (D) context assembly (context_broker) 4.7 — correctness lives in
admission policy and degrade-not-crash semantics, not speed; compaction is itself an LLM call;
(E) reliability/queueing (failure_auto_filer) 4.2 — honest lowest: on paper the best Rust
candidate (queue + worker + crash-safe JSONL spool), but perf requirement ≈ nil and the
correctness burden is threading semantics a rewrite would re-risk for no win ("nothing
breaks today"); (F) declarative config/brokers (work_manager, hook_engine, capability_broker)
4.9 — literal data, no-op runtime, portable to anything valuable in nothing.
- **Candidate matrix summary:** Python 6/6 · Go 0/6 (paper verdicts only — evidence-starved,
not installed, kill tests unrunnable locally) · Rust 0/6 (kill tests runnable via the
existing std-only interop harness) · C unscored (no FFI-adjacent cluster surfaced).
Best-language moment found anywhere: robfig/cron replacing ~37 hand-rolled cron-parsing
lines (cron_engine.py:199-236) — ergonomics nicety, not a fit verdict.
- **Kill tests (frozen in synthesis.md):** Rust-A — re-express `_run_command`
(tool_executor.py:1064) with std::process + manual timeout; kill if >2× the ~40-line
Python wrapper (Q4 predicts it is); Go-A — port `cron_engine._run_loop` with os/exec,
measure task-fire latency delta (prediction: zero — 60 s sleep and child runtime dominate);
B — parse the largest observed tool-protocol stream 1000× in Python; kill if total <1 s
(no rewrite yields an observable win); C — instrument `_run_loop`
(local_tool_runtime.py:417), log non-HTTP CPU per iteration (prediction <1% of wall);
D — time `assemble`+`_admit` on the largest stored context, kill if <10 ms; E — fault-inject
kill -9 during spool append; if recovery semantics already hold, the rewrite has no
correctness win to buy; F — none needed: absence of any win condition IS the disproof.
- **Sources + lessons:** live-fetched 2026-09-29 — annotations in
`research/wave1/runtime-core/outward-sources.md`; headline lessons: Rust std::process lacks
child timeout (Q4); Go os/exec reaches parity but changes only syntax (Q1); Python
json/regex C-backed (Q5b); serde typed structs mismatch dynamic model output (Q5a);
concurrency features irrelevant at ≤2 threads (Q6).
- **Evidence sufficiency:** Python verdicts fully evidenced inward+outward; Go execution-side
evidence honestly starved (paper kill tests carried forward); E's JSONL-spool crash-safety
claim rests on inward semantics only — flagged in synthesis.md.
## W7 — Operator + TUI (units: rune_operator 61 fns, rune_tui 161 fns — largest module, tui_validator)
- **Research round actor:** general-3 (COMPLETED 2026-09-29 — artifacts at
`research/wave1/tui/`: inward.md, outward/ ×8, outward-sources.md, synthesis.md)
- **Intention profile:** terminal rendering, input handling, rich layout, hot store state, supervisory
command surface. Design decision 4 holds: TUI consumes the interop contract, never rewritten first.
- **Decision:** **KEEP-PYTHON (TUI shell; all 4 clusters, zero RECODE, no carve-out found).**
Cluster scores (1 = recode, 5 = keep): (A) rendering/widgets 4 — no hot loop exists; render is
plain Rich `Text` concatenation (rune_tui.py:1331, chat_bubble.py:157), only animation a 0.12 s
spinner (:263), no markdown/Syntax parsing anywhere in src/rune; (B) event glue (~50 `_cmd_*`
handlers, reactives, tool-widget maps) 5 — this is Textual's core competency (workers,
`exclusive=True`, `call_from_thread`); vendor docs (ratatui) admit "little to no help in organizing
large applications"; (C) model send path + envelope 5 — synchronous `send_task` in async worker;
the envelope shape (`success/response/thinking/tool_events/degraded`, rune_tui.py:3624ff) is
ALREADY the de-facto interop contract; (D) subprocess/streaming 5 — JSONL streaming precedent
already in-tree (`_cmd_pull` :3443-3517): the pattern for consuming recoded units, not a recode
target. Whole-TUI native recode 1/5 REJECTED — 80+ first-party module coupling, framework-provided
concurrency re-implemented by hand; Go additionally toolchain-blocked. Countervailing evidence
(Bubble Tea/tview/Codex TUI maturity) recorded honestly in outward-sources.md — it does not
outweigh the coupling.
- **⚠ CONTRACT GAP FLAGGED (binds Waves 2–7):** `shared/native_units.py` parses only the LAST
stdout line — it must gain a JSONL streaming reader (per-line `json.loads` + terminal-envelope
validation) before any recoded long-running unit can stream progress to the TUI. Small, testable,
non-breaking; candidate for the first Wave 2 implementation issue.
- **Adapter surface (TUI as consumer — adopted as the rubric's interop-contract restatement):**
(1) single-invocation stateless CLI (`native_unit <op> [args]`, no daemon); (2) success = final
stdout line `{"ok":true, …}` generalizing the model-send envelope — recoded units emitting
`response`/`tool_events`/`degraded` plug in with zero TUI changes; (3) failure = `ok:false` +
nonzero exit, stderr human-only, success = exit 0 AND not timed out; (4) mandatory `--version`
on every unit (status bar + `canary_tui_drift.py` detect live units vs Python fallback);
(5) JSONL streaming mode for long ops with terminal sentinel envelope; (6) per-call timeout
budget, `timed_out` surfaced in `NativeResult` for graceful degradation; (7) streamed events
referencing UI objects carry the TUI's stable keys — `(channel, call_id)` for tool widgets
(:3676), WorkPane `_entries` object ids (:1239) — preserved verbatim; (8) std-only for
proof-phase units.
- **Candidate matrix summary:** Python 4/4 clusters · Rust/Go whole-TUI 1/5 rejected · carve-out
candidates: none found (future markdown parsing would carve as a Rust native unit behind the
adapter, not now).
- **Kill tests (frozen in synthesis.md):** stay-Python — killed if >16 ms UI-thread stalls under
heavy event bursts OR IPC latency dominates message round-trip (Textual pilot measurement);
Rust recode — killed if the adapter must expose >~15 first-party modules (count today ≈80:
killed NOW); Go recode — additionally killed by toolchain absence (`command -v go` empty).
- **Sources + lessons:** 8 live-fetched 2026-09-29 (Textual workers guide, ratatui vendor docs,
Bubble Tea/tview/Codex countervailing evidence, JSON Lines spec, push-granular-work guidance) —
annotations in `research/wave1/tui/outward-sources.md`.
- **Evidence sufficiency (honest):** full inward read of all 5 TUI files with line refs; ≥2 sources
per major claim; gap = no runtime profiling (stall kill test unmeasured — but no mechanism exists
for the stall to occur; the 120 ms 21-step pipeline was already removed, `_run_warm_router`
:3575 is a no-op).
## W2 — Leaf utilities & canaries (~40 modules, low coupling: pins, time_awareness, message_queue, event_envelope, attention_capsule, complexity_scorer, token_optimizer, cache 501/17, prompt_sync, chat_bubble, ~15 canary_* modules)
- **Research round actor:** general-6 (COMPLETED 2026-09-29 — full report in actor notification)
- **Intention profile:** small pure helpers, envelopes, scoring, cache; smallest blast radius; the
factory + interop proving ground at real scale.
- **Ground-truth measurements (local, this session):** Python fn call ~46 ns (100k timed
iterations in-process) · native CLI roundtrip ~658 µs (subprocess to
`interop/rebase-proof/target/release/rebase-proof --version`) · Python startup floor ~18 ms ·
overhead ratio ≈ **14,000×**. The only CI-proven interop contract is the subprocess CLI boundary
(`shared/native_units.py`: argv in → JSON stdout, `--version`, timeout, typed `NativeResult`).
PyO3/in-process bindings excluded by Wave 0 policy. Therefore every per-call crossing costs
≥658 µs — a per-turn helper would need to run >14,000× faster in Rust to break even.
- **Candidate matrix (representative — full per-cluster 0–2 scoring in round report):**
Python 12/12 in every cluster · Rust 5–6/12 · Go 1–3/12 (D5=0, not installed) · C 1–2/12.
No module contains a hot loop (heaviest: 2 `re.sub` per turn in token_optimizer).
- **Decision:** **KEEP-PYTHON (all ~40, zero RECODE).** Cluster verdicts: (a) pure CRUD helpers
pins/time_awareness/token_optimizer — KEEP, CPython 3.11+ already 10–60% faster on this shape
(PEP 659); (b) message/envelope/capsule structures — KEEP, recode would insert a boundary
between the chattiest neighbors; (c) cache.py (501 L, largest in wave) — KEEP, the fast part is
already C (sqlite3 DB-API 2.0 over SQLite's C library), Python is thin orchestration, batching
(threshold 50 / 30 s) already amortizes writes; (d) chat_bubble.py — KEEP, **hard-pinned** to
Python by the Textual framework (recode = full framework abandonment, bottleneck is human
typing speed); (e) prompt_sync.py glue — KEEP, glue into Python siblings (memory_env,
voice_sync, build_pipeline_policy); (f) ~19 canary_* — KEEP **categorically**, a canary that
verifies Python importability cannot exist in another language.
- **Kill tests (reopening criteria, all fail today on first cheap measurement):** Rust — microbench
one per-turn helper behind the native_units CLI, must beat the Python in-process call >10×
end-to-end including the 658 µs crossing (whole call <~46 µs; a trivial `--version` already
costs 658 µs); Go — identical benchmark post-install, uninstall if roundtrip >100 µs with no
named hot path; C — identical benchmark + required fuzz-clean JSON roundtrip, plus a
demonstrated GC-free allocation-hot loop first; cache.py — recode only if Python-side
orchestration (excl. sqlite3 waits) exceeds 5% of end-to-end latency; capsule/envelope/queue —
recode only if one turn's Python total exceeds ~5 ms AND a candidate's CLI path is >10× cheaper.
- **Sources + lessons:** (1) https://docs.python.org/3/whatsnew/3.11.html — 10–60% faster,
PEP 659 specializing interpreter: CPython already optimized for helper-shaped code, the D2
baseline is a moving improving target; (2) https://docs.python.org/3/whatsnew/3.13.html —
free-threading/JIT experimental, KEEP-PYTHON ≠ frozen but no bets on experimental paths;
(3) https://www.maturin.rs/ — in-process Rust↔Python needs a whole binding/packaging toolchain
atop the CLI boundary, D3 worse than measured 658 µs (Wave 0 policy excludes it anyway);
(4) https://go.dev/doc/install — Go is a pre-install prerequisite, D5=0 everywhere, never earned
installation; (5) https://textual.textualize.io/ — Textual is a Python TUI framework,
chat_bubble.py framework-pinned; (6) https://docs.python.org/3/library/sqlite3.html — sqlite3 is
DB-API 2.0 over C, cache.py's perf layer already native. All fetched live 2026-09-29.
- **⚠ Inventory drift (VERIFIED 2026-09-29):** complexity_scorer.py listed in
rebase-inventory.json (2 mentions) but absent on disk — deleted in commit 867321e
("governed retirement — complexity_scorer deleted (602 lines)"). Wave 0 inventory needs a
correction pass; the module was scored nowhere (no source to characterize).
---
## Wave-plan consequence (T5 scope, from the verdict above)
With **0 recode units awarded**, the tranche's hypothesized rebase volume is real only as the
work below. Waves 2–9 are **disposition + named improvements, NOT mass recoding**:
**Waves 2–7 — per-issue disposition (the 1,852 `[rebase w{wave}]` issues):**
Each issue receives its tranche §7 disposition — improve / fix / missing-connectors answered
with evidence — plus a language verdict of **KEEP-PYTHON with its rubric evidence line**
(the round section for that subsystem names the claim). Disposition runs leaf-first
(Wave 2 → 7), the canonical suite stays green throughout, and no issue closes without its
evidence line. A recode is off the table unless a frozen kill test is reopened — and then
tranche §6 still governs: **no benchmark win, no recode.** Go is not installed and earned no
installation; Rust stays toolchain-ready but unjustified (standing option behind the
CI-proven adapter).
**Named improvements — the actual rebase volume (ship alongside disposition):**
1. **JSONL streaming reader for `shared/native_units.py`** (W7 contract gap): the adapter
parses only the LAST stdout line; it must gain per-line `json.loads` + terminal-envelope
validation before any recoded long-running unit could stream progress. Small, testable,
non-breaking — first implementation issue of Wave 2.
2. **httpx retry/timeout wrapper for the provider layer** (W4): the honest reliability
upgrade is a feature-add, not a language change.
3. **orjson upgrade path** (W3 cluster 1): the only credible Python-side JSON win —
in-process, no boundary crossing. **DISPOSITION (2026-09-29, kill test EXECUTED and
FAILED — KEEP-PYTHON stands, no dependency declared):** microbench on the live
`~/.rune/consolidation/knowledge.json` (3,540,959 bytes, n=30 best-of): load 13.15 ms
std vs 7.85 ms orjson = **1.68× (bar ≥2×: FAIL)**; dump 9.58 ms std vs 5.61 ms
orjson-with-decode = **1.71× (bar ≥5×: FAIL)**; raw-bytes dump 1.39 ms = 6.91× but is
not drop-in (every `write_text`/`json.dump`-to-file site would need a bytes→str or
binary-write conversion). Roundtrip equality verified. Bench: `/tmp/orjson_bench.py`
shape (uvx --with orjson). The bytes trap measured real: decode() eats ~60% of the
dump win.
4. **complexity_scorer.py inventory correction**: listed in `rebase-inventory.json` but
deleted on disk (commit `867321e`, 602 lines) — Wave 0 inventory needs the correction
pass. **DISPOSITION (2026-09-29, DONE):** inventory regenerated per the script's own
header (never hand-edit) — 177→213 files, 1,852→2,314 functions, 13 dead files
dropped; committed + pushed `5bc837c`. The W2 round's "2 mentions" drift claim
verified FALSE against the live inventory (zero hits).
5. **robfig/cron** (W6): recorded as a best-language footnote only; no action.
**Wave 8 — TUI integration proof:** demonstrate the TUI reading/utilising the mixed-language
surfaces that exist (the `interop/rebase-proof` unit through the upgraded adapter), per the
8-point adapter surface adopted above.
**Wave 9 — close-out audit:** 100% of the 1,852 issues dispositioned, debt-table revision,
version bump.
## Cross-cutting candidates (evaluated once, referenced by every subsystem)
- **Rust (present, 1.98.1):** interop proof unit already CI-proven (std-only, zero crates, `interop/rebase-proof/`, adapter in `shared/native_units.py`, ok = exit 0 AND not timed out).
- **Go (absent):** must earn installation through D1–D5 evidence here before any recode targets it.
- **C (present, GCC 16.2.1):** expected to lose D6 in most subsystems; scored where FFI-adjacent work exists.
- **Python (KEEP baseline):** the default. Every subsystem begins here; evidence must move it.
## Evidence ledger (per-source lessons, all rounds)
Index of per-source evidence. Full annotations live in each round's
`research/wave1/<subsystem>/outward-sources.md`; this ledger records the headline lesson
each source contributed and the rubric claims it supports.
| Round | Sources (live-fetched 2026-09-29) | Headline lessons | Supports |
|---|---|---|---|
| W2 leaf utilities | 6: python.org 3.11 + 3.13 whatsnew, maturin.rs, go.dev/doc/install, textual.textualize.io, docs.python.org sqlite3 | CPython 3.11+ already 10–60% faster on helper-shaped code (PEP 659) — D2 baseline is a moving target; free-threading/JIT experimental, no bets; in-process bindings need a whole toolchain atop the CLI boundary (excluded by Wave 0 policy anyway); Go is pre-install prerequisite, D5=0; Textual framework-pins chat_bubble.py; sqlite3 perf layer already C | W2 KEEP-PYTHON ×~40; the 14,000× crossing-overhead law |
| W3 memory | 6: orjson, sled, sqlite-vec, SQLite FTS5 docs, Go json/v2 rewrite issue, pydantic-core | orjson is the only credible Python-side JSON win (in-process, no boundary); SQLite FTS5/vec0 compute is already C; Go's JSON stack mid-rewrite upstream (versioning risk); pydantic-core shows the Rust-as-wheel pattern, excluded by policy | W3 KEEP-PYTHON 7/7; orjson upgrade path on cluster 1 |
| W4 provider-layer | 8: live-fetched O1–O8 (HTTP/SSE ecosystem, tokio-stack precedents incl. mistral.rs, pkg.go.dev/net/http, ollama Go client/server) | stdlib-only Rust has no realistic HTTP/SSE precedent — the zero-crate contract decides Rust out of all network-wire clusters by construction; Go's stdlib HTTP+NDJSON streaming is capability-proven (its losses here are evidence-absence, not capability-absence); the honest reliability upgrade is a Python httpx retry/timeout wrapper — feature-add, not language change | W4 KEEP-PYTHON 5/5; SSE-cluster Rust kill-by-construction |
| W5 orchestration | 4: pydantic-core, ruff blog, go.dev FAQ, Python 3.14 hashlib docs + 1 LOCAL EXPERIMENT (n=100 adapter benchmark) | adapter round-trips at 0.63ms/op — subprocess-per-op viable for coarse ops, never per-event; Python "slowness" dissolves where compute already runs in C via stdlib; the language question is really "where is the hot loop" (here: nowhere) | W5 KEEP-PYTHON 6/6; K1 kill test already run |
| W6 runtime-core | headline lessons (full set in round outward-sources.md) | Rust std::process lacks built-in child timeout — strictly more code under zero-crate; Go os/exec reaches parity but changes only syntax; Python json/regex C-backed so rewrite swaps C for compiled code at equal asymptotic cost; serde typed structs mismatch schema-flexible model output; concurrency features irrelevant at ≤2 threads | W6 STAY-PYTHON 6/6; robfig/cron as best-language footnote |
| W7 TUI | 8: Textual workers guide, ratatui vendor docs, Bubble Tea/tview/Codex TUI (countervailing), JSON Lines spec, push-granular-work guidance | Textual workers/`call_from_thread` are the core competency for event glue; ratatui admits "little to no help in organizing large applications"; countervailing native-TUI frameworks recorded honestly — do not outweigh 80+ module coupling; JSONL per-line parse + terminal sentinel is the streaming contract; vendor guidance: push granular speed-sensitive work to subcomponents only when measured | W7 KEEP-PYTHON 4/4; the 8-point adapter surface (adopted as the rubric's interop-contract restatement) |
+50
View File
@@ -0,0 +1,50 @@
# Wave 0 close-out report — language-rebasing tranche R1
Branch: `tranche-r1-wave-0` · Date: 2026-09-29
## 1. Inventory (docs/rebase/rebase-inventory.json)
- 177 Python modules, ~52,750 lines, 227 classes
- TRUE unit count: **1,852 = 1,030 top-level functions + 822 class methods**
- Zero duplicate call edges, zero method leaks (generator double-walk defect
root-caused and fixed in scripts/rebase_inventory.py)
- Cross-verified against an independent fresh AST walk: zero missing units;
six inventory-"extras" are real nested/if-try-block classes the naive walk
misses (inventory is MORE complete, not less)
## 2. Issue factory (scripts/rebase_issue_factory.py)
- One issue per unit following R1 §7; wave-batched (2–7); dedup-safe via
deterministic title `[rebase w{wave}] {relpath}::{unit}` + existing-titles
scan per run (retries never duplicate — preserve-first)
- `plan` → {2: 239, 3: 251, 4: 129, 5: 544, 6: 415, 7: 274} = 1,852,
zero unassigned
- `sample --wave 5` → full §7 template holds; `_compute_hash` exactly once as
labelled callee with line number
- Contract fix during Wave 0: `--limit` default changed 5 → 0 (unlimited) for
`create`; `sample` keeps its 5. A full-wave create with the old default
would have silently truncated each wave to five issues.
## 3. Interop proof unit (R1 §5)
- Rust unit at interop/rebase-proof/ (std-only, zero crates): argv in → JSON
on stdout, non-zero exit on failure, mandatory `--version`
- Shell-proven: version flag, JSON out, exit 2 on failure, stderr diagnostic
- Python thin adapter shared/native_units.py: typed NativeResult, timeout +
stderr capture, unit failures as data (never exceptions)
- Adapter contract fix during Wave 0: `ok` no longer requires a JSON
`ok:true` field — plain-text output (`--version`) succeeds on exit code
- tests/test_native_interop_proof.py: **8 passed** — version contract, sum
happy path, hash determinism, failure paths, timeout, stderr capture
## 4. Per-wave issue creation
<!-- FILLED BY THE COMPLETION LOOP — do not hand-edit -->
- Wave 4 (129 units): created=127 skipped(dedup)=2 (includes smoke #417–418)
- Wave 2 (239 units): created=239 skipped(dedup)=0
- Wave 3 (251 units): created=251 skipped(dedup)=0
- Wave 5 (544 units): created=544 skipped(dedup)=0
- Wave 6 (415 units): created=415 skipped(dedup)=0
- Wave 7 (274 units): created=274 skipped(dedup)=0
- TOTAL: 1,850 created + 2 dedup = 1,852 / 1,852 ✅ full coverage
File diff suppressed because it is too large. Load diff
+12
View File
@@ -0,0 +1,12 @@
{
"units_per_wave": {
"2": 239,
"3": 251,
"4": 129,
"5": 544,
"6": 415,
"7": 274
},
"unassigned_modules": {},
"unassigned_module_count": 0
}
+60
View File
@@ -0,0 +1,60 @@
# SUPERSESSION REPORT — programmatic evidence
_Created 2026-10-07. Question: is curator superseded by memory_consolidation? Can supersession be determined programmatically?_
## Answer in one paragraph
No — they are NOT supersessions of each other. They are two different machines in one pipeline: **memory_consolidation compresses what already exists** (warm neurons, graph entities, abstraction facts → consolidated knowledge records), while **curator gates what gets written** (evidence → worthiness filter → graded routing → tiers). Both are wired into sleep_time's nightly run: consolidation runs in-process, curator runs as a subprocess. The real finding is darker: **curator has NEVER successfully run in production** — three of its four dependencies were never ported from the Odysseus branch, so every invocation died at import. Both diagnoses were reached programmatically.
## The programmatic supersession test (reusable)
Six signals, each binary and evidence-based (`/tmp/ss_engine.py`, promote to scripts/):
| Signal | Test | Weight |
|---|---|---|
| R1 Import graph | production importers vs canary-only importers | high |
| R2 Subprocess summon | module executed as a script by another module | high |
| R3 Output artifacts | documented output files exist with recent mtimes | high |
| R4 Function overlap | name overlap with the suspected successor | low (weak signal) |
| R5 Importability | `import X` succeeds (phantom dep = dead) | gate — fails = dead regardless |
| R6 End-to-end probe | documented entry path executes with real input | decisive |
## Applied to curator vs memory_consolidation
| Signal | curator | memory_consolidation |
|---|---|---|
| R1 | 0 production importers | 2 (memory_selector, sleep_time) |
| R2 | summoned as script by sleep_time + dependency_tracker | summoned by dependency_tracker |
| R3 | **index/blocks_meta.json DOES NOT EXIST — no journal files — never wrote output** | graph.sqlite exists but **stale (Aug 22)**; consolidation/*.json never created |
| R4 | 2% overlap — different function sets (confirms: different jobs) | 2% |
| R5 | importable NOW (after repair); dead since #314 | importable |
| R6 | dry-run probe NOW green (post-repair); was JSONDecodeError-crash | consolidate() wired, but its outputs absent too |
## Verdict
1. **NOT a supersession.** 2% function overlap + different roles: curator = write-path gate (worthiness → grade → tier), memory_consolidation = read-path compressor (existing memories → knowledge records). One writes, one compresses. Both belong.
2. **Both are broken in production.** Curator never ran (phantom deps). memory_consolidation's outputs are stale/absent — its last graph write was Aug 22. The whole sleep-time memory pipeline has been quietly degraded.
3. **Disposition: REPAIR, not retire.** grading.py + worthiness.py are now recovered from the Odysseus source branch (provenance, not reconstruction) with frozen tests; curator's dry-run executes end-to-end. Remaining phantom for curator: lexicon_evolution (guarded — degrades), aaak (guarded — optional compression). Both degradable, not blocking.
## The phantom-import census (the general finding)
16 phantom imports found across src/rune. The important ones:
| Module | Importers | Guarded? | Disposition |
|---|---|---|---|
| worthiness | curator | NO (hard crash) | ✅ RECOVERED from Odysseus + frozen tests |
| grading | curator | NO | ✅ RECOVERED from Odysseus + frozen tests |
| lexicon_evolution | curator | yes (degrades) | recover-or-rename decision needed |
| aaak | curator, memory_store, warm_router | yes | optional compression; recover or accept degrade |
| build_pipeline_policy | prompt_sync | NO | phantom — prompt_sync is dead on arrival |
| gitea_auth | forge | NO | forge.py dead; is forge itself superseded? |
| faster_whisper, pynput | voicebox | yes | optional deps — correct as-is |
| research_lens | claim_audit | yes | lives in ~/.config/opencode/scripts — cross-repo dep |
| ep_loader | 5 modules | false positive | resolves via sys.path hack to src/rust/ |
| autonomy_loop, cognitive_loop, dream_cycle | rune_tui | yes | TUI plugins — recover or retire |
| commitments, dedupe_policy, local_memory | warm_router, memory_store, sleep_time | yes | recover-or-accept-degrade |
**Hard-crash phantoms mean the owning module is dead code:** prompt_sync, forge, and (until today) curator.
## What the dictonomy rebase inherits
The Go/Rust port queue should SKIP dead code until its disposition is decided. A module that never ran has no behavioral parity to prove — porting it would faithfully reproduce a ghost. The audit instrument now needs an R5 importability column so dead modules are marked before they're counted.
+2
View File
@@ -9,6 +9,8 @@ new surface._
|---|---|---|
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
| `tui_backlog.py` | Not a surface — the durable TUI Upgrade Backlog ledger (outstanding projection/control work per backend feature) | SCAFFOLD-TRANCHE-007; ledger only, never structural authority |
| `tui_transport.py` | Not a surface — the transport seam for the TUI's operator calls (in-process default / /v1 wire opt-in) | H4 slice 1 (contract BUILDER-2026-10-04T10:32:00Z-cli); RUNE_TUI_TRANSPORT selects; coverage: canary_tui_transport.py |
## Adding a TUI surface
-1
View File
@@ -1 +0,0 @@
def broken(:
+1
View File
@@ -0,0 +1 @@
1.27.1
+39 -2
View File
@@ -1,7 +1,31 @@
settings:
warn_about_similar_files:
above: 0.85
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**"]
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**",
# shared/ is the DEPLOYED-VINTAGE mirror tree (strangler era):
# every src/rune <-> shared similarity here is intentional
# (the shim delegates to the crate; the mirror is the deployed
# vintage it strangles). Registering the deliberate pairs.
"shared/**",
"src/rune/drift_core.py", "src/rune/drift_compression.py",
"src/rune/drift_observability.py", "src/rune/shard_manager.py",
"src/rune/subconscious_pass.py",
# deliberate mirror pairs outside the strangler set:
# issue templates are mirrored into shared/issue_templates;
# completion_file.py is the bridge/daemon mirror of scripts/;
# interop/rebase-proof is a committed proof fixture.
".gitea/ISSUE_TEMPLATE/**", "shared/issue_templates/**",
"bridge/daemon/completion_file.py", "scripts/completion_file.py",
"interop/rebase-proof/**",
# per-crate copies by zero-dep design: each crate is
# self-contained (no shared crate dependency), so the pyjson
# JSON module is copied verbatim per crate and the tiny
# zero-dep lockfiles are near-identical by construction.
"src/rust/*/Cargo.lock", "src/rust/*/src/pyjson.rs",
"src/rust/query_memory/src/json_min.rs",
# store/scaffold is data (proof fixtures, gate objects),
# same class as the already-ignored store/spine.
"store/scaffold/**"]
henxels:
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
@@ -20,7 +44,7 @@ henxels:
- henxel: "Top-level docs are markdown or yaml references"
in: ./docs/*
allowed_filetypes: [.md, .yaml]
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json"]
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json", "./docs/rebase/**"]
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
in: ./src/rune/*
@@ -70,6 +94,19 @@ henxels:
in: ./src/rune/*.py
new_rune_modules_have_canary_or_test_coverage: true
- henxel: "Rust crate files stay under a line budget"
in: ./src/rust/*/src/*.rs
except: ["./src/rust/*/src/pyjson.rs", "./src/rust/*/src/json_min.rs"]
max_lines: 3000
- henxel: "Go modules stay under a line budget"
in: ./src/go/**/*.go
max_lines: 2000
- henxel: "No secrets in Rust or Go source files"
in: [./src/rust/*/src/*.rs, ./src/go/**/*.go]
no_secrets: true
- henxel: "PRDs are living documents — their generated state sections regenerate identically from governed sources"
changed_with:
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "rebase-proof"
version = "0.1.0"
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "rebase-proof"
version = "0.1.0"
edition = "2021"
# Wave 0 interop proof unit (R1 §5). Std-only by design: no crates, no network,
# deterministic build. The pattern under proof is the CLI boundary itself, not the
# computation inside it.
[[bin]]
name = "rebase-proof"
path = "src/main.rs"
[profile.release]
strip = true
+117
View File
@@ -0,0 +1,117 @@
//! Wave 0 interop proof unit (R1 §5 of the language-rebasing tranche).
//!
//! Contract under proof:
//! - standalone binary; argv in, JSON on stdout
//! - non-zero exit on failure (JSON error object still printed to stdout)
//! - `--version` flag mandatory
//! - Python wraps it in a thin adapter (`shared/native_units.py`); tests call
//! it the same way.
//!
//! Ops: sum / echo / hash (fnv1a64) / sleep / fail. All std-only.
use std::env;
use std::process::exit;
use std::thread;
use std::time::Duration;
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// FNV-1a 64-bit — dependency-free deterministic hash for the proof unit.
fn fnv1a64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for byte in data {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
fn print_json(line: &str) {
println!("{line}");
}
fn emit_ok(body: &str) {
print_json(&format!("{{\"ok\":true,{body}}}"));
exit(0);
}
fn emit_err(message: &str) -> ! {
// Escape the minimal set so the JSON stays parseable for arbitrary op names.
let escaped: String = message
.chars()
.map(|c| match c {
'"' => "\\\"".to_string(),
'\\' => "\\\\".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
other => other.to_string(),
})
.collect();
print_json(&format!("{{\"ok\":false,\"error\":\"{escaped}\"}}"));
exit(2);
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
if args.as_slice() == ["--version"] || args.as_slice() == ["-V"] {
println!("rebase-proof {VERSION}");
exit(0);
}
if args.is_empty() {
eprintln!("usage: rebase-proof <op> [args...] | --version");
emit_err("no op given");
}
let op = args[0].as_str();
let rest = &args[1..];
match op {
"sum" => {
let mut total: i128 = 0;
for a in rest {
match a.parse::<i128>() {
Ok(v) => total += v,
Err(_) => emit_err(&format!("sum: not an integer: {a}")),
}
}
emit_ok(&format!("\"op\":\"sum\",\"result\":{total}"));
}
"echo" => {
let joined = rest.join(" ");
let escaped: String = joined
.chars()
.map(|c| match c {
'"' => "\\\"".to_string(),
'\\' => "\\\\".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
other => other.to_string(),
})
.collect();
emit_ok(&format!("\"op\":\"echo\",\"text\":\"{escaped}\""));
}
"hash" => {
let joined = rest.join(" ");
let h = fnv1a64(joined.as_bytes());
emit_ok(&format!("\"op\":\"hash\",\"fnv1a64\":\"{h:016x}\""));
}
"sleep" => {
let ms: u64 = match rest.first().map(|s| s.parse::<u64>()) {
Some(Ok(v)) => v,
_ => emit_err("sleep: expected milliseconds"),
};
thread::sleep(Duration::from_millis(ms));
emit_ok(&format!("\"op\":\"sleep\",\"slept_ms\":{ms}"));
}
"fail" => {
// Deliberate failure: stderr diagnostic + JSON error + exit 2.
// Proves stderr capture and non-zero exit in the adapter.
eprintln!("rebase-proof: deliberate failure for adapter proof");
emit_err("deliberate failure");
}
other => emit_err(&format!("unknown op: {other}")),
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
{
"BLE001": 352,
"S110": 80
}
"BLE001": 345,
"S110": 75
}
+401 -1
View File
@@ -1 +1,401 @@
[]
[
{
"name": "read_file",
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
"capabilities": [
"read_file"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"path exists and is a readable file"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "read_file read a file with optional windowed viewing (offset/limit). returns numbered lines. use offset/limit for large files (100 lines at a time). native tool"
},
{
"name": "edit_file",
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
"capabilities": [
"edit_file"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "write",
"preconditions": [
"path is inside an allowed write directory",
"search string matches exactly once"
],
"side_effects": [
"rewrites the target file atomically after approval",
"creates a .bak backup under ~/.rune/undo"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "edit_file surgical search/replace edit. more reliable than full file rewrites. the search string must match exactly. native tool"
},
{
"name": "write_file",
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
"capabilities": [
"write_file"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "write",
"preconditions": [
"path is inside an allowed write directory"
],
"side_effects": [
"writes the full file content atomically after approval",
"creates a .bak backup under ~/.rune/undo"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "write_file write content to a file. creates parent directories if needed. shows diff before writing. use for new files or full rewrites. native tool"
},
{
"name": "glob_files",
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
"capabilities": [
"glob_files"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"search directory is readable"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "glob_files find files matching a glob pattern. returns file paths sorted by modification time. native tool"
},
{
"name": "grep_files",
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
"capabilities": [
"grep_files"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"search directory is readable"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "grep_files search for a pattern in files. returns file paths and line numbers. use include to filter by file type. native tool"
},
{
"name": "list_directory",
"description": "List files and directories at a path. Returns names with / suffix for directories.",
"capabilities": [
"list_directory"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"path is a readable directory"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "list_directory list files and directories at a path. returns names with / suffix for directories. native tool"
},
{
"name": "lint_file",
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
"capabilities": [
"lint_file"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"file exists",
"a linter is configured for the language"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "lint_file lint a file for syntax errors and style issues. blocks bad edits. supports python (ruff), js/ts (eslint), go (golangci-lint), and more. native tool"
},
{
"name": "run_tests",
"description": "Run tests for a file or project. Auto-detects test runner from language.",
"capabilities": [
"run_tests"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "write",
"preconditions": [
"a test runner is configured for the project"
],
"side_effects": [
"runs project test commands which may create or delete files"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "run_tests run tests for a file or project. auto-detects test runner from language. native tool"
},
{
"name": "format_code",
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
"capabilities": [
"format_code"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "write",
"preconditions": [
"path is inside an allowed write directory",
"a formatter is configured for the language"
],
"side_effects": [
"rewrites files in place to the formatter's style"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "format_code auto-format code files. supports python (black/ruff), js/ts (prettier), go (gofmt), and more. native tool"
},
{
"name": "git_diff",
"description": "Show git diff for staged or unstaged changes.",
"capabilities": [
"git_diff"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"working directory is a git repository"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "git_diff show git diff for staged or unstaged changes. native tool"
},
{
"name": "git_log",
"description": "Show recent git commits.",
"capabilities": [
"git_log"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"working directory is a git repository"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "git_log show recent git commits. native tool"
},
{
"name": "git_commit",
"description": "Commit changes with a message. Auto-stages modified files.",
"capabilities": [
"git_commit"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "irreversible",
"preconditions": [
"working directory is a git repository",
"there are changes to commit"
],
"side_effects": [
"creates a commit on the current branch"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "git_commit commit changes with a message. auto-stages modified files. native tool"
},
{
"name": "repo_map",
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
"capabilities": [
"repo_map"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"target directory is readable"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "repo_map generate an ast-based codebase map showing key symbols (classes, functions, imports). uses tree-sitter for 100+ languages. native tool"
},
{
"name": "run_command",
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
"capabilities": [
"run_command"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "irreversible",
"preconditions": [
"operator approves the command",
"required binaries are on PATH"
],
"side_effects": [
"executes arbitrary commands with their full side effects"
],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "run_command run an approved command. prefer argv (shell=false); shell syntax requires shell=true and explicit user approval. native tool"
},
{
"name": "read_persona",
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
"capabilities": [
"read_persona"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"persona MEMORY.md exists"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "read_persona read rune's persona definition. returns the full persona block with voice, identity, and rules. native tool"
},
{
"name": "query_memory",
"description": "Query Rune's memory store. Returns relevant memories for a query.",
"capabilities": [
"query_memory"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"memory store database is available"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "query_memory query rune's memory store. returns relevant memories for a query. native tool"
},
{
"name": "check_growth",
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
"capabilities": [
"check_growth"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"growth log file exists"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "check_growth check rune's growth log. returns recent growth entries and patterns. native tool"
},
{
"name": "diagnose_behavior",
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
"capabilities": [
"diagnose_behavior"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"recent response text provided"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "diagnose_behavior diagnose rune's current behavior. returns analysis of persona adherence, voice consistency, and drift. native tool"
},
{
"name": "find_capability",
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
"capabilities": [
"find_capability"
],
"file": "tool_executor.py",
"full_path": "",
"priority": 100,
"semantic_examples": [],
"risk": "read-only",
"preconditions": [
"a capability query describing the needed outcome"
],
"side_effects": [],
"auto_invoked": false,
"hooks": [],
"native_tool": true,
"searchable_text": "find_capability search all installed capabilities for what you need. returns matching tool names with descriptions and argument schemas. use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session. native tool"
}
]
+38886 -15
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,48 @@
# Phase G slice 1 — retrieval-index authority supersession ledger
**Date:** 2026-10-02 · **Branch:** `phase-g-strangler-retrieval-index` · **Unit issue:** conductor/rune#2448
## What was superseded
The pure-Python `query_memory` implementation (formerly `src/rune/query_memory.py`,
deployed at `~/.config/opencode/scripts/query_memory.py`) is RETIRED as a production
implementation. Production authority now rests with:
1. **Implementation:** Rust crate `src/rust/query_memory/` (lib.rs core + cdylib, ten
`extern "C"` FFI symbols, zero external crates).
2. **Production service surface:** `shared/query_memory.py` — a ctypes-backed shim
(179 lines) over `qm_loader.py`, byte-deployed to `~/.config/opencode/scripts/query_memory.py`,
serving the FULL prior Python class API including the `self.history` snapshot.
## Strangler doctrine (master doc lines 685–770) — step discharge
| Step | Requirement | Evidence |
|------|-------------|----------|
| 1–4 | candidate built, contract frozen, parity proven | commits `9b76585`+`54d2db9`; `/tmp/phaseG_retrieval_index_contract.md` frozen; parity harness ALL GREEN |
| 5 | qualification | `/tmp/phaseG_qual_bench.out` (Rust CLI ~45× faster end-to-end); canaries both sides |
| 6 | canaries conformance | Python canary 12/12; Rust canary 27/27 |
| 7 | shadow comparison vs live store | `/tmp/phaseG_shadow.py` 46/46 exact-equal vs 77-record production store |
| 8 | authority switch + observed consumer evidence | commit `6f44a87`; both consumers verified live — `memory_store.py` (lines 820–821, live 76-record store, nested stats shape) + `growth_simulator.py` (record_query through Rust core); #2448 comment id 33795 |
| 9 | runtime evidence post-switch | post-switch verification wall ALL GREEN: Python canary 12/12, FFI smoke 41/41, Rust canary 27/27, parity harness ALL GREEN |
| 10 | old production path unreachable | ALL import paths (`import query_memory`) now resolve to the deployed shim; `shared/graph_shard_bridge.py` tolerant import serves the shim; the orphan `src/rune/query_memory.py` had ZERO importers (verified by sweep incl. path-string references) |
| 11 | old implementation removed or reference-only | `src/rune/query_memory.py` DELETED (this commit); the only stale reference (`scripts/structural_coverage_audit.py` registry mapping shared/ as a mirror of src/rune) corrected — query_memory removed from `SRC_A`/`SHARED_D`, added to `SHARED_B` (canonical, ops-harness owned) |
| 12 | supersession ledger | THIS FILE |
Deletion discipline (language-governance rule ⑥) satisfied: old code removed only after
the new implementation was PROVEN — wall green, shadow 46/46, both production consumers
observed live on the Rust core before deletion.
## Consumer map at supersession
- `~/.config/opencode/scripts/memory_store.py` — in-process, `adaptive_exploration_rate` (line 820).
- `~/.config/opencode/scripts/growth_simulator.py` — in-process, `record_query`.
- `shared/graph_shard_bridge.py` — tolerant import (try/except), follows the switch.
- `src/rune/tool_executor.py` — NOT a consumer (nominal grep hit only; goes through `memory_store.cached_recall`).
## Verification commands (reproducible)
- `/home/nick/.venvs/memory/bin/python3 src/rune/canary_query_memory.py` → 12/12
- `/home/nick/.venvs/memory/bin/python3 src/rust/query_memory/canary_rust.py` → 27/27
- `/home/nick/.venvs/memory/bin/python3 src/rune/ffi_query_memory_smoke.py` → 41/41
- `/home/nick/.venvs/memory/bin/python3 /tmp/phaseG_parity.py` → ALL GREEN
- `/home/nick/.venvs/memory/bin/python3 scripts/structural_coverage_audit.py` → exit 0, coverage 100%
@@ -0,0 +1,66 @@
# Phase G Slice 2 — FROZEN CONTRACT: evidence-provenance (T7.3)
Frozen: 2026-10-03, session ses_ffe5f08a7892affeGzZWhhm0wR. Branch `phase-g-strangler-evidence-provenance` (cut from slice-1 head `2ac2d0a`). Gitea issue #2449. Nothing below may change without a recorded amendment in this file.
## 1. Scope (observed, not inferred)
Six modules in `src/rune/`, 1796 lines total, one test file each as marked:
| Module | Lines | Surface (grep-verified 2026-10-03) |
|---|---|---|
| `audit_ledger.py` | 231 | `_compute_hash(prev_hash, entry)`, `append_entry(entry_type, entry_id, details)`, `verify_chain() -> {valid, entries, reason}`, `get_entries(limit=50)`, `log_promotion(entry_id, old_status, new_status, wash_count, reason)`, `log_gate_result(entry_id, verdict, critics)`, `log_drift_alert(alert_type, message, severity)`, `main()` |
| `behavior.py` | 411 | `connect()`, `add_correction(original, corrected, scope_tags=None, domain="", pattern="")`, `add_habit(trigger, action, name="", context_tags=None, min_activations=3)`, `match_context(context, match_type=None)`, `reinforce(item_id, outcome, item_type="habit")`, `list_items(item_type="all", limit=20)`, `behavior_stats()`, `main()`, `PLUGIN` dict |
| `behaviour_ledger.py` | 251 | 9 state constants (UNVALIDATED…NOT_APPLICABLE) + `STATES` + `CONFIRMED_STATES` tuples, `BehaviourLedger` class |
| `claim_audit.py` | 430 | `PATTERNS` list, `EVIDENCE_TAG` regex, `LEDGER_MARKERS`, `reframe(text)`, `scan(text)`, `verify(claim)`, `degrade(claim, verdict)`, `journal(entry)`, `report()`, `audit(draft)`, `handle_hook(event, data=None)`, `main()`, `PLUGIN` |
| `evidence_probe.py` | 214 | `PROBE_STATUSES`, `PROBE_FIELDS`, `MAX_PROBES_PER_TASK=2`, `ProbeBudgetExceeded`, `ProbeResult`, `build_probe_prompt(question, artifact_path)`, `validate_probe(artifact)`, `count_probes(artifact_dir)`, `check_budget(artifact_dir)`, `record_probe(result, artifact_dir)`, `run_probe(question, artifact_dir, runner=None, model="UNKNOWN")` |
| `verification_layer.py` | 259 | `VerificationResult`, seven `verify_*` functions (exit_code, unit_test, schema, file_exists, file_changed, tool_receipt, constraint_check), `verify(result, verifier_type)`, `handle_hook`, `PLUGIN` |
Plus `shared/audit_ledger.py` (210 ln) — OLDER vintage of audit_ledger.
## 2. Authority decisions (per module)
- **src/rune/ is the authority vintage for all six modules** — hardened newer code (memory_env-pathed storage, defensive handling). The deployed `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` copies are OLDER vintages (hardcoded expanduser paths). The authority switch is a VINTAGE UPGRADE plus a language swap — not a mirror replacement.
- `shared/audit_ledger.py` is superseded alongside the deployed copy (same older vintage, same defects).
## 3. Live-data contract (the linchpin — observed)
- **Live ledger:** `/home/nick/.config/opencode/memory/promotion_ledger.jsonl` (21 entries). `verify_chain()` under the deployed module's own defaults returns `{'valid': True, 'entries': 21, 'reason': 'chain intact'}` — OBSERVED 2026-10-03.
- **Live signing key: `rune-drift-integrity-2024`** (the deployed/shared default). The src/rune default `rune-governed-adaptation-2024` FAILS the live chain — it is a latent break, recorded as a defect this slice fixes.
- **Hash scheme (both vintages identical):** `HMAC-SHA256(key, (prev_hash + json.dumps(entry_without_hash, sort_keys=True)).encode()).hexdigest()`; genesis prev_hash = `"genesis"`; entry lines written with `json.dumps(entry)` in insertion order (type, entry_id, details, timestamp, prev_hash, hash); timestamp = `datetime.now(timezone.utc).isoformat()`.
- **Key rule for the Rust candidate:** read `AUDIT_LEDGER_KEY` env; DEFAULT = `rune-drift-integrity-2024` (the key that verifies the live chain). Never default to the governed key — it verifies nothing that exists.
- **Live `behavior.db`:** SQLite at memory_env memory dir, touched Sep 24 — must remain readable by the Rust candidate's storage layer.
## 4. Rust candidate design
Crate `/mnt/workdrive/rune/src/rust/evidence_provenance/` following the proven slice-1 shape: zero-dep core in `lib.rs`, `cdylib` FFI exports, hand-rolled JSON serialization where byte-parity demands it. **Port order within the slice** (dependency + risk order):
1. **audit_ledger** — linchpin: live data, external consumers, key contract.
2. **verification_layer** — pure logic, no storage, one test file; fastest port.
3. **evidence_probe** — file-based budget/records; `rune_operator.py:581` consumer.
4. **behaviour_ledger** — state machine, src/rune-only.
5. **behavior** — SQLite habits; live DB; deployed vintage upgrade + `rune_tui.py` consumer.
6. **claim_audit** — heaviest (warm-neuron store + graph DB + regex engine); last.
Python-side ctypes loaders (`*_loader.py`) + deployed shims per slice-1 pattern, so every consumer keeps its import surface unchanged.
## 5. Parity + qualification criteria
- **Byte-parity:** Rust `verify_chain` output byte-identical to Python's on the live ledger AND on a mutation battery (tampered entry, broken chain, invalid JSON, empty ledger). Parity harness both sides, slice-1 pattern.
- **Canaries:** Python-side canary module per ported module + Rust canary; both green before any switch.
- **Shadow:** shadow-compare Rust vs Python on live-shaped data before authority switch.
- **Performance:** like-for-like benchmark on the real consumption shape; no regression (Nick's law: combination does not decrease speed).
## 6. Consumers + adoption plan (Nick's directive: adoption during the refactor)
| Consumer | Imports | Switch |
|---|---|---|
| `~/.config/opencode/scripts/curator.py` | audit_ledger | at audit_ledger switch |
| `canary_tui_drift.py`, `canary_full_system.py` | audit_ledger | at audit_ledger switch |
| `rune_tui.py` (L62, L83) | claim_audit, behavior | at each module's switch |
| `rune_operator.py:581` | evidence_probe (relative) | at evidence_probe switch |
Deployed copies become ctypes shims at each module's authority switch (vintage upgrade included — the shim serves the src/rune contract, not the old vintage's). Old deployed Python deletions follow language-governance ⑥: only after the new is proven.
## 7. Exit criteria
All twelve doctrine steps discharged per module-port with observed evidence on #2449; coverage-audit registry remapped; supersession ledger written; audit regression gate green.
@@ -0,0 +1,62 @@
# Phase G slice 2 — evidence-provenance authority supersession ledger
**Date:** 2026-10-03 · **Branch:** `phase-g-strangler-evidence-provenance` · **Unit issue:** conductor/rune#2449
## What was superseded
The pure-Python evidence-provenance implementations (six modules in `src/rune/`, 1796 lines; plus the two OLD deployed vintages and `shared/audit_ledger.py`) are RETIRED as production implementations for their ported surfaces. Production authority now rests with:
1. **Rust crate `src/rust/evidence_provenance/`** — six cores: `lib.rs` (audit_ledger chain + FFI), `verification.rs` (7 verifiers), `probe.rs` (schema gate + budget), `behaviour.rs` (state machine), `habitmath.rs` (reinforce math), `claims.rs` (claim-audit decision skeleton); cdylib FFI (`ep_*` symbols, 21 total), zero external crates.
2. **Python shims** at `src/rune/{audit_ledger,verification_layer,evidence_probe,behaviour_ledger,behavior,claim_audit}.py` — serving the src/rune authority contract through `ep_loader.py` (in-process, no interpreter startup for the ported logic).
3. **Deployed shims** at `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` — same shape; audit_ledger + claim_audit + behavior carry the LIVE data-path rules (drift-key ledger, live behavior.db, real journal); claim_audit additionally carries the preserved SYMMETRY GUARD (deployed-vintage feature) and the repaired `scan_text()` TUI surface.
## Port ledger (all six, in contract §4 order)
| Port | Module | Rust core | Parity | Commit | Consumer proof |
|------|--------|-----------|--------|--------|----------------|
| 1 | audit_ledger | lib.rs chain ops | shadow 18/18; canaries 28/28+23/23 | `114c394`+`4b0d1d2` | live ledger entries 4242 (post-switch write), verify 23-intact |
| 2 | verification_layer | verification.rs | 33/33 (7 verifiers + CLI render) | `9cb93e6` | contracts.py, canary_native, rune_operator:323 dataclass |
| 3 | evidence_probe | probe.rs | 24/24 + consumer tests 23/23 | `3450a1c` | rune_operator:581/603, research_stage_executor:312 |
| 4 | behaviour_ledger | behaviour.rs | 13/13 full-scenario | `8973caa` | testing_mode:19, iteration:23, rune_tui:3152 |
| 5 | behavior | habitmath.rs | 16/16 + full-stack live-db | `2732677` | rune_tui:83; deployed vintage upgraded (live db read-verified) |
| 6 | claim_audit | claims.rs | 17/17 + real-journal aggregate (1127 rows) | `969e39f` | rune_tui:62/2468/3752, contracts.py, hook_engine:270 |
Errors: byte-equal (duplicate/unknown-behaviour/unknown-state; probe schema-gate lists incl. Python repr rendering; CLI argparse shapes + exit codes).
## Strangler doctrine (master doc lines 685–770) — step discharge (slice level)
| Step | Requirement | Evidence |
|------|-------------|----------|
| 1–4 | candidate built, contract frozen, parity proven | commits `114c394` → `969e39f`; frozen contract `plans/2026-10-03-phase-g-evidence-provenance-contract.md` (commit `7c68e41`); per-port parity harnesses ALL GREEN |
| 5 | qualification | per-port parity + decision-math batteries; canary pair per port (Python 74/74, Rust 66/66 — all six port sections accumulated); performance follows the slice-1 in-process shape (no interpreter startup; same cdylib) |
| 6 | canaries conformance | canary_audit_ledger.py 74/74; canary_evidence_provenance_rust.py 66/66 (per-port sections for ALL six ports; dual-mode LIVE/ISOLATED for live-ledger pins — run 18141 repair); embedded tests through shims (verification test, 23 consumer tests) |
| 7 | shadow comparison vs live-shaped data | port-1 shadow 18/18 vs `939883d:shared/audit_ledger.py` (banked at `114c394`); ports 2–6 use direct byte-parity batteries vs the live originals (same discipline, stronger: full-document byte-quality) |
| 8 | authority switch + observed consumer evidence | shims deployed + live-verified per port: entry 4242 (first post-switch ledger write), live behavior.db read through the port-5 shim, real 1127-row journal through the port-6 aggregation, TUI backend trio imports, TUI scan_text FIRST-EVER live call |
| 9 | runtime evidence post-switch | post-switch walls per port: parity round-trips, embedded tests, consumer chains (rune.behaviour_ledger / rune.evidence_probe / rune.claim_audit + deployed trio) — all GREEN |
| 10 | old production path unreachable | `import audit_ledger` (deployed consumers) → shim; `rune.behaviour_ledger` / `rune.evidence_probe` / `rune.claim_audit` / `rune.verification_layer` → shims; `shared/audit_ledger.py` had ZERO importers (verified by sweep incl. path-string forms) — DELETED |
| 11 | old implementation removed or reference-only | `shared/audit_ledger.py` DELETED (this commit, after the proven wall + zero-importer sweep); six src/rune modules remain as shims (the API contract); original Python lives in git history. Coverage registry remapped: `audit_ledger` removed from SHARED_D; gate exit 0 post-deletion |
| 12 | supersession ledger | THIS FILE |
Deletion discipline (language-governance rule ⑥): `shared/audit_ledger.py` removed only after (a) the deployed shim proved live against the real ledger, (b) the shadow wall was green, (c) a zero-importer sweep across src/ shared/ scripts/ tests/. The remaining five src/rune originals were rewritten AS shims in their own commits (git history holds each original); they are the API contract and stay.
## Vintage-merge disclosures
- **SYMMETRY GUARD preserved:** the deployed `claim_audit` vintage carried `research_lens.symmetry_check` integration (2026-08-15) absent from src/rune — preserved in both shims (feature-preserving merge; load-bearing behavior).
- **DEAD CONSUMER REPAIRED:** `rune_tui.py:3752` `claim_audit.scan_text()` had NO implementation in either vintage (silent try/except death) — now implemented, first-class, live-verified.
- **Sequence break (port 1):** the audit_ledger shim was deployed BEFORE its canary pair existed; both canaries subsequently written and green — disclosed on #2449 comment 33817, not retrofitted.
## Consumer map at supersession
- `~/.config/opencode/scripts/rune_tui.py` — claim_audit (report L2468, scan_text L3752), behaviour (L3152), behavior (L83).
- `src/rune/rune_operator.py` — evidence_probe (L581/603), VerificationResult dataclass (L323).
- `src/rune/contracts.py` — dynamic `__import__` surfaces (verification_layer, claim_audit).
- `src/rune/testing_mode.py:19` / `src/rune/iteration.py:23` — behaviour_ledger constants + class.
- `scripts/research_stage_executor.py:312` — evidence_probe orchestration.
- `~/.config/opencode/scripts/curator.py` / `canary_full_system.py` — audit_ledger (deployed shim).
- `src/rune/dependency_tracker.py` — map-strings only (NOT an import).
## Live-data rules (contract §3) — honored in the shims
- Live promotion ledger: drift key verified 23-intact through the shim + FFI; explicit `AUDIT_LEDGER_KEY` override honored.
- Live behavior.db: read/written through the deployed shim at `~/.config/opencode/memory/behavior.db`; parity scenarios ran on temp dirs only.
- Live claim_audit journal (1127 rows): read-only aggregate through the Rust core; writes stay Python.
@@ -0,0 +1,97 @@
# Phase H plan — Go front end / Rust back end (Nick's architecture directive)
Status: PLAN (awaiting Nick's sign-off before any code)
Directive: USER-LOCKED (2026-10-04). Supersedes the KEEP-PYTHON verdicts in
docs/rebase-language-rubric.md wherever they conflict.
## 1. The directive
Go owns the front end: runtime entry points, the first-call surface, the
user-facing interface and its calls. Rust owns the back end: every core
logic module. Python survives only as transitional glue inside a module
until its own Go/Rust surface lands.
## 2. What "front end" means concretely here
From the runtime-audit memory (2026-09-10): the live entry chain is
rune_tui -> send_task -> ContextBroker -> ModelRouter -> local/Cloud
transports. The user-facing calls are: the TUI process itself, the CLI
entry (python -m rune / rune binary), the operator message path, and the
OpenAI-compatible /v1 endpoints MiMoCode serves.
Candidates for the Go first-call layer, in dependency order:
- rune binary (process entry, arg parsing, process lifecycle) — replaces
the Python module-loading bootstrap.
- CLI/IPC command dispatch — the first-call surface that routes a user
command to the right back-end service.
- The OpenAI-compatible HTTP endpoint surface (/v1 chat completions) —
stdlib net/http + NDJSON streaming is exactly the area the rubric called
Go's only capability-proven fit (W4 evidence, unchanged by the directive).
- TUI: NOT in the first Go surface. Textual coupling is deep (80+ modules
per the rubric); the front-end Go layer first exposes the API the TUI
consumes, and the TUI itself migrates later as its own wave.
## 3. What "back end" means concretely here
All pure-logic modules move into the two existing zero-dep crates
(evidence_provenance, query_memory) or a third crate for the remaining
domains. The zero-external-crate discipline stands (the rubric's W4 Rust
kill-rule for network code stays valid INSIDE Rust crates: no network code
in crates, that belongs to the Go layer).
Back-end candidates not yet ported (pure logic, no external deps):
- drift_core, drift_cache, drift_compression (scored math) — memory umbrella
- warm_neuron_store/warm_router scoring paths — memory umbrella
- memory_store consolidation/scoring helpers — memory umbrella
- capability_fit/capability_broker scoring — capability umbrella
- tokenizer/counting helpers in token_optimizer — inference umbrella
Each port follows the slice-2 pipeline: contract, zero-dep Rust, byte-parity
battery vs the Python vintage, dual-mode canary, consumer import chain,
CI green, supersession ledger.
## 4. Sequencing (per wave, each independently mergeable)
H0 — enablement (no behavior change):
- Install Go on the host + the runner user's PATH; pin a version in
requirements-ci-equivalent (a go-version file + CI check).
- CI: add a Go build+vet+test job (empty module, exits 0) so the pipeline
carries Go before any code lands.
H1 — Go first-call shim:
- Go binary `rune` replacing the Python entrypoint's process bootstrap:
arg parsing, env resolution, spawn the Python runtime (transitional).
- Canary: same first-call contract (exit codes, env handoff, TTY attach).
H2 — Go command dispatch + /v1 endpoint:
- Go serves /v1 chat completions + CLI dispatch, calling the Python core
through the same local transport the TUI uses today.
- Parity battery: request/response shapes vs the Python server for the
same canned conversations (byte-level for metadata, streaming NDJSON).
H3 — back-end wave (parallel to H1/H2 where independent):
- Ports listed in section 3, one or two modules per slice, same slice-2
discipline. No port starts without its parity battery written first.
H4 — TUI migration wave (its own contract, later):
- Only after H1-H2 prove the Go front end serves the same surfaces the TUI
consumes. The TUI is the largest single module (4k+ lines, Textual) and
earns its own contract.
## 5. Honest constraints (carried from the rubric, not contradicted)
- Go must be installed first (rubric D5 gate: it failed this before; the
directive now outweighs the gate but installation is still step zero).
- Network/streaming code lives in the Go layer, never in Rust crates
(rubric W4: stdlib-only Rust has no credible HTTP/SSE precedent).
- SQLite/regex/file-IO stay at their current language boundaries per the
zero-dep split law from slice 2 (math Rust, IO at the edges).
- Every surface gets the same proof pipeline as slice 2: observed-not-
invented expectations, byte parity, dual-mode canary, CI green.
## 6. Success measure
The tranche is complete when: the `rune` process starts as a Go binary;
user-facing calls (CLI, /v1, operator path) are served by Go; every core
logic module on the port list is byte-parity Rust; Python remains only in
surfaces explicitly contracted as keep-Python (TUI internals until H4,
test harness, canary frameworks).
+285
View File
@@ -0,0 +1,285 @@
# TOTAL REBASE TRANCHE — Rune to Go/Rust (R2)
**Status:** DESIGN — awaiting Nick's approval gate (TRANCHE-DEPTH TEMPLATE §4)
**Supersedes:** the KEEP-PYTHON rubric verdicts (already superseded by Nick's
Go-front/Rust-back directive) and extends Phase H (H0–H4 slice 1, merged
`693a494`, v0.90.0) from first ports to total rebase.
**Nick's directives (verbatim anchors):**
- "i want the whole codebase rebased in rust and go"
- "everything should initiate with go as the runtime, and reactive adaptable
runtime should be go"
- "the deterministic backend i.e memory storage, architectures and elastic
cognition and everything like that should be in rust"
- "go should be the reactive front end to what interfaces with the user —
not gui — the deliverables and chat interactions as well as how rune
handles thoughts and runtime"
- "the zero dependency rule should be in place but the rebase should rebuild
those aspects into the new architecture and make them operational"
- "the textual tui should be last but everything should be wired into the
textual tui"
- "start with the scaffolding first as everything relies on that, then use
the new rust scaffolding to make sure everything else is built towards
scaffold protocol"
- "the scaffolding function should be a rust execution but go start"
## 1. Grounding summary (measured 2026-10-04, from the spine store + wc)
- 161 modules with source, **64,482 lines** of `src/rune/` Python.
- Draft classification (rule-based, to be function-level verified in Wave 0):
- **RUST (deterministic backend):** 49 modules, 26,772 lines — memory,
drift, warm, capability, token, spine, scaffold, construction_*,
structural_store, curator, consolidation, elastic cognition, ledgers,
audit, evidence, contracts, sleep_time, subconscious, persona_gate,
context_broker, growth.
- **GO (reactive runtime + user-facing):** 10 modules, 7,043 lines —
operator, transports, dispatch, provider registry, adaptive router,
hook engine, local tool runtime, work manager, deploy_qualify.
- **TUI-last:** 4 modules, 4,889 lines — rune_tui (4,041), tui_validator,
tui_backlog, tui_transport.
- **UNCLASSIFIED (Wave 0 must disposition each with evidence):** 73
modules, 21,473 lines — voice_*, tool_executor, model_routing/registry,
event_bus, cron_engine, message_queue, identity_anchors, pins,
stage_guards, tier_manager, witness, etc.
- **Test infra:** 23 canary/test modules, 4,211 lines — rewritten per wave
(Rust canaries port with their surface; Go canaries for the front).
- **Retires:** `__init__`, `__main__`, `memory_env` (bootstrap glue —
subsumed by the Go runtime's env contract).
- Existing Rust (merged, production-proven): 6 zero-dep crates
(query_memory, evidence_provenance, rune_spine, memory_scoring,
capability_scoring, token_counting), 338k bytes, all with parity
batteries. Go: rune-entry (H1 shim + H2 /v1 front door).
- Stragglers from R1 folded in: #2171, #2119 (TUI), #1844 (salience_gate),
#1755 (goal._load), #1738 (failure_auto_filer).
## 2. Objective
Every line of rune's Python runtime is rebased into the dichotomy —
**Go initiates and reacts; Rust determines and stores** — with the
scaffolding protocol rebuilt FIRST (Go start / Rust execution) so every
subsequent conversion is driven, checked, and closed by the new protocol
itself. Python is deleted only after the replacement is parity-proven and
CI-green (strangler exit law). The Textual TUI is last and stays the
integration surface: everything wires into it over the transport seam.
## 3. The hard constraint analysis (zero-dep + total rebase — feasibility)
Nick asked: confirm or deny. **Confirmed possible**, amended per his
critiques (2026-10-04):
- **Hybridized membrane, not a wall (Nick's correction):** Go remains the
reactive initiator, but "Rust never touches a socket" is WITHDRAWN as an
absolute. The languages interoperate where coherence demands: Rust cores
callable from Go (the proven cdylib pattern), Go surfaces invocable from
Rust through defined channels, shared wire protocols. The dichotomy is
the DEFAULT placement rule; hybrid junctions are deliberate, contracted
exceptions — each recorded in the unit's contract.
- **SQLite STAYS (Nick's ruling):** no bespoke storage engine in this
tranche. SQLite remains and is wired into the new architecture — the
Rust side reaches it through system-library FFI (legal under zero-dep:
a system library is not a crate dependency). The bespoke memory-system
redesign is a LATER, separate program.
- **Reactivity/async:** Go goroutines + channels own the event loops;
Rust's deterministic cores stay synchronous behind the membrane.
- **Regex:** hand-rolled matchers per case (proven: pyjson, word-run
scanner, run-collapse). No general regex engine needed.
- **Parity-plus-improvement (Nick's upgrade):** parity is the FLOOR, not
the ceiling. Every unit answers "parity OR measurably better" —
efficiency improvements are in scope per function (Rust zero-copy and
exactness, Go goroutine-native concurrency), bloat from bad syntax is
removed, and functionality is the non-negotiable gate. Improvements
carry measurements; the parity battery proves no behavior regression
either way.
- **TUI wiring is continuous (Nick's correction):** each landed surface
wires toward the Textual TUI over the transport seam as part of its
unit contract — the final TUI wave integrates a system already aimed
at it, not a retrofit.
- **Language intelligence in the protocol (Nick's addition):** the
scaffolding gains a first-class placement decision — for rune, the
Go/Rust dichotomy rule (reactive/initiating → Go; deterministic/
stateful → Rust); for other projects, a language-fit assessment at
construction-classification time (the protocol recommends the right
language for the task's characteristics).
- **Provenance lock (Nick's verification):** the scaffolding being ported
in Wave 1 is the V1-ACCEPTED system per docs/SCAFFOLDING_V1_DEFINITION.md
(T18 §18B; North Star = Nick's verbatim words; 17/17 acceptance gates).
The Wave 1 parity replay against the production contract ledger proves
the rebuild is that design, not a drift.
## 4. Wave structure (each wave independently mergeable, scaffolding-gated)
### Wave 0 — Grounding + inventory verification (the template's bar)
- Function-level mapping of all 161 modules (the spine gives structure;
wave 0 verifies 100% of functions and classifies the 73 UNCLASSIFIED
with evidence).
- Disposition the 5 R1 stragglers.
- Produce the per-unit issue factory input: one issue per function,
each answering improve/fix/missing-connectors with evidence.
- Deliverable: `plans/total-rebase-inventory.md` + issues filed.
- Exit gate: inventory signed off by Nick (the approval gate).
### Wave 1 — The scaffolding engine reborn (Go start / Rust execution)
**Everything depends on this — it goes first, per Nick's directive.**
- The rebuild target is the V1-ACCEPTED system (Nick's design —
docs/SCAFFOLDING_V1_DEFINITION.md, North Star verbatim, 17/17
acceptance gates). The parity replay against the production contract
ledger is the proof the rebuild IS that design.
- Rust crate `scaffold_core`: the construction machinery as pure
deterministic logic — contract lifecycle (draft→approved→building→
verifying→closed), closure classification (correctly_predicted et al.),
scope resolution, legal transitions, the contract store.
Source modules: scaffold.py, construction_interface.py,
construction_decision.py, construction_intent.py, structural_store.py
(parts not already in rune_spine), contracts.py, toolchain.py.
- Go surface `rune-scaffold` (extends rune-entry): the initiation layer —
submit-need/observe/transition/close CLI + /v1 endpoints, delegating to
the Rust core.
- NEW per Nick: the **language-placement decision** lands in the protocol
here — construction classification gains a language-fit assessment
(rune: the Go/Rust dichotomy rule; other projects: characteristic-based
recommendation). Every later contract carries its placement verdict.
- NEW per Nick: parity is the floor — the engine itself is the first
beneficiary of the parity-plus-improvement rule (measured improvements
allowed, functionality preserved, no-regression proven).
- Exit gate: the R2 tranche's own remaining waves are driven THROUGH the
new engine (the protocol builds itself under its own rules).
### Wave 2 — Memory/storage backend (Rust, SQLite retained)
- SQLite is NOT replaced (Nick's ruling). The Rust memory core reaches it
via system-library FFI (zero-dep legal); memory_store.py (2,435 lines)
is decomposed: query paths and consolidation logic into Rust, the SQLite
mechanics behind a thin storage trait that maps to libsqlite3 today and
to the future bespoke engine LATER without consumer churn.
- elastic_cognition, memory_consolidation, sleep_time, curator,
subconscious_pass, drift_* (remaining DB-backed surfaces), journal,
recall_* (recall_router is routing → boundary checked at Wave 0
disposition).
- Data safety gate: checksum-verified read-back of the live store through
the new engine; zero writes until the read path is proven identical.
### Wave 3 — Cognition + architecture engines (Rust)
- The remaining deterministic cores: capability_broker decision logic,
context_broker, persona_gate, growth, identity_anchors, pins,
stage_guards, tier_manager, witness, safety_override, abstraction,
verification surfaces.
- Ports already merged (memory_scoring, capability_scoring,
token_counting, rune_spine) are verified against the new scaffold
protocol and wired into the final architecture.
### Wave 4 — The reactive runtime (Go)
- The operator turn loop (rune_operator.py, 2,697 lines), chat
interactions, deliverables production, model routing at call time
(adaptive_router, model_registry/profiles/routing, provider_*),
tool execution (tool_executor, tool_protocol, local_tool_runtime),
event surfaces (event_bus, message_queue, cron_engine, hook_engine),
voice output chain (voice_*) as deliverable production.
- The /v1 surface deepens to carry the full interaction contract;
NDJSON streaming becomes the standard response shape.
### Wave 5 — Python retirement sweep
- Every Python module whose surfaces have landed is deleted (strangler
exit: parity proven + CI green + deployed one cycle). Shared/ vintage
mirrors retired with them.
- The glue shims (ms_loader, cs_loader, tc_loader, spine_loader) retire
when their consumers no longer speak Python at all.
### Wave 6 — TUI last (but wired continuously)
- Every wave before this one ships its transport-seam wiring toward the
TUI as part of each unit's contract (Nick's correction: natural,
continuous wiring — not a retrofit wave).
- Then the TUI itself: a thin Textual render shell over the wire (all
logic already Go/Rust by then) or a port — disposition decided on the
evidence of everything that came before.
### Wave 7 — Final audit
- Language-bar measurement, the full scaffolding ledger replayed, the
tranche's closure report with per-wave evidence.
## 5. Per-unit discipline (the template, unchanged)
- One issue per function via the issue factory; each answers
improve/fix/missing-connectors with evidence.
- Every unit runs the scaffolding pipeline end-to-end: submit-need →
contract → approval → canary-first (red proven) → implementation →
parity battery vs the Python vintage → closure audit (structural ids,
never file paths — the closure vocabulary law).
- Per-unit decisions on evidence, never pre-decided globally: each
function's Go-vs-Rust placement is assessed by the dichotomy rule with
the measured call graph, not by the draft classification alone.
- Bug-fix contract governs every defect found en route (red test first,
regression frozen).
## 6. Verification gates
- CI green per wave (crate builds, Go vet/test/build, canaries, henxels,
type ratchet until the Python ratchet itself retires in Wave 5).
- Parity batteries are byte-level for deterministic surfaces, shape-level
modulo volatile fields for wire surfaces.
- The scaffolding closure audit for every contract: correctly_predicted
or honestly flagged, never silent.
- Deploy qualification per merge (the pipeline that proved itself today).
### The 90/100 coverage law (Nick, 2026-10-05 — GOVERNING from W3.4 on)
Per Nick: "if it's realistically achievable but aim higher also if that's
realistically achievable. go to 100% where achievable."
- **100% where achievable — the deterministic Rust kernels.** Every kernel
crate (memory_retrieval, cognition_core, scaffold_core, memory_storage,
and every future crate) targets 100% BRANCH coverage via its parity
matrices + the pytest shim battery: every branch has real reachable
semantics, and exhaustive enumeration is exactly what the batteries do.
Achievability is measured per crate at wave close; a kernel below 100%
carries the named unreachable case or it isn't done.
- **≥90% on the changed-lines bar (diff-cover), aiming higher.** The floor
stays a floor; the working standard per slice is 90%+ on changed lines;
whatever a slice achieves beyond 90 becomes the next slice's personal
benchmark (per-unit "aim higher" — the number climbs wave over wave).
The floor ratchet itself moves after consecutive clean waves
(70 → 76 → 84 → 90; each raise is a commit, never a step down).
- **Glue/failure branches: 90%+ with byte-compare proofs.** The stubs and
probe branches that only fire in a broken world are covered by honest
fixtures (real import-failure probes like the from-import resolution
law) — never by fake-triggered pragmas. A branch covered by a lying
fixture is WORSE than uncovered; if it can't be honestly reached, it's
named in the wave's closure report with the reason.
- **Assertion quality is part of the bar.** A test that touches a line
without verifying its OUTPUT doesn't count toward the bar — byte-compare
/ exact-value asserts are the standard; "is not None" assertions reject
at review.
## 7. Risk register
- **SQLite FFI correctness** (Wave 2) — the storage layer is retained but
re-wired; mitigations: read-path proven identical before any write,
checksum-verified read-back of the live store, the storage trait
boundary keeps the future bespoke swap cheap.
- **Hybrid membrane drift** — junctions multiply ad-hoc if ungoverned;
every Go↔Rust interaction point is a contracted, recorded decision in
its unit's contract (the dichotomy is the default; hybrids justify
themselves per unit).
- **Improvement regressions** — parity-plus-improvement invites subtle
behavior changes; every improvement carries a measurement AND the parity
battery stays authoritative for no-regression.
- **Go↔Rust call latency** — measure in Wave 1; the architecture assumes
local transport cost is negligible vs model latency; if measured wrong,
the membrane's transport moves (decision recorded, not improvised).
- **Context-window economics** — 1,852-style issue volumes mean the
pipeline must multiplex (no-wait law) and local models carry routine
units (custodian pattern).
- **Scope creep during Wave 0** — the inventory is measured and frozen
before design; additions go through scope amendments.
## 8. Rollback
- Every wave merges independently; master stays deployable at each
boundary (the strangler's whole point).
- The Python vintage remains until Wave 5 deletion gates pass; a wave
revert = revert the merge commit (deploy qualification re-runs).
- The scaffolding engine's Wave 1 parity replay means even the protocol's
own replacement is reversible against recorded history.
## 9. Approval gate
This tranche does not execute until Nick approves. On approval, Wave 0
starts immediately and runs without consent blockers (standing directive),
with self-verified checkpoints and defect filing.
@@ -0,0 +1,354 @@
# TRANCHE v3: THE FULL DICHOTOMY REBASE — with HOW, with language advantages, with automated verification
Nick's corrections applied:
- Umbrella manifest feeds the spine (not superseded); both need updating
- construction_decision.py itself gets ported (Go start + Rust execution)
- Language advantages are part of every port — improvements where the target language is better
- Automated parity verification BEFORE gutting the Python (hard gate, no manual checking)
- A testing document that proves bigger features work after each wave
- Maximise Bubble Tea addons (research the ecosystem)
- No arbitrary timeframe — it gets done when it gets done
- Every task follows on to the next — never stopping
- CI stacking: keep submitting PRs; address CI issues in batches of 5, not individually
---
## THE CORE MECHANISM: How every module gets replaced
Every module follows the same seven-step process. No exceptions. The process IS the verification.
### Step 1: Understand the Python module
Read every function. Identify: what does it compute (→ Rust), what does it initiate (→ Go), what does it store (→ Rust), what does it route (→ Go). Note every caller. Note every file it reads/writes.
### Step 2: Write the replacement in the target language
For Rust: a `pub fn` in the appropriate crate, with serde-compatible structs.
For Go: a method on a struct in the appropriate `src/go/<name>/` package.
**Language advantage check**: at this step, ask "does Go/Rust do this BETTER than Python did?" If yes, implement the improvement. Examples: Go goroutines replace Python threading for parallel operations. Rust's zero-cost abstractions replace Python duck typing for hot-path scoring. Go's `net/http` replaces Python `urllib` for model connections with connection pooling and keep-alive.
### Step 3: Write the parity test (RED first)
Before touching the Python module, write a test that runs the same input through BOTH the Python oracle and the new Go/Rust implementation, and asserts the outputs are identical. This test must FAIL before the implementation is correct, and PASS once it is. The test is the gate.
### Step 4: Verify parity — the HARD GATE
Run the parity test. If it passes, the replacement is PROVEN to work. Only now can you proceed to Step 5. If it fails, debug the Go/Rust implementation until it passes. **Do not gut the Python until this gate passes.** This is automated — you never have to manually check.
### Step 5: Rewire the Python module as a delegation shim
Replace the Python function body with a call to the Go binary (subprocess) or the Rust kernel (ctypes/cgo). Every existing caller of the Python module keeps working — they don't know the implementation changed.
### Step 6: Register the canary in CI
The parity test from Step 3 is registered in `.gitea/workflows/ci.yml`. Now every future commit is checked against the parity contract automatically.
### Step 7: PR → CI → merge
Push the branch, let CI run. If CI raises issues, DON'T STOP — continue to the next module's port. Address CI issues in batches of 5. The pipeline is proven; it catches things; you fix them in batches.
**The key insight**: Step 4 (the parity gate) is the answer to "how do you confirm replacements working before you gut it?" You don't gut anything until the automated test proves the replacement produces identical output. The test is the contract.
---
## THE TESTING DOCUMENT: proving bigger features after each wave
After each wave completes, a WAVE VERIFICATION DOCUMENT is produced that proves the system works end-to-end. Not unit tests — integration tests.
### What it contains:
1. **The operator turn loop works**: send a message through the Go operator, get a response back, verify the response is correct
2. **Memory operations work**: store a memory, recall it, verify the content matches
3. **Tool execution works**: execute a tool call, verify the output
4. **The TUI renders**: launch the Bubble Tea TUI, verify it displays
5. **The event journal works**: emit an event, read it back
6. **The scaffolding works**: submit a need, verify the contract is created
### How it's generated — BY THE GITEA PIPELINE, not by the agent:
A shell script (`scripts/wave_verify.sh`) is written by the agent. A CI workflow step runs the script after each wave merge. The CI step formats the results as markdown and posts them as a comment on the wave's Gitea epic. The document is permanent — attached to the epic, survives session restarts, reproducible (the commands are in the document), automated (CI runs it, not the agent).
### Implementation:
- `scripts/wave_verify.sh` — the integration verification script
- A CI workflow step (in ci.yml or a new `wave-verify.yml`) that runs the script and posts results to the Gitea epic using the existing GITEA_TOKEN secret
- The same mechanism the merge-robot already uses for PR comments
### What "done" means for a wave:
The wave's Gitea epic has a verification comment showing every integration path PASSING. The agent does not produce testing documents — the pipeline does.
### What "done" means for a wave:
Not just "the code compiles" — the wave verification document shows every integration path working.
---
## WAVE 0: INFRASTRUCTURE (before any porting)
### 0.1 Henxels scope extension
Open `henxels.yaml`. Add scopes for `src/rust/*/src/*.rs` and `src/go/**/*.go`. Same quality gates: max_lines, no_secrets, canary registration, coverage existence. Without this, new Go/Rust files have no governance.
### 0.2 Umbrella manifest extension
The umbrella manifest IS the spine's input for non-module surfaces. Extend it:
- Add the 10 existing Rust crates as `rust_crates` entries
- Add the 2 existing Go modules as `go_surfaces` entries
- Add placeholder entries for the planned Go modules
- Every new Go/Rust file gets tracked, so the spine knows about it
### 0.3 Scaffolding protocol update (construction_decision.py port)
`construction_decision.py` is itself a Python file. Port it:
- **Go start call**: the CLI surface that receives the classification request
- **Rust execution**: the deterministic classification logic (keyword matching, language placement)
- The GO_SIGNALS and RUST_SIGNALS keyword lists are updated to match the corrected dichotomy
- The Python version becomes a delegation shim
- After this port, `rune scaffold classify` routes through Go→Rust, not Python
### 0.4 CI pipeline update
- Build every Rust crate in the unit-tests job
- Build every Go module in a new `go-build` job
- Set CC_RUST_LIB, SC_RUST_LIB, etc. to the CARGO_TARGET_DIR paths
- Add `go vet` and `go test ./...` steps
### 0.5 Gitea epic + MiMoCode tasks
- Post the parent epic on Gitea: the full wave structure as a folded checklist
- Post per-wave sub-epics
- Mirror in MiMoCode's task tool
- Every subsequent task updates both
---
## WAVE 4: GO REACTIVE INFRASTRUCTURE (14 modules)
### event_bus (244 lines) — DONE
Already built as `src/go/event-bus/`. Reads/writes the same events.jsonl.
### event_envelope (303 lines)
Merge into event-bus. The EventEnvelope struct already exists in Go. Add the OrchestrationRecord methods (to_dict, from_dict) as MarshalJSON/UnmarshalJSON. Add the content_hash function.
### message_queue (10KB)
**What it does**: holds approved messages until they're executed. The Python version uses a JSONL file with a mutex.
**Go advantage**: Go channels are designed for this. Use a buffered channel for in-memory queue + a JSONL file for durability. The Go version is concurrent-safe by construction.
**HOW**: Create `src/go/message-queue/`. Define `type Message struct` with the same fields as the Python version. Implement `Enqueue`, `Dequeue`, `GetApproved`, `MarkExecuted`. The Python module becomes a subprocess shim.
**Parity test**: enqueue 10 messages in Python, read them in Go, verify identical.
### hook_engine (28KB)
**What it does**: loads plugin hooks from a config, fires them on events.
**Go advantage**: goroutines enable parallel hook dispatch (Python's GIL prevents this).
**HOW**: Create `src/go/hook-engine/`. `type HookEngine struct { hooks map[string][]HookFunc; mu sync.RWMutex }`. `Fire(name, data)` iterates hooks sequentially; `FireAsync` dispatches each hook in a goroutine and collects errors via channels.
**Language advantage**: parallel hook dispatch — Python can't do this due to the GIL.
### microkernel (14KB)
**What it does**: the plugin lifecycle — register, activate, dispatch.
**Go advantage**: Go interfaces replace Python duck typing with compile-time guarantees.
**HOW**: `type Plugin interface { Name() string; Capabilities() []string; OnMessage(data) error }`. Registry with `Register(p Plugin)`, `Fire(event, data)`. The hook_engine plugs into this.
### subsystem_health (6KB)
**What it does**: tracks failures per subsystem, escalates tiers.
**HOW**: `type SubsystemHealth struct { failures map[string][]Failure }`. RecordFailure, GetHealth. Write to JSON for durability.
### parallel_processor (5KB)
**What it does**: runs operations in parallel.
**Go advantage**: goroutines + sync.WaitGroup. This is what Go was BORN for.
**HOW**: `func ParallelMap[T, R any](items []T, fn func(T) R) []R`. Launch goroutines, collect via channels.
### work_manager (20KB)
**What it does**: manages work dispatch to builders.
**HOW**: Go worker pool with `chan Task`. `type WorkManager struct { workers int; queue chan Task; results chan TaskResult }`. Start, Submit, Stop.
### failure_auto_filer (21KB)
**What it does**: auto-files failures to the forge.
**HOW**: Go struct with spool file + forge client. Same pattern as the existing forge_client.
### cache (16KB) + connection_pool (3KB)
**What they do**: cache responses, pool connections.
**Go advantage**: Go's `sync.Map` and channel-based pools are built for this.
**HOW**: `src/go/cache/` — LRU cache with `sync.RWMutex`. `src/go/connection-pool/` — channel-based pool.
### cron_engine (14KB) + night_mode (14KB)
**What they do**: scheduled tasks, night consolidation.
**Go advantage**: `time.Ticker` + goroutines replace Python's threading.Timer.
**HOW**: `src/go/cron-engine/` — task list with cron expressions. `src/go/night-mode/` — uses the cron engine for scheduled consolidation.
### recall_router (5KB)
**What it does**: routes recall queries to the right memory backend.
**HOW**: `src/go/recall-router/` — routing table with `map[string]RecallFn`.
---
## WAVE 4b: GO TOOL EXECUTION (3 modules)
### tool_executor (73KB) — STARTED
The Go binary already handles read_file, list_directory, glob_files, grep_files, run_command. Add the remaining operations: write_file (with path validation), edit_file (with diff preview), run_tests, lint_file, git_commit, git_diff, git_log, repo_map, and the query/diagnose operations.
**Language advantage**: Go's `os/exec` with `context.Context` for timeouts is cleaner than Python's `subprocess.run(timeout=)`. Go's goroutines enable parallel tool execution (Python serialises them).
### tool_protocol (16KB)
Go types: `ToolRequest`, `ToolEvent`. Validation against a schema registry. The approval boundary stays in the Go operator (not Python).
### local_tool_runtime (35KB)
The Go loop: receive model response → parse tool calls → validate → dispatch to executor → feed results back. `type LocalToolRuntime struct { transport Transport; tools []ToolDef; maxIterations int }`. This is the loop that makes Rune respond to tool results.
---
## WAVE 5a: GO MODEL/PROVIDER (10 modules → 1 binary)
All 10 modules collapse into `src/go/model-connect/`.
**Language advantage**: Go's `net/http` with connection pooling and keep-alive replaces Python's `urllib` (which opens a new connection per request). Go's goroutines enable parallel model calls (Python serialises them). Go's `context.Context` enables timeout propagation through the call chain.
### model_connect (16KB)
`type OllamaClient struct { baseURL string; client *http.Client }`. Chat, Show, Pull, ListModels. Use `net/http` with a shared transport for connection reuse.
### model_routing (16KB) + model_registry (6KB) + model_profiles (16KB)
Go router that scores tasks against model capabilities and routes to the right model. One `ModelRouter` struct replaces three Python files.
### provider_budget (12KB)
Go budget tracker with `sync.Mutex`. Record, GetStatus. Token counting per provider.
### provider_registry (23KB) + standardized_providers (9KB) + provider_context (2KB)
Go `Provider` interface with OllamaProvider and CompatProvider implementations. Registry manages connections.
### tiered_executor (9KB)
Go executor with tier escalation. Execute, Escalate.
---
## WAVE 5b: GO OPERATOR (19 modules → 1 binary)
**rune_operator (126KB)** — the turn loop. This is the hardest port.
**How**: break it into sub-systems. Port each one at a time. After each, run the full test suite.
1. The state management (RuneState, TaskStateRepo, ConversationRepo) → Go structs with JSON persistence
2. The turn loop → Go function: `Respond(channel, message) OperatorResponse`
3. The routing → Go AdaptiveRouter
4. The tool dispatch → Go LocalToolRuntime (already ported in Wave 4b)
5. The memory interaction → Go MemoryStore (Wave 5c) + Rust kernels via cgo
6. The model connection → Go ModelConnect (Wave 5a)
**Language advantage**: Go goroutines replace Python threading for parallel context gathering. Go channels replace Python queues. Go's `context.Context` replaces Python's ad-hoc timeout management. The turn loop becomes truly concurrent — memory recall and model inference can overlap.
**The other 18 modules** are supporting cast: adaptive_router (23KB), build_mode (20KB), builder_bridge (12KB), core_server (9KB), auto_context (8KB), auto_tasks (15KB), event_wait (23KB), evidence_probe (7KB), forge (5KB), forge_client (7KB), goal (14KB), growth_delta (16KB), warm_router (55KB), release_notes (18KB), persona_definition (7KB), voicebox (4KB), voice_injector (3KB). Each follows the same pattern: read Python → write Go → parity test → rewire → commit.
---
## WAVE 5c: GO MEMORY OPERATIONS (8 modules)
### memory_store (99KB) — THE BIGGEST SINGLE PORT
**What it does**: every SELECT, INSERT, UPDATE, DELETE against the memory database.
**Go advantage**: `modernc.org/sqlite` is pure Go — no cgo, no C compiler, cross-compiles trivially. Go's `database/sql` interface with prepared statements is cleaner than Python's raw sqlite3.
**HOW**: `type MemoryStore struct { db *sql.DB }`. Every Python function becomes a Go method. The SQL statements ARE the specification.
**Parity test**: same SQLite file, same queries, byte-identical results. Run after every operation.
### curator (70KB)
Scans the store, identifies conflicts, promotes/demotes. Go state machine.
### sleep_time (38KB)
Idle-period consolidation. Go goroutine on a timer.
### memory_consolidation (31KB)
Calls the Rust kernel (already ported). Go handles the I/O.
### The other 4 (persona_gate, abstraction, witness, stage_guards)
Each follows the same pattern.
---
## WAVE 6: RUST BACKEND (85 modules)
Each Python module's pure functions become `pub fn` in the appropriate crate.
**The pattern for each:**
1. Read the Python module, identify pure functions
2. Write the Rust equivalent in the crate
3. Add an FFI symbol
4. Write a parity test
5. The Python module becomes a thin FFI caller
**The rhythm**: most modules are 3-10KB. At that size, a port takes 15-30 minutes once you're in the rhythm. 85 modules at that pace is 2-3 days of focused work.
**Language advantage**: Rust's type system catches bugs at compile time that Python catches at runtime. Rust's `Result<T, E>` replaces Python's try/except for error handling. Rust's pattern matching replaces Python's if/elif chains. Rust's zero-cost abstractions mean the ports are FASTER than the Python originals.
---
## WAVE 7: BUBBLE TEA TUI (Go)
### Maximising the Bubble Tea ecosystem
Research the full Bubble Tea + Lipgloss + Bubbles ecosystem for every available addon:
**Bubble Tea** (the runtime): Elm architecture, goroutine-safe, tea.Tick for animations, tea.Every for recurring updates, tea.WithAltScreen for fullscreen, tea.WithMouseCellMotion for mouse support.
**Lipgloss** (styling): truecolor (16.7M colors), borders (rounded, thick, double, hidden), padding/margin/width/height, alignment, flex layout, gradients, inline styling, copy/blend styles.
**Bubbles** (pre-built components):
- `spinner`: rotating indicators (line, dot, mini-dot, jump, pulse, points, globe, moon, monkey, meter, hamburger, ellipsis)
- `progress`: animated progress bars with gradient colors
- `textinput`: text input with cursor, placeholder, validation
- `textarea`: multi-line text input
- `table`: sortable, selectable data tables
- `list`: filtered, paginated lists
- `viewport`: scrollable content area
- `paginator`: pagination controls
- `key`: key binding documentation
- `help`: help bubble
- `timer`: countdown timer
- `stopwatch`: elapsed time
**Additional libraries in the ecosystem:**
- `glamour`: markdown rendering with syntax highlighting (for chat messages that contain code)
- `harmonica`: spring animation physics (for smooth pane transitions)
- `bubble-lipgloss-extras`: additional styling utilities
**What the current TUI has that maps:**
- Two-column layout (chat LEFT, task RIGHT) → `lipgloss.JoinHorizontal()` with width ratios
- Chat bubbles with sender name, timestamp, content → custom component with `lipgloss.Style{}.Border(lipgloss.RoundedBorder()).Padding(0, 1)`
- Thinking indicator (spinner) → `bubbles/spinner` with the pulse style
- Tool call display → bordered component with syntax highlighting via `glamour`
- Status bar (version, mode, model, project) → fixed bottom bar via `lipgloss.JoinVertical()`
- Command selector with autocomplete → `bubbles/textinput` + custom suggestion loop
- Work pane (operational index) → side panel with `bubbles/list`
- Scrollback → `bubbles/viewport` with `SetContent()` and `GotoBottom()`
- Keybindings → `tea.KeyMap` with `tea.KeyCtrl1`, `tea.KeyCtrl2`, `tea.KeyCtrl3`
- Streaming text → `tea.Cmd` that sends partial string updates
- Growth/status widgets → `bubbles/table` or custom rendering
**Improvements the language enables:**
- True concurrent streaming: the Go operator can push partial results to the TUI via Go channels while the model is still generating — Python can't do this because the GIL serialises everything
- Sub-millisecond rendering: Go's zero-allocation string building means the TUI renders at 60fps without GC pauses
- Statically linked binary: deploy the TUI as a single file, no Python runtime, no virtual environment, no pip install
---
## CI STACKING PROTOCOL
The CI pipeline is proven — 2,300+ tests, 25 canaries, required checks on master. Use it as a throughput engine:
1. Submit PR after PR after PR. Don't wait for one to merge before starting the next.
2. If CI raises an issue, DON'T STOP the current work. Note it, continue.
3. After every 5 PR submissions, go back and address the accumulated CI issues in one batch.
4. The merge-robot and auto-version handle the merging — you keep producing.
5. If a required check fails on master, that's the one thing that stops the conveyor. Fix it immediately.
**The principle**: CI is the quality gate, not the bottleneck. It runs while you work. Issues get batch-fixed. The conveyor keeps moving.
---
## GITEA EPIC STRUCTURE
The FIRST Gitea task: **Epic: "Full Dichotomy Rebase"** containing:
- [ ] Wave 0: Infrastructure (henxels scope, umbrella manifest, scaffolding protocol, CI)
- [ ] Wave 4: Go reactive infrastructure (14 sub-epics)
- [ ] Wave 4b: Go tool execution (3 sub-epics)
- [ ] Wave 5a: Go model/provider (10 sub-epics, folded into 1 binary)
- [ ] Wave 5b: Go operator (19 sub-epics, folded into 1 binary)
- [ ] Wave 5c: Go memory operations (8 sub-epics)
- [ ] Wave 6a: Rust structural core (30 sub-epics)
- [ ] Wave 6b: Rust cognition (30 sub-epics)
- [ ] Wave 6c: Rust remaining (25 sub-epics)
- [ ] Wave 7: Bubble Tea TUI (7 sub-epics)
Each sub-epic contains the HOW instructions (the seven-step process), the language advantage notes, the parity test spec, and the CI registration step.
MiMoCode tasks mirror the same tree.
---
## WHAT I STILL NEED FROM YOU
1. **The cgo pattern** — Go calls Rust `.so` files via cgo. The Rust crates don't change, only the caller changes from Python-ctypes to Go-cgo. Is this the membrane you want?
2. **The Bubble Tea ecosystem** — I've listed every addon above. Do you want any specific visual features beyond what's listed?
3. **The umbrella answer** — the spine relies on umbrellas (umbrellas are the input, spine is the consumer). Both need updating. Is this how you understood it?
4. **The CI stacking** — submit PRs continuously, batch CI fixes every 5 submissions. Does this match what you described?
5. **Approve to start Wave 0** — henxels scope, umbrella manifest, scaffolding protocol, CI update, Gitea epic. This is the foundation everything else builds on.
@@ -0,0 +1,274 @@
# TRANCHE: THE FULL DICHOTOMY REBASE — every Python module replaced
_Presented for operator approval per TRANCHE-TEMPLATE.md §4. Nick's mandate: "go through the whole codebase and replace it with the dichotomy." Measured: 180 Python files, 68,224 lines. Target: 0 Python logic files (TUI stays last per plan, test infra stays as test infra)._
---
## §0 — GROUNDING (measured, not guessed)
### The honest numbers
| Language | Files | Lines | % | Status |
|---|---|---|---|---|
| Python | 180 | 68,224 | 70% | **THE TARGET — every module ported** |
| Rust | 106 | 27,304 | 28% | 10 crates exist, 85 more modules to port |
| Go | 7 | 1,125 | 1% | 2 modules exist, 55 more to port |
| **Total** | **293** | **96,653** | | |
### The corrected dichotomy (re-classified 2026-10-06, 64ba7cf)
| Side | Modules | Python lines to replace | Role |
|---|---|---|---|
| **GO** | 57 | 32,081 | The reactive shell: initiates, responds, routes, interfaces |
| **RUST** | 85 | 25,218 | The deterministic backend: computes, stores, pure functions |
| **TUI** | 7 | 5,435 | Textual UI — last per plan |
| **TEST** | 29 | 5,397 | Test infrastructure — stays as test infra |
### Existing infrastructure (§0.2 — route through what exists)
- **CI pipeline**: `.gitea/workflows/ci.yml` — lint, unit-tests, go-surface, canary, best-practices, documentation, type-check, gitea-offline, required-checks
- **Deploy pipeline**: `.gitea/workflows/deploy.yml` — event-driven CI qualification, stage/verify/promote
- **Scaffolding**: `construction_interface.py` + `src/go/rune-entry/scaffold.go` — every module's port goes through a construction contract
- **Canaries**: 25 registered in CI, each crate has its own parity battery
- **Umbrella manifest**: `config/umbrellas.json` — 165 Python modules tracked (needs Rust/Go extension)
- **Henxels**: `henxels.yaml` — structural contract, line caps, no-secrets, canary registration (needs Rust/Go scope)
- **Gitea**: conductor/rune, required checks on master, merge-robot, auto-version
### Keep-greens (§0.5 — must stay green through every wave)
- Full test suite: 2,300+ tests, 0 failures
- All 25 registered canaries
- diff-cover ≥70% on changed lines
- Silent-handler ratchet (BLE001 ≤345, S110 ≤75)
- Henxels: all hold
- Lint-baseline: no increase
- Deploy: monotonic promotion, runtime verification
---
## §1 — OBJECTIVE
**Every Python module in `src/rune/` is replaced by its Go or Rust equivalent per the dichotomy. The TUI stays Python until Wave 7. Test infrastructure stays Python. The FFI boundary is the membrane between Go and Rust. Zero Python logic files remain (only the TUI shell and test files).**
Measured end-state: `ls src/rune/*.py | wc -l` returns ≤7 (TUI only) + test files. `src/go/` contains the full reactive runtime. `src/rust/` contains the full deterministic backend.
---
## §2 — WAVE STRUCTURE
### Wave 4: The Go reactive infrastructure (14 modules, ~5,271 lines)
The backbone that everything else routes through. These are the modules that make the system *respond*.
| Module | Python lines | Go target | Notes |
|---|---|---|---|
| event_bus | 244 | **DONE** (src/go/event-bus/) | Same events.jsonl, same envelope |
| event_envelope | 303 | src/go/event-bus/ | Envelope type in the same binary |
| message_queue | 10KB | src/go/message-queue/ | Durable queue |
| hook_engine | 28KB | src/go/hook-engine/ | Plugin dispatch |
| microkernel | 14KB | src/go/microkernel/ | Hook lifecycle |
| subsystem_health | 6KB | src/go/subsystem-health/ | Failure tiers |
| parallel_processor | 5KB | src/go/parallel-processor/ | Goroutines |
| work_manager | 20KB | src/go/work-manager/ | Work dispatch |
| failure_auto_filer | 21KB | src/go/failure-filer/ | Auto-filing |
| cache | 16KB | src/go/cache/ | Connection pooling |
| connection_pool | 3KB | src/go/connection-pool/ | Pooled connections |
| cron_engine | 14KB | src/go/cron-engine/ | Scheduled tasks |
| night_mode | 14KB | src/go/night-mode/ | Night consolidation |
| recall_router | 5KB | src/go/recall-router/ | Memory routing |
**Entry condition**: event_bus already landed. **Exit condition**: all 14 modules exist as Go binaries, Python versions delegate or are deleted, CI go-surface job passes.
### Wave 4b: The Go tool execution layer (3 modules, ~3,079 lines)
| Module | Python lines | Go target |
|---|---|---|
| tool_executor | 73KB | **STARTED** (src/go/tool-executor/) |
| tool_protocol | 16KB | src/go/tool-executor/ |
| local_tool_runtime | 35KB | src/go/tool-executor/ |
**Exit condition**: tool executor handles all operations in Go, Python delegates via subprocess, approval machinery documented.
### Wave 5a: The Go model/provider layer (10 modules, ~3,398 lines)
| Module | Python lines | Go target |
|---|---|---|
| model_connect | 16KB | src/go/model-connect/ |
| model_profiles | 16KB | src/go/model-connect/ |
| model_registry | 6KB | src/go/model-connect/ |
| model_routing | 16KB | src/go/model-connect/ |
| openai_compat_transport | 10KB | src/go/model-connect/ |
| provider_budget | 12KB | src/go/model-connect/ |
| provider_context | 2KB | src/go/model-connect/ |
| provider_registry | 23KB | src/go/model-connect/ |
| standardized_providers | 9KB | src/go/model-connect/ |
| tiered_executor | 9KB | src/go/model-connect/ |
**Exit condition**: all model/provider operations in Go, Ollama + OpenAI-compatible connections working.
### Wave 5b: The Go operator (19 modules, ~9,637 lines)
The biggest wave. The operator turn loop, routing, and the modules that serve it.
| Module | Python lines | Notes |
|---|---|---|
| rune_operator | 126KB | THE core — turn loop, task management |
| adaptive_router | 23KB | Route decisions |
| build_mode | 20KB | Build orchestration |
| builder_bridge | 12KB | Builder dispatch |
| builder_bridge_cli | 1KB | CLI surface |
| core_server | 9KB | HTTP server |
| persona_definition | 7KB | Persona loading |
| voicebox | 4KB | Voice |
| voice_injector | 3KB | Voice injection |
| release_notes | 18KB | Release generation |
| auto_context | 8KB | Auto context |
| auto_tasks | 15KB | Auto tasks |
| event_wait | 23KB | Event-driven waits |
| evidence_probe | 7KB | Evidence probes |
| forge | 5KB | Forge adapter |
| forge_client | 7KB | Forge client |
| goal | 14KB | Goal management |
| growth_delta | 16KB | Growth tracking |
| warm_router | 55KB | Warm routing |
**Exit condition**: the operator turn loop runs in Go, routes through Go model connections, dispatches to Rust kernels via FFI.
### Wave 5c: The Go memory operations (8 modules, ~8,240 lines)
| Module | Python lines | Notes |
|---|---|---|
| memory_consolidation | 31KB | Consolidation operations (kernel already in Rust) |
| memory_store | 99KB | THE memory store — SQLite operations |
| curator | 70KB | Memory curation |
| sleep_time | 38KB | Sleep-time consolidation |
| abstraction | 22KB | Fact extraction (kernel already in Rust) |
| persona_gate | 24KB | Persona gating |
| witness | 15KB | Witness reflection |
| stage_guards | 12KB | Stage guards |
**Exit condition**: memory operations run in Go, calling Rust kernels for the deterministic compute.
### Wave 6: The Rust deterministic backend (85 modules, ~25,218 lines)
Three sub-waves by dependency order:
**Wave 6a** (30 modules, ~11,515 lines): the structural core — spine, structural_store, construction_*, contracts, capability_*, audit_ledger, behaviour_ledger, claim_audit, etc.
**Wave 6b** (30 modules, ~6,849 lines): the cognition layer — drift_*, graph_memory, memory_selector, identity_anchors, pins, salience_gate, self_improve, token_optimizer, etc.
**Wave 6c** (25 modules, ~6,854 lines): the remaining modules — voice_hybrid, voice_sync, prompt_sync, time_awareness, toolchain, verification_layer, etc.
**Exit condition**: every deterministic module exists as a Rust function in a crate, the Python version is a thin FFI caller or deleted.
### Wave 7: The TUI + test infrastructure (36 modules, ~10,832 lines)
The TUI stays Python (Textual is Python-native) but its backend calls go through the Go/Rust dichotomy. Test infrastructure stays Python (pytest is Python-native).
**Exit condition**: the TUI is the only Python surface, everything behind it is Go/Rust.
---
## §3 — PER-FILE EPIC TEMPLATE (§7 anti-ballooning: 180 files → per-file epics)
Each file's port gets ONE epic. The epic body contains:
1. **File identity**: `src/rune/<module>.py` — N lines, M functions, K classes
2. **Three questions**: (a) can it be IMPROVED in the target language? (b) does it need FIXING? (c) are there MISSING CONNECTORS?
3. **Language assessment**: confirmed GO/RUST per classification, with the evidence
4. **Constraint context**: which henxels, line caps, canary requirements apply
5. **Pipeline plan**: branch name, PR route, canary battery spec
6. **Verification**: the specific tests + canaries that prove the port
7. **Provenance**: links to the wave, the tranche, the classification record
---
## §4 — DECISION RUBRIC
Per-unit language decisions are made against:
1. **Does the module initiate/respond/route?** → Go
2. **Does it compute/store/pure-function?** → Rust
3. **Does it have BOTH?** → Hybrid: Go front end calls Rust kernel via FFI (contracted membrane junction)
4. **Is it the TUI?** → Python (last)
5. **Is it test infrastructure?** → Python (stays)
Evidence required: the module's actual imports, its primary purpose, its callers, and what breaks if it's removed.
---
## §5 — PIPELINE ROUTING
Every port flows through:
1. **Construction contract**: `rune scaffold submit-need` → decision → contract → building
2. **Branch**: `feat/port-<module>-to-<lang>` from master
3. **Implementation**: Go module in `src/go/<name>/` or Rust module in `src/rust/<crate>/`
4. **Python oracle**: the original Python function preserved as `_py_<function>` for parity
5. **Delegation**: the Python public delegates to the Go/Rust implementation
6. **Canary**: a parity battery proving kernel↔oracle equivalence
7. **CI**: the canary registered in `.gitea/workflows/ci.yml`
8. **PR**: → master, required checks, merge-robot
9. **Deploy**: auto-version bump, deploy pipeline, runtime verification
---
## §6 — VERIFICATION GATES
Per-wave exit:
- All modules in the wave exist as Go/Rust implementations
- Python versions delegate (or are deleted if nothing calls them)
- CI go-surface job passes (for Go modules)
- CI canary job passes (for Rust kernels)
- Full test suite green
- diff-cover ≥70% on changed lines
- Deploy advances the current symlink
Per-tranche exit:
- `ls src/rune/*.py` returns ≤7 files (TUI only)
- Full test suite green
- All canaries green
- All henxels hold
- Deploy verified at the final version
---
## §7 — RISK REGISTER
| Risk | Prevention | Detection |
|---|---|---|
| Big-bang rewrite breaks everything | Incremental waves, each independently deployable | CI red on any wave |
| Dual-truth drift (Python and Go/Rust diverge) | Parity canaries, _py_ oracles | Canary failure |
| Broken keep-greens | Named up front, checked per-wave | CI red |
| Silent scope creep | Per-file epics with explicit scope | Code review |
| FFI boundary breaks | CC_RUST_LIB/SC_RUST_LIB env contract | Kernel probe test |
| Memory store data loss | SQLite retained, reads proven byte-identical | Canary hash check |
| Operator turn loop regression | Full test suite + runtime verification | Deploy verification |
| Henxels line-cap violations | Carve before commit (spine_language pattern) | CI lint red |
| New module without canary | New-module henxel | CI lint red |
---
## §8 — ROLLBACK / KNOWN-GOOD
Each wave is independently deployable. If a wave breaks:
1. The Python version is still in the tree (delegation, not deletion)
2. `RUNE_COGNITION_CORE=python` pins the fallback
3. The deploy symlink can be pointed back to the previous revision
4. Git revert the wave's merge commit
---
## §9 — SCALE HONESTY
This is **weeks of work, not hours**. 180 modules at ~1-2 modules per session = 90-180 sessions. The previous attempt (R2) ported 10 crates + 2 Go modules in multiple sessions. This tranche is the FULL job.
The wave structure is designed for incremental progress: each wave is independently deployable, each module's port is independently verifiable, and the system works at every intermediate state. The Python versions delegate to the Go/Rust implementations — they don't get deleted until the port is proven.
---
## §10 — APPROVAL GATE
This tranche is presented for operator approval. The operator reads the wave structure, the module assignments, the per-wave exit conditions, and either approves (execution starts at Wave 4) or requests changes.
The previous attempt (R2) ported 10 crates + 2 Go modules — less than 10% of the codebase. This tranche is the FULL job: every module, every line, every language decision, with the dichotomy honestly applied.
+196
View File
@@ -0,0 +1,196 @@
# Total-Rebase Inventory (R2 Wave 0.1) — measured 2026-10-04
Generated from `store/rebase/inventory.json` (AST function-level extraction) +
`store/rebase/classification.json` (import/content evidence per module).
Placement law: reactive/initiating/user-facing/network/turn-time → GO;
deterministic/stateful/storage/cognition → RUST; Textual rendering → TUI-LAST;
pure compute with no I/O markers defaults to the deterministic backend.
## Totals
| Side | Modules | Lines | Functions |
|---|---|---|---|
| GO | 43 | 19495 | 708 |
| RUST | 93 | 36215 | 1091 |
| TUI-LAST | 7 | 5438 | 225 |
| TEST-INFRA | 29 | 5395 | 158 |
| RETIRES | 2 | 93 | 9 |
## Per-module classification (evidence-carrying)
| Module | Side | Lines | Functions | Evidence |
|---|---|---|---|---|
| rune_tui | TUI-LAST | 4041 | 166 | textual/rich imports: ['rich', 'textual'] |
| rune_operator | GO | 2697 | 68 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
| memory_store | RUST | 2435 | 56 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
| curator | RUST | 1838 | 43 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
| tool_executor | GO | 1812 | 47 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
| warm_router | RUST | 1536 | 36 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; reactive/initiating domain; deterministic/state domain |
| construction_interface | RUST | 1039 | 25 | deterministic/state domain |
| spine | RUST | 1019 | 31 | deterministic/state domain |
| sleep_time | RUST | 971 | 25 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
| construction_decision | RUST | 949 | 18 | deterministic/state domain |
| capability_broker | RUST | 900 | 21 | deterministic/state domain |
| hook_engine | GO | 860 | 8 | reactive/initiating domain |
| scaffold | RUST | 854 | 31 | network/concurrency imports: ['subprocess']; deterministic/state domain |
| local_tool_runtime | GO | 806 | 32 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
| context_broker | GO | 768 | 23 | network/concurrency imports: ['threading'] |
| elastic_cognition | RUST | 765 | 13 | network/concurrency imports: ['urllib']; deterministic/state domain |
| memory_consolidation | RUST | 733 | 20 | network/concurrency imports: ['subprocess']; deterministic/state domain |
| provider_registry | GO | 666 | 28 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
| persona_gate | RUST | 661 | 21 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
| structural_store | RUST | 656 | 33 | deterministic/state domain |
| contracts | RUST | 626 | 7 | deterministic/state domain |
| adaptive_router | GO | 625 | 19 | reactive/initiating domain |
| work_manager | GO | 591 | 25 | reactive/initiating domain |
| abstraction | RUST | 587 | 12 | sqlite3 (storage-backed); network/concurrency imports: ['urllib']; deterministic/state domain |
| construction_intent | RUST | 555 | 16 | deterministic/state domain |
| canary_audit_ledger | TEST-INFRA | 540 | 2 | canary/test module |
| safety_override | RUST | 530 | 18 | no network/reactive markers — pure compute defaults to the deterministic backend |
| failure_auto_filer | GO | 527 | 28 | network/concurrency imports: ['threading'] |
| build_mode | GO | 517 | 34 | network/concurrency imports: ['subprocess'] |
| drift_core | RUST | 517 | 14 | deterministic/state domain |
| event_wait | GO | 517 | 14 | network/concurrency imports: ['subprocess', 'urllib']; reactive/initiating domain |
| tui_validator | TUI-LAST | 514 | 13 | tui family |
| memory_selector | RUST | 511 | 14 | deterministic/state domain |
| system_registry | RUST | 501 | 14 | no network/reactive markers — pure compute defaults to the deterministic backend |
| construction_pattern | RUST | 496 | 13 | deterministic/state domain |
| cache | GO | 492 | 17 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
| claim_audit | RUST | 483 | 14 | sqlite3 (storage-backed); deterministic/state domain |
| graph_memory | RUST | 474 | 9 | sqlite3 (storage-backed); deterministic/state domain |
| identity_anchors | RUST | 474 | 8 | deterministic/state domain |
| model_connect | GO | 462 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| auto_tasks | GO | 460 | 14 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess'] |
| capability_fit | RUST | 455 | 14 | deterministic/state domain |
| tool_protocol | GO | 455 | 27 | reactive/initiating domain |
| drift_ledger | RUST | 453 | 16 | sqlite3 (storage-backed); deterministic/state domain |
| witness | RUST | 451 | 10 | network/concurrency imports: ['urllib']; deterministic/state domain |
| goal | RUST | 441 | 20 | network/concurrency imports: ['urllib']; deterministic/state domain |
| release_notes | GO | 439 | 17 | network/concurrency imports: ['urllib'] |
| cron_engine | GO | 436 | 28 | network/concurrency imports: ['subprocess', 'threading']; reactive/initiating domain |
| model_profiles | GO | 435 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| microkernel | GO | 434 | 12 | network/concurrency imports: ['subprocess', 'threading'] |
| model_routing | GO | 432 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| voice_hybrid | RUST | 421 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
| behavior | RUST | 407 | 9 | sqlite3 (storage-backed); deterministic/state domain |
| token_optimizer | RUST | 407 | 23 | deterministic/state domain |
| capability_ledger | RUST | 405 | 19 | deterministic/state domain |
| night_mode | GO | 405 | 19 | network/concurrency imports: ['subprocess', 'threading', 'urllib'] |
| chat_bubble | TUI-LAST | 401 | 16 | textual/rich imports: ['rich', 'textual'] |
| growth_delta | RUST | 400 | 16 | network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
| construction_mode | RUST | 395 | 5 | deterministic/state domain |
| prompt_sync | RUST | 376 | 14 | sqlite3 (storage-backed) |
| provider_budget | GO | 354 | 21 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| structural_model | RUST | 352 | 21 | deterministic/state domain |
| programme_continuation | RUST | 349 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_rune_serve | TEST-INFRA | 329 | 13 | canary/test module |
| builder_bridge | GO | 326 | 11 | reactive/initiating domain |
| canary_p1_07 | TEST-INFRA | 324 | 12 | canary/test module |
| layout_contract | RUST | 316 | 6 | deterministic/state domain |
| message_queue | GO | 316 | 14 | reactive/initiating domain |
| drift_observability | RUST | 315 | 9 | deterministic/state domain |
| build_basis | RUST | 314 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_p0_07 | TEST-INFRA | 310 | 11 | canary/test module |
| drift_compression | RUST | 307 | 12 | deterministic/state domain |
| event_envelope | GO | 303 | 8 | reactive/initiating domain |
| attention_capsule | RUST | 296 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_p1_01 | TEST-INFRA | 294 | 14 | canary/test module |
| voice_sync | RUST | 293 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
| drift_cache | RUST | 292 | 16 | deterministic/state domain |
| outcome_ledger | RUST | 286 | 8 | sqlite3 (storage-backed); deterministic/state domain |
| time_awareness | RUST | 283 | 15 | no network/reactive markers — pure compute defaults to the deterministic backend |
| tiered_executor | GO | 282 | 9 | network/concurrency imports: ['urllib'] |
| stage_guards | RUST | 281 | 11 | network/concurrency imports: ['subprocess']; deterministic/state domain |
| standardized_providers | GO | 281 | 5 | reactive/initiating domain |
| canary_p1_06 | TEST-INFRA | 280 | 9 | canary/test module |
| progressive_escalation | RUST | 279 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
| pins | RUST | 276 | 11 | deterministic/state domain |
| canary_p1_03 | TEST-INFRA | 271 | 9 | canary/test module |
| auto_context | GO | 270 | 8 | network/concurrency imports: ['subprocess'] |
| task_persist | RUST | 269 | 10 | no network/reactive markers — pure compute defaults to the deterministic backend |
| context_pressure | RUST | 262 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_gitea_automation | TEST-INFRA | 261 | 3 | canary/test module |
| canary_p1_02 | TEST-INFRA | 253 | 12 | canary/test module |
| construction_patterns_builtin | RUST | 249 | 1 | deterministic/state domain |
| canary_full_system | TEST-INFRA | 244 | 2 | canary/test module |
| event_bus | GO | 244 | 7 | reactive/initiating domain |
| salience_gate | RUST | 243 | 4 | deterministic/state domain |
| self_improve | RUST | 242 | 5 | sqlite3 (storage-backed) |
| construction_activation | RUST | 237 | 4 | deterministic/state domain |
| toolchain | RUST | 236 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
| task_gate | RUST | 235 | 8 | sqlite3 (storage-backed) |
| core_server | GO | 234 | 13 | network/concurrency imports: ['http', 'urllib']; reactive/initiating domain |
| audit_ledger | RUST | 231 | 8 | deterministic/state domain |
| canary_native | TEST-INFRA | 231 | 3 | canary/test module |
| openai_compat_transport | GO | 227 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| shard_manager | RUST | 223 | 10 | sqlite3 (storage-backed) |
| response_adapter | RUST | 222 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
| evidence_probe | RUST | 212 | 9 | network/concurrency imports: ['subprocess']; deterministic/state domain |
| model_registry | GO | 211 | 8 | reactive/initiating domain |
| persona_definition | GO | 210 | 8 | network/concurrency imports: ['subprocess'] |
| cognitive_budgeter | RUST | 208 | 3 | deterministic/state domain |
| canary_p0_04 | TEST-INFRA | 207 | 8 | canary/test module |
| forge_client | GO | 203 | 19 | network/concurrency imports: ['urllib']; reactive/initiating domain |
| dependency_tracker | RUST | 202 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_context_broker | TEST-INFRA | 199 | 10 | canary/test module |
| canary_p0_05 | TEST-INFRA | 199 | 16 | canary/test module |
| testing_mode | RUST | 196 | 13 | no network/reactive markers — pure compute defaults to the deterministic backend |
| parallel_processor | GO | 194 | 17 | network/concurrency imports: ['threading'] |
| canary_spine_meta | TEST-INFRA | 192 | 7 | canary/test module |
| iteration | RUST | 190 | 8 | no network/reactive markers — pure compute defaults to the deterministic backend |
| verification_layer | RUST | 189 | 12 | deterministic/state domain |
| subconscious_pass | RUST | 187 | 6 | deterministic/state domain |
| ffi_query_memory_smoke | RUST | 182 | 5 | deterministic/state domain |
| tier_manager | RUST | 182 | 9 | deterministic/state domain |
| canary_tui_transport | TEST-INFRA | 174 | 7 | canary/test module |
| behaviour_ledger | RUST | 173 | 13 | deterministic/state domain |
| construction_commands | RUST | 173 | 4 | deterministic/state domain |
| canary_tui_drift | TEST-INFRA | 170 | 2 | canary/test module |
| intent_continuity | RUST | 168 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
| rune_profiler | RUST | 165 | 3 | no network/reactive markers — pure compute defaults to the deterministic backend |
| inference_trace | RUST | 164 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
| ms_loader | RUST | 163 | 16 | no network/reactive markers — pure compute defaults to the deterministic backend |
| subsystem_health | GO | 163 | 8 | network/concurrency imports: ['threading'] |
| recall_router | GO | 162 | 3 | reactive/initiating domain |
| voicebox | GO | 161 | 11 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
| canary_gitea_pipeline | TEST-INFRA | 159 | 2 | canary/test module |
| forge | GO | 146 | 12 | network/concurrency imports: ['subprocess', 'urllib'] |
| rune_graphic | TUI-LAST | 145 | 5 | textual/rich imports: ['rich'] |
| connection_pool | GO | 144 | 9 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
| recall_uplift | RUST | 141 | 1 | sqlite3 (storage-backed) |
| project_registry | RUST | 139 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
| construction_depth | RUST | 138 | 2 | deterministic/state domain |
| voice_injector | GO | 138 | 5 | reactive/initiating domain |
| canary_rune_entry | TEST-INFRA | 137 | 3 | canary/test module |
| construction_projection | RUST | 135 | 3 | deterministic/state domain |
| rune_spinner | RUST | 130 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
| tui_backlog | TUI-LAST | 125 | 5 | tui family |
| warm_neuron_store | RUST | 122 | 6 | sqlite3 (storage-backed); deterministic/state domain |
| drift_resume | RUST | 118 | 3 | deterministic/state domain |
| wait_guard | RUST | 116 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_conversation_state | TEST-INFRA | 115 | 2 | canary/test module |
| canary_query_memory | TEST-INFRA | 110 | 2 | canary/test module |
| failure_recovery | RUST | 108 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
| graph_projection | RUST | 108 | 5 | deterministic/state domain |
| tui_transport | TUI-LAST | 108 | 12 | tui family |
| spine_loader | RUST | 105 | 7 | deterministic/state domain |
| canary_unbounded | TEST-INFRA | 104 | 2 | canary/test module |
| message_widgets | TUI-LAST | 104 | 8 | textual/rich imports: ['rich', 'textual'] |
| cs_loader | RUST | 90 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
| tc_loader | RUST | 88 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_schema_migration | TEST-INFRA | 86 | 1 | canary/test module |
| programme_gates | RUST | 85 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
| structural_migration | RUST | 77 | 4 | deterministic/state domain |
| memory_env | RETIRES | 72 | 8 | bootstrap glue subsumed by the Go runtime env contract |
| impact_preview | RUST | 69 | 2 | no network/reactive markers — pure compute defaults to the deterministic backend |
| provider_context | GO | 48 | 1 | reactive/initiating domain |
| wait_guard_corpus | RUST | 46 | 0 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_cognitive_strata | TEST-INFRA | 44 | 1 | canary/test module |
| builder_bridge_cli | GO | 42 | 1 | reactive/initiating domain |
| route_risk_flags | RUST | 37 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
| canary_memory_synthesis | TEST-INFRA | 33 | 1 | canary/test module |
| canary_resource_authority | TEST-INFRA | 33 | 1 | canary/test module |
| canary_surface_parity | TEST-INFRA | 33 | 1 | canary/test module |
| canary_orchestration_foundation | TEST-INFRA | 32 | 1 | canary/test module |
| canary_gitea_delegation | TEST-INFRA | 31 | 1 | canary/test module |
| __main__ | RETIRES | 21 | 1 | bootstrap glue subsumed by the Go runtime env contract |
+3 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.14"
version = "0.131.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
@@ -16,12 +16,13 @@ dependencies = [
"aiohttp>=3.9.0",
"websockets>=12.0",
"psutil>=5.9.0",
"pillow>=10.0",
"pyyaml>=6.0",
]
[project.optional-dependencies]
memory = ["sqlite-vec>=0.1.0"]
dev = ["pytest>=8.0", "pytest-subtests", "ruff==0.16.5", "pyright", "henxels==0.12.0", "coverage>=7.6", "diff-cover>=9.2"]
dev = ["pytest>=8.0", "pytest-subtests", "ruff==0.16.5", "pyright", "henxels==0.12.0", "coverage>=7.6", "diff-cover>=9.2", "hypothesis>=6.100"]
[tool.setuptools.packages.find]
where = ["src"]
+2 -2
View File
@@ -1,5 +1,5 @@
{
"errorCount": 120,
"warningCount": 465,
"errorCount": 117,
"warningCount": 454,
"informationCount": 0
}
+37 -28
View File
@@ -1,29 +1,38 @@
{
"typeCheckingMode": "standard",
"pythonVersion": "3.14",
"reportMissingImports": "warning",
"reportMissingModuleSource": "none",
"reportMissingTypeStubs": "none",
"reportUnknownArgumentType": "none",
"reportUnknownParameterType": "none",
"reportUnknownVariableType": "none",
"reportUnknownMemberType": "none",
"reportMissingTypeArgument": "none",
"reportUntypedFunctionDecorator": "none",
"reportUntypedClassDecorator": "none",
"reportUntypedBaseClass": "none",
"reportPrivateUsage": "none",
"reportOptionalMemberAccess": "warning",
"reportUnnecessaryIsInstance": "warning",
"reportUnnecessaryComparison": "warning",
"reportUnnecessaryTypeIgnoreComment": "warning",
"reportPossiblyUnboundVariable": "error",
"reportUndefinedVariable": "error",
"reportAttributeAccessIssue": "warning",
"include": ["src/rune"],
"exclude": [
"src/rune/__pycache__",
"**/node_modules",
"**/__pycache__"
]
}
"typeCheckingMode": "standard",
"pythonVersion": "3.14",
"reportMissingImports": "warning",
"reportMissingModuleSource": "none",
"reportMissingTypeStubs": "none",
"reportUnknownArgumentType": "none",
"reportUnknownParameterType": "none",
"reportUnknownVariableType": "none",
"reportUnknownMemberType": "none",
"reportMissingTypeArgument": "none",
"reportUntypedFunctionDecorator": "none",
"reportUntypedClassDecorator": "none",
"reportUntypedBaseClass": "none",
"reportPrivateUsage": "none",
"reportOptionalMemberAccess": "warning",
"reportUnnecessaryIsInstance": "warning",
"reportUnnecessaryComparison": "warning",
"reportUnnecessaryTypeIgnoreComment": "warning",
"reportPossiblyUnboundVariable": "error",
"reportUndefinedVariable": "error",
"reportAttributeAccessIssue": "warning",
"include": [
"src/rune"
],
"exclude": [
"src/rune/__pycache__",
"**/node_modules",
"**/__pycache__"
],
"extraPaths": [
"scripts",
"src/rune",
"shared",
"src/rust/evidence_provenance",
"src/rust/query_memory"
]
}
+18 -12
View File
@@ -2,7 +2,7 @@
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
aiohappyeyeballs==2.7.1
# via aiohttp
aiohttp==3.14.3
aiohttp==3.14.4
# via rune (pyproject.toml)
aiosignal==1.4.0
# via aiohttp
@@ -18,9 +18,9 @@ certifi==2026.7.22
# httpx
chardet==7.6.0
# via diff-cover
coverage==7.16.0
coverage==7.16.2
# via rune (pyproject.toml)
diff-cover==10.5.1
diff-cover==10.6.0
# via rune (pyproject.toml)
frozenlist==1.8.0
# via
@@ -34,12 +34,14 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via rune (pyproject.toml)
idna==3.19
hypothesis==6.168.5
# via rune (pyproject.toml)
idna==3.20
# via
# anyio
# httpx
# yarl
iniconfig==2.3.0
iniconfig==2.3.1
# via pytest
jinja2==3.1.6
# via diff-cover
@@ -50,27 +52,29 @@ markdown-it-py==4.2.0
# mdit-py-plugins
# rich
# textual
markupsafe==3.0.3
markupsafe==3.0.4
# via jinja2
mdit-py-plugins==0.6.1
# via textual
mdurl==0.1.2
# via markdown-it-py
multidict==6.8.0
multidict==6.9.1
# via
# aiohttp
# yarl
nodeenv==1.10.0
nodeenv==1.11.0
# via pyright
packaging==26.3
# via pytest
platformdirs==4.11.8
pillow==12.3.0
# via rune (pyproject.toml)
platformdirs==4.12.3
# via textual
pluggy==1.6.0
# via
# diff-cover
# pytest
propcache==0.5.2
propcache==0.5.4
# via
# aiohttp
# yarl
@@ -100,6 +104,8 @@ rich==15.0.0
# textual
ruff==0.16.5
# via rune (pyproject.toml)
sortedcontainers==2.4.0
# via hypothesis
sqlite-vec==0.1.9
# via rune (pyproject.toml)
textual==8.2.8
@@ -116,7 +122,7 @@ typing-extensions==4.16.0
# pyright
# textual
# textual-autocomplete
websockets==17.1
websockets==17.2
# via rune (pyproject.toml)
yarl==1.24.5
yarl==1.25.1
# via aiohttp
View File
Whitespace-only changes.
+494
View File
@@ -0,0 +1,494 @@
"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
execution boundary.
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
The model may express continuation intent in any quality — `await_operation
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
"sleep 300 then check CI", "poll until done". When a matching
PendingOperation exists (or the deterministic resolver can derive the
operation truth from git/Gitea state), the controller executes the
CANONICAL continuation and discards model-supplied scheduling policy.
Authority provenance for timing:
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
GOVERNED_OPERATION_POLICY -> authoritative, retained
MODEL_INVENTED -> non-authoritative, ignored
This module owns NO storage: it reads the existing W/R wait store
(rune.event_wait) and git/Gitea state. The canonical continuation remains
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
if _REPO_SRC not in sys.path:
sys.path.insert(0, _REPO_SRC)
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
REPO = Path("/mnt/workdrive/issue161-harness")
GIT_TIMEOUT = 10
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
# The model's numbers never become policy; these are the controller's.
POLICY_DEADLINES = {
"branch_ci": 1800,
"pull_request_checks": 1800,
"master_ci": 1800,
"deploy": 900,
"release": 900,
}
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
"deploy", "release")
# ── timing provenance ───────────────────────────────────────────────────────
def classify_timing_provenance(model_deadline: Any = None,
user_deadline: Any = None,
contract_deadline: Any = None) -> tuple[int, str]:
"""Resolve the effective deadline + its provenance.
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
NEVER used as policy.
"""
if model_deadline is not None:
try:
model_deadline = int(model_deadline)
except (TypeError, ValueError):
model_deadline = None
if user_deadline is not None:
return int(user_deadline), "USER_EXPLICIT"
if contract_deadline is not None:
return int(contract_deadline), "EXTERNAL_CONTRACT"
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
# ── PendingOperation lookup over the existing W/R wait store ────────────────
def _load_wait_records() -> list[dict]:
"""All durable wait records from the bridge store (read-only)."""
out = []
waits = WR_BRIDGE / "waits"
if not waits.is_dir():
return out
for f in sorted(waits.glob("*.json")):
try:
rec = json.loads(f.read_text())
rec["_file"] = f.name
out.append(rec)
except (OSError, ValueError):
continue
return out
def _rec_operation_id(rec: dict) -> str:
return str((rec.get("ext") or {}).get("operation_id")
or rec.get("operation_id") or "")
def _rec_sha(rec: dict) -> str:
return str(rec.get("sha") or "")
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
"""Pending (non-terminal) operations, optionally filtered."""
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
out = []
for rec in _load_wait_records():
status = str(rec.get("status", "")).upper()
if status in terminal:
continue
if sha and _rec_sha(rec) != sha:
continue
out.append(rec)
return out
def find_op_type_for_sha(sha: str) -> str | None:
"""The op_type a prior canonical wait already recorded for this SHA
(objective text embeds the type: 'event-driven {op_type} qualification')."""
for rec in _load_wait_records():
if _rec_sha(rec) != sha:
continue
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
if m and m.group(1) in VALID_OP_TYPES:
return m.group(1)
return None
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
def _git(*args: str) -> str:
try:
r = subprocess.run(["git", "-C", str(REPO), *args],
capture_output=True, text=True, timeout=GIT_TIMEOUT)
return r.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
return ""
def sha_branch(sha: str) -> str:
"""Branch(es) containing this commit in the local repo."""
return _git("branch", "--all", "--contains", sha)
def sha_on_master(sha: str) -> bool:
"""SHA is an ancestor of master (local or remote-tracking refs).
M10 correction (2026-09-19): --all so a freshly fetched master
(refs/remotes/gitea/master) counts; local branches alone go stale
between fetches. derive_op_type refreshes refs before calling this.
"""
out = _git("branch", "--all", "--contains", sha)
for l in out.splitlines():
s = l.strip().removeprefix("remotes/").removesuffix(" *")
if s == "master" or s.endswith("/master"):
return True
return False
def open_pr_for_sha(sha: str) -> int | None:
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
try:
sys.path.insert(0, str(_SCRIPT_DIR))
import gitea_auth # noqa: E402
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
if st != 200:
return None
for p in prs:
hs = str(p.get("head") or {}).get("sha") if isinstance(
p.get("head"), dict) else None
if hs and str(hs).startswith(sha[:12]):
return int(p.get("number"))
except Exception: # noqa: BLE001 — resolver must never crash the tool
return None
return None
def derive_op_type(sha: str) -> tuple[str, str]:
"""Deterministically derive the canonical op_type for a SHA.
Returns (op_type, evidence). Order (A-patch §2 amendment):
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
stored operation truth, wins outright;
2. open PR for the sha -> pull_request_checks;
3. sha ancestor of master -> master_ci;
4. sha on a feature branch -> branch_ci;
5. legacy (pre-patch) records only CONFIRM, never override — they
were created from model-supplied defaults and cannot be trusted.
Never trusts model input.
"""
# 0. M10 correction (2026-09-19): refresh remote-tracking refs before
# inference. A just-merged master commit is invisible to
# branch --contains until refs are fetched; stale local git state
# must never outrank repo truth. Bounded by GIT_TIMEOUT, fail-open.
_git("fetch", "--quiet", "gitea", "+refs/heads/*:refs/remotes/gitea/*")
for rec in _load_wait_records():
if _rec_sha(rec) != sha:
continue
if (rec.get("ext") or {}).get("intent_normalized"):
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
if m and m.group(1) in VALID_OP_TYPES:
return m.group(1), "normalized_record:" + m.group(1)
pr = open_pr_for_sha(sha)
if pr:
return "pull_request_checks", f"open_pr:#{pr}"
if sha_on_master(sha):
return "master_ci", "commit_ancestor_of_master"
return "branch_ci", "commit_on_feature_branch"
# ── intent normalization ────────────────────────────────────────────────────
def normalize_action(action: dict, *,
model_deadline: Any = None,
user_deadline: Any = None,
contract_deadline: Any = None,
task_id: str = "") -> dict:
"""Normalize a model continuation action into the canonical continuation.
action: {kind: await_operation|sleep_poll|check_status|unknown,
sha?: str, operation_id?: str, op_type?: str, raw?: str}
task_id: REAL_ASYNC_BRIDGE §9 — when the caller knows its task
binding, pending-operation resolution is scoped to operations
registered with that task_id; stale global operations are
never consulted when a binding exists.
Returns:
{normalized: bool, action: "ContinuePendingOperation", operation_id,
op_type, authority, deadline_secs, deadline_provenance,
model_deadline_ignored, model_op_type_ignored, evidence}
or {normalized: False, reason: ...} / {normalized: "AMBIGUOUS", candidates}
"""
sha = str(action.get("sha") or "")
op_id = str(action.get("operation_id") or "")
model_op_type = action.get("op_type")
model_deadline_ignored = model_deadline is not None
model_op_type_ignored = model_op_type is not None
# 0. task binding scope (§9): task-bound operations beat global state.
# Stored operation wins over ANY model hint (sha/op_type/deadline).
if task_id and not op_id:
bound = [r for r in pending_operations()
if str(r.get("ext", {}).get("task_id", "")) == task_id]
if len(bound) == 1:
rec = bound[0]
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"task_bound",
user_deadline, contract_deadline)
norm["sha_for_wait"] = _rec_sha(rec)
norm["task_id"] = task_id
return norm
if len(bound) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in bound],
"reason": f"multiple pending operations bound to task {task_id}"}
return {"normalized": False,
"reason": f"no pending operation bound to task {task_id}"}
# 1. explicit operation_id wins
if op_id:
from rune import event_wait as ew # noqa: E402
rec = ew.operation_wait(op_id)
if rec is None:
# REAL_ASYNC_BRIDGE §10: a FRESH operation has no wait record
# yet — registration happens at the park step (ci_event_wait).
# For an explicit, well-formed, EXISTING commit id, return the
# provisional canonical continuation (policy deadline, own-runs
# authority) so the model-facing tool can park on first call.
# A typo'd/ghost sha is refused here (A-patch lesson): the
# commit must exist in the repository.
import re as _re
m = _re.fullmatch(r"ci-([0-9a-f]{40})", op_id)
if m:
sha40 = m.group(1)
probe = subprocess.run(
["git", "-C", str(REPO), "cat-file", "-e",
f"{sha40}^{{commit}}"],
capture_output=True, timeout=15)
if probe.returncode == 0:
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": op_id,
"sha_for_wait": sha40,
"op_type": "branch_ci",
"authority": "ci_runs",
"deadline_secs": POLICY_DEADLINES["branch_ci"],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"evidence": "explicit_id_fresh_registration",
}
return {"normalized": False,
"reason": f"operation_id {op_id} has no wait record"}
return _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored, "explicit_id",
user_deadline, contract_deadline)
# 2. resolve by sha / active operation.
# H-PATCH H2/H3: the model sha is a HINT. If it doesn't resolve but
# unambiguous pending operations exist, the STORED target wins — a
# mistyped model sha must never create a ghost operation (the A-patch
# live failure: a typo'd sha derived branch_ci for a nonexistent commit
# and parked a wait on a ghost).
# 2. no explicit operation_id: resolve by sha / pending operations.
# H-PATCH: when the model provides NO sha (operation-id-first mode),
# resolve from unambiguous pending operations — the model should not
# need to reconstruct a 40-char sha to express "wait for it".
if not sha:
all_pending = pending_operations()
if len(all_pending) == 1:
rec = all_pending[0]
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"auto_resolved_no_sha",
user_deadline, contract_deadline)
norm["sha_for_wait"] = _rec_sha(rec)
return norm
if len(all_pending) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in all_pending],
"reason": "multiple pending operations, no sha hint"}
return {"normalized": False,
"reason": "no pending operations and no sha provided"}
candidates = pending_operations(sha=sha)
if len(candidates) > 1 and not sha:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in candidates],
"reason": "multiple pending operations, no target"}
if len(candidates) == 1:
rec = candidates[0]
stored_sha = _rec_sha(rec)
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored, "pending_match",
user_deadline, contract_deadline)
if sha and stored_sha and sha != stored_sha:
norm["model_sha_mismatch"] = {
"model_sha": sha, "stored_sha": stored_sha,
"resolution": "stored_target_used"}
return norm
# model sha unresolved: any unambiguous pending op at all? then the
# model probably mistyped the sha — use the stored target, log it.
all_pending = pending_operations()
if len(all_pending) == 1 and sha:
rec = all_pending[0]
stored_sha = _rec_sha(rec)
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"unresolved_sha_single_pending",
user_deadline, contract_deadline)
norm["model_sha_mismatch"] = {
"model_sha": sha, "stored_sha": stored_sha,
"resolution": "stored_target_used"}
return norm
# 3. no stored record: derive truth deterministically
if not sha:
return {"normalized": False,
"reason": "no pending operation and no sha to resolve"}
op_type, evidence = derive_op_type(sha)
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": f"ci-{sha}",
"op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": POLICY_DEADLINES[op_type],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"evidence": evidence,
}
def _canonical_from_record(rec: dict, model_deadline_ignored: bool,
model_op_type_ignored: bool, how: str,
user_deadline: Any, contract_deadline: Any) -> dict:
op_id = _rec_operation_id(rec)
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
recorded = m.group(1) if (m and m.group(1) in VALID_OP_TYPES) else None
rec_sha = _rec_sha(rec)
if (rec.get("ext") or {}).get("intent_normalized") or how == "explicit_id":
# normalized record or explicit id: stored truth wins
op_type = recorded or "master_ci"
else:
# legacy record: git/Gitea derivation outranks the model-era default
op_type, _ev = derive_op_type(rec_sha) if rec_sha else \
(recorded or "master_ci", "no_sha")
deadline, prov = classify_timing_provenance(
user_deadline=user_deadline, contract_deadline=contract_deadline)
if prov == "MODEL_INVENTED" or not deadline:
deadline = POLICY_DEADLINES.get(op_type, 1800)
prov = "GOVERNED_OPERATION_POLICY"
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": op_id,
"op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": deadline,
"deadline_provenance": prov,
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"matched_by": how,
}
def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
"""Recognize sleep/check/poll intent aimed at a known pending operation.
Returns a normalization record if the command's semantic intent is
continuation of a known external operation; None if the command is
legitimate local timing (short sync, user delay, backoff) with no
pending operation to continue. NEVER returns an executed sleep.
"""
if not command:
return None
cmd = command.strip()
# long sleep + (status-check language or an explicit sha hint)
sleep_m = re.search(r"\bsleep\s+(\d{2,5})\b", cmd)
if not sleep_m:
return None
secs = int(sleep_m.group(1))
check_intent = bool(re.search(
r"\b(ci|deploy|release|status|check|poll|pipeline|run)\b", cmd,
re.IGNORECASE))
sha = sha_hint or ""
m = re.search(r"\b([0-9a-f]{40}|[0-9a-f]{12})\b", cmd)
if m:
sha = m.group(1)
if not check_intent and not sha:
return None # plain sleep, no continuation intent
cands = pending_operations(sha=sha) if sha else pending_operations()
if not cands and not sha:
return None
if len(cands) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in cands],
"raw_command_kind": "sleep_poll"}
rec = cands[0] if cands else None
if rec is not None:
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
None, None)
norm["sha_for_wait"] = _rec_sha(rec)
elif sha:
op_type, evidence = derive_op_type(sha)
norm = {
"normalized": True, "action": "ContinuePendingOperation",
"operation_id": f"ci-{sha}", "op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": POLICY_DEADLINES[op_type],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": True, "model_op_type_ignored": False,
"evidence": evidence, "sha_for_wait": sha,
}
else:
return None
norm["raw_command_kind"] = "sleep_poll"
norm["raw_sleep_seconds_suppressed"] = secs
return norm
def emit_normalized_record(record: dict) -> None:
"""Compact structured observability — one JSONL line per normalization.
No payloads, no secrets."""
try:
log = _SCRIPT_DIR.parent / "store" / "await_intent_log.jsonl"
log.parent.mkdir(parents=True, exist_ok=True)
slim = {k: record.get(k) for k in (
"normalized", "action", "operation_id", "op_type", "authority",
"deadline_secs", "deadline_provenance", "model_deadline_ignored",
"model_op_type_ignored", "matched_by", "evidence",
"raw_command_kind", "raw_sleep_seconds_suppressed", "reason",
"candidates") if k in record}
slim["at"] = __import__("time").strftime(
"%Y-%m-%dT%H:%M:%SZ", __import__("time").gmtime())
with open(log, "a") as f:
f.write(json.dumps(slim, sort_keys=True) + "\n")
except OSError:
pass
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""await_intent_cli.py — CLI surface for the intent normalization layer.
Used by the await-operation harness tool so the MODEL's scheduling hints
never reach the wait engine. Prints one JSON line.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import await_intent # noqa: E402
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
sub = ap.add_subparsers(dest="cmd", required=True)
n = sub.add_parser("normalize", help="normalize a continuation intent")
n.add_argument("--sha", default="")
n.add_argument("--operation-id", default="")
n.add_argument("--task-id", default="",
help="§9 task binding: scope pending resolution to "
"operations registered for this task")
n.add_argument("--model-op-type", default=None)
n.add_argument("--model-deadline", default=None)
n.add_argument("--user-deadline", default=None,
help="USER_EXPLICIT deadline (authoritative)")
n.add_argument("--contract-deadline", default=None,
help="EXTERNAL_CONTRACT deadline (authoritative)")
rb = sub.add_parser("route-bash",
help="route a Bash command expressing wait intent")
rb.add_argument("--command", required=True)
rb.add_argument("--sha-hint", default="")
args = ap.parse_args()
if args.cmd == "route-bash":
record = await_intent.normalize_bash(args.command,
sha_hint=args.sha_hint)
if record is None:
print(json.dumps({"normalized": False,
"reason": "no external continuation intent"}))
return 0
if record.get("normalized") is True:
# the compiled wait needs the SHA: derive from operation_id
oid = str(record.get("operation_id", ""))
if oid.startswith("ci-"):
record["sha_for_wait"] = oid[3:]
await_intent.emit_normalized_record(record)
print(json.dumps(record, sort_keys=True))
return 0
action = {"kind": "await_operation", "sha": args.sha,
"operation_id": args.operation_id,
"op_type": args.model_op_type}
model_deadline = (int(args.model_deadline)
if args.model_deadline else None)
user_deadline = (int(args.user_deadline)
if args.user_deadline else None)
contract_deadline = (int(args.contract_deadline)
if args.contract_deadline else None)
record = await_intent.normalize_action(
action, model_deadline=model_deadline,
user_deadline=user_deadline, contract_deadline=contract_deadline,
task_id=args.task_id)
await_intent.emit_normalized_record(record)
print(json.dumps(record, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main())
+588
View File
@@ -0,0 +1,588 @@
#!/usr/bin/env python3
"""ci_event_wait.py — T16 §16C/§16D: event-driven CI qualification wait.
Replaces the deploy.yml sleep-30 → query → repeat polling loop with the
EXISTING W/R event bridge (src/rune/event_wait.py — the SAME primitive the
builder adapter uses; §16E: one bridge, adapters differ only in identity
and authority probe).
Flow (mandate §16C):
push / CI workflow starts
↓
register correlation (provider="gitea", event_type="ci_completed",
run_ref="ci-<sha>", sha=<sha>)
↓
WAITING_FOR_EVENT (await_resume_event: select() on FIFO, no floor)
↓
ci.yml final job POSTs /process-event (the WAKE)
↓
daemon matches → resume_cmd fires → FIFO token → controller resumes
↓
canonical Gitea state INDEPENDENTLY re-read (deploy_qualify.py verdict)
↓
verdict
The event is a WAKE, never truth. The final verdict always comes from
deploy_qualify.py (required-checks context + exact-SHA ci.yml run, both
legs). Watchdog deadline is a FAILURE DETECTOR, never the scheduler:
- event arrives → authority re-read; pending → bounded re-arm until the
overall deadline (ack-window semantics, same as the daemon's wake sweep).
- event lost/suppressed → watchdog expires → ONE-SHOT authority query:
terminal verdict → recovered truthfully (no invented wait);
CI still running (non-terminal Gitea runs for the SHA) → re-arm;
nothing alive and no verdict → truthful failure (deployment refused).
Exit codes: 0 = qualified (success), 1 = refused (failure/timeout).
Output: one JSON line with the §16D latency-proof timestamps.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(_SCRIPT_DIR))
sys.path.insert(0, str(_SCRIPT_DIR.parent / "src"))
sys.path.insert(0, str(_SCRIPT_DIR))
from rune import event_wait as ew # noqa: E402
import completion_file as cf # noqa: E402
REPO_ROOT = _SCRIPT_DIR.parent
QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py"
def authority_verdict(sha: str = "") -> str:
"""ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only
completion truth. Never parses the event payload.
H-PATCH H4: the target sha is passed EXPLICITLY (as the subprocess's
--sha argument) — ambient DEPLOY_SHA can no longer change which
operation is verified. The env var remains only as a CLI backward
compat for deploy_qualify.py invoked standalone."""
cmd = [sys.executable, str(QUALIFY_SCRIPT)]
if sha:
cmd += ["--sha", sha]
proc = subprocess.run(
cmd,
capture_output=True, text=True, timeout=120,
env={k: v for k, v in os.environ.items() if k != "DEPLOY_SHA"}
if sha else dict(os.environ, PYTHONPATH=""),
)
out = (proc.stdout or "").strip().splitlines()
return (out[-1] if out else "pending").strip()
# ── T21.5 §4: operation-specific completion authority ──────────────────────
#
# T21.4 root cause: the wait loop had ONE hard-coded authority
# (deploy qualification = master-ref CI) while operations waited on
# branch CI, PR checks, or merges. Wrong authority => nonterminal
# returns => the MODEL had to re-arm (the generator of the sleeps).
# The wait verifier MUST match the operation being waited for.
OPERATION_SPECS = {
# op_type: {
# workflow_path: Actions run path filter (None = any),
# head_branches: head-branch filter (None = any),
# authority: which canonical verifier decides completion,
# event_types: expected wake event types,
# terminal: terminal verdicts, failure: failure verdicts,
# }
"branch_ci": {
"workflow_path": "ci.yml",
"head_branches": None,
"authority": "ci_runs", # any completed run for this SHA
"event_types": ("ci_completed",),
"terminal": ("success",),
"failure": ("failure", "cancelled"),
},
"pull_request_checks": {
"workflow_path": None, # all PR-triggered runs for SHA
"head_branches": None,
"authority": "ci_runs",
"event_types": ("ci_completed",),
"terminal": ("success",),
"failure": ("failure", "cancelled"),
},
"master_ci": {
"workflow_path": "ci.yml",
"head_branches": ("master",),
"authority": "deploy_qualify", # full two-leg qualification
"event_types": ("ci_completed",),
"terminal": ("success",),
"failure": ("failure", "cancelled"),
},
"deploy": {
"workflow_path": "deploy.yml",
"head_branches": ("master",),
"authority": "deploy_qualify",
"event_types": ("ci_completed", "deploy_completed"),
"terminal": ("success",),
"failure": ("failure", "cancelled"),
},
"release": {
"workflow_path": None,
"head_branches": ("master",),
"authority": "deploy_qualify",
"event_types": ("ci_completed", "release_completed"),
"terminal": ("success",),
"failure": ("failure", "cancelled"),
},
}
DEFAULT_OP_TYPE = "master_ci"
def normalize_verdict(verdict: str) -> str:
"""T21.5 §18 audit fix: pending-class verdicts are ALL nonterminal.
deploy_qualify emits "pending (probe failed)" for a transient probe
failure — that is pending, never a terminal answer. The wait loop
must not return control to the model over a string variant."""
v = (verdict or "").strip().lower()
if v.startswith("pending"):
return "pending"
return (verdict or "").strip()
def op_spec(op_type: str) -> dict:
spec = OPERATION_SPECS.get(op_type)
if spec is None:
raise ValueError(
f"unknown op_type {op_type!r} — must be one of "
f"{sorted(OPERATION_SPECS)} (the wait verifier must match "
"the operation being waited for)")
return spec
def op_authority_verdict(op_type: str, sha: str) -> str:
"""Canonical completion verdict FOR THIS OPERATION TYPE.
ci_runs — the op's own Actions runs for this exact SHA: every
required workflow run completed; terminal iff every
run of the op's workflow set concluded success
(branch/PR ops are their own truth; deploy
qualification is NOT their authority).
deploy_qualify — the two-leg deploy qualification (master CI +
required-checks) via deploy_qualify.py.
"""
spec = op_spec(op_type)
authority = spec["authority"]
if authority == "deploy_qualify":
v = normalize_verdict(authority_verdict(sha))
if v != "pending":
return v
# E-PATCH resilience: Gitea's combined commit-status endpoint can
# 404 for the newest commit (observed live on auto-version bump
# commits — the status object materializes late/never). The runs
# ARE the same evidence the two legs read; fall back to the
# operation-scoped runs verdict so authority stays resolvable.
if _status_endpoint_unreadable(sha):
return _ci_runs_verdict(spec, sha)
return v
if authority == "ci_runs":
return _ci_runs_verdict(spec, sha)
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
def _status_endpoint_unreadable(sha: str) -> bool:
"""True when the combined commit-status endpoint 404s for the SHA
(index lag / never-materialized status object) — the documented
fallback trigger, verified against the API once per call."""
try:
sys.path.insert(0, str(_SCRIPT_DIR))
import gitea_auth # noqa: E402
st, _body = gitea_auth.api(f"/commits/{sha}/status")
return st == 404
except Exception: # noqa: BLE001
return False
def _ci_runs_verdict(spec: dict, sha: str) -> str:
"""Operation-scoped Actions-run verdict (import the API client from
deploy_qualify to keep ONE Gitea client)."""
sys.path.insert(0, str(_SCRIPT_DIR))
import deploy_qualify as dq # noqa: E402
import gitea_auth # noqa: E402
server = os.environ.get("GITEA_SERVER",
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
repo = os.environ.get("GITEA_REPO",
getattr(gitea_auth, "DEFAULT_REPO", ""))
sha_env = sha # H-PATCH: explicit parameter is the only authority
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
if not (server and repo and sha_env and token):
return "pending" # cannot verify -> not verified (fail closed)
runs = dq._fetch_runs(server, repo, sha_env, token)
if runs is None:
return "pending" # probe failure -> keep waiting
wfp = spec["workflow_path"]
if wfp is not None:
runs = [r for r in runs
if str(r.get("path", "")).startswith(wfp + "@")]
branches = spec["head_branches"]
if branches is not None:
runs = [r for r in runs
if str(r.get("head_branch", "")) in branches]
if not runs:
return "pending" # no runs visible yet -> not terminal
for r in runs:
if r.get("status") != "completed":
return "pending" # still running
if r.get("conclusion") not in ("success", "skipped"):
return "failure" # any non-success conclusion
return "success"
def op_ci_alive(op_type: str, sha: str) -> bool:
"""Liveness probe FOR THIS OPERATION TYPE: any non-terminal run in
the op's run set (failure DETECTOR, never the scheduler)."""
sys.path.insert(0, str(_SCRIPT_DIR))
import deploy_qualify as dq # noqa: E402
import gitea_auth # noqa: E402
spec = op_spec(op_type)
server = os.environ.get("GITEA_SERVER",
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
repo = os.environ.get("GITEA_REPO",
getattr(gitea_auth, "DEFAULT_REPO", ""))
sha_env = sha # H-PATCH: explicit parameter is the only authority
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
if not (server and repo and sha_env and token):
return False
runs = dq._fetch_runs(server, repo, sha_env, token)
if not runs:
return False
wfp = spec["workflow_path"]
if wfp is not None:
runs = [r for r in runs
if str(r.get("path", "")).startswith(wfp + "@")]
branches = spec["head_branches"]
if branches is not None:
runs = [r for r in runs
if str(r.get("head_branch", "")) in branches]
terminal = {"success", "failure", "cancelled", "skipped"}
return any(str(r.get("status", "")).lower() not in terminal
for r in runs)
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
max_tries: int = 3, gap_secs: int = 4) -> str:
"""(sha is EXPLICIT — ambient env never selects the target.)"""
"""Post-wake authority read. The daemon's own wake sweep uses a
bounded event-triggered retry (4s ×3) for the job-finished-microseconds-
ago ack window; a verdict read here can hit the same window. This is a
bounded schedule, not a polling loop — 3 reads max, then back to the
event wait / watchdog."""
verdict = "pending"
for attempt in range(max_tries):
verdict = op_authority_verdict(op_type, sha)
if verdict != "pending":
return verdict
if attempt < max_tries - 1:
time.sleep(gap_secs)
return verdict
def ci_alive() -> bool:
"""LEGACY liveness probe (master-ref deploy runs) — kept for callers
predating op specs; run() uses op_ci_alive(op_type, sha)."""
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
def _set_cognition_state(op: str, state: str) -> None:
"""E-PATCH §12: stamp cognitive quiescence on the durable record so a
fresh process knows cognition is parked (WAITING_EXTERNAL) vs resumable
(RESUMABLE). Best-effort: observability never breaks the wait."""
try:
rec = ew.operation_wait(op)
if rec is None:
return
wait_file = ew.wr_root() / "waits" / f"{rec.get('wait_id')}.json"
if not wait_file.is_file():
return
data = json.loads(wait_file.read_text())
data.setdefault("ext", {})["continuation_state"] = state
wait_file.write_text(json.dumps(data, indent=1, sort_keys=True))
except (OSError, ValueError):
pass
def run(*, sha: str, deadline_secs: int,
watchdog_secs: int = 300,
op_type: str = DEFAULT_OP_TYPE,
task_id: str = "",
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
"""T21.5 §3: ONE LOGICAL AWAIT.
One logical external wait is one invocation of this function. All
nonterminal conditions (event-seen-authority-pending, watchdog expiry
with live runs, bounded authority-retry exhausted) re-arm INTERNALLY —
control returns to the caller ONLY on a terminal verdict or on real
deadline expiry. Watchdogs are failure detectors inside this
abstraction, never the scheduler and never completion truth.
"""
spec = op_spec(op_type) # unknown op_type refused upfront
op = f"ci-{sha}"
# A-PATCH: the authority subprocess reads DEPLOY_SHA from the env;
# python-level run() callers (tool normalization flow) must export it.
if sha and not os.environ.get("DEPLOY_SHA"):
os.environ["DEPLOY_SHA"] = sha
# H-PATCH: resolve short SHAs to full 40-char — _fetch_runs and
# deploy_qualify match on the FULL sha; a 12-char prefix would
# silently fail equality and park the waiter until deadline
# (the 962e351 root cause).
if sha and len(sha) < 40:
try:
full = subprocess.run(
["git", "-C", str(_SCRIPT_DIR.parent), "rev-parse", sha],
capture_output=True, text=True, timeout=10)
if full.returncode == 0:
sha = full.stdout.strip()
os.environ["DEPLOY_SHA"] = sha
except (OSError, subprocess.TimeoutExpired):
pass
t0 = time.time()
# REAL_ASYNC_BRIDGE: FIFO is replaced by completion-file inotify.
# The completion file write is the wake signal; inotify is the
# kernel-level zero-latency, zero-CPU wake primitive.
# Event replay (T16 §16C live finding): on a serialized runner the
# completion event usually arrives BEFORE the waiter registers.
# A matching durable event means the wake already happened — skip
# the rendezvous entirely and go straight to the authority read.
prior = ew.find_matching_event("gitea", "ci_completed", op)
if prior is not None and prior.get("exit_code") is not None:
rec = ew.register_process_wait(
op, project="rune",
objective=f"event-driven {op_type} qualification for {sha}",
watched_for="Gitea CI completion event (replayed from the "
"durable event log)",
provider="gitea", event_type="ci_completed", sha=sha,
watchdog_secs=min(watchdog_secs, deadline_secs),
continuation_state="RESUMABLE", # event already arrived
op_type=op_type,
authority=spec["authority"])
registered_at = rec["created_at"]
event_at = time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = event_at
# the event is a WAKE, never truth — canonical authority decides
verdict = normalize_verdict(authority_after_wake(op_type, sha))
if verdict == "pending" and not op_ci_alive(op_type, sha):
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
mode = ("event_replayed" if verdict != "pending"
else "authority_unresolved")
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
out = {
"sha": sha,
"op_type": op_type,
"authority": spec["authority"],
"registered_at": registered_at,
"event_received_at": event_at,
"controller_resumed_at": resumed_at,
"verdict_at": verdict_at,
"mode": mode,
"rearm_reason": "completion event replayed from durable log "
"(arrived before registration)",
"verdict": verdict,
"wall_secs": round(time.time() - t0, 3),
}
print(json.dumps(out))
return 0 if verdict == "success" else 1
rec = ew.register_process_wait(
op, project="rune",
objective=f"event-driven {op_type} qualification for {sha}",
watched_for="Gitea CI completion event (wake; verdict re-read "
f"from canonical {spec['authority']} authority)",
provider="gitea", event_type="ci_completed", sha=sha,
watchdog_secs=min(watchdog_secs, deadline_secs),
continuation_state="WAITING_EXTERNAL",
op_type=op_type,
authority=spec["authority"],
task_id=task_id, # §9: bind the operation to the calling task
resume_mode="self") # §3: waiter owns its continuation via
# completion-file/inotify; the daemon has NO
# session-delivery role for this wait
registered_at = rec["created_at"]
_set_cognition_state(op, "WAITING_EXTERNAL") # cognition parked
deadline = time.monotonic() + deadline_secs
verdict = "pending"
mode = "event"
event_at = ""
resumed_at = ""
rearm_reason = ""
rearm_count = 0
while time.monotonic() < deadline:
remaining = max(1, int(deadline - time.monotonic()))
# REAL_ASYNC_BRIDGE: block on inotify for the completion file.
# Zero CPU, zero arbitrary timer. The inotify timeout is a
# FAILURE DETECTOR — if no event arrives within the watchdog
# window, we check the authority directly (lost-event recovery).
# The deadline is containment only.
watch_timeout = min(remaining, watchdog_secs)
result = cf.wait_for_completion(
op, timeout_secs=watch_timeout)
# REAL_ASYNC_BRIDGE: check durable event log AND wait record
# status on EVERY cycle — the daemon may have matched and
# transitioned the record via /process-event.
wr = ew.operation_wait(op)
if wr and wr.get("status") in ("COMPLETED", "FAILED",
"READY_TO_RESUME"):
event_at = event_at or time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime())
verdict = normalize_verdict(
authority_after_wake(op_type, sha))
if verdict != "pending":
mode = "event"
break
prior = ew.find_matching_event("gitea", "ci_completed", op)
if prior is not None and prior.get("exit_code") is not None:
event_at = event_at or time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime())
verdict = normalize_verdict(
authority_after_wake(op_type, sha))
if verdict != "pending":
mode = "event"
break
if result is not None:
# completion file exists: event arrived
event_at = event_at or time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime())
verdict = normalize_verdict(
authority_after_wake(op_type, sha))
if verdict != "pending":
mode = "event"
break
rearm_count += 1
rearm_reason = ("completion file seen; authority "
"pending — re-subscribing")
continue
# inotify timeout: no event within watchdog window
# FAILURE DETECTOR: check authority directly
verdict = normalize_verdict(
op_authority_verdict(op_type, sha))
if verdict != "pending":
mode = "watchdog_authority_recovered"
break
rearm_count += 1
rearm_reason = ("watchdog expiry; op pending (not started "
"or lagging) — re-subscribing inotify")
continue
# REAL_ASYNC_BRIDGE: no FIFO to close. The completion file is durable
# and survives restarts. Lease release is not needed (no lease system
# for file-based delivery).
if verdict == "pending":
if mode == "event":
mode = "deadline_pending"
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
_set_cognition_state(op, "RESUMABLE" if verdict != "pending"
else "RECOVERY_REQUIRED")
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
# §2 STATE OWNERSHIP: the WAITER owns the OPERATION domain. On a
# terminal authority verdict, stamp the durable record terminal with
# the evidence — the daemon's session-delivery machinery must never
# (and can no longer) overwrite this truth. Also updates the parked
# continuation_state so fresh-process recovery sees reality.
try:
rec2 = ew.operation_wait(op) or rec
if verdict != "pending":
rec2["status"] = "COMPLETED" if verdict == "success" else "FAILED"
rec2["verdict"] = verdict
rec2["resume_evidence"] = {
"kind": "authority_verified_terminal",
"verdict": verdict,
"mode": mode,
"authority": spec["authority"],
"sha": sha,
"op_type": op_type,
"verdict_at": verdict_at,
}
rec2["ext"] = {**rec2.get("ext", {}),
"continuation_state": ("RESUMABLE" if verdict != "pending"
else "RECOVERY_REQUIRED")}
ew._load_waitstore().save(rec2)
except Exception as _e: # noqa: BLE001 — record stamping must never corrupt the envelope (the envelope + authority remain the truth)
print(f"ci_event_wait: terminal record stamp failed: {_e}",
file=sys.stderr)
out = {
"sha": sha,
"op_type": op_type,
"authority": spec["authority"],
"registered_at": registered_at,
"event_received_at": event_at,
"controller_resumed_at": resumed_at,
"verdict_at": verdict_at,
"mode": mode,
"rearm_reason": rearm_reason,
"internal_rearm_count": rearm_count,
"verdict": verdict,
"wall_secs": round(time.time() - t0, 3),
}
print(json.dumps(out))
return 0 if verdict == "success" else 1
KNOWN_FLAGS = ("--sha", "--deadline", "--op-type", "--watchdog", "--task-id")
def main() -> int:
"""§5 CLI CONTRACT: no silent argument loss. Every flag is either
explicitly supported (and takes effect) or loudly rejected — an
acceptance envelope must reflect the configuration actually used."""
sha = os.environ.get("DEPLOY_SHA", "")
args = sys.argv[1:]
deadline = 900
op_type = DEFAULT_OP_TYPE
watchdog = 300
task_id = ""
i = 0
while i < len(args):
a = args[i]
if a == "--sha":
sha = args[i + 1]; i += 2
elif a == "--deadline":
deadline = int(args[i + 1]); i += 2
elif a == "--op-type":
op_type = args[i + 1]; i += 2
elif a == "--watchdog":
watchdog = int(args[i + 1]); i += 2
elif a == "--task-id":
task_id = args[i + 1]; i += 2
elif a.startswith("--"):
print(json.dumps({"error": f"unknown flag {a!r}",
"known_flags": list(KNOWN_FLAGS),
"contract": "no silent argument loss"}))
return 2
else:
print(json.dumps({"error": f"unexpected positional {a!r}",
"known_flags": list(KNOWN_FLAGS)}))
return 2
if not sha:
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
return 1
# A-PATCH fix: run()'s authority subprocess (deploy_qualify.py) reads
# DEPLOY_SHA from the environment — a CLI `--sha` consumer previously
# got a silent "pending" verdict forever because the var was never
# exported. Export it so the canonical authority can resolve.
os.environ["DEPLOY_SHA"] = sha
return run(sha=sha, deadline_secs=deadline, op_type=op_type,
watchdog_secs=watchdog, task_id=task_id)
if __name__ == "__main__":
sys.exit(main())
+195
View File
@@ -0,0 +1,195 @@
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
with inotify wake. Replaces FIFO+lease for process-wait delivery.
DESIGN:
Producer (bridge/daemon/notify):
1. Validates operation identity
2. Atomically writes a completion file:
resumes/<operation_id>.done
containing the event/verdict JSON (write to tmp, rename for atomicity)
Waiter (ci_event_wait / any continuation consumer):
1. Checks if the completion file already exists (event-before-waiter)
2. If not: registers inotify watch on the resumes/ directory
3. Re-checks after watch registration (closes the registration race)
4. Blocks in kernel on inotify — zero CPU, zero timer
5. On file-created event: reads the file, verifies authority, returns
The event is a WAKE SIGNAL. Authority still decides terminal truth.
The completion file is durable — survives daemon and waiter restarts.
WHY NOT FIFO:
FIFO is one-shot (token consumed), requires lease management, and
fails under session-busy guards. A completion file is atomic, durable,
and works across restarts. inotify wakes the process with zero CPU
cost and zero latency between file creation and wake.
WHY NOT POLLING:
Polling introduces an arbitrary interval. inotify is a kernel callback
that fires exactly when the filesystem changes — the latency tracks
the real completion event, not a configured interval.
"""
from __future__ import annotations
import json
import os
import select
import struct
import time
from pathlib import Path
def _wr_bridge() -> Path:
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
# and any isolated test root always resolve the SAME directory.
root = (os.environ.get("WR_ROOT")
or os.environ.get("RUNE_WR_ROOT")
or "/mnt/workdrive/wr-bridge")
return Path(root)
def _resumes_dir() -> Path:
return _wr_bridge() / "resumes"
# inotify constants (Linux)
IN_CREATE = 0x100
IN_CLOSE_WRITE = 0x8
IN_MOVED_TO = 0x80
def completion_path(operation_id: str) -> Path:
return _resumes_dir() / f"{operation_id}.done"
def write_completion(operation_id: str, event: dict) -> Path:
"""Atomically write a durable completion file. The rename(2) ensures
the waiter never sees a partial file."""
_resumes_dir().mkdir(parents=True, exist_ok=True)
target = completion_path(operation_id)
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
os.rename(str(tmp), str(target)) # atomic on same filesystem
os.chmod(str(target), 0o666) # cross-user bridge
return target
def read_completion(operation_id: str) -> dict | None:
"""Read a durable completion file. Returns None if absent."""
p = completion_path(operation_id)
if not p.is_file():
return None
try:
return json.loads(p.read_text())
except (OSError, ValueError):
return None
def completion_exists(operation_id: str) -> bool:
return completion_path(operation_id).is_file()
def _inotify_init_and_add_watch(directory: str) -> int:
"""Create an inotify instance and add a watch on the directory.
Returns the inotify fd."""
libc = __import__("ctypes").CDLL(None, use_errno=True)
fd = libc.inotify_init()
if fd < 0:
raise OSError("inotify_init failed")
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
mask = IN_CREATE | IN_MOVED_TO
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
if wd < 0:
os.close(fd)
raise OSError(f"inotify_add_watch failed for {directory}")
return fd
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
"""Read pending inotify events. Returns list of filenames that were
created/moved-to. Blocks until at least one event or timeout."""
poll = select.poll()
poll.register(fd, select.POLLIN)
events = []
buf = os.read(fd, 4096)
pos = 0
while pos < len(buf):
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
pos += 16
name = buf[pos:pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
pos += name_len
if mask & (IN_CREATE | IN_MOVED_TO):
events.append(name)
return events
def wait_for_completion(operation_id: str, *,
timeout_secs: float | None = None,
poll_callback=None) -> dict | None:
"""Block until a completion file exists for this operation.
Uses inotify for zero-latency, zero-CPU wake. Falls back to
polling_callback (if provided) when inotify is unavailable.
The completion file is a WAKE SIGNAL. The caller must still verify
canonical authority after reading it.
Returns the event dict from the completion file, or None on timeout.
"""
target = completion_path(operation_id)
# 1. check-before-watch (event may have arrived before we started)
existing = read_completion(operation_id)
if existing is not None:
return existing
_resumes_dir().mkdir(parents=True, exist_ok=True)
try:
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
except OSError:
# inotify unavailable: fall back to polling callback
if poll_callback:
return poll_callback(operation_id, timeout_secs)
return None
try:
# 2. re-check after watch registration (closes the race between
# the initial check and the watch being active)
existing = read_completion(operation_id)
if existing is not None:
return existing
# 3. block in kernel on inotify — zero CPU, exact latency
import select as _select
poll = _select.poll()
poll.register(fd, _select.POLLIN)
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
while True:
remaining = None
if deadline is not None:
remaining = max(0.1, deadline - time.monotonic())
if remaining <= 0.1:
return None # timeout
events = poll.poll(remaining * 1000 if remaining else None)
if not events:
continue # poll timeout, re-check
names = _inotify_read(fd)
if target.name in names:
# 4. file appeared — read it
existing = read_completion(operation_id)
if existing is not None:
return existing
# partial/corrupt: keep watching
finally:
os.close(fd)
return None
def clear_completion(operation_id: str) -> None:
"""Remove a consumed completion file (idempotent cleanup)."""
p = completion_path(operation_id)
try:
p.unlink()
except OSError:
pass
+123
View File
@@ -0,0 +1,123 @@
"""construction_guards.py — deterministic guards for observed process
defects (SCAFFOLD-TRANCHE-006 §21-22).
Two real defect classes from recent sessions:
A. DURABLE-STORE MUTATION WITHOUT PERSISTENCE (Tranche 5: closure script
mutated the contract in memory, never called save(); the next read saw
stale state). Guard: a durable-boundary operation verifies by
RELOAD/READBACK, not by successful in-memory mutation.
B. BLIND SOURCE-EDIT NO-OP (multiple sessions: python str.replace() on
ruff-reformatted files silently matched nothing; the script reported
success while changing nothing). Guard: an automated edit verifies its
POSTCONDITION — expected text present / old text absent — and a
zero-match replacement is an explicit, truthful no-op, never success.
Not a generic editor framework. Two small, deterministic postcondition
checks at the boundaries where the defects actually occurred.
"""
from __future__ import annotations
import json
from pathlib import Path
__all__ = ["verify_persisted", "assert_persisted", "apply_source_edit", "EditNoOp", "EditPostconditionFailed"]
class EditNoOp(Exception):
"""The edit matched zero targets — an explicit, truthful no-op."""
class EditPostconditionFailed(Exception):
"""The edit claimed success but the expected postcondition is absent."""
# ── Guard A: durable mutation verified by readback ──────────────────────────
def verify_persisted(path: Path | str, selector, expect) -> bool:
"""Reload the durable store from disk and check the expectation against
the RELOADED state. `selector(reloaded_doc) -> value`; pass == value
equals `expect` (or expect(value) is truthy when callable).
The reloaded document is parsed as JSON when the file is JSON-shaped;
otherwise the raw text is passed to the selector (plain-text stores).
This is the Tranche 5 defect guard: an in-memory mutation that was
never save()d fails here, because the disk state is what gets read.
"""
raw = Path(path).read_text(encoding="utf-8")
try:
doc = json.loads(raw)
except ValueError:
doc = raw
value = selector(doc)
if callable(expect):
return bool(expect(value))
return value == expect
def assert_persisted(path: Path | str, selector, expect, *, what: str = "durable state") -> None:
"""verify_persisted that raises on mismatch — for use right after a
save() at durable boundaries."""
if not verify_persisted(path, selector, expect):
raise AssertionError(
f"persistence verification FAILED for {what}: reloaded state "
f"does not match expectation — the mutation may not have been "
f"saved (Tranche 5 defect class A)"
)
# ── Guard B: source edit with postcondition ─────────────────────────────────
def apply_source_edit(
text: str,
old: str,
new: str,
*,
expected_count: int | None = None,
postcondition: str | None = None,
allow_noop: bool = False,
) -> str:
"""Apply a source edit WITH postcondition verification (Guard B).
- zero matches: raises EditNoOp (truthful no-op) unless allow_noop=True
- expected_count given: match count must equal it
- postcondition given: the RESULT must contain it, else
EditPostconditionFailed
Returns the new text. Never claims success for a change that did not
happen."""
count = text.count(old)
if count == 0:
if allow_noop:
return text # truthful, explicit no-op — caller records it
raise EditNoOp(
f"edit matched 0 targets — nothing changed (Tranche 5 defect "
f"class B: str.replace silently no-ops on reformatted files)"
)
if expected_count is not None and count != expected_count:
raise EditPostconditionFailed(f"edit matched {count} targets, expected {expected_count}")
result = text.replace(old, new)
if postcondition and postcondition not in result:
raise EditPostconditionFailed(
"postcondition text absent from result — edit did not achieve its expected effect"
)
return result
def apply_source_edit_file(
path: Path | str, old: str, new: str, *, expected_count: int | None = None, postcondition: str | None = None
) -> int:
"""File-level convenience: read, apply_source_edit, write back, then
VERIFY BY READBACK (both guards composed). Returns match count."""
p = Path(path)
text = p.read_text(encoding="utf-8")
result = apply_source_edit(text, old, new, expected_count=expected_count, postcondition=postcondition)
p.write_text(result, encoding="utf-8")
# Guard A composed: the written file must contain the postcondition
if postcondition:
assert_persisted(p, lambda t: t, lambda t: postcondition in t, what=f"edit of {p.name}")
return text.count(old)
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""deploy_promotion.py — monotonic promotion authority for Rune deploys.
SCAFFOLD contract DEPLOY-MONOTONIC-001. Root cause of the 2026-09-13
deploy-order race (deploys 7355/7359): deploy_stage.py promoted
unconditionally in job-finish order — no comparison against the currently
deployed commit, no lock, no truthful outcome for a stale candidate.
This module owns the PROMOTION decision. The invariant:
current never regresses along the canonical release branch.
Ordering authority: GIT ANCESTRY on the canonical release branch
(refs/heads/master). Rune's release flow is linear history — every deploy
candidate is a master push (tranche commit or its auto-bump descendant).
candidate == deployed commit -> NOOP_ALREADY_CURRENT (idempotent)
candidate is DESCENDANT of it -> allowed (normal forward deploy)
candidate is ANCESTOR of it -> STALE_SUPERSEDED (refuse; truthful)
no ancestry relationship -> REFUSED_DIVERGENT (fail closed)
deployed commit unknown/unreadable-> REFUSED_UNKNOWN_CURRENT (fail closed)
Mechanism (smallest that provides the invariant, per brief §4-5):
1. promotion-time stale check — ancestry comparison against the commit
current ACTUALLY points to at promotion time (not at job start);
2. an flock'd critical section around the read-compare-write of
current/last-good so two concurrent promotions cannot interleave
between the ancestry check and the symlink swap.
Workflow-level concurrency is NOT relied on (brief §5): the fence lives at
the promotion operation itself.
Truthful outcomes (brief §8) — build/stage success != promotion success:
PROMOTED candidate became current (verified below)
NOOP_ALREADY_CURRENT same commit already deployed; nothing mutated
STALE_SUPERSEDED a newer commit is already current; nothing mutated
REFUSED_DIVERGENT ordering unprovable; fail closed
REFUSED_UNKNOWN_CURRENT deployed commit unreadable; fail closed
FAILED_* verification/staging failures (in deploy_stage)
A stale or refused outcome is a NON-ERROR terminal state for the release
pipeline (the newer build simply won) but MUST NOT print "PROMOTED" and
MUST NOT mutate current/last-good. deploy_stage's exit code and the
workflow's post-deploy runtime verification enforce this: the workflow
asserts current == candidate after a PROMOTED outcome, so a stale outcome
makes the job report honestly rather than masquerade.
"""
from __future__ import annotations
import fcntl
import json
import os
import subprocess
from pathlib import Path
DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
RELEASE_BRANCH = "refs/heads/master"
OUTCOME_PROMOTED = "PROMOTED"
OUTCOME_NOOP = "NOOP_ALREADY_CURRENT"
OUTCOME_STALE = "STALE_SUPERSEDED"
OUTCOME_DIVERGENT = "REFUSED_DIVERGENT"
OUTCOME_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
# ── pure decision logic (unit-tested; no I/O) ──────────────────────────────
def decide_promotion(candidate_sha: str, deployed_sha: str, ancestry: str) -> str:
"""Pure promotion decision from a precomputed ancestry relationship.
ancestry is one of: 'same' | 'descendant' | 'ancestor' | 'divergent' |
'unknown'. Fail-closed: anything not provably allowed is refused."""
if candidate_sha == deployed_sha or ancestry == "same":
return OUTCOME_NOOP
if ancestry == "descendant":
return OUTCOME_PROMOTED
if ancestry == "ancestor":
return OUTCOME_STALE
if ancestry == "divergent":
return OUTCOME_DIVERGENT
return OUTCOME_UNKNOWN_CURRENT
def ancestry_of(candidate_sha: str, deployed_sha: str, repo: Path, fetch_on_divergent: bool = True) -> str:
"""Classify candidate vs deployed using git merge-base --is-ancestor on
the canonical release branch's repository. Returns 'unknown' on any git
failure (fail-closed callers treat that as refusal).
Observed live 2026-09-13 (deploy 7449, SHA 535c2909): a DELAYED older
candidate's checkout cannot contain commits newer than itself — its
local object store lacks the deployed commit entirely, so ancestry
classifies 'divergent' and fail-closed refuses. Correct refusal, wrong
reason: the candidate IS an ancestor. When the local classification is
divergent, fetch origin's canonical branch first (best-effort; the
deploy workflow's checkout has credentials configured) and re-classify.
If still divergent afterwards, the refusal stands with better
evidence."""
def _is_ancestor(a: str, b: str) -> bool:
r = subprocess.run(
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "merge-base", "--is-ancestor", a, b],
capture_output=True,
text=True,
)
return r.returncode == 0
if candidate_sha == deployed_sha:
return "same"
if _is_ancestor(candidate_sha, deployed_sha):
return "ancestor" # candidate is OLDER than what's deployed
if _is_ancestor(deployed_sha, candidate_sha):
return "descendant" # candidate is NEWER (normal forward deploy)
if fetch_on_divergent:
try:
subprocess.run(
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "fetch", "origin", "master", "--quiet"],
capture_output=True,
text=True,
timeout=60,
)
except (OSError, subprocess.TimeoutExpired):
pass # best-effort: classification below still fails closed
if _is_ancestor(candidate_sha, deployed_sha):
return "ancestor"
if _is_ancestor(deployed_sha, candidate_sha):
return "descendant"
return "divergent"
def read_deployed_commit(deploy_root: Path = DEPLOY_ROOT) -> str:
"""Read the commit the `current` symlink ACTUALLY points to right now —
from the deployed build identity, the deployment authority (never from
a stale job-start artifact). Empty string if unreadable."""
current = deploy_root / "current"
try:
target = Path(os.readlink(current))
except OSError:
return ""
identity = target / "src" / "rune" / "_build_identity.json"
try:
data = json.loads(identity.read_text(encoding="utf-8"))
return str(data.get("commit", ""))
except (OSError, ValueError):
return ""
# ── the fenced promotion operation ──────────────────────────────────────────
def promote(
candidate_sha: str,
stage: Path,
tmp_stage: Path,
repo: Path,
deploy_root: Path = DEPLOY_ROOT,
on_promoted=None,
) -> tuple[str, str]:
"""Atomically decide + promote under an exclusive lock.
Returns (outcome, detail). ONLY the PROMOTED outcome mutates
current/last-good, and it does so inside the same critical section as
the decision — no interleaving window between check and swap.
Crash-safety ordering (brief §16): the stage rename happens BEFORE the
lock (a staged-but-unpromoted tree is inert), the symlink swap is a
single rename(2) via a temporary link, so current is never absent and
never half-updated. last-good is refreshed BEFORE the swap only from a
value that was genuinely current a moment ago.
"""
deploy_root.mkdir(parents=True, exist_ok=True)
lock_path = deploy_root / ".promotion.lock"
with open(lock_path, "w") as lock_fd:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
try:
deployed = read_deployed_commit(deploy_root)
if not deployed:
return (OUTCOME_UNKNOWN_CURRENT, "deployed commit unreadable — fail closed")
rel = ancestry_of(candidate_sha, deployed, repo)
outcome = decide_promotion(candidate_sha, deployed, rel)
if outcome != OUTCOME_PROMOTED:
return (outcome, f"deployed={deployed[:12]} ancestry={rel}")
# --- mutate (still inside the lock) ---
current = deploy_root / "current"
last_good = deploy_root / "last-good"
prev_target = os.readlink(current) if current.is_symlink() else ""
if prev_target and (deploy_root / "last-good").is_symlink():
(deploy_root / "last-good").unlink()
if prev_target:
_atomic_symlink(deploy_root, "last-good", prev_target)
if stage.exists():
# re-deploying an already-staged SHA: restage over it
import shutil
shutil.rmtree(stage)
os.rename(tmp_stage, stage)
_atomic_symlink_target(deploy_root, "current", stage)
detail = f"current -> {stage.name}"
if on_promoted is not None:
# §12C: race-safe convergence — the WINNER refreshes release
# history atomically with its promotion, inside the lock.
# Failure degrades to STALE (software valid, history lags;
# retry by any later deploy converges idempotently).
try:
on_promoted(candidate_sha)
detail += " (+ release-history converged)"
except Exception as exc: # noqa: BLE001
detail += f" (release-history refresh FAILED: {exc!s:.120} — STALE, retry converges)"
return (OUTCOME_PROMOTED, detail)
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
def _atomic_symlink(deploy_root: Path, name: str, target: str) -> None:
"""Symlink swap that never leaves `name` absent: write temp link, rename
over the final name (rename(2) is atomic on the same filesystem)."""
_atomic_symlink_target(deploy_root, name, Path(target))
def _atomic_symlink_target(deploy_root: Path, name: str, target: Path) -> None:
tmp = deploy_root / f".{name}.swap-{os.getpid()}"
if tmp.is_symlink() or tmp.exists():
tmp.unlink()
tmp.symlink_to(target)
os.replace(tmp, deploy_root / name)
+143 -21
View File
@@ -6,6 +6,16 @@ has no embedded multi-line scripts — a YAML block-scalar indentation hazard).
Prints one of: success | failure | pending
Exit code 0 always (the shell decides what to do with the verdict).
RELEASE-SAFETY HARDENING (2026-09-13, after red-CI promotions 0.66.28/29):
the verdict is the AND of two independent legs —
1. required-checks context (newest observation for this exact SHA);
2. the latest COMPLETED ci.yml@refs/heads/master run for this exact SHA,
every job concluded success (branch+tag fan-out resolved by path;
jobs envelope unwrapped — no aggregate-commit-status fallback, the
exact bug class verify.py hit in W/R-v0.3).
Any disagreement, pending, or missing evidence → pending/failure.
FAIL-CLOSED: not verified → not deployable.
"""
from __future__ import annotations
@@ -20,34 +30,146 @@ if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
REQUIRED_CONTEXT = "CI Pipeline / required-checks"
CI_WORKFLOW_PATH = "ci.yml@refs/heads/master"
# ── pure verdict logic (unit-tested; fixtures from the real incidents) ────
def required_check_verdict(statuses: list[dict]) -> str:
"""Newest non-skipped required-checks observation for the SHA.
Missing → pending. Newest wins (stale success never overrides a newer
failure, and vice versa)."""
candidates = [
s for s in statuses
if str(s.get("context", "")).startswith(REQUIRED_CONTEXT)
and s.get("status") != "skipped"
]
if not candidates:
return "pending"
newest = max(candidates, key=lambda s: str(s.get("created_at", "")))
return str(newest.get("status", "pending"))
def independent_jobs_verdict(runs: list[dict], jobs_fetch, sha: str = "") -> str:
"""Latest COMPLETED ci.yml@refs/heads/master run for the exact SHA:
every job must conclude success. Fan-out-safe (tag runs and sibling
workflows are excluded by path); the run is resolved by head_sha, never
by 'latest branch state'. Missing run → pending. Any non-success
conclusion → failure."""
mine = [r for r in runs if r.get("path") == CI_WORKFLOW_PATH]
if sha:
mine = [r for r in mine if str(r.get("head_sha", "")) == sha]
completed = [r for r in mine if r.get("status") == "completed"]
if not completed:
return "pending"
latest = max(completed, key=lambda r: str(r.get("created_at", "") or r.get("run_started_at", "")))
jobs = jobs_fetch(latest.get("id"))
if jobs is None:
return "pending" # jobs probe failed → not verified
if not jobs:
return "pending" # no jobs visible → cannot verify
for j in jobs:
if j.get("conclusion") != "success":
return "failure"
return "success"
def combine(a: str, b: str) -> str:
"""Fail-closed conjunction: any failure → failure; any pending → pending;
success only when BOTH legs are success."""
if "failure" in (a, b):
return "failure"
if "pending" in (a, b):
return "pending"
return "success"
def qualify(statuses: list[dict], runs: list[dict], jobs_fetch, sha: str = "") -> str:
return combine(required_check_verdict(statuses),
independent_jobs_verdict(runs, jobs_fetch, sha=sha))
# ── API fetches (thin; injectable in tests) ───────────────────────────────
def _get_json(url: str, token: str, timeout: int = 30):
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except (OSError, ValueError):
return None
def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
data = _get_json(f"{server}/api/v1/repos/{repo}/commits/{sha}/status", token)
return None if data is None else data.get("statuses", [])
def _fetch_runs(server: str, repo: str, sha: str, token: str,
max_pages: int = 4) -> list[dict] | None:
"""H-PATCH: paginate the runs list. The repo's scheduled merge-robot
runs flood the first page (~50 slots exhaust within hours), pushing
older-but-relevant SHAs to later pages — the cause of the observed
authority flapping (terminal commit read as pending). Paginate until
the sha is found or max_pages exhausted."""
out: list[dict] = []
for page in range(1, max_pages + 1):
data = _get_json(
f"{server}/api/v1/repos/{repo}/actions/runs?limit=50&page={page}",
token)
if data is None:
return None if page == 1 else (out or None)
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
out.extend(r for r in runs if str(r.get("head_sha", "")) == sha)
if len(runs) < 50:
break # exhausted
return out
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs/{run_id}/jobs", token)
if data is None:
return None
# jobs envelope unwrap (this Gitea wraps in {"jobs": [...]}; some
# versions return a bare list) — never fall back to aggregate status
if isinstance(data, dict):
return data.get("jobs", [])
return data if isinstance(data, list) else None
# ── CLI ───────────────────────────────────────────────────────────────────
def main() -> int:
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
sha = os.environ.get("DEPLOY_SHA", "")
# T21.4 fix: fall back to the canonical defaults when the caller did
# not export GITEA_SERVER/GITEA_REPO (CI always exports them; local
# and first-class-tool consumers previously got a silent "pending"
# verdict — a wrong answer, not a fail-closed one).
# H-PATCH H4: the target sha arrives EXPLICITLY (--sha) from callers
# that hold structured operation state. DEPLOY_SHA env remains only
# for standalone/workflow backward compat — the canonical path never
# depends on ambient env for operation identity.
ap = __import__("argparse").ArgumentParser()
ap.add_argument("--sha", dest="sha_arg", default=None)
args, _unknown = ap.parse_known_args()
server = (os.environ.get("GITEA_SERVER", "")
or gitea_auth.DEFAULT_SERVER).rstrip("/")
repo = os.environ.get("GITEA_REPO", "") or gitea_auth.DEFAULT_REPO
sha = args.sha_arg or os.environ.get("DEPLOY_SHA", "")
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
if not (server and repo and sha and token):
print("pending") # missing config → cannot verify → not qualified
return 0
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
except (OSError, ValueError) as exc:
# transient API/network failure → keep polling (urllib raises
# URLError (OSError subclass); bad JSON raises ValueError)
print(f"pending (status probe failed: {exc})")
statuses = _fetch_status(server, repo, sha, token)
runs = _fetch_runs(server, repo, sha, token)
if statuses is None or runs is None:
# transient API/network failure → keep polling
print("pending (probe failed)")
return 0
required = [
s
for s in data.get("statuses", [])
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
]
if not required:
print("pending")
else:
print(required[0].get("status", "pending"))
jobs_fetch = lambda run_id: _fetch_jobs(server, repo, run_id, token) # noqa: E731
verdict = qualify(statuses, runs, jobs_fetch, sha=sha)
print(verdict)
return 0
+137 -24
View File
@@ -3,12 +3,15 @@
Called by .gitea/workflows/deploy.yml. Stages src/ into
/mnt/workdrive/rune-code/<short-sha>, stamps _build_identity.json, verifies
the staged tree (import + SHA assert + authority smoke), then ATOMICALLY
promotes the `current` symlink and records `last-good` (safe rollback:
the previous deployment remains available).
the staged tree (import + SHA assert + authority smoke), then promotes
through the MONOTONIC promotion authority (deploy_promotion.py): an older
candidate finishing after a newer promoted one is fenced out (STALE_SUPERSEDED)
and reports truthfully — build/stage success != promotion success.
Exit 0 = deployed and verified. Non-zero = deployment refused; the previous
deployment stays current.
Exit 0 = PROMOTED and verified, or NOOP_ALREADY_CURRENT, or a truthful
STALE_SUPERSEDED (newer build already current; nothing mutated).
Non-zero = deployment refused (divergent/unknown ordering, verification
failure); the previous deployment stays current.
"""
from __future__ import annotations
@@ -25,6 +28,92 @@ DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
REPO_ROOT = Path(__file__).resolve().parent.parent
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
sys.path.insert(0, str(REPO_ROOT / "scripts"))
sys.path.insert(0, str(REPO_ROOT / "src"))
import deploy_promotion # noqa: E402
from rune.release_notes import ( # noqa: E402
ReleaseNote as _ReleaseNote,
auto_initial_note as _auto_note,
converged_deployed_sha as _authority_sha,
convergence_refresh as _refresh,
fetch_release as _fetch_release,
publish_release as _publish,
sha_from_body as _sha_from_body,
summary_from_body as _summary_from_body,
)
def publish_out_of_lock(short: str, sha: str, *,
deploy_root: Path = DEPLOY_ROOT,
scaffold_store: Path | None = None) -> None:
"""§13D/§14B: release-history publication AFTER the promotion lock is
released. Network I/O never holds the flock.
§14B INVARIANT: publication does NOT trust the originating job's SHA.
The CURRENT deployment authority is re-read immediately before
publishing; the note is constructed from CURRENT authority. A stale
job whose candidate was superseded converges the NEW authority (or
refuses a version it cannot truthfully describe) — it may help
converge B, but may never reassert itself as deployed.
Failure degrades to STALE (software valid, history lags; retry
converges)."""
try:
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
token = os.environ.get("GITEA_TOKEN", "")
if not (server and repo and token):
return
# §14B: re-read CURRENT authority — never trust the job's SHA.
authority_sha = _authority_sha(str(deploy_root))
if not authority_sha:
print("WARNING: release-history convergence skipped: "
"deployment authority unreadable — STALE, retry converges")
return
store = str(scaffold_store or (REPO_ROOT / "store" / "scaffold" / "current.json"))
identity = deploy_root / authority_sha[:12] / "src" / "rune" / "_build_identity.json"
try:
version = json.loads(identity.read_text()).get("version", "")
except (OSError, ValueError):
version = ""
if not version:
print("WARNING: release-history convergence skipped: "
"no version on current authority — STALE, retry converges")
return
tag = f"v{version}"
existing = _fetch_release(server, repo, token, tag)
if existing is None:
note = _auto_note(
version=version,
summary=(f"Rune v{version} — evidence-derived release "
f"history (automatic convergence)"),
scaffold_store=store,
release_sha=authority_sha, deployed_sha=authority_sha,
provenance=["automatic convergence publish "
"(deploy workflow, authority-derived)"])
else:
note = _refresh(
_ReleaseNote.from_dict({
"version": version,
"summary": _summary_from_body(existing.get("body", "")),
"release_sha": _sha_from_body(existing.get("body", ""), "tag")
or authority_sha,
"provenance": [],
}),
deployed_sha=authority_sha, scaffold_store=store,
extra_provenance="automatic convergence publish "
"(deploy workflow, authority-derived)")
# preserve the tag SHA the stored body carries
tag_sha = _sha_from_body(existing.get("body", ""), "tag")
if tag_sha:
note.release_sha = tag_sha
result = _publish(server, repo, token, note)
print(f"release-history convergence: "
f"{result.get('action', 'unknown')} "
f"(authority {authority_sha[:12]})")
except Exception as conv_exc: # noqa: BLE001 — history STALE, software valid
print(f"WARNING: release-history convergence failed: "
f"{conv_exc!s:.200} — STALE, retry converges")
def main() -> int:
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
@@ -119,25 +208,49 @@ print("VERIFIED:", json.dumps(info))
shutil.rmtree(tmp_stage, ignore_errors=True)
return 1
# --- promote (atomic) + last-known-good ------------------------------
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
current = DEPLOY_ROOT / "current"
if current.is_symlink():
last_good = DEPLOY_ROOT / "last-good"
if last_good.is_symlink():
last_good.unlink()
last_good.symlink_to(os.readlink(current))
if current.is_symlink() or current.exists():
current.unlink()
if stage.exists():
shutil.rmtree(stage) # re-deploying an already-staged SHA: restage
os.rename(tmp_stage, stage)
current.symlink_to(stage)
print(f"PROMOTED: {short}")
print("current ->", os.readlink(current))
if (DEPLOY_ROOT / "last-good").is_symlink():
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
return 0
# --- promote: FENCED through the promotion authority -----------------
# SCAFFOLD contract DEPLOY-MONOTONIC-001: promotion is monotonic. The
# ancestry check + lock live in deploy_promotion.py; this script only
# handles the truthful outcome reporting. A stale candidate must NOT
# claim promotion success — build/stage success != promotion success.
#
# §14C: NO separate convergence-intent ledger. The durable pending-
# convergence state IS the deployment authority itself (atomically
# swapped `current` symlink + stamped build identity): publication
# derives from it, idempotently, outside the lock. The old intent
# file had exactly one writer and zero readers, non-atomic writes,
# and a bogus recorded_at (SECONDS is a bash builtin, never exported
# — always "0"). Deriving from authority gives atomicity, restart
# readability, corruption immunity, monotonicity (stale publishers
# read CURRENT authority, not their own job) and idempotent retry
# for free. REMOVE more complexity than you add.
outcome, detail = deploy_promotion.promote(
sha, DEPLOY_ROOT / short, tmp_stage, repo)
# §13D: publish release history OUTSIDE the promotion lock (network
# I/O must not hold the flock). Only for PROMOTED outcomes.
if outcome == deploy_promotion.OUTCOME_PROMOTED:
publish_out_of_lock(short, sha)
if outcome == deploy_promotion.OUTCOME_PROMOTED:
print(f"PROMOTED: {short}")
print("current ->", os.readlink(DEPLOY_ROOT / "current"))
if (DEPLOY_ROOT / "last-good").is_symlink():
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
return 0
if outcome == deploy_promotion.OUTCOME_NOOP:
print(f"NOOP_ALREADY_CURRENT: {short} is the deployed commit")
print(detail)
return 0
if outcome == deploy_promotion.OUTCOME_STALE:
print(f"STALE_SUPERSEDED: {short} is older than the deployed commit — promotion refused (monotonic promotion)")
print(detail)
print("current unchanged:", os.readlink(DEPLOY_ROOT / "current"))
return 0 # truthful non-error terminal state: the newer build won
# divergent / unknown-current: fail closed
print(f"REFUSED ({outcome}): {short} — {detail}", file=sys.stderr)
return 1
if __name__ == "__main__":
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""dichotomy_audit.py — the honest completion instrument for the dichotomy rebase.
Function-level, not directory-level: a module counts as covered only when its
PYTHON FUNCTIONS have identifiable counterparts in the Go/Rust implementation.
A Go directory with 3 structs and none of the module's 70 functions is a
facade, and this audit names it as one.
Matching rule (name-preserving ports): a Python function name matches when
its snake_case form, or its Go/Rust camelCase/PascalCase form, appears in the
candidate implementation sources. Deliberately strict — novel Go names that
don't echo the Python names do NOT count, because unverifiable "trust me,
it's in there somewhere" is exactly the failure mode this instrument exists
to kill.
Usage: python3 scripts/dichotomy_audit.py [--write]
--write regenerates docs/dichotomy-rebase-honest-audit.md
"""
from __future__ import annotations
import ast
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src" / "rune"
GO = ROOT / "src" / "go"
RUST = ROOT / "src" / "rust"
REPORT = ROOT / "docs" / "dichotomy-rebase-honest-audit.md"
CLASSIFICATION = ROOT / "store" / "rebase" / "classification.json"
TEST_PREFIXES = ("test_",)
# Modules that stay Python by decision (test infra stays pytest)
TEST_INFRA = {
"conftest", "test_truth", "test_adaptive_router", "test_canaries",
}
RETIRES = {"memory_env", "__main__"}
def snake_to_camel(name: str) -> list[str]:
"""Candidate identifier forms for a Python name in Go/Rust sources."""
parts = name.split("_")
camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
pascal = "".join(p.capitalize() for p in parts)
return [name, camel, pascal]
def module_functions(path: Path) -> list[str]:
"""Every function and method name defined in the module."""
try:
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
except SyntaxError:
return []
names = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name.startswith("__") and node.name.endswith("__"):
continue # dunders are runtime machinery, not port surface
names.append(node.name)
return names
def load_classification() -> dict:
if CLASSIFICATION.exists():
return json.loads(CLASSIFICATION.read_text())
return {}
def implementation_corpus() -> dict[str, str]:
"""module-kebab -> concatenated Go/Rust source text for that module."""
corpus: dict[str, str] = {}
for d in sorted(GO.iterdir()):
if not d.is_dir():
continue
chunks = []
for f in d.rglob("*.go"):
if f.name.endswith("_test.go"):
continue
chunks.append(f.read_text(encoding="utf-8", errors="replace"))
if chunks:
corpus[d.name] = "\n".join(chunks)
# Survivor packages (per-crate Go membranes under cognition/)
cog = GO / "cognition"
if cog.exists():
for d in sorted(cog.iterdir()):
if d.is_dir():
chunks = [f.read_text(encoding="utf-8", errors="replace")
for f in d.rglob("*.go") if not f.name.endswith("_test.go")]
if chunks:
corpus[f"cognition-{d.name}"] = "\n".join(chunks)
for d in sorted(RUST.iterdir()):
src = d / "src"
if src.exists():
chunks = [f.read_text(encoding="utf-8", errors="replace")
for f in src.rglob("*.rs")]
if chunks:
corpus[d.name] = "\n".join(chunks)
return corpus
def module_to_keys(module_stem: str) -> list[str]:
"""Candidate implementation keys for a Python module stem."""
kebab = module_stem.replace("_", "-")
keys = [kebab, module_stem]
# Deep ports and multi-module binaries register aliases:
aliases = {
"rune_operator": ["operator", "operator-deep", "operator-support", "core-server"],
"rune_tui": ["tui"],
"rune_operator_cli": ["operator"],
"memory_store": ["memory-ops", "memory-store-deep"],
"warm_router": ["warm-router", "operator-support"],
"tool_executor": ["tool-executor", "local-tool-runtime", "tool-protocol", "tool-protocol-rw"],
"local_tool_runtime": ["local-tool-runtime", "operator"],
"provider_registry": ["provider-registry", "model-connect"],
"adaptive_router": ["adaptive-router", "operator-support"],
"hook_engine": ["hook-engine"],
"event_bus": ["event-bus"],
"event_envelope": ["event-bus"],
"message_queue": ["message-queue"],
"subsystem_health": ["subsystem-health"],
"parallel_processor": ["parallel-processor"],
"connection_pool": ["connection-pool"],
"recall_router": ["recall-router"],
"cron_engine": ["cron-engine"],
"cache": ["cache"],
"night_mode": ["night-mode"],
"work_manager": ["work-manager"],
"failure_auto_filer": ["failure-filer"],
"microkernel": ["microkernel"],
"construction_interface": ["cognition-cognition", "cognition"],
"construction_decision": ["cognition-cognition", "cognition"],
"rune_spine": ["cognition-spine", "cognition"],
"spine": ["cognition-spine", "cognition"],
"memory_storage": ["cognition-storage", "cognition"],
"memory_retrieval": ["cognition-retrieval", "cognition"],
"scaffold_core": ["cognition-scaffold", "cognition"],
"evidence_provenance": ["cognition-provenance", "cognition"],
"memory_scoring": ["cognition-scoring", "cognition"],
"capability_scoring": ["cognition-capability", "cognition"],
"token_counting": ["cognition-token", "cognition"],
"query_memory": ["cognition-query", "cognition"],
}
if module_stem in aliases:
keys = aliases[module_stem] + keys
# cognition_core broker.rs hosts many kernels
if module_stem in {
"tier_manager", "safety_override", "pins", "identity_anchors",
"abstraction", "growth_delta", "witness", "persona_gate",
"memory_consolidation", "drift_compression", "context_broker",
"persona_definition", "goal",
}:
keys.append("cognition_core")
keys.append("cognition-cognition")
return keys
def audit() -> dict:
classification = load_classification()
corpus = implementation_corpus()
rows = []
for path in sorted(SRC.glob("*.py")):
stem = path.stem
if stem in RETIRES:
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
"fns": 0, "matched": 0, "side": "RETIRES", "status": "RETIRES", "missing": []})
continue
if stem.startswith(TEST_PREFIXES) or stem in TEST_INFRA:
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
"fns": 0, "matched": 0, "side": "TEST", "status": "TEST", "missing": []})
continue
fns = module_functions(path)
lines = len(path.read_text(errors="replace").splitlines())
entry = classification.get(stem) or classification.get(path.name) or {}
side = entry.get("side", entry.get("language", "unclassified"))
sources = []
for key in module_to_keys(stem):
if key in corpus:
sources.append(corpus[key])
blob = "\n".join(sources)
matched, missing = 0, []
for fn in fns:
hits = any(cand in blob for cand in snake_to_camel(fn))
if hits:
matched += 1
else:
missing.append(fn)
ratio = matched / len(fns) if fns else (1.0 if lines < 30 else 0.0)
if stem in RETIRES:
status = "RETIRES"
elif not blob:
status = "NOT TOUCHED"
elif ratio >= 0.8:
status = "FULL"
elif ratio >= 0.2:
status = "PARTIAL"
else:
status = "NOT TOUCHED"
rows.append({"module": stem, "lines": lines, "fns": len(fns),
"matched": matched, "side": side, "status": status, "missing": missing[:12]})
return {"rows": rows}
def summarize(result: dict) -> str:
rows = result["rows"]
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
full = [r for r in runtime if r["status"] == "FULL"]
partial = [r for r in runtime if r["status"] == "PARTIAL"]
untouched = [r for r in runtime if r["status"] == "NOT TOUCHED"]
total_fns = sum(r["fns"] for r in runtime)
total_matched = sum(r["matched"] for r in runtime)
total_lines = sum(r["lines"] for r in runtime)
fn_pct = 100 * total_matched / total_fns if total_fns else 0
out = []
out.append("# DICHOTOMY REBASE — HONEST AUDIT (function-level)")
out.append("")
out.append("_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._")
out.append("")
out.append(f"**Function-level completion: {fn_pct:.0f}%** ({total_matched}/{total_fns} runtime functions matched)")
out.append("")
out.append("| Status | Modules | Lines | Meaning |")
out.append("|---|---|---|---|")
out.append(f"| FULL | {len(full)} | {sum(r['lines'] for r in full)} | 80%+ of functions ported |")
out.append(f"| PARTIAL | {len(partial)} | {sum(r['lines'] for r in partial)} | 20–80% — real logic exists, gaps named below |")
out.append(f"| NOT TOUCHED | {len(untouched)} | {sum(r['lines'] for r in untouched)} | no name-preserving implementation |")
out.append(f"| Total runtime | {len(runtime)} | {total_lines} | |")
out.append("")
if untouched:
out.append("## NOT TOUCHED — the port queue, in priority order (largest first)")
out.append("")
out.append("| Module | Lines | Fns | Side |")
out.append("|---|---|---|---|")
for r in sorted(untouched, key=lambda r: -r["lines"]):
out.append(f"| {r['module']} | {r['lines']} | {r['fns']} | {r['side']} |")
out.append("")
if partial:
out.append("## PARTIAL — gaps to close (matched/total fns; first missing named)")
out.append("")
out.append("| Module | Lines | Coverage | Missing (first named) |")
out.append("|---|---|---|---|")
for r in sorted(partial, key=lambda r: -(r["fns"] - r["matched"])):
miss = ", ".join(r["missing"][:6]) if r["missing"] else "(structural)"
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {miss} |")
out.append("")
out.append("## FULL — ported, parity-gated")
out.append("")
out.append("| Module | Lines | Fns | Side |")
out.append("|---|---|---|---|")
for r in sorted(full, key=lambda r: -r["lines"]):
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {r['side']} |")
out.append("")
return "\n".join(out)
def main() -> int:
write = "--write" in sys.argv
result = audit()
report = summarize(result)
if write:
REPORT.write_text(report)
print(f"written: {REPORT}")
# stderr summary for the session log
rows = result["rows"]
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
tf = sum(r["fns"] for r in runtime)
tm = sum(r["matched"] for r in runtime)
print(f"FUNCTION-LEVEL: {tm}/{tf} ({100*tm/tf if tf else 0:.0f}%) | "
f"FULL {sum(1 for r in runtime if r['status']=='FULL')} | "
f"PARTIAL {sum(1 for r in runtime if r['status']=='PARTIAL')} | "
f"NOT TOUCHED {sum(1 for r in runtime if r['status']=='NOT TOUCHED')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+111
View File
@@ -0,0 +1,111 @@
import json, sys
sys.path.insert(0, 'src')
from rune.adaptive_router import (
classify_route, derive_sufficiency, body_has_floor_trigger, floor_applies,
escalate_route, downgrade_route, escalate_on_verification_failure,
build_research_dispatch, FloorViolation,
)
cases = []
ESC_EVIDENCE = "the fix works locally but the production parser accepts different input shapes — input contract uncertain"
cr_cases = [
("local_fix_happy", "fix the broken typewriter regex in warm_router, crash on empty input",
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
("standard_change", "add a progress bar to the export dialog",
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
("defect_word_absent", "update the version number in the footer",
dict(has_local_repro=True, owning_authority_known=True, testable_locally=False, narrow_and_reversible=True)),
("floor_new_subsystem", "adopt a new subsystem for memory indexing, replacement of spine",
dict(has_local_repro=False, owning_authority_known=False)),
("risk_new_authority", "this creates a new authority path for dispatch",
dict(has_local_repro=False, owning_authority_known=False)),
("uncertainty_competing", "there are competing designs here, expensive reversal likely",
dict(has_local_repro=True, owning_authority_known=True)),
("not_grounded", "add support for markdown export",
dict(has_local_repro=False, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
("inspection_verb", "explain why the failing test fails",
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
]
for name, body, kw in cr_cases:
d = classify_route(body, **kw)
cases.append({"fn": "classify", "name": name,
"input": {"body": body, **kw, "pipeline_sha": "UNKNOWN"},
"expected": {"route": d.route, "confidence": d.confidence, "reasons": d.reasons,
"risk": d.risk_flags, "uncertainty": d.uncertainty_flags,
"floor": d.floor, "task_description": d.task_description}})
ds_cases = [
("grounded_fix", "fix crash", "the parser crashes on empty input, see tests/test_parse.py"),
("mechanism", "investigate", "performance is bad under load, benchmark needed"),
("external_url", "read the docs", "see https://example.com/api docs for the endpoint"),
("version_risk", "upgrade", "upgrade the library to latest version, current behaviour differs"),
("inspection", "explain", "why does this happen"),
("new_subsystem", "adopt", "this adopts a replacement architecture"),
("neutral", "write", "a poem about the sea"),
]
for name, title, body in ds_cases:
s = derive_sufficiency(title, body)
cases.append({"fn": "derive", "name": name,
"input": {"title": title, "body": body, "ambiguous": False},
"expected": {"local_authority": s.local_authority,
"external_dependency": s.external_dependency,
"version_freshness_risk": s.version_freshness_risk,
"verifiability": s.verifiability, "scope_novelty": s.scope_novelty,
"unknowns": s.unknowns(),
"task_class": s.provenance.get("task_class_language")}})
cases.append({"fn": "floor_trigger", "name": "trigger_yes",
"input": {"body": "adopt a replacement for the cache"},
"expected": {"result": body_has_floor_trigger("adopt a replacement for the cache")}})
cases.append({"fn": "floor_trigger", "name": "trigger_no",
"input": {"body": "fix the typo"},
"expected": {"result": body_has_floor_trigger("fix the typo")}})
cases.append({"fn": "floor_applies", "name": "res_yes", "input": {"route": "RESEARCHED_CHANGE", "flags": ["new_authority"]},
"expected": {"result": floor_applies("RESEARCHED_CHANGE", ["new_authority"])}})
cases.append({"fn": "floor_applies", "name": "local_no", "input": {"route": "LOCAL_FIX", "flags": ["new_authority"]},
"expected": {"result": floor_applies("LOCAL_FIX", ["new_authority"])}})
base = classify_route("fix the crash", has_local_repro=True, owning_authority_known=True,
testable_locally=True, narrow_and_reversible=True)
esc = escalate_route(base, "RESEARCHED_CHANGE", "verify_fail", ESC_EVIDENCE)
cases.append({"fn": "escalate", "name": "esc",
"input": {"decision_route": base.route, "new_route": "RESEARCHED_CHANGE",
"trigger": "verify_fail", "evidence": ESC_EVIDENCE},
"expected": {"route": esc.route, "previous": esc.previous_route,
"decided_by": esc.decided_by, "trigger": esc.trigger}})
try:
downgrade_route(base, "STANDARD_CHANGE", "try", "ev")
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {"base_route": base.route, "base_floor": False,
"new_route": "STANDARD_CHANGE"}, "expected": {"route": "STANDARD_CHANGE", "error": None}})
except FloorViolation:
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {}, "expected": {"route": None, "error": "FloorViolation"}})
floored = classify_route("adopt a replacement for the memory index")
try:
downgrade_route(floored, "STANDARD_CHANGE", "efficiency", "faster")
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
"expected": {"route": "STANDARD_CHANGE", "error": None}})
except FloorViolation:
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
"expected": {"route": None, "error": "FloorViolation"}})
try:
escalate_on_verification_failure(base, "test failed", "/tmp/ar_probe.jsonl")
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
"expected": {"result": "ACCEPTED"}})
except ValueError:
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
"expected": {"result": "REJECTED"}})
disp = build_research_dispatch("T-1", "research the memory index options", floored)
cases.append({"fn": "dispatch", "name": "disp",
"input": {"task_id": "T-1", "description": "research the memory index options"},
"expected": {"task_id": disp["task_id"], "title": disp["title"], "route": disp["route"],
"floor": disp["floor"], "executor_entry": disp["executor_entry"]}})
json.dump(cases, open('src/go/adaptive-router/truth.json', 'w'), indent=1)
print(f"{len(cases)} truth cases with inputs")
for c in cases:
print(f" {c['fn']:14} {c['name']}")
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""gen_build_mode_truth.py — build-mode batch 1 truth battery
(contract BUILDER-2026-10-07T17:08:00Z-6e177d-unknown).
The LIVE BuildManager runs with the clock FROZEN (module-patched datetime)
and create_worktree no-op'd (reactive piece, deferred to batch 2). Cases
capture: the start_build id format, the ledger flow across phases
(including a failure + correction), resume-point scans at several states,
and the status shape. The Go replay compares state dicts key-for-key.
Usage: python3 scripts/gen_build_mode_truth.py (from repo root)
"""
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import build_mode as bm # noqa: E402
FROZEN = "2026-10-07T16:08:00"
FROZEN_DT = datetime(2026, 10, 7, 16, 8, 0, tzinfo=timezone.utc)
class FrozenDatetime(datetime):
@classmethod
def now(cls, tz=None):
return FROZEN_DT
def main():
# Freeze the clock (the manager reads datetime.now at two call sites)
bm.datetime = FrozenDatetime
def flow(name, steps):
"""Run a phase flow with the worktree op no-op'd; record the CALLS."""
mgr = bm.BuildManager()
mgr.create_worktree = lambda: "/nonexistent-worktree" # reactive no-op
build_id = mgr.start_build("truth battery build")
calls = []
for fn, args in steps:
fn(mgr, **args)
calls.append({"fn": fn.__name__, "args": args})
return {
"name": name,
"build_id": build_id,
"calls": calls,
"state": _jsonify(mgr.state),
"resume_point": mgr.get_resume_point(),
"can_resume": mgr.can_resume(),
"status": mgr.get_status(),
}
def step(fn, **args):
return (fn, args)
cases = [
flow("fresh_start_assess_in_progress", []),
flow("assess_complete", [
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
]),
flow("through_plan", [
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
step(bm.BuildManager.complete_research, findings={"findings": "dichotomy applies"}),
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
step(bm.BuildManager.complete_consult, patterns=["oracle-battery pattern"]),
step(bm.BuildManager.complete_consent, approved=True),
step(bm.BuildManager.complete_plan, batches=[{"batch": 1, "fns": 11}]),
]),
flow("verify_with_failures", [
step(bm.BuildManager.complete_assess, complexity_report={}),
step(bm.BuildManager.complete_research, findings={}),
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
step(bm.BuildManager.complete_consult, patterns=[]),
step(bm.BuildManager.complete_consent, approved=True),
step(bm.BuildManager.complete_plan, batches=[]),
step(bm.BuildManager.complete_build_phase, results=[{"ok": True}]),
step(bm.BuildManager.complete_verify, passed=False, failures=["test_x failed"]),
]),
flow("correction_cycle", [
step(bm.BuildManager.complete_assess, complexity_report={}),
step(bm.BuildManager.complete_research, findings={}),
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
step(bm.BuildManager.complete_consult, patterns=[]),
step(bm.BuildManager.complete_consent, approved=True),
step(bm.BuildManager.complete_plan, batches=[{"batch": 1}]),
step(bm.BuildManager.start_correct),
step(bm.BuildManager.complete_correct, fixed=True),
]),
]
out = {
"oracle": "src/rune/build_mode.py (live BuildManager, clock frozen)",
"generated": "2026-10-07",
"frozen_now": FROZEN,
"cases": cases,
}
target = REPO / "src/go/build-mode" / "truth_buildmode.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} flow cases)")
def _jsonify(state):
"""asdict + the worktree path the no-op returned."""
from dataclasses import asdict
d = asdict(state)
return d
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""gen_build_mode_wt_truth.py — build-mode batch 2 truth battery
(contract BUILDER-2026-10-07T18:26:40Z-8be89a).
The LIVE BuildManager drives a REAL tmp git repo: create → state captured,
merge → state captured, delete → state captured. Cases carry the exact
argv sequences (from monkeypatched subprocess.run) + the state after each
op + the error texts. The Go plan layer must produce the same argv; the
Go exec harness proves the plans drive real git.
Usage: python3 scripts/gen_build_mode_wt_truth.py (from repo root)
"""
import json
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import build_mode as bm # noqa: E402
FROZEN = "2026-10-07T18:28:00"
class FrozenDatetime(datetime):
@classmethod
def now(cls, tz=None):
return datetime(2026, 10, 7, 18, 28, 0, tzinfo=timezone.utc)
def main():
bm.datetime = FrozenDatetime
bm.REPO_DIR = str(make_git_repo())
calls = []
def spy_run(cmd, **kwargs):
calls.append(list(cmd))
return subprocess.CompletedProcess(cmd, 0, "", "")
bm.subprocess.run = spy_run
mgr = bm.BuildManager()
build_id = mgr.start_build("worktree battery") # create_worktree inside
create_call = calls[0]
after_create = mgr.state.worktree_path
n_before_merge = len(calls)
mgr.merge_worktree()
merge_call = calls[n_before_merge] # the merge itself
after_merge = mgr.state.worktree_path # None: delete follows the merge
delete_call = calls[n_before_merge + 1], calls[n_before_merge + 2]
cases = [
{
"fn": "plan", "name": "create",
"expected": {"argv": create_call},
},
{
"fn": "plan", "name": "merge",
"expected": {"argv": merge_call},
},
{
"fn": "plan", "name": "delete_pair",
"expected": {"argv": [list(delete_call[0]), list(delete_call[1])]},
},
{
"fn": "state", "name": "round_trip",
"expected": {
"build_id": build_id,
"after_create_is_set": after_create is not None,
"after_merge_is_none": after_merge is None,
},
},
]
# error texts: merge failure mapping
def fail_run(cmd, **kwargs):
if cmd[1] == "merge":
return subprocess.CompletedProcess(cmd, 128, "", "fatal: not something we can merge")
return subprocess.CompletedProcess(cmd, 0, "", "")
bm.subprocess.run = fail_run
mgr2 = bm.BuildManager()
mgr2.state = bm.BuildState(
build_id="20261007-182800", description="error battery",
created_at=FROZEN)
wt = "/home/nick/.rune/worktrees/20261007-182800"
mgr2.state.worktree_path = wt # merge_worktree requires a live worktree path
try:
mgr2.merge_worktree()
merge_err = None
except RuntimeError as ex:
merge_err = str(ex)
cases.append({
"fn": "error", "name": "merge_failure_text",
"expected": {"error": merge_err},
})
def fail_create(cmd, **kwargs):
if cmd[1] == "worktree":
return subprocess.CompletedProcess(cmd, 128, "", "'x' already exists")
return subprocess.CompletedProcess(cmd, 0, "", "")
bm.subprocess.run = fail_create
mgr3 = bm.BuildManager()
mgr3.state = bm.BuildState(
build_id="20261007-182800", description="error battery 2",
created_at=FROZEN)
mgr3._save_state()
try:
mgr3.create_worktree()
create_err = None
except RuntimeError as ex:
create_err = str(ex)
cases.append({
"fn": "error", "name": "create_failure_text",
"expected": {"error": create_err},
})
out = {
"oracle": "src/rune/build_mode.py (live BuildManager on a real tmp git repo, clock frozen)",
"generated": "2026-10-07",
"frozen_now": FROZEN,
"cases": cases,
}
target = REPO / "src/go/build-mode" / "truth_worktree.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
def make_git_repo():
root = tempfile.mkdtemp(prefix="bm-wt-truth-")
env_realloc = subprocess.run(
["git", "init", "-q"], cwd=root, capture_output=True, text=True)
assert env_realloc.returncode == 0
subprocess.run(["git", "config", "user.email", "t@t"], cwd=root, capture_output=True)
subprocess.run(["git", "config", "user.name", "t"], cwd=root, capture_output=True)
(Path(root) / "seed.txt").write_text("seed\n")
subprocess.run(["git", "add", "."], cwd=root, capture_output=True)
subprocess.run(["git", "commit", "-qm", "seed"], cwd=root, capture_output=True)
return root
if __name__ == "__main__":
main()
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""gen_cache_hash_truth.py — factory batch 6 truth battery (live cache._hash)."""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import cache as ch # noqa: E402
def main():
cases = [
{"fn": "hash", "name": "ascii", "input": {"text": s},
"expected": {"hash": ch._hash(s)}}
for s in ["", "hello world", "unicode ümlaut ✅", "a" * 500]
]
out = {"oracle": "cache._hash", "generated": "2026-10-07", "cases": cases}
(REPO / "src/go/cache" / "truth_hash.json").write_text(json.dumps(out, indent=1) + "\n")
print(f"wrote {len(cases)} cases")
if __name__ == "__main__":
main()
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""gen_curator_lexicon_truth.py — regenerate the lexicon truth batteries
(contract BUILDER-2026-10-07T16:31:17Z-997c9d).
Why: batch 1's truth.json encoded the LEXICON DEGRADE — the phantom shadow
("lexicon module unavailable — strength gate only") — because
lexicon_evolution.py was absent when it was generated. The phantom has now
been recovered from provenance (commit 5749029), so the LIVE oracle returns
real lexicon verdicts and the Go port must match THEM.
What this does:
1. Rewrites ONLY the fn=='le_promote_eligible' cases inside truth.json,
regenerated against the live curator.le_promote_eligible (which now
delegates to the recovered lexicon_evolution). Other cases untouched.
2. Emits truth_lexicon.json — direct live-oracle cases for the recovered
module's full surface: coherence (conflict/consistent/novel), grounded,
distinctive_terms, promote_eligible (shared / novel-grounded /
novel-parked / no-signal / strength-gate).
Note on `existing`: the truth carries the block values as a single string;
the oracle is called with a one-element list ([existing]) — the batch-1
convention, documented here so the Go test wraps identically.
Usage: python3 scripts/gen_curator_lexicon_truth.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
from rune import curator # noqa: E402 — live oracle (imports lexicon_evolution)
import lexicon_evolution as le # noqa: E402
def main():
core = REPO / "src/go/curator-core"
# ── 1. regenerate the le_promote_eligible cases in truth.json ──────────
truth_path = core / "truth.json"
truth = json.loads(truth_path.read_text())
replaced = 0
for case in truth:
if case.get("fn") != "le_promote_eligible":
continue
inp = case["input"]
r = curator.le_promote_eligible(
inp["proposal"],
[inp["existing"]],
inp["strength"],
min_strength=3,
wash_count=inp["wash_count"],
)
case["expected"] = {"eligible": r}
replaced += 1
assert replaced >= 4, f"expected >=4 le cases, found {replaced}"
# Self-check: no case may still encode the degrade shadow.
for case in truth:
if case.get("fn") == "le_promote_eligible":
assert "lexicon module unavailable" not in json.dumps(case["expected"]), case["name"]
truth_path.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
print(f"truth.json: {replaced} le_promote_eligible cases regenerated (live lexicon)")
# ── 2. the direct lexicon-surface battery ──────────────────────────────
corpus = [
"nick writes Go and Rust code daily in the rune repository",
"the curator gates memory writes through worthiness and grading",
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust",
]
cases = []
def add(fn, name, inp, expected):
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
# coherence: conflict (distinctive shared term + negation ON it)
add("coherence", "conflict",
{"fact": "nick never writes Rust code", "existing": ["nick writes Go and Rust code daily"],
"corpus": corpus},
{"verdict": le.coherence("nick never writes Rust code",
["nick writes Go and Rust code daily"], corpus)})
# coherence: consistent (distinctive shared term, no negation)
add("coherence", "consistent",
{"fact": "nick writes Rust code on weekends", "existing": ["nick writes Go and Rust code daily"],
"corpus": corpus},
{"verdict": le.coherence("nick writes Rust code on weekends",
["nick writes Go and Rust code daily"], corpus)})
# coherence: novel (nothing distinctive shared)
add("coherence", "novel_axis",
{"fact": "nick plays bass guitar in a band", "existing": ["nick writes Go and Rust code daily"],
"corpus": corpus},
{"verdict": le.coherence("nick plays bass guitar in a band",
["nick writes Go and Rust code daily"], corpus)})
# coherence: common word only is NEVER a relation
add("coherence", "common_word_not_relation",
{"fact": "nick likes black coffee", "existing": ["black holes bend light"],
"corpus": ["black coffee in the morning", "black holes bend light"]},
{"verdict": le.coherence("nick likes black coffee", ["black holes bend light"],
["black coffee in the morning", "black holes bend light"])})
# coherence: empty existing -> novel
add("coherence", "empty_existing",
{"fact": "nick writes Rust code daily", "existing": [], "corpus": []},
{"verdict": le.coherence("nick writes Rust code daily", [], [])})
# grounded: stem-aware (migrated matches migration)
add("grounded", "stem_match",
{"fact": "nick migrated the modules", "sources": corpus},
{"grounded": le.grounded("nick migrated the modules", corpus)})
add("grounded", "invented_token",
{"fact": "nick collects vintage railway timetables", "sources": corpus},
{"grounded": le.grounded("nick collects vintage railway timetables", corpus)})
add("grounded", "empty_sources",
{"fact": "nick migrated the modules", "sources": []},
{"grounded": le.grounded("nick migrated the modules", [])})
# distinctive_terms: rare survives, common does not
add("distinctive_terms", "rare_vs_common",
{"fact": "nick writes Rust code daily", "corpus": corpus},
{"terms": sorted(le.distinctive_terms("nick writes Rust code daily", corpus))})
# promote_eligible: the four verdict shapes
add("promote_eligible", "strength_gate",
{"proposal": "add support for retries", "existing": ["support for retries in the fetch loop"],
"strength": 2, "min_strength": 3, "corpus": []},
{"eligible": le.promote_eligible("add support for retries",
["support for retries in the fetch loop"], 2, 3, ())})
add("promote_eligible", "shared_distinctive",
{"proposal": "add retries to the fetch loop", "existing": ["support for retries in the fetch loop"],
"strength": 3, "min_strength": 3, "corpus": []},
{"eligible": le.promote_eligible("add retries to the fetch loop",
["support for retries in the fetch loop"], 3, 3, ())})
add("promote_eligible", "novel_grounded",
{"proposal": "the curator gates every memory write", "existing": ["nick writes Go and Rust code daily"],
"strength": 3, "min_strength": 3, "corpus": corpus},
{"eligible": le.promote_eligible("the curator gates every memory write",
["nick writes Go and Rust code daily"], 3, 3, corpus)})
add("promote_eligible", "novel_parked",
{"proposal": "nick collects vintage railway timetables", "existing": ["nick writes Go and Rust code daily"],
"strength": 3, "min_strength": 3, "corpus": corpus},
{"eligible": le.promote_eligible("nick collects vintage railway timetables",
["nick writes Go and Rust code daily"], 3, 3, corpus)})
add("promote_eligible", "first_principle",
{"proposal": "parking is never wrong for growth", "existing": [],
"strength": 3, "min_strength": 3, "corpus": []},
{"eligible": le.promote_eligible("parking is never wrong for growth", [], 3, 3, ())})
out = {
"oracle": "src/rune/lexicon_evolution.py (recovered provenance 0308ccc6) via curator.py",
"generated": "2026-10-07 (post-recovery regeneration)",
"cases": cases,
}
(core / "truth_lexicon.json").write_text(
json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"truth_lexicon.json: {len(cases)} cases")
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""gen_hook_placement_truth.py — factory batch 4 truth battery
(contract BUILDER-2026-10-07T23:06:18Z-ec6208).
The LIVE recommend_placement oracle over a factor matrix covering every
branch: hook-point combos, frequency ladder, cache scopes, concurrency,
failure modes, context budget.
Usage: python3 scripts/gen_hook_placement_truth.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import hook_engine as he # noqa: E402
def main():
factor_sets = [
{"output_dep": True},
{"input_dep": True},
{"input_dep": True, "output_dep": True},
{},
{"side_effects": "read-write"},
{"volatility": "session"},
{"volatility": "per-turn"},
{"scope": "persistent"},
{"scope": "session"},
{"concurrent": True},
{"failure": "fail-loud"},
{"context_budget": 500},
{"input_dep": True, "scope": "session", "concurrent": True,
"context_budget": 250},
{"side_effects": "read-write", "failure": "fail-loud",
"scope": "persistent"},
]
cases = []
for i, factors in enumerate(factor_sets):
cases.append({
"fn": "placement", "name": f"matrix_{i}",
"input": {"factors": factors},
"expected": he.recommend_placement(f"hook_{i}", factors),
})
out = {
"oracle": "src/rune/hook_engine.py recommend_placement (live)",
"generated": "2026-10-07",
"cases": cases,
}
target = REPO / "src/go/hook-engine" / "truth_placement.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""gen_memory_store_gate_truth.py — memory-store batch 4 truth battery
(contract BUILDER-2026-10-07T19:47:37Z-1cf1dc).
The LIVE oracle: the dense-side meta-judgement extracted from recall()
(lines 655-676) exercised against a FIXTURE STORE (sqlite entries + df
counts), plus the entries-side fusion vs the chunk-side one (identity
check), plus the abstain constants.
Usage: python3 scripts/gen_memory_store_gate_truth.py (from repo root)
"""
import json
import sqlite3
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import memory_store as ms # noqa: E402
def make_fixture_store(db_path):
"""Entries table + df data mirroring recall()'s df-of-word query."""
db = sqlite3.connect(db_path)
db.row_factory = sqlite3.Row
db.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, text TEXT, status TEXT)")
rows = [
(1, "the curator gates memory writes through worthiness"),
(2, "black holes bend light near the event horizon"),
(3, "black coffee in the morning keeps nick running"),
(4, "the spine rebuild parity proofs live in store"),
(5, "memory writes require the operator approval gate"),
(6, "unrelated filler text about weather patterns"),
(7, "curator provenance recovery from odysseus"),
(8, "another filler row to pad the document frequency"),
]
db.executemany("INSERT INTO entries VALUES (?,?,?)",
[(i, t, "active") for (i, t) in rows])
db.commit()
return db
def main():
tmp = Path(tempfile.mkdtemp(prefix="ms-gate-truth-"))
db = make_fixture_store(str(tmp / "store.db"))
total = db.execute("SELECT COUNT(*) AS n FROM entries WHERE status='active'").fetchone()["n"]
def df_of(word):
return db.execute(
"SELECT COUNT(*) AS n FROM entries WHERE status='active' AND text LIKE ?",
(f"%{word}%",),
).fetchone()["n"]
# replicate recall()'s dense-gate block verbatim
def oracle_gate(query, hits):
q_content = {t for t in query.lower().split() if len(t) > 3 and t not in ms._STOPWORDS}
dense = []
for rowid, cos in hits:
etxt = (db.execute("SELECT text FROM entries WHERE id=?", (rowid,)).fetchone()["text"] or "").lower()
distinctive = [w for w in q_content if w in etxt and df_of(w) / total < 0.05]
if distinctive:
dense.append((rowid, cos))
elif not q_content or cos >= 0.80:
dense.append((rowid, cos))
return dense
cases = []
def add(fn, name, inp, expected):
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
# gate: shared DISTINCTIVE word (curator: df=2/8=0.25? no — check)
# df(curator) = rows 1,7 = 2 → 0.25 ≥ 0.05 → NOT distinctive!
# The oracle's threshold means common-store words fail; only truly
# rare words pass. Fixture exercise:
for name, query, hits in [
("distinctive_shared", "worthiness gates", [(1, 0.9), (2, 0.85)]),
("common_word_falls_back", "black", [(2, 0.86), (3, 0.79)]),
("strong_cos_no_share", "unrelated query terms", [(6, 0.91), (4, 0.70)]),
("no_content_words", "the was and", [(1, 0.60), (6, 0.95)]),
]:
add("dense_gate", name,
{"query": query, "hits": [[r, c] for r, c in hits], "total": total},
{"kept": [[r, c] for r, c in oracle_gate(query, hits)],
"q_content": sorted({t for t in query.lower().split() if len(t) > 3 and t not in ms._STOPWORDS}),
"df": {w: df_of(w) for w in query.lower().split() if len(w) > 3}})
# entries-side fusion identity with the chunk-side one
dense = [[1, 0.9], [2, 0.8]]
bm25 = [[2, 10.0], [3, 4.0]]
add("fusion_identity", "entries_eq_chunks",
{"dense": dense, "bm25": bm25},
{"fusion": ms._py_rank_fusion(dense, bm25)})
add("constants", "abstain",
{},
{"budget": ms.RECALL_BUDGET, "min_score": ms.RECALL_MIN_SCORE})
import shutil
shutil.rmtree(tmp, ignore_errors=True)
out = {
"oracle": "src/rune/memory_store.py recall() dense gate + _py_rank_fusion",
"generated": "2026-10-07",
"fixture_df_note": "df via LIKE-substring over active entries; threshold 0.05",
"cases": cases,
}
target = REPO / "src/go/memory-store-deep" / "truth_gate.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""gen_memory_store_score_truth.py — memory-store batch 3 truth battery
(contract BUILDER-2026-10-07T18:57:06Z-c2429c).
The LIVE _py_score_recall_rows oracle: metadata scoring across modes
(hybrid / dense-only / lexical-only), recency decay, the abstention
ladder (min_score gates, the 0.74 high-confidence gate, the 0.08 gap
detection), position-aware re-rank.
Usage: python3 scripts/gen_memory_store_score_truth.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import memory_store as ms # noqa: E402
NOW = "2026-10-07T18:58:00"
def rows_for(ids):
"""Fixture rows: confidence/temperature/recency variety."""
out = []
for i, eid in enumerate(ids):
out.append({
"id": eid,
"text": f"entry {eid} body",
"importance": 0.5 + i * 0.01,
"topic": "project",
"entities": ["a"],
"source": "fixture",
"confidence": 0.9 if i % 2 == 0 else 0.6,
"temperature": 1.2 if i % 3 == 0 else 0.8,
"last_accessed": ["2026-10-07T18:00:00", "2026-09-01T10:00:00",
"2026-05-01T10:00:00", None][i % 4],
})
return out
def main():
cases = []
def add(fn, name, inp, expected):
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
# hybrid: ranks over 4 rows, dense + bm25 present
fusion = {"ranks": [[1, 0.12], [2, 0.11], [3, 0.10], [4, 0.09]],
"best_dense": 0.8, "mode": "hybrid"}
dense = [[1, 0.8], [2, 0.7], [3, 0.6]]
bm25 = [[2, 10.0], [4, 5.0], [1, 2.0]]
rows = rows_for([1, 2, 3, 4])
add("score", "hybrid",
{"fusion": fusion, "rows": rows, "dense": dense, "bm25": bm25,
"budget": 3, "min_score": 0.3, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion, rows, dense, bm25, 3, 0.3, NOW)})
# dense-only: high confidence (>= 0.74) → position-aware
fusion_d = {"ranks": [[1, 0.06], [2, 0.055], [3, 0.05]],
"best_dense": 0.8, "mode": "dense-only"}
add("score", "dense_only_high_conf",
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense,
"bm25": [], "budget": 5, "min_score": 0.3, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense, [], 5, 0.3, NOW)})
# dense-only: narrow gap → abstain
dense_close = [[1, 0.80], [2, 0.75]]
add("score", "dense_only_gap_abstain",
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense_close,
"bm25": [], "budget": 5, "min_score": 0.3, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense_close, [], 5, 0.3, NOW)})
# dense-only: below min_score → abstain
add("score", "dense_only_min_score",
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense,
"bm25": [], "budget": 5, "min_score": 0.9, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense, [], 5, 0.9, NOW)})
# lexical-only: max relevance gate
fusion_l = {"ranks": [[2, 0.9], [4, 0.7]], "best_dense": 0.0, "mode": "lexical-only"}
add("score", "lexical_only_gate",
{"fusion": fusion_l, "rows": rows_for([2, 4]), "dense": [], "bm25": bm25,
"budget": 5, "min_score": 1.0, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion_l, rows_for([2, 4]), [], bm25, 5, 1.0, NOW)})
# neither → []
add("score", "neither",
{"fusion": fusion_l, "rows": rows_for([2, 4]), "dense": [], "bm25": [],
"budget": 5, "min_score": 0.3, "now": NOW},
{"scored": ms._py_score_recall_rows(fusion_l, rows_for([2, 4]), [], [], 5, 0.3, NOW)})
out = {
"oracle": "src/rune/memory_store.py _py_score_recall_rows",
"generated": "2026-10-07",
"cases": cases,
}
target = REPO / "src/go/memory-store-deep" / "truth_score.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""gen_memory_store_truth2.py — memory-store batch 2 truth battery
(contract BUILDER-2026-10-07T18:05:47Z-152c16).
The LIVE _py_* oracles of memory_store.py. Cases: FTS conjunctive builder
(stopword filtering, fallback, 8-cap), chunk_text (recombine + hard-split),
rank fusion (hybrid/dense-only/lexical-only), project_chunk_rows,
map_reduce, fuse_results (dedup + scale-alignment).
Usage: python3 scripts/gen_memory_store_truth2.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import memory_store as ms # noqa: E402
def main():
cases = []
def add(fn, name, inp, expected):
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
# ── FTS conjunctive ───────────────────────────────────────────────────
add("fts", "significant_terms",
{"query": "the curator gates memory writes"},
{"q": ms._py_fts_query_conjunctive("the curator gates memory writes")})
add("fts", "stopwords_dropped",
{"query": "the and was with nick"},
{"q": ms._py_fts_query_conjunctive("the and was with nick")})
add("fts", "short_fallback",
{"query": "go is fun ai"},
{"q": ms._py_fts_query_conjunctive("go is fun ai")})
long_q = "alpha beta gamma delta epsilon zeta eta theta iota kappa"
add("fts", "cap_eight",
{"query": long_q},
{"q": ms._py_fts_query_conjunctive(long_q)})
add("fts", "empty_returns_raw",
{"query": "a an of"},
{"q": ms._py_fts_query_conjunctive("a an of")})
# ── chunk_text ────────────────────────────────────────────────────────
small = "one paragraph only"
add("chunk", "under_size", {"text": small, "size": 1500, "overlap": 150},
{"chunks": ms._py_chunk_text(small, 1500, 150)})
para = "\n\n".join(f"paragraph {i} " + "x" * 40 for i in range(30))
add("chunk", "recombine", {"text": para, "size": 400, "overlap": 150},
{"chunks": ms._py_chunk_text(para, 400, 150)})
noline = "y" * 3000
add("chunk", "hard_split", {"text": noline, "size": 1000, "overlap": 150},
{"chunks": ms._py_chunk_text(noline, 1000, 150)})
add("chunk", "empty", {"text": "", "size": 1500, "overlap": 150},
{"chunks": ms._py_chunk_text("", 1500, 150)})
# ── rank fusion ───────────────────────────────────────────────────────
dense = [[1, 0.9], [2, 0.8], [3, 0.7]]
bm25 = [[2, 12.0], [4, 8.0], [1, 3.0]]
add("fuse_ranks", "hybrid", {"dense": dense, "bm25": bm25},
ms._py_rank_fusion(dense, bm25))
add("fuse_ranks", "dense_only", {"dense": dense, "bm25": []},
ms._py_rank_fusion(dense, []))
add("fuse_ranks", "lexical_only", {"dense": [], "bm25": bm25},
ms._py_rank_fusion([], bm25))
add("fuse_ranks", "neither", {"dense": [], "bm25": []},
ms._py_rank_fusion([], []))
# ── project_chunk_rows ────────────────────────────────────────────────
rows = [
{"rowid": 1, "chunk_id": "c1", "text": "t" * 500, "wing": "w1", "room": "r1",
"source_path": "a.md", "page": 3},
{"rowid": 2, "chunk_id": "c2", "text": "short", "wing": "w1", "room": "r1",
"source_path": "b.md"},
]
ranks = {"2": 0.0625, "1": 0.05}
add("project", "budget_and_shape", {"ranks": ranks},
{"rows": ms._py_project_chunk_rows(ranks, rows, 6)})
# ── map_reduce ────────────────────────────────────────────────────────
entries = [{"id": i, "relevance": i * 0.1} for i in range(25)]
add("map_reduce", "top3_per_chunk", {"entries": entries, "chunk_size": 10},
{"mapped": ms._py_map_reduce(entries, 10)})
add("map_reduce", "under_threshold", {"entries": entries[:5], "chunk_size": 10},
{"mapped": ms._py_map_reduce(entries[:5], 10)})
# ── fuse_results ──────────────────────────────────────────────────────
ents = [
{"id": 1, "text": "nick prefers coffee", "topic": "human",
"relevance": 2.5, "confidence": 0.9},
{"id": 2, "text": "NICK PREFERS COFFEE", "topic": "human",
"relevance": 1.1, "confidence": 0.8}, # dedup hit
{"id": 3, "text": "rune gates memory writes", "topic": "project",
"relevance": 5.6, "confidence": 0.95},
]
res = [
{"chunk_id": "c1", "text": "the spine rebuild parity", "wing": "research",
"rrf": 0.0625, "confidence": 0.7},
{"chunk_id": "c2", "text": "curator provenance recovery", "wing": "research",
"rrf": 0.05},
]
add("fuse_results", "dedup_scale_align", {"entries": ents, "research": res},
{"merged": ms._py_fuse_results(ents, res)})
out = {
"oracle": "src/rune/memory_store.py _py_* oracles",
"generated": "2026-10-07",
"cases": cases,
}
target = REPO / "src/go/memory-store-deep" / "truth_recall.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""gen_provider_format_truth.py — factory batch 5 truth battery (live
provider_registry._format_tools_* oracles)."""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import provider_registry as pr # noqa: E402
def main():
tools = [
{"name": "read_file", "description": "Read a file",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}}},
{"name": "write_file", "description": "Write a file"}, # no parameters key
]
cases = [
{"fn": "openai", "name": "shape", "input": {"tools": tools},
"expected": pr._format_tools_openai(tools)},
{"fn": "anthropic", "name": "shape", "input": {"tools": tools},
"expected": pr._format_tools_anthropic(tools)},
]
out = {"oracle": "provider_registry._format_tools_*", "generated": "2026-10-07", "cases": cases}
target = REPO / "src/go/provider-registry" / "truth_format.json"
target.write_text(json.dumps(out, indent=1) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""gen_scaffold_close_truth.py — scaffold batch 3 truth battery
(contract BUILDER-2026-10-07T18:42:38Z-52829e).
The LIVE oracle: _py_close_quality bucket math + close_contract on a doc
built through the real lifecycle (new_contract → transitions → snapshot →
close). now() frozen at scaffold AND structural_store namespaces.
Usage: python3 scripts/gen_scaffold_close_truth.py (from repo root)
"""
import json
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import scaffold as sc # noqa: E402
from rune.structural_store import new_doc, add_entity # noqa: E402
FROZEN = "2026-10-07T18:46:00"
def main():
sc.now = lambda: FROZEN
cases = []
# ── CloseQuality bucket math (direct, edge-covering) ──────────────────
inputs = {
"contract_id": "q1",
"phase": "superseding",
"affected_entities": ["module:a", "module:b"],
"expected_new_entities": ["module:new1", "module:new2"],
"predicted_existing_affected": ["module:c"],
"predicted_frontier": {
"level0_direct": ["module:a"],
"level1_obligations": ["module:d"],
"contracts": ["contract:x"],
"level2_verification": ["canary:v"],
},
"expected_runtime_effects": ["module:a/ctl", "module:zz/never"],
"observed_evidence": [
{"entity": "module:a", "source": "tests", "detail": "d" * 400, "authority": "pytest"},
{"entity": "module:new1", "source": "build", "detail": "created"},
{"entity": "module:surprise", "source": "runtime", "detail": "unplanned"},
{"entity": "", "source": "runtime", "detail": "no entity — ignored"},
],
"now": FROZEN,
}
s = sc.Scaffold(Path(tempfile.mkdtemp()), "q")
cases.append({
"fn": "close_quality", "name": "buckets",
"input": {"inputs": inputs},
"expected": s._py_close_quality(inputs),
})
# ── CloseContract through the real lifecycle ──────────────────────────
tmp = Path(tempfile.mkdtemp(prefix="scaffold-close-truth-"))
s2 = sc.Scaffold(tmp, "close-project")
s2.init(purpose="close battery")
doc = s2.load()
s2.new_contract(doc, contract_id="c1", need="n", scope=["src/go/x/"],
affected_entities=["module:a"],
expected_new_entities=["module:new1"])
s2.transition_contract(doc, "c1", "approved")
s2.transition_contract(doc, "c1", "building")
s2.transition_contract(doc, "c1", "verifying")
# predicted frontier on the entity
e = s2.contract(doc, "c1")
e.setdefault("ext", {})["predicted_frontier"] = {
"level0_direct": ["module:a"], "level1_obligations": [],
"contracts": [], "level2_verification": [],
}
ev = [{"entity": "module:a", "source": "tests", "detail": "passed"}]
e2 = s2.close_contract(doc, "c1", observed_evidence=ev,
scope_files=["src/go/x/main.go", "src/other/rogue.py"])
cases.append({
"fn": "close_contract", "name": "full_lifecycle",
"input": {},
"expected": {"entity": e2},
})
# phase gate
doc2 = new_doc()
add_entity(doc2, id="contract:early", kind="construction_contract",
identities={"need": "n"}, maturity="DECLARED",
evidence_refs=["x"])
doc2["entities"][-1]["phase"] = "draft"
doc2["entities"][-1]["status"] = "open"
doc2["entities"][-1]["checkpoints"] = []
try:
s2.close_contract(doc2, "early", observed_evidence=[])
gate = {"ok": True}
except Exception as ex: # noqa: BLE001 — intentional: the case IS the gate error; the oracle's text becomes the truth column
gate = {"ok": False, "error": str(ex)}
cases.append({
"fn": "close_gate", "name": "phase_guard",
"input": {},
"expected": gate,
})
# missing contract
try:
s2.close_contract(doc2, "ghost", observed_evidence=[])
miss = {"ok": True}
except Exception as ex: # noqa: BLE001 — intentional: same oracle-error-as-truth pattern as above
miss = {"ok": False, "error": str(ex)}
cases.append({
"fn": "close_gate", "name": "missing_contract",
"input": {},
"expected": miss,
})
out = {
"oracle": "src/rune/scaffold.py (live, now frozen at both namespaces)",
"generated": "2026-10-07",
"frozen_now": FROZEN,
"cases": cases,
}
target = REPO / "src/go/scaffold" / "truth_close.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
import shutil
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
main()
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""gen_scaffold_store_truth.py — scaffold batch 2 truth battery
(contract BUILDER-2026-10-07T17:47:00Z-960f12).
The LIVE oracle (scaffold.Scaffold + structural_store) on a tmp store with
now() frozen. Cases: canonical digest stability + secret redaction, commit
semantics (success + exact failure texts), verify_on_disk forgotten-save
detection, impact frontier (contract_change / implementation_change /
contextual), snapshot_predicted_frontier, spine_bridge_entity.
Usage: python3 scripts/gen_scaffold_store_truth.py (from repo root)
"""
import json
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
FROZEN = "2026-10-07T17:50:00"
def main():
sc.now = lambda: FROZEN
ss.now = lambda: FROZEN
tmp = Path(tempfile.mkdtemp(prefix="scaffold-store-truth-"))
s = sc.Scaffold(tmp, "truth-project")
s.init(purpose="store battery")
cases = []
# ── canonical digest: stability + redaction ───────────────────────────
doc = s.load()
d1 = ss.canonical_digest(doc)
# key-order permutation must NOT change the digest (semantic content)
permuted = {k: doc[k] for k in reversed(list(doc.keys()))}
d2 = ss.canonical_digest(permuted)
cases.append({
"fn": "digest", "name": "key_order_stable",
"input": {},
"expected": {"digest": d1, "permuted_same": d2 == d1},
})
secret_doc = {"entities": [{"id": "repo:x", "identities": {
"token": "hunter2", "api_key": "sk-123", "full_name": "o/r",
"api_token": "zz", "password": "pw", "private_key": "-----BEGIN",
"note": "keepme", "my_secret_sauce": "redacted-too"}}]}
cases.append({
"fn": "digest", "name": "secret_redaction",
"input": {},
"expected": {"redacted": ss._redact_secrets(secret_doc)},
})
# ── commit: success + digest readback ─────────────────────────────────
s.commit(doc, what="battery")
cases.append({
"fn": "commit", "name": "success",
"input": {},
"expected": {"ok": True, "on_disk_schema": s.load()["schema_version"]},
})
# forgotten-save detection: mutate in memory, DON'T save
doc2 = s.load()
doc2["entities"].append({"id": "ghost:1", "kind": "ghost", "identities": {}})
try:
s.verify_on_disk(lambda d: d["entities"], None, what="battery") if False else None
# the real call: selector/expect form
s.verify_on_disk(lambda d: [e for e in d["entities"] if e["id"] == "ghost:1"],
lambda v: bool(v), what="battery")
forgotten = {"ok": True}
except AssertionError as ex:
forgotten = {"ok": False, "error": str(ex)}
cases.append({
"fn": "verify_on_disk", "name": "forgotten_save",
"input": {},
"expected": forgotten,
})
# corrupt store -> commit-style load returns None -> reload failure path
# (simulate: point at a nonexistent path)
class FakeScaffold:
current = tmp / "nonexistent" / "current.json"
try:
ss_load = ss.load(FakeScaffold.current)
reload_none = ss_load is None
except Exception: # noqa: BLE001 — intentional: the case IS the degrade verdict (load failure → loads_none=False); error text is not the truth column here
reload_none = False
cases.append({
"fn": "load", "name": "missing_store",
"input": {},
"expected": {"loads_none": reload_none},
})
# ── impact frontier ───────────────────────────────────────────────────
doc3 = new_frontier_doc()
cases.append({
"fn": "frontier", "name": "contract_change",
"input": {"seeds": ["contract:c1"], "doc": doc3},
"expected": ss.impact_frontier(doc3, ["contract:c1"]),
})
cases.append({
"fn": "frontier", "name": "implementation_change",
"input": {"seeds": ["module:impl_a"], "doc": doc3},
"expected": ss.impact_frontier(doc3, ["module:impl_a"]),
})
cases.append({
"fn": "frontier", "name": "violation_escalation",
"input": {"seeds": ["module:impl_a"], "change_kind": "contract_violation", "doc": doc3},
"expected": ss.impact_frontier(doc3, ["module:impl_a"], change_kind="contract_violation"),
})
# ── snapshot_predicted_frontier + spine bridge ────────────────────────
doc4 = new_frontier_doc()
s4 = sc.Scaffold(tmp / "p4", "frontier-project")
s4.init(purpose="frontier battery")
doc4 = s4.load()
s4.new_contract(doc4, contract_id="f1", need="n", scope=["src/"])
# wire the frontier doc entities into the project doc
doc4["entities"].extend(e for e in doc3["entities"] if e["id"] not in {x["id"] for x in doc4["entities"]})
doc4["relationships"].extend(doc3["relationships"])
frontier = s4.snapshot_predicted_frontier(doc4, "f1", ["contract:c1"])
e = s4.contract(doc4, "f1")
cases.append({
"fn": "snapshot_frontier", "name": "persisted",
"input": {"doc": doc4},
"expected": {
"frontier": frontier,
"checkpoint_note": e["checkpoints"][-1]["note"],
"ext_has": sorted((e.get("ext", {}).get("predicted_frontier") or {}).keys()),
},
})
cases.append({
"fn": "bridge", "name": "identity_only",
"input": {},
"expected": {"bridge": s4.spine_bridge_entity()},
})
out = {
"oracle": "src/rune/scaffold.py + structural_store.py (live, now frozen)",
"generated": "2026-10-07",
"frozen_now": FROZEN,
"cases": cases,
}
target = REPO / "src/go/scaffold" / "truth_store.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
import shutil
shutil.rmtree(tmp, ignore_errors=True)
def new_frontier_doc():
"""A small structural doc exercising the frontier walk."""
doc = ss.new_doc()
ss.add_entity(doc, id="contract:c1", kind="contract", identities={})
ss.add_entity(doc, id="module:impl_a", kind="module", identities={})
ss.add_entity(doc, id="module:impl_b", kind="module", identities={})
ss.add_entity(doc, id="module:lib", kind="module", identities={})
ss.add_entity(doc, id="canary:x", kind="canary", identities={})
ss.add_entity(doc, id="deploy:live", kind="deployment", identities={})
for rel in (
{"type": "implements", "from": "module:impl_a", "to": "contract:c1"},
{"type": "implements", "from": "module:impl_b", "to": "contract:c1"},
{"type": "depends_on", "from": "module:impl_a", "to": "module:lib"},
{"type": "verified_by", "from": "contract:c1", "to": "canary:x"},
{"type": "deployed_as", "from": "module:impl_a", "to": "deploy:live"},
{"type": "supersedes", "from": "module:impl_b", "to": "module:impl_a"}, # non-obligation
):
doc["relationships"].append(rel)
return doc
if __name__ == "__main__":
main()
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""gen_scaffold_truth.py — scaffold batch 1 truth battery
(contract BUILDER-2026-10-07T17:27:25Z-6bc737).
The LIVE Scaffold runs against a tmp store with now() frozen (module-patch
on rune.scaffold's imported binding). Cases: the transition machine (legal
+ illegal + unknown phase, exact error texts), new_contract/transition_
contract state evolution, check_scope (green/red/unknown), layout_
obligations (FLEXIBLE vs USER-LOCKED), json_frontier projection.
Usage: python3 scripts/gen_scaffold_truth.py (from repo root)
"""
import json
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import scaffold as sc # noqa: E402
from rune.structural_store import new_doc, add_entity # noqa: E402
FROZEN = "2026-10-07T17:30:00"
def main():
# Freeze the clock at scaffold's namespace binding.
sc.now = lambda: FROZEN
tmp = Path(tempfile.mkdtemp(prefix="scaffold-truth-"))
s = sc.Scaffold(tmp, "truth-project")
s.init(purpose="truth battery")
cases = []
# ── transition machine ────────────────────────────────────────────────
def tv(name, current, to):
try:
sc.Scaffold.transition_contract # noqa: B018 — presence probe
# validate via a throwaway entity path
doc = new_doc()
e = s.new_contract(doc, contract_id="probe", need="probe")
if current != "draft":
e["phase"] = current
s.transition_contract(doc, "probe", to)
cases.append({"fn": "transition", "name": name,
"input": {"current": current, "to": to},
"expected": {"ok": True}})
except Exception as ex: # noqa: BLE001 — intentional: the case IS the error verdict; the oracle's error text becomes the truth column
cases.append({"fn": "transition", "name": name,
"input": {"current": current, "to": to},
"expected": {"ok": False, "error": str(ex)}})
tv("legal_approved", "draft", "approved")
tv("legal_building", "approved", "building")
tv("legal_verifying", "building", "verifying")
tv("legal_migrating", "verifying", "migrating")
tv("legal_superseding", "migrating", "superseding")
tv("legal_closed", "superseding", "closed")
tv("legal_failed_retry", "failed", "building")
tv("legal_abandon", "building", "abandoned")
tv("illegal_closed_from_draft", "draft", "closed")
tv("illegal_rewind", "verifying", "draft")
tv("illegal_skip", "draft", "verifying")
tv("unknown_phase", "draft", "nonexistent_phase")
# ── contract lifecycle evolution ──────────────────────────────────────
doc = new_doc()
s.new_contract(doc, contract_id="life", need="port the lifecycle",
origin="builder", scope=["src/go/scaffold/"],
acceptance=["truth battery passes"])
e = s.transition_contract(doc, "life", "approved", evidence_ref="e1")
e = s.transition_contract(doc, "life", "building", evidence_ref="e2", note="x" * 300)
cases.append({
"fn": "lifecycle", "name": "evolution",
"input": {},
"expected": {"entity": e},
})
# duplicate contract
try:
s.new_contract(doc, contract_id="life", need="again")
dup = {"ok": True}
except ValueError as ex:
dup = {"ok": False, "error": str(ex)}
cases.append({"fn": "dup", "name": "duplicate", "input": {},
"expected": dup})
# ── check_scope ───────────────────────────────────────────────────────
doc2 = new_doc()
s.new_contract(doc2, contract_id="scoped", need="n", scope=["src/go/a/", "src/go/b/main.go"])
e2 = s.contract(doc2, "scoped")
cases.append({
"fn": "check_scope", "name": "mixed",
"input": {"changed": ["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"], "scope": ["src/go/a/", "src/go/b/main.go"]},
"expected": s.check_scope(doc2, "scoped",
["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"]),
})
doc3 = new_doc()
s.new_contract(doc3, contract_id="unscoped", need="n")
cases.append({
"fn": "check_scope", "name": "unscoped",
"input": {"changed": ["anything.py"], "scope": []},
"expected": s.check_scope(doc3, "unscoped", ["anything.py"]),
})
# ── layout obligations ────────────────────────────────────────────────
doc4 = new_doc()
add_entity(doc4, id="layout:panel", kind="layout_contract",
identities={"component": "Panel", "region": "sidebar",
"implementation": "src/go/panel.go", "lock": False,
"provenance": "scaffold"}, maturity="DECLARED")
add_entity(doc4, id="layout:header", kind="layout_contract",
identities={"component": "Header", "region": "top",
"implementation": "src/go/header.go", "lock": True,
"provenance": "user"}, maturity="DECLARED")
cases.append({
"fn": "layout", "name": "mixed_locks",
"input": {"affected": ["Panel", "region:top"], "doc": doc4},
"expected": {"obligations": s.layout_obligations(doc4, ["Panel", "region:top"])},
})
cases.append({
"fn": "layout", "name": "no_hit",
"input": {"affected": ["Unrelated"], "doc": doc4},
"expected": {"obligations": []},
})
# ── json_frontier ─────────────────────────────────────────────────────
f = {"contracts": ["c1"], "level2_verification": ["v1"]}
cases.append({
"fn": "frontier", "name": "projection",
"input": {"frontier": f},
"expected": {"json": sc.json_frontier(f)},
})
out = {
"oracle": "src/rune/scaffold.py (live Scaffold, now frozen)",
"generated": "2026-10-07",
"frozen_now": FROZEN,
"cases": cases,
}
target = REPO / "src/go/scaffold" / "truth_scaffold.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
import shutil
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""gen_tool_executor_auth_truth.py — the tool-executor authority truth
(contract BUILDER-2026-10-07T19:10:38Z-cc6be2).
Freezes the LIVE READ_ONLY_TOOLS set + the prepare-tool routing verdicts
so CI asserts the Go set equals the Python set FOREVER. The run_command
wrong-add this battery pins was a security-adjacent divergence: a mutating
tool treated as read-only bypasses operator approval.
Usage: python3 scripts/gen_tool_executor_auth_truth.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import tool_executor as te # noqa: E402
def main():
cases = [
{
"fn": "read_only_tools", "name": "exact_set",
"input": {},
"expected": {"tools": sorted(te.READ_ONLY_TOOLS)},
},
{
"fn": "prepare_routing", "name": "run_command_requires_approval",
"input": {"tool": "run_command", "args": {"command": "ls -la"}},
"expected": {"read_only": "run_command" in te.READ_ONLY_TOOLS},
},
{
"fn": "prepare_routing", "name": "read_file_executes_now",
"input": {"tool": "read_file", "args": {"path": "/tmp"}},
"expected": {"read_only": "read_file" in te.READ_ONLY_TOOLS},
},
]
out = {
"oracle": "src/rune/tool_executor.py READ_ONLY_TOOLS + routing",
"generated": "2026-10-07",
"cases": cases,
}
target = REPO / "src/go/tool-executor" / "truth_auth.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""gen_tool_executor_truth.py — regenerate the tool-executor truth batteries
HERMETICALLY (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
Why: the original batch-2/3/4 generators baked the generation host's temp
paths (/mnt/workdrive/tmp*) into truth_fileops.json / truth_ops.json /
truth_paths.json — inputs AND expectations. CI's runner user cannot read
those paths, so the parity gate failed with permission-denied (go-surface,
run 19792 job 56712) despite a locally green tree.
Design (the environment leak, closed):
- Fixtures are created at generation time under roots the ORACLE ITSELF
allows: read root = first creatable dir in ALLOWED_READ_DIRS (prefers
/tmp), write root = first creatable dir in ALLOWED_WRITE_DIRS.
- Every occurrence of those roots — in inputs AND expectations — is
replaced with {{TMP}} / {{WROOT}} placeholders.
- Each truth file carries a `fixtures` manifest (files + dirs) so the Go
test recreates the SAME fixture content under ITS OWN roots at runtime.
Both sides then run the same case semantics on their own environment;
substituted bytes must agree exactly.
- The grep-hit comparison stays order-insensitive (sorted line sets) on the
Go side: the oracle's os.walk order is OS-scheduling dependent, not a
parity-relevant property. Everything else is exact after substitution.
Oracle authority: src/rune/tool_executor.py is called LIVE for every case
except the two cmd_gate verdicts whose oracle path would execute a shell
command (plain_ok / shell_ok use harmless commands instead: "ls -la" and
"true" — the gate branches they exercise do not inspect command content
beyond non-emptiness).
Usage: python3 scripts/gen_tool_executor_truth.py (from repo root)
"""
import json
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import tool_executor as te # noqa: E402 — the live oracle
def mk_root(dirs, tag):
"""First creatable temp dir under the given allowed dirs."""
for d in dirs:
try:
return tempfile.mkdtemp(prefix="rune-truth-", dir=d)
except OSError:
continue
raise RuntimeError(f"no creatable root among {dirs}")
def build_fixtures(root):
"""The shared fixture set: exactly what the cases observe."""
(Path(root) / "w.txt").write_text(
"".join(f"line {i}\n" for i in range(1, 21)), encoding="utf-8"
)
(Path(root) / "a.txt").write_text("the spine rebuild parity\n", encoding="utf-8")
(Path(root) / "b.log").write_text("spine appears here too\n", encoding="utf-8")
(Path(root) / "file.txt").write_text("content\n", encoding="utf-8")
(Path(root) / "adir").mkdir()
return {
"files": [
{"path": "{{TMP}}/w.txt",
"content": "".join(f"line {i}\n" for i in range(1, 21))},
{"path": "{{TMP}}/a.txt", "content": "the spine rebuild parity\n"},
{"path": "{{TMP}}/b.log", "content": "spine appears here too\n"},
{"path": "{{TMP}}/file.txt", "content": "content\n"},
],
"dirs": ["{{TMP}}/adir"],
}
def main():
read_root = mk_root(te.ALLOWED_READ_DIRS, "read")
write_root = mk_root(te.ALLOWED_WRITE_DIRS, "write")
fixtures = build_fixtures(read_root)
wpath = f"{read_root}/w.txt"
edited = "line 1\nCHANGED\n" + "".join(f"line {i}\n" for i in range(3, 21))
cases_fileops = [
{
"fn": "read_window", "name": "mid",
"input": {"path": f"{wpath}", "offset": 3, "limit": 5},
"expected": None,
},
{
"fn": "read_window", "name": "default",
"input": {"path": f"{wpath}"},
"expected": None,
},
{
"fn": "diff", "name": "simple",
"input": {"path": wpath, "new_content": edited},
"expected": None,
},
]
r = te._read_file({"path": wpath, "offset": 3, "limit": 5})
assert r["success"] is True, r
cases_fileops[0]["expected"] = {"ok": True, "result": r["result"]}
r = te._read_file({"path": wpath})
assert r["success"] is True, r
cases_fileops[1]["expected"] = {
"ok": True, "lines": len(r["result"].split("\n")),
}
diff = te.generate_diff(Path(wpath), edited)
dlines = diff.split("\n")
cases_fileops[2]["expected"] = {
"diff_head": dlines[:3],
"has_minus": any(l.startswith("-line 2") for l in dlines),
"has_plus": any(l.startswith("+CHANGED") for l in dlines),
}
cases_ops = [
{
"fn": "list_dir", "name": "basic",
"input": {"path": read_root},
"expected": None,
},
{
"fn": "list_dir", "name": "pattern",
"input": {"path": read_root, "pattern": "*.txt"},
"expected": None,
},
{
"fn": "grep", "name": "hit",
"input": {"path": read_root, "pattern": "spine"},
"expected": None,
},
{
"fn": "grep", "name": "miss",
"input": {"path": read_root, "pattern": "absent-token"},
"expected": None,
},
# The gate branches: pipe_refused and shell_empty return BEFORE any
# execution (live oracle); plain_ok / shell_ok would execute, so they
# run harmless commands ("ls -la", "true") — the gate branches they
# exercise do not inspect command content beyond non-emptiness.
{
"fn": "cmd_gate", "name": "pipe_refused",
"input": {"command": "ls -la | head"},
"expected": None,
},
{
"fn": "cmd_gate", "name": "plain_ok",
"input": {"command": "ls -la"},
"expected": None,
},
{
"fn": "cmd_gate", "name": "shell_ok",
"input": {"command": "true", "shell": True},
"expected": None,
},
{
"fn": "cmd_gate", "name": "shell_empty",
"input": {"shell": True},
"expected": None,
},
]
r = te._list_directory({"path": read_root})
assert r["success"] is True, r
cases_ops[0]["expected"] = {"ok": True, "result": r["result"]}
r = te._list_directory({"path": read_root, "pattern": "*.txt"})
assert r["success"] is True, r
cases_ops[1]["expected"] = {"ok": True, "result": r["result"]}
r = te._search_files({"path": read_root, "pattern": "spine"})
assert r["success"] is True and r["result"] != "(no matches)", r
cases_ops[2]["expected"] = {"ok": True, "result": r["result"]}
r = te._search_files({"path": read_root, "pattern": "absent-token"})
assert r["success"] is True and r["result"] == "(no matches)", r
cases_ops[3]["expected"] = {"ok": True, "result": "(no matches)"}
r = te._run_command({"command": "ls -la | head"})
cases_ops[4]["expected"] = {"success": r["success"], "error": r.get("error", "")}
assert r["success"] is False, r
r = te._run_command({"command": "ls -la"})
cases_ops[5]["expected"] = {"success": r["success"], "error": r.get("error", "")}
assert r["success"] is True, r
r = te._run_command({"command": "true", "shell": True})
cases_ops[6]["expected"] = {"success": r["success"], "error": r.get("error", "")}
assert r["success"] is True, r
r = te._run_command({"shell": True})
cases_ops[7]["expected"] = {"success": r["success"], "error": r.get("error", "")}
assert r["success"] is False, r
cases_paths = [
{
"fn": "resolve", "name": "abs",
"input": {"path": f"{read_root}/file.txt"},
"expected": None,
},
{
"fn": "validate_read", "name": "hit",
"input": {"path": f"{read_root}/file.txt"},
"expected": None,
},
{
"fn": "validate_read", "name": "miss",
"input": {"path": f"{read_root}/missing.txt"},
"expected": None,
},
{
"fn": "validate_read", "name": "blocked",
"input": {"path": "/etc/hostname"},
"expected": None,
},
{
"fn": "validate_write", "name": "protected",
"input": {"path": "/etc/x.txt"},
"expected": None,
},
{
"fn": "validate_write", "name": "outside",
"input": {"path": "/usr/share/x.txt"},
"expected": None,
},
{
"fn": "validate_write", "name": "ok",
"input": {"path": f"{write_root}/new.txt"},
"expected": None,
},
]
resolved = str(te.resolve_path(f"{read_root}/file.txt"))
cases_paths[0]["expected"] = {"resolved": resolved}
ok, msg, _ = te.validate_read_path(f"{read_root}/file.txt")
cases_paths[1]["expected"] = {"ok": ok}
assert ok is True, msg
ok, msg, _ = te.validate_read_path(f"{read_root}/missing.txt")
cases_paths[2]["expected"] = {"ok": ok, "msg": msg}
assert ok is False, msg
ok, msg, _ = te.validate_read_path("/etc/hostname")
cases_paths[3]["expected"] = {"ok": ok}
assert ok is False, msg
ok, msg, _ = te.validate_write_path("/etc/x.txt")
cases_paths[4]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
assert ok is False, msg
ok, msg, _ = te.validate_write_path("/usr/share/x.txt")
cases_paths[5]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
assert ok is False, msg
ok, msg, resolved = te.validate_write_path(f"{write_root}/new.txt")
cases_paths[6]["expected"] = {"ok": ok, "resolved": str(resolved)}
assert ok is True, msg
def templated(obj):
s = json.dumps(obj, ensure_ascii=False)
s = s.replace(read_root, "{{TMP}}").replace(write_root, "{{WROOT}}")
return json.loads(s)
out = {
"oracle": "src/rune/tool_executor.py",
"generated": "2026-10-07 (hermetic regeneration)",
"placeholders": {
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)",
},
"fixtures": fixtures,
"cases": None,
}
for name, cases in (
("truth_fileops.json", cases_fileops),
("truth_ops.json", cases_ops),
("truth_paths.json", cases_paths),
):
doc = dict(out)
doc["cases"] = templated(cases)
text = json.dumps(doc, indent=1, ensure_ascii=False)
# Self-check: no generation-time root may leak into the artifact.
assert read_root not in text and write_root not in text, name
target = REPO / "src/go/tool-executor" / name
target.write_text(text + "\n", encoding="utf-8")
print(f"wrote {target} ({len(doc['cases'])} cases)")
# Cleanup the generation-time fixtures; the truth carries the manifest.
import shutil
shutil.rmtree(read_root, ignore_errors=True)
shutil.rmtree(write_root, ignore_errors=True)
if __name__ == "__main__":
main()
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""gen_tool_protocol_truth.py — factory batch 1 truth battery
(contract BUILDER-2026-10-07T22:11:52Z-9ffb73).
Live tool_protocol oracle fixtures: validate error texts, alias
canonicalization, ollama envelope normalization (incl. string-arguments
JSON), internal-protocol detection, extract_visible_text span stripping.
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import tool_protocol as tp # noqa: E402
def case(fn, name, inp, fn_lambda):
try:
expected = {"ok": True, "result": fn_lambda()}
except Exception as ex:
expected = {"ok": False, "error": str(ex)}
return {"fn": fn, "name": name, "input": inp, "expected": expected}
def main():
cases = []
R = lambda cid, n, a: {"call_id": cid, "name": n, "arguments": a, "source": "test"}
# validate: alias canonicalization
cases.append(case("validate", "alias_grep",
{"request": R("c1", "grep", {"pattern": "x", "path": "."})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c1", "grep", {"pattern": "x", "path": "."}))).to_dict()))
cases.append(case("validate", "alias_bash",
{"request": R("c2", "bash", {"command": "ls"})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c2", "bash", {"command": "ls"}))).to_dict()))
cases.append(case("validate", "unknown_tool",
{"request": R("c3", "nonexistent_tool", {})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c3", "nonexistent_tool", {}))).to_dict()))
cases.append(case("validate", "missing_required",
{"request": R("c4", "read_file", {})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c4", "read_file", {}))).to_dict()))
cases.append(case("validate", "unknown_arg",
{"request": R("c5", "read_file", {"path": "/tmp", "bogus": 1})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c5", "read_file", {"path": "/tmp", "bogus": 1}))).to_dict()))
cases.append(case("validate", "type_mismatch",
{"request": R("c6", "read_file", {"path": 123})},
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c6", "read_file", {"path": 123}))).to_dict()))
# ollama normalization: string arguments JSON
cases.append(case("ollama", "string_args",
{"message": {"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": "{\"path\": \"/tmp/x\"}"}}]}},
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": json.dumps({"path": "/tmp/x"})}}]})]))
cases.append(case("ollama", "malformed_args_json",
{"message": {"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]}},
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]})]))
# internal protocol detection
for name, text in [
("result_marker", "[tool result] output here"),
("error_marker", "[tool error] boom"),
("xml_call", "<tool_call>{\"tool\": \"x\"}</tool_call>"),
("clean_text", "just ordinary assistant text with JSON: {\"a\": 1}"),
]:
cases.append(case("internal", name, {"text": text},
lambda t=text: {"contains": tp.contains_internal_protocol(t)}))
# extract_visible_text
for name, text in [
("inline_call", 'before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}} after'),
("fenced_call", '```json\n{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}}\n```'),
("xml_call", 'before <tool_call>{\"tool\": \"x\", \"args\": {}}</tool_call> after'),
("plain", "no protocol here at all"),
]:
cases.append(case("visible", name, {"text": text},
lambda t=text: {"visible": tp.extract_visible_text(t)}))
# normalize_text_tool_calls end-to-end
cases.append(case("normalize", "text_fallback",
{"text": '{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}'},
lambda: [r.to_dict() for r in tp.normalize_text_tool_calls(
'{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}')]))
out = {"oracle": "src/rune/tool_protocol.py (live)", "generated": "2026-10-07", "cases": cases}
target = REPO / "src/go/tool-protocol" / "truth_protocol.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
if __name__ == "__main__":
main()
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""gen_warm_router_digest_truth.py — warm-router deep truth battery
(contract BUILDER-2026-10-07T19:35:07Z-4d6b4c).
The LIVE _py oracles: lexical_signal/lexical_overlap/universality and the
always_digest SELECTION over FIXTURE ROWS (the DB read is monkeypatched
so the pure core is what's under test).
Usage: python3 scripts/gen_warm_router_digest_truth.py (from repo root)
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import warm_router as wr # noqa: E402
def main():
cases = []
def add(fn, name, inp, expected):
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
# ── lexical signal/overlap ────────────────────────────────────────────
add("lexical", "signal_strips_punct",
{"body": "The curator gates memory writes (always)."},
{"words": sorted(wr._lexical_signal("The curator gates memory writes (always)."))})
add("lexical", "overlap_jaccard",
{"a": "the curator gates memory writes", "b": "the curator gates memory recall"},
{"ov": wr._lexical_overlap("the curator gates memory writes",
"the curator gates memory recall")})
add("lexical", "overlap_empty",
{"a": "the and was", "b": "curator gates writes"},
{"ov": wr._lexical_overlap("the and was", "curator gates writes")})
# ── universality ──────────────────────────────────────────────────────
add("universality", "dated_zero",
{"body": "Fixed the parser on 2026-10-07 after the incident."},
{"u": wr._universality("Fixed the parser on 2026-10-07 after the incident.")})
add("universality", "incident_markers_zero",
{"body": "Session record: bug cause was the stale kernel; workaround applied."},
{"u": wr._universality("Session record: bug cause was the stale kernel; workaround applied.")})
add("universality", "one_marker_quarter",
{"body": "This addresses the issue: streaming recovery."},
{"u": wr._universality("This addresses the issue: streaming recovery.")})
add("universality", "directive_rule_high",
{"body": "Never pipe gate commands through tail; always check the exit code before continuing work."},
{"u": wr._universality("Never pipe gate commands through tail; always check the exit code before continuing work.")})
add("universality", "rambling_penalty",
{"body": " ".join(["word"] * 120) + " never always must keep"},
{"u": wr._universality(" ".join(["word"] * 120) + " never always must keep")})
# ── always_digest selection over fixture rows (DB monkeypatched) ─────
rows = [
# dated record — gated out despite always_on
("2026-10-07 fixed the parser bug after the incident report", "project-parser", "parser", 0.9, "", 1, 0.8, "2026-10-07"),
# universal rule, operating topic — should rank first
("Never pipe gate commands through tail; always check the exit code before continuing any work in the repository.", "operating-gates", "gate", 0.7, "", 1, 0.9, "2026-10-07"),
# stopword soup — lexical gate skips
("the and was with that this from into", "general-soup", "soup", 0.5, "", 0, 0.5, "2026-10-06"),
# near-duplicate of the operating rule — dedup skips
("Always check the exit code before continuing any work; never pipe gate commands through tail in the repo.", "operating-gates-copy", "gate2", 0.8, "", 1, 0.7, "2026-10-05"),
# universal persona rule — second select
("Prefer coconut milk in coffee; you do not drink eggs at breakfast time.", "human-prefs", "coffee", 0.6, "", 0, 0.6, "2026-10-04"),
# oversized body (tok > max_tokens when max_tokens small)
("x" * 2000, "operating-huge", "huge", 1.0, "", 1, 1.0, "2026-10-03"),
]
fixture_rows = [tuple(r) for r in rows]
class FakeDB:
def execute(self, *_a, **_k):
return self
def fetchall(self):
return fixture_rows
def close(self):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
saved_db = wr._db
wr._db = lambda: FakeDB()
try:
digest = wr.always_digest(max_tokens=220)
finally:
wr._db = saved_db
add("digest", "selection_full",
{"rows": rows},
{"digest": digest})
out = {
"oracle": "src/rune/warm_router.py _py surfaces + always_digest (DB monkeypatched)",
"generated": "2026-10-07",
"cases": cases,
}
target = REPO / "src/go/warm-router" / "truth_digest.json"
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
print(f"wrote {target} ({len(cases)} cases)")
print("digest slugs:", [d["slug"] for d in digest])
if __name__ == "__main__":
main()
+5 -2
View File
@@ -86,9 +86,12 @@ def main() -> int:
if live_state == "open" and rec["tracker_state"] == "closed" and rec["reconciliation_state"] in TERMINAL_STATES:
drift.append(f"#{num}: live state open but artifact says closed/{rec['reconciliation_state']}")
live_bug = "bug" in [l["name"] for l in li.get("labels", [])]
if live_bug and not rec.get("live_defect"):
# Same open-only exemption as the artifact-records block above: a
# closed issue's labels are historical record; FIXED_VERIFIED issues
# legitimately keep the bug label after closure.
if live_bug and rec.get("tracker_state") == "open" and not rec.get("live_defect"):
drift.append(f"#{num}: live bug label present but artifact records no live defect")
if not live_bug and rec.get("live_defect"):
if not live_bug and rec.get("live_defect") and rec.get("tracker_state") == "open":
drift.append(f"#{num}: artifact records live defect but live bug label absent")
# Live defects in the artifact must all be OPEN in the tracker
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""install_mimo_hook.py — T21.2 §21.2D (T22: multi-hook): canonical MiMo
hook installer.
Canonical sources: config/mimo-hooks/*.ts
Targets: ~/.mimocode/hooks/<same-name>.ts
Every hook is hash-verified after install; drift is detected in both
directions. T22 added builder-bridge.ts to the managed set — it was
previously installed by hand, unversioned, which is how it silently rotted
(hardcoded ephemeral interpreter). A hook that reaches the builder must be
version-controlled and drift-checked like any other authority.
Usage:
python3 scripts/install_mimo_hook.py # sync all managed hooks
python3 scripts/install_mimo_hook.py --check # verify only, install nothing
"""
import hashlib
import shutil
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
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"]
def sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def sync_one(name: str, check_only: bool) -> bool:
canonical = CANONICAL_DIR / name
target = TARGET_DIR / name
if not canonical.is_file():
print(f"canonical source missing: {canonical}")
return False
canon_hash = sha256(canonical)
if target.is_file() and sha256(target) == canon_hash:
print(f"in-sync: {name} {canon_hash[:12]}")
return True
if check_only:
state = "DRIFT" if target.is_file() else "MISSING"
inst = sha256(target)[:12] if target.is_file() else "-"
print(f"{state}: {name} canonical={canon_hash[:12]} installed={inst}")
return False
if target.is_file():
print(f"DRIFT: {name} canonical={canon_hash[:12]} installed={sha256(target)[:12]}")
else:
print(f"hook missing at {target}")
TARGET_DIR.mkdir(parents=True, exist_ok=True)
shutil.copy2(canonical, target)
if sha256(target) != canon_hash:
print(f"INSTALL FAILED: {name} hash mismatch after copy")
return False
print(f"installed: {name} {canon_hash[:12]}")
return True
def main() -> int:
check_only = "--check" in sys.argv
ok = True
for name in MANAGED:
ok = sync_one(name, check_only) and ok
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""install_mimo_tools.py — canonical MiMo tool installer (TTY-repair tranche).
Canonical sources: config/mimo-tools/{bash.ts,await-operation.ts}
Targets: ~/.mimocode/tools/{bash.ts,await-operation.ts}
Verifies hashes after install; detects drift in both directions
(the parity test reads these hashes).
"""
import hashlib
import shutil
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
TOOLS = ["bash.ts", "await-operation.ts"]
def sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def main() -> int:
rc = 0
for name in TOOLS:
canon = REPO / "config" / "mimo-tools" / name
target = Path.home() / ".mimocode" / "tools" / name
if not canon.is_file():
print(f"{name}: canonical source missing: {canon}")
rc = 1
continue
canon_hash = sha256(canon)
if target.is_file() and sha256(target) == canon_hash:
print(f"{name}: in-sync {canon_hash[:12]}")
continue
drift = f"DRIFT canonical={canon_hash[:12]}"
if target.is_file():
drift += f" installed={sha256(target)[:12]}"
else:
drift += " installed=MISSING"
print(f"{name}: {drift} — installing")
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(canon, target)
if sha256(target) != canon_hash:
print(f"{name}: INSTALL FAILED (hash mismatch after copy)")
rc = 1
else:
print(f"{name}: installed {canon_hash[:12]}")
return rc
if __name__ == "__main__":
sys.exit(main())
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""install_wr_daemon.py — REAL_ASYNC_BRIDGE §6: daemon governance.
Canonical source lives IN THIS REPO at bridge/daemon/. The active install
is ~/.local/share/mimo-wait-resume/. This script is the ONLY supported
way to move code between them:
- copy canonical source → install dir (atomic, per file)
- verify sha256 convergence (fail loud on divergence)
- optionally restart the daemon via scripts/start_wr_daemon.py
Never hand-edit the installed copy: edit the repo source, install, restart.
"""
from __future__ import annotations
import argparse
import hashlib
import shutil
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
CANONICAL = REPO / "bridge" / "daemon"
INSTALL = Path.home() / ".local/share/mimo-wait-resume"
FILES = ("daemon.py", "waitstore.py", "verify.py", "completion_file.py")
def sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--check", action="store_true", help="verify convergence only; no writes")
ap.add_argument("--restart", action="store_true", help="restart the daemon after install")
args = ap.parse_args()
if not CANONICAL.is_dir():
print(f"canonical source missing: {CANONICAL}")
return 1
INSTALL.mkdir(parents=True, exist_ok=True)
diverged = []
for name in FILES:
src, dst = CANONICAL / name, INSTALL / name
if args.check:
if not dst.is_file() or sha256(src) != sha256(dst):
diverged.append(name)
continue
if not dst.is_file() or sha256(src) != sha256(dst):
tmp = dst.with_suffix(f".tmp.{name}")
shutil.copy2(src, tmp)
tmp.replace(dst) # atomic
print(f"installed: {name} {sha256(src)[:12]}")
else:
print(f"unchanged: {name} {sha256(src)[:12]}")
if args.check:
if diverged:
print(f"DIVERGED: {', '.join(diverged)} — run install_wr_daemon.py")
return 1
print("CONVERGED: installed copy == canonical repo source")
return 0
# post-install convergence proof
bad = [n for n in FILES if sha256(CANONICAL / n) != sha256(INSTALL / n)]
if bad:
print(f"INSTALL FAILED convergence: {bad}")
return 1
print("CONVERGED: installed copy == canonical repo source")
if args.restart:
rc = subprocess.call([sys.executable, str(REPO / "scripts" / "start_wr_daemon.py")])
if rc != 0:
print("restart failed")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""notify_event_bridge.py — T16 §16C: ci.yml's final job posts the CI
completion WAKE to the local W/R event bridge (daemon /process-event).
The event is a WAKE, never completion truth: the waiting controller
(ci_event_wait.py) re-reads the canonical Gitea verdict via
deploy_qualify.py after resuming. Best-effort by design — if the bridge
is unreachable the waiter's watchdog still recovers truthfully (that is
the §16D lost-event contract), so a notify failure must NEVER fail CI.
REAL_ASYNC_BRIDGE (R3): the completion event is ALSO written directly as
a durable completion file (resumes/<operation_id>.done). Delivery no
longer depends on the daemon being alive: the waiter blocks on inotify
for the file itself (kernel wake, zero latency, zero CPU), and the
durable file survives daemon AND waiter restarts (R9). The write is
best-effort on the same terms as the POST — never fails CI.
"""
from __future__ import annotations
import json
import os
import sys
import time
import urllib.request
BRIDGE_URL = os.environ.get("RUNE_EVENT_BRIDGE_URL",
"http://127.0.0.1:8791")
def main() -> int:
sha = os.environ.get("CI_SHA", "")
result = os.environ.get("CI_RESULT", "") # needs.<job>.result
if not sha:
print("notify-event-bridge: no CI_SHA — nothing to wake", flush=True)
return 0
payload = {
"operation_id": f"ci-{sha}",
"provider": "gitea",
"event_type": "ci_completed",
"sha": sha,
# exit_code is informational only; the wake is not the verdict
"exit_code": 0 if result == "success" else 1,
"completed_at": time.time(),
}
# R8 lost-event failure-recovery: operation-scoped fault injection.
# A rule keyed by the EXACT operation_id suppresses BOTH normal
# delivery channels (durable completion file AND bridge POST), so the
# waiter's watchdog/failure-detector path is the only route to truth.
# Disabled by default (no rule file => zero behavior change) and
# observable: every decision is recorded in the R8 fault log.
try:
import r8_fault # noqa: E402 — sibling module
if r8_fault.should_suppress(payload["operation_id"]):
r8_fault.record(payload["operation_id"], "suppressed",
{"sha": sha, "exit_code": payload["exit_code"],
"source_terminal_at": payload["completed_at"]})
print("notify-event-bridge: R8 fault injection — completion "
f"event intentionally suppressed for "
f"{payload['operation_id']} (no .done file, no POST); "
"watchdog authority recovery is the expected path",
flush=True)
return 0
r8_fault.record(payload["operation_id"], "delivered_normally",
{"sha": sha, "rule_active": False})
except Exception as exc: # noqa: BLE001 — fault seam must never break delivery
print(f"notify-event-bridge: R8 fault-seam probe failed ({exc}); "
"continuing with normal delivery", flush=True)
# R3 durable delivery: write the completion file FIRST — it works even
# if the bridge daemon is down, and the atomic rename means any waiter
# (inotify-watching or starting later) sees a complete file or none.
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import completion_file as cf # noqa: E402 — sibling module
written = cf.write_completion(payload["operation_id"], payload)
print(f"notify-event-bridge: durable completion file {written}",
flush=True)
except Exception as exc: # noqa: BLE001 — delivery is best-effort; watchdog recovery covers a lost completion file (§16D contract)
print(f"notify-event-bridge: completion-file write failed ({exc}); "
f"bridge POST and watchdog recovery still cover delivery",
flush=True)
for attempt in range(2):
try:
req = urllib.request.Request(
BRIDGE_URL + "/process-event",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=5) as r:
resp = json.loads(r.read())
print(f"notify-event-bridge: woke bridge "
f"(matched={resp.get('matched')})", flush=True)
return 0
except Exception as exc: # noqa: BLE001 — wake is best-effort by design; failure is logged and the waiter's watchdog recovery covers a lost wake (§16D contract)
if attempt == 0:
time.sleep(2)
else:
print(f"notify-event-bridge: bridge unreachable ({exc}); "
f"watchdog recovery covers a lost wake", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""polyglot_lint.py — the language-AWARE lint gate (contract
BUILDER-2026-10-07T20:01:40Z-ac6140).
Why: the pipeline linted Python hard (ruff + ratchet) but Rust NOT AT ALL
(crates build, never clippy) and Go only via vet (gofmt drifted across 41
files, including landed batches). This gate applies the RIGHT linter to
whatever changed:
Python → ruff check
Go → go vet (per module) + gofmt -l (formatting is a gate)
Rust → cargo clippy -- -D warnings (per changed crate) + cargo fmt --check
other → noted and skipped
Modes:
(default) changed files vs the push range (git diff against
origin/master / the last push ref; falls back to the
last commit when no upstream exists)
--all full tree
--files a,b,c explicit list
Exit non-zero on any finding. The cron fault-watcher and the pre-push
routine both run this; CI runs the Go+Rust halves natively.
Usage:
python3 scripts/polyglot_lint.py # changed-only
python3 scripts/polyglot_lint.py --all # everything
"""
import argparse
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
LANG_BY_EXT = {
".py": "python",
".go": "go",
".rs": "rust",
}
LANG_BY_PATH = {
"src/go/": "go",
"src/rust/": "rust",
}
def changed_files(explicit=None, all_files=False):
if explicit:
return list(explicit)
if all_files:
return [
str(p.relative_to(REPO))
for p in REPO.rglob("*")
if p.is_file() and not str(p).startswith((".git", "store/", "target"))
]
base = None
for ref in ("origin/master", "gitea/master", "HEAD~1"):
r = subprocess.run(["git", "rev-parse", "--verify", ref],
capture_output=True, text=True, cwd=REPO)
if r.returncode == 0:
base = ref
break
if base is None:
return []
r = subprocess.run(["git", "diff", "--name-only", f"{base}...HEAD"],
capture_output=True, text=True, cwd=REPO)
return [f for f in r.stdout.splitlines() if f.strip()]
def group_by_language(files):
groups = {"python": [], "go": [], "rust": []}
for f in files:
lang = None
for prefix, l in LANG_BY_PATH.items():
if f.startswith(prefix):
lang = l
break
if lang is None:
lang = LANG_BY_EXT.get(Path(f).suffix)
if lang:
groups[lang].append(f)
return groups
def run(cmd, cwd=None):
r = subprocess.run(cmd, cwd=cwd or REPO, capture_output=True, text=True)
return r.returncode, r.stdout, r.stderr
def lint_python(files):
findings = []
if not files:
return findings
rc, out, err = run(["ruff", "check", *files])
if rc != 0:
findings.append(f"ruff: {out}{err}")
return findings
def lint_go(files):
findings = []
modules = sorted({str(Path(f).parent) for f in files})
# collapse to module roots (dirs containing go.mod)
for mod in modules:
mod_path = REPO / mod
while mod_path != REPO and not (mod_path / "go.mod").exists():
mod_path = mod_path.parent
if mod_path == REPO:
continue
rel = mod_path.relative_to(REPO)
rc, out, err = run(["go", "vet", "./..."], cwd=mod_path)
if rc != 0:
findings.append(f"go vet ({rel}): {out}{err}")
# gofmt across the changed files' modules (formatting is a GATE)
fmt_targets = [f for f in files if f.endswith(".go")]
if fmt_targets:
rc, out, _ = run(["gofmt", "-l", *fmt_targets])
if rc != 0:
findings.append(f"gofmt: failed to run")
elif out.strip():
findings.append(
"gofmt unformatted (run `gofmt -w` on these):\n "
+ "\n ".join(out.splitlines())
)
return findings
def lint_rust(files):
findings = []
crates = sorted({str(Path(f).relative_to(REPO)) for f in files})
crate_roots = set()
for c in crates:
p = REPO / c
while p != REPO and not (p / "Cargo.toml").exists():
p = p.parent
if p != REPO:
crate_roots.add(p)
for crate in sorted(crate_roots):
rc, out, err = run(["cargo", "clippy", "--", "-D", "warnings"], cwd=crate)
if rc != 0:
findings.append(f"clippy ({crate.name}):\n{out}{err}")
rc, out, err = run(["cargo", "fmt", "--check"], cwd=crate)
if rc != 0:
findings.append(f"rustfmt ({crate.name}):\n{out}{err}")
return findings
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--all", action="store_true", help="lint the full tree")
ap.add_argument("--files", help="comma-separated explicit file list")
args = ap.parse_args()
explicit = args.files.split(",") if args.files else None
files = changed_files(explicit, args.all)
if not files:
print("polyglot lint: no changed files detected — nothing to do")
return 0
groups = group_by_language(files)
print(f"polyglot lint: {len(files)} changed → "
f"python={len(groups['python'])} go={len(groups['go'])} "
f"rust={len(groups['rust'])}")
all_findings = []
all_findings += lint_python(groups["python"])
all_findings += lint_go(groups["go"])
all_findings += lint_rust(groups["rust"])
if all_findings:
print(f"POLYGLOT LINT: {len(all_findings)} finding(s):")
for f in all_findings:
print(f" - {f}")
return 1
print("polyglot lint: clean")
return 0
if __name__ == "__main__":
sys.exit(main())
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""r8_fault.py — REAL_ASYNC_BRIDGE R8: operation-scoped lost-event fault
injection (evidence-only harness).
DESIGN CONTRACT (R8 §3):
disabled by default no rule file -> zero behavior change
operation-scoped rule keyed by EXACT operation_id
reversible delete the rule file / rule entry
observable every decision appended to a JSONL log
cannot affect other waits rule file checked by operation_id only
The ONLY consumer is scripts/notify_event_bridge.py: before normal
delivery it consults should_suppress(operation_id). Suppression means:
- NO durable completion file is written
- NO bridge POST (no /process-event wake)
- the suppression decision is recorded as R8 fault evidence
Nothing else in the stack reads the rule. The daemon, waitstore,
authority verification, and completion-file machinery are untouched.
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
RUNE_WR_ROOT = os.environ.get("RUNE_WR_ROOT") or os.environ.get("WR_ROOT") or "/mnt/workdrive/wr-bridge"
RULES_PATH = Path(RUNE_WR_ROOT) / "r8_suppress_rules.json"
FAULT_LOG = Path(RUNE_WR_ROOT) / "r8_fault_log.jsonl"
REASON = "event intentionally suppressed for R8 lost-event failure-recovery"
def _rules() -> dict:
"""Read the rule file. Absent/unreadable => no suppression (fail-open
to NORMAL delivery; the fault must never take the healthy path down)."""
try:
return json.loads(RULES_PATH.read_text())
except Exception: # noqa: BLE001 — absent rule file is the default state
return {}
def lookup(operation_id: str) -> dict | None:
"""Return the active suppression rule for EXACTLY this operation_id,
or None. Expired rules are ignored (not deleted — deletion is the
operator's explicit disable action)."""
spec = _rules()
if not spec.get("enabled"):
return None
rule = spec.get("operations", {}).get(operation_id)
if not rule:
return None
expires = rule.get("expires_at")
if expires:
try:
if time.time() > time.mktime(time.strptime(expires, "%Y-%m-%dT%H:%M:%SZ")) - time.timezone:
return None
except ValueError:
return None
return rule
def should_suppress(operation_id: str) -> bool:
return lookup(operation_id) is not None
def record(operation_id: str, stage: str, extra: dict | None = None) -> None:
"""Append a durable fault-evidence line (best-effort, never raises)."""
try:
entry = {
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"operation_id": operation_id,
"reason": REASON,
"stage": stage,
**(extra or {}),
}
FAULT_LOG.parent.mkdir(parents=True, exist_ok=True)
with FAULT_LOG.open("a") as f:
f.write(json.dumps(entry, sort_keys=True) + "\n")
except OSError as exc: # noqa: BLE001 — evidence logging must never break CI
print(f"r8_fault: evidence log write failed: {exc}", file=sys.stderr)
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Tranche R1 Wave 0 — whole-project inventory generator.
Parses every .py file under src/rune/ with the stdlib AST and emits
docs/rebase/rebase-inventory.json: files, classes, functions (with signatures and
line ranges), imports, and best-effort caller/callee edges (intra-module call
graph + cross-module edges resolved through import tables).
Deterministic and idempotent: same tree in, same JSON out (sorted keys,
sorted files). The issue-factory reads this file; nothing else may edit it.
"""
from __future__ import annotations
import ast
import json
import sys
from collections import defaultdict
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SRC = REPO / "src" / "rune"
OUT = REPO / "docs" / "rebase" / "rebase-inventory.json"
def _arg_str(a: ast.arg) -> str:
s = a.arg
if a.annotation is not None:
# Degraded signature (no annotation) is deterministic metadata — never
# let one pathological annotation node abort the whole inventory walk.
try:
s += f": {ast.unparse(a.annotation)}"
except (AttributeError, RecursionError, ValueError):
pass
return s
def _signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
parts = [_arg_str(a) for a in node.args.args]
if node.args.vararg:
parts.append(f"*{node.args.vararg.arg}")
elif node.args.kwonlyargs:
parts.append("*")
parts += [_arg_str(a) for a in node.args.kwonlyargs]
if node.args.kwarg:
parts.append(f"**{node.args.kwarg.arg}")
ret = ""
if node.returns is not None:
# Same degraded-metadata contract as _arg_str.
try:
ret = f" -> {ast.unparse(node.returns)}"
except (AttributeError, RecursionError, ValueError):
pass
return f"({', '.join(parts)}){ret}"
class FileVisitor(ast.NodeVisitor):
def __init__(self) -> None:
self.functions: list[dict] = []
self.classes: list[dict] = []
self.imports: list[dict] = []
self.calls: list[tuple[int, str]] = [] # (lineno, dotted call name)
def _fn(self, node, parents: list[str]) -> dict:
return {
"name": ".".join([*parents, node.name]),
"signature": _signature(node),
"line_start": node.lineno,
"line_end": node.end_lineno or node.lineno,
"decorators": [ast.unparse(d) for d in node.decorator_list
if isinstance(d, (ast.Name, ast.Attribute, ast.Call))],
"async": isinstance(node, ast.AsyncFunctionDef),
}
def visit_ClassDef(self, node: ast.ClassDef) -> None:
methods = []
for child in node.body:
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
methods.append(self._fn(child, [node.name]))
self.calls.extend(
(n.lineno, ast.unparse(n.func))
for n in ast.walk(child)
if isinstance(n, ast.Call) and not isinstance(n.func, ast.Name if False else type(None))
and isinstance(n.func, (ast.Name, ast.Attribute))
)
self.classes.append({
"name": node.name,
"bases": [ast.unparse(b) for b in node.bases],
"line_start": node.lineno,
"line_end": node.end_lineno or node.lineno,
"methods": methods,
})
# visit only non-function children: methods are already captured above,
# and a generic_visit would re-fire visit_FunctionDef on them (duplicate
# units + duplicate call edges). Nested classes are still visited.
for child in node.body:
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.visit(child)
def visit_FunctionDef(self, node) -> None:
# top-level only reached here (methods captured in ClassDef)
self.functions.append(self._fn(node, []))
self.calls.extend(
(n.lineno, ast.unparse(n.func))
for n in ast.walk(node)
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute))
)
# do not descend into nested defs separately; they are folded into parents
# (their calls are already captured by the ast.walk above). No generic_visit:
# it would re-collect nested defs as top-level functions and duplicate edges.
visit_AsyncFunctionDef = visit_FunctionDef
def visit_Import(self, node: ast.Import) -> None:
for a in node.names:
self.imports.append({"module": a.name, "as": a.asname, "line": node.lineno})
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
mod = node.module or ""
for a in node.names:
self.imports.append({"module": f"{mod}.{a.name}" if a.name != "*" else mod,
"as": a.asname or a.name, "line": node.lineno,
"from": True})
def collect_file(path: Path) -> dict:
src_text = path.read_text(encoding="utf-8", errors="replace")
tree = ast.parse(src_text)
v = FileVisitor()
v.visit(tree)
# module-level calls (outside defs/classes) — walk tree body directly.
# SKIP def/class statements: their bodies are walked by the visit_ methods;
# walking them here duplicated every call edge inside every top-level function.
for stmt in tree.body:
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
continue
for n in ast.walk(stmt):
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute)):
v.calls.append((n.lineno, ast.unparse(n.func)))
defined = {f["name"].split(".")[-1] for f in v.functions}
defined |= {m["name"].split(".")[-1] for c in v.classes for m in c["methods"]}
defined |= {c["name"] for c in v.classes}
callees = []
for lineno, call in v.calls:
root = call.split(".")[0].split("(")[0].strip()
callees.append({"line": lineno, "call": call,
"internal": root in defined or root == "self"})
return {
"path": str(path.relative_to(REPO)),
"lines": len(src_text.splitlines()),
"imports": sorted({i["module"] for i in v.imports}),
"import_lines": v.imports,
"functions": sorted(v.functions, key=lambda f: (f["line_start"], f["name"])),
"classes": sorted(v.classes, key=lambda c: c["line_start"]),
"calls": callees,
}
def cross_module_edges(files: dict[str, dict]) -> dict[str, set[str]]:
"""Resolve import tables: module-stem -> set of imported module stems
(rune-internal only), so the factory can attribute cross-module edges."""
graph = defaultdict(set)
for rel, info in files.items():
stem = Path(rel).stem
for imp in info["imports"]:
top = imp.split(".")[0]
second = ".".join(imp.split(".")[:2])
if top == "rune":
graph[stem].add(imp.replace("rune.", "", 1).split(".")[0])
elif not imp.startswith("_") and top not in sys.stdlib_module_names:
graph[stem].add(f"ext:{top}")
return graph
def main() -> None:
py_files = sorted(SRC.rglob("*.py"))
files: dict[str, dict] = {}
for p in py_files:
try:
info = collect_file(p)
except SyntaxError as e: # a file that cannot parse is a finding, not a skip
info = {"path": str(p.relative_to(REPO)), "lines": 0, "imports": [],
"import_lines": [], "functions": [], "classes": [], "calls": [],
"parse_error": f"{type(e).__name__}: {e}"}
files[info["path"]] = info
graph = cross_module_edges(files)
total_fns = sum(len(f["functions"]) + sum(len(c["methods"]) for c in f["classes"])
for f in files.values())
doc = {
"_meta": {
"generator": "scripts/rebase_inventory.py",
"tranche": "R1 language rebase, Wave 0",
"note": "Machine-generated. Regenerate with the script; never hand-edit.",
"files": len(files),
"functions_total": total_fns,
"classes_total": sum(len(f["classes"]) for f in files.values()),
"lines_total": sum(f["lines"] for f in files.values()),
},
"import_graph": {k: sorted(v) for k, v in sorted(graph.items())},
"files": dict(sorted(files.items())),
}
OUT.write_text(json.dumps(doc, indent=1, sort_keys=False) + "\n", encoding="utf-8")
m = doc["_meta"]
print(f"inventory written: {OUT}")
print(f"files={m['files']} functions={m['functions_total']} "
f"classes={m['classes_total']} lines={m['lines_total']}")
if __name__ == "__main__":
main()
+356
View File
@@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""Tranche R1 Wave 0 — per-function issue factory.
Reads docs/rebase/rebase-inventory.json and emits one issue per unit (top-level
function or class method) following tranche R1 §7. Wave-batched, dedup-safe
(deterministic title + existing-issue scan before create), rate-limited.
Modes:
plan — coverage report: units per wave, unassigned modules (no silent drops)
sample — emit N rendered issues to a file for review (no Gitea call)
create — create issues in Gitea for one wave
Dedup contract: issue titles are deterministic —
[rebase w{wave}] {relpath}::{unit}
The factory fetches existing issue titles once per run and skips any that
already exist, so retries never duplicate (preserve-first Gitea addendum).
The factory NEVER writes behaviour prose. Current-behaviour paragraphs and
the three evidence answers (IMPROVE / FIX / MISSING CONNECTORS) are filled at
disposition time by the working agent — the issue carries the mandatory
fields and line ranges, not fabricated content.
"""
import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
INVENTORY = REPO_ROOT / "docs" / "rebase" / "rebase-inventory.json"
COVERAGE_OUT = REPO_ROOT / "docs" / "rebase" / "rebase-wave-coverage.json"
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
REPO = "conductor/rune"
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "shared"))
from gitea_auth import load_gitea_token # noqa: E402
TRANCHE_REF = "tranche R1 language rebase (2026-09-29), ~/.config/opencode/tranches/2026-09-29-language-rebase-tranche-R1.md"
# Wave membership from R1 §4 (initial, measured). Directory entries match by
# path prefix; bare stems match the file stem. Modules matching no entry are
# reported UNASSIGNED by `plan` — never silently dropped (R1 §9 risk 7).
WAVE_PLAN = {
2: [ # leaf utilities & canaries
"pins", "time_awareness", "message_queue", "event_envelope",
"attention_capsule", "complexity_scorer", "token_optimizer", "cache",
"prompt_sync", "chat_bubble", "canary_*",
],
3: [ # memory & persistence
"memory_store", "curator", "warm_router", "memory_consolidation",
"graph_memory", "sleep_time", "night_mode", "identity_anchors",
"query_memory", "memory_env", "warm_neuron_store", "subconscious_pass",
"task_persist",
],
4: [ # provider/model layer
"provider_registry", "model_connect", "model_routing", "model_profiles",
"provider_budget", "standardized_providers", "model_registry",
"connection_pool", "forge_client",
],
5: [ # orchestration, authority, plugins, skills, governance
"orchestration/", "microkernel", "contracts", "build_mode", "plugins/",
"skills/", "construction_authority", "construction_authority_cli",
"construction_decision", "construction_mode", "verification_construction",
"authority_recovery", "spine", "drift_core", "drift_ledger",
"drift_observability", "persona_gate", "safety_override", "witness",
"behavior", "growth_delta", "claim_audit", "verification_layer",
"drift_compression", "drift_cache", "self_improve", "persona_definition",
"outcome_ledger", "audit_ledger", "subsystem_health",
],
6: [ # runtime core & tooling
"tool_executor", "tool_protocol", "local_tool_runtime",
"capability_broker", "context_broker", "work_manager", "cron_engine",
"auto_tasks", "goal", "failure_auto_filer", "hook_engine",
"elastic_cognition", "event_bus", "system_registry", "abstraction",
"tiered_executor", "progressive_escalation", "tier_manager",
"parallel_processor", "shard_manager", "response_adapter",
"context_pressure", "auto_context", "cognitive_budgeter",
"recall_router", "inference_trace", "task_gate", "salience_gate",
"dependency_tracker", "rune_profiler",
],
7: [ # operator + TUI, last by design
"rune_operator", "rune_tui", "tui_validator", "voice_hybrid",
"voice_sync", "voicebox", "voice_injector", "message_widgets",
"rune_graphic", "rune_spinner", "__main__",
],
}
WAVE_NAMES = {
2: "leaf utilities & canaries",
3: "memory & persistence",
4: "provider/model layer",
5: "orchestration, authority, plugins, skills, governance",
6: "runtime core & tooling",
7: "operator + TUI",
}
def match_module(relpath: str):
"""Return the wave number for a module path, or None if unassigned."""
stem = Path(relpath).stem
for wave, patterns in WAVE_PLAN.items():
for pat in patterns:
if pat.endswith("/"):
if pat in relpath:
return wave
elif pat.endswith("*"):
if stem.startswith(pat[:-1]):
return wave
elif stem == pat:
return wave
return None
def iter_units(inv):
"""Yield (relpath, unit_name, entry) for every function and method."""
for relpath, mod in sorted(inv["files"].items()):
for fn in mod["functions"]:
yield relpath, fn["name"], fn
for cls in mod["classes"]:
for m in cls.get("methods", []):
yield relpath, f"{cls['name']}.{m['name']}", m
def internal_edges(relpath: str, entry: dict):
"""Collect internal (in-repo) call names and line numbers for a unit."""
lines = (entry["line_start"], entry["line_end"])
mod = inv_cache["files"][relpath]
calls = [c for c in mod.get("calls", [])
if lines[0] <= c["line"] <= lines[1] and c.get("internal")]
return calls
inv_cache = {}
def issue_title(wave: int, relpath: str, unit: str) -> str:
return f"[rebase w{wave}] {relpath}::{unit}"
def render_issue(wave: int, relpath: str, unit: str, entry: dict) -> dict:
calls = internal_edges(relpath, entry)
call_lines = "\n".join(
f" - `{c['call']}` (line {c['line']})" for c in calls[:20]
) or " - (no internal call edges in range)"
sig = entry.get("signature", "()")
body = f"""\
## Identity (from the Wave 0 inventory — machine-generated)
- **File**: `{relpath}`
- **Unit**: `{unit}`
- **Signature**: `def {unit}{sig}` (async: {entry.get('async', False)})
- **Line range**: {entry['line_start']}–{entry['line_end']}
- **Decorators**: {entry.get('decorators', []) or 'none'}
- **Internal call edges** (callers/callees inside the repo, from inventory):
{call_lines}
## Current behaviour
One honest paragraph from READING THE CODE at the line range above — what it
actually does, not what the docstring promises. Write this at disposition
time; the factory does not fabricate it.
## The three questions — each answered WITH EVIDENCE
1. **IMPROVE?** Can the unit be improved within its scope? Evidence or "no — because …".
(Bounded to unit scope; larger findings spawn NEW issues, never expand this one.)
2. **FIX?** Is it broken, buggy, or failing its expected outcome? Evidence: a failing
test, a reproduced fault, or a code-level defect citation. A "no" needs evidence too.
3. **MISSING CONNECTORS?** Are there expected outcomes this unit should connect to but
doesn't (unwired events, dead parameters, promised-but-absent calls)? Evidence required.
A silent skip on any question is a factory bug — answer all three.
## Language verdict (per tranche R1 §6 rubric — evidence beats instinct)
Choose exactly one, with the evidence line:
- **KEEP-PYTHON** — one sentence of justification.
- **RECODE-<lang>** — rubric scores (all 7 dimensions) + benchmark-gate plan: the compiled
replacement must beat the Python original on the MEASURED hot path, or the recode is
rejected and recorded. No win, no recode.
- **MERGE-into:<module>** — name the target and why.
- **DEPRECATE** — name the caller that no longer needs it.
Go is NOT pre-decided. Python is not pre-decided. The rubric decides.
## Constraint context
- henxels structural contract: this unit's module ≤ 1,200 lines (or on the debt table's
exception list — check before adding lines).
- Spine: any module-surface change requires `store/spine/current.json` regen with the code
change (`test_manifest_maps_every_module` is the arbiter for new top-level modules).
- Debt table: docs/structural-contract.md.
## Pipeline plan
1. Branch `issue-<N>` from current master (existing issue-N convention).
2. Scaffolding activation for any construction (construction_mode/decision per unit change).
3. Research rounds are MANDATORY for any RECODE verdict (research-stage-headless; ≥2
live-verified external sources per claim; per-source lessons; kill test per candidate).
4. Implementation behind the interop contract (R1 §5): compiled units are standalone
binaries — argv in, JSON on stdout, non-zero exit on failure, `--version` mandatory;
Python wraps in a thin adapter; store stays the shared truth.
5. Local gates: pytest + ruff + spine-store regen (if surface changed) + henxels.
6. PR → pipeline-gate + ci + required checks → merge-robot → auto-version.
## Verification
- The specific test proving this unit's change: (write at disposition).
- The keep-green set it must not break: full suite must stay green; named
keep-green tests for this unit's callers.
## Provenance
- Tranche: {TRANCHE_REF}
- Wave: {wave} ({WAVE_NAMES.get(wave, 'unassigned')})
- Inventory: docs/rebase/rebase-inventory.json (regenerate with scripts/rebase_inventory.py — never hand-edit)
- Factory: scripts/rebase_issue_factory.py
"""
return {
"title": issue_title(wave, relpath, unit),
"body": body,
"labels": ["rebase", f"wave-{wave}"],
"meta": {
"wave": wave,
"file": relpath,
"unit": unit,
"line_start": entry["line_start"],
"line_end": entry["line_end"],
"async": entry.get("async", False),
},
}
def fetch_existing_titles() -> set:
"""Fetch all issue titles in the repo (paginated) for dedup."""
token = load_gitea_token()
titles = set()
page = 1
while True:
url = (f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
f"?state=all&type=issues&limit=50&page={page}")
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
with urllib.request.urlopen(req, timeout=30) as resp:
batch = json.loads(resp.read())
if not batch:
break
titles.update(i["title"] for i in batch)
if len(batch) < 50:
break
page += 1
return titles
def create_issue(entry: dict) -> dict:
token = load_gitea_token()
url = f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
payload = json.dumps({
"title": entry["title"],
"body": entry["body"],
"labels": [], # label NAMES are set via the labels endpoint by auto-label; keep create minimal
}).encode()
req = urllib.request.Request(url, data=payload, method="POST",
headers={"Authorization": f"token {token}",
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
def cmd_plan(inv):
counts = {}
unassigned_modules = {}
for relpath, unit, entry in iter_units(inv):
wave = match_module(relpath)
counts[wave] = counts.get(wave, 0) + 1
if wave is None:
unassigned_modules.setdefault(relpath, inv["files"][relpath]["lines"])
report = {
"units_per_wave": {str(k if k is not None else "UNASSIGNED"): v
for k, v in sorted(counts.items(), key=lambda kv: str(kv[0]))},
"unassigned_modules": dict(sorted(unassigned_modules.items(), key=lambda kv: -kv[1])),
"unassigned_module_count": len(unassigned_modules),
}
print(json.dumps(report, indent=1))
COVERAGE_OUT.write_text(json.dumps(report, indent=1) + "\n")
print(f"\ncoverage written to {COVERAGE_OUT.relative_to(REPO_ROOT)}", file=sys.stderr)
return 0 if not unassigned_modules else 1
def cmd_sample(inv, wave, limit, out):
emitted = 0
records = []
for relpath, unit, entry in iter_units(inv):
if match_module(relpath) != wave:
continue
records.append(render_issue(wave, relpath, unit, entry))
emitted += 1
if emitted >= limit:
break
Path(out).write_text("\n".join(json.dumps(r) for r in records) + "\n")
print(f"sampled {emitted} issues for wave {wave} -> {out}", file=sys.stderr)
return 0
def cmd_create(inv, wave, limit, dry_gap):
existing = fetch_existing_titles()
created, skipped = 0, 0
for relpath, unit, entry in iter_units(inv):
if match_module(relpath) != wave:
continue
title = issue_title(wave, relpath, unit)
if title in existing:
skipped += 1
continue
issue = create_issue(render_issue(wave, relpath, unit, entry))
created += 1
print(f"created #{issue.get('number')}: {title}", file=sys.stderr)
if limit and created >= limit:
break
time.sleep(dry_gap)
print(f"\nwave {wave}: created={created} skipped(dedup)={skipped}", file=sys.stderr)
return 0
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("command", choices=["plan", "sample", "create"])
ap.add_argument("--wave", type=int, help="wave number (2-7)")
ap.add_argument("--limit", type=int, default=0, help="max issues for sample/create (0 = unlimited; sample defaults to 5)")
ap.add_argument("--out", default=None, help="sample output file")
ap.add_argument("--gap", type=float, default=0.5, help="seconds between creates")
args = ap.parse_args()
inv_cache.update(json.loads(INVENTORY.read_text()))
inv = inv_cache
if args.command == "plan":
return cmd_plan(inv)
if args.command == "sample":
if not args.wave:
ap.error("--wave required")
out = args.out or f"docs/rebase-factory-sample-w{args.wave}.json"
return cmd_sample(inv, args.wave, args.limit or 5, out)
if args.command == "create":
if not args.wave:
ap.error("--wave required")
return cmd_create(inv, args.wave, args.limit, args.gap)
return 2
if __name__ == "__main__":
sys.exit(main())
Loaded 100 of 997 files, more files were not shown because too many files have changed in this diff. Show more