Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396ae120dc | ||
|
|
01b57297b8 | ||
|
|
497db5a144 | ||
|
|
9425ccc1f4 | ||
|
|
3ed547f86f | ||
|
|
5bc837c232 | ||
|
|
08bc1a6b62 | ||
|
|
201ca05b56 | ||
|
|
5cd90f2104 | ||
|
|
c8bc454e33 | ||
|
|
b81edb2029 | ||
|
|
87a890dce3 | ||
|
|
a0821be8a5 | ||
|
|
cb137bd37a | ||
|
|
72afb1a55a | ||
|
|
c9095f5af5 | ||
|
|
b5422f0772 | ||
|
|
da36b99184 | ||
|
|
873d0025ad | ||
|
|
a9ce967557 | ||
|
|
eb3a2c681c | ||
|
|
1bc1cdeaf0 | ||
|
|
31565844d7 | ||
|
|
c08b211f0e | ||
|
|
46c5032208 | ||
|
|
92584daeef | ||
|
|
bb5e243c88 | ||
|
|
ac936a8ba9 | ||
|
|
e6c8c5444d | ||
|
|
11270cb06a | ||
|
|
f8a568714b | ||
|
|
c3f81e60ec | ||
|
|
efb99d4682 | ||
|
|
489718ab1b | ||
|
|
9db580ac6d | ||
|
|
80ed978fbf | ||
|
|
3f119e50d8 | ||
|
|
517379313b | ||
|
|
643f9aa9d1 | ||
|
|
61a5082633 | ||
|
|
9121bbd8b0 | ||
|
|
cba9045982 | ||
|
|
0c92f482a7 | ||
|
|
3120e8ae08 | ||
|
|
ea3c20e205 | ||
|
|
a93327d107 | ||
|
|
e08625f629 | ||
|
|
021fef2065 | ||
|
|
3788875410 | ||
|
|
d96b89bd4c | ||
|
|
fe45c6a8dd | ||
|
|
c7b2edd4fc |
No files matched your search
@@ -1,16 +1,46 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Fix a bug or issue
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
@@ -29,10 +59,6 @@ assignees: conductor
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
|
||||
understand the goal without decoding internals. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete example of the intended behavior, in plain terms:
|
||||
"If X happens, the system will Y instead of Z." -->
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
@@ -37,7 +63,6 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
@@ -69,4 +94,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -11,7 +11,7 @@ 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'
|
||||
if: github.event_name == 'issues' && github.event.action != 'labeled' && github.event.issue.state == 'open'
|
||||
runs-on: rune-infra-pi
|
||||
steps:
|
||||
- name: Enforce priority label
|
||||
@@ -50,6 +50,20 @@ jobs:
|
||||
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();
|
||||
@@ -58,9 +72,43 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'priority: high', 'bug', 'feature',
|
||||
'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.
|
||||
let isBug = false, isFeature = false, isResearch = false;
|
||||
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
|
||||
if (kindMatch) {
|
||||
const kind = kindMatch[1];
|
||||
isBug = kind === 'bug';
|
||||
isFeature = kind === 'feature';
|
||||
isResearch = kind === 'research';
|
||||
} else if (action === 'opened') {
|
||||
// KEYWORD FALLBACK (#407): pre-template issues only, and
|
||||
// ONLY at filing time. Re-running keyword guesses on every
|
||||
// edit/label event overrides human curation of spine-less
|
||||
// issues (documented rule: keyword heuristics are the
|
||||
// pre-template fallback, not a standing authority).
|
||||
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
isBug = textBug;
|
||||
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
|
||||
}
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
if (isResearch) {
|
||||
desiredLabels.add('research');
|
||||
}
|
||||
|
||||
// Umbrella labels
|
||||
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
|
||||
desiredLabels.add('memory');
|
||||
@@ -92,16 +140,6 @@ jobs:
|
||||
desiredLabels.add('priority: high');
|
||||
}
|
||||
|
||||
// Type labels
|
||||
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
const current = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -58,3 +58,26 @@ fix, full pipeline re-entry (suite + ratchet + ruff + canaries + henxels;
|
||||
gates for anything beyond a mechanical minor), regression tests frozen forever,
|
||||
discharge recorded finding → test → commit. Symptom patches are rejected at
|
||||
review. To escape: edit `henxels.yaml` or this file — visibly.
|
||||
|
||||
## Issue organisation standard (binding, 2026-09-19)
|
||||
|
||||
One open note per idea. Issues use the template spine (kind, subsystem,
|
||||
version intent) with a plain-English heading that restates the subject —
|
||||
no meta-labels. Before minting a standalone issue, check whether the work
|
||||
belongs inside an existing parent:
|
||||
|
||||
- **Parked ideas under a blocked/future epic** are filed as checklist items
|
||||
in the parent's "Absorbed children" section, NOT as standalone issues.
|
||||
Absorption is reversible — re-mint standalone when the idea becomes live.
|
||||
- **Live, dependency-tracked, or starting work** gets a standalone issue.
|
||||
- **Closures state the reason as data**: `closed_because` in the spine
|
||||
(completed / superseded-by-#N / absorbed-into-#N / wont-do / duplicate),
|
||||
plus a standard closing comment (plain summary first).
|
||||
- The reconciliation checker (`scripts/gitea_reconciliation_check.py`) must
|
||||
stay green after any issue state change; the artifact is updated in the
|
||||
same change that moves issues.
|
||||
|
||||
## Async continuation final flow proofs (R13/M9) — 2026-09-19
|
||||
|
||||
PR-flow live proof marker: harmless documentation addition exercising the
|
||||
canonical single-continuation path on the PC-local forge. No code change.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.86.0**
|
||||
**Current release: v0.89.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.**
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { existsSync, appendFileSync } from "node:fs"
|
||||
|
||||
// builder-bridge — Rune Core preflight for the MiMo builder (handoff
|
||||
// 2026-09-10 §3–§7, Option C bridge tranche). For each user turn, the hook
|
||||
// extracts the user's message from the outgoing LLM request, runs the Rune
|
||||
// Core builder-bridge preflight (deterministic: route classification +
|
||||
// memory selection + active-Scaffolding projection, ZERO model calls), and
|
||||
// appends a compact capsule to the final user message. Same Rune Core the
|
||||
// TUI/operator path uses — this file is a thin adapter with no cognition.
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/builder-bridge.ts
|
||||
// Installed to ~/.mimocode/hooks/builder-bridge.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detecting — the same
|
||||
// contract as event-wait-guard.ts). T22: this file was previously
|
||||
// UNVERSIONED and hardcoded an ephemeral interpreter path (/tmp/ci-venv),
|
||||
// which silently killed the whole edge; both defects are repaired here.
|
||||
//
|
||||
// Overhead contract (§7/§8): capsule ≤ ~300 tokens; a repeat/empty task
|
||||
// costs ~0ms (TTL cache); a fresh task ~150ms (bounded retrieval). The hook
|
||||
// degrades silently to no-injection on any failure — it must never block
|
||||
// the builder.
|
||||
//
|
||||
// Injection policy: only append when the capsule is non-empty. A plain
|
||||
// "hello" gets nothing (§5: if nothing relevant exists, inject nothing).
|
||||
|
||||
const BRIDGE = "/mnt/workdrive/issue161-harness/src/rune/builder_bridge_cli.py"
|
||||
const MARKER_PREFIX = "/tmp/rune-bridge-"
|
||||
|
||||
// Interpreter resolution (T22 repair): the previous hardcoded
|
||||
// "/tmp/ci-venv/bin/python" is an EPHEMERAL path that no longer exists, so
|
||||
// every bridge call failed and the hook silently degraded — the MiMo edge
|
||||
// received no Rune Core preflight at all (zero fire-log lines). Resolve
|
||||
// against durable candidates, first existing wins; env override wins over
|
||||
// all of them. If nothing exists the hook degrades as before.
|
||||
const PY_CANDIDATES = [
|
||||
process.env.MIMO_BRIDGE_PY,
|
||||
"/mnt/workdrive/issue161-harness/.test-venv/bin/python",
|
||||
"/home/nick/.venvs/memory/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
].filter((p) => typeof p === "string" && p.length > 0)
|
||||
|
||||
function resolvePy() {
|
||||
for (const p of PY_CANDIDATES) {
|
||||
try {
|
||||
if (existsSync(p)) return p
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Per-process dedupe: identical task text within the cache TTL does not need
|
||||
// a second bridge call — the CLI has its own TTL cache, but skipping the
|
||||
// process spawn entirely for the same message text within 10 minutes keeps
|
||||
// repeats at ~0ms.
|
||||
const lastText = new Map() // text -> timestamp
|
||||
const TTL = 600_000
|
||||
|
||||
function extractUserText(messages) {
|
||||
// messages: Array<{ info: Message, parts: Part[] }> — newest last.
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (m?.info?.role !== "user") continue
|
||||
const texts = []
|
||||
for (const p of m.parts ?? []) {
|
||||
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
|
||||
}
|
||||
if (texts.length) return texts.join("\n").slice(0, 4000)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export default {
|
||||
"experimental.chat.messages.transform": async (input, output) => {
|
||||
try {
|
||||
if (!Array.isArray(output.messages) || output.messages.length === 0) return
|
||||
const userText = extractUserText(output.messages)
|
||||
if (!userText || userText.length < 12) return // trivial/noise guard
|
||||
// Harness-internal steps (checkpoint writer, compaction, system
|
||||
// reminders) are not builder tasks — no capsule (measured: the fire
|
||||
// log showed checkpoint-writer steps receiving capsules 2026-09-10).
|
||||
if (/^(<system-reminder>|EXTRACTION REQUIRED)/.test(userText.trim())) return
|
||||
const now = Date.now()
|
||||
const prev = lastText.get(userText)
|
||||
if (prev !== undefined && now - prev < TTL) return // dedupe repeats
|
||||
lastText.set(userText, now)
|
||||
if (lastText.size > 64) {
|
||||
// evict oldest
|
||||
let oldestKey = null, oldestVal = Infinity
|
||||
for (const [k, v] of lastText) if (v < oldestVal) { oldestVal = v; oldestKey = k }
|
||||
if (oldestKey) lastText.delete(oldestKey)
|
||||
}
|
||||
|
||||
const py = resolvePy()
|
||||
if (!py) return // no durable interpreter — degrade (never block)
|
||||
|
||||
const capsule = await new Promise((resolve) => {
|
||||
execFile(py, [BRIDGE, userText], { timeout: 15_000 }, (err, stdout) => {
|
||||
resolve(err ? "" : (stdout?.toString() ?? ""))
|
||||
})
|
||||
})
|
||||
if (!capsule.trim()) return
|
||||
|
||||
// Durable fire-trace: one line per ACTUAL injection into an outgoing
|
||||
// model request (handoff §1 proof requirement). Not a canary — this is
|
||||
// standing observability for the bridge's consumer edge.
|
||||
try {
|
||||
appendFileSync("/tmp/rune-bridge-fire.log",
|
||||
JSON.stringify({ ts: new Date().toISOString(), task: userText.slice(0, 80), capsule_tokens: Math.round(capsule.length / 4), py }) + "\n")
|
||||
} catch {}
|
||||
|
||||
// Append the capsule to the LAST user message (the one being answered).
|
||||
const last = output.messages[output.messages.length - 1]
|
||||
if (last?.parts && Array.isArray(last.parts)) {
|
||||
last.parts.push({
|
||||
type: "text",
|
||||
text: "\n\n<rune-preflight>\n" + capsule.trim() + "\n</rune-preflight>\n(Injected by Rune Core builder bridge — deterministic preflight, zero model calls. Relevant state above; ignore if not applicable to this task.)",
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// never block the builder on bridge failure
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"native_units",
|
||||
"openai_compat_transport",
|
||||
"provider_context",
|
||||
"recall_uplift",
|
||||
|
||||
@@ -11,24 +11,22 @@
|
||||
"NEEDS_REVALIDATION",
|
||||
"DEFERRED_FUTURE"
|
||||
],
|
||||
"live_defects": [
|
||||
324
|
||||
],
|
||||
"live_defects": [],
|
||||
"records": [
|
||||
{
|
||||
"number": 324,
|
||||
"title": "Engine defect: checkpoint writer fails deterministically on aborted worker sessions (MessageAbortedError -> empty checkpoint shells)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"bug",
|
||||
"custodian",
|
||||
"life",
|
||||
"memory"
|
||||
],
|
||||
"live_defect": true,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 defect no longer reproduces: zero MessageAbortedError in current logs (incl. active worker), six most recent session checkpoints full documents (111-396 lines); verified by present-absence 2026-09-19"
|
||||
},
|
||||
{
|
||||
"number": 316,
|
||||
@@ -237,8 +235,8 @@
|
||||
{
|
||||
"number": 183,
|
||||
"title": "Future: typed temporal semantics for facts, relationships, beliefs, and preferences",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -250,14 +248,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 182,
|
||||
"title": "Future: GitHub code-search prior-art audit for graph and vector memory",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -266,8 +264,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 181,
|
||||
@@ -287,13 +285,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #182 #183 + research watchlist #164 #165 #166; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 179,
|
||||
"title": "Future: succession/privacy semantics and lineage evaluation",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -305,14 +303,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 178,
|
||||
"title": "Future: evidence-grounded ancestral perspective with explicit uncertainty",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -322,14 +320,14 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 177,
|
||||
"title": "Future: temporal autobiographical model with provenance and reinterpretation lineage",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -340,14 +338,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 176,
|
||||
"title": "Future: lineage-aware situated curiosity for meaningful autobiographical gaps",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"feature",
|
||||
@@ -358,8 +356,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 175,
|
||||
@@ -379,13 +377,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #176-#179 as checklist items; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 166,
|
||||
"title": "RESEARCH WATCHLIST: Memory-R1 (RL memory ops), Titans (test-time memory), Vespa/Weaviate/Milvus (retrieval competitors)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -394,14 +392,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 165,
|
||||
"title": "RESEARCH: SimpleMem semantic compression + A-MEM dynamic linking + Letta sleep-time compute",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"infrastructure",
|
||||
@@ -410,14 +408,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 164,
|
||||
"title": "RESEARCH: HippoRAG 2 \u2014 PPR-style associative propagation as warm-neuron activation candidate",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -426,27 +424,27 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 163,
|
||||
"title": "Temporal graph backend spike: Neo4j vs FalkorDB (LadybugDB experimental; Graphiti as reference)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "run the backend spike; compare on same corpus/query set; record evidence-backed recommendation"
|
||||
},
|
||||
{
|
||||
"number": 162,
|
||||
"title": "Qdrant associative-memory projection spike (dense/sparse/multivector + ColBERT late-interaction reranking)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"custodian",
|
||||
@@ -454,8 +452,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #316 (ADAPT-1 B3-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 159,
|
||||
@@ -478,8 +476,8 @@
|
||||
{
|
||||
"number": 158,
|
||||
"title": "Generated wiring-status / architecture-truth report",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P2 \u2014 medium",
|
||||
"growth",
|
||||
@@ -489,8 +487,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #315 (ADAPT-1 B2-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 156,
|
||||
@@ -1151,6 +1149,6 @@
|
||||
"next_action": ""
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
# Language-Fit Rubric — Wave 1 (Language-Rebasing Tranche R1)
|
||||
|
||||
> STATUS: FINAL — self-verified 2026-09-29 per tranche §12 (all 6 rounds completed,
|
||||
> evidence-sufficiency statements recorded, kill tests frozen; execution proceeds on the
|
||||
> evidence, every RECODE still passes its benchmark gate — no win, no recode).
|
||||
> Per-subsystem sections filled from live research rounds (≥2 live-verified external sources per claim,
|
||||
> per-source lessons, evidence→decision mapping, kill test per candidate). No code changes in Wave 1.
|
||||
> Governing law from §11: **KEEP-PYTHON honesty determines how much of the rebase volume is real.**
|
||||
> Toolchain baseline: Rust 1.98.1 + GCC 16.2.1 present; Go NOT installed — Go earns installation only
|
||||
> through rubric evidence here.
|
||||
|
||||
## Verdict (all rounds, one paragraph)
|
||||
|
||||
**KEEP-PYTHON across the board: 40 clusters scored, 0 recode units awarded.** Six research
|
||||
rounds (W2 leaf utilities, W3 memory, W4 provider-layer, W5 orchestration, W6 runtime-core,
|
||||
W7 TUI) each characterized their subsystem's actual workload inward (file:line-anchored) and
|
||||
tested Python/Rust/Go/C against it with live-verified sources and frozen kill tests. The
|
||||
pattern is consistent: the compute-shaped work already executes in C (hashlib, sqlite3/FTS5/vec0,
|
||||
json, regex), the dominant latency is network or LLM-bound, and no cluster contains a hot loop
|
||||
that pays for a process-boundary recode through the 658 µs–0.63 ms interop crossing. **Go:
|
||||
REJECTED — does not earn installation this tranche** (D5=0 everywhere; nothing measured or
|
||||
fetch-confirmed earns it; orchestration additionally rejects its design fit; K-GO kill tests
|
||||
frozen per round should the case be revived). **Rust: toolchain-ready, justified nowhere this
|
||||
tranche** — keep as the standing option behind the CI-proven std-only adapter for any future
|
||||
measured hot loop. **C: rejected everywhere scored** (duplicates C already inside SQLite/hashlib;
|
||||
adds an unsafe-memory surface). The rebase volume the tranche hypothesized is real only as a
|
||||
small set of named improvements — see "Wave-plan consequence" below.
|
||||
|
||||
## Rubric dimensions (applied to every candidate language per subsystem)
|
||||
|
||||
| Dimension | Question | Evidence required |
|
||||
|---|---|---|
|
||||
| D1 Workload fit | Does the subsystem's dominant workload (I/O-bound, CPU-bound, FFI, protocol, UI) actually favour this language? | Benchmarks or authoritative docs, live-verified |
|
||||
| D2 Measured gain | What wins, concretely — latency, memory, startup, binary distribution? | Named hot paths from the Wave 0 inventory + numbers |
|
||||
| D3 Interop cost | What does the interop contract (§5) cost to carry for this boundary? | Adapter surface size, ABI/build complexity |
|
||||
| D4 Ecosystem | Do the needed libraries exist and are they maintained? | Live-verified crate/package/module listings |
|
||||
| D5 Toolchain reality | Is the toolchain present (or installable and justified) on this machine and the CI runner? | Runner + local `--version` evidence |
|
||||
| D6 Maintenance continuity | Can the Rune ecosystem's single-maintainer loop sustain code in this language here? | Honest judgement, stated plainly |
|
||||
| D7 Kill test | What is the CHEAPEST disproof of this candidate? | One falsifiable early test, frozen before any recode |
|
||||
|
||||
Scoring: each candidate gets D1–D6 scored 0–2 with one-line evidence each, D7 written regardless.
|
||||
Decision per subsystem: **KEEP-PYTHON** / **RECODE→<language>** — KEEP-PYTHON is the expected default;
|
||||
a RECODE that cannot pass its own kill test reverts to KEEP-PYTHON before implementation.
|
||||
|
||||
---
|
||||
|
||||
## W3 — Memory & persistence (units: memory_store 56 fns, curator 43, warm_router 32, memory_consolidation 20, sleep_time 25, + graph_memory, night_mode, identity_anchors)
|
||||
|
||||
- **Research round actor:** general-2 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/memory/`: inward.md, outward/ ×6, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** JSONL-heavy appends, BM25/FTS ranking, consolidation passes, long-running
|
||||
scheduled jobs, file-backed state.
|
||||
- **Decision:** **KEEP-PYTHON (all 7 clusters, zero RECODE).** Cluster verdicts:
|
||||
(1) hot-store JSON persistence — KEEP with upgrade path: orjson in-process wheel
|
||||
(~10× dumps / 2× loads); a bespoke Rust CLI unit is dead on spawn cost (the
|
||||
`shared/native_units.py` crossing ~1–5 ms ≈ orjson's entire 3.4 MB `knowledge.json` decode);
|
||||
(2) SQLite store + FTS5/vec0 recall — KEEP, the compute is already C inside SQLite
|
||||
(`memory_store.py:646` vec0 MATCH, `:693` bm25); (3) neuron scoring/routing — KEEP,
|
||||
network-bound `_embed` (Ollama subprocess curl) dominates; (4) graph BFS — KEEP,
|
||||
hops=1 default, per-node SQL, tiny N; the future fix is a SQL CTE, not a language;
|
||||
(5) batch consolidation/curator — KEEP, LLM/I-O-bound and latency-insensitive,
|
||||
asyncio sweet spot; (6) orchestration/glue — KEEP, pure glue pinned by the Python-TUI
|
||||
interop contract; (7) embedding transport — KEEP, network-bound.
|
||||
- **Candidate matrix summary:** Python 7/7 · Go 0/7 (not installed, nothing measured or
|
||||
fetch-confirmed earns installation; Go's JSON stack mid-rewrite upstream) · hand-written C
|
||||
0/7 (C already battle-tested inside SQLite — hand-writing would duplicate it) · Rust 0/7
|
||||
as CLI units (spawn cost kills the one credible vector; in-process wheels excluded by
|
||||
Wave 0 policy).
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-JSON — orjson microbench on live 3.4 MB
|
||||
`knowledge.json` (if drop-in and ≥2× load / ≥5× dump, the CLI unit is disproven on spawn
|
||||
cost); Rust-general — kill unless a profile shows ≥50% wall time in pure Python compute
|
||||
(none exists; reopen only on materially changed workload); Go — instrument one
|
||||
`consolidate_all` run, kill if Python-compute share <50% (it will be — LLM/I-O bound);
|
||||
C — kill unless a hot kernel absent from the SQLite extension ecosystem is identified
|
||||
(none).
|
||||
- **Sources + lessons:** 6 live-fetched 2026-09-29 (orjson, sled, sqlite-vec, SQLite FTS5
|
||||
docs, Go json/v2 rewrite issue, pydantic-core) — full annotations in
|
||||
`research/wave1/memory/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** static analysis only, no runtime profiling;
|
||||
memory.store SQLite DB not materialized locally — kill tests specify *measured*
|
||||
thresholds rather than assertions. Verdicts rest on "Python is glue around C/network,"
|
||||
which profiling would only strengthen.
|
||||
|
||||
## W4 — Provider/model layer (units: provider_registry 28, model_connect 10, model_routing 15, model_profiles 15, provider_budget 21, + standardized_providers, model_registry, connection_pool)
|
||||
|
||||
- **Research round actor:** general-5 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/provider-layer/`: inward.md, outward-sources.md, outward/ (8 evidence
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** HTTP/SSE streaming, connection pooling, retries/backoff, protocol parsing,
|
||||
tool-call reliability.
|
||||
- **Intention profile correction (inward-verified):** the layer has **NO retry/backoff and NO
|
||||
SSE stub at all** (grep-verified) — `stream: False` everywhere, ~900 lines, remote-LLM-latency-
|
||||
bound. Rewrite pitches are therefore feature-adds, not ports.
|
||||
- **Candidate matrix:** 5 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) HTTP wire + transport (provider_registry, openai_compat_transport, model_connect
|
||||
discovery) Python 5 / Go 2 / Rust 1; (C2) connection pool (connection_pool.py, 144 lines —
|
||||
in-process object guards under threading.Lock, NOT a TCP pool) Python 5 / Go 2 / Rust 2;
|
||||
(C3) routing decisions (model_routing.py, 432 lines, pure decision logic, one httpx call)
|
||||
Python 5 / Go 1 / Rust 1; (C4) budget accounting (provider_budget.py, 354 lines, pure
|
||||
arithmetic + lock, no I/O) Python 5 / Go 1 / Rust 1; (C5) config/registry/normalization
|
||||
(model_profiles, model_registry, standardized_providers, model_connect tables — dataclasses +
|
||||
JSON, editable by non-systems programmers) Python 5 / Go 1 / Rust 1.
|
||||
- **Decision:** **KEEP-PYTHON (all 5 clusters).**
|
||||
- **Kill tests:** C1 Go — KILLED: toolchain absent (`which go` → not found); 28 call-site
|
||||
behaviors incl. the tool-call envelope contract (openai_compat_transport.py:67-107, 160-207)
|
||||
would be re-implemented with zero incumbent test port and NO performance signal (remote-LLM-
|
||||
latency-bound, kilobyte payloads). Re-earn path if Go is ever installed: std-only proof via
|
||||
`interop/rebase-proof/` + a benchmark harness on C1 only. C1 Rust — KILLED BY CONSTRUCTION:
|
||||
SSE parsing requires reqwest+tokio+eventsource-stream+nom (outward O3/O4/O5 — even mistral.rs
|
||||
takes the tokio stack), violating the std-only interop contract. C2–C5 Go/Rust — KILLED:
|
||||
burden of proof sits on the rewrite candidates; kill tests need contention/throughput/
|
||||
counter-evidence that does not exist; routing runs once per request at millisecond scale
|
||||
against multi-second LLM calls (5 orders of magnitude below any measurable threshold).
|
||||
- **Sources + lessons:** 8 live-fetched (O1–O8), ≥2 per major claim, coverage map in
|
||||
`outward-sources.md`. Lessons: (1) stdlib-only Rust has no realistic precedent for HTTP/SSE
|
||||
workloads — the interop contract's zero-crate standard effectively decides Rust out of all
|
||||
network-wire clusters; (2) Go's stdlib HTTP+NDJSON streaming is capability-proven
|
||||
(pkg.go.dev/net/http, ollama api/client.go, ollama's Go API server) — Go's losses here are
|
||||
evidence-absence, not capability-absence; (3) the honest reliability upgrade for this layer
|
||||
is a small Python-level httpx retry/timeout wrapper — a feature-add, not a language change.
|
||||
|
||||
## W5 — Orchestration, authority, plugins, skills (units: orchestration/ 44 files 244 fns, contracts, build_mode 34, construction_authority*, verification_construction, spine, + governance surfaces: drift_*, persona_gate, safety_override, witness, behavior, growth_delta, claim_audit)
|
||||
|
||||
- **Research round actor:** general-1 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/orchestration/`: inward.md, outward-sources.md, outward/ (4 search
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** contract/lifecycle state machines, authority gating, fail-closed validation,
|
||||
HMAC-chained ledgers, policy dispatch.
|
||||
- **Intention profile correction (inward-verified):** the subsystem is durable-ledger I/O,
|
||||
authority wiring, deterministic policy gates, and TUI-facing coordination — NOT hot loops
|
||||
(2 RLocks, 0 sockets, 0 thread pools across 43 files). **Scope gap: `construction_authority.py`
|
||||
and `verification_construction.py` do not exist in the tree** — the construction cluster is
|
||||
covered by build_mode.py; no evidence invented for the missing files.
|
||||
- **Candidate matrix:** 6 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) durable state & ledger stores (event_store.py:56-101, atomic.py, evidence_store.py
|
||||
— fsync-bound; hashing already C via hashlib) Python 5; (C2) composition roots & authority wiring
|
||||
(composition.py, authority.py, cognitive.py — pure glue, runtime cost ≈ 0) Python 5; (C3)
|
||||
policy/state machines/gates (lifecycle.py, scheduler.py, broker.py, no_change.py, reflex.py —
|
||||
deterministic dict/enum decisions, sub-ms; testability is the requirement) Python 5; (C4)
|
||||
structural self-model & schema tooling (spine.py:24-25, microkernel.py, contracts.py, records.py
|
||||
— introspection + canonical JSON; only candidate for a future Rust spine-diff if snapshots grow
|
||||
1000×) Python 4; (C5) TUI-facing orchestration & build pipeline (build_mode.py 11-stage pipeline,
|
||||
gateway.py, transport.py — the consumer IS the Python TUI; latency dominated by LLM/subprocess
|
||||
waits) Python 5; (C6) benchmark/migration harnesses (retrieval_benchmark.py, migrate.py,
|
||||
benchmark.py — offline tooling, recode buys nothing, loses iteration speed) Python 4.
|
||||
- **Decision:** **KEEP-PYTHON (all 6 clusters). This subsystem contributes 0 recode units to
|
||||
the wave plan.** Go: REJECTED, do not install — differentiator (goroutine-dense servers)
|
||||
targets a workload absent here; cost is exception→error-value rewrite of ~260 functions.
|
||||
Rust: FEASIBLE (toolchain proven) but UNJUSTIFIED anywhere — no cluster has the hot loop
|
||||
that pays for a process-boundary recode; keep as toolchain-ready for other subsystems.
|
||||
C: REJECTED — the only compute-shaped work (sha256) already executes C via stdlib hashlib.
|
||||
- **Kill tests:** K1 (frozen, ALREADY RUN): 100 round-trips through the rebase-proof adapter —
|
||||
mean 0.63ms, p95 0.74ms, cold 0.87ms, 100% exit-0 + ok:true → adapter overhead does NOT block
|
||||
recodes on cost; recodes die on value grounds. Re-open any cluster if profiling shows a
|
||||
function consuming >10% of a real interactive session. K-GO (frozen, ~30 min): Go prototype
|
||||
of event_store.py's batched append+fsync vs Python at 1000 events; if Python's p95 is within
|
||||
2× of Go's, Go is dead here permanently. K-RUST (frozen, ~20 min): measure spine diff on a
|
||||
10k-entity snapshot; <50ms kills any Rust spine recode by absence of pain.
|
||||
- **Sources + lessons:** 4 live-fetched web sources (pydantic-core, ruff blog, go.dev FAQ,
|
||||
Python 3.14 hashlib docs) + 1 LOCAL EXPERIMENT (n=100 adapter benchmark). Lessons: (1) the
|
||||
rebase-proof adapter round-trips at 0.63ms/op — subprocess-per-op is viable for coarse ops,
|
||||
never per-event; (2) Python's "slowness" argument dissolves where the actual compute already
|
||||
runs in C via stdlib (hashlib, sqlite) — the language question is really "where is the hot
|
||||
loop", and here the answer is nowhere; (3) assumed limits flagged honestly: human-scale event
|
||||
cadence (batch_size=8) and Linux spawn cost — both would need revisit at 10⁴+ events/sec or
|
||||
cross-platform targets.
|
||||
|
||||
## W6 — Runtime core & tooling (units: tool_executor 46, tool_protocol 27, local_tool_runtime 32, capability_broker 19, context_broker 21, work_manager 25, cron_engine 28, + failure_auto_filer 28, hook_engine, elastic_cognition, auto_tasks, goal)
|
||||
|
||||
- **Research round actor:** general-4 (COMPLETED 2026-09-29 on disk despite cancellation
|
||||
notification — artifacts at `research/wave1/runtime-core/`: inward.md, outward/,
|
||||
outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** subprocess/process supervision, native execution boundary (native_units
|
||||
adapter already exists), event loops, cron scheduling, hook dispatch.
|
||||
- **Decision:** **STAY-PYTHON (all 6 clusters).** Round scores (1–5 = fit for the cluster's
|
||||
actual inward-characterized workload): (A) subprocess lifecycle (tool_executor, cron_engine,
|
||||
auto_tasks) 4.7 — `subprocess.run(timeout=, capture_output=)` is a one-call expression of the
|
||||
pattern; Rust std::process has NO built-in child timeout (outward-verified), strictly more
|
||||
code under zero-crate; (B) protocol/JSON parsing (tool_protocol, brokers) 4.5 — inputs are
|
||||
KB-scale model fragments; Python json/regex is already C-backed, so a rewrite swaps C for
|
||||
compiled code at the same asymptotic cost; serde's typed-struct model actively mismatches
|
||||
fence-wrapped schema-flexible output; (C) model→tool control loop + HTTP
|
||||
(local_tool_runtime, elastic_cognition, goal) 4.8 — wall time is LLM HTTP latency in seconds;
|
||||
Python overhead is µs–ms noise; the loop's content is policy (circuit breaker, escalation
|
||||
ladders, routing tables); (D) context assembly (context_broker) 4.7 — correctness lives in
|
||||
admission policy and degrade-not-crash semantics, not speed; compaction is itself an LLM call;
|
||||
(E) reliability/queueing (failure_auto_filer) 4.2 — honest lowest: on paper the best Rust
|
||||
candidate (queue + worker + crash-safe JSONL spool), but perf requirement ≈ nil and the
|
||||
correctness burden is threading semantics a rewrite would re-risk for no win ("nothing
|
||||
breaks today"); (F) declarative config/brokers (work_manager, hook_engine, capability_broker)
|
||||
4.9 — literal data, no-op runtime, portable to anything valuable in nothing.
|
||||
- **Candidate matrix summary:** Python 6/6 · Go 0/6 (paper verdicts only — evidence-starved,
|
||||
not installed, kill tests unrunnable locally) · Rust 0/6 (kill tests runnable via the
|
||||
existing std-only interop harness) · C unscored (no FFI-adjacent cluster surfaced).
|
||||
Best-language moment found anywhere: robfig/cron replacing ~37 hand-rolled cron-parsing
|
||||
lines (cron_engine.py:199-236) — ergonomics nicety, not a fit verdict.
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-A — re-express `_run_command`
|
||||
(tool_executor.py:1064) with std::process + manual timeout; kill if >2× the ~40-line
|
||||
Python wrapper (Q4 predicts it is); Go-A — port `cron_engine._run_loop` with os/exec,
|
||||
measure task-fire latency delta (prediction: zero — 60 s sleep and child runtime dominate);
|
||||
B — parse the largest observed tool-protocol stream 1000× in Python; kill if total <1 s
|
||||
(no rewrite yields an observable win); C — instrument `_run_loop`
|
||||
(local_tool_runtime.py:417), log non-HTTP CPU per iteration (prediction <1% of wall);
|
||||
D — time `assemble`+`_admit` on the largest stored context, kill if <10 ms; E — fault-inject
|
||||
kill -9 during spool append; if recovery semantics already hold, the rewrite has no
|
||||
correctness win to buy; F — none needed: absence of any win condition IS the disproof.
|
||||
- **Sources + lessons:** live-fetched 2026-09-29 — annotations in
|
||||
`research/wave1/runtime-core/outward-sources.md`; headline lessons: Rust std::process lacks
|
||||
child timeout (Q4); Go os/exec reaches parity but changes only syntax (Q1); Python
|
||||
json/regex C-backed (Q5b); serde typed structs mismatch dynamic model output (Q5a);
|
||||
concurrency features irrelevant at ≤2 threads (Q6).
|
||||
- **Evidence sufficiency:** Python verdicts fully evidenced inward+outward; Go execution-side
|
||||
evidence honestly starved (paper kill tests carried forward); E's JSONL-spool crash-safety
|
||||
claim rests on inward semantics only — flagged in synthesis.md.
|
||||
|
||||
## W7 — Operator + TUI (units: rune_operator 61 fns, rune_tui 161 fns — largest module, tui_validator)
|
||||
|
||||
- **Research round actor:** general-3 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/tui/`: inward.md, outward/ ×8, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** terminal rendering, input handling, rich layout, hot store state, supervisory
|
||||
command surface. Design decision 4 holds: TUI consumes the interop contract, never rewritten first.
|
||||
- **Decision:** **KEEP-PYTHON (TUI shell; all 4 clusters, zero RECODE, no carve-out found).**
|
||||
Cluster scores (1 = recode, 5 = keep): (A) rendering/widgets 4 — no hot loop exists; render is
|
||||
plain Rich `Text` concatenation (rune_tui.py:1331, chat_bubble.py:157), only animation a 0.12 s
|
||||
spinner (:263), no markdown/Syntax parsing anywhere in src/rune; (B) event glue (~50 `_cmd_*`
|
||||
handlers, reactives, tool-widget maps) 5 — this is Textual's core competency (workers,
|
||||
`exclusive=True`, `call_from_thread`); vendor docs (ratatui) admit "little to no help in organizing
|
||||
large applications"; (C) model send path + envelope 5 — synchronous `send_task` in async worker;
|
||||
the envelope shape (`success/response/thinking/tool_events/degraded`, rune_tui.py:3624ff) is
|
||||
ALREADY the de-facto interop contract; (D) subprocess/streaming 5 — JSONL streaming precedent
|
||||
already in-tree (`_cmd_pull` :3443-3517): the pattern for consuming recoded units, not a recode
|
||||
target. Whole-TUI native recode 1/5 REJECTED — 80+ first-party module coupling, framework-provided
|
||||
concurrency re-implemented by hand; Go additionally toolchain-blocked. Countervailing evidence
|
||||
(Bubble Tea/tview/Codex TUI maturity) recorded honestly in outward-sources.md — it does not
|
||||
outweigh the coupling.
|
||||
- **⚠ CONTRACT GAP FLAGGED (binds Waves 2–7):** `shared/native_units.py` parses only the LAST
|
||||
stdout line — it must gain a JSONL streaming reader (per-line `json.loads` + terminal-envelope
|
||||
validation) before any recoded long-running unit can stream progress to the TUI. Small, testable,
|
||||
non-breaking; candidate for the first Wave 2 implementation issue.
|
||||
- **Adapter surface (TUI as consumer — adopted as the rubric's interop-contract restatement):**
|
||||
(1) single-invocation stateless CLI (`native_unit <op> [args]`, no daemon); (2) success = final
|
||||
stdout line `{"ok":true, …}` generalizing the model-send envelope — recoded units emitting
|
||||
`response`/`tool_events`/`degraded` plug in with zero TUI changes; (3) failure = `ok:false` +
|
||||
nonzero exit, stderr human-only, success = exit 0 AND not timed out; (4) mandatory `--version`
|
||||
on every unit (status bar + `canary_tui_drift.py` detect live units vs Python fallback);
|
||||
(5) JSONL streaming mode for long ops with terminal sentinel envelope; (6) per-call timeout
|
||||
budget, `timed_out` surfaced in `NativeResult` for graceful degradation; (7) streamed events
|
||||
referencing UI objects carry the TUI's stable keys — `(channel, call_id)` for tool widgets
|
||||
(:3676), WorkPane `_entries` object ids (:1239) — preserved verbatim; (8) std-only for
|
||||
proof-phase units.
|
||||
- **Candidate matrix summary:** Python 4/4 clusters · Rust/Go whole-TUI 1/5 rejected · carve-out
|
||||
candidates: none found (future markdown parsing would carve as a Rust native unit behind the
|
||||
adapter, not now).
|
||||
- **Kill tests (frozen in synthesis.md):** stay-Python — killed if >16 ms UI-thread stalls under
|
||||
heavy event bursts OR IPC latency dominates message round-trip (Textual pilot measurement);
|
||||
Rust recode — killed if the adapter must expose >~15 first-party modules (count today ≈80:
|
||||
killed NOW); Go recode — additionally killed by toolchain absence (`command -v go` empty).
|
||||
- **Sources + lessons:** 8 live-fetched 2026-09-29 (Textual workers guide, ratatui vendor docs,
|
||||
Bubble Tea/tview/Codex countervailing evidence, JSON Lines spec, push-granular-work guidance) —
|
||||
annotations in `research/wave1/tui/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** full inward read of all 5 TUI files with line refs; ≥2 sources
|
||||
per major claim; gap = no runtime profiling (stall kill test unmeasured — but no mechanism exists
|
||||
for the stall to occur; the 120 ms 21-step pipeline was already removed, `_run_warm_router`
|
||||
:3575 is a no-op).
|
||||
|
||||
## W2 — Leaf utilities & canaries (~40 modules, low coupling: pins, time_awareness, message_queue, event_envelope, attention_capsule, complexity_scorer, token_optimizer, cache 501/17, prompt_sync, chat_bubble, ~15 canary_* modules)
|
||||
|
||||
- **Research round actor:** general-6 (COMPLETED 2026-09-29 — full report in actor notification)
|
||||
- **Intention profile:** small pure helpers, envelopes, scoring, cache; smallest blast radius; the
|
||||
factory + interop proving ground at real scale.
|
||||
- **Ground-truth measurements (local, this session):** Python fn call ~46 ns (100k timed
|
||||
iterations in-process) · native CLI roundtrip ~658 µs (subprocess to
|
||||
`interop/rebase-proof/target/release/rebase-proof --version`) · Python startup floor ~18 ms ·
|
||||
overhead ratio ≈ **14,000×**. The only CI-proven interop contract is the subprocess CLI boundary
|
||||
(`shared/native_units.py`: argv in → JSON stdout, `--version`, timeout, typed `NativeResult`).
|
||||
PyO3/in-process bindings excluded by Wave 0 policy. Therefore every per-call crossing costs
|
||||
≥658 µs — a per-turn helper would need to run >14,000× faster in Rust to break even.
|
||||
- **Candidate matrix (representative — full per-cluster 0–2 scoring in round report):**
|
||||
Python 12/12 in every cluster · Rust 5–6/12 · Go 1–3/12 (D5=0, not installed) · C 1–2/12.
|
||||
No module contains a hot loop (heaviest: 2 `re.sub` per turn in token_optimizer).
|
||||
- **Decision:** **KEEP-PYTHON (all ~40, zero RECODE).** Cluster verdicts: (a) pure CRUD helpers
|
||||
pins/time_awareness/token_optimizer — KEEP, CPython 3.11+ already 10–60% faster on this shape
|
||||
(PEP 659); (b) message/envelope/capsule structures — KEEP, recode would insert a boundary
|
||||
between the chattiest neighbors; (c) cache.py (501 L, largest in wave) — KEEP, the fast part is
|
||||
already C (sqlite3 DB-API 2.0 over SQLite's C library), Python is thin orchestration, batching
|
||||
(threshold 50 / 30 s) already amortizes writes; (d) chat_bubble.py — KEEP, **hard-pinned** to
|
||||
Python by the Textual framework (recode = full framework abandonment, bottleneck is human
|
||||
typing speed); (e) prompt_sync.py glue — KEEP, glue into Python siblings (memory_env,
|
||||
voice_sync, build_pipeline_policy); (f) ~19 canary_* — KEEP **categorically**, a canary that
|
||||
verifies Python importability cannot exist in another language.
|
||||
- **Kill tests (reopening criteria, all fail today on first cheap measurement):** Rust — microbench
|
||||
one per-turn helper behind the native_units CLI, must beat the Python in-process call >10×
|
||||
end-to-end including the 658 µs crossing (whole call <~46 µs; a trivial `--version` already
|
||||
costs 658 µs); Go — identical benchmark post-install, uninstall if roundtrip >100 µs with no
|
||||
named hot path; C — identical benchmark + required fuzz-clean JSON roundtrip, plus a
|
||||
demonstrated GC-free allocation-hot loop first; cache.py — recode only if Python-side
|
||||
orchestration (excl. sqlite3 waits) exceeds 5% of end-to-end latency; capsule/envelope/queue —
|
||||
recode only if one turn's Python total exceeds ~5 ms AND a candidate's CLI path is >10× cheaper.
|
||||
- **Sources + lessons:** (1) https://docs.python.org/3/whatsnew/3.11.html — 10–60% faster,
|
||||
PEP 659 specializing interpreter: CPython already optimized for helper-shaped code, the D2
|
||||
baseline is a moving improving target; (2) https://docs.python.org/3/whatsnew/3.13.html —
|
||||
free-threading/JIT experimental, KEEP-PYTHON ≠ frozen but no bets on experimental paths;
|
||||
(3) https://www.maturin.rs/ — in-process Rust↔Python needs a whole binding/packaging toolchain
|
||||
atop the CLI boundary, D3 worse than measured 658 µs (Wave 0 policy excludes it anyway);
|
||||
(4) https://go.dev/doc/install — Go is a pre-install prerequisite, D5=0 everywhere, never earned
|
||||
installation; (5) https://textual.textualize.io/ — Textual is a Python TUI framework,
|
||||
chat_bubble.py framework-pinned; (6) https://docs.python.org/3/library/sqlite3.html — sqlite3 is
|
||||
DB-API 2.0 over C, cache.py's perf layer already native. All fetched live 2026-09-29.
|
||||
- **⚠ Inventory drift (VERIFIED 2026-09-29):** complexity_scorer.py listed in
|
||||
rebase-inventory.json (2 mentions) but absent on disk — deleted in commit 867321e
|
||||
("governed retirement — complexity_scorer deleted (602 lines)"). Wave 0 inventory needs a
|
||||
correction pass; the module was scored nowhere (no source to characterize).
|
||||
|
||||
---
|
||||
|
||||
## Wave-plan consequence (T5 scope, from the verdict above)
|
||||
|
||||
With **0 recode units awarded**, the tranche's hypothesized rebase volume is real only as the
|
||||
work below. Waves 2–9 are **disposition + named improvements, NOT mass recoding**:
|
||||
|
||||
**Waves 2–7 — per-issue disposition (the 1,852 `[rebase w{wave}]` issues):**
|
||||
Each issue receives its tranche §7 disposition — improve / fix / missing-connectors answered
|
||||
with evidence — plus a language verdict of **KEEP-PYTHON with its rubric evidence line**
|
||||
(the round section for that subsystem names the claim). Disposition runs leaf-first
|
||||
(Wave 2 → 7), the canonical suite stays green throughout, and no issue closes without its
|
||||
evidence line. A recode is off the table unless a frozen kill test is reopened — and then
|
||||
tranche §6 still governs: **no benchmark win, no recode.** Go is not installed and earned no
|
||||
installation; Rust stays toolchain-ready but unjustified (standing option behind the
|
||||
CI-proven adapter).
|
||||
|
||||
**Named improvements — the actual rebase volume (ship alongside disposition):**
|
||||
1. **JSONL streaming reader for `shared/native_units.py`** (W7 contract gap): the adapter
|
||||
parses only the LAST stdout line; it must gain per-line `json.loads` + terminal-envelope
|
||||
validation before any recoded long-running unit could stream progress. Small, testable,
|
||||
non-breaking — first implementation issue of Wave 2.
|
||||
2. **httpx retry/timeout wrapper for the provider layer** (W4): the honest reliability
|
||||
upgrade is a feature-add, not a language change.
|
||||
3. **orjson upgrade path** (W3 cluster 1): the only credible Python-side JSON win —
|
||||
in-process, no boundary crossing. **DISPOSITION (2026-09-29, kill test EXECUTED and
|
||||
FAILED — KEEP-PYTHON stands, no dependency declared):** microbench on the live
|
||||
`~/.rune/consolidation/knowledge.json` (3,540,959 bytes, n=30 best-of): load 13.15 ms
|
||||
std vs 7.85 ms orjson = **1.68× (bar ≥2×: FAIL)**; dump 9.58 ms std vs 5.61 ms
|
||||
orjson-with-decode = **1.71× (bar ≥5×: FAIL)**; raw-bytes dump 1.39 ms = 6.91× but is
|
||||
not drop-in (every `write_text`/`json.dump`-to-file site would need a bytes→str or
|
||||
binary-write conversion). Roundtrip equality verified. Bench: `/tmp/orjson_bench.py`
|
||||
shape (uvx --with orjson). The bytes trap measured real: decode() eats ~60% of the
|
||||
dump win.
|
||||
4. **complexity_scorer.py inventory correction**: listed in `rebase-inventory.json` but
|
||||
deleted on disk (commit `867321e`, 602 lines) — Wave 0 inventory needs the correction
|
||||
pass. **DISPOSITION (2026-09-29, DONE):** inventory regenerated per the script's own
|
||||
header (never hand-edit) — 177→213 files, 1,852→2,314 functions, 13 dead files
|
||||
dropped; committed + pushed `5bc837c`. The W2 round's "2 mentions" drift claim
|
||||
verified FALSE against the live inventory (zero hits).
|
||||
5. **robfig/cron** (W6): recorded as a best-language footnote only; no action.
|
||||
|
||||
**Wave 8 — TUI integration proof:** demonstrate the TUI reading/utilising the mixed-language
|
||||
surfaces that exist (the `interop/rebase-proof` unit through the upgraded adapter), per the
|
||||
8-point adapter surface adopted above.
|
||||
**Wave 9 — close-out audit:** 100% of the 1,852 issues dispositioned, debt-table revision,
|
||||
version bump.
|
||||
|
||||
## Cross-cutting candidates (evaluated once, referenced by every subsystem)
|
||||
|
||||
- **Rust (present, 1.98.1):** interop proof unit already CI-proven (std-only, zero crates, `interop/rebase-proof/`, adapter in `shared/native_units.py`, ok = exit 0 AND not timed out).
|
||||
- **Go (absent):** must earn installation through D1–D5 evidence here before any recode targets it.
|
||||
- **C (present, GCC 16.2.1):** expected to lose D6 in most subsystems; scored where FFI-adjacent work exists.
|
||||
- **Python (KEEP baseline):** the default. Every subsystem begins here; evidence must move it.
|
||||
|
||||
## Evidence ledger (per-source lessons, all rounds)
|
||||
|
||||
Index of per-source evidence. Full annotations live in each round's
|
||||
`research/wave1/<subsystem>/outward-sources.md`; this ledger records the headline lesson
|
||||
each source contributed and the rubric claims it supports.
|
||||
|
||||
| Round | Sources (live-fetched 2026-09-29) | Headline lessons | Supports |
|
||||
|---|---|---|---|
|
||||
| W2 leaf utilities | 6: python.org 3.11 + 3.13 whatsnew, maturin.rs, go.dev/doc/install, textual.textualize.io, docs.python.org sqlite3 | CPython 3.11+ already 10–60% faster on helper-shaped code (PEP 659) — D2 baseline is a moving target; free-threading/JIT experimental, no bets; in-process bindings need a whole toolchain atop the CLI boundary (excluded by Wave 0 policy anyway); Go is pre-install prerequisite, D5=0; Textual framework-pins chat_bubble.py; sqlite3 perf layer already C | W2 KEEP-PYTHON ×~40; the 14,000× crossing-overhead law |
|
||||
| W3 memory | 6: orjson, sled, sqlite-vec, SQLite FTS5 docs, Go json/v2 rewrite issue, pydantic-core | orjson is the only credible Python-side JSON win (in-process, no boundary); SQLite FTS5/vec0 compute is already C; Go's JSON stack mid-rewrite upstream (versioning risk); pydantic-core shows the Rust-as-wheel pattern, excluded by policy | W3 KEEP-PYTHON 7/7; orjson upgrade path on cluster 1 |
|
||||
| W4 provider-layer | 8: live-fetched O1–O8 (HTTP/SSE ecosystem, tokio-stack precedents incl. mistral.rs, pkg.go.dev/net/http, ollama Go client/server) | stdlib-only Rust has no realistic HTTP/SSE precedent — the zero-crate contract decides Rust out of all network-wire clusters by construction; Go's stdlib HTTP+NDJSON streaming is capability-proven (its losses here are evidence-absence, not capability-absence); the honest reliability upgrade is a Python httpx retry/timeout wrapper — feature-add, not language change | W4 KEEP-PYTHON 5/5; SSE-cluster Rust kill-by-construction |
|
||||
| W5 orchestration | 4: pydantic-core, ruff blog, go.dev FAQ, Python 3.14 hashlib docs + 1 LOCAL EXPERIMENT (n=100 adapter benchmark) | adapter round-trips at 0.63ms/op — subprocess-per-op viable for coarse ops, never per-event; Python "slowness" dissolves where compute already runs in C via stdlib; the language question is really "where is the hot loop" (here: nowhere) | W5 KEEP-PYTHON 6/6; K1 kill test already run |
|
||||
| W6 runtime-core | headline lessons (full set in round outward-sources.md) | Rust std::process lacks built-in child timeout — strictly more code under zero-crate; Go os/exec reaches parity but changes only syntax; Python json/regex C-backed so rewrite swaps C for compiled code at equal asymptotic cost; serde typed structs mismatch schema-flexible model output; concurrency features irrelevant at ≤2 threads | W6 STAY-PYTHON 6/6; robfig/cron as best-language footnote |
|
||||
| W7 TUI | 8: Textual workers guide, ratatui vendor docs, Bubble Tea/tview/Codex TUI (countervailing), JSON Lines spec, push-granular-work guidance | Textual workers/`call_from_thread` are the core competency for event glue; ratatui admits "little to no help in organizing large applications"; countervailing native-TUI frameworks recorded honestly — do not outweigh 80+ module coupling; JSONL per-line parse + terminal sentinel is the streaming contract; vendor guidance: push granular speed-sensitive work to subcomponents only when measured | W7 KEEP-PYTHON 4/4; the 8-point adapter surface (adopted as the rubric's interop-contract restatement) |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Wave 0 close-out report — language-rebasing tranche R1
|
||||
|
||||
Branch: `tranche-r1-wave-0` · Date: 2026-09-29
|
||||
|
||||
## 1. Inventory (docs/rebase/rebase-inventory.json)
|
||||
|
||||
- 177 Python modules, ~52,750 lines, 227 classes
|
||||
- TRUE unit count: **1,852 = 1,030 top-level functions + 822 class methods**
|
||||
- Zero duplicate call edges, zero method leaks (generator double-walk defect
|
||||
root-caused and fixed in scripts/rebase_inventory.py)
|
||||
- Cross-verified against an independent fresh AST walk: zero missing units;
|
||||
six inventory-"extras" are real nested/if-try-block classes the naive walk
|
||||
misses (inventory is MORE complete, not less)
|
||||
|
||||
## 2. Issue factory (scripts/rebase_issue_factory.py)
|
||||
|
||||
- One issue per unit following R1 §7; wave-batched (2–7); dedup-safe via
|
||||
deterministic title `[rebase w{wave}] {relpath}::{unit}` + existing-titles
|
||||
scan per run (retries never duplicate — preserve-first)
|
||||
- `plan` → {2: 239, 3: 251, 4: 129, 5: 544, 6: 415, 7: 274} = 1,852,
|
||||
zero unassigned
|
||||
- `sample --wave 5` → full §7 template holds; `_compute_hash` exactly once as
|
||||
labelled callee with line number
|
||||
- Contract fix during Wave 0: `--limit` default changed 5 → 0 (unlimited) for
|
||||
`create`; `sample` keeps its 5. A full-wave create with the old default
|
||||
would have silently truncated each wave to five issues.
|
||||
|
||||
## 3. Interop proof unit (R1 §5)
|
||||
|
||||
- Rust unit at interop/rebase-proof/ (std-only, zero crates): argv in → JSON
|
||||
on stdout, non-zero exit on failure, mandatory `--version`
|
||||
- Shell-proven: version flag, JSON out, exit 2 on failure, stderr diagnostic
|
||||
- Python thin adapter shared/native_units.py: typed NativeResult, timeout +
|
||||
stderr capture, unit failures as data (never exceptions)
|
||||
- Adapter contract fix during Wave 0: `ok` no longer requires a JSON
|
||||
`ok:true` field — plain-text output (`--version`) succeeds on exit code
|
||||
- tests/test_native_interop_proof.py: **8 passed** — version contract, sum
|
||||
happy path, hash determinism, failure paths, timeout, stderr capture
|
||||
|
||||
## 4. Per-wave issue creation
|
||||
|
||||
<!-- FILLED BY THE COMPLETION LOOP — do not hand-edit -->
|
||||
|
||||
- Wave 4 (129 units): created=127 skipped(dedup)=2 (includes smoke #417–418)
|
||||
- Wave 2 (239 units): created=239 skipped(dedup)=0
|
||||
- Wave 3 (251 units): created=251 skipped(dedup)=0
|
||||
- Wave 5 (544 units): created=544 skipped(dedup)=0
|
||||
- Wave 6 (415 units): created=415 skipped(dedup)=0
|
||||
- Wave 7 (274 units): created=274 skipped(dedup)=0
|
||||
- TOTAL: 1,850 created + 2 dedup = 1,852 / 1,852 ✅ full coverage
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"units_per_wave": {
|
||||
"2": 239,
|
||||
"3": 251,
|
||||
"4": 129,
|
||||
"5": 544,
|
||||
"6": 415,
|
||||
"7": 274
|
||||
},
|
||||
"unassigned_modules": {},
|
||||
"unassigned_module_count": 0
|
||||
}
|
||||
+1
-1
@@ -20,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"]
|
||||
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json", "./docs/rebase/**"]
|
||||
|
||||
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
|
||||
in: ./src/rune/*
|
||||
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Wave 0 interop proof unit (R1 §5). Std-only by design: no crates, no network,
|
||||
# deterministic build. The pattern under proof is the CLI boundary itself, not the
|
||||
# computation inside it.
|
||||
|
||||
[[bin]]
|
||||
name = "rebase-proof"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Wave 0 interop proof unit (R1 §5 of the language-rebasing tranche).
|
||||
//!
|
||||
//! Contract under proof:
|
||||
//! - standalone binary; argv in, JSON on stdout
|
||||
//! - non-zero exit on failure (JSON error object still printed to stdout)
|
||||
//! - `--version` flag mandatory
|
||||
//! - Python wraps it in a thin adapter (`shared/native_units.py`); tests call
|
||||
//! it the same way.
|
||||
//!
|
||||
//! Ops: sum / echo / hash (fnv1a64) / sleep / fail. All std-only.
|
||||
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// FNV-1a 64-bit — dependency-free deterministic hash for the proof unit.
|
||||
fn fnv1a64(data: &[u8]) -> u64 {
|
||||
let mut hash: u64 = 0xcbf29ce484222325;
|
||||
for byte in data {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn print_json(line: &str) {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
fn emit_ok(body: &str) {
|
||||
print_json(&format!("{{\"ok\":true,{body}}}"));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fn emit_err(message: &str) -> ! {
|
||||
// Escape the minimal set so the JSON stays parseable for arbitrary op names.
|
||||
let escaped: String = message
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
print_json(&format!("{{\"ok\":false,\"error\":\"{escaped}\"}}"));
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
|
||||
if args.as_slice() == ["--version"] || args.as_slice() == ["-V"] {
|
||||
println!("rebase-proof {VERSION}");
|
||||
exit(0);
|
||||
}
|
||||
if args.is_empty() {
|
||||
eprintln!("usage: rebase-proof <op> [args...] | --version");
|
||||
emit_err("no op given");
|
||||
}
|
||||
|
||||
let op = args[0].as_str();
|
||||
let rest = &args[1..];
|
||||
|
||||
match op {
|
||||
"sum" => {
|
||||
let mut total: i128 = 0;
|
||||
for a in rest {
|
||||
match a.parse::<i128>() {
|
||||
Ok(v) => total += v,
|
||||
Err(_) => emit_err(&format!("sum: not an integer: {a}")),
|
||||
}
|
||||
}
|
||||
emit_ok(&format!("\"op\":\"sum\",\"result\":{total}"));
|
||||
}
|
||||
"echo" => {
|
||||
let joined = rest.join(" ");
|
||||
let escaped: String = joined
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
emit_ok(&format!("\"op\":\"echo\",\"text\":\"{escaped}\""));
|
||||
}
|
||||
"hash" => {
|
||||
let joined = rest.join(" ");
|
||||
let h = fnv1a64(joined.as_bytes());
|
||||
emit_ok(&format!("\"op\":\"hash\",\"fnv1a64\":\"{h:016x}\""));
|
||||
}
|
||||
"sleep" => {
|
||||
let ms: u64 = match rest.first().map(|s| s.parse::<u64>()) {
|
||||
Some(Ok(v)) => v,
|
||||
_ => emit_err("sleep: expected milliseconds"),
|
||||
};
|
||||
thread::sleep(Duration::from_millis(ms));
|
||||
emit_ok(&format!("\"op\":\"sleep\",\"slept_ms\":{ms}"));
|
||||
}
|
||||
"fail" => {
|
||||
// Deliberate failure: stderr diagnostic + JSON error + exit 2.
|
||||
// Proves stderr capture and non-zero exit in the adapter.
|
||||
eprintln!("rebase-proof: deliberate failure for adapter proof");
|
||||
emit_err("deliberate failure");
|
||||
}
|
||||
other => emit_err(&format!("unknown op: {other}")),
|
||||
}
|
||||
}
|
||||
@@ -1 +1,401 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"capabilities": [
|
||||
"read_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_file read a file with optional windowed viewing (offset/limit). returns numbered lines. use offset/limit for large files (100 lines at a time). native tool"
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"capabilities": [
|
||||
"edit_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"search string matches exactly once"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites the target file atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "edit_file surgical search/replace edit. more reliable than full file rewrites. the search string must match exactly. native tool"
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"capabilities": [
|
||||
"write_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory"
|
||||
],
|
||||
"side_effects": [
|
||||
"writes the full file content atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "write_file write content to a file. creates parent directories if needed. shows diff before writing. use for new files or full rewrites. native tool"
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"capabilities": [
|
||||
"glob_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "glob_files find files matching a glob pattern. returns file paths sorted by modification time. native tool"
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"capabilities": [
|
||||
"grep_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "grep_files search for a pattern in files. returns file paths and line numbers. use include to filter by file type. native tool"
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"capabilities": [
|
||||
"list_directory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "list_directory list files and directories at a path. returns names with / suffix for directories. native tool"
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"capabilities": [
|
||||
"lint_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "lint_file lint a file for syntax errors and style issues. blocks bad edits. supports python (ruff), js/ts (eslint), go (golangci-lint), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"capabilities": [
|
||||
"run_tests"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_tests run tests for a file or project. auto-detects test runner from language. native tool"
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"capabilities": [
|
||||
"format_code"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"a formatter is configured for the language"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites files in place to the formatter's style"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "format_code auto-format code files. supports python (black/ruff), js/ts (prettier), go (gofmt), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"capabilities": [
|
||||
"git_diff"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_diff show git diff for staged or unstaged changes. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"capabilities": [
|
||||
"git_log"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_log show recent git commits. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"capabilities": [
|
||||
"git_commit"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_commit commit changes with a message. auto-stages modified files. native tool"
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"capabilities": [
|
||||
"repo_map"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "repo_map generate an ast-based codebase map showing key symbols (classes, functions, imports). uses tree-sitter for 100+ languages. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"capabilities": [
|
||||
"run_command"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_command run an approved command. prefer argv (shell=false); shell syntax requires shell=true and explicit user approval. native tool"
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"capabilities": [
|
||||
"read_persona"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_persona read rune's persona definition. returns the full persona block with voice, identity, and rules. native tool"
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"capabilities": [
|
||||
"query_memory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "query_memory query rune's memory store. returns relevant memories for a query. native tool"
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"capabilities": [
|
||||
"check_growth"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "check_growth check rune's growth log. returns recent growth entries and patterns. native tool"
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"capabilities": [
|
||||
"diagnose_behavior"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "diagnose_behavior diagnose rune's current behavior. returns analysis of persona adherence, voice consistency, and drift. native tool"
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"capabilities": [
|
||||
"find_capability"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "find_capability search all installed capabilities for what you need. returns matching tool names with descriptions and argument schemas. use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session. native tool"
|
||||
}
|
||||
]
|
||||
+38287
-15
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.86.0"
|
||||
version = "0.89.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o /tmp/req_new.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
@@ -18,9 +18,9 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.1
|
||||
coverage==7.16.2
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
diff-cover==10.6.0
|
||||
# via rune (pyproject.toml)
|
||||
frozenlist==1.8.0
|
||||
# via
|
||||
@@ -56,15 +56,15 @@ mdit-py-plugins==0.6.1
|
||||
# via textual
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.9.0
|
||||
multidict==6.9.1
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
nodeenv==1.10.0
|
||||
nodeenv==1.11.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.11
|
||||
platformdirs==4.12.2
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
|
||||
+17
-3
@@ -148,9 +148,18 @@ def sha_branch(sha: str) -> str:
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
"""SHA is an ancestor of master (local or remote-tracking refs).
|
||||
|
||||
M10 correction (2026-09-19): --all so a freshly fetched master
|
||||
(refs/remotes/gitea/master) counts; local branches alone go stale
|
||||
between fetches. derive_op_type refreshes refs before calling this.
|
||||
"""
|
||||
out = _git("branch", "--all", "--contains", sha)
|
||||
for l in out.splitlines():
|
||||
s = l.strip().removeprefix("remotes/").removesuffix(" *")
|
||||
if s == "master" or s.endswith("/master"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
@@ -184,6 +193,11 @@ 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
|
||||
|
||||
@@ -86,9 +86,12 @@ def main() -> int:
|
||||
if live_state == "open" and rec["tracker_state"] == "closed" and rec["reconciliation_state"] in TERMINAL_STATES:
|
||||
drift.append(f"#{num}: live state open but artifact says closed/{rec['reconciliation_state']}")
|
||||
live_bug = "bug" in [l["name"] for l in li.get("labels", [])]
|
||||
if live_bug and not rec.get("live_defect"):
|
||||
# Same open-only exemption as the artifact-records block above: a
|
||||
# closed issue's labels are historical record; FIXED_VERIFIED issues
|
||||
# legitimately keep the bug label after closure.
|
||||
if live_bug and rec.get("tracker_state") == "open" and not rec.get("live_defect"):
|
||||
drift.append(f"#{num}: live bug label present but artifact records no live defect")
|
||||
if not live_bug and rec.get("live_defect"):
|
||||
if not live_bug and rec.get("live_defect") and rec.get("tracker_state") == "open":
|
||||
drift.append(f"#{num}: artifact records live defect but live bug label absent")
|
||||
|
||||
# Live defects in the artifact must all be OPEN in the tracker
|
||||
|
||||
@@ -1,43 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
"""install_mimo_hook.py — T21.2 §21.2D (T22: multi-hook): canonical MiMo
|
||||
hook installer.
|
||||
|
||||
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."""
|
||||
Canonical sources: config/mimo-hooks/*.ts
|
||||
Targets: ~/.mimocode/hooks/<same-name>.ts
|
||||
|
||||
Every hook is hash-verified after install; drift is detected in both
|
||||
directions. T22 added builder-bridge.ts to the managed set — it was
|
||||
previously installed by hand, unversioned, which is how it silently rotted
|
||||
(hardcoded ephemeral interpreter). A hook that reaches the builder must be
|
||||
version-controlled and drift-checked like any other authority.
|
||||
|
||||
Usage:
|
||||
python3 scripts/install_mimo_hook.py # sync all managed hooks
|
||||
python3 scripts/install_mimo_hook.py --check # verify only, install nothing
|
||||
"""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL_DIR = REPO / "config" / "mimo-hooks"
|
||||
TARGET_DIR = Path.home() / ".mimocode" / "hooks"
|
||||
|
||||
# The managed set. Adding a file here is what makes a hook version-controlled.
|
||||
MANAGED = ["event-wait-guard.ts", "builder-bridge.ts"]
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
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]}")
|
||||
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.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
|
||||
print(f"hook missing at {target}")
|
||||
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(canonical, target)
|
||||
if sha256(target) != canon_hash:
|
||||
print(f"INSTALL FAILED: {name} hash mismatch after copy")
|
||||
return False
|
||||
print(f"installed: {name} {canon_hash[:12]}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
check_only = "--check" in sys.argv
|
||||
ok = True
|
||||
for name in MANAGED:
|
||||
ok = sync_one(name, check_only) and ok
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — whole-project inventory generator.
|
||||
|
||||
Parses every .py file under src/rune/ with the stdlib AST and emits
|
||||
docs/rebase/rebase-inventory.json: files, classes, functions (with signatures and
|
||||
line ranges), imports, and best-effort caller/callee edges (intra-module call
|
||||
graph + cross-module edges resolved through import tables).
|
||||
|
||||
Deterministic and idempotent: same tree in, same JSON out (sorted keys,
|
||||
sorted files). The issue-factory reads this file; nothing else may edit it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SRC = REPO / "src" / "rune"
|
||||
OUT = REPO / "docs" / "rebase" / "rebase-inventory.json"
|
||||
|
||||
|
||||
def _arg_str(a: ast.arg) -> str:
|
||||
s = a.arg
|
||||
if a.annotation is not None:
|
||||
# Degraded signature (no annotation) is deterministic metadata — never
|
||||
# let one pathological annotation node abort the whole inventory walk.
|
||||
try:
|
||||
s += f": {ast.unparse(a.annotation)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return s
|
||||
|
||||
|
||||
def _signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
||||
parts = [_arg_str(a) for a in node.args.args]
|
||||
if node.args.vararg:
|
||||
parts.append(f"*{node.args.vararg.arg}")
|
||||
elif node.args.kwonlyargs:
|
||||
parts.append("*")
|
||||
parts += [_arg_str(a) for a in node.args.kwonlyargs]
|
||||
if node.args.kwarg:
|
||||
parts.append(f"**{node.args.kwarg.arg}")
|
||||
ret = ""
|
||||
if node.returns is not None:
|
||||
# Same degraded-metadata contract as _arg_str.
|
||||
try:
|
||||
ret = f" -> {ast.unparse(node.returns)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return f"({', '.join(parts)}){ret}"
|
||||
|
||||
|
||||
class FileVisitor(ast.NodeVisitor):
|
||||
def __init__(self) -> None:
|
||||
self.functions: list[dict] = []
|
||||
self.classes: list[dict] = []
|
||||
self.imports: list[dict] = []
|
||||
self.calls: list[tuple[int, str]] = [] # (lineno, dotted call name)
|
||||
|
||||
def _fn(self, node, parents: list[str]) -> dict:
|
||||
return {
|
||||
"name": ".".join([*parents, node.name]),
|
||||
"signature": _signature(node),
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"decorators": [ast.unparse(d) for d in node.decorator_list
|
||||
if isinstance(d, (ast.Name, ast.Attribute, ast.Call))],
|
||||
"async": isinstance(node, ast.AsyncFunctionDef),
|
||||
}
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
methods = []
|
||||
for child in node.body:
|
||||
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
methods.append(self._fn(child, [node.name]))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(child)
|
||||
if isinstance(n, ast.Call) and not isinstance(n.func, ast.Name if False else type(None))
|
||||
and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
self.classes.append({
|
||||
"name": node.name,
|
||||
"bases": [ast.unparse(b) for b in node.bases],
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"methods": methods,
|
||||
})
|
||||
# visit only non-function children: methods are already captured above,
|
||||
# and a generic_visit would re-fire visit_FunctionDef on them (duplicate
|
||||
# units + duplicate call edges). Nested classes are still visited.
|
||||
for child in node.body:
|
||||
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
self.visit(child)
|
||||
|
||||
def visit_FunctionDef(self, node) -> None:
|
||||
# top-level only reached here (methods captured in ClassDef)
|
||||
self.functions.append(self._fn(node, []))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(node)
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
# do not descend into nested defs separately; they are folded into parents
|
||||
# (their calls are already captured by the ast.walk above). No generic_visit:
|
||||
# it would re-collect nested defs as top-level functions and duplicate edges.
|
||||
|
||||
visit_AsyncFunctionDef = visit_FunctionDef
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
for a in node.names:
|
||||
self.imports.append({"module": a.name, "as": a.asname, "line": node.lineno})
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
mod = node.module or ""
|
||||
for a in node.names:
|
||||
self.imports.append({"module": f"{mod}.{a.name}" if a.name != "*" else mod,
|
||||
"as": a.asname or a.name, "line": node.lineno,
|
||||
"from": True})
|
||||
|
||||
|
||||
def collect_file(path: Path) -> dict:
|
||||
src_text = path.read_text(encoding="utf-8", errors="replace")
|
||||
tree = ast.parse(src_text)
|
||||
v = FileVisitor()
|
||||
v.visit(tree)
|
||||
|
||||
# module-level calls (outside defs/classes) — walk tree body directly.
|
||||
# SKIP def/class statements: their bodies are walked by the visit_ methods;
|
||||
# walking them here duplicated every call edge inside every top-level function.
|
||||
for stmt in tree.body:
|
||||
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
continue
|
||||
for n in ast.walk(stmt):
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute)):
|
||||
v.calls.append((n.lineno, ast.unparse(n.func)))
|
||||
|
||||
defined = {f["name"].split(".")[-1] for f in v.functions}
|
||||
defined |= {m["name"].split(".")[-1] for c in v.classes for m in c["methods"]}
|
||||
defined |= {c["name"] for c in v.classes}
|
||||
|
||||
callees = []
|
||||
for lineno, call in v.calls:
|
||||
root = call.split(".")[0].split("(")[0].strip()
|
||||
callees.append({"line": lineno, "call": call,
|
||||
"internal": root in defined or root == "self"})
|
||||
|
||||
return {
|
||||
"path": str(path.relative_to(REPO)),
|
||||
"lines": len(src_text.splitlines()),
|
||||
"imports": sorted({i["module"] for i in v.imports}),
|
||||
"import_lines": v.imports,
|
||||
"functions": sorted(v.functions, key=lambda f: (f["line_start"], f["name"])),
|
||||
"classes": sorted(v.classes, key=lambda c: c["line_start"]),
|
||||
"calls": callees,
|
||||
}
|
||||
|
||||
|
||||
def cross_module_edges(files: dict[str, dict]) -> dict[str, set[str]]:
|
||||
"""Resolve import tables: module-stem -> set of imported module stems
|
||||
(rune-internal only), so the factory can attribute cross-module edges."""
|
||||
graph = defaultdict(set)
|
||||
for rel, info in files.items():
|
||||
stem = Path(rel).stem
|
||||
for imp in info["imports"]:
|
||||
top = imp.split(".")[0]
|
||||
second = ".".join(imp.split(".")[:2])
|
||||
if top == "rune":
|
||||
graph[stem].add(imp.replace("rune.", "", 1).split(".")[0])
|
||||
elif not imp.startswith("_") and top not in sys.stdlib_module_names:
|
||||
graph[stem].add(f"ext:{top}")
|
||||
return graph
|
||||
|
||||
|
||||
def main() -> None:
|
||||
py_files = sorted(SRC.rglob("*.py"))
|
||||
files: dict[str, dict] = {}
|
||||
for p in py_files:
|
||||
try:
|
||||
info = collect_file(p)
|
||||
except SyntaxError as e: # a file that cannot parse is a finding, not a skip
|
||||
info = {"path": str(p.relative_to(REPO)), "lines": 0, "imports": [],
|
||||
"import_lines": [], "functions": [], "classes": [], "calls": [],
|
||||
"parse_error": f"{type(e).__name__}: {e}"}
|
||||
files[info["path"]] = info
|
||||
|
||||
graph = cross_module_edges(files)
|
||||
total_fns = sum(len(f["functions"]) + sum(len(c["methods"]) for c in f["classes"])
|
||||
for f in files.values())
|
||||
doc = {
|
||||
"_meta": {
|
||||
"generator": "scripts/rebase_inventory.py",
|
||||
"tranche": "R1 language rebase, Wave 0",
|
||||
"note": "Machine-generated. Regenerate with the script; never hand-edit.",
|
||||
"files": len(files),
|
||||
"functions_total": total_fns,
|
||||
"classes_total": sum(len(f["classes"]) for f in files.values()),
|
||||
"lines_total": sum(f["lines"] for f in files.values()),
|
||||
},
|
||||
"import_graph": {k: sorted(v) for k, v in sorted(graph.items())},
|
||||
"files": dict(sorted(files.items())),
|
||||
}
|
||||
OUT.write_text(json.dumps(doc, indent=1, sort_keys=False) + "\n", encoding="utf-8")
|
||||
m = doc["_meta"]
|
||||
print(f"inventory written: {OUT}")
|
||||
print(f"files={m['files']} functions={m['functions_total']} "
|
||||
f"classes={m['classes_total']} lines={m['lines_total']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — per-function issue factory.
|
||||
|
||||
Reads docs/rebase/rebase-inventory.json and emits one issue per unit (top-level
|
||||
function or class method) following tranche R1 §7. Wave-batched, dedup-safe
|
||||
(deterministic title + existing-issue scan before create), rate-limited.
|
||||
|
||||
Modes:
|
||||
plan — coverage report: units per wave, unassigned modules (no silent drops)
|
||||
sample — emit N rendered issues to a file for review (no Gitea call)
|
||||
create — create issues in Gitea for one wave
|
||||
|
||||
Dedup contract: issue titles are deterministic —
|
||||
[rebase w{wave}] {relpath}::{unit}
|
||||
The factory fetches existing issue titles once per run and skips any that
|
||||
already exist, so retries never duplicate (preserve-first Gitea addendum).
|
||||
|
||||
The factory NEVER writes behaviour prose. Current-behaviour paragraphs and
|
||||
the three evidence answers (IMPROVE / FIX / MISSING CONNECTORS) are filled at
|
||||
disposition time by the working agent — the issue carries the mandatory
|
||||
fields and line ranges, not fabricated content.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INVENTORY = REPO_ROOT / "docs" / "rebase" / "rebase-inventory.json"
|
||||
COVERAGE_OUT = REPO_ROOT / "docs" / "rebase" / "rebase-wave-coverage.json"
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
|
||||
REPO = "conductor/rune"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "shared"))
|
||||
from gitea_auth import load_gitea_token # noqa: E402
|
||||
|
||||
TRANCHE_REF = "tranche R1 language rebase (2026-09-29), ~/.config/opencode/tranches/2026-09-29-language-rebase-tranche-R1.md"
|
||||
|
||||
# Wave membership from R1 §4 (initial, measured). Directory entries match by
|
||||
# path prefix; bare stems match the file stem. Modules matching no entry are
|
||||
# reported UNASSIGNED by `plan` — never silently dropped (R1 §9 risk 7).
|
||||
WAVE_PLAN = {
|
||||
2: [ # leaf utilities & canaries
|
||||
"pins", "time_awareness", "message_queue", "event_envelope",
|
||||
"attention_capsule", "complexity_scorer", "token_optimizer", "cache",
|
||||
"prompt_sync", "chat_bubble", "canary_*",
|
||||
],
|
||||
3: [ # memory & persistence
|
||||
"memory_store", "curator", "warm_router", "memory_consolidation",
|
||||
"graph_memory", "sleep_time", "night_mode", "identity_anchors",
|
||||
"query_memory", "memory_env", "warm_neuron_store", "subconscious_pass",
|
||||
"task_persist",
|
||||
],
|
||||
4: [ # provider/model layer
|
||||
"provider_registry", "model_connect", "model_routing", "model_profiles",
|
||||
"provider_budget", "standardized_providers", "model_registry",
|
||||
"connection_pool", "forge_client",
|
||||
],
|
||||
5: [ # orchestration, authority, plugins, skills, governance
|
||||
"orchestration/", "microkernel", "contracts", "build_mode", "plugins/",
|
||||
"skills/", "construction_authority", "construction_authority_cli",
|
||||
"construction_decision", "construction_mode", "verification_construction",
|
||||
"authority_recovery", "spine", "drift_core", "drift_ledger",
|
||||
"drift_observability", "persona_gate", "safety_override", "witness",
|
||||
"behavior", "growth_delta", "claim_audit", "verification_layer",
|
||||
"drift_compression", "drift_cache", "self_improve", "persona_definition",
|
||||
"outcome_ledger", "audit_ledger", "subsystem_health",
|
||||
],
|
||||
6: [ # runtime core & tooling
|
||||
"tool_executor", "tool_protocol", "local_tool_runtime",
|
||||
"capability_broker", "context_broker", "work_manager", "cron_engine",
|
||||
"auto_tasks", "goal", "failure_auto_filer", "hook_engine",
|
||||
"elastic_cognition", "event_bus", "system_registry", "abstraction",
|
||||
"tiered_executor", "progressive_escalation", "tier_manager",
|
||||
"parallel_processor", "shard_manager", "response_adapter",
|
||||
"context_pressure", "auto_context", "cognitive_budgeter",
|
||||
"recall_router", "inference_trace", "task_gate", "salience_gate",
|
||||
"dependency_tracker", "rune_profiler",
|
||||
],
|
||||
7: [ # operator + TUI, last by design
|
||||
"rune_operator", "rune_tui", "tui_validator", "voice_hybrid",
|
||||
"voice_sync", "voicebox", "voice_injector", "message_widgets",
|
||||
"rune_graphic", "rune_spinner", "__main__",
|
||||
],
|
||||
}
|
||||
|
||||
WAVE_NAMES = {
|
||||
2: "leaf utilities & canaries",
|
||||
3: "memory & persistence",
|
||||
4: "provider/model layer",
|
||||
5: "orchestration, authority, plugins, skills, governance",
|
||||
6: "runtime core & tooling",
|
||||
7: "operator + TUI",
|
||||
}
|
||||
|
||||
|
||||
def match_module(relpath: str):
|
||||
"""Return the wave number for a module path, or None if unassigned."""
|
||||
stem = Path(relpath).stem
|
||||
for wave, patterns in WAVE_PLAN.items():
|
||||
for pat in patterns:
|
||||
if pat.endswith("/"):
|
||||
if pat in relpath:
|
||||
return wave
|
||||
elif pat.endswith("*"):
|
||||
if stem.startswith(pat[:-1]):
|
||||
return wave
|
||||
elif stem == pat:
|
||||
return wave
|
||||
return None
|
||||
|
||||
|
||||
def iter_units(inv):
|
||||
"""Yield (relpath, unit_name, entry) for every function and method."""
|
||||
for relpath, mod in sorted(inv["files"].items()):
|
||||
for fn in mod["functions"]:
|
||||
yield relpath, fn["name"], fn
|
||||
for cls in mod["classes"]:
|
||||
for m in cls.get("methods", []):
|
||||
yield relpath, f"{cls['name']}.{m['name']}", m
|
||||
|
||||
|
||||
def internal_edges(relpath: str, entry: dict):
|
||||
"""Collect internal (in-repo) call names and line numbers for a unit."""
|
||||
lines = (entry["line_start"], entry["line_end"])
|
||||
mod = inv_cache["files"][relpath]
|
||||
calls = [c for c in mod.get("calls", [])
|
||||
if lines[0] <= c["line"] <= lines[1] and c.get("internal")]
|
||||
return calls
|
||||
|
||||
|
||||
inv_cache = {}
|
||||
|
||||
|
||||
def issue_title(wave: int, relpath: str, unit: str) -> str:
|
||||
return f"[rebase w{wave}] {relpath}::{unit}"
|
||||
|
||||
|
||||
def render_issue(wave: int, relpath: str, unit: str, entry: dict) -> dict:
|
||||
calls = internal_edges(relpath, entry)
|
||||
call_lines = "\n".join(
|
||||
f" - `{c['call']}` (line {c['line']})" for c in calls[:20]
|
||||
) or " - (no internal call edges in range)"
|
||||
sig = entry.get("signature", "()")
|
||||
body = f"""\
|
||||
|
||||
## Identity (from the Wave 0 inventory — machine-generated)
|
||||
|
||||
- **File**: `{relpath}`
|
||||
- **Unit**: `{unit}`
|
||||
- **Signature**: `def {unit}{sig}` (async: {entry.get('async', False)})
|
||||
- **Line range**: {entry['line_start']}–{entry['line_end']}
|
||||
- **Decorators**: {entry.get('decorators', []) or 'none'}
|
||||
- **Internal call edges** (callers/callees inside the repo, from inventory):
|
||||
{call_lines}
|
||||
|
||||
## Current behaviour
|
||||
|
||||
One honest paragraph from READING THE CODE at the line range above — what it
|
||||
actually does, not what the docstring promises. Write this at disposition
|
||||
time; the factory does not fabricate it.
|
||||
|
||||
## The three questions — each answered WITH EVIDENCE
|
||||
|
||||
1. **IMPROVE?** Can the unit be improved within its scope? Evidence or "no — because …".
|
||||
(Bounded to unit scope; larger findings spawn NEW issues, never expand this one.)
|
||||
2. **FIX?** Is it broken, buggy, or failing its expected outcome? Evidence: a failing
|
||||
test, a reproduced fault, or a code-level defect citation. A "no" needs evidence too.
|
||||
3. **MISSING CONNECTORS?** Are there expected outcomes this unit should connect to but
|
||||
doesn't (unwired events, dead parameters, promised-but-absent calls)? Evidence required.
|
||||
|
||||
A silent skip on any question is a factory bug — answer all three.
|
||||
|
||||
## Language verdict (per tranche R1 §6 rubric — evidence beats instinct)
|
||||
|
||||
Choose exactly one, with the evidence line:
|
||||
- **KEEP-PYTHON** — one sentence of justification.
|
||||
- **RECODE-<lang>** — rubric scores (all 7 dimensions) + benchmark-gate plan: the compiled
|
||||
replacement must beat the Python original on the MEASURED hot path, or the recode is
|
||||
rejected and recorded. No win, no recode.
|
||||
- **MERGE-into:<module>** — name the target and why.
|
||||
- **DEPRECATE** — name the caller that no longer needs it.
|
||||
|
||||
Go is NOT pre-decided. Python is not pre-decided. The rubric decides.
|
||||
|
||||
## Constraint context
|
||||
|
||||
- henxels structural contract: this unit's module ≤ 1,200 lines (or on the debt table's
|
||||
exception list — check before adding lines).
|
||||
- Spine: any module-surface change requires `store/spine/current.json` regen with the code
|
||||
change (`test_manifest_maps_every_module` is the arbiter for new top-level modules).
|
||||
- Debt table: docs/structural-contract.md.
|
||||
|
||||
## Pipeline plan
|
||||
|
||||
1. Branch `issue-<N>` from current master (existing issue-N convention).
|
||||
2. Scaffolding activation for any construction (construction_mode/decision per unit change).
|
||||
3. Research rounds are MANDATORY for any RECODE verdict (research-stage-headless; ≥2
|
||||
live-verified external sources per claim; per-source lessons; kill test per candidate).
|
||||
4. Implementation behind the interop contract (R1 §5): compiled units are standalone
|
||||
binaries — argv in, JSON on stdout, non-zero exit on failure, `--version` mandatory;
|
||||
Python wraps in a thin adapter; store stays the shared truth.
|
||||
5. Local gates: pytest + ruff + spine-store regen (if surface changed) + henxels.
|
||||
6. PR → pipeline-gate + ci + required checks → merge-robot → auto-version.
|
||||
|
||||
## Verification
|
||||
|
||||
- The specific test proving this unit's change: (write at disposition).
|
||||
- The keep-green set it must not break: full suite must stay green; named
|
||||
keep-green tests for this unit's callers.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Tranche: {TRANCHE_REF}
|
||||
- Wave: {wave} ({WAVE_NAMES.get(wave, 'unassigned')})
|
||||
- Inventory: docs/rebase/rebase-inventory.json (regenerate with scripts/rebase_inventory.py — never hand-edit)
|
||||
- Factory: scripts/rebase_issue_factory.py
|
||||
"""
|
||||
return {
|
||||
"title": issue_title(wave, relpath, unit),
|
||||
"body": body,
|
||||
"labels": ["rebase", f"wave-{wave}"],
|
||||
"meta": {
|
||||
"wave": wave,
|
||||
"file": relpath,
|
||||
"unit": unit,
|
||||
"line_start": entry["line_start"],
|
||||
"line_end": entry["line_end"],
|
||||
"async": entry.get("async", False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def fetch_existing_titles() -> set:
|
||||
"""Fetch all issue titles in the repo (paginated) for dedup."""
|
||||
token = load_gitea_token()
|
||||
titles = set()
|
||||
page = 1
|
||||
while True:
|
||||
url = (f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
f"?state=all&type=issues&limit=50&page={page}")
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
batch = json.loads(resp.read())
|
||||
if not batch:
|
||||
break
|
||||
titles.update(i["title"] for i in batch)
|
||||
if len(batch) < 50:
|
||||
break
|
||||
page += 1
|
||||
return titles
|
||||
|
||||
|
||||
def create_issue(entry: dict) -> dict:
|
||||
token = load_gitea_token()
|
||||
url = f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
payload = json.dumps({
|
||||
"title": entry["title"],
|
||||
"body": entry["body"],
|
||||
"labels": [], # label NAMES are set via the labels endpoint by auto-label; keep create minimal
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=payload, method="POST",
|
||||
headers={"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def cmd_plan(inv):
|
||||
counts = {}
|
||||
unassigned_modules = {}
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
wave = match_module(relpath)
|
||||
counts[wave] = counts.get(wave, 0) + 1
|
||||
if wave is None:
|
||||
unassigned_modules.setdefault(relpath, inv["files"][relpath]["lines"])
|
||||
report = {
|
||||
"units_per_wave": {str(k if k is not None else "UNASSIGNED"): v
|
||||
for k, v in sorted(counts.items(), key=lambda kv: str(kv[0]))},
|
||||
"unassigned_modules": dict(sorted(unassigned_modules.items(), key=lambda kv: -kv[1])),
|
||||
"unassigned_module_count": len(unassigned_modules),
|
||||
}
|
||||
print(json.dumps(report, indent=1))
|
||||
COVERAGE_OUT.write_text(json.dumps(report, indent=1) + "\n")
|
||||
print(f"\ncoverage written to {COVERAGE_OUT.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 0 if not unassigned_modules else 1
|
||||
|
||||
|
||||
def cmd_sample(inv, wave, limit, out):
|
||||
emitted = 0
|
||||
records = []
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
records.append(render_issue(wave, relpath, unit, entry))
|
||||
emitted += 1
|
||||
if emitted >= limit:
|
||||
break
|
||||
Path(out).write_text("\n".join(json.dumps(r) for r in records) + "\n")
|
||||
print(f"sampled {emitted} issues for wave {wave} -> {out}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_create(inv, wave, limit, dry_gap):
|
||||
existing = fetch_existing_titles()
|
||||
created, skipped = 0, 0
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
title = issue_title(wave, relpath, unit)
|
||||
if title in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
issue = create_issue(render_issue(wave, relpath, unit, entry))
|
||||
created += 1
|
||||
print(f"created #{issue.get('number')}: {title}", file=sys.stderr)
|
||||
if limit and created >= limit:
|
||||
break
|
||||
time.sleep(dry_gap)
|
||||
print(f"\nwave {wave}: created={created} skipped(dedup)={skipped}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("command", choices=["plan", "sample", "create"])
|
||||
ap.add_argument("--wave", type=int, help="wave number (2-7)")
|
||||
ap.add_argument("--limit", type=int, default=0, help="max issues for sample/create (0 = unlimited; sample defaults to 5)")
|
||||
ap.add_argument("--out", default=None, help="sample output file")
|
||||
ap.add_argument("--gap", type=float, default=0.5, help="seconds between creates")
|
||||
args = ap.parse_args()
|
||||
|
||||
inv_cache.update(json.loads(INVENTORY.read_text()))
|
||||
inv = inv_cache
|
||||
|
||||
if args.command == "plan":
|
||||
return cmd_plan(inv)
|
||||
if args.command == "sample":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
out = args.out or f"docs/rebase-factory-sample-w{args.wave}.json"
|
||||
return cmd_sample(inv, args.wave, args.limit or 5, out)
|
||||
if args.command == "create":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
return cmd_create(inv, args.wave, args.limit, args.gap)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+88
-12
@@ -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", "https://gitea.thingscouldgetdicey.online")
|
||||
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_TOKEN = load_gitea_token()
|
||||
REPO = "conductor/rune"
|
||||
TUI_DIR = "/mnt/workdrive/rune"
|
||||
@@ -54,6 +54,15 @@ UMBRELLA_TAGS = {
|
||||
}
|
||||
|
||||
|
||||
# Issue template directory — canonical copies live beside this script
|
||||
# (mirrored to .gitea/ISSUE_TEMPLATE/ for the web-UI submission path).
|
||||
ISSUE_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "issue_templates")
|
||||
|
||||
# kind -> Gitea label that MUST be present on the issue
|
||||
KIND_LABEL = {"feature": "feature", "bug": "bug", "research": "research"}
|
||||
VERSION_INTENTS = ("major", "minor", "patch", "none")
|
||||
|
||||
|
||||
class Custodian:
|
||||
"""The Custodian agent for Gitea-based self-improvement."""
|
||||
|
||||
@@ -105,23 +114,90 @@ class Custodian:
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:200]}
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: List[str] = None) -> dict:
|
||||
"""Create a Gitea issue with plain-language description.
|
||||
def _ensure_label(self, name: str, description: str = "", color: str = "#6f42c1") -> bool:
|
||||
"""Make sure a label exists on the repo; return its id or None."""
|
||||
labels = self._gitea_api("labels")
|
||||
if isinstance(labels, list):
|
||||
for l in labels:
|
||||
if l["name"] == name:
|
||||
return l["id"]
|
||||
data = {"name": name, "description": description or name, "color": color}
|
||||
created = self._gitea_api("labels", method="POST", data=data)
|
||||
return created.get("id") if isinstance(created, dict) else None
|
||||
|
||||
Labels should be umbrella names (memory, governance, etc).
|
||||
They get converted to IDs automatically.
|
||||
def _load_issue_template(self, kind: str) -> str:
|
||||
"""Load the canonical issue template for a kind, WITHOUT the web-UI
|
||||
frontmatter (name/about/title/labels — that metadata belongs to the
|
||||
submission form, not the issue body). Empty string if absent."""
|
||||
path = os.path.join(ISSUE_TEMPLATE_DIR, f"{kind}.md")
|
||||
try:
|
||||
with open(path) as f:
|
||||
text = f.read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
text = text[end + 4:]
|
||||
return text.lstrip("\n")
|
||||
|
||||
def create_issue(
|
||||
self,
|
||||
title: str,
|
||||
body: str,
|
||||
labels: List[str] = None,
|
||||
kind: str = "feature",
|
||||
version_intent: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a Gitea issue assembled from the canonical template.
|
||||
|
||||
kind: feature | bug | research — selects the template and forces the
|
||||
matching label (the research label is auto-created if missing).
|
||||
version_intent: major | minor | patch | none — pre-ticks the matching
|
||||
box in the template's Version impact section. The pipeline
|
||||
verifies the real bump at merge and flags mismatches.
|
||||
labels: umbrella names (memory, governance, ...); converted to IDs.
|
||||
|
||||
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.
|
||||
"""
|
||||
# Convert label names to IDs
|
||||
# 1. assemble body from template (graceful: unformatted if template missing)
|
||||
template = self._load_issue_template(kind)
|
||||
text = body.strip()
|
||||
if template and "<!-- SUBMITTED_BODY -->" in template:
|
||||
text = template.replace("<!-- SUBMITTED_BODY -->", text, 1)
|
||||
if version_intent in VERSION_INTENTS:
|
||||
text = text.replace(f"- [ ] {version_intent} ", f"- [x] {version_intent} ", 1)
|
||||
|
||||
# 2. labels: umbrella names + mandatory kind label
|
||||
labels = list(labels or [])
|
||||
kind_label = KIND_LABEL.get(kind)
|
||||
if kind_label and kind_label not in labels:
|
||||
labels.append(kind_label)
|
||||
|
||||
all_labels = self._gitea_api("labels")
|
||||
label_map = {l["name"]: l["id"] for l in all_labels} if isinstance(all_labels, list) else {}
|
||||
label_ids = []
|
||||
if labels:
|
||||
all_labels = self._gitea_api("labels")
|
||||
if isinstance(all_labels, list):
|
||||
label_map = {l["name"]: l["id"] for l in all_labels}
|
||||
label_ids = [label_map[l] for l in labels if l in label_map]
|
||||
for l in labels:
|
||||
if l in label_map:
|
||||
label_ids.append(label_map[l])
|
||||
else:
|
||||
lid = self._ensure_label(l)
|
||||
if lid:
|
||||
label_ids.append(lid)
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"body": text,
|
||||
"labels": label_ids,
|
||||
}
|
||||
result = self._gitea_api("issues", method="POST", data=data)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
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. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
|
||||
**WHAT (bug):**
|
||||
**WHERE:**
|
||||
**EVIDENCE:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every fix must clear these before the issue closes. -->
|
||||
- [ ] TDD: regression test written first, red → green
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
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
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
WHY — design-spec reference (S#) or reason.
|
||||
FILES — affected components.
|
||||
ACCEPTANCE — frozen-test file + test count + canary name.
|
||||
DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
|
||||
**WHAT:**
|
||||
**WHY:**
|
||||
**FILES:**
|
||||
**ACCEPTANCE:**
|
||||
**DEPENDENCIES:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every stage must clear these before the issue closes. -->
|
||||
- [ ] TDD: frozen tests written first, red → green (N passed)
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -0,0 +1,216 @@
|
||||
"""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,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.86.0"
|
||||
__version__ = "0.89.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
|
||||
|
||||
@@ -64,6 +64,7 @@ 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"
|
||||
|
||||
@@ -75,6 +76,7 @@ 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,
|
||||
}
|
||||
@@ -87,10 +89,14 @@ 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"]]
|
||||
@@ -107,6 +113,56 @@ 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:
|
||||
@@ -166,6 +222,11 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.86.0"' in rune_version,
|
||||
'__version__ = "0.89.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -181,9 +181,16 @@ class ConstructionInterface:
|
||||
if contract_id:
|
||||
target = self.scaffold.contract(doc, contract_id)
|
||||
else:
|
||||
# most recent non-closed contract = the active construction
|
||||
# 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
|
||||
open_contracts = [e for e in contracts
|
||||
if e.get("status") == "open"]
|
||||
if e.get("status") == "open"
|
||||
and e.get("phase") not in TERMINAL_PHASES]
|
||||
target = (sorted(open_contracts,
|
||||
key=lambda e: e.get("observed_at", ""))[-1]
|
||||
if open_contracts else None)
|
||||
|
||||
@@ -805,13 +805,22 @@ class RuneOperator:
|
||||
except Exception: # noqa: S110 — notes are best-effort by design
|
||||
pass
|
||||
|
||||
def _construction_activation(self, description: str, task_id: str) -> None:
|
||||
def _construction_activation(self, description: str, task_id: str,
|
||||
channel: str = OPERATOR_CHANNEL) -> None:
|
||||
"""SCAFFOLD-TRANCHE-0011/0012 §11: contextual construction
|
||||
classification + activation on the NORMAL input path. Deterministic;
|
||||
the mode lands in the durable scaffold store where the shared
|
||||
Construction Context (and the future TUI) reads it. Delegates all
|
||||
lifecycle decisions to construction_mode/construction_activation —
|
||||
NO duplicate intelligence here."""
|
||||
NO duplicate intelligence here.
|
||||
|
||||
§17J criterion 1 (T22 wiring): the canonical intent layer
|
||||
(construction_intent) resolves MEANING before the ONE
|
||||
classifier decides mode — the boundary the intent module
|
||||
documents ("the intent layer resolves MEANING; classify_mode
|
||||
decides mode"). A turn the intent layer resolves as
|
||||
DISCUSSION is not a construction demand, so no construction
|
||||
state is recorded for it."""
|
||||
from .construction_mode import classify_mode, _STANDALONE_WORDS
|
||||
from .construction_activation import activate
|
||||
# §15J case F: manual override feeds the SAME decision path — a
|
||||
@@ -849,6 +858,56 @@ class RuneOperator:
|
||||
from .scaffold import Scaffold
|
||||
s = Scaffold(self._construction_store(), "issue161-harness")
|
||||
doc = s.load()
|
||||
# §17J criterion 1: meaning gate BEFORE the classifier.
|
||||
# Context = the operator's own canonical conversation window
|
||||
# (no new state store); the current message is excluded so it
|
||||
# is not double-recorded as its own referent.
|
||||
try:
|
||||
from .construction_intent import (
|
||||
ConversationContext, resolve_construction_intent)
|
||||
ctx = ConversationContext()
|
||||
for _m in self.conversation_window(channel, n=8):
|
||||
_txt = str(_m.get("content", ""))
|
||||
if _txt and _txt != description:
|
||||
ctx.record_turn(str(_m.get("role", "user")), _txt)
|
||||
_intent = resolve_construction_intent(description, ctx)
|
||||
except Exception as exc: # noqa: BLE001 — intent layer
|
||||
# unavailable: PRESERVE the pre-existing behaviour exactly
|
||||
# (classify+activate) rather than silently changing
|
||||
# governance in an unproven direction. The note is appended to
|
||||
# THIS doc (not via _construction_note, which does its own
|
||||
# load/save) because the normal path saves `doc` afterwards —
|
||||
# a separate write would be clobbered (lost update).
|
||||
_intent = None
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_note",
|
||||
"text": (f"intent layer unavailable ({exc}); "
|
||||
f"classifier path unchanged")[:300],
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
# An explicit user override is USER AUTHORITY and outranks the
|
||||
# meaning gate (§15J case F: the override is already carried with
|
||||
# user provenance into the SAME classify_mode call).
|
||||
if (override is None and _intent is not None
|
||||
and _intent.action_intent == "DISCUSSION"):
|
||||
# The intent layer resolved this turn as DISCUSSION: it is not a
|
||||
# construction demand, so no construction state is created. The
|
||||
# record keeps the SAME canonical observation kind/shape every
|
||||
# other input produces (mode vocabulary unchanged — DISCUSSION
|
||||
# maps to NOT_CONSTRUCTION, "no construction state"), with the
|
||||
# intent evidence retained so the verdict is auditable.
|
||||
doc.setdefault("observations", []).append({
|
||||
"kind": "construction_activated",
|
||||
"task": task_id,
|
||||
"mode": "NOT_CONSTRUCTION",
|
||||
"activated": False,
|
||||
"lifecycle": "",
|
||||
"classification_evidence": _intent.classification_evidence[:3],
|
||||
"intent_layer": "DISCUSSION",
|
||||
"observed_at": __import__("datetime").datetime.utcnow()
|
||||
.strftime("%Y-%m-%dT%H:%M:%SZ")})
|
||||
s.save(doc)
|
||||
return
|
||||
rec = activate(mode_decision, need=type("N", (), {
|
||||
"objective": description,
|
||||
"project": "rune",
|
||||
@@ -929,7 +988,8 @@ class RuneOperator:
|
||||
# failure NEVER blocks the ordinary task path (same lifecycle
|
||||
# guarantee as the route above).
|
||||
try:
|
||||
self._construction_activation(description, state_task.task_id)
|
||||
self._construction_activation(description, state_task.task_id,
|
||||
channel=channel)
|
||||
except Exception as exc: # noqa: BLE001 — construction must not break tasks
|
||||
try:
|
||||
self._task_state.transition(
|
||||
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.86.0
|
||||
Version: v0.89.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1885,7 +1885,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.86.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.88.4 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -57,7 +57,7 @@ from rune.structural_store import ( # shared generic core — no Rune constants
|
||||
task_entities,
|
||||
)
|
||||
|
||||
__all__ = ["Scaffold", "PHASES", "LEGAL_TRANSITIONS", "REPO_PLACEMENTS", "REPO_SECRET_FIELDS"]
|
||||
__all__ = ["Scaffold", "PHASES", "LEGAL_TRANSITIONS", "TERMINAL_PHASES", "REPO_PLACEMENTS", "REPO_SECRET_FIELDS"]
|
||||
|
||||
# ── constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,6 +73,10 @@ LEGAL_TRANSITIONS = {
|
||||
"failed": {"building", "abandoned"}, # retry path
|
||||
"abandoned": set(),
|
||||
}
|
||||
# Terminal phases are DERIVED from the canonical transition table: a
|
||||
# phase with no legal successor ends the contract. One authority —
|
||||
# consumers must not re-declare their own terminal list.
|
||||
TERMINAL_PHASES = frozenset(p for p, nxt in LEGAL_TRANSITIONS.items() if not nxt)
|
||||
STATUSES = {"open", "closed", "failed", "abandoned"}
|
||||
REPO_PLACEMENTS = {"RUNE_REPO", "EXISTING_PROJECT_REPO", "NEW_MANAGED_REPO"}
|
||||
REPO_SECRET_FIELDS = {"token", "api_key", "password", "secret", "auth_header", "credential", "api_token"}
|
||||
|
||||
+252
-153
File diff suppressed because it is too large.
Load diff
@@ -23,6 +23,83 @@ from rune.builder_bridge import ( # noqa: E402
|
||||
)
|
||||
|
||||
|
||||
class ConstructionProjectionActive(unittest.TestCase):
|
||||
"""T22: with a LIVE contract the projection surfaces canonical state
|
||||
(contract/phase/scope/obligations/next-action) and renders it in the
|
||||
capsule markdown; the read-only projection never mutates the store."""
|
||||
|
||||
def _active_store(self, td):
|
||||
from pathlib import Path
|
||||
from rune.scaffold import Scaffold
|
||||
store = Path(td) / "scaffold"
|
||||
sc = Scaffold(store, "fixture-proj")
|
||||
sc.init(purpose="fixture")
|
||||
doc = sc.load()
|
||||
sc.new_contract(doc, contract_id="c-t22", need="add the retry backoff",
|
||||
origin="user", scope=["src/rune/retry.py"],
|
||||
requirement="prove the projection")
|
||||
sc.save(doc)
|
||||
return str(store)
|
||||
|
||||
def test_active_contract_projects_and_renders(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
store = self._active_store(td)
|
||||
old = os.environ.get("RUNE_CONSTRUCTION_STORE")
|
||||
os.environ["RUNE_CONSTRUCTION_STORE"] = store
|
||||
try:
|
||||
lines = builder_bridge._active_construction_lines()
|
||||
self.assertTrue(lines, "an open non-terminal contract must project")
|
||||
self.assertIn("c-t22", lines[0])
|
||||
self.assertTrue(any("scope" in ln for ln in lines), lines)
|
||||
cap = builder_bridge.preflight("add the retry backoff")
|
||||
self.assertTrue(cap.construction_lines)
|
||||
md = cap.to_markdown()
|
||||
self.assertIn("Active Scaffolding construction", md)
|
||||
# read-only: the store is unchanged by projection
|
||||
before = (Path(store) / "current.json").read_text()
|
||||
builder_bridge._active_construction_lines()
|
||||
self.assertEqual(before, (Path(store) / "current.json").read_text())
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_CONSTRUCTION_STORE", None)
|
||||
else:
|
||||
os.environ["RUNE_CONSTRUCTION_STORE"] = old
|
||||
|
||||
def test_store_resolution_honours_env_override(self):
|
||||
self.assertEqual(builder_bridge._construction_store(),
|
||||
os.environ.get("RUNE_CONSTRUCTION_STORE"))
|
||||
|
||||
|
||||
class ConstructionProjection(unittest.TestCase):
|
||||
"""T22: the bridge surfaces the ACTIVE Scaffolding construction (the
|
||||
shared Construction Context) to the builder — read-only projection,
|
||||
inject-nothing preserved when no contract is active."""
|
||||
|
||||
def test_no_active_contract_projects_nothing(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
old = os.environ.get("RUNE_CONSTRUCTION_STORE")
|
||||
os.environ["RUNE_CONSTRUCTION_STORE"] = str(Path(td) / "scaffold")
|
||||
try:
|
||||
self.assertEqual(builder_bridge._active_construction_lines(), [])
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUNE_CONSTRUCTION_STORE", None)
|
||||
else:
|
||||
os.environ["RUNE_CONSTRUCTION_STORE"] = old
|
||||
|
||||
def test_terminal_phase_is_not_active_construction(self):
|
||||
"""A contract left status='open' with a TERMINAL phase must not be
|
||||
reported as active (live store had 8 such records)."""
|
||||
from rune.scaffold import TERMINAL_PHASES
|
||||
self.assertIn("abandoned", TERMINAL_PHASES)
|
||||
self.assertIn("closed", TERMINAL_PHASES)
|
||||
self.assertNotIn("building", TERMINAL_PHASES)
|
||||
|
||||
|
||||
class BridgeContract(unittest.TestCase):
|
||||
def test_preflight_local_task_is_cheap_and_bounded(self):
|
||||
cap = preflight("Fix the telemetry summary log level constant in stage_telemetry.py")
|
||||
|
||||
@@ -34,6 +34,16 @@ def test_artifact_exists_and_parses():
|
||||
|
||||
|
||||
def test_single_live_defect():
|
||||
"""The bug audit converged on exactly one live defect: #324."""
|
||||
"""Live-defect record is pinned: no unverified defect may appear silently.
|
||||
|
||||
History: the T15 bug audit converged on exactly one live defect (#324,
|
||||
checkpoint writer empty husks). 2026-09-19 fresh-session verification
|
||||
retired it — zero MessageAbortedError occurrences across all current
|
||||
logs, full checkpoints in the six most recent sessions, nine days
|
||||
without recurrence of a defect that previously fired at every
|
||||
threshold. Verified by present-absence (09-10 logs rotated). #324 is
|
||||
closed FIXED_VERIFIED and the record is now empty. A new live defect
|
||||
must be re-audited and this pin updated deliberately.
|
||||
"""
|
||||
data = __import__("json").loads(grc.ARTIFACT.read_text())
|
||||
assert data["live_defects"] == [324]
|
||||
assert data["live_defects"] == []
|
||||
+17
-10
@@ -4,14 +4,17 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL = REPO / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
CANONICAL_DIR = REPO / "config" / "mimo-hooks"
|
||||
# T22: the managed set is the installer's authority — a hook that reaches the
|
||||
# builder must be version-controlled and drift-checked like any other.
|
||||
MANAGED = ["event-wait-guard.ts", "builder-bridge.ts"]
|
||||
# T21.3: the hook lives at the ACTUAL controller user's home, not the
|
||||
# test runner's. Use an env override or skip when unavailable.
|
||||
import os as _os
|
||||
INSTALLED = Path(_os.environ.get(
|
||||
"MIMOCODE_HOOK_TARGET",
|
||||
HOOK_DIR = Path(_os.environ.get(
|
||||
"MIMOCODE_HOOK_DIR",
|
||||
str(Path(_os.environ.get("RUNE_HOOK_TARGET_HOME", "/home/nick"))
|
||||
/ ".mimocode" / "hooks" / "event-wait-guard.ts")))
|
||||
/ ".mimocode" / "hooks")))
|
||||
|
||||
|
||||
def _sha(p: Path) -> str:
|
||||
@@ -19,15 +22,19 @@ def _sha(p: Path) -> str:
|
||||
|
||||
|
||||
def test_213c_canonical_exists():
|
||||
assert CANONICAL.is_file()
|
||||
for name in MANAGED:
|
||||
assert (CANONICAL_DIR / name).is_file(), f"canonical missing: {name}"
|
||||
|
||||
|
||||
def test_213c_installed_matches_canonical():
|
||||
assert INSTALLED.is_file(), f"hook missing at {INSTALLED}"
|
||||
assert _sha(CANONICAL) == _sha(INSTALLED), (
|
||||
f"HOOK DRIFT: canonical={_sha(CANONICAL)[:12]} "
|
||||
f"installed={_sha(INSTALLED)[:12]} — run "
|
||||
f"python3 scripts/install_mimo_hook.py to sync")
|
||||
for name in MANAGED:
|
||||
canonical = CANONICAL_DIR / name
|
||||
installed = HOOK_DIR / name
|
||||
assert installed.is_file(), f"hook missing at {installed}"
|
||||
assert _sha(canonical) == _sha(installed), (
|
||||
f"HOOK DRIFT: {name} canonical={_sha(canonical)[:12]} "
|
||||
f"installed={_sha(installed)[:12]} — run "
|
||||
f"python3 scripts/install_mimo_hook.py to sync")
|
||||
|
||||
|
||||
def test_213c_installer_exists():
|
||||
|
||||
@@ -97,6 +97,61 @@ def test_15j_native_not_construction_touches_nothing(op):
|
||||
assert not projects.exists() or not any(projects.glob("*/current.json"))
|
||||
|
||||
|
||||
def test_15j_intent_layer_unavailable_preserves_classifier_path(op, monkeypatch):
|
||||
"""If the meaning layer is unavailable the PRE-EXISTING behaviour is
|
||||
preserved exactly (classify + activate) and the reason is recorded —
|
||||
governance is never silently changed in an unproven direction."""
|
||||
import rune.construction_intent as cint
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise RuntimeError("intent layer offline")
|
||||
|
||||
monkeypatch.setattr(cint, "resolve_construction_intent", _boom)
|
||||
op, tmp = op
|
||||
op.send_task("fix the broken retry handling in this project")
|
||||
rec = _last_activation(tmp)
|
||||
assert rec["mode"] == "EXISTING_PROJECT_CHANGE", rec
|
||||
assert rec["activated"] is True
|
||||
assert rec.get("intent_layer") is None
|
||||
doc = json.loads((tmp / ".rune" / "scaffold" / "current.json").read_text())
|
||||
notes = [o for o in doc.get("observations", [])
|
||||
if o.get("kind") == "construction_note"]
|
||||
assert any("intent layer unavailable" in str(n.get("text", "")) for n in notes), notes
|
||||
|
||||
|
||||
def test_15j_native_question_with_target_is_not_construction(op):
|
||||
"""§17J criterion 1 on the NORMAL path (T22 wiring repair).
|
||||
|
||||
A pure question that happens to carry a target reference
|
||||
("this module") must NOT record construction state. Before the
|
||||
intent-layer gate this classified EXISTING_PROJECT_CHANGE (a target
|
||||
reference was read as scope evidence) and recorded an activation for
|
||||
a discussion turn. The canonical meaning layer resolves it as
|
||||
DISCUSSION; the classifier is never reached.
|
||||
"""
|
||||
op, tmp = op
|
||||
op.send_task("explain how this module works")
|
||||
rec = _last_activation(tmp)
|
||||
assert rec is not None, "every input records its classification outcome"
|
||||
assert rec["mode"] == "NOT_CONSTRUCTION", rec
|
||||
assert rec["activated"] is False
|
||||
assert rec.get("intent_layer") == "DISCUSSION", rec
|
||||
# no project invention
|
||||
projects = (tmp / ".rune" / "scaffold" / "projects")
|
||||
assert not projects.exists() or not any(projects.glob("*/current.json"))
|
||||
|
||||
|
||||
def test_15j_native_real_construction_still_activates(op):
|
||||
"""The meaning gate must not suppress genuine construction demands —
|
||||
the guard is a question/discussion gate, not an activation blocker."""
|
||||
op, tmp = op
|
||||
op.send_task("fix the broken retry handling in this project")
|
||||
rec = _last_activation(tmp)
|
||||
assert rec["mode"] == "EXISTING_PROJECT_CHANGE", rec
|
||||
assert rec["activated"] is True
|
||||
assert rec.get("intent_layer") is None, rec
|
||||
|
||||
|
||||
def test_15j_native_unknown_probes_without_invention(op):
|
||||
op, tmp = op
|
||||
op.send_task("Build something useful for this")
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Wave 0 interop proof tests (R1 §5): Rust unit callable from tests via the
|
||||
thin Python adapter. Skips when the binary is not built so CI without Rust
|
||||
stays green; build locally with:
|
||||
cargo build --release --quiet (from interop/rebase-proof/)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from shared.native_units import PROOF_UNIT, NativeResult, run_native, run_proof # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not PROOF_UNIT.is_file(), reason="rebase-proof binary not built (cargo build --release in interop/rebase-proof)"
|
||||
)
|
||||
|
||||
|
||||
def test_version_flag_is_mandatory_contract():
|
||||
result = run_proof(["--version"])
|
||||
assert result.ok
|
||||
assert result.exit_code == 0
|
||||
assert result.data is None or result.data == {} # version prints plain text, not JSON
|
||||
assert result.raw_stdout.strip() == "rebase-proof 0.1.0"
|
||||
|
||||
|
||||
def test_sum_happy_path_json_shape():
|
||||
result = run_proof(["sum", "1", "2", "3"])
|
||||
assert result.ok
|
||||
assert result.data == {"ok": True, "op": "sum", "result": 6}
|
||||
|
||||
|
||||
def test_hash_is_deterministic():
|
||||
first = run_proof(["hash", "hello"])
|
||||
second = run_proof(["hash", "hello"])
|
||||
assert first.ok and second.ok
|
||||
assert first.data == second.data
|
||||
assert first.data["fnv1a64"] == "a430d84680aabd0b"
|
||||
|
||||
|
||||
def test_error_op_exits_nonzero_with_json_error():
|
||||
result = run_proof(["fail"])
|
||||
assert not result.ok
|
||||
assert result.exit_code == 2
|
||||
assert result.data == {"ok": False, "error": "deliberate failure"}
|
||||
assert "deliberate failure for adapter proof" in result.stderr
|
||||
|
||||
|
||||
def test_unknown_op_is_data_not_exception():
|
||||
result = run_proof(["no-such-op"])
|
||||
assert not result.ok
|
||||
assert result.exit_code == 2
|
||||
assert "unknown op" in result.data["error"]
|
||||
|
||||
|
||||
def test_timeout_enforcement_kills_unit():
|
||||
result = run_proof(["sleep", "30000"], timeout_s=0.5)
|
||||
assert result.timed_out
|
||||
assert not result.ok
|
||||
assert result.exit_code == -1
|
||||
|
||||
|
||||
def test_missing_binary_raises_adapter_misuse():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
run_native(ROOT / "interop" / "rebase-proof" / "target" / "release" / "does-not-exist", ["sum", "1"])
|
||||
|
||||
|
||||
def test_typed_result_is_the_contract_surface():
|
||||
result = run_proof(["sum", "10", "20"])
|
||||
assert isinstance(result, NativeResult)
|
||||
assert result.duration_ms >= 0
|
||||
assert result.stderr == ""
|
||||
@@ -0,0 +1,131 @@
|
||||
"""JSONL streaming reader for native units (W7 contract gap, rubric §W7 point 5).
|
||||
|
||||
Contract under test: a long-running native unit streams one JSON object per
|
||||
line — ``{"ok":true,"event":...}`` progress objects — and ends with a terminal
|
||||
envelope as the final stdout line. The adapter yields per-line events and
|
||||
validates the terminal envelope. Fixtures are Python scripts standing in for
|
||||
compiled units; the adapter treats any executable path identically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from shared.native_units import NativeResult, stream_native # noqa: E402
|
||||
|
||||
FIXTURE = """\
|
||||
#!/usr/bin/env python3
|
||||
import sys, time
|
||||
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "stream"
|
||||
|
||||
if mode == "stream":
|
||||
for i in range(3):
|
||||
print(f'{{"ok":true,"event":"step-{i}"}}', flush=True)
|
||||
time.sleep(0.02)
|
||||
print('{"ok":true,"result":"done"}', flush=True)
|
||||
elif mode == "slow":
|
||||
print('{"ok":true,"event":"started"}', flush=True)
|
||||
time.sleep(30)
|
||||
print('{"ok":true,"result":"done"}', flush=True)
|
||||
elif mode == "bad":
|
||||
print('{"ok":true,"event":"one"}', flush=True)
|
||||
print('not json at all', flush=True)
|
||||
print('{"ok":true,"result":"done"}', flush=True)
|
||||
elif mode == "junk-last":
|
||||
print('{"ok":true,"event":"one"}', flush=True)
|
||||
print('trailing noise', flush=True)
|
||||
elif mode == "fail":
|
||||
print('{"ok":false,"error":"boom"}', flush=True)
|
||||
sys.exit(2)
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unit(tmp_path: Path) -> Path:
|
||||
script = tmp_path / "stream-unit.py"
|
||||
script.write_text(FIXTURE)
|
||||
script.chmod(0o755)
|
||||
return script
|
||||
|
||||
|
||||
def _split(yielded: list) -> tuple[list[dict], NativeResult]:
|
||||
terminal = yielded[-1]
|
||||
assert isinstance(terminal, NativeResult), f"last yield must be the terminal NativeResult, got {type(terminal)}"
|
||||
events = [item for item in yielded if isinstance(item, dict)]
|
||||
return events, terminal
|
||||
|
||||
|
||||
def test_streaming_yields_events_then_terminal_result(unit: Path):
|
||||
yielded = list(stream_native(unit, ["stream"]))
|
||||
events, terminal = _split(yielded)
|
||||
# 3 progress events + the terminal envelope (also a parsed dict line)
|
||||
assert [e.get("event") for e in events] == ["step-0", "step-1", "step-2", None]
|
||||
assert events[-1] == {"ok": True, "result": "done"}
|
||||
assert terminal.ok
|
||||
assert terminal.exit_code == 0
|
||||
assert terminal.data == {"ok": True, "result": "done"}
|
||||
|
||||
|
||||
def test_events_arrive_before_process_exit(unit: Path):
|
||||
"""Progress is visible DURING the run, not after it — the point of streaming."""
|
||||
seen: list[str] = []
|
||||
for item in stream_native(unit, ["stream"]):
|
||||
if isinstance(item, dict) and "event" in item:
|
||||
seen.append(item["event"])
|
||||
assert seen == ["step-0", "step-1", "step-2"]
|
||||
|
||||
|
||||
def test_timeout_kills_midstream(unit: Path):
|
||||
yielded = list(stream_native(unit, ["slow"], timeout_s=0.5))
|
||||
events, terminal = _split(yielded)
|
||||
assert terminal.timed_out
|
||||
assert not terminal.ok
|
||||
assert terminal.exit_code == -1
|
||||
# the first progress event was still delivered before the kill
|
||||
assert events and events[0] == {"ok": True, "event": "started"}
|
||||
|
||||
|
||||
def test_malformed_interior_lines_skipped(unit: Path):
|
||||
yielded = list(stream_native(unit, ["bad"]))
|
||||
events, terminal = _split(yielded)
|
||||
# the malformed line is skipped; progress + terminal envelope remain
|
||||
progress = [e for e in events if "event" in e]
|
||||
assert [e["event"] for e in progress] == ["one"]
|
||||
assert terminal.ok
|
||||
assert terminal.data == {"ok": True, "result": "done"}
|
||||
|
||||
|
||||
def test_trailing_noise_invalidates_terminal_envelope(unit: Path):
|
||||
"""JSON was emitted, but the FINAL line is not an envelope — contract violation."""
|
||||
yielded = list(stream_native(unit, ["junk-last"]))
|
||||
events, terminal = _split(yielded)
|
||||
assert not terminal.ok
|
||||
assert events and events[0] == {"ok": True, "event": "one"}
|
||||
|
||||
|
||||
def test_failure_envelope_is_data_not_exception(unit: Path):
|
||||
yielded = list(stream_native(unit, ["fail"]))
|
||||
events, terminal = _split(yielded)
|
||||
assert not terminal.ok
|
||||
assert terminal.exit_code == 2
|
||||
assert terminal.data == {"ok": False, "error": "boom"}
|
||||
|
||||
|
||||
def test_events_attached_to_terminal_result(unit: Path):
|
||||
yielded = list(stream_native(unit, ["stream"]))
|
||||
_, terminal = _split(yielded)
|
||||
assert len(terminal.events) == 4 # 3 progress + 1 terminal envelope
|
||||
assert terminal.events[-1] == {"ok": True, "result": "done"}
|
||||
|
||||
|
||||
def test_missing_binary_raises_adapter_misuse(tmp_path: Path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
list(stream_native(tmp_path / "no-such-binary", ["stream"]))
|
||||
@@ -233,6 +233,22 @@ class TestLifecycleExtended(unittest.TestCase):
|
||||
"""The consolidated helper preserves: task state, durable
|
||||
task_state, outcome event, current-task pointer, error text."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# Isolation law: the event-bus journal dir is a module-level
|
||||
# constant resolved from the ambient RUNE_HOME at import time
|
||||
# (memory_env.memory_dir() → hardcoded default). Without
|
||||
# pinning it here, the test reads whatever journal the host
|
||||
# resolves to — on a CI runner the default path is
|
||||
# unwritable/nonexistent, emit()'s deliberate degradation
|
||||
# swallows the write, and the scan below finds nothing.
|
||||
from rune import event_bus as _eb
|
||||
orig_journal = _eb.JOURNAL_DIR
|
||||
orig_event_log = _eb.EVENT_LOG
|
||||
journal_dir = Path(tmp) / "journal"
|
||||
_eb.JOURNAL_DIR = str(journal_dir)
|
||||
_eb.EVENT_LOG = str(journal_dir / "events.jsonl")
|
||||
self.addCleanup(setattr, _eb, "JOURNAL_DIR", orig_journal)
|
||||
self.addCleanup(setattr, _eb, "EVENT_LOG", orig_event_log)
|
||||
|
||||
op = self._make_op("execute", tmp)
|
||||
result = op.send_task("Fix the failing test in tests/test_x.py")
|
||||
task_id = result["task"]["task_id"]
|
||||
@@ -242,8 +258,6 @@ class TestLifecycleExtended(unittest.TestCase):
|
||||
self.assertTrue(events_path.exists())
|
||||
# task.outcome rides the event_bus journal (memory_dir/journal),
|
||||
# not the operator's task-transition events.jsonl.
|
||||
from rune import event_bus as _eb
|
||||
journal_dir = Path(_eb.JOURNAL_DIR)
|
||||
outcomes = []
|
||||
if journal_dir.exists():
|
||||
for j in journal_dir.glob("*.jsonl"):
|
||||
|
||||
Reference in new issue
Block a user