Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da61fd7d44 | ||
|
|
7342d77e6f | ||
|
|
436f760675 | ||
|
|
d62b717542 | ||
|
|
97de570b27 | ||
|
|
22aebbb102 | ||
|
|
7e33294527 | ||
|
|
fcc9989393 | ||
|
|
ead4a24866 | ||
|
|
cf51f7fd7c | ||
|
|
f182ab8601 | ||
|
|
5f9ba5784c | ||
|
|
9a05fdfa1f | ||
|
|
2298768353 | ||
|
|
a4afc77d53 | ||
|
|
d68fd1e94d | ||
|
|
470d82f307 | ||
|
|
7a988cbaff | ||
|
|
12c3c488be | ||
|
|
247c5b3936 | ||
|
|
946b8366ef | ||
|
|
5e539f7da3 | ||
|
|
83960c541d | ||
|
|
980fcd364f | ||
|
|
c35018f543 | ||
|
|
d2600dff29 | ||
|
|
5b268fde8e | ||
|
|
9e0b85534a | ||
|
|
d01df797c1 | ||
|
|
95bac995a3 | ||
|
|
1afadfddfc | ||
|
|
54ab4d9e64 | ||
|
|
f4c73d5142 | ||
|
|
bad13ceb57 | ||
|
|
c35b604976 | ||
|
|
4eefc15837 | ||
|
|
2bf43eafcd | ||
|
|
ca4f7593d7 | ||
|
|
24537e46e7 | ||
|
|
52d810d358 | ||
|
|
7ea4a62960 | ||
|
|
e7e4541d47 | ||
|
|
92b6f47ccf | ||
|
|
d90a79a231 | ||
|
|
29390534d9 | ||
|
|
a8ae37df21 | ||
|
|
fa81f4bd4f | ||
|
|
81edc1356c | ||
|
|
6dcea9dbc7 | ||
|
|
885bfc181a | ||
|
|
0e1e7ce94a | ||
|
|
a125c023e6 | ||
|
|
20e68b793f | ||
|
|
1163a13e13 | ||
|
|
a4b0bd6f55 | ||
|
|
76f215f9ab | ||
|
|
dc5757ff04 | ||
|
|
fc12bb5b44 | ||
|
|
a620171524 | ||
|
|
4486ea4558 | ||
|
|
996d0d9cb1 | ||
|
|
4e3b4be67b | ||
|
|
a872abd962 | ||
|
|
be136de452 | ||
|
|
afa276aaf4 | ||
|
|
b0dbb80778 | ||
|
|
0018fe8c39 |
No files matched your search
@@ -71,7 +71,13 @@ jobs:
|
||||
echo "HEAD is a release bump — loop guard, no re-bump"
|
||||
exit 0
|
||||
fi
|
||||
WARRANTED=$(python3 scripts/version_convergence.py | python3 -c "import json,sys; print(json.load(sys.stdin).get('warranted_bump') or '')")
|
||||
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"
|
||||
@@ -179,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:
|
||||
@@ -208,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
|
||||
|
||||
@@ -11,3 +11,6 @@ build/
|
||||
.env
|
||||
.venv/
|
||||
state/
|
||||
# Runtime artifacts (cwd-local event journal + capability index)
|
||||
index/
|
||||
journal/
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.4**
|
||||
**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.**
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -52,9 +52,11 @@ This detour is complete when:
|
||||
- [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
|
||||
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
|
||||
|
||||
**PL1 Process Learning** (T9). ADAPT-LIVE closed the route-execution gap; PL1's input (route accuracy, probe outcomes, escalation evidence, verification results, task characteristics) is now produced live by the operator spine. The matrix in the ADAPT RECONCILIATION section stands, updated: LOCAL_FIX/STANDARD_CHANGE paths = BUILT_AND_LIVE; probe = BUILT_AND_LIVE (verification-driven); escalation/downgrade = BUILT_AND_LIVE. 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.
|
||||
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)
|
||||
|
||||
@@ -144,3 +146,34 @@ Reasoning from recovered obligations, not momentum:
|
||||
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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
def broken(:
|
||||
+2
-2
@@ -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
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 353,
|
||||
"BLE001": 352,
|
||||
"S110": 80
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.4"
|
||||
version = "0.66.14"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -14,13 +14,18 @@ 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 = os.environ.get("GITEA_TOKEN", "")
|
||||
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
|
||||
@@ -37,10 +42,7 @@ def main() -> int:
|
||||
required = [
|
||||
s
|
||||
for s in data.get("statuses", [])
|
||||
if (
|
||||
str(s.get("context", "")).startswith("CI Pipeline / required-checks")
|
||||
and s.get("status") != "skipped"
|
||||
)
|
||||
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
|
||||
]
|
||||
if not required:
|
||||
print("pending")
|
||||
|
||||
@@ -99,7 +99,14 @@ print("VERIFIED:", json.dumps(info))
|
||||
# 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")],
|
||||
[
|
||||
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")},
|
||||
|
||||
+72
-16
@@ -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))
|
||||
@@ -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:
|
||||
|
||||
@@ -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]}
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -35,6 +35,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import gitea_auth # noqa: E402 — after scripts-dir path insert
|
||||
# Dependency direction (handoff 2026-09-10 §3): the research executor MAY
|
||||
# import Rune Core; Core never imports the executor. Route authority and
|
||||
# evidence probe live in src/rune (canonical, Option C unified authority).
|
||||
@@ -2092,7 +2093,7 @@ class StageExecutor:
|
||||
def _fetch_issue(self) -> dict | None:
|
||||
import urllib.request
|
||||
|
||||
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")
|
||||
base = server.rstrip("/") + "/api/v1/repos/conductor/rune"
|
||||
if not token:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -199,6 +212,218 @@ 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(
|
||||
@@ -213,7 +438,10 @@ def main(argv: list[str] | None = None) -> dict[str, Any]:
|
||||
import sys as _sys
|
||||
|
||||
_sys.exit(0 if is_release_commit(args.is_release_subject) else 1)
|
||||
decision = decision_in_repo(args.repo)
|
||||
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
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.4"
|
||||
__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
|
||||
|
||||
+57
-42
@@ -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,
|
||||
}
|
||||
@@ -562,7 +562,25 @@ def derive_sufficiency(
|
||||
# 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")
|
||||
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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]")
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
+31
-40
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---")
|
||||
|
||||
@@ -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.4"' in rune_version,
|
||||
'__version__ = "0.66.14"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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>")
|
||||
|
||||
@@ -140,5 +140,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["connection_pool"],
|
||||
"priority": 1,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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']}")
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
+24
-20
@@ -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
@@ -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
@@ -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
@@ -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", {})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
@@ -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]")
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -20,9 +20,7 @@ Writer = Callable[[TextIO], None]
|
||||
def atomic_write(path: Path, writer: Writer) -> None:
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
dir=str(path.parent), prefix=path.name + ".", suffix=".tmp"
|
||||
)
|
||||
fd, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix=path.name + ".", suffix=".tmp")
|
||||
tmp = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
|
||||
@@ -21,13 +21,7 @@ Scenario = Callable[[int], float] # batch index -> seconds per op
|
||||
|
||||
def fingerprint() -> dict:
|
||||
try:
|
||||
rev = (
|
||||
subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL
|
||||
)
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
rev = subprocess.check_output(["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL).decode().strip()
|
||||
except Exception:
|
||||
rev = "unknown"
|
||||
return {
|
||||
@@ -37,9 +31,7 @@ def fingerprint() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def run_batches(
|
||||
scenario: Scenario, batches: int = 5, warmup: int = 10, ops: int = 100
|
||||
) -> list[list[float]]:
|
||||
def run_batches(scenario: Scenario, batches: int = 5, warmup: int = 10, ops: int = 100) -> list[list[float]]:
|
||||
out: list[list[float]] = []
|
||||
for b in range(batches):
|
||||
for _ in range(warmup):
|
||||
@@ -54,9 +46,7 @@ def p95(values: list[float]) -> float:
|
||||
return ordered[idx]
|
||||
|
||||
|
||||
def evaluate_p95_regression(
|
||||
baseline: list[float], candidate: list[float], floor_ns: float = 1_000_000.0
|
||||
) -> bool:
|
||||
def evaluate_p95_regression(baseline: list[float], candidate: list[float], floor_ns: float = 1_000_000.0) -> bool:
|
||||
if len(baseline) != len(candidate) or len(baseline) == 0:
|
||||
raise ValueError("paired batches required")
|
||||
pairs = [c - b for b, c in zip(baseline, candidate, strict=True)]
|
||||
|
||||
@@ -21,25 +21,17 @@ class ElasticBudget:
|
||||
max_cost: float
|
||||
|
||||
def within(self, depth: int, concurrency: int, cost: float) -> bool:
|
||||
return (
|
||||
depth <= self.depth
|
||||
and concurrency <= self.concurrency
|
||||
and cost <= self.max_cost
|
||||
)
|
||||
return depth <= self.depth and concurrency <= self.concurrency and cost <= self.max_cost
|
||||
|
||||
|
||||
class ResourceBroker:
|
||||
def __init__(self, scheduler: ResourceScheduler):
|
||||
self._scheduler = scheduler
|
||||
|
||||
def acquire(
|
||||
self, task_signature: str, requirement: dict
|
||||
) -> ResourceAssignment | None:
|
||||
def acquire(self, task_signature: str, requirement: dict) -> ResourceAssignment | None:
|
||||
return self._scheduler.acquire(task_signature, requirement)
|
||||
|
||||
def inherit(
|
||||
self, parent: ResourceAssignment, depth_limit: int = 3
|
||||
) -> ElasticBudget:
|
||||
def inherit(self, parent: ResourceAssignment, depth_limit: int = 3) -> ElasticBudget:
|
||||
provider = _provider_for(parent.resource_id)
|
||||
return ElasticBudget(
|
||||
depth=min(depth_limit, 1 + parent_task_depth(parent.task_signature)),
|
||||
|
||||
@@ -41,6 +41,4 @@ class BudgetRegistry:
|
||||
return self._used.get(priority, 0)
|
||||
|
||||
def pressure(self) -> list[str]:
|
||||
return sorted(
|
||||
p for p, b in self._budgets.items() if self._used.get(p, 0) > b.max_bytes
|
||||
)
|
||||
return sorted(p for p, b in self._budgets.items() if self._used.get(p, 0) > b.max_bytes)
|
||||
@@ -31,13 +31,9 @@ class HotBus:
|
||||
def publish(self, env: EventEnvelope) -> int:
|
||||
offset = self._store.append(env)
|
||||
self._store.flush()
|
||||
row = next(
|
||||
(r for o, r in self._store.read(from_offset=offset) if o == offset), None
|
||||
)
|
||||
row = next((r for o, r in self._store.read(from_offset=offset) if o == offset), None)
|
||||
if row is None:
|
||||
raise DurableEventStore.CorruptEventStoreError(
|
||||
f"missing event at offset {offset}"
|
||||
)
|
||||
raise DurableEventStore.CorruptEventStoreError(f"missing event at offset {offset}")
|
||||
for fn in self._subscribers:
|
||||
fn(dict(row))
|
||||
return offset
|
||||
@@ -50,9 +46,7 @@ class HotBus:
|
||||
offsets = list(range(start, start + len(envs)))
|
||||
rows = [r for _, r in self._store.read(from_offset=start)]
|
||||
if len(rows) < len(envs):
|
||||
raise DurableEventStore.CorruptEventStoreError(
|
||||
f"missing event at offset {start + len(rows)}"
|
||||
)
|
||||
raise DurableEventStore.CorruptEventStoreError(f"missing event at offset {start + len(rows)}")
|
||||
for row in rows:
|
||||
for fn in self._subscribers:
|
||||
fn(dict(row))
|
||||
|
||||
@@ -40,6 +40,4 @@ class CadenceCursor:
|
||||
def advance_to(self, offset: int, fingerprint: str) -> None:
|
||||
self._data = {"offset": int(offset), "fingerprint": fingerprint}
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(
|
||||
json.dumps(self._data, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
self.path.write_text(json.dumps(self._data, sort_keys=True) + "\n", encoding="utf-8")
|
||||
@@ -60,9 +60,7 @@ class _SynthesisWorker:
|
||||
if not gate.should_run(fp):
|
||||
return WorkerResult(self.name, self.cadence, 0, 1, True)
|
||||
gate.record(fp)
|
||||
sink.submit(
|
||||
ProposalRecord(worker=self.name, action="synthesize", confidence=0.5)
|
||||
)
|
||||
sink.submit(ProposalRecord(worker=self.name, action="synthesize", confidence=0.5))
|
||||
return WorkerResult(self.name, self.cadence, 1, 0, True)
|
||||
|
||||
|
||||
@@ -99,10 +97,7 @@ class CognitiveRuntime:
|
||||
)
|
||||
# Proposals that entered the sink during THIS run and remain pending:
|
||||
# a skipped pass submits nothing, so an unchanged-evidence run reports 0.
|
||||
pending = {
|
||||
cadence: self.sinks[cadence].pending() - before[cadence]
|
||||
for cadence in self.sinks
|
||||
}
|
||||
pending = {cadence: self.sinks[cadence].pending() - before[cadence] for cadence in self.sinks}
|
||||
return {
|
||||
"facts": self.reflex.facts(),
|
||||
"results": [vars(r) for r in results],
|
||||
|
||||
@@ -41,13 +41,9 @@ class DelegationPlanner:
|
||||
"body": f"Stage {stage_name} of {plan_version}.",
|
||||
}
|
||||
|
||||
def _mutation(
|
||||
self, ordering_key: int, remote_target: str, payload: dict
|
||||
) -> OutboxRecord:
|
||||
def _mutation(self, ordering_key: int, remote_target: str, payload: dict) -> OutboxRecord:
|
||||
payload_hash = _sha256(str(payload))
|
||||
canon = "|".join(
|
||||
[self._plan_version, remote_target, str(ordering_key), payload_hash]
|
||||
)
|
||||
canon = "|".join([self._plan_version, remote_target, str(ordering_key), payload_hash])
|
||||
mutation_id = _sha256(canon)[:12]
|
||||
return OutboxRecord(
|
||||
mutation_id=mutation_id,
|
||||
@@ -58,13 +54,9 @@ class DelegationPlanner:
|
||||
payload_hash=payload_hash,
|
||||
)
|
||||
|
||||
def materialize(
|
||||
self, approval: ApprovalRecord, stage_names: list[str]
|
||||
) -> list[OutboxRecord]:
|
||||
def materialize(self, approval: ApprovalRecord, stage_names: list[str]) -> list[OutboxRecord]:
|
||||
muts: list[OutboxRecord] = []
|
||||
muts.append(
|
||||
self._mutation(0, ISSUE_TARGET, self.parent_payload(self._plan_version))
|
||||
)
|
||||
muts.append(self._mutation(0, ISSUE_TARGET, self.parent_payload(self._plan_version)))
|
||||
for n, stage in enumerate(stage_names, start=1):
|
||||
muts.append(
|
||||
self._mutation(
|
||||
|
||||
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user