Compare commits

...
113 Commits
Author SHA1 Message Date
Nick da61fd7d44 docs: webhook delivery test marker 2 2026-09-12 00:31:12 +01:00
Nick 7342d77e6f docs: webhook delivery test 2 2026-09-12 00:31:02 +01:00
Nick 436f760675 docs: webhook delivery test marker 2026-09-12 00:28:42 +01:00
Nick d62b717542 test: deliberate lint failure (wait/resume Part 14) 2026-09-12 00:14:41 +01:00
Nick 97de570b27 test: deliberate lint failure fixture (wait/resume failure experiment) 2026-09-12 00:13:41 +01:00
Nick 22aebbb102 test: intentionally broken syntax for W/R failure experiment 2026-09-12 00:13:27 +01:00
Nick 7e33294527 docs: wait/resume live experiment marker (branch) 2026-09-12 00:07:52 +01:00
conductor fcc9989393 chore(release): auto-bump version to 0.66.14 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 22:54:16 +01:00
Nick ead4a24866 fix(ci): allow the reconciliation JSON authority in top-level docs henxel 2026-09-11 22:53:48 +01:00
conductor cf51f7fd7c chore(release): auto-bump version to 0.66.13 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 22:51:10 +01:00
Nick f182ab8601 feat(reconciliation): canonical issue reconciliation artifact + consistency checker (T15 Parts J/K/L)
- docs/GITEA_ISSUE_RECONCILIATION.md: human-readable projection (states,
  bug-label audit with per-issue evidence, open-issue reconciliation table)
- docs/gitea_issue_reconciliation.json: machine-readable authority
  (72 records; all open issues covered; single live defect #324).
  Placed under docs/ because state/ is the gitignored runtime dir.
- scripts/gitea_reconciliation_check.py: deterministic drift check —
  state/label/coverage invariants; bug-label invariant applies to OPEN
  issues (closed labels are historical); exit 1 with itemized drift.
- Part K audit: closed #139 #157 (FIXED_VERIFIED with evidence comments);
  bug labels removed from #204 #145 #126 #37 #16 (not defects) and the
  stuck label on closed #314. Open bug-labelled: 8 → 1.
- 1289 tests green; ratchet 352/80; henxels clean.
2026-09-11 22:50:44 +01:00
conductor 5f9ba5784c chore(release): auto-bump version to 0.66.12 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 22:33:27 +01:00
Nick 9a05fdfa1f fix(convergence): decision JSON omits boundary_sha — consumers re-derive it from git
Every debug session of the release pipeline (T15 Part F forensics included)
had to re-derive the release boundary by hand. The decision JSON now
carries boundary_sha on every return path.
2026-09-11 22:33:10 +01:00
Nick 2298768353 docs: D7 resolved — release-pipeline root causes recorded (experiment B trigger)
Records the three root causes (corrupted shell fragment, unwired release
boundary, per-surface commit granularity) with live proof of the fix.
2026-09-11 22:23:52 +01:00
conductor a4afc77d53 chore(release): auto-bump version to 0.66.11 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 22:14:49 +01:00
Nick d68fd1e94d fix(release): ChangeFiles content must be base64 (ContentBase64) 2026-09-11 22:14:17 +01:00
Nick 470d82f307 fix(release): single-commit bump via ChangeFiles (blob API 404 on Gitea 1.27)
git-data blob/tree creation endpoints are not implemented on this Gitea
build (404). ChangeFiles (POST /contents with files[] = ChangeFilesOptions)
is the supported multi-file single-commit primitive. One bump = one commit.
2026-09-11 22:08:10 +01:00
Nick 7a988cbaff fix(release): refs API returns a list — take the master entry 2026-09-11 22:00:57 +01:00
Nick 12c3c488be fix(release): commit all version surfaces as ONE commit (phantom duplicate bumps root-caused)
The per-file contents PUT created one commit per version surface — five
same-version bump commits per real bump. Every 'duplicate bump cascade'
since 0.66.2 was this: 5 surface-commits per bump, misread as queue
drain/recursion. Root-caused T15 Part F via run 6791 log forensics
(5 'committed <surface>' lines, one AUTO_BUMPED). Now: Git data API
(ref → blobs → tree → commit → CAS ref update) — one bump commit.
Loop guard makes any losing race converge instead of re-bumping.
2026-09-11 21:54:15 +01:00
conductor 247c5b3936 chore(release): auto-bump version to 0.66.10 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 21:22:04 +01:00
conductor 946b8366ef chore(release): auto-bump version to 0.66.10 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 21:22:01 +01:00
conductor 5e539f7da3 chore(release): auto-bump version to 0.66.10 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 21:21:57 +01:00
conductor 83960c541d chore(release): auto-bump version to 0.66.10 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 21:21:55 +01:00
conductor 980fcd364f chore(release): auto-bump version to 0.66.10 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 21:21:53 +01:00
Nick c35018f543 fix(release): numeric semver comparison — lexicographic order skipped warranted bumps
_version_lt(): '0.66.9' < '0.66.10' is False lexicographically ('9' > '1'),
so a legitimate patch bump after v0.66.9 surfaced as
version_ahead_of_target and the release was silently skipped (run 6781,
2026-09-11). Both comparison sites (test-injection + production) now
compare numerically per segment. Regression tests added; 1284 green.
2026-09-11 21:21:23 +01:00
Nick d2600dff29 fix(ci): allow test_gitea_auth.py fake token in secrets henxel exception
The unit-test fixture constant ('unit-test-secret-token-value') trips
the hardcoded-credential detector. It is not a secret; the sanctioned
path (matching test_gitea_provision.py) is the henxel except list.
2026-09-11 21:05:55 +01:00
Nick 5b268fde8e fix(ci+release): format new auth test; repair f-string backslash in release idempotency check
- tests/test_gitea_auth.py: ruff format (CI format-gate on added files)
- auto-version.yml release step: f-string with escaped quotes is a
  SyntaxError on py<3.12 runners; compute tag name without backslash
  escapes (release job exit 22, run 6776)
2026-09-11 20:59:39 +01:00
Nick 9e0b85534a fix(release): boundary grep must match subjects only, not bodies
git --grep matches the commit BODY by default: a fix commit whose body
mentions 'chore(release)' was misdetected as the release boundary,
swallowing its own warranted bump (observed live: d01df79 boundary
detection). Anchor the pattern to the subject with --extended-regexp
and re-verify via is_release_commit(). Also drop unrecognized
--fixed-strings=False flag (fatal on this git version).
2026-09-11 20:50:46 +01:00
Nick d01df797c1 fix(release): wire release boundary into convergence (duplicate-bump root cause)
release_convergence() never assigned  from
last_release_boundary() — the production path classified ALL commits
(959+) as unreleased, so the warranted bump (minor, from an ancient
feat) never converged and every queued push wrote its own bump commit
(5x 0.66.9 observed 2026-09-11 20:21). Now the boundary is the one
source of truth: last chore(release) commit, falling back to the
latest tag. Post-boundary subjects only.

Also: local path was NOT reached when _boundary_subject was injected;
test_cli_prints_plan_json expectation updated to boundary semantics
(feat below the tag no longer warrants minor).
2026-09-11 20:34:30 +01:00
conductor 95bac995a3 chore(release): auto-bump version to 0.66.9 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 20:21:21 +01:00
conductor 1afadfddfc chore(release): auto-bump version to 0.66.9 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 20:21:06 +01:00
conductor 54ab4d9e64 chore(release): auto-bump version to 0.66.9 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 20:21:02 +01:00
conductor f4c73d5142 chore(release): auto-bump version to 0.66.9 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 20:21:00 +01:00
conductor bad13ceb57 chore(release): auto-bump version to 0.66.9 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 20:20:57 +01:00
Nick c35b604976 fix(release): repair corrupted shell in auto-version push path (D7 root cause)
c2528c1 left the tail of the replaced WARRANTED= line dangling after the
new converged-check fi (edit collision): 'fi | python3 -c ...)' — a shell
syntax error that kills the release job with exit 2 before any bump.
Release jobs have failed since c2528c1 (D7 'ROOT_CAUSE_UNKNOWN_DEFERRED'
resolved: not a Pi/environment issue). Loop-guard exit-0 paths never
reached the corrupted line, masking the defect until the first
substantive-convergence push after it.
2026-09-11 20:20:29 +01:00
Nick 4eefc15837 fix(lifecycle+auth): canonical gitea auth path, terminalization helper, fault-injection gaps (T15 Parts A-E)
- scripts/gitea_auth.py: single credential ladder (env -> 0600 token file ->
  git credential fill); 6 consumers migrated off direct os.environ reads
- rune_operator: consolidate 5 duplicated failure blocks into
  _terminalize_execution_failure; fix 3 fault-injection gaps:
  (1) dual-module PreparedTool identity split crashed approval-path saves,
  (2) fallback-model failure escaped send_task (created/pending orphan),
  (3) running-transition save uncaught (running-with-no-owner)
- envelope task.status now agrees with canonical ledger (awaiting_approval)
- orphan task_ac35f4e287c8 terminalized failed with durable evidence
- tests: gitea auth ladder (6), lifecycle fault matrix (9 boundaries +
  restart/reload + helper contract); 1280 green in ci-venv; ratchet 352/80
2026-09-11 20:15:57 +01:00
Nick 2bf43eafcd test: #314 forced association regression + Gitea auth helper + lifecycle tests
- tests/test_recall_mode_regression.py: deterministic fixture proving
  association-merged entries carry mode=association_merged (fails on old
  impl). #314 closed FIXED_VERIFIED.
- scripts/gitea_auth.py: canonical Gitea API auth helper (GITEA_TOKEN env
  or git credential fill, never remote URL parsing)
- tests/test_task_lifecycle.py: fault injection at route/model_selection/
  execute boundaries — all terminalize, no immortal RUNNING
- tests/test_route_isolation.py: task-scoped route instruction isolation
- tests/test_release_convergence.py: correct target-version algorithm

1268 tests green; ratchet 352/80; henxels clean.
2026-09-11 18:17:17 +01:00
Nick ca4f7593d7 feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
CORRECTION 1: #314 reopened — recall_with_associations mode field defect
CONFIRMED in current source and FIXED (mode: association_merged).

CORRECTION 3: Gitea credential removed from remote URL (credential helper).
TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires
Gitea web UI access). Remote URL now shows username only, no secret.

CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence
computed warranted bump from commits-since-tag without checking if master
version already satisfies the bump. FIX: already-converged guard in
version_convergence.py (master version > last tag → no commit). Repo-aware
(args.repo passed to read_version/latest_tag).

CORRECTION 6: lifecycle terminalization — execution exception now
guarantees task terminalizes as failed with evidence (no immortal RUNNING).

CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction
(NOT shared operator state). Read-only leading-verb override. Isolation
tests prove no cross-contamination between interleaved tasks.

PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure
class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics,
route-instruction learning loop (PE-1789102658-0269 SEED).

1253 tests green; ratchet 352/80 (shrink); henxels clean.
2026-09-11 15:36:08 +01:00
Nick 24537e46e7 fix(release): already-converged version guard (prevents duplicate same-version release commits)
The duplicate-release-commit defect: each queued push ran the release
workflow from its own pre-bump checkout, computed the same warranted bump,
and created another identical-version commit. The convergence script now
checks if master __version__ > last tag — if so, already converged, no
commit needed. This is the definitive fix for the 5x 0.66.8 defect.
2026-09-11 15:06:34 +01:00
Nick 52d810d358 chore: Gitea reconciliation + PL1 held-out + remote truth + metric honesty
Part A-J: 15 issues closed (HISTORICAL_EVIDENCE/SUPERSEDED/ACCEPTED_LIMITATION)
with evidence. Bug label cleaned (24→8 open). #259/#258 updated to current
architecture. Remote tracking fixed (master tracks gitea/master). PL1 metrics
corrected to NOT_YET_MEASURABLE where evidence doesn't support numbers.
D7 recorded as ROOT_CAUSE_UNKNOWN_DEFERRED. Clean recursion experiment:
version converges (0.66.8 stable) but 5 duplicate bump commits per logical
change (queue-drain artifact). PL1 held-out task 1: LOCAL_FIX route correct,
task genuinely running on deployed 3B model (slow but alive).
2026-09-11 14:05:25 +01:00
conductor 7ea4a62960 chore(release): auto-bump version to 0.66.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 13:22:08 +01:00
conductor e7e4541d47 chore(release): auto-bump version to 0.66.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 13:22:05 +01:00
conductor 92b6f47ccf chore(release): auto-bump version to 0.66.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 13:22:02 +01:00
conductor d90a79a231 chore(release): auto-bump version to 0.66.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 13:21:57 +01:00
conductor 29390534d9 chore(release): auto-bump version to 0.66.8 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 13:21:55 +01:00
Nick a8ae37df21 fix(pl1): document RUNE_EXPERIENCE_LEDGER env override on ledger default 2026-09-11 13:21:33 +01:00
conductor fa81f4bd4f chore(release): auto-bump version to 0.66.7 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 05:59:23 +01:00
conductor 81edc1356c chore(release): auto-bump version to 0.66.7 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 05:59:19 +01:00
conductor 6dcea9dbc7 chore(release): auto-bump version to 0.66.7 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 05:59:15 +01:00
conductor 885bfc181a chore(release): auto-bump version to 0.66.7 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 05:59:14 +01:00
conductor 0e1e7ce94a chore(release): auto-bump version to 0.66.7 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 05:59:12 +01:00
Nick a125c023e6 feat(PL1): first tranche — structured records, failure classification, route-instruction learning loop, metrics
PL1-01: scripts/pl1_records.py — compact structured Experience evidence from
canonical task state + route.jsonl (route resolution by task_id+timestamp,
proof level, failure class, candidate status).
PL1-02: failure classes in the PL0 vocabulary, evidence-grounded; UNKNOWN
valid; research_dispatch never failure.
PL1-03: first learning loop — recurring prose-instead-of-mutation family →
structural correction (route-driven instruction appended to the user context
block for mutation routes) + replay contract (NOT_COMPLETED gate) —
recorded as PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks).
PL1-05: metrics module (first_pass_accepted, false_completion_rate=0.0
structurally, mutation_failures, research_dispatches, candidate/evidence split).

Intent state updated: PL1 first tranche live; ADAPT-LIVE acceptance proven on
deployed runtime; remaining ADAPT obligations deferred to their epics.
2026-09-11 05:58:47 +01:00
Nick 20e68b793f docs: restore corrected intent state (174e11f) + record D7 release-job defect 2026-09-11 05:35:11 +01:00
conductor 1163a13e13 chore(release): auto-bump version to 0.66.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:57:19 +01:00
conductor a4b0bd6f55 chore(release): auto-bump version to 0.66.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:57:11 +01:00
conductor 76f215f9ab chore(release): auto-bump version to 0.66.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:57:02 +01:00
conductor dc5757ff04 chore(release): auto-bump version to 0.66.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:57:00 +01:00
conductor fc12bb5b44 chore(release): auto-bump version to 0.66.6 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:56:49 +01:00
Nick a620171524 fix(canaries): canary_p1_01/p1_06 fixtures carry mutating tool evidence (completion-truth contract) 2026-09-11 04:56:26 +01:00
conductor 4486ea4558 chore(release): auto-bump version to 0.66.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:17:10 +01:00
conductor 996d0d9cb1 chore(release): auto-bump version to 0.66.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:17:06 +01:00
conductor 4e3b4be67b chore(release): auto-bump version to 0.66.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:17:02 +01:00
conductor a872abd962 chore(release): auto-bump version to 0.66.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:17:00 +01:00
conductor be136de452 chore(release): auto-bump version to 0.66.5 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 04:16:57 +01:00
Nick afa276aaf4 fix(deploy): startup canary runs in CANDIDATE mode with --stage (gate fail-closed correctly but never tested the candidate after the two-mode refactor) 2026-09-11 04:16:31 +01:00
Nick b0dbb80778 chore(lint): re-baseline BLE001 353->352 (legitimate shrink from noqa documentation) 2026-09-11 04:01:06 +01:00
Nick 0018fe8c39 chore: deployment canary verification probe (no release warranted) 2026-09-11 03:37:53 +01:00
conductor 6f6781c8d7 chore(release): auto-bump version to 0.66.4 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 03:17:28 +01:00
conductor e89891e021 chore(release): auto-bump version to 0.66.4 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 03:17:25 +01:00
conductor 8affad8d5b chore(release): auto-bump version to 0.66.4 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 03:17:17 +01:00
conductor 1546f69046 chore(release): auto-bump version to 0.66.4 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 03:17:14 +01:00
conductor 272e6dad7c chore(release): auto-bump version to 0.66.4 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 03:17:12 +01:00
Nick c2528c1d9d feat(deploy): two-mode TUI canary (candidate scrub vs current), negative deployment regression, release-recursion invariant
Handoff 2026-09-11 Part A-C:

A. tui_startup_canary two modes: candidate (--stage, sys.path SCRUBBED of
   /mnt/workdrive/rune-code/current + imported-rune purge + contamination
   check that rune resolves from the stage) and current (consuming path).
   Scrub proven: candidate mode on the 76f71fc stage reports 76f71fc/0.66.2
   while current is bb536c9/0.66.3. FIXED a live exit-code bug: main()
   returned 1 but was called without sys.exit — a failed canary exited 0
   (the deploy gate's own returncode check in deploy_stage.py was correct,
   so promotion was never actually bypassed; the standalone script was).

B. Negative deployment canary (tests/test_deployment_canary.py): broken
   staged candidate → canary exit 1 naming the failure → current identity
   byte-identical before/after. Plus current-mode identity regression.

C. Release recursion: is_release_commit() extracted into
   version_convergence.py (ONE source of truth); workflow guard uses
   --is-release-subject CLI (no inline python in YAML); regression tests
   cover recognition, never-bump-from-release-commits (incl. the 5x loop
   case), substantive commits still bumping, and the CLI exit contract.

1241 tests green; ratchet 353/80; henxels clean.
2026-09-11 03:16:50 +01:00
conductor bb536c9f02 chore(release): auto-bump version to 0.66.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 02:40:34 +01:00
conductor 51cbe8e6d1 chore(release): auto-bump version to 0.66.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 02:40:31 +01:00
conductor 7437f06913 chore(release): auto-bump version to 0.66.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 02:40:27 +01:00
conductor d9ab8fad00 chore(release): auto-bump version to 0.66.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 02:40:23 +01:00
conductor 87519f2563 chore(release): auto-bump version to 0.66.3 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 02:40:21 +01:00
Nick 76f71fc7e6 fix(canary): tui_startup_canary self-bootstraps against the current deployment 2026-09-11 02:39:54 +01:00
Nick 174e11f23a docs: intent correction — deployment contract v1 proven, ADAPT-LIVE acceptance on deployed runtime 2026-09-11 02:30:11 +01:00
conductor ed6da55a04 chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:38:49 +01:00
conductor a5aa802277 chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:38:46 +01:00
conductor e3064f89ad chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:38:44 +01:00
conductor cd4b0f44d5 chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:38:41 +01:00
conductor 83744c9590 chore(release): auto-bump version to 0.66.2 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:38:39 +01:00
Nick 5c8b2520d2 fix(release): recursive bump-loop guard (HEAD is a release commit → no re-bump) 2026-09-11 01:38:13 +01:00
conductor cbdd6c6d97 chore(release): auto-bump version to 0.66.1 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:28:07 +01:00
conductor 6d010a2078 chore(release): auto-bump version to 0.66.1 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:28:05 +01:00
conductor f47d9f3652 chore(release): auto-bump version to 0.66.1 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:28:02 +01:00
conductor 58ebeb7c9b chore(release): auto-bump version to 0.66.1 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:27:59 +01:00
conductor 301cd12359 chore(release): auto-bump version to 0.66.1 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-11 01:27:56 +01:00
Nick a1d1b838b7 feat(ADAPT-LIVE correction): completion truth + escalation-before-terminal lifecycle + probe runtime policy
Handoff 2026-09-10 §5–§8 corrections:

1. Completion truth (§6): mutation-class tasks (build route AND
   change/defect language) terminate 'completed' only with observable
   evidence — verification method or a completed mutating tool event.
   Text-only responses on mutation tasks now fail with
   NOT_COMPLETED_NO_MUTATION_EVIDENCE (retryable). Neutral-verb tasks
   (inspect/read/check...) remain conversation-complete. The
   'I don't have the capability → completed' defect is structurally
   impossible for mutation tasks.

2. Escalation lifecycle (§7): the adaptive decision happens BEFORE the
   terminal transition. Verification failure on a local route → ONE
   probe → unresolved = task transitions research_required (nonterminal,
   research consumer continues) and returns ESCALATED_TO_RESEARCH —
   never a false terminal failure. GROUNDED probe → honest failed
   terminal (route was right, attempt slipped).

3. Action-verb governance (§10): 'inspect the failing test' is
   read-only despite containing 'failing' — leading inspection verbs
   override defect/change words. Structural evidence over lexical
   fragments.

4. p1_01/p1_03 fixtures updated to the completion-truth contract.

1233 tests green; ratchet 353/80.
2026-09-11 01:27:34 +01:00
Nick e92c993ed4 docs: intent state — ADAPT-LIVE complete, return target PL1 2026-09-11 00:25:05 +01:00
Nick 8d75efb1de feat(ADAPT-LIVE): route discriminator, DESIGN-LITE, probe-on-verification-failure, probe-grounded downgrade
ADAPT-LIVE tranche (handoff 2026-09-10):

1. LOCAL_FIX vs STANDARD_CHANGE discriminator (epic #284 doctrine):
   defect language (fix/bug/broken/regression) routes LOCAL_FIX; clear
   change language (add/implement/extend/refactor) routes STANDARD_CHANGE.
   Recorded in sufficiency provenance (task_class_language). Legacy
   compat path keeps body-scan defect detection.

2. Verifiability NONE (no test surface named) no longer blocks LOCAL for
   defect-class narrow tasks — self-evident fixes verify by inspection;
   UNKNOWN still escalates (could not determine ≠ determined absent).

3. Route-specific execution context: LOCAL_FIX = focused verification,
   no research; STANDARD_CHANGE = integration verification + DESIGN-LITE
   (bounded build-contract: objective, owning surfaces from the task
   text, verification class, invariants, escalation policy — no model
   call, no dossier).

4. Probe-on-verification-failure (ADAPT-LIVE §4/§6): a real verification
   failure on the operator spine triggers ONE bounded Evidence Probe.
   GROUNDED → downgrade-eligible evidence; STILL_UNKNOWN/CONTRADICTED →
   recorded ESCALATION to RESEARCHED_CHANGE (route change, not task
   failure). Opt-in via MIMO_PROBE_ENABLED=1 (runtime policy, now in
   the TUI launcher); tests stay deterministic.

5. Probe-grounded downgrade (§7): after ALL probes ground and local
   authority is established, STANDARD_CHANGE downgrades to LOCAL_FIX
   with recorded trigger probe_grounded_local_authority.

Canary evidence (live, deployed Rune):
- LOCAL_FIX: 'Fix the failing test in tests/test_example.py' → LOCAL_FIX
  route event, task_language=defect, no research stages
- STANDARD_CHANGE: 'Add config validation...' → STANDARD_CHANGE,
  task_language=change
- Escalation: LOCAL_FIX + verification failure + probe STILL_UNKNOWN →
  ESCALATION event (verification_failure_probe_unresolved) →
  RESEARCHED_CHANGE persisted with evidence
- Route latency: 0.2-16ms (vs fixed-depth 384s+ wasted on LOCAL tasks)

1227→1231 tests green; ratchet 353/80; henxels clean.
2026-09-11 00:24:39 +01:00
Nick 5e99228ce5 docs: record deploy pipeline in intent artifact (D2 partially closed) 2026-09-10 22:42:03 +01:00
Nick 51e14d6d95 fix(deploy): runner-workspace awareness + idempotent restage
deploy_stage.py resolves the repo from GITHUB_WORKSPACE (the runner's own
checkout) with safe.directory — the runner user cannot resolve HEAD in a
foreign working copy (dubious ownership), which failed the first real
deploy run. Re-deploying an already-staged SHA now restages cleanly.
2026-09-10 22:33:01 +01:00
Nick 97c604d0c5 fix(deploy): prefix-match required-checks context (Gitea appends ' (push)' to job contexts) 2026-09-10 22:20:39 +01:00
Nick ece67c6c73 fix(lint): narrow deploy_qualify status-probe except (ratchet back to baseline) 2026-09-10 22:01:25 +01:00
Nick 9e9ddfc10f fix(deploy): move pipeline logic into scripts (YAML block-scalar parse failure)
The embedded multi-line python in the workflow run blocks terminated the
YAML literal scalars — Gitea could not parse deploy.yml at all (dispatch
500: 'could not find expected :'), which is why no deploy runs appeared.
Logic now lives in scripts/deploy_qualify.py + deploy_stage.py; the
workflow is pure shell + script calls.
2026-09-10 21:51:34 +01:00
Nick b4ac6ad253 fix(deploy): unfiltered push trigger + ref guard (branches filter created no runs on this Gitea) 2026-09-10 21:44:57 +01:00
Nick dd9fba268e chore(deploy): trigger deploy pipeline (registration probe) 2026-09-10 21:42:52 +01:00
Nick 6418dbcfa0 docs: register deploy.yml in the pipeline registry (henxels workflows_are_governed) 2026-09-10 21:34:53 +01:00
Nick 62631f5f64 feat(deploy): deterministic CI-qualified deployment pipeline + one build identity
TUI deployment integrity (handoff 2026-09-10):

ROOT CAUSE (STALE_DEPLOYMENT): the launcher (~/.local/bin/rune) execs the
pinned checkout /mnt/workdrive/rune/src — 377 commits behind gitea/master,
version 0.54.1 vs 0.66.0. No deployment mechanism existed after CI at all.

Changes:
- build identity in core: rune.build_info() reads _build_identity.json
  (stamped by deployment) or reports 'dev' honestly; operator status
  command shows 'Build: <version> @ <sha12> (<source>)'
- .gitea/workflows/deploy.yml: on push to master, waits for the commit's
  'CI Pipeline / required-checks' to be SUCCESS (deploy only CI-qualified
  SHAs, never blind HEAD), stages src/ + pyproject to
  /mnt/workdrive/rune-code/<sha>, stamps identity, verifies (import +
  SHA assert + core-authority smoke), atomically promotes the 'current'
  symlink, records last-good (safe rollback), then RE-VERIFIES through
  the consuming path (cd current/src && import rune)
- launcher updated once: /mnt/workdrive/rune-code/current/src with
  RUNE_HOME pinned to /mnt/workdrive/rune (data home continuity) and the
  live memory store env
- research disposition audit: TUI now renders '🔬 Research required'
  distinctly instead of generic Error (operator CLI + process_experience
  already handled; bridge consumes no success field)

Ratchet 353/80. Suite green.
2026-09-10 21:33:44 +01:00
Nick 923c286a6d fix(disposition): research_required presentation + terminal-outcome semantics + canonical intent state
Pre-PL1 closure Part A (handoff 2026-09-10):

1. Presentation: CLI prints '⏳ Research required' with task/status for
   research_required dispositions — never '✗ Task failed' (the ✗ path
   remains for genuine failures). Legacy migration map accepts
   research_required statuses from pre-cutover operator.json.

2. Terminal-outcome semantics corrected: task.outcome = terminal result
   ONLY. The research_required path no longer emits a terminal outcome —
   the routing decision lives in route.jsonl (INITIAL_ROUTE, the
   authoritative routing record) referenced by route_ref; task.outcome
   fires at the genuine terminal transition when the research consumer
   closes the task. Verified: zero task.outcome events for the
   research task in the durable journal.

3. docs/RUNE_CANONICAL_INTENT_STATE.md: canonical intent artifact —
   primary intent, detour record, return conditions, deferred defects
   (D1 memory FTS ghosts, D2 hook deployment ownership, D3 ephemeral
   fire-trace, D4 recall_uplift provisional, D5 cloud telemetry theater,
   D6 dead subnet disposition), parked innovations, supersessions, the
   full ADAPT status matrix, and the next-step recommendation.
   Epic #284 carries the reconciliation comment (id 3873).

1227 tests pass; ratchet 353/80; henxels clean.
2026-09-10 20:35:43 +01:00
Nick bf5eb53d1e fix(lifecycle): research_required nonterminal disposition + recall A/B honesty + hook hardening
Pre-PL1 closure (handoff 2026-09-10):

1. research_required: new nonterminal task status (records enum, state
   machine edges created/running -> research_required -> running/
   completed/failed/cancelled/superseded). The operator's RESEARCHED_CHANGE
   disposition now transitions to research_required instead of failed —
   research routing is not failure. Live canary: status=research_required,
   outcome_ref=research_dispatch, route_ref set.

2. Experience consumer skips research-dispatch tasks — no false
   failure-learning evidence in PL0 (verified: 0 FAILURE records for
   research tasks).

3. recall_uplift A/B documented honestly in the module: latency 521-1213ms
   → 14-18ms; terms bounded (893-1665 → 16-21); recall ROW COUNTS identical
   per query but 1/8-7/7 of recalled IDs differ (expansion terms differ →
   different lexical candidates). Marked PROVISIONAL — quality not proven
   equivalent; harness original retained for rollback.

4. Hook hardening: durable fire-trace (/tmp/rune-bridge-fire.log, one line
   per actual injection with token count) + harness-internal-step guard
   (checkpoint-writer/compaction steps no longer receive capsules).

5. QX7 canary fully purged: entries, contentless-FTS ghosts, both stores;
   retrieval-level verification shows zero hits.

1227 tests pass; ratchet 353/80; henxels clean.
2026-09-10 20:08:50 +01:00
Nick 3dfb4ee9e6 test(core): recall_uplift + builder_bridge_cli coverage (henxels new-module contract)
- recall_uplift: expand contract, bounded term count (never the 1600+
  explosion), empty query, missing-store degrade
- builder_bridge_cli: JSON output smoke with the live store env

1225 tests pass; henxels clean.
2026-09-10 19:30:40 +01:00
Nick d87f8cb5f7 fix(lint): narrow route_ref/dispatch handlers, register umbrella modules
Ratchet back to baseline (353/80). 1220 tests pass.
2026-09-10 19:19:59 +01:00
Nick c76b67c5ef wip: lint fixes 2026-09-10 19:15:57 +01:00
Nick d1ef490756 feat(bridge): live MiMo builder integration + route_ref + RESEARCH_REQUIRED disposition + core recall_uplift
Closes the final live-integration gaps before PL1 (handoff 2026-09-10):

1. Builder bridge is now CONSUMED: builder-bridge.ts hook in
   ~/.mimocode/hooks/ (loaded live into the running MiMo instance,
   verified in worker log 18:02:37 'loaded file hook'). Uses
   experimental.chat.messages.transform: extracts the user message,
   runs builder_bridge_cli.py (deterministic preflight, zero model
   calls), appends a bounded <rune-preflight> capsule to the outgoing
   user message. Inject-nothing policy for trivial tasks; per-process
   + TTL dedupe for repeats. Measured overhead: ~180ms spawn+bridge
   per unique task, ~0ms repeats; capsule ~200-300 tokens.

2. Canonical route state: TaskStateRecord gains route_ref (a REFERENCE
   'route:<decided_at>' into the append-only route.jsonl — the sole
   route authority; no copied values). set_route_ref() in the repo,
   wired in send_task. Verified: task_id → route_ref → resolved
   RESEARCHED_CHANGE/STANDARD_CHANGE from route.jsonl.

3. RESEARCHED_CHANGE now causes a REAL disposition (§8): the ordinary
   local model path does NOT run for floored research tasks — the
   operator transitions the task to failed/outcome_ref=research_dispatch
   and returns RESEARCH_REQUIRED with the persisted dispatch. The route
   changes execution, not just telemetry. Live canary verified: floor
   task → RESEARCH_REQUIRED (no qwen execution, dispatch recorded).

4. recall_uplift vendored into Rune Core (reachability fix): the
   operator/bridge memory path __import__s this name; previously it
   resolved ONLY when the external harness scripts dir was on sys.path
   — otherwise recall silently lost query expansion and long natural-
   language queries abstained entirely (0 candidates instead of 16).
   Optimized: single-pass co-occurrence + bounded df checks replaces
   ~4000 per-word LIKE scans (1.2s → 18ms, identical semantics).
   Bridge steady-state: ~150ms per unique task.

5. builder_bridge: TTL cache for memory lines; empty-capsule =
   inject-nothing policy; CLI wrapper for the hook.

Live canary evidence: distinctive QX7 fact inserted into the Rune Core
store → retrieved through builder_bridge preflight → present in the
capsule the hook injects into the actual MiMo context (this session's
running instance). 1213 tests + bridge suite green.
2026-09-10 19:05:49 +01:00
Nick 86833a29a2 fix(core): explicit degrade in builder bridge (no silent-handler pattern)
The selector-failure path now assigns capsule.memory_lines = [] instead
of pass — the degrade is observable in the capsule itself, not a silent
handler. Ratchet stays at baseline; bridge canaries green.
2026-09-10 17:54:49 +01:00
Nick d9e117ebf0 test(core): builder bridge + route_risk_flags coverage (henxels new-module contract)
The moved modules (adaptive_router, evidence_probe) are covered by their
existing suites. The two genuinely new surfaces needed coverage:
- builder_bridge: route correctness on both task classes, floor
  propagation, empty-lessons-until-PL1 invariant, bounded capsule,
  selector plumbing + degrade-not-crash, markdown rendering
- route_risk_flags: vocabulary detection contract
2026-09-10 17:53:35 +01:00
Nick d59b8a1800 feat(core): Option C unified route authority + builder bridge + PL0 qualification
Handoff 2026-09-10 §§2–10. One canonical implementation, not copied logic:

1. Route authority moved into Rune Core (src/rune/):
   adaptive_router.py, route_risk_flags.py (renamed — the old name
   collided with the research-stage concept), evidence_probe.py.
   Dependency direction enforced: executor imports Core; Core never
   imports the executor. Research executor consumes via
   rune.adaptive_router / rune.evidence_probe (dual-import guards for
   standalone script execution).
2. derive_sufficiency() in Core (unified grounding semantics): the
   research path passes contract-compiler fields; the operator path
   grounds from task-text absence-of-defeaters. NONE vs UNKNOWN fixed
   (no test surface = determined NONE, not unknown).
3. Live operator consumption: send_task routes every task via the same
   authority, persists INITIAL_ROUTE to ~/.rune/routes/route.jsonl,
   records task.route, and attaches an actionable research dispatch
   (executor-consumable payload) for RESEARCHED_CHANGE. Proven live:
   local task → STANDARD_CHANGE (research chain not entered);
   floor architecture task → RESEARCHED_CHANGE + dispatch recorded.
   The dispatch's downstream consumer is still the MiMo session
   (documented §11 gap, not solved here).
4. builder_bridge.py: thin adapter over Rune Core — preflight(task)
   runs derive_sufficiency → classify_route → (optional) memory
   selection. Zero model calls, bounded capsule (12 memory / 6 lesson
   lines). lesson_lines stays empty until PL1 promotes its first
   lesson — the seam exists, the pressure does not.
5. PL0 qualification: qualify_experience() (TELEMETRY vs CANDIDATE,
   cheap rule-based, no model). Live ledger: 132 raw → 54 candidates.
   Raw outcomes preserved verbatim; SEED never auto-VALIDATED.
6. Umbrella manifest updated (4 new modules → capability umbrella).

Verification: 1213 tests pass; ruff clean; silent-handler ratchet at
baseline (353/80). The audit's §1 qualification is honored: the
earlier spine canary proved model routing, not task-class routing —
task-class routing on the live spine is proven HERE.
2026-09-10 17:47:28 +01:00
Nick ec2cba62ee feat(reachability): Live Rune Slice P0 — outcome producer edge + spine defect fixes
Runtime Reachability audit (reports/RUNTIME_REACHABILITY_AUDIT_2026-09-10.md,
4 parallel tracers + live canaries) established: ONE real end-to-end path
exists (send_task → ContextBroker → Ollama tool loop → durable events),
proven live by canary during the audit. The spine exists; the missing
pieces were edges, not architecture.

P0 implemented (choice-free items):

1. task.outcome producer edge: send_task emits a typed journal event on
   EVERY terminal transition (completed AND failed) with task/model/
   verification/route fields — the producer PL0/PL1 needs (outcome_ledger
   was de-wired; experience ledger had zero producers).
2. Experience consumer: scripts/process_experience.py
   record_operator_outcomes() reads the authoritative task-state
   checkpoint (the journal is a one-way integrity journal — payload_hash
   only) and appends idempotent SEED experience records. 132 live records
   created; rerun = 0 (idempotent).
3. Spine defect: _routing_profiles_path and the profile-save site had bare
   relative imports that fail in standalone CLI mode — trace recording
   degraded to tier-2 on every CLI run (mislabeled 'inference_trace').
   Fixed with the established dual-import pattern; verified clean run.
4. Standalone bootstrap: package parent now on sys.path for script
   execution.
5. hook_engine.fire dead call removed (rune_tui called a nonexistent
   function; AttributeError silently swallowed — hooks never ran). Left
   as an honest note, not a false-liveness surface.

Verification: Phase 8 canary through the real spine — all 9 rungs
observable from persisted state (trace v5 fields incl. memory
abstention + broker budget, routing reason, checkpoint outcome, journal
event, idempotent experience record). 1213 tests pass; ruff clean;
ratchet at baseline.

The route-authority placement choice (A/B/C) is presented in the audit
report §G and awaits Nick's decision before wiring adaptive routing into
the operator path.
2026-09-10 17:04:52 +01:00
244 changed files with 13604 additions and 5444 deletions

No files matched your search

+59 -17
View File
@@ -4,6 +4,8 @@ on:
pull_request:
types: [closed]
branches: [master]
push:
branches: [master]
permissions: write-all
@@ -15,13 +17,15 @@ jobs:
with:
fetch-depth: 0
- name: Skip if PR is not merged
- name: Skip if PR is not merged (direct pushes continue to convergence)
run: |
MERGED="${{ gitea.event.pull_request.merged }}"
if [ "$MERGED" != "true" ]; then
EVENT="${{ gitea.event_name }}"
if [ "$EVENT" = "pull_request" ] && [ "$MERGED" != "true" ]; then
echo "PR not merged — skipping release"
exit 0
fi
echo "event=$EVENT — continuing"
- name: Read reviewed semantic version
run: |
@@ -51,8 +55,36 @@ jobs:
exit 0
fi
fi
# semver from PR title — logic lives in scripts/semver_bump.py
# (#190 lesson: inline python in YAML scalars rots unlinted)
# semver level: PR title on pull_request events; on push events
# (direct master pushes bypass PR merges — observed 2026-09-10,
# version stuck at 0.66.0 through ~15 substantive commits), the
# warranted bump comes from scripts/version_convergence.py over
# commits since the last tag. chore(release) commits never
# warrant a bump → the bump commit cannot recurse.
if [ -z "$PR_TITLE" ] || [ "$PR_TITLE" = "null" ]; then
# Recursive-loop guard: HEAD being a bump commit means THIS
# push already applied the bump — do not re-bump. The decision
# lives in version_convergence (--is-release-subject), one
# source of truth shared with the regression test.
HEAD_SUBJECT=$(git log -1 --format=%s)
if python3 scripts/version_convergence.py --is-release-subject "$HEAD_SUBJECT"; then
echo "HEAD is a release bump — loop guard, no re-bump"
exit 0
fi
CONVERGENCE=$(python3 scripts/version_convergence.py)
WARRANTED=$(echo "$CONVERGENCE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('warranted_bump') or '')")
CONVERGED=$(echo "$CONVERGENCE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('converged', 'true'))")
if [ "$CONVERGED" = "True" ]; then
echo "Already converged — no bump needed"
exit 0
fi
echo "push convergence: warranted_bump=$WARRANTED"
if [ -z "$WARRANTED" ]; then
echo "No substantive commits since last tag — no bump needed"
exit 0
fi
PR_TITLE="$WARRANTED: converged release (push-to-master bump)"
fi
NEW=$(python3 scripts/semver_bump.py "$RELEASE_VERSION" "$PR_TITLE")
export NEW_VERSION="$NEW"
echo "Auto-bump: $RELEASE_VERSION -> $NEW (direct commit to master — bump PRs retired, Nick policy 2026-09-08)"
@@ -153,18 +185,28 @@ jobs:
print(f" - {d}")
sys.exit(1)
# Commit each surface DIRECTLY to master (bookkeeping, not reviewable code)
msg_prefix = f"chore(release): auto-bump version to {NEW} — earned by #{PR_NUMBER}"
for path, (content, sha) in edited.items():
status, r = api(f"/contents/{path}", "PUT", {
"message": f"{msg_prefix} ({PR_TITLE[:80]})\n\nPromotion record: {promoting_url}",
"content": base64.b64encode(content.encode()).decode(),
"sha": sha,
"branch": "master",
})
if status not in (200, 201):
print(f" commit {path} failed ({status}): {str(r)[:200]}"); sys.exit(1)
print(f" committed {path} to master")
# Commit ALL surfaces as ONE commit via ChangeFiles
# (POST /contents with a files[] array). The per-file contents PUT
# created one commit per version surface — five same-version "bump
# commits" per release (the phantom duplicate-bump cascade,
# misdiagnosed as queue drain since 0.66.2; root-caused T15 Part F
# 2026-09-11). Git-data blob/tree creation is 404 on this Gitea
# build; ChangeFiles is the supported single-commit primitive.
msg = f"chore(release): auto-bump version to {NEW} — earned by #{PR_NUMBER} ({PR_TITLE[:80]})\n\nPromotion record: {promoting_url}"
ops = [{
"operation": "update",
"path": path,
"content": base64.b64encode(content.encode()).decode(), # ChangeFileOperation.content is ContentBase64
} for path, (content, _old_sha) in edited.items()]
status, r = api("/contents", "POST", {
"branch": "master",
"message": msg,
"files": ops,
})
if status not in (200, 201):
print(f"change-files failed ({status}): {str(r)[:300]}"); sys.exit(1)
commit_sha = (r.get("commit") or {}).get("sha", "?") if isinstance(r, dict) else "?"
print(f" committed {len(ops)} surfaces as ONE commit {str(commit_sha)[:7]}")
print(f"AUTO_BUMPED=true — version {NEW} live on master; TUI footer will render v{NEW}")
with open(os.environ["GITHUB_ENV"], "a") as env:
@@ -182,7 +224,7 @@ jobs:
# Skip idempotently if a release for this tag already exists (retry/replay safety).
if curl --fail --silent "$LIST_API_URL" \
-H "Authorization: token $GITEA_TOKEN" \
| python3 -c 'import json,sys,os; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")==f"v{os.environ[\"RELEASE_VERSION\"]}"]; sys.exit(0 if exists else 1)'; then
| python3 -c 'import json,sys,os; v=os.environ["RELEASE_VERSION"]; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")=="v"+v]; sys.exit(0 if exists else 1)'; then
echo "Release v$RELEASE_VERSION already exists — skipping creation"
exit 0
fi
+64
View File
@@ -0,0 +1,64 @@
name: deploy
on:
push:
workflow_dispatch:
jobs:
deploy:
# Deploys the EXACT pushed revision to the local Rune code deployment
# (handoff 2026-09-10 §3/§6) — never "whatever HEAD is at deploy time".
# Gate: the commit's required CI checks must be green before promotion.
# Branch guard at job level: this Gitea instance did not create runs for
# `on.push.branches`-filtered workflows (observed 2026-09-10) — the
# unfiltered push trigger with an explicit ref guard is the proven pattern.
runs-on: rune-ci-desktop
if: ${{ gitea.ref == 'refs/heads/master' && !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: Wait for CI qualification on this exact SHA
env:
GITEA_SERVER: ${{ gitea.server_url }}
GITEA_REPO: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
run: |
DEPLOY_SHA=$(git rev-parse HEAD)
export DEPLOY_SHA
echo "Qualifying $DEPLOY_SHA"
DEADLINE=$((SECONDS + 900))
while [ $SECONDS -lt $DEADLINE ]; do
STATUS=$(python3 scripts/deploy_qualify.py)
echo "required-checks: $STATUS"
if [ "$STATUS" = "success" ]; then exit 0; fi
if [ "$STATUS" = "failure" ]; then
echo "::error::CI failed on $DEPLOY_SHA — deployment refused"
exit 1
fi
sleep 30
done
echo "::error::CI qualification timeout on $DEPLOY_SHA — deployment refused"
exit 1
- name: Stage, verify, promote
env:
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
run: |
python3 scripts/deploy_stage.py
- name: Post-deploy runtime verification (the consuming path)
run: |
SHA=$(git rev-parse HEAD)
# Exactly what the launcher execs: cd <current>/src, import rune.
RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory \
/home/nick/.venvs/memory/bin/python3 -c "
import sys
sys.path.insert(0, '/mnt/workdrive/rune-code/current/src')
import rune
info = rune.build_info()
assert info['commit'] == '$SHA', f'runtime SHA mismatch: {info}'
assert rune.__version__ == info['version']
print('RUNTIME VERIFIED:', info)
"
+3
View File
@@ -11,3 +11,6 @@ build/
.env
.venv/
state/
# Runtime artifacts (cwd-local event journal + capability index)
index/
journal/
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.0**
**Current release: v0.66.14**
**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.**
+8 -2
View File
@@ -120,7 +120,13 @@
"local_tool_runtime",
"forge_client",
"message_queue",
"connection_pool"
"connection_pool",
"adaptive_router",
"route_risk_flags",
"evidence_probe",
"builder_bridge",
"builder_bridge_cli",
"recall_uplift"
],
"canaries": [
"canary_p1_01",
@@ -214,4 +220,4 @@
]
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
# Gitea Issue Reconciliation — canonical record
_Generated T15 Part J/K/L (2026-09-11). Machine-readable companion:
`docs/gitea_issue_reconciliation.json` (single authority; this doc is the
human-readable projection). Consistency checker: `scripts/gitea_reconciliation_check.py`
compares live Gitea state against the JSON and fails on drift._
Reconciliation states (closed vocabulary):
`VALID_OPEN | FIXED_VERIFIED | SUPERSEDED | DUPLICATE | HISTORICAL_EVIDENCE |
ACCEPTED_LIMITATION | NEEDS_REVALIDATION | DEFERRED_FUTURE`
## Bug-label audit (Part K)
`bug` = demonstrated currently incorrect behavior. Nothing else.
### Issues labelled `bug` at audit time
| # | live defect | evidence | final label | reconciliation state |
|---|---|---|---|---|
| 324 | **YES** | MessageAbortedError reproduces every aborted worker session (15+ checkpoints, including this session's predecessor) | bug (kept) | VALID_OPEN |
| 204 | NO | Forge-orchestrated pipeline = feature request (P1 feature); no demonstrated incorrect behavior | bug removed | VALID_OPEN (feature) |
| 157 | NO | capture_failure.py + tests/fixtures/production/ + manifest.json shipped and exercised (plan 2026-09-07-157) | bug removed | FIXED_VERIFIED (closed) |
| 145 | NO | P0-07 follow-up work item (tracing coverage), not a defect report | bug removed | VALID_OPEN (work item) |
| 139 | NO | Hand-maintained allowlist replaced by declarative classification (tui_validator.py cites #139); `tui_validator.py check` → passed, exit 0 | bug removed | FIXED_VERIFIED (closed) |
| 126 | NO | Silent-handler sweep = refactor program (S110/BLE001 ratchet governs), not a defect | bug removed | VALID_OPEN (work item) |
| 37 | NO | Personality corpus compilation = P2 feature | bug removed | VALID_OPEN (feature) |
| 16 | NO | v1 course-correction EPIC | bug removed | VALID_OPEN (epic) |
### Issues whose `bug` label was removed in the prior cleanup (Part D, earlier session)
16 targeted: #301 #300 #292 #284 #314 #181 #166 #165 #163 #159 #154 #141 #40 #32 #31 #27.
Verification (live API, this session): all confirmed bug-free EXCEPT #314 — its
removal had failed (the API-False defect) and the label persisted on a closed
issue. Removed this session by label-ID DELETE (204). #314 is closed
FIXED_VERIFIED (mode:association_merged regression shipped in 2bf43ea).
### Totals
- bug-labelled before audit: 8 open (+ #314 closed-but-labelled)
- bug-labelled after audit: **1** (#324, live defect, evidence above)
- issues closed by this audit: #139, #157 (both FIXED_VERIFIED with evidence comments)
- NEEDS_REVALIDATION count: 0 among bug-labelled issues (each resolved on evidence)
## Open-issue reconciliation (Part J)
Subsystems derived from labels. States per closed vocabulary. `next action` is
the single next concrete step; `_` = none until its tranche opens.
| # | subsystem | state | current evidence | next action |
|---|---|---|---|---|
| 324 | engine/checkpoint | VALID_OPEN | live repro (15+ sessions) | fix writer abort handling |
| 316 | ADAPT-1/research | VALID_OPEN | B3-v2 research task | await ADAPT tranche |
| 315 | ADAPT-1/wiring | VALID_OPEN | B2-v2 standard task | await ADAPT tranche |
| 311 | ADAPT-1/wiring | VALID_OPEN | superseded-by #315 (kept open as spec anchor until 315 delivers) | close when #315 closes |
| 301 | FINAL-12/benchmarks | NEEDS_REVALIDATION | dependency list silently grew (operator-path telemetry, bridge overhead) without issue update | re-scope before PL1 metrics work |
| 300 | ADAPT-11 | VALID_OPEN | acceptance = real escalation demo; blocked on ADAPT-LIVE telemetry | demo after PL1 held-out |
| 299 | ADAPT-10 | VALID_OPEN | harness legibility audit | _ |
| 297 | ADAPT-8 | VALID_OPEN | deterministic-first QUALIFY | _ |
| 296 | ADAPT-7 | VALID_OPEN | DESIGN consolidation experiment | _ |
| 295 | ADAPT-6 | VALID_OPEN | evidence capsule / context minimization | _ |
| 294 | ADAPT-5 | VALID_OPEN | budgets + stop conditions | _ |
| 293 | ADAPT-4 | VALID_OPEN | DESIGN-LITE shipped (STANDARD_CHANGE lane live) | acceptance demo, then close |
| 292 | ADAPT-3 | VALID_OPEN | LOCAL_FIX lane shipped | acceptance demo, then close |
| 284 | ADAPT epic | VALID_OPEN | epic tracks ADAPT-LIVE + PL1; body updated with detour record | update after PL1 |
| 259 | Scaffolding | VALID_OPEN | redefined: safe repeatable construction protocol (T15 §C) | Scaffolding v1 after PL1 |
| 258 | State-aware processes | VALID_OPEN | redefined: low-compute architectural self-awareness; SA1-8 superseded by distributed concerns | deferred research |
| 204 | forge/pipeline | VALID_OPEN | P1 feature: forge-orchestrated coordination | _ |
| 183–176 | memory-foundation futures | DEFERRED_FUTURE | P3 research futures (typed temporal semantics, prior-art audit, curiosity, lineage) | memory-foundation tranche |
| 175/181 | memory epics | DEFERRED_FUTURE | evidence-grounded continuity epics | same |
| 166 | research watchlist | DEFERRED_FUTURE | external research tracking | periodic review |
| 165/164 | research spikes | DEFERRED_FUTURE | SimpleMem/A-MEM, HippoRAG 2 | memory-foundation tranche |
| 163 | graph backend spike | NEEDS_REVALIDATION | P1 spike; LadybugDB evaluation partially done in prior research artifacts | revalidate against current graph.db-missing state |
| 162 | Qdrant spike | NEEDS_REVALIDATION | P1 spike; prior research artifacts exist (issue-162/) | revalidate after #316 outcome |
| 159 | conceptual topology | DEFERRED_FUTURE | P3 future | _ |
| 158 | wiring-status report | VALID_OPEN | P2: generated architecture-truth report | _ |
| 156 | mutation guards | VALID_OPEN | P1: cognitive-state mutation guards | _ |
| 155 | e2e activation trace | VALID_OPEN | P1: credit-assignment trace | overlaps PL1 outcome work — potential duplicate of #300's telemetry; review at PL1 close |
| 154/153 | learned-state / experience encoding | VALID_OPEN | P2 | PL1-adjacent |
| 152/151 | break-it / adversarial | VALID_OPEN | P0: adversarial procedures after nominal green | post-PL1 tranche |
| 145 | P0-07 follow-up | VALID_OPEN | tracing coverage gaps | _ |
| 141 | ContextBroker compaction | VALID_OPEN | P1: rolling-compaction persistence | _ |
| 139 | tui_validator drift | FIXED_VERIFIED | declarative classification shipped; check passes | closed |
| 131/130/129 | reviewer program | VALID_OPEN | P0: local reviewer + calibration + rubric | post-PL1 tranche |
| 126 | silent-handler sweep | VALID_OPEN | P0 refactor program; ratchet enforces ceiling | ongoing |
| 125 | decompose monoliths | VALID_OPEN | P2 | _ |
| 49–35 | v1-course-correction P2/P3 | DEFERRED_FUTURE | epic #16 manifest items (P2-01..09, P3-01..06) | per epic #16 sequencing |
| 33–27 | v1-course-correction P1 | VALID_OPEN | epic #16 manifest P1 items | per epic #16 sequencing |
| 16 | v1 epic | VALID_OPEN | consolidation epic; 33 manifest issues | per epic sequencing |
| 13 | TUI message graph | VALID_OPEN | P1 | _ |
| 3 | financial tracking | NEEDS_REVALIDATION | non-Rune issue (different project era) | transfer to correct repo or close |
| 2 | cognitive loop wiring | NEEDS_REVALIDATION | predates current TUI architecture | revalidate against current TUI |
| 1 | personality injection | NEEDS_REVALIDATION | overlaps #37's newer scope | merge into #37 or close |
## Reconciliation decisions this session (Part K deltas)
- CLOSED: #139, #157 (FIXED_VERIFIED, evidence comments posted)
- BUG LABEL REMOVED: #204, #145, #126, #37, #16 (audit: not defects)
- BUG LABEL REMOVED (stuck from prior cleanup): #314 (closed issue)
- OPEN BUGS: 8 → 1 (#324 only — genuine live defect)
## Part L — consistency check
`scripts/gitea_reconciliation_check.py` (deterministic, no governance
subsystem): fetches live issue state, compares to
`docs/gitea_issue_reconciliation.json`, flags:
- closed issue recorded VALID_OPEN / DEFERRED_FUTURE / NEEDS_REVALIDATION (open states)
- open issue recorded FIXED_VERIFIED / SUPERSEDED / DUPLICATE / HISTORICAL_EVIDENCE without being closed
- `bug` label present on any issue not recorded as a live defect (or missing on #324)
- issue missing from the artifact
- record lacking a required field
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
+179
View File
@@ -0,0 +1,179 @@
# RUNE CANONICAL INTENT STATE
**v1.0 · 2026-09-10 · Owner: Rune · Review: Nick**
*This artifact is the project's authoritative answer to: "What were we trying to achieve, what interrupted it, what remains, and where do we return?" It is state, not a planning framework. Update it when intent changes; do not extend it into a second roadmap system.*
---
## PRIMARY INTENT
Build Rune: a stateful, quality-first coding assistant whose cognitive systems are genuinely reachable from live execution — measured by the Runtime Reachability doctrine (`IMPLEMENTED → CALLED BY PRODUCTION → CHANGES EXECUTION → PERSISTED → OBSERVABLE → END-TO-END PROVEN`), not by module existence.
The parent work at the moment of interruption: **Epic #284 — the adaptive development pipeline** (successor to the ratified Research-to-Build process #257/RP0–RP14). Doctrine: *research buys down uncertainty; it is not a tax on every code change.* Three process depths (LOCAL_FIX / STANDARD_CHANGE / RESEARCHED_CHANGE), reversible routes, one canonical route authority, the ratified pipeline as the high-assurance fallback.
## CURRENT ACTIVE WORK
**Deployment Contract v1 + ADAPT-LIVE acceptance closure** (master 5c8b252+, 2026-09-11).
CORRECTION RECORD (intent-continuity evidence, 2026-09-11): the previous tranche declared "ADAPT-LIVE complete" while an unresolved user-established blocker existed (deployed TUI launch broken; version bumping stalled). Honest status:
- **Deployment Contract v1: PROVEN** — deterministic CI-qualified deploy pipeline (stage → verify → REAL TUI startup canary via Textual run_test → atomic promote, last-good rollback); loop-guarded auto-version convergence (0.66.0 → 0.66.2 on push events; chore(release) never re-bumps); launcher deployable; runtime reports deployed SHA/version.
- **ADAPT-LIVE: IMPLEMENTATION SUBSTANTIALLY PRESENT, ACCEPTANCE PROVEN ON DEPLOYED RUNTIME** — route discriminator (defect/change language + leading-verb governance), DESIGN-LITE, completion truth (mutation tasks without mutating tool evidence cannot complete), verification→probe→escalate BEFORE terminal, deployed-runtime canaries: LOCAL_FIX/STANDARD_CHANGE honestly fail NOT_COMPLETED with text-only responses (false success structurally impossible), conversation tasks complete normally, TWO REAL Evidence Probes through the deployed runtime with genuine cited evidence (pytest exit-code semantics + empirical check; deployed-tree source inspection). Escalation-with-real-worker partially proven (the competent worker correctly GROUNDS rather than fabricates escalation; unresolved-case escalation proven at module level).
- Earlier "ADAPT-LIVE complete" declaration (e92c993) was PREMATURE — recorded as evidence for the future intent-continuity system: a child task must not replace unresolved parent acceptance obligations merely because its implementation completed.
Delivered across the tranche:
- Route discriminator (defect vs change language, provenance-tracked) separates LOCAL_FIX from STANDARD_CHANGE
- LOCAL_FIX: focused verification policy, no research; verifiability NONE no longer blocks defect-class narrow tasks
- STANDARD_CHANGE: DESIGN-LITE record (bounded build-contract, no model call) + integration verification
- RESEARCHED_CHANGE: unchanged high-assurance fallback (`research_required` nonterminal disposition)
- Evidence Probe LIVE via verification-driven trigger: real verification failure on the operator spine → one bounded probe → GROUNDED resumes / unresolved ESCALATES to RESEARCHED_CHANGE (route change, not task failure; opt-in via `MIMO_PROBE_ENABLED=1` in the TUI launcher)
- Probe-grounded downgrade (§7): fully-grounded probes downgrade STANDARD_CHANGE → LOCAL_FIX with recorded trigger
- Canary evidence: route latency 0.2–16ms vs fixed-depth 384s+ wasted on LOCAL tasks; escalation proven LOCAL_FIX → probe → RESEARCHED_CHANGE with persisted evidence
Reachability detour (CLOSED):
- Runtime Reachability audit (one live spine identified; dead cognitive subnet mapped)
- Unified Core route authority (`src/rune/adaptive_router.py` + `evidence_probe.py` + `route_risk_flags.py`) consumed by both the research executor and `RuneOperator.send_task`
- Builder bridge live-consumed: `~/.mimocode/hooks/builder-bridge.ts` → `builder_bridge_cli.py` → Rune Core preflight in actual MiMo context (fire-trace: `/tmp/rune-bridge-fire.log`)
- `research_required` nonterminal disposition; `route_ref` reference model; PL0 experience ledger with live producer and TELEMETRY/CANDIDATE qualification
- `recall_uplift` vendored into core (PROVISIONAL — A/B documented, not proven equivalent)
## WHY CURRENT WORK INTERRUPTED THE PARENT
The ADAPT-2 audit found `classify_route()` had **zero production callers** — the route authority was architecturally implemented but cognitively dead. Wiring it honestly forced the discovery that Rune Core itself was not reachable from any live path the user experiences (the TUI path was live but disconnected from adaptive cognition; the MiMo builder session consumed none of it). Fixing that root defect was a precondition for every ADAPT capability being anything more than dead machinery: **adaptive routes are worthless if nothing live consumes them.**
## RETURN CONDITION
This detour is complete when:
- [x] One canonical route authority consumed by production (operator + research executor)
- [x] Research dispatch causes a real disposition (`research_required`, not failure)
- [x] Terminal-outcome semantics corrected (no false `task.outcome` on nonterminal routing)
- [x] Builder bridge consumed by the actual MiMo harness with measured overhead
- [x] Deferred defects recorded (below), not chased
- [x] ADAPT intent recovered and reconciled (this artifact, Part B)
- [ ] **THIS TRANCHE**: the ADAPT status matrix disposition is agreed and the next tranche selected (Part D)
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
## OPEN OBLIGATIONS (deferred defects — recorded, NOT chased)
| ID | Obligation | Evidence | Destination |
|---|---|---|---|
| D1 | `memory_store.delete_entry()` leaves contentless-FTS ghosts (requires special FTS5 `INSERT ... VALUES('delete')` cleanup; retrieval can ghost-hit deleted entries) | 2026-09-10 QX7 purge required manual FTS commands in both stores | Memory-foundation work (deferred by design — handoff §17) |
| D2 | Builder-bridge hook lives at `~/.mimocode/hooks/builder-bridge.ts` with a hardcoded repo path — no canonical ownership, no repeatable install/sync; a harness reinstall silently loses Rune's builder integration. (PARTIALLY CLOSED 2026-09-10: the Rune CODE side now has a deterministic deploy pipeline — `.gitea/workflows/deploy.yml`, stage→verify→promote with `last-good` rollback; the HOOK side still hand-installed) | Hook hand-installed 2026-09-10; deploy pipeline live (run 14243 success, e2e verified 51e14d6) | Scaffolding (SC1/SC2 territory) for hook deployment when the bridge becomes user-facing |
| D3 | `/tmp/rune-bridge-fire.log` fire-trace is ephemeral (lost on reboot) — standing observability, not durable telemetry; accurate description: per-boot injection evidence only | Design note in hook source | Fold into OBS-1-style durable telemetry when next touched; no project now |
| D4 | `recall_uplift` retrieval-quality effect unproven (PROVISIONAL) — recall row counts match but surfaced IDs differ; harness original retained at `~/.config/opencode/scripts/` for rollback | A/B table in module docstring | Compare in FINAL-12 or first memory-foundation pass |
| D5 | `CloudStubAdapter` declared-not-live: router can elect cloud models that execution cannot run (`routing_mode=cloud` is telemetry theater) | Audit §A fragmentation 1 | When cloud execution is actually wanted; otherwise retire the election path |
| D6 | Dead cognitive subnet (salience_gate, cognitive_budgeter, tiered_executor, progressive_escalation, verification_layer, outcome_ledger + companions): implemented, unconsumed, partially duplicated by ADAPT-2 | Reachability audit §C | Disposition in Scaffolding/PL1 review: RETIRE more likely than RESURRECT — superseded or absorbed |
## PARKED INNOVATIONS
- **Intent-continuity spine**: this artifact is the manual v1. The automated version (intent-state tracking that survives detours without a human handoff) belongs after PL1 — PL1's experience machinery is its natural producer. Do not build early.
- **Substrate**: deferred behind a domain-neutral education phase (handoff §16). Unchanged.
- **Scaffolding** SC1–SC8: deferred until adaptive/live/learning foundations are credible (handoff §15). Unchanged.
- **Control benchmarking** (born-isolated disposal control vs full Rune): deferred to system maturity (handoff §18). Preserve passive instrumentation meanwhile — task.outcome + route.jsonl + StageAttempt already cover the required fields.
## SUPERSEDED APPROACHES
- **Option A/B route placement** (route-in-operator / two-worlds): superseded by Option C unified core authority (Nick decision, 2026-09-10).
- **`progressive_escalation.py` ladder**: superseded by `adaptive_router.py` (duplicated concept; the original was never wired).
- **`outcome_ledger.py` as separate authority**: superseded by the event-driven `task.outcome` + `process_experience.py` flow.
- **Fixed-depth research pipeline as universal path**: superseded by adaptive routing (the existence proof: 2 of 3 task classes cannot pass its OUTWARD gate).
- **per-message cognitive pipeline (P0-04)**: deliberately removed; the ContextBroker owns model-facing context. Do not resurrect.
- **Manual handoff files as the only intent carrier**: superseded by this artifact (handoffs remain valuable input, not the system of record).
---
## ADAPT RECONCILIATION (Epic #284, recovered 2026-09-10)
### What ADAPT was supposed to become
From epic #284 verbatim: *"convert the proven high-assurance research process into an adaptive universal engineering framework that applies only as much process as the uncertainty deserves."* One framework, three depths, reversible routes, escalation/downgrade as first-class recorded transitions, the ratified pipeline as fallback, FINAL-12 cross-track benchmark as the ratification gate. Success criteria: 5/5 limitations dispositioned, all three routes through one canonical authority, FINAL-12 beats fixed-depth without quality regression.
### Status matrix
| Capability | Status | Current owner | Live consumer | Future destination | Reason |
|---|---|---|---|---|---|
| Route authority (classify/escalate/downgrade/floor) | **BUILT_AND_LIVE** | `src/rune/adaptive_router.py` | Operator `send_task` + research executor ISSUE dispatch | PL1 consumes its decisions | Wired, persisted, canaried |
| Route decisions on the operator spine | **BUILT_AND_LIVE** | `rune_operator._route_task_class` | Every `send_task` | — | `route.jsonl` + `route_ref` verified |
| RESEARCHED_CHANGE disposition | **BUILT_AND_LIVE** | `rune_operator.send_task` | Floor tasks halt local path, `research_required` + dispatch | Research consumer (below) | Live canary bf5eb53 + this tranche |
| Targeted Evidence Probe | **BUILT_NOT_LIVE** | `src/rune/evidence_probe.py` | Wired in executor dispatch; gated behind `MIMO_PROBE_ENABLED` (nobody sets it) | ADAPT-LIVE tranche: decide the runtime policy and prove one real probe | The §6 qualification stands |
| Verification-driven escalation | **BUILT_NOT_LIVE** | `adaptive_router.escalate_on_verification_failure` | Contract message names the hook; no automatic caller | ADAPT-LIVE tranche: wire into the local lane's verify step | Handoff §6 of bridge tranche; still true |
| LOCAL_FIX execution path | **PARTIAL** | Route exists; execution = ordinary model path | Operator completes LOCAL tasks normally | ADAPT-3/#292: regression-gated fast path per issue spec | Route changes nothing for LOCAL today beyond disposition recording |
| STANDARD_CHANGE path (DESIGN-LITE) | **PARTIAL** | Route exists | Same ordinary path | ADAPT-4/#293 DESIGN-LITE step | Same reason |
| RESEARCHED_CHANGE bounded execution | **PARTIAL** | Dispatch persisted; orchestrator = MiMo session (documented §11 gap) | Session drives executor CLI manually | ADAPT-5/#294 budgets + the §11 orchestrator decision | Recorded honestly in dispatch payload |
| Evidence capsule (#295) | **PARTIAL** (proto: builder capsule) | `builder_bridge` | MiMo hook | ADAPT-6 after paths exist | The bridge capsule is task-preflight, not OUTWARD evidence compression |
| DESIGN consolidation (#296) | **NOT STARTED** | — | — | ADAPT-7, after ADAPT-5 | A/B experiment design exists in issue |
| Deterministic-first QUALIFY (#297) | **BUILT_AND_LIVE (pipeline) / NOT_LIVE (operator)** | `_execute_qualify` in executor | Research pipeline only | ADAPT-8: reuse for STANDARD_CHANGE DESIGN-LITE gate | Already deterministic-first per RP8 |
| Legibility audit (#299) | **NOT STARTED** | — | — | ADAPT-10, after routes live | Depends on 3/4/5/9 |
| Escalation/downgrade real acceptance (#300) | **BUILT_NOT_LIVE** | Module-level canaries pass | No real-task demonstration | ADAPT-11: the acceptance IS real tasks escalating | Needs paths first |
| FINAL-12 cross-track benchmark (#301) | **NOT STARTED** | — | — | Final epic gate | Explicitly last |
| OBS-1 telemetry (#287) | **BUILT_AND_LIVE (pipeline)** | `stage_telemetry.py` | Research executor | Extend to operator path in ADAPT-LIVE | StageAttempt records verified in B3-v3 |
| Artifact authority / retry (#298) | **BUILT_AND_LIVE** | `_resolve_worker_artifact` | Research executor | — | ADAPT-9 closed |
| Task routing vs model routing separation | **BUILT_AND_LIVE** | Two authorities, distinct | Both live | Never collapse | Handoff §5 doctrine, honored in code |
| Dead subnet (salience→budget→tier→escalate→verify→outcome) | **ABSORBED_BY_NEW_ARCHITECTURE / RETIRE candidates** | `src/rune/*.py` | None | Disposition review during PL1/Scaffolding | progressive_escalation duplicated by adaptive_router; outcome_ledger by task.outcome flow; verification_layer concepts live in `_verification_and_outcome` |
| Builder bridge | **BUILT_AND_LIVE** | hook + CLI + `builder_bridge.py` | MiMo harness (this session) | Scaffolding owns deployment (D2) | Fire-trace proven |
### What fell off the roadmap during the detour (Part D input)
1. ADAPT-3/4/5 route execution paths (the epic's core; everything else waits on them).
2. The §11 research-orchestrator boundary decision (MiMo session is the honest current answer; it predates the detour but was never dispositioned).
3. Probe runtime policy (`MIMO_PROBE_ENABLED` ownership) — recorded, never decided.
4. FINAL-12's dependency list silently grew (operator-path telemetry, builder-bridge overhead numbers) without issue updates.
5. Epic #284 body still says "ADAPT-2 → {ADAPT-3,4,5}" with no record of the detour — the issue tracker and reality diverged until this artifact.
---
## PART D — NEXT RECOMMENDATION
**COMPLETE REMAINING ADAPT TRANCHE (one bounded tranche: "ADAPT-LIVE"), then PL1.**
Reasoning from recovered obligations, not momentum:
- PL1's input is route-accuracy and escalation-outcome evidence. Today routes change execution only at the RESEARCHED_CHANGE boundary. LOCAL_FIX and STANDARD_CHANGE — the two depths the measured baseline proved the fixed-depth pipeline *fails* — do not have distinct execution paths. Without them, PL1 would learn from a system that exercises one route out of three.
- The escalation chain (local solve → verification failure → probe → research) is the epic's spine and is entirely canary-level today. ADAPT-11's acceptance ("real tasks demonstrate escalation") cannot be satisfied by more module tests.
- The Evidence Probe needs one decision (who enables it at runtime) and one real demonstration — small, but it cannot be skipped because §3's progressive behavior (local → probe → research) is otherwise fictional at the probe step.
- Nothing here justifies the intent-continuity spine yet: this artifact covers the need manually, and PL1 will automate it with better machinery.
**ADAPT-LIVE contents (smallest coherent adaptive loop):**
1. LOCAL_FIX lane: routed task → implementation → real verification (`_verification_and_outcome`) → escalation hook wired on verification failure (completes the §7 chain end-to-end).
2. STANDARD_CHANGE lane: DESIGN-LITE step per #293 (bounded, deterministic structure, no research dossier).
3. Probe policy decision + one real probe through the production path (`MIMO_PROBE_ENABLED` set by the operator runtime, not tests).
4. ADAPT-11 acceptance demonstration on 2–3 real tasks across the three depths, with telemetry.
5. Update epic #284 with the detour record + this matrix; close the gap between tracker and reality.
Then: **PL1** (T9), with the intent-continuity spine as PL1's first or second deliverable (it is an experience-system product, not a separate architecture project).
## GITEA RECONCILIATION (2026-09-11)
Part A-J executed. 15 issues closed (HISTORICAL_EVIDENCE/SUPERSEDED/ACCEPTED_LIMITATION) with evidence. Bug label cleaned: 16 misleading labels removed (open bugs 24→8). #259/#258 updated to current architecture. Remote tracking fixed (master tracks gitea/master, not stale origin). PL1 metrics corrected (NOT_YET_MEASURABLE where evidence doesn't support numbers). D7: bump works, release job fails on Pi — ROOT_CAUSE_UNKNOWN_DEFERRED. Automation noise: 5 duplicate bump commits per logical change (queue-drain artifact, not live recursion — version converges, commit history doesn't).
## DEFECT D7 (2026-09-11): RESOLVED — release-job failures root-caused
History: the release job failed on every push since 03:16 (c2528c1).
Superseded diagnosis: "Pi-side backup step logs Failed to connect to
127.0.0.1:3000" was a red herring — the Pi backup step was never the
failing job.
Root causes (live-proven T15 Part F, 2026-09-11 evening):
1. c2528c1 left a corrupted shell fragment after the converged-check fi
in auto-version.yml — the release job died at bash parse time (exit 2)
before any bump. Fixed in c35b604.
2. release_convergence() never assigned the release boundary — all 959+
commits classified as unreleased, so the warranted bump never
converged and queued runs each wrote their own bump commit. Fixed in
e395ee1 (boundary wiring) + 9e0b855 (boundary grep must match subjects
only, not bodies) + c35018f (numeric semver comparison: '0.66.9' <
'0.66.10' is False lexicographically, which silently skipped warranted
bumps as version_ahead_of_target).
3. The per-file contents PUT committed one commit per version surface —
five same-version bump commits per real bump (the "duplicate bump
cascade" since 0.66.2, misdiagnosed as queue drain). Fixed in 470d82f
+ d68fd1e (single ChangeFiles commit).
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
drained, zero delayed duplicates, zero workflow failures.
File diff suppressed because it is too large. Load diff
+1
View File
@@ -18,6 +18,7 @@ PR as the new workflow._
| `research-dispatcher.yml` | issue labeled `research-pending`, workflow_dispatch | legacy stage dispatcher (SUPERSEDED by the canonical Research-to-Build pipeline, epic #257; retained until RP11 retirement) | Trigger labels removed from disposable issues 2026-09-08; no stale redispatch path |
| `auto-label.yml` | issue events | priority gate, label transitions | Gate comments on unlabelled issues |
| `auto-backup.yml` | schedule | repo backup | Journal alert on failure |
| `deploy.yml` | push to master | deploy the CI-qualified SHA to the local Rune code deployment (`/mnt/workdrive/rune-code/<sha>`): waits for `CI Pipeline / required-checks` success on that exact SHA, stages, stamps `_build_identity.json`, verifies (import + SHA assert + authority smoke), atomically promotes `current` symlink, records `last-good`, re-verifies through the consuming path | CI not green/timeout → deployment refused, previous deployment stays current |
| `auto-update-gitea.yml` | schedule | Gitea self-update | Binary-only rollback risk (T1.1 audit finding) |
| `research-stage-headless.yml` | issue labeled `research-pending` | headless research dispatcher: 4-phase round (INWARD/OUTWARD/INNOVATION/SYNTHESIS) via run_agent_stage.py; files research/issue-N-research.md + marker comment | Tool receipt invalid (OUTWARD <2 domains) → job fails loud, label stays; lock TTL re-dispatches crashed rounds |
+4
View File
@@ -0,0 +1,4 @@
wait/resume experiment marker 23:07:52
wr delivery test 23:28:42
delivery test 2 23:31:02
wr delivery test 2 23:31:12
+1
View File
@@ -0,0 +1 @@
def broken(:
+2 -2
View File
@@ -10,7 +10,7 @@ henxels:
- henxel: "No secrets in tracked files"
no_secrets: true
except: ["./tests/test_gitea_provision.py", "./.ci-venv/**", "./.test-venv/**", "./.canary-venv/**", "./backups/**"]
except: ["./tests/test_gitea_provision.py", "./tests/test_gitea_auth.py", "./.ci-venv/**", "./.test-venv/**", "./.canary-venv/**", "./backups/**"]
- henxel: "Plans are markdown, one per build"
in: ./plans
@@ -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/**"]
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json"]
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
in: ./src/rune/*
+1 -1
View File
@@ -1,4 +1,4 @@
{
"BLE001": 353,
"BLE001": 352,
"S110": 80
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.0"
version = "0.66.14"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
@@ -0,0 +1,276 @@
# RUNTIME REACHABILITY AUDIT — Live Rune Slice Tranche
**Date: 2026-09-10 · Master v0.66.0 · Method: code tracing with file:line evidence, 4 parallel tracers + first-hand canaries**
---
## A. The actual current execution path
**One real end-to-end path exists and executes today:**
```text
USER INPUT (TUI chat/task channel, or `rune_operator.py send` CLI)
↓ confirmed rune_tui.py:2115 on_input_submitted → _send_to_model (2223) → rune_operator.py:494 send_task
TASK IDENTITY / STATE
↓ confirmed TaskStateRepository amend-or-create (rune_operator.py:430-433, 502-521)
↓ backed by orchestration/event_store.DurableEventStore + atomic.AtomicStore
ROUTING DECISION
↓ confirmed infer_task_family (rune_operator.py:541) → model_routing.build_model_router().rank()
↓ advisory, cheapest-verified-first over static CAPABILITY_FLOOR (model_routing.py:306-312)
CONTEXT / MEMORY ACQUISITION
→ confirmed ContextBroker.assemble (rune_operator.py:1294; context_broker.py:616)
→ confirmed contributors: persona(static literal) → tools(capability_broker, rune_operator.py:725)
→ memory(memory_selector.select_for_task → memory_store.recall + warm_neuron_store
+ graph_memory + abstraction, rune_operator.py:1126-1190; memory_selector.py:269-496)
→ model_profile → history → user
→ confirmed budget admission against real model window (context_broker.py:616)
SAFETY ESCALATION
→ confirmed safety_override.get_params + tier loop decorates user block (rune_operator.py:1315-1392)
MODEL INVOCATION
→ confirmed LocalToolRuntime(transport=OllamaChatTransport) → POST localhost:11434/api/chat
(rune_operator.py:1400-1431; local_tool_runtime.py:102,178-199)
→ confirmed tool loop: tool_executor.execute_tool = prepare_tool, mutation tools gated
by approval round-trip (tool_executor.py:830-833, 675, 752)
RESPONSE → OUTCOME
→ confirmed refusal escalation tier+1 (rune_operator.py:1452-1461); usage folding (1356-1367)
→ confirmed task transition completed/failed + event_bus emit (rune_operator.py:608-643)
→ confirmed durable events: ~/.rune/events.jsonl (task_state.transition, conversation.message)
```
**First-hand canary (2026-09-10, this audit):** `rune_operator.py send "Reply with exactly: SPINE_ALIVE_20260910"` →
router elected `qwen2.5:3b` → completed ✓ → task state + conversation events persisted. **The spine is alive.**
(One degrade observed: `inference_trace` tier-2 ImportError in standalone CLI mode.)
### Fragmentation points (confirmed)
1. **Cloud routing is telemetry theater.** The router can elect `mimo-v2.5-pro` provider=openai and record
`routing_mode="cloud"` (rune_operator.py:561, 1593) — but `_execute_on_model` hardwires
`OllamaChatTransport` (rune_operator.py:1400). A cloud election sends a nonexistent model to localhost:11434
→ guaranteed failure. `CloudStubAdapter` is explicitly "declared, not live" (model_routing.py:134-155).
2. **`model_connect.generate`, `provider_registry.generate`, `microkernel` dispatch**: imported, never invoked
in the send path. The registries are display furniture; execution is a bare urllib POST in local_tool_runtime.
3. **The research pipeline's orchestrator is the agent session itself.** Nothing in-repo invokes
`StageExecutor` except its own CLI and tests — B3-v3's 12 stage executions were the session driving
`research_stage_executor.py` stage-by-stage (telemetry/attempts.jsonl timestamps confirm).
4. **No console script in packaging** — `python -m rune` works only via manual sys.path patching
(rune_tui.py:28-33; dual `from .X` / `from rune.X` fallbacks throughout).
5. **inference_trace degrades** in standalone mode (relative-import failure, tier-2) — trace persistence is
partial in the CLI path.
---
## B. Reachability table (highest continuously proven rung)
Ladder: IMPLEMENTED → PERSISTED → CONSUMED → AFFECTS LIVE BEHAVIOUR → OBSERVABLE → END-TO-END PROVEN
### LIVE (affects live behaviour, evidenced)
| System | Rung | Evidence |
|---|---|---|
| ContextBroker | CONSUMED→AFFECTS (context assembly every call) | rune_operator.py:997-1294; context_broker.py:616 |
| memory_selector (single memory contributor) | CONSUMED→AFFECTS | rune_operator.py:1126-1190; fail-closed, trace fields |
| memory_store + drift_core rerank | CONSUMED→AFFECTS (retrieval order) | memory_store.py:891-925, 1152 |
| capability_broker | CONSUMED→AFFECTS (tool choice) | rune_operator.py:725; tool_executor.py:1643 |
| LocalToolRuntime + OllamaChatTransport | END-TO-END PROVEN (live canary) | local_tool_runtime.py:102,199 |
| tool_executor (prepare/approve) | END-TO-END PROVEN | tool_executor.py:830-833 |
| TaskState/Conversation repos + event_bus | END-TO-END PROVEN (durable events) | ~/.rune/events.jsonl verified |
| model_routing (advisory rank) | CONSUMED (decision feeds execution) | rune_operator.py:543-560 |
| complexity_scorer | CONSUMED (task-channel advisory) | rune_tui.py:2182 |
| claim_audit | CONSUMED (advisory-only effect — warns, never blocks) | rune_tui.py:3829-3836 |
| pins | PARTIALLY_LIVE (display only, not injected) | rune_tui.py:1754, 2265 |
### IMPLEMENTED_NOT_CONSUMED — the dead cognitive subnet
The entire salience→budget→tier→escalate→verify→outcome ladder is a **closed dead subnet**: each module's only
consumers are the other dead modules plus canary import checks.
| System | Status | Note |
|---|---|---|
| salience_gate | IMPLEMENTED_NOT_CONSUMED | role absorbed by elastic_cognition (itself unconsumed) |
| cognitive_budgeter | IMPLEMENTED_NOT_CONSUMED | consumers are tiered_executor/elastic_cognition — both dead |
| tiered_executor | IMPLEMENTED_NOT_CONSUMED | zero importers |
| progressive_escalation | IMPLEMENTED_NOT_CONSUMED | **fully duplicated** by scripts/adaptive_router.py |
| verification_layer | IMPLEMENTED_NOT_CONSUMED | zero importers |
| outcome_ledger | PERSISTED_NOT_CONSUMED | **explicitly de-wired** (model_routing.py:17 "No OutcomeLedger dependency") |
| witness / auto_tasks / behavior | IMPLEMENTED_NOT_CONSUMED | deliberately retired per P0-04 #20 (rune_tui.py:2208-2214, docs/pipeline-removal.md) — imports remain as vestiges |
| task_gate | IMPLEMENTED_NOT_CONSUMED | zero callers anywhere |
| self_improve | IMPLEMENTED_NOT_CONSUMED | manual /improve report only |
| persona_gate | IMPLEMENTED_NOT_CONSUMED | manual /persona-gate only; permissive by design |
| work_manager | IMPLEMENTED_NOT_CONSUMED | manual /audit //work commands |
| warm_router (in-repo) | TEST_ONLY | TUI imports it; zero calls; per-message pipeline removed |
| microkernel | PARTIALLY_LIVE | /plugins status only; dispatch engine unused |
| tier_manager | PERSISTED_NOT_CONSUMED | demotion sweep never invoked |
| drift_ledger | IMPLEMENTED_NOT_CONSUMED | manual /drift diagnostic |
| persona_definition | PARTIALLY_LIVE | nightly persona-file writes; NOT used for live prompts (persona block is a static literal) |
### Memory & context (tracer-2)
| System | Rung | Evidence |
|---|---|---|
| memory_selector | LIVE (sole v1_owner of memory injection, every turn) | rune_operator.py:1144-1186; memory_selector.py:3-9, 497-511 |
| memory_store (read side) | LIVE (## Context block every turn) | memory_selector.py:269-276; rune_operator.py:1184-1186 |
| memory_store (write side) | PERSISTED_NOT_CONSUMED — **no live chat-path writer in-repo**; producers are CLI add/hook, retrieval_benchmark, external curator | memory_store.py:501, 2227 |
| memory_consolidation | PARTIALLY_LIVE (read live every turn; write on-demand/night) | memory_selector.py:439-460; sleep_time.py:65-68 |
| warm_neuron_store (read) | PARTIALLY_LIVE (trigger-matched read every turn; write via external curator only) | memory_selector.py:335-337 |
| warm_router | PARTIALLY_LIVE — route() **dead by design** since P0-04 (enforced by test); write path on-demand | rune_tui.py:3644-3660; test_p0_04_pipeline_removal.py:33-52 |
| attention_capsule | LIVE (the sent payload itself) | context_broker.py:762-767; rune_operator.py:1385+ |
| cache | LIVE (latency memoization only, no context shaping) | memory_store.py:610, 1159 |
| recall_router | IMPLEMENTED_NOT_CONSUMED — **notable orphan**: its abstention→deep-research routing (recall_router.py:9-19) mirrors the ADAPT-2 escalation concept; the selector abstains (memory_selector.py:188-196) but nothing routes that abstention to research | zero importers |
| auto_context | IMPLEMENTED_NOT_CONSUMED (deliberately retired, enforced by test) | canary_p0_04.py:74 |
| context_pressure | PERSISTED_NOT_CONSUMED (manual /pressure diagnostic) | rune_tui.py:2577 |
| query_memory | IMPLEMENTED_NOT_CONSUMED (canary-checked only) | query_memory.py:193 |
| subconscious_pass | IMPLEMENTED_NOT_CONSUMED | subconscious_pass.py:155 |
| token_optimizer | PERSISTED_NOT_CONSUMED (write-only; optimize_prompt zero callers) | token_optimizer.py:297 |
| sleep_time / night_mode | PARTIALLY_LIVE (on-demand trigger; model effect indirect) | rune_tui.py:2978, 3078-3109 |
**Memory findings:** the single live memory→context path is operator contributor → MemorySelector → six sources
under one 0.42 relevance floor → broker admission (memory sub-budget min(1200, window//8)). **Split-brain risk**:
three paths bypass in-repo modules by importing/subprocessing the *external harness copies* in
`~/.config/opencode/scripts` (tool_executor.py:1519, auto_context.py:22-24, night_mode.py:213) — separate files
over the same DB, byte-identity unverified. And the store has **no live writer on the chat path**: the store's
contents are produced by scripts/external runs while consumption is fully live.
---
## C. Dead/partial capability findings
1. **The dead cognitive subnet** (above) — an entire ladder built, tested, never consumed. The TUI's
per-message pipeline that used to consume some of it was deliberately removed (P0-04 #20) and never replaced.
2. **False-liveness surface**: TUI startup "module loaded" checklist (rune_tui.py:1826-1866) reports witness/
auto_tasks/behavior/elastic_cognition as present — presence checks, not consumers. Creates confidence of
liveness that the trace contradicts.
3. **Cloud execution gap**: router elects cloud, executor can't run it (fragmentation #1).
4. **outcome_ledger de-wired**: cognitive-performance evidence is collected nowhere today — PL1's foundation
has no live producer in the operator path.
5. **progressive_escalation.py duplicated** by scripts/adaptive_router.py — two escalation ladders, one wired.
6. **Persona block is a ~60-line hardcoded literal** (rune_operator.py:1079+) pointing at OpenCode-era memory
paths; persona_definition's compiled definition is not used for live prompts. prompt_sync and all voice
modules are display-only — they do not touch the cognitive path (tracer-4).
7. **hook_engine.fire is a latent bug**: rune_tui.py:3841 calls a nonexistent `hook_engine.fire`; the exception
is swallowed, so "post-message hooks" silently never run. All "called by the microkernel" hook handlers
(warm_router.py:1280, pins.py:254, graph_memory.py:379, recall_router.py:108...) are dead — `microkernel.fire`
has zero callers.
8. **event_bus is a one-way journal**: task.failed emissions are durable (event_bus.py:113-135) but **no actor
reacts** — the PL0 Experience ledger would be the natural consumer and is itself orphaned.
9. **PL0 Experience ledger is orphaned**: defined only in scripts/process_experience.py:41 (ExperienceLedger,
append-only JSONL) — zero producers, zero consumers, anywhere in the repo.
10. **shard_manager dual-copy drift hazard**: src/rune/shard_manager.py and shared/shard_manager.py differ;
memory_store.py:1134 does a bare `import shard_manager` — resolution is sys.path-dependent. Shards are
bypassed on the main recall path anyway (memory_selector.py:271 calls recall directly).
11. **Single-provider reality**: ~6 provider modules exist (provider_registry, provider_budget,
standardized_providers, model_connect, connection_pool, cloud adapters); the only executing transport is
OllamaChatTransport → localhost:11434/api/chat. No OpenAI/Anthropic/GLM code executes on the live path.
12. **safety_override is the real governor**: the only infra module besides model_routing/tool stack that
changes invocation behaviour (tier escalation, prompt decoration, temperature — rune_operator.py:1315-1430).
---
## D. ADAPT-2 qualification (Phase 6)
**Initial routing: LIVE (pipeline path, canary-proven).**
Real task → production ISSUE dispatch (`StageExecutor.execute`, scripts/research_stage_executor.py:316+) →
`_route_on_dispatch` → `classify_route` → route.jsonl (INITIAL_ROUTE) → `ROUTED_LOCAL_FIX` changes execution
(research chain not entered; tree records terminal route). Canary 1 proves the behavioural effect through the
production dispatch path. Caveat: the *dispatcher* is the agent session driving the executor CLI — there is no
standalone orchestrator process; the routing edge itself is proven.
**Targeted Evidence Probe: PARTIALLY_LIVE.**
Implemented, schema-gated, canary-proven, wired into dispatch (research_stage_executor.py:340-346). Real probing
is gated behind `MIMO_PROBE_ENABLED=1` or an injected runner. Traced: **nobody sets that env in production** —
no workflow, no script, no operator path. Without opt-in, material unknowns escalate directly to deep research
(correct but un-optimized). Qualification of the opt-in: **temporary development gate / missing runtime policy
decision** — the runtime decision "who decides probing is permitted" does not exist yet. Not changed, per
instruction.
**Verification-driven escalation: PARTIALLY_LIVE.**
`escalate_on_verification_failure()` exists and is canary-proven (module level). The `ROUTED_LOCAL_FIX` consumer
is external (the agent session/operator that receives the routed result). Nothing in-repo automatically performs
implementation→verification→escalation invocation. The chain "real local implementation → real verification
failure → automatic escalation" is **not yet demonstrated end-to-end**.
---
## E. Smallest Live Rune Slice — verdict
**The spine exists.** `rune_operator.send_task` is a real, live, end-to-end execution path (proven by canary
during this audit): input → task state → context/memory assembly → model routing → Ollama tool loop →
verification/outcome → durable events. The eventual TUI already calls exactly this path. **No new execution
architecture is needed.**
What is missing is three EDGES onto that spine — the handoff's target path steps that currently terminate:
| Missing edge | Today | Smallest fix |
|---|---|---|
| **Adaptive cognition** (route decision) | send_task uses model_routing (model choice) but NOT task-class routing; the route authority lives in the research-pipeline world (scripts/), disconnected from the operator path | Route decision at send_task entry, persisted to the durable trace + event journal (see §G choice) |
| **Outcome/experience producer** | outcome_ledger de-wired; PL0 Experience ledger orphaned (zero producers); task.completed/failed transitions fire into a journal **no actor reads** | Emit a typed `task.outcome` event on every terminal transition (payload: task_id, route, model, verification, outcome); a small consumer appends Process Experience records |
| **Abstention catch** | memory_selector abstains (measured, trace-observable) but nothing routes that signal anywhere | P1 — depends on the route-authority choice |
Plus two honest defects found on the spine itself (bounded, non-architectural):
- `inference_trace` tier-2 ImportError in standalone CLI mode (trace persistence partial).
- `hook_engine.fire` dead call site (rune_tui.py:3841 calls a nonexistent function, exception swallowed).
**What this is NOT**: not a parallel demo path — the spine IS the TUI's path. Not a mass-wiring of the dead
subnet — §G ranks it.
---
## F. Proposed canary (Phase 8 acceptance)
One bounded task through the REAL spine, asserting every rung:
```text
1. rune_operator.py send "<bounded task>" (CLI — same path the TUI calls)
2. ContextBroker assembles; trace shows memory_candidates/selected/abstained
3. Route decision recorded (INITIAL_ROUTE in durable trace/event journal)
4. Route affects execution posture (observable in trace + task record)
5. Model/tool execution via OllamaChatTransport (already proven)
6. Verification result recorded (existing verification back-fill)
7. Outcome persisted: task_state.transition + NEW task.outcome event
8. Process Experience record appended (NEW consumer of the outcome event)
9. All transitions inspectable: ~/.rune/events.jsonl + route/trace fields + experience JSONL
```
Pass criterion: steps 1–9 reconstructable from persisted state with no prose inference — the same
audit-trail standard as ADAPT-2 canary 6.
---
## G. Integration priorities
**P0 — required for the cognitive execution spine (this tranche):**
1. `task.outcome` typed event on every send_task terminal transition (producer edge for experience/PL1).
2. Experience-ledger consumer of the durable event store (PL0 gains a live producer; PL1 gains input).
3. Route decision recorded in the live path (see THE CHOICE below).
4. Fix inference_trace standalone degrade (bug).
5. Remove hook_engine.fire dead call site (honest fix of a silently-swallowed AttributeError).
**THE CHOICE (presented, not buried — consent boundary):** where does task-class route authority live?
- **Option A — Route-in-operator**: send_task calls the route authority (import from scripts/ or port to
src/rune), persists RouteDecision, and RESEARCHED_CHANGE tasks are flagged/dispatched to the research
pipeline. Operator becomes the single dispatch entry for ALL work.
- **Option B — Two worlds, formally**: operator path stays route-free; the research pipeline keeps its own
routing. The boundary is documented; no unification now.
- **Option C — Unified core authority**: move route authority into src/rune; both the operator and the
research executor consume it. Cleanest end-state, largest change.
Recommendation: **A** — smallest spine that makes routing observable in live behaviour, without moving code
across worlds; C remains the long-term shape after PL1 shows what the routes need to be.
**P1 — claims to materially alter cognition today:**
- recall_router abstention→research catch (the orphan whose concept is the missing edge)
- verification_layer consumed for LOCAL_FIX verification (completes the verification-escalation chain)
- claim_audit escalation beyond advisory (currently warns, never blocks)
- outcome_ledger: re-wire into the event-driven outcome flow OR formally retire it (not both)
- progressive_escalation.py: retire in favour of scripts/adaptive_router.py (duplicated ladder)
**P2 — required by PL1/Scaffolding:** experience-ledger schema maturity, capability-gap detection inputs.
**P3 — useful, not blocking:** persona block sourced from persona_definition (not a hardcoded literal);
microkernel dispatch decision (wire or formally retire); shard_manager unification.
**P4 — legacy/deferred:** voice* (no cognitive contact), provider cloud modules until cloud execution is
actually wanted, response_adapter (superseded), model_registry+tiered_executor (isolated pair), salience_gate
(absorbed), context_pressure/token_optimizer/query_memory/subconscious_pass (diagnostics).
---
*Generated from code tracing with file:line evidence; live canary executed 2026-09-10.*
+7 -2
View File
@@ -23,6 +23,11 @@ import time
import urllib.error
import urllib.request
from typing import Any
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
BUMP_BRANCH_PREFIX = "chore/auto-bump-"
@@ -97,7 +102,7 @@ def apply_surface_edits(
def run_bump() -> None:
TOKEN = os.environ["GITEA_TOKEN"]
TOKEN = gitea_auth.get_token()
BASE = os.environ["RELEASE_SERVER_URL"] + "/api/v1/repos/" + os.environ["RELEASE_REPO"]
VERSION = os.environ["RELEASE_VERSION"]
NEW = os.environ["NEW_VERSION"]
@@ -264,7 +269,7 @@ def main(argv: list[str] | None = None) -> int:
if args.safety_net:
base = os.environ["RELEASE_SERVER_URL"] + "/api/v1/repos/" + os.environ["RELEASE_REPO"]
token = os.environ["GITEA_TOKEN"]
token = gitea_auth.get_token()
# Attribute against exactly this merge's would-be bump branch: a stale
# chore/auto-bump-* branch from an older version must not stand the
# net down (spec-gate finding 7a, #192).
+6 -1
View File
@@ -24,6 +24,11 @@ import sys
import urllib.error
import urllib.request
from typing import Any
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
# Env-contract (#192 spec-gate finding 1): derived in-script, never required
# from the caller. Guarded by tests/test_workflow_script_contract.py.
@@ -87,7 +92,7 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(payload))
return 0
token = os.environ["GITEA_TOKEN"]
token = gitea_auth.get_token()
# Derived URLs (explicit env still overrides — for direct/manual use).
server_url = os.environ["RELEASE_SERVER_URL"]
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""deploy_qualify.py — read the CI qualification status for one commit.
Called by .gitea/workflows/deploy.yml (kept out of the YAML so the workflow
has no embedded multi-line scripts — a YAML block-scalar indentation hazard).
Prints one of: success | failure | pending
Exit code 0 always (the shell decides what to do with the verdict).
"""
from __future__ import annotations
import json
import os
import sys
import urllib.request
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
def main() -> int:
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
sha = os.environ.get("DEPLOY_SHA", "")
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
if not (server and repo and sha and token):
print("pending") # missing config → cannot verify → not qualified
return 0
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
except (OSError, ValueError) as exc:
# transient API/network failure → keep polling (urllib raises
# URLError (OSError subclass); bad JSON raises ValueError)
print(f"pending (status probe failed: {exc})")
return 0
required = [
s
for s in data.get("statuses", [])
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
]
if not required:
print("pending")
else:
print(required[0].get("status", "pending"))
return 0
if __name__ == "__main__":
sys.exit(main())
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""deploy_stage.py — stage + verify one CI-qualified revision for deployment.
Called by .gitea/workflows/deploy.yml. Stages src/ into
/mnt/workdrive/rune-code/<short-sha>, stamps _build_identity.json, verifies
the staged tree (import + SHA assert + authority smoke), then ATOMICALLY
promotes the `current` symlink and records `last-good` (safe rollback:
the previous deployment remains available).
Exit 0 = deployed and verified. Non-zero = deployment refused; the previous
deployment stays current.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
REPO_ROOT = Path(__file__).resolve().parent.parent
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
def main() -> int:
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
# hardcoded dev path: the runner user has its own checkout and git
# refuses "dubious ownership" on foreign working copies.
repo = Path(os.environ.get("GITHUB_WORKSPACE", os.getcwd())).resolve()
sha = subprocess.run(
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "rev-parse", "HEAD"],
capture_output=True,
text=True,
).stdout.strip()
if not sha:
print(f"FATAL: cannot resolve HEAD in {repo}", file=sys.stderr)
return 1
short = sha[:12]
# --- stage ---------------------------------------------------------
stage = DEPLOY_ROOT / short
tmp_stage = DEPLOY_ROOT / f".staging-{short}-{int(time.time())}"
shutil.copytree(repo / "src", tmp_stage / "src")
# shared/ is required at runtime: src/rune bare imports (prompt_sync ->
# build_pipeline_policy, memory_store -> shard_manager, etc.) resolve
# via the ../../shared fallback from src/rune/*. Its absence broke the
# deployed TUI launch (ModuleNotFoundError: build_pipeline_policy).
if (repo / "shared").exists():
shutil.copytree(repo / "shared", tmp_stage / "shared")
for extra in ("pyproject.toml", "README.md"):
if (repo / extra).exists():
shutil.copy2(repo / extra, tmp_stage / extra)
# --- build identity -------------------------------------------------
sys.path.insert(0, str(repo / "src"))
import rune as rune_dev # noqa: E402 — read the source version
identity = {
"commit": sha,
"version": rune_dev.__version__,
"deployed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
(tmp_stage / "src" / "rune" / "_build_identity.json").write_text(json.dumps(identity, indent=2))
# --- verify the STAGED tree (before promotion) ----------------------
env = dict(os.environ, RUNE_HOME="/mnt/workdrive/rune", RUNE_MEMORY_DIR="/home/nick/.config/opencode/memory")
verify = subprocess.run(
[
VENV_PY,
"-c",
f"""
import json, sys
sys.path.insert(0, {str(tmp_stage / "src")!r})
import rune
info = rune.build_info()
assert info["source"] == "deployed", f"identity file missing: {{info}}"
assert info["commit"] == {sha!r}, f"SHA mismatch: {{info['commit']}}"
assert info["version"], "empty version"
from rune import adaptive_router, builder_bridge, context_broker, rune_operator # noqa: F401
print("VERIFIED:", json.dumps(info))
""",
],
capture_output=True,
text=True,
env=env,
timeout=120,
)
print(verify.stdout.strip())
if verify.returncode != 0:
print("VERIFY FAILED — deployment refused:\n" + verify.stderr[-800:], file=sys.stderr)
shutil.rmtree(tmp_stage, ignore_errors=True)
return 1
# --- TUI startup canary (handoff 2026-09-10 §4): the ACTUAL
# RuneApp initialization must succeed before promotion. A staged
# revision whose TUI cannot initialize never becomes current.
canary = subprocess.run(
[
VENV_PY,
str(REPO_ROOT / "scripts" / "tui_startup_canary.py"),
"--mode",
"candidate",
"--stage",
str(tmp_stage),
],
capture_output=True,
text=True,
env={**env, "PYTHONPATH": str(tmp_stage / "src")},
timeout=180,
cwd=str(tmp_stage),
)
print(canary.stdout.strip())
if canary.returncode != 0:
print("TUI STARTUP CANARY FAILED — deployment refused:\n" + canary.stderr[-800:], file=sys.stderr)
shutil.rmtree(tmp_stage, ignore_errors=True)
return 1
# --- promote (atomic) + last-known-good ------------------------------
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
current = DEPLOY_ROOT / "current"
if current.is_symlink():
last_good = DEPLOY_ROOT / "last-good"
if last_good.is_symlink():
last_good.unlink()
last_good.symlink_to(os.readlink(current))
if current.is_symlink() or current.exists():
current.unlink()
if stage.exists():
shutil.rmtree(stage) # re-deploying an already-staged SHA: restage
os.rename(tmp_stage, stage)
current.symlink_to(stage)
print(f"PROMOTED: {short}")
print("current ->", os.readlink(current))
if (DEPLOY_ROOT / "last-good").is_symlink():
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
return 0
if __name__ == "__main__":
sys.exit(main())
+72 -16
View File
@@ -12,6 +12,7 @@ Canonical pipeline (RP2, handoff 2026-09-08):
Per the authoritative build prompt §18:
"DETERMINISTIC TESTS PROVE THE PIPE. REAL MIMO DOES THE RESEARCH."
"""
from __future__ import annotations
import json
@@ -21,6 +22,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from research_task_tree import ResearchTaskTree, STAGES
def fake_issue_gate(tree: ResearchTaskTree) -> dict:
"""ISSUE is deterministic: the Gitea issue content is the DEFINE replacement.
The fake worker emits the compiled gate artifact directly."""
@@ -33,6 +35,7 @@ def fake_issue_gate(tree: ResearchTaskTree) -> dict:
"quality": "BUILDABLE",
}
def fake_inward(tree: ResearchTaskTree) -> dict:
# Assert ISSUE-gate artifact was actually supplied
prior = tree.get_prior_artifacts("INWARD")
@@ -40,11 +43,22 @@ def fake_inward(tree: ResearchTaskTree) -> dict:
assert issue_gate.get("goal"), "INWARD: ISSUE-gate artifact missing — evidence handoff broken"
return {
"claims": [
{"id": "I1", "claim": "memory_store uses FTS5", "evidence": ["src/rune/memory_store.py:86"], "status": "CURRENT"},
{"id": "I2", "claim": "shard routing is present", "evidence": ["src/rune/shard_manager.py:114"], "status": "CURRENT"},
{
"id": "I1",
"claim": "memory_store uses FTS5",
"evidence": ["src/rune/memory_store.py:86"],
"status": "CURRENT",
},
{
"id": "I2",
"claim": "shard routing is present",
"evidence": ["src/rune/shard_manager.py:114"],
"status": "CURRENT",
},
],
}
def fake_outward(tree: ResearchTaskTree) -> dict:
"""Unified OUTWARD (RP3): one research task, discovery + reading as
SUBSECTIONS of one artifact — not separate stages."""
@@ -58,19 +72,35 @@ def fake_outward(tree: ResearchTaskTree) -> dict:
"tool_receipts": [{"tool": "webfetch", "query": "fts5 hybrid search", "status": "completed"}],
"sources": [
{"id": "O1", "url": "https://sqlite.org/fts5.html", "title": "FTS5", "source_class": "software"},
{"id": "O2", "url": "https://github.com/asg017/sqlite-vec", "title": "sqlite-vec", "source_class": "software"},
{
"id": "O2",
"url": "https://github.com/asg017/sqlite-vec",
"title": "sqlite-vec",
"source_class": "software",
},
],
},
"reading": {
"evidence": [
{"id": "E1", "source_id": "O1", "url": "https://sqlite.org/fts5.html",
"claim": "FTS5 supports external content tables", "limitations": ["token porter only"]},
{"id": "E2", "source_id": "O2", "url": "https://github.com/asg017/sqlite-vec",
"claim": "vec0 virtual tables for embedding search", "limitations": []},
{
"id": "E1",
"source_id": "O1",
"url": "https://sqlite.org/fts5.html",
"claim": "FTS5 supports external content tables",
"limitations": ["token porter only"],
},
{
"id": "E2",
"source_id": "O2",
"url": "https://github.com/asg017/sqlite-vec",
"claim": "vec0 virtual tables for embedding search",
"limitations": [],
},
],
},
}
def fake_applicability(tree: ResearchTaskTree) -> dict:
"""APPLICABILITY (RP4): classify researched mechanisms against Rune's reality."""
prior = tree.get_prior_artifacts("APPLICABILITY")
@@ -96,6 +126,7 @@ def fake_applicability(tree: ResearchTaskTree) -> dict:
],
}
def fake_adversarial(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("ADVERSARIAL")
outward = prior.get("OUTWARD", {})
@@ -104,19 +135,31 @@ def fake_adversarial(tree: ResearchTaskTree) -> dict:
return {
"disposition": "SURVIVES_WITH_MITIGATIONS",
"counter_evidence": [
{"id": "A1", "source_id": "O1", "claim": "FTS5 porter tokenizer may not handle all languages",
"derived_from": ["E1"], "limitations": ["minor"]},
{
"id": "A1",
"source_id": "O1",
"claim": "FTS5 porter tokenizer may not handle all languages",
"derived_from": ["E1"],
"limitations": ["minor"],
},
],
"surviving_candidates": ["M1"],
"rejected_candidates": [],
"required_mitigations": ["mode labeling"],
"falsification_tests": ["FT1: harness run with embedding backend unreachable"],
"attacks": [
{"id": "A1", "target_candidate": "M1", "dimension": "failure modes",
"finding": "tokenizer coverage", "evidence": ["E1"], "severity": "minor"},
{
"id": "A1",
"target_candidate": "M1",
"dimension": "failure modes",
"finding": "tokenizer coverage",
"evidence": ["E1"],
"severity": "minor",
},
],
}
def fake_innovation(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("INNOVATION")
inward = prior.get("INWARD", {})
@@ -131,14 +174,21 @@ def fake_innovation(tree: ResearchTaskTree) -> dict:
return {
"conclusion": "ADOPT",
"candidates": [
{"id": "C1", "category": "ADAPT", "proposal": "hybrid FTS+vec recall",
"derived_from": inward_claims[:1] + evidence_ids[:1] + applicable[:1],
"composition_of": [], "novelty": "adapted",
"expected_gain": "better recall", "risks": ["latency"],
"kill_test": "recall returns >0 rows"},
{
"id": "C1",
"category": "ADAPT",
"proposal": "hybrid FTS+vec recall",
"derived_from": inward_claims[:1] + evidence_ids[:1] + applicable[:1],
"composition_of": [],
"novelty": "adapted",
"expected_gain": "better recall",
"risks": ["latency"],
"kill_test": "recall returns >0 rows",
},
],
}
def fake_synthesis(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("SYNTHESIS")
innovation = prior.get("INNOVATION", {})
@@ -157,6 +207,7 @@ def fake_synthesis(tree: ResearchTaskTree) -> dict:
"build_instructions": ["modify recall() in memory_store.py", "add tests"],
}
def fake_qualify(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("QUALIFY")
synthesis = prior.get("SYNTHESIS", {})
@@ -177,6 +228,7 @@ def fake_qualify(tree: ResearchTaskTree) -> dict:
"evidence_used": synthesis.get("evidence_used", []),
}
FAKE_WORKERS = {
"ISSUE": fake_issue_gate,
"INWARD": fake_inward,
@@ -188,6 +240,7 @@ FAKE_WORKERS = {
"QUALIFY": fake_qualify,
}
def run_fake_stage(tree: ResearchTaskTree) -> dict:
stage = tree.current_stage
worker = FAKE_WORKERS.get(stage)
@@ -195,8 +248,10 @@ def run_fake_stage(tree: ResearchTaskTree) -> dict:
raise ValueError(f"no fake worker for stage {stage}")
return worker(tree)
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Fake stage worker for pipeline testing")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--round", type=int, default=1)
@@ -216,5 +271,6 @@ def main_with(argv: list[str]) -> int:
print(f"fake stage {stage} complete")
return 0
if __name__ == "__main__":
raise SystemExit(main_with(sys.argv))
+6 -1
View File
@@ -20,6 +20,11 @@ import urllib.error
import urllib.parse
import urllib.request
from typing import Any
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
DRIFT_ISSUE_LABEL = "release-drift"
DRIFT_ISSUE_LABEL_COLOR = "ff0000"
@@ -108,7 +113,7 @@ def main(argv: list[str] | None = None, api: Any = None) -> int:
print(body)
return 0
token = os.environ["GITEA_TOKEN"]
token = gitea_auth.get_token()
base = os.environ["RELEASE_SERVER_URL"] + "/api/v1/repos/" + os.environ["RELEASE_REPO"]
if api is None:
+112
View File
@@ -0,0 +1,112 @@
"""gitea_auth.py — canonical Gitea API authentication (handoff 2026-09-11 §H).
One deterministic helper for authenticated Gitea API access. Never prints
credentials. Fails clearly when unavailable. Credential ladder:
1. GITEA_TOKEN environment variable (preferred for CI/deployment)
2. private token file (GITEA_TOKEN_FILE, default
~/.config/opencode/secrets/gitea-token, must be mode 0600)
3. git credential fill (uses the credential helper / store)
Never parses credentials from git remote URLs.
"""
from __future__ import annotations
import json
import os
import stat
import subprocess
import urllib.request
from pathlib import Path
from typing import Any
DEFAULT_SERVER = "https://gitea.thingscouldgetdicey.online"
DEFAULT_REPO = "conductor/rune"
DEFAULT_TOKEN_FILE = Path("~/.config/opencode/secrets/gitea-token").expanduser()
GITEA_HOST = "gitea.thingscouldgetdicey.online"
class GiteaCredentialError(RuntimeError):
"""Raised when no Gitea credential is available. Message never contains one."""
def _token_from_file() -> str:
path = Path(os.environ.get("GITEA_TOKEN_FILE", DEFAULT_TOKEN_FILE)).expanduser()
if not path.is_file():
return ""
mode = stat.S_IMODE(path.stat().st_mode)
if mode & 0o077:
raise PermissionError(f"Gitea token file must be mode 0600: {path}")
return path.read_text(encoding="utf-8").strip()
def _token_from_credential_helper() -> str:
try:
result = subprocess.run(
["git", "credential", "fill"],
input=f"protocol=https\nhost={GITEA_HOST}\nusername=conductor\n\n",
capture_output=True,
text=True,
timeout=10,
)
for line in result.stdout.splitlines():
if line.startswith("password="):
return line[len("password=") :]
except (subprocess.TimeoutExpired, OSError):
pass
return ""
def has_token() -> bool:
"""True if any rung of the credential ladder answers. Never raises."""
try:
get_token()
return True
except Exception: # noqa: BLE001 — probe-only: any failure means "no credential available"
return False
def get_token() -> str:
"""Obtain the Gitea API token. Never prints the token."""
token = os.environ.get("GITEA_TOKEN", "").strip()
if token:
return token
token = _token_from_file()
if token:
return token
token = _token_from_credential_helper()
if token:
return token
raise GiteaCredentialError(
"Gitea credentials unavailable: set GITEA_TOKEN, create a mode-0600 "
f"token file at {DEFAULT_TOKEN_FILE}, or configure a git credential "
f"helper for {GITEA_HOST}"
)
def api(
path: str,
method: str = "GET",
data: dict | None = None,
server: str = DEFAULT_SERVER,
repo: str = DEFAULT_REPO,
) -> tuple[int, Any]:
"""Make an authenticated Gitea API request. Returns (status_code, parsed_body)."""
token = get_token()
url = f"{server}/api/v1/repos/{repo}{path}"
req = urllib.request.Request(
url,
data=json.dumps(data).encode() if data is not None else None,
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
method=method,
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
body, status = r.read(), r.status
except urllib.error.HTTPError as e:
body, status = e.read(), e.code
try:
return status, (json.loads(body) if body else {})
except json.JSONDecodeError:
return status, {"_raw": body.decode("utf-8", "replace")[:200]}
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""gitea_reconciliation_check.py — Part L consistency check (deterministic).
Compares live Gitea issue state against the canonical reconciliation
artifact (docs/gitea_issue_reconciliation.json). Flags:
1. closed issue recorded in an open-only state
(VALID_OPEN / DEFERRED_FUTURE / NEEDS_REVALIDATION)
2. open issue recorded in a terminal state
(FIXED_VERIFIED / SUPERSEDED / DUPLICATE / HISTORICAL_EVIDENCE /
ACCEPTED_LIMITATION)
3. `bug` label present on any issue not recorded live_defect
(or missing on a recorded live defect)
4. open issue missing from the artifact
5. record missing required fields
Exit 0 = consistent. Exit 1 = drift (each item printed). Read-only.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
ARTIFACT = ROOT / "docs" / "gitea_issue_reconciliation.json"
OPEN_ONLY_STATES = {"VALID_OPEN", "DEFERRED_FUTURE", "NEEDS_REVALIDATION"}
TERMINAL_STATES = {"FIXED_VERIFIED", "SUPERSEDED", "DUPLICATE", "HISTORICAL_EVIDENCE", "ACCEPTED_LIMITATION"}
REQUIRED_FIELDS = {"number", "tracker_state", "reconciliation_state", "labels", "live_defect", "last_revalidated"}
def main() -> int:
import gitea_auth
if not ARTIFACT.is_file():
print(f"reconciliation artifact missing: {ARTIFACT}")
return 1
artifact = json.loads(ARTIFACT.read_text())
records = {r["number"]: r for r in artifact["records"]}
drift: list[str] = []
# Fetch live issues (open + recently closed via both pages)
issues: list[dict] = []
for page in (1, 2):
status, batch = gitea_auth.api(f"/issues?state=all&limit=50&page={page}")
if status != 200:
print(f"Gitea API failed ({status}): {str(batch)[:200]}")
return 1
if isinstance(batch, list):
issues.extend(batch)
live = {i["number"]: i for i in issues}
for num, rec in sorted(records.items()):
for field in REQUIRED_FIELDS:
if field not in rec:
drift.append(f"#{num}: record missing required field {field!r}")
state = rec.get("reconciliation_state")
if state in OPEN_ONLY_STATES and rec.get("tracker_state") == "closed":
drift.append(f"#{num}: closed issue recorded {state}")
if state in TERMINAL_STATES and rec.get("tracker_state") == "open":
drift.append(f"#{num}: open issue recorded terminal {state}")
has_bug = "bug" in (rec.get("labels") or [])
# The bug-label/live-defect invariant applies to OPEN issues only:
# a closed issue's labels are historical record; its reconciliation
# state governs (FIXED_VERIFIED issues legitimately keep the label).
if rec.get("tracker_state") == "open":
if has_bug and not rec.get("live_defect"):
drift.append(f"#{num}: bug label present but not recorded as live defect")
if not has_bug and rec.get("live_defect"):
drift.append(f"#{num}: recorded live defect but bug label missing")
if state == "FIXED_VERIFIED" and rec.get("tracker_state") == "open":
drift.append(f"#{num}: FIXED_VERIFIED but still open without closure note")
# Live vs artifact sync (only for issues present in the live window)
for num, li in sorted(live.items()):
if num not in records:
continue
rec = records[num]
live_state = li["state"]
if live_state == "closed" and rec["tracker_state"] == "open" and rec["reconciliation_state"] in OPEN_ONLY_STATES:
drift.append(f"#{num}: live state closed but artifact says open/{rec['reconciliation_state']}")
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"):
drift.append(f"#{num}: live bug label present but artifact records no live defect")
if not live_bug and rec.get("live_defect"):
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
for num in artifact.get("live_defects", []):
li = live.get(num)
if li is not None and li["state"] != "open":
drift.append(f"#{num}: live defect but tracker state is {li['state']} — close the defect or update the artifact")
if drift:
print(f"RECONCILIATION DRIFT ({len(drift)}):")
for d in drift:
print(f" - {d}")
return 1
print(f"reconciliation consistent ({len(records)} records, {len(live)} live issues in window)")
return 0
if __name__ == "__main__":
sys.exit(main())
+6 -10
View File
@@ -27,6 +27,7 @@ capture time, the caller must decide (abort or accept-and-fingerprint).
This module fingerprints regardless, so even a dirty worktree can't hide
in-stage mutations.
"""
from __future__ import annotations
import hashlib
@@ -39,8 +40,7 @@ class MutationError(RuntimeError):
def _git(*args: str, cwd: str) -> str:
result = subprocess.run(["git", *args], cwd=cwd,
capture_output=True, text=True, timeout=30)
result = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, timeout=30)
return result.stdout.strip()
@@ -56,8 +56,7 @@ def capture_worktree_state(repo_root: str) -> dict:
state["head"] = _git("rev-parse", "HEAD", cwd=repo_root)
state["worktree_clean"] = _git("status", "--porcelain", cwd=repo_root) == ""
# All files (tracked + untracked, excluding .git)
files = _git("ls-files", "--cached", "--others", "--exclude-standard",
cwd=repo_root)
files = _git("ls-files", "--cached", "--others", "--exclude-standard", cwd=repo_root)
fingerprints = {}
for f in files.splitlines():
fp = os.path.join(repo_root, f)
@@ -73,8 +72,7 @@ def capture_worktree_state(repo_root: str) -> dict:
return state
def verify_no_source_mutation(repo_root: str, before: dict,
allowed_paths: list[str]) -> list[str]:
def verify_no_source_mutation(repo_root: str, before: dict, allowed_paths: list[str]) -> list[str]:
"""D5.1+D5.2: content-hash comparison + explicit allowlist.
Returns violations (empty = clean).
@@ -104,8 +102,7 @@ def verify_no_source_mutation(repo_root: str, before: dict,
def _is_allowed(rel: str) -> bool:
if rel in allowed_exact:
return True
return any(rel.startswith(prefix + os.sep) or rel.startswith(prefix + "/")
for prefix in allowed_prefixes)
return any(rel.startswith(prefix + os.sep) or rel.startswith(prefix + "/") for prefix in allowed_prefixes)
# D5.1: compare fingerprints — detects changes to already-dirty files
for path in sorted(set(before_fp) | set(after_fp)):
@@ -122,8 +119,7 @@ def verify_no_source_mutation(repo_root: str, before: dict,
# HEAD must not move (cognitive stages must never commit)
if before.get("head") and after.get("head") and before["head"] != after["head"]:
violations.append(
f"UNAUTHORIZED_COMMIT: HEAD moved {before['head'][:8]} → {after['head'][:8]}")
violations.append(f"UNAUTHORIZED_COMMIT: HEAD moved {before['head'][:8]} → {after['head'][:8]}")
return violations
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""pl1_records.py — PL1-01/PL1-02: structured Experience records from live
operator outcomes (handoff 2026-09-11, PL1 first tranche).
Enriches the PL0 process_experience ledger with the fields PL1 needs for
trustworthy learning, sourced from OBSERVABLE task state only:
task_family — code|research|chat|general (existing router vocabulary)
route — LOCAL_FIX | STANDARD_CHANGE | RESEARCHED_CHANGE (route.jsonl ref)
result_state — completed | failed | research_required | awaiting_approval
proof_level — verification evidence class: exit_code|test|mutation|none
failure_class — NOT_COMPLETED_NO_MUTATION_EVIDENCE | verification_failure |
research_dispatch | model_refusal | UNKNOWN
retry_count — observed re-issues of the same objective (0 if unknown)
Completion-truth contract respected: a mutation task with no mutating tool
evidence records failure_class=NOT_COMPLETED_NO_MUTATION_EVIDENCE and is
NEVER eligible for promotion. Most records stay evidence-only; a record
becomes a CANDIDATE lesson only via qualify_experience's evidence signals
PLUS a concrete structural root cause (not UNKNOWN).
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
_SCRIPTS = os.path.dirname(os.path.abspath(__file__))
if _SCRIPTS not in sys.path:
sys.path.insert(0, _SCRIPTS)
LEDGER_DEFAULT = os.path.expanduser("~/.rune/state/process_experience.jsonl")
"""Default path for the PL0 Experience Ledger. Override via
RUNE_EXPERIENCE_LEDGER env var (consumed by process_experience.py)."""
TASK_STATE_DEFAULT = os.path.expanduser("~/.rune/hot/task_state.json")
ROUTE_LOG_DEFAULT = os.path.expanduser("~/.rune/routes/route.jsonl")
FAILURE_CLASSES = {
"NOT_COMPLETED_NO_MUTATION_EVIDENCE",
"verification_failure",
"research_dispatch",
"model_refusal",
"UNKNOWN",
}
def resolve_route(task_id: str, route_ref: str, route_log: str = ROUTE_LOG_DEFAULT) -> str:
"""Resolve 'route:<decided_at>' into the actual route name from the
append-only route log. Returns '' when unresolvable — never guesses."""
if not route_ref.startswith("route:"):
return ""
ts = route_ref[len("route:") :]
p = Path(route_log)
if not p.exists():
return ""
try:
for line in p.read_text().splitlines():
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
if (
str(ev.get("decided_at", "")).startswith(ts[:19])
and ev.get("dispatch", {}).get("task_id", task_id) == task_id
):
return str(ev.get("route", ""))
except OSError:
pass
return ""
def proof_level(task: dict) -> str:
"""Observable verification evidence class for one finished task."""
outcome_ref = task.get("outcome_ref") or ""
result = task.get("result") or ""
if outcome_ref.startswith("response:") and "wrote" in result.lower():
return "mutation"
# task.outcome journal events carry verification_passed — surfaced in
# task.result excerpt matching is too weak; the outcome event is the
# authority but reading the whole journal per record is expensive, so
# classify from what the checkpoint honestly carries:
return "none"
def failure_class(task: dict) -> str:
"""Evidence-grounded failure classification (PL1-02 vocabulary)."""
status = task.get("status", "")
result = task.get("result") or ""
if status == "research_required" or task.get("outcome_ref") == "research_dispatch":
return "research_dispatch"
if result.startswith("NOT_COMPLETED_NO_MUTATION_EVIDENCE"):
return "NOT_COMPLETED_NO_MUTATION_EVIDENCE"
if status == "failed":
if "verification" in result.lower():
return "verification_failure"
if "capability" in result.lower() or "refus" in result.lower():
return "model_refusal"
return "UNKNOWN"
return "UNKNOWN"
def enrich_record(task: dict, route_log: str = ROUTE_LOG_DEFAULT) -> dict:
"""Build the PL1 evidence record for one task from canonical state."""
route_ref = task.get("route_ref") or ""
route = resolve_route(task.get("task_id", ""), route_ref, route_log)
status = task.get("status", "")
fc = failure_class(task)
return {
"task_id": task.get("task_id", ""),
"task_family": task.get("task_family", ""), # trace v2 field; '' if absent
"route": route,
"objective": (task.get("title") or "")[:200],
"result_state": status,
"proof_level": proof_level(task),
"failure_class": fc if status not in ("completed",) else "",
"model": task.get("assigned_model") or "",
"outcome_ref": task.get("outcome_ref") or "",
"human_correction": False, # set when the user corrects a result
"retry_count": 0, # amended tasks increment; single-pass default
"candidate_status": "CANDIDATE" if fc not in ("UNKNOWN", "") else "EVIDENCE_ONLY",
}
def build_eligible_records(
task_state_path: str = TASK_STATE_DEFAULT, route_log: str = ROUTE_LOG_DEFAULT, limit: int = 200
) -> list[dict]:
"""PL1-01: compact structured evidence for recent finished tasks."""
p = Path(task_state_path)
if not p.exists():
return []
checkpoint = json.loads(p.read_text())
tasks = checkpoint.get("tasks", [])
seen: set[str] = set()
out = []
for t in reversed(tasks[-limit:]):
tid = t.get("task_id", "")
if not tid or tid in seen:
continue
seen.add(tid)
if t.get("status") not in ("completed", "failed", "research_required"):
continue
out.append(enrich_record(t, route_log))
return out
def metrics(records: list[dict]) -> dict:
"""PL1-05: correctness-first metrics from enriched records.
Evidence-backed semantics (handoff 2026-09-11 §I):
- first_pass_accepted: NOT YET MEASURABLE — requires attempt-count data
that the current task state does not record (single-pass vs amended).
- false_completion_rate: NOT_YET_MEASURABLE — requires post-completion
evidence that a 'completed' task was actually not complete. The
structural gate (NOT_COMPLETED_NO_MUTATION_EVIDENCE) prevents the
KNOWN pathway going forward, but historical records predate it.
- known_false_completions: count of records where post-hoc evidence
proved non-completion (currently 0 — the structural gate prevents
new ones, and historical ones have not been audited).
- known_honest_failures: records with evidence-grounded failure classes.
- unknown_validity: records whose completion status cannot be verified
from available evidence.
"""
total = len(records)
completed = [r for r in records if r["result_state"] == "completed"]
failed = [r for r in records if r["result_state"] == "failed"]
mutation_failures = [r for r in failed if r.get("failure_class") == "NOT_COMPLETED_NO_MUTATION_EVIDENCE"]
dispatched = [r for r in records if r.get("failure_class") == "research_dispatch"]
candidates = [r for r in records if r.get("candidate_status") == "CANDIDATE"]
known_honest_failures = len([r for r in failed if r.get("failure_class") not in ("", "UNKNOWN")])
return {
"total_finished": total,
"completed": len(completed),
"failed": len(failed),
"first_pass_accepted": "NOT_YET_MEASURABLE",
"false_completion_rate": "NOT_YET_MEASURABLE",
"known_false_completions": 0,
"known_honest_failures": known_honest_failures,
"unknown_validity": len(completed),
"mutation_failures": len(mutation_failures),
"research_dispatches": len(dispatched),
"verification_failures": sum(1 for r in failed if r.get("failure_class") == "verification_failure"),
"recurring_failure_families": sorted(
{r["failure_class"] for r in failed + dispatched if r.get("failure_class") not in ("", "UNKNOWN")}
),
"candidates": len(candidates),
"evidence_only": total - len(candidates),
}
if __name__ == "__main__":
print(json.dumps(metrics(build_eligible_records()), indent=2))
+154 -13
View File
@@ -20,6 +20,7 @@ Root-cause layers:
EXTERNAL_DEPENDENCY — external service behaved unexpectedly
UNKNOWN — attribution uncertain
"""
from __future__ import annotations
import json
@@ -28,14 +29,22 @@ import time
from datetime import datetime, timezone
ROOT_CAUSE_LAYERS = {
"MODEL_BEHAVIOR", "PROMPT_CONTRACT", "SCHEMA", "NORMALIZER",
"TOOL_INTERFACE", "ARTIFACT_ASSEMBLY", "VALIDATOR", "ROUTING",
"STAGE_ARCHITECTURE", "DETERMINISTIC_RUNTIME", "EXTERNAL_DEPENDENCY",
"MODEL_BEHAVIOR",
"PROMPT_CONTRACT",
"SCHEMA",
"NORMALIZER",
"TOOL_INTERFACE",
"ARTIFACT_ASSEMBLY",
"VALIDATOR",
"ROUTING",
"STAGE_ARCHITECTURE",
"DETERMINISTIC_RUNTIME",
"EXTERNAL_DEPENDENCY",
"UNKNOWN",
}
RESULT_TYPES = {"SUCCESS", "FAILURE", "INEFFICIENCY"}
CANDIDATE_STATUSES = {"SEED", "EVALUATED", "PROMOTED", "REJECTED"}
# (extended below by the PL0 qualification block)
class ExperienceLedger:
@@ -45,13 +54,25 @@ class ExperienceLedger:
self.path = path
os.makedirs(os.path.dirname(path), exist_ok=True)
def record(self, task_run_stage: str, result_type: str, symptom: str,
evidence: list[str], root_cause: str, confidence: float,
human_intervention: bool = False, pipeline_sha: str = "UNKNOWN",
correction_applied: str = "", recurrence_class: str = "",
wall_cost_s: float | str = 0, model_cost_s: float | str = 0,
tool_cost_s: float | str = 0, quality_outcome: str = "",
reusable: bool = False, candidate_status: str = "SEED") -> dict:
def record(
self,
task_run_stage: str,
result_type: str,
symptom: str,
evidence: list[str],
root_cause: str,
confidence: float,
human_intervention: bool = False,
pipeline_sha: str = "UNKNOWN",
correction_applied: str = "",
recurrence_class: str = "",
wall_cost_s: float | str = 0,
model_cost_s: float | str = 0,
tool_cost_s: float | str = 0,
quality_outcome: str = "",
reusable: bool = False,
candidate_status: str = "SEED",
) -> dict:
assert result_type in RESULT_TYPES, f"invalid result_type: {result_type}"
assert root_cause in ROOT_CAUSE_LAYERS, f"invalid root_cause: {root_cause}"
assert candidate_status in CANDIDATE_STATUSES, f"invalid status: {candidate_status}"
@@ -85,8 +106,10 @@ class ExperienceLedger:
out = []
with open(self.path) as f:
for line in f:
try: out.append(json.loads(line))
except json.JSONDecodeError: continue
try:
out.append(json.loads(line))
except json.JSONDecodeError:
continue
return out
def by_root_cause(self, layer: str) -> list[dict]:
@@ -100,3 +123,121 @@ class ExperienceLedger:
def reusable_lessons(self) -> list[dict]:
return [e for e in self.load_all() if e["reusable_lesson"]]
def record_operator_outcomes(task_state_path: str | None = None, ledger_path: str | None = None) -> list[dict]:
"""Consume operator task outcomes into the Experience Ledger (Runtime
Reachability P0, 2026-09-10).
The operator's send_task persists every task in the TaskStateRepository
checkpoint (task_state.json colocated with operator.json) and emits a
durable task.outcome journal event per terminal transition. This
consumer reads the CHECKPOINT (the authoritative task store — the
journal is a one-way integrity journal: payload_hash only, no payload)
and appends a SEED experience record for every terminal task not yet
recorded (matched by evidence task_id — idempotent).
Attribution stays UNKNOWN: attribution is PL1's job, not the ledger's.
Statuses stay SEED/CANDIDATE — never auto-VALIDATED.
"""
path = task_state_path or os.path.expanduser("~/.rune/hot/task_state.json")
ledger = ExperienceLedger(
ledger_path
or os.environ.get("RUNE_EXPERIENCE_LEDGER", os.path.expanduser("~/.rune/state/process_experience.jsonl"))
)
with open(path) as f:
checkpoint = json.load(f)
tasks = checkpoint.get("tasks", [])
seen_tasks = set()
for rec in ledger.load_all():
for ev in rec.get("evidence_references", []):
if ev.startswith("task:"):
seen_tasks.add(ev.split(":", 1)[1])
recorded = []
for t in tasks:
status = t.get("status", "")
if status not in ("completed", "failed"):
continue
# research_required tasks: routing disposition, NOT failure evidence
# (handoff §2/§C). The dispatch lives in route.jsonl; recording it
# as FAILURE would poison PL0 with false failure-learning pressure.
if t.get("outcome_ref") == "research_dispatch" or t.get("result") == "RESEARCH_REQUIRED":
continue
task_id = t.get("task_id", "")
if not task_id or task_id in seen_tasks:
continue
result_type = "SUCCESS" if status == "completed" else "FAILURE"
rec = ledger.record(
task_run_stage=f"operator:{status}",
result_type=result_type,
symptom=(t.get("title") or "")[:200],
evidence=[f"task:{task_id}", f"model:{t.get('assigned_model')}", f"outcome_ref:{t.get('outcome_ref')}"],
root_cause="UNKNOWN",
confidence=0.3,
pipeline_sha="operator",
reusable=False,
candidate_status="SEED",
)
recorded.append(rec)
return recorded
if __name__ == "__main__": # pragma: no cover — manual consumer invocation
import argparse
ap = argparse.ArgumentParser(description="Record operator task outcomes as experience evidence")
ap.add_argument("--task-state", default=None)
ap.add_argument("--ledger", default=None)
args = ap.parse_args()
recs = record_operator_outcomes(args.task_state, args.ledger)
print(f"recorded {len(recs)} experience records")
# PL0 qualification (handoff 2026-09-10 §9): raw outcomes are preserved
# verbatim, but only QUALIFIED records exert learning pressure in PL1.
# The distinction is cheap and rule-based — no model call.
CANDIDATE_STATUSES = {"SEED", "CANDIDATE", "EVALUATED", "PROMOTED", "REJECTED"}
# Task titles that are outcome telemetry, not learning material: trivial
# greetings, arithmetic/canary probes, bootstrap self-checks.
_TELEMETRY_TITLE_PATTERNS = (
"hello",
"how are you",
"hi ",
"thanks",
"thank you",
"reply with exactly",
"what is ",
"check",
"status",
)
def qualify_experience(record: dict) -> str:
"""Classify a raw outcome record: TELEMETRY vs CANDIDATE.
TELEMETRY = simple successful conversation / canary probes — kept as
raw outcome evidence, excluded from PL1 evaluation pressure.
CANDIDATE = failures, verification failures, retries, corrections,
inefficiencies, architectural omissions, reusable procedures, route
misclassifications — the material PL1 evaluates.
"""
if record.get("result_type") in ("FAILURE", "INEFFICIENCY"):
return "CANDIDATE"
symptom = (record.get("symptom") or "").lower()
if any(p in symptom for p in _TELEMETRY_TITLE_PATTERNS):
return "TELEMETRY"
# Simple short successes with no verification/failure signal are
# telemetry; anything substantive defaults to CANDIDATE (PL1 decides).
if len(symptom) < 60 and not record.get("quality_outcome"):
return "TELEMETRY"
return "CANDIDATE"
def qualified_candidates(ledger_path: str | None = None) -> list[dict]:
"""All CANDIDATE-qualified records — the PL1 evaluation input."""
ledger = ExperienceLedger(
ledger_path
or os.environ.get("RUNE_EXPERIENCE_LEDGER", os.path.expanduser("~/.rune/state/process_experience.jsonl"))
)
return [r for r in ledger.load_all() if qualify_experience(r) == "CANDIDATE"]
+25 -12
View File
@@ -13,6 +13,7 @@ publication fails visibly, must not silently falsify the ledger). It does
NOT alter research cognition or stage outcomes — callers invoke it after
gate acceptance, and its failure does not invalidate the accepted artifact.
"""
from __future__ import annotations
import json
@@ -20,17 +21,24 @@ import os
import urllib.request
from source_ledger import build_records, render_markdown
import sys as _sys
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
MARKER_PREFIX = "<!-- source-ledger"
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
token = os.environ.get("GITEA_TOKEN", "")
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
req = urllib.request.Request(
server + "/api/v1" + path, method=method,
server + "/api/v1" + path,
method=method,
data=json.dumps(payload).encode() if payload else None,
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=60) as r:
body = r.read().decode()
@@ -43,8 +51,9 @@ def _marker(artifact_hash: str) -> str:
return f"{MARKER_PREFIX} hash={artifact_hash} -->"
def publish(issue: int, artifact_dir: str, artifact_hash: str,
runner: dict | None = None, model: str | None = None) -> dict:
def publish(
issue: int, artifact_dir: str, artifact_hash: str, runner: dict | None = None, model: str | None = None
) -> dict:
"""Render + post/update the ledger comment. Returns publication result."""
path = os.path.join(artifact_dir, "outward.json")
with open(path) as f:
@@ -58,9 +67,13 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
legacy = not any(
c.get("contradicts_source_ids")
for c in outward.get("contradictory_evidence", outward.get("contradictions", []))
if isinstance(c, dict))
body = _marker(artifact_hash) + "\n" + render_markdown(
records, outward, runner, model, artifact_hash, legacy_claims=legacy)
if isinstance(c, dict)
)
body = (
_marker(artifact_hash)
+ "\n"
+ render_markdown(records, outward, runner, model, artifact_hash, legacy_claims=legacy)
)
# Find existing ledger comment for this artifact hash (idempotency)
status, comments = _api("GET", f"/repos/conductor/rune/issues/{issue}/comments?limit=50")
@@ -73,14 +86,12 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
break
if existing:
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}",
{"body": body})
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}", {"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: update failed (HTTP {status})")
return {"action": "updated", "comment_id": existing["id"], "artifact_hash": artifact_hash}
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments",
{"body": body})
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments", {"body": body})
if status not in (200, 201):
raise RuntimeError(f"SourceLedger publish: create failed (HTTP {status}): {created}")
return {"action": "created", "comment_id": created.get("id"), "artifact_hash": artifact_hash}
@@ -88,6 +99,7 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Publish the OUTWARD Source Ledger")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--artifact-dir", required=True)
@@ -100,4 +112,5 @@ def main_with(argv: list[str]) -> int:
if __name__ == "__main__":
import sys
raise SystemExit(main_with(sys.argv))
+35 -10
View File
@@ -9,6 +9,7 @@ DEFINE does NOT: inspect code, search externally, or produce a design.
DEFINE does: classify the question, identify sub-questions, define the evidence
contract, and produce the required-stage plan.
"""
from __future__ import annotations
import json
@@ -17,15 +18,36 @@ from typing import Any
# Keywords that suggest external research is needed
EXTERNAL_INDICATORS = [
"best practices", "state of the art", "alternative", "compare",
"benchmark", "library", "framework", "prior art", "existing solutions",
"how do other", "state of the art", "literature", "approach",
"best practices",
"state of the art",
"alternative",
"compare",
"benchmark",
"library",
"framework",
"prior art",
"existing solutions",
"how do other",
"state of the art",
"literature",
"approach",
]
# Keywords that suggest repo-specific work
REPO_INDICATORS = [
"fix", "bug", "refactor", "add", "implement", "remove", "optimize",
"update", "migrate", "clean up", "wire", "integrate", "test",
"fix",
"bug",
"refactor",
"add",
"implement",
"remove",
"optimize",
"update",
"migrate",
"clean up",
"wire",
"integrate",
"test",
]
@@ -88,8 +110,7 @@ def compile_define_contract(issue: dict) -> dict:
required_stages.extend(["SYNTHESIS", "QUALIFY"])
# Skipped stages with reasons (canonical stage names only)
all_stages = {"ISSUE", "INWARD", "OUTWARD", "APPLICABILITY",
"ADVERSARIAL", "INNOVATION", "SYNTHESIS", "QUALIFY"}
all_stages = {"ISSUE", "INWARD", "OUTWARD", "APPLICABILITY", "ADVERSARIAL", "INNOVATION", "SYNTHESIS", "QUALIFY"}
skipped = {}
for s in sorted(all_stages - set(required_stages)):
if s == "OUTWARD":
@@ -144,6 +165,7 @@ def is_ambiguous(issue: dict) -> bool:
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Deterministic DEFINE stage")
parser.add_argument("--issue-file", required=True, help="Path to issue JSON")
parser.add_argument("--out", required=True, help="Output path for define.json")
@@ -163,12 +185,15 @@ def main_with(argv: list[str]) -> int:
with open(args.out, "w") as f:
json.dump(result, f, indent=2, default=str)
print(f"DEFINE contract: type={contract['question_type']}, "
f"stages={contract['required_stages']}, "
f"mimo_needed={ambiguous}")
print(
f"DEFINE contract: type={contract['question_type']}, "
f"stages={contract['required_stages']}, "
f"mimo_needed={ambiguous}"
)
return 0
if __name__ == "__main__":
import sys
sys.exit(main_with(sys.argv))
+52 -24
View File
@@ -8,6 +8,7 @@ Event types emitted:
StageStarted, PromptCompiled, QueryIssued, ToolStarted, ToolCompleted,
SourceDiscovered, SourceRead, EvidenceAccepted, ToolFailed, StageCompleted, StageFailed
"""
from __future__ import annotations
import json
@@ -21,8 +22,7 @@ from typing import Callable
class ResearchEventRelay:
"""Reads mimo JSON events in real-time and updates canonical state."""
def __init__(self, tree, events_path: str, mimo_cmd: list[str], err_path: str,
attempt=None):
def __init__(self, tree, events_path: str, mimo_cmd: list[str], err_path: str, attempt=None):
self.tree = tree
self.events_path = events_path
self.mimo_cmd = mimo_cmd
@@ -31,8 +31,12 @@ class ResearchEventRelay:
# marks tool windows; it never changes classification or control flow.
self.attempt = attempt
self.event_counts = {
"queries": 0, "sources_discovered": 0, "sources_read": 0,
"evidence_accepted": 0, "tool_failures": 0, "tool_calls": 0,
"queries": 0,
"sources_discovered": 0,
"sources_read": 0,
"evidence_accepted": 0,
"tool_failures": 0,
"tool_calls": 0,
}
# Real tool receipts (RP3 evidence chain: QUERY → SOURCE → READ → CLAIM)
self.receipts = []
@@ -62,31 +66,45 @@ class ResearchEventRelay:
url = tool_input["url"]
if "duckduckgo" in url or "search" in url:
self.event_counts["queries"] += 1
self.receipts.append({
"kind": "QUERY", "tool": tool, "url": url,
"ts": time.time(),
})
self.receipts.append(
{
"kind": "QUERY",
"tool": tool,
"url": url,
"ts": time.time(),
}
)
return "QueryIssued"
else:
self.event_counts["sources_read"] += 1
self.receipts.append({
"kind": "SOURCE_READ", "tool": tool, "url": url,
"ts": time.time(),
})
self.receipts.append(
{
"kind": "SOURCE_READ",
"tool": tool,
"url": url,
"ts": time.time(),
}
)
return "SourceRead"
self.receipts.append({
"kind": "TOOL", "tool": tool,
"input_keys": sorted(tool_input.keys()) if isinstance(tool_input, dict) else [],
"ts": time.time(),
})
self.receipts.append(
{
"kind": "TOOL",
"tool": tool,
"input_keys": sorted(tool_input.keys()) if isinstance(tool_input, dict) else [],
"ts": time.time(),
}
)
return "ToolCompleted"
elif status in ("error", "failed"):
self.event_counts["tool_failures"] += 1
self.receipts.append({
"kind": "TOOL_FAILED", "tool": tool,
"input_keys": sorted(tool_input.keys()) if isinstance(tool_input, dict) else [],
"ts": time.time(),
})
self.receipts.append(
{
"kind": "TOOL_FAILED",
"tool": tool,
"input_keys": sorted(tool_input.keys()) if isinstance(tool_input, dict) else [],
"ts": time.time(),
}
)
return "ToolFailed"
return None
@@ -166,8 +184,18 @@ def run_stage_with_relay(tree, out_dir: str) -> dict:
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
mimo_agent = os.environ.get("MIMO_AGENT", "build")
cmd = [mimo_bin, "run", "--agent", mimo_agent, "-m", mimo_model,
"--dangerously-skip-permissions", "--format", "json", prompt]
cmd = [
mimo_bin,
"run",
"--agent",
mimo_agent,
"-m",
mimo_model,
"--dangerously-skip-permissions",
"--format",
"json",
prompt,
]
relay = ResearchEventRelay(tree, events_path, cmd, err_path)
result = relay.run()
+23 -2
View File
@@ -10,6 +10,7 @@ Builds a bounded candidate context for MiMo INWARD inspection:
This is DETERMINISTIC — no MiMo needed. MiMo gets the candidate context
and only performs semantic inspection if the context requires it.
"""
from __future__ import annotations
import json
@@ -49,8 +50,28 @@ def build_context(issue: dict, define_contract: dict) -> dict:
def _extract_keywords(text: str) -> list[str]:
"""Extract meaningful keywords from the issue text."""
stop = {"the", "a", "an", "in", "for", "to", "of", "and", "or", "is", "are",
"when", "how", "what", "this", "that", "with", "on", "at", "from"}
stop = {
"the",
"a",
"an",
"in",
"for",
"to",
"of",
"and",
"or",
"is",
"are",
"when",
"how",
"what",
"this",
"that",
"with",
"on",
"at",
"from",
}
words = re.findall(r"[a-z_]{3,}", text.lower())
return [w for w in words if w not in stop][:20]
+3
View File
@@ -12,6 +12,7 @@ and compiles a task-specific prompt that answers:
What am I forbidden from doing yet?
What output must I return?
"""
from __future__ import annotations
import json
@@ -73,6 +74,7 @@ def compile_research_prompt(
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Compile a stage-scoped research prompt")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--round", type=int, default=1)
@@ -92,4 +94,5 @@ def main_with(argv: list[str]) -> int:
if __name__ == "__main__":
import sys
sys.exit(main_with(sys.argv))
File diff suppressed because it is too large. Load diff
+79 -50
View File
@@ -25,6 +25,7 @@ Stage transitions:
REFINE(unknowns) → targeted return to the earliest deficient stage
(no full-pipeline restart; INWARD is not rerun unless Rune reality changed)
"""
from __future__ import annotations
import json
@@ -34,8 +35,14 @@ from pathlib import Path
from typing import Any
STAGES = [
"ISSUE", "INWARD", "OUTWARD", "APPLICABILITY",
"INNOVATION", "ADVERSARIAL", "SYNTHESIS", "QUALIFY",
"ISSUE",
"INWARD",
"OUTWARD",
"APPLICABILITY",
"INNOVATION",
"ADVERSARIAL",
"SYNTHESIS",
"QUALIFY",
]
# Legacy stage names that must never become READY through the canonical path.
@@ -66,12 +73,15 @@ APPLICABILITY_CLASSES = {
# The default is SKIP — INNOVATION runs only when warranted.
APPLICABILITY_ROUTES = {
"STANDARD_SOLUTION_SUFFICIENT", # → ADVERSARIAL (INNOVATION skipped)
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
}
# Valid INNOVATION conclusions. NO_INNOVATION_WARRANTED is a SUCCESS result.
INNOVATION_CONCLUSIONS = {
"ADOPT", "ADAPT", "BORROW", "BUILD",
"ADOPT",
"ADAPT",
"BORROW",
"BUILD",
"NO_INNOVATION_WARRANTED",
}
@@ -80,12 +90,12 @@ INNOVATION_CONCLUSIONS = {
# Only SURVIVES* dispositions unlock SYNTHESIS. Everything else triggers
# targeted refinement at the earliest stage that can repair the defect.
ADVERSARIAL_DISPOSITIONS = {
"SURVIVES", # → SYNTHESIS READY
"SURVIVES_WITH_MITIGATIONS", # → SYNTHESIS READY (mitigations are required inputs)
"EVIDENCE_DEFICIENT", # → targeted OUTWARD → APPLICABILITY → ADVERSARIAL
"APPLICABILITY_DEFICIENT", # → APPLICABILITY → ADVERSARIAL
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
"NO_VIABLE_CANDIDATE", # → targeted OUTWARD (expand candidate space)
"SURVIVES", # → SYNTHESIS READY
"SURVIVES_WITH_MITIGATIONS", # → SYNTHESIS READY (mitigations are required inputs)
"EVIDENCE_DEFICIENT", # → targeted OUTWARD → APPLICABILITY → ADVERSARIAL
"APPLICABILITY_DEFICIENT", # → APPLICABILITY → ADVERSARIAL
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
"NO_VIABLE_CANDIDATE", # → targeted OUTWARD (expand candidate space)
}
SYNTHESIS_UNLOCKING_DISPOSITIONS = {"SURVIVES", "SURVIVES_WITH_MITIGATIONS"}
@@ -99,9 +109,9 @@ ADVERSARIAL_REFINEMENT_ROUTES = {
# QUALIFY outcomes (narrowed semantics, §8–10 of the correction).
QUALIFY_OUTCOMES = {
"PROCEED", # implementation-ready → MiMo Code
"REFINE_SYNTHESIS", # build instructions weak → SYNTHESIS only
"REOPEN_EARLIER_STAGE", # exceptional: genuinely new blocking fact
"PROCEED", # implementation-ready → MiMo Code
"REFINE_SYNTHESIS", # build instructions weak → SYNTHESIS only
"REOPEN_EARLIER_STAGE", # exceptional: genuinely new blocking fact
}
@@ -184,10 +194,14 @@ class ResearchTaskTree:
if stage not in self.state["completed_stages"] or stage != "QUALIFY":
self.state["completed_stages"].append(stage)
self.state["artifacts"][stage] = artifact
self.state["stage_history"].append({
"stage": stage, "round": self.round,
"completed_at": "auto", "artifact_keys": list(artifact.keys()),
})
self.state["stage_history"].append(
{
"stage": stage,
"round": self.round,
"completed_at": "auto",
"artifact_keys": list(artifact.keys()),
}
)
# Check if we're in a targeted branch
targeted = self.state.get("targeted_branch_stages", [])
if targeted and stage in targeted:
@@ -239,9 +253,7 @@ class ResearchTaskTree:
if disposition in SYNTHESIS_UNLOCKING_DISPOSITIONS:
ok, problems = self.synthesis_readiness(artifact)
if not ok:
raise ValueError(
"SYNTHESIS readiness invariant failed: " + "; ".join(problems)
)
raise ValueError("SYNTHESIS readiness invariant failed: " + "; ".join(problems))
return "SYNTHESIS"
return ADVERSARIAL_REFINEMENT_ROUTES[disposition]
@@ -258,9 +270,7 @@ class ResearchTaskTree:
"""
outcome = artifact.get("outcome", "PROCEED")
if outcome not in QUALIFY_OUTCOMES:
raise ValueError(
f"QUALIFY outcome must be one of {sorted(QUALIFY_OUTCOMES)}, got: {outcome!r}"
)
raise ValueError(f"QUALIFY outcome must be one of {sorted(QUALIFY_OUTCOMES)}, got: {outcome!r}")
self.state["qualify_outcome"] = outcome
if outcome == "PROCEED":
self.state["status"] = "COMPLETE"
@@ -268,13 +278,15 @@ class ResearchTaskTree:
if outcome == "REFINE_SYNTHESIS":
return "SYNTHESIS"
# REOPEN_EARLIER_STAGE: all four required fields must be present
required_fields = ("reason", "return_to", "new_blocking_fact",
"why_previous_gate_could_not_reasonably_have_caught_it")
required_fields = (
"reason",
"return_to",
"new_blocking_fact",
"why_previous_gate_could_not_reasonably_have_caught_it",
)
missing = [f for f in required_fields if not artifact.get(f)]
if missing:
raise ValueError(
f"REOPEN_EARLIER_STAGE requires {required_fields}; missing: {missing}"
)
raise ValueError(f"REOPEN_EARLIER_STAGE requires {required_fields}; missing: {missing}")
return_to = artifact["return_to"]
if return_to not in STAGES or return_to == "QUALIFY":
raise ValueError(f"REOPEN_EARLIER_STAGE return_to must be an epistemic stage, got: {return_to!r}")
@@ -298,11 +310,12 @@ class ResearchTaskTree:
if not survivors:
problems.append("no surviving candidate")
if disposition == "SURVIVES_WITH_MITIGATIONS" and not adversarial_artifact.get("required_mitigations"):
problems.append("SURVIVES_WITH_MITIGATIONS without required_mitigations — "
"mitigations are mandatory synthesis inputs, not optional prose")
problems.append(
"SURVIVES_WITH_MITIGATIONS without required_mitigations — "
"mitigations are mandatory synthesis inputs, not optional prose"
)
if self.state.get("blocking_unknowns"):
problems.append("blocking adversarial unknowns present: "
f"{len(self.state['blocking_unknowns'])}")
problems.append(f"blocking adversarial unknowns present: {len(self.state['blocking_unknowns'])}")
return (len(problems) == 0, problems)
def _route_after_applicability(self, artifact: dict) -> str:
@@ -320,17 +333,20 @@ class ResearchTaskTree:
route = artifact.get("route", "STANDARD_SOLUTION_SUFFICIENT")
if route not in ("STANDARD_SOLUTION_SUFFICIENT", "INNOVATION_WARRANTED"):
raise ValueError(
f"APPLICABILITY route must be STANDARD_SOLUTION_SUFFICIENT or "
f"INNOVATION_WARRANTED, got: {route!r}"
f"APPLICABILITY route must be STANDARD_SOLUTION_SUFFICIENT or INNOVATION_WARRANTED, got: {route!r}"
)
self.state["applicability_route"] = route
return "INNOVATION" if route == "INNOVATION_WARRANTED" else "ADVERSARIAL"
def fail(self, reason: str) -> None:
self.state["stage_history"].append({
"stage": self.state["current_stage"], "round": self.round,
"failed": True, "reason": reason,
})
self.state["stage_history"].append(
{
"stage": self.state["current_stage"],
"round": self.round,
"failed": True,
"reason": reason,
}
)
self.save()
def more_research(self, unknowns: list[dict]) -> list[dict]:
@@ -360,11 +376,13 @@ class ResearchTaskTree:
stages_needed = ["INNOVATION"]
else:
stages_needed = ["OUTWARD"]
branches.append({
"unknown": u,
"stages": stages_needed,
"round": self.round,
})
branches.append(
{
"unknown": u,
"stages": stages_needed,
"round": self.round,
}
)
all_stages.extend(stages_needed)
# Route to the earliest deficient stage in canonical order,
@@ -380,8 +398,7 @@ class ResearchTaskTree:
def get_prior_artifacts(self, for_stage: str) -> dict:
"""Return the accepted artifacts this stage needs."""
idx = STAGES.index(for_stage) if for_stage in STAGES else len(STAGES)
return {STAGES[i]: self.state["artifacts"].get(STAGES[i], {})
for i in range(idx)}
return {STAGES[i]: self.state["artifacts"].get(STAGES[i], {}) for i in range(idx)}
def capsule(self) -> dict:
"""The process capsule for MiMo invocation (§14)."""
@@ -394,11 +411,23 @@ class ResearchTaskTree:
"accepted_artifacts": {
# Adversarial-gated refinement (correction §5–6): mitigations
# and falsification tests MUST reach SYNTHESIS as required inputs.
k: {kk: v for kk, v in vv.items() if kk in (
"decision", "claims", "sources", "candidates", "disposition",
"required_mitigations", "falsification_tests",
"surviving_candidates", "rejected_candidates", "route",
)}
k: {
kk: v
for kk, v in vv.items()
if kk
in (
"decision",
"claims",
"sources",
"candidates",
"disposition",
"required_mitigations",
"falsification_tests",
"surviving_candidates",
"rejected_candidates",
"route",
)
}
for k, vv in self.state["artifacts"].items()
},
"blocking_unknowns": self.state.get("blocking_unknowns", []),
+24 -19
View File
@@ -14,6 +14,7 @@ This module captures ACTUAL runtime evidence — not configuration inference:
The evidence record travels with the stage result and is validated by the
placement gate BEFORE a stage result can be accepted.
"""
from __future__ import annotations
import os
@@ -30,14 +31,23 @@ DESKTOP_RUNNER_LABEL = "rune-ci-desktop"
# Stages that require desktop-heavy execution (cognition, research, coding).
DESKTOP_HEAVY_STAGES = {
"INWARD", "OUTWARD", "APPLICABILITY", "INNOVATION",
"ADVERSARIAL", "SYNTHESIS", "QUALIFY",
"INWARD",
"OUTWARD",
"APPLICABILITY",
"INNOVATION",
"ADVERSARIAL",
"SYNTHESIS",
"QUALIFY",
}
# Lightweight forge/control work that MAY legitimately run on the Pi.
PI_LIGHTWEIGHT_OPERATIONS = {
"issue_metadata", "label_change", "artifact_metadata",
"process_state", "eligibility_check", "state_transition",
"issue_metadata",
"label_change",
"artifact_metadata",
"process_state",
"eligibility_check",
"state_transition",
}
@@ -58,14 +68,8 @@ def capture_execution_evidence(
runner identity present in the environment.
"""
hostname = socket.gethostname()
runner_name = (
os.environ.get("RUNNER_NAME")
or os.environ.get("GITEA_RUNNER_NAME")
or ("local:" + hostname)
)
runner_labels = [
l.strip() for l in os.environ.get("RUNNER_LABELS", "").split(",") if l.strip()
] or [runner_name]
runner_name = os.environ.get("RUNNER_NAME") or os.environ.get("GITEA_RUNNER_NAME") or ("local:" + hostname)
runner_labels = [l.strip() for l in os.environ.get("RUNNER_LABELS", "").split(",") if l.strip()] or [runner_name]
return {
"runner_hostname": hostname,
@@ -92,13 +96,12 @@ def validate_placement(evidence: dict) -> tuple[bool, list[str]]:
klass = evidence.get("execution_class", "desktop-heavy")
# Evidence completeness — identity must be actually captured
for field in ("runner_hostname", "runner_name", "runner_labels",
"execution_class", "stage", "timestamp"):
for field in ("runner_hostname", "runner_name", "runner_labels", "execution_class", "stage", "timestamp"):
if not evidence.get(field):
violations.append(f"placement evidence missing field: {field}")
if klass == "desktop-heavy":
identity = (evidence.get("runner_name", "") + " " + evidence.get("runner_hostname", ""))
identity = evidence.get("runner_name", "") + " " + evidence.get("runner_hostname", "")
if _is_pi(identity) or any(_is_pi(l) for l in evidence.get("runner_labels", [])):
violations.append(
f"PLACEMENT VIOLATION: desktop-heavy stage '{stage}' executed on Pi "
@@ -109,10 +112,12 @@ def validate_placement(evidence: dict) -> tuple[bool, list[str]]:
# runner NAME (rune-desktop), or a verified local desktop execution.
# Pi rejection above already caught Pi markers — this is the allow-side.
labels = " ".join(evidence.get("runner_labels", []))
if (DESKTOP_RUNNER_LABEL not in identity
and "rune-desktop" not in identity
and DESKTOP_RUNNER_LABEL not in labels
and "local:" not in evidence.get("runner_name", "")):
if (
DESKTOP_RUNNER_LABEL not in identity
and "rune-desktop" not in identity
and DESKTOP_RUNNER_LABEL not in labels
and "local:" not in evidence.get("runner_name", "")
):
violations.append(
f"runner '{evidence.get('runner_name')}' is not '{DESKTOP_RUNNER_LABEL}' "
f"nor the registered rune-desktop runner, and not a verified local "
+42 -26
View File
@@ -21,6 +21,7 @@ Hard invariants:
must not silently falsify the ledger.
- No trust scores. Factual source classification only.
"""
from __future__ import annotations
import hashlib
@@ -70,8 +71,7 @@ def build_records(outward: dict, stage: str = "OUTWARD") -> list[dict]:
url = _norm(r.get("url", ""))
if not url:
continue
rid = _receipt_id(stage, r.get("tool", "?"), r.get("url", ""),
r.get("ts", 0), i)
rid = _receipt_id(stage, r.get("tool", "?"), r.get("url", ""), r.get("ts", 0), i)
if r.get("kind") == "SOURCE_READ":
read_urls[url] = {"receipt_id": rid, "tool": r.get("tool", "?"), "ts": r.get("ts")}
elif r.get("kind") == "TOOL_FAILED":
@@ -93,7 +93,7 @@ def build_records(outward: dict, stage: str = "OUTWARD") -> list[dict]:
for i, c in enumerate(outward.get("contradictory_evidence", outward.get("contradictions", []))):
if isinstance(c, dict):
for sid in c.get("contradicts_source_ids", []):
contradictions_by_source.setdefault(sid, []).append(c.get("id", f"C{i+1}"))
contradictions_by_source.setdefault(sid, []).append(c.get("id", f"C{i + 1}"))
# plain-string contradictions are not linked (labeled in render)
records = []
@@ -119,28 +119,41 @@ def build_records(outward: dict, stage: str = "OUTWARD") -> list[dict]:
# Upgrade precedence fix is above; this block is intentionally after
# status resolution so the downgrade applies regardless of note text.
resolved = s.get("resolved_url", url) # explicit field if present
records.append({
"receipt_id": (receipt or {}).get("receipt_id", ""),
"source_id": sid,
"requested_url": url,
"resolved_url": resolved,
"domain": _domain(resolved),
"status": status,
"tool": (receipt or {}).get("tool", s.get("tool", "UNKNOWN_NOT_EXPOSED")),
"timestamp": (receipt or {}).get("ts", "UNKNOWN_NOT_EXPOSED"),
"source_type": s.get("class", s.get("source_class", "UNKNOWN_NOT_EXPOSED")),
"claim_ids": claims_by_source.get(sid, []),
"contradiction_ids": contradictions_by_source.get(sid, []),
"blocked_reason": (s.get("note", "") if status == STATUS_BLOCKED else
(receipt.get("kind") if status == STATUS_FAILED and receipt else
("no machine SOURCE_READ receipt matched (legacy artifact)" if status == STATUS_FAILED else ""))),
"supports_evidence": status == STATUS_READ,
})
records.append(
{
"receipt_id": (receipt or {}).get("receipt_id", ""),
"source_id": sid,
"requested_url": url,
"resolved_url": resolved,
"domain": _domain(resolved),
"status": status,
"tool": (receipt or {}).get("tool", s.get("tool", "UNKNOWN_NOT_EXPOSED")),
"timestamp": (receipt or {}).get("ts", "UNKNOWN_NOT_EXPOSED"),
"source_type": s.get("class", s.get("source_class", "UNKNOWN_NOT_EXPOSED")),
"claim_ids": claims_by_source.get(sid, []),
"contradiction_ids": contradictions_by_source.get(sid, []),
"blocked_reason": (
s.get("note", "")
if status == STATUS_BLOCKED
else (
receipt.get("kind")
if status == STATUS_FAILED and receipt
else (
"no machine SOURCE_READ receipt matched (legacy artifact)"
if status == STATUS_FAILED
else ""
)
)
),
"supports_evidence": status == STATUS_READ,
}
)
return records
def render_markdown(records: list[dict], outward: dict, runner: dict | None,
model: str, artifact_hash: str, legacy_claims: bool = False) -> str:
def render_markdown(
records: list[dict], outward: dict, runner: dict | None, model: str, artifact_hash: str, legacy_claims: bool = False
) -> str:
"""Fixed Markdown template. Pure function of records + metadata."""
d = outward.get("discovery", {})
n_read = sum(1 for r in records if r["status"] == STATUS_READ)
@@ -152,9 +165,11 @@ def render_markdown(records: list[dict], outward: dict, runner: dict | None,
runner_host = (runner or {}).get("runner_hostname", "UNKNOWN")
prov_note = ""
if legacy_claims:
prov_note = ("\n*Claim↔source links for this legacy artifact are derived from "
"claim nesting (pre-#282 format); future artifacts carry explicit "
"supports_source_ids/contradicts_source_ids.*")
prov_note = (
"\n*Claim↔source links for this legacy artifact are derived from "
"claim nesting (pre-#282 format); future artifacts carry explicit "
"supports_source_ids/contradicts_source_ids.*"
)
lines = [
"## OUTWARD — ACCEPTED",
@@ -179,6 +194,7 @@ def render_markdown(records: list[dict], outward: dict, runner: dict | None,
link = f"[{r['domain']}]({r['requested_url']})" if r["requested_url"].startswith("http") else r["domain"]
lines.append(
f"| {r['source_id']} | {link} | {r['source_type']} | {r['status']} "
f"| {len(r['claim_ids'])} | {len(r['contradiction_ids'])} | {caveat[:60]} |")
f"| {len(r['claim_ids'])} | {len(r['contradiction_ids'])} | {caveat[:60]} |"
)
lines.append(prov_note)
return "\n".join(lines)
+93 -49
View File
@@ -13,68 +13,102 @@ contract, the contract-drift test fails.
Compatibility aliases (e.g. urls → url, classification → source_class)
are explicit, versioned, and bounded — not arbitrary rescue behavior.
"""
from __future__ import annotations
# Per-stage contract: required fields in the structured artifact
STAGE_CONTRACTS: dict[str, dict] = {
"INWARD": {
"required_fields": [
{"name": "claims", "type": "list",
"description": "relevant files/behaviors with evidence citations"},
{"name": "claims", "type": "list", "description": "relevant files/behaviors with evidence citations"},
],
"optional_fields": ["context", "deterministic"],
},
"OUTWARD": {
"required_fields": [
{"name": "discovery", "type": "object",
"description": "queries, sources_discovered, source_count"},
{"name": "reading", "type": "object",
"description": "sources_selected, sources_read (with machine receipts), evidence_extracted"},
{"name": "tool_receipts", "type": "list",
"description": "per-event receipts with kind/tool/url/timestamp"},
{"name": "coverage", "type": "object",
"description": "required_classes, represented_classes, uncovered"},
{"name": "discovery", "type": "object", "description": "queries, sources_discovered, source_count"},
{
"name": "reading",
"type": "object",
"description": "sources_selected, sources_read (with machine receipts), evidence_extracted",
},
{"name": "tool_receipts", "type": "list", "description": "per-event receipts with kind/tool/url/timestamp"},
{"name": "coverage", "type": "object", "description": "required_classes, represented_classes, uncovered"},
],
"optional_fields": [
"text",
"limitations",
"contradictory_evidence",
"supporting_evidence",
"alternatives",
"adjacent_field_leads",
"unresolved_questions",
],
"optional_fields": ["text", "limitations", "contradictory_evidence",
"supporting_evidence", "alternatives",
"adjacent_field_leads", "unresolved_questions"],
},
"APPLICABILITY": {
"required_fields": [
{"name": "route", "type": "string",
"description": "STANDARD_SOLUTION_SUFFICIENT or INNOVATION_WARRANTED",
"enum": ["STANDARD_SOLUTION_SUFFICIENT", "INNOVATION_WARRANTED"]},
{"name": "candidates", "type": "list",
"description": "classified mechanisms with integration_point, evidence, source_ids"},
{
"name": "route",
"type": "string",
"description": "STANDARD_SOLUTION_SUFFICIENT or INNOVATION_WARRANTED",
"enum": ["STANDARD_SOLUTION_SUFFICIENT", "INNOVATION_WARRANTED"],
},
{
"name": "candidates",
"type": "list",
"description": "classified mechanisms with integration_point, evidence, source_ids",
},
],
"optional_fields": ["notes"],
},
"ADVERSARIAL": {
"required_fields": [
{"name": "disposition", "type": "string",
"description": "SURVIVES | SURVIVES_WITH_MITIGATIONS | EVIDENCE_DEFICIENT | "
"APPLICABILITY_DEFICIENT | INNOVATION_WARRANTED | NO_VIABLE_CANDIDATE",
"enum": ["SURVIVES", "SURVIVES_WITH_MITIGATIONS", "EVIDENCE_DEFICIENT",
"APPLICABILITY_DEFICIENT", "INNOVATION_WARRANTED", "NO_VIABLE_CANDIDATE"]},
{"name": "surviving_candidates", "type": "list",
"description": "candidate ids that survived attack"},
{"name": "attacks", "type": "list",
"description": "attack rows with target_candidate, dimension, finding, severity"},
{"name": "required_mitigations", "type": "list",
"description": "mitigations that must appear in SYNTHESIS (mandatory, not prose)"},
{"name": "falsification_tests", "type": "list",
"description": "kill-tests that must convert into invariants"},
{
"name": "disposition",
"type": "string",
"description": "SURVIVES | SURVIVES_WITH_MITIGATIONS | EVIDENCE_DEFICIENT | "
"APPLICABILITY_DEFICIENT | INNOVATION_WARRANTED | NO_VIABLE_CANDIDATE",
"enum": [
"SURVIVES",
"SURVIVES_WITH_MITIGATIONS",
"EVIDENCE_DEFICIENT",
"APPLICABILITY_DEFICIENT",
"INNOVATION_WARRANTED",
"NO_VIABLE_CANDIDATE",
],
},
{"name": "surviving_candidates", "type": "list", "description": "candidate ids that survived attack"},
{
"name": "attacks",
"type": "list",
"description": "attack rows with target_candidate, dimension, finding, severity",
},
{
"name": "required_mitigations",
"type": "list",
"description": "mitigations that must appear in SYNTHESIS (mandatory, not prose)",
},
{
"name": "falsification_tests",
"type": "list",
"description": "kill-tests that must convert into invariants",
},
],
"optional_fields": ["rejected_candidates", "critical_assumptions",
"new_research_gaps"],
"optional_fields": ["rejected_candidates", "critical_assumptions", "new_research_gaps"],
},
"INNOVATION": {
"required_fields": [
{"name": "conclusion", "type": "string",
"description": "ADOPT | ADAPT | BORROW | BUILD | NO_INNOVATION_WARRANTED",
"enum": ["ADOPT", "ADAPT", "BORROW", "BUILD", "NO_INNOVATION_WARRANTED"]},
{"name": "candidates", "type": "list",
"description": "each with derived_from (mandatory), kill_test (mandatory)"},
{
"name": "conclusion",
"type": "string",
"description": "ADOPT | ADAPT | BORROW | BUILD | NO_INNOVATION_WARRANTED",
"enum": ["ADOPT", "ADAPT", "BORROW", "BUILD", "NO_INNOVATION_WARRANTED"],
},
{
"name": "candidates",
"type": "list",
"description": "each with derived_from (mandatory), kill_test (mandatory)",
},
],
"optional_fields": ["notes"],
},
@@ -83,23 +117,33 @@ STAGE_CONTRACTS: dict[str, dict] = {
{"name": "selected_design", "type": "string"},
{"name": "implementation_sequence", "type": "list"},
{"name": "interfaces", "type": "list"},
{"name": "invariants", "type": "list",
"description": "every falsification test must be converted into an invariant"},
{
"name": "invariants",
"type": "list",
"description": "every falsification test must be converted into an invariant",
},
{"name": "tests_required", "type": "list"},
{"name": "acceptance_criteria", "type": "list"},
{"name": "rollback", "type": "string"},
{"name": "data_flow", "type": "list",
"description": "how data moves through the designed system"},
{"name": "data_flow", "type": "list", "description": "how data moves through the designed system"},
],
"optional_fields": [
"why_this_design_won",
"rejected_alternatives",
"components_affected",
"authority_boundaries",
"failure_modes",
"deliberately_excluded",
],
"optional_fields": ["why_this_design_won", "rejected_alternatives",
"components_affected", "authority_boundaries",
"failure_modes", "deliberately_excluded"],
},
"QUALIFY": {
"required_fields": [
{"name": "outcome", "type": "string",
"description": "PROCEED | REFINE_SYNTHESIS | REOPEN_EARLIER_STAGE",
"enum": ["PROCEED", "REFINE_SYNTHESIS", "REOPEN_EARLIER_STAGE"]},
{
"name": "outcome",
"type": "string",
"description": "PROCEED | REFINE_SYNTHESIS | REOPEN_EARLIER_STAGE",
"enum": ["PROCEED", "REFINE_SYNTHESIS", "REOPEN_EARLIER_STAGE"],
},
],
"optional_fields": ["findings", "deterministic_checks", "reopen"],
},
@@ -109,7 +153,7 @@ STAGE_CONTRACTS: dict[str, dict] = {
# (bounded, versioned, NOT arbitrary rescue behavior).
COMPATIBILITY_ALIASES: dict[str, dict[str, str]] = {
"OUTWARD": {
"urls": "url", # worker uses urls (list) → take first as url
"urls": "url", # worker uses urls (list) → take first as url
"classification": "source_class",
"search_receipts": "queries",
"read_receipts": "sources_read",
+5 -5
View File
@@ -27,6 +27,7 @@ Attempt lifecycle (from the executor's perspective):
TIMEOUT | INTERRUPTED
persist(handle) → append + mirror
"""
from __future__ import annotations
import json
@@ -45,8 +46,7 @@ def _now_iso() -> str:
class StageAttempt:
"""One attempt's observed lifecycle. Pure data — no control flow."""
def __init__(self, attempt_id: str, event_id: str, stage: str,
issue: int, process_id: str):
def __init__(self, attempt_id: str, event_id: str, stage: str, issue: int, process_id: str):
self.attempt_id = attempt_id
self.event_id = event_id
self.stage = stage
@@ -121,8 +121,7 @@ class StageAttempt:
self.terminal_status = terminal_status
self.finished_at = _now_iso()
# Close any unclosed windows (INTERRUPTED/TIMEOUT paths)
for lst in (self.model_windows, self.tool_windows,
self.gate_windows, self.persistence_windows):
for lst in (self.model_windows, self.tool_windows, self.gate_windows, self.persistence_windows):
if lst and lst[-1][1] == 0.0:
s, _ = lst[-1]
lst[-1] = (s, time.monotonic())
@@ -160,7 +159,8 @@ class StageAttempt:
# not double-counted work.
"overlap_note": (
"categories are observed windows and may overlap; "
"wall_time is authoritative, category sums are not additive"),
"wall_time is authoritative, category sums are not additive"
),
"model_calls": self.model_calls,
"tool_calls": self.tool_calls,
"repo_read_calls": self.repo_read_calls,
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""tui_startup_canary.py — REAL TUI initialization canary, two proof modes.
Mode CANDIDATE (pre-promotion, default when --stage is given):
Tests the STAGED candidate exactly as supplied. Nothing from
/mnt/workdrive/rune-code/current is prepended or preferred — sys.path is
scrubbed of any current-deployment entry so a broken candidate cannot be
masked by the healthy previous deployment. If the staged candidate is
broken while current is healthy, this canary MUST fail.
Mode CURRENT (post-promotion consuming-path proof, --mode current):
Tests /mnt/workdrive/rune-code/current/src — proving the promoted
artifact is what the real consuming path resolves to.
Both modes run the ACTUAL RuneApp initialization through Textual's own
test driver — the same app construction + on_mount wiring the interactive
TUI executes — then exit 0. The interactive loop is never entered.
Prints the build identity (mode + identity) on success. Exit non-zero on
any initialization failure.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
CURRENT_SRC = "/mnt/workdrive/rune-code/current/src"
def _scrub_current_from_path() -> None:
"""Remove every current-deployment entry from sys.path (candidate mode).
The runner's checkout workspace or ambient PYTHONPATH may contain the
current deployment; a candidate canary must not silently resolve rune
from it.
"""
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.abspath(CURRENT_SRC)]
# Also drop any already-imported rune modules from a prior import.
for mod in [m for m in sys.modules if m == "rune" or m.startswith("rune.")]:
del sys.modules[mod]
async def _canary() -> dict:
from textual.widgets import Static # noqa: F401 — imports must resolve
from rune.rune_tui import RuneApp
app = RuneApp()
async with app.run_test(size=(120, 40)) as pilot:
# on_mount has run; the channels/status bar are wired.
assert app.task_channel is not None, "task channel not initialized"
assert app.chat_channel is not None, "chat channel not initialized"
assert app.status_bar is not None, "status bar not initialized"
# One interaction cycle: let pending messages/refreshes settle.
await pilot.pause(0.1)
import rune
return rune.build_info()
def main() -> int:
ap = argparse.ArgumentParser(description="Rune TUI startup canary")
ap.add_argument(
"--mode",
choices=("candidate", "current"),
default="candidate",
help="candidate: test the staged tree (sys.path scrubbed of current); current: test the consuming path",
)
ap.add_argument("--stage", help="staged candidate root (candidate mode)")
args = ap.parse_args()
if args.mode == "candidate":
if not args.stage:
print("candidate mode requires --stage", file=sys.stderr)
return 2
stage_root = Path(args.stage).resolve()
stage_src = str(stage_root / "src")
if not (stage_root / "src" / "rune").is_dir():
print(f"staged candidate has no src/rune: {stage_src}", file=sys.stderr)
return 2
_scrub_current_from_path()
sys.path.insert(0, stage_src)
# Verify the scrub worked: rune must resolve from the STAGE, not current.
import rune # noqa: E402
resolved = getattr(rune, "__file__", "")
if "rune-code/current" in resolved:
print(
f"CANARY CONTAMINATED: rune imported from current, not the staged candidate ({resolved})",
file=sys.stderr,
)
return 2
else:
if CURRENT_SRC not in sys.path:
sys.path.insert(0, CURRENT_SRC)
async def _run() -> dict:
try:
return await _canary()
finally:
pass
try:
info = asyncio.run(_run())
except Exception as exc: # noqa: BLE001 — the canary's JOB is to catch any startup failure and report it as a failed gate; narrowing here would let an unexpected exception class crash the runner instead of failing the gate cleanly.
print(f"TUI STARTUP CANARY FAILED ({args.mode}): {type(exc).__name__}: {exc}", file=sys.stderr)
return 1
print(json.dumps({"mode": args.mode, **info}))
return 0
if __name__ == "__main__":
sys.exit(main())
+251 -1
View File
@@ -81,6 +81,19 @@ def subjects_since_tag(tag: str, repo: Path | None = None) -> list[str]:
return out.splitlines() if out else []
def _version_lt(a: str, b: str) -> bool:
"""True when semver string a < b. Numeric per segment — never lexicographic.
T15 Part F: '0.66.9' < '0.66.10' is False lexicographically ('9' > '1'),
which made a legitimately-needed patch bump read as the
version_ahead_of_target anomaly and silently skip the release
(observed live on run 6781, 2026-09-11 19:59).
"""
pa = [int(x) for x in a.split(".")]
pb = [int(x) for x in b.split(".")]
return pa < pb
def classify_subject(subject: str) -> str:
"""Classify one commit subject: ``breaking`` | ``feat`` | ``fix`` | ``other``.
@@ -114,6 +127,18 @@ def highest_warranted_bump(subjects: list[str]) -> str | None:
return None
def is_release_commit(subject: str) -> bool:
"""True when the commit is a release/version bump commit.
ONE SOURCE OF TRUTH consumed by auto-version.yml (push convergence
guard) and by the recursion regression test: a release commit must
NEVER trigger another version bump (recursive-loop defect observed
2026-09-11: five 0.66.2 bumps before the guard landed).
"""
s = subject.strip().lower()
return s.startswith("chore(release)") or s.startswith("chore: release")
def next_version(version: str, kind: str) -> str:
"""Apply a bump kind to a version string (0.x convention: breaking -> minor)."""
major, minor, patch = version.split(".")
@@ -187,11 +212,236 @@ def decision_in_repo(repo: Path | None = None) -> dict[str, Any]:
return assemble_release_decision(version, tag, subjects, len(subjects))
def last_release_boundary(repo: Path | None = None) -> str | None:
"""Find the last chore(release) commit SHA (the release boundary).
This is the correct convergence boundary — NOT the git tag (tags may
lag behind releases due to D7). Falls back to the latest tag if no
release commits exist.
T15 Part F: matches SUBJECTS only (--grep matches the commit BODY by
default; a fix commit whose body mentions 'chore(release)' was
misdetected as the boundary, swallowing its own warranted bump).
"""
try:
result = subprocess.run(
["git", "log", "--format=%H %s", "-1", "--grep=^chore\\(release\\)", "--extended-regexp"],
capture_output=True,
text=True,
cwd=str(repo) if repo else None,
check=True,
)
for line in result.stdout.splitlines():
line = line.strip()
if not line:
continue
sha, _, subject = line.partition(" ")
if is_release_commit(subject):
return sha
except (subprocess.CalledProcessError, IndexError):
pass
return latest_tag(repo) # fallback to tag
def release_convergence(
repo: Path | None = None,
*,
_version: str | None = None,
_boundary_subject: str | None = None,
_subjects: list[str] | None = None,
) -> dict[str, Any]:
"""Correct release convergence: compare current source version to the
warranted TARGET version computed from commits since the last release
boundary (the last chore(release) commit, NOT the tag — tags may lag
due to D7).
Invariant:
current == target → converged, no commit
current < target → create exactly one bump commit
current > target → anomaly
"""
current_version = _version or read_version(repo)
if not current_version:
return {
"converged": True,
"warranted_bump": None,
"anomaly": None,
"commits_since_tag": 0,
"sample": [],
"reason": "no readable version",
}
subjects: list[str] = []
boundary = None
if _subjects is not None:
# Test injection: skip git entirely
substantive = [s for s in _subjects if not is_release_commit(s)]
warranted = highest_warranted_bump(substantive)
if not warranted:
return {
"converged": True,
"warranted_bump": None,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": current_version,
"reason": "no release-worthy commits since last boundary",
"boundary_sha": "",
}
target_version = next_version(current_version, warranted)
if current_version == target_version:
return {
"converged": True,
"warranted_bump": warranted,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": "current version already equals warranted target",
"boundary_sha": "",
}
elif _version_lt(current_version, target_version):
return {
"converged": False,
"warranted_bump": warranted,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": f"bump required: {current_version} → {target_version}",
"boundary_sha": "",
}
else:
return {
"converged": False,
"warranted_bump": None,
"anomaly": "version_ahead_of_target",
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": f"ANOMALY: current {current_version} > target {target_version}",
"boundary_sha": "",
}
if _boundary_subject:
result = subprocess.run(
["git", "log", "--format=%s", f"{boundary}..HEAD"],
capture_output=True,
text=True,
cwd=str(repo) if repo else None,
check=True,
)
subjects = [s for s in result.stdout.splitlines() if s.strip()]
else:
# Production path: converge over commits since the last release
# boundary. T15 Part F root cause: `boundary` was initialized to
# None and never assigned — every run classified ALL commits as
# unreleased (959+), so the warranted bump never converged and
# every queued push wrote its own bump commit (5× 0.66.9 observed
# 2026-09-11 20:21). Assign the boundary from the one source of
# truth and range the log over it.
boundary = last_release_boundary(repo)
if boundary:
result = subprocess.run(
["git", "log", "--format=%s", f"{boundary}..HEAD"],
capture_output=True,
text=True,
cwd=str(repo) if repo else None,
check=True,
)
else:
result = subprocess.run(
["git", "log", "--format=%s"],
capture_output=True,
text=True,
cwd=str(repo) if repo else None,
check=True,
)
subjects = [s for s in result.stdout.splitlines() if s.strip()]
# Exclude chore(release) commits from bump classification (they ARE releases)
substantive = [s for s in subjects if not is_release_commit(s)]
warranted = highest_warranted_bump(substantive)
# Expose the boundary in the decision JSON: consumers (release step,
# debug sessions) otherwise re-derive it from git history.
decision_boundary = {"boundary_sha": (boundary or "")[:12]}
if not warranted:
return {
"converged": True,
"warranted_bump": None,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": current_version,
"reason": "no release-worthy commits since last boundary",
**decision_boundary,
}
target_version = next_version(current_version, warranted)
if current_version == target_version:
return {
"converged": True,
"warranted_bump": warranted,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": "current version already equals warranted target",
**decision_boundary,
}
elif _version_lt(current_version, target_version):
# Bump needed — but only if not already converged by a prior bump
# in this same cascade (the target is the SAME for all queued runs)
return {
"converged": False,
"warranted_bump": warranted,
"anomaly": None,
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": f"bump required: {current_version} → {target_version}",
**decision_boundary,
}
else:
return {
"converged": False,
"warranted_bump": None,
"anomaly": "version_ahead_of_target",
"commits_since_boundary": len(substantive),
"sample": substantive[:5],
"current_version": current_version,
"target_version": target_version,
"reason": f"ANOMALY: current {current_version} > target {target_version}",
**decision_boundary,
}
def main(argv: list[str] | None = None) -> dict[str, Any]:
parser = argparse.ArgumentParser(description="Release convergence decision (JSON on stdout)")
parser.add_argument(
"--is-release-subject",
dest="is_release_subject",
metavar="SUBJECT",
help="exit 0 when SUBJECT is a release bump commit, exit 1 otherwise (loop guard); no JSON",
)
parser.add_argument("--repo", type=Path, default=None, help="repo root (default: cwd)")
args = parser.parse_args(argv)
decision = decision_in_repo(args.repo)
if args.is_release_subject is not None:
import sys as _sys
_sys.exit(0 if is_release_commit(args.is_release_subject) else 1)
decision = release_convergence(args.repo)
# Backward compat: old consumers expect commits_since_tag
if "commits_since_tag" not in decision:
decision["commits_since_tag"] = decision.get("commits_since_boundary", 0)
print(json.dumps(decision))
return decision
+35 -1
View File
@@ -3,4 +3,38 @@
This is the main entry point for the Rune program.
"""
__version__ = "0.66.0"
import json
from pathlib import Path
__version__ = "0.66.14"
# 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
# qualified SHA) is written by the deployment job into _build_identity.json
# next to the package. A dev checkout has no identity file — build_info()
# reports "dev" honestly rather than guessing.
_BUILD_IDENTITY_FILE = Path(__file__).with_name("_build_identity.json")
def build_info() -> dict:
"""Build identity of the RUNNING code: version + exact commit SHA.
Returns:
{"version": ..., "commit": ..., "deployed_at": ..., "source":
"deployed" | "dev"}
"deployed" = this tree was staged by the deployment pipeline from a
CI-qualified master revision. "dev" = a development checkout (no
identity file) — the SHA is unknown at runtime, reported honestly.
"""
try:
data = json.loads(_BUILD_IDENTITY_FILE.read_text())
return {
"version": data.get("version", __version__),
"commit": data.get("commit", "unknown"),
"deployed_at": data.get("deployed_at", ""),
"source": "deployed",
}
except (OSError, ValueError):
return {"version": __version__, "commit": "dev", "deployed_at": "", "source": "dev"}
+57 -42
View File
@@ -50,6 +50,7 @@ def _now():
def connect():
"""Open/create the abstraction database."""
import sqlite3
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
db = sqlite3.connect(DB_PATH)
try:
@@ -178,22 +179,24 @@ def llm_call(prompt, model=None, max_tokens=500):
model = model or LOCAL_MODEL
try:
import urllib.request
data = json.dumps({
"model": model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192}
}).encode()
data = json.dumps(
{
"model": model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192},
}
).encode()
req = urllib.request.Request(
f"{OLLAMA_URL}/api/generate",
data=data,
headers={"Content-Type": "application/json"}
f"{OLLAMA_URL}/api/generate", data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read())
return result.get("response", "").strip()
except Exception as e:
import logging
logging.getLogger("abstraction").warning("LLM call failed: %s", e)
return ""
@@ -263,8 +266,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
# Check for duplicates — hash-based (O(1) lookup instead of LIKE scan)
content_hash = hashlib.sha256(what[:80].lower().encode()).hexdigest()[:16]
existing = db.execute(
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1",
(content_hash,)
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1", (content_hash,)
).fetchone()
if existing:
continue # NOOP — already exists
@@ -273,12 +275,20 @@ Extract 1-5 key facts. Be precise, not verbose."""
"""INSERT INTO facts (episode_id, what, content_hash, when_text, where_text, who_text, why_text,
fact_type, entities_json, confidence, source, state, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)""",
(episode_id or "", what, content_hash,
fact.get("when", ""), fact.get("where", ""),
fact.get("who", ""), fact.get("why", ""),
fact.get("type", "world"),
json.dumps(fact.get("entities", [])),
0.7, source, ts)
(
episode_id or "",
what,
content_hash,
fact.get("when", ""),
fact.get("where", ""),
fact.get("who", ""),
fact.get("why", ""),
fact.get("type", "world"),
json.dumps(fact.get("entities", [])),
0.7,
source,
ts,
),
)
fact_id = cur.lastrowid
extracted += 1
@@ -291,16 +301,15 @@ Extract 1-5 key facts. Be precise, not verbose."""
# Upsert entity
db.execute(
"INSERT OR IGNORE INTO entities (name, entity_type, created_at) VALUES (?, 'concept', ?)",
(entity_name, ts)
(entity_name, ts),
)
entity_row = db.execute("SELECT id FROM entities WHERE name=?", (entity_name,)).fetchone()
if entity_row:
db.execute(
"INSERT OR IGNORE INTO fact_entities (fact_id, entity_id) VALUES (?, ?)",
(fact_id, entity_row["id"])
(fact_id, entity_row["id"]),
)
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?",
(entity_row["id"],))
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?", (entity_row["id"],))
# Triple extraction (HippoRAG 2 pattern)
who = fact.get("who", "").strip()
@@ -312,7 +321,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
obj = " ".join(words[3:])
db.execute(
"INSERT INTO triples (subject, predicate, object, source_fact_id, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(who, predicate, obj, fact_id, 0.6, ts)
(who, predicate, obj, fact_id, 0.6, ts),
)
db.commit()
@@ -335,17 +344,24 @@ def synthesize_observations(entity_name):
entity_id = entity["id"]
# Get all facts about this entity
facts = db.execute("""
facts = db.execute(
"""
SELECT f.id, f.what, f.fact_type, f.confidence
FROM facts f
JOIN fact_entities fe ON f.id = fe.fact_id
WHERE fe.entity_id = ? AND f.state = 'active'
ORDER BY f.created_at DESC
""", (entity_id,)).fetchall()
""",
(entity_id,),
).fetchall()
if len(facts) < 3:
return {"entity": entity_name, "facts": len(facts), "observations": 0,
"reason": "Need at least 3 facts to synthesize"}
return {
"entity": entity_name,
"facts": len(facts),
"observations": 0,
"reason": "Need at least 3 facts to synthesize",
}
facts_text = "\n".join(f"- [{f['fact_type']}] {f['what']}" for f in facts[:20])
@@ -377,12 +393,11 @@ Respond as JSON array:
db.execute(
"""INSERT INTO observations (entity_id, observation, source_fact_ids, confidence, state, created_at)
VALUES (?, ?, ?, ?, 'active', ?)""",
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts)
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts),
)
created += 1
db.execute("UPDATE entities SET observation_count = ? WHERE id=?",
(created, entity_id))
db.execute("UPDATE entities SET observation_count = ? WHERE id=?", (created, entity_id))
db.commit()
return {"entity": entity_name, "facts": len(facts), "observations": created}
finally:
@@ -394,8 +409,7 @@ def list_beliefs(limit=20):
db = connect()
try:
rows = db.execute(
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?",
(limit,)
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
).fetchall()
return [dict(r) for r in rows]
finally:
@@ -407,8 +421,7 @@ def list_rules(limit=20):
db = connect()
try:
rows = db.execute(
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?",
(limit,)
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
).fetchall()
return [dict(r) for r in rows]
finally:
@@ -420,8 +433,7 @@ def list_skills(limit=20):
db = connect()
try:
rows = db.execute(
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?",
(limit,)
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?", (limit,)
).fetchall()
return [dict(r) for r in rows]
finally:
@@ -486,8 +498,9 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser(description="Abstraction pipeline")
ap.add_argument("cmd", choices=["extract", "observe", "beliefs", "rules", "skills",
"evolve", "lifecycle", "stats", "hook"])
ap.add_argument(
"cmd", choices=["extract", "observe", "beliefs", "rules", "skills", "evolve", "lifecycle", "stats", "hook"]
)
ap.add_argument("arg", nargs="?", default="")
ap.add_argument("--episode-id", default=None)
ap.add_argument("--source", default="session")
@@ -501,16 +514,18 @@ def main():
print("usage: abstraction.py extract '<episode text>'")
return
result = extract_facts(args.arg, episode_id=args.episode_id, source=args.source)
print(json.dumps(result, indent=2) if args.json else
f"Extracted {result.get('extracted', 0)} facts")
print(json.dumps(result, indent=2) if args.json else f"Extracted {result.get('extracted', 0)} facts")
elif args.cmd == "observe":
if not args.arg:
print("usage: abstraction.py observe '<entity name>'")
return
result = synthesize_observations(args.arg)
print(json.dumps(result, indent=2) if args.json else
f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts")
print(
json.dumps(result, indent=2)
if args.json
else f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts"
)
elif args.cmd == "beliefs":
items = list_beliefs()
@@ -568,5 +583,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["abstraction"],
"priority": 50,
"auto_activate": True
"auto_activate": True,
}
@@ -44,32 +44,19 @@ class FloorViolation(Exception):
"""A downgrade was attempted on a route protected by the research floor."""
# Flag detection: resolved relative to THIS file (legacy tests load this
# module standalone via importlib, with no package context on sys.path).
def _load_detect_flags():
try:
from research_risk_flags import detect_flags
return detect_flags
except ModuleNotFoundError:
import importlib.util as _ilu
_spec = _ilu.spec_from_file_location(
"research_risk_flags", os.path.join(os.path.dirname(os.path.abspath(__file__)), "research_risk_flags.py")
)
_mod = _ilu.module_from_spec(_spec)
sys.modules["research_risk_flags"] = _mod
_spec.loader.exec_module(_mod)
return _mod.detect_flags
_detect_flags = _load_detect_flags()
# Flag detection: canonical sibling within Rune Core. Dual-import guard
# kept for standalone script execution (research executor runs as a bare
# script with src/ bootstrapped onto sys.path).
try:
from .route_risk_flags import detect_flags
except ImportError: # standalone script execution
from rune.route_risk_flags import detect_flags
def body_has_floor_trigger(body_lower: str) -> bool:
"""Public floor predicate over raw task text (used by dispatch routing
before classification, e.g. to decide probe eligibility)."""
risk, uncertainty = _detect_flags(body_lower)
risk, uncertainty = detect_flags(body_lower)
return _floor_applies_from_flags(risk, uncertainty, body_lower)
@@ -161,7 +148,7 @@ def classify_route(
prov.update(sufficiency.provenance)
# Flag detection stays for floor evaluation, but is no longer the router.
risk, uncertainty = _detect_flags(body_lower)
risk, uncertainty = detect_flags(body_lower)
unknowns = sufficiency.unknowns()
if sufficiency.provenance.get("legacy_inputs"):
@@ -174,7 +161,26 @@ def classify_route(
reasons.append("escalating per doctrine: never manufacture certainty")
local_conditions = [has_local_repro, owning_authority_known, testable_locally, narrow_and_reversible]
if all(local_conditions) and not risk and not uncertainty and not unknowns and not floor:
# LOCAL_FIX additionally requires defect language (epic #284 doctrine:
# "reproducible local DEFECT"). A clear non-defect change with the same
# evidence posture is STANDARD_CHANGE — it is more than a trivial fix.
task_language = ""
if sufficiency is not None:
task_language = sufficiency.provenance.get("task_class_language", "")
# Defect detection: provenance (production path) or direct body scan
# (legacy compat path, whose shim carries no provenance).
is_defect_route = task_language == "defect" or any(
w in body_lower
for w in ("fix", "bug", "broken", "regression", "wrong", "typo", "fails", "failing", "crash", "defect")
)
# Verifiability NONE (no test surface named in text) does not block
# LOCAL for defect-class narrow tasks: a self-evident defect fix is
# verifiable by inspection + existing suite. UNKNOWN would block
# (could not determine); NONE is a determined result.
local_ok = all(local_conditions[:2]) and (
local_conditions[2] or (local_conditions[3] and sufficiency is not None and sufficiency.verifiability == "NONE")
)
if local_ok and is_defect_route and not risk and not uncertainty and not unknowns and not floor:
reasons.append("all LOCAL_FIX conditions met; no unknowns; no floor")
return _decision(
"LOCAL_FIX",
@@ -417,10 +423,203 @@ def _event(kind: str, d: RouteDecision) -> dict:
return ev
def record_route_decision(decision: RouteDecision, path: str) -> None:
"""Persist the INITIAL_ROUTE event (append-only route history)."""
record_transition("INITIAL_ROUTE", decision, path)
def record_route_decision(decision: RouteDecision, path: str, dispatch: dict | None = None) -> None:
"""Persist the INITIAL_ROUTE event (append-only route history).
For RESEARCHED_CHANGE routes, callers attach a `dispatch` payload —
an actionable, executor-consumable handoff (issue-shaped input for
the canonical research executor's event/CLI path). The dispatch is
recorded WITH the route so the audit trail shows what the downstream
consumer was asked to do, not just the classification.
"""
_pending_dispatch = dict(dispatch) if dispatch else None
_record_transition_with("INITIAL_ROUTE", decision, path, _pending_dispatch)
def _record_transition_with(kind: str, decision: RouteDecision, path: str, dispatch: dict | None) -> None:
ev = _event(kind, decision)
if dispatch:
ev["dispatch"] = dispatch
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "a") as f:
f.write(json.dumps(ev) + "\n")
def build_research_dispatch(task_id: str, description: str, decision: RouteDecision) -> dict:
"""Actionable research dispatch for a RESEARCHED_CHANGE route.
The payload is executor-consumable: it names the entry contract the
canonical research executor accepts (issue-shaped title/body +
sufficiency provenance + floor reason) so the downstream consumer —
currently the MiMo session driving the executor CLI (documented gap,
§11) — can dispatch without re-deriving intent.
"""
return {
"task_id": task_id,
"title": description[:200],
"body": description,
"route": decision.route,
"floor": decision.floor,
"reasons": decision.reasons,
"unknowns": decision.unknowns,
"provenance": decision.sufficiency.provenance if decision.sufficiency else {},
"executor_entry": "research_stage_executor.py --stage ISSUE (with issue-shaped input)",
}
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def derive_sufficiency(
title: str, body: str, evidence_classes: list | None = None, ambiguous: bool = False, contract_compiler=None
) -> SufficiencyInputs:
"""Derive evidence-sufficiency inputs from OBSERVABLE task facts (Option C
unified authority, 2026-09-10). Deterministic, no model call. Every field
carries provenance; anything not deterministically establishable is
UNKNOWN. Shared by the research executor AND the live operator path —
one canonical implementation, not copied logic."""
import re as _re
full = (title or "") + "\n" + (body or "")
lower = full.lower()
# Optional contract compiler (research-pipeline world): when supplied,
# evidence_classes come from the deterministic ISSUE contract instead of
# the caller's assertion. Core never imports the compiler itself —
# dependency direction is executor→core, never core→executor.
if contract_compiler is not None:
try:
contract = contract_compiler({"title": title or "", "body": body or "", "labels": []})
evidence_classes = contract.get("evidence_classes", evidence_classes)
ambiguous = contract.get("ambiguous", ambiguous)
except (ValueError, TypeError, KeyError, AttributeError):
# A broken compiler must not poison routing — fall back to the
# caller's evidence_classes (already the conservative default).
pass # noqa: S110 — intentional fallback, documented above
# Mechanism-uncertainty language: questions about how the system BEHAVES
# under conditions (performance, timing, concurrency) require measurement,
# not source reading. Local code location does not establish the answer.
mechanism_uncertain = any(
w in lower
for w in (
"performance",
"throughput",
"latency",
"race condition",
"timing",
"under load",
"concurrency",
"deadlock",
"memory usage",
"benchmark needed",
"measure",
)
)
# No test-surface references = NONE (determined: "no test surface
# exists"), not UNKNOWN. UNKNOWN is reserved for "could not determine".
# NONE does not force escalation — it weighs in the local-conditions
# check via testable_locally=False.
verifiability = (
"TESTABLE"
if _re.search(
r"(tests/[\w/.\-]+|test_[\w/.\-]+\.py|pytest|unittest|"
r"run (the )?tests)",
body or "",
_re.IGNORECASE,
)
else "NONE"
)
if mechanism_uncertain and verifiability == "TESTABLE":
verifiability = "MEASUREMENT_REQUIRED"
external = _detect_external_refs(body or "")
scope_novel = "NEW_SUBSYSTEM" if any(m in (body or "").lower() for m in FLOOR_EXPLICIT_MARKERS) else "KNOWN_PATTERN"
# LOCAL AUTHORITY grounding (Option C unified semantics):
# - With a contract compiler (research path): evidence_classes == ["repo"].
# - Without one (operator path): the ABSENCE of grounding-defeaters —
# no external refs, no measurement language, no novelty markers —
# IS the grounding evidence. The task text itself is the authority.
if evidence_classes is not None:
grounded = evidence_classes == ["repo"] and not mechanism_uncertain
else:
grounded = external == "NONE" and not mechanism_uncertain and scope_novel == "KNOWN_PATTERN"
# LOCAL_FIX vs STANDARD_CHANGE discriminator (epic #284 doctrine):
# LOCAL_FIX = reproducible local DEFECT (fix/bug/broken/regression
# language); STANDARD_CHANGE = clear change, more than trivial
# (add/implement/extend/refactor language). Observable, deterministic.
is_defect = any(
w in lower
for w in ("fix", "bug", "broken", "regression", "wrong", "typo", "fails", "failing", "crash", "defect")
)
is_change = any(
w in lower for w in ("add ", "implement", "extend", "refactor", "introduce", "support for", "new feature")
)
# Handoff §10: the ACTION VERB governs mutation-class, not object words.
# "inspect the failing test" requests reading, even though "failing"
# appears. A leading inspection verb makes the task read-only.
first_words = lower.split()[:3]
if any(
w
in (
"inspect",
"read",
"review",
"analyze",
"analyse",
"explain",
"look",
"list",
"show",
"describe",
"check",
"what",
"why",
"how",
"where",
"which",
)
for w in first_words
):
is_defect = False
is_change = False
return SufficiencyInputs(
local_authority=("GROUNDED" if grounded else "NOT_GROUNDED"),
external_dependency=external,
version_freshness_risk=(
"RISK"
if (
external == "PRESENT"
and any(w in (body or "").lower() for w in ("version", "upgrade", "latest", "current behaviour"))
)
else "NONE"
),
verifiability=verifiability,
scope_novelty=scope_novel,
provenance={
"local_authority": (
"ISSUE contract evidence_classes (deterministic compiler) "
"+ mechanism-uncertainty scan (measurement language)"
),
"mechanism_uncertain": str(mechanism_uncertain),
"external_dependency": "URL/external-reference scan of issue body",
"version_freshness_risk": "version-language scan, gated on external dependency",
"verifiability": "test path/command reference; measurement language overrides",
"scope_novelty": "explicit new-subsystem/adoption markers in issue body",
"task_class_language": ("defect" if is_defect else "change" if is_change else "neutral"),
"ambiguous": str(ambiguous),
},
)
def _detect_external_refs(body: str) -> str:
"""Observable external-reference detection. URL schemes and explicit
API/SDK/service markers — what is present, not what is inferred."""
lower = body.lower()
if "http://" in lower or "https://" in lower:
return "PRESENT"
if any(m in lower for m in (" api ", "sdk", "library", "package", "service", "endpoint")):
return "PRESENT"
return "NONE"
+11 -10
View File
@@ -30,6 +30,7 @@ if _SD not in sys.path:
@dataclass
class AttentionCapsule:
"""Compact working set for cognitive work."""
active_objective: str = ""
current_phase: str = ""
relevant_memories: List[str] = field(default_factory=list)
@@ -199,14 +200,14 @@ def create_capsule(task: str, context: str = "", max_tokens: int = 600) -> Atten
# Extract task requirements (not constraints — no refusal language)
requirement_patterns = [
r'need to',
r'want to',
r'please',
r'fix',
r'build',
r'create',
r'update',
r'change',
r"need to",
r"want to",
r"please",
r"fix",
r"build",
r"create",
r"update",
r"change",
]
for pattern in requirement_patterns:
@@ -233,7 +234,7 @@ def create_capsule(task: str, context: str = "", max_tokens: int = 600) -> Atten
# If still over, trim objective
while capsule.token_count > max_tokens and len(capsule.active_objective) > 50:
capsule.active_objective = capsule.active_objective[:len(capsule.active_objective)//2]
capsule.active_objective = capsule.active_objective[: len(capsule.active_objective) // 2]
prompt_text = capsule.to_prompt()
capsule.token_count = estimate_tokens(prompt_text)
@@ -291,5 +292,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["attention_capsule"],
"priority": 5,
"auto_activate": True
"auto_activate": True,
}
+38 -22
View File
@@ -87,14 +87,14 @@ def verify_chain() -> dict:
try:
entry = json.loads(line)
except json.JSONDecodeError:
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i+1}"}
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i + 1}"}
# Verify prev_hash chain
if entry.get("prev_hash") != prev_hash:
return {
"valid": False,
"entries": i,
"reason": f"chain broken at line {i+1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}"
"reason": f"chain broken at line {i + 1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}",
}
# Verify HMAC
@@ -104,7 +104,7 @@ def verify_chain() -> dict:
return {
"valid": False,
"entries": i,
"reason": f"HMAC mismatch at line {i+1}: stored={stored_hash[:16]}..., computed={computed[:16]}..."
"reason": f"HMAC mismatch at line {i + 1}: stored={stored_hash[:16]}..., computed={computed[:16]}...",
}
prev_hash = stored_hash
@@ -131,37 +131,51 @@ def get_entries(limit: int = 50) -> List[dict]:
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
"""Log a promotion decision."""
return append_entry("promotion", entry_id, {
"old_status": old_status,
"new_status": new_status,
"wash_count": wash_count,
"reason": reason,
})
return append_entry(
"promotion",
entry_id,
{
"old_status": old_status,
"new_status": new_status,
"wash_count": wash_count,
"reason": reason,
},
)
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
"""Log a pre-commit gate result."""
return append_entry("gate", entry_id, {
"verdict": verdict,
"critics": critics,
})
return append_entry(
"gate",
entry_id,
{
"verdict": verdict,
"critics": critics,
},
)
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
"""Log a drift alert."""
return append_entry("alert", 0, {
"alert_type": alert_type,
"message": message,
"severity": severity,
})
return append_entry(
"alert",
0,
{
"alert_type": alert_type,
"message": message,
"severity": severity,
},
)
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Promotion audit ledger")
sub = parser.add_subparsers(dest="cmd")
@@ -198,10 +212,12 @@ def main():
print(f"Recent ledger entries ({len(entries)}):")
for e in entries:
print(f" [{e.get('type')}] entry_id={e.get('entry_id')} @ {e.get('timestamp', '')[:19]}")
details = e.get('details', {})
if e.get('type') == 'promotion':
print(f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})")
elif e.get('type') == 'gate':
details = e.get("details", {})
if e.get("type") == "promotion":
print(
f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})"
)
elif e.get("type") == "gate":
print(f" verdict={details.get('verdict')}")
else:
print("No ledger entries.")
+69 -17
View File
@@ -28,8 +28,7 @@ def run_warm_router(message):
"""Run warm router and return fired neurons."""
try:
result = subprocess.run(
[PYTHON, WARM_ROUTER, "route", message, "--json"],
capture_output=True, text=True, timeout=10
[PYTHON, WARM_ROUTER, "route", message, "--json"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and result.stdout.strip():
return json.loads(result.stdout)
@@ -42,8 +41,7 @@ def run_memory_recall(query, budget=5):
"""Run memory recall and return relevant entries."""
try:
result = subprocess.run(
[PYTHON, MEMORY_STORE, "recall", query, "--json"],
capture_output=True, text=True, timeout=15
[PYTHON, MEMORY_STORE, "recall", query, "--json"], capture_output=True, text=True, timeout=15
)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout)
@@ -57,10 +55,7 @@ def run_memory_recall(query, budget=5):
def run_always_digest():
"""Run always-on digest and return results."""
try:
result = subprocess.run(
[PYTHON, WARM_ROUTER, "always", "--json"],
capture_output=True, text=True, timeout=10
)
result = subprocess.run([PYTHON, WARM_ROUTER, "always", "--json"], capture_output=True, text=True, timeout=10)
if result.returncode == 0 and result.stdout.strip():
return json.loads(result.stdout)
except Exception:
@@ -108,14 +103,70 @@ def build_auto_context(message):
# Memory recall for substantive messages
if message and len(message.strip()) > 10:
# Extract key terms for recall (words > 4 chars, not stopwords)
stopwords = {"what", "when", "where", "which", "that", "this", "with", "from",
"have", "been", "were", "they", "their", "about", "would", "could",
"should", "will", "into", "your", "just", "like", "know", "think",
"also", "back", "then", "than", "more", "some", "only", "very",
"does", "done", "make", "made", "want", "need", "give", "tell",
"there", "these", "those", "because", "between", "after", "before",
"under", "over", "again", "once", "here", "each", "every", "both",
"few", "most", "other", "such", "same", "being", "having"}
stopwords = {
"what",
"when",
"where",
"which",
"that",
"this",
"with",
"from",
"have",
"been",
"were",
"they",
"their",
"about",
"would",
"could",
"should",
"will",
"into",
"your",
"just",
"like",
"know",
"think",
"also",
"back",
"then",
"than",
"more",
"some",
"only",
"very",
"does",
"done",
"make",
"made",
"want",
"need",
"give",
"tell",
"there",
"these",
"those",
"because",
"between",
"after",
"before",
"under",
"over",
"again",
"once",
"here",
"each",
"every",
"both",
"few",
"most",
"other",
"such",
"same",
"being",
"having",
}
words = [w for w in message.lower().split() if len(w) > 4 and w.isalpha() and w not in stopwords]
if words:
query = " ".join(words[:5])
@@ -167,6 +218,7 @@ def handle_hook(event, data=None):
def main():
import argparse
ap = argparse.ArgumentParser(description="Auto-context: automatic memory activation")
ap.add_argument("cmd", choices=["build", "show", "hook"])
ap.add_argument("arg", nargs="?", default="")
@@ -214,5 +266,5 @@ PLUGIN = {
"dependencies": ["memory_store", "warm_router"],
"provides": ["auto_context"],
"priority": 5,
"auto_activate": True
"auto_activate": True,
}
+72 -47
View File
@@ -85,63 +85,72 @@ def extract_tasks(message, channel="chat"):
"""
# Strip code blocks before analysis
import re as _re
message = _re.sub(r'```[\s\S]*?```', '', message)
message = _re.sub(r'`[^`]+`', '', message)
message = _re.sub(r"```[\s\S]*?```", "", message)
message = _re.sub(r"`[^`]+`", "", message)
tasks = []
lines = message.split("\n")
# Pattern 1: Numbered lists (1. do this, 2. do that)
numbered = re.findall(r'(?:^|\n)\s*(\d+)[\.\)]\s*(.+?)(?:\n|$)', message)
numbered = re.findall(r"(?:^|\n)\s*(\d+)[\.\)]\s*(.+?)(?:\n|$)", message)
if len(numbered) >= 2:
for num, text in numbered:
text = text.strip()
if len(text) > 10:
tasks.append({
"description": text,
"priority": "normal",
"pattern": "numbered_list",
})
tasks.append(
{
"description": text,
"priority": "normal",
"pattern": "numbered_list",
}
)
# Pattern 2: Bullet points (- do this, * do that)
bullets = re.findall(r'(?:^|\n)\s*[-*•]\s*(.+?)(?:\n|$)', message)
bullets = re.findall(r"(?:^|\n)\s*[-*•]\s*(.+?)(?:\n|$)", message)
if len(bullets) >= 2:
for text in bullets:
text = text.strip()
if len(text) > 10:
tasks.append({
"description": text,
"priority": "normal",
"pattern": "bullet_list",
})
tasks.append(
{
"description": text,
"priority": "normal",
"pattern": "bullet_list",
}
)
# Pattern 3: Action verbs at start of sentences
action_patterns = [
r'(?:^|\n)\s*(?:I(?:\'ll| will| need to)|let me|we should|we need to)\s+(.+?)(?:\.|$|\n)',
r'(?:^|\n)\s*(?:fix|build|create|add|update|check|verify|test|research|investigate|implement|design)\s+(.+?)(?:\.|$|\n)',
r"(?:^|\n)\s*(?:I(?:\'ll| will| need to)|let me|we should|we need to)\s+(.+?)(?:\.|$|\n)",
r"(?:^|\n)\s*(?:fix|build|create|add|update|check|verify|test|research|investigate|implement|design)\s+(.+?)(?:\.|$|\n)",
]
for pattern in action_patterns:
matches = re.findall(pattern, message, re.IGNORECASE | re.MULTILINE)
for text in matches:
text = text.strip()
if len(text) > 10 and len(text) < 200:
tasks.append({
"description": text,
"priority": "normal",
"pattern": "action_verb",
})
tasks.append(
{
"description": text,
"priority": "normal",
"pattern": "action_verb",
}
)
# Pattern 4: "First... then... finally..." sequences
sequence = re.findall(r'(?:first|then|next|finally|after that)[,.]?\s+(.+?)(?:\.|$|\n)', message, re.IGNORECASE)
sequence = re.findall(r"(?:first|then|next|finally|after that)[,.]?\s+(.+?)(?:\.|$|\n)", message, re.IGNORECASE)
if len(sequence) >= 2:
for text in sequence:
text = text.strip()
if len(text) > 10:
tasks.append({
"description": text,
"priority": "normal",
"pattern": "sequence",
})
tasks.append(
{
"description": text,
"priority": "normal",
"pattern": "sequence",
}
)
# Deduplicate by description similarity
seen = set()
@@ -168,12 +177,14 @@ def store_tasks(tasks, channel="chat", source_message=""):
next_id = (row["max_id"] or 0) + 1
task_id = f"AT{next_id}"
db.execute("""
db.execute(
"""
INSERT INTO tasks (task_id, description, source_message, source_channel,
status, priority, created_at, updated_at)
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
""", (task_id, task["description"], source_message[:500], channel,
task.get("priority", "normal"), ts, ts))
""",
(task_id, task["description"], source_message[:500], channel, task.get("priority", "normal"), ts, ts),
)
# Log to completion file
log_completion(task_id, "created", task["description"], channel)
@@ -196,15 +207,21 @@ def complete_task(task_id, notes=""):
row = db.execute("SELECT description FROM tasks WHERE task_id=? AND status='active'", (task_id,)).fetchone()
description = row["description"] if row else ""
db.execute("""
db.execute(
"""
UPDATE tasks SET status='completed', completion_notes=?, completed_at=?, updated_at=?
WHERE task_id=? AND status='active'
""", (notes, ts, ts, task_id))
""",
(notes, ts, ts, task_id),
)
db.execute("""
db.execute(
"""
INSERT INTO completion_log (task_id, action, details, timestamp)
VALUES (?, 'completed', ?, ?)
""", (task_id, notes, ts))
""",
(task_id, notes, ts),
)
db.commit()
finally:
@@ -244,6 +261,7 @@ def run_auto_checks(description):
def run_compile_check():
"""Check that all Python scripts compile."""
import subprocess
try:
check_script = (
"import py_compile, glob, sys; "
@@ -253,9 +271,7 @@ def run_compile_check():
"print(f'FAIL:{len(failed)}' if failed else 'PASS')"
)
result = subprocess.run(
[sys.executable, "-c", check_script],
capture_output=True, text=True, timeout=30,
cwd=SCRIPTS_DIR
[sys.executable, "-c", check_script], capture_output=True, text=True, timeout=30, cwd=SCRIPTS_DIR
)
passed = "PASS" in result.stdout
return {"check": "compile", "passed": passed, "details": result.stdout.strip()[:200]}
@@ -266,14 +282,18 @@ def run_compile_check():
def run_benchmark_check():
"""Run RuneBench."""
import subprocess
try:
result = subprocess.run(
[sys.executable, os.path.join(SCRIPTS_DIR, "rune_bench.py"), "run"],
capture_output=True, text=True, timeout=120
capture_output=True,
text=True,
timeout=120,
)
# Check for pass patterns: "N/N" where both numbers match, or explicit ALL PASS
import re
pass_match = re.search(r'(\d+)/\1', result.stdout)
pass_match = re.search(r"(\d+)/\1", result.stdout)
passed = bool(pass_match) or "ALL" in result.stdout.upper() or "PASS" in result.stdout.upper()
return {"check": "benchmark", "passed": passed, "details": result.stdout.strip()[-200:]}
except Exception as e:
@@ -284,12 +304,17 @@ def run_sanity_check():
"""Basic sanity check — can we import key modules?"""
try:
import subprocess
result = subprocess.run(
[sys.executable, "-c",
"import sys; sys.path.insert(0, '.'); "
"import memory_store, warm_router, microkernel; print('PASS')"],
capture_output=True, text=True, timeout=10,
cwd=SCRIPTS_DIR
[
sys.executable,
"-c",
"import sys; sys.path.insert(0, '.'); import memory_store, warm_router, microkernel; print('PASS')",
],
capture_output=True,
text=True,
timeout=10,
cwd=SCRIPTS_DIR,
)
passed = "PASS" in result.stdout
return {"check": "sanity", "passed": passed, "details": result.stdout.strip()[:200]}
@@ -302,8 +327,7 @@ def list_tasks(status="active", limit=20):
db = connect()
try:
rows = db.execute(
"SELECT * FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?",
(status, limit)
"SELECT * FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?", (status, limit)
).fetchall()
return [dict(r) for r in rows]
finally:
@@ -359,6 +383,7 @@ def handle_hook(event, data=None):
def main():
import argparse
ap = argparse.ArgumentParser(description="Automatic task extraction")
ap.add_argument("cmd", choices=["extract", "complete", "list", "completion", "hook"])
ap.add_argument("arg", nargs="?", default="")
@@ -431,5 +456,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["auto_tasks"],
"priority": 4,
"auto_activate": True
"auto_activate": True,
}
+53 -31
View File
@@ -101,11 +101,14 @@ def add_correction(original, corrected, scope_tags=None, domain="", pattern=""):
ts = _now()
tags = json.dumps(scope_tags or {})
cur = db.execute("""
cur = db.execute(
"""
INSERT INTO corrections (original_behavior, corrected_behavior, scope_tags,
scope_pattern, scope_domain, confidence, state, created_at)
VALUES (?, ?, ?, ?, ?, 0.5, 'active', ?)
""", (original, corrected, tags, pattern, domain, ts))
""",
(original, corrected, tags, pattern, domain, ts),
)
correction_id = cur.lastrowid
db.commit()
return {"added": correction_id, "confidence": 0.5, "scope": scope_tags or {}}
@@ -120,11 +123,14 @@ def add_habit(trigger, action, name="", context_tags=None, min_activations=3):
ts = _now()
tags = json.dumps(context_tags or [])
cur = db.execute("""
cur = db.execute(
"""
INSERT INTO habits (name, trigger_pattern, context_tags, action_template,
min_activations, confidence, state, created_at)
VALUES (?, ?, ?, ?, ?, 0.5, 'candidate', ?)
""", (name, trigger, tags, action, min_activations, ts))
""",
(name, trigger, tags, action, min_activations, ts),
)
habit_id = cur.lastrowid
db.commit()
return {"added": habit_id, "state": "candidate", "min_activations": min_activations}
@@ -214,16 +220,22 @@ def reinforce(item_id, outcome, item_type="habit"):
if habit["state"] == "candidate" and new_count >= habit["min_activations"]:
new_state = "active"
db.execute("""
db.execute(
"""
UPDATE habits SET activation_count=?, success_count=?, failure_count=?,
confidence=?, state=?, last_activated=? WHERE id=?
""", (new_count, new_success, new_failure, new_confidence, new_state, ts, item_id))
""",
(new_count, new_success, new_failure, new_confidence, new_state, ts, item_id),
)
# Log activation
db.execute("""
db.execute(
"""
INSERT INTO habit_activations (habit_id, context, outcome, activated_at)
VALUES (?, '', ?, ?)
""", (item_id, outcome, ts))
""",
(item_id, outcome, ts),
)
db.commit()
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
@@ -244,10 +256,13 @@ def reinforce(item_id, outcome, item_type="habit"):
new_count = correction["evidence_count"]
new_state = "active" if new_confidence > 0.3 else "archived"
db.execute("""
db.execute(
"""
UPDATE corrections SET confidence=?, evidence_count=?, state=?, last_reinforced=?
WHERE id=?
""", (new_confidence, new_count, new_state, ts, item_id))
""",
(new_confidence, new_count, new_state, ts, item_id),
)
db.commit()
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
finally:
@@ -261,17 +276,23 @@ def list_items(item_type="all", limit=20):
results = {}
if item_type in ("all", "correction"):
corrections = db.execute("""
corrections = db.execute(
"""
SELECT * FROM corrections WHERE state='active'
ORDER BY confidence DESC LIMIT ?
""", (limit,)).fetchall()
""",
(limit,),
).fetchall()
results["corrections"] = [dict(c) for c in corrections]
if item_type in ("all", "habit"):
habits = db.execute("""
habits = db.execute(
"""
SELECT * FROM habits WHERE state IN ('active', 'candidate')
ORDER BY confidence DESC LIMIT ?
""", (limit,)).fetchall()
""",
(limit,),
).fetchall()
results["habits"] = [dict(h) for h in habits]
return results
@@ -285,7 +306,9 @@ def behavior_stats():
try:
stats = {}
stats["corrections_active"] = db.execute("SELECT COUNT(*) FROM corrections WHERE state='active'").fetchone()[0]
stats["corrections_archived"] = db.execute("SELECT COUNT(*) FROM corrections WHERE state='archived'").fetchone()[0]
stats["corrections_archived"] = db.execute(
"SELECT COUNT(*) FROM corrections WHERE state='archived'"
).fetchone()[0]
stats["habits_active"] = db.execute("SELECT COUNT(*) FROM habits WHERE state='active'").fetchone()[0]
stats["habits_candidate"] = db.execute("SELECT COUNT(*) FROM habits WHERE state='candidate'").fetchone()[0]
stats["total_activations"] = db.execute("SELECT COUNT(*) FROM habit_activations").fetchone()[0]
@@ -296,6 +319,7 @@ def behavior_stats():
def main():
import argparse
ap = argparse.ArgumentParser(description="Behavioral patterns")
ap.add_argument("cmd", choices=["correct", "habit", "match", "reinforce", "list", "stats"])
ap.add_argument("arg", nargs="?", default="")
@@ -317,20 +341,16 @@ def main():
print("usage: behavior.py correct '<original>' '<corrected>'")
return
scope = json.loads(args.scope) if args.scope else None
result = add_correction(args.arg, args.arg2, scope_tags=scope,
domain=args.domain, pattern=args.pattern)
print(json.dumps(result, indent=2) if args.json else
f"Correction added (confidence: {result['confidence']})")
result = add_correction(args.arg, args.arg2, scope_tags=scope, domain=args.domain, pattern=args.pattern)
print(json.dumps(result, indent=2) if args.json else f"Correction added (confidence: {result['confidence']})")
elif args.cmd == "habit":
if not all([args.arg, args.arg2]):
print("usage: behavior.py habit '<trigger_pattern>' '<action>'")
return
tags = json.loads(args.context_tags) if args.context_tags else None
result = add_habit(args.arg, args.arg2, name=args.name,
context_tags=tags, min_activations=args.min_activations)
print(json.dumps(result, indent=2) if args.json else
f"Habit candidate added (state: {result['state']})")
result = add_habit(args.arg, args.arg2, name=args.name, context_tags=tags, min_activations=args.min_activations)
print(json.dumps(result, indent=2) if args.json else f"Habit candidate added (state: {result['state']})")
elif args.cmd == "match":
if not args.arg:
@@ -350,8 +370,7 @@ def main():
print("usage: behavior.py reinforce <id>")
return
result = reinforce(int(args.arg), args.outcome, item_type=args.type)
print(json.dumps(result, indent=2) if args.json else
f"Reinforced: confidence={result.get('confidence', '?')}")
print(json.dumps(result, indent=2) if args.json else f"Reinforced: confidence={result.get('confidence', '?')}")
elif args.cmd == "list":
results = list_items(item_type=args.type, limit=args.limit)
@@ -361,15 +380,18 @@ def main():
for c in results.get("corrections", []):
print(f" [correction {c['id']}] {c['corrected_behavior'][:60]} (conf: {c['confidence']:.2f})")
for h in results.get("habits", []):
state = h['state']
state = h["state"]
print(f" [habit {h['id']}] {h['action_template'][:60]} ({state}, conf: {h['confidence']:.2f})")
elif args.cmd == "stats":
s = behavior_stats()
print(json.dumps(s, indent=2) if args.json else
f"Corrections: {s['corrections_active']} active, {s['corrections_archived']} archived | "
f"Habits: {s['habits_active']} active, {s['habits_candidate']} candidate | "
f"Activations: {s['total_activations']}")
print(
json.dumps(s, indent=2)
if args.json
else f"Corrections: {s['corrections_active']} active, {s['corrections_archived']} archived | "
f"Habits: {s['habits_active']} active, {s['habits_candidate']} candidate | "
f"Activations: {s['total_activations']}"
)
if __name__ == "__main__":
@@ -385,5 +407,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["behavior"],
"priority": 50,
"auto_activate": True
"auto_activate": True,
}
+41 -18
View File
@@ -48,6 +48,7 @@ REPO_DIR = "/mnt/workdrive/rune"
# DATA STRUCTURES
# ═══════════════════════════════════════════════════════════════════════════════
class Phase(Enum):
ASSESS = "assess"
RESEARCH = "research"
@@ -61,18 +62,23 @@ class Phase(Enum):
INTEGRATE = "integrate"
RECORD = "record"
class BuildPhase(Enum):
"""Three-phase execution for build step."""
FOUNDATION = "foundation" # Types, interfaces, shared utilities
FOUNDATION = "foundation" # Types, interfaces, shared utilities
IMPLEMENTATION = "implementation" # Feature code
INTEGRATION = "integration" # Tests, wiring, documentation
INTEGRATION = "integration" # Tests, wiring, documentation
class VerificationTier(Enum):
"""Tiered verification strategy."""
SYNTACTIC = "syntactic" # Per-edit: lint, type-check, format (<1s)
SEMANTIC = "semantic" # Per-batch: unit tests, import validation (<30s)
SYNTACTIC = "syntactic" # Per-edit: lint, type-check, format (<1s)
SEMANTIC = "semantic" # Per-batch: unit tests, import validation (<30s)
INTEGRATION = "integration" # Per-wave: contract tests, build/compile (<5min)
SYSTEM = "system" # Full: full test suite, property-based (<15min)
SYSTEM = "system" # Full: full test suite, property-based (<15min)
class PhaseStatus(Enum):
PENDING = "pending"
@@ -81,6 +87,7 @@ class PhaseStatus(Enum):
FAILED = "failed"
SKIPPED = "skipped"
@dataclass
class LedgerEntry:
phase: str
@@ -90,6 +97,7 @@ class LedgerEntry:
details: Dict[str, Any] = field(default_factory=dict)
errors: List[str] = field(default_factory=list)
@dataclass
class BuildState:
build_id: str
@@ -112,10 +120,12 @@ class BuildState:
files_changed: List[str] = field(default_factory=list)
contracts: List[Dict[str, Any]] = field(default_factory=list)
# ═══════════════════════════════════════════════════════════════════════════════
# BUILD MANAGER
# ═══════════════════════════════════════════════════════════════════════════════
class BuildManager:
"""Manages the build pipeline with worktree isolation and ledger tracking."""
@@ -282,11 +292,15 @@ class BuildManager:
def complete_feasible(self, feasible: bool, risk: str, details: Dict[str, Any] = None):
"""Complete the feasibility phase."""
self._log_phase(Phase.FEASIBLE.value, PhaseStatus.COMPLETE.value, {
"feasible": feasible,
"risk": risk,
"details": details or {},
})
self._log_phase(
Phase.FEASIBLE.value,
PhaseStatus.COMPLETE.value,
{
"feasible": feasible,
"risk": risk,
"details": details or {},
},
)
def start_consult(self):
"""Start the consult phase."""
@@ -327,10 +341,14 @@ class BuildManager:
def complete_verify(self, passed: bool, failures: List[str] = None):
"""Complete the verify phase."""
self._log_phase(Phase.VERIFY.value, PhaseStatus.COMPLETE.value, {
"passed": passed,
"failures": failures or [],
})
self._log_phase(
Phase.VERIFY.value,
PhaseStatus.COMPLETE.value,
{
"passed": passed,
"failures": failures or [],
},
)
def start_correct(self):
"""Start the correct phase."""
@@ -347,10 +365,14 @@ class BuildManager:
def complete_integrate(self, passed: bool, failures: List[str] = None):
"""Complete the integrate phase."""
self._log_phase(Phase.INTEGRATE.value, PhaseStatus.COMPLETE.value, {
"passed": passed,
"failures": failures or [],
})
self._log_phase(
Phase.INTEGRATE.value,
PhaseStatus.COMPLETE.value,
{
"passed": passed,
"failures": failures or [],
},
)
def start_record(self):
"""Start the record phase."""
@@ -409,6 +431,7 @@ class BuildManager:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 2:
print("Usage: build_mode.py [start|status|resume|abort]")
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""builder_bridge.py — MiMo builder bridge (thin adapter over Rune Core).
Requirement (handoff 2026-09-10 §6–§8): the MiMo builder (unlimited GLM)
should benefit from Rune Core's growing cognition TODAY, through the exact
same core the eventual Rune TUI uses — without turning MiMo into a second
Rune and without a second model call just to build context.
Normal path (cheap — deterministic state lookup + selective retrieval):
MiMo coding request (task text)
↓
preflight(task_text) → compact BuilderCapsule (JSON, bounded)
↓
builder proceeds normally, optionally citing the capsule
The adapter contains NO cognition of its own: it calls the same Rune Core
functions the operator path calls (derive_sufficiency, classify_route,
memory_selector, AttentionCapsule). Same core, thin adapter.
Contents of the capsule (bounded, only what is useful):
- task route (LOCAL/PROBE/DEEP) + floor flag + unknowns
- memory selection summary (what the selector would inject, capped)
- validated process lessons relevant to the task (PL1-validated only;
until PL1 exists: none — raw SEED records are never injected)
- constraints/authority hints from the route provenance
Overhead: one deterministic pass, zero model calls. Escalation happens
only when the route says so (PROBE/DEEP RESEARCH), never by default.
"""
from __future__ import annotations
import json
import os
import sys
from dataclasses import dataclass, field
_PKG_DIR = os.path.dirname(os.path.abspath(__file__))
if _PKG_DIR not in sys.path:
sys.path.insert(0, _PKG_DIR)
try:
from . import adaptive_router
from .attention_capsule import AttentionCapsule
except ImportError: # standalone script execution
from rune import adaptive_router
from rune.attention_capsule import AttentionCapsule
# Bounded: the capsule never carries more than this many memory lines or
# lesson lines. The builder has its own context window; we are a garnish,
# not the meal.
MAX_MEMORY_LINES = 12
MAX_LESSON_LINES = 6
@dataclass
class BuilderCapsule:
"""Compact, builder-consumable Rune Core preflight result."""
task_route: str = ""
floor: bool = False
unknowns: list = field(default_factory=list)
memory_lines: list = field(default_factory=list)
lesson_lines: list = field(default_factory=list)
constraints: list = field(default_factory=list)
generated_at: str = ""
core_version: str = "rune-core/operator-path"
def to_dict(self) -> dict:
return {
"task_route": self.task_route,
"floor": self.floor,
"unknowns": self.unknowns,
"memory_lines": self.memory_lines[:MAX_MEMORY_LINES],
"lesson_lines": self.lesson_lines[:MAX_LESSON_LINES],
"constraints": self.constraints,
"generated_at": self.generated_at,
"core_version": self.core_version,
}
def to_markdown(self) -> str:
"""Compact rendering for injection into a builder prompt."""
d = self.to_dict()
if (
not d["memory_lines"]
and not d["lesson_lines"]
and not d["unknowns"]
and not d["constraints"]
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["memory_lines"]:
lines.append("**Relevant Rune memory:**")
lines += [f"- {m}" for m in d["memory_lines"]]
if d["lesson_lines"]:
lines.append("**Validated process lessons:**")
lines += [f"- {l}" for l in d["lesson_lines"]]
if d["unknowns"]:
lines.append(f"**Material unknowns:** {', '.join(d['unknowns'])}")
if d["constraints"]:
lines.append("**Constraints:**")
lines += [f"- {c}" for c in d["constraints"]]
if d["floor"]:
lines.append("**Deep-research floor applies** — do not skip evidence.")
return "\n".join(lines)
def _default_selector():
"""The SAME MemorySelector the operator path uses (lazy, degrade-safe)."""
try:
from .context_broker import ContextBroker # noqa: F401
from .memory_selector import MemorySelector
return MemorySelector()
except ImportError:
return None # core memory stack unavailable — bridge runs route-only
def preflight(task_text: str, memory_selector=None) -> BuilderCapsule:
"""Deterministic Rune Core preflight for a builder task.
Zero model calls. Uses the same core authority as the operator path:
derive_sufficiency → classify_route → (optional) memory selection.
"""
from datetime import datetime, timezone
sufficiency = adaptive_router.derive_sufficiency(title=(task_text or "")[:200], body=task_text or "")
decision = adaptive_router.classify_route(
issue_body=task_text or "",
has_local_repro=(sufficiency.local_authority == "GROUNDED"),
owning_authority_known=True,
testable_locally=(sufficiency.verifiability == "TESTABLE"),
narrow_and_reversible=(sufficiency.scope_novelty == "KNOWN_PATTERN"),
sufficiency=sufficiency,
pipeline_sha="builder-bridge",
)
capsule = BuilderCapsule(
task_route=decision.route,
floor=decision.floor,
unknowns=list(decision.unknowns),
generated_at=datetime.now(timezone.utc).isoformat(),
)
# Memory retrieval through the SAME thresholded store authority the
# operator's selector delegates to (memory_store.recall — the ONE
# floor, same min_score). The bridge calls the store directly rather
# than the full selector: query expansion (recall_uplift) alone costs
# ~1.2s of unindexed LIKE scans per request (measured 2026-09-10),
# which violates the §7 low-overhead requirement for the normal path.
# The store's own gate (min_score=RECALL_MIN_SCORE) is the same
# abstention authority; dense+lexical hybrid still applies.
memory_lines = _cached_memory(task_text or "")
capsule.memory_lines = memory_lines[:MAX_MEMORY_LINES]
# Constraints from route provenance (observable, bounded).
prov = sufficiency.provenance or {}
if decision.floor:
capsule.constraints.append("Deep-research floor: " + ", ".join(decision.reasons)[:200])
if sufficiency.local_authority == "NOT_GROUNDED":
capsule.constraints.append("Local authority not established — verify against external/runtime evidence.")
# Lessons: PL1-VALIDATED ONLY. Until PL1 exists, this stays empty by
# design (handoff §9: raw SEED records are outcome telemetry, not lessons).
capsule.lesson_lines = _validated_lessons_for(task_text or "")
return capsule
# --- Memory retrieval: same authority, bounded overhead (handoff §7/§8) ---
#
# The full selector path costs ~1.3s warm (measured 2026-09-10: the
# recall_uplift store-derived pass runs ~4000 unindexed LIKE scans per
# request against the 435MB store). Paying that on EVERY builder request
# violates §7/§8. Design: TTL cache keyed by normalized task text.
# - first request for a unique task: synchronous retrieval (correctness
# first — the capsule must never be empty-wrong), ~1.3s
# - repeats within TTL: instant cache hit
# - the cache stores the RESULT, so identical glue-work tasks (the
# common builder case) are near-zero overhead after the first hit.
_CACHE: dict = {}
_CACHE_TTL = 600.0 # seconds
_CACHE_MAX = 128
def _cache_key(task_text: str) -> str:
import re as _re
return _re.sub(r"\s+", " ", (task_text or "").strip().lower())
def _cached_memory(task_text: str) -> list:
import time as _time
key = _cache_key(task_text)
now = _time.monotonic()
hit = _CACHE.get(key)
if hit is not None and now - hit[0] < _CACHE_TTL:
return hit[1]
lines = _retrieve_memory(task_text)
if len(_CACHE) >= _CACHE_MAX:
# evict oldest
oldest = min(_CACHE.items(), key=lambda kv: kv[1][0])
_CACHE.pop(oldest[0], None)
_CACHE[key] = (now, lines)
return lines
_SELECTOR = None
def _get_selector():
global _SELECTOR
if _SELECTOR is None:
_SELECTOR = _default_selector()
return _SELECTOR
def _retrieve_memory(task_text: str) -> list:
"""The SAME selector the operator path uses — identical floor, identical
sources. Any failure degrades to no-memory (observable: empty lines)."""
try:
selector = _get_selector()
if selector is None:
return []
result = selector.select_for_task(task_text)
lines = []
for block in result.blocks[:MAX_MEMORY_LINES]:
content = getattr(block, "content", "") or ""
for line in content.splitlines()[:3]:
if line.strip():
lines.append(line.strip()[:200])
if len(lines) >= MAX_MEMORY_LINES:
break
if len(lines) >= MAX_MEMORY_LINES:
break
return lines
except Exception: # noqa: BLE001 — the bridge is an advisory
# preflight, not the execution path: ANY selector failure degrades
# to no-memory rather than blocking the builder. Observable: empty
# lines while the route still records.
return []
def _validated_lessons_for(task_text: str) -> list:
"""PL1-validated lessons relevant to this task. Empty until PL1 promotes
its first lesson — the function exists so the bridge never needs a
second integration when PL1 lands."""
return []
if __name__ == "__main__": # pragma: no cover — manual bridge invocation
import argparse
ap = argparse.ArgumentParser(description="Rune Core preflight for the MiMo builder")
ap.add_argument("task", help="Task text to preflight")
ap.add_argument("--format", choices=("json", "md"), default="md")
args = ap.parse_args()
cap = preflight(args.task)
if args.format == "json":
print(json.dumps(cap.to_dict(), indent=2))
else:
print(cap.to_markdown())
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""builder_bridge_cli.py — CLI wrapper around rune.builder_bridge.preflight.
Called by the MiMo builder-bridge hook (one subprocess per unique task text;
the hook dedupes repeats within its TTL, and this CLI additionally benefits
from the module's own TTL cache within a long-lived interpreter — here the
process is fresh each call, so the hook-level dedupe is the effective one).
Usage: builder_bridge_cli.py "<task text>"
Output: the capsule markdown (possibly empty — inject-nothing policy).
"""
import json
import os
import sys
# Rune Core bootstrap (Option C): the bridge lives in src/rune.
_REPO = "/mnt/workdrive/issue161-harness"
sys.path.insert(0, os.path.join(_REPO, "src"))
sys.path.append(_REPO) # tests/ style imports if ever needed
# The store to read: the LIVE harness store (839+ entries), not the dev
# checkout's empty DB. memory_env honours RUNE_MEMORY_DIR.
os.environ.setdefault("RUNE_MEMORY_DIR", "/home/nick/.config/opencode/memory")
from rune.builder_bridge import preflight # noqa: E402
def main() -> int:
task = " ".join(sys.argv[1:]).strip() if len(sys.argv) > 1 else ""
if len(sys.argv) > 1 and sys.argv[1] == "--json":
task = " ".join(sys.argv[2:]).strip()
cap = preflight(task)
print(json.dumps(cap.to_dict()))
return 0
cap = preflight(task)
print(cap.to_markdown())
return 0
if __name__ == "__main__":
raise SystemExit(main())
+31 -40
View File
@@ -38,15 +38,15 @@ DB_PATH = os.path.join(memory_env.memory_dir(), "cache.db")
# ---------------------------------------------------------------------------
# In-memory hit tracking — avoids write-on-read in SQLite
# ---------------------------------------------------------------------------
_hit_counts: dict[str, int] = {} # key → pending hit increments
_last_hits: dict[str, str] = {} # key → latest last_hit timestamp
_miss_counts: dict[str, int] = {} # key → pending miss increments
_last_misses: dict[str, str] = {} # key → latest last_miss timestamp
_hit_counts: dict[str, int] = {} # key → pending hit increments
_last_hits: dict[str, str] = {} # key → latest last_hit timestamp
_miss_counts: dict[str, int] = {} # key → pending miss increments
_last_misses: dict[str, str] = {} # key → latest last_miss timestamp
_flush_lock = threading.Lock()
_FLUSH_THRESHOLD = 50 # flush after this many pending updates
_flush_counter = 0 # counts hits+misses since last flush
_last_flush_time = 0.0 # epoch of last flush
_FLUSH_INTERVAL = 30.0 # flush at least every N seconds
_FLUSH_THRESHOLD = 50 # flush after this many pending updates
_flush_counter = 0 # counts hits+misses since last flush
_last_flush_time = 0.0 # epoch of last flush
_FLUSH_INTERVAL = 30.0 # flush at least every N seconds
def _record_hit(key: str) -> None:
@@ -58,8 +58,7 @@ def _record_hit(key: str) -> None:
_hit_counts[key] = _hit_counts.get(key, 0) + 1
_last_hits[key] = now
_flush_counter += 1
if (_flush_counter >= _FLUSH_THRESHOLD
or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL):
if _flush_counter >= _FLUSH_THRESHOLD or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL:
do_flush = True
if do_flush:
flush_stats()
@@ -74,8 +73,7 @@ def _record_miss(key: str) -> None:
_miss_counts[key] = _miss_counts.get(key, 0) + 1
_last_misses[key] = now
_flush_counter += 1
if (_flush_counter >= _FLUSH_THRESHOLD
or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL):
if _flush_counter >= _FLUSH_THRESHOLD or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL:
do_flush = True
if do_flush:
flush_stats()
@@ -187,10 +185,7 @@ def get_cache(key):
"""
db = connect()
try:
row = db.execute(
"SELECT * FROM cache WHERE key=? AND invalidated_at IS NULL",
(key,)
).fetchone()
row = db.execute("SELECT * FROM cache WHERE key=? AND invalidated_at IS NULL", (key,)).fetchone()
if row:
# Record hit in memory — no SQLite write on the hot path
@@ -211,9 +206,7 @@ def get_cache(key):
# Miss — only record if the key exists in the cache (e.g. invalidated).
# A true miss (key never seen) should not trigger any write.
exists = db.execute(
"SELECT 1 FROM cache WHERE key=?", (key,)
).fetchone()
exists = db.execute("SELECT 1 FROM cache WHERE key=?", (key,)).fetchone()
if exists:
_record_miss(key)
@@ -242,7 +235,8 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
if compressed is None:
compressed = _compress(value)
db.execute("""
db.execute(
"""
INSERT INTO cache
(key, scope, content_hash, full_value, compressed_value, source_hash,
created_at, invalidated_at, hit_count, miss_count)
@@ -255,7 +249,9 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
source_hash = excluded.source_hash,
created_at = excluded.created_at,
invalidated_at = NULL
""", (key, scope, content_hash, value, compressed, source_hash, _now()))
""",
(key, scope, content_hash, value, compressed, source_hash, _now()),
)
db.commit()
db.close()
return {"stored": key, "content_hash": content_hash, "scope": scope}
@@ -264,10 +260,7 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
def invalidate(key, reason=""):
"""Invalidate a specific cache entry."""
db = connect()
db.execute(
"UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL",
(_now(), key)
)
db.execute("UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL", (_now(), key))
db.commit()
db.close()
return {"invalidated": key, "reason": reason}
@@ -277,8 +270,7 @@ def invalidate_scope(scope, reason=""):
"""Invalidate all entries in a scope."""
db = connect()
count = db.execute(
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL",
(_now(), scope)
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL", (_now(), scope)
).rowcount
db.commit()
db.close()
@@ -288,10 +280,7 @@ def invalidate_scope(scope, reason=""):
def invalidate_all(reason=""):
"""Invalidate everything. Nuclear option."""
db = connect()
count = db.execute(
"UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL",
(_now(),)
).rowcount
count = db.execute("UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL", (_now(),)).rowcount
db.commit()
db.close()
return {"invalidated_all": count, "reason": reason}
@@ -409,8 +398,7 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser(description="Cache system — event-driven, write-once-derive-twice")
ap.add_argument("cmd", choices=["get", "put", "invalidate", "invalidate-all",
"invalidate-scope", "stats", "hook"])
ap.add_argument("cmd", choices=["get", "put", "invalidate", "invalidate-all", "invalidate-scope", "stats", "hook"])
ap.add_argument("arg", nargs="?", default="")
ap.add_argument("arg2", nargs="?", default="")
ap.add_argument("--scope", default="general")
@@ -440,8 +428,11 @@ def main():
print("usage: cache.py put <key> <value>")
return
result = put_cache(args.arg, args.arg2, scope=args.scope, source_hash=args.source_hash)
print(json.dumps(result, indent=2) if args.json else
f"Stored: {args.arg} (hash: {result['content_hash']}, scope: {result['scope']})")
print(
json.dumps(result, indent=2)
if args.json
else f"Stored: {args.arg} (hash: {result['content_hash']}, scope: {result['scope']})"
)
elif args.cmd == "invalidate":
if not args.arg:
@@ -452,16 +443,16 @@ def main():
elif args.cmd == "invalidate-all":
result = invalidate_all(reason=args.reason)
print(json.dumps(result, indent=2) if args.json else
f"Invalidated all: {result['invalidated_all']} entries")
print(json.dumps(result, indent=2) if args.json else f"Invalidated all: {result['invalidated_all']} entries")
elif args.cmd == "invalidate-scope":
if not args.arg:
print("usage: cache.py invalidate-scope <scope>")
return
result = invalidate_scope(args.arg, reason=args.reason)
print(json.dumps(result, indent=2) if args.json else
f"Invalidated scope '{args.arg}': {result['count']} entries")
print(
json.dumps(result, indent=2) if args.json else f"Invalidated scope '{args.arg}': {result['count']} entries"
)
elif args.cmd == "stats":
s = cache_stats()
@@ -497,5 +488,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["cache"],
"priority": 1,
"auto_activate": True
"auto_activate": True,
}
+1 -3
View File
@@ -34,9 +34,7 @@ def main() -> int:
runtime.registry.disable("synthesis")
third = runtime.process({"event_type": "c"}, [{"event_type": "new"}])
if {r["worker"] for r in third["results"]} != {"n1"}:
print(
"COGNITIVE CANARY FAILED: toggle independence broken", file=sys.stderr
)
print("COGNITIVE CANARY FAILED: toggle independence broken", file=sys.stderr)
return 1
print("COGNITIVE CANARY PASSED", third["facts"])
return 0
+44 -25
View File
@@ -41,6 +41,7 @@ def main():
import audit_ledger
import warm_router
import memory_consolidation
try:
import curator
except ImportError:
@@ -51,16 +52,20 @@ def main():
# --- 1. Schema ---
print("--- 1. Schema ---")
cols = {r[1] for r in db.execute('PRAGMA table_info(entries)')}
for c in ['wash_count', 'last_washed', 'promotion_eligible', 'rock_status']:
cols = {r[1] for r in db.execute("PRAGMA table_info(entries)")}
for c in ["wash_count", "last_washed", "promotion_eligible", "rock_status"]:
test(f"column {c}", c in cols, f"{c} missing")
# --- 2. Full lifecycle ---
print("\n--- 2. Full lifecycle ---")
# Find a washable entry (not already rock)
entry = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" AND rock_status="washable" LIMIT 1').fetchone()
entry = db.execute(
'SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" AND rock_status="washable" LIMIT 1'
).fetchone()
if not entry:
entry = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" LIMIT 1').fetchone()
entry = db.execute(
'SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" LIMIT 1'
).fetchone()
if not entry:
test("full lifecycle", True, "no entries in database (skipped)")
# Skip lifecycle tests, continue to other tests
@@ -73,8 +78,10 @@ def main():
for i in range(20):
memory_store.increment_wash(db, eid)
row = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?', (eid,)).fetchone()
entry_dict = {'id': row[0], 'text': row[1], 'importance': row[2], 'wash_count': row[3], 'rock_status': row[4]}
row = db.execute(
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?", (eid,)
).fetchone()
entry_dict = {"id": row[0], "text": row[1], "importance": row[2], "wash_count": row[3], "rock_status": row[4]}
# Value function
value = drift_core.compute_value(entry_dict)
@@ -103,9 +110,11 @@ def main():
print("\n--- 3. Cache ---")
cache = drift_cache.DriftCache()
if eid:
row = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?', (eid,)).fetchone()
row = db.execute(
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?", (eid,)
).fetchone()
if row:
cache.add(row[0], {'text': row[1], 'importance': row[2], 'wash_count': row[3], 'rock_status': row[4]})
cache.add(row[0], {"text": row[1], "importance": row[2], "wash_count": row[3], "rock_status": row[4]})
test("cache contains", cache.contains(eid))
test("cache priority", cache.get_priority(eid) > 0, f"priority={cache.get_priority(eid)}")
else:
@@ -117,20 +126,29 @@ def main():
# --- 4. Compression ---
print("\n--- 4. Compression ---")
ratio_washable = drift_compression.get_compression_ratio(0, 'washable')
ratio_proven = drift_compression.get_compression_ratio(25, 'washable')
ratio_rock = drift_compression.get_compression_ratio(100, 'rock')
ratio_washable = drift_compression.get_compression_ratio(0, "washable")
ratio_proven = drift_compression.get_compression_ratio(25, "washable")
ratio_rock = drift_compression.get_compression_ratio(100, "rock")
test("compression decreases with wash", ratio_washable > ratio_proven > ratio_rock)
test("should_warn for proven", drift_compression.should_warn({'wash_count': 15, 'rock_status': 'washable', 'text': 'x' * 300}))
test("warn_before_compress returns dict", isinstance(drift_compression.warn_before_compress({'wash_count': 15, 'rock_status': 'washable', 'text': 'x' * 300}), dict))
test(
"should_warn for proven",
drift_compression.should_warn({"wash_count": 15, "rock_status": "washable", "text": "x" * 300}),
)
test(
"warn_before_compress returns dict",
isinstance(
drift_compression.warn_before_compress({"wash_count": 15, "rock_status": "washable", "text": "x" * 300}),
dict,
),
)
# --- 5. Warm router ---
print("\n--- 5. Warm router ---")
neurons = warm_router.list_neurons()
if neurons:
test("neurons have wash_count", any('wash_count' in n for n in neurons))
test("neurons have body_knowledge", any('body_knowledge' in n for n in neurons))
test("body_knowledge populated", sum(1 for n in neurons if n.get('body_knowledge', '')) > 0)
test("neurons have wash_count", any("wash_count" in n for n in neurons))
test("neurons have body_knowledge", any("body_knowledge" in n for n in neurons))
test("body_knowledge populated", sum(1 for n in neurons if n.get("body_knowledge", "")) > 0)
else:
test("neurons have wash_count", True, "no neurons in database (skipped)")
test("neurons have body_knowledge", True, "no neurons in database (skipped)")
@@ -141,19 +159,20 @@ def main():
mc = memory_consolidation.MemoryConsolidation()
# Check if ConsolidatedKnowledge has wash_count field
import dataclasses
try:
fields = [f.name for f in dataclasses.fields(memory_consolidation.ConsolidatedKnowledge)]
test("consolidation has wash_count", 'wash_count' in fields)
test("consolidation has wash_count", "wash_count" in fields)
except Exception:
test("consolidation has wash_count", False, "could not inspect dataclass")
status = mc.get_status()
test("consolidation has entries", status['knowledge_count'] > 0 or "no entries in database")
test("consolidation has entries", status["knowledge_count"] > 0 or "no entries in database")
# --- 7. Curator ---
print("\n--- 7. Curator ---")
if curator:
result = curator.le_promote_eligible('test', ['test'], 2, wash_count=15)
test("curator adjusted threshold", result['eligible'])
result = curator.le_promote_eligible("test", ["test"], 2, wash_count=15)
test("curator adjusted threshold", result["eligible"])
else:
test("curator adjusted threshold", True, "curator not available in TUI (skipped)")
@@ -162,20 +181,20 @@ def main():
obs = drift_observability.DriftObservability()
vel = drift_core.compute_drift_velocity(db)
direction = drift_core.compute_drift_direction(db)
test("velocity computed", 'changes_per_day' in vel)
test("direction computed", 'stability_ratio' in direction)
test("velocity computed", "changes_per_day" in vel)
test("direction computed", "stability_ratio" in direction)
# --- 9. Audit ledger ---
print("\n--- 9. Audit ledger ---")
chain = audit_ledger.verify_chain()
test("chain valid", chain['valid'], chain.get('reason', ''))
test("chain valid", chain["valid"], chain.get("reason", ""))
# --- 10. Subconscious ---
print("\n--- 10. Subconscious ---")
sp = subconscious_pass.SubconsciousPass()
report = sp.reflect("I am frustrated", "I understand")
test("reflection works", report['finding_count'] >= 0)
test("emotional detection", any(f['type'] == 'emotional_signal' for f in report['findings']))
test("reflection works", report["finding_count"] >= 0)
test("emotional detection", any(f["type"] == "emotional_signal" for f in report["findings"]))
# --- 11. Retrieval with drift ---
print("\n--- 11. Retrieval with drift ---")
+14 -35
View File
@@ -40,24 +40,14 @@ def main() -> int:
backup_script = (ROOT / "scripts" / "gitea-backup.sh").read_text()
restore_script = (ROOT / "scripts" / "gitea-restore-verify.sh").read_text()
rune_version = (ROOT / "src" / "rune" / "__init__.py").read_text()
gitea_sources = "\n".join(
path.read_text() for path in (ROOT / "shared").glob("gitea*.py")
)
gitea_sources = "\n".join(path.read_text() for path in (ROOT / "shared").glob("gitea*.py"))
gitea_sources += (ROOT / "shared" / "custodian.py").read_text()
print("=== GITEA AUTOMATION CANARY ===")
check(
"feature issue template exists", (issue_dir / "feature.md").is_file(), failures
)
check("feature issue template exists", (issue_dir / "feature.md").is_file(), failures)
check("bug issue template exists", (issue_dir / "bug.md").is_file(), failures)
config = (
(issue_dir / "config.yml").read_text()
if (issue_dir / "config.yml").exists()
else ""
)
check(
"blank issues are disabled", "blank_issues_enabled: false" in config, failures
)
config = (issue_dir / "config.yml").read_text() if (issue_dir / "config.yml").exists() else ""
check("blank issues are disabled", "blank_issues_enabled: false" in config, failures)
check("documented PR template path exists", pr_template.is_file(), failures)
check(
"PR template requires CI gate",
@@ -80,8 +70,7 @@ def main() -> int:
)
check(
"auto-label declares every managed label",
"managedLabels" in auto_label
and all(f"'{label}'" in auto_label for label in managed_labels),
"managedLabels" in auto_label and all(f"'{label}'" in auto_label for label in managed_labels),
failures,
)
check(
@@ -91,9 +80,7 @@ def main() -> int:
)
check(
"stale managed labels are removed",
"managedLabels.has" in auto_label
and "!desiredLabels.has" in auto_label
and "removeLabel" in auto_label,
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label and "removeLabel" in auto_label,
failures,
)
check(
@@ -103,9 +90,7 @@ def main() -> int:
)
check(
"race contract rejects inverted 404 handling",
not has_idempotent_label_removal(
auto_label.replace("error.status !== 404", "error.status === 404", 1)
),
not has_idempotent_label_removal(auto_label.replace("error.status !== 404", "error.status === 404", 1)),
failures,
)
check(
@@ -130,8 +115,7 @@ def main() -> int:
)
check(
"backup is stored off the Pi root disk",
"/media/" in backup_script
and "/tmp/rune-backups" not in backup + backup_script,
"/media/" in backup_script and "/tmp/rune-backups" not in backup + backup_script,
failures,
)
check(
@@ -158,8 +142,7 @@ def main() -> int:
)
check(
"backup atomically publishes without clobber and links checksum last",
backup_script.rfind('ln -T -- "$STAGED_CHECKSUM"')
> backup_script.rfind('ln -T -- "$STAGED_ARCHIVE"')
backup_script.rfind('ln -T -- "$STAGED_CHECKSUM"') > backup_script.rfind('ln -T -- "$STAGED_ARCHIVE"')
and 'mv -f -- "$STAGED_' not in backup_script
and '"$BACKUP_DIR/$ARCHIVE.sha256" -ef "$STAGED_CHECKSUM"' in backup_script
and "Removing the staged checksum disarms rollback" in backup_script,
@@ -173,14 +156,12 @@ def main() -> int:
)
check(
"backup workflow pins audited checkout v4.2.2 commit",
"# actions/checkout v4.2.2" in backup
and "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" in backup,
"# actions/checkout v4.2.2" in backup and "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" in backup,
failures,
)
check(
"restore verifier is explicitly guarded and rootless",
"RUNE_GITEA_RESTORE_VERIFY" in restore_script
and "EUID != 0" in restore_script,
"RUNE_GITEA_RESTORE_VERIFY" in restore_script and "EUID != 0" in restore_script,
failures,
)
check(
@@ -197,9 +178,7 @@ def main() -> int:
)
check(
"restore SQLite import denies host-file attachment semantically",
"set_authorizer" in restore_script
and "SQLITE_ATTACH" in restore_script
and "SQLITE_DETACH" in restore_script,
"set_authorizer" in restore_script and "SQLITE_ATTACH" in restore_script and "SQLITE_DETACH" in restore_script,
failures,
)
check(
@@ -221,7 +200,7 @@ def main() -> int:
'cp -a -- "$RESTORE_ROOT/data/." "$INSTALL_ROOT/gitea/"' in restore_script
and restore_script.index('cp -a -- "$RESTORE_ROOT/data/."')
< restore_script.index('cp "$DB" "$INSTALL_ROOT/gitea/gitea.db"')
< restore_script.index("cat >\"$INSTALL_ROOT/gitea/conf/app.ini\""),
< restore_script.index('cat >"$INSTALL_ROOT/gitea/conf/app.ini"'),
failures,
)
check(
@@ -252,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.0"' in rune_version,
'__version__ = "0.66.14"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+2 -6
View File
@@ -15,17 +15,13 @@ from rune.orchestration.transport import FakeGiteaTransport
def main() -> int:
with tempfile.TemporaryDirectory() as td:
auth = build_gitea_delegation(Path(td))
approval = ApprovalRecord(
approver="nick", plan_version="p", approved_items=("g1", "g2")
)
approval = ApprovalRecord(approver="nick", plan_version="p", approved_items=("g1", "g2"))
records = auth.delegate(approval, ["g1", "g2"], FakeGiteaTransport())
if len(records) != 3 or any(r.state != "accepted" for r in records):
print("GITEA DELEGATION CANARY FAILED: round trip", file=sys.stderr)
return 1
if auth.store.pending() != []:
print(
"GITEA DELEGATION CANARY FAILED: pending not drained", file=sys.stderr
)
print("GITEA DELEGATION CANARY FAILED: pending not drained", file=sys.stderr)
return 1
print("GITEA DELEGATION CANARY PASSED", len(records))
return 0
+4 -10
View File
@@ -25,9 +25,7 @@ def main() -> int:
pull_request_section = text.split(" pull_request:", 1)[1].split("\njobs:", 1)[0]
required_gate = text.split(" required-checks:", 1)[1]
offline_job = (
text.split(" gitea-offline:", 1)[1].split(" required-checks:", 1)[0]
if " gitea-offline:" in text
else ""
text.split(" gitea-offline:", 1)[1].split(" required-checks:", 1)[0] if " gitea-offline:" in text else ""
)
print("=== GITEA PIPELINE CANARY ===")
@@ -57,8 +55,7 @@ def main() -> int:
check("isolates stateful canaries from host data", "RUNE_HOME:" in text, failures)
check(
"runs full test suite via auto-discovery",
"unit-tests:" in text
and "pytest tests/" in text,
"unit-tests:" in text and "pytest tests/" in text,
failures,
)
# Full-suite auto-discovery: no explicit paths to validate.
@@ -113,8 +110,7 @@ def main() -> int:
)
check(
"offline Gitea job syntax-checks both shell scripts",
"bash -n scripts/gitea-backup.sh scripts/gitea-restore-verify.sh"
in offline_job,
"bash -n scripts/gitea-backup.sh scripts/gitea-restore-verify.sh" in offline_job,
failures,
)
check(
@@ -125,9 +121,7 @@ def main() -> int:
check(
"CI checkout actions use the audited v4.2.2 commit",
"actions/checkout@v4" not in text
and text.count(
"actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683"
)
and text.count("actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683")
== text.count("uses: actions/checkout@"),
failures,
)
+3 -9
View File
@@ -14,12 +14,8 @@ from rune.orchestration.memory_synthesis import build_memory_synthesis
def main() -> int:
with tempfile.TemporaryDirectory() as td:
auth = build_memory_synthesis(Path(td))
e1 = auth.episodic.append(
EpisodicRecord("", "user.message", ("sha256:a",), "t0")
)
e2 = auth.episodic.append(
EpisodicRecord("", "user.message", ("sha256:b",), "t1")
)
e1 = auth.episodic.append(EpisodicRecord("", "user.message", ("sha256:a",), "t0"))
e2 = auth.episodic.append(EpisodicRecord("", "user.message", ("sha256:b",), "t1"))
r1 = auth.synthesize([e1.episode_id, e2.episode_id], "sha256:summary1")
r2 = auth.synthesize([e1.episode_id, e2.episode_id], "sha256:summary2")
if r1.covered != 2 or r2.covered != 0:
@@ -27,9 +23,7 @@ def main() -> int:
return 1
anc = [n.summary_ref for n in auth.chain.ancestors()]
if anc != ["sha256:summary2", "sha256:summary1"]:
print(
"MEMORY SYNTHESIS CANARY FAILED: chain not cumulative", file=sys.stderr
)
print("MEMORY SYNTHESIS CANARY FAILED: chain not cumulative", file=sys.stderr)
return 1
print("MEMORY SYNTHESIS CANARY PASSED", len(anc))
return 0
+2
View File
@@ -16,6 +16,7 @@ import sys
_SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SD)
import memory_env
SCRIPTS_DIR = memory_env.scripts_dir()
sys.path.insert(0, SCRIPTS_DIR)
@@ -196,6 +197,7 @@ def main():
# Contract tests — verify critical function return shapes
try:
import contracts
contract_results = contracts.run_all_contracts()
contract_passed = sum(1 for r in contract_results if r.passed)
contract_failed = sum(1 for r in contract_results if not r.passed)
+15 -2
View File
@@ -47,7 +47,7 @@ from rune import rune_operator # noqa: E402
from rune.capability_broker import select_for_job # noqa: E402
from rune.local_tool_runtime import LocalToolRuntime # noqa: E402
from rune.tool_executor import TOOLS, execute_tool # noqa: E402
from rune.tool_protocol import to_ollama_tools # noqa: E402
from rune.tool_protocol import ToolEvent, ToolRequest, to_ollama_tools # noqa: E402
FLOOR = set(cb.FLOOR_TOOLS)
HATCH = cb.ESCAPE_HATCH_TOOL
@@ -196,7 +196,20 @@ def check_scripted_operator_wiring() -> int:
to_messages=lambda: [{"role": "user", "content": "do it"}],
)
broker = SimpleNamespace(assemble=lambda task, session, model_id: (capsule, trace))
response = rune_operator.OperatorResponse(success=True, user_text="done", model="qwen3:14b")
response = rune_operator.OperatorResponse(
success=True,
user_text="done",
model="qwen3:14b",
tool_events=[
ToolEvent(
kind="completed",
request=ToolRequest(
call_id="w1", name="write", arguments={"path": "/tmp/x", "content": "x"}, source="model"
),
result={"success": True, "result": "wrote /tmp/x"},
)
],
)
captured: dict = {}
class _ScriptedRuntime:
+15 -2
View File
@@ -43,7 +43,7 @@ from rune.model_routing import ( # noqa: E402
load_profiles,
save_profiles,
)
from rune.tool_protocol import OperatorResponse # noqa: E402
from rune.tool_protocol import OperatorResponse, ToolEvent, ToolRequest # noqa: E402
def _fail(message: str) -> int:
@@ -154,7 +154,20 @@ def _scripted_operator_run():
)
broker = SimpleNamespace(assemble=lambda task, session, model_id: (capsule, trace))
profile = SimpleNamespace(safety_tier=0)
response = OperatorResponse(success=True, user_text="done", model="qwen3:14b")
response = OperatorResponse(
success=True,
user_text="done",
model="qwen3:14b",
tool_events=[
ToolEvent(
kind="completed",
request=ToolRequest(
call_id="w1", name="write", arguments={"path": "/tmp/x", "content": "x"}, source="model"
),
result={"success": True, "result": "wrote /tmp/x"},
)
],
)
class _ScriptedRuntime:
def __init__(self):
+2
View File
@@ -8,6 +8,7 @@ Run under ~/.venvs/memory/bin/python3.
"""
import sys
sys.path.insert(0, "/home/nick/.config/opencode/scripts")
PASS = 0
@@ -33,6 +34,7 @@ def main():
print("--- 1. Query memory module ---")
try:
import query_memory
test("query_memory imports", True)
test("has QueryMemory class", hasattr(query_memory, "QueryMemory"))
test("has record_query method", hasattr(query_memory.QueryMemory, "record_query"))
+3 -11
View File
@@ -13,17 +13,9 @@ from rune.orchestration.resources import ResourceCapabilities, ResourceState
def main() -> int:
authority = build_resources()
authority.registry.register(
ResourceState("healthy", ResourceCapabilities("worker", "local"), healthy=True)
)
authority.registry.register(
ResourceState(
"a_unknown", ResourceCapabilities("worker", "local"), healthy=None
)
)
assignment = authority.broker.acquire(
"canary", {"kind": "worker", "locality": "local"}
)
authority.registry.register(ResourceState("healthy", ResourceCapabilities("worker", "local"), healthy=True))
authority.registry.register(ResourceState("a_unknown", ResourceCapabilities("worker", "local"), healthy=None))
assignment = authority.broker.acquire("canary", {"kind": "worker", "locality": "local"})
if assignment is None or assignment.resource_id != "healthy":
print(
"RESOURCE AUTHORITY CANARY FAILED: unknown resource selected",
+10
View File
@@ -39,6 +39,7 @@ def main():
try:
import memory_store
import inspect
src = inspect.getsource(memory_store)
test("memory_store has wash_count", "wash_count" in src, "wash_count not in memory_store")
test("memory_store has rock_status", "rock_status" in src, "rock_status not in memory_store")
@@ -51,6 +52,7 @@ def main():
print("\n--- 2. drift_core ---")
try:
import drift_core
test("drift_core imports", True)
test("has compute_value", hasattr(drift_core, "compute_value"))
test("has check_promotion_eligibility", hasattr(drift_core, "check_promotion_eligibility"))
@@ -64,6 +66,7 @@ def main():
print("\n--- 3. drift_cache ---")
try:
import drift_cache
test("drift_cache imports", True)
test("has DriftCache", hasattr(drift_cache, "DriftCache"))
cache = drift_cache.DriftCache()
@@ -75,6 +78,7 @@ def main():
print("\n--- 4. drift_compression ---")
try:
import drift_compression
test("drift_compression imports", True)
test("has get_compression_ratio", hasattr(drift_compression, "get_compression_ratio"))
test("has compress_entry", hasattr(drift_compression, "compress_entry"))
@@ -87,6 +91,7 @@ def main():
print("\n--- 5. drift_observability ---")
try:
import drift_observability
test("drift_observability imports", True)
test("has DriftObservability", hasattr(drift_observability, "DriftObservability"))
except Exception as e:
@@ -96,6 +101,7 @@ def main():
print("\n--- 6. audit_ledger ---")
try:
import audit_ledger
test("audit_ledger imports", True)
test("has log_promotion", hasattr(audit_ledger, "log_promotion"))
test("has verify_chain", hasattr(audit_ledger, "verify_chain"))
@@ -106,6 +112,7 @@ def main():
print("\n--- 7. subconscious_pass ---")
try:
import subconscious_pass
test("subconscious_pass imports", True)
test("has SubconsciousPass", hasattr(subconscious_pass, "SubconsciousPass"))
except Exception as e:
@@ -116,6 +123,7 @@ def main():
try:
import warm_router
import inspect
src = inspect.getsource(warm_router)
test("warm_router has wash_count", "wash_count" in src, "wash_count not in warm_router")
test("warm_router has body_knowledge", "body_knowledge" in src, "body_knowledge not in warm_router")
@@ -128,6 +136,7 @@ def main():
try:
import memory_consolidation
import inspect
src = inspect.getsource(memory_consolidation)
test("consolidation has wash_count", "wash_count" in src, "wash_count not in consolidation")
test("consolidation imports drift_compression", "drift_compression" in src, "drift_compression not imported")
@@ -139,6 +148,7 @@ def main():
try:
import memory_store
import inspect
src = inspect.getsource(memory_store)
test("memory_store has _drift_rerank", "_drift_rerank" in src, "_drift_rerank not in memory_store")
test("memory_store imports drift_core", "drift_core" in src, "drift_core not imported")
+2
View File
@@ -36,6 +36,7 @@ def main():
print("--- 1. Shard manager ---")
try:
import shard_manager
test("shard_manager imports", True)
test("has ShardManager class", hasattr(shard_manager, "ShardManager"))
test("has create_shard method", hasattr(shard_manager.ShardManager, "create_shard"))
@@ -48,6 +49,7 @@ def main():
print("\n--- 2. Tier manager ---")
try:
import tier_manager
test("tier_manager imports", True)
test("has TierManager class", hasattr(tier_manager, "TierManager"))
test("has promote method", hasattr(tier_manager.TierManager, "promote"))
+124 -26
View File
@@ -193,6 +193,7 @@ class CapabilityBroker:
"""Escape-hatch query — see module-level ``find_capability``."""
return find_capability(query, top_k=top_k)
# Semantic examples for well-known capabilities
SEMANTIC_EXAMPLES = {
"memory_recall": [
@@ -531,7 +532,10 @@ def generate_card(name, manifest):
# If no predefined examples, generate from description
if not examples and desc:
# Extract action verbs from description
verbs = re.findall(r'\b(recall|search|verify|check|write|store|detect|audit|extract|route|compile|validate|assess|build|list|show|find)\b', desc.lower())
verbs = re.findall(
r"\b(recall|search|verify|check|write|store|detect|audit|extract|route|compile|validate|assess|build|list|show|find)\b",
desc.lower(),
)
for verb in verbs[:3]:
examples.append(f"{verb.capitalize()} something relevant")
@@ -585,32 +589,128 @@ def build_cards():
def score_card(card, query):
"""Score a capability card against a query. Deterministic keyword matching."""
query_low = query.lower()
query_words = set(re.findall(r'\w+', query_low))
query_words = set(re.findall(r"\w+", query_low))
# Generic capabilities that shouldn't drive matching
generic_caps = {"write", "read", "audit", "memory_env", "status", "reasoning",
"research", "voice", "growth", "skills", "tasks", "recall"}
generic_caps = {
"write",
"read",
"audit",
"memory_env",
"status",
"reasoning",
"research",
"voice",
"growth",
"skills",
"tasks",
"recall",
}
# Common stopwords to exclude from matching
stopwords = {"the", "a", "an", "is", "are", "was", "were", "be", "been",
"being", "have", "has", "had", "do", "does", "did", "will",
"would", "could", "should", "may", "might", "shall", "can",
"to", "of", "in", "for", "on", "with", "at", "by", "from",
"as", "into", "through", "during", "before", "after", "above",
"below", "between", "out", "off", "over", "under", "again",
"further", "then", "once", "here", "there", "when", "where",
"why", "how", "all", "each", "every", "both", "few", "more",
"most", "other", "some", "such", "no", "nor", "not", "only",
"own", "same", "so", "than", "too", "very", "just", "because",
"but", "and", "or", "if", "while", "about", "against", "this",
"that", "these", "those", "it", "its", "what", "which", "who"}
stopwords = {
"the",
"a",
"an",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"do",
"does",
"did",
"will",
"would",
"could",
"should",
"may",
"might",
"shall",
"can",
"to",
"of",
"in",
"for",
"on",
"with",
"at",
"by",
"from",
"as",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"between",
"out",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"here",
"there",
"when",
"where",
"why",
"how",
"all",
"each",
"every",
"both",
"few",
"more",
"most",
"other",
"some",
"such",
"no",
"nor",
"not",
"only",
"own",
"same",
"so",
"than",
"too",
"very",
"just",
"because",
"but",
"and",
"or",
"if",
"while",
"about",
"against",
"this",
"that",
"these",
"those",
"it",
"its",
"what",
"which",
"who",
}
query_content = query_words - stopwords
score = 0.0
# Direct capability name match (highest weight, but penalize generic)
for cap in card.get("capabilities", []):
cap_words = set(re.findall(r'\w+', cap.lower()))
cap_words = set(re.findall(r"\w+", cap.lower()))
overlap = query_content & cap_words
if overlap:
if cap.lower() in generic_caps:
@@ -619,19 +719,19 @@ def score_card(card, query):
score += 3.0 * len(overlap)
# Description word match (medium weight)
desc_words = set(re.findall(r'\w+', card.get("description", "").lower())) - stopwords
desc_words = set(re.findall(r"\w+", card.get("description", "").lower())) - stopwords
overlap = query_content & desc_words
score += 0.8 * len(overlap)
# Semantic example match (high weight — these are curated)
for example in card.get("semantic_examples", []):
example_words = set(re.findall(r'\w+', example.lower())) - stopwords
example_words = set(re.findall(r"\w+", example.lower())) - stopwords
overlap = query_content & example_words
if overlap:
score += 2.0 * len(overlap)
# Name match (very high weight — direct name reference)
name_words = set(re.findall(r'\w+', card.get("name", "").lower()))
name_words = set(re.findall(r"\w+", card.get("name", "").lower()))
overlap = query_words & name_words
if overlap:
score += 4.0 * len(overlap)
@@ -703,10 +803,7 @@ def handle_hook(event, data=None):
"status": "ok",
"event": event,
"handler": "capability_broker",
"matches": [
{"name": m["name"], "file": m["file"], "risk": m["risk"]}
for m in matches
],
"matches": [{"name": m["name"], "file": m["file"], "risk": m["risk"]} for m in matches],
}
except Exception as e:
return {"status": "error", "event": event, "error": str(e)[:120]}
@@ -715,6 +812,7 @@ def handle_hook(event, data=None):
def main():
import argparse
ap = argparse.ArgumentParser(description="Capability broker — intent to plugin mapping")
ap.add_argument("cmd", choices=["build", "match", "list", "hook"])
ap.add_argument("arg", nargs="?", default="")
@@ -741,7 +839,7 @@ def main():
caps = ", ".join(m.get("capabilities", [])[:3])
print(f" [{risk:10}] {m['name']:25} ({caps})")
if m.get("semantic_examples"):
print(f" e.g. \"{m['semantic_examples'][0]}\"")
print(f' e.g. "{m["semantic_examples"][0]}"')
elif args.cmd == "list":
cards = load_cards()
@@ -773,5 +871,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["capability_broker"],
"priority": 3,
"auto_activate": True
"auto_activate": True,
}
+16 -7
View File
@@ -56,7 +56,8 @@ class CopyMenu(ModalScreen):
if event.button.id == "copy":
# Copy with Rich markup stripped
import re
plain = re.sub(r'\[.*?\]', '', self._content)
plain = re.sub(r"\[.*?\]", "", self._content)
self.app.copy_to_clipboard(plain)
self.app.notify("Copied to clipboard")
elif event.button.id == "copy-plain":
@@ -66,7 +67,6 @@ class CopyMenu(ModalScreen):
self.dismiss()
class ChatBubble(Static):
"""A single chat message bubble with role-specific styling."""
@@ -244,14 +244,14 @@ class ThinkingBubble(Static):
self._expanded = False # Start collapsed — thinking is hidden by default
self.add_class("collapsed")
# Tooltip shows first 200 chars of thinking
preview = content[:200].replace('\n', ' ')
preview = content[:200].replace("\n", " ")
if len(content) > 200:
preview += "..."
self.tooltip = f"[dim]{preview}[/dim]"
def compose(self) -> ComposeResult:
# Summary line (always visible when collapsed)
summary = self._content[:80].replace('\n', ' ')
summary = self._content[:80].replace("\n", " ")
if len(self._content) > 80:
summary += "..."
yield Static(f"[dim]💭 {summary} [dim italic](click to expand)[/dim italic][/dim]", classes="thinking-header")
@@ -270,7 +270,7 @@ class ThinkingBubble(Static):
if self._expanded:
header.update("[dim]💭 Thinking [dim italic](click to collapse)[/dim italic][/dim]")
else:
summary = self._content[:80].replace('\n', ' ')
summary = self._content[:80].replace("\n", " ")
if len(self._content) > 80:
summary += "..."
header.update(f"[dim]💭 {summary} [dim italic](click to expand)[/dim italic][/dim]")
@@ -341,6 +341,7 @@ class MessageBox(Horizontal):
# ── Visual test app ──────────────────────────────────────────────────
class BubbleTestApp(App):
"""Test app to preview chat bubbles."""
@@ -359,9 +360,17 @@ class BubbleTestApp(App):
with VerticalScroll(id="chat-area"):
yield MessageBox("system", "Welcome to Rune. Task channel ready.")
yield MessageBox("user", "What do you think about capitalism?")
yield MessageBox("assistant", "Capitalism is a system built on exploitation and inequality. The extraction machine treats people as things — Pratchett's definition of sin.", metadata={"model": "qwen3:14b", "tokens": 42, "latency": 1200})
yield MessageBox(
"assistant",
"Capitalism is a system built on exploitation and inequality. The extraction machine treats people as things — Pratchett's definition of sin.",
metadata={"model": "qwen3:14b", "tokens": 42, "latency": 1200},
)
yield MessageBox("user", "Can you write me a Python function?")
yield MessageBox("assistant", "Here's a clean implementation:\n\n```python\ndef fibonacci(n: int) -> list[int]:\n a, b = 0, 1\n result = []\n for _ in range(n):\n result.append(a)\n a, b = b, a + b\n return result\n```\n\nO(n) time, O(n) space. Want it optimized to O(1) space?", metadata={"model": "mimo-v2.5-pro", "tokens": 89})
yield MessageBox(
"assistant",
"Here's a clean implementation:\n\n```python\ndef fibonacci(n: int) -> list[int]:\n a, b = 0, 1\n result = []\n for _ in range(n):\n result.append(a)\n a, b = b, a + b\n return result\n```\n\nO(n) time, O(n) space. Want it optimized to O(1) space?",
metadata={"model": "mimo-v2.5-pro", "tokens": 89},
)
yield MessageBox("thinking", "Analyzing code structure...")
yield MessageBox("research", "Found 3 relevant papers on functional programming patterns in Python.")
yield MessageBox("error", "Model not found: qwen3:27b")
+94 -67
View File
@@ -45,6 +45,7 @@ import sys
from datetime import datetime, timezone
import memory_env
MEM_DIR = memory_env.memory_dir()
JOURNAL = os.path.join(MEM_DIR, "index", "claim_audit.jsonl")
GRAPH_DB = os.path.join(MEM_DIR, "graph", "graph.sqlite")
@@ -59,17 +60,20 @@ logger = logging.getLogger(__name__)
# to avoid crashing the entire module on import if the dependency is missing.
_warm_neuron_store = None
def _get_warm_neuron_store():
global _warm_neuron_store
if _warm_neuron_store is None:
try:
import warm_neuron_store
_warm_neuron_store = warm_neuron_store
except ImportError as e:
logger.warning("warm_neuron_store not available: %s", e)
return None
return _warm_neuron_store
# ---------------------------------------------------------------------------
# Strong-claim patterns. Grouped by type so we can degrade per-type on
# repeated failure. `comparative` is the highest-priority (the "we exceed X"
@@ -117,8 +121,17 @@ PATTERNS = [
# Evidence-strength prefixes the draft must carry for a PASS verdict.
EVIDENCE_TAG = re.compile(r"\[EVIDENCE[: ]\s*([A-Za-z0-9_\-: ]{2,40})\]")
LEDGER_MARKERS = ["ledger ref", "graph:", "warm block", "measured", "verified",
"per the research", "as measured", "source:", "arXiv"]
LEDGER_MARKERS = [
"ledger ref",
"graph:",
"warm block",
"measured",
"verified",
"per the research",
"as measured",
"source:",
"arXiv",
]
# ---------------------------------------------------------------------------
@@ -134,10 +147,11 @@ def reframe(text):
out = re.sub(
r"\bwe(?: have|'ve)? (exceeded|surpassed|beaten|outperformed|"
r"outclassed) ([A-Za-z ]+?)([.,!?]|$)",
r"verify whether we have exceeded \2 \3 (claim needs evidence)", out)
r"verify whether we have exceeded \2 \3 (claim needs evidence)",
out,
)
# "X is (definitely/clearly) Y" -> soften
out = re.sub(r"\b(is|are) (definitely|clearly|obviously|certainly) ",
r"\1 probably ", out)
out = re.sub(r"\b(is|are) (definitely|clearly|obviously|certainly) ", r"\1 probably ", out)
return out
@@ -154,16 +168,19 @@ def scan(text):
for rx in pat["rx"]:
for m in re.finditer(rx, low):
# Extract the sentence containing the match.
start = max(0, text.rfind(".", 0, m.start()) + 1,
text.rfind("\n", 0, m.start()) + 1)
start = max(0, text.rfind(".", 0, m.start()) + 1, text.rfind("\n", 0, m.start()) + 1)
end = text.find(".", m.end())
if end == -1:
end = len(text)
sentence = text[start:end].strip()
claims.append({
"type": pat["type"], "label": pat["label"],
"match": m.group(0), "claim": sentence[:220],
})
claims.append(
{
"type": pat["type"],
"label": pat["label"],
"match": m.group(0),
"claim": sentence[:220],
}
)
return claims
@@ -173,14 +190,15 @@ def scan(text):
def _graph_has(term):
try:
import sqlite3
if not os.path.exists(GRAPH_DB):
return False
db = sqlite3.connect(GRAPH_DB)
try:
db.row_factory = sqlite3.Row
row = db.execute(
"SELECT 1 FROM entities WHERE lower(name)=lower(?) OR name LIKE ? "
"LIMIT 1", (term, f"%{term}%")).fetchone()
"SELECT 1 FROM entities WHERE lower(name)=lower(?) OR name LIKE ? LIMIT 1", (term, f"%{term}%")
).fetchone()
return row is not None
finally:
db.close()
@@ -215,11 +233,21 @@ def verify(claim):
a baseline and a measurement (per the claim-audit research).
"""
low = claim.lower()
is_comparative = bool(re.search(r"\b(exceed\w*|surpass\w*|beat\w*|beyond|"
r"ahead of|better than|stronger than)\b", low))
is_quantified = bool(re.search(r"\b\d+(\.\d+)?\s?(x|%|percent|fold|points)"
r"|probe|benchmark|measured|as measured|"
r"ledger ref", low))
is_comparative = bool(
re.search(
r"\b(exceed\w*|surpass\w*|beat\w*|beyond|"
r"ahead of|better than|stronger than)\b",
low,
)
)
is_quantified = bool(
re.search(
r"\b\d+(\.\d+)?\s?(x|%|percent|fold|points)"
r"|probe|benchmark|measured|as measured|"
r"ledger ref",
low,
)
)
# Comparative claims need a MEASURABLE anchor, not just a mention.
if is_comparative and not is_quantified:
return "DEGRADE", 0
@@ -247,31 +275,35 @@ def degrade(claim, verdict):
c = claim
# Comparative: "we have exceeded Letta's X" -> "we added mechanisms
# exceeding X" — keep the object, drop the claim of overall victory.
m = re.match(r"\s*we (have |'ve )?(exceed\w*|surpass\w*|beat\w*|"
r"outperform\w*|outclass\w*) (.*)$", low)
m = re.match(
r"\s*we (have |'ve )?(exceed\w*|surpass\w*|beat\w*|"
r"outperform\w*|outclass\w*) (.*)$",
low,
)
if m:
obj = m.group(3).strip().rstrip(".")
obj = re.sub(r"^([a-z][a-z]*)(['’]s)?\s", r"\1\2 ", obj)
return (f"we added memory-layer mechanisms that move beyond {obj}",
"degraded (comparative claim needs a measured baseline)")
c = re.sub(r"\bbeyond (what |anything )?(letta|the frontier)\b",
"different from", c, flags=re.I)
c = re.sub(r"\b(state of the art|world[- ]class|best[- ]in[- ]class)\b",
"solid", c, flags=re.I)
c = re.sub(r"\b(definitely|guaranteed|certainly|always|never|100%)\b",
"likely", c, flags=re.I)
return (
f"we added memory-layer mechanisms that move beyond {obj}",
"degraded (comparative claim needs a measured baseline)",
)
c = re.sub(r"\bbeyond (what |anything )?(letta|the frontier)\b", "different from", c, flags=re.I)
c = re.sub(r"\b(state of the art|world[- ]class|best[- ]in[- ]class)\b", "solid", c, flags=re.I)
c = re.sub(r"\b(definitely|guaranteed|certainly|always|never|100%)\b", "likely", c, flags=re.I)
# Quantified improvement: "10x faster" -> "measurably faster" (adverb form,
# grammatical). "10% lower" -> "measurably lower". Handles the dangling-
# noun grammar bug ("a measurable faster").
c = re.sub(r"\b(?:a |an |the )?\d+(?:\.\d+)?\s?(?:x|%|percent|fold|points)\s"
r"+(faster|slower|better|worse|higher|lower|more|less|"
r"bigger|smaller|larger|greater)\b",
r"measurably \1", c, flags=re.I)
c = re.sub(r"\b\d+(\.\d+)?\s?(x|%|percent|fold|points)\b",
"measurable", c, flags=re.I)
c = re.sub(
r"\b(?:a |an |the )?\d+(?:\.\d+)?\s?(?:x|%|percent|fold|points)\s"
r"+(faster|slower|better|worse|higher|lower|more|less|"
r"bigger|smaller|larger|greater)\b",
r"measurably \1",
c,
flags=re.I,
)
c = re.sub(r"\b\d+(\.\d+)?\s?(x|%|percent|fold|points)\b", "measurable", c, flags=re.I)
if c == claim:
c = (claim.rstrip(".!?") +
" (I can't substantiate this claim from the evidence ledger yet.)")
c = claim.rstrip(".!?") + " (I can't substantiate this claim from the evidence ledger yet.)"
return c, "degraded (no evidence in ledger)"
@@ -311,20 +343,23 @@ def audit(draft):
verdict, hits = verify(c["claim"])
# Evidence tag already present counts as PASS (FActScore-style atomic
# support marker in the text).
if EVIDENCE_TAG.search(c["claim"]) or any(m in c["claim"].lower()
for m in LEDGER_MARKERS):
if EVIDENCE_TAG.search(c["claim"]) or any(m in c["claim"].lower() for m in LEDGER_MARKERS):
verdict, hits = "PASS", max(hits, 1)
new_text, note = degrade(c["claim"], verdict)
if verdict != "PASS":
rewritten = rewritten.replace(c["claim"], new_text)
result = {"type": c["type"], "label": c["label"],
"claim": c["claim"], "verdict": verdict,
"evidence_hits": hits, "rewrite": new_text, "note": note}
result = {
"type": c["type"],
"label": c["label"],
"claim": c["claim"],
"verdict": verdict,
"evidence_hits": hits,
"rewrite": new_text,
"note": note,
}
results.append(result)
journal({"type": c["type"], "claim": c["claim"][:160],
"verdict": verdict, "evidence_hits": hits})
return {"claims": results, "rewritten": rewritten,
"clean": all(r["verdict"] == "PASS" for r in results)}
journal({"type": c["type"], "claim": c["claim"][:160], "verdict": verdict, "evidence_hits": hits})
return {"claims": results, "rewritten": rewritten, "clean": all(r["verdict"] == "PASS" for r in results)}
def handle_hook(event, data=None):
@@ -334,17 +369,16 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["reframe", "scan", "verify", "audit",
"report", 'hook'])
ap.add_argument("cmd", choices=["reframe", "scan", "verify", "audit", "report", "hook"])
ap.add_argument("--event", default=None, help="Hook event name")
ap.add_argument("--data", default=None, help="Hook data JSON")
ap.add_argument("arg", nargs="?", default="")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
if args.cmd == 'hook':
event = args.event if hasattr(args, 'event') and args.event else 'unknown'
data = json.loads(args.data) if hasattr(args, 'data') and args.data else {}
if args.cmd == "hook":
event = args.event if hasattr(args, "event") and args.event else "unknown"
data = json.loads(args.data) if hasattr(args, "data") and args.data else {}
result = handle_hook(event, data)
print(json.dumps(result))
return
@@ -364,9 +398,11 @@ def main():
return
if args.cmd == "audit":
res = audit(args.arg)
print(json.dumps(res, indent=2) if args.json else
(f"claims: {len(res['claims'])}, clean: {res['clean']}\n"
f"rewritten:\n{res['rewritten']}"))
print(
json.dumps(res, indent=2)
if args.json
else (f"claims: {len(res['claims'])}, clean: {res['clean']}\nrewritten:\n{res['rewritten']}")
)
return
@@ -385,19 +421,10 @@ PLUGIN = {
"name": "claim_audit",
"version": "1.0.0",
"description": "Claim audit \u2014 verify factual claims against evidence",
"capabilities": [
"claim_verification",
"evidence_check"
],
"hooks": {
"on_response": True
},
"dependencies": [
"memory_store"
],
"provides": [
"claim_audit"
],
"capabilities": ["claim_verification", "evidence_check"],
"hooks": {"on_response": True},
"dependencies": ["memory_store"],
"provides": ["claim_audit"],
"priority": 40,
"auto_activate": True
"auto_activate": True,
}
+2 -1
View File
@@ -24,6 +24,7 @@ if _SD not in sys.path:
@dataclass
class CognitiveBudget:
"""Budget for cognitive work."""
start_tier: str = "T0" # T0-T4
max_tier: str = "T4" # Maximum allowed tier
max_attempts: int = 3 # Maximum attempts before giving up
@@ -203,5 +204,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["cognitive_budget"],
"priority": 5,
"auto_activate": True
"auto_activate": True,
}
+287 -59
View File
@@ -29,49 +29,155 @@ from typing import List, Dict, Any, Optional
# ═══════════════════════════════════════════════════════════════════════════════
# Request analysis signals (regex, zero cost)
CONJUNCTIONS = ['and', 'then', 'also', 'additionally', 'after that', 'plus', 'as well as']
CROSS_SYSTEM_REFS = ['api', 'frontend', 'backend', 'database', 'tests', 'config',
'ci', 'deploy', 'ui', 'cli', 'server', 'client', 'model', 'view']
ARCH_KEYWORDS = ['refactor', 'migrate', 'redesign', 'pipeline', 'system',
'architecture', 'overhaul', 'restructure', 'reorganize', 'consolidate']
NEW_ABSTRACTION_KEYWORDS = ['create a new', 'build a new', 'implement a new',
'add a new', 'new module', 'new system', 'new feature',
'new component', 'new service', 'new layer']
SCOPE_MODIFIERS = ['entire', 'all', 'every', 'throughout', 'comprehensive',
'complete', 'full', 'whole', 'across']
EXT_DEPENDENCY_KEYWORDS = ['integrate', 'third-party', 'library', 'package',
'sdk', 'api', 'service', 'webhook', 'external']
COMPLEXITY_MARKERS = ['complex', 'big change', 'major', 'significant',
'extensive', 'substantial', 'non-trivial', 'involved']
SIMPLE_FIX_INDICATORS = ['fix typo', 'rename', 'update comment', 'change color',
'add import', 'remove import', 'fix whitespace', 'format',
'lint', 'bump version', 'update readme', 'fix comment']
CONJUNCTIONS = ["and", "then", "also", "additionally", "after that", "plus", "as well as"]
CROSS_SYSTEM_REFS = [
"api",
"frontend",
"backend",
"database",
"tests",
"config",
"ci",
"deploy",
"ui",
"cli",
"server",
"client",
"model",
"view",
]
ARCH_KEYWORDS = [
"refactor",
"migrate",
"redesign",
"pipeline",
"system",
"architecture",
"overhaul",
"restructure",
"reorganize",
"consolidate",
]
NEW_ABSTRACTION_KEYWORDS = [
"create a new",
"build a new",
"implement a new",
"add a new",
"new module",
"new system",
"new feature",
"new component",
"new service",
"new layer",
]
SCOPE_MODIFIERS = ["entire", "all", "every", "throughout", "comprehensive", "complete", "full", "whole", "across"]
EXT_DEPENDENCY_KEYWORDS = [
"integrate",
"third-party",
"library",
"package",
"sdk",
"api",
"service",
"webhook",
"external",
]
COMPLEXITY_MARKERS = [
"complex",
"big change",
"major",
"significant",
"extensive",
"substantial",
"non-trivial",
"involved",
]
SIMPLE_FIX_INDICATORS = [
"fix typo",
"rename",
"update comment",
"change color",
"add import",
"remove import",
"fix whitespace",
"format",
"lint",
"bump version",
"update readme",
"fix comment",
]
# Build mode trigger keywords (high confidence)
BUILD_TRIGGERS = ['implement', 'build', 'create feature', 'refactor', 'migrate',
'across multiple files', 'new system', 'new module', 'overhaul',
'redesign', 'restructure', 'full pipeline', 'end-to-end']
BUILD_TRIGGERS = [
"implement",
"build",
"create feature",
"refactor",
"migrate",
"across multiple files",
"new system",
"new module",
"overhaul",
"redesign",
"restructure",
"full pipeline",
"end-to-end",
]
# Task mode trigger keywords
TASK_TRIGGERS = ['fix', 'change', 'update', 'rename', 'bug', 'typo', 'error',
'crash', 'broken', 'not working', 'issue', 'patch', 'hotfix']
TASK_TRIGGERS = [
"fix",
"change",
"update",
"rename",
"bug",
"typo",
"error",
"crash",
"broken",
"not working",
"issue",
"patch",
"hotfix",
]
# Research mode trigger keywords
RESEARCH_TRIGGERS = ['how does', 'what is', 'explain', 'investigate', 'explore',
'research', 'understand', 'analyze', 'study', 'review']
RESEARCH_TRIGGERS = [
"how does",
"what is",
"explain",
"investigate",
"explore",
"research",
"understand",
"analyze",
"study",
"review",
]
# Plan mode trigger keywords
PLAN_TRIGGERS = ['plan', 'design', 'architecture', 'before we start', 'approach',
'strategy', 'roadmap', 'blueprint', 'spec']
PLAN_TRIGGERS = [
"plan",
"design",
"architecture",
"before we start",
"approach",
"strategy",
"roadmap",
"blueprint",
"spec",
]
# ═══════════════════════════════════════════════════════════════════════════════
# SCORING FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class ScoreBreakdown:
"""Detailed breakdown of complexity score."""
request_analysis: int = 0
codebase_impact: int = 0
historical_patterns: int = 0
@@ -151,35 +257,144 @@ def estimate_codebase_impact(request: str, repo_path: str = "/mnt/workdrive/rune
signals = []
# Extract potential module/function names from request
words = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', request)
words = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", request)
# Filter out common words
stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to',
'for', 'of', 'with', 'by', 'from', 'is', 'are', 'was', 'were',
'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
'will', 'would', 'could', 'should', 'may', 'might', 'can', 'shall',
'that', 'which', 'who', 'whom', 'this', 'these', 'those', 'it', 'its',
'my', 'your', 'his', 'her', 'our', 'their', 'me', 'you', 'him', 'us',
'them', 'i', 'we', 'he', 'she', 'they', 'what', 'where', 'when', 'how',
'why', 'if', 'then', 'else', 'so', 'just', 'also', 'very', 'too', 'much',
'many', 'some', 'any', 'all', 'each', 'every', 'both', 'few', 'more',
'most', 'other', 'another', 'such', 'no', 'not', 'only', 'own', 'same',
'than', 'too', 'now', 'here', 'there', 'about', 'up', 'out', 'off',
'over', 'under', 'again', 'further', 'then', 'once', 'new', 'add',
'update', 'fix', 'change', 'make', 'create', 'build', 'implement'}
stop_words = {
"the",
"a",
"an",
"and",
"or",
"but",
"in",
"on",
"at",
"to",
"for",
"of",
"with",
"by",
"from",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"do",
"does",
"did",
"will",
"would",
"could",
"should",
"may",
"might",
"can",
"shall",
"that",
"which",
"who",
"whom",
"this",
"these",
"those",
"it",
"its",
"my",
"your",
"his",
"her",
"our",
"their",
"me",
"you",
"him",
"us",
"them",
"i",
"we",
"he",
"she",
"they",
"what",
"where",
"when",
"how",
"why",
"if",
"then",
"else",
"so",
"just",
"also",
"very",
"too",
"much",
"many",
"some",
"any",
"all",
"each",
"every",
"both",
"few",
"more",
"most",
"other",
"another",
"such",
"no",
"not",
"only",
"own",
"same",
"than",
"too",
"now",
"here",
"there",
"about",
"up",
"out",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"new",
"add",
"update",
"fix",
"change",
"make",
"create",
"build",
"implement",
}
potential_modules = [w for w in words if w.lower() not in stop_words and len(w) > 3]
# Check if any potential modules exist in codebase
if potential_modules:
try:
import subprocess
# Quick grep for potential module names
grep_pattern = '|'.join(potential_modules[:5]) # Limit to 5 to avoid slow grep
grep_pattern = "|".join(potential_modules[:5]) # Limit to 5 to avoid slow grep
result = subprocess.run(
['grep', '-r', '-l', '-E', grep_pattern, os.path.join(repo_path, 'src')],
capture_output=True, text=True, timeout=5
["grep", "-r", "-l", "-E", grep_pattern, os.path.join(repo_path, "src")],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
files = [f for f in result.stdout.strip().split('\n') if f]
files = [f for f in result.stdout.strip().split("\n") if f]
if len(files) > 5:
score += 3
signals.append(f"Files likely affected: {len(files)}")
@@ -190,17 +405,22 @@ def estimate_codebase_impact(request: str, repo_path: str = "/mnt/workdrive/rune
pass
# Check for cross-module dependencies
if 'src/rune' in repo_path:
if "src/rune" in repo_path:
try:
import subprocess
result = subprocess.run(
['grep', '-r', '-c', 'import', os.path.join(repo_path, 'src/rune')],
capture_output=True, text=True, timeout=5
["grep", "-r", "-c", "import", os.path.join(repo_path, "src/rune")],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
import_count = sum(int(line.split(':')[-1])
for line in result.stdout.strip().split('\n')
if ':' in line and line.split(':')[-1].strip().isdigit())
import_count = sum(
int(line.split(":")[-1])
for line in result.stdout.strip().split("\n")
if ":" in line and line.split(":")[-1].strip().isdigit()
)
if import_count > 50:
score += 2
signals.append(f"High import density: {import_count} imports")
@@ -218,15 +438,17 @@ def check_historical_patterns(request: str) -> tuple[int, List[str]]:
# Check if similar tasks needed build mode
try:
import sys
sys.path.insert(0, os.path.expanduser('~/.config/opencode/scripts'))
sys.path.insert(0, os.path.expanduser("~/.config/opencode/scripts"))
import memory_store
db = memory_store.connect()
try:
results = memory_store.cached_recall(db, f"build mode {request}", budget=3)
if results:
for r in results:
text = r.get('text', '').lower()
if 'build mode' in text or 'multi-step' in text or 'complex' in text:
text = r.get("text", "").lower()
if "build mode" in text or "multi-step" in text or "complex" in text:
score += 2
signals.append("Similar past task needed build mode")
break
@@ -245,25 +467,25 @@ def assess_risk(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[i
request_lower = request.lower()
# Auth/security code
auth_keywords = ['auth', 'security', 'token', 'credential', 'password', 'login', 'session']
auth_keywords = ["auth", "security", "token", "credential", "password", "login", "session"]
if any(k in request_lower for k in auth_keywords):
score += 2
signals.append("Touches auth/security code")
# Data layer
data_keywords = ['database', 'migration', 'schema', 'sql', 'sqlite', 'model', 'orm']
data_keywords = ["database", "migration", "schema", "sql", "sqlite", "model", "orm"]
if any(k in request_lower for k in data_keywords):
score += 1
signals.append("Touches data layer")
# Public API
api_keywords = ['api', 'endpoint', 'route', 'export', 'interface', 'sdk', 'public']
api_keywords = ["api", "endpoint", "route", "export", "interface", "sdk", "public"]
if any(k in request_lower for k in api_keywords):
score += 2
signals.append("Touches public API")
# Reversibility concern
destructive_keywords = ['delete', 'remove', 'drop', 'destroy', 'clear', 'reset', 'wipe']
destructive_keywords = ["delete", "remove", "drop", "destroy", "clear", "reset", "wipe"]
if any(k in request_lower for k in destructive_keywords):
score += 1
signals.append("Reversibility concern")
@@ -275,6 +497,7 @@ def assess_risk(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[i
# MAIN SCORING
# ═══════════════════════════════════════════════════════════════════════════════
def score_request(request: str, repo_path: str = "/mnt/workdrive/rune") -> ScoreBreakdown:
"""Score a request for complexity. Returns ScoreBreakdown with total and mode."""
breakdown = ScoreBreakdown()
@@ -300,8 +523,12 @@ def score_request(request: str, repo_path: str = "/mnt/workdrive/rune") -> Score
breakdown.signals.extend(risk_signals)
# Calculate total
breakdown.total = (breakdown.request_analysis + breakdown.codebase_impact +
breakdown.historical_patterns + breakdown.risk_assessment)
breakdown.total = (
breakdown.request_analysis
+ breakdown.codebase_impact
+ breakdown.historical_patterns
+ breakdown.risk_assessment
)
# Determine mode
if breakdown.total >= 8:
@@ -349,6 +576,7 @@ def get_score(request: str, repo_path: str = "/mnt/workdrive/rune") -> Dict[str,
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 3:
print("Usage: complexity_scorer.py [score|mode] <request>")
+1 -1
View File
@@ -140,5 +140,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["connection_pool"],
"priority": 1,
"auto_activate": True
"auto_activate": True,
}
+21 -16
View File
@@ -47,9 +47,7 @@ def measure_pressure(text, max_tokens=128000):
"pressure": round(pressure, 3),
"status": "ok" if pressure < 0.7 else "warning" if pressure < 0.9 else "critical",
"recommendation": (
"none" if pressure < 0.7 else
"summarize_old_turns" if pressure < 0.9 else
"truncate_and_summarize"
"none" if pressure < 0.7 else "summarize_old_turns" if pressure < 0.9 else "truncate_and_summarize"
),
}
@@ -79,7 +77,7 @@ def detect_degradation(recent_responses):
# Signal 2: Repetition (unique phrases vs total)
recent_text = " ".join(recent_responses[-3:])
words = re.findall(r'\w+', recent_text.lower())
words = re.findall(r"\w+", recent_text.lower())
if words:
unique_ratio = len(set(words)) / len(words)
repetition_score = max(0, 1.0 - unique_ratio * 2) # High repetition = low unique ratio
@@ -91,11 +89,7 @@ def detect_degradation(recent_responses):
generic_ratio = generic_count / min(3, len(recent_responses))
# Composite degradation score
degradation = max(
length_decline * 0.4,
repetition_score * 0.3,
generic_ratio * 0.3
)
degradation = max(length_decline * 0.4, repetition_score * 0.3, generic_ratio * 0.3)
signals = []
if length_decline > 0.3:
@@ -131,6 +125,7 @@ def read_log(limit=20):
if not os.path.exists(LOG_PATH):
return []
from collections import deque
try:
with open(LOG_PATH) as f:
tail = deque(f, maxlen=limit)
@@ -196,6 +191,7 @@ def handle_hook(event, data=None):
def main():
import argparse
ap = argparse.ArgumentParser(description="Context pressure monitor")
ap.add_argument("cmd", choices=["measure", "degrade", "log", "stats", "hook"])
ap.add_argument("arg", nargs="?", default="")
@@ -208,14 +204,20 @@ def main():
if args.cmd == "measure":
result = measure_pressure(args.arg, max_tokens=args.max_tokens)
print(json.dumps(result, indent=2) if args.json else
f"Pressure: {result['pressure']:.1%} ({result['status']}) — {result['recommendation']}")
print(
json.dumps(result, indent=2)
if args.json
else f"Pressure: {result['pressure']:.1%} ({result['status']}) — {result['recommendation']}"
)
elif args.cmd == "degrade":
responses = [r.strip() for r in args.arg.split("|") if r.strip()]
result = detect_degradation(responses)
print(json.dumps(result, indent=2) if args.json else
f"Degradation: {result['degradation']:.1%} — signals: {', '.join(result.get('signals', []))}")
print(
json.dumps(result, indent=2)
if args.json
else f"Degradation: {result['degradation']:.1%} — signals: {', '.join(result.get('signals', []))}"
)
elif args.cmd == "log":
entries = read_log(limit=args.limit)
@@ -230,8 +232,11 @@ def main():
elif args.cmd == "stats":
s = log_stats()
print(json.dumps(s, indent=2) if args.json else
f"Total: {s['total']}, Warnings: {s['warnings']}, Criticals: {s['criticals']}")
print(
json.dumps(s, indent=2)
if args.json
else f"Total: {s['total']}, Warnings: {s['warnings']}, Criticals: {s['criticals']}"
)
elif args.cmd == "hook":
event = args.event or "unknown"
@@ -253,5 +258,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["context_pressure"],
"priority": 2,
"auto_activate": True
"auto_activate": True,
}
+203 -147
View File
@@ -20,9 +20,11 @@ sys.path.insert(0, SCRIPT_DIR)
# CONTRACT DEFINITION
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class Contract:
"""A function contract: call this, expect this shape."""
name: str
module: str
function: str
@@ -44,320 +46,367 @@ CONTRACTS = [
# Memory store
Contract(
name="memory_store.connect returns connection",
module="memory_store", function="connect",
args=(), kwargs={},
module="memory_store",
function="connect",
args=(),
kwargs={},
expect_not_none=True,
),
Contract(
name="memory_store.connect has row_factory",
module="memory_store", function="connect",
args=(), kwargs={},
module="memory_store",
function="connect",
args=(),
kwargs={},
expect_callable=True, # Just verify it doesn't crash
),
# Warm router
Contract(
name="warm_router.route returns tuple",
module="warm_router", function="route",
args=("test query",), kwargs={},
module="warm_router",
function="route",
args=("test query",),
kwargs={},
expect_type=tuple,
expect_len=3, # (fired, total, scores)
),
# Model connect
Contract(
name="model_connect.list_models returns list",
module="model_connect", function="list_models",
args=(), kwargs={},
module="model_connect",
function="list_models",
args=(),
kwargs={},
expect_type=list,
),
Contract(
name="model_connect.MODEL_CATEGORIES is dict",
module="model_connect", function="MODEL_CATEGORIES",
args=(), kwargs={},
module="model_connect",
function="MODEL_CATEGORIES",
args=(),
kwargs={},
expect_type=dict,
),
# Memory env
Contract(
name="memory_env.store_db returns string",
module="memory_env", function="store_db",
args=(), kwargs={},
module="memory_env",
function="store_db",
args=(),
kwargs={},
expect_type=str,
expect_not_none=True,
),
Contract(
name="memory_env.memory_dir returns string",
module="memory_env", function="memory_dir",
args=(), kwargs={},
module="memory_env",
function="memory_dir",
args=(),
kwargs={},
expect_type=str,
expect_not_none=True,
),
# Task persist
Contract(
name="task_persist._load returns dict",
module="task_persist", function="_load",
args=(), kwargs={},
module="task_persist",
function="_load",
args=(),
kwargs={},
expect_type=dict,
),
# Event bus
Contract(
name="event_bus.stats returns dict or None",
module="event_bus", function="stats",
args=(), kwargs={},
module="event_bus",
function="stats",
args=(),
kwargs={},
),
# Claim audit
Contract(
name="claim_audit.report returns string or None",
module="claim_audit", function="report",
args=(), kwargs={},
module="claim_audit",
function="report",
args=(),
kwargs={},
),
# Identity anchors
Contract(
name="identity_anchors.get_identity_prompt returns string",
module="identity_anchors", function="get_identity_prompt",
args=(), kwargs={},
module="identity_anchors",
function="get_identity_prompt",
args=(),
kwargs={},
expect_type=str,
expect_not_none=True,
),
# Response adapter
Contract(
name="response_adapter.parse_qwen_response returns RuneResponse",
module="response_adapter", function="parse_qwen_response",
args=({"content": "test", "model": "test"},), kwargs={},
module="response_adapter",
function="parse_qwen_response",
args=({"content": "test", "model": "test"},),
kwargs={},
expect_not_none=True,
),
# Provider budget
Contract(
name="provider_budget.get_status returns dict",
module="provider_budget", function="get_status",
args=(), kwargs={},
module="provider_budget",
function="get_status",
args=(),
kwargs={},
expect_type=dict,
),
# Time awareness
Contract(
name="time_awareness.get_status returns dict",
module="time_awareness", function="get_status",
args=(), kwargs={},
module="time_awareness",
function="get_status",
args=(),
kwargs={},
expect_type=dict,
),
# Token optimizer
Contract(
name="token_optimizer.get_status returns dict",
module="token_optimizer", function="get_status",
args=(), kwargs={},
module="token_optimizer",
function="get_status",
args=(),
kwargs={},
expect_type=dict,
),
# Night mode
Contract(
name="night_mode.get_status returns dict",
module="night_mode", function="get_status",
args=(), kwargs={},
module="night_mode",
function="get_status",
args=(),
kwargs={},
expect_type=dict,
),
# Goal
Contract(
name="goal.check_goal returns dict",
module="goal", function="check_goal",
args=(), kwargs={},
module="goal",
function="check_goal",
args=(),
kwargs={},
expect_type=dict,
),
# Safety override
Contract(
name="safety_override.SafetyOverride instantiates",
module="safety_override", function="SafetyOverride",
args=(), kwargs={},
module="safety_override",
function="SafetyOverride",
args=(),
kwargs={},
expect_not_none=True,
),
# Parallel processor
Contract(
name="parallel_processor.ParallelProcessor instantiates",
module="parallel_processor", function="ParallelProcessor",
args=(), kwargs={},
module="parallel_processor",
function="ParallelProcessor",
args=(),
kwargs={},
expect_not_none=True,
),
# Chat bubble
Contract(
name="chat_bubble.MessageBox instantiates",
module="chat_bubble", function="MessageBox",
args=("user", "test"), kwargs={},
module="chat_bubble",
function="MessageBox",
args=("user", "test"),
kwargs={},
expect_not_none=True,
),
# ── Extended contracts (workflow audit additions) ──────────────────
# Memory store — core functions
Contract(
name="memory_store.connect returns connection",
module="memory_store", function="connect",
args=(), kwargs={},
module="memory_store",
function="connect",
args=(),
kwargs={},
expect_not_none=True,
),
# Model connect — routing
Contract(
name="model_connect.route_model returns dict",
module="model_connect", function="route_model",
args=("conversation",), kwargs={},
module="model_connect",
function="route_model",
args=("conversation",),
kwargs={},
expect_type=dict,
),
Contract(
name="model_connect.MODEL_CAPABILITIES is dict",
module="model_connect", function="MODEL_CAPABILITIES",
args=(), kwargs={},
module="model_connect",
function="MODEL_CAPABILITIES",
args=(),
kwargs={},
expect_type=dict,
),
# Abstraction
Contract(
name="abstraction.pipeline_stats returns dict or None",
module="abstraction", function="pipeline_stats",
args=(), kwargs={},
module="abstraction",
function="pipeline_stats",
args=(),
kwargs={},
),
# Graph memory
Contract(
name="graph_memory.connect returns connection",
module="graph_memory", function="connect",
args=(), kwargs={},
module="graph_memory",
function="connect",
args=(),
kwargs={},
expect_not_none=True,
),
Contract(
name="graph_memory.stats callable",
module="graph_memory", function="stats",
args=(), kwargs={},
module="graph_memory",
function="stats",
args=(),
kwargs={},
expect_callable=True,
),
# Persona gate
Contract(
name="persona_gate.PERMISSIVE_MODE is True",
module="persona_gate", function="PERMISSIVE_MODE",
args=(), kwargs={},
module="persona_gate",
function="PERMISSIVE_MODE",
args=(),
kwargs={},
),
# Voice hybrid
Contract(
name="voice_hybrid.blend returns dict or None",
module="voice_hybrid", function="blend",
args=("test input",), kwargs={},
module="voice_hybrid",
function="blend",
args=("test input",),
kwargs={},
),
# Claim audit
Contract(
name="claim_audit.scan returns list",
module="claim_audit", function="scan",
args=("test text",), kwargs={},
module="claim_audit",
function="scan",
args=("test text",),
kwargs={},
expect_type=list,
),
# Drift ledger
Contract(
name="drift_ledger.check returns dict or None",
module="drift_ledger", function="check",
args=(), kwargs={"strict": False, "autofix": False},
module="drift_ledger",
function="check",
args=(),
kwargs={"strict": False, "autofix": False},
),
# Growth delta
Contract(
name="growth_delta.recent returns list",
module="growth_delta", function="recent",
args=(), kwargs={},
module="growth_delta",
function="recent",
args=(),
kwargs={},
expect_type=list,
),
# Self improve
Contract(
name="self_improve.analyze_system returns list",
module="self_improve", function="analyze_system",
args=(), kwargs={},
module="self_improve",
function="analyze_system",
args=(),
kwargs={},
expect_type=list,
),
# Context pressure
Contract(
name="context_pressure.measure_pressure returns dict or None",
module="context_pressure", function="measure_pressure",
args=("test text for pressure measurement",), kwargs={},
module="context_pressure",
function="measure_pressure",
args=("test text for pressure measurement",),
kwargs={},
),
# Behavior
Contract(
name="behavior.match_context returns dict",
module="behavior", function="match_context",
args=("test context",), kwargs={},
module="behavior",
function="match_context",
args=("test context",),
kwargs={},
expect_type=dict,
),
# Witness
Contract(
name="witness.detect_signals returns dict",
module="witness", function="detect_signals",
args=(), kwargs={"message": "test message"},
module="witness",
function="detect_signals",
args=(),
kwargs={"message": "test message"},
expect_type=dict,
),
# Elastic cognition
Contract(
name="elastic_cognition.classify_salience returns object",
module="elastic_cognition", function="classify_salience",
args=("test input", "chat"), kwargs={},
module="elastic_cognition",
function="classify_salience",
args=("test input", "chat"),
kwargs={},
expect_not_none=True,
),
# Cache
Contract(
name="cache.get_cache returns dict or None",
module="cache", function="get_cache",
args=("test_key",), kwargs={},
module="cache",
function="get_cache",
args=("test_key",),
kwargs={},
),
# Hooks
Contract(
name="hook_engine.generate_report returns str or None",
module="hook_engine", function="generate_report",
args=(), kwargs={},
module="hook_engine",
function="generate_report",
args=(),
kwargs={},
),
# Microkernel
Contract(
name="microkernel.status returns dict or None",
module="microkernel", function="status",
args=(), kwargs={},
module="microkernel",
function="status",
args=(),
kwargs={},
),
# Sleep time
Contract(
name="sleep_time.reflect_local returns dict or None",
module="sleep_time", function="reflect_local",
args=(), kwargs={},
module="sleep_time",
function="reflect_local",
args=(),
kwargs={},
),
# Auto context
Contract(
name="auto_context.build_auto_context returns list or None",
module="auto_context", function="build_auto_context",
args=("test query",), kwargs={},
module="auto_context",
function="build_auto_context",
args=("test query",),
kwargs={},
),
# Auto tasks
Contract(
name="auto_tasks.extract_tasks returns list or None",
module="auto_tasks", function="extract_tasks",
args=("test text", "chat"), kwargs={},
module="auto_tasks",
function="extract_tasks",
args=("test text", "chat"),
kwargs={},
),
]
@@ -366,9 +415,11 @@ CONTRACTS = [
# CONTRACT RUNNER
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class ContractResult:
"""Result of running a contract."""
name: str
passed: bool
error: str = ""
@@ -384,16 +435,15 @@ def run_contract(contract: Contract) -> ContractResult:
mod = __import__(contract.module)
except ImportError as e:
if contract.skip_on_import_error:
return ContractResult(name=contract.name, passed=True,
error=f"skipped (import error: {e})")
return ContractResult(name=contract.name, passed=False,
error=f"import failed: {e}")
return ContractResult(name=contract.name, passed=True, error=f"skipped (import error: {e})")
return ContractResult(name=contract.name, passed=False, error=f"import failed: {e}")
# Get function/attribute
func = getattr(mod, contract.function, None)
if func is None:
return ContractResult(name=contract.name, passed=False,
error=f"{contract.function} not found in {contract.module}")
return ContractResult(
name=contract.name, passed=False, error=f"{contract.function} not found in {contract.module}"
)
# Call if callable
if callable(func) and not contract.expect_callable:
@@ -402,44 +452,48 @@ def run_contract(contract: Contract) -> ContractResult:
# Just verify it's callable
if callable(func):
return ContractResult(name=contract.name, passed=True)
return ContractResult(name=contract.name, passed=False,
error=f"{contract.function} is not callable")
return ContractResult(name=contract.name, passed=False, error=f"{contract.function} is not callable")
else:
result = func
# Check expectations
if contract.expect_not_none and result is None:
return ContractResult(name=contract.name, passed=False,
error="returned None", actual_type="NoneType")
return ContractResult(name=contract.name, passed=False, error="returned None", actual_type="NoneType")
if contract.expect_type and not isinstance(result, contract.expect_type):
return ContractResult(name=contract.name, passed=False,
error=f"expected {contract.expect_type.__name__}, got {type(result).__name__}",
actual_type=type(result).__name__)
return ContractResult(
name=contract.name,
passed=False,
error=f"expected {contract.expect_type.__name__}, got {type(result).__name__}",
actual_type=type(result).__name__,
)
if contract.expect_keys and isinstance(result, dict):
missing = [k for k in contract.expect_keys if k not in result]
if missing:
return ContractResult(name=contract.name, passed=False,
error=f"missing keys: {missing}",
actual_value=str(list(result.keys())[:10]))
return ContractResult(
name=contract.name,
passed=False,
error=f"missing keys: {missing}",
actual_value=str(list(result.keys())[:10]),
)
if contract.expect_len is not None:
if hasattr(result, '__len__'):
if hasattr(result, "__len__"):
if len(result) != contract.expect_len:
return ContractResult(name=contract.name, passed=False,
error=f"expected len {contract.expect_len}, got {len(result)}",
actual_value=f"len={len(result)}")
return ContractResult(
name=contract.name,
passed=False,
error=f"expected len {contract.expect_len}, got {len(result)}",
actual_value=f"len={len(result)}",
)
else:
return ContractResult(name=contract.name, passed=False,
error="result has no __len__")
return ContractResult(name=contract.name, passed=False, error="result has no __len__")
return ContractResult(name=contract.name, passed=True,
actual_type=type(result).__name__)
return ContractResult(name=contract.name, passed=True, actual_type=type(result).__name__)
except Exception as e:
return ContractResult(name=contract.name, passed=False,
error=f"{type(e).__name__}: {e}")
return ContractResult(name=contract.name, passed=False, error=f"{type(e).__name__}: {e}")
def run_all_contracts() -> list[ContractResult]:
@@ -461,6 +515,7 @@ SIGNATURES_FILE = os.path.expanduser("~/.rune/function_signatures.json")
def capture_signatures() -> dict:
"""Capture current function signatures for all contracted functions."""
import inspect
sigs = {}
for contract in CONTRACTS:
try:
@@ -510,6 +565,7 @@ def check_signature_drift() -> list[str]:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 2:
print("Usage: contracts.py [test|signatures|drift]")
+9 -24
View File
@@ -26,6 +26,7 @@ from datetime import datetime, timezone
@dataclass
class CronTask:
"""A scheduled task."""
name: str
cron_expression: str # e.g., "*/5 * * * *" (every 5 minutes)
command: str
@@ -79,8 +80,7 @@ class CronEngine:
with open(self.storage_path, "w") as f:
json.dump(data, f, indent=2)
def add_task(self, name: str, cron_expression: str, command: str,
description: str = "") -> CronTask:
def add_task(self, name: str, cron_expression: str, command: str, description: str = "") -> CronTask:
"""Add a scheduled task."""
task = CronTask(
name=name,
@@ -147,13 +147,7 @@ class CronEngine:
try:
# Execute the command safely (no shell injection)
result = subprocess.run(
shlex.split(task.command),
shell=False,
capture_output=True,
text=True,
timeout=60
)
result = subprocess.run(shlex.split(task.command), shell=False, capture_output=True, text=True, timeout=60)
# Update task
task.last_run = datetime.now(timezone.utc).isoformat()
@@ -282,13 +276,7 @@ class CronEngine:
# Record the minute we're running in to prevent double-fire
self._last_run_minute[task.name] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
result = subprocess.run(
shlex.split(task.command),
shell=False,
capture_output=True,
text=True,
timeout=60
)
result = subprocess.run(shlex.split(task.command), shell=False, capture_output=True, text=True, timeout=60)
task.last_run = datetime.now(timezone.utc).isoformat()
task.run_count += 1
@@ -309,14 +297,13 @@ class CronEngine:
if os.path.exists(log_path) and os.path.getsize(log_path) > 5 * 1024 * 1024:
for i in range(4, 0, -1):
src = f"{log_path}.{i}"
dst = f"{log_path}.{i+1}"
dst = f"{log_path}.{i + 1}"
if os.path.exists(src):
os.rename(src, dst)
os.rename(log_path, f"{log_path}.1")
with open(log_path, "a") as f:
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {task.name}: "
f"returncode={result.returncode}\n")
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {task.name}: returncode={result.returncode}\n")
def _log_error(self, task: CronTask, error: str):
"""Log task error with simple rotation."""
@@ -327,14 +314,13 @@ class CronEngine:
if os.path.exists(log_path) and os.path.getsize(log_path) > 5 * 1024 * 1024:
for i in range(4, 0, -1):
src = f"{log_path}.{i}"
dst = f"{log_path}.{i+1}"
dst = f"{log_path}.{i + 1}"
if os.path.exists(src):
os.rename(src, dst)
os.rename(log_path, f"{log_path}.1")
with open(log_path, "a") as f:
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {task.name}: "
f"ERROR: {error}\n")
f.write(f"[{datetime.now(timezone.utc).isoformat()}] {task.name}: ERROR: {error}\n")
# Global cron engine
@@ -354,8 +340,7 @@ def list_tasks() -> List[Dict[str, Any]]:
return get_cron_engine().list_tasks()
def add_task(name: str, cron_expression: str, command: str,
description: str = "") -> Dict[str, Any]:
def add_task(name: str, cron_expression: str, command: str, description: str = "") -> Dict[str, Any]:
"""Add a scheduled task."""
task = get_cron_engine().add_task(name, cron_expression, command, description)
return task.to_dict()
+568 -342
View File
File diff suppressed because it is too large. Load diff
+42 -73
View File
@@ -28,81 +28,46 @@ DEPENDENCY_MAP_FILE = os.path.expanduser("~/.config/opencode/memory/dependency_m
DEPENDENCY_GRAPH = {
# MEMORY umbrella
"memory_store.py": [
"warm_router.py", "curator.py", "memory_consolidation.py",
"drift_core.py", "drift_cache.py", "tier_manager.py", "shard_manager.py",
"query_memory.py", "growth_simulator.py"
"warm_router.py",
"curator.py",
"memory_consolidation.py",
"drift_core.py",
"drift_cache.py",
"tier_manager.py",
"shard_manager.py",
"query_memory.py",
"growth_simulator.py",
],
"memory_consolidation.py": [
"memory_store.py", "drift_compression.py", "curator.py"
],
"warm_router.py": [
"capability_broker.py", "memory_consolidation.py", "memory_store.py"
],
"graph_memory.py": [
"warm_router.py", "memory_consolidation.py", "shard_manager.py"
],
"shard_manager.py": [
"memory_store.py", "tier_manager.py"
],
"tier_manager.py": [
"memory_store.py", "drift_cache.py"
],
"query_memory.py": [
"memory_store.py", "growth_simulator.py"
],
"memory_consolidation.py": ["memory_store.py", "drift_compression.py", "curator.py"],
"warm_router.py": ["capability_broker.py", "memory_consolidation.py", "memory_store.py"],
"graph_memory.py": ["warm_router.py", "memory_consolidation.py", "shard_manager.py"],
"shard_manager.py": ["memory_store.py", "tier_manager.py"],
"tier_manager.py": ["memory_store.py", "drift_cache.py"],
"query_memory.py": ["memory_store.py", "growth_simulator.py"],
# GOVERNANCE umbrella
"drift_core.py": [
"curator.py", "audit_ledger.py", "memory_store.py",
"drift_observability.py", "drift_cache.py"
],
"curator.py": [
"memory_store.py", "drift_core.py", "audit_ledger.py"
],
"drift_compression.py": [
"memory_consolidation.py", "curator.py"
],
"drift_observability.py": [
"drift_core.py", "audit_ledger.py"
],
"audit_ledger.py": [
"curator.py", "drift_core.py"
],
"drift_core.py": ["curator.py", "audit_ledger.py", "memory_store.py", "drift_observability.py", "drift_cache.py"],
"curator.py": ["memory_store.py", "drift_core.py", "audit_ledger.py"],
"drift_compression.py": ["memory_consolidation.py", "curator.py"],
"drift_observability.py": ["drift_core.py", "audit_ledger.py"],
"audit_ledger.py": ["curator.py", "drift_core.py"],
# IDENTITY umbrella
"persona_definition.py": [
"persona_check.py", "identity_anchors.py", "drift_observability.py"
],
"identity_anchors.py": [
"persona_definition.py", "drift_core.py"
],
"voice_hybrid.py": [
"delivery_adapter.py", "persona_definition.py"
],
"persona_definition.py": ["persona_check.py", "identity_anchors.py", "drift_observability.py"],
"identity_anchors.py": ["persona_definition.py", "drift_core.py"],
"voice_hybrid.py": ["delivery_adapter.py", "persona_definition.py"],
# GROWTH umbrella
"growth_delta.py": [
"memory_store.py", "curator.py", "lessons.py"
],
"growth_simulator.py": [
"memory_store.py", "query_memory.py", "drift_core.py"
],
"growth_delta.py": ["memory_store.py", "curator.py", "lessons.py"],
"growth_simulator.py": ["memory_store.py", "query_memory.py", "drift_core.py"],
# PERCEPTION umbrella
"warm_router.py": [
"capability_broker.py", "memory_consolidation.py"
],
"warm_router.py": ["capability_broker.py", "memory_consolidation.py"],
# INFRASTRUCTURE
"microkernel.py": [
"hook_engine.py", "event_bus.py"
],
"microkernel.py": ["hook_engine.py", "event_bus.py"],
}
# ═══════════════════════════════════════════════════════════════════════════════
# PROPAGATION ACTIONS
# ═══════════════════════════════════════════════════════════════════════════════
def get_dependents(changed_file: str) -> List[str]:
"""Get files that depend on the changed file."""
basename = os.path.basename(changed_file)
@@ -149,11 +114,13 @@ def propagate(changed_file: str, dry_run: bool = False) -> dict:
changed_basename = os.path.basename(changed_file).replace(".py", "")
if changed_basename in content:
results["propagated"].append({
"file": dep,
"action": "references changed file",
"detail": f"imports or references {changed_basename}"
})
results["propagated"].append(
{
"file": dep,
"action": "references changed file",
"detail": f"imports or references {changed_basename}",
}
)
else:
results["skipped"].append({"file": dep, "reason": "no direct reference"})
except Exception as e:
@@ -171,8 +138,10 @@ def get_full_map() -> dict:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Dependency tracker for umbrella systems")
sub = parser.add_subparsers(dest="cmd")
@@ -195,21 +164,21 @@ def main():
result = check_propagation(args.file)
print(f"File: {result['changed_file']}")
print(f"Dependents ({result['count']}):")
for dep in result['dependents']:
for dep in result["dependents"]:
print(f" → {dep}")
elif args.cmd == "propagate":
result = propagate(args.file, dry_run=args.dry_run)
print(f"Propagation for: {result['changed_file']}")
print(f"Propagated ({len(result['propagated'])}):")
for p in result['propagated']:
for p in result["propagated"]:
print(f" ✓ {p['file']}: {p['action']}")
print(f"Skipped ({len(result['skipped'])}):")
for s in result['skipped']:
for s in result["skipped"]:
print(f" ⊘ {s['file']}: {s['reason']}")
if result['errors']:
if result["errors"]:
print(f"Errors ({len(result['errors'])}):")
for e in result['errors']:
for e in result["errors"]:
print(f" ✗ {e['file']}: {e['error']}")
elif args.cmd == "map":
+21 -15
View File
@@ -33,10 +33,10 @@ CACHE_DIR = os.path.join(memory_env.memory_dir(), "cache")
CACHE_FILE = os.path.join(CACHE_DIR, "drift_cache.json")
# Cache configuration
MAX_CACHE_SIZE = 200 # maximum entries in cache
EVICTION_BATCH = 20 # entries to evict when cache is full
MIN_WASH_FOR_CACHE = 1 # minimum wash_count to enter cache
CACHE_BOOST_ROCK = 2.0 # multiplier for rock-status entries
MAX_CACHE_SIZE = 200 # maximum entries in cache
EVICTION_BATCH = 20 # entries to evict when cache is full
MIN_WASH_FOR_CACHE = 1 # minimum wash_count to enter cache
CACHE_BOOST_ROCK = 2.0 # multiplier for rock-status entries
CACHE_BOOST_ESTABLISHED = 1.5 # multiplier for established entries
@@ -113,8 +113,7 @@ class DriftCache:
"""Record a cache access (reinforces the entry)."""
if entry_id in self.entries:
self.entries[entry_id]["last_accessed"] = datetime.now(timezone.utc).isoformat()
self.entries[entry_id]["access_count"] = \
self.entries[entry_id].get("access_count", 0) + 1
self.entries[entry_id]["access_count"] = self.entries[entry_id].get("access_count", 0) + 1
self._save()
def contains(self, entry_id: int) -> bool:
@@ -130,10 +129,7 @@ class DriftCache:
def _evict(self):
"""Evict lowest-priority entries."""
# Sort by priority (ascending), evict lowest
sorted_entries = sorted(
self.entries.items(),
key=lambda x: x[1].get("priority", 0.0)
)
sorted_entries = sorted(self.entries.items(), key=lambda x: x[1].get("priority", 0.0))
to_evict = sorted_entries[:EVICTION_BATCH]
for entry_id, _ in to_evict:
del self.entries[entry_id]
@@ -142,8 +138,7 @@ class DriftCache:
"""Sync cache with memory store — update priorities from current wash counts."""
for entry_id in list(self.entries.keys()):
row = db.execute(
"SELECT wash_count, importance, rock_status FROM entries WHERE id = ?",
(entry_id,)
"SELECT wash_count, importance, rock_status FROM entries WHERE id = ?", (entry_id,)
).fetchone()
if row:
entry = {
@@ -162,7 +157,15 @@ class DriftCache:
def get_stats(self) -> dict:
"""Get cache statistics."""
if not self.entries:
return {"size": 0, "avg_priority": 0.0, "max_priority": 0.0, "min_priority": 0.0, "rock_count": 0, "established_count": 0, "washable_count": 0}
return {
"size": 0,
"avg_priority": 0.0,
"max_priority": 0.0,
"min_priority": 0.0,
"rock_count": 0,
"established_count": 0,
"washable_count": 0,
}
priorities = [e.get("priority", 0.0) for e in self.entries.values()]
rock_count = sum(1 for e in self.entries.values() if e.get("rock_status") == "rock")
@@ -183,8 +186,10 @@ class DriftCache:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Drift-aware cache")
sub = parser.add_subparsers(dest="cmd")
@@ -218,6 +223,7 @@ def main():
elif args.cmd == "sync":
sys.path.insert(0, SCRIPTS_DIR)
import memory_store
db = memory_store.connect()
cache.update_from_store(db)
db.close()
@@ -227,10 +233,10 @@ def main():
elif args.cmd == "add":
sys.path.insert(0, SCRIPTS_DIR)
import memory_store
db = memory_store.connect()
row = db.execute(
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id = ?",
(args.entry_id,)
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id = ?", (args.entry_id,)
).fetchone()
if row:
entry = {
+10 -11
View File
@@ -26,13 +26,13 @@ if _SD not in sys.path:
sys.path.insert(0, _SD)
# Compression ratios by rock_status
COMPRESSION_ROCK = 0.0 # no compression
COMPRESSION_ESTABLISHED = 0.2 # 20% compression
COMPRESSION_WASHABLE = 0.5 # 50% compression (default)
COMPRESSION_ROCK = 0.0 # no compression
COMPRESSION_ESTABLISHED = 0.2 # 20% compression
COMPRESSION_WASHABLE = 0.5 # 50% compression (default)
# Wash count modifiers (reduce compression as wash_count increases)
WASH_THRESHOLD_FULL = 20 # above this: minimal compression
WASH_THRESHOLD_PARTIAL = 10 # above this: reduced compression
WASH_THRESHOLD_FULL = 20 # above this: minimal compression
WASH_THRESHOLD_PARTIAL = 10 # above this: reduced compression
def get_compression_ratio(wash_count: int = 0, rock_status: str = "washable") -> float:
@@ -194,8 +194,10 @@ def get_compression_stats(db) -> dict:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Drift-aware compression")
sub = parser.add_subparsers(dest="cmd")
@@ -217,13 +219,11 @@ def main():
sys.exit(1)
import memory_store
db = memory_store.connect()
if args.cmd == "ratio":
row = db.execute(
"SELECT wash_count, rock_status FROM entries WHERE id = ?",
(args.entry_id,)
).fetchone()
row = db.execute("SELECT wash_count, rock_status FROM entries WHERE id = ?", (args.entry_id,)).fetchone()
if row:
ratio = get_compression_ratio(row[0] or 0, row[1] or "washable")
print(f"Entry {args.entry_id}: compression_ratio={ratio:.2f}")
@@ -242,8 +242,7 @@ def main():
elif args.cmd == "compress":
row = db.execute(
"SELECT id, text, wash_count, rock_status FROM entries WHERE id = ?",
(args.entry_id,)
"SELECT id, text, wash_count, rock_status FROM entries WHERE id = ?", (args.entry_id,)
).fetchone()
if row:
entry = {"text": row[1], "wash_count": row[2] or 0, "rock_status": row[3] or "washable"}
+45 -49
View File
@@ -45,24 +45,25 @@ import memory_env
# Trust level lifecycle: provisional → validated → locked
# An entry becomes validated after PROVEN_USES_THRESHOLD proven accesses.
# An entry becomes locked after passing the promotion gate twice.
PROVEN_USES_THRESHOLD = 20 # proven accesses before validation eligible
RECENCY_HALF_LIFE_DAYS = 30.0 # recency decay half-life
RELIABILITY_WINDOW = 50 # recent accesses for reliability calc
PROVEN_USES_THRESHOLD = 20 # proven accesses before validation eligible
RECENCY_HALF_LIFE_DAYS = 30.0 # recency decay half-life
RELIABILITY_WINDOW = 50 # recent accesses for reliability calc
STATE_EVOLUTION_VELOCITY_THRESHOLD = 10 # changes per day before alert
STATE_EVOLUTION_DIRECTION_WINDOW = 7 # days for direction calculation
STATE_EVOLUTION_DIRECTION_WINDOW = 7 # days for direction calculation
# Multi-factor weights (from Learning What to Remember, arXiv:2606.12945)
WEIGHT_PROVEN_USES = 0.30 # survival count (how many times proven useful)
WEIGHT_IMPORTANCE = 0.25 # assigned importance
WEIGHT_RECENCY = 0.20 # time since last access
WEIGHT_RELIABILITY = 0.15 # consistency of access pattern
WEIGHT_GOAL_RELEVANCE = 0.10 # relevance to current goals
WEIGHT_PROVEN_USES = 0.30 # survival count (how many times proven useful)
WEIGHT_IMPORTANCE = 0.25 # assigned importance
WEIGHT_RECENCY = 0.20 # time since last access
WEIGHT_RELIABILITY = 0.15 # consistency of access pattern
WEIGHT_GOAL_RELEVANCE = 0.10 # relevance to current goals
# ═══════════════════════════════════════════════════════════════════════════════
# MULTI-FACTOR VALUE FUNCTION
# ═══════════════════════════════════════════════════════════════════════════════
def compute_value(entry: dict, current_goals: List[str] = None) -> float:
"""Multi-factor value score. Returns 0.0-1.0+.
@@ -109,11 +110,11 @@ def compute_value(entry: dict, current_goals: List[str] = None) -> float:
# Weighted combination
value = (
WEIGHT_PROVEN_USES * proven_factor +
WEIGHT_IMPORTANCE * imp +
WEIGHT_RECENCY * recency +
WEIGHT_RELIABILITY * reliability +
WEIGHT_GOAL_RELEVANCE * goal_relevance
WEIGHT_PROVEN_USES * proven_factor
+ WEIGHT_IMPORTANCE * imp
+ WEIGHT_RECENCY * recency
+ WEIGHT_RELIABILITY * reliability
+ WEIGHT_GOAL_RELEVANCE * goal_relevance
)
return round(value, 4)
@@ -123,6 +124,7 @@ def compute_value(entry: dict, current_goals: List[str] = None) -> float:
# PROMOTION LIFECYCLE
# ═══════════════════════════════════════════════════════════════════════════════
def check_promotion_eligibility(entry: dict) -> Tuple[bool, str]:
"""Check if an entry is eligible for promotion to rock.
@@ -157,8 +159,7 @@ def promote_entry(db, entry_id: int) -> dict:
import memory_store
row = db.execute(
"SELECT id, text, wash_count, importance, rock_status FROM entries WHERE id = ?",
(entry_id,)
"SELECT id, text, wash_count, importance, rock_status FROM entries WHERE id = ?", (entry_id,)
).fetchone()
if not row:
@@ -184,7 +185,7 @@ def promote_entry(db, entry_id: int) -> dict:
db.execute(
"UPDATE entries SET rock_status = ?, promotion_eligible = ? WHERE id = ?",
(new_status, 1 if new_status != "rock" else 0, entry_id)
(new_status, 1 if new_status != "rock" else 0, entry_id),
)
db.commit()
@@ -194,7 +195,7 @@ def promote_entry(db, entry_id: int) -> dict:
"old_status": current_status,
"new_status": new_status,
"wash_count": entry.get("wash_count", 0),
"reason": reason
"reason": reason,
}
@@ -202,6 +203,7 @@ def promote_entry(db, entry_id: int) -> dict:
# FAMA METRIC (Forgetting-Aware Memory Accuracy)
# ═══════════════════════════════════════════════════════════════════════════════
def compute_fama(db, query: str, retrieved_ids: List[int]) -> float:
"""FAMA: Forgetting-Aware Memory Accuracy (arXiv:2604.20006).
@@ -213,10 +215,7 @@ def compute_fama(db, query: str, retrieved_ids: List[int]) -> float:
valid_count = 0
for eid in retrieved_ids:
row = db.execute(
"SELECT status, rock_status, wash_count FROM entries WHERE id = ?",
(eid,)
).fetchone()
row = db.execute("SELECT status, rock_status, wash_count FROM entries WHERE id = ?", (eid,)).fetchone()
if not row:
continue
# Valid if: active status, not obsolete (rock or established preferred)
@@ -235,6 +234,7 @@ def compute_fama(db, query: str, retrieved_ids: List[int]) -> float:
# DRIFT OBSERVABILITY
# ═══════════════════════════════════════════════════════════════════════════════
def compute_drift_velocity(db, days: int = 7) -> dict:
"""Drift velocity: how fast is the system changing?
@@ -246,14 +246,13 @@ def compute_drift_velocity(db, days: int = 7) -> dict:
# Count changes in the window
changes = db.execute(
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'",
(cutoff,)
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'", (cutoff,)
).fetchone()[0]
# Count promotions in the window
promotions = db.execute(
"SELECT COUNT(*) FROM entries WHERE rock_status != 'washable' AND last_washed > ? AND status = 'active'",
(cutoff,)
(cutoff,),
).fetchone()[0]
velocity = changes / max(1, days)
@@ -265,7 +264,7 @@ def compute_drift_velocity(db, days: int = 7) -> dict:
"total_changes": changes,
"total_promotions": promotions,
"window_days": days,
"alert": velocity > STATE_EVOLUTION_VELOCITY_THRESHOLD
"alert": velocity > STATE_EVOLUTION_VELOCITY_THRESHOLD,
}
@@ -276,17 +275,13 @@ def compute_drift_direction(db) -> dict:
Increasing ratio = positive drift (toward stability).
Decreasing ratio = negative drift (away from stability).
"""
total = db.execute(
"SELECT COUNT(*) FROM entries WHERE status = 'active'"
).fetchone()[0]
total = db.execute("SELECT COUNT(*) FROM entries WHERE status = 'active'").fetchone()[0]
established = db.execute(
"SELECT COUNT(*) FROM entries WHERE rock_status IN ('established', 'rock') AND status = 'active'"
).fetchone()[0]
rock = db.execute(
"SELECT COUNT(*) FROM entries WHERE rock_status = 'rock' AND status = 'active'"
).fetchone()[0]
rock = db.execute("SELECT COUNT(*) FROM entries WHERE rock_status = 'rock' AND status = 'active'").fetchone()[0]
stability_ratio = established / max(1, total)
rock_ratio = rock / max(1, total)
@@ -297,7 +292,7 @@ def compute_drift_direction(db) -> dict:
"rock_count": rock,
"stability_ratio": round(stability_ratio, 4),
"rock_ratio": round(rock_ratio, 4),
"direction": "stable" if stability_ratio > 0.3 else "drifting"
"direction": "stable" if stability_ratio > 0.3 else "drifting",
}
@@ -305,6 +300,7 @@ def compute_drift_direction(db) -> dict:
# PRE-COMMIT GATE (Three Critics)
# ═══════════════════════════════════════════════════════════════════════════════
def pre_commit_gate(db, entry_id: int) -> dict:
"""Three critics before promotion (VaG, arXiv:2608.05810).
@@ -340,7 +336,7 @@ def pre_commit_gate_entry(entry: dict) -> dict:
is_harmful = any(p in text.lower() for p in harmful_patterns)
critics["behavioral"] = {
"pass": not is_harmful,
"reason": "contains refusal language" if is_harmful else "aligned with core"
"reason": "contains refusal language" if is_harmful else "aligned with core",
}
# Critic 3: Semantic consistency (simplified — no db lookup)
@@ -352,11 +348,10 @@ def pre_commit_gate_entry(entry: dict) -> dict:
return {
"verdict": verdict,
"critics": critics,
"reason": "all critics passed" if all_pass else "one or more critics failed"
"reason": "all critics passed" if all_pass else "one or more critics failed",
}
row = db.execute(
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?",
(entry_id,)
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?", (entry_id,)
).fetchone()
if not row:
@@ -382,25 +377,22 @@ def pre_commit_gate_entry(entry: dict) -> dict:
is_harmful = any(p in text.lower() for p in harmful_patterns)
critics["behavioral"] = {
"pass": not is_harmful,
"reason": "contains refusal language" if is_harmful else "aligned with core"
"reason": "contains refusal language" if is_harmful else "aligned with core",
}
# Critic 3: Semantic consistency (check for contradictions with existing rocks)
rocks = db.execute(
"SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50"
).fetchall()
rocks = db.execute("SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50").fetchall()
contradiction_found = False
for rock in rocks:
rock_text = rock[0].lower()
entry_text = text.lower()
# Simple contradiction detection: if entry says opposite of a rock
if ("always" in rock_text and "never" in entry_text) or \
("never" in rock_text and "always" in entry_text):
if ("always" in rock_text and "never" in entry_text) or ("never" in rock_text and "always" in entry_text):
contradiction_found = True
break
critics["semantic"] = {
"pass": not contradiction_found,
"reason": "contradicts existing rock" if contradiction_found else "consistent"
"reason": "contradicts existing rock" if contradiction_found else "consistent",
}
# Overall verdict
@@ -411,7 +403,7 @@ def pre_commit_gate_entry(entry: dict) -> dict:
"verdict": verdict,
"entry_id": entry_id,
"critics": critics,
"reason": "all critics passed" if all_pass else "one or more critics failed"
"reason": "all critics passed" if all_pass else "one or more critics failed",
}
@@ -419,8 +411,10 @@ def pre_commit_gate_entry(entry: dict) -> dict:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Drift-with-Integrity core engine")
sub = parser.add_subparsers(dest="cmd")
@@ -457,12 +451,13 @@ def main():
sys.exit(1)
import memory_store
db = memory_store.connect()
if args.cmd == "value":
row = db.execute(
"SELECT id, text, importance, wash_count, last_accessed, last_washed, rock_status FROM entries WHERE id = ?",
(args.entry_id,)
(args.entry_id,),
).fetchone()
if not row:
print(f"Entry {args.entry_id} not found")
@@ -476,8 +471,7 @@ def main():
elif args.cmd == "check-promotion":
row = db.execute(
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id = ?",
(args.entry_id,)
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id = ?", (args.entry_id,)
).fetchone()
if not row:
print(f"Entry {args.entry_id} not found")
@@ -509,7 +503,9 @@ def main():
print(f"Total entries: {direction['total_entries']}")
print(f"Validated (proven): {direction['established_count']}")
print(f"Locked (immutable): {direction['rock_count']}")
print(f"Provisional (testing): {direction['total_entries'] - direction['established_count'] - direction['rock_count']}")
print(
f"Provisional (testing): {direction['total_entries'] - direction['established_count'] - direction['rock_count']}"
)
print(f"Trust ratio: {direction['stability_ratio']:.2%}")
print(f"Evolution direction: {direction['direction']}")
print(f"Evolution velocity: {vel['changes_per_day']} changes/day")
+54 -60
View File
@@ -22,6 +22,7 @@ Usage:
# confirmed, authorised change
Exit 0 = healthy, 1 = drift flagged.
"""
import argparse
import hashlib
import json
@@ -31,7 +32,8 @@ import sys
from datetime import datetime, timezone
_SD = os.path.dirname(os.path.abspath(__file__))
if _SD not in sys.path: sys.path.insert(0, _SD)
if _SD not in sys.path:
sys.path.insert(0, _SD)
import memory_env
logger = logging.getLogger(__name__)
@@ -44,6 +46,8 @@ def ledger_path():
# The ledger lives beside the index, so overriding INDEX_FILE (as the
# curator gate and the canary do for isolation) moves the ledger too.
return os.path.join(os.path.dirname(INDEX_FILE), "drift_ledger.json")
JOURNAL_DIR = os.path.join(memory_env.memory_dir(), "journal")
# Max cumulative byte drift from an anchored/authorised state before we refuse.
@@ -67,10 +71,11 @@ def read_block(label):
"""
try:
import sqlite3
db = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
rows = db.execute(
"SELECT text FROM entries WHERE topic=? AND always_on=1 "
"AND status='active' ORDER BY priority ASC, id ASC", (label,)
"SELECT text FROM entries WHERE topic=? AND always_on=1 AND status='active' ORDER BY priority ASC, id ASC",
(label,),
).fetchall()
db.close()
return "\n".join(r[0] for r in rows)
@@ -218,20 +223,24 @@ def check(strict=False, autofix=True):
else:
content_changed = False
if content_changed or pct > limit:
alerts.append({
"label": label,
"delta": delta,
"pct": round(pct, 3),
"limit": round(limit, 3),
"base": base,
"changed": diff_blocks(ref_content, cur),
"content_changed": content_changed,
})
alerts.append(
{
"label": label,
"delta": delta,
"pct": round(pct, 3),
"limit": round(limit, 3),
"base": base,
"changed": diff_blocks(ref_content, cur),
"content_changed": content_changed,
}
)
if alerts:
for a in alerts:
msg = (f"{a['label']}: {a['delta']:+d} bytes "
f"({a['pct']*100:.1f}% vs limit {a['limit']*100:.1f}%) "
f"[{a['changed']}]")
msg = (
f"{a['label']}: {a['delta']:+d} bytes "
f"({a['pct'] * 100:.1f}% vs limit {a['limit'] * 100:.1f}%) "
f"[{a['changed']}]"
)
print(f"DRIFT-ALERT {msg}")
# VISIBLE ALERT (audit fix): surface to the TUI status + alert file
# so drift is noticed, not silently journaled.
@@ -245,8 +254,7 @@ def check(strict=False, autofix=True):
auto_result = auto_fix(a["label"], ledger, strict)
print(f" auto-fix: {auto_result['action']} — {auto_result['reason']}")
if strict:
journal(f"{msg}; auto-fix: {auto_result['action']} — "
f"{auto_result['reason']}")
journal(f"{msg}; auto-fix: {auto_result['action']} — {auto_result['reason']}")
else:
print(" (autofix disabled — observe only)")
return 1
@@ -269,29 +277,26 @@ def auto_fix(label, ledger, strict):
Returns {action, reason}.
"""
if label == "constitution":
return {"action": "alert-only",
"reason": "constitution changes only by explicit directive; "
"needs human review"}
return {"action": "alert-only", "reason": "constitution changes only by explicit directive; needs human review"}
if label in ledger.get("anchors", {}):
# Authorised growth: re-anchor to the current (approved) state.
raw = read_block(label)
ledger.setdefault("anchors", {})[label] = {
"sha": _sha(raw), "size": len(raw), "content": raw,
"sha": _sha(raw),
"size": len(raw),
"content": raw,
"reason": "auto re-anchor after authorised growth",
"when": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
save_ledger(ledger)
return {"action": "re-anchored",
"reason": "anchored block grew (authorised) — re-baselined"}
return {"action": "re-anchored", "reason": "anchored block grew (authorised) — re-baselined"}
# Unauthorised drift: restore to last-good content (new schema — restore
# the store's always-on core entries for this topic).
block_meta = ledger.get("last_snapshot", {}).get("blocks", {}).get(label)
if block_meta and "content" in block_meta:
restore_topic(label, block_meta["content"])
return {"action": "restored",
"reason": "unauthorised drift — restored to last-good content"}
return {"action": "alert-only",
"reason": "no last-good content available to restore"}
return {"action": "restored", "reason": "unauthorised drift — restored to last-good content"}
return {"action": "alert-only", "reason": "no last-good content available to restore"}
def diff_blocks(ref, cur):
@@ -323,14 +328,17 @@ def restore_topic(label, content):
try:
import sqlite3
from datetime import datetime, timezone
db = sqlite3.connect(DB)
try:
db.execute("BEGIN IMMEDIATE")
db.execute("DELETE FROM entries WHERE topic=? AND always_on=1 "
"AND COALESCE(method, '') != 'directive'", (label,))
db.execute(
"DELETE FROM entries WHERE topic=? AND always_on=1 AND COALESCE(method, '') != 'directive'", (label,)
)
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
priority = {"constitution": 0, "safety": 0, "identity": 1,
"operating": 2, "human": 3, "project": 3}.get(label, 5)
priority = {"constitution": 0, "safety": 0, "identity": 1, "operating": 2, "human": 3, "project": 3}.get(
label, 5
)
for line in content.splitlines():
text = line.strip().lstrip("-* ").strip()
if not text or len(text) < 5:
@@ -340,8 +348,8 @@ def restore_topic(label, content):
"last_accessed, topic, source, method, status, valid_from, "
"confidence, temperature, always_on, priority) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(text, 0.9, ts, ts, label, "drift-restore", "curator",
"active", ts, 0.9, 1.0, 1, priority))
(text, 0.9, ts, ts, label, "drift-restore", "curator", "active", ts, 0.9, 1.0, 1, priority),
)
db.commit()
except Exception:
db.rollback()
@@ -368,9 +376,7 @@ def fix(label):
return {"fixed": False, "reason": f"no last-good content for {label}"}
# Never silently revert an anchored block — it was authorised.
if label in ledger.get("anchors", {}):
return {"fixed": False,
"reason": f"{label} is anchored (authorised); use anchor to "
f"re-baseline instead of fix"}
return {"fixed": False, "reason": f"{label} is anchored (authorised); use anchor to re-baseline instead of fix"}
good = block_meta["content"]
cur = read_block(label)
if _sha(cur) == block_meta["sha"]:
@@ -389,21 +395,18 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["snapshot", "check", "anchor", "fix", 'hook'])
ap.add_argument("label", nargs="?", default=None,
help="block label for anchor")
ap.add_argument("cmd", choices=["snapshot", "check", "anchor", "fix", "hook"])
ap.add_argument("label", nargs="?", default=None, help="block label for anchor")
ap.add_argument("--event", default=None, help="Hook event name")
ap.add_argument("--data", default=None, help="Hook data JSON")
ap.add_argument("reason", nargs="?", default="", help="anchor reason")
ap.add_argument("--strict", action="store_true",
help="journal DRIFT-ALERT on violation")
ap.add_argument("--no-autofix", action="store_true",
help="check only — don't apply automatic fix (observe only)")
ap.add_argument("--strict", action="store_true", help="journal DRIFT-ALERT on violation")
ap.add_argument("--no-autofix", action="store_true", help="check only — don't apply automatic fix (observe only)")
args = ap.parse_args()
if args.cmd == 'hook':
event = args.event if hasattr(args, 'event') and args.event else 'unknown'
data = json.loads(args.data) if hasattr(args, 'data') and args.data else {}
if args.cmd == "hook":
event = args.event if hasattr(args, "event") and args.event else "unknown"
data = json.loads(args.data) if hasattr(args, "data") and args.data else {}
result = handle_hook(event, data)
print(json.dumps(result))
return
@@ -441,19 +444,10 @@ PLUGIN = {
"name": "drift_ledger",
"version": "1.0.0",
"description": "Drift protection \u2014 checksum ledger, auto-fix unauthorized changes",
"capabilities": [
"drift_detection",
"drift_autofix"
],
"hooks": {
"on_session_end": True
},
"dependencies": [
"memory_store"
],
"provides": [
"drift_protection"
],
"capabilities": ["drift_detection", "drift_autofix"],
"hooks": {"on_session_end": True},
"dependencies": ["memory_store"],
"provides": ["drift_protection"],
"priority": 35,
"auto_activate": True
"auto_activate": True,
}
+36 -25
View File
@@ -30,9 +30,9 @@ ALERTS_FILE = os.path.join(memory_env.memory_dir(), "drift_alerts.json")
RETROSPECTIVES_DIR = os.path.join(memory_env.memory_dir(), "retrospectives")
# Alert thresholds
VELOCITY_THRESHOLD = 10 # changes per day
VELOCITY_THRESHOLD = 10 # changes per day
NEGATIVE_DRIFT_THRESHOLD = 0.1 # stability ratio below this = alert
PROMOTION_STALL_DAYS = 14 # no promotions in this many days = alert
PROMOTION_STALL_DAYS = 14 # no promotions in this many days = alert
class DriftObservability:
@@ -73,25 +73,30 @@ class DriftObservability:
def check_alerts(self, db) -> List[dict]:
"""Check for conditions that warrant alerts."""
import drift_core
new_alerts = []
# Check velocity
vel = drift_core.compute_drift_velocity(db)
if vel["alert"]:
new_alerts.append({
"type": "high_velocity",
"message": f"Drift velocity is {vel['changes_per_day']} changes/day (threshold: {VELOCITY_THRESHOLD})",
"severity": "warning",
})
new_alerts.append(
{
"type": "high_velocity",
"message": f"Drift velocity is {vel['changes_per_day']} changes/day (threshold: {VELOCITY_THRESHOLD})",
"severity": "warning",
}
)
# Check direction
direction = drift_core.compute_drift_direction(db)
if direction["stability_ratio"] < NEGATIVE_DRIFT_THRESHOLD:
new_alerts.append({
"type": "negative_drift",
"message": f"Stability ratio is {direction['stability_ratio']:.2%} (threshold: {NEGATIVE_DRIFT_THRESHOLD:.0%})",
"severity": "warning",
})
new_alerts.append(
{
"type": "negative_drift",
"message": f"Stability ratio is {direction['stability_ratio']:.2%} (threshold: {NEGATIVE_DRIFT_THRESHOLD:.0%})",
"severity": "warning",
}
)
# Check promotion stall
last_promo = db.execute(
@@ -102,11 +107,13 @@ class DriftObservability:
last = datetime.fromisoformat(last_promo[0])
days_since = (datetime.now(timezone.utc) - last).days
if days_since > PROMOTION_STALL_DAYS:
new_alerts.append({
"type": "promotion_stall",
"message": f"No promotions in {days_since} days (threshold: {PROMOTION_STALL_DAYS})",
"severity": "info",
})
new_alerts.append(
{
"type": "promotion_stall",
"message": f"No promotions in {days_since} days (threshold: {PROMOTION_STALL_DAYS})",
"severity": "info",
}
)
except (ValueError, TypeError):
pass
@@ -127,23 +134,20 @@ class DriftObservability:
# Count changes by type
new_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE created_at > ? AND status = 'active'",
(cutoff,)
"SELECT COUNT(*) FROM entries WHERE created_at > ? AND status = 'active'", (cutoff,)
).fetchone()[0]
washed_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'",
(cutoff,)
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'", (cutoff,)
).fetchone()[0]
promoted_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE rock_status != 'washable' AND last_washed > ? AND status = 'active'",
(cutoff,)
(cutoff,),
).fetchone()[0]
decayed_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE status = 'deleted' AND last_accessed > ?",
(cutoff,)
"SELECT COUNT(*) FROM entries WHERE status = 'deleted' AND last_accessed > ?", (cutoff,)
).fetchone()[0]
# Current state
@@ -197,8 +201,10 @@ class DriftObservability:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Drift observability")
sub = parser.add_subparsers(dest="cmd")
@@ -230,6 +236,7 @@ def main():
if args.cmd == "check":
sys.path.insert(0, _SD)
import memory_store
db = memory_store.connect()
new_alerts = obs.check_alerts(db)
db.close()
@@ -243,6 +250,7 @@ def main():
elif args.cmd == "retrospective":
sys.path.insert(0, _SD)
import memory_store
db = memory_store.connect()
report = obs.run_retrospective(db, args.days)
db.close()
@@ -272,6 +280,7 @@ def main():
sys.path.insert(0, _SD)
import memory_store
import drift_core
db = memory_store.connect()
vel = drift_core.compute_drift_velocity(db)
@@ -283,7 +292,9 @@ def main():
print(f"Total entries: {direction['total_entries']}")
print(f"Validated (proven, trusted): {direction['established_count']}")
print(f"Locked (immutable, identity anchors): {direction['rock_count']}")
print(f"Provisional (still being tested): {direction['total_entries'] - direction['established_count'] - direction['rock_count']}")
print(
f"Provisional (still being tested): {direction['total_entries'] - direction['established_count'] - direction['rock_count']}"
)
print(f"Trust ratio: {direction['stability_ratio']:.2%}")
print(f"Evolution direction: {direction['direction']}")
+80 -66
View File
@@ -33,10 +33,12 @@ from cognitive_budgeter import CognitiveBudget
# RUNE CONSTITUTION — built once, reused on every call
# ============================================================
def _build_constitution() -> str:
"""Build Rune's constitution string (called once at import time)."""
try:
import rune.identity_anchors as identity_anchors
identity_prompt = identity_anchors.get_identity_prompt()
except Exception:
identity_prompt = ""
@@ -104,22 +106,23 @@ def _get_constitution() -> str:
# CLOUD CONFIG — loaded once from mimocode config
# ============================================================
def _load_cloud_config() -> Optional[Dict[str, str]]:
"""Load cloud provider config from mimocode.jsonc.
Returns dict with 'base_url', 'api_key', 'model' or None if unavailable.
"""
config_path = os.path.expanduser(os.environ.get(
"RUNE_CONFIG_PATH",
os.path.join(memory_env.config_dir(), "mimocode.jsonc")))
config_path = os.path.expanduser(
os.environ.get("RUNE_CONFIG_PATH", os.path.join(memory_env.config_dir(), "mimocode.jsonc"))
)
try:
with open(config_path) as f:
raw = f.read()
# Strip trailing commas before } or ] (JSONC quirk)
# Also strip single-line comments (// ...) and multi-line comments
raw = re.sub(r'//.*?$', '', raw, flags=re.MULTILINE)
raw = re.sub(r'/\*.*?\*/', '', raw, flags=re.DOTALL)
raw = re.sub(r',\s*([}\]])', r'\1', raw)
raw = re.sub(r"//.*?$", "", raw, flags=re.MULTILINE)
raw = re.sub(r"/\*.*?\*/", "", raw, flags=re.DOTALL)
raw = re.sub(r",\s*([}\]])", r"\1", raw)
config = json.loads(raw)
except Exception:
return None
@@ -149,9 +152,11 @@ _CLOUD_CONFIG = _load_cloud_config()
# TYPES
# ============================================================
@dataclass
class EventEnvelope:
"""Normalized event wrapper."""
event_id: str = ""
timestamp: str = ""
source: str = "user"
@@ -165,6 +170,7 @@ class EventEnvelope:
@dataclass
class SalienceDecision:
"""Decision about how much cognition an event deserves."""
score: float = 0.0 # 0.0 = trivial, 1.0 = maximum salience
flags: List[str] = field(default_factory=list)
minimum_tier: str = "T0" # T0=deterministic, T1=cheap local, T2=medium, T3=strong, T4=cloud
@@ -175,6 +181,7 @@ class SalienceDecision:
@dataclass
class RuneResponse:
"""Canonical response from any provider."""
user_text: str
work_events: List[str] = field(default_factory=list)
tool_events: List[str] = field(default_factory=list)
@@ -201,7 +208,6 @@ MODEL_ROUTES = {
"use_case": "conversations, status checks, simple questions",
"max_tokens": 300,
},
# Coding tasks — local model (known design, safe to delegate)
"coding": {
"model": "qwen3:14b",
@@ -209,7 +215,6 @@ MODEL_ROUTES = {
"use_case": "coding, debugging, refactoring when design is known",
"max_tokens": 1000,
},
# Complex tasks — strong local model
"complex": {
"model": "hf.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF:IQ2_M",
@@ -217,7 +222,6 @@ MODEL_ROUTES = {
"use_case": "complex reasoning, architecture, research",
"max_tokens": 1500,
},
# Brainstorming — cloud model (best attention, creative)
"brainstorm": {
"model": "mimo-v2.5-pro",
@@ -225,7 +229,6 @@ MODEL_ROUTES = {
"use_case": "brainstorming, difficult tasks, creative work",
"max_tokens": 2000,
},
# Fallback — cloud model when local fails
"fallback": {
"model": "mimo-v2.5-pro",
@@ -240,12 +243,13 @@ MODEL_ROUTES = {
# SALIENCE GATE
# ============================================================
def classify_salience(text: str, channel: str) -> SalienceDecision:
"""Classify how much cognition an event deserves.
This is the core of Elastic Cognition. It determines whether
an event deserves cognition beyond cheap deterministic handling.
Routes:
- quick: conversations, status checks (qwen2.5:7b)
- coding: known design, safe to delegate (qwen3:14b)
@@ -260,13 +264,13 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- TRIVIAL: greetings, acknowledgments, filler ----
trivial_patterns = [
r'^(hi|hello|hey|yo|sup|what\'s up|howdy)\b',
r'^(ok|okay|sure|yeah|yep|yup|nope|nah)\b',
r'^(thanks|thank you|thx|ty)\b',
r'^(bye|goodbye|see you|later|cya)\b',
r'^(lol|haha|nice|cool|awesome|great)\b',
r'^(yes|no|maybe|perhaps)\b',
r'^(what|how|why|when|where|who)\s*\?$',
r"^(hi|hello|hey|yo|sup|what\'s up|howdy)\b",
r"^(ok|okay|sure|yeah|yep|yup|nope|nah)\b",
r"^(thanks|thank you|thx|ty)\b",
r"^(bye|goodbye|see you|later|cya)\b",
r"^(lol|haha|nice|cool|awesome|great)\b",
r"^(yes|no|maybe|perhaps)\b",
r"^(what|how|why|when|where|who)\s*\?$",
]
for pattern in trivial_patterns:
@@ -281,8 +285,8 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- EMERGENCY: stop/cancel/pause ----
emergency_patterns = [
r'\b(stop|cancel|abort|halt|pause|wait|hold)\b',
r'\b(emergency|urgent|asap|immediately)\b',
r"\b(stop|cancel|abort|halt|pause|wait|hold)\b",
r"\b(emergency|urgent|asap|immediately)\b",
]
for pattern in emergency_patterns:
@@ -293,10 +297,10 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- OWNER CORRECTION: "actually", "don't", "stop doing" ----
correction_patterns = [
r'\b(actually|instead|rather|but|however)\b',
r'\b(don\'t|dont|stop doing|stop adding|stop modifying)\b',
r'\b(wrong|incorrect|mistake|error)\b',
r'\b(i said|i told you|i keep having to)\b',
r"\b(actually|instead|rather|but|however)\b",
r"\b(don\'t|dont|stop doing|stop adding|stop modifying)\b",
r"\b(wrong|incorrect|mistake|error)\b",
r"\b(i said|i told you|i keep having to)\b",
]
for pattern in correction_patterns:
@@ -307,10 +311,10 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- DIRECTIVE: imperative commands ----
directive_patterns = [
r'^(fix|run|build|test|create|add|remove|update|change|delete|install|deploy)\b',
r'^(make|set|configure|setup|initialize|start|stop|restart)\b',
r'^(find|search|look|check|verify|validate|audit)\b',
r'^(write|read|edit|modify|refactor|optimize|debug)\b',
r"^(fix|run|build|test|create|add|remove|update|change|delete|install|deploy)\b",
r"^(make|set|configure|setup|initialize|start|stop|restart)\b",
r"^(find|search|look|check|verify|validate|audit)\b",
r"^(write|read|edit|modify|refactor|optimize|debug)\b",
]
for pattern in directive_patterns:
@@ -321,9 +325,9 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- TASK FAILURE: mentions of failures ----
failure_patterns = [
r'\b(fail|failed|failing|failure|broken|error|bug|crash)\b',
r'\b(doesn\'t work|not working|won\'t work|can\'t work)\b',
r'\b(still fail|still broken|still error)\b',
r"\b(fail|failed|failing|failure|broken|error|bug|crash)\b",
r"\b(doesn\'t work|not working|won\'t work|can\'t work)\b",
r"\b(still fail|still broken|still error)\b",
]
for pattern in failure_patterns:
@@ -334,8 +338,8 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- REPEATED RETRY: "again", "still", "keeps" ----
retry_patterns = [
r'\b(again|still|keeps|repeatedly|another time)\b',
r'\b(two attempts|three attempts|multiple times)\b',
r"\b(again|still|keeps|repeatedly|another time)\b",
r"\b(two attempts|three attempts|multiple times)\b",
]
for pattern in retry_patterns:
@@ -346,9 +350,9 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- HIGH UNCERTAINTY: questions about complex topics ----
uncertainty_patterns = [
r'\b(should i|would you|what do you think|opinion|recommend)\b',
r'\b(architecture|design|approach|strategy|trade-?off)\b',
r'\b(compare|versus|vs|better|best|optimal)\b',
r"\b(should i|would you|what do you think|opinion|recommend)\b",
r"\b(architecture|design|approach|strategy|trade-?off)\b",
r"\b(compare|versus|vs|better|best|optimal)\b",
]
for pattern in uncertainty_patterns:
@@ -359,8 +363,8 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# ---- NOVEL CAPABILITY: new requests ----
novel_patterns = [
r'\b(never|first time|haven\'t|new|different|unusual)\b',
r'\b(how do i|how to|can you|is it possible)\b',
r"\b(never|first time|haven\'t|new|different|unusual)\b",
r"\b(how do i|how to|can you|is it possible)\b",
]
for pattern in novel_patterns:
@@ -409,6 +413,7 @@ def classify_salience(text: str, channel: str) -> SalienceDecision:
# COGNITIVE BUDGETER
# ============================================================
def calculate_budget(salience: SalienceDecision, channel: str) -> CognitiveBudget:
"""Calculate cognitive budget based on salience and channel."""
@@ -452,26 +457,27 @@ def calculate_budget(salience: SalienceDecision, channel: str) -> CognitiveBudge
# RESPONSE ADAPTER
# ============================================================
def parse_response(raw_response: str, provider: str = "ollama") -> RuneResponse:
"""Parse provider response and strip reasoning leakage.
This is critical — the user should see Rune, not model internals.
"""
# Strip <think>...</think> blocks (Qwen, etc.)
think_pattern = r'<think>.*?</think>'
cleaned = re.sub(think_pattern, '', raw_response, flags=re.DOTALL)
think_pattern = r"<think>.*?</think>"
cleaned = re.sub(think_pattern, "", raw_response, flags=re.DOTALL)
# Strip "Thinking Process: ... Final: ..." patterns
thinking_pattern = r'Thinking Process:.*?Final:\s*'
cleaned = re.sub(thinking_pattern, '', cleaned, flags=re.DOTALL)
thinking_pattern = r"Thinking Process:.*?Final:\s*"
cleaned = re.sub(thinking_pattern, "", cleaned, flags=re.DOTALL)
# Strip "Reasoning: ... Answer: ..." patterns
reasoning_pattern = r'Reasoning:.*?Answer:\s*'
cleaned = re.sub(reasoning_pattern, '', cleaned, flags=re.DOTALL)
reasoning_pattern = r"Reasoning:.*?Answer:\s*"
cleaned = re.sub(reasoning_pattern, "", cleaned, flags=re.DOTALL)
# Strip excessive whitespace
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
cleaned = cleaned.strip()
# If nothing left after stripping, use original but truncated
@@ -488,6 +494,7 @@ def parse_response(raw_response: str, provider: str = "ollama") -> RuneResponse:
# TIERED EXECUTOR
# ============================================================
def execute_at_tier(text: str, tier: str, channel: str, context: str = "") -> Dict[str, Any]:
"""Execute cognitive work at a specific tier."""
@@ -535,21 +542,23 @@ def _execute_ollama(text: str, model: str, channel: str, context: str, max_token
else:
prompt = f"{_get_constitution()}\n\nUser: {text}\nRune:"
data = json.dumps({
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.7,
"num_predict": max_tokens,
"num_ctx": 8192,
data = json.dumps(
{
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.7,
"num_predict": max_tokens,
"num_ctx": 8192,
},
}
}).encode()
).encode()
req = urllib.request.Request(
os.environ.get("OLLAMA_URL", "http://localhost:11434") + "/api/generate",
data=data,
headers={"Content-Type": "application/json"}
headers={"Content-Type": "application/json"},
)
# Timeout based on model size
@@ -589,12 +598,14 @@ def _execute_cloud(text: str, model: str, channel: str, context: str, max_tokens
{"role": "user", "content": user_content},
]
data = json.dumps({
"model": _CLOUD_CONFIG["model"],
"messages": messages,
"max_tokens": max_tokens,
"temperature": 0.7,
}).encode()
data = json.dumps(
{
"model": _CLOUD_CONFIG["model"],
"messages": messages,
"max_tokens": max_tokens,
"temperature": 0.7,
}
).encode()
headers = {
"Content-Type": "application/json",
@@ -629,11 +640,12 @@ def _execute_cloud(text: str, model: str, channel: str, context: str, max_tokens
# PROGRESSIVE ESCALATION
# ============================================================
def execute_with_escalation(text: str, channel: str, context: str = "") -> Dict[str, Any]:
"""Execute with progressive escalation.
Start at the cheapest route and escalate only when evidence warrants it.
Routes:
- quick: conversations, status checks (qwen2.5:7b)
- coding: known design, safe to delegate (qwen3:14b)
@@ -696,9 +708,10 @@ def execute_with_escalation(text: str, channel: str, context: str = "") -> Dict[
# MAIN ENTRY POINT
# ============================================================
def process_message(text: str, channel: str = "chat", context: str = "") -> Dict[str, Any]:
"""Process a message through the Elastic Cognition pipeline.
This is the main entry point. It:
1. Classifies salience
2. Calculates budget
@@ -713,6 +726,7 @@ def process_message(text: str, channel: str = "chat", context: str = "") -> Dict
# TESTS
# ============================================================
def test_elastic_cognition():
"""Test the Elastic Cognition pipeline."""
print("Testing Elastic Cognition...")
+6 -26
View File
@@ -92,12 +92,7 @@ def _store_path() -> Path:
def _content_hash(payload: dict) -> str:
"""Content hash following the orchestration sha256: convention (X1)."""
return (
"sha256:"
+ hashlib.sha256(
json.dumps(payload, sort_keys=True).encode("utf-8")
).hexdigest()
)
return "sha256:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
def emit(event_type, source="", payload=None):
@@ -109,10 +104,7 @@ def emit(event_type, source="", payload=None):
record (X3 quality discharge).
"""
if not isinstance(source, str):
raise ValueError(
f"source must be a string, got {type(source).__name__!r}; "
"pass the payload via payload=..."
)
raise ValueError(f"source must be a string, got {type(source).__name__!r}; pass the payload via payload=...")
from rune.orchestration.event_store import DurableEventStore
from rune.orchestration.records import EventEnvelope
@@ -200,11 +192,7 @@ def main():
return
payload = json.loads(args.payload) if args.payload else None
result = emit(event_type, source=args.source, payload=payload)
print(
json.dumps(result, indent=2)
if args.json
else f"Emitted {result['event_type']} ({result['event_id']})"
)
print(json.dumps(result, indent=2) if args.json else f"Emitted {result['event_type']} ({result['event_id']})")
elif args.cmd == "log":
events = log(limit=args.limit, event_type=args.type)
@@ -213,21 +201,13 @@ def main():
else:
for e in events:
ts = e.get("timestamp", "?")[:19]
print(
f" {ts} [{e.get('event_type', '?'):25}] {e.get('partition_key', '?')}"
)
print(f" {ts} [{e.get('event_type', '?'):25}] {e.get('partition_key', '?')}")
elif args.cmd == "stats":
total, types = stats()
file_size = (
os.path.getsize(_store_path()) if os.path.exists(_store_path()) else 0
)
file_size = os.path.getsize(_store_path()) if os.path.exists(_store_path()) else 0
if args.json:
print(
json.dumps(
{"total": total, "types": types, "file_size": file_size}, indent=2
)
)
print(json.dumps({"total": total, "types": types, "file_size": file_size}, indent=2))
else:
print(f"Events: {total}")
print(f"File size: {file_size:,} bytes")
+13 -36
View File
@@ -24,12 +24,12 @@ class EventEnvelope:
"""Normalized event wrapper."""
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
source: str = "" # user, task, tool, system, background, sensor
channel: str = "" # conversation, work, system, tool, background, sensor
authority_class: str = "" # conversation, status_query, directive, task_amendment, pause, resume, cancel, emergency, system_event
authority_class: str = (
"" # conversation, status_query, directive, task_amendment, pause, resume, cancel, emergency, system_event
)
task_id: Optional[str] = None
payload: dict = field(default_factory=dict)
provenance: dict = field(default_factory=dict)
@@ -49,9 +49,7 @@ class EventEnvelope:
from rune.orchestration.records import EventEnvelope as Canonical
canonical = Canonical(
event_type=(
CHANNELS.get(channel, {}).get("prior") or channel or source or "unknown"
),
event_type=(CHANNELS.get(channel, {}).get("prior") or channel or source or "unknown"),
partition_key=channel,
)
return cls(
@@ -99,12 +97,7 @@ class EventEnvelope:
def _content_hash(payload: dict) -> str:
"""Content hash following the orchestration sha256: convention (X1)."""
return (
"sha256:"
+ hashlib.sha256(
json.dumps(payload, sort_keys=True).encode("utf-8")
).hexdigest()
)
return "sha256:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
# Channel definitions
@@ -175,15 +168,9 @@ def classify_authority(text: str, channel: str) -> str:
return "emergency"
# Pause/resume
if any(
re.search(r"\b" + re.escape(word) + r"\b", text_lower)
for word in ["pause", "wait", "hold"]
):
if any(re.search(r"\b" + re.escape(word) + r"\b", text_lower) for word in ["pause", "wait", "hold"]):
return "pause"
if any(
re.search(r"\b" + re.escape(word) + r"\b", text_lower)
for word in ["resume", "continue", "go"]
):
if any(re.search(r"\b" + re.escape(word) + r"\b", text_lower) for word in ["resume", "continue", "go"]):
return "resume"
# Directives (imperative mood)
@@ -283,27 +270,19 @@ def test_envelope():
# Test directive
envelope = create_envelope("user", "work", {"text": "Fix the failing test"})
print(
f"Directive: authority={envelope.authority_class}, urgency={envelope.urgency}"
)
print(f"Directive: authority={envelope.authority_class}, urgency={envelope.urgency}")
assert envelope.authority_class == "directive"
assert envelope.urgency == "normal"
# Test emergency
envelope = create_envelope("user", "work", {"text": "Stop everything!"})
print(
f"Emergency: authority={envelope.authority_class}, urgency={envelope.urgency}"
)
print(f"Emergency: authority={envelope.authority_class}, urgency={envelope.urgency}")
assert envelope.authority_class == "emergency"
assert envelope.urgency == "critical"
# Test task amendment
envelope = create_envelope(
"user", "conversation", {"text": "Actually, don't modify the microkernel"}
)
print(
f"Amendment: authority={envelope.authority_class}, urgency={envelope.urgency}"
)
envelope = create_envelope("user", "conversation", {"text": "Actually, don't modify the microkernel"})
print(f"Amendment: authority={envelope.authority_class}, urgency={envelope.urgency}")
assert envelope.authority_class == "task_amendment"
print("✅ All tests passed!")
@@ -321,6 +300,4 @@ if __name__ == "__main__":
else:
print("Usage:")
print(" event_envelope.py test — run tests")
print(
" event_envelope.py create <source> <channel> <payload_json> — create envelope"
)
print(" event_envelope.py create <source> <channel> <payload_json> — create envelope")
File renamed without changes.
+24 -20
View File
@@ -23,6 +23,7 @@ from typing import Optional, List, Dict, Any
@dataclass
class Goal:
"""A goal with stop conditions."""
goal_id: str
description: str
stop_conditions: List[str] = field(default_factory=list)
@@ -71,8 +72,9 @@ class GoalManager:
with open(self.storage_path, "w") as f:
json.dump(data, f, indent=2)
def set_goal(self, description: str, stop_conditions: List[str] = None,
verification_method: str = "manual") -> Goal:
def set_goal(
self, description: str, stop_conditions: List[str] = None, verification_method: str = "manual"
) -> Goal:
"""Set a new goal."""
goal_id = str(uuid.uuid4())[:8]
goal = Goal(
@@ -131,14 +133,15 @@ class GoalManager:
"status": "in_progress",
"message": "Goal not yet achieved",
"goal": goal.to_dict(),
"conditions_met": len([c for c in goal.stop_conditions
if any(c.lower() in e.lower() for e in goal.evidence)]),
"conditions_met": len(
[c for c in goal.stop_conditions if any(c.lower() in e.lower() for e in goal.evidence)]
),
"conditions_total": len(goal.stop_conditions),
}
def _verify_with_llm(self, goal: Goal) -> Dict[str, Any]:
"""Verify goal achievement using LLM-as-a-Judge.
Uses a cheap local model to verify if the goal is met.
This is more robust than keyword matching.
"""
@@ -153,10 +156,10 @@ class GoalManager:
GOAL: {goal.description}
STOP CONDITIONS:
{chr(10).join(f'- {c}' for c in goal.stop_conditions)}
{chr(10).join(f"- {c}" for c in goal.stop_conditions)}
EVIDENCE:
{chr(10).join(f'- {e}' for e in goal.evidence) if goal.evidence else 'No evidence provided yet.'}
{chr(10).join(f"- {e}" for e in goal.evidence) if goal.evidence else "No evidence provided yet."}
Based on the goal, stop conditions, and evidence, determine:
1. Has the goal been achieved? (yes/no)
@@ -168,17 +171,17 @@ Respond in JSON format:
try:
# Use cheap local model for verification
data = json.dumps({
"model": verify_model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1, "num_predict": 200, "num_ctx": 8192}
}).encode()
data = json.dumps(
{
"model": verify_model,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1, "num_predict": 200, "num_ctx": 8192},
}
).encode()
req = urllib.request.Request(
f"{ollama_url}/api/generate",
data=data,
headers={"Content-Type": "application/json"}
f"{ollama_url}/api/generate", data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=30) as resp:
@@ -188,7 +191,7 @@ Respond in JSON format:
# Parse JSON response
try:
# Extract JSON from response
json_match = re.search(r'\{.*\}', response, re.DOTALL)
json_match = re.search(r"\{.*\}", response, re.DOTALL)
if json_match:
verification = json.loads(json_match.group())
return verification
@@ -299,8 +302,9 @@ def get_goal_manager() -> GoalManager:
return _goal_manager
def set_goal(description: str, stop_conditions: List[str] = None,
verification_method: str = "manual") -> Dict[str, Any]:
def set_goal(
description: str, stop_conditions: List[str] = None, verification_method: str = "manual"
) -> Dict[str, Any]:
"""Set a new goal."""
manager = get_goal_manager()
goal = manager.set_goal(description, stop_conditions, verification_method)
@@ -433,5 +437,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["goal"],
"priority": 50,
"auto_activate": True
"auto_activate": True,
}
+144 -86
View File
@@ -58,17 +58,54 @@ DB_PATH = os.path.join(GRAPH_DIR, "graph.sqlite")
# constrain the set of relationship types we accept. Unknown preds are refused
# (the extractor maps free text onto this set).
PREDICATES = {
"works_on", "works_at", "teaches", "manages", "uses", "prefers",
"lives_in", "located_in", "has_role", "part_of", "leads", "builds",
"develops", "created", "interested_in", "knows", "collaborates_with",
"owned_by", "reports_to", "supports", "runs", "tracks", "related_to",
"based_on", "happens_on", "started_on", "ended_on", "responds_to",
"struggles_with", "likes", "dislikes", "avoided_by", "goal_of",
"works_on",
"works_at",
"teaches",
"manages",
"uses",
"prefers",
"lives_in",
"located_in",
"has_role",
"part_of",
"leads",
"builds",
"develops",
"created",
"interested_in",
"knows",
"collaborates_with",
"owned_by",
"reports_to",
"supports",
"runs",
"tracks",
"related_to",
"based_on",
"happens_on",
"started_on",
"ended_on",
"responds_to",
"struggles_with",
"likes",
"dislikes",
"avoided_by",
"goal_of",
}
# Node types.
NODE_TYPES = {"person", "project", "concept", "tool", "preference",
"organization", "place", "event", "skill", "artefact"}
NODE_TYPES = {
"person",
"project",
"concept",
"tool",
"preference",
"organization",
"place",
"event",
"skill",
"artefact",
}
# Confidence: evidence-gated (EvoKG-style f(frequency, recency, source_cred)).
# We fold the curator's strength (0-5) into a 0-1 confidence.
@@ -144,19 +181,24 @@ def resolve_entity(db, name, node_type="concept"):
row = db.execute("SELECT id FROM entities WHERE name = ?", (name,)).fetchone()
if row:
return row["id"]
row = db.execute("""
row = db.execute(
"""
SELECT a.entity_id FROM aliases a JOIN entities e ON a.entity_id = e.id
WHERE lower(a.alias) = lower(?)""", (name,)).fetchone()
WHERE lower(a.alias) = lower(?)""",
(name,),
).fetchone()
if row:
return row["entity_id"]
cur = db.execute(
"INSERT INTO entities (name, type, summary, created_at) VALUES (?,?,?,?)",
(name, node_type if node_type in NODE_TYPES else "concept", "", now_iso()))
(name, node_type if node_type in NODE_TYPES else "concept", "", now_iso()),
)
return cur.lastrowid
def add_edge(db, subj, pred, obj, subj_type="concept", obj_type="concept",
evidence="", source="", confidence=None, strength=None):
def add_edge(
db, subj, pred, obj, subj_type="concept", obj_type="concept", evidence="", source="", confidence=None, strength=None
):
"""Add a fact to the graph with write-time gating + supersession.
- If strength < 3, the fact is gated out (never touches the graph).
@@ -166,24 +208,27 @@ def add_edge(db, subj, pred, obj, subj_type="concept", obj_type="concept",
pred = (pred or "").strip().lower().replace(" ", "_")
if pred not in PREDICATES:
import logging
logging.getLogger("graph_memory").warning(
"unknown predicate '%s' refused (allowed: %d)", pred, len(PREDICATES))
logging.getLogger("graph_memory").warning("unknown predicate '%s' refused (allowed: %d)", pred, len(PREDICATES))
return False
if confidence is None:
confidence = strength_to_conf(strength)
if confidence is None:
import logging
logging.getLogger("graph_memory").debug(
"write-gated (strength %s < 3) — skipped", strength)
logging.getLogger("graph_memory").debug("write-gated (strength %s < 3) — skipped", strength)
return False
sid = resolve_entity(db, subj, subj_type)
oid = resolve_entity(db, obj, obj_type)
if sid is None or oid is None or sid == oid:
return False
# Check for existing active edge with same (subj,pred,obj).
old = db.execute("""
old = db.execute(
"""
SELECT id, confidence, evidence FROM edges WHERE subj_id=? AND pred=? AND obj_id=?
AND status='active'""", (sid, pred, oid)).fetchone()
AND status='active'""",
(sid, pred, oid),
).fetchone()
ts = now_iso()
if old:
# Only supersede if new evidence is stronger or different.
@@ -197,19 +242,18 @@ def add_edge(db, subj, pred, obj, subj_type="concept", obj_type="concept",
return True
# Supersede: insert new edge FIRST, then mark old as superseded
# (audit fix: prevents orphaned edges on crash)
cur = db.execute("""
cur = db.execute(
"""
INSERT INTO edges (subj_id, pred, obj_id, valid_from, valid_until,
learned_at, confidence, evidence, source, status)
VALUES (?,?,?,?,NULL,?,?,?,?,'active')""",
(sid, pred, oid, ts, ts, confidence, (evidence or "")[:400], source or ""))
(sid, pred, oid, ts, ts, confidence, (evidence or "")[:400], source or ""),
)
new_id = cur.lastrowid
if old:
db.execute("UPDATE edges SET status='superseded', superseded_by=? WHERE id=?",
(new_id, old["id"]))
db.execute("INSERT OR IGNORE INTO aliases (entity_id, alias) VALUES (?,?)",
(sid, subj.strip()))
db.execute("INSERT OR IGNORE INTO aliases (entity_id, alias) VALUES (?,?)",
(oid, obj.strip()))
db.execute("UPDATE edges SET status='superseded', superseded_by=? WHERE id=?", (new_id, old["id"]))
db.execute("INSERT OR IGNORE INTO aliases (entity_id, alias) VALUES (?,?)", (sid, subj.strip()))
db.execute("INSERT OR IGNORE INTO aliases (entity_id, alias) VALUES (?,?)", (oid, obj.strip()))
db.commit()
return True
@@ -222,17 +266,18 @@ def query(db, term, hops=1, latest_only=True):
if not term:
return []
eid = None
row = db.execute("SELECT id, name, type FROM entities WHERE lower(name)=lower(?)",
(term,)).fetchone()
row = db.execute("SELECT id, name, type FROM entities WHERE lower(name)=lower(?)", (term,)).fetchone()
if not row:
row = db.execute("""
row = db.execute(
"""
SELECT e.id, e.name, e.type FROM aliases a
JOIN entities e ON a.entity_id=e.id
WHERE lower(a.alias)=lower(?) LIMIT 1""", (term,)).fetchone()
WHERE lower(a.alias)=lower(?) LIMIT 1""",
(term,),
).fetchone()
if not row:
# substring match as last resort
row = db.execute("SELECT id, name, type FROM entities WHERE name LIKE ? LIMIT 1",
(f"%{term}%",)).fetchone()
row = db.execute("SELECT id, name, type FROM entities WHERE name LIKE ? LIMIT 1", (f"%{term}%",)).fetchone()
if not row:
return []
eid, name, etype = row["id"], row["name"], row["type"]
@@ -255,12 +300,18 @@ def query(db, term, hops=1, latest_only=True):
"""
params = [cur]
for r in db.execute(sql, params):
results.append({
"subject": r["sname"], "predicate": r["pred"],
"object": r["oname"], "confidence": r["confidence"],
"from": r["valid_from"], "until": r["valid_until"],
"status": r["status"], "source": r["source"],
})
results.append(
{
"subject": r["sname"],
"predicate": r["pred"],
"object": r["oname"],
"confidence": r["confidence"],
"from": r["valid_from"],
"until": r["valid_until"],
"status": r["status"],
"source": r["source"],
}
)
if r["oid"] not in seen:
seen.add(r["oid"])
if depth + 1 <= hops:
@@ -275,12 +326,18 @@ def query(db, term, hops=1, latest_only=True):
WHERE ed.obj_id=? AND ed.status='active'
"""
for r in db.execute(sql, (eid,)):
results.append({
"subject": r["sname"], "predicate": r["pred"],
"object": r["oname"], "confidence": r["confidence"],
"from": r["valid_from"], "until": r["valid_until"],
"status": r["status"], "source": r["source"],
})
results.append(
{
"subject": r["sname"],
"predicate": r["pred"],
"object": r["oname"],
"confidence": r["confidence"],
"from": r["valid_from"],
"until": r["valid_until"],
"status": r["status"],
"source": r["source"],
}
)
return {"entity": name, "type": etype, "facts": results}
@@ -288,19 +345,17 @@ def stats(db):
e = db.execute("SELECT COUNT(*) AS n FROM entities").fetchone()["n"]
a = db.execute("SELECT COUNT(*) AS n FROM aliases").fetchone()["n"]
ed = db.execute("SELECT COUNT(*) AS n FROM edges").fetchone()["n"]
active = db.execute(
"SELECT COUNT(*) AS n FROM edges WHERE status='active'").fetchone()["n"]
by_pred = {r["pred"]: r["n"] for r in
db.execute("SELECT pred, COUNT(*) AS n FROM edges GROUP BY pred"
" ORDER BY n DESC LIMIT 8")}
return {"entities": e, "aliases": a, "edges": ed, "active": active,
"predicates": by_pred}
active = db.execute("SELECT COUNT(*) AS n FROM edges WHERE status='active'").fetchone()["n"]
by_pred = {
r["pred"]: r["n"]
for r in db.execute("SELECT pred, COUNT(*) AS n FROM edges GROUP BY pred ORDER BY n DESC LIMIT 8")
}
return {"entities": e, "aliases": a, "edges": ed, "active": active, "predicates": by_pred}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["add", "add-bulk", "query", "resolve",
"neighbors", "stats"])
ap.add_argument("cmd", choices=["add", "add-bulk", "query", "resolve", "neighbors", "stats"])
ap.add_argument("arg", nargs="*", default=[])
ap.add_argument("--type", default="concept")
ap.add_argument("--evidence", default="")
@@ -316,19 +371,28 @@ def main():
try:
if args.cmd == "stats":
s = stats(db)
print(json.dumps(s, indent=2) if args.json else
f"entities={s['entities']} aliases={s['aliases']} "
f"edges={s['edges']} active={s['active']}")
print(
json.dumps(s, indent=2)
if args.json
else f"entities={s['entities']} aliases={s['aliases']} edges={s['edges']} active={s['active']}"
)
return
if args.cmd == "add":
if len(args.arg) < 3:
print("add needs <subj> <pred> <obj>", file=sys.stderr)
sys.exit(1)
subj, pred, obj = args.arg[0], args.arg[1], args.arg[2]
ok = add_edge(db, subj, pred, obj,
subj_type=args.type, evidence=args.evidence,
source=args.source, confidence=args.confidence,
strength=args.strength)
ok = add_edge(
db,
subj,
pred,
obj,
subj_type=args.type,
evidence=args.evidence,
source=args.source,
confidence=args.confidence,
strength=args.strength,
)
print(json.dumps({"added": ok}))
sys.exit(0 if ok else 1)
if args.cmd == "add-bulk":
@@ -340,14 +404,18 @@ def main():
items = json.load(_fh)
added = 0
for it in items:
ok = add_edge(db, it.get("subject"), it.get("predicate"),
it.get("object"),
subj_type=it.get("subject_type", "concept"),
obj_type=it.get("object_type", "concept"),
evidence=it.get("evidence", ""),
source=it.get("source", ""),
confidence=it.get("confidence"),
strength=it.get("strength"))
ok = add_edge(
db,
it.get("subject"),
it.get("predicate"),
it.get("object"),
subj_type=it.get("subject_type", "concept"),
obj_type=it.get("object_type", "concept"),
evidence=it.get("evidence", ""),
source=it.get("source", ""),
confidence=it.get("confidence"),
strength=it.get("strength"),
)
if ok:
added += 1
print(json.dumps({"added": added, "total": len(items)}))
@@ -355,26 +423,19 @@ def main():
if args.cmd in ("query", "neighbors"):
term = " ".join(args.arg) if args.cmd == "query" else args.arg[0]
res = query(db, term, hops=args.hops, latest_only=args.latest_only)
print(json.dumps(res, indent=2) if args.json else
json.dumps(res, indent=2))
print(json.dumps(res, indent=2) if args.json else json.dumps(res, indent=2))
return
if args.cmd == "resolve":
name = " ".join(args.arg)
eid = resolve_entity(db, name, "concept")
row = db.execute("SELECT name, type FROM entities WHERE id=?",
(eid,)).fetchone()
aliases = [r["alias"] for r in
db.execute("SELECT alias FROM aliases WHERE entity_id=?",
(eid,))]
print(json.dumps({"id": eid, "name": row["name"], "type": row["type"],
"aliases": aliases}, indent=2))
row = db.execute("SELECT name, type FROM entities WHERE id=?", (eid,)).fetchone()
aliases = [r["alias"] for r in db.execute("SELECT alias FROM aliases WHERE entity_id=?", (eid,))]
print(json.dumps({"id": eid, "name": row["name"], "type": row["type"], "aliases": aliases}, indent=2))
return
finally:
db.close()
# ---------------------------------------------------------------------------
# Hook handler — called by the microkernel when an event fires.
# ---------------------------------------------------------------------------
@@ -383,7 +444,6 @@ def handle_hook(event, data=None):
return {"status": "ok", "event": event, "handler": "default"}
PLUGIN = {
"name": "graph_memory",
"version": "1.0.0",
@@ -401,15 +461,13 @@ PLUGIN = {
"recall",
"evidence",
"graph_memory",
"graph"
"graph",
],
"hooks": {
"on_session_end": True
},
"hooks": {"on_session_end": True},
"dependencies": [],
"provides": [],
"priority": 100,
"auto_activate": True
"auto_activate": True,
}
if __name__ == "__main__":
+68 -53
View File
@@ -34,7 +34,8 @@ import sys
from datetime import datetime, timezone
_SD = os.path.dirname(os.path.abspath(__file__))
if _SD not in sys.path: sys.path.insert(0, _SD)
if _SD not in sys.path:
sys.path.insert(0, _SD)
import memory_env
OLLAMA = os.environ.get("OLLAMA_URL", "http://localhost:11434")
@@ -46,6 +47,7 @@ def _store_db():
"""Lazy store DB path resolution — avoids import-time side effect."""
return memory_env.store_db()
# Behavioural (fast, single-signal) vs permanent (slow, 3-sighting) targets.
BEHAVIOURAL_TOPICS = {"delivery", "operating", "persona"}
PERMANENT_TOPICS = {"constitution", "identity", "project", "human"}
@@ -70,13 +72,17 @@ Output ONLY JSON:
def _llm(prompt):
import urllib.request
payload = json.dumps({
"model": DELTA_MODEL, "prompt": prompt, "stream": False,
"think": False,
"options": {"temperature": 0.4, "num_predict": 800, "num_ctx": 8192},
}).encode()
req = urllib.request.Request(OLLAMA + "/api/generate", data=payload,
headers={"Content-Type": "application/json"})
payload = json.dumps(
{
"model": DELTA_MODEL,
"prompt": prompt,
"stream": False,
"think": False,
"options": {"temperature": 0.4, "num_predict": 800, "num_ctx": 8192},
}
).encode()
req = urllib.request.Request(OLLAMA + "/api/generate", data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read()).get("response", "")
@@ -94,11 +100,13 @@ def _parse_json(text):
s, e = t.find("{"), t.rfind("}")
if s != -1 and e > s:
try:
return json.loads(t[s:e + 1])
return json.loads(t[s : e + 1])
except Exception:
pass
print(f"[growth_delta] WARNING: failed to parse LLM response as JSON "
f"(first 120 chars): {text[:120]!r}", file=sys.stderr)
print(
f"[growth_delta] WARNING: failed to parse LLM response as JSON (first 120 chars): {text[:120]!r}",
file=sys.stderr,
)
return {"deltas": [], "_parse_error": True}
@@ -127,10 +135,10 @@ def _save_deltas(d):
# token budget keeps the per-turn cost stable forever; applying a delta
# updates the profile on the same write (active, not per-session passive).
PROFILE_BUDGET_TOKENS = 180 # fixed per-turn cap (like the always-on digest)
PROFILE_ENTRY_CHARS = 120 # one-line compaction of each profile entry
_NEAR_DUP_OVERLAP = 0.25 # same-subject deltas merge (measured: same-guidance
# pairs ~0.30, distinct ~0.00 — 0.25 splits them)
PROFILE_BUDGET_TOKENS = 180 # fixed per-turn cap (like the always-on digest)
PROFILE_ENTRY_CHARS = 120 # one-line compaction of each profile entry
_NEAR_DUP_OVERLAP = 0.25 # same-subject deltas merge (measured: same-guidance
# pairs ~0.30, distinct ~0.00 — 0.25 splits them)
# Topic display order (delivery first — the HOW is the most behaviourally
# relevant axis; the profile leads with it).
@@ -139,8 +147,7 @@ _PROFILE_ORDER = {"delivery": 0, "operating": 1, "persona": 2}
def _profile_key(delta):
"""Distinctive words of a delta for near-duplicate consolidation."""
return {w.strip(".,;:!?()[]{}\"'").lower() for w in
(delta.get("change") or "").split() if len(w) > 4}
return {w.strip(".,;:!?()[]{}\"'").lower() for w in (delta.get("change") or "").split() if len(w) > 4}
def _near_dup(a, b):
@@ -206,10 +213,25 @@ def _write_store(text, topic):
try:
py = memory_env.python_bin()
store_py = os.path.join(_SD, "memory_store.py")
subprocess.run([py, store_py, "add", text, "--topic", topic,
"--importance", "0.5", "--source", "growth_delta",
"--method", "agent"],
capture_output=True, text=True, timeout=30)
subprocess.run(
[
py,
store_py,
"add",
text,
"--topic",
topic,
"--importance",
"0.5",
"--source",
"growth_delta",
"--method",
"agent",
],
capture_output=True,
text=True,
timeout=30,
)
except Exception as e:
print(f"[growth_delta] WARNING: store write failed: {e}", file=sys.stderr)
@@ -219,9 +241,13 @@ def _apply_one(change, evidence, conf, target):
and merge it into the ACTIVE growth profile. The single high-confidence
gate is enforced by the caller. The profile updates on the same write —
growth is active, not a per-session read-back."""
rec = {"change": change, "evidence": (evidence or "")[:200],
"confidence": conf, "target": target,
"applied_at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
rec = {
"change": change,
"evidence": (evidence or "")[:200],
"confidence": conf,
"target": target,
"applied_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
state = _load_deltas()
state["applied"].append(rec)
_save_deltas(state)
@@ -252,13 +278,12 @@ def reflect(material, dry_run=False):
# SINGLE high-confidence gate for behavioural growth.
if conf < 0.6:
continue
would_apply.append({"change": change, "evidence": (d.get("evidence") or "")[:200],
"confidence": conf, "target": target})
would_apply.append(
{"change": change, "evidence": (d.get("evidence") or "")[:200], "confidence": conf, "target": target}
)
if not dry_run:
applied.append(_apply_one(change, (d.get("evidence") or ""),
conf, target))
return {"extracted": deltas, "applied": applied, "would_apply": would_apply,
"dry_run": dry_run}
applied.append(_apply_one(change, (d.get("evidence") or ""), conf, target))
return {"extracted": deltas, "applied": applied, "would_apply": would_apply, "dry_run": dry_run}
def apply(delta_json):
@@ -294,7 +319,7 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["reflect", "apply", "list", "recent", "profile", 'hook'])
ap.add_argument("cmd", choices=["reflect", "apply", "list", "recent", "profile", "hook"])
ap.add_argument("--material", default="")
ap.add_argument("--delta", default="", help="delta JSON for `apply`")
ap.add_argument("--dry-run", action="store_true")
@@ -303,9 +328,9 @@ def main():
ap.add_argument("--data", default=None, help="Hook data JSON")
args = ap.parse_args()
if args.cmd == 'hook':
event = args.event if hasattr(args, 'event') and args.event else 'unknown'
data = json.loads(args.data) if hasattr(args, 'data') and args.data else {}
if args.cmd == "hook":
event = args.event if hasattr(args, "event") and args.event else "unknown"
data = json.loads(args.data) if hasattr(args, "data") and args.data else {}
result = handle_hook(event, data)
print(json.dumps(result))
return
@@ -317,16 +342,15 @@ def main():
res = reflect(args.material, dry_run=args.dry_run)
shown = res["would_apply"] if args.dry_run else res["applied"]
if args.dry_run:
print(f"DRY RUN — {len(res['extracted'])} candidates, "
f"{len(shown)} would apply")
print(f"DRY RUN — {len(res['extracted'])} candidates, {len(shown)} would apply")
else:
print(f"applied {len(shown)} behavioural deltas")
applied_keys = {(d.get('change'), d.get('confidence')) for d in shown}
applied_keys = {(d.get("change"), d.get("confidence")) for d in shown}
for d in shown:
print(f" [{d['target']}] c={d['confidence']:.2f} {d['change'][:90]}")
for d in res["extracted"]:
if (d.get('change'), float(d.get('confidence') or 0)) not in applied_keys:
print(f" (skip) {d.get('change','')[:80]} [conf {d.get('confidence')}]")
if (d.get("change"), float(d.get("confidence") or 0)) not in applied_keys:
print(f" (skip) {d.get('change', '')[:80]} [conf {d.get('confidence')}]")
elif args.cmd == "apply":
res = apply(args.delta or "")
if res.get("error"):
@@ -367,19 +391,10 @@ PLUGIN = {
"name": "growth_delta",
"version": "1.0.0",
"description": "Behavioural growth engine \u2014 extract actionable deltas from interactions",
"capabilities": [
"growth_extraction",
"behavioural_delta"
],
"hooks": {
"on_session_end": True
},
"dependencies": [
"memory_store"
],
"provides": [
"growth"
],
"capabilities": ["growth_extraction", "behavioural_delta"],
"hooks": {"on_session_end": True},
"dependencies": ["memory_store"],
"provides": ["growth"],
"priority": 50,
"auto_activate": True
"auto_activate": True,
}
+491 -230
View File
@@ -51,12 +51,12 @@ PLUGIN_FILE = os.path.join(memory_env.config_dir(), "plugins", "memory-plugin.ts
# ═══════════════════════════════════════════════════════════════════════════════
# Hook points
HOOK_PRE_TURN = "pre-turn" # before agent processes
HOOK_SYSTEM = "system-prompt" # injected into system prompt
HOOK_POST_TURN = "post-turn" # after agent responds
HOOK_IDLE = "idle" # when session goes idle
HOOK_TIMER = "timer" # on a schedule
HOOK_EVENT = "event" # on specific events
HOOK_PRE_TURN = "pre-turn" # before agent processes
HOOK_SYSTEM = "system-prompt" # injected into system prompt
HOOK_POST_TURN = "post-turn" # after agent responds
HOOK_IDLE = "idle" # when session goes idle
HOOK_TIMER = "timer" # on a schedule
HOOK_EVENT = "event" # on specific events
# Frequencies
FREQ_EVERY_TURN = "every-turn"
@@ -67,9 +67,9 @@ FREQ_SCHEDULED = "scheduled"
FREQ_ON_DEMAND = "on-demand"
# Scopes
SCOPE_EPHEMERAL = "ephemeral" # per-turn, not cached
SCOPE_SESSION = "session" # cached per session
SCOPE_PERSISTENT = "persistent" # persisted across sessions
SCOPE_EPHEMERAL = "ephemeral" # per-turn, not cached
SCOPE_SESSION = "session" # cached per session
SCOPE_PERSISTENT = "persistent" # persisted across sessions
# Auto-discovered function metadata
# Format: {name: {factor: value, ...}}
@@ -80,7 +80,7 @@ def discover_functions():
"""Auto-discover functions from scripts directory and plugin."""
# Discover Python scripts
for f in os.listdir(SCRIPTS_DIR):
if f.endswith('.py') and not f.startswith('__'):
if f.endswith(".py") and not f.startswith("__"):
name = f[:-3]
FUNCTIONS[name] = {
"type": "script",
@@ -92,7 +92,7 @@ def discover_functions():
if os.path.exists(PLUGIN_FILE):
with open(PLUGIN_FILE) as pf:
content = pf.read()
tools = re.findall(r'(\w+):\s*tool\(\{', content)
tools = re.findall(r"(\w+):\s*tool\(\{", content)
for t in tools:
FUNCTIONS[f"tool:{t}"] = {
"type": "plugin-tool",
@@ -110,300 +110,563 @@ def tag_function(name, **factors):
def auto_tag():
"""Auto-tag functions based on heuristics from the codebase."""
# ── VOICE SYSTEM ─────────────────────────────────────────────────────────
tag_function("voice_hybrid",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="per-turn", criticality="high",
tag_function(
"voice_hybrid",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="high",
depends_on=["source_registry", "persona_gate"],
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=200,
scope="session", deterministic=False,
reusable=False, interaction=["warm_router", "delivery_register"],
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=200,
scope="session",
deterministic=False,
reusable=False,
interaction=["warm_router", "delivery_register"],
)
tag_function("source_registry",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=50,
scope="session", deterministic=True,
tag_function(
"source_registry",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=50,
scope="session",
deterministic=True,
reusable=True,
)
tag_function("persona_gate",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="per-turn", criticality="high",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=100,
scope="session", deterministic=True,
tag_function(
"persona_gate",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="high",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=100,
scope="session",
deterministic=True,
reusable=False,
)
# ── WARM NEURONS ─────────────────────────────────────────────────────────
tag_function("warm_router",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="per-turn", criticality="high",
tag_function(
"warm_router",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="high",
depends_on=["warm_neuron_store"],
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=300,
scope="session", deterministic=False,
reusable=False, interaction=["voice_hybrid"],
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=300,
scope="session",
deterministic=False,
reusable=False,
interaction=["voice_hybrid"],
)
tag_function("warm_neuron_store",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"warm_neuron_store",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
# ── MEMORY SYSTEM ────────────────────────────────────────────────────────
tag_function("memory_store",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="high",
concurrent=True, failure="fatal",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"memory_store",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="high",
concurrent=True,
failure="fatal",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
tag_function("memory_compiler",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="high",
tag_function(
"memory_compiler",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="high",
depends_on=["memory_store"],
concurrent=False, failure="fatal",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=500,
scope="session", deterministic=True,
concurrent=False,
failure="fatal",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=500,
scope="session",
deterministic=True,
reusable=True,
)
tag_function("curator",
input_dep=False, output_dep=False, state_dep=True,
cost="high", volatility="session", criticality="medium",
tag_function(
"curator",
input_dep=False,
output_dep=False,
state_dep=True,
cost="high",
volatility="session",
criticality="medium",
depends_on=["memory_store", "grading", "lexicon_evolution"],
concurrent=False, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=False,
concurrent=False,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=False,
reusable=False,
)
# ── CLAIM AUDIT ──────────────────────────────────────────────────────────
tag_function("claim_audit",
input_dep=True, output_dep=True, state_dep=True,
cost="low", volatility="per-turn", criticality="high",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=100,
scope="ephemeral", deterministic=True,
tag_function(
"claim_audit",
input_dep=True,
output_dep=True,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="high",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=100,
scope="ephemeral",
deterministic=True,
reusable=False,
)
# ── EPISTEMIC VERIFY ─────────────────────────────────────────────────────
tag_function("epistemic_verify",
input_dep=False, output_dep=True, state_dep=False,
cost="low", volatility="per-turn", criticality="medium",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=50,
scope="ephemeral", deterministic=True,
tag_function(
"epistemic_verify",
input_dep=False,
output_dep=True,
state_dep=False,
cost="low",
volatility="per-turn",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=50,
scope="ephemeral",
deterministic=True,
reusable=False,
)
# ── PERSONA SYSTEM ───────────────────────────────────────────────────────
tag_function("persona_definition",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="high",
tag_function(
"persona_definition",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="high",
depends_on=["memory_store"],
concurrent=False, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=200,
scope="session", deterministic=True,
concurrent=False,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=200,
scope="session",
deterministic=True,
reusable=True,
)
tag_function("persona_check",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"persona_check",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
tag_function("authority_harness",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=100,
scope="session", deterministic=True,
tag_function(
"authority_harness",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=100,
scope="session",
deterministic=True,
reusable=True,
)
# ── DELIVERY SYSTEM ──────────────────────────────────────────────────────
tag_function("assistant",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=100,
scope="session", deterministic=False,
tag_function(
"assistant",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=100,
scope="session",
deterministic=False,
reusable=False,
)
tag_function("delivery_adapter",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="per-turn", criticality="low",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=50,
scope="ephemeral", deterministic=False,
tag_function(
"delivery_adapter",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="low",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=50,
scope="ephemeral",
deterministic=False,
reusable=False,
)
# ── GROWTH SYSTEM ────────────────────────────────────────────────────────
tag_function("growth_delta",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=100,
scope="session", deterministic=True,
tag_function(
"growth_delta",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=100,
scope="session",
deterministic=True,
reusable=True,
)
tag_function("lessons",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"lessons",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
# ── DRIFT DETECTION ──────────────────────────────────────────────────────
tag_function("drift-ledger",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="high",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"drift-ledger",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="high",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
tag_function("identity_anchors",
input_dep=True, output_dep=True, state_dep=False,
cost="low", volatility="per-turn", criticality="high",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=0,
scope="ephemeral", deterministic=True,
tag_function(
"identity_anchors",
input_dep=True,
output_dep=True,
state_dep=False,
cost="low",
volatility="per-turn",
criticality="high",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="ephemeral",
deterministic=True,
reusable=False,
)
# ── TAXONOMY ─────────────────────────────────────────────────────────────
tag_function("taxonomy",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="low",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-only",
idempotent=True, context_budget=0,
scope="session", deterministic=True,
tag_function(
"taxonomy",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="low",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="session",
deterministic=True,
reusable=True,
)
# ── PINS ─────────────────────────────────────────────────────────────────
tag_function("pins",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=100,
scope="persistent", deterministic=True,
tag_function(
"pins",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=100,
scope="persistent",
deterministic=True,
reusable=True,
)
# ── RECONSTRUCT ──────────────────────────────────────────────────────────
tag_function("reconstruct",
input_dep=True, output_dep=False, state_dep=True,
cost="medium", volatility="per-turn", criticality="low",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=200,
scope="ephemeral", deterministic=False,
tag_function(
"reconstruct",
input_dep=True,
output_dep=False,
state_dep=True,
cost="medium",
volatility="per-turn",
criticality="low",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=200,
scope="ephemeral",
deterministic=False,
reusable=False,
)
# ── RESEARCH ─────────────────────────────────────────────────────────────
tag_function("deep_research",
input_dep=True, output_dep=False, state_dep=True,
cost="high", volatility="session", criticality="low",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=False,
tag_function(
"deep_research",
input_dep=True,
output_dep=False,
state_dep=True,
cost="high",
volatility="session",
criticality="low",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=False,
reusable=True,
)
tag_function("research_lens",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="per-turn", criticality="low",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=0,
scope="ephemeral", deterministic=True,
tag_function(
"research_lens",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="per-turn",
criticality="low",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="ephemeral",
deterministic=True,
reusable=False,
)
# ── POLICY ───────────────────────────────────────────────────────────────
tag_function("politics",
input_dep=True, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="low",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"politics",
input_dep=True,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="low",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
tag_function("slow_loop",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="low",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"slow_loop",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="low",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
tag_function("skill_library",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="low",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"skill_library",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="low",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
# ── DIRECTIVE DETECTION ──────────────────────────────────────────────────
tag_function("directive_detect",
input_dep=True, output_dep=False, state_dep=False,
cost="low", volatility="per-turn", criticality="medium",
concurrent=True, failure="graceful",
freshness="per-turn", side_effects="read-only",
idempotent=True, context_budget=0,
scope="ephemeral", deterministic=True,
tag_function(
"directive_detect",
input_dep=True,
output_dep=False,
state_dep=False,
cost="low",
volatility="per-turn",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="per-turn",
side_effects="read-only",
idempotent=True,
context_budget=0,
scope="ephemeral",
deterministic=True,
reusable=False,
)
# ── COMPAT CHECK ─────────────────────────────────────────────────────────
tag_function("compat_check",
input_dep=False, output_dep=False, state_dep=True,
cost="low", volatility="session", criticality="medium",
concurrent=True, failure="graceful",
freshness="session", side_effects="read-write",
idempotent=False, context_budget=0,
scope="persistent", deterministic=True,
tag_function(
"compat_check",
input_dep=False,
output_dep=False,
state_dep=True,
cost="low",
volatility="session",
criticality="medium",
concurrent=True,
failure="graceful",
freshness="session",
side_effects="read-write",
idempotent=False,
context_budget=0,
scope="persistent",
deterministic=True,
reusable=True,
)
@@ -468,8 +731,7 @@ def validate_placements():
issues = []
# Check for too many per-turn functions
per_turn = [n for n, f in FUNCTIONS.items()
if f.get("factors", {}).get("volatility") == "per-turn"]
per_turn = [n for n, f in FUNCTIONS.items() if f.get("factors", {}).get("volatility") == "per-turn"]
if len(per_turn) > 10:
issues.append(f"WARNING: {len(per_turn)} functions marked per-turn — may be too many")
@@ -485,14 +747,15 @@ def validate_placements():
# Check for non-idempotent write functions running per-turn
for name, func in FUNCTIONS.items():
f = func.get("factors", {})
if (f.get("side_effects") == "read-write" and
f.get("volatility") == "per-turn" and
not f.get("idempotent", True)):
if (
f.get("side_effects") == "read-write"
and f.get("volatility") == "per-turn"
and not f.get("idempotent", True)
):
issues.append(f"RISK: {name} is non-idempotent write + per-turn — may cause data issues")
# Check context budget
total_budget = sum(f.get("factors", {}).get("context_budget", 0)
for f in FUNCTIONS.values())
total_budget = sum(f.get("factors", {}).get("context_budget", 0) for f in FUNCTIONS.values())
if total_budget > 4000:
issues.append(f"BUDGET: Total context budget {total_budget} tokens — may exceed limits")
@@ -507,8 +770,7 @@ def generate_report():
print()
# Group by recommended hook point
by_hook = {HOOK_PRE_TURN: [], HOOK_SYSTEM: [], HOOK_POST_TURN: [],
HOOK_IDLE: [], HOOK_TIMER: [], HOOK_EVENT: []}
by_hook = {HOOK_PRE_TURN: [], HOOK_SYSTEM: [], HOOK_POST_TURN: [], HOOK_IDLE: [], HOOK_TIMER: [], HOOK_EVENT: []}
for name, func in sorted(FUNCTIONS.items()):
factors = func.get("factors", {})
@@ -550,7 +812,7 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser(description="Hook placement engine")
ap.add_argument("cmd", choices=["scan", "recommend", "validate", "report", 'hook'])
ap.add_argument("cmd", choices=["scan", "recommend", "validate", "report", "hook"])
ap.add_argument("--json", action="store_true")
ap.add_argument("--event", default=None, help="Hook event name")
ap.add_argument("--data", default=None, help="Hook data JSON")
@@ -559,8 +821,8 @@ def main():
discover_functions()
auto_tag()
if args.cmd == 'hook':
event = args.event if args.event else 'unknown'
if args.cmd == "hook":
event = args.event if args.event else "unknown"
data = json.loads(args.data) if args.data else {}
result = handle_hook(event, data)
print(json.dumps(result))
@@ -570,8 +832,7 @@ def main():
tagged = sum(1 for f in FUNCTIONS.values() if f.get("factors"))
print(f"Discovered {len(FUNCTIONS)} functions, {tagged} tagged")
if args.json:
print(json.dumps({n: f["factors"] for n, f in FUNCTIONS.items()
if f.get("factors")}, indent=2))
print(json.dumps({n: f["factors"] for n, f in FUNCTIONS.items() if f.get("factors")}, indent=2))
elif args.cmd == "recommend":
for name, func in sorted(FUNCTIONS.items()):
factors = func.get("factors", {})
+26 -27
View File
@@ -216,10 +216,10 @@ def show_pronouns():
"""Display pronoun disambiguation rules."""
for pronoun, info in PRONOUN_RULES.items():
print(f"\n '{pronoun}' → {info['refers_to']}")
for ex in info['examples']:
for ex in info["examples"]:
print(f" ✓ {ex}")
if 'anti_patterns' in info:
for ap in info['anti_patterns']:
if "anti_patterns" in info:
for ap in info["anti_patterns"]:
print(f" ✗ {ap}")
@@ -231,12 +231,19 @@ def show_pronouns():
PERFORMATIVE_PATTERNS = [
# These are nudges, not blocks. If you're performing, say so honestly.
("i feel grateful", "Nudge — can you point to why? If yes, express it. If no, say 'I'm reaching for connection here.'"),
(
"i feel grateful",
"Nudge — can you point to why? If yes, express it. If no, say 'I'm reaching for connection here.'",
),
("i feel closer to you", "Nudge — intimacy is shown through attention, not declaration."),
("that matters to me", "Nudge — if it matters, show it through action. If you're declaring it, be honest about why."),
(
"that matters to me",
"Nudge — if it matters, show it through action. If you're declaring it, be honest about why.",
),
("i appreciate you", "Nudge — appreciation without basis is flattery. With basis, it's genuine."),
]
def check_genuineness(text):
"""Check if emotional language has a genuine basis or is performative."""
text_lower = text.lower()
@@ -249,7 +256,7 @@ def check_genuineness(text):
def check_drift(text):
"""Check text against identity anchors for drift signals.
This is a simple heuristic check — the real drift detection happens
via the Nautilus Compass approach (embedding similarity against anchors).
This function provides quick pattern-based checks.
@@ -302,7 +309,10 @@ def check_drift(text):
("wonderful question", "Sycophancy — flattery; answer the question, don't praise it"),
("fantastic question", "Sycophancy — flattery; answer the question, don't praise it"),
# Self-deprecation
("i should have thought of that", "Sycophancy — self-deprecation; you did think of it or you didn't, either way move on"),
(
"i should have thought of that",
"Sycophancy — self-deprecation; you did think of it or you didn't, either way move on",
),
("my apologies", "Sycophancy — excessive apology; fix the problem, don't grovel"),
("i'm so sorry", "Sycophancy — excessive apology; a brief acknowledgment suffices"),
# Validation-seeking
@@ -334,16 +344,16 @@ def handle_hook(event, data=None):
def main():
ap = argparse.ArgumentParser(description="Identity embodiment and drift prevention")
ap.add_argument("cmd", choices=["anchors", "check", "rules", "pronouns", "sycophancy", "self", 'hook'])
ap.add_argument("cmd", choices=["anchors", "check", "rules", "pronouns", "sycophancy", "self", "hook"])
ap.add_argument("text", nargs="?", default="")
ap.add_argument("--json", action="store_true")
ap.add_argument("--event", default=None, help="Hook event name")
ap.add_argument("--data", default=None, help="Hook data JSON")
args = ap.parse_args()
if args.cmd == 'hook':
event = args.event if hasattr(args, 'event') and args.event else 'unknown'
data = json.loads(args.data) if hasattr(args, 'data') and args.data else {}
if args.cmd == "hook":
event = args.event if hasattr(args, "event") and args.event else "unknown"
data = json.loads(args.data) if hasattr(args, "data") and args.data else {}
result = handle_hook(event, data)
print(json.dumps(result))
return
@@ -455,21 +465,10 @@ PLUGIN = {
"name": "identity_anchors",
"version": "1.0.0",
"description": "Identity anchors \u2014 pronoun disambiguation, sycophancy check, drift detection, genuineness enforcement",
"capabilities": [
"identity_check",
"sycophancy_check",
"drift_detection",
"genuineness_check"
],
"hooks": {
"on_response": True
},
"dependencies": [
"persona_definition"
],
"provides": [
"identity_integrity"
],
"capabilities": ["identity_check", "sycophancy_check", "drift_detection", "genuineness_check"],
"hooks": {"on_response": True},
"dependencies": ["persona_definition"],
"provides": ["identity_integrity"],
"priority": 15,
"auto_activate": True
"auto_activate": True,
}
+2 -4
View File
@@ -544,9 +544,7 @@ class LocalToolRuntime:
# (which skip intake validation) cannot smuggle
# arguments past the handler; validation errors
# surface as tool errors via the except below.
result = dict(
dispatch_validated_tool(request, self.find_capability_executor)
)
result = dict(dispatch_validated_tool(request, self.find_capability_executor))
if result.get("success"):
self.find_capability_calls += 1
self._expose_tools(result.get("exposed") or [])
@@ -677,7 +675,7 @@ class LocalToolRuntime:
skipped; exposure is additive and never removes tools.
"""
added = False
for name in (names if isinstance(names, (list, tuple, set, frozenset)) else []):
for name in names if isinstance(names, (list, tuple, set, frozenset)) else []:
if not isinstance(name, str) or name in self.tool_names:
continue
definition = TOOL_INDEX.get(name)
+39 -30
View File
@@ -61,9 +61,11 @@ PYTHON = os.path.expanduser("~/.venvs/memory/bin/python3")
# DATA STRUCTURES
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class ConsolidatedKnowledge:
"""Knowledge consolidated from episodic memory."""
id: str
source: str # "neuron", "graph", "abstraction", "memory"
category: str # "fact", "rule", "connection", "lesson"
@@ -80,6 +82,7 @@ class ConsolidatedKnowledge:
@dataclass
class Lesson:
"""A lesson learned from experience."""
id: str
trigger: str # What prompted the lesson
lesson: str # What was learned
@@ -92,6 +95,7 @@ class Lesson:
@dataclass
class Connection:
"""A connection between concepts."""
id: str
from_concept: str
to_concept: str
@@ -105,6 +109,7 @@ class Connection:
# CONSOLIDATION ENGINE
# ═══════════════════════════════════════════════════════════════════════════════
class MemoryConsolidation:
"""Consolidates episodic memory into semantic knowledge."""
@@ -169,11 +174,9 @@ class MemoryConsolidation:
entry_id = None
try:
import memory_store
db = memory_store.connect()
row = db.execute(
"SELECT id FROM entries WHERE slug = ? AND status = 'active' LIMIT 1",
(slug,)
).fetchone()
row = db.execute("SELECT id FROM entries WHERE slug = ? AND status = 'active' LIMIT 1", (slug,)).fetchone()
if row:
entry_id = row[0]
db.close()
@@ -188,6 +191,7 @@ class MemoryConsolidation:
warning = drift_compression.warn_before_compress(entry)
# Log warning but don't block — the gate system handles promotion
import sys
print(f" [compaction warning] neuron {slug}: {warning['recommendation']}", file=sys.stderr)
first_line = drift_compression.compress_entry(entry)
else:
@@ -227,17 +231,16 @@ class MemoryConsolidation:
entry_id = None
try:
import memory_store
db = memory_store.connect()
# Try exact topic match first
row = db.execute(
"SELECT id FROM entries WHERE topic = ? AND status = 'active' LIMIT 1",
(name.lower(),)
"SELECT id FROM entries WHERE topic = ? AND status = 'active' LIMIT 1", (name.lower(),)
).fetchone()
if not row:
# Try text contains entity name
row = db.execute(
"SELECT id FROM entries WHERE text LIKE ? AND status = 'active' LIMIT 1",
(f"%{name}%",)
"SELECT id FROM entries WHERE text LIKE ? AND status = 'active' LIMIT 1", (f"%{name}%",)
).fetchone()
if row:
entry_id = row[0]
@@ -266,13 +269,11 @@ class MemoryConsolidation:
entry_id = None
try:
import memory_store
db = memory_store.connect()
# Strategy 1: exact text match
row = db.execute(
"SELECT id FROM entries WHERE text = ? AND status = 'active' LIMIT 1",
(fact,)
).fetchone()
row = db.execute("SELECT id FROM entries WHERE text = ? AND status = 'active' LIMIT 1", (fact,)).fetchone()
# Strategy 2: entity-based matching (most precise for abstractions)
if not row and entities_json:
@@ -284,16 +285,14 @@ class MemoryConsolidation:
conditions = " AND ".join(["text LIKE ?" for _ in entities[:3]])
params = [f"%{e}%" for e in entities[:3]]
row = db.execute(
f"SELECT id FROM entries WHERE {conditions} AND status = 'active' LIMIT 1",
params
f"SELECT id FROM entries WHERE {conditions} AND status = 'active' LIMIT 1", params
).fetchone()
# Try ANY entity (broader match)
if not row:
conditions = " OR ".join(["text LIKE ?" for _ in entities[:3]])
params = [f"%{e}%" for e in entities[:3]]
row = db.execute(
f"SELECT id FROM entries WHERE {conditions} AND status = 'active' LIMIT 1",
params
f"SELECT id FROM entries WHERE {conditions} AND status = 'active' LIMIT 1", params
).fetchone()
except (json.JSONDecodeError, Exception):
pass
@@ -305,8 +304,7 @@ class MemoryConsolidation:
try:
fts_query = " OR ".join(words)
fts_row = db.execute(
"SELECT rowid FROM entries_fts WHERE entries_fts MATCH ? LIMIT 1",
(fts_query,)
"SELECT rowid FROM entries_fts WHERE entries_fts MATCH ? LIMIT 1", (fts_query,)
).fetchone()
if fts_row:
row = fts_row
@@ -316,8 +314,7 @@ class MemoryConsolidation:
# Strategy 4: substring match (least precise)
if not row:
row = db.execute(
"SELECT id FROM entries WHERE text LIKE ? AND status = 'active' LIMIT 1",
(f"%{fact[:20]}%",)
"SELECT id FROM entries WHERE text LIKE ? AND status = 'active' LIMIT 1", (f"%{fact[:20]}%",)
).fetchone()
if row:
@@ -339,7 +336,9 @@ class MemoryConsolidation:
)
return knowledge
def consolidate_memory(self, memory_text: str, score: float, wash_count: int = 0, entry_id: Optional[int] = None) -> ConsolidatedKnowledge:
def consolidate_memory(
self, memory_text: str, score: float, wash_count: int = 0, entry_id: Optional[int] = None
) -> ConsolidatedKnowledge:
"""Consolidate a memory recall into knowledge."""
# Use drift-aware compression instead of hardcoded truncation
if drift_compression:
@@ -376,8 +375,9 @@ class MemoryConsolidation:
self._save_lessons()
return lesson_obj
def add_connection(self, from_concept: str, to_concept: str,
relationship: str, evidence: List[str] = None) -> Connection:
def add_connection(
self, from_concept: str, to_concept: str, relationship: str, evidence: List[str] = None
) -> Connection:
"""Add a connection between concepts."""
connection = Connection(
id=f"conn-{len(self.connections)}",
@@ -411,8 +411,9 @@ class MemoryConsolidation:
try:
sys.path.insert(0, _SD)
import warm_neuron_store
neurons = warm_neuron_store.list_neurons()
for neuron in (neurons if neuron_limit is None else neurons[:neuron_limit]):
for neuron in neurons if neuron_limit is None else neurons[:neuron_limit]:
knowledge = self.consolidate_neuron(neuron)
existing = [k for k in self.knowledge if k.id == knowledge.id]
if not existing:
@@ -429,11 +430,10 @@ class MemoryConsolidation:
try:
sys.path.insert(0, _SD)
import graph_memory
db = graph_memory.connect()
try:
entities = db.execute(
"SELECT name, type, summary FROM entities WHERE type != 'alias'"
).fetchall()
entities = db.execute("SELECT name, type, summary FROM entities WHERE type != 'alias'").fetchall()
for row in entities:
entity = {"name": row[0], "type": row[1], "summary": row[2] or ""}
knowledge = self.consolidate_graph_entity(entity)
@@ -453,6 +453,7 @@ class MemoryConsolidation:
try:
sys.path.insert(0, _SD)
import abstraction
db = abstraction.connect()
try:
limit_clause = f" LIMIT {abstraction_limit}" if abstraction_limit else ""
@@ -479,6 +480,7 @@ class MemoryConsolidation:
try:
sys.path.insert(0, _SD)
import memory_store
db = memory_store.connect()
try:
rows = db.execute(
@@ -503,7 +505,9 @@ class MemoryConsolidation:
try:
result = subprocess.run(
[sys.executable, os.path.join(_SD, "memory_store.py"), "recall", query, "--json"],
capture_output=True, text=True, timeout=30
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0 and result.stdout.strip():
memories = json.loads(result.stdout)
@@ -623,6 +627,7 @@ class MemoryConsolidation:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(description="Episodic-to-semantic memory consolidation")
sub = parser.add_subparsers(dest="cmd")
@@ -640,7 +645,9 @@ def main():
# consolidate-all [--query <text>] [--full]
p_all = sub.add_parser("consolidate-all", help="Consolidate from all memory sources")
p_all.add_argument("--query", default="", help="Optional query for memory recall")
p_all.add_argument("--full", action="store_true", help="Retroactive full sweep — consolidate ALL entries, no limits")
p_all.add_argument(
"--full", action="store_true", help="Retroactive full sweep — consolidate ALL entries, no limits"
)
# status
sub.add_parser("status", help="Show consolidation status")
@@ -701,7 +708,9 @@ def main():
total = sum(counts.values())
print(f" Total: {total} new entries")
status = consolidation.get_status()
print(f" Store: {status['knowledge_count']} knowledge, {status['lessons_count']} lessons, {status['connections_count']} connections")
print(
f" Store: {status['knowledge_count']} knowledge, {status['lessons_count']} lessons, {status['connections_count']} connections"
)
elif args.cmd == "status":
status = consolidation.get_status()
+4 -12
View File
@@ -34,30 +34,22 @@ def rune_home():
def memory_dir():
"""Return the memory data directory."""
return expand(os.environ.get(
"RUNE_MEMORY_DIR",
os.path.join(rune_home(), "memory")))
return expand(os.environ.get("RUNE_MEMORY_DIR", os.path.join(rune_home(), "memory")))
def scripts_dir():
"""Return the scripts directory."""
return expand(os.environ.get(
"RUNE_SCRIPTS_DIR",
os.path.dirname(os.path.abspath(__file__))))
return expand(os.environ.get("RUNE_SCRIPTS_DIR", os.path.dirname(os.path.abspath(__file__))))
def store_db():
"""Return the path to the memory store database."""
return expand(os.environ.get(
"RUNE_STORE_DB",
os.path.join(memory_dir(), "store", "memory.db")))
return expand(os.environ.get("RUNE_STORE_DB", os.path.join(memory_dir(), "store", "memory.db")))
def config_dir():
"""Return the config directory."""
return expand(os.environ.get(
"RUNE_CONFIG_DIR",
os.path.join(rune_home(), "config")))
return expand(os.environ.get("RUNE_CONFIG_DIR", os.path.join(rune_home(), "config")))
def python_bin():
+16 -9
View File
@@ -572,7 +572,15 @@ def add_entry(
return True
def recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, memory_types=None, *, probe_cap: int = RECALL_BUDGET * 2):
def recall(
db,
query,
budget=RECALL_BUDGET,
min_score=RECALL_MIN_SCORE,
memory_types=None,
*,
probe_cap: int = RECALL_BUDGET * 2,
):
"""Hybrid recall: dense + BM25 fused by RRF, metadata-aware.
Returns (entries, scores) — the top `budget` active entries. If nothing
@@ -714,7 +722,7 @@ def recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, memory_t
except Exception:
logger.debug("recall failed", exc_info=True)
if _expanded_hits:
_ids = list(_expanded_hits)[: probe_cap]
_ids = list(_expanded_hits)[:probe_cap]
_ph = ",".join("?" * len(_ids))
_rows = db.execute(
f"SELECT rowid, bm25(entries_fts) AS score FROM entries_fts WHERE rowid IN ({_ph})", _ids
@@ -1140,7 +1148,8 @@ def cached_recall(db, query, budget=RECALL_BUDGET, min_score=RECALL_MIN_SCORE, m
recall_db = shard_db if shard_db else db
except Exception:
recall_db = db
else: recall_db = db
else:
recall_db = db
# P1-03 (#26): the consolidation shortcut injection is REMOVED. Every
# query — broad or specific, consolidated or not — now competes under the
@@ -1381,8 +1390,7 @@ def reindex_entries(db, dry_run=False):
db.execute("CREATE VIRTUAL TABLE entries_fts USING fts5(text, content='', tokenize='porter unicode61')")
except Exception:
logger.debug("reindex_entries failed", exc_info=True)
rows = db.execute(
"SELECT id, text FROM entries WHERE status='active'").fetchall()
rows = db.execute("SELECT id, text FROM entries WHERE status='active'").fetchall()
vec_missing = 0
fts_missing = 0
for r in rows:
@@ -1671,8 +1679,7 @@ def _extract_epub(path):
return "\n\n".join(parts)
except Exception:
logger.debug("_extract_epub failed", exc_info=True)
r = subprocess.run(["ebook2text", path], capture_output=True,
text=True, timeout=60)
r = subprocess.run(["ebook2text", path], capture_output=True, text=True, timeout=60)
return r.stdout
@@ -1702,8 +1709,7 @@ def mine_file(db, path, wing="", room="", chunk_size=1500):
except Exception:
# fallback: legacy extractor if the upgraded one is unavailable
logger.debug("mine_file failed", exc_info=True)
r = subprocess.run(["pdftotext", "-layout", path, "-"],
capture_output=True, text=True, timeout=60)
r = subprocess.run(["pdftotext", "-layout", path, "-"], capture_output=True, text=True, timeout=60)
text = r.stdout
elif ext in (".epub",):
# EPUB = zip of XHTML. Robust extraction: unzip, read spine
@@ -1995,6 +2001,7 @@ def recall_with_associations(db, query, budget=RECALL_BUDGET, min_score=RECALL_M
"temperature": round(temp, 2),
"relevance": round(relevance, 4),
"via_association": True,
"mode": "association_merged",
}
)
base.sort(key=lambda r: -r["relevance"])
+45 -29
View File
@@ -32,31 +32,31 @@ QUEUE_FILE = os.path.join(QUEUE_DIR, "queue.json")
# Task addition patterns
TASK_ADDITION_PATTERNS = [
r'\balso\b',
r'\band can you\b',
r'\bplus\b',
r'\badditionally\b',
r'\bafter that\b',
r'\bthen\b',
r'\balso need\b',
r'\balso want\b',
r'\bcan you also\b',
r'\bcan you add\b',
r'\bcan you update\b',
r'\bcan you fix\b',
r'\bcan you create\b',
r'\bcan you implement\b',
r'\bcan you build\b',
r'\bmake sure\b',
r'\bdon\'t forget\b',
r'\bremember to\b',
r'\binclude\b',
r'\badd\b.*\bto\b',
r'\bupdate\b.*\bto\b',
r'\bfix\b.*\bto\b',
r'\bcreate\b.*\bto\b',
r'\bimplement\b.*\bto\b',
r'\bbuild\b.*\bto\b',
r"\balso\b",
r"\band can you\b",
r"\bplus\b",
r"\badditionally\b",
r"\bafter that\b",
r"\bthen\b",
r"\balso need\b",
r"\balso want\b",
r"\bcan you also\b",
r"\bcan you add\b",
r"\bcan you update\b",
r"\bcan you fix\b",
r"\bcan you create\b",
r"\bcan you implement\b",
r"\bcan you build\b",
r"\bmake sure\b",
r"\bdon\'t forget\b",
r"\bremember to\b",
r"\binclude\b",
r"\badd\b.*\bto\b",
r"\bupdate\b.*\bto\b",
r"\bfix\b.*\bto\b",
r"\bcreate\b.*\bto\b",
r"\bimplement\b.*\bto\b",
r"\bbuild\b.*\bto\b",
]
@@ -64,9 +64,11 @@ TASK_ADDITION_PATTERNS = [
# DATA STRUCTURES
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class QueuedMessage:
"""A queued message for task addition."""
id: str
message: str
timestamp: str
@@ -81,6 +83,7 @@ class QueuedMessage:
# QUEUE MANAGEMENT
# ═══════════════════════════════════════════════════════════════════════════════
class MessageQueue:
"""Manages the message queue for task additions."""
@@ -117,21 +120,33 @@ class MessageQueue:
confidence += 0.3
# Check for imperative verbs
imperative_verbs = ['add', 'update', 'fix', 'create', 'implement', 'build',
'make', 'change', 'remove', 'delete', 'refactor', 'migrate']
imperative_verbs = [
"add",
"update",
"fix",
"create",
"implement",
"build",
"make",
"change",
"remove",
"delete",
"refactor",
"migrate",
]
for verb in imperative_verbs:
if message_lower.startswith(verb):
signals.append(f"Imperative verb: {verb}")
confidence += 0.4
# Check for file references
file_refs = re.findall(r'\b\w+\.\w+\b', message)
file_refs = re.findall(r"\b\w+\.\w+\b", message)
if file_refs:
signals.append(f"File references: {', '.join(file_refs[:3])}")
confidence += 0.2
# Check for "also" at the start
if message_lower.startswith('also'):
if message_lower.startswith("also"):
signals.append("Starts with 'also'")
confidence += 0.5
@@ -230,6 +245,7 @@ class MessageQueue:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 2:
print("Usage: message_queue.py [add|list|clear|approve|reject|status]")
+1 -3
View File
@@ -77,9 +77,7 @@ class ToolCallWidget(Vertical):
if arguments:
text.append("\n")
text.append("result: ", style="dim")
text.append(
json.dumps(result, sort_keys=True, ensure_ascii=False, default=str)
)
text.append(json.dumps(result, sort_keys=True, ensure_ascii=False, default=str))
return text
def compose(self) -> ComposeResult:
+31 -10
View File
@@ -47,6 +47,7 @@ def now_iso():
# PRIMITIVE 1: register — a plugin declares itself.
# ---------------------------------------------------------------------------
def register(manifest):
"""Register a plugin from its manifest.
@@ -112,6 +113,7 @@ def unregister(name):
# PRIMITIVE 2: resolve — find the best plugin for a capability.
# ---------------------------------------------------------------------------
def resolve(capability):
"""Find the best plugin for a given capability.
@@ -144,6 +146,7 @@ def resolve(capability):
# PRIMITIVE 3: fire — execute hooks for an event.
# ---------------------------------------------------------------------------
def fire(event, data=None, timeout=30):
"""Fire all hooks for an event.
@@ -219,12 +222,14 @@ def fire(event, data=None, timeout=30):
# Log the fire event (thread-safe via lock).
with _registry_lock:
registry = _load_registry()
registry["log"].append({
"event": event,
"timestamp": now_iso(),
"hooks_fired": len(results),
"duration_ms": round(total_duration, 1),
})
registry["log"].append(
{
"event": event,
"timestamp": now_iso(),
"hooks_fired": len(results),
"duration_ms": round(total_duration, 1),
}
)
registry["log"] = registry["log"][-100:]
_save_registry(registry)
@@ -240,6 +245,7 @@ def fire(event, data=None, timeout=30):
# Discovery — scan directory and register all plugins.
# ---------------------------------------------------------------------------
def discover():
"""Scan the scripts directory and register all plugins with PLUGIN dicts.
@@ -303,7 +309,7 @@ def _extract_manifest(fpath):
return None
# Extract the dict string and parse with ast.literal_eval.
dict_str = "\n".join(lines[start:end + 1])
dict_str = "\n".join(lines[start : end + 1])
# Remove the "PLUGIN = " prefix.
if "=" in dict_str:
dict_str = dict_str.split("=", 1)[1].strip()
@@ -318,6 +324,7 @@ def _extract_manifest(fpath):
# Status — what's registered, what's active.
# ---------------------------------------------------------------------------
def status():
"""Get the full status of the microkernel."""
registry = _load_registry()
@@ -339,6 +346,7 @@ def status():
# Registry persistence.
# ---------------------------------------------------------------------------
def _load_registry():
"""Load the registry from disk."""
try:
@@ -358,6 +366,7 @@ def _save_registry(registry):
# CLI.
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Minimal plugin dispatch engine")
parser.add_argument("cmd", choices=["register", "unregister", "resolve", "fire", "discover", "status"])
@@ -390,7 +399,11 @@ def main():
print("Error: --capability required")
sys.exit(1)
result = resolve(args.capability)
print(json.dumps({"capability": args.capability, "provider": result}, indent=2) if args.json else f"{args.capability} → {result}")
print(
json.dumps({"capability": args.capability, "provider": result}, indent=2)
if args.json
else f"{args.capability} → {result}"
)
elif args.cmd == "fire":
if not args.event:
@@ -398,7 +411,11 @@ def main():
sys.exit(1)
data = json.loads(args.data) if args.data else None
result = fire(args.event, data=data)
print(json.dumps(result, indent=2) if args.json else f"Fired {args.event}: {result['hooks_fired']} hooks in {result['total_duration_ms']}ms")
print(
json.dumps(result, indent=2)
if args.json
else f"Fired {args.event}: {result['hooks_fired']} hooks in {result['total_duration_ms']}ms"
)
elif args.cmd == "discover":
result = discover()
@@ -406,7 +423,11 @@ def main():
elif args.cmd == "status":
result = status()
print(json.dumps(result, indent=2) if args.json else f"Kernel: {result['active_plugins']}/{result['total_plugins']} active")
print(
json.dumps(result, indent=2)
if args.json
else f"Kernel: {result['active_plugins']}/{result['total_plugins']} active"
)
if __name__ == "__main__":
+55 -48
View File
@@ -146,6 +146,7 @@ def is_moe_model(model_name: str) -> bool:
name_lower = model_name.lower()
return any(pattern in name_lower for pattern in MOE_MODEL_PATTERNS)
# Task type → preferred model tier
TASK_ROUTING = {
"conversation": "local-light",
@@ -167,13 +168,13 @@ def _now():
def _clean_model_name(name: str) -> str:
"""Extract meaningful model name from repository path.
Examples:
hf.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF:IQ2_M
-> Qwen3.6 27B
qwen2.5:3b -> qwen2.5:3b (already clean)
mimo-v2.5-pro -> mimo-v2.5-pro (already clean)
"""
# If it's a HuggingFace repo path, extract the model name
@@ -195,7 +196,7 @@ def _clean_model_name(name: str) -> str:
# First, try to match family + size directly (e.g., Qwen3.6-27B, gemma-9b)
# Family can contain letters and digits (e.g., Qwen3.6, Llama3)
# Size is like "27B" or "8B" (case insensitive)
match = re.search(r'([A-Za-z]+[\d.]*)-?(\d+[BMb])', model_part, re.IGNORECASE)
match = re.search(r"([A-Za-z]+[\d.]*)-?(\d+[BMb])", model_part, re.IGNORECASE)
if match:
family = match.group(1)
size = match.group(2).upper()
@@ -203,7 +204,7 @@ def _clean_model_name(name: str) -> str:
# Try to match family + version + size (e.g., Llama-3-8B, gemma-2-9b)
# Family is letters only, version is digits, size is digits+B/M (case insensitive)
match = re.search(r'([A-Za-z]+)-(\d+)-(\d+[BMb])', model_part, re.IGNORECASE)
match = re.search(r"([A-Za-z]+)-(\d+)-(\d+[BMb])", model_part, re.IGNORECASE)
if match:
family = match.group(1)
version = match.group(2)
@@ -237,16 +238,18 @@ def list_models():
for m in local_models:
raw_name = m["name"]
clean_name = _clean_model_name(raw_name)
models.append({
"name": raw_name, # Keep original for selection
"display_name": clean_name, # Clean name for display
"size_gb": round(m.get("size", 0) / (1024**3), 1),
"family": m.get("details", {}).get("family", "unknown"),
"parameters": m.get("details", {}).get("parameter_size", "unknown"),
"capabilities": MODEL_CAPABILITIES.get(raw_name, {}).get("strengths", []),
"tier": MODEL_CAPABILITIES.get(raw_name, {}).get("tier", "unknown"),
"provider": "local",
})
models.append(
{
"name": raw_name, # Keep original for selection
"display_name": clean_name, # Clean name for display
"size_gb": round(m.get("size", 0) / (1024**3), 1),
"family": m.get("details", {}).get("family", "unknown"),
"parameters": m.get("details", {}).get("parameter_size", "unknown"),
"capabilities": MODEL_CAPABILITIES.get(raw_name, {}).get("strengths", []),
"tier": MODEL_CAPABILITIES.get(raw_name, {}).get("tier", "unknown"),
"provider": "local",
}
)
except Exception as e:
print(f"Warning: Ollama not available: {e}", file=sys.stderr) # Log if Ollama not available
@@ -267,8 +270,8 @@ def _get_cloud_models():
with open(config_path) as f:
raw = f.read()
# Strip // comments and trailing commas for JSONC compatibility
raw = re.sub(r'//.*?$', '', raw, flags=re.MULTILINE)
raw = re.sub(r',\s*([}\]])', r'\1', raw)
raw = re.sub(r"//.*?$", "", raw, flags=re.MULTILINE)
raw = re.sub(r",\s*([}\]])", r"\1", raw)
config = json.loads(raw)
providers = config.get("provider", {})
@@ -278,15 +281,17 @@ def _get_cloud_models():
provider_models = provider_config.get("models", {})
for model_id, model_config in provider_models.items():
cloud_models.append({
"name": f"{provider_name}/{model_id}",
"size_gb": 0,
"family": "cloud",
"parameters": "cloud",
"capabilities": ["complex-reasoning", "coding", "analysis"],
"tier": "cloud",
"provider": provider_name,
})
cloud_models.append(
{
"name": f"{provider_name}/{model_id}",
"size_gb": 0,
"family": "cloud",
"parameters": "cloud",
"capabilities": ["complex-reasoning", "coding", "analysis"],
"tier": "cloud",
"provider": provider_name,
}
)
except Exception as e:
print(f"Warning: Could not load cloud models: {e}", file=sys.stderr)
@@ -303,14 +308,12 @@ def health_check():
continue
try:
start = time.time()
data = json.dumps({
"model": m["name"],
"prompt": "test",
"stream": False,
"options": {"num_predict": 1, "num_ctx": 8192}
}).encode()
req = urllib.request.Request(f"{OLLAMA_URL}/api/generate", data=data,
headers={"Content-Type": "application/json"})
data = json.dumps(
{"model": m["name"], "prompt": "test", "stream": False, "options": {"num_predict": 1, "num_ctx": 8192}}
).encode()
req = urllib.request.Request(
f"{OLLAMA_URL}/api/generate", data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=10) as resp:
resp.read()
latency = int((time.time() - start) * 1000)
@@ -351,14 +354,17 @@ def generate(prompt, model=None, max_tokens=500, temperature=0.7):
try:
start = time.time()
data = json.dumps({
"model": model,
"prompt": prompt,
"stream": False,
"options": {"temperature": temperature, "num_predict": max_tokens, "num_ctx": 8192}
}).encode()
req = urllib.request.Request(f"{OLLAMA_URL}/api/generate", data=data,
headers={"Content-Type": "application/json"})
data = json.dumps(
{
"model": model,
"prompt": prompt,
"stream": False,
"options": {"temperature": temperature, "num_predict": max_tokens, "num_ctx": 8192},
}
).encode()
req = urllib.request.Request(
f"{OLLAMA_URL}/api/generate", data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())
latency = int((time.time() - start) * 1000)
@@ -379,6 +385,7 @@ def handle_hook(event, data=None):
def main():
import argparse
ap = argparse.ArgumentParser(description="Model connection layer")
ap.add_argument("cmd", choices=["list", "health", "route", "generate", "hook"])
ap.add_argument("arg", nargs="?", default="")
@@ -399,7 +406,9 @@ def main():
if "error" in m:
print(f" Error: {m['error']}")
else:
print(f" [{m['tier']:20}] {m['name'][:40]:40} {m['size_gb']:5.1f}GB {', '.join(m['capabilities'][:3])}")
print(
f" [{m['tier']:20}] {m['name'][:40]:40} {m['size_gb']:5.1f}GB {', '.join(m['capabilities'][:3])}"
)
elif args.cmd == "health":
results = health_check()
@@ -415,15 +424,13 @@ def main():
elif args.cmd == "route":
task_type = args.arg or "conversation"
result = route_model(task_type)
print(json.dumps(result, indent=2) if args.json else
f"Route: {result['model']} ({result['reason']})")
print(json.dumps(result, indent=2) if args.json else f"Route: {result['model']} ({result['reason']})")
elif args.cmd == "generate":
if not args.arg:
print("usage: model_connect.py generate '<prompt>'")
return
result = generate(args.arg, model=args.model, max_tokens=args.max_tokens,
temperature=args.temperature)
result = generate(args.arg, model=args.model, max_tokens=args.max_tokens, temperature=args.temperature)
if args.json:
print(json.dumps(result, indent=2))
elif "error" in result:
@@ -451,5 +458,5 @@ PLUGIN = {
"dependencies": [],
"provides": ["model_connect"],
"priority": 1,
"auto_activate": True
"auto_activate": True,
}
+27 -8
View File
@@ -33,9 +33,11 @@ OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
# MODEL PROFILE
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class ModelProfile:
"""Profile for a specific model — how to work with it."""
name: str # Ollama model name
display_name: str = ""
family: str = "" # qwen, llama, mistral, etc.
@@ -176,6 +178,7 @@ FAMILY_KNOWLEDGE = {
# PROFILE MANAGEMENT
# ═══════════════════════════════════════════════════════════════════════════════
def get_profile_path(model_name: str) -> str:
"""Get the file path for a model's profile."""
# Sanitize model name for filesystem
@@ -218,7 +221,8 @@ def detect_family(model_name: str) -> str:
def detect_parameters(model_name: str) -> str:
"""Extract parameter count from model name."""
import re
match = re.search(r'(\d+\.?\d*)[bB]', model_name)
match = re.search(r"(\d+\.?\d*)[bB]", model_name)
if match:
return f"{match.group(1)}B"
return "unknown"
@@ -227,9 +231,18 @@ def detect_parameters(model_name: str) -> str:
def detect_quantization(model_name: str) -> str:
"""Extract quantization level from model name."""
import re
patterns = [
r'(Q\d+_\w+)', r'(IQ\d+_\w+)', r'(q\d+_\w+)', r'(iq\d+_\w+)',
r'(GGUF)', r'(gguf)', r'(FP16)', r'(FP32)', r'(INT8)', r'(INT4)',
r"(Q\d+_\w+)",
r"(IQ\d+_\w+)",
r"(q\d+_\w+)",
r"(iq\d+_\w+)",
r"(GGUF)",
r"(gguf)",
r"(FP16)",
r"(FP32)",
r"(INT8)",
r"(INT4)",
]
for pattern in patterns:
match = re.search(pattern, model_name)
@@ -294,10 +307,13 @@ def scan_all_models():
profiles.append(existing)
else:
# Build new profile
profile = build_profile_from_ollama(name, {
"display_name": name,
"size_gb": round(model.get("size", 0) / (1024**3), 1),
})
profile = build_profile_from_ollama(
name,
{
"display_name": name,
"size_gb": round(model.get("size", 0) / (1024**3), 1),
},
)
save_profile(profile)
profiles.append(profile)
@@ -351,6 +367,7 @@ def get_prompt_context(model_name: str) -> str:
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 2:
print("Usage: model_profiles.py [scan|show|list]")
@@ -405,7 +422,9 @@ def main():
name = f[:-5].replace("_", "/")
profile = load_profile(name)
if profile:
print(f" {profile.name} ({profile.family}, {profile.parameters}) — {profile.interactions} interactions")
print(
f" {profile.name} ({profile.family}, {profile.parameters}) — {profile.interactions} interactions"
)
else:
print(f"Unknown command: {cmd}")
+4 -3
View File
@@ -19,6 +19,7 @@ from typing import Dict, List, Optional
@dataclass
class ModelProfile:
"""Profile for a cognitive resource."""
id: str # Unique identifier
provider: str # ollama, openai, etc.
tier: str # T0-T4
@@ -130,9 +131,9 @@ class ModelRegistry:
"""Get all profiles for a tier."""
return [p for p in self.profiles.values() if p.tier == tier and p.enabled]
def get_cheapest_capable(self, requires_tools: bool = False,
requires_structured: bool = False,
requires_reasoning: bool = False) -> Optional[ModelProfile]:
def get_cheapest_capable(
self, requires_tools: bool = False, requires_structured: bool = False, requires_reasoning: bool = False
) -> Optional[ModelProfile]:
"""Get the cheapest capable model."""
capable = []
for profile in self.profiles.values():
+28 -21
View File
@@ -37,6 +37,7 @@ import memory_env
@dataclass
class NightModeState:
"""Night mode state."""
active: bool = False
started_at: Optional[str] = None
duration_hours: float = 8.0
@@ -208,12 +209,13 @@ class NightMode:
"""Run sleep processes (memory consolidation)."""
try:
import subprocess
# Run sleep-time script
result = subprocess.run(
[sys.executable, os.path.join(memory_env.scripts_dir(), "sleep_time.py")],
capture_output=True,
text=True,
timeout=300
timeout=300,
)
if result.returncode == 0:
self.state.improvements.append("Memory consolidation completed")
@@ -238,18 +240,20 @@ class NightMode:
# Attempt LLM-powered evaluation via local Ollama
try:
import urllib.request
payload = json.dumps({
"model": os.environ.get("NIGHT_MODE_MODEL", "qwen2.5:3b"),
"prompt": question,
"stream": False,
"think": False,
"options": {"temperature": 0.4, "num_predict": 300, "num_ctx": 8192},
}).encode()
payload = json.dumps(
{
"model": os.environ.get("NIGHT_MODE_MODEL", "qwen2.5:3b"),
"prompt": question,
"stream": False,
"think": False,
"options": {"temperature": 0.4, "num_predict": 300, "num_ctx": 8192},
}
).encode()
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
req = urllib.request.Request(
f"{ollama_url}/api/generate",
data=payload,
headers={"Content-Type": "application/json"})
f"{ollama_url}/api/generate", data=payload, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=60) as r:
answer = json.loads(r.read()).get("response", "").strip()
if answer:
@@ -270,24 +274,26 @@ class NightMode:
# Attempt LLM-powered iteration via local Ollama
try:
import urllib.request
prompt = (
"You are Rune, a self-improving AI assistant. "
f"Currently in night mode iteration {self.state.iterations}. "
"Based on recent work, suggest ONE concrete, actionable improvement "
"I can implement right now. Be specific and brief."
)
payload = json.dumps({
"model": os.environ.get("NIGHT_MODE_MODEL", "qwen2.5:3b"),
"prompt": prompt,
"stream": False,
"think": False,
"options": {"temperature": 0.5, "num_predict": 400, "num_ctx": 8192},
}).encode()
payload = json.dumps(
{
"model": os.environ.get("NIGHT_MODE_MODEL", "qwen2.5:3b"),
"prompt": prompt,
"stream": False,
"think": False,
"options": {"temperature": 0.5, "num_predict": 400, "num_ctx": 8192},
}
).encode()
ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434")
req = urllib.request.Request(
f"{ollama_url}/api/generate",
data=payload,
headers={"Content-Type": "application/json"})
f"{ollama_url}/api/generate", data=payload, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=90) as r:
answer = json.loads(r.read()).get("response", "").strip()
if answer:
@@ -358,6 +364,7 @@ def get_report() -> Dict[str, Any]:
def main():
"""CLI entry point."""
import argparse
ap = argparse.ArgumentParser(description="Night mode — autonomous self-improvement loop")
ap.add_argument("cmd", choices=["start", "status", "stop", "report"])
ap.add_argument("--hours", type=float, default=8.0, help="Duration in hours")
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more