Compare commits

..
Author SHA1 Message Date
conductor 9835261517 templates: plain-English heading carries the issue summary
CI Pipeline / lint (push) Successful in 42s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 15s
deploy / deploy (push) Skipped
CI Pipeline / unit-tests (push) Successful in 4m28s
CI Pipeline / required-checks (push) Successful in 5s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 8s
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:36:16 +01:00
242 changed files with 2461 additions and 357436 deletions

No files matched your search

-20
View File
@@ -1,20 +0,0 @@
---
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,9 +15,6 @@
# 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
@@ -25,7 +22,6 @@ omit =
tests/*
scripts/*
src/rune/canary_*.py
src/rune/ffi_query_memory_smoke.py
[report]
fail_under = 29
+5 -6
View File
@@ -6,12 +6,11 @@ labels: bug
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. -->
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
+5 -6
View File
@@ -6,12 +6,11 @@ labels: feature
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. A non-technical reader must be able to retell this
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
+5 -6
View File
@@ -6,12 +6,11 @@ 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. -->
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
backup:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+23 -84
View File
@@ -11,8 +11,8 @@ jobs:
# 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' && github.event.issue.state == 'open'
runs-on: rune-ci-desktop
if: github.event_name == 'issues' && github.event.action != 'labeled'
runs-on: rune-infra-pi
steps:
- name: Enforce priority label
uses: actions/github-script@v7
@@ -23,13 +23,12 @@ jobs:
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) {
if (has.length >= 1) {
// Prioritized — clear the flag if it was set.
if (labels.includes('unprioritized')) {
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
await github.rest.issues.removeLabel({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: issue.number,
id: issue.labels.find(l => l.name === 'unprioritized').id,
issue_number: issue.number, name: 'unprioritized',
}).catch(e => { if (e.status !== 404) throw e; });
}
return;
@@ -45,26 +44,12 @@ jobs:
});
auto-label:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- name: Analyze and label
uses: actions/github-script@v7
with:
script: |
// 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;
}
const action = context.payload.action;
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();
@@ -73,29 +58,25 @@ jobs:
const managedLabels = new Set([
'memory', 'governance', 'identity', 'growth',
'perception', 'infrastructure', 'life', 'custodian',
'bug', 'feature', 'research',
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
'priority: high', 'bug', 'feature', 'research',
]);
// 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.
// (kind: bug|feature|research) is authoritative. Keyword
// heuristics are a fallback for issues filed before the
// template system existed — they misclassify honest prose
// ("what is broken", "the fix") as bugs (T15 audit finding).
let kind = null;
const spineMatch = body && body.match(/^kind:\s*(bug|feature|research)\s*$/m);
if (spineMatch) {
kind = spineMatch[1];
}
let isBug = false, isFeature = false, isResearch = false;
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
if (kindMatch) {
const kind = kindMatch[1];
if (kind) {
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).
} else {
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'));
@@ -137,31 +118,9 @@ jobs:
desiredLabels.add('custodian');
}
// 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
// priority-gate job above).
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
// Priority labels
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
desiredLabels.add('priority: high');
}
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
@@ -171,7 +130,6 @@ 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)
);
@@ -181,14 +139,11 @@ jobs:
for (const name of staleManaged) {
try {
// 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}', {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
id: currentLabelIds.get(name),
name,
});
} catch (error) {
if (error.status !== 404) {
@@ -206,21 +161,5 @@ jobs:
labels: missingDesired,
});
}
// The priority gate can flag a duplicate before this job
// reconciles it. Clear the flag only AFTER verifying exactly one
// surviving P-label against Gitea's current state.
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 && 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(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
update-gitea:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- name: Check current Gitea version
id: current
+3 -8
View File
@@ -11,7 +11,7 @@ permissions: write-all
jobs:
release:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
with:
@@ -131,12 +131,8 @@ jobs:
"pyproject.toml": [(f'version = "{VERSION}"', f'version = "{NEW}"')],
}
# 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.
# Fetch current master content, apply replacements, VERIFY all five
# surfaces agree on NEW before committing anything (TUI gate).
edited = {}
for path, pairs in surfaces.items():
status, cur = api(f"/contents/{path}?ref=master")
@@ -155,7 +151,6 @@ 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})"),
-109
View File
@@ -67,20 +67,6 @@ jobs:
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
steps:
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -116,25 +102,6 @@ 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
- name: Test with coverage (global floor + changed-lines ratchet)
run: |
# #149 coverage ratchet — both floors live in .coveragerc; raise,
@@ -146,45 +113,12 @@ 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: go vet + go test + go build
run: |
cd src/go/rune-entry
go vet ./...
go test ./...
go build -o /tmp/rune-entry-ci .
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
@@ -196,34 +130,6 @@ 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
- name: Run canary suite
run: |
FAILED=0
@@ -250,21 +156,6 @@ 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
+5 -5
View File
@@ -8,7 +8,7 @@ on:
jobs:
lint-and-format:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
- name: Lint Python files
@@ -18,7 +18,7 @@ jobs:
ruff format shared/ src/ || true
canary-suite:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
- name: Run all canaries
@@ -37,7 +37,7 @@ jobs:
exit $FAILED
best-practices:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
- name: Check best practices
@@ -72,7 +72,7 @@ jobs:
grep -rn "TODO\|FIXME\|HACK\|XXX" shared/ src/ --include="*.py" | head -10 || echo " Clean"
documentation-check:
runs-on: rune-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
- name: Check documentation coverage
@@ -113,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-ci-desktop
runs-on: rune-infra-pi
steps:
- uses: actions/checkout@v4
with:
-1
View File
@@ -14,4 +14,3 @@ state/
# Runtime artifacts (cwd-local event journal + capability index)
index/
journal/
src/rust/*/target/
-23
View File
@@ -58,26 +58,3 @@ 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.99.7**
**Current release: v0.87.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.**
-124
View File
@@ -1,124 +0,0 @@
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
}
},
}
+40 -78
View File
@@ -1,92 +1,54 @@
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.",
// await_operation — T21.3 §21.3C: FIRST-CLASS async wait tool.
// Uses the plugin tool() API with args (not parameters).
export const AwaitOperation = tool({
description: [
"Wait for an asynchronous external operation (CI, deploy, release) to complete.",
"Registers on the event bridge, yields until the completion event arrives, then returns the verified result.",
"USE THIS instead of `sleep N; curl status` — it is the canonical event-driven wait.",
].join(" "),
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."),
sha: tool.schema.string().describe(
"The exact commit SHA the operation is running against"),
deadline_secs: tool.schema.number().optional().describe(
"Maximum wait in seconds (default 900)"),
},
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}`
async execute(args, ctx) {
const repoRoot = "/mnt/workdrive/issue161-harness"
const deadline = args.deadline_secs || 900
const cmd = [
"python3", `${repoRoot}/scripts/ci_event_wait.py`,
"--sha", args.sha,
"--deadline", String(deadline),
].join(" ")
try {
const result = execSync(cmd, {
cwd: "/mnt/workdrive/issue161-harness",
timeout: (norm.deadline_secs + 60) * 1000,
cwd: repoRoot,
timeout: (deadline + 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",
env: {
...process.env,
DEPLOY_SHA: args.sha,
RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge",
RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume",
},
})
const lines = result.trim().split("\n")
const jsonLine = lines.find((l: string) => l.startsWith("{"))
if (jsonLine) return jsonLine
return "await_operation: no JSON output"
} 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) })
const stdout = e.stdout || ""
const lines = stdout.trim().split("\n")
const jsonLine = lines.find((l: string) => l.startsWith("{"))
if (jsonLine) return jsonLine
return JSON.stringify({
ok: false,
error: `await_operation failed: ${(e.stderr || e.message || "?").slice(0, 200)}`,
})
}
},
})
-167
View File
@@ -1,167 +0,0 @@
// 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)"
},
})
+69 -116
View File
@@ -4,37 +4,19 @@
"generated_at": "2026-09-16T18:23:38Z",
"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"
],
"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",
@@ -42,30 +24,21 @@
"tool_executor",
"tool_protocol"
],
"rust_crates": [
"capability_scoring"
"depends_on": [
"memory"
],
"mirrors": [],
"canaries": [
"canary_native",
"canary_p0_05",
"canary_p1_01"
]
},
"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",
@@ -73,24 +46,17 @@
"drift_ledger",
"drift_observability",
"failure_auto_filer",
"programme_gates",
"sc_loader"
"programme_gates"
],
"rust_crates": [
"evidence_provenance",
"scaffold_core"
"depends_on": [],
"mirrors": [],
"canaries": [
"canary_full_system",
"canary_schema_migration"
]
},
"growth": {
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance",
"memory"
],
"description": "Learning and improvement",
"mirrors": [],
"modules": [
"autonomy_loop",
"cognitive_loop",
@@ -101,17 +67,18 @@
"growth_simulator",
"self_improve",
"subconscious_pass"
],
"depends_on": [
"governance",
"memory"
],
"mirrors": [],
"canaries": [
"canary_full_system"
]
},
"identity": {
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
],
"description": "Who the agent is \u2014 persona, voice, anchors",
"mirrors": [],
"modules": [
"behavior",
"identity_anchors",
@@ -122,19 +89,17 @@
"voice_injector",
"voice_sync",
"voicebox"
],
"depends_on": [
"governance"
],
"mirrors": [],
"canaries": [
"canary_full_system"
]
},
"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",
@@ -145,39 +110,31 @@
"provider_registry",
"response_adapter",
"standardized_providers",
"tc_loader",
"token_optimizer"
],
"rust_crates": [
"token_counting"
"depends_on": [
"capability"
],
"mirrors": [],
"canaries": [
"canary_p1_02",
"canary_p1_06",
"canary_resource_authority"
]
},
"memory": {
"canaries": [
"canary_full_system",
"canary_memory_synthesis",
"canary_query_memory"
],
"depends_on": [
"governance"
],
"description": "Storing, retrieving, and managing knowledge",
"mirrors": [],
"modules": [
"abstraction",
"cache",
"drift_cache",
"drift_compression",
"ffi_query_memory_smoke",
"graph_memory",
"graph_shard_bridge",
"memory_consolidation",
"memory_env",
"memory_selector",
"memory_store",
"mems_loader",
"mr_loader",
"ms_loader",
"query_memory",
"recall_router",
"retrieval_benchmark",
@@ -185,33 +142,18 @@
"tier_manager",
"warm_neuron_store"
],
"rust_crates": [
"memory_retrieval",
"memory_scoring",
"memory_storage",
"query_memory"
"depends_on": [
"governance"
],
"mirrors": [],
"canaries": [
"canary_full_system",
"canary_memory_synthesis",
"canary_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",
"mirrors": [],
"modules": [
"auto_detect",
"build_mode",
@@ -246,24 +188,32 @@
"task_persist",
"tiered_executor",
"time_awareness",
"tui_transport",
"tui_validator",
"umbrella_update",
"verification_layer",
"warm_router",
"work_manager"
],
"depends_on": [
"capability",
"growth",
"identity",
"inference",
"perception"
],
"mirrors": [],
"canaries": [
"canary_cognitive_strata",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_orchestration_foundation",
"canary_p1_07",
"canary_surface_parity",
"canary_tui_drift"
]
},
"perception": {
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
],
"description": "Input processing and context assembly",
"mirrors": [],
"modules": [
"attention_capsule",
"auto_context",
@@ -298,8 +248,6 @@
"salience_gate",
"scaffold",
"spine",
"spine_language",
"spine_loader",
"stage_guards",
"structural_migration",
"structural_model",
@@ -311,8 +259,13 @@
"wait_guard_corpus",
"witness"
],
"rust_crates": [
"rune_spine"
"depends_on": [
"memory"
],
"mirrors": [],
"canaries": [
"canary_context_broker",
"canary_full_system"
]
}
}
+1 -16
View File
@@ -272,19 +272,4 @@ observations, fresh resume.
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.
history: skipped (weak evidence — honest limitation).
+39 -39
View File
@@ -235,8 +235,8 @@
{
"number": 183,
"title": "Future: typed temporal semantics for facts, relationships, beliefs, and preferences",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -248,14 +248,14 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 182,
"title": "Future: GitHub code-search prior-art audit for graph and vector memory",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -264,8 +264,8 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 181,
@@ -285,13 +285,13 @@
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": "epic holds absorbed children #182 #183 + research watchlist #164 #165 #166; closes when prerequisites land or ideas dropped"
"next_action": ""
},
{
"number": 179,
"title": "Future: succession/privacy semantics and lineage evaluation",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -303,14 +303,14 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 178,
"title": "Future: evidence-grounded ancestral perspective with explicit uncertainty",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -320,14 +320,14 @@
"perception"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 177,
"title": "Future: temporal autobiographical model with provenance and reinterpretation lineage",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"governance",
@@ -338,14 +338,14 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 176,
"title": "Future: lineage-aware situated curiosity for meaningful autobiographical gaps",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"feature",
@@ -356,8 +356,8 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 175,
@@ -377,13 +377,13 @@
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": "epic holds absorbed children #176-#179 as checklist items; closes when prerequisites land or ideas dropped"
"next_action": ""
},
{
"number": 166,
"title": "RESEARCH WATCHLIST: Memory-R1 (RL memory ops), Titans (test-time memory), Vespa/Weaviate/Milvus (retrieval competitors)",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"growth",
@@ -392,14 +392,14 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 165,
"title": "RESEARCH: SimpleMem semantic compression + A-MEM dynamic linking + Letta sleep-time compute",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"infrastructure",
@@ -408,14 +408,14 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 164,
"title": "RESEARCH: HippoRAG 2 \u2014 PPR-style associative propagation as warm-neuron activation candidate",
"tracker_state": "closed",
"reconciliation_state": "HISTORICAL_EVIDENCE",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"P3 \u2014 low",
"growth",
@@ -424,8 +424,8 @@
"ready"
],
"live_defect": false,
"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"
"last_revalidated": "2026-09-11",
"next_action": ""
},
{
"number": 163,
@@ -1150,5 +1150,5 @@
}
],
"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"
"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"
}
-382
View File
@@ -1,382 +0,0 @@
# 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
@@ -1,50 +0,0 @@
# 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
@@ -1,12 +0,0 @@
{
"units_per_wave": {
"2": 239,
"3": 251,
"4": 129,
"5": 544,
"6": 415,
"7": 274
},
"unassigned_modules": {},
"unassigned_module_count": 0
}
-1
View File
@@ -10,7 +10,6 @@ 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 @@
1.27.1
+2 -26
View File
@@ -1,31 +1,7 @@
settings:
warn_about_similar_files:
above: 0.85
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/**"]
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
henxels:
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
@@ -44,7 +20,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", "./docs/rebase/**"]
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json"]
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
in: ./src/rune/*
-7
View File
@@ -1,7 +0,0 @@
# 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
@@ -1,15 +0,0 @@
[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
@@ -1,117 +0,0 @@
//! 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}")),
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"BLE001": 345,
"BLE001": 347,
"S110": 75
}
+1 -401
View File
@@ -1,401 +1 @@
[
{
"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"
}
]
[]
+15 -38886
View File
File diff suppressed because it is too large. Load diff
@@ -1,48 +0,0 @@
# 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%
@@ -1,66 +0,0 @@
# 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.
@@ -1,62 +0,0 @@
# 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.
@@ -1,97 +0,0 @@
# 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
@@ -1,285 +0,0 @@
# 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.
-196
View File
@@ -1,196 +0,0 @@
# 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 |
+1 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.99.7"
version = "0.87.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
@@ -16,7 +16,6 @@ dependencies = [
"aiohttp>=3.9.0",
"websockets>=12.0",
"psutil>=5.9.0",
"pillow>=10.0",
"pyyaml>=6.0",
]
+1 -5
View File
@@ -29,10 +29,6 @@
"**/__pycache__"
],
"extraPaths": [
"scripts",
"src/rune",
"shared",
"src/rust/evidence_provenance",
"src/rust/query_memory"
"scripts"
]
}
+9 -11
View File
@@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
# uv pip compile pyproject.toml -o /tmp/req_new.txt --extra dev --extra memory --python-version 3.11
aiohappyeyeballs==2.7.1
# via aiohttp
aiohttp==3.14.4
aiohttp==3.14.3
# 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.2
coverage==7.16.1
# via rune (pyproject.toml)
diff-cover==10.6.0
diff-cover==10.5.1
# via rune (pyproject.toml)
frozenlist==1.8.0
# via
@@ -50,23 +50,21 @@ markdown-it-py==4.2.0
# mdit-py-plugins
# rich
# textual
markupsafe==3.0.4
markupsafe==3.0.3
# via jinja2
mdit-py-plugins==0.6.1
# via textual
mdurl==0.1.2
# via markdown-it-py
multidict==6.9.1
multidict==6.9.0
# via
# aiohttp
# yarl
nodeenv==1.11.0
nodeenv==1.10.0
# via pyright
packaging==26.3
# via pytest
pillow==12.3.0
# via rune (pyproject.toml)
platformdirs==4.12.3
platformdirs==4.11.11
# via textual
pluggy==1.6.0
# via
@@ -118,7 +116,7 @@ typing-extensions==4.16.0
# pyright
# textual
# textual-autocomplete
websockets==17.2
websockets==17.1
# via rune (pyproject.toml)
yarl==1.25.1
# via aiohttp
+3 -17
View File
@@ -148,18 +148,9 @@ def sha_branch(sha: str) -> str:
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
out = _git("branch", "--contains", sha)
return any(l.strip().endswith("master") or l.strip() == "master"
for l in out.splitlines())
def open_pr_for_sha(sha: str) -> int | None:
@@ -193,11 +184,6 @@ def derive_op_type(sha: str) -> tuple[str, str]:
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
+26 -54
View File
@@ -1,71 +1,43 @@
#!/usr/bin/env python3
"""install_mimo_hook.py — T21.2 §21.2D (T22: multi-hook): canonical MiMo
hook installer.
"""install_mimo_hook.py — T21.2 §21.2D: 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
"""
Canonical source: config/mimo-hooks/event-wait-guard.ts
Target: ~/.mimocode/hooks/event-wait-guard.ts
Verifies hash after install. Detects drift in both directions."""
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"]
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.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 not CANONICAL.is_file():
print(f"canonical source missing: {CANONICAL}")
return 1
canon_hash = sha256(CANONICAL)
if TARGET.is_file():
target_hash = sha256(TARGET)
if target_hash == canon_hash:
print(f"in-sync: {target_hash[:12]}")
return 0
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
else:
print(f"hook missing at {TARGET}")
TARGET.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(CANONICAL, TARGET)
installed_hash = sha256(TARGET)
if installed_hash != canon_hash:
print(f"INSTALL FAILED: hash mismatch after copy")
return 1
print(f"installed: {installed_hash[:12]}")
return 0
if __name__ == "__main__":
-52
View File
@@ -1,52 +0,0 @@
#!/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())
-23
View File
@@ -42,29 +42,6 @@ def main() -> int:
"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.
-83
View File
@@ -1,83 +0,0 @@
#!/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
@@ -1,214 +0,0 @@
#!/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
@@ -1,356 +0,0 @@
#!/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())
+4 -3
View File
@@ -50,7 +50,7 @@ SRC_A = {
"memory_store": "memory", "memory_selector": "memory",
"memory_consolidation": "memory", "memory_env": "memory",
"shard_manager": "memory", "tier_manager": "memory",
"graph_memory": "memory",
"graph_memory": "memory", "query_memory": "memory",
"curator": "memory", "sleep_time": "memory",
"subconscious_pass": "memory", "warm_neuron_store": "memory",
"warm_router": "memory", "abstraction": "memory",
@@ -166,12 +166,14 @@ SCRIPTS_C = {"fake_worker", "tui_startup_canary"}
# shared/ — harness-facing layer; mirrors of canonical src modules = D
SHARED_D = {
"audit_ledger": "src/rune/audit_ledger.py",
"dependency_tracker": "src/rune/dependency_tracker.py",
"drift_cache": "src/rune/drift_cache.py",
"drift_compression": "src/rune/drift_compression.py",
"drift_core": "src/rune/drift_core.py",
"drift_observability": "src/rune/drift_observability.py",
"gitea_auth": "scripts/gitea_auth.py",
"query_memory": "src/rune/query_memory.py",
"shard_manager": "src/rune/shard_manager.py",
"subconscious_pass": "src/rune/subconscious_pass.py",
"tier_manager": "src/rune/tier_manager.py",
@@ -180,8 +182,7 @@ SHARED_B_OWNER = "ops-harness"
SHARED_B = {"auto_detect", "autonomy_loop", "build_orchestrator",
"build_pipeline_policy", "cognitive_loop", "custodian",
"dream_cycle", "gitea_pipeline", "gitea_provision",
"graph_shard_bridge", "growth_simulator", "query_memory",
"umbrella_update"}
"graph_shard_bridge", "growth_simulator", "umbrella_update"}
def main() -> int:
+1 -13
View File
@@ -47,19 +47,7 @@ def main() -> int:
p for p in env.get("PATH", "").split(os.pathsep)
if "ci-venv" not in p and ".type-venv" not in p and "venv/bin" not in p)
result = subprocess.run(
# §16V AMBIENT-INTERPRETER CLOSURE (T7.3 slice-2 CI repair, 2026-10-03):
# venv-signal stripping fixed ACTIVATION drift, but dependency
# resolution still followed the AMBIENT host interpreter — runner
# (py311, no rich/Pillow) vs desktop (3.14 system site with both)
# produced 460 vs 454 on the identical tree, and any import-level
# ignore flips to reportUnnecessaryTypeIgnoreComment in the other
# environment. Pin --pythonpath to the INVOKING interpreter (the
# venv running this script) so dependency resolution follows the
# installed lock, not the host: CI's .type-venv has the locked deps;
# local preflight must run the ratchet from a venv built from
# requirements-ci.txt (same lock) — that is now the contract.
cmd + [str(ROOT / "src" / "rune"), "--outputjson",
"--pythonpath", sys.executable],
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
capture_output=True,
text=True,
timeout=600,
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""audit_ledger.py — HMAC-chained tamper-evident promotion ledger.
Every promotion decision is recorded in a hash-chained ledger.
Tampering with any entry breaks the chain.
Research basis:
- PoEM (arXiv:2608.16032): HMAC-chained tamper-evident execution ledger
- Autogenesis (arXiv:2604.15034): versioned interfaces, auditable lineage
Run under ~/.venvs/memory/bin/python3.
"""
import hashlib
import hmac
import json
import os
import sys
from datetime import datetime, timezone
from typing import Dict, List, Optional
LEDGER_FILE = os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl")
SECRET_KEY = os.environ.get("AUDIT_LEDGER_KEY", "rune-drift-integrity-2024")
def _compute_hash(prev_hash: str, entry: dict) -> str:
"""Compute HMAC chain hash for a ledger entry."""
payload = json.dumps(entry, sort_keys=True).encode()
msg = (prev_hash + payload.decode()).encode()
return hmac.new(SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest()
def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
"""Append an entry to the promotion ledger.
Returns the ledger entry with chain hash.
"""
# Read last hash
prev_hash = "genesis"
if os.path.exists(LEDGER_FILE):
with open(LEDGER_FILE, "r") as f:
lines = f.readlines()
if lines:
try:
last = json.loads(lines[-1])
prev_hash = last.get("hash", "genesis")
except json.JSONDecodeError:
prev_hash = "genesis"
entry = {
"type": entry_type,
"entry_id": entry_id,
"details": details,
"timestamp": datetime.now(timezone.utc).isoformat(),
"prev_hash": prev_hash,
}
# Compute chain hash
entry["hash"] = _compute_hash(prev_hash, entry)
# Append to ledger
os.makedirs(os.path.dirname(LEDGER_FILE), exist_ok=True)
with open(LEDGER_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry
def verify_chain() -> dict:
"""Verify the integrity of the entire ledger chain.
Returns dict with verification result.
"""
if not os.path.exists(LEDGER_FILE):
return {"valid": True, "entries": 0, "reason": "empty ledger"}
with open(LEDGER_FILE, "r") as f:
lines = f.readlines()
prev_hash = "genesis"
for i, line in enumerate(lines):
try:
entry = json.loads(line)
except json.JSONDecodeError:
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i+1}"}
# Verify prev_hash chain
if entry.get("prev_hash") != prev_hash:
return {
"valid": False,
"entries": i,
"reason": f"chain broken at line {i+1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}"
}
# Verify HMAC
stored_hash = entry.get("hash")
computed = _compute_hash(prev_hash, {k: v for k, v in entry.items() if k != "hash"})
if stored_hash != computed:
return {
"valid": False,
"entries": i,
"reason": f"HMAC mismatch at line {i+1}: stored={stored_hash[:16]}..., computed={computed[:16]}..."
}
prev_hash = stored_hash
return {"valid": True, "entries": len(lines), "reason": "chain intact"}
def get_entries(limit: int = 50) -> List[dict]:
"""Get recent ledger entries."""
if not os.path.exists(LEDGER_FILE):
return []
with open(LEDGER_FILE, "r") as f:
lines = f.readlines()
entries = []
for line in lines[-limit:]:
try:
entries.append(json.loads(line))
except json.JSONDecodeError:
continue
return entries
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
"""Log a promotion decision."""
return append_entry("promotion", entry_id, {
"old_status": old_status,
"new_status": new_status,
"wash_count": wash_count,
"reason": reason,
})
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
"""Log a pre-commit gate result."""
return append_entry("gate", entry_id, {
"verdict": verdict,
"critics": critics,
})
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
"""Log a drift alert."""
return append_entry("alert", 0, {
"alert_type": alert_type,
"message": message,
"severity": severity,
})
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Promotion audit ledger")
sub = parser.add_subparsers(dest="cmd")
# verify
sub.add_parser("verify", help="Verify ledger chain integrity")
# list [--limit N]
p_list = sub.add_parser("list", help="Show recent ledger entries")
p_list.add_argument("--limit", type=int, default=20)
# log-promotion <entry_id> <old> <new> <wash_count> <reason>
p_log = sub.add_parser("log-promotion", help="Log a promotion")
p_log.add_argument("entry_id", type=int)
p_log.add_argument("old_status")
p_log.add_argument("new_status")
p_log.add_argument("wash_count", type=int)
p_log.add_argument("reason")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(1)
if args.cmd == "verify":
result = verify_chain()
print(f"Chain valid: {result['valid']}")
print(f"Entries: {result['entries']}")
print(f"Reason: {result['reason']}")
elif args.cmd == "list":
entries = get_entries(args.limit)
if entries:
print(f"Recent ledger entries ({len(entries)}):")
for e in entries:
print(f" [{e.get('type')}] entry_id={e.get('entry_id')} @ {e.get('timestamp', '')[:19]}")
details = e.get('details', {})
if e.get('type') == 'promotion':
print(f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})")
elif e.get('type') == 'gate':
print(f" verdict={details.get('verdict')}")
else:
print("No ledger entries.")
elif args.cmd == "log-promotion":
entry = log_promotion(args.entry_id, args.old_status, args.new_status, args.wash_count, args.reason)
print(f"Logged: {entry['hash'][:16]}...")
if __name__ == "__main__":
main()
+1 -10
View File
@@ -33,7 +33,7 @@ SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
SHARED_DIR = os.path.expanduser("~/.config/opencode/shared")
CUSTODIAN_STATE_FILE = os.path.expanduser("~/.config/opencode/memory/custodian_state.json")
GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000") # gitea.internal (PC); external = https://gitea.thingscouldgetdicey.online via Pi Caddy
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
GITEA_TOKEN = load_gitea_token()
REPO = "conductor/rune"
TUI_DIR = "/mnt/workdrive/rune"
@@ -160,15 +160,6 @@ class Custodian:
The caller's body replaces the SUBMITTED_BODY marker inside the
template's Plain English section — plain terms first, always.
ORGANISATION STANDARD (one open note per idea, 2026-09-19): before
minting a standalone issue, check whether the work belongs inside an
existing parent. Ideas parked under a blocked/future epic are filed
as checklist items in the parent's "Absorbed children" section, NOT
as standalone issues — the board stays uncluttered and every idea
has exactly one home. Mint a standalone issue only when the work is
live, dependency-tracked, or about to start. Absorbed ideas are
reversible: re-mint standalone when they become live.
"""
# 1. assemble body from template (graceful: unformatted if template missing)
template = self._load_issue_template(kind)
+5 -6
View File
@@ -6,12 +6,11 @@ labels: bug
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. -->
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
+5 -6
View File
@@ -6,12 +6,11 @@ labels: feature
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. A non-technical reader must be able to retell this
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
+5 -6
View File
@@ -6,12 +6,11 @@ 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. -->
## In plain English: <restate the issue title here in everyday words>
<!-- The heading ITSELF must carry the summary — 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 -->
-216
View File
@@ -1,216 +0,0 @@
"""Thin adapter for native (compiled) units — R1 §5 interop contract, Wave 0.
The contract under proof: compiled units are standalone binaries exposing a
CLI boundary — argv in, JSON on stdout, non-zero exit on failure, a mandatory
``--version`` flag. This module is the Python side: run a unit, enforce a
timeout, capture stderr, and return a TYPED result. It never raises for unit
failures (those are data in the result); it raises only for adapter misuse
(missing binary, bad timeout).
No IPC daemon, no PyO3 — the file-based store stays the shared truth. If
research later surfaces a materially better pattern it competes through the
research pipeline; the CLI boundary is the default, not a cage.
"""
from __future__ import annotations
import json
import subprocess
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
PROOF_UNIT = REPO_ROOT / "interop" / "rebase-proof" / "target" / "release" / "rebase-proof"
@dataclass
class NativeResult:
"""Typed result of one native-unit invocation.
ok — the invocation succeeded (exit 0 AND not timed out); when
the unit emitted JSON with an ``ok`` field, that field must
not be False (plain-text output like ``--version`` has none)
exit_code — raw process exit code
data — parsed JSON object from stdout, or None if unparseable
stderr — captured stderr text (may be non-empty even on success)
duration_ms — wall-clock cost of the invocation
timed_out — True when the unit exceeded the timeout and was killed
events — in streaming mode: every parsed JSON object line, in
arrival order (empty for non-streaming :func:`run_native`)
"""
ok: bool
exit_code: int
data: dict | None
stderr: str
duration_ms: int
timed_out: bool = False
raw_stdout: str = field(default="", repr=False)
events: list[dict] = field(default_factory=list)
def run_native(binary: Path, args: list[str], timeout_s: float = 10.0) -> NativeResult:
"""Invoke a native unit and return a typed result.
Raises FileNotFoundError only when *binary* itself is absent (adapter
misuse) — every unit-level failure is reported via the result, never an
exception, so callers can branch on ``result.ok`` without try/except.
"""
if not binary.is_file():
raise FileNotFoundError(f"native unit not built: {binary}")
started = time.monotonic()
timed_out = False
try:
proc = subprocess.run(
[str(binary), *args],
capture_output=True,
text=True,
timeout=timeout_s,
check=False, # exit codes are data, never exceptions
)
exit_code, stdout, stderr = proc.returncode, proc.stdout, proc.stderr
except subprocess.TimeoutExpired as exc:
timed_out = True
exit_code = -1
stdout = exc.stdout or "" if isinstance(exc.stdout, str) else (exc.stdout or b"").decode(errors="replace")
stderr = exc.stderr or "" if isinstance(exc.stderr, str) else (exc.stderr or b"").decode(errors="replace")
duration_ms = int((time.monotonic() - started) * 1000)
data: dict | None = None
stripped = stdout.strip()
if stripped:
try:
parsed = json.loads(stripped.splitlines()[-1])
if isinstance(parsed, dict):
data = parsed
except json.JSONDecodeError:
data = None
ok = (not timed_out) and exit_code == 0 and (data is None or data.get("ok") is not False)
return NativeResult(
ok=ok,
exit_code=exit_code,
data=data,
stderr=stderr,
duration_ms=duration_ms,
timed_out=timed_out,
raw_stdout=stdout,
)
def run_proof(args: list[str], timeout_s: float = 10.0) -> NativeResult:
"""Run the Wave 0 proof unit with the standard contract defaults."""
return run_native(PROOF_UNIT, args, timeout_s=timeout_s)
def stream_native(
binary: Path,
args: list[str],
timeout_s: float = 10.0,
on_event=None,
):
"""Run a native unit in JSONL streaming mode, yielding events as they arrive.
The JSONL streaming contract (rubric §W7): one JSON object per stdout
line — ``{"ok": true, "event": ...}`` progress objects along the way, and
a terminal envelope as the FINAL line. This generator yields each parsed
event dict the moment it arrives (so a long op surfaces partial progress
instead of blocking until exit), then yields exactly one terminal
:class:`NativeResult`. *on_event*, when given, is called with each parsed
event as it arrives (the TUI wraps this in ``call_from_thread``).
Stream-mode ok-semantics are stricter than :func:`run_native`: ``ok``
requires exit 0, no timeout, AND a valid terminal envelope (the final
stdout line parsed as a JSON object whose ``ok`` is not False). Malformed
interior lines are skipped and counted in ``malformed_lines``; malformed
FINAL line means no valid terminal envelope, so ``ok`` is False — partial
progress stays available in ``events``. That is the point of streaming.
Same failure discipline as :func:`run_native`: raises FileNotFoundError
when *binary* is absent; every unit-level failure (bad envelope, timeout,
non-zero exit) is data on the terminal result, never an exception.
"""
if on_event is not None and not callable(on_event):
raise TypeError("on_event must be callable")
if not binary.is_file():
raise FileNotFoundError(f"native unit not built: {binary}")
started = time.monotonic()
proc = subprocess.Popen(
[str(binary), *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# stderr must drain concurrently — reading it after the stdout loop
# deadlocks when a unit floods stderr past the pipe buffer mid-stream.
stderr_parts: list[str] = []
stderr_drain = threading.Thread(
target=lambda: stderr_parts.append(proc.stderr.read()), daemon=True
)
stderr_drain.start()
killed = threading.Event()
# The watchdog's kill and the flag are one step so an adapter-side
# timeout is never mistaken for a unit dying by its own signal.
watchdog = threading.Timer(timeout_s, lambda: (killed.set(), proc.kill()))
watchdog.start()
events: list[dict] = []
malformed_lines = 0
# True only while the most recent non-empty stdout line parsed as a JSON
# object — a malformed final line invalidates the terminal envelope.
final_line_is_json = False
try:
for line in proc.stdout:
line = line.strip()
if not line:
continue
try:
parsed = json.loads(line)
except json.JSONDecodeError:
malformed_lines += 1
final_line_is_json = False
continue
if not isinstance(parsed, dict):
malformed_lines += 1
final_line_is_json = False
continue
events.append(parsed)
final_line_is_json = True
if on_event is not None:
on_event(parsed)
yield parsed
proc.wait()
finally:
watchdog.cancel()
stderr_drain.join(timeout=5.0)
timed_out = killed.is_set()
duration_ms = int((time.monotonic() - started) * 1000)
exit_code = proc.returncode if proc.returncode is not None else -1
if timed_out:
exit_code = -1 # adapter-side kill, never a unit's own exit
terminal = events[-1] if (events and final_line_is_json) else None
ok = (
not timed_out
and exit_code == 0
and terminal is not None
and terminal.get("ok") is not False
)
yield NativeResult(
ok=ok,
exit_code=exit_code,
data=terminal,
stderr=stderr_parts[0] if stderr_parts else "",
duration_ms=duration_ms,
timed_out=timed_out,
events=events,
)
+134 -60
View File
@@ -1,25 +1,17 @@
#!/usr/bin/env python3
"""query_memory.py — Query memory system for adaptive retrieval.
AUTHORITY-SWITCHED (Phase G strangler slice 1, doctrine step 8): the
classification core now lives in Rust (`src/rust/query_memory/src/lib.rs`,
cdylib `libquery_memory.so`). This module is a thin ctypes-backed shim over
`qm_loader.py` serving the FULL historical Python class API — the deployed
in-process consumers (`memory_store.py`, `growth_simulator.py`) call
`QueryMemory()` exactly as before and get the Rust core.
The system remembers what kinds of queries it gets, what retrieval
strategy worked, and adapts its gate logic accordingly.
Parity evidence: FFI-gap parity EXACT-EQUAL on `get_topic_stats` /
`get_dead_weight_topics`; verification wall green (FFI smoke 41/41, parity
harness ALL GREEN, Python canary 12/12, Rust canary 27/27); shadow comparison
46/46 exact-equal vs the live production store.
This prevents dead weight — memories get tested based on usage
patterns, not just random exploration.
The Python implementation this replaces is preserved in git history on branch
`phase-g-strangler-retrieval-index` (pre-shim commit); per deletion discipline
it is not kept as a silent fallback — a missing cdylib fails loudly.
Store resolution is unchanged: `$HOME/.config/opencode/memory/query_memory`
(the Rust core resolves `$HOME` natively; history.jsonl / topic_stats.json
formats are byte-parity-proven).
Research basis:
- IGMiRAG (arXiv:2602.07525): Intuition-guided retrieval
- Task-Adaptive Retrieval (arXiv:2604.07863): Learning from interaction history
- MemCon (arXiv:2607.13591): Learned policy for consolidation/forgetting
- Agent-Native Memory (arXiv:2606.24775): Workload-adaptive routing
Run under ~/.venvs/memory/bin/python3.
"""
@@ -27,14 +19,15 @@ Run under ~/.venvs/memory/bin/python3.
import json
import os
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from typing import Dict, List, Optional, Tuple
QUERY_MEMORY_DIR = os.path.expanduser("~/.config/opencode/memory/query_memory")
QUERY_HISTORY_FILE = os.path.join(QUERY_MEMORY_DIR, "history.jsonl")
TOPIC_STATS_FILE = os.path.join(QUERY_MEMORY_DIR, "topic_stats.json")
# Topic keywords (classification table — tie-break law: insertion order is
# part of the contract; the Rust core carries the identical table).
# Topic keywords for classification
TOPIC_KEYWORDS = {
"guitar": ["guitar", "music", "teaching", "course", "lesson", "chord", "scale"],
"coding": ["code", "python", "script", "function", "debug", "build", "deploy"],
@@ -48,47 +41,17 @@ TOPIC_KEYWORDS = {
"creative": ["write", "design", "create", "build", "make", "art", "music"],
}
# Where the Rust core (qm_loader.py + target/release/libquery_memory.so) lives.
_QM_CORE_DIR = os.environ.get(
"QM_CORE_DIR", "/mnt/workdrive/rune/src/rust/query_memory"
)
if _QM_CORE_DIR not in sys.path:
sys.path.insert(0, _QM_CORE_DIR)
try:
from qm_loader import QueryMemory as _RustQueryMemory
except ImportError as _e: # pragma: no cover - deployment integrity failure
raise ImportError(
f"query_memory shim: cannot load the Rust core from {_QM_CORE_DIR!r} "
f"({_e}). The cdylib must be built: `cargo build --release` in "
f"{_QM_CORE_DIR}. No Python fallback exists by design."
) from _e
class QueryMemory(_RustQueryMemory):
"""Query memory system for adaptive retrieval (Rust core, Python API).
API (identical to the historical Python implementation):
* classify_query(query) -> str
* adaptive_exploration_rate(query) -> float
* record_query(query, strategy, success) -> None
* get_topic_stats() -> dict (topic -> stats, insertion order)
* get_dead_weight_topics() -> list[str]
* get_health_report() -> dict
"""
class QueryMemory:
"""Query memory system for adaptive retrieval."""
def __init__(self):
# Match the historical constructor: ensure the store dir exists,
# resolve the store from $HOME (qm_new's native behaviour).
os.makedirs(QUERY_MEMORY_DIR, exist_ok=True)
super().__init__()
# Historical in-memory attribute: query history loaded at construction,
# appended by record_query. (canary check 2 reads len(qm.history).)
self.history = self._load_history()
self.history: List[dict] = self._load_history()
self.topic_stats: Dict[str, dict] = self._load_topic_stats()
def _load_history(self):
"""Load query history from disk (historical shape: list of dicts)."""
def _load_history(self) -> List[dict]:
"""Load query history from disk."""
if not os.path.exists(QUERY_HISTORY_FILE):
return []
entries = []
@@ -100,16 +63,127 @@ class QueryMemory(_RustQueryMemory):
continue
return entries
def _save_history(self, entry: dict):
"""Append a query entry to history."""
with open(QUERY_HISTORY_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
def _load_topic_stats(self) -> Dict[str, dict]:
"""Load topic statistics from disk."""
if os.path.exists(TOPIC_STATS_FILE):
try:
with open(TOPIC_STATS_FILE) as f:
return json.load(f)
except json.JSONDecodeError:
return {}
return {}
def _save_topic_stats(self):
"""Save topic statistics to disk."""
with open(TOPIC_STATS_FILE, "w") as f:
json.dump(self.topic_stats, f, indent=2)
def classify_query(self, query: str) -> str:
"""Classify a query into a topic.
Returns the topic with the most keyword matches.
Falls back to "general" if no strong match.
"""
query_lower = query.lower()
scores = {}
for topic, keywords in TOPIC_KEYWORDS.items():
score = sum(1 for kw in keywords if kw in query_lower)
if score > 0:
scores[topic] = score
if not scores:
return "general"
return max(scores, key=scores.get)
def record_query(self, query: str, strategy: str, success: bool):
"""Record through the Rust core, then mirror the historical in-memory append."""
super().record_query(query, strategy, success)
self.history.append({
"""Record a query and its outcome.
Args:
query: The query text
strategy: "consolidated" or "full_retrieval"
success: Whether the retrieval produced useful results
"""
topic = self.classify_query(query)
entry = {
"query": query[:100],
"topic": self.classify_query(query),
"topic": topic,
"strategy": strategy,
"success": success,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
}
self.history.append(entry)
self._save_history(entry)
# Update topic stats
if topic not in self.topic_stats:
self.topic_stats[topic] = {
"total_queries": 0,
"successful_queries": 0,
"consolidated_hits": 0,
"full_retrieval_hits": 0,
"exploration_rate": 0.30, # default
}
stats = self.topic_stats[topic]
stats["total_queries"] += 1
if success:
stats["successful_queries"] += 1
if strategy == "consolidated":
stats["consolidated_hits"] += 1
else:
stats["full_retrieval_hits"] += 1
# Adaptive exploration rate: decrease for well-tested topics
if stats["total_queries"] >= 10:
success_rate = stats["successful_queries"] / stats["total_queries"]
if success_rate > 0.8:
stats["exploration_rate"] = max(0.10, stats["exploration_rate"] - 0.05)
elif success_rate < 0.5:
stats["exploration_rate"] = min(0.50, stats["exploration_rate"] + 0.05)
self._save_topic_stats()
def get_topic_stats(self) -> Dict[str, dict]:
"""Get topic statistics."""
return self.topic_stats
def adaptive_exploration_rate(self, query: str) -> float:
"""Get the exploration rate for a query based on its topic.
Well-tested topics have lower exploration rates.
Untested topics have higher exploration rates.
"""
topic = self.classify_query(query)
if topic in self.topic_stats:
return self.topic_stats[topic].get("exploration_rate", 0.30)
return 0.50 # untested topics get high exploration
def get_dead_weight_topics(self) -> List[str]:
"""Get topics with low proven uses (dead weight)."""
dead = []
for topic, stats in self.topic_stats.items():
if stats["total_queries"] < 5:
dead.append(topic)
return dead
def get_health_report(self) -> dict:
"""Get a health report of the query memory system."""
total_queries = len(self.history)
topics = len(self.topic_stats)
dead_topics = len(self.get_dead_weight_topics())
return {
"total_queries": total_queries,
"topics_tracked": topics,
"dead_weight_topics": dead_topics,
"topic_stats": self.topic_stats,
}
# ═══════════════════════════════════════════════════════════════════════════════
-3
View File
@@ -1,3 +0,0 @@
module rune-entry
go 1.27.1
-172
View File
@@ -1,172 +0,0 @@
// Command rune is the Go first-call entry for the Rune runtime (Phase H1).
//
// Directive (USER-LOCKED 2026-10-04): Go owns the front end — runtime entry
// points, the first-call surface, user-facing calls. This binary is the
// transitional first-call shim: it replicates the EXACT contract of the
// deployed bash launcher (~/.local/bin/rune) — same cwd, same env, same
// handoff — and exec-replaces itself with the Python runtime so exit codes
// and signals pass through 1:1. The Python core shrinks behind this surface
// as the back-end ports land.
//
// Contract (H1 canary, observed from the deployed launcher — not invented):
// cwd: /mnt/workdrive/rune-code/current/src (the deployed revision)
// env: RUNE_HOME=/mnt/workdrive/rune
// RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory
// MIMO_PROBE_ENABLED=1
// handoff: ~/.venvs/memory/bin/python3 -m rune [args...]
//
// Flags consumed by the Go layer (everything else passes through):
// --version print "rune <version>" (deployed pyproject), exit 0
// --python-only print the resolved handoff command, exit 0
// --help usage, exit 0
// unknown flag usage on stderr, exit 2
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
const usage = `rune — Go first-call entry (Phase H1 transitional shim)
usage:
rune [args...] hand off to the Python runtime (deployed revision)
rune serve H2 front door: /v1 surface proxying the Python core
rune scaffold R2 construction protocol initiation (Go start / Rust core)
rune --version print the runtime version
rune --python-only print the resolved Python handoff command and exit
rune --help this text
Environment contract (same as the deployed bash launcher):
cwd /mnt/workdrive/rune-code/current/src
RUNE_HOME data home (stable across code deploys)
RUNE_MEMORY_DIR memory dir
MIMO_PROBE_ENABLED=1
Explicitly-set RUNE_HOME / RUNE_MEMORY_DIR in the ambient environment win.
`
const (
deployRoot = "/mnt/workdrive/rune-code"
dataHome = "/mnt/workdrive/rune"
defaultMemDir = "/home/nick/.config/opencode/memory"
)
// deployedSrc resolves the runtime source dir: the deployed current
// revision when it exists (the launcher's authority), else the binary's own
// repo checkout (development runs), else the desktop default.
func deployedSrc() string {
current := filepath.Join(deployRoot, "current", "src")
if st, err := os.Stat(filepath.Join(current, "rune", "__main__.py")); err == nil && !st.IsDir() {
return current
}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
for i := 0; i < 8; i++ {
cand := filepath.Join(dir, "src")
if st, err := os.Stat(filepath.Join(cand, "rune", "__main__.py")); err == nil && !st.IsDir() {
return cand
}
dir = filepath.Dir(dir)
}
}
return filepath.Join(dataHome, "src")
}
func version(srcDir string) string {
data, err := os.ReadFile(filepath.Join(filepath.Dir(srcDir), "pyproject.toml"))
if err != nil {
return "unknown"
}
const prefix = `version = "`
for _, line := range strings.Split(string(data), "\n") {
if rest, ok := strings.CutPrefix(line, prefix); ok {
if end := strings.IndexByte(rest, '"'); end > 0 {
return rest[:end]
}
}
}
return "unknown"
}
func pythonBin() string {
cand := filepath.Join(os.Getenv("HOME"), ".venvs", "memory", "bin", "python3")
if _, err := os.Stat(cand); err == nil {
return cand
}
return "python3"
}
func upsertEnv(env []string, key, val string) []string {
prefix := key + "="
for i, e := range env {
if strings.HasPrefix(e, prefix) {
env[i] = prefix + val
return env
}
}
return append(env, prefix+val)
}
func launchEnv(base []string) []string {
env := base
if os.Getenv("RUNE_HOME") == "" {
env = upsertEnv(env, "RUNE_HOME", dataHome)
}
if os.Getenv("RUNE_MEMORY_DIR") == "" {
env = upsertEnv(env, "RUNE_MEMORY_DIR", defaultMemDir)
}
env = upsertEnv(env, "MIMO_PROBE_ENABLED", "1")
return env
}
func main() {
args := os.Args[1:]
// H2: the front-door subcommand owns the /v1 surface (no Python handoff).
if len(args) > 0 && args[0] == "serve" {
os.Exit(runServe(args[1:]))
}
// R2 Wave 1: the scaffolding initiation surface (Go starts, Rust executes).
if len(args) > 0 && args[0] == "scaffold" {
os.Exit(runScaffold(args[1:]))
}
srcDir := deployedSrc()
for _, a := range args {
switch a {
case "--help", "-h":
fmt.Print(usage)
return
case "--version":
fmt.Printf("rune %s\n", version(srcDir))
return
case "--python-only":
fmt.Printf("(cd %s && %s -m rune %s)\n", srcDir, pythonBin(), strings.Join(args[1:], " "))
return
default:
if strings.HasPrefix(a, "-") {
fmt.Fprintf(os.Stderr, "rune: unknown flag %q\n\n%s", a, usage)
os.Exit(2)
}
}
}
argv := append([]string{pythonBin(), "-m", "rune"}, args...)
bin, err := exec.LookPath(argv[0])
if err != nil {
fmt.Fprintf(os.Stderr, "rune: cannot find python runtime %q: %v\n", argv[0], err)
os.Exit(127)
}
env := launchEnv(os.Environ())
// chdir before exec: the launcher's cwd contract (the deployed src dir
// makes the rune package importable for `-m rune`).
if err := syscall.Chdir(srcDir); err != nil {
fmt.Fprintf(os.Stderr, "rune: cannot enter %s: %v\n", srcDir, err)
os.Exit(126)
}
if err := syscall.Exec(bin, argv, env); err != nil {
fmt.Fprintf(os.Stderr, "rune: handoff failed: %v\n", err)
os.Exit(126)
}
}
-211
View File
@@ -1,211 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestVersionReadsPyprojectBesideSrc(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "src", "rune"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "pyproject.toml"),
[]byte("[project]\nversion = \"0.99.1\"\n"), 0o644); err != nil {
t.Fatal(err)
}
if got := version(filepath.Join(root, "src")); got != "0.99.1" {
t.Fatalf("want 0.99.1, got %q", got)
}
}
func TestVersionUnknownWhenMissing(t *testing.T) {
if got := version("/nonexistent-src"); got != "unknown" {
t.Fatalf("missing root -> unknown, got %q", got)
}
}
func TestUpsertEnv(t *testing.T) {
env := []string{"A=1", "RUNE_HOME=/old"}
got := upsertEnv(env, "RUNE_HOME", "/new")
if got[1] != "RUNE_HOME=/new" {
t.Fatalf("existing key replaced, got %v", got)
}
got = upsertEnv([]string{"A=1"}, "B", "2")
if got[len(got)-1] != "B=2" || len(got) != 2 {
t.Fatalf("new key appended, got %v", got)
}
}
func TestLaunchEnvContract(t *testing.T) {
t.Setenv("RUNE_HOME", "")
t.Setenv("RUNE_MEMORY_DIR", "")
env := launchEnv(nil)
joined := strings.Join(env, "\n")
for _, want := range []string{
"RUNE_HOME=/mnt/workdrive/rune",
"RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory",
"MIMO_PROBE_ENABLED=1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("launch env missing %q\ngot:\n%s", want, joined)
}
}
}
func TestLaunchEnvHonorsExplicitOverrides(t *testing.T) {
t.Setenv("RUNE_HOME", "/tmp/custom-home")
env := launchEnv([]string{"RUNE_HOME=/tmp/custom-home"})
joined := strings.Join(env, "\n")
if !strings.Contains(joined, "RUNE_HOME=/tmp/custom-home") {
t.Fatalf("explicit RUNE_HOME must win, got:\n%s", joined)
}
if strings.Contains(joined, "RUNE_HOME=/mnt/workdrive/rune") {
t.Fatalf("default must NOT override explicit env, got:\n%s", joined)
}
}
func TestPythonBinFallsBack(t *testing.T) {
t.Setenv("HOME", "/nonexistent-home")
if got := pythonBin(); got != "python3" {
t.Fatalf("missing venv -> python3 on PATH, got %q", got)
}
}
func TestDeployedSrcPrefersLiveCurrent(t *testing.T) {
// The deployed current revision exists on this host (the launcher
// contract) — the shim must resolve to it, not to a dev checkout.
got := deployedSrc()
if !strings.HasPrefix(got, "/mnt/workdrive/rune-code/current") &&
!strings.Contains(got, "/src") {
t.Fatalf("deployed current must win, got %q", got)
}
}
// ── H2 front door (serve.go) ────────────────────────────────────────────────
func TestServeHelpExitsZero(t *testing.T) {
if rc := runServe([]string{"--help"}); rc != 0 {
t.Fatalf("serve --help must exit 0, got %d", rc)
}
}
func TestServeUnknownFlagExitsTwo(t *testing.T) {
if rc := runServe([]string{"--bogus"}); rc != 2 {
t.Fatalf("unknown serve flag must exit 2, got %d", rc)
}
}
func TestFrontDoorNotFoundEnvelope(t *testing.T) {
f := &frontDoor{core: "http://127.0.0.1:1"}
req := httptest.NewRequest("GET", "/v1/nope", nil)
rec := httptest.NewRecorder()
f.mux().ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("unknown route must 404, got %d", rec.Code)
}
var body map[string]map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("404 body must be the error envelope: %v", err)
}
if body["error"]["code"] != "not_found" {
t.Fatalf("404 code must be not_found, got %q", body["error"]["code"])
}
}
func TestFrontDoorCoreDownEnvelope(t *testing.T) {
// Nothing listening on port 1 — the proxy must answer 502
// core_unavailable, not hang and not 500.
f := &frontDoor{core: "http://127.0.0.1:1",
client: &http.Client{Timeout: 2 * time.Second}}
req := httptest.NewRequest("POST", "/v1/chat/completions",
strings.NewReader(`{"messages":[{"role":"user","content":"x"}]}`))
rec := httptest.NewRecorder()
f.mux().ServeHTTP(rec, req)
if rec.Code != http.StatusBadGateway {
t.Fatalf("core down must 502, got %d", rec.Code)
}
var body map[string]map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("502 body must be the error envelope: %v", err)
}
if body["error"]["code"] != "core_unavailable" {
t.Fatalf("502 code must be core_unavailable, got %q", body["error"]["code"])
}
}
func TestFrontDoorProxiesByteForByte(t *testing.T) {
// A canned upstream: the front door must pass status + body through
// verbatim (the H2 parity law in its strongest form).
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"object":"list","data":[]}`)
}))
defer upstream.Close()
f := &frontDoor{core: upstream.URL, client: upstream.Client()}
req := httptest.NewRequest("GET", "/v1/models", nil)
rec := httptest.NewRecorder()
f.mux().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("proxy must pass status through, got %d", rec.Code)
}
if rec.Body.String() != `{"object":"list","data":[]}` {
t.Fatalf("proxy must pass the body byte-for-byte, got %q", rec.Body.String())
}
}
// ── R2 Wave 1: scaffold initiation surface ──────────────────────────────────
func TestScaffoldHelpExitsZero(t *testing.T) {
if rc := runScaffold([]string{"--help"}); rc != 0 {
t.Fatalf("scaffold --help must exit 0, got %d", rc)
}
}
func TestScaffoldUnknownFlagExitsTwo(t *testing.T) {
if rc := runScaffold([]string{"--bogus"}); rc != 2 {
t.Fatalf("unknown scaffold flag must exit 2, got %d", rc)
}
}
func TestScaffoldCoreCallEnvelope(t *testing.T) {
// The membrane junction with a stub binary emitting a canned envelope.
dir := t.TempDir()
stub := filepath.Join(dir, "scaffold_core")
script := "#!/bin/sh\ncat >/dev/null\nprintf '%s\n' '{\"ok\":true,\"value\":{\"legal\":true}}'\n"
if err := os.WriteFile(stub, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
env, err := scaffoldCoreCall(stub, "check_transition",
map[string]string{"current": "draft", "target": "approved"})
if err != nil {
t.Fatalf("stub junction failed: %v", err)
}
if env["ok"] != true {
t.Fatalf("envelope ok must be true, got %v", env)
}
value, _ := env["value"].(map[string]any)
if value["legal"] != true {
t.Fatalf("value.legal must be true, got %v", value)
}
}
func TestScaffoldCoreCallFailureSurfaces(t *testing.T) {
dir := t.TempDir()
stub := filepath.Join(dir, "scaffold_core")
script := "#!/bin/sh\nexit 3\n"
if err := os.WriteFile(stub, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
if _, err := scaffoldCoreCall(stub, "check_transition", map[string]string{}); err == nil {
t.Fatal("a failing core binary must surface an error, not a zero value")
}
}
-160
View File
@@ -1,160 +0,0 @@
// scaffold.go — the scaffolding initiation surface (R2 Wave 1, contract
// BUILDER-2026-10-04T15:25:33Z-cli). Go STARTS, Rust EXECUTES: pure
// operations (check-transition, close-quality) go straight to the
// scaffold_core binary over stdio NDJSON (the hybrid membrane's first
// contracted junction); store-backed operations proxy to the Python engine
// (whose own core is now Rust-delegated) until Wave 1's later slices port
// the contract store.
//
// Stdlib only. The binary path resolves from SC_RUST_BIN, else the repo
// crate target, else PATH.
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const scaffoldUsage = `rune scaffold — the construction protocol initiation surface (R2 Wave 1)
usage:
rune scaffold check-transition <current> <target> legality verdict (Rust core, direct)
rune scaffold close-quality <inputs.json> closure buckets (Rust core, direct)
rune scaffold <op> [args...] store-backed ops proxy to the engine:
submit-need, inspect-decision, scope,
obligations, observe, verify, transition, close
Go initiates; Rust executes the deterministic core; the Python engine
carries store-backed operations during the transition (strangler law).
`
// scaffoldCoreBin resolves the scaffold_core CLI binary.
func scaffoldCoreBin() string {
if p := os.Getenv("SC_RUST_BIN"); p != "" {
return p
}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
for i := 0; i < 8; i++ {
cand := filepath.Join(dir, "src", "rust", "scaffold_core", "target", "release", "scaffold_core")
if st, err := os.Stat(cand); err == nil && !st.IsDir() {
return cand
}
dir = filepath.Dir(dir)
}
}
if p, err := exec.LookPath("scaffold_core"); err == nil {
return p
}
return ""
}
// scaffoldCoreCall runs one pure op through the Rust binary (NDJSON stdin
// -> envelope stdout). The envelope contract is the doc-envelope law.
func scaffoldCoreCall(bin, op string, arg any) (map[string]any, error) {
payload, err := json.Marshal(map[string]any{"op": op, "arg": arg})
if err != nil {
return nil, err
}
cmd := exec.Command(bin)
cmd.Stdin = bytes.NewReader(payload)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("scaffold_core %s failed: %w", op, err)
}
var env map[string]any
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
return nil, fmt.Errorf("scaffold_core %s returned non-JSON: %w", op, err)
}
return env, nil
}
// scaffoldProxy hands a store-backed op to the Python engine (the
// transitional membrane — the engine's core is Rust-backed already).
func scaffoldProxy(args []string) int {
py := pythonBin()
argv := append([]string{py, "-m", "rune.construction_interface"}, args...)
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Dir = deployedSrc()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
env := launchEnv(os.Environ())
cmd.Env = env
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "rune scaffold: engine call failed: %v\n", err)
return 1
}
return 0
}
// runScaffold implements `rune scaffold`. Returns the process exit code.
func runScaffold(args []string) int {
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
fmt.Print(scaffoldUsage)
return 0
}
switch args[0] {
case "check-transition":
if len(args) != 3 {
fmt.Fprintln(os.Stderr, "usage: rune scaffold check-transition <current> <target>")
return 2
}
bin := scaffoldCoreBin()
if bin == "" {
fmt.Fprintln(os.Stderr, "rune scaffold: scaffold_core binary not found (build the crate)")
return 127
}
env, err := scaffoldCoreCall(bin, "check_transition",
map[string]string{"current": args[1], "target": args[2]})
if err != nil {
fmt.Fprintln(os.Stderr, "rune scaffold:", err)
return 1
}
out, _ := json.Marshal(env)
fmt.Println(string(out))
return 0
case "close-quality":
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "usage: rune scaffold close-quality <inputs.json>")
return 2
}
raw, err := os.ReadFile(args[1])
if err != nil {
fmt.Fprintln(os.Stderr, "rune scaffold: cannot read inputs:", err)
return 2
}
var inputs any
if err := json.Unmarshal(raw, &inputs); err != nil {
fmt.Fprintln(os.Stderr, "rune scaffold: inputs are not JSON:", err)
return 2
}
bin := scaffoldCoreBin()
if bin == "" {
fmt.Fprintln(os.Stderr, "rune scaffold: scaffold_core binary not found (build the crate)")
return 127
}
env, err := scaffoldCoreCall(bin, "close_quality", inputs)
if err != nil {
fmt.Fprintln(os.Stderr, "rune scaffold:", err)
return 1
}
out, _ := json.Marshal(env)
fmt.Println(string(out))
return 0
default:
if strings.HasPrefix(args[0], "-") {
fmt.Fprintf(os.Stderr, "rune scaffold: unknown flag %q\n\n%s", args[0], scaffoldUsage)
return 2
}
return scaffoldProxy(args)
}
}
-173
View File
@@ -1,173 +0,0 @@
// serve.go — H2 front door (T4.3, contract BUILDER-2026-10-04T08:32:43Z-cli).
//
// Go owns the front end (USER-LOCKED directive): this subcommand serves the
// OpenAI-compatible /v1 surface on 127.0.0.1 and proxies to the Python core
// sidecar (core_server.py) during the transition. Parity law: response
// bodies pass through byte-for-byte (the canary compares against the Python
// server modulo volatile id/created), and locally-generated errors use the
// same envelope the core emits: {"error": {"code", "message"}}.
//
// Stdlib only (net/http, io) — the plan's "stdlib net/http + NDJSON" line.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const serveUsage = `rune serve — H2 front door (OpenAI-compatible /v1 surface)
usage:
rune serve [--port N] [--core URL]
flags:
--port N listen port on 127.0.0.1 (default 8377)
--core URL Python core sidecar base URL (default http://127.0.0.1:8376)
Endpoints (identical shapes to the core — the parity battery enforces it):
GET /v1/models
GET /v1/status
POST /v1/chat/completions stream=true → NDJSON chunk lines
`
type frontDoor struct {
core string
client *http.Client
}
// errorEnvelope mirrors core_server._envelope_error exactly.
func errorEnvelope(w http.ResponseWriter, status int, code, message string) {
body, _ := json.Marshal(map[string]any{
"error": map[string]string{"code": code, "message": message},
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write(body)
}
// proxy forwards one request to the core and streams the response back
// verbatim — byte-parity with the Python server is the H2 contract, and a
// pure pass-through is the strongest form of it.
func (f *frontDoor) proxy(w http.ResponseWriter, r *http.Request) {
var body io.Reader
if r.Body != nil {
raw, err := io.ReadAll(r.Body)
if err != nil {
errorEnvelope(w, http.StatusBadRequest, "bad_request", "unreadable body")
return
}
body = bytes.NewReader(raw)
}
req, err := http.NewRequest(r.Method, f.core+r.URL.Path, body)
if err != nil {
errorEnvelope(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
resp, err := f.client.Do(req)
if err != nil {
errorEnvelope(w, http.StatusBadGateway, "core_unavailable",
"core sidecar unreachable: "+err.Error())
return
}
defer resp.Body.Close()
for k, vs := range resp.Header {
if strings.EqualFold(k, "Content-Type") {
w.Header().Set(k, vs[0])
}
}
w.WriteHeader(resp.StatusCode)
// NDJSON streaming: flush per write so chunk lines reach the caller as
// the core emits them (the framing is the contract).
flusher, canFlush := w.(http.Flusher)
buf := make([]byte, 4096)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
if _, werr := w.Write(buf[:n]); werr != nil {
return
}
if canFlush {
flusher.Flush()
}
}
if readErr != nil {
return
}
}
}
func (f *frontDoor) mux() *http.ServeMux {
mux := http.NewServeMux()
handle := func(path string) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path {
errorEnvelope(w, http.StatusNotFound, "not_found",
"no route "+r.URL.Path)
return
}
f.proxy(w, r)
})
}
handle("/v1/models")
handle("/v1/status")
handle("/v1/chat/completions")
// H4 slice 1: the TUI endpoints (same pass-through law).
handle("/v1/conversation")
handle("/v1/approvals/resolve")
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
errorEnvelope(w, http.StatusNotFound, "not_found", "no route "+r.URL.Path)
})
return mux
}
// runServe implements `rune serve`. Returns the process exit code.
func runServe(args []string) int {
port := 8377
core := "http://127.0.0.1:8376"
for i := 0; i < len(args); i++ {
switch args[i] {
case "--help", "-h":
fmt.Print(serveUsage)
return 0
case "--port":
if i+1 >= len(args) {
fmt.Fprintln(os.Stderr, "rune serve: --port needs a value")
return 2
}
i++
if _, err := fmt.Sscanf(args[i], "%d", &port); err != nil {
fmt.Fprintf(os.Stderr, "rune serve: bad --port %q\n", args[i])
return 2
}
case "--core":
if i+1 >= len(args) {
fmt.Fprintln(os.Stderr, "rune serve: --core needs a value")
return 2
}
i++
core = strings.TrimSuffix(args[i], "/")
default:
fmt.Fprintf(os.Stderr, "rune serve: unknown flag %q\n\n%s", args[i], serveUsage)
return 2
}
}
f := &frontDoor{
core: core,
client: &http.Client{Timeout: 130 * time.Second},
}
addr := fmt.Sprintf("127.0.0.1:%d", port)
fmt.Printf("rune front door on %s (core %s)\n", addr, core)
if err := http.ListenAndServe(addr, f.mux()); err != nil {
fmt.Fprintf(os.Stderr, "rune serve: %v\n", err)
return 1
}
return 0
}
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.99.7"
__version__ = "0.87.0"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+2 -62
View File
@@ -43,42 +43,6 @@ LOCAL_MODEL = "qwen2.5:3b"
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
_ABSTRACTION_STOPWORDS_FROZEN = frozenset(
"the a an is are was were be been being have has had do does did will "
"would could should may might shall can to of in for on with at by from "
"as into through during before after this that these those it its what "
"which who how when where why and or but not no".split()
)
def _ABSTRACTION_STOPWORDS() -> list:
"""Gather seam (W3.4): the fact-extraction stopword table as a sorted
list — data received by the kernel; identical filter on both engines."""
return sorted(_ABSTRACTION_STOPWORDS_FROZEN)
def _core():
"""The Rust cognition_core when available (strangler: Python fallback
load-bearing). RUNE_COGNITION_CORE=python pins the fallback."""
return None if cc_loader_pin() else _cc_probe()
def _cc_probe():
from rune import cc_loader as _cl
try:
return _cl if _cl.available() else None
except Exception: # noqa: BLE001, S110 — loader probe never crashes the fallback
return None
def cc_loader_pin() -> bool:
"""The env pin: RUNE_COGNITION_CORE=python → kernels OFF (shared probe).
"""
import os
return os.environ.get("RUNE_COGNITION_CORE", "auto") == "python"
def _now():
return datetime.now(timezone.utc).isoformat()
@@ -277,41 +241,17 @@ Extract 1-5 key facts. Be precise, not verbose."""
response = llm_call(prompt, max_tokens=800)
# Parse response
facts: list = []
try:
# Try to extract JSON from response
start = response.find("[")
end = response.rfind("]") + 1
if start >= 0 and end > start:
facts = json.loads(response[start:end])
else:
facts = []
except json.JSONDecodeError:
facts = []
# LLM unavailable / unparseable → DETERMINISTIC KERNEL FALLBACK
# (W3.4: cc_abstraction_extract_facts — sentence split, stopwords
# filtered, min-length gate; the same facts schema it already emits:
# text/words/content_words mapped onto what= text with empty dims).
if not facts:
core = _core()
if core is not None:
try:
from rune import cc_loader as _cl
raw = _cl.abstraction_extract_facts(
episode_text, _ABSTRACTION_STOPWORDS(), 10)
for item in raw or []:
text_frag = str(item.get("text", "")).strip()
if not text_frag:
continue
facts.append({
"what": text_frag, "when": "", "where": "", "who": "",
"why": "", "type": "experience", "entities": [],
"kernel": True,
"content_words": item.get("content_words"),
})
except Exception: # noqa: BLE001, S110 — kernel failure degrades to empty, intentional silence
pass
if not facts:
return {"extracted": 0, "error": "LLM did not return valid JSON", "raw": response[:200]}
+33 -29
View File
@@ -15,17 +15,6 @@ Usage:
behavior.py reinforce <id> [--outcome success|failure]
behavior.py list [--type correction|habit|all]
behavior.py stats
Phase G strangler slice 2 (port 5): the decision math (reinforce
counting/confidence/promotion/state formulas) is served IN-PROCESS by the
Rust `evidence_provenance` cdylib through ep_loader.py. STORAGE STAYS
PYTHON — SQLite I/O and the regex matching layer remain here (the crate is
zero-dep by design; hand-rolling SQLite/regex would be hubris, not
hardening). Vintage upgrade per the frozen contract §2: this module is the
src/rune authority vintage; the deployed ~/.config/opencode/scripts copy
is superseded by the same shim shape pointing at the live db path.
Timestamps are pinned by the shim (`now`) — the core is deterministic.
API, CLI, and PLUGIN shapes unchanged (git history holds the original).
"""
import json
@@ -35,12 +24,6 @@ import sqlite3
import sys
from datetime import datetime, timezone
# ── Rust core wiring (slice-2 port-5 shim pattern) ──────────────────────────
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
if _CRATE_DIR not in sys.path:
sys.path.insert(0, _CRATE_DIR)
import ep_loader # noqa: E402 (loads target/release cdylib)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import memory_env
@@ -211,10 +194,7 @@ def match_context(context, match_type=None):
def reinforce(item_id, outcome, item_type="habit"):
"""Reinforce a correction or habit based on outcome.
Decision math (counting, confidence formula, promotion, state) served
by the Rust core; SQL stays here."""
"""Reinforce a correction or habit based on outcome."""
db = connect()
try:
ts = _now()
@@ -224,14 +204,28 @@ def reinforce(item_id, outcome, item_type="habit"):
if not habit:
return {"error": f"Habit {item_id} not found"}
upd = ep_loader.habit_reinforce(dict(habit), outcome)
new_count = habit["activation_count"] + 1
new_success = habit["success_count"] + (1 if outcome == "success" else 0)
new_failure = habit["failure_count"] + (1 if outcome == "failure" else 0)
# Update confidence based on success rate
if new_count > 0:
success_rate = new_success / new_count
new_confidence = min(1.0, 0.5 + success_rate * 0.5)
else:
new_confidence = habit["confidence"]
# Promote from candidate to active if enough activations
new_state = habit["state"]
if habit["state"] == "candidate" and new_count >= habit["min_activations"]:
new_state = "active"
db.execute(
"""
UPDATE habits SET activation_count=?, success_count=?, failure_count=?,
confidence=?, state=?, last_activated=? WHERE id=?
""",
(upd["activation_count"], upd["success_count"], upd["failure_count"],
upd["confidence"], upd["state"], ts, item_id),
(new_count, new_success, new_failure, new_confidence, new_state, ts, item_id),
)
# Log activation
@@ -244,23 +238,33 @@ def reinforce(item_id, outcome, item_type="habit"):
)
db.commit()
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
else: # correction
correction = db.execute("SELECT * FROM corrections WHERE id=?", (item_id,)).fetchone()
if not correction:
return {"error": f"Correction {item_id} not found"}
upd = ep_loader.correction_reinforce(dict(correction), outcome)
if outcome == "success":
new_confidence = min(1.0, correction["confidence"] + 0.1)
new_count = correction["evidence_count"] + 1
elif outcome == "failure":
new_confidence = max(0.0, correction["confidence"] - 0.3)
new_count = correction["evidence_count"]
else:
new_confidence = correction["confidence"]
new_count = correction["evidence_count"]
new_state = "active" if new_confidence > 0.3 else "archived"
db.execute(
"""
UPDATE corrections SET confidence=?, evidence_count=?, state=?, last_reinforced=?
WHERE id=?
""",
(upd["confidence"], upd["evidence_count"], upd["state"], ts, item_id),
(new_confidence, new_count, new_state, ts, item_id),
)
db.commit()
return {"reinforced": item_id, "confidence": upd["confidence"], "state": upd["state"]}
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
finally:
db.close()
@@ -404,4 +408,4 @@ PLUGIN = {
"provides": ["behavior"],
"priority": 50,
"auto_activate": True,
}
}
+126 -48
View File
@@ -16,29 +16,12 @@ dependencies changed are invalidated; every invalidation carries a
reason; unrelated hardened confirmations are preserved.
Storage: <project_store>/behaviour_ledger.json (versioned document).
Phase G strangler slice 2 (port 4): the state machine (define / get /
transitions / selective invalidation / queues / summary) is served
IN-PROCESS by the Rust `evidence_provenance` cdylib through ep_loader.py
— parity 13/13 proven vs this module's Python vintage (full-scenario
doc parity, byte-equal error messages, save render indent=1 sort_keys).
File glue (load/save) stays Python; timestamps are pinned by this shim
(`now`) so the core stays deterministic. State constants and class API
unchanged (git history holds the original).
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
# ── Rust core wiring (slice-2 port-4 shim pattern) ──────────────────────────
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
if _CRATE_DIR not in sys.path:
sys.path.insert(0, _CRATE_DIR)
import ep_loader # noqa: E402 (loads target/release cdylib)
LEDGER_SCHEMA_VERSION = 1
# hardening lifecycle (rule 19G semantics)
@@ -73,11 +56,13 @@ class BehaviourLedger:
def _load(self) -> dict:
if self.path.is_file():
import json
return json.loads(self.path.read_text())
return {"schema_version": LEDGER_SCHEMA_VERSION,
"behaviours": {}, "updated_at": _now()}
def save(self) -> None:
import json
self.doc["updated_at"] = _now()
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
@@ -92,34 +77,66 @@ class BehaviourLedger:
dependencies: list[str] | None = None,
automated_ref: str = "",
environment: str = "") -> dict:
self.doc, rec = ep_loader.bl_define(
self.doc, behaviour_id=behaviour_id,
owning_project=owning_project,
intended_behaviour=intended_behaviour,
origin_intent=origin_intent,
pass_criteria=pass_criteria,
fail_criteria=fail_criteria,
procedure=procedure,
owner_component=owner_component,
dependencies=dependencies,
automated_ref=automated_ref,
environment=environment,
now=_now())
bid = (behaviour_id if behaviour_id.startswith("behaviour:")
else f"behaviour:{behaviour_id}")
if bid in self.doc["behaviours"]:
raise ValueError(f"behaviour exists: {bid}")
rec = {
"behaviour_id": bid,
"owning_project": owning_project,
"intended_behaviour": intended_behaviour,
"origin_intent": origin_intent,
"pass_criteria": pass_criteria,
"fail_criteria": fail_criteria,
"procedure": procedure,
"owner_component": owner_component, # structural entity id
"dependencies": list(dependencies or []), # structural ids
"automated_ref": automated_ref,
"environment": environment,
"state": UNVALIDATED,
"evidence": [], # [{at, generation, kind, detail}]
"history": [], # every state transition, never lost
"invalidations": [], # [{at, generation, because}]
"limitations": [],
"created_at": _now(),
"updated_at": _now(),
}
self.doc["behaviours"][bid] = rec
return rec
def get(self, behaviour_id: str) -> dict | None:
return ep_loader.bl_get(self.doc, behaviour_id)
return self.doc["behaviours"].get(
behaviour_id if behaviour_id.startswith("behaviour:")
else f"behaviour:{behaviour_id}")
# -- state transitions (all recorded in history — rule 19G) -------------
def _transition(self, rec: dict, new_state: str, *, kind: str,
detail: str = "", generation: str = "") -> None:
if new_state not in STATES:
raise ValueError(f"unknown validation state {new_state!r}")
rec["history"].append({
"at": _now(), "from": rec["state"], "to": new_state,
"kind": kind, "detail": detail[:200],
"generation": generation})
rec["state"] = new_state
rec["updated_at"] = _now()
def record_auto_verification(self, behaviour_id: str, *,
passed: bool, detail: str = "",
generation: str = "") -> dict:
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
self.doc, rec = ep_loader.bl_record_auto(
self.doc, behaviour_id, passed=passed, detail=detail,
generation=generation, now=_now())
rec = self.get(behaviour_id)
if rec is None:
raise ValueError(f"unknown behaviour {behaviour_id!r}")
rec["evidence"].append({"at": _now(), "kind": "automated",
"passed": passed,
"detail": detail[:200],
"generation": generation})
self._transition(rec, AUTO_VERIFIED if passed else TEST_FAILED,
kind="automated_verification", detail=detail,
generation=generation)
return rec
def record_real_world(self, behaviour_id: str, *, passed: bool,
@@ -127,16 +144,32 @@ class BehaviourLedger:
confirmed_by: str = "user") -> dict:
"""Real-world/manual test result (testing mode). USER_CONFIRMED
requires an explicit confirmed_by='user' pass (rule 19M)."""
self.doc, rec = ep_loader.bl_record_real_world(
self.doc, behaviour_id, passed=passed, detail=detail,
generation=generation, confirmed_by=confirmed_by, now=_now())
rec = self.get(behaviour_id)
if rec is None:
raise ValueError(f"unknown behaviour {behaviour_id!r}")
rec["evidence"].append({"at": _now(), "kind": "real_world",
"passed": passed,
"detail": detail[:200],
"generation": generation,
"by": confirmed_by})
if passed:
state = (USER_CONFIRMED if confirmed_by == "user"
else REAL_WORLD_PASSED)
else:
state = TEST_FAILED
self._transition(rec, state, kind="real_world", detail=detail,
generation=generation)
return rec
def accept_limitation(self, behaviour_id: str, *, note: str,
approved_by: str = "user") -> dict:
self.doc, rec = ep_loader.bl_accept_limitation(
self.doc, behaviour_id, note=note,
approved_by=approved_by, now=_now())
rec = self.get(behaviour_id)
if rec is None:
raise ValueError(f"unknown behaviour {behaviour_id!r}")
rec["limitations"].append({"at": _now(), "note": note[:200],
"approved_by": approved_by})
self._transition(rec, ACCEPTED_LIMITATION, kind="accept_limitation",
detail=note, )
return rec
# -- selective invalidation (rule 19I) -----------------------------------
@@ -149,15 +182,44 @@ class BehaviourLedger:
structural dependencies intersect the changed closure. Every
invalidation carries a reason; unrelated confirmations stay.
Returns the invalidated records."""
self.doc, recs = ep_loader.bl_invalidate(
self.doc, changed_entities=sorted(changed_entities),
generation=generation, reasons=reasons, now=_now())
return recs
reasons = reasons or {}
invalidated = []
for rec in self.doc["behaviours"].values():
if rec["state"] not in CONFIRMED_STATES:
continue
overlap = set(rec.get("dependencies") or []) & changed_entities
if rec.get("owner_component") in changed_entities:
overlap.add(rec["owner_component"])
if not overlap:
continue # unrelated hardened behaviour: PRESERVED (19S-5)
because = reasons.get(
rec["behaviour_id"],
f"structural change touched: {sorted(overlap)}")
rec["invalidations"].append({
"at": _now(), "generation": generation,
"because": because[:200],
"was": rec["state"],
"changed_entities": sorted(overlap)})
# HISTORY IS PRESERVED — prior confirmation stays queryable
self._transition(rec, RETEST_REQUIRED,
kind="selective_invalidation",
detail=f"was {rec['state']}; {because}",
generation=generation)
invalidated.append(rec)
return invalidated
def retest_history(self, behaviour_id: str) -> list[dict]:
"""Generation-aware history: all confirmations + invalidations
for one behaviour, oldest first (rule 19S-12)."""
return ep_loader.bl_retest_history(self.doc, behaviour_id)
rec = self.get(behaviour_id)
if rec is None:
return []
events = []
for h in rec["history"]:
events.append({"at": h["at"], "generation": h.get("generation"),
"event": f"{h['from']}→{h['to']}",
"kind": h["kind"], "detail": h.get("detail", "")})
return events
# -- queue (testing mode, 19L/M) ------------------------------------------
@@ -166,8 +228,24 @@ class BehaviourLedger:
"""Eligible testing items: anything not confirmed/accepted/n.a.
Dynamic — the queue re-derives from states after every fix
(rule 19M)."""
return ep_loader.bl_outstanding(
self.doc, component=component or "", project=project or "")
out = []
for rec in self.doc["behaviours"].values():
if rec["state"] in (USER_CONFIRMED, ACCEPTED_LIMITATION,
NOT_APPLICABLE):
continue
if project and rec.get("owning_project") != project:
continue
if component and rec.get("owner_component") != component:
continue
out.append(rec)
out.sort(key=lambda r: (0 if r["state"] == TEST_FAILED else 1,
r["behaviour_id"]))
return out
def summary(self) -> dict:
return ep_loader.bl_summary(self.doc)
counts: dict[str, int] = {}
for rec in self.doc["behaviours"].values():
counts[rec["state"]] = counts.get(rec["state"], 0) + 1
return {"total": len(self.doc["behaviours"]),
"states": counts,
"outstanding": len(self.outstanding())}
-61
View File
@@ -64,7 +64,6 @@ class BuilderCapsule:
memory_lines: list = field(default_factory=list)
lesson_lines: list = field(default_factory=list)
constraints: list = field(default_factory=list)
construction_lines: list = field(default_factory=list)
generated_at: str = ""
core_version: str = "rune-core/operator-path"
@@ -76,7 +75,6 @@ class BuilderCapsule:
"memory_lines": self.memory_lines[:MAX_MEMORY_LINES],
"lesson_lines": self.lesson_lines[:MAX_LESSON_LINES],
"constraints": self.constraints,
"construction_lines": self.construction_lines,
"generated_at": self.generated_at,
"core_version": self.core_version,
}
@@ -89,14 +87,10 @@ class BuilderCapsule:
and not d["lesson_lines"]
and not d["unknowns"]
and not d["constraints"]
and not d["construction_lines"]
and d["task_route"] in ("LOCAL_FIX", "STANDARD_CHANGE")
):
return "" # nothing useful — inject nothing (handoff §5)
lines = [f"### Rune Core preflight ({d['task_route']})"]
if d["construction_lines"]:
lines.append("**Active Scaffolding construction (read-only projection):**")
lines += [f"- {c}" for c in d["construction_lines"]]
if d["memory_lines"]:
lines.append("**Relevant Rune memory:**")
lines += [f"- {m}" for m in d["memory_lines"]]
@@ -113,56 +107,6 @@ class BuilderCapsule:
return "\n".join(lines)
def _construction_store() -> str:
"""The SAME construction store the operator path uses (env override
honoured; default derives from the repo root, never a bare relative
path). Kept identical to rune_operator._construction_store."""
import os as _os
env = _os.environ.get("RUNE_CONSTRUCTION_STORE")
if env:
return env
from pathlib import Path as _P
return str(_P(__file__).resolve().parent.parent.parent / "store" / "scaffold")
def _active_construction_lines() -> list:
"""Read-only projection of the ACTIVE Scaffolding construction into
builder-consumable lines. This is the MiMo edge of the shared
Construction Context (§15) — it surfaces canonical state; it is NOT a
second construction authority and never mutates. Degrade-safe: any
failure returns [] (the bridge must never block the builder)."""
try:
from .construction_interface import ConstructionInterface
env = ConstructionInterface(builder="mimo-bridge",
project_store=_construction_store(),
project_id="issue161-harness"
).construction_context()
st = env.get("state") or {}
if not st.get("active"):
return []
out = [f"contract {st.get('contract_id')} · phase {st.get('phase')}"
f" · status {st.get('status')}"
f" · mode {st.get('construction_mode')}"]
if st.get("project"):
out.append(f"project: {st['project']}")
if st.get("need"):
out.append(f"need: {st['need']}")
scope = st.get("scope") or []
if scope:
out.append("scope: " + ", ".join(str(x) for x in scope[:8]))
obl = st.get("obligations") or []
if obl:
out.append(f"outstanding obligations: {len(obl)}")
for o in obl[:3]:
txt = o.get("obligation") or o.get("kind") or o
out.append(f" - {str(txt)[:120]}")
if st.get("next_action"):
out.append(f"next action: {st['next_action']}")
return out
except Exception: # noqa: BLE001 — projection only; never block the builder
return []
def _default_selector():
"""The SAME MemorySelector the operator path uses (lazy, degrade-safe)."""
try:
@@ -222,11 +166,6 @@ def preflight(task_text: str, memory_selector=None) -> BuilderCapsule:
# design (handoff §9: raw SEED records are outcome telemetry, not lessons).
capsule.lesson_lines = _validated_lessons_for(task_text or "")
# Active Scaffolding construction state (read-only projection of the
# shared Construction Context). Empty when no contract is active, so
# the inject-nothing policy is preserved for ordinary turns.
capsule.construction_lines = _active_construction_lines()
return capsule
-540
View File
@@ -1,540 +0,0 @@
#!/usr/bin/env python3
"""canary_audit_ledger.py — Python canary for slice-2 ports 1-6.
Slice-1 PASS/FAIL discipline (counter harness, exit 1 on any FAIL), pinned to
the FROZEN CONTRACT /mnt/workdrive/rune/plans/2026-10-03-phase-g-evidence-provenance-contract.md:
§5 requires the canary pair to carry per-port sections for ALL SIX ports.
DUAL-MODE LAW (added 2026-10-03 after run 18141 went red): sections 2/3/4/5
pin the LIVE promotion ledger when it exists in this process's HOME (desktop
runs — the drift-key pin is the §3 linchpin). Under CI's sandboxed HOME the
ledger file does not exist, so the same sections run an ISOLATED battery
against the sandboxed ledger with count expectations re-derived from the
seeded entries — identical test count, identical shapes, honest MODE print.
The artifact coupling is disclosed, not hidden: the Rust binary + cdylib
resolve to the checkout's own target/release/ (CI's host-executor topology
makes the desktop build visible; if they ever go missing, ep_loader raises
and this canary FAILS LOUDLY rather than testing nothing).
Canary expectations are OBSERVED, not invented:
- Deployed shim head + verify_chain() on the live 23-entry ledger:
{'valid': True, 'entries': 23, 'reason': 'chain intact'} (observed 2026-10-03).
- verify_chain() return shape is a 3-key dict, no tuple unpacking.
- Deployed SECRET_KEY default: 'rune-drift-integrity-2024' (the live-chain key).
- Empty-ledger contract: missing ledger file -> valid-and-empty dict.
- Shadow battery (18/18 GREEN, banked): read/write/CLI-error shapes all match
the old vintage at the shim.
Run under ~/.venvs/memory/bin/python3.
"""
import os
import sys
sys.path.insert(0, "/home/nick/.config/opencode/scripts")
# Repo-derived roots (no /mnt/workdrive hardcodes): this file lives in
# src/rune/, so the crate is two parents up plus src/rust/evidence_provenance.
_REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
_CRATE_DIR = os.path.join(_REPO, "src", "rust", "evidence_provenance")
if os.environ.get("EP_RUST_LIB"):
# CI: the build step emits to a runner-owned /tmp target dir and the job
# points EP_RUST_LIB at the cdylib FILE (that's the form ep_loader's
# override takes). The binary lives next to it.
_BIN = os.path.join(os.path.dirname(os.environ["EP_RUST_LIB"]),
"evidence_provenance")
else:
_BIN = os.path.join(_CRATE_DIR, "target", "release", "evidence_provenance")
# Dual-mode switch (DUAL-MODE LAW in the docstring): LIVE when this process's
# HOME carries the real promotion ledger AND we are not inside the CI sandbox.
# The discriminator is the repo's own CI-sandbox convention — the workflow sets
# RUNE_HOME=/tmp/rune-ci-<run_id> and its cleanup guard matches /tmp/rune-ci-*
# exactly. File EXISTENCE is NOT a valid discriminator: run 18141's shim writes
# left a zombie 1-entry ledger in the runner's persistent HOME, which would
# flip a naive isfile() gate to LIVE with the wrong data (proven 2026-10-03
# on run 18161: "entries >= 23 — got 1").
_LIVE_LEDGER = os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl")
_IN_CI_SANDBOX = os.environ.get("RUNE_HOME", "").startswith("/tmp/rune-ci-")
LIVE = os.path.isfile(_LIVE_LEDGER) and not _IN_CI_SANDBOX
PASS = 0
FAIL = 0
def test(name, condition, detail=""):
global PASS, FAIL
if condition:
PASS += 1
print(f" ✓ {name}")
else:
FAIL += 1
print(f" ✗ {name} — {detail}")
def main():
global PASS, FAIL
print("=== AUDIT LEDGER CANARY (slice-2 ports 1-6) ===")
print(f"MODE: {'LIVE (deployed surface + live ledger)' if LIVE else 'ISOLATED (sandboxed ledger — CI mode)'}\n")
bin_home = None # binary HOME override for §5 in ISOLATED mode
# --- 1. Deployed module surface ---
print("--- 1. Deployed module surface ---")
try:
import audit_ledger
test("audit_ledger imports (deployed shim)", True)
for name in (
"_compute_hash",
"append_entry",
"verify_chain",
"get_entries",
"log_promotion",
"log_gate_result",
"log_drift_alert",
"main",
):
test(f"has {name}", hasattr(audit_ledger, name))
test("LEDGER_FILE is the live ledger path",
audit_ledger.LEDGER_FILE ==
os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl"),
f"got {audit_ledger.LEDGER_FILE}")
test("SECRET_KEY default is the drift key (live-chain key)",
audit_ledger.SECRET_KEY == "rune-drift-integrity-2024",
f"got {audit_ledger.SECRET_KEY}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("audit_ledger module", False, str(e))
print(f"CANARY ABORT: import failed — {e}")
sys.exit(1)
# --- 2. Ledger verification through the shim (live on desktop / isolated in CI) ---
print("\n--- 2. Ledger verification ---")
if not LIVE:
# Seed a PER-RUN isolated ledger (never the runner's persistent HOME —
# leftover writes there are what poisoned run 18161's mode detection):
# point the shim's module-level LEDGER_FILE at a temp path; all §2/§3/§4
# reads/writes then land in the temp file. Binary §5 gets HOME=<temp>.
# Placed AFTER §1 so the surface pins observe the shim's DEFAULT
# resolution before the override.
import tempfile as _tf
bin_home = _tf.mkdtemp(prefix="alcanary_home_")
iso_ledger = os.path.join(bin_home, ".config", "opencode", "memory",
"promotion_ledger.jsonl")
os.makedirs(os.path.dirname(iso_ledger), exist_ok=True)
audit_ledger.LEDGER_FILE = iso_ledger
for seed_id in (9001, 9002):
audit_ledger.append_entry("promotion", seed_id,
{"reason": "canary-ci-seed", "old_status": "a",
"new_status": "b", "wash_count": 1})
try:
result = audit_ledger.verify_chain()
test("verify_chain() returns dict", isinstance(result, dict), f"got {type(result)}") # type: ignore[reportUnnecessaryIsInstance]
test("valid is True on the chain", result.get("valid") is True,
f"got {result.get('valid')}")
if LIVE:
test("entries >= 23 (ledger grew through prior canaries)",
result.get("entries", 0) >= 23, f"got {result.get('entries')}")
else:
test("entries == 2 (the §2 seeds this process wrote)",
result.get("entries") == 2, f"got {result.get('entries')}")
test("reason is 'chain intact'", result.get("reason") == "chain intact",
f"got {result.get('reason')}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("live verify_chain()", False, str(e))
# --- 3. Read path: get_entries ---
print("\n--- 3. Read path ---")
try:
entries = audit_ledger.get_entries(limit=5)
test("get_entries(5) returns a list", isinstance(entries, list)) # type: ignore[reportUnnecessaryIsInstance]
if LIVE:
test("get_entries(5) returns 5 entries on the live ledger", len(entries) == 5,
f"got {len(entries)}")
else:
test("get_entries(5) returns the 2 seeded entries (isolated)", len(entries) == 2,
f"got {len(entries)}")
first = entries[0] if entries else {}
test("entry carries hash chain fields",
"hash" in first and "prev_hash" in first,
f"keys: {sorted(first.keys())}")
# Observed deployed-vintage serving order: ASCENDING (oldest-first
# within the limit window) — verified against the live ledger 2026-10-03.
test("entries served in ascending timestamp order (deployed-vintage shape)",
all(entries[i]["timestamp"] <= entries[i + 1]["timestamp"]
for i in range(len(entries) - 1)),
"timestamps not monotonic-ascending")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("get_entries", False, str(e))
# --- 4. Write path: append through the deployed shim (LIVE ledger,
# the established canary pattern — entries 89/90 are this shape) ---
print("\n--- 4. Write path (canary append through the shim) ---")
try:
e1 = audit_ledger.append_entry("promotion", 4242, {"old_status": "established",
"new_status": "rock",
"wash_count": 20,
"reason": "canary"})
test("append_entry returns dict", isinstance(e1, dict), f"got {type(e1)}") # type: ignore[reportUnnecessaryIsInstance]
test("entry has prev_hash + hash", "prev_hash" in e1 and "hash" in e1)
test("hash is 64-hex", isinstance(e1.get("hash"), str) and len(e1["hash"]) == 64
and all(c in "0123456789abcdef" for c in e1["hash"]))
test("append flowed through the shim: live chain still intact",
audit_ledger.verify_chain()["valid"] is True,
"live chain invalid after append")
tail = audit_ledger.get_entries(limit=1)
test("newest live entry is the canary append (entry_id 4242)",
tail and tail[0].get("entry_id") == 4242,
f"got {tail[0].get('entry_id') if tail else 'empty'}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("append_entry write path", False, str(e))
# --- 5. Rust binary CLI parity (the compiled candidate) ---
print("\n--- 5. Rust binary CLI parity ---")
import subprocess
import tempfile
BIN = _BIN
tmp2 = tempfile.mkdtemp(prefix="alcanary_cli_")
# The binary honors RUNE_MEMORY_DIR/RUNE_HOME first (main.rs ledger_path);
# the deployed shim resolves expanduser($HOME). Strip the RUNE_* overrides
# from binary envs so both surfaces resolve the SAME ledger — without this,
# CI's RUNE_MEMORY_DIR silently pointed the binary at an empty ledger while
# the shim wrote the sandboxed-HOME one (run 18141's ghost divergence).
clean_env = {k: v for k, v in os.environ.items()
if k not in ("RUNE_MEMORY_DIR", "RUNE_HOME")}
if not os.path.exists(BIN):
# FAIL loud, never crash: run 18164 died on an uncaught Popen
# FileNotFoundError when the artifact was absent from the checkout.
# CI now builds the crate (canary job); a missing binary here means
# the build step regressed and this canary must go red, not skip.
test("release binary exists in the checkout (CI builds it)",
False, f"missing: {BIN}")
else:
try:
env = dict(clean_env, HOME=tmp2)
r = subprocess.run([BIN, "verify"], capture_output=True, text=True, env=env)
test("binary verify on missing ledger -> valid-empty",
r.returncode == 0 and "Chain valid: True" in r.stdout
and "Entries: 0" in r.stdout and "empty ledger" in r.stdout,
f"rc={r.returncode} out={r.stdout!r} err={r.stderr!r}")
r = subprocess.run([BIN], capture_output=True, text=True, env=env)
test("binary no-args -> help + exit 1", r.returncode == 1
and "usage:" in r.stdout, f"rc={r.returncode}")
r = subprocess.run([BIN, "frobnicate"], capture_output=True, text=True, env=env)
test("binary invalid choice -> exit 2", r.returncode == 2
and "invalid choice" in r.stderr, f"rc={r.returncode} err={r.stderr!r}")
r = subprocess.run([BIN, "list", "--limit", "3"], capture_output=True, text=True,
env=dict(clean_env, HOME=bin_home) if bin_home else clean_env)
test("binary list --limit 3 on the shim's ledger renders 3 entries",
r.returncode == 0 and "Recent ledger entries (3):" in r.stdout,
f"rc={r.returncode} out={r.stdout[:120]!r}")
finally:
import shutil
shutil.rmtree(tmp2, ignore_errors=True)
# --- 6. Port 2 (verification_layer) through the same cdylib ---
print("\n--- 6. Port 2: verification_layer via ep_loader ---")
import json
if _CRATE_DIR not in sys.path:
sys.path.insert(0, _CRATE_DIR)
try:
import ep_loader # type: ignore[import-not-found] (ctypes cdylib wrapper — runtime import by design)
v = ep_loader.verify('{"exit_code": 0}', "exit_code")
out = json.loads(v)
test("ep_verify exit_code=0 -> passed True", out.get("passed") is True)
v = ep_loader.verify('{"exit_code": false}', "exit_code")
out = json.loads(v)
test("ep_verify bool-false renders str() shape (False, not 0)",
out.get("passed") is True and out.get("evidence_refs") == ["exit_code=False"],
f"got {out}")
v = ep_loader.verify('{}', "frobnicate")
out = json.loads(v)
test("ep_verify unknown verifier -> passed False retryable False",
out.get("passed") is False and out.get("retryable") is False
and out.get("escalation_reason") == "unknown_verifier",
f"got {out}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("port-2 ep_verify FFI", False, str(e))
# --- 7. src/rune shim module ---
print("\n--- 7. src/rune verification_layer shim ---")
try:
sys.path.insert(0, os.path.join(_REPO, "src", "rune"))
import verification_layer as vl
r = vl.verify({"exit_code": 1}, "exit_code")
test("shim verify returns VerificationResult dataclass",
type(r).__name__ == "VerificationResult")
test("shim CLI parity: fail case escalation_reason=exit_code_nonzero",
r.to_dict()["escalation_reason"] == "exit_code_nonzero")
test("shim PLUGIN name preserved", vl.PLUGIN["name"] == "verification_layer")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("src/rune verification_layer shim", False, str(e))
# --- 8. Port 3 (evidence_probe): schema gate + budget count ---
# Expectations probed against the serving shim 2026-10-03 (zero-dep split:
# schema gate + count Rust; orchestration Python).
print("\n--- 8. Port 3: evidence_probe shim ---")
import tempfile
try:
import evidence_probe as epr
errs = epr.validate_probe({})
test("validate_probe({}) -> 5 missing-field errors in field order",
errs == ["missing required field: question",
"missing required field: source",
"missing required field: answer",
"missing required field: implication",
"missing required field: status"], f"got {errs}")
good = {"question": "q", "source": "s", "answer": "a",
"implication": "i", "status": "STILL_UNKNOWN"}
test("valid STILL_UNKNOWN probe -> no errors",
epr.validate_probe(good) == [], f"got {epr.validate_probe(good)}")
bad_status = dict(good, status="PASS")
test("status 'PASS' -> membership error with Python repr rendering",
epr.validate_probe(bad_status) ==
["status must be one of ['CONTRADICTED', 'GROUNDED', 'STILL_UNKNOWN'], got 'PASS'"],
f"got {epr.validate_probe(bad_status)}")
int_status = dict(good, status=7)
test("non-string status -> field error AND membership error (Python law)",
epr.validate_probe(int_status) ==
["field status must be a string",
"status must be one of ['CONTRADICTED', 'GROUNDED', 'STILL_UNKNOWN'], got 7"],
f"got {epr.validate_probe(int_status)}")
extra = dict(good, extra=1)
test("extra field -> unexpected-fields error appended last",
epr.validate_probe(extra)[-1] == "unexpected extra fields: ['extra']",
f"got {epr.validate_probe(extra)}")
grounded_blank = dict(good, question="", status="GROUNDED")
test("blank question + GROUNDED -> both errors (citation gate)",
epr.validate_probe(grounded_blank) ==
["field question must be a non-empty string",
"GROUNDED probe requires a concrete source citation"],
f"got {epr.validate_probe(grounded_blank)}")
probe_dir = tempfile.mkdtemp(prefix="eprcanary_")
try:
with open(os.path.join(probe_dir, "probe.jsonl"), "w") as f:
f.write('{"q": 1}\n\n{"r": 2}\n\n\n{"s": 3}\n')
test("count_probes counts non-empty lines only",
epr.count_probes(probe_dir) == 3,
f"got {epr.count_probes(probe_dir)}")
test("count_probes on missing dir -> 0",
epr.count_probes(probe_dir + "/nope") == 0)
test("MAX_PROBES_PER_TASK is 2 (the no-recursion budget)",
epr.MAX_PROBES_PER_TASK == 2)
try:
epr.check_budget(probe_dir)
test("check_budget raises at the budget", False, "no raise")
except epr.ProbeBudgetExceeded as e:
test("check_budget raises ProbeBudgetExceeded at 2 probes",
str(e).startswith("probe budget exhausted (2)"), f"got {e}")
finally:
import shutil
shutil.rmtree(probe_dir, ignore_errors=True)
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("port-3 evidence_probe shim", False, str(e))
# --- 9. Port 4 (behaviour_ledger): doc-envelope state machine ---
# Expectations probed against ep_loader 2026-10-03: doc-in/doc-out,
# {ok, err} envelope, CONFIRMED_STATES-only invalidation, TEST_FAILED-first
# queue ordering.
print("\n--- 9. Port 4: behaviour_ledger via ep_loader ---")
try:
import ep_loader # type: ignore[import-not-found] (already imported in §6; re-import is a cache hit)
doc = {"behaviours": {}}
doc, rec = ep_loader.bl_define(
doc, behaviour_id="alpha", owning_project="proj",
intended_behaviour="ib", origin_intent="oi", pass_criteria="pc",
fail_criteria="fc", procedure="proc", owner_component="comp:alpha",
dependencies=["dep:one"], automated_ref="ar", environment="env",
now="2026-10-03T23:00:00Z")
test("define returns (doc, rec) with normalized id + UNVALIDATED",
rec.get("behaviour_id") == "behaviour:alpha"
and rec.get("state") == "UNVALIDATED",
f"got id={rec.get('behaviour_id')} state={rec.get('state')}")
test("define record carries the 18-key shape",
set(rec.keys()) == {"automated_ref", "behaviour_id", "created_at",
"dependencies", "environment", "evidence",
"fail_criteria", "history", "intended_behaviour",
"invalidations", "limitations", "origin_intent",
"owner_component", "owning_project", "pass_criteria",
"procedure", "state", "updated_at"},
f"got {sorted(rec.keys())}")
doc, _ = ep_loader.bl_record_auto(doc, "alpha", passed=True,
detail="tests green", generation="g1",
now="2026-10-03T23:01:00Z")
rec_a = ep_loader.bl_get(doc, "alpha")
test("record_auto pass -> AUTO_VERIFIED (a CONFIRMED state)",
rec_a is not None and rec_a["state"] == "AUTO_VERIFIED")
doc, _ = ep_loader.bl_define(doc, behaviour_id="beta", owning_project="proj",
intended_behaviour="ib2", now="2026-10-03T23:02:00Z")
doc, _ = ep_loader.bl_record_auto(doc, "beta", passed=False, detail="flaky",
now="2026-10-03T23:03:00Z")
rec_b = ep_loader.bl_get(doc, "beta")
test("record_auto fail -> TEST_FAILED",
rec_b is not None and rec_b["state"] == "TEST_FAILED")
# Selective invalidation (rule 19I): only CONFIRMED_STATES with
# dep/owner overlap reopen; unrelated change preserves.
doc, untouched = ep_loader.bl_invalidate(
doc, changed_entities=["dep:unrelated"], now="2026-10-03T23:04:00Z")
rec_a = ep_loader.bl_get(doc, "alpha")
test("unrelated change invalidates nothing; AUTO_VERIFIED preserved",
untouched == [] and rec_a is not None and rec_a["state"] == "AUTO_VERIFIED")
doc, hit = ep_loader.bl_invalidate(
doc, changed_entities=["dep:one"], generation="g2",
reasons={"behaviour:alpha": "dep one changed"}, now="2026-10-03T23:05:00Z")
test("dependency hit invalidates exactly the confirmed behaviour",
len(hit) == 1 and hit[0]["state"] == "RETEST_REQUIRED",
f"got {[r['state'] for r in hit]}")
inv_entry = hit[0]["invalidations"][0]
test("invalidation entry carries because/generation/was/changed_entities",
inv_entry.get("because") == "dep one changed"
and inv_entry.get("generation") == "g2"
and inv_entry.get("was") == "AUTO_VERIFIED"
and inv_entry.get("changed_entities") == ["dep:one"],
f"got {inv_entry}")
doc, rehit = ep_loader.bl_invalidate(
doc, changed_entities=["dep:one"], now="2026-10-03T23:06:00Z")
rec_a = ep_loader.bl_get(doc, "alpha")
test("re-invalidating a non-confirmed record is a no-op",
rehit == [] and rec_a is not None and rec_a["state"] == "RETEST_REQUIRED")
out = ep_loader.bl_outstanding(doc)
test("outstanding() puts TEST_FAILED first (queue ordering)",
out and out[0]["state"] == "TEST_FAILED",
f"got {[(r['behaviour_id'], r['state']) for r in out]}")
summ = ep_loader.bl_summary(doc)
test("summary() shape {total, states, outstanding}",
summ.get("total") == 2 and isinstance(summ.get("states"), dict)
and "outstanding" in summ, f"got {summ}")
hist = ep_loader.bl_retest_history(doc, "alpha")
test("retest_history(alpha) -> [automated_verification, selective_invalidation]",
[e.get("kind") for e in hist] == ["automated_verification", "selective_invalidation"],
f"got {[e.get('kind') for e in hist]}")
try:
ep_loader.bl_define(doc, behaviour_id="alpha", owning_project="x",
intended_behaviour="y", now="2026-10-03T23:07:00Z")
test("duplicate define raises ValueError", False, "no raise")
except ValueError as e:
test("duplicate define error message byte-exact",
str(e) == "behaviour exists: behaviour:alpha", f"got {e}")
try:
ep_loader.bl_record_auto(doc, "ghost", passed=True, now="2026-10-03T23:08:00Z")
test("unknown behaviour record raises ValueError", False, "no raise")
except ValueError as e:
test("unknown behaviour error message byte-exact",
str(e) == "unknown behaviour 'ghost'", f"got {e}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("port-4 behaviour_ledger FFI", False, str(e))
# --- 10. Port 5 (behavior): decision math through the shim on an
# isolated db (RUNE_HOME + RUNE_MEMORY_DIR pinned BEFORE import —
# memory_dir() honors RUNE_MEMORY_DIR first, and DB_PATH resolves at
# import time). SQLite stays Python; the math is Rust.
print("\n--- 10. Port 5: behavior shim (isolated db) ---")
_iso_home = tempfile.mkdtemp(prefix="alcanary_runehome_")
os.environ["RUNE_HOME"] = _iso_home
os.environ["RUNE_MEMORY_DIR"] = os.path.join(_iso_home, "memory")
try:
import behavior as bh
test("behavior DB resolved under the isolated RUNE_HOME",
bh.DB_PATH.startswith(os.environ["RUNE_HOME"]), f"got {bh.DB_PATH}")
cid = bh.add_correction("say X", "say Y", domain="canary")["added"]
r = bh.reinforce(cid, "success", item_type="correction")
test("correction reinforce success: confidence 0.5 -> 0.6, stays active",
r == {"reinforced": cid, "confidence": 0.6, "state": "active"},
f"got {r}")
r = bh.reinforce(cid, "failure", item_type="correction")
test("correction reinforce failure: 0.6 -> 0.3, archives at the boundary",
r == {"reinforced": cid, "confidence": 0.3, "state": "archived"},
f"got {r}")
hid = bh.add_habit("trigger t", "action a", name="canary habit")["added"]
for _ in range(3):
r = bh.reinforce(hid, "success")
test("habit promotion: candidate -> active at min_activations (3)",
r["state"] == "active" and r["confidence"] == 1,
f"got {r}")
test("habit not-found returns error dict (not raise)",
bh.reinforce(99999, "success") == {"error": "Habit 99999 not found"})
test("correction not-found returns error dict (not raise)",
bh.reinforce(99999, "success", item_type="correction")
== {"error": "Correction 99999 not found"})
stats = bh.behavior_stats()
test("behavior_stats carries the five-key shape",
set(stats.keys()) == {"corrections_active", "corrections_archived",
"habits_active", "habits_candidate",
"total_activations"}, f"got {sorted(stats.keys())}")
test("stats reflect the battery: 1 archived correction, 1 active habit",
stats["corrections_archived"] == 1 and stats["habits_active"] == 1,
f"got {stats}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("port-5 behavior shim", False, str(e))
# --- 11. Port 6 (claim_audit): full pipeline through the shim on an
# isolated journal (MEM_DIR resolves under the pinned RUNE_HOME).
# SYMMETRY GUARD never-fatal: research_lens absence must not crash.
print("\n--- 11. Port 6: claim_audit shim (isolated journal) ---")
try:
import claim_audit as ca
claims = ca.scan("We exceeded the frontier. Results are flawless.")
test("scan flags the comparative + absolute claims",
[(c["type"], c["match"]) for c in claims]
== [("comparative", "we exceeded"), ("absolute", "flawless")],
f"got {[(c['type'], c['match']) for c in claims]}")
st = ca.scan_text("We exceeded anything.")
test("scan_text returns the repaired {claims, clean, degraded, rewritten} surface",
set(st.keys()) == {"claims", "clean", "degraded", "rewritten"},
f"got {sorted(st.keys())}")
test("scan_text degrades the unsupported comparative (clean False)",
st["clean"] is False and len(st["degraded"]) == 1
and st["degraded"][0]["verdict"] == "DEGRADE",
f"clean={st['clean']} degraded={st['degraded']}")
res = ca.audit("We exceeded the frontier. Results are flawless.")
test("audit() -> both claims DEGRADE, clean False",
res["clean"] is False
and [c["verdict"] for c in res["claims"]] == ["DEGRADE", "DEGRADE"],
f"got {[(c['verdict']) for c in res['claims']]} clean={res['clean']}")
test("audit() rewrote the draft (degrade templates applied)",
res["rewritten"] != "We exceeded the frontier. Results are flawless.")
with open(ca.JOURNAL) as f:
rows = f.readlines()
test("journal wrote one row per claim + scan_text row (isolated)",
len(rows) == 3, f"got {len(rows)} rows at {ca.JOURNAL}")
rep = ca.report()
test("report() aggregates through the Rust core: total + by_verdict + by_type",
rep.get("total") == 3
and rep.get("by_verdict", {}).get("DEGRADE") == 3
and rep.get("by_type", {}).get("comparative") == 2
and rep.get("by_type", {}).get("absolute") == 1,
f"got {rep}")
test("verify() on an unsupported plain claim -> ('DEGRADE', 0)",
ca.verify("the module renders a menu when opened") == ("DEGRADE", 0))
test("evidence-term stoplist through the Rust core (>=4 chars, trio dropped)",
ca.ep_loader.ca_evidence_terms(["this", "hash", "canary", "ru"])
== ["this", "hash", "canary"],
f"got {ca.ep_loader.ca_evidence_terms(['this', 'hash', 'canary', 'ru'])}")
except Exception as e: # noqa: BLE001 — canary battery law: a failed probe is a FAIL test, not a crash
test("port-6 claim_audit shim", False, str(e))
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
sys.exit(1 if FAIL else 0)
if __name__ == "__main__":
app = os.path.join(sys.path[0], "canary_audit_ledger.py")
if len(sys.argv) > 1 and sys.argv[1] == "--selfcheck":
print("selfcheck ok")
sys.exit(0)
main()
+4 -6
View File
@@ -19,8 +19,7 @@ def has_idempotent_label_removal(workflow: str) -> bool:
return bool(
re.search(
r"for \(const name of staleManaged\) \{\s*"
r"try \{.*?await github\.request\('DELETE /repos/\{owner\}/\{repo\}/issues/\{issue_number\}/labels/\{id\}', \{.*?"
r"id: currentLabelIds\.get\(name\),\s*"
r"try \{\s*await github\.rest\.issues\.removeLabel\(\{.*?"
r"\}\);\s*\} catch \(error\) \{\s*"
r"if \(error\.status !== 404\) \{\s*throw error;\s*\}\s*"
r"console\.log\(`Managed label already absent: \$\{name\}`\);\s*"
@@ -65,7 +64,7 @@ def main() -> int:
"infrastructure",
"life",
"custodian",
"P0 — critical", "P1 — high", "P2 — medium", "P3 — low",
"priority: high",
"bug",
"feature",
)
@@ -81,8 +80,7 @@ def main() -> int:
)
check(
"stale managed labels are removed",
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label
and "currentLabelIds.get(name)" in auto_label,
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label and "removeLabel" in auto_label,
failures,
)
check(
@@ -233,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.99.7"' in rune_version,
'__version__ = "0.87.0"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
-137
View File
@@ -1,137 +0,0 @@
#!/usr/bin/env python3
"""canary_rune_entry.py — H1 canary for the Go first-call shim.
Same PASS/FAIL discipline as the slice canaries (counter harness, exit 1 on
any FAIL). The canary builds the zero-dep Go module into a temp dir at run
start (works on the desktop and in CI — system Go is world-readable), then
verifies the observed launcher contract:
--version -> "rune <x.y.z>" matching the deployed pyproject version
--python-only -> the exact handoff shape of the deployed bash launcher
(cd <deployed>/src && <venv-python> -m rune <args>)
--help -> usage text, exit 0
unknown flag -> usage on stderr, exit 2
go test -> the module's own unit battery green (subprocess)
Expectations are OBSERVED, not invented: the bash launcher at
~/.local/bin/rune (cd + env contract) and the deployed pyproject version.
Run under ~/.venvs/memory/bin/python3.
"""
import os
import subprocess
import sys
import tempfile
PASS = 0
FAIL = 0
REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
GO_MOD = os.path.join(REPO, "src", "go", "rune-entry")
def test(name, cond, detail=""):
global PASS, FAIL
mark = "✓" if cond else "✗"
print(f" {mark} {name}" + (f" — {detail}" if detail and not cond else ""))
if cond:
PASS += 1
else:
FAIL += 1
def deployed_version():
for cand in ("/mnt/workdrive/rune-code/current/pyproject.toml",
os.path.join(REPO, "pyproject.toml")):
try:
for line in open(cand):
if line.startswith('version = "'):
return line.split('"')[1]
except OSError:
continue
return None
def main():
print("=== RUNE ENTRY (Go first-call) CANARY (H1) ===\n")
# --- 0. Build the artifact (zero-dep; system Go) ---
print("--- 0. Build ---")
tmp = tempfile.mkdtemp(prefix="runeentry_canary_")
bin_path = os.path.join(tmp, "rune")
r = subprocess.run(["go", "build", "-o", bin_path, "."],
capture_output=True, text=True, cwd=GO_MOD)
test("go build succeeds (zero-dep, system toolchain)", r.returncode == 0,
f"rc={r.returncode} err={r.stderr[:200]!r}")
if r.returncode != 0:
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
sys.exit(1)
# --- 1. Version surface ---
print("\n--- 1. Version ---")
want = deployed_version()
r = subprocess.run([bin_path, "--version"], capture_output=True, text=True)
test("--version exits 0", r.returncode == 0, f"rc={r.returncode}")
test("--version prints 'rune <x.y.z>'",
r.stdout.startswith("rune ") and r.stdout.split()[-1].count(".") == 2,
f"got {r.stdout!r}")
test("version matches the deployed pyproject",
want is not None and r.stdout.strip() == f"rune {want}",
f"got {r.stdout.strip()!r} want 'rune {want}'")
# --- 2. Handoff shape (the bash launcher's contract) ---
print("\n--- 2. Handoff shape ---")
r = subprocess.run([bin_path, "--python-only", "status"],
capture_output=True, text=True)
out = r.stdout.strip()
test("--python-only exits 0", r.returncode == 0, f"rc={r.returncode}")
test("handoff cd's into the deployed src revision",
out.startswith("(cd /mnt/workdrive/rune-code/current/src"),
f"got {out!r}")
venv_py = os.path.join(os.environ.get("HOME", ""), ".venvs", "memory", "bin", "python3")
if os.path.isfile(venv_py):
test("handoff execs the memory-venv python with -m rune",
".venvs/memory/bin/python3 -m rune" in out, f"got {out!r}")
else:
test("handoff falls back to PATH python3 when the memory venv is absent "
"(bare HOME — CI sandbox)",
"python3 -m rune" in out, f"got {out!r}")
test("args pass through the handoff",
out.endswith("status)"), f"got {out!r}")
# --- 3. Flag discipline ---
print("\n--- 3. Flag discipline ---")
r = subprocess.run([bin_path, "--help"], capture_output=True, text=True)
test("--help exits 0 with usage text",
r.returncode == 0 and "usage:" in r.stdout,
f"rc={r.returncode} out={r.stdout[:60]!r}")
r = subprocess.run([bin_path, "--frobnicate"], capture_output=True, text=True)
test("unknown flag -> exit 2 + usage on stderr",
r.returncode == 2 and "usage:" in r.stderr,
f"rc={r.returncode} err={r.stderr[:80]!r}")
# --- 4. The module's own unit battery ---
print("\n--- 4. go test ---")
r = subprocess.run(["go", "test", "./..."], capture_output=True, text=True,
cwd=GO_MOD)
test("go test ./... green (the Go unit battery)",
r.returncode == 0 and "ok" in r.stdout,
f"rc={r.returncode} out={r.stdout[-120:]!r}")
# --- 5. go vet ---
print("\n--- 5. go vet ---")
r = subprocess.run(["go", "vet", "./..."], capture_output=True, text=True,
cwd=GO_MOD)
test("go vet clean", r.returncode == 0,
f"rc={r.returncode} err={r.stderr[:120]!r}")
import shutil
shutil.rmtree(tmp, ignore_errors=True)
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
sys.exit(1 if FAIL else 0)
if __name__ == "__main__":
main()
-329
View File
@@ -1,329 +0,0 @@
#!/usr/bin/env python3
"""canary_rune_serve.py — H2 parity battery (T4.3, contract
BUILDER-2026-10-04T08:32:43Z-cli).
The H2 contract: the Go front door (`rune serve`) serves IDENTICAL shapes
to the Python core server for the same canned conversations — byte-level
for metadata (modulo volatile id/created), exact for NDJSON line sequences.
Determinism law: no live model calls. Both servers run against a stub
operator with canned responses (the established _execute_on_model stub
class of seam — the server's shape is the contract under test, not the
cognition behind it).
Sections:
1. Python core server boots (stdlib, ephemeral port)
2. Python shape freeze (models / status / chat / stream / 404)
3. Go serve subcommand builds and answers --help
4. Cross-language parity (Go front → Python core, same canned set)
5. Core-down honesty (502 envelope, both servers agree)
Run: python3 src/rune/canary_rune_serve.py
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO / "src"))
PASS = 0
FAIL = 0
def test(name: str, ok: bool, detail: str = "") -> None:
global PASS, FAIL
if ok:
PASS += 1
print(f" ✓ {name}")
else:
FAIL += 1
print(f" ✗ {name} — {detail}")
# ── stub operator: canned, deterministic ────────────────────────────────────
CANNED_TASK = {
"task_id": "task-canned-001",
"status": "completed",
"model": "stub-model",
"route": "direct",
"user_text": "Canned reply: the shape is the contract.",
"success": True,
"degraded": [],
"iterations_used": 1,
"iteration_budget": 6,
}
class StubOperator:
def list_models(self):
return [{"name": "stub-model-a", "size_gb": 4.1},
{"name": "stub-model-b", "size_gb": 8.2}]
def get_status(self):
return {"identity": "rune", "role": "operator", "current_task": None,
"active_model": "stub-model-a", "tasks_pending": 0,
"tasks_running": 0, "tasks_completed": 3}
def send_task(self, description, model=None, channel="operator"):
out = dict(CANNED_TASK)
out["echo"] = description # proves the request reached the core
return out
def conversation_window(self, channel="operator", n=8):
return [{"role": "user", "content": "earlier", "channel_id": channel}][:n]
def resolve_pending_approval(self, approved):
return {"resolved": bool(approved), "approval_id": "appr-1"}
# ── http helpers ─────────────────────────────────────────────────────────────
def http(method: str, url: str, body: dict | None = None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
raw = resp.read().decode()
return resp.status, raw
except urllib.error.HTTPError as e:
return e.code, e.read().decode()
def normalize(obj):
"""Strip volatile fields (id, created) for parity comparison."""
if isinstance(obj, dict):
return {k: normalize(v) for k, v in obj.items()
if k not in ("id", "created")}
if isinstance(obj, list):
return [normalize(v) for v in obj]
return obj
def wait_ready(url: str, timeout: float = 10.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=1) as r:
if r.status == 200:
return True
except Exception: # noqa: BLE001 — readiness poll: any failure = not yet
time.sleep(0.1)
return False
CANNED_CHAT = {"model": "stub-model-a",
"messages": [{"role": "user", "content": "Say the canned thing."}]}
def fire_shape_set(base: str) -> dict:
"""The canned request battery against one server; returns raw results."""
out = {}
out["models"] = http("GET", f"{base}/v1/models")
out["status"] = http("GET", f"{base}/v1/status")
out["chat"] = http("POST", f"{base}/v1/chat/completions", CANNED_CHAT)
out["stream"] = http("POST", f"{base}/v1/chat/completions",
{**CANNED_CHAT, "stream": True})
out["conversation"] = http("GET", f"{base}/v1/conversation?channel=operator&n=8")
out["approve"] = http("POST", f"{base}/v1/approvals/resolve", {"approved": True})
out["missing"] = http("GET", f"{base}/v1/definitely-not-here")
return out
def check_shapes(prefix: str, results: dict) -> None:
s, raw = results["models"]
body = json.loads(raw) if s == 200 else {}
test(f"{prefix}: /v1/models 200 + list envelope",
s == 200 and body.get("object") == "list"
and [m.get("id") for m in body.get("data", [])] == ["stub-model-a", "stub-model-b"]
and all(m.get("object") == "model" and m.get("owned_by") == "rune"
for m in body.get("data", [])),
f"status={s} body={raw[:160]!r}")
s, raw = results["status"]
body = json.loads(raw) if s == 200 else {}
test(f"{prefix}: /v1/status pass-through keys",
s == 200 and body.get("active_model") == "stub-model-a"
and body.get("tasks_completed") == 3,
f"status={s} body={raw[:160]!r}")
s, raw = results["chat"]
body = json.loads(raw) if s == 200 else {}
choices = body.get("choices", [{}])
test(f"{prefix}: chat.completion envelope + rune metadata",
s == 200 and body.get("object") == "chat.completion"
and choices[0].get("message", {}).get("role") == "assistant"
and "Canned reply" in choices[0].get("message", {}).get("content", "")
and body.get("rune", {}).get("task_id") == "task-canned-001"
and body.get("rune", {}).get("echo") == "Say the canned thing.",
f"status={s} body={raw[:200]!r}")
s, raw = results["stream"]
lines = [l for l in raw.strip().splitlines() if l.strip()]
parsed = []
for l in lines:
try:
parsed.append(json.loads(l))
except json.JSONDecodeError:
parsed.append(None)
test(f"{prefix}: NDJSON stream — chunk(s) then terminal",
s == 200 and len(parsed) >= 2 and None not in parsed
and all(p.get("object") == "chat.completion.chunk" for p in parsed)
and parsed[-1].get("choices", [{}])[0].get("finish_reason") == "stop"
and "Canned reply" in (parsed[0].get("choices", [{}])[0]
.get("delta", {}).get("content", "")),
f"status={s} lines={len(lines)} raw={raw[:200]!r}")
s, raw = results["conversation"]
body = json.loads(raw) if s == 200 else {}
test(f"{prefix}: /v1/conversation window shape",
s == 200 and isinstance(body.get("messages"), list)
and body["messages"][0]["role"] == "user",
f"status={s} body={raw[:160]!r}")
s, raw = results["approve"]
body = json.loads(raw) if s == 200 else {}
test(f"{prefix}: /v1/approvals/resolve round-trip",
s == 200 and body.get("resolved") is True,
f"status={s} body={raw[:160]!r}")
s, raw = results["missing"]
body = json.loads(raw) if raw else {}
test(f"{prefix}: 404 error envelope",
s == 404 and body.get("error", {}).get("code") == "not_found",
f"status={s} body={raw[:160]!r}")
def compare_results(py: dict, go: dict) -> None:
for key in ("models", "status", "chat", "conversation", "approve", "missing"):
(ps, praw), (gs, graw) = py[key], go[key]
same_status = ps == gs
try:
same_body = normalize(json.loads(praw)) == normalize(json.loads(graw))
except json.JSONDecodeError:
same_body = praw == graw
test(f"parity: {key} identical modulo volatile",
same_status and same_body,
f"py=({ps}) {praw[:140]!r} go=({gs}) {graw[:140]!r}")
# NDJSON: exact line-sequence equality modulo volatile
(ps, praw), (gs, graw) = py["stream"], go["stream"]
pl = [normalize(json.loads(l)) for l in praw.strip().splitlines() if l.strip()]
gl = [normalize(json.loads(l)) for l in graw.strip().splitlines() if l.strip()]
test("parity: NDJSON line sequences identical modulo volatile",
ps == gs and pl == gl,
f"py_lines={len(pl)} go_lines={len(gl)}")
def main() -> int:
print("=" * 47)
print("canary_rune_serve — H2 Go front-door parity battery")
print("=" * 47)
# ── 1. Python core server boots ───────────────────────────────────────
print("\n--- 1. Python core server ---")
try:
from rune import core_server
except ImportError as e:
test("core_server imports", False, str(e))
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
return 1
test("core_server imports", True)
server = core_server.make_server("127.0.0.1", 0, StubOperator())
port = server.server_address[1]
import threading
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
py_base = f"http://127.0.0.1:{port}"
test("core server answers on an ephemeral port",
wait_ready(f"{py_base}/v1/status"), py_base)
# ── 2. Python shape freeze ────────────────────────────────────────────
print("\n--- 2. Python shape freeze ---")
py_results = fire_shape_set(py_base)
check_shapes("python", py_results)
# ── 3. Go serve subcommand ────────────────────────────────────────────
print("\n--- 3. Go serve subcommand ---")
go_dir = REPO / "src" / "go" / "rune-entry"
env = dict(os.environ)
env["GOFLAGS"] = env.get("GOFLAGS", "") + " -mod=mod"
# Per-run output path: a fixed /tmp name collides cross-user (nick owns
# the local build; the runner cannot overwrite it — run 18322 EACCES).
go_bin = f"/tmp/rune-serve-canary-{os.getpid()}"
r = subprocess.run(["go", "build", "-o", go_bin, "."],
cwd=go_dir, env=env, capture_output=True, text=True, timeout=300)
test("go build exits 0", r.returncode == 0,
f"rc={r.returncode} err={r.stderr[-200:]!r}")
if r.returncode != 0:
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
return 1
r = subprocess.run([go_bin, "serve", "--help"],
capture_output=True, text=True, timeout=30)
test("rune serve --help exits 0 with usage",
r.returncode == 0 and "core" in (r.stdout + r.stderr).lower(),
f"rc={r.returncode} out={(r.stdout + r.stderr)[:160]!r}")
# ── 4. Cross-language parity ──────────────────────────────────────────
print("\n--- 4. Parity: Go front → Python core ---")
go_port = port + 1
proc = subprocess.Popen(
[go_bin, "serve", "--port", str(go_port),
"--core", py_base],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
go_base = f"http://127.0.0.1:{go_port}"
try:
ready = wait_ready(f"{go_base}/v1/status")
test("go front answers on its port", ready, go_base)
if ready:
go_results = fire_shape_set(go_base)
check_shapes("go", go_results)
compare_results(py_results, go_results)
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
# ── 5. Core-down honesty ──────────────────────────────────────────────
print("\n--- 5. Core-down honesty ---")
proc = subprocess.Popen(
[go_bin, "serve", "--port", str(go_port),
"--core", "http://127.0.0.1:1"], # nothing listening
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
try:
time.sleep(0.5) # listener comes up even though the core never will
s, raw = http("POST", f"{go_base}/v1/chat/completions", CANNED_CHAT)
body = json.loads(raw) if raw else {}
test("go 502 envelope when the core is down",
s == 502 and body.get("error", {}).get("code") == "core_unavailable",
f"status={s} body={raw[:160]!r}")
s2, raw2 = http("POST", f"{py_base}/v1/chat/completions", CANNED_CHAT)
test("python core still healthy after go core-down probe",
s2 == 200, f"status={s2}")
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
server.shutdown()
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
return 1 if FAIL else 0
if __name__ == "__main__":
sys.exit(main())
-192
View File
@@ -1,192 +0,0 @@
#!/usr/bin/env python3
"""canary_spine_meta.py — meta-scaffold canary (H-meta).
Contract BUILDER-2026-10-04T06:24:56Z-cli (Nick directive 2026-10-04):
every part of rune must exist as a modeled entity in rune's spine — the
spine is the meta-scaffold. This canary proves the projection models the
surface classes the closures have been honestly reporting as unmodeled:
1. rust-crate entities for both crates (evidence_provenance, query_memory)
2. go-surface entity for src/go/rune-entry
3. module entities for canary_audit_ledger / canary_rune_entry /
canary_evidence_provenance_rust (with obligated_canary edges)
4. workflow entities for every .gitea/workflows/*.yml
5. config entities for the tranche config files
6. determinism: two consecutive builds differ only in generated_at
7. open-world law: every pre-existing entity survives the rebuild
PASS/FAIL counter harness, exit 1 on any FAIL. Expectations observed from
the spine's own projection rules (src/rune/spine.py) and the umbrella
manifest — never invented.
Run under ~/.venvs/memory/bin/python3.
"""
import json
import os
import subprocess
import sys
PASS = 0
FAIL = 0
REPO = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
SPINE_STORE = os.path.join(REPO, "store", "spine", "current.json")
def test(name, cond, detail=""):
global PASS, FAIL
mark = "✓" if cond else "✗"
print(f" {mark} {name}" + (f" — {detail}" if detail and not cond else ""))
if cond:
PASS += 1
else:
FAIL += 1
def build_spine():
r = subprocess.run(
[sys.executable, "-m", "rune.spine", "build"],
capture_output=True, text=True, timeout=180,
env={"PYTHONPATH": os.path.join(REPO, "src"), "PATH": os.environ.get("PATH", ""),
"HOME": os.environ.get("HOME", "")},
cwd=REPO)
return r
def load_spine():
return json.load(open(SPINE_STORE))
def main():
print("=== SPINE META-SCAFFOLD CANARY (H-meta) ===\n")
pre = load_spine() if os.path.isfile(SPINE_STORE) else {"entities": []}
pre_ids = {e["id"] for e in pre["entities"]}
# --- 0. Build ---
print("--- 0. Build ---")
r = build_spine()
test("spine build exits 0", r.returncode == 0, f"rc={r.returncode} err={r.stderr[-200:]!r}")
if r.returncode != 0:
print(f"\nRESULTS: {PASS} pass, {FAIL} fail")
sys.exit(1)
doc = load_spine()
ents = {e["id"]: e for e in doc["entities"]}
rels = doc.get("relationships", [])
# --- 1. Rust crates ---
print("\n--- 1. Rust crates ---")
for crate in ("evidence_provenance", "query_memory"):
eid = f"rust-crate:{crate}"
test(f"{eid} exists with kind rust-crate",
eid in ents and ents[eid]["kind"] == "rust-crate",
f"ids matching: {[i for i in ents if crate in i][:4]}")
# --- 2. Go surface ---
print("\n--- 2. Go surface ---")
test("go-surface:rune-entry exists with kind go-surface",
"go-surface:rune-entry" in ents
and ents["go-surface:rune-entry"]["kind"] == "go-surface",
f"ids matching: {[i for i in ents if 'rune-entry' in i][:4]}")
# --- 3. Missing canaries modeled ---
print("\n--- 3. Canaries ---")
for canary in ("canary_audit_ledger", "canary_rune_entry",
"canary_evidence_provenance_rust"):
mid = f"module:{canary}"
test(f"{mid} entity exists", mid in ents,
f"present: {mid in ents}")
edge = any(r["type"] == "obligated_canary" and r["to"] == mid for r in rels)
test(f"{mid} has an obligated_canary edge from its umbrella", edge)
# --- 4. Workflows ---
print("\n--- 4. Workflows ---")
import glob
wf_files = sorted(os.path.basename(p)
for p in glob.glob(os.path.join(REPO, ".gitea", "workflows", "*.yml")))
test("workflow inventory is non-empty", len(wf_files) >= 10, f"found {len(wf_files)}")
missing = [w for w in wf_files if f"workflow:{w}" not in ents]
test("every workflow file has a workflow entity", not missing,
f"missing: {missing[:4]}")
# --- 5. Configs ---
print("\n--- 5. Configs ---")
for cfg in ("go-version.txt", "lint-baseline.json", "pyright-baseline.json",
".coveragerc"):
cid = f"config:{cfg}"
test(f"{cid} entity exists with kind config",
cid in ents and ents[cid]["kind"] == "config",
f"present: {cid in ents}")
# --- 6. Determinism (steady-state pair) ---
# The honest determinism contract: once the store already carries the
# projector's facts, two consecutive builds with identical inputs must
# differ only in generated_at. A MIGRATION pair (store predating the
# projector vs store carrying it) is not a determinism failure — the
# first build after any projector change legitimately rewrites state.
# The warm-up build makes this check judge steady-state, not migration.
print("\n--- 6. Determinism (steady-state) ---")
warm = build_spine()
test("warm-up build exits 0", warm.returncode == 0,
f"rc={warm.returncode} err={warm.stderr[-160:]!r}")
r = build_spine()
first = load_spine()
r = build_spine()
second = load_spine()
def strip_ts(d):
"""Deep-strip time-varying fields: determinism is a STRUCTURAL
contract (same entities/relationships/observation content), not a
clock contract. observed_at/deployed_at/generated_at tick every
build by design — comparing them made this check pass or fail on
whether the two builds crossed a one-second boundary."""
TIME_KEYS = {"generated_at", "observed_at", "deployed_at", "updated_at",
"last_success"}
def walk(x):
if isinstance(x, dict):
return {k: walk(v) for k, v in x.items() if k not in TIME_KEYS}
if isinstance(x, list):
return [walk(v) for v in x]
return x
def normalize(d):
d = walk(d)
# build() appends one deployed_identity observation per pass and
# the store dedups on the FULL observation (timestamp included),
# so the observations list grows by one identical-modulo-timestamp
# entry per build. Structural content = the deduped set.
seen, obs = set(), []
for o in d.get("observations", []):
key = json.dumps(o, sort_keys=True)
if key not in seen:
seen.add(key)
obs.append(o)
if "observations" in d:
d["observations"] = obs
return d
return normalize(json.loads(json.dumps(d)))
fa, fb = strip_ts(first), strip_ts(second)
if fa != fb:
# Diagnostic: write the stripped pair so the divergence is inspectable
# instead of a bare boolean verdict.
with open("/tmp/spine_meta_determinism_first.json", "w") as f:
json.dump(fa, f, indent=2, sort_keys=True)
with open("/tmp/spine_meta_determinism_second.json", "w") as f:
json.dump(fb, f, indent=2, sort_keys=True)
test("two consecutive steady-state builds are structurally identical "
"(timestamps free to tick)",
fa == fb,
"rebuild not structurally stable "
"(stripped pair dumped to /tmp/spine_meta_determinism_*.json)")
# --- 7. Open-world law ---
print("\n--- 7. Open-world law ---")
lost = pre_ids - {e["id"] for e in doc["entities"]}
test("every pre-existing entity survives the rebuild", not lost,
f"lost: {sorted(lost)[:4]}")
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
sys.exit(1 if FAIL else 0)
if __name__ == "__main__":
main()
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env python3
"""canary_tui_transport.py — H4 slice 1 battery (T4.5, contract
BUILDER-2026-10-04T10:32:00Z-cli).
The TUI transport seam contract: InProcessTransport and V1Transport answer
the TUI's four-method call set EQUIVALENTLY against the same stub core —
V1 through the real HTTP stack (core_server), InProcess against the stub
operator directly. And the default is inprocess: the seam changes nothing
until RUNE_TUI_TRANSPORT=v1 opts in.
Sections:
1. /v1 surface carries the TUI endpoints (conversation + approvals)
2. transport factory defaults to inprocess (zero behavior change)
3. four-method equivalence (send_task, get_status,
get_conversation_window, resolve_pending_approval)
4. approval boolean round-trip both ways (True and False)
5. V1 transport against a dead core degrades loudly, never silently
Run: python3 src/rune/canary_tui_transport.py
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import threading
import urllib.error
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO / "src"))
PASS = 0
FAIL = 0
def test(name: str, ok: bool, detail: str = "") -> None:
global PASS, FAIL
if ok:
PASS += 1
print(f" ✓ {name}")
else:
FAIL += 1
print(f" ✗ {name} — {detail}")
class StubOperator:
"""The TUI call set, canned."""
def get_status(self):
return {"identity": "rune", "role": "operator", "current_task": None,
"active_model": "stub-model", "tasks_pending": 1,
"tasks_running": 0, "tasks_completed": 2}
def send_task(self, description, model=None, channel="operator"):
return {"task_id": "task-stub-1", "status": "completed",
"user_text": f"stub answer to: {description}",
"model": model or "stub-model", "success": True,
"degraded": []}
def conversation_window(self, channel="operator", n=8):
return [{"role": "user", "content": "earlier question",
"channel_id": channel},
{"role": "assistant", "content": "earlier answer",
"channel_id": channel}][:n]
def resolve_pending_approval(self, approved):
return {"resolved": bool(approved), "approval_id": "appr-stub-1"}
def http(method: str, url: str, body: dict | None = None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status, resp.read().decode()
except urllib.error.HTTPError as e:
return e.code, e.read().decode()
def main() -> int:
print("=" * 47)
print("canary_tui_transport — H4 slice 1 transport seam battery")
print("=" * 47)
from rune import core_server # noqa: E402
server = core_server.make_server("127.0.0.1", 0, StubOperator())
port = server.server_address[1]
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
base = f"http://127.0.0.1:{port}"
# ── 1. /v1 surface carries the TUI endpoints ──────────────────────────
print("\n--- 1. TUI endpoints on the /v1 surface ---")
s, raw = http("GET", f"{base}/v1/conversation?channel=operator&n=8")
body = json.loads(raw) if s == 200 else {}
test("GET /v1/conversation returns the window",
s == 200 and isinstance(body.get("messages"), list)
and body["messages"][0]["role"] == "user",
f"status={s} body={raw[:140]!r}")
s, raw = http("POST", f"{base}/v1/approvals/resolve", {"approved": True})
body = json.loads(raw) if s == 200 else {}
test("POST /v1/approvals/resolve round-trips True",
s == 200 and body.get("resolved") is True,
f"status={s} body={raw[:140]!r}")
# ── 2. factory default ────────────────────────────────────────────────
print("\n--- 2. Factory default ---")
from rune import tui_transport # noqa: E402
os.environ.pop("RUNE_TUI_TRANSPORT", None)
default_transport = tui_transport.get_transport()
test("default transport is in-process (zero behavior change)",
isinstance(default_transport, tui_transport.InProcessTransport),
type(default_transport).__name__)
# ── 3. four-method equivalence ────────────────────────────────────────
print("\n--- 3. Four-method equivalence ---")
inproc = tui_transport.InProcessTransport(operator=StubOperator())
v1 = tui_transport.V1Transport(base)
py_status = inproc.get_status()
v1_status = v1.get_status()
test("get_status equivalent", py_status == v1_status,
f"inproc={py_status!r} v1={v1_status!r}")
py_task = inproc.send_task("the canned question", model="stub-model",
channel="operator")
v1_task = v1.send_task("the canned question", model="stub-model",
channel="operator")
test("send_task equivalent (rune block carried)",
py_task == v1_task,
f"inproc={py_task!r}\n v1={v1_task!r}")
py_win = inproc.get_conversation_window("operator", n=8)
v1_win = v1.get_conversation_window("operator", n=8)
test("get_conversation_window equivalent (list shape preserved)",
py_win == v1_win and isinstance(v1_win, list),
f"inproc={py_win!r} v1={v1_win!r}")
# ── 4. approval boolean round-trips ───────────────────────────────────
print("\n--- 4. Approval round-trips ---")
for value in (True, False):
py_r = inproc.resolve_pending_approval(value)
v1_r = v1.resolve_pending_approval(value)
test(f"resolve_pending_approval({value}) equivalent",
py_r == v1_r and v1_r.get("resolved") is value,
f"inproc={py_r!r} v1={v1_r!r}")
# ── 5. dead core degrades loudly ──────────────────────────────────────
print("\n--- 5. Dead core honesty ---")
dead = tui_transport.V1Transport("http://127.0.0.1:1")
try:
dead.get_status()
test("dead core raises (never a silent empty status)", False,
"returned without error")
except Exception as e: # noqa: BLE001 — the battery asserts the class, not the subtype
test("dead core raises (never a silent empty status)", True,
type(e).__name__)
server.shutdown()
print(f"\n{'=' * 47}")
print(f"RESULTS: {PASS} pass, {FAIL} fail")
return 1 if FAIL else 0
if __name__ == "__main__":
sys.exit(main())
+1 -26
View File
@@ -586,33 +586,8 @@ def build_cards():
return cards
def _cs_core():
"""The Rust capability_scoring core when available (strangler: Python
fallback is load-bearing). RUNE_CAPABILITY_CORE=python pins the fallback."""
if os.environ.get("RUNE_CAPABILITY_CORE", "auto") == "python":
return None
try:
from rune import cs_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return cs_loader if cs_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
def score_card(card, query):
"""Score a capability card against a query. Deterministic keyword matching.
H3 slice 4: delegates to the Rust core when built (Python fallback)."""
core = _cs_core()
if core is not None:
return core.score_card(card, query)
return _py_score_card(card, query)
def _py_score_card(card, query):
"""Python core of score_card (parity oracle for the Rust core)."""
"""Score a capability card against a query. Deterministic keyword matching."""
query_low = query.lower()
query_words = set(re.findall(r"\w+", query_low))
+1 -27
View File
@@ -211,36 +211,10 @@ def discover_candidates(doc: dict, req: CapabilityRequirement) -> list[dict]:
# ── fit evaluation ─────────────────────────────────────────────────────────
def _cs_core():
"""The Rust capability_scoring core when available (strangler: Python
fallback is load-bearing). RUNE_CAPABILITY_CORE=python pins the fallback."""
import os
if os.environ.get("RUNE_CAPABILITY_CORE", "auto") == "python":
return None
try:
from rune import cs_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return cs_loader if cs_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
def evaluate_fit(req: CapabilityRequirement, candidate: dict) -> dict:
"""DETERMINISTIC fit of one candidate against a requirement.
fit: EXACT | PARTIAL | INCOMPATIBLE | UNKNOWN.
Names are ignored; contracts + environment + health decide.
H3 slice 4: delegates to the Rust core when built (Python fallback)."""
core = _cs_core()
if core is not None:
return core.evaluate_fit(req.contracts, candidate)
return _py_evaluate_fit(req, candidate)
def _py_evaluate_fit(req: CapabilityRequirement, candidate: dict) -> dict:
"""Python core of evaluate_fit (parity oracle for the Rust core)."""
Names are ignored; contracts + environment + health decide."""
missing_contracts = sorted(set(req.contracts) - set(candidate["provides"]))
covered = len(req.contracts) - len(missing_contracts)
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust cognition_core (R2 W3.1).
Contract BUILDER-2026-10-05T07:19:27Z-cli. Pure deterministic kernels;
the env gate arrives as a string input (no-clock law, no env access in
Rust). Override with CC_RUST_LIB (a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "cognition_core"
_LIB_PATH = Path(
os.environ.get("CC_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcognition_core.so"))
_lib = None
def core():
"""The shared kernel probe (strangler law): the delegation entry the
kernel-consuming modules use. RUNE_COGNITION_CORE=python pins the
fallback — THIS is where the pin is honored, so every consumer gets it
(a module-local probe that ignores the pin silently defeats it)."""
import os
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
return None
try:
return _load()
except Exception: # noqa: BLE001 — build/loader failures mean fallback
return None
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise FileNotFoundError(f"cognition_core not built: {_LIB_PATH}")
lib = ctypes.CDLL(str(_LIB_PATH))
lib.cc_free_string.argtypes = [ctypes.c_void_p]
lib.cc_free_string.restype = None
for sym in ("cc_default_estimator", "cc_default_budget",
"cc_default_memory_budget", "cc_memory_disabled",
"cc_persona_lex", "cc_persona_overlap", "cc_persona_decide",
"cc_drift_check", "cc_identity_prompt", "cc_growth_near_dup",
"cc_growth_profile", "cc_pins_recall", "cc_tier_get",
"cc_tier_promote", "cc_tier_demote", "cc_witness_extract_topic",
"cc_witness_signals", "cc_safety_get_params",
"cc_safety_update_pattern", "cc_abstraction_extract_facts"):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_char_p]
fn.restype = ctypes.c_void_p
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
if not ptr:
raise RuntimeError(f"{sym} returned null")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.cc_free_string(ptr)
env = json.loads(text)
if not env.get("ok"):
raise RuntimeError(f"{sym} core error: {env.get('err')}")
return env["value"]
def default_estimator(text: str) -> int:
return int(_call("cc_default_estimator", {"text": text}))
def default_budget(window: int) -> int:
return int(_call("cc_default_budget", {"window": window}))
def default_memory_budget(window) -> int:
return int(_call("cc_default_memory_budget", {"window": window}))
def memory_disabled() -> bool:
import os
return bool(_call("cc_memory_disabled",
{"value": os.environ.get("RUNE_MEMORY_OFF")}))
def persona_lex(text) -> list:
return _call("cc_persona_lex", {"text": text})
def persona_overlap(a: list, b: list) -> float:
return float(_call("cc_persona_overlap", {"a": a, "b": b}))
def persona_decide(text: str, rules: list, permissive: bool,
sem_scores: list | None = None) -> dict:
return _call("cc_persona_decide", {
"text": text, "rules": rules, "permissive": permissive,
"sem_scores": sem_scores})
def drift_check(text: str, patterns: list) -> list:
return _call("cc_drift_check", {"text": text, "patterns": patterns})
def identity_prompt(pronouns: dict, self_awareness: dict, anchors: dict,
embodiment: list) -> str:
return _call("cc_identity_prompt", {
"pronouns": pronouns, "self_awareness": self_awareness,
"anchors": anchors, "embodiment": embodiment})
def growth_near_dup(a: str, b: str) -> bool:
return bool(_call("cc_growth_near_dup", {"a": a, "b": b}))
def growth_profile(state: dict, order: list, entry_chars: int,
max_tokens: int) -> list:
return _call("cc_growth_profile", {
"state": state, "order": order, "entry_chars": entry_chars,
"max_tokens": max_tokens})
def pins_recall(pins: list, term: str, limit: int = 4) -> list:
return _call("cc_pins_recall", {"pins": pins, "term": term, "limit": limit})
def tier_get(entry: dict, now: str, hot_days: float = 7.0,
warm_days: float = 30.0) -> str:
return _call("cc_tier_get", {"entry": entry, "now": now,
"hot_days": hot_days, "warm_days": warm_days})
def tier_promote(entry: dict, now: str, hot_days: float = 7.0,
warm_days: float = 30.0) -> str:
return _call("cc_tier_promote", {"entry": entry, "now": now,
"hot_days": hot_days, "warm_days": warm_days})
def tier_demote(entry: dict, now: str, hot_days: float = 7.0,
warm_days: float = 30.0) -> str:
return _call("cc_tier_demote", {"entry": entry, "now": now,
"hot_days": hot_days, "warm_days": warm_days})
def witness_extract_topic(text: str, stopwords: list) -> list:
return _call("cc_witness_extract_topic",
{"text": text, "stopwords": stopwords})
def witness_signals(message: str, tables: dict, tool_error: str = "",
task_status: str = "", recent_topics: list | None = None) -> dict:
return _call("cc_witness_signals", {
"message": message, "tables": tables, "tool_error": tool_error,
"task_status": task_status, "recent_topics": recent_topics or []})
def safety_get_params(rows: list) -> dict:
return _call("cc_safety_get_params", {"rows": rows})
def safety_update_pattern(rows: list, pattern: str) -> list:
return _call("cc_safety_update_pattern", {"rows": rows, "pattern": pattern})
def abstraction_extract_facts(text: str, stopwords: list, min_len: int = 10):
return _call("cc_abstraction_extract_facts", {
"text": text, "stopwords": stopwords, "min_len": min_len})
+28 -81
View File
@@ -34,38 +34,16 @@ Usage:
claim_audit.py verify "<claim>" # ledger lookup
claim_audit.py audit "<draft text>" # full pipeline, JSON
claim_audit.py report # claimed-vs-verified stats
Phase G strangler slice 2 (port 6 — FINAL): the decision skeleton (verify
verdict tree, report aggregation, clean fold, evidence-term stoplist) is
served IN-PROCESS by the Rust `evidence_provenance` cdylib through
ep_loader.py — parity 17/17 proven vs this module's Python vintage. The
regex engine stays Python (zero-dep crate law): PATTERNS scan + sentence
extraction + degrade templates + reframe are regex machinery. I/O (journal)
stays Python. Timestamps pinned by the shim. API/CLI/PLUGIN unchanged.
VINTAGE MERGE (disclosed on #2449): the deployed scripts vintage carried the
SYMMETRY GUARD (research_lens.symmetry_check integration, 2026-08-15) which
src/rune's vintage lacked — preserved here (feature-preserving merge; the
guard is load-bearing behavior). Also REPAIRED: the TUI's `scan_text()`
consumer (rune_tui.py:3752, expecting {degraded: [...], claims: [...]}) had
NO implementation in EITHER vintage (silent try/except death since the TUI
call landed) — scan_text() is provided now as a first-class surface.
"""
import argparse
import json
import logging
import os
import re
import sys
from datetime import datetime, timezone
# ── Rust core wiring (slice-2 port-6 shim pattern) ──────────────────────────
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
if _CRATE_DIR not in sys.path:
sys.path.insert(0, _CRATE_DIR)
import ep_loader # noqa: E402 (loads target/release cdylib)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import memory_env
MEM_DIR = memory_env.memory_dir()
@@ -76,7 +54,7 @@ _SD = os.path.dirname(os.path.abspath(__file__))
if _SD not in sys.path:
sys.path.insert(0, _SD)
logger = __import__("logging").getLogger(__name__)
logger = logging.getLogger(__name__)
# Lazy import: warm_neuron_store is imported inside functions that use it
# to avoid crashing the entire module on import if the dependency is missing.
@@ -97,7 +75,10 @@ def _get_warm_neuron_store():
# ---------------------------------------------------------------------------
# Strong-claim patterns (unchanged — regex machinery stays Python).
# Strong-claim patterns. Grouped by type so we can degrade per-type on
# repeated failure. `comparative` is the highest-priority (the "we exceed X"
# class). Regexes are deliberately conservative: they flag CLAIM SHAPES, not
# innocent uses of words.
# ---------------------------------------------------------------------------
PATTERNS = [
{
@@ -203,22 +184,6 @@ def scan(text):
return claims
def scan_text(text):
"""TUI consumer surface (rune_tui.py:3752): run the pipeline over a text
snippet and return {degraded: [...], claims: [...], clean: bool}.
REPAIRED: neither vintage implemented this — the TUI call died silently
in try/except since it landed. Semantics: audit the text, list degraded
claims (verdict != PASS), surface them to the TUI.
"""
if not text:
return {"degraded": [], "claims": [], "clean": True}
res = audit(text)
degraded = [r for r in res.get("claims", []) if r.get("verdict") != "PASS"]
return {"degraded": degraded, "claims": res.get("claims", []),
"clean": res.get("clean", True), "rewritten": res.get("rewritten", "")}
# ---------------------------------------------------------------------------
# 3. Evidence-ledger lookup
# ---------------------------------------------------------------------------
@@ -237,7 +202,7 @@ def _graph_has(term):
return row is not None
finally:
db.close()
except Exception: # noqa: BLE001 — ledger lookup degrades to no-evidence by design; a broken graph must not crash claim verification
except Exception:
return False
@@ -249,7 +214,7 @@ def _warm_has(term):
if wns is None:
return False
return wns.neuron_has(term)
except Exception: # noqa: BLE001 — warm-block lookup degrades to no-evidence by design; a broken store must not crash claim verification
except Exception:
return False
@@ -266,9 +231,6 @@ def verify(claim):
it. Those claims only PASS if they carry a measurable anchor (a number, a
probe run, a benchmark), because that's the kind of claim that needs both
a baseline and a measurement (per the claim-audit research).
Decision tree served by the Rust core (parity-proven); the regex flag
derivation + ledger lookups stay Python.
"""
low = claim.lower()
is_comparative = bool(
@@ -286,16 +248,19 @@ def verify(claim):
low,
)
)
# Candidate ledger terms: filtered by the Rust stoplist/length rule.
# Comparative claims need a MEASURABLE anchor, not just a mention.
if is_comparative and not is_quantified:
return "DEGRADE", 0
# Extract candidate ledger terms: the subject noun phrase.
terms = re.findall(r"\b([A-Za-z][A-Za-z ]{3,30})\b", claim)
terms = ep_loader.ca_evidence_terms(terms)
hits = 0
for t in terms:
if len(t) < 4 or t.lower() in ("this is", "that is", "for the"):
continue
if _graph_has(t) or _warm_has(t):
hits += 1
gate = ep_loader.ca_verify_gate(is_comparative, is_quantified, hits, True)
if gate["verdict"] == "PASS":
return "PASS", gate["evidence_hits"]
if hits >= 1:
return "PASS", hits
if re.search(r"\b(exceed|surpass|beat|beyond)\b", low):
return "DEGRADE", 0 # comparative with no ledger support = the exact case
return "DEGRADE", 0
@@ -357,7 +322,11 @@ def report():
return {"total": 0, "by_verdict": {}, "by_type": {}}
with open(JOURNAL) as f:
rows = [json.loads(l) for l in f if l.strip()]
return ep_loader.ca_report_aggregate(rows)
by_verdict, by_type = {}, {}
for r in rows:
by_verdict[r.get("verdict", "?")] = by_verdict.get(r.get("verdict", "?"), 0) + 1
by_type[r.get("type", "?")] = by_type.get(r.get("type", "?"), 0) + 1
return {"total": len(rows), "by_verdict": by_verdict, "by_type": by_type}
# ---------------------------------------------------------------------------
@@ -367,19 +336,6 @@ def audit(draft):
"""Run the full active gate over a draft. Returns verdicts + rewritten text."""
claims = scan(draft)
if not claims:
# SYMMETRY GUARD (deployed-vintage feature, preserved in the merge):
# even with no strong claim, check the draft for asymmetric-skepticism
# markers so the baloney detector cannot be applied one-sidedly.
try:
sys.path.insert(0, _SD)
from research_lens import symmetry_check
sym = symmetry_check(draft)
if sym.get("verdict") == "ASYMMETRY-FLAG":
return {"claims": [], "rewritten": draft, "clean": False,
"symmetry": sym}
except Exception: # noqa: BLE001 — guard is never-fatal by design (deployed-vintage discipline); audit output degrades to no-symmetry, not silence
logger.debug("symmetry_check unavailable/failed — continuing without symmetry", exc_info=True)
pass
return {"claims": [], "rewritten": draft, "clean": True}
rewritten = draft
results = []
@@ -401,23 +357,9 @@ def audit(draft):
"rewrite": new_text,
"note": note,
}
# SYMMETRY GUARD per-claim (deployed-vintage feature, preserved):
result["symmetry"] = _symmetry_safe(c["claim"])
results.append(result)
journal({"type": c["type"], "claim": c["claim"][:160], "verdict": verdict, "evidence_hits": hits})
return {"claims": results, "rewritten": rewritten,
"clean": ep_loader.ca_audit_clean(results)}
def _symmetry_safe(text):
"""symmetry_check wrapper — never fatal (deployed-vintage discipline)."""
try:
from research_lens import symmetry_check
return symmetry_check(text)
except Exception: # noqa: BLE001 — wrapper is never-fatal by design; a symmetry outage must not kill claim auditing
logger.debug("symmetry_check unavailable/failed — no symmetry annotation", exc_info=True)
pass
return {}
return {"claims": results, "rewritten": rewritten, "clean": all(r["verdict"] == "PASS" for r in results)}
def handle_hook(event, data=None):
@@ -470,6 +412,11 @@ if __name__ == "__main__":
# ---------------------------------------------------------------------------
# Plugin declaration — self-registers with the plugin registry.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Hook handler — called by the microkernel when an event fires.
# ---------------------------------------------------------------------------
PLUGIN = {
"name": "claim_audit",
"version": "1.0.0",
@@ -480,4 +427,4 @@ PLUGIN = {
"provides": ["claim_audit"],
"priority": 40,
"auto_activate": True,
}
}
-95
View File
@@ -171,98 +171,3 @@ def _accept(command_id: str, construction_id: str, correlation_id: str,
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime()),
"idempotent_replay": False}
def dispatch(command_type: str, *, construction_id: str,
correlation_id: str = "", command_id: str = "",
payload: dict | None = None,
store_path: str = "store/scaffold") -> dict:
"""The SERVING layer (T5.15 slice 2): validate the command against the
canonical store, execute the accepted intent through the Construction
Interface, and return the interface's envelope with the command
provenance attached.
Single entry point for `rune scaffold <command>` (the Go proxy) and
any future transport. Idempotency: a repeated command_id replays the
recorded outcome without re-executing.
Refusal still returns success=False envelope? NO — a refused command
is a VALID protocol outcome: the envelope carries accepted=False and
success=True (the dispatch itself worked; the lifecycle refused the
intent). Execution failures carry success=False.
"""
iface_path = None
if store_path and store_path != "store/scaffold":
iface_path = store_path
try:
from rune import construction_interface as ci
# store_path carries the serving layer's "path|project_id" contract
# (a bare path = last segment is the project id, matching how the
# Go proxy and the interface's own default name it).
if iface_path:
path_part, _, proj_part = iface_path.partition("|")
proj = proj_part or path_part.rstrip("/").split("/")[-1]
iface = ci.ConstructionInterface(builder="substrate",
project_store=path_part,
project_id=proj)
else:
iface = ci.ConstructionInterface(builder="substrate")
except Exception as exc: # noqa: BLE001 — serving failure is an envelope, not a crash
return {"accepted": False, "executed": False,
"success": False, "error": f"serving unavailable: {exc}"[:200],
"command_type": command_type,
"construction_id": construction_id}
intent = command(command_type, construction_id=construction_id,
correlation_id=correlation_id, command_id=command_id,
payload=payload, scaffold=iface.scaffold)
if intent.get("idempotent_replay"):
# Repeated delivery: return the recorded outcome WITHOUT executing
# again (the ledger's whole point). The first execution's envelope
# is not re-derivable here, so the replay carries the intent and
# the flag — the caller knows the effect already happened.
return {"accepted": True, "executed": False,
"success": True, "idempotent_replay": True,
"op": intent.get("op"),
"command_type": command_type,
"construction_id": construction_id,
"correlation_id": intent.get("correlation_id", ""),
"command_id": intent.get("command_id", "")}
if not intent.get("accepted"):
return {"accepted": False, "executed": False, "success": True,
"reason": intent.get("reason"),
"command_type": command_type,
"construction_id": construction_id,
"correlation_id": intent.get("correlation_id", ""),
"command_id": intent.get("command_id", ""),
"idempotent_replay": intent.get("idempotent_replay", False)}
op = intent["op"]
args = intent["args"]
ops = {
"resume": lambda: iface.resume(args["contract_id"]),
"transition": lambda: iface.transition(
args["contract_id"], args["phase"],
evidence_ref=f"command:{intent['command_id']}"
+ (f" approved_by:{args['approved_by']}" if args.get("approved_by") else "")),
"amend_scope": lambda: iface.amend_scope(
args["contract_id"], args["add_paths"],
reason=args.get("reason", "substrate context"),
authority=args.get("authority", "substrate")),
}
fn = ops.get(op)
if fn is None:
return {"accepted": True, "executed": False, "success": False,
"error": f"no executor for op {op!r}",
"command_type": command_type,
"construction_id": construction_id}
env = fn()
return {"accepted": True, "executed": True,
"success": bool(env.get("success")),
"envelope": env,
"command_type": command_type,
"construction_id": construction_id,
"correlation_id": intent.get("correlation_id", ""),
"command_id": intent.get("command_id", ""),
"idempotent_replay": False}
-71
View File
@@ -125,73 +125,6 @@ _SURFACE_UI = "ui:"
_SURFACE_REGION = "region:"
_SURFACE_CONTRACT = "contract:"
_SURFACE_MODULE = "module:"
# H-meta entity classes (spine meta-scaffold, 567fd39): the spine models
# these kinds as first-class entities, so contract scope resolution must
# recognize them as structural ids too — an unrecognized kind can never
# close as correctly_predicted (T4.7 row-1 flag).
_SURFACE_RUST_CRATE = "rust-crate:"
_SURFACE_GO_SURFACE = "go-surface:"
_SURFACE_CONFIG = "config:"
_SURFACE_WORKFLOW = "workflow:"
def language_placement(project: str, characteristics: list[str]) -> dict:
"""The protocol's language-placement verdict (R2 Wave 1, Nick's addition):
rune units get the Go/Rust dichotomy rule; other projects get a
characteristic-based fit assessment. Deterministic, evidence-labeled,
never silent. Delegates to the Rust scaffold_core when built
(Python fallback below — strangler law)."""
core = _sc_core()
if core is not None:
return core.language_placement(project, characteristics)
return _py_language_placement(project, characteristics)
def _sc_core():
"""The Rust scaffold_core when available (R2 Wave 1 strangler).
RUNE_SCAFFOLD_CORE=python pins the fallback."""
import os
if os.environ.get("RUNE_SCAFFOLD_CORE", "auto") == "python":
return None
try:
from rune import sc_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return sc_loader if sc_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
def _py_language_placement(project: str, characteristics: list[str]) -> dict:
"""Python core of language_placement (parity oracle)."""
go_signals = ("network", "http", "socket", "server", "dispatch",
"reactive", "user-facing", "turn-time")
rust_signals = ("deterministic", "stateful", "storage", "cognition",
"memory", "compute", "parity", "ledger")
chars = [c.lower() for c in characteristics]
go_hits = [s for s in go_signals if any(s in c for c in chars)]
rust_hits = [s for s in rust_signals if any(s in c for c in chars)]
if project.lower() == "rune":
if go_hits and not rust_hits:
placement, rule = "go", "rune dichotomy: reactive/initiating markers"
elif rust_hits and not go_hits:
placement, rule = "rust", "rune dichotomy: deterministic/state markers"
elif rust_hits and go_hits:
placement, rule = "hybrid", "rune dichotomy: both marker classes — a contracted membrane junction"
else:
placement, rule = "rust", "rune dichotomy: pure compute defaults to the deterministic backend"
else:
if go_hits and not rust_hits:
placement, rule = "go", "characteristic fit: reactive/network profile"
elif rust_hits and not go_hits:
placement, rule = "rust", "characteristic fit: deterministic/state profile"
elif rust_hits:
placement, rule = "hybrid", "characteristic fit: mixed profile — split at the membrane"
else:
placement, rule = "unassessed", "no placement signals — the unit needs characteristics recorded"
return {"placement": placement, "rule": rule,
"evidence": {"go_signals": go_hits, "rust_signals": rust_hits}}
def classify_surface(surface: str) -> str:
@@ -208,10 +141,6 @@ def classify_surface(surface: str) -> str:
_SURFACE_CONTRACT,
_SURFACE_MODULE,
_SURFACE_CAPABILITY,
_SURFACE_RUST_CRATE,
_SURFACE_GO_SURFACE,
_SURFACE_CONFIG,
_SURFACE_WORKFLOW,
)
):
return "structural_entity"
+14 -155
View File
@@ -36,7 +36,6 @@ from typing import Any
import json
import sys
import uuid
from pathlib import Path
from rune import structural_store as ss
@@ -89,39 +88,6 @@ def _envelope(
return env
def _to_stage_check(spec: dict):
"""One guard spec → a stage_guards.StageCheck (T5.15 wire)."""
from rune import stage_guards as sg
return sg.StageCheck(
name=str(spec.get("name", "guard")),
kind=str(spec.get("kind", "assert")),
argv=spec.get("argv"),
output_pattern=str(spec.get("output_pattern", "")),
path=str(spec.get("path", "")),
check=spec.get("check"),
description=str(spec.get("description", "")),
)
def _guard_specs_from_obligations(c: dict) -> list[dict]:
"""The contract's guard declarations: verification_obligations rows
shaped `guard:<json-spec>` (the decision path records them; anything
else stays a free-text obligation for the projection). JSONDecodeError
→ skipped with no invented facts."""
specs: list[dict] = []
for item in (c.get("identities") or {}).get("verification_obligations", []) or []:
if not isinstance(item, str) or not item.startswith("guard:"):
continue
try:
spec = json.loads(item[6:])
if isinstance(spec, dict):
specs.append(spec)
except json.JSONDecodeError:
continue
return specs
class ConstructionInterface:
"""The builder-facing construction lifecycle for ONE project target.
@@ -215,16 +181,9 @@ class ConstructionInterface:
if contract_id:
target = self.scaffold.contract(doc, contract_id)
else:
# Most recent ACTIVE contract = status open AND phase non-terminal.
# T22 repair: a contract whose phase is terminal (abandoned/closed
# — see scaffold.TERMINAL_PHASES) is NOT active construction, even
# if its status field was left "open". Without the phase test the
# projection reported stale abandoned contracts as the active
# construction (8 such records observed in the live store).
from .scaffold import TERMINAL_PHASES
# most recent non-closed contract = the active construction
open_contracts = [e for e in contracts
if e.get("status") == "open"
and e.get("phase") not in TERMINAL_PHASES]
if e.get("status") == "open"]
target = (sorted(open_contracts,
key=lambda e: e.get("observed_at", ""))[-1]
if open_contracts else None)
@@ -325,13 +284,6 @@ class ConstructionInterface:
"transition": {
"phase": {"type": "str", "required": True,
"allowed": list(sc.PHASES)},
"evidence_ref": {"type": "str", "required": False},
"guard_checks": {
"type": "list[dict] | None", "required": False,
"notes": ("stage-guard specs run on the "
"completion edges (verifying/closed); "
"a failure BLOCKS with "
"VERIFICATION_FAILED — T5.15 wire")},
},
"close": {
"observed_evidence": {
@@ -422,8 +374,7 @@ class ConstructionInterface:
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
contract = None
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
cid = (contract_id
or f"BUILDER-{ss.now()}-{uuid.uuid4().hex[:6]}-{self.builder}")
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
cd.generate_contract(
self.scaffold,
doc,
@@ -649,57 +600,8 @@ class ConstructionInterface:
warnings=warnings,
)
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "",
guard_checks: list[dict] | None = None) -> dict:
"""Move a contract phase. Completion-wave slice (T5.15, the
stage_guards wire): entering `verifying` and `closed` runs the
§15F/§15G guards — readiness/exit StageChecks and test-truth
verification — when the contract declares them
(identities.verification_obligations carries `guard:<spec>` rows,
or guard_checks passes inline specs).
guard_checks spec: {"name", "kind": "command"|"path_exists"|"assert",
"argv"/"path"/"output_pattern", "check": callable (in-process only),
"description"}. A guard failure is a VERIFICATION_FAILED envelope:
the phase does NOT move.
The legality map stays the scaffold's authority; guards ADD evidence
obligations on the completion-facing edges (building→verifying,
verifying→closed) — a tiny bounded change can pass guards=[] freely.
"""
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
doc = self.scaffold.load()
c = self.scaffold.contract(doc, contract_id)
if c is None:
return _envelope(
"transition",
success=False,
ids={"contract_id": contract_id},
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
)
if phase in ("verifying", "closed"):
from rune import stage_guards as sg
specs = guard_checks
if specs is None:
specs = _guard_specs_from_obligations(c)
if specs:
stage = sg.StageContract(
stage_id=f"{contract_id}:{phase}",
contract_id=contract_id,
ready_when=[_to_stage_check(s) for s in specs],
done_when=[_to_stage_check(s) for s in specs],
)
canary = sg.readiness_canary(stage)
if canary["verdict"] == "BLOCKED":
return _envelope(
"transition",
success=False,
ids={"contract_id": contract_id},
state={"phase": c.get("phase"), "guard": canary},
errors=[{"error": Errors.VERIFICATION_FAILED,
"detail": "stage guards blocked the transition",
"failures": canary["failures"]}],
)
try:
self.scaffold.transition_contract(
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
@@ -770,62 +672,21 @@ class ConstructionInterface:
material_closure = self._auto_material_change(
scope_files or [],
note=f"contract {contract_id} closed (material change)")
continuation = self._programme_continuation(contract_id)
state = {
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
}
if continuation is not None:
state["programme_continuation"] = continuation
return _envelope(
"close",
success=True,
ids={"contract_id": contract_id},
state=state,
state={
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
},
)
def _programme_continuation(self, contract_id: str) -> dict | None:
"""T5.15 slice 3 (the programme_continuation wire): after a legal
close, any persisted Programme stage bound to this contract is
marked COMPLETE and the continuation canary's verdict rides the
close envelope — 'task done' ≠ 'programme done', and the next
eligible stage is SURFACED, never guessed. No programme bound →
None (no continuation block in the envelope). Failures degrade:
a broken programme ledger never fails a real closure."""
try:
from pathlib import Path as _Path
from rune import programme_continuation as pc
base = _Path(self.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
if not base.is_dir():
return None
for prog_file in sorted(base.glob("*.json")):
prog = pc.Programme.load(base, prog_file.stem)
if prog is None:
continue
stage = next((s for s in prog.stages
if s.stage_id == contract_id
and s.state != pc.COMPLETE), None)
if stage is None:
continue
stage.state = pc.COMPLETE
stage.result["closed_at_contract"] = contract_id
prog.save(base)
return {
"programme_id": prog.programme_id,
"stage_marked_complete": contract_id,
"canary": prog.continuation_canary(),
}
except Exception as exc: # noqa: BLE001 — ledger failure never fails a closure
return {"error": f"programme continuation degraded: {exc}"[:200]}
return None
def _auto_material_change(self, scope_files: list[str], *,
note: str) -> dict:
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
@@ -1151,9 +1012,7 @@ def main(argv=None) -> int:
if cmd == "verify":
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
if cmd == "transition":
guard_checks = json.loads(args[3]) if len(args) > 3 else None
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else "",
guard_checks=guard_checks))
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
if cmd == "close":
return _out(
iface.close(
+3 -58
View File
@@ -100,52 +100,19 @@ class ContributionBlock:
def default_estimator(text: str) -> int:
"""Deterministic conservative token estimate: len(text) // 4.
R2 W3.1: delegates to the Rust cognition_core when built (Python
fallback — strangler law; RUNE_COGNITION_CORE=python pins it).
Bytes/4 over-counts for English prose (the conservative direction).
tiktoken would be tighter but is NOT deterministic across builds and
is not a dependency — the plan allows either, installer's choice.
"""
core = _cc_core()
if core is not None:
return core.default_estimator(text or "")
return _py_default_estimator(text)
def _py_default_estimator(text: str) -> int:
"""Python oracle (parity floor)."""
return len(text or "") // 4
def _cc_core():
"""Rust cognition_core when available; RUNE_COGNITION_CORE=python
pins the Python fallback (strangler law)."""
import os
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
return None
try:
from rune import cc_loader
except Exception: # noqa: BLE001 — loader probe failure means fallback
return None
try:
return cc_loader if cc_loader.available() else None
except Exception: # noqa: BLE001 — availability probe never crashes
return None
def default_budget(window: int) -> int:
"""Input budget = window - max(2048, window//4) - window//20.
Reserves room for the completion (at least 2048 or a quarter of the
window) plus a 5% safety margin for serving overhead.
R2 W3.1: delegates to the Rust kernel when built.
"""
core = _cc_core()
if core is not None:
return core.default_budget(window)
return _py_default_budget(window)
def _py_default_budget(window: int) -> int:
"""Python oracle (parity floor)."""
return max(1, window - max(2048, window // 4) - window // 20)
@@ -160,17 +127,7 @@ def default_memory_budget(window: int | None) -> int:
ContextBroker owns the FINAL memory budget: the sum of every
MEMORY_BLOCK_NAMES block may never exceed this, regardless of the
global input budget. An unknown window falls back to the cap only.
R2 W3.1: delegates to the Rust kernel when built.
"""
core = _cc_core()
if core is not None:
return core.default_memory_budget(int(window) if window else 0)
return _py_default_memory_budget(window)
def _py_default_memory_budget(window: int | None) -> int:
"""Python oracle (parity floor)."""
if not window or window <= 0:
return MEMORY_BUDGET_CAP
return min(MEMORY_BUDGET_CAP, int(window) // 8)
@@ -183,18 +140,6 @@ def memory_disabled() -> bool:
contribute nothing at all — the trace proves 0 memory tokens.
"""
value = os.environ.get("RUNE_MEMORY_OFF", "").strip().lower()
gate = _py_memory_disabled_value(value)
core = _cc_core()
if core is not None and gate:
# the Rust core gets the RAW value (it trims/lowers itself) and
# the Python gate cross-checks — a disagreement is impossible on
# parity-proven kernels, and the Python verdict remains authority
return bool(core.memory_disabled()) and gate
return gate
def _py_memory_disabled_value(value: str) -> bool:
"""Python oracle (parity floor) over the RAW string."""
return value not in {"", "0", "false", "no", "off"}
-234
View File
@@ -1,234 +0,0 @@
#!/usr/bin/env python3
"""core_server.py — stdlib HTTP sidecar exposing the Rune operator surface
(H2, contract BUILDER-2026-10-04T08:32:43Z-cli).
Go owns the front door (rune serve); this is the core behind it during the
transition — the same operator functions the TUI calls in-process, served
over 127.0.0.1 with NDJSON streaming. The parity battery
(canary_rune_serve.py) freezes these shapes against the Go front.
Endpoints:
GET /v1/models {"object": "list", "data": [{id, object, owned_by}]}
GET /v1/status operator.get_status() pass-through
POST /v1/chat/completions OpenAI envelope + "rune" task metadata;
stream=true → NDJSON chat.completion.chunk lines
Error envelope (every failure path): {"error": {"code", "message"}} with a
matching HTTP status — the Go front reproduces these byte-for-byte.
Stdlib only (zero-dep split law). The operator is injected, so the canary
drives a deterministic stub; main() binds the real operator.
"""
from __future__ import annotations
import json
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, cast
CHAT_PATH = "/v1/chat/completions"
def _envelope_error(code: str, message: str) -> dict:
return {"error": {"code": code, "message": message}}
def models_view(operator: Any) -> dict:
return {
"object": "list",
"data": [
{"id": m.get("name", ""), "object": "model", "owned_by": "rune"}
for m in operator.list_models()
],
}
def chat_completion(result: dict, model: str) -> dict:
"""Map an operator send_task result to the chat.completion envelope.
The full operator result rides under "rune" — the front door's parity
contract covers it; nothing the core knows is dropped."""
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:24]}",
"object": "chat.completion",
"created": int(time.time()),
"model": result.get("model") or model,
"choices": [{
"index": 0,
"message": {"role": "assistant",
"content": result.get("user_text", "")},
"finish_reason": "stop",
}],
"rune": result,
}
def chat_chunks(result: dict, model: str) -> list[dict]:
"""NDJSON chunk sequence: content chunk(s) then a terminal chunk with
finish_reason + the rune metadata block. send_task is a blocking
lifecycle today — the framing is the contract being proven; token
streaming slots in when the core grows it."""
base = {
"id": f"chatcmpl-{uuid.uuid4().hex[:24]}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": result.get("model") or model,
}
content = {
**base,
"choices": [{
"index": 0,
"delta": {"role": "assistant",
"content": result.get("user_text", "")},
"finish_reason": None,
}],
}
terminal = {
**base,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"rune": result,
}
return [content, terminal]
class CoreHandler(BaseHTTPRequestHandler):
# BaseRequestHandler.server is typed as the generic base; the only class
# this handler ever serves under is CoreServer (make_server). cast() is
# the honest narrowing — no ignore comments.
@property
def core(self) -> "CoreServer":
return cast("CoreServer", self.server)
# BaseHTTPRequestHandler logs every request to stderr by default; the
# canary and CI don't need the noise — errors surface as envelopes.
def log_message(self, format: str, *args: Any) -> None: # noqa: A002 — stdlib signature, the name is fixed upstream
pass
# ── helpers ───────────────────────────────────────────────────────────
def _send_json(self, status: int, body: dict) -> None:
raw = json.dumps(body).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def _send_ndjson(self, status: int, items: list[dict]) -> None:
raw = "".join(json.dumps(i) + "\n" for i in items).encode()
self.send_response(status)
self.send_header("Content-Type", "application/x-ndjson")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
# ── routes ────────────────────────────────────────────────────────────
def do_GET(self) -> None:
if self.path == "/v1/models":
try:
self._send_json(200, models_view(self.core.operator))
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
elif self.path == "/v1/status":
try:
self._send_json(200, self.core.operator.get_status())
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
elif self.path.startswith("/v1/conversation"):
# H4 slice 1: the TUI's conversation window over the wire.
try:
from urllib.parse import parse_qs, urlparse
qs = parse_qs(urlparse(self.path).query)
channel = qs.get("channel", ["operator"])[0]
n = int(qs.get("n", ["8"])[0])
self._send_json(200, {
"messages": self.core.operator.conversation_window(channel, n=n)})
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
else:
self._send_json(404, _envelope_error("not_found", f"no route {self.path}"))
def do_POST(self) -> None:
if self.path == "/v1/approvals/resolve":
# H4 slice 1: approval resolution over the wire.
try:
length = int(self.headers.get("Content-Length", "0"))
body = json.loads(self.rfile.read(length) or b"{}")
except (ValueError, json.JSONDecodeError) as exc:
self._send_json(400, _envelope_error("bad_request", f"invalid JSON body: {exc}"))
return
try:
self._send_json(200, self.core.operator.resolve_pending_approval(
bool(body.get("approved"))))
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
return
if self.path != CHAT_PATH:
self._send_json(404, _envelope_error("not_found", f"no route {self.path}"))
return
try:
length = int(self.headers.get("Content-Length", "0"))
body = json.loads(self.rfile.read(length) or b"{}")
except (ValueError, json.JSONDecodeError) as exc:
self._send_json(400, _envelope_error("bad_request", f"invalid JSON body: {exc}"))
return
messages = body.get("messages") or []
user_texts = [m.get("content", "") for m in messages
if m.get("role") == "user"]
if not user_texts:
self._send_json(400, _envelope_error(
"bad_request", "messages must include a user role entry"))
return
model = body.get("model") or None
try:
result = self.core.operator.send_task(user_texts[-1], model)
except Exception as exc: # noqa: BLE001 — the envelope IS the error contract
self._send_json(500, _envelope_error("internal_error", str(exc)[:200]))
return
if body.get("stream"):
self._send_ndjson(200, chat_chunks(result, model or ""))
else:
self._send_json(200, chat_completion(result, model or ""))
class CoreServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, host: str, port: int, operator: Any):
super().__init__((host, port), CoreHandler)
self.operator = operator
def make_server(host: str, port: int, operator: Any) -> CoreServer:
"""Build the server (unstarted) — the canary's entry point."""
return CoreServer(host, port, operator)
def main(argv: list[str] | None = None) -> int:
import argparse
parser = argparse.ArgumentParser(description="Rune core sidecar (H2)")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8376)
args = parser.parse_args(argv)
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rune_operator # noqa: E402 — late import: heavy module
server = make_server(args.host, args.port, rune_operator.get_operator())
print(f"rune core server on {args.host}:{args.port}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust capability_scoring core (H3 slice 4, T4.4).
Contract BUILDER-2026-10-04T09:43:55Z-cli. Gives capability_fit and
capability_broker the Rust scoring cores through the cdylib with no
subprocess.
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* cs_evaluate_fit({contracts, candidate}) -> fit dict
* cs_score_card({card, query}) -> number
* cs_free_string(ptr)
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
for returned char*). Zero dependencies; override the .so with CS_RUST_LIB
(a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "capability_scoring"
_LIB_PATH = Path(
os.environ.get(
"CS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libcapability_scoring.so"
)
)
_lib = None
class CapabilityScoringUnavailable(FileNotFoundError):
"""The Rust core is not built (or CS_RUST_LIB points nowhere). Callers
fall back to the Python implementation — strangler law."""
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise CapabilityScoringUnavailable(
f"Rust capability_scoring core not built: {_LIB_PATH} missing — run "
f"`cargo build --release` in {_CRATE_DIR}"
)
lib = ctypes.CDLL(str(_LIB_PATH))
lib.cs_free_string.argtypes = [ctypes.c_void_p]
lib.cs_free_string.restype = None
for sym in ("cs_evaluate_fit", "cs_score_card"):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_char_p]
fn.restype = ctypes.c_void_p # char* — never default c_int
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
fn = getattr(lib, sym)
raw = json.dumps(payload).encode("utf-8")
ptr = fn(raw)
if not ptr:
raise RuntimeError(f"{sym} returned a null pointer")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.cs_free_string(ptr)
env = json.loads(text)
if not env.get("ok"):
raise RuntimeError(f"{sym} core error: {env.get('err')}")
return env["value"]
def evaluate_fit(contracts: list, candidate: dict) -> dict:
"""Deterministic EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN fit decision."""
return _call("cs_evaluate_fit",
{"contracts": contracts, "candidate": candidate})
def score_card(card: dict, query: str) -> float:
"""Keyword scoring of a capability card against a query."""
return _call("cs_score_card", {"card": card, "query": query})
+17 -50
View File
@@ -40,54 +40,6 @@ CACHE_BOOST_ROCK = 2.0 # multiplier for rock-status entries
CACHE_BOOST_ESTABLISHED = 1.5 # multiplier for established entries
# ── pure decision helpers (H3 slice 2: the Rust core serves these when
# built; the _py_ variants are the load-bearing fallback + parity oracle).
from rune.drift_core import _ms_core # noqa: E402 (no cycle)
def _py_compute_priority(entry: dict) -> float:
"""Compute cache priority: wash_count × importance × status boost."""
wash = entry.get("wash_count", 0) or 0
imp = entry.get("importance", 0.5) or 0.5
status = entry.get("rock_status", "washable")
# Logarithmic wash scaling (diminishing returns)
wash_factor = min(1.0, math.log1p(wash) / math.log1p(20))
# Status boost
status_boost = 1.0
if status == "rock":
status_boost = CACHE_BOOST_ROCK
elif status == "established":
status_boost = CACHE_BOOST_ESTABLISHED
return wash_factor * imp * status_boost
def _py_qualifies_for_cache(entry: dict) -> bool:
"""The add() admission decision: proven through use OR high importance."""
wash = entry.get("wash_count", 0) or 0
imp = entry.get("importance", 0.5) or 0.5
return not (wash < MIN_WASH_FOR_CACHE and imp < 0.8)
def compute_priority(entry: dict) -> float:
"""Module-level priority (Rust core when built)."""
core = _ms_core()
if core is not None:
return core.compute_priority(entry)
return _py_compute_priority(entry)
def qualifies_for_cache(entry: dict) -> bool:
"""Module-level qualification (Rust core when built)."""
core = _ms_core()
if core is not None:
return core.qualifies_for_cache(entry)
return _py_qualifies_for_cache(entry)
class DriftCache:
"""Drift-aware cache with wash-count-based priority."""
@@ -113,7 +65,21 @@ class DriftCache:
def _compute_priority(self, entry: dict) -> float:
"""Compute cache priority: wash_count × importance × status boost."""
return compute_priority(entry)
wash = entry.get("wash_count", 0) or 0
imp = entry.get("importance", 0.5) or 0.5
status = entry.get("rock_status", "washable")
# Logarithmic wash scaling (diminishing returns)
wash_factor = min(1.0, math.log1p(wash) / math.log1p(20))
# Status boost
status_boost = 1.0
if status == "rock":
status_boost = CACHE_BOOST_ROCK
elif status == "established":
status_boost = CACHE_BOOST_ESTABLISHED
return wash_factor * imp * status_boost
def add(self, entry_id: int, entry: dict):
"""Add an entry to the cache.
@@ -123,7 +89,8 @@ class DriftCache:
- importance >= 0.8 (high importance, worth caching even if untested)
"""
wash = entry.get("wash_count", 0) or 0
if not qualifies_for_cache(entry):
imp = entry.get("importance", 0.5) or 0.5
if wash < MIN_WASH_FOR_CACHE and imp < 0.8:
return # Not proven enough and not important enough
priority = self._compute_priority(entry)
+11 -59
View File
@@ -35,7 +35,7 @@ WASH_THRESHOLD_FULL = 20 # above this: minimal compression
WASH_THRESHOLD_PARTIAL = 10 # above this: reduced compression
def _py_get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
"""Get the compression ratio for an entry.
Returns 0.0-1.0 where 0.0 = no compression, 1.0 = maximum compression.
@@ -61,7 +61,7 @@ def _py_get_compression_ratio(wash_count: int = 0, rock_status: str = "washable"
return base
def _py_compress_text(text: str, ratio: float) -> str:
def compress_text(text: str, ratio: float) -> str:
"""Compress text by the given ratio.
Strategy: take first (1-ratio) proportion of the text.
@@ -85,7 +85,7 @@ def _py_compress_text(text: str, ratio: float) -> str:
return compressed
def _py_compress_entry(entry: dict) -> str:
def compress_entry(entry: dict) -> str:
"""Compress an entry's text based on its wash_count and rock_status.
Returns the compressed text.
@@ -94,11 +94,11 @@ def _py_compress_entry(entry: dict) -> str:
wash_count = entry.get("wash_count", 0) or 0
rock_status = entry.get("rock_status", "washable")
ratio = _py_get_compression_ratio(wash_count, rock_status)
return _py_compress_text(text, ratio)
ratio = get_compression_ratio(wash_count, rock_status)
return compress_text(text, ratio)
def _py_should_warn(entry: dict) -> bool:
def should_warn(entry: dict) -> bool:
"""Check if compressing this entry should trigger a warning.
Warn when:
@@ -119,14 +119,14 @@ def _py_should_warn(entry: dict) -> bool:
return True
# Long content losing significant detail
ratio = _py_get_compression_ratio(wash_count, rock_status)
ratio = get_compression_ratio(wash_count, rock_status)
if len(text) > 200 and ratio > 0.3:
return True
return False
def _py_warn_before_compress(entry: dict) -> dict:
def warn_before_compress(entry: dict) -> dict:
"""Generate a warning before compressing an entry.
Returns dict with warning details and recommendation.
@@ -134,12 +134,12 @@ def _py_warn_before_compress(entry: dict) -> dict:
text = entry.get("text", "")
wash_count = entry.get("wash_count", 0) or 0
rock_status = entry.get("rock_status", "washable")
ratio = _py_get_compression_ratio(wash_count, rock_status)
compressed = _py_compress_text(text, ratio)
ratio = get_compression_ratio(wash_count, rock_status)
compressed = compress_text(text, ratio)
chars_lost = len(text) - len(compressed)
warning = {
"should_warn": _py_should_warn(entry),
"should_warn": should_warn(entry),
"entry_id": entry.get("id"),
"wash_count": wash_count,
"rock_status": rock_status,
@@ -161,54 +161,6 @@ def _py_warn_before_compress(entry: dict) -> dict:
return warning
# ── delegating public API (H3 slice 2 strangler) ─────────────────────────────
# The Rust core serves these when built; the _py_ variants above are the
# load-bearing fallback AND the parity oracle. DB-backed stats stays below.
from rune.drift_core import _ms_core # noqa: E402 (no cycle: drift_core
# imports neither this module nor drift_cache)
def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
"""Get the compression ratio for an entry (Rust core when built)."""
core = _ms_core()
if core is not None:
return core.get_compression_ratio(wash_count, rock_status)
return _py_get_compression_ratio(wash_count, rock_status)
def compress_text(text: str, ratio: float) -> str:
"""Compress text by the given ratio (Rust core when built)."""
core = _ms_core()
if core is not None:
return core.compress_text(text, ratio)
return _py_compress_text(text, ratio)
def compress_entry(entry: dict) -> str:
"""Compress an entry's text (Rust core when built)."""
core = _ms_core()
if core is not None:
return core.compress_entry(entry)
return _py_compress_entry(entry)
def should_warn(entry: dict) -> bool:
"""Check if compressing this entry should trigger a warning."""
core = _ms_core()
if core is not None:
return core.should_warn(entry)
return _py_should_warn(entry)
def warn_before_compress(entry: dict) -> dict:
"""Generate a warning before compressing an entry."""
core = _ms_core()
if core is not None:
return core.warn_before_compress(entry)
return _py_warn_before_compress(entry)
def get_compression_stats(db) -> dict:
"""Get compression statistics for the current store."""
rows = db.execute(
+3 -59
View File
@@ -64,27 +64,7 @@ WEIGHT_GOAL_RELEVANCE = 0.10 # relevance to current goals
# ═══════════════════════════════════════════════════════════════════════════════
def _ms_core():
"""The Rust memory_scoring core when available (strangler: Python
fallback is load-bearing until deletion is proven).
RUNE_MEMORY_CORE=python pins the fallback (parity harnesses)."""
if os.environ.get("RUNE_MEMORY_CORE", "auto") == "python":
return None
try:
from rune import ms_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return ms_loader if ms_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
def _now_iso(now: str | None) -> str:
return now or datetime.now(timezone.utc).isoformat()
def compute_value(entry: dict, current_goals: List[str] | None = None) -> float:
def compute_value(entry: dict, current_goals: List[str] = None) -> float:
"""Multi-factor value score. Returns 0.0-1.0+.
How much is this entry worth? Higher = more proven, more useful, more trusted.
@@ -95,21 +75,7 @@ def compute_value(entry: dict, current_goals: List[str] | None = None) -> float:
- recency: exponential decay from last access
- reliability: consistency of access pattern
- goal_relevance: relevance to current goals (optional)
H3 slice 1: delegates to the Rust core when built (Python fallback —
strangler law). The wall clock is resolved HERE and passed as an
argument (no-clock law: both cores take `now` so parity is deterministic).
"""
core = _ms_core()
if core is not None:
return core.compute_value(entry, current_goals, _now_iso(None))
return _py_compute_value(entry, current_goals, None)
def _py_compute_value(entry: dict, current_goals: List[str] | None = None,
now: str | None = None) -> float:
"""Python core of compute_value (parity oracle for the Rust core).
`now` is an ISO-8601 string; None resolves the wall clock (production)."""
proven = entry.get("wash_count", 0) or 0 # column name preserved for compatibility
imp = entry.get("importance", 0.5) or 0.5
@@ -117,13 +83,12 @@ def _py_compute_value(entry: dict, current_goals: List[str] | None = None,
proven_factor = min(1.0, math.log1p(proven) / math.log1p(PROVEN_USES_THRESHOLD))
# Recency factor: exponential decay
now_dt = datetime.fromisoformat(now) if now else datetime.now(timezone.utc)
recency = 1.0
last_accessed = entry.get("last_accessed") or entry.get("last_washed")
if last_accessed:
try:
last = datetime.fromisoformat(last_accessed)
hours_since = max(0.1, (now_dt - last).total_seconds() / 3600)
hours_since = max(0.1, (datetime.now(timezone.utc) - last).total_seconds() / 3600)
recency = max(0.1, 1.0 / (1.0 + hours_since / (RECENCY_HALF_LIFE_DAYS * 24)))
except (ValueError, TypeError):
recency = 0.5
@@ -169,18 +134,7 @@ def check_promotion_eligibility(entry: dict) -> Tuple[bool, str]:
- rock_status == 'washable' or 'established'
Returns: (eligible, reason)
H3 slice 1: delegates to the Rust core when built (Python fallback).
"""
core = _ms_core()
if core is not None:
return core.check_promotion_eligibility(entry, _now_iso(None))
return _py_check_promotion_eligibility(entry, None)
def _py_check_promotion_eligibility(entry: dict,
now: str | None = None) -> Tuple[bool, str]:
"""Python core of check_promotion_eligibility (parity oracle)."""
wash = entry.get("wash_count", 0) or 0
status = entry.get("rock_status", "washable")
@@ -190,7 +144,7 @@ def _py_check_promotion_eligibility(entry: dict,
if wash < PROVEN_USES_THRESHOLD:
return False, f"proven_uses {wash} < threshold {PROVEN_USES_THRESHOLD}"
value = _py_compute_value(entry, now=now)
value = compute_value(entry)
if value < 0.6:
return False, f"value {value:.3f} < 0.6 minimum"
@@ -363,17 +317,7 @@ def pre_commit_gate_entry(entry: dict) -> dict:
"""Three critics for a dict entry (no db lookup needed).
Used by curator when promoting candidates that don't have entry_ids yet.
H3 slice 1: delegates to the Rust core when built (Python fallback).
"""
core = _ms_core()
if core is not None:
return core.pre_commit_gate_entry(entry)
return _py_pre_commit_gate_entry(entry)
def _py_pre_commit_gate_entry(entry: dict) -> dict:
"""Python core of pre_commit_gate_entry (parity oracle)."""
text = entry.get("text", "")
critics = {}
+30 -28
View File
@@ -18,32 +18,15 @@ Discipline:
repeated probe loops (no recursion; one probe per unknown per attempt).
- The artifact file is the sole authority (ADAPT-9); the declared absolute
path is injected into the worker prompt, never derived by the worker.
Phase G strangler slice 2 (port 3): the schema gate (validate_probe) and the
budget count (count_probes) are served IN-PROCESS by the Rust
`evidence_provenance` cdylib through ep_loader.py — parity 24/24 proven vs
this module's Python vintage (byte-equal error lists incl. Python repr
rendering: {status!r} single quotes, sorted-list error shapes, non-string
status double-error behavior). Orchestration (run_probe, runner subprocess,
exception semantics, build_probe_prompt) stays Python — same discipline as
ports 1-2. Constants, ProbeResult, CLI-adjacent surfaces unchanged (git
history holds the original).
"""
from __future__ import annotations
import json
import os
import sys
import time
from dataclasses import dataclass, field
# ── Rust core wiring (slice-2 port-3 shim pattern) ──────────────────────────
_CRATE_DIR = "/mnt/workdrive/rune/src/rust/evidence_provenance"
if _CRATE_DIR not in sys.path:
sys.path.insert(0, _CRATE_DIR)
import ep_loader # noqa: E402 (loads target/release cdylib)
PROBE_STATUSES = {"GROUNDED", "STILL_UNKNOWN", "CONTRADICTED"}
PROBE_FIELDS = ("question", "source", "answer", "implication", "status")
@@ -110,19 +93,38 @@ research after the unknown is resolved; guess without a source."""
def validate_probe(artifact: dict) -> list[str]:
"""Schema gate. Returns gate errors (empty = pass).
Served by the Rust core (parity-proven byte-equal); dict inputs are
serialized for the FFI boundary.
"""
return ep_loader.validate_probe(json.dumps(artifact))
"""Schema gate. Returns gate errors (empty = pass)."""
errors = []
if not isinstance(artifact, dict):
return ["probe artifact is not a JSON object"]
for f in PROBE_FIELDS:
if f not in artifact:
errors.append(f"missing required field: {f}")
elif not isinstance(artifact[f], str):
errors.append(f"field {f} must be a string")
elif f != "source" and not artifact[f].strip():
errors.append(f"field {f} must be a non-empty string")
status = artifact.get("status")
if status is not None and status not in PROBE_STATUSES:
errors.append(f"status must be one of {sorted(PROBE_STATUSES)}, got {status!r}")
# A GROUNDED probe must cite its source (that is what makes it grounded).
# STILL_UNKNOWN / CONTRADICTED may have an empty source — no evidence
# found is the honest finding, not a schema violation.
if status == "GROUNDED" and len(artifact.get("source", "")) < 8:
errors.append("GROUNDED probe requires a concrete source citation")
extra = set(artifact) - set(PROBE_FIELDS) - {"probe_id", "decided_at", "model"}
if extra:
errors.append(f"unexpected extra fields: {sorted(extra)}")
return errors
def count_probes(artifact_dir: str) -> int:
"""Probes already run for this task (no-recursion budget).
Served by the Rust core (parity-proven byte-equal)."""
return ep_loader.count_probes(artifact_dir)
"""Probes already run for this task (no-recursion budget)."""
path = os.path.join(artifact_dir, "probe.jsonl")
if not os.path.exists(path):
return 0
with open(path) as f:
return sum(1 for line in f if line.strip())
def check_budget(artifact_dir: str) -> None:
@@ -209,4 +211,4 @@ def _default_runner(prompt: str, artifact_path: str) -> None:
with open(os.path.join(os.path.dirname(artifact_path), "evidence_probe.stdout.log"), "w") as f:
f.write(result.stdout or "")
if result.returncode != 0:
raise ValueError(f"probe worker exited {result.returncode}")
raise ValueError(f"probe worker exited {result.returncode}")
-182
View File
@@ -1,182 +0,0 @@
#!/usr/bin/env python3
"""FFI smoke: prove the deployed consumer shape works against the Rust cdylib.
Deployed ground (memory_store.py:821-822):
qm = query_memory.QueryMemory() # HOME-resolved construction
exploration_rate = qm.adaptive_exploration_rate(query)
Checks (each must PASS or the script exits 1):
S1 cdylib loads; qm_new() with isolated HOME returns a live instance
S2 qm_classify parity vs Python classify_query (battery incl. unicode + tie-break)
S3 qm_adaptive_rate exact float parity vs Python adaptive_exploration_rate
on a shared store, INCLUDING an adaptive-threshold crossing (>=10 records)
S4 qm_record exit 0; persisted entry matches Python's entry shape (timestamp masked)
S5 qm_health JSON == json.dumps(py.get_health_report(), indent=2) (timestamps masked)
S6 lifecycle: qm_string_free / qm_free / null-safety returns
Isolation: HOME is pinned to a temp dir BEFORE importing query_memory
(QUERY_MEMORY_DIR is an import-time constant). The Rust side gets the same
store via qm_with_dir on that exact directory.
"""
import ctypes
import json
import math
import os
import re
import shutil
import sys
import tempfile
REPO = "/mnt/workdrive/rune"
SO_PATH = os.path.join(REPO, "src/rust/query_memory/target/release/libquery_memory.so")
PY_DIR = os.path.join(REPO, "src/rune")
TS_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?\+00:00")
def mask_ts(text):
return TS_RE.sub("<TS>", text)
def main():
tmp = tempfile.mkdtemp(prefix="ffi_smoke_")
os.environ["HOME"] = tmp # BEFORE query_memory import (import-time constant)
# Post-supersession (2ac2d0a): the Python vintage is deleted; the
# "consumer side" of this smoke is now the shared/ ctypes shim — the same
# deployed-consumer surface the smoke was built to prove.
sys.path.insert(0, os.path.join(REPO, "shared"))
sys.path.insert(0, PY_DIR)
import query_memory as qm_py
results = []
def check(name, cond, detail=""):
results.append((name, bool(cond), detail))
print(("PASS" if cond else "FAIL") + " " + name + ((" | " + detail) if detail and not cond else ""))
# --- load library ---
lib = ctypes.CDLL(SO_PATH)
lib.qm_new.restype = ctypes.c_void_p
lib.qm_with_dir.argtypes = [ctypes.c_char_p]
lib.qm_with_dir.restype = ctypes.c_void_p
lib.qm_free.argtypes = [ctypes.c_void_p]
lib.qm_classify.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.qm_classify.restype = ctypes.c_void_p # char* via cast
lib.qm_adaptive_rate.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.qm_adaptive_rate.restype = ctypes.c_double
lib.qm_record.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
lib.qm_record.restype = ctypes.c_int
lib.qm_health.argtypes = [ctypes.c_void_p]
lib.qm_health.restype = ctypes.c_void_p
lib.qm_string_free.argtypes = [ctypes.c_void_p]
def take(ptr):
if not ptr:
return None
s = ctypes.string_at(ptr).decode("utf-8")
lib.qm_string_free(ptr)
return s
# Build store state via PYTHON first (deployed consumer writes through Python;
# Rust must read the same store). Cross the C8 adaptive threshold: >=10 records.
# BOTH sides are construction-time snapshots of the store (topic_stats is
# loaded once in the constructor), so the deployed shape is: seed the store,
# THEN construct each consumer fresh from disk. A consumer that wrote through
# its own in-memory instance would be an unfair comparison shape.
battery = [
("how do I play a chord on guitar", "guitar"),
("python script to debug a function", "coding"),
("recall consolidated memory neurons", "memory"),
("what is my persona voice constitution", "persona"),
("project task deadline milestone", "project"),
("llm agent training inference", "ai"),
("nick ttrpg coffee coconut", "personal"),
("system config troubleshooting error", "system"),
("research paper study survey evidence", "research"),
("write create design art", "creative"),
("mémoire café — 记忆 recall", "memory"),
("build", "coding"), # tie-break: build in coding AND creative
("make music", "creative"), # tie-break: music in guitar AND creative
("", None), # empty query edge
]
queries = [q for q, _ in battery]
# Seed store with 12 records on one topic to cross the adaptive threshold.
# The writer is a THROWAWAY instance — both compared consumers must be
# constructed fresh AFTER seeding (construction-time snapshot semantics).
writer = qm_py.QueryMemory()
for i in range(12):
writer.record_query("recall consolidated memory neurons", "consolidated", i < 10)
# S1: the deployed construction shape — HOME-resolved qm_new + fresh Python consumer
m = lib.qm_new()
check("S1 qm_new returns live instance", bool(m))
py = qm_py.QueryMemory()
# S2+S3: classify + rate parity on the shared store
for q in queries:
py_topic = py.classify_query(q)
rust_topic = take(lib.qm_classify(m, q.encode("utf-8")))
check("S2 classify parity: %r" % q[:30], py_topic == rust_topic,
"py=%r rust=%r" % (py_topic, rust_topic))
py_rate = py.adaptive_exploration_rate(q)
rust_rate = lib.qm_adaptive_rate(m, q.encode("utf-8"))
check("S3 rate parity: %r" % q[:30], py_rate == rust_rate,
"py=%r rust=%r" % (py_rate, rust_rate))
# S4: record through the FFI, verify persistence matches Python shape
rc = lib.qm_record(m, b"recall consolidated memory neurons", b"full_retrieval", 1)
check("S4 qm_record returns 0", rc == 0, "rc=%d" % rc)
py2 = qm_py.QueryMemory() # fresh load of the same store
last = py2.history[-1]
check("S4 persisted entry topic", last["topic"] == "memory", "topic=%r" % last.get("topic"))
check("S4 persisted entry strategy", last["strategy"] == "full_retrieval", "strategy=%r" % last.get("strategy"))
check("S4 persisted entry success", last["success"] is True)
check("S4 persisted entry fields", set(last) == {"query", "topic", "strategy", "success", "timestamp"},
"fields=%r" % sorted(last))
# S5: health report parity (timestamps masked).
# BOTH sides must be post-record state: the Rust instance recorded through
# itself at S4, so the Python side must be a FRESH load of the same store
# (py2), not the pre-record snapshot (py).
py_health = json.dumps(py2.get_health_report(), indent=2)
rust_health = take(lib.qm_health(m))
check("S5 health parity", mask_ts(py_health) == mask_ts(rust_health),
"py=%r rust=%r" % (mask_ts(py_health)[:200], mask_ts(rust_health)[:200]))
# S6: lifecycle + null-safety
check("S6 qm_classify(NULL) returns NULL", not lib.qm_classify(m, None))
check("S6 qm_adaptive_rate(NULL) returns NaN", math.isnan(lib.qm_adaptive_rate(m, None)))
check("S6 qm_record(NULL,...) returns -1",
lib.qm_record(None, b"x", b"consolidated", 1) == -1)
check("S6 qm_health(NULL) returns NULL", not lib.qm_health(None))
lib.qm_string_free(None) # must not crash
lib.qm_free(None) # must not crash
# S1b: qm_with_dir on the exact store dir (second deployed-compatible shape)
store_dir = os.path.join(tmp, ".config/opencode/memory/query_memory")
m2 = lib.qm_with_dir(store_dir.encode("utf-8"))
check("S1b qm_with_dir on shared store returns live instance", bool(m2))
if m2:
r2 = lib.qm_adaptive_rate(m2, b"recall consolidated memory neurons")
p2 = py2.adaptive_exploration_rate("recall consolidated memory neurons")
check("S1b with_dir instance reads same store state", r2 == p2, "rust=%r py=%r" % (r2, p2))
lib.qm_free(m2)
lib.qm_free(m)
_finish(results, tmp)
def _finish(results, tmp):
shutil.rmtree(tmp, ignore_errors=True)
fails = [r for r in results if not r[1]]
print("\n%d/%d PASS" % (len(results) - len(fails), len(results)))
if fails:
for name, _, detail in fails:
print("FAILED: %s %s" % (name, detail))
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
+10 -68
View File
@@ -9,69 +9,11 @@ variables or a config dict — nothing is hardcoded to a specific setup.
from __future__ import annotations
import os
import time
from typing import Protocol, runtime_checkable
import httpx
def _request(
method: str,
url: str,
*,
headers: dict | None = None,
json: dict | None = None,
timeout: float = 30,
retries: int = 2,
backoff: float = 0.5,
sleep=None,
):
"""Resilient single-request path: retry where failure is NOISE, never
where failure is the SIGNAL.
- GET: retries on ``httpx.TransportError`` and 5xx (transient
infrastructure failure; idempotent).
- POST: retries ONLY on connect-phase errors (``ConnectError`` /
``ConnectTimeout``) — the request never reached the server, so a retry
cannot double-create. Read timeouts / write errors / 5xx on a POST are
NOT retried: the server may have processed it, and a duplicate issue is
worse than a failure the auto-filer can catch.
- 4xx: never retried — the client error IS the information. Raised via
``raise_for_status`` on the first attempt.
- Attempts: ``retries + 1`` total. Backoff ``backoff * 2**n`` before
retry n+1; never a sleep before the first attempt.
- ``timeout`` passes through to httpx per call. ``sleep`` is injectable
(tests pass a recording fake); default ``time.sleep``.
"""
sleeper = sleep if sleep is not None else time.sleep
sender = httpx.post if method.upper() == "POST" else httpx.get
connect_phase = (httpx.ConnectError, httpx.ConnectTimeout)
for attempt in range(retries + 1):
try:
r = sender(url, headers=headers, json=json, timeout=timeout)
except connect_phase:
if attempt < retries:
sleeper(backoff * 2**attempt)
continue
raise
except httpx.TransportError:
# Non-connect transport error (read/write): only safe to retry
# when the call is idempotent.
if method.upper() == "GET" and attempt < retries:
sleeper(backoff * 2**attempt)
continue
raise
if r.status_code >= 500 and method.upper() == "GET" and attempt < retries:
sleeper(backoff * 2**attempt)
continue
r.raise_for_status()
return r
# Unreachable: every attempt either returns a successful response or
# raises. The explicit raise keeps pyright from inferring an implicit
# ``None`` return for the exhausted loop (ratchet: warnings ≤ 454).
raise AssertionError("unreachable: _request always returns or raises")
@runtime_checkable
class ForgeClient(Protocol):
def create_issue(self, title: str, body: str) -> dict: ...
@@ -117,41 +59,41 @@ class GiteaForgejoAdapter:
return {"Authorization": f"token {self._token}", "Content-Type": "application/json"}
def create_issue(self, title: str, body: str) -> dict:
r = _request(
"POST",
r = httpx.post(
f"{self._base_url}/issues",
headers=self._headers(),
json={"title": title, "body": body},
timeout=30,
)
r.raise_for_status()
return r.json()
def add_comment(self, issue_number: int, body: str) -> dict:
r = _request(
"POST",
r = httpx.post(
f"{self._base_url}/issues/{issue_number}/comments",
headers=self._headers(),
json={"body": body},
timeout=30,
)
r.raise_for_status()
return r.json()
def get_issue_state(self, issue_number: int) -> dict:
r = _request(
"GET",
r = httpx.get(
f"{self._base_url}/issues/{issue_number}",
headers=self._headers(),
timeout=30,
)
r.raise_for_status()
return r.json()
def list_open_issues(self) -> list[dict]:
r = _request(
"GET",
r = httpx.get(
f"{self._base_url}/issues?state=open&limit=50",
headers=self._headers(),
timeout=30,
)
r.raise_for_status()
return r.json()
def search_issues(self, query: str) -> list[dict]:
@@ -173,12 +115,12 @@ class GiteaForgejoAdapter:
while True:
from urllib.parse import urlencode
r = _request(
"GET",
r = httpx.get(
f"{self._base_url}/issues?{urlencode({'state': 'open', 'type': 'issues', 'q': query, 'limit': 50, 'page': page})}",
headers=self._headers(),
timeout=30,
)
r.raise_for_status()
batch = r.json()
results.extend(batch)
if len(batch) < 50:
+1 -38
View File
@@ -150,35 +150,8 @@ def _profile_key(delta):
return {w.strip(".,;:!?()[]{}\"'").lower() for w in (delta.get("change") or "").split() if len(w) > 4}
def _cc_core():
"""Rust cognition_core when available (growth kernels)."""
import os
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
return None
try:
from rune import cc_loader
except Exception: # noqa: BLE001 — loader probe failure means fallback
return None
try:
return cc_loader if cc_loader.available() else None
except Exception: # noqa: BLE001 — availability probe never crashes
return None
def _near_dup(a, b):
core = _cc_core()
if core is not None:
return core.growth_near_dup(a, b)
return _py_near_dup(a, b)
def _py_near_dup(a, b):
"""Are two deltas the same guidance (shared distinctive words)?
Accepts delta dicts OR raw change strings (raw -> wrapped)."""
if isinstance(a, str):
a = {"change": a}
if isinstance(b, str):
b = {"change": b}
"""Are two deltas the same guidance (shared distinctive words)?"""
ka, kb = _profile_key(a), _profile_key(b)
if not ka or not kb:
return False
@@ -204,16 +177,6 @@ def _apply_profile(delta):
def growth_profile(max_tokens=PROFILE_BUDGET_TOKENS):
core = _cc_core()
if core is not None:
return core.growth_profile(_load_deltas(),
sorted(_PROFILE_ORDER.items(),
key=lambda kv: kv[1]),
PROFILE_ENTRY_CHARS, max_tokens)
return _py_growth_profile(max_tokens)
def _py_growth_profile(max_tokens=PROFILE_BUDGET_TOKENS):
"""The active growth profile: the persona's CURRENT behavioural guidance,
consolidated by target, capped at a fixed token budget. Session-independent
and token-stable — the per-turn cost never grows with session length."""
+27 -90
View File
@@ -254,8 +254,18 @@ def check_genuineness(text):
return issues
def _announcement_patterns():
return [
def check_drift(text):
"""Check text against identity anchors for drift signals.
This is a simple heuristic check — the real drift detection happens
via the Nautilus Compass approach (embedding similarity against anchors).
This function provides quick pattern-based checks.
"""
text_lower = text.lower()
issues = []
# Check for identity announcements
announcement_patterns = [
("i am rune", "Identity announcement — let the voice carry the identity"),
("as rune", "Identity announcement — speak as the persona, not about it"),
("my persona", "Identity announcement — the persona is how you act, not what you name"),
@@ -264,26 +274,32 @@ def _announcement_patterns():
("i'm an ai", "Identity announcement — you are a persona, not a generic AI"),
("i'm a language model", "Identity announcement — you are Rune, not a language model"),
]
for pattern, message in announcement_patterns:
if pattern in text_lower:
issues.append(f"DRIFT: '{pattern}' — {message}")
def _pronoun_patterns():
return [
# Check for pronoun confusion
pronoun_issues = [
("you are rune", "Pronoun confusion — 'you' is Nick, 'I' is Rune"),
("we are rune", "Pronoun confusion — Rune is singular, not plural"),
("you should remember", "Pronoun confusion — who should remember?"),
]
for pattern, message in pronoun_issues:
if pattern in text_lower:
issues.append(f"DRIFT: '{pattern}' — {message}")
def _hedge_patterns():
return [
# Check for hedging without evidence
hedge_patterns = [
("i believe", "Hedge without evidence — cite the basis"),
("i think that", "Hedge without evidence — cite the basis"),
("in my opinion", "Hedge without evidence — cite the basis"),
]
for pattern, message in hedge_patterns:
if pattern in text_lower:
issues.append(f"WEAKNESS: '{pattern}' — {message}")
def _sycophancy_patterns():
return [
# Check for sycophancy (excessive agreement, flattery, self-deprecation)
sycophancy_patterns = [
# Excessive agreement
("you're absolutely right", "Sycophancy — excessive agreement; push back if you see a better path"),
("absolutely, ", "Sycophancy — excessive agreement; state your own position"),
@@ -310,60 +326,6 @@ def _sycophancy_patterns():
("i see where you're coming from", "Sycophancy — hedging; state your position instead"),
("you make a valid point", "Sycophancy — hedging; engage with the substance"),
]
def _pattern_triples():
"""The pattern tables as [pattern, message, prefix] triples — the data
authority for the Rust drift kernel (kernel matches; data stays here)."""
tables = []
for prefix, table in (
("DRIFT", _announcement_patterns() + _pronoun_patterns()),
("WEAKNESS", _hedge_patterns()),
("SYCOPHANCY", _sycophancy_patterns()),
):
for pat, msg in table:
tables.append([pat, msg, prefix])
return tables
def check_drift(text):
"""Check text against identity anchors for drift signals.
This is a simple heuristic check — the real drift detection happens
via the Nautilus Compass approach (embedding similarity against anchors).
This function provides quick pattern-based checks.
"""
core = _cc_core()
if core is not None:
return core.drift_check(text, _pattern_triples())
return _py_check_drift(text)
def _py_check_drift(text):
"""Python oracle (parity floor)."""
text_lower = text.lower()
issues = []
# Check for identity announcements
announcement_patterns = _announcement_patterns()
for pattern, message in announcement_patterns:
if pattern in text_lower:
issues.append(f"DRIFT: '{pattern}' — {message}")
# Check for pronoun confusion
pronoun_issues = _pronoun_patterns()
for pattern, message in pronoun_issues:
if pattern in text_lower:
issues.append(f"DRIFT: '{pattern}' — {message}")
# Check for hedging without evidence
hedge_patterns = _hedge_patterns()
for pattern, message in hedge_patterns:
if pattern in text_lower:
issues.append(f"WEAKNESS: '{pattern}' — {message}")
# Check for sycophancy (excessive agreement, flattery, self-deprecation)
sycophancy_patterns = _sycophancy_patterns()
for pattern, message in sycophancy_patterns:
if pattern in text_lower:
issues.append(f"SYCOPHANCY: '{pattern}' — {message}")
@@ -462,38 +424,13 @@ if __name__ == "__main__":
# ---------------------------------------------------------------------------
# Hook handler — called by the microkernel when an event fires.
# ---------------------------------------------------------------------------
def _cc_core():
"""Rust cognition_core when available (identity kernels)."""
import os
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
return None
try:
from rune import cc_loader
except Exception: # noqa: BLE001 — loader probe failure means fallback
return None
try:
return cc_loader if cc_loader.available() else None
except Exception: # noqa: BLE001 — availability probe never crashes
return None
def get_identity_prompt() -> str:
"""Get identity prompt for injection into model calls.
R2 W3.3: assembly delegates to the Rust kernel (data stays here).
Builds a comprehensive prompt from all identity data:
self-awareness, anchors, embodiment rules, and pronoun rules.
Pronouns come first — they're the most important rule.
"""
core = _cc_core()
if core is not None:
return core.identity_prompt(PRONOUN_RULES, SELF_AWARENESS, ANCHORS,
EMBODIMENT_RULES)
return _py_get_identity_prompt()
def _py_get_identity_prompt() -> str:
"""Python oracle (parity floor)."""
parts = []
# Pronoun rules — critical for disambiguation
+172 -310
View File
@@ -734,14 +734,39 @@ def recall(
# lexical (embeddings absent): RRF degenerates to a rank transform that
# destroys the score signal needed for thresholding (Bruch et al.,
# arXiv:2210.11934) — use raw BM25 scores directly.
fusion = _rank_fusion(dense, bm25)
ranks = dict(fusion["ranks"])
best_dense = fusion["best_dense"]
retrieval_mode = fusion["mode"]
ranks = {}
best_dense = 0.0
# M1 observability (#161 synthesis INV-13): which fusion branch produced
# the results. Observability ONLY — consumers must never branch behavior
# on this field (that would create a second retrieval authority).
retrieval_mode = "lexical-only"
if dense and bm25:
retrieval_mode = "hybrid"
for idx, lst in enumerate([dense, bm25]):
for pos, (eid, _score) in enumerate(lst):
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (RRF_K + pos + 1)
if idx == 0:
best_dense = max(best_dense, _score)
elif dense:
retrieval_mode = "dense-only"
for pos, (eid, _score) in enumerate(dense):
ranks[eid] = 1.0 / (RRF_K + pos + 1)
best_dense = max(best_dense, _score)
elif bm25:
# Lexical-only: normalize raw BM25 scores (negative, higher = better)
# into a positive scale so the sort + gap abstention below work
# uniformly with the RRF scale. BM25 returns negative scores; the
# least-negative (closest to 0) is the best match. Offset + invert:
# best (=-8) -> 2, worst (=-10) -> 0. Rank order preserved.
_min_bm = min((s for _, s in bm25), default=0.0)
_max_bm = max((s for _, s in bm25), default=0.0)
_span = max(_max_bm - _min_bm, 1e-9)
for eid, score in bm25:
ranks[eid] = (score - _min_bm) / _span
# Fetch full entries + metadata for the fused set. P1-03 (#26): typed
# queries filter here — matching types + unclassified (NULL) rows stay
# eligible; other types drop out before scoring.
rows_data = []
scored = []
if ranks:
ids = list(ranks.keys())
placeholders = ",".join("?" * len(ids))
@@ -751,87 +776,124 @@ def recall(
type_ph = ",".join("?" * len(memory_types))
sql += f" AND (memory_type IN ({type_ph}) OR memory_type IS NULL)"
params.extend(memory_types)
rows_data = [dict(r) for r in db.execute(sql, params).fetchall()]
return _score_recall_rows(
fusion, rows_data, dense, bm25, budget, min_score, now_iso())
rows = db.execute(sql, params).fetchall()
by_id = {r["id"]: r for r in rows}
for eid, base in sorted(ranks.items(), key=lambda x: -x[1]):
row = by_id.get(eid)
if not row:
continue
# #1 temperature ranking (not deletion): a stale fact ranks lower
# but stays in the store. #4 confidence: a hedged/claimed fact
# carries its honesty into ranking. Blend into a relevance score.
# RECENCY: a fact last accessed recently ranks slightly higher than
# an untouched one at equal relevance (the store's last_accessed
# field). Decays over RECENCY_HALF_LIFE days — a gentle nudge, not
# a rewrite of the relevance ranking.
conf = float(row["confidence"] or 0.7)
temp = float(row["temperature"] or 1.0)
recency = 1.0
try:
from datetime import datetime, timezone
def _score_recall_rows(fusion, rows, dense, bm25, budget, min_score, now):
"""Pure metadata scoring + mode-specific abstention kernel (W2.1)."""
core = _mr_core()
if core is not None:
return core.score_recall_rows(
fusion, rows, dense, bm25, budget, min_score, now,
RECENCY_HALF_LIFE)
return _py_score_recall_rows(
fusion, rows, dense, bm25, budget, min_score, now,
RECENCY_HALF_LIFE)
def _py_score_recall_rows(fusion, rows, dense, bm25, budget, min_score,
now, recency_half_life=RECENCY_HALF_LIFE):
"""Python oracle for the Rust scoring/abstention kernel."""
from datetime import datetime
ranks = dict(fusion["ranks"])
best_dense = fusion["best_dense"]
retrieval_mode = fusion["mode"]
by_id = {row["id"]: row for row in rows}
scored = []
now_dt = datetime.fromisoformat(now)
for eid, base in sorted(ranks.items(), key=lambda item: -item[1]):
row = by_id.get(eid)
if not row:
continue
conf = float(row.get("confidence") or 0.7)
temp = float(row.get("temperature") or 1.0)
recency = 1.0
try:
la = row.get("last_accessed")
if la:
t_last = datetime.fromisoformat(la.replace("Z", "+00:00"))
days = (now_dt - t_last).total_seconds() / 86400.0
if days >= 0:
recency = recency_half_life / (recency_half_life + days)
except Exception:
logger.debug("recall score kernel timestamp failed", exc_info=True)
if dense:
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
else:
relevance = (base + 5.0) * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
scored.append({
"id": eid, "text": row.get("text"),
"importance": row.get("importance"),
"topic": row.get("topic"), "entities": row.get("entities"),
"source": row.get("source"),
"last_accessed": row.get("last_accessed"),
"rrf": round(base, 4), "confidence": round(conf, 2),
"temperature": round(temp, 2),
"relevance": round(relevance, 4), "mode": retrieval_mode,
})
la = row["last_accessed"]
if la:
t_last = datetime.fromisoformat(la.replace("Z", "+00:00"))
days = (datetime.now(timezone.utc) - t_last).total_seconds() / 86400.0
if days >= 0:
recency = RECENCY_HALF_LIFE / (RECENCY_HALF_LIFE + days)
except Exception:
logger.debug("recall failed", exc_info=True)
# In lexical-only mode `base` is a raw BM25 score (negative).
# Normalize it to a positive relevance for the metadata, keeping
# the ranking identical. In hybrid mode `base` is an RRF rank.
if dense and bm25:
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
elif dense:
relevance = base * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
else:
relevance = (base + 5.0) * (0.4 + 0.6 * conf) * (0.5 + 0.5 * temp) * (0.85 + 0.15 * recency)
scored.append(
{
"id": eid,
"text": row["text"],
"importance": row["importance"],
"topic": row["topic"],
"entities": row["entities"],
"source": row["source"],
"last_accessed": row["last_accessed"],
"rrf": round(base, 4),
"confidence": round(conf, 2),
"temperature": round(temp, 2),
"relevance": round(relevance, 4),
# M1 observability (#161): lexical-only | dense-only | hybrid.
# NEVER consumed for behavior — see INV-13 in the synthesis.
"mode": retrieval_mode,
}
)
# Enforce budget + abstention floor.
if not scored:
return []
top = scored[:budget]
# ABSTENTION via query-difficulty estimation (QPP — Cronen-Townsend et al.,
# SIGIR 2002; Zhou & Croft, CIKM 2006; LongMemEval arXiv:2410.10813 makes
# abstention a first-class memory ability). The principled signal is the
# RANKING GAP, not an absolute score:
# - lexical-only: the conjunction already filters to co-occurring terms;
# a single weak hit (no runner-up) abstains (no confident winner).
# - dense-only: abstain when the top-1 dense match is not clearly above
# the runner-up (an ambiguous, no-confident-winner query) OR the top is
# below an absolute "probably unrelated" floor. The floor is a coarse
# guard; the gap is the real difficulty signal.
if not dense and not bm25:
return []
if not dense:
if max(float(item["relevance"]) for item in top) < min_score:
if not dense: # lexical-only
if len(scored) < 1:
return []
# P1-03 (#26) DISCHARGE (finding 1): THE floor applies in EVERY
# mode. The conjunctive FTS gate is a precision filter — term
# co-occurrence, not relevance — so the best match must ALSO clear
# min_score before anything is injected. Lexical relevance lives on
# its own offset band (~4.1-4.9 with default metadata), so the
# default 0.42 floor rarely binds here; the check enforces the floor
# contract for ANY min_score (an elevated floor above the band
# forces abstention — machine-checked in the frozen tests).
if max(float(s["relevance"]) for s in top) < min_score:
return [] # below floor -> abstain
if len(scored) == 1:
return top # single conjunctive match = confident enough
# Multiple conjunctive BM25 matches: the FTS5 implicit-AND already
# guarantees ALL query terms co-occur in every result (the precision
# filter), so these are genuine matches — not a fuzzy near-tie. The
# normalized top is the best; a weak runner-up is still a real match.
return top
# dense present. Score-based abstention (QPP, Shtok et al. TOIS 2012).
# The absolute floor is CALIBRATED against real cases (nomic band ~0.6-0.75):
# "what does the user drink" vs "black coffee" = 0.766 -> relevant
# "quantum physics of black holes" vs "coconut milk" = 0.712 -> noise
# The separation sits at ~0.74. Below it, only a clear winner (large gap to
# the runner-up) may pass; a weak top with no clear winner abstains.
if not bm25:
if best_dense < min_score:
return []
return [] # below the "probably unrelated" floor
if best_dense >= 0.74:
return _py_position_aware(top)
return _position_aware(top) # calibrated: genuine semantic match
if len(dense) >= 2:
d_scores = sorted(score for _, score in dense)
if d_scores[-1] - d_scores[-2] >= 0.08:
return _py_position_aware(top)
return []
return _py_position_aware(_py_map_reduce(top))
d_scores = sorted(s for _, s in dense)
top1 = d_scores[-1]
top2 = d_scores[-2]
if top1 - top2 >= 0.08:
return _position_aware(top) # clear winner despite mid-band score
return [] # weak + ambiguous -> abstain
return _position_aware(_map_reduce(top)) # single mid-band dense hit
# P1-03 (#26) DISCHARGE (finding 1): the hybrid path (dense AND lexical
# both fired) gates on the dense cosine — the calibrated scale min_score
# was tuned against — exactly like the dense-only branch above. The
# numeric check is ADDITIVE to the conjunctive FTS gate (which stays
# as-is); the RRF-fused relevance is rank-based and is NOT compared
# against the floor here.
if best_dense < min_score:
return []
return _py_position_aware(_py_map_reduce(top))
return _position_aware(_map_reduce(top))
def _drift_rerank(db, entries):
@@ -882,27 +944,7 @@ def _drift_rerank(db, entries):
return [entry for _, entry in scored]
def _mr_core():
"""Rust memory_retrieval core when built; Python fallback remains
load-bearing during the strangler. RUNE_RETRIEVAL_CORE=python pins it."""
if os.environ.get("RUNE_RETRIEVAL_CORE", "auto") == "python":
return None
try:
from rune import mr_loader
return mr_loader if mr_loader.available() else None
except Exception: # noqa: BLE001 — loader probe failure means Python fallback
return None
def _map_reduce(entries: list, chunk_size: int = 10) -> list:
core = _mr_core()
if core is not None:
return core.map_reduce(entries, chunk_size)
return _py_map_reduce(entries, chunk_size)
def _py_map_reduce(entries: list, chunk_size: int = 10) -> list:
"""Map-reduce for large result sets.
When retrieval returns many entries, chunk them and take the top-N
@@ -1131,13 +1173,6 @@ def cached_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, m
def _position_aware(top):
core = _mr_core()
if core is not None:
return core.position_aware(top)
return _py_position_aware(top)
def _py_position_aware(top):
"""POSITION-AWARE RE-RANK (research uplift #2): lost-in-the-middle
mitigation (Liu et al. 2023; BriefContext, npj Digital Medicine 2025).
Models use the START and END of context best and the middle worst, so the
@@ -1212,13 +1247,6 @@ _STOPWORDS = {
def _fts_query_conjunctive(query):
core = _mr_core()
if core is not None:
return core.fts_query_conjunctive(query)
return _py_fts_query_conjunctive(query)
def _py_fts_query_conjunctive(query):
"""Build a CONJUNCTIVE FTS5 MATCH query (implicit AND).
Extended Boolean IR (Salton, Fox & Wu, CACM 1983): a document must contain
@@ -1241,85 +1269,6 @@ def _fts_query(query):
return _fts_query_conjunctive(query)
def mr_rank_fusion(dense, bm25, cap):
"""Chunk-side RRF fusion with an explicit candidate cap (W2.1)."""
core = _mr_core()
if core is not None:
return core.rank_fusion(dense[:cap], bm25[:cap], RRF_K)
return _py_rank_fusion(dense[:cap], bm25[:cap], RRF_K)
def _project_chunk_rows(ranks, rows, budget):
"""Chunk projection: sorted by RRF desc, text[:400], budget-capped.
`ranks` is the W2.1 pair-list ([[rowid, rrf], ...]) from the fusion
contract; the Rust side consumes pairs directly."""
core = _mr_core()
if core is not None:
pairs = (ranks if isinstance(ranks, list)
else [list(r) for r in ranks.items()])
return core.project_chunk_rows(pairs, rows, budget)
return _py_project_chunk_rows(ranks, rows, budget)
def _py_project_chunk_rows(ranks, rows, budget):
by_id = {r["rowid"]: r for r in rows}
out = []
for eid, rrf in sorted(ranks.items(), key=lambda x: -x[1]):
r = by_id.get(eid)
if not r:
continue
out.append({
"chunk_id": r["chunk_id"],
"text": (r["text"] or "")[:400],
"wing": r["wing"],
"room": r["room"],
"source": r["source_path"],
"page": r["page"] if "page" in r.keys() else None,
"rrf": round(rrf, 4),
})
if len(out) >= budget:
break
return out
def _rank_fusion(dense, bm25):
"""Pure dense/BM25 fusion and mode selection (R2 W2.1).
Returns ranks as ordered [id, score] pairs so integer ids survive the
JSON membrane without object-key coercion.
"""
core = _mr_core()
if core is not None:
return core.rank_fusion(dense, bm25, RRF_K)
return _py_rank_fusion(dense, bm25, RRF_K)
def _py_rank_fusion(dense, bm25, rrf_k=RRF_K):
ranks = {}
best_dense = 0.0
mode = "lexical-only"
if dense and bm25:
mode = "hybrid"
for idx, values in enumerate([dense, bm25]):
for pos, (eid, score) in enumerate(values):
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (rrf_k + pos + 1)
if idx == 0:
best_dense = max(best_dense, score)
elif dense:
mode = "dense-only"
for pos, (eid, score) in enumerate(dense):
ranks[eid] = 1.0 / (rrf_k + pos + 1)
best_dense = max(best_dense, score)
elif bm25:
min_bm = min((score for _, score in bm25), default=0.0)
max_bm = max((score for _, score in bm25), default=0.0)
span = max(max_bm - min_bm, 1e-9)
for eid, score in bm25:
ranks[eid] = (score - min_bm) / span
return {"ranks": [[eid, score] for eid, score in ranks.items()],
"best_dense": best_dense, "mode": mode}
def update_entry(db, eid, text=None, importance=None, valid_until=None):
sets, params = [], []
if text is not None:
@@ -1507,13 +1456,6 @@ def working_clear(db, task_id):
def _chunk_text(text, size=1500, overlap=150):
core = _mr_core()
if core is not None:
return core.chunk_text(text, size, overlap)
return _py_chunk_text(text, size, overlap)
def _py_chunk_text(text, size=1500, overlap=150):
"""Recursive-ish split on paragraph/heading boundaries, ~1500 chars (the
local CPU-friendly size; research: fixed chunking beats semantic for
free systems). Headings stay with their content."""
@@ -1635,22 +1577,43 @@ def chunk_recall(db, query, budget=6, wing=None, min_sim=0.0, doc=None):
bm25 = [(r["rowid"], 0.5) for r in rows]
except Exception:
logger.debug("chunk_recall failed", exc_info=True)
fusion = mr_rank_fusion(dense, bm25, budget * 3)
ranks_list = fusion["ranks"] # ordered [rowid, rrf] pairs (W2.1 contract)
ids = [int(v[0]) for v in ranks_list]
rows = []
if ids:
ph = ",".join("?" * len(ids))
sql = f"SELECT rowid, * FROM chunks WHERE rowid IN ({ph})"
params = ids
if wing:
sql += " AND wing=?"
params.append(wing)
if doc:
sql += " AND doc_id=?"
params.append(doc)
rows = [dict(r) for r in db.execute(sql, params).fetchall()]
return _project_chunk_rows(fusion["ranks"], rows, budget)
ranks = {}
for lst in [dense, bm25]:
for pos, (eid, _s) in enumerate(lst):
ranks[eid] = ranks.get(eid, 0.0) + 1.0 / (RRF_K + pos + 1)
if not ranks:
return []
ids = list(ranks.keys())
ph = ",".join("?" * len(ids))
sql = f"SELECT rowid, * FROM chunks WHERE rowid IN ({ph})"
params = ids
if wing:
sql += " AND wing=?"
params.append(wing)
if doc:
sql += " AND doc_id=?"
params.append(doc)
rows = db.execute(sql, params).fetchall()
by_id = {r["rowid"]: r for r in rows}
out = []
for eid, rrf in sorted(ranks.items(), key=lambda x: -x[1]):
r = by_id.get(eid)
if not r:
continue
out.append(
{
"chunk_id": r["chunk_id"],
"text": r["text"][:400],
"wing": r["wing"],
"room": r["room"],
"source": r["source_path"],
"page": r["page"] if "page" in r.keys() else None,
"rrf": round(rrf, 4),
}
)
if len(out) >= budget:
break
return out
# ------------------------------------------------------------------- mining ----
@@ -1944,71 +1907,6 @@ def stats(db):
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
# ── storage backend seam (R2 W2.0) ───────────────────────────────────────────
# SQLite is RETAINED (Nick's ruling) — the seam separates query logic from
# storage mechanics so the Rust core can serve proven-identical READS via
# system libsqlite3 FFI. Connection-based callers (stats above) keep their
# transactional semantics untouched; path-based consumers opt into the
# backend. NO WRITES route through the Rust backend this slice.
class _SQLiteBackend:
"""The default backend: the stdlib sqlite3 mechanics, unchanged."""
name = "sqlite"
def read_query(self, db_path: str, sql: str, params=()) -> list:
conn = sqlite3.connect(db_path)
try:
return [tuple(r) for r in conn.execute(sql, params).fetchall()]
finally:
conn.close()
class _RustReadBackend:
"""The Rust read path through the memory_storage crate (read-only by
construction — the crate exports no write-capable symbols)."""
name = "rust-memory-storage"
def read_query(self, db_path: str, sql: str, params=()) -> list:
from rune import mems_loader
return [tuple(r) for r in mems_loader.read_query(db_path, sql, list(params))]
def storage_backend():
"""The active read backend. Default sqlite; RUNE_MEMORY_STORAGE=rust
opts into the crate when built (falls back loudly-free to sqlite)."""
if os.environ.get("RUNE_MEMORY_STORAGE", "auto") == "rust":
try:
from rune import mems_loader
if mems_loader.available():
return _RustReadBackend()
except Exception: # noqa: BLE001, S110 — backend probe failure means the default serves (fallback, not silence)
pass
return _SQLiteBackend()
def stats_for_path(db_path: str) -> dict:
"""stats() through the storage backend seam — the opt-in consumer the
parity battery and future Rust callers use. Same result dict shape."""
backend = storage_backend()
e = backend.read_query(db_path, "SELECT COUNT(*) FROM entries WHERE status='active'")[0][0]
w = backend.read_query(db_path, "SELECT COUNT(*) FROM working")[0][0]
a = backend.read_query(db_path, "SELECT COUNT(*) FROM audit_log")[0][0]
by_topic = {
r[0]: r[1]
for r in backend.read_query(
db_path,
"SELECT topic, COUNT(*) AS n FROM entries WHERE status='active' GROUP BY topic ORDER BY n DESC LIMIT 6",
)
if r[0]
}
return {"active_entries": e, "working": w, "audit": a, "top_topics": by_topic}
# ------------------------------------------------------------------ associations ----
@@ -2080,24 +1978,8 @@ def recall_with_associations(db, query, budget=RECALL_BUDGET, min_score=RECALL_M
ids = list(boost_map.keys())
placeholders = ",".join("?" * len(ids))
rows = db.execute(f"SELECT * FROM entries WHERE id IN ({placeholders}) AND status='active'", ids).fetchall()
boost_items = [{"id": eid, "strength": strength}
for eid, strength in sorted(boost_map.items(), key=lambda x: -x[1])]
return _merge_association_rows(base, boost_items, [dict(r) for r in rows], budget)
def _merge_association_rows(base, boost_items, rows, budget):
"""Pure association projection/boost/order/budget kernel (R2 W2.1)."""
core = _mr_core()
if core is not None:
return core.merge_association_rows(base, boost_items, rows, budget)
return _py_merge_association_rows(base, boost_items, rows, budget)
def _py_merge_association_rows(base, boost_items, rows, budget):
base = list(base)
by_id2 = {r["id"]: r for r in rows}
for item in boost_items:
eid, strength = item["id"], item["strength"]
for eid, strength in sorted(boost_map.items(), key=lambda x: -x[1]):
row = by_id2.get(eid)
if not row:
continue
@@ -2140,19 +2022,6 @@ def fused_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, ch
"""
entries = recall_with_associations(db, query, budget=budget, min_score=min_score)
research = chunk_recall(db, query, budget=chunk_budget, wing=wings, doc=None)
return _fuse_results(entries, research)
def _fuse_results(entries, research):
"""Pure fact/research projection, de-duplication and per-kind scale
alignment (R2 W2.1). Rust core when built; Python core is the oracle."""
core = _mr_core()
if core is not None:
return core.fuse_results(entries, research)
return _py_fuse_results(entries, research)
def _py_fuse_results(entries, research):
seen = set()
merged = []
for e in entries:
@@ -2206,13 +2075,6 @@ def _py_fuse_results(entries, research):
def _norm_key(text):
core = _mr_core()
if core is not None:
return core.norm_key(text)
return _py_norm_key(text)
def _py_norm_key(text):
"""Normalize text for cross-table de-duplication (entries vs chunks of the
same absorbed material may differ in whitespace/case)."""
return " ".join((text or "").lower().split())
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust memory_storage core (R2 W2.0).
Contract BUILDER-2026-10-04T15:54:17Z-cli. READ-ONLY by construction:
the only operation is read_query — the crate exports no write-capable
symbols (Nick's ruling: reads proven byte-identical before any writes
exist). SQLite is retained; the crate reaches it via system libsqlite3.
FFI contract (doc-envelope law): mems_read_query({db_path, sql, params})
-> [[cell, ...], ...]; mems_free_string(ptr). FFI segfault law: argtypes +
restype pinned per symbol. Override the .so with MEMS_RUST_LIB (FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "memory_storage"
_LIB_PATH = Path(
os.environ.get(
"MEMS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_storage.so"
)
)
_lib = None
class MemoryStorageUnavailable(FileNotFoundError):
"""The Rust core is not built (or MEMS_RUST_LIB points nowhere)."""
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise MemoryStorageUnavailable(
f"Rust memory_storage core not built: {_LIB_PATH} missing — run "
f"`cargo build --release` in {_CRATE_DIR}"
)
lib = ctypes.CDLL(str(_LIB_PATH))
lib.mems_free_string.argtypes = [ctypes.c_void_p]
lib.mems_free_string.restype = None
lib.mems_read_query.argtypes = [ctypes.c_char_p]
lib.mems_read_query.restype = ctypes.c_void_p # char* — never c_int
_lib = lib
return lib
def _call(payload: dict) -> dict:
"""Raw envelope: {"ok", "value"|"err"} — the error-honesty section of
the canary needs the envelope, not an exception."""
lib = _load()
raw = json.dumps(payload).encode("utf-8")
ptr = lib.mems_read_query(raw)
if not ptr:
raise RuntimeError("mems_read_query returned a null pointer")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.mems_free_string(ptr)
return json.loads(text)
def read_query(db_path: str, sql: str, params: list) -> list:
"""Read-only query through the Rust core; raises on the error envelope."""
env = _call({"db_path": db_path, "sql": sql, "params": params})
if not env.get("ok"):
raise RuntimeError(f"mems_read_query core error: {env.get('err')}")
return env["value"]
def read_query_result(db_path: str, sql: str, params: list) -> dict:
"""The raw envelope (error-honesty checks consume this)."""
return _call({"db_path": db_path, "sql": sql, "params": params})
-38
View File
@@ -1,38 +0,0 @@
"""ctypes loader for the zero-dependency memory_retrieval core (R2 W2.1)."""
from __future__ import annotations
import ctypes, json, os
from pathlib import Path
_ROOT=Path(__file__).resolve().parents[2]
_CRATE=_ROOT/"src/rust/memory_retrieval"
_LIB=Path(os.environ.get("MR_RUST_LIB",_CRATE/"target/release/libmemory_retrieval.so"))
_lib=None
def available(): return _LIB.is_file()
def _load():
global _lib
if _lib is not None:return _lib
if not _LIB.exists():raise FileNotFoundError(f"memory_retrieval core not built: {_LIB}")
lib=ctypes.CDLL(str(_LIB));lib.mr_call.argtypes=[ctypes.c_char_p];lib.mr_call.restype=ctypes.c_void_p
lib.mr_free_string.argtypes=[ctypes.c_void_p];lib.mr_free_string.restype=None;_lib=lib;return lib
def _call(op,arg):
# Send UTF-8 directly. ensure_ascii=True emits surrogate-pair escapes; the
# shared zero-dep parser treats those as independent code points, which
# would replace astral characters (emoji) and break Python char-count
# parity in chunking/normalization.
lib=_load();ptr=lib.mr_call(json.dumps({"op":op,"arg":arg},ensure_ascii=False).encode())
if not ptr:raise RuntimeError("mr_call returned null")
try:text=ctypes.cast(ptr,ctypes.c_char_p).value.decode()
finally:lib.mr_free_string(ptr)
env=json.loads(text)
if not env.get("ok"):raise RuntimeError(env.get("err"))
return env["value"]
def map_reduce(entries,chunk_size=10):return _call("map_reduce",{"entries":entries,"chunk_size":chunk_size})
def position_aware(entries):return _call("position_aware",{"entries":entries})
def fts_query_conjunctive(query):return _call("fts_query_conjunctive",{"query":query})
def chunk_text(text,size=1500,overlap=150):return _call("chunk_text",{"text":text,"size":size,"overlap":overlap})
def norm_key(text):return _call("norm_key",{"text":text})
def fuse_results(entries,research):return _call("fuse_results",{"entries":entries,"research":research})
def merge_association_rows(base,boosts,rows,budget):return _call("merge_association_rows",{"base":base,"boosts":boosts,"rows":rows,"budget":budget})
def rank_fusion(dense,bm25,rrf_k=60):return _call("rank_fusion",{"dense":dense,"bm25":bm25,"rrf_k":rrf_k})
def score_recall_rows(fusion,rows,dense,bm25,budget,min_score,now,half_life):return _call("score_recall_rows",{"fusion":fusion,"rows":rows,"dense":dense,"bm25":bm25,"budget":budget,"min_score":min_score,"now":now,"half_life":half_life})
def project_chunk_rows(ranks, rows, budget):
return _call("project_chunk_rows", {"ranks": ranks, "rows": rows, "budget": budget})
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust memory_scoring core (H3 slice 1, T4.4).
Contract BUILDER-2026-10-04T08:57:33Z-cli. Gives drift_core the Rust
scoring trio through the cdylib with no subprocess. No-clock law: `now` is
always an argument (ISO-8601 string) — the Python originals read the wall
clock for recency; callers pass it so parity is deterministic.
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* ms_compute_value({entry, goals, now}) -> number
* ms_check_promotion_eligibility({entry, now}) -> [eligible, reason]
* ms_pre_commit_gate_entry({entry}) -> {verdict, critics, reason}
* ms_free_string(ptr)
FFI segfault law: every symbol gets explicit argtypes + restype (c_void_p
for returned char* — the default c_int truncates 64-bit pointers).
Zero dependencies. The .so resolves against this repo's crate target dir;
override with MS_RUST_LIB (a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "memory_scoring"
_LIB_PATH = Path(
os.environ.get(
"MS_RUST_LIB", _CRATE_DIR / "target" / "release" / "libmemory_scoring.so"
)
)
_lib = None
class MemoryScoringUnavailable(FileNotFoundError):
"""The Rust core is not built (or MS_RUST_LIB points nowhere). Callers
fall back to the Python implementation — strangler law: the fallback is
load-bearing until deletion is proven."""
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.exists():
raise MemoryScoringUnavailable(
f"Rust memory_scoring core not built: {_LIB_PATH} missing — run "
f"`cargo build --release` in {_CRATE_DIR}"
)
lib = ctypes.CDLL(str(_LIB_PATH))
lib.ms_free_string.argtypes = [ctypes.c_void_p]
lib.ms_free_string.restype = None
for sym in ("ms_compute_value", "ms_check_promotion_eligibility",
"ms_pre_commit_gate_entry", "ms_get_compression_ratio",
"ms_compress_text", "ms_compress_entry", "ms_should_warn",
"ms_warn_before_compress", "ms_compute_priority",
"ms_qualifies_for_cache", "ms_score_gate", "ms_score_finish",
"ms_base_level_from_uses"):
fn = getattr(lib, sym)
fn.argtypes = [ctypes.c_char_p]
fn.restype = ctypes.c_void_p # char* — never default c_int
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
fn = getattr(lib, sym)
raw = json.dumps(payload).encode("utf-8")
ptr = fn(raw)
if not ptr:
raise RuntimeError(f"{sym} returned a null pointer")
try:
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
finally:
lib.ms_free_string(ptr)
env = json.loads(text)
if not env.get("ok"):
raise RuntimeError(f"{sym} core error: {env.get('err')}")
return env["value"]
def compute_value(entry: dict, goals: list | None, now: str) -> float:
"""Multi-factor value score (0.0-1.0+); now is an ISO-8601 string."""
return _call("ms_compute_value", {"entry": entry, "goals": goals, "now": now})
def check_promotion_eligibility(entry: dict, now: str) -> tuple:
"""(eligible, reason) — unpacked from the core's 2-element array."""
eligible, reason = _call("ms_check_promotion_eligibility",
{"entry": entry, "now": now})
return (eligible, reason)
def pre_commit_gate_entry(entry: dict) -> dict:
"""Three-critic promotion gate for a dict entry (no db context)."""
return _call("ms_pre_commit_gate_entry", {"entry": entry})
# ── H3 slice 2: drift_compression / drift_cache pure surfaces ────────────────
def get_compression_ratio(wash_count: float, rock_status: str) -> float:
return _call("ms_get_compression_ratio",
{"wash_count": wash_count, "rock_status": rock_status})
def compress_text(text: str, ratio: float) -> str:
return _call("ms_compress_text", {"text": text, "ratio": ratio})
def compress_entry(entry: dict) -> str:
return _call("ms_compress_entry", {"entry": entry})
def should_warn(entry: dict) -> bool:
return _call("ms_should_warn", {"entry": entry})
def warn_before_compress(entry: dict) -> dict:
return _call("ms_warn_before_compress", {"entry": entry})
def compute_priority(entry: dict) -> float:
return _call("ms_compute_priority", {"entry": entry})
def qualifies_for_cache(entry: dict) -> bool:
return _call("ms_qualifies_for_cache", {"entry": entry})
# ── H3 slice 3: warm scoring math ────────────────────────────────────────────
def score_gate(neuron: dict, low_text: str, session_context: str | None,
semantic_score: float) -> tuple:
"""(fires, total, keyword_hits) — unpacked from the 3-element array."""
fires, total, kw = _call("ms_score_gate", {
"neuron": neuron, "low_text": low_text,
"session_context": session_context, "semantic_score": semantic_score})
return (fires, total, kw)
def score_finish(neuron: dict, low_text: str, total: float,
b_i: float | None, cache_priority: float | None,
keyword_hits: float) -> float:
return _call("ms_score_finish", {
"neuron": neuron, "low_text": low_text, "total": total,
"b_i": b_i, "cache_priority": cache_priority,
"keyword_hits": keyword_hits})
def base_level_from_uses(used_at: list, now: str) -> float:
return _call("ms_base_level_from_uses", {"used_at": used_at, "now": now})
-63
View File
@@ -161,33 +161,7 @@ def load_rules():
return rules
def _cc_pg_core():
"""Rust cognition_core when available (persona kernels); pin with
RUNE_COGNITION_CORE=python."""
import os
if os.environ.get("RUNE_COGNITION_CORE", "auto") == "python":
return None
try:
from rune import cc_loader
except Exception: # noqa: BLE001 — loader probe failure means fallback
return None
try:
return cc_loader if cc_loader.available() else None
except Exception: # noqa: BLE001 — availability probe never crashes
return None
def _lex(text):
"""Distinctive content words (R2 W3.2: Rust kernel when built)."""
core = _cc_pg_core()
if core is not None:
return set(core.persona_lex(text))
return _py_lex(text)
def _py_lex(text):
"""Python oracle (parity floor)."""
"""Distinctive content words of a rule body (lowercase, >3 chars,
light-stemmed so plural/singular engage each other; preserves 3-char
ALL-CAPS acronyms so technical abbreviations match their expansion)."""
@@ -279,15 +253,6 @@ def _lex_input(text):
def _overlap(a, b):
"""Overlap ratio (R2 W3.2: Rust kernel when built)."""
core = _cc_pg_core()
if core is not None:
return core.persona_overlap(sorted(a), sorted(b))
return _py_overlap(a, b)
def _py_overlap(a, b):
"""Python oracle (parity floor)."""
if not a or not b:
return 0.0
return len(a & b) / max(1, min(len(a), len(b)))
@@ -373,34 +338,6 @@ def _semantic_engagement(text, rule):
def decide(text, rules=None):
"""The persona decides on a request, deterministically (R2 W3.2:
Rust kernel when built; the semantic cosines are gathered here —
embedder calls are IO — and passed into the kernel)."""
rules = rules if rules is not None else load_rules()
core = _cc_pg_core()
if core is not None:
text_s = (text or "").strip()
sems = [_semantic_engagement(text_s, r) for r in rules]
return core.persona_decide(text_s, [_rule_json(r) for r in rules],
PERMISSIVE_MODE, sems)
return _py_decide(text, rules)
def _rule_json(r) -> dict:
"""A rule in the kernel's JSON contract (citation rides along when
present so _cite stays data-driven)."""
return {
"text": r["text"], "words": sorted(r["words"]),
"core": r["core"], "priority": r.get("priority", 0),
"restrictive": r.get("restrictive", False),
"user_rule": r.get("user_rule", False),
"citation": r.get("citation"),
}
def _py_decide(text, rules=None):
"""Python oracle (parity floor) — the ORIGINAL decide body."""
"""The persona decides on a request, deterministically, from its own
stored rules. No generative LLM in the decision path (a local embedding
model may contribute semantic similarity). Returns a full verdict."""
+1 -9
View File
@@ -162,16 +162,8 @@ def recall(context, limit=4):
term = (context or "").lower()
if not term:
return []
from rune.identity_anchors import _cc_core
core = _cc_core()
if core is not None:
return core.pins_recall(_load(), term, limit)
return _py_pin_recall(_load(), term, limit)
def _py_pin_recall(recs, term, limit=4):
out = []
for rec in recs:
for rec in _load():
if rec.get("state") != "open":
continue
p = _project(rec)
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""query_memory.py — Query memory system for adaptive retrieval.
The system remembers what kinds of queries it gets, what retrieval
strategy worked, and adapts its gate logic accordingly.
This prevents dead weight — memories get tested based on usage
patterns, not just random exploration.
Research basis:
- IGMiRAG (arXiv:2602.07525): Intuition-guided retrieval
- Task-Adaptive Retrieval (arXiv:2604.07863): Learning from interaction history
- MemCon (arXiv:2607.13591): Learned policy for consolidation/forgetting
- Agent-Native Memory (arXiv:2606.24775): Workload-adaptive routing
Run under ~/.venvs/memory/bin/python3.
"""
import json
import os
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from typing import Dict, List, Optional, Tuple
QUERY_MEMORY_DIR = os.path.expanduser("~/.config/opencode/memory/query_memory")
QUERY_HISTORY_FILE = os.path.join(QUERY_MEMORY_DIR, "history.jsonl")
TOPIC_STATS_FILE = os.path.join(QUERY_MEMORY_DIR, "topic_stats.json")
# Topic keywords for classification
TOPIC_KEYWORDS = {
"guitar": ["guitar", "music", "teaching", "course", "lesson", "chord", "scale"],
"coding": ["code", "python", "script", "function", "debug", "build", "deploy"],
"memory": ["memory", "recall", "remember", "forget", "consolidat", "neuron"],
"persona": ["persona", "identity", "voice", "constitution", "character"],
"project": ["project", "task", "goal", "deadline", "milestone", "progress"],
"ai": ["ai", "model", "llm", "agent", "neural", "training", "inference"],
"personal": ["nick", "nottingham", "coffee", "coconut", "dog", "ttrpg"],
"system": ["system", "config", "install", "setup", "troubleshoot", "error"],
"research": ["research", "paper", "study", "analysis", "survey", "evidence"],
"creative": ["write", "design", "create", "build", "make", "art", "music"],
}
class QueryMemory:
"""Query memory system for adaptive retrieval."""
def __init__(self):
os.makedirs(QUERY_MEMORY_DIR, exist_ok=True)
self.history: List[dict] = self._load_history()
self.topic_stats: Dict[str, dict] = self._load_topic_stats()
def _load_history(self) -> List[dict]:
"""Load query history from disk."""
if not os.path.exists(QUERY_HISTORY_FILE):
return []
entries = []
with open(QUERY_HISTORY_FILE) as f:
for line in f:
try:
entries.append(json.loads(line.strip()))
except json.JSONDecodeError:
continue
return entries
def _save_history(self, entry: dict):
"""Append a query entry to history."""
with open(QUERY_HISTORY_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
def _load_topic_stats(self) -> Dict[str, dict]:
"""Load topic statistics from disk."""
if os.path.exists(TOPIC_STATS_FILE):
try:
with open(TOPIC_STATS_FILE) as f:
return json.load(f)
except json.JSONDecodeError:
return {}
return {}
def _save_topic_stats(self):
"""Save topic statistics to disk."""
with open(TOPIC_STATS_FILE, "w") as f:
json.dump(self.topic_stats, f, indent=2)
def classify_query(self, query: str) -> str:
"""Classify a query into a topic.
Returns the topic with the most keyword matches.
Falls back to "general" if no strong match.
"""
query_lower = query.lower()
scores = {}
for topic, keywords in TOPIC_KEYWORDS.items():
score = sum(1 for kw in keywords if kw in query_lower)
if score > 0:
scores[topic] = score
if not scores:
return "general"
return max(scores, key=scores.get)
def record_query(self, query: str, strategy: str, success: bool):
"""Record a query and its outcome.
Args:
query: The query text
strategy: "consolidated" or "full_retrieval"
success: Whether the retrieval produced useful results
"""
topic = self.classify_query(query)
entry = {
"query": query[:100],
"topic": topic,
"strategy": strategy,
"success": success,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
self.history.append(entry)
self._save_history(entry)
# Update topic stats
if topic not in self.topic_stats:
self.topic_stats[topic] = {
"total_queries": 0,
"successful_queries": 0,
"consolidated_hits": 0,
"full_retrieval_hits": 0,
"exploration_rate": 0.30, # default
}
stats = self.topic_stats[topic]
stats["total_queries"] += 1
if success:
stats["successful_queries"] += 1
if strategy == "consolidated":
stats["consolidated_hits"] += 1
else:
stats["full_retrieval_hits"] += 1
# Adaptive exploration rate: decrease for well-tested topics
if stats["total_queries"] >= 10:
success_rate = stats["successful_queries"] / stats["total_queries"]
if success_rate > 0.8:
stats["exploration_rate"] = max(0.10, stats["exploration_rate"] - 0.05)
elif success_rate < 0.5:
stats["exploration_rate"] = min(0.50, stats["exploration_rate"] + 0.05)
self._save_topic_stats()
def get_topic_stats(self) -> Dict[str, dict]:
"""Get topic statistics."""
return self.topic_stats
def adaptive_exploration_rate(self, query: str) -> float:
"""Get the exploration rate for a query based on its topic.
Well-tested topics have lower exploration rates.
Untested topics have higher exploration rates.
"""
topic = self.classify_query(query)
if topic in self.topic_stats:
return self.topic_stats[topic].get("exploration_rate", 0.30)
return 0.50 # untested topics get high exploration
def get_dead_weight_topics(self) -> List[str]:
"""Get topics with low proven uses (dead weight)."""
dead = []
for topic, stats in self.topic_stats.items():
if stats["total_queries"] < 5:
dead.append(topic)
return dead
def get_health_report(self) -> dict:
"""Get a health report of the query memory system."""
total_queries = len(self.history)
topics = len(self.topic_stats)
dead_topics = len(self.get_dead_weight_topics())
return {
"total_queries": total_queries,
"topics_tracked": topics,
"dead_weight_topics": dead_topics,
"topic_stats": self.topic_stats,
}
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Query memory system")
sub = parser.add_subparsers(dest="cmd")
# record <query> --strategy <s> --success
p_rec = sub.add_parser("record", help="Record a query")
p_rec.add_argument("query", nargs="+")
p_rec.add_argument("--strategy", default="consolidated")
p_rec.add_argument("--success", action="store_true")
# stats
sub.add_parser("stats", help="Show topic statistics")
# rate <query>
p_rate = sub.add_parser("rate", help="Get exploration rate for a query")
p_rate.add_argument("query", nargs="+")
# health
sub.add_parser("health", help="Show health report")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(1)
qm = QueryMemory()
if args.cmd == "record":
query = " ".join(args.query)
qm.record_query(query, args.strategy, args.success)
print(f"Recorded: {query[:50]} → {args.strategy} ({'success' if args.success else 'fail'})")
elif args.cmd == "stats":
stats = qm.get_topic_stats()
if stats:
print("Topic Statistics:")
for topic, s in stats.items():
print(
f" {topic}: {s['total_queries']} queries, {s['successful_queries']} success, rate={s['exploration_rate']:.2f}"
)
else:
print("No topic statistics yet.")
elif args.cmd == "rate":
query = " ".join(args.query)
rate = qm.adaptive_exploration_rate(query)
topic = qm.classify_query(query)
print(f"Query: {query[:50]}")
print(f"Topic: {topic}")
print(f"Exploration rate: {rate:.2f}")
elif args.cmd == "health":
report = qm.get_health_report()
print("Query Memory Health:")
print(f" Total queries: {report['total_queries']}")
print(f" Topics tracked: {report['topics_tracked']}")
print(f" Dead weight topics: {report['dead_weight_topics']}")
if __name__ == "__main__":
main()
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more