Compare commits
113
Commits
v0.66.0
...
wr-experiment
| 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 | ||
|
|
6f6781c8d7 | ||
|
|
e89891e021 | ||
|
|
8affad8d5b | ||
|
|
1546f69046 | ||
|
|
272e6dad7c | ||
|
|
c2528c1d9d | ||
|
|
bb536c9f02 | ||
|
|
51cbe8e6d1 | ||
|
|
7437f06913 | ||
|
|
d9ab8fad00 | ||
|
|
87519f2563 | ||
|
|
76f71fc7e6 | ||
|
|
174e11f23a | ||
|
|
ed6da55a04 | ||
|
|
a5aa802277 | ||
|
|
e3064f89ad | ||
|
|
cd4b0f44d5 | ||
|
|
83744c9590 | ||
|
|
5c8b2520d2 | ||
|
|
cbdd6c6d97 | ||
|
|
6d010a2078 | ||
|
|
f47d9f3652 | ||
|
|
58ebeb7c9b | ||
|
|
301cd12359 | ||
|
|
a1d1b838b7 | ||
|
|
e92c993ed4 | ||
|
|
8d75efb1de | ||
|
|
5e99228ce5 | ||
|
|
51e14d6d95 | ||
|
|
97c604d0c5 | ||
|
|
ece67c6c73 | ||
|
|
9e9ddfc10f | ||
|
|
b4ac6ad253 | ||
|
|
dd9fba268e | ||
|
|
6418dbcfa0 | ||
|
|
62631f5f64 | ||
|
|
923c286a6d | ||
|
|
bf5eb53d1e | ||
|
|
3dfb4ee9e6 | ||
|
|
d87f8cb5f7 | ||
|
|
c76b67c5ef | ||
|
|
d1ef490756 | ||
|
|
86833a29a2 | ||
|
|
d9e117ebf0 | ||
|
|
d59b8a1800 | ||
|
|
ec2cba62ee |
No files matched your search
@@ -4,6 +4,8 @@ on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions: write-all
|
||||
|
||||
@@ -15,13 +17,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Skip if PR is not merged
|
||||
- name: Skip if PR is not merged (direct pushes continue to convergence)
|
||||
run: |
|
||||
MERGED="${{ gitea.event.pull_request.merged }}"
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
EVENT="${{ gitea.event_name }}"
|
||||
if [ "$EVENT" = "pull_request" ] && [ "$MERGED" != "true" ]; then
|
||||
echo "PR not merged — skipping release"
|
||||
exit 0
|
||||
fi
|
||||
echo "event=$EVENT — continuing"
|
||||
|
||||
- name: Read reviewed semantic version
|
||||
run: |
|
||||
@@ -51,8 +55,36 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# semver from PR title — logic lives in scripts/semver_bump.py
|
||||
# (#190 lesson: inline python in YAML scalars rots unlinted)
|
||||
# semver level: PR title on pull_request events; on push events
|
||||
# (direct master pushes bypass PR merges — observed 2026-09-10,
|
||||
# version stuck at 0.66.0 through ~15 substantive commits), the
|
||||
# warranted bump comes from scripts/version_convergence.py over
|
||||
# commits since the last tag. chore(release) commits never
|
||||
# warrant a bump → the bump commit cannot recurse.
|
||||
if [ -z "$PR_TITLE" ] || [ "$PR_TITLE" = "null" ]; then
|
||||
# Recursive-loop guard: HEAD being a bump commit means THIS
|
||||
# push already applied the bump — do not re-bump. The decision
|
||||
# lives in version_convergence (--is-release-subject), one
|
||||
# source of truth shared with the regression test.
|
||||
HEAD_SUBJECT=$(git log -1 --format=%s)
|
||||
if python3 scripts/version_convergence.py --is-release-subject "$HEAD_SUBJECT"; then
|
||||
echo "HEAD is a release bump — loop guard, no re-bump"
|
||||
exit 0
|
||||
fi
|
||||
CONVERGENCE=$(python3 scripts/version_convergence.py)
|
||||
WARRANTED=$(echo "$CONVERGENCE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('warranted_bump') or '')")
|
||||
CONVERGED=$(echo "$CONVERGENCE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('converged', 'true'))")
|
||||
if [ "$CONVERGED" = "True" ]; then
|
||||
echo "Already converged — no bump needed"
|
||||
exit 0
|
||||
fi
|
||||
echo "push convergence: warranted_bump=$WARRANTED"
|
||||
if [ -z "$WARRANTED" ]; then
|
||||
echo "No substantive commits since last tag — no bump needed"
|
||||
exit 0
|
||||
fi
|
||||
PR_TITLE="$WARRANTED: converged release (push-to-master bump)"
|
||||
fi
|
||||
NEW=$(python3 scripts/semver_bump.py "$RELEASE_VERSION" "$PR_TITLE")
|
||||
export NEW_VERSION="$NEW"
|
||||
echo "Auto-bump: $RELEASE_VERSION -> $NEW (direct commit to master — bump PRs retired, Nick policy 2026-09-08)"
|
||||
@@ -153,18 +185,28 @@ jobs:
|
||||
print(f" - {d}")
|
||||
sys.exit(1)
|
||||
|
||||
# Commit each surface DIRECTLY to master (bookkeeping, not reviewable code)
|
||||
msg_prefix = f"chore(release): auto-bump version to {NEW} — earned by #{PR_NUMBER}"
|
||||
for path, (content, sha) in edited.items():
|
||||
status, r = api(f"/contents/{path}", "PUT", {
|
||||
"message": f"{msg_prefix} ({PR_TITLE[:80]})\n\nPromotion record: {promoting_url}",
|
||||
"content": base64.b64encode(content.encode()).decode(),
|
||||
"sha": sha,
|
||||
"branch": "master",
|
||||
})
|
||||
if status not in (200, 201):
|
||||
print(f" commit {path} failed ({status}): {str(r)[:200]}"); sys.exit(1)
|
||||
print(f" committed {path} to master")
|
||||
# Commit ALL surfaces as ONE commit via ChangeFiles
|
||||
# (POST /contents with a files[] array). The per-file contents PUT
|
||||
# created one commit per version surface — five same-version "bump
|
||||
# commits" per release (the phantom duplicate-bump cascade,
|
||||
# misdiagnosed as queue drain since 0.66.2; root-caused T15 Part F
|
||||
# 2026-09-11). Git-data blob/tree creation is 404 on this Gitea
|
||||
# build; ChangeFiles is the supported single-commit primitive.
|
||||
msg = f"chore(release): auto-bump version to {NEW} — earned by #{PR_NUMBER} ({PR_TITLE[:80]})\n\nPromotion record: {promoting_url}"
|
||||
ops = [{
|
||||
"operation": "update",
|
||||
"path": path,
|
||||
"content": base64.b64encode(content.encode()).decode(), # ChangeFileOperation.content is ContentBase64
|
||||
} for path, (content, _old_sha) in edited.items()]
|
||||
status, r = api("/contents", "POST", {
|
||||
"branch": "master",
|
||||
"message": msg,
|
||||
"files": ops,
|
||||
})
|
||||
if status not in (200, 201):
|
||||
print(f"change-files failed ({status}): {str(r)[:300]}"); sys.exit(1)
|
||||
commit_sha = (r.get("commit") or {}).get("sha", "?") if isinstance(r, dict) else "?"
|
||||
print(f" committed {len(ops)} surfaces as ONE commit {str(commit_sha)[:7]}")
|
||||
|
||||
print(f"AUTO_BUMPED=true — version {NEW} live on master; TUI footer will render v{NEW}")
|
||||
with open(os.environ["GITHUB_ENV"], "a") as env:
|
||||
@@ -182,7 +224,7 @@ jobs:
|
||||
# Skip idempotently if a release for this tag already exists (retry/replay safety).
|
||||
if curl --fail --silent "$LIST_API_URL" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
| python3 -c 'import json,sys,os; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")==f"v{os.environ[\"RELEASE_VERSION\"]}"]; sys.exit(0 if exists else 1)'; then
|
||||
| python3 -c 'import json,sys,os; v=os.environ["RELEASE_VERSION"]; exists=[r for r in json.load(sys.stdin) if r.get("tag_name")=="v"+v]; sys.exit(0 if exists else 1)'; then
|
||||
echo "Release v$RELEASE_VERSION already exists — skipping creation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# Deploys the EXACT pushed revision to the local Rune code deployment
|
||||
# (handoff 2026-09-10 §3/§6) — never "whatever HEAD is at deploy time".
|
||||
# Gate: the commit's required CI checks must be green before promotion.
|
||||
# Branch guard at job level: this Gitea instance did not create runs for
|
||||
# `on.push.branches`-filtered workflows (observed 2026-09-10) — the
|
||||
# unfiltered push trigger with an explicit ref guard is the proven pattern.
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ gitea.ref == 'refs/heads/master' && !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Wait for CI qualification on this exact SHA
|
||||
env:
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
run: |
|
||||
DEPLOY_SHA=$(git rev-parse HEAD)
|
||||
export DEPLOY_SHA
|
||||
echo "Qualifying $DEPLOY_SHA"
|
||||
DEADLINE=$((SECONDS + 900))
|
||||
while [ $SECONDS -lt $DEADLINE ]; do
|
||||
STATUS=$(python3 scripts/deploy_qualify.py)
|
||||
echo "required-checks: $STATUS"
|
||||
if [ "$STATUS" = "success" ]; then exit 0; fi
|
||||
if [ "$STATUS" = "failure" ]; then
|
||||
echo "::error::CI failed on $DEPLOY_SHA — deployment refused"
|
||||
exit 1
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
echo "::error::CI qualification timeout on $DEPLOY_SHA — deployment refused"
|
||||
exit 1
|
||||
|
||||
- name: Stage, verify, promote
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
- name: Post-deploy runtime verification (the consuming path)
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
# Exactly what the launcher execs: cd <current>/src, import rune.
|
||||
RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory \
|
||||
/home/nick/.venvs/memory/bin/python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '/mnt/workdrive/rune-code/current/src')
|
||||
import rune
|
||||
info = rune.build_info()
|
||||
assert info['commit'] == '$SHA', f'runtime SHA mismatch: {info}'
|
||||
assert rune.__version__ == info['version']
|
||||
print('RUNTIME VERIFIED:', info)
|
||||
"
|
||||
@@ -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.0**
|
||||
**Current release: v0.66.14**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -120,7 +120,13 @@
|
||||
"local_tool_runtime",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool"
|
||||
"connection_pool",
|
||||
"adaptive_router",
|
||||
"route_risk_flags",
|
||||
"evidence_probe",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"recall_uplift"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
@@ -214,4 +220,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -0,0 +1,179 @@
|
||||
# RUNE CANONICAL INTENT STATE
|
||||
**v1.0 · 2026-09-10 · Owner: Rune · Review: Nick**
|
||||
*This artifact is the project's authoritative answer to: "What were we trying to achieve, what interrupted it, what remains, and where do we return?" It is state, not a planning framework. Update it when intent changes; do not extend it into a second roadmap system.*
|
||||
|
||||
---
|
||||
|
||||
## PRIMARY INTENT
|
||||
|
||||
Build Rune: a stateful, quality-first coding assistant whose cognitive systems are genuinely reachable from live execution — measured by the Runtime Reachability doctrine (`IMPLEMENTED → CALLED BY PRODUCTION → CHANGES EXECUTION → PERSISTED → OBSERVABLE → END-TO-END PROVEN`), not by module existence.
|
||||
|
||||
The parent work at the moment of interruption: **Epic #284 — the adaptive development pipeline** (successor to the ratified Research-to-Build process #257/RP0–RP14). Doctrine: *research buys down uncertainty; it is not a tax on every code change.* Three process depths (LOCAL_FIX / STANDARD_CHANGE / RESEARCHED_CHANGE), reversible routes, one canonical route authority, the ratified pipeline as the high-assurance fallback.
|
||||
|
||||
## CURRENT ACTIVE WORK
|
||||
|
||||
**Deployment Contract v1 + ADAPT-LIVE acceptance closure** (master 5c8b252+, 2026-09-11).
|
||||
|
||||
CORRECTION RECORD (intent-continuity evidence, 2026-09-11): the previous tranche declared "ADAPT-LIVE complete" while an unresolved user-established blocker existed (deployed TUI launch broken; version bumping stalled). Honest status:
|
||||
|
||||
- **Deployment Contract v1: PROVEN** — deterministic CI-qualified deploy pipeline (stage → verify → REAL TUI startup canary via Textual run_test → atomic promote, last-good rollback); loop-guarded auto-version convergence (0.66.0 → 0.66.2 on push events; chore(release) never re-bumps); launcher deployable; runtime reports deployed SHA/version.
|
||||
- **ADAPT-LIVE: IMPLEMENTATION SUBSTANTIALLY PRESENT, ACCEPTANCE PROVEN ON DEPLOYED RUNTIME** — route discriminator (defect/change language + leading-verb governance), DESIGN-LITE, completion truth (mutation tasks without mutating tool evidence cannot complete), verification→probe→escalate BEFORE terminal, deployed-runtime canaries: LOCAL_FIX/STANDARD_CHANGE honestly fail NOT_COMPLETED with text-only responses (false success structurally impossible), conversation tasks complete normally, TWO REAL Evidence Probes through the deployed runtime with genuine cited evidence (pytest exit-code semantics + empirical check; deployed-tree source inspection). Escalation-with-real-worker partially proven (the competent worker correctly GROUNDS rather than fabricates escalation; unresolved-case escalation proven at module level).
|
||||
- Earlier "ADAPT-LIVE complete" declaration (e92c993) was PREMATURE — recorded as evidence for the future intent-continuity system: a child task must not replace unresolved parent acceptance obligations merely because its implementation completed.
|
||||
|
||||
Delivered across the tranche:
|
||||
|
||||
- Route discriminator (defect vs change language, provenance-tracked) separates LOCAL_FIX from STANDARD_CHANGE
|
||||
- LOCAL_FIX: focused verification policy, no research; verifiability NONE no longer blocks defect-class narrow tasks
|
||||
- STANDARD_CHANGE: DESIGN-LITE record (bounded build-contract, no model call) + integration verification
|
||||
- RESEARCHED_CHANGE: unchanged high-assurance fallback (`research_required` nonterminal disposition)
|
||||
- Evidence Probe LIVE via verification-driven trigger: real verification failure on the operator spine → one bounded probe → GROUNDED resumes / unresolved ESCALATES to RESEARCHED_CHANGE (route change, not task failure; opt-in via `MIMO_PROBE_ENABLED=1` in the TUI launcher)
|
||||
- Probe-grounded downgrade (§7): fully-grounded probes downgrade STANDARD_CHANGE → LOCAL_FIX with recorded trigger
|
||||
- Canary evidence: route latency 0.2–16ms vs fixed-depth 384s+ wasted on LOCAL tasks; escalation proven LOCAL_FIX → probe → RESEARCHED_CHANGE with persisted evidence
|
||||
|
||||
Reachability detour (CLOSED):
|
||||
- Runtime Reachability audit (one live spine identified; dead cognitive subnet mapped)
|
||||
- Unified Core route authority (`src/rune/adaptive_router.py` + `evidence_probe.py` + `route_risk_flags.py`) consumed by both the research executor and `RuneOperator.send_task`
|
||||
- Builder bridge live-consumed: `~/.mimocode/hooks/builder-bridge.ts` → `builder_bridge_cli.py` → Rune Core preflight in actual MiMo context (fire-trace: `/tmp/rune-bridge-fire.log`)
|
||||
- `research_required` nonterminal disposition; `route_ref` reference model; PL0 experience ledger with live producer and TELEMETRY/CANDIDATE qualification
|
||||
- `recall_uplift` vendored into core (PROVISIONAL — A/B documented, not proven equivalent)
|
||||
|
||||
## WHY CURRENT WORK INTERRUPTED THE PARENT
|
||||
|
||||
The ADAPT-2 audit found `classify_route()` had **zero production callers** — the route authority was architecturally implemented but cognitively dead. Wiring it honestly forced the discovery that Rune Core itself was not reachable from any live path the user experiences (the TUI path was live but disconnected from adaptive cognition; the MiMo builder session consumed none of it). Fixing that root defect was a precondition for every ADAPT capability being anything more than dead machinery: **adaptive routes are worthless if nothing live consumes them.**
|
||||
|
||||
## RETURN CONDITION
|
||||
|
||||
This detour is complete when:
|
||||
- [x] One canonical route authority consumed by production (operator + research executor)
|
||||
- [x] Research dispatch causes a real disposition (`research_required`, not failure)
|
||||
- [x] Terminal-outcome semantics corrected (no false `task.outcome` on nonterminal routing)
|
||||
- [x] Builder bridge consumed by the actual MiMo harness with measured overhead
|
||||
- [x] Deferred defects recorded (below), not chased
|
||||
- [x] ADAPT intent recovered and reconciled (this artifact, Part B)
|
||||
- [ ] **THIS TRANCHE**: the ADAPT status matrix disposition is agreed and the next tranche selected (Part D)
|
||||
|
||||
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
|
||||
|
||||
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
|
||||
|
||||
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
|
||||
|
||||
## OPEN OBLIGATIONS (deferred defects — recorded, NOT chased)
|
||||
|
||||
| ID | Obligation | Evidence | Destination |
|
||||
|---|---|---|---|
|
||||
| D1 | `memory_store.delete_entry()` leaves contentless-FTS ghosts (requires special FTS5 `INSERT ... VALUES('delete')` cleanup; retrieval can ghost-hit deleted entries) | 2026-09-10 QX7 purge required manual FTS commands in both stores | Memory-foundation work (deferred by design — handoff §17) |
|
||||
| D2 | Builder-bridge hook lives at `~/.mimocode/hooks/builder-bridge.ts` with a hardcoded repo path — no canonical ownership, no repeatable install/sync; a harness reinstall silently loses Rune's builder integration. (PARTIALLY CLOSED 2026-09-10: the Rune CODE side now has a deterministic deploy pipeline — `.gitea/workflows/deploy.yml`, stage→verify→promote with `last-good` rollback; the HOOK side still hand-installed) | Hook hand-installed 2026-09-10; deploy pipeline live (run 14243 success, e2e verified 51e14d6) | Scaffolding (SC1/SC2 territory) for hook deployment when the bridge becomes user-facing |
|
||||
| D3 | `/tmp/rune-bridge-fire.log` fire-trace is ephemeral (lost on reboot) — standing observability, not durable telemetry; accurate description: per-boot injection evidence only | Design note in hook source | Fold into OBS-1-style durable telemetry when next touched; no project now |
|
||||
| D4 | `recall_uplift` retrieval-quality effect unproven (PROVISIONAL) — recall row counts match but surfaced IDs differ; harness original retained at `~/.config/opencode/scripts/` for rollback | A/B table in module docstring | Compare in FINAL-12 or first memory-foundation pass |
|
||||
| D5 | `CloudStubAdapter` declared-not-live: router can elect cloud models that execution cannot run (`routing_mode=cloud` is telemetry theater) | Audit §A fragmentation 1 | When cloud execution is actually wanted; otherwise retire the election path |
|
||||
| D6 | Dead cognitive subnet (salience_gate, cognitive_budgeter, tiered_executor, progressive_escalation, verification_layer, outcome_ledger + companions): implemented, unconsumed, partially duplicated by ADAPT-2 | Reachability audit §C | Disposition in Scaffolding/PL1 review: RETIRE more likely than RESURRECT — superseded or absorbed |
|
||||
|
||||
## PARKED INNOVATIONS
|
||||
|
||||
- **Intent-continuity spine**: this artifact is the manual v1. The automated version (intent-state tracking that survives detours without a human handoff) belongs after PL1 — PL1's experience machinery is its natural producer. Do not build early.
|
||||
- **Substrate**: deferred behind a domain-neutral education phase (handoff §16). Unchanged.
|
||||
- **Scaffolding** SC1–SC8: deferred until adaptive/live/learning foundations are credible (handoff §15). Unchanged.
|
||||
- **Control benchmarking** (born-isolated disposal control vs full Rune): deferred to system maturity (handoff §18). Preserve passive instrumentation meanwhile — task.outcome + route.jsonl + StageAttempt already cover the required fields.
|
||||
|
||||
## SUPERSEDED APPROACHES
|
||||
|
||||
- **Option A/B route placement** (route-in-operator / two-worlds): superseded by Option C unified core authority (Nick decision, 2026-09-10).
|
||||
- **`progressive_escalation.py` ladder**: superseded by `adaptive_router.py` (duplicated concept; the original was never wired).
|
||||
- **`outcome_ledger.py` as separate authority**: superseded by the event-driven `task.outcome` + `process_experience.py` flow.
|
||||
- **Fixed-depth research pipeline as universal path**: superseded by adaptive routing (the existence proof: 2 of 3 task classes cannot pass its OUTWARD gate).
|
||||
- **per-message cognitive pipeline (P0-04)**: deliberately removed; the ContextBroker owns model-facing context. Do not resurrect.
|
||||
- **Manual handoff files as the only intent carrier**: superseded by this artifact (handoffs remain valuable input, not the system of record).
|
||||
|
||||
---
|
||||
|
||||
## ADAPT RECONCILIATION (Epic #284, recovered 2026-09-10)
|
||||
|
||||
### What ADAPT was supposed to become
|
||||
|
||||
From epic #284 verbatim: *"convert the proven high-assurance research process into an adaptive universal engineering framework that applies only as much process as the uncertainty deserves."* One framework, three depths, reversible routes, escalation/downgrade as first-class recorded transitions, the ratified pipeline as fallback, FINAL-12 cross-track benchmark as the ratification gate. Success criteria: 5/5 limitations dispositioned, all three routes through one canonical authority, FINAL-12 beats fixed-depth without quality regression.
|
||||
|
||||
### Status matrix
|
||||
|
||||
| Capability | Status | Current owner | Live consumer | Future destination | Reason |
|
||||
|---|---|---|---|---|---|
|
||||
| Route authority (classify/escalate/downgrade/floor) | **BUILT_AND_LIVE** | `src/rune/adaptive_router.py` | Operator `send_task` + research executor ISSUE dispatch | PL1 consumes its decisions | Wired, persisted, canaried |
|
||||
| Route decisions on the operator spine | **BUILT_AND_LIVE** | `rune_operator._route_task_class` | Every `send_task` | — | `route.jsonl` + `route_ref` verified |
|
||||
| RESEARCHED_CHANGE disposition | **BUILT_AND_LIVE** | `rune_operator.send_task` | Floor tasks halt local path, `research_required` + dispatch | Research consumer (below) | Live canary bf5eb53 + this tranche |
|
||||
| Targeted Evidence Probe | **BUILT_NOT_LIVE** | `src/rune/evidence_probe.py` | Wired in executor dispatch; gated behind `MIMO_PROBE_ENABLED` (nobody sets it) | ADAPT-LIVE tranche: decide the runtime policy and prove one real probe | The §6 qualification stands |
|
||||
| Verification-driven escalation | **BUILT_NOT_LIVE** | `adaptive_router.escalate_on_verification_failure` | Contract message names the hook; no automatic caller | ADAPT-LIVE tranche: wire into the local lane's verify step | Handoff §6 of bridge tranche; still true |
|
||||
| LOCAL_FIX execution path | **PARTIAL** | Route exists; execution = ordinary model path | Operator completes LOCAL tasks normally | ADAPT-3/#292: regression-gated fast path per issue spec | Route changes nothing for LOCAL today beyond disposition recording |
|
||||
| STANDARD_CHANGE path (DESIGN-LITE) | **PARTIAL** | Route exists | Same ordinary path | ADAPT-4/#293 DESIGN-LITE step | Same reason |
|
||||
| RESEARCHED_CHANGE bounded execution | **PARTIAL** | Dispatch persisted; orchestrator = MiMo session (documented §11 gap) | Session drives executor CLI manually | ADAPT-5/#294 budgets + the §11 orchestrator decision | Recorded honestly in dispatch payload |
|
||||
| Evidence capsule (#295) | **PARTIAL** (proto: builder capsule) | `builder_bridge` | MiMo hook | ADAPT-6 after paths exist | The bridge capsule is task-preflight, not OUTWARD evidence compression |
|
||||
| DESIGN consolidation (#296) | **NOT STARTED** | — | — | ADAPT-7, after ADAPT-5 | A/B experiment design exists in issue |
|
||||
| Deterministic-first QUALIFY (#297) | **BUILT_AND_LIVE (pipeline) / NOT_LIVE (operator)** | `_execute_qualify` in executor | Research pipeline only | ADAPT-8: reuse for STANDARD_CHANGE DESIGN-LITE gate | Already deterministic-first per RP8 |
|
||||
| Legibility audit (#299) | **NOT STARTED** | — | — | ADAPT-10, after routes live | Depends on 3/4/5/9 |
|
||||
| Escalation/downgrade real acceptance (#300) | **BUILT_NOT_LIVE** | Module-level canaries pass | No real-task demonstration | ADAPT-11: the acceptance IS real tasks escalating | Needs paths first |
|
||||
| FINAL-12 cross-track benchmark (#301) | **NOT STARTED** | — | — | Final epic gate | Explicitly last |
|
||||
| OBS-1 telemetry (#287) | **BUILT_AND_LIVE (pipeline)** | `stage_telemetry.py` | Research executor | Extend to operator path in ADAPT-LIVE | StageAttempt records verified in B3-v3 |
|
||||
| Artifact authority / retry (#298) | **BUILT_AND_LIVE** | `_resolve_worker_artifact` | Research executor | — | ADAPT-9 closed |
|
||||
| Task routing vs model routing separation | **BUILT_AND_LIVE** | Two authorities, distinct | Both live | Never collapse | Handoff §5 doctrine, honored in code |
|
||||
| Dead subnet (salience→budget→tier→escalate→verify→outcome) | **ABSORBED_BY_NEW_ARCHITECTURE / RETIRE candidates** | `src/rune/*.py` | None | Disposition review during PL1/Scaffolding | progressive_escalation duplicated by adaptive_router; outcome_ledger by task.outcome flow; verification_layer concepts live in `_verification_and_outcome` |
|
||||
| Builder bridge | **BUILT_AND_LIVE** | hook + CLI + `builder_bridge.py` | MiMo harness (this session) | Scaffolding owns deployment (D2) | Fire-trace proven |
|
||||
|
||||
### What fell off the roadmap during the detour (Part D input)
|
||||
|
||||
1. ADAPT-3/4/5 route execution paths (the epic's core; everything else waits on them).
|
||||
2. The §11 research-orchestrator boundary decision (MiMo session is the honest current answer; it predates the detour but was never dispositioned).
|
||||
3. Probe runtime policy (`MIMO_PROBE_ENABLED` ownership) — recorded, never decided.
|
||||
4. FINAL-12's dependency list silently grew (operator-path telemetry, builder-bridge overhead numbers) without issue updates.
|
||||
5. Epic #284 body still says "ADAPT-2 → {ADAPT-3,4,5}" with no record of the detour — the issue tracker and reality diverged until this artifact.
|
||||
|
||||
---
|
||||
|
||||
## PART D — NEXT RECOMMENDATION
|
||||
|
||||
**COMPLETE REMAINING ADAPT TRANCHE (one bounded tranche: "ADAPT-LIVE"), then PL1.**
|
||||
|
||||
Reasoning from recovered obligations, not momentum:
|
||||
|
||||
- PL1's input is route-accuracy and escalation-outcome evidence. Today routes change execution only at the RESEARCHED_CHANGE boundary. LOCAL_FIX and STANDARD_CHANGE — the two depths the measured baseline proved the fixed-depth pipeline *fails* — do not have distinct execution paths. Without them, PL1 would learn from a system that exercises one route out of three.
|
||||
- The escalation chain (local solve → verification failure → probe → research) is the epic's spine and is entirely canary-level today. ADAPT-11's acceptance ("real tasks demonstrate escalation") cannot be satisfied by more module tests.
|
||||
- The Evidence Probe needs one decision (who enables it at runtime) and one real demonstration — small, but it cannot be skipped because §3's progressive behavior (local → probe → research) is otherwise fictional at the probe step.
|
||||
- Nothing here justifies the intent-continuity spine yet: this artifact covers the need manually, and PL1 will automate it with better machinery.
|
||||
|
||||
**ADAPT-LIVE contents (smallest coherent adaptive loop):**
|
||||
1. LOCAL_FIX lane: routed task → implementation → real verification (`_verification_and_outcome`) → escalation hook wired on verification failure (completes the §7 chain end-to-end).
|
||||
2. STANDARD_CHANGE lane: DESIGN-LITE step per #293 (bounded, deterministic structure, no research dossier).
|
||||
3. Probe policy decision + one real probe through the production path (`MIMO_PROBE_ENABLED` set by the operator runtime, not tests).
|
||||
4. ADAPT-11 acceptance demonstration on 2–3 real tasks across the three depths, with telemetry.
|
||||
5. Update epic #284 with the detour record + this matrix; close the gap between tracker and reality.
|
||||
|
||||
Then: **PL1** (T9), with the intent-continuity spine as PL1's first or second deliverable (it is an experience-system product, not a separate architecture project).
|
||||
|
||||
## GITEA RECONCILIATION (2026-09-11)
|
||||
|
||||
Part A-J executed. 15 issues closed (HISTORICAL_EVIDENCE/SUPERSEDED/ACCEPTED_LIMITATION) with evidence. Bug label cleaned: 16 misleading labels removed (open bugs 24→8). #259/#258 updated to current architecture. Remote tracking fixed (master tracks gitea/master, not stale origin). PL1 metrics corrected (NOT_YET_MEASURABLE where evidence doesn't support numbers). D7: bump works, release job fails on Pi — ROOT_CAUSE_UNKNOWN_DEFERRED. Automation noise: 5 duplicate bump commits per logical change (queue-drain artifact, not live recursion — version converges, commit history doesn't).
|
||||
|
||||
## DEFECT D7 (2026-09-11): RESOLVED — release-job failures root-caused
|
||||
|
||||
History: the release job failed on every push since 03:16 (c2528c1).
|
||||
Superseded diagnosis: "Pi-side backup step logs Failed to connect to
|
||||
127.0.0.1:3000" was a red herring — the Pi backup step was never the
|
||||
failing job.
|
||||
|
||||
Root causes (live-proven T15 Part F, 2026-09-11 evening):
|
||||
1. c2528c1 left a corrupted shell fragment after the converged-check fi
|
||||
in auto-version.yml — the release job died at bash parse time (exit 2)
|
||||
before any bump. Fixed in c35b604.
|
||||
2. release_convergence() never assigned the release boundary — all 959+
|
||||
commits classified as unreleased, so the warranted bump never
|
||||
converged and queued runs each wrote their own bump commit. Fixed in
|
||||
e395ee1 (boundary wiring) + 9e0b855 (boundary grep must match subjects
|
||||
only, not bodies) + c35018f (numeric semver comparison: '0.66.9' <
|
||||
'0.66.10' is False lexicographically, which silently skipped warranted
|
||||
bumps as version_ahead_of_target).
|
||||
3. The per-file contents PUT committed one commit per version surface —
|
||||
five same-version bump commits per real bump (the "duplicate bump
|
||||
cascade" since 0.66.2, misdiagnosed as queue drain). Fixed in 470d82f
|
||||
+ d68fd1e (single ChangeFiles commit).
|
||||
|
||||
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
|
||||
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
|
||||
drained, zero delayed duplicates, zero workflow failures.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -18,6 +18,7 @@ PR as the new workflow._
|
||||
| `research-dispatcher.yml` | issue labeled `research-pending`, workflow_dispatch | legacy stage dispatcher (SUPERSEDED by the canonical Research-to-Build pipeline, epic #257; retained until RP11 retirement) | Trigger labels removed from disposable issues 2026-09-08; no stale redispatch path |
|
||||
| `auto-label.yml` | issue events | priority gate, label transitions | Gate comments on unlabelled issues |
|
||||
| `auto-backup.yml` | schedule | repo backup | Journal alert on failure |
|
||||
| `deploy.yml` | push to master | deploy the CI-qualified SHA to the local Rune code deployment (`/mnt/workdrive/rune-code/<sha>`): waits for `CI Pipeline / required-checks` success on that exact SHA, stages, stamps `_build_identity.json`, verifies (import + SHA assert + authority smoke), atomically promotes `current` symlink, records `last-good`, re-verifies through the consuming path | CI not green/timeout → deployment refused, previous deployment stays current |
|
||||
| `auto-update-gitea.yml` | schedule | Gitea self-update | Binary-only rollback risk (T1.1 audit finding) |
|
||||
|
||||
| `research-stage-headless.yml` | issue labeled `research-pending` | headless research dispatcher: 4-phase round (INWARD/OUTWARD/INNOVATION/SYNTHESIS) via run_agent_stage.py; files research/issue-N-research.md + marker comment | Tool receipt invalid (OUTWARD <2 domains) → job fails loud, label stays; lock TTL re-dispatches crashed rounds |
|
||||
|
||||
@@ -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.0"
|
||||
version = "0.66.14"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
# RUNTIME REACHABILITY AUDIT — Live Rune Slice Tranche
|
||||
**Date: 2026-09-10 · Master v0.66.0 · Method: code tracing with file:line evidence, 4 parallel tracers + first-hand canaries**
|
||||
|
||||
---
|
||||
|
||||
## A. The actual current execution path
|
||||
|
||||
**One real end-to-end path exists and executes today:**
|
||||
|
||||
```text
|
||||
USER INPUT (TUI chat/task channel, or `rune_operator.py send` CLI)
|
||||
↓ confirmed rune_tui.py:2115 on_input_submitted → _send_to_model (2223) → rune_operator.py:494 send_task
|
||||
TASK IDENTITY / STATE
|
||||
↓ confirmed TaskStateRepository amend-or-create (rune_operator.py:430-433, 502-521)
|
||||
↓ backed by orchestration/event_store.DurableEventStore + atomic.AtomicStore
|
||||
ROUTING DECISION
|
||||
↓ confirmed infer_task_family (rune_operator.py:541) → model_routing.build_model_router().rank()
|
||||
↓ advisory, cheapest-verified-first over static CAPABILITY_FLOOR (model_routing.py:306-312)
|
||||
CONTEXT / MEMORY ACQUISITION
|
||||
→ confirmed ContextBroker.assemble (rune_operator.py:1294; context_broker.py:616)
|
||||
→ confirmed contributors: persona(static literal) → tools(capability_broker, rune_operator.py:725)
|
||||
→ memory(memory_selector.select_for_task → memory_store.recall + warm_neuron_store
|
||||
+ graph_memory + abstraction, rune_operator.py:1126-1190; memory_selector.py:269-496)
|
||||
→ model_profile → history → user
|
||||
→ confirmed budget admission against real model window (context_broker.py:616)
|
||||
SAFETY ESCALATION
|
||||
→ confirmed safety_override.get_params + tier loop decorates user block (rune_operator.py:1315-1392)
|
||||
MODEL INVOCATION
|
||||
→ confirmed LocalToolRuntime(transport=OllamaChatTransport) → POST localhost:11434/api/chat
|
||||
(rune_operator.py:1400-1431; local_tool_runtime.py:102,178-199)
|
||||
→ confirmed tool loop: tool_executor.execute_tool = prepare_tool, mutation tools gated
|
||||
by approval round-trip (tool_executor.py:830-833, 675, 752)
|
||||
RESPONSE → OUTCOME
|
||||
→ confirmed refusal escalation tier+1 (rune_operator.py:1452-1461); usage folding (1356-1367)
|
||||
→ confirmed task transition completed/failed + event_bus emit (rune_operator.py:608-643)
|
||||
→ confirmed durable events: ~/.rune/events.jsonl (task_state.transition, conversation.message)
|
||||
```
|
||||
|
||||
**First-hand canary (2026-09-10, this audit):** `rune_operator.py send "Reply with exactly: SPINE_ALIVE_20260910"` →
|
||||
router elected `qwen2.5:3b` → completed ✓ → task state + conversation events persisted. **The spine is alive.**
|
||||
(One degrade observed: `inference_trace` tier-2 ImportError in standalone CLI mode.)
|
||||
|
||||
### Fragmentation points (confirmed)
|
||||
|
||||
1. **Cloud routing is telemetry theater.** The router can elect `mimo-v2.5-pro` provider=openai and record
|
||||
`routing_mode="cloud"` (rune_operator.py:561, 1593) — but `_execute_on_model` hardwires
|
||||
`OllamaChatTransport` (rune_operator.py:1400). A cloud election sends a nonexistent model to localhost:11434
|
||||
→ guaranteed failure. `CloudStubAdapter` is explicitly "declared, not live" (model_routing.py:134-155).
|
||||
2. **`model_connect.generate`, `provider_registry.generate`, `microkernel` dispatch**: imported, never invoked
|
||||
in the send path. The registries are display furniture; execution is a bare urllib POST in local_tool_runtime.
|
||||
3. **The research pipeline's orchestrator is the agent session itself.** Nothing in-repo invokes
|
||||
`StageExecutor` except its own CLI and tests — B3-v3's 12 stage executions were the session driving
|
||||
`research_stage_executor.py` stage-by-stage (telemetry/attempts.jsonl timestamps confirm).
|
||||
4. **No console script in packaging** — `python -m rune` works only via manual sys.path patching
|
||||
(rune_tui.py:28-33; dual `from .X` / `from rune.X` fallbacks throughout).
|
||||
5. **inference_trace degrades** in standalone mode (relative-import failure, tier-2) — trace persistence is
|
||||
partial in the CLI path.
|
||||
|
||||
---
|
||||
|
||||
## B. Reachability table (highest continuously proven rung)
|
||||
|
||||
Ladder: IMPLEMENTED → PERSISTED → CONSUMED → AFFECTS LIVE BEHAVIOUR → OBSERVABLE → END-TO-END PROVEN
|
||||
|
||||
### LIVE (affects live behaviour, evidenced)
|
||||
|
||||
| System | Rung | Evidence |
|
||||
|---|---|---|
|
||||
| ContextBroker | CONSUMED→AFFECTS (context assembly every call) | rune_operator.py:997-1294; context_broker.py:616 |
|
||||
| memory_selector (single memory contributor) | CONSUMED→AFFECTS | rune_operator.py:1126-1190; fail-closed, trace fields |
|
||||
| memory_store + drift_core rerank | CONSUMED→AFFECTS (retrieval order) | memory_store.py:891-925, 1152 |
|
||||
| capability_broker | CONSUMED→AFFECTS (tool choice) | rune_operator.py:725; tool_executor.py:1643 |
|
||||
| LocalToolRuntime + OllamaChatTransport | END-TO-END PROVEN (live canary) | local_tool_runtime.py:102,199 |
|
||||
| tool_executor (prepare/approve) | END-TO-END PROVEN | tool_executor.py:830-833 |
|
||||
| TaskState/Conversation repos + event_bus | END-TO-END PROVEN (durable events) | ~/.rune/events.jsonl verified |
|
||||
| model_routing (advisory rank) | CONSUMED (decision feeds execution) | rune_operator.py:543-560 |
|
||||
| complexity_scorer | CONSUMED (task-channel advisory) | rune_tui.py:2182 |
|
||||
| claim_audit | CONSUMED (advisory-only effect — warns, never blocks) | rune_tui.py:3829-3836 |
|
||||
| pins | PARTIALLY_LIVE (display only, not injected) | rune_tui.py:1754, 2265 |
|
||||
|
||||
### IMPLEMENTED_NOT_CONSUMED — the dead cognitive subnet
|
||||
|
||||
The entire salience→budget→tier→escalate→verify→outcome ladder is a **closed dead subnet**: each module's only
|
||||
consumers are the other dead modules plus canary import checks.
|
||||
|
||||
| System | Status | Note |
|
||||
|---|---|---|
|
||||
| salience_gate | IMPLEMENTED_NOT_CONSUMED | role absorbed by elastic_cognition (itself unconsumed) |
|
||||
| cognitive_budgeter | IMPLEMENTED_NOT_CONSUMED | consumers are tiered_executor/elastic_cognition — both dead |
|
||||
| tiered_executor | IMPLEMENTED_NOT_CONSUMED | zero importers |
|
||||
| progressive_escalation | IMPLEMENTED_NOT_CONSUMED | **fully duplicated** by scripts/adaptive_router.py |
|
||||
| verification_layer | IMPLEMENTED_NOT_CONSUMED | zero importers |
|
||||
| outcome_ledger | PERSISTED_NOT_CONSUMED | **explicitly de-wired** (model_routing.py:17 "No OutcomeLedger dependency") |
|
||||
| witness / auto_tasks / behavior | IMPLEMENTED_NOT_CONSUMED | deliberately retired per P0-04 #20 (rune_tui.py:2208-2214, docs/pipeline-removal.md) — imports remain as vestiges |
|
||||
| task_gate | IMPLEMENTED_NOT_CONSUMED | zero callers anywhere |
|
||||
| self_improve | IMPLEMENTED_NOT_CONSUMED | manual /improve report only |
|
||||
| persona_gate | IMPLEMENTED_NOT_CONSUMED | manual /persona-gate only; permissive by design |
|
||||
| work_manager | IMPLEMENTED_NOT_CONSUMED | manual /audit //work commands |
|
||||
| warm_router (in-repo) | TEST_ONLY | TUI imports it; zero calls; per-message pipeline removed |
|
||||
| microkernel | PARTIALLY_LIVE | /plugins status only; dispatch engine unused |
|
||||
| tier_manager | PERSISTED_NOT_CONSUMED | demotion sweep never invoked |
|
||||
| drift_ledger | IMPLEMENTED_NOT_CONSUMED | manual /drift diagnostic |
|
||||
| persona_definition | PARTIALLY_LIVE | nightly persona-file writes; NOT used for live prompts (persona block is a static literal) |
|
||||
|
||||
### Memory & context (tracer-2)
|
||||
|
||||
| System | Rung | Evidence |
|
||||
|---|---|---|
|
||||
| memory_selector | LIVE (sole v1_owner of memory injection, every turn) | rune_operator.py:1144-1186; memory_selector.py:3-9, 497-511 |
|
||||
| memory_store (read side) | LIVE (## Context block every turn) | memory_selector.py:269-276; rune_operator.py:1184-1186 |
|
||||
| memory_store (write side) | PERSISTED_NOT_CONSUMED — **no live chat-path writer in-repo**; producers are CLI add/hook, retrieval_benchmark, external curator | memory_store.py:501, 2227 |
|
||||
| memory_consolidation | PARTIALLY_LIVE (read live every turn; write on-demand/night) | memory_selector.py:439-460; sleep_time.py:65-68 |
|
||||
| warm_neuron_store (read) | PARTIALLY_LIVE (trigger-matched read every turn; write via external curator only) | memory_selector.py:335-337 |
|
||||
| warm_router | PARTIALLY_LIVE — route() **dead by design** since P0-04 (enforced by test); write path on-demand | rune_tui.py:3644-3660; test_p0_04_pipeline_removal.py:33-52 |
|
||||
| attention_capsule | LIVE (the sent payload itself) | context_broker.py:762-767; rune_operator.py:1385+ |
|
||||
| cache | LIVE (latency memoization only, no context shaping) | memory_store.py:610, 1159 |
|
||||
| recall_router | IMPLEMENTED_NOT_CONSUMED — **notable orphan**: its abstention→deep-research routing (recall_router.py:9-19) mirrors the ADAPT-2 escalation concept; the selector abstains (memory_selector.py:188-196) but nothing routes that abstention to research | zero importers |
|
||||
| auto_context | IMPLEMENTED_NOT_CONSUMED (deliberately retired, enforced by test) | canary_p0_04.py:74 |
|
||||
| context_pressure | PERSISTED_NOT_CONSUMED (manual /pressure diagnostic) | rune_tui.py:2577 |
|
||||
| query_memory | IMPLEMENTED_NOT_CONSUMED (canary-checked only) | query_memory.py:193 |
|
||||
| subconscious_pass | IMPLEMENTED_NOT_CONSUMED | subconscious_pass.py:155 |
|
||||
| token_optimizer | PERSISTED_NOT_CONSUMED (write-only; optimize_prompt zero callers) | token_optimizer.py:297 |
|
||||
| sleep_time / night_mode | PARTIALLY_LIVE (on-demand trigger; model effect indirect) | rune_tui.py:2978, 3078-3109 |
|
||||
|
||||
**Memory findings:** the single live memory→context path is operator contributor → MemorySelector → six sources
|
||||
under one 0.42 relevance floor → broker admission (memory sub-budget min(1200, window//8)). **Split-brain risk**:
|
||||
three paths bypass in-repo modules by importing/subprocessing the *external harness copies* in
|
||||
`~/.config/opencode/scripts` (tool_executor.py:1519, auto_context.py:22-24, night_mode.py:213) — separate files
|
||||
over the same DB, byte-identity unverified. And the store has **no live writer on the chat path**: the store's
|
||||
contents are produced by scripts/external runs while consumption is fully live.
|
||||
|
||||
---
|
||||
|
||||
## C. Dead/partial capability findings
|
||||
|
||||
1. **The dead cognitive subnet** (above) — an entire ladder built, tested, never consumed. The TUI's
|
||||
per-message pipeline that used to consume some of it was deliberately removed (P0-04 #20) and never replaced.
|
||||
2. **False-liveness surface**: TUI startup "module loaded" checklist (rune_tui.py:1826-1866) reports witness/
|
||||
auto_tasks/behavior/elastic_cognition as present — presence checks, not consumers. Creates confidence of
|
||||
liveness that the trace contradicts.
|
||||
3. **Cloud execution gap**: router elects cloud, executor can't run it (fragmentation #1).
|
||||
4. **outcome_ledger de-wired**: cognitive-performance evidence is collected nowhere today — PL1's foundation
|
||||
has no live producer in the operator path.
|
||||
5. **progressive_escalation.py duplicated** by scripts/adaptive_router.py — two escalation ladders, one wired.
|
||||
6. **Persona block is a ~60-line hardcoded literal** (rune_operator.py:1079+) pointing at OpenCode-era memory
|
||||
paths; persona_definition's compiled definition is not used for live prompts. prompt_sync and all voice
|
||||
modules are display-only — they do not touch the cognitive path (tracer-4).
|
||||
7. **hook_engine.fire is a latent bug**: rune_tui.py:3841 calls a nonexistent `hook_engine.fire`; the exception
|
||||
is swallowed, so "post-message hooks" silently never run. All "called by the microkernel" hook handlers
|
||||
(warm_router.py:1280, pins.py:254, graph_memory.py:379, recall_router.py:108...) are dead — `microkernel.fire`
|
||||
has zero callers.
|
||||
8. **event_bus is a one-way journal**: task.failed emissions are durable (event_bus.py:113-135) but **no actor
|
||||
reacts** — the PL0 Experience ledger would be the natural consumer and is itself orphaned.
|
||||
9. **PL0 Experience ledger is orphaned**: defined only in scripts/process_experience.py:41 (ExperienceLedger,
|
||||
append-only JSONL) — zero producers, zero consumers, anywhere in the repo.
|
||||
10. **shard_manager dual-copy drift hazard**: src/rune/shard_manager.py and shared/shard_manager.py differ;
|
||||
memory_store.py:1134 does a bare `import shard_manager` — resolution is sys.path-dependent. Shards are
|
||||
bypassed on the main recall path anyway (memory_selector.py:271 calls recall directly).
|
||||
11. **Single-provider reality**: ~6 provider modules exist (provider_registry, provider_budget,
|
||||
standardized_providers, model_connect, connection_pool, cloud adapters); the only executing transport is
|
||||
OllamaChatTransport → localhost:11434/api/chat. No OpenAI/Anthropic/GLM code executes on the live path.
|
||||
12. **safety_override is the real governor**: the only infra module besides model_routing/tool stack that
|
||||
changes invocation behaviour (tier escalation, prompt decoration, temperature — rune_operator.py:1315-1430).
|
||||
|
||||
---
|
||||
|
||||
## D. ADAPT-2 qualification (Phase 6)
|
||||
|
||||
**Initial routing: LIVE (pipeline path, canary-proven).**
|
||||
Real task → production ISSUE dispatch (`StageExecutor.execute`, scripts/research_stage_executor.py:316+) →
|
||||
`_route_on_dispatch` → `classify_route` → route.jsonl (INITIAL_ROUTE) → `ROUTED_LOCAL_FIX` changes execution
|
||||
(research chain not entered; tree records terminal route). Canary 1 proves the behavioural effect through the
|
||||
production dispatch path. Caveat: the *dispatcher* is the agent session driving the executor CLI — there is no
|
||||
standalone orchestrator process; the routing edge itself is proven.
|
||||
|
||||
**Targeted Evidence Probe: PARTIALLY_LIVE.**
|
||||
Implemented, schema-gated, canary-proven, wired into dispatch (research_stage_executor.py:340-346). Real probing
|
||||
is gated behind `MIMO_PROBE_ENABLED=1` or an injected runner. Traced: **nobody sets that env in production** —
|
||||
no workflow, no script, no operator path. Without opt-in, material unknowns escalate directly to deep research
|
||||
(correct but un-optimized). Qualification of the opt-in: **temporary development gate / missing runtime policy
|
||||
decision** — the runtime decision "who decides probing is permitted" does not exist yet. Not changed, per
|
||||
instruction.
|
||||
|
||||
**Verification-driven escalation: PARTIALLY_LIVE.**
|
||||
`escalate_on_verification_failure()` exists and is canary-proven (module level). The `ROUTED_LOCAL_FIX` consumer
|
||||
is external (the agent session/operator that receives the routed result). Nothing in-repo automatically performs
|
||||
implementation→verification→escalation invocation. The chain "real local implementation → real verification
|
||||
failure → automatic escalation" is **not yet demonstrated end-to-end**.
|
||||
|
||||
---
|
||||
|
||||
## E. Smallest Live Rune Slice — verdict
|
||||
|
||||
**The spine exists.** `rune_operator.send_task` is a real, live, end-to-end execution path (proven by canary
|
||||
during this audit): input → task state → context/memory assembly → model routing → Ollama tool loop →
|
||||
verification/outcome → durable events. The eventual TUI already calls exactly this path. **No new execution
|
||||
architecture is needed.**
|
||||
|
||||
What is missing is three EDGES onto that spine — the handoff's target path steps that currently terminate:
|
||||
|
||||
| Missing edge | Today | Smallest fix |
|
||||
|---|---|---|
|
||||
| **Adaptive cognition** (route decision) | send_task uses model_routing (model choice) but NOT task-class routing; the route authority lives in the research-pipeline world (scripts/), disconnected from the operator path | Route decision at send_task entry, persisted to the durable trace + event journal (see §G choice) |
|
||||
| **Outcome/experience producer** | outcome_ledger de-wired; PL0 Experience ledger orphaned (zero producers); task.completed/failed transitions fire into a journal **no actor reads** | Emit a typed `task.outcome` event on every terminal transition (payload: task_id, route, model, verification, outcome); a small consumer appends Process Experience records |
|
||||
| **Abstention catch** | memory_selector abstains (measured, trace-observable) but nothing routes that signal anywhere | P1 — depends on the route-authority choice |
|
||||
|
||||
Plus two honest defects found on the spine itself (bounded, non-architectural):
|
||||
- `inference_trace` tier-2 ImportError in standalone CLI mode (trace persistence partial).
|
||||
- `hook_engine.fire` dead call site (rune_tui.py:3841 calls a nonexistent function, exception swallowed).
|
||||
|
||||
**What this is NOT**: not a parallel demo path — the spine IS the TUI's path. Not a mass-wiring of the dead
|
||||
subnet — §G ranks it.
|
||||
|
||||
---
|
||||
|
||||
## F. Proposed canary (Phase 8 acceptance)
|
||||
|
||||
One bounded task through the REAL spine, asserting every rung:
|
||||
|
||||
```text
|
||||
1. rune_operator.py send "<bounded task>" (CLI — same path the TUI calls)
|
||||
2. ContextBroker assembles; trace shows memory_candidates/selected/abstained
|
||||
3. Route decision recorded (INITIAL_ROUTE in durable trace/event journal)
|
||||
4. Route affects execution posture (observable in trace + task record)
|
||||
5. Model/tool execution via OllamaChatTransport (already proven)
|
||||
6. Verification result recorded (existing verification back-fill)
|
||||
7. Outcome persisted: task_state.transition + NEW task.outcome event
|
||||
8. Process Experience record appended (NEW consumer of the outcome event)
|
||||
9. All transitions inspectable: ~/.rune/events.jsonl + route/trace fields + experience JSONL
|
||||
```
|
||||
|
||||
Pass criterion: steps 1–9 reconstructable from persisted state with no prose inference — the same
|
||||
audit-trail standard as ADAPT-2 canary 6.
|
||||
|
||||
---
|
||||
|
||||
## G. Integration priorities
|
||||
|
||||
**P0 — required for the cognitive execution spine (this tranche):**
|
||||
1. `task.outcome` typed event on every send_task terminal transition (producer edge for experience/PL1).
|
||||
2. Experience-ledger consumer of the durable event store (PL0 gains a live producer; PL1 gains input).
|
||||
3. Route decision recorded in the live path (see THE CHOICE below).
|
||||
4. Fix inference_trace standalone degrade (bug).
|
||||
5. Remove hook_engine.fire dead call site (honest fix of a silently-swallowed AttributeError).
|
||||
|
||||
**THE CHOICE (presented, not buried — consent boundary):** where does task-class route authority live?
|
||||
- **Option A — Route-in-operator**: send_task calls the route authority (import from scripts/ or port to
|
||||
src/rune), persists RouteDecision, and RESEARCHED_CHANGE tasks are flagged/dispatched to the research
|
||||
pipeline. Operator becomes the single dispatch entry for ALL work.
|
||||
- **Option B — Two worlds, formally**: operator path stays route-free; the research pipeline keeps its own
|
||||
routing. The boundary is documented; no unification now.
|
||||
- **Option C — Unified core authority**: move route authority into src/rune; both the operator and the
|
||||
research executor consume it. Cleanest end-state, largest change.
|
||||
|
||||
Recommendation: **A** — smallest spine that makes routing observable in live behaviour, without moving code
|
||||
across worlds; C remains the long-term shape after PL1 shows what the routes need to be.
|
||||
|
||||
**P1 — claims to materially alter cognition today:**
|
||||
- recall_router abstention→research catch (the orphan whose concept is the missing edge)
|
||||
- verification_layer consumed for LOCAL_FIX verification (completes the verification-escalation chain)
|
||||
- claim_audit escalation beyond advisory (currently warns, never blocks)
|
||||
- outcome_ledger: re-wire into the event-driven outcome flow OR formally retire it (not both)
|
||||
- progressive_escalation.py: retire in favour of scripts/adaptive_router.py (duplicated ladder)
|
||||
|
||||
**P2 — required by PL1/Scaffolding:** experience-ledger schema maturity, capability-gap detection inputs.
|
||||
|
||||
**P3 — useful, not blocking:** persona block sourced from persona_definition (not a hardcoded literal);
|
||||
microkernel dispatch decision (wire or formally retire); shard_manager unification.
|
||||
|
||||
**P4 — legacy/deferred:** voice* (no cognitive contact), provider cloud modules until cloud execution is
|
||||
actually wanted, response_adapter (superseded), model_registry+tiered_executor (isolated pair), salience_gate
|
||||
(absorbed), context_pressure/token_optimizer/query_memory/subconscious_pass (diagnostics).
|
||||
|
||||
---
|
||||
|
||||
*Generated from code tracing with file:line evidence; live canary executed 2026-09-10.*
|
||||
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy_qualify.py — read the CI qualification status for one commit.
|
||||
|
||||
Called by .gitea/workflows/deploy.yml (kept out of the YAML so the workflow
|
||||
has no embedded multi-line scripts — a YAML block-scalar indentation hazard).
|
||||
|
||||
Prints one of: success | failure | pending
|
||||
Exit code 0 always (the shell decides what to do with the verdict).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import sys as _sys
|
||||
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
|
||||
if _SCRIPT_DIR not in _sys.path:
|
||||
_sys.path.insert(0, _SCRIPT_DIR)
|
||||
import gitea_auth
|
||||
|
||||
|
||||
def main() -> int:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha and token):
|
||||
print("pending") # missing config → cannot verify → not qualified
|
||||
return 0
|
||||
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
data = json.loads(r.read())
|
||||
except (OSError, ValueError) as exc:
|
||||
# transient API/network failure → keep polling (urllib raises
|
||||
# URLError (OSError subclass); bad JSON raises ValueError)
|
||||
print(f"pending (status probe failed: {exc})")
|
||||
return 0
|
||||
required = [
|
||||
s
|
||||
for s in data.get("statuses", [])
|
||||
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
|
||||
]
|
||||
if not required:
|
||||
print("pending")
|
||||
else:
|
||||
print(required[0].get("status", "pending"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy_stage.py — stage + verify one CI-qualified revision for deployment.
|
||||
|
||||
Called by .gitea/workflows/deploy.yml. Stages src/ into
|
||||
/mnt/workdrive/rune-code/<short-sha>, stamps _build_identity.json, verifies
|
||||
the staged tree (import + SHA assert + authority smoke), then ATOMICALLY
|
||||
promotes the `current` symlink and records `last-good` (safe rollback:
|
||||
the previous deployment remains available).
|
||||
|
||||
Exit 0 = deployed and verified. Non-zero = deployment refused; the previous
|
||||
deployment stays current.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
|
||||
# hardcoded dev path: the runner user has its own checkout and git
|
||||
# refuses "dubious ownership" on foreign working copies.
|
||||
repo = Path(os.environ.get("GITHUB_WORKSPACE", os.getcwd())).resolve()
|
||||
sha = subprocess.run(
|
||||
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if not sha:
|
||||
print(f"FATAL: cannot resolve HEAD in {repo}", file=sys.stderr)
|
||||
return 1
|
||||
short = sha[:12]
|
||||
|
||||
# --- stage ---------------------------------------------------------
|
||||
stage = DEPLOY_ROOT / short
|
||||
tmp_stage = DEPLOY_ROOT / f".staging-{short}-{int(time.time())}"
|
||||
shutil.copytree(repo / "src", tmp_stage / "src")
|
||||
# shared/ is required at runtime: src/rune bare imports (prompt_sync ->
|
||||
# build_pipeline_policy, memory_store -> shard_manager, etc.) resolve
|
||||
# via the ../../shared fallback from src/rune/*. Its absence broke the
|
||||
# deployed TUI launch (ModuleNotFoundError: build_pipeline_policy).
|
||||
if (repo / "shared").exists():
|
||||
shutil.copytree(repo / "shared", tmp_stage / "shared")
|
||||
for extra in ("pyproject.toml", "README.md"):
|
||||
if (repo / extra).exists():
|
||||
shutil.copy2(repo / extra, tmp_stage / extra)
|
||||
|
||||
# --- build identity -------------------------------------------------
|
||||
sys.path.insert(0, str(repo / "src"))
|
||||
import rune as rune_dev # noqa: E402 — read the source version
|
||||
|
||||
identity = {
|
||||
"commit": sha,
|
||||
"version": rune_dev.__version__,
|
||||
"deployed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
(tmp_stage / "src" / "rune" / "_build_identity.json").write_text(json.dumps(identity, indent=2))
|
||||
|
||||
# --- verify the STAGED tree (before promotion) ----------------------
|
||||
env = dict(os.environ, RUNE_HOME="/mnt/workdrive/rune", RUNE_MEMORY_DIR="/home/nick/.config/opencode/memory")
|
||||
verify = subprocess.run(
|
||||
[
|
||||
VENV_PY,
|
||||
"-c",
|
||||
f"""
|
||||
import json, sys
|
||||
sys.path.insert(0, {str(tmp_stage / "src")!r})
|
||||
import rune
|
||||
info = rune.build_info()
|
||||
assert info["source"] == "deployed", f"identity file missing: {{info}}"
|
||||
assert info["commit"] == {sha!r}, f"SHA mismatch: {{info['commit']}}"
|
||||
assert info["version"], "empty version"
|
||||
from rune import adaptive_router, builder_bridge, context_broker, rune_operator # noqa: F401
|
||||
print("VERIFIED:", json.dumps(info))
|
||||
""",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=120,
|
||||
)
|
||||
print(verify.stdout.strip())
|
||||
if verify.returncode != 0:
|
||||
print("VERIFY FAILED — deployment refused:\n" + verify.stderr[-800:], file=sys.stderr)
|
||||
shutil.rmtree(tmp_stage, ignore_errors=True)
|
||||
return 1
|
||||
|
||||
# --- TUI startup canary (handoff 2026-09-10 §4): the ACTUAL
|
||||
# RuneApp initialization must succeed before promotion. A staged
|
||||
# revision whose TUI cannot initialize never becomes current.
|
||||
canary = subprocess.run(
|
||||
[
|
||||
VENV_PY,
|
||||
str(REPO_ROOT / "scripts" / "tui_startup_canary.py"),
|
||||
"--mode",
|
||||
"candidate",
|
||||
"--stage",
|
||||
str(tmp_stage),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**env, "PYTHONPATH": str(tmp_stage / "src")},
|
||||
timeout=180,
|
||||
cwd=str(tmp_stage),
|
||||
)
|
||||
print(canary.stdout.strip())
|
||||
if canary.returncode != 0:
|
||||
print("TUI STARTUP CANARY FAILED — deployment refused:\n" + canary.stderr[-800:], file=sys.stderr)
|
||||
shutil.rmtree(tmp_stage, ignore_errors=True)
|
||||
return 1
|
||||
|
||||
# --- promote (atomic) + last-known-good ------------------------------
|
||||
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
current = DEPLOY_ROOT / "current"
|
||||
if current.is_symlink():
|
||||
last_good = DEPLOY_ROOT / "last-good"
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
last_good.symlink_to(os.readlink(current))
|
||||
if current.is_symlink() or current.exists():
|
||||
current.unlink()
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage) # re-deploying an already-staged SHA: restage
|
||||
os.rename(tmp_stage, stage)
|
||||
current.symlink_to(stage)
|
||||
print(f"PROMOTED: {short}")
|
||||
print("current ->", os.readlink(current))
|
||||
if (DEPLOY_ROOT / "last-good").is_symlink():
|
||||
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+72
-16
@@ -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))
|
||||
+154
-13
@@ -20,6 +20,7 @@ Root-cause layers:
|
||||
EXTERNAL_DEPENDENCY — external service behaved unexpectedly
|
||||
UNKNOWN — attribution uncertain
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -28,14 +29,22 @@ import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
ROOT_CAUSE_LAYERS = {
|
||||
"MODEL_BEHAVIOR", "PROMPT_CONTRACT", "SCHEMA", "NORMALIZER",
|
||||
"TOOL_INTERFACE", "ARTIFACT_ASSEMBLY", "VALIDATOR", "ROUTING",
|
||||
"STAGE_ARCHITECTURE", "DETERMINISTIC_RUNTIME", "EXTERNAL_DEPENDENCY",
|
||||
"MODEL_BEHAVIOR",
|
||||
"PROMPT_CONTRACT",
|
||||
"SCHEMA",
|
||||
"NORMALIZER",
|
||||
"TOOL_INTERFACE",
|
||||
"ARTIFACT_ASSEMBLY",
|
||||
"VALIDATOR",
|
||||
"ROUTING",
|
||||
"STAGE_ARCHITECTURE",
|
||||
"DETERMINISTIC_RUNTIME",
|
||||
"EXTERNAL_DEPENDENCY",
|
||||
"UNKNOWN",
|
||||
}
|
||||
|
||||
RESULT_TYPES = {"SUCCESS", "FAILURE", "INEFFICIENCY"}
|
||||
CANDIDATE_STATUSES = {"SEED", "EVALUATED", "PROMOTED", "REJECTED"}
|
||||
# (extended below by the PL0 qualification block)
|
||||
|
||||
|
||||
class ExperienceLedger:
|
||||
@@ -45,13 +54,25 @@ class ExperienceLedger:
|
||||
self.path = path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
def record(self, task_run_stage: str, result_type: str, symptom: str,
|
||||
evidence: list[str], root_cause: str, confidence: float,
|
||||
human_intervention: bool = False, pipeline_sha: str = "UNKNOWN",
|
||||
correction_applied: str = "", recurrence_class: str = "",
|
||||
wall_cost_s: float | str = 0, model_cost_s: float | str = 0,
|
||||
tool_cost_s: float | str = 0, quality_outcome: str = "",
|
||||
reusable: bool = False, candidate_status: str = "SEED") -> dict:
|
||||
def record(
|
||||
self,
|
||||
task_run_stage: str,
|
||||
result_type: str,
|
||||
symptom: str,
|
||||
evidence: list[str],
|
||||
root_cause: str,
|
||||
confidence: float,
|
||||
human_intervention: bool = False,
|
||||
pipeline_sha: str = "UNKNOWN",
|
||||
correction_applied: str = "",
|
||||
recurrence_class: str = "",
|
||||
wall_cost_s: float | str = 0,
|
||||
model_cost_s: float | str = 0,
|
||||
tool_cost_s: float | str = 0,
|
||||
quality_outcome: str = "",
|
||||
reusable: bool = False,
|
||||
candidate_status: str = "SEED",
|
||||
) -> dict:
|
||||
assert result_type in RESULT_TYPES, f"invalid result_type: {result_type}"
|
||||
assert root_cause in ROOT_CAUSE_LAYERS, f"invalid root_cause: {root_cause}"
|
||||
assert candidate_status in CANDIDATE_STATUSES, f"invalid status: {candidate_status}"
|
||||
@@ -85,8 +106,10 @@ class ExperienceLedger:
|
||||
out = []
|
||||
with open(self.path) as f:
|
||||
for line in f:
|
||||
try: out.append(json.loads(line))
|
||||
except json.JSONDecodeError: continue
|
||||
try:
|
||||
out.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return out
|
||||
|
||||
def by_root_cause(self, layer: str) -> list[dict]:
|
||||
@@ -100,3 +123,121 @@ class ExperienceLedger:
|
||||
|
||||
def reusable_lessons(self) -> list[dict]:
|
||||
return [e for e in self.load_all() if e["reusable_lesson"]]
|
||||
|
||||
|
||||
def record_operator_outcomes(task_state_path: str | None = None, ledger_path: str | None = None) -> list[dict]:
|
||||
"""Consume operator task outcomes into the Experience Ledger (Runtime
|
||||
Reachability P0, 2026-09-10).
|
||||
|
||||
The operator's send_task persists every task in the TaskStateRepository
|
||||
checkpoint (task_state.json colocated with operator.json) and emits a
|
||||
durable task.outcome journal event per terminal transition. This
|
||||
consumer reads the CHECKPOINT (the authoritative task store — the
|
||||
journal is a one-way integrity journal: payload_hash only, no payload)
|
||||
and appends a SEED experience record for every terminal task not yet
|
||||
recorded (matched by evidence task_id — idempotent).
|
||||
|
||||
Attribution stays UNKNOWN: attribution is PL1's job, not the ledger's.
|
||||
Statuses stay SEED/CANDIDATE — never auto-VALIDATED.
|
||||
"""
|
||||
path = task_state_path or os.path.expanduser("~/.rune/hot/task_state.json")
|
||||
ledger = ExperienceLedger(
|
||||
ledger_path
|
||||
or os.environ.get("RUNE_EXPERIENCE_LEDGER", os.path.expanduser("~/.rune/state/process_experience.jsonl"))
|
||||
)
|
||||
with open(path) as f:
|
||||
checkpoint = json.load(f)
|
||||
tasks = checkpoint.get("tasks", [])
|
||||
seen_tasks = set()
|
||||
for rec in ledger.load_all():
|
||||
for ev in rec.get("evidence_references", []):
|
||||
if ev.startswith("task:"):
|
||||
seen_tasks.add(ev.split(":", 1)[1])
|
||||
recorded = []
|
||||
for t in tasks:
|
||||
status = t.get("status", "")
|
||||
if status not in ("completed", "failed"):
|
||||
continue
|
||||
# research_required tasks: routing disposition, NOT failure evidence
|
||||
# (handoff §2/§C). The dispatch lives in route.jsonl; recording it
|
||||
# as FAILURE would poison PL0 with false failure-learning pressure.
|
||||
if t.get("outcome_ref") == "research_dispatch" or t.get("result") == "RESEARCH_REQUIRED":
|
||||
continue
|
||||
task_id = t.get("task_id", "")
|
||||
if not task_id or task_id in seen_tasks:
|
||||
continue
|
||||
result_type = "SUCCESS" if status == "completed" else "FAILURE"
|
||||
rec = ledger.record(
|
||||
task_run_stage=f"operator:{status}",
|
||||
result_type=result_type,
|
||||
symptom=(t.get("title") or "")[:200],
|
||||
evidence=[f"task:{task_id}", f"model:{t.get('assigned_model')}", f"outcome_ref:{t.get('outcome_ref')}"],
|
||||
root_cause="UNKNOWN",
|
||||
confidence=0.3,
|
||||
pipeline_sha="operator",
|
||||
reusable=False,
|
||||
candidate_status="SEED",
|
||||
)
|
||||
recorded.append(rec)
|
||||
return recorded
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover — manual consumer invocation
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Record operator task outcomes as experience evidence")
|
||||
ap.add_argument("--task-state", default=None)
|
||||
ap.add_argument("--ledger", default=None)
|
||||
args = ap.parse_args()
|
||||
recs = record_operator_outcomes(args.task_state, args.ledger)
|
||||
print(f"recorded {len(recs)} experience records")
|
||||
|
||||
|
||||
# PL0 qualification (handoff 2026-09-10 §9): raw outcomes are preserved
|
||||
# verbatim, but only QUALIFIED records exert learning pressure in PL1.
|
||||
# The distinction is cheap and rule-based — no model call.
|
||||
CANDIDATE_STATUSES = {"SEED", "CANDIDATE", "EVALUATED", "PROMOTED", "REJECTED"}
|
||||
|
||||
# Task titles that are outcome telemetry, not learning material: trivial
|
||||
# greetings, arithmetic/canary probes, bootstrap self-checks.
|
||||
_TELEMETRY_TITLE_PATTERNS = (
|
||||
"hello",
|
||||
"how are you",
|
||||
"hi ",
|
||||
"thanks",
|
||||
"thank you",
|
||||
"reply with exactly",
|
||||
"what is ",
|
||||
"check",
|
||||
"status",
|
||||
)
|
||||
|
||||
|
||||
def qualify_experience(record: dict) -> str:
|
||||
"""Classify a raw outcome record: TELEMETRY vs CANDIDATE.
|
||||
|
||||
TELEMETRY = simple successful conversation / canary probes — kept as
|
||||
raw outcome evidence, excluded from PL1 evaluation pressure.
|
||||
CANDIDATE = failures, verification failures, retries, corrections,
|
||||
inefficiencies, architectural omissions, reusable procedures, route
|
||||
misclassifications — the material PL1 evaluates.
|
||||
"""
|
||||
if record.get("result_type") in ("FAILURE", "INEFFICIENCY"):
|
||||
return "CANDIDATE"
|
||||
symptom = (record.get("symptom") or "").lower()
|
||||
if any(p in symptom for p in _TELEMETRY_TITLE_PATTERNS):
|
||||
return "TELEMETRY"
|
||||
# Simple short successes with no verification/failure signal are
|
||||
# telemetry; anything substantive defaults to CANDIDATE (PL1 decides).
|
||||
if len(symptom) < 60 and not record.get("quality_outcome"):
|
||||
return "TELEMETRY"
|
||||
return "CANDIDATE"
|
||||
|
||||
|
||||
def qualified_candidates(ledger_path: str | None = None) -> list[dict]:
|
||||
"""All CANDIDATE-qualified records — the PL1 evaluation input."""
|
||||
ledger = ExperienceLedger(
|
||||
ledger_path
|
||||
or os.environ.get("RUNE_EXPERIENCE_LEDGER", os.path.expanduser("~/.rune/state/process_experience.jsonl"))
|
||||
)
|
||||
return [r for r in ledger.load_all() if qualify_experience(r) == "CANDIDATE"]
|
||||
@@ -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))
|
||||
+664
-407
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""tui_startup_canary.py — REAL TUI initialization canary, two proof modes.
|
||||
|
||||
Mode CANDIDATE (pre-promotion, default when --stage is given):
|
||||
Tests the STAGED candidate exactly as supplied. Nothing from
|
||||
/mnt/workdrive/rune-code/current is prepended or preferred — sys.path is
|
||||
scrubbed of any current-deployment entry so a broken candidate cannot be
|
||||
masked by the healthy previous deployment. If the staged candidate is
|
||||
broken while current is healthy, this canary MUST fail.
|
||||
|
||||
Mode CURRENT (post-promotion consuming-path proof, --mode current):
|
||||
Tests /mnt/workdrive/rune-code/current/src — proving the promoted
|
||||
artifact is what the real consuming path resolves to.
|
||||
|
||||
Both modes run the ACTUAL RuneApp initialization through Textual's own
|
||||
test driver — the same app construction + on_mount wiring the interactive
|
||||
TUI executes — then exit 0. The interactive loop is never entered.
|
||||
|
||||
Prints the build identity (mode + identity) on success. Exit non-zero on
|
||||
any initialization failure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CURRENT_SRC = "/mnt/workdrive/rune-code/current/src"
|
||||
|
||||
|
||||
def _scrub_current_from_path() -> None:
|
||||
"""Remove every current-deployment entry from sys.path (candidate mode).
|
||||
|
||||
The runner's checkout workspace or ambient PYTHONPATH may contain the
|
||||
current deployment; a candidate canary must not silently resolve rune
|
||||
from it.
|
||||
"""
|
||||
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.abspath(CURRENT_SRC)]
|
||||
# Also drop any already-imported rune modules from a prior import.
|
||||
for mod in [m for m in sys.modules if m == "rune" or m.startswith("rune.")]:
|
||||
del sys.modules[mod]
|
||||
|
||||
|
||||
async def _canary() -> dict:
|
||||
from textual.widgets import Static # noqa: F401 — imports must resolve
|
||||
|
||||
from rune.rune_tui import RuneApp
|
||||
|
||||
app = RuneApp()
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
# on_mount has run; the channels/status bar are wired.
|
||||
assert app.task_channel is not None, "task channel not initialized"
|
||||
assert app.chat_channel is not None, "chat channel not initialized"
|
||||
assert app.status_bar is not None, "status bar not initialized"
|
||||
# One interaction cycle: let pending messages/refreshes settle.
|
||||
await pilot.pause(0.1)
|
||||
import rune
|
||||
|
||||
return rune.build_info()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Rune TUI startup canary")
|
||||
ap.add_argument(
|
||||
"--mode",
|
||||
choices=("candidate", "current"),
|
||||
default="candidate",
|
||||
help="candidate: test the staged tree (sys.path scrubbed of current); current: test the consuming path",
|
||||
)
|
||||
ap.add_argument("--stage", help="staged candidate root (candidate mode)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.mode == "candidate":
|
||||
if not args.stage:
|
||||
print("candidate mode requires --stage", file=sys.stderr)
|
||||
return 2
|
||||
stage_root = Path(args.stage).resolve()
|
||||
stage_src = str(stage_root / "src")
|
||||
if not (stage_root / "src" / "rune").is_dir():
|
||||
print(f"staged candidate has no src/rune: {stage_src}", file=sys.stderr)
|
||||
return 2
|
||||
_scrub_current_from_path()
|
||||
sys.path.insert(0, stage_src)
|
||||
# Verify the scrub worked: rune must resolve from the STAGE, not current.
|
||||
import rune # noqa: E402
|
||||
|
||||
resolved = getattr(rune, "__file__", "")
|
||||
if "rune-code/current" in resolved:
|
||||
print(
|
||||
f"CANARY CONTAMINATED: rune imported from current, not the staged candidate ({resolved})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
else:
|
||||
if CURRENT_SRC not in sys.path:
|
||||
sys.path.insert(0, CURRENT_SRC)
|
||||
|
||||
async def _run() -> dict:
|
||||
try:
|
||||
return await _canary()
|
||||
finally:
|
||||
pass
|
||||
|
||||
try:
|
||||
info = asyncio.run(_run())
|
||||
except Exception as exc: # noqa: BLE001 — the canary's JOB is to catch any startup failure and report it as a failed gate; narrowing here would let an unexpected exception class crash the runner instead of failing the gate cleanly.
|
||||
print(f"TUI STARTUP CANARY FAILED ({args.mode}): {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(json.dumps({"mode": args.mode, **info}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -81,6 +81,19 @@ def subjects_since_tag(tag: str, repo: Path | None = None) -> list[str]:
|
||||
return out.splitlines() if out else []
|
||||
|
||||
|
||||
def _version_lt(a: str, b: str) -> bool:
|
||||
"""True when semver string a < b. Numeric per segment — never lexicographic.
|
||||
|
||||
T15 Part F: '0.66.9' < '0.66.10' is False lexicographically ('9' > '1'),
|
||||
which made a legitimately-needed patch bump read as the
|
||||
version_ahead_of_target anomaly and silently skip the release
|
||||
(observed live on run 6781, 2026-09-11 19:59).
|
||||
"""
|
||||
pa = [int(x) for x in a.split(".")]
|
||||
pb = [int(x) for x in b.split(".")]
|
||||
return pa < pb
|
||||
|
||||
|
||||
def classify_subject(subject: str) -> str:
|
||||
"""Classify one commit subject: ``breaking`` | ``feat`` | ``fix`` | ``other``.
|
||||
|
||||
@@ -114,6 +127,18 @@ def highest_warranted_bump(subjects: list[str]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def is_release_commit(subject: str) -> bool:
|
||||
"""True when the commit is a release/version bump commit.
|
||||
|
||||
ONE SOURCE OF TRUTH consumed by auto-version.yml (push convergence
|
||||
guard) and by the recursion regression test: a release commit must
|
||||
NEVER trigger another version bump (recursive-loop defect observed
|
||||
2026-09-11: five 0.66.2 bumps before the guard landed).
|
||||
"""
|
||||
s = subject.strip().lower()
|
||||
return s.startswith("chore(release)") or s.startswith("chore: release")
|
||||
|
||||
|
||||
def next_version(version: str, kind: str) -> str:
|
||||
"""Apply a bump kind to a version string (0.x convention: breaking -> minor)."""
|
||||
major, minor, patch = version.split(".")
|
||||
@@ -187,11 +212,236 @@ def decision_in_repo(repo: Path | None = None) -> dict[str, Any]:
|
||||
return assemble_release_decision(version, tag, subjects, len(subjects))
|
||||
|
||||
|
||||
def last_release_boundary(repo: Path | None = None) -> str | None:
|
||||
"""Find the last chore(release) commit SHA (the release boundary).
|
||||
|
||||
This is the correct convergence boundary — NOT the git tag (tags may
|
||||
lag behind releases due to D7). Falls back to the latest tag if no
|
||||
release commits exist.
|
||||
|
||||
T15 Part F: matches SUBJECTS only (--grep matches the commit BODY by
|
||||
default; a fix commit whose body mentions 'chore(release)' was
|
||||
misdetected as the boundary, swallowing its own warranted bump).
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%H %s", "-1", "--grep=^chore\\(release\\)", "--extended-regexp"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo) if repo else None,
|
||||
check=True,
|
||||
)
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
sha, _, subject = line.partition(" ")
|
||||
if is_release_commit(subject):
|
||||
return sha
|
||||
except (subprocess.CalledProcessError, IndexError):
|
||||
pass
|
||||
return latest_tag(repo) # fallback to tag
|
||||
|
||||
|
||||
def release_convergence(
|
||||
repo: Path | None = None,
|
||||
*,
|
||||
_version: str | None = None,
|
||||
_boundary_subject: str | None = None,
|
||||
_subjects: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Correct release convergence: compare current source version to the
|
||||
warranted TARGET version computed from commits since the last release
|
||||
boundary (the last chore(release) commit, NOT the tag — tags may lag
|
||||
due to D7).
|
||||
|
||||
Invariant:
|
||||
current == target → converged, no commit
|
||||
current < target → create exactly one bump commit
|
||||
current > target → anomaly
|
||||
"""
|
||||
current_version = _version or read_version(repo)
|
||||
if not current_version:
|
||||
return {
|
||||
"converged": True,
|
||||
"warranted_bump": None,
|
||||
"anomaly": None,
|
||||
"commits_since_tag": 0,
|
||||
"sample": [],
|
||||
"reason": "no readable version",
|
||||
}
|
||||
|
||||
subjects: list[str] = []
|
||||
boundary = None
|
||||
if _subjects is not None:
|
||||
# Test injection: skip git entirely
|
||||
substantive = [s for s in _subjects if not is_release_commit(s)]
|
||||
warranted = highest_warranted_bump(substantive)
|
||||
if not warranted:
|
||||
return {
|
||||
"converged": True,
|
||||
"warranted_bump": None,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": current_version,
|
||||
"reason": "no release-worthy commits since last boundary",
|
||||
"boundary_sha": "",
|
||||
}
|
||||
target_version = next_version(current_version, warranted)
|
||||
if current_version == target_version:
|
||||
return {
|
||||
"converged": True,
|
||||
"warranted_bump": warranted,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": "current version already equals warranted target",
|
||||
"boundary_sha": "",
|
||||
}
|
||||
elif _version_lt(current_version, target_version):
|
||||
return {
|
||||
"converged": False,
|
||||
"warranted_bump": warranted,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": f"bump required: {current_version} → {target_version}",
|
||||
"boundary_sha": "",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"converged": False,
|
||||
"warranted_bump": None,
|
||||
"anomaly": "version_ahead_of_target",
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": f"ANOMALY: current {current_version} > target {target_version}",
|
||||
"boundary_sha": "",
|
||||
}
|
||||
if _boundary_subject:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%s", f"{boundary}..HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo) if repo else None,
|
||||
check=True,
|
||||
)
|
||||
subjects = [s for s in result.stdout.splitlines() if s.strip()]
|
||||
else:
|
||||
# Production path: converge over commits since the last release
|
||||
# boundary. T15 Part F root cause: `boundary` was initialized to
|
||||
# None and never assigned — every run classified ALL commits as
|
||||
# unreleased (959+), so the warranted bump never converged and
|
||||
# every queued push wrote its own bump commit (5× 0.66.9 observed
|
||||
# 2026-09-11 20:21). Assign the boundary from the one source of
|
||||
# truth and range the log over it.
|
||||
boundary = last_release_boundary(repo)
|
||||
if boundary:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%s", f"{boundary}..HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo) if repo else None,
|
||||
check=True,
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%s"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo) if repo else None,
|
||||
check=True,
|
||||
)
|
||||
subjects = [s for s in result.stdout.splitlines() if s.strip()]
|
||||
|
||||
# Exclude chore(release) commits from bump classification (they ARE releases)
|
||||
substantive = [s for s in subjects if not is_release_commit(s)]
|
||||
warranted = highest_warranted_bump(substantive)
|
||||
# Expose the boundary in the decision JSON: consumers (release step,
|
||||
# debug sessions) otherwise re-derive it from git history.
|
||||
decision_boundary = {"boundary_sha": (boundary or "")[:12]}
|
||||
|
||||
if not warranted:
|
||||
return {
|
||||
"converged": True,
|
||||
"warranted_bump": None,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": current_version,
|
||||
"reason": "no release-worthy commits since last boundary",
|
||||
**decision_boundary,
|
||||
}
|
||||
|
||||
target_version = next_version(current_version, warranted)
|
||||
|
||||
if current_version == target_version:
|
||||
return {
|
||||
"converged": True,
|
||||
"warranted_bump": warranted,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": "current version already equals warranted target",
|
||||
**decision_boundary,
|
||||
}
|
||||
elif _version_lt(current_version, target_version):
|
||||
# Bump needed — but only if not already converged by a prior bump
|
||||
# in this same cascade (the target is the SAME for all queued runs)
|
||||
return {
|
||||
"converged": False,
|
||||
"warranted_bump": warranted,
|
||||
"anomaly": None,
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": f"bump required: {current_version} → {target_version}",
|
||||
**decision_boundary,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"converged": False,
|
||||
"warranted_bump": None,
|
||||
"anomaly": "version_ahead_of_target",
|
||||
"commits_since_boundary": len(substantive),
|
||||
"sample": substantive[:5],
|
||||
"current_version": current_version,
|
||||
"target_version": target_version,
|
||||
"reason": f"ANOMALY: current {current_version} > target {target_version}",
|
||||
**decision_boundary,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> dict[str, Any]:
|
||||
parser = argparse.ArgumentParser(description="Release convergence decision (JSON on stdout)")
|
||||
parser.add_argument(
|
||||
"--is-release-subject",
|
||||
dest="is_release_subject",
|
||||
metavar="SUBJECT",
|
||||
help="exit 0 when SUBJECT is a release bump commit, exit 1 otherwise (loop guard); no JSON",
|
||||
)
|
||||
parser.add_argument("--repo", type=Path, default=None, help="repo root (default: cwd)")
|
||||
args = parser.parse_args(argv)
|
||||
decision = decision_in_repo(args.repo)
|
||||
if args.is_release_subject is not None:
|
||||
import sys as _sys
|
||||
|
||||
_sys.exit(0 if is_release_commit(args.is_release_subject) else 1)
|
||||
decision = release_convergence(args.repo)
|
||||
# Backward compat: old consumers expect commits_since_tag
|
||||
if "commits_since_tag" not in decision:
|
||||
decision["commits_since_tag"] = decision.get("commits_since_boundary", 0)
|
||||
print(json.dumps(decision))
|
||||
return decision
|
||||
|
||||
|
||||
+35
-1
@@ -3,4 +3,38 @@
|
||||
This is the main entry point for the Rune program.
|
||||
"""
|
||||
|
||||
__version__ = "0.66.0"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.14"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
# qualified SHA) is written by the deployment job into _build_identity.json
|
||||
# next to the package. A dev checkout has no identity file — build_info()
|
||||
# reports "dev" honestly rather than guessing.
|
||||
|
||||
_BUILD_IDENTITY_FILE = Path(__file__).with_name("_build_identity.json")
|
||||
|
||||
|
||||
def build_info() -> dict:
|
||||
"""Build identity of the RUNNING code: version + exact commit SHA.
|
||||
|
||||
Returns:
|
||||
{"version": ..., "commit": ..., "deployed_at": ..., "source":
|
||||
"deployed" | "dev"}
|
||||
|
||||
"deployed" = this tree was staged by the deployment pipeline from a
|
||||
CI-qualified master revision. "dev" = a development checkout (no
|
||||
identity file) — the SHA is unknown at runtime, reported honestly.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(_BUILD_IDENTITY_FILE.read_text())
|
||||
return {
|
||||
"version": data.get("version", __version__),
|
||||
"commit": data.get("commit", "unknown"),
|
||||
"deployed_at": data.get("deployed_at", ""),
|
||||
"source": "deployed",
|
||||
}
|
||||
except (OSError, ValueError):
|
||||
return {"version": __version__, "commit": "dev", "deployed_at": "", "source": "dev"}
|
||||
+57
-42
@@ -50,6 +50,7 @@ def _now():
|
||||
def connect():
|
||||
"""Open/create the abstraction database."""
|
||||
import sqlite3
|
||||
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
@@ -178,22 +179,24 @@ def llm_call(prompt, model=None, max_tokens=500):
|
||||
model = model or LOCAL_MODEL
|
||||
try:
|
||||
import urllib.request
|
||||
data = json.dumps({
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192}
|
||||
}).encode()
|
||||
|
||||
data = json.dumps(
|
||||
{
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192},
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_URL}/api/generate",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
f"{OLLAMA_URL}/api/generate", data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return result.get("response", "").strip()
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.getLogger("abstraction").warning("LLM call failed: %s", e)
|
||||
return ""
|
||||
|
||||
@@ -263,8 +266,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
# Check for duplicates — hash-based (O(1) lookup instead of LIKE scan)
|
||||
content_hash = hashlib.sha256(what[:80].lower().encode()).hexdigest()[:16]
|
||||
existing = db.execute(
|
||||
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1",
|
||||
(content_hash,)
|
||||
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1", (content_hash,)
|
||||
).fetchone()
|
||||
if existing:
|
||||
continue # NOOP — already exists
|
||||
@@ -273,12 +275,20 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
"""INSERT INTO facts (episode_id, what, content_hash, when_text, where_text, who_text, why_text,
|
||||
fact_type, entities_json, confidence, source, state, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)""",
|
||||
(episode_id or "", what, content_hash,
|
||||
fact.get("when", ""), fact.get("where", ""),
|
||||
fact.get("who", ""), fact.get("why", ""),
|
||||
fact.get("type", "world"),
|
||||
json.dumps(fact.get("entities", [])),
|
||||
0.7, source, ts)
|
||||
(
|
||||
episode_id or "",
|
||||
what,
|
||||
content_hash,
|
||||
fact.get("when", ""),
|
||||
fact.get("where", ""),
|
||||
fact.get("who", ""),
|
||||
fact.get("why", ""),
|
||||
fact.get("type", "world"),
|
||||
json.dumps(fact.get("entities", [])),
|
||||
0.7,
|
||||
source,
|
||||
ts,
|
||||
),
|
||||
)
|
||||
fact_id = cur.lastrowid
|
||||
extracted += 1
|
||||
@@ -291,16 +301,15 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
# Upsert entity
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO entities (name, entity_type, created_at) VALUES (?, 'concept', ?)",
|
||||
(entity_name, ts)
|
||||
(entity_name, ts),
|
||||
)
|
||||
entity_row = db.execute("SELECT id FROM entities WHERE name=?", (entity_name,)).fetchone()
|
||||
if entity_row:
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO fact_entities (fact_id, entity_id) VALUES (?, ?)",
|
||||
(fact_id, entity_row["id"])
|
||||
(fact_id, entity_row["id"]),
|
||||
)
|
||||
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?",
|
||||
(entity_row["id"],))
|
||||
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?", (entity_row["id"],))
|
||||
|
||||
# Triple extraction (HippoRAG 2 pattern)
|
||||
who = fact.get("who", "").strip()
|
||||
@@ -312,7 +321,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
obj = " ".join(words[3:])
|
||||
db.execute(
|
||||
"INSERT INTO triples (subject, predicate, object, source_fact_id, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(who, predicate, obj, fact_id, 0.6, ts)
|
||||
(who, predicate, obj, fact_id, 0.6, ts),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
@@ -335,17 +344,24 @@ def synthesize_observations(entity_name):
|
||||
entity_id = entity["id"]
|
||||
|
||||
# Get all facts about this entity
|
||||
facts = db.execute("""
|
||||
facts = db.execute(
|
||||
"""
|
||||
SELECT f.id, f.what, f.fact_type, f.confidence
|
||||
FROM facts f
|
||||
JOIN fact_entities fe ON f.id = fe.fact_id
|
||||
WHERE fe.entity_id = ? AND f.state = 'active'
|
||||
ORDER BY f.created_at DESC
|
||||
""", (entity_id,)).fetchall()
|
||||
""",
|
||||
(entity_id,),
|
||||
).fetchall()
|
||||
|
||||
if len(facts) < 3:
|
||||
return {"entity": entity_name, "facts": len(facts), "observations": 0,
|
||||
"reason": "Need at least 3 facts to synthesize"}
|
||||
return {
|
||||
"entity": entity_name,
|
||||
"facts": len(facts),
|
||||
"observations": 0,
|
||||
"reason": "Need at least 3 facts to synthesize",
|
||||
}
|
||||
|
||||
facts_text = "\n".join(f"- [{f['fact_type']}] {f['what']}" for f in facts[:20])
|
||||
|
||||
@@ -377,12 +393,11 @@ Respond as JSON array:
|
||||
db.execute(
|
||||
"""INSERT INTO observations (entity_id, observation, source_fact_ids, confidence, state, created_at)
|
||||
VALUES (?, ?, ?, ?, 'active', ?)""",
|
||||
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts)
|
||||
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts),
|
||||
)
|
||||
created += 1
|
||||
|
||||
db.execute("UPDATE entities SET observation_count = ? WHERE id=?",
|
||||
(created, entity_id))
|
||||
db.execute("UPDATE entities SET observation_count = ? WHERE id=?", (created, entity_id))
|
||||
db.commit()
|
||||
return {"entity": entity_name, "facts": len(facts), "observations": created}
|
||||
finally:
|
||||
@@ -394,8 +409,7 @@ def list_beliefs(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -407,8 +421,7 @@ def list_rules(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -420,8 +433,7 @@ def list_skills(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -486,8 +498,9 @@ def handle_hook(event, data=None):
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Abstraction pipeline")
|
||||
ap.add_argument("cmd", choices=["extract", "observe", "beliefs", "rules", "skills",
|
||||
"evolve", "lifecycle", "stats", "hook"])
|
||||
ap.add_argument(
|
||||
"cmd", choices=["extract", "observe", "beliefs", "rules", "skills", "evolve", "lifecycle", "stats", "hook"]
|
||||
)
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
ap.add_argument("--episode-id", default=None)
|
||||
ap.add_argument("--source", default="session")
|
||||
@@ -501,16 +514,18 @@ def main():
|
||||
print("usage: abstraction.py extract '<episode text>'")
|
||||
return
|
||||
result = extract_facts(args.arg, episode_id=args.episode_id, source=args.source)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Extracted {result.get('extracted', 0)} facts")
|
||||
print(json.dumps(result, indent=2) if args.json else f"Extracted {result.get('extracted', 0)} facts")
|
||||
|
||||
elif args.cmd == "observe":
|
||||
if not args.arg:
|
||||
print("usage: abstraction.py observe '<entity name>'")
|
||||
return
|
||||
result = synthesize_observations(args.arg)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts")
|
||||
print(
|
||||
json.dumps(result, indent=2)
|
||||
if args.json
|
||||
else f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts"
|
||||
)
|
||||
|
||||
elif args.cmd == "beliefs":
|
||||
items = list_beliefs()
|
||||
@@ -568,5 +583,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["abstraction"],
|
||||
"priority": 50,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
@@ -44,32 +44,19 @@ class FloorViolation(Exception):
|
||||
"""A downgrade was attempted on a route protected by the research floor."""
|
||||
|
||||
|
||||
# Flag detection: resolved relative to THIS file (legacy tests load this
|
||||
# module standalone via importlib, with no package context on sys.path).
|
||||
def _load_detect_flags():
|
||||
try:
|
||||
from research_risk_flags import detect_flags
|
||||
|
||||
return detect_flags
|
||||
except ModuleNotFoundError:
|
||||
import importlib.util as _ilu
|
||||
|
||||
_spec = _ilu.spec_from_file_location(
|
||||
"research_risk_flags", os.path.join(os.path.dirname(os.path.abspath(__file__)), "research_risk_flags.py")
|
||||
)
|
||||
_mod = _ilu.module_from_spec(_spec)
|
||||
sys.modules["research_risk_flags"] = _mod
|
||||
_spec.loader.exec_module(_mod)
|
||||
return _mod.detect_flags
|
||||
|
||||
|
||||
_detect_flags = _load_detect_flags()
|
||||
# Flag detection: canonical sibling within Rune Core. Dual-import guard
|
||||
# kept for standalone script execution (research executor runs as a bare
|
||||
# script with src/ bootstrapped onto sys.path).
|
||||
try:
|
||||
from .route_risk_flags import detect_flags
|
||||
except ImportError: # standalone script execution
|
||||
from rune.route_risk_flags import detect_flags
|
||||
|
||||
|
||||
def body_has_floor_trigger(body_lower: str) -> bool:
|
||||
"""Public floor predicate over raw task text (used by dispatch routing
|
||||
before classification, e.g. to decide probe eligibility)."""
|
||||
risk, uncertainty = _detect_flags(body_lower)
|
||||
risk, uncertainty = detect_flags(body_lower)
|
||||
return _floor_applies_from_flags(risk, uncertainty, body_lower)
|
||||
|
||||
|
||||
@@ -161,7 +148,7 @@ def classify_route(
|
||||
prov.update(sufficiency.provenance)
|
||||
|
||||
# Flag detection stays for floor evaluation, but is no longer the router.
|
||||
risk, uncertainty = _detect_flags(body_lower)
|
||||
risk, uncertainty = detect_flags(body_lower)
|
||||
|
||||
unknowns = sufficiency.unknowns()
|
||||
if sufficiency.provenance.get("legacy_inputs"):
|
||||
@@ -174,7 +161,26 @@ def classify_route(
|
||||
reasons.append("escalating per doctrine: never manufacture certainty")
|
||||
|
||||
local_conditions = [has_local_repro, owning_authority_known, testable_locally, narrow_and_reversible]
|
||||
if all(local_conditions) and not risk and not uncertainty and not unknowns and not floor:
|
||||
# LOCAL_FIX additionally requires defect language (epic #284 doctrine:
|
||||
# "reproducible local DEFECT"). A clear non-defect change with the same
|
||||
# evidence posture is STANDARD_CHANGE — it is more than a trivial fix.
|
||||
task_language = ""
|
||||
if sufficiency is not None:
|
||||
task_language = sufficiency.provenance.get("task_class_language", "")
|
||||
# Defect detection: provenance (production path) or direct body scan
|
||||
# (legacy compat path, whose shim carries no provenance).
|
||||
is_defect_route = task_language == "defect" or any(
|
||||
w in body_lower
|
||||
for w in ("fix", "bug", "broken", "regression", "wrong", "typo", "fails", "failing", "crash", "defect")
|
||||
)
|
||||
# Verifiability NONE (no test surface named in text) does not block
|
||||
# LOCAL for defect-class narrow tasks: a self-evident defect fix is
|
||||
# verifiable by inspection + existing suite. UNKNOWN would block
|
||||
# (could not determine); NONE is a determined result.
|
||||
local_ok = all(local_conditions[:2]) and (
|
||||
local_conditions[2] or (local_conditions[3] and sufficiency is not None and sufficiency.verifiability == "NONE")
|
||||
)
|
||||
if local_ok and is_defect_route and not risk and not uncertainty and not unknowns and not floor:
|
||||
reasons.append("all LOCAL_FIX conditions met; no unknowns; no floor")
|
||||
return _decision(
|
||||
"LOCAL_FIX",
|
||||
@@ -417,10 +423,203 @@ def _event(kind: str, d: RouteDecision) -> dict:
|
||||
return ev
|
||||
|
||||
|
||||
def record_route_decision(decision: RouteDecision, path: str) -> None:
|
||||
"""Persist the INITIAL_ROUTE event (append-only route history)."""
|
||||
record_transition("INITIAL_ROUTE", decision, path)
|
||||
def record_route_decision(decision: RouteDecision, path: str, dispatch: dict | None = None) -> None:
|
||||
"""Persist the INITIAL_ROUTE event (append-only route history).
|
||||
|
||||
For RESEARCHED_CHANGE routes, callers attach a `dispatch` payload —
|
||||
an actionable, executor-consumable handoff (issue-shaped input for
|
||||
the canonical research executor's event/CLI path). The dispatch is
|
||||
recorded WITH the route so the audit trail shows what the downstream
|
||||
consumer was asked to do, not just the classification.
|
||||
"""
|
||||
_pending_dispatch = dict(dispatch) if dispatch else None
|
||||
_record_transition_with("INITIAL_ROUTE", decision, path, _pending_dispatch)
|
||||
|
||||
|
||||
def _record_transition_with(kind: str, decision: RouteDecision, path: str, dispatch: dict | None) -> None:
|
||||
ev = _event(kind, decision)
|
||||
if dispatch:
|
||||
ev["dispatch"] = dispatch
|
||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||
with open(path, "a") as f:
|
||||
f.write(json.dumps(ev) + "\n")
|
||||
|
||||
|
||||
def build_research_dispatch(task_id: str, description: str, decision: RouteDecision) -> dict:
|
||||
"""Actionable research dispatch for a RESEARCHED_CHANGE route.
|
||||
|
||||
The payload is executor-consumable: it names the entry contract the
|
||||
canonical research executor accepts (issue-shaped title/body +
|
||||
sufficiency provenance + floor reason) so the downstream consumer —
|
||||
currently the MiMo session driving the executor CLI (documented gap,
|
||||
§11) — can dispatch without re-deriving intent.
|
||||
"""
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"title": description[:200],
|
||||
"body": description,
|
||||
"route": decision.route,
|
||||
"floor": decision.floor,
|
||||
"reasons": decision.reasons,
|
||||
"unknowns": decision.unknowns,
|
||||
"provenance": decision.sufficiency.provenance if decision.sufficiency else {},
|
||||
"executor_entry": "research_stage_executor.py --stage ISSUE (with issue-shaped input)",
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def derive_sufficiency(
|
||||
title: str, body: str, evidence_classes: list | None = None, ambiguous: bool = False, contract_compiler=None
|
||||
) -> SufficiencyInputs:
|
||||
"""Derive evidence-sufficiency inputs from OBSERVABLE task facts (Option C
|
||||
unified authority, 2026-09-10). Deterministic, no model call. Every field
|
||||
carries provenance; anything not deterministically establishable is
|
||||
UNKNOWN. Shared by the research executor AND the live operator path —
|
||||
one canonical implementation, not copied logic."""
|
||||
import re as _re
|
||||
|
||||
full = (title or "") + "\n" + (body or "")
|
||||
lower = full.lower()
|
||||
|
||||
# Optional contract compiler (research-pipeline world): when supplied,
|
||||
# evidence_classes come from the deterministic ISSUE contract instead of
|
||||
# the caller's assertion. Core never imports the compiler itself —
|
||||
# dependency direction is executor→core, never core→executor.
|
||||
if contract_compiler is not None:
|
||||
try:
|
||||
contract = contract_compiler({"title": title or "", "body": body or "", "labels": []})
|
||||
evidence_classes = contract.get("evidence_classes", evidence_classes)
|
||||
ambiguous = contract.get("ambiguous", ambiguous)
|
||||
except (ValueError, TypeError, KeyError, AttributeError):
|
||||
# A broken compiler must not poison routing — fall back to the
|
||||
# caller's evidence_classes (already the conservative default).
|
||||
pass # noqa: S110 — intentional fallback, documented above
|
||||
|
||||
# Mechanism-uncertainty language: questions about how the system BEHAVES
|
||||
# under conditions (performance, timing, concurrency) require measurement,
|
||||
# not source reading. Local code location does not establish the answer.
|
||||
mechanism_uncertain = any(
|
||||
w in lower
|
||||
for w in (
|
||||
"performance",
|
||||
"throughput",
|
||||
"latency",
|
||||
"race condition",
|
||||
"timing",
|
||||
"under load",
|
||||
"concurrency",
|
||||
"deadlock",
|
||||
"memory usage",
|
||||
"benchmark needed",
|
||||
"measure",
|
||||
)
|
||||
)
|
||||
|
||||
# No test-surface references = NONE (determined: "no test surface
|
||||
# exists"), not UNKNOWN. UNKNOWN is reserved for "could not determine".
|
||||
# NONE does not force escalation — it weighs in the local-conditions
|
||||
# check via testable_locally=False.
|
||||
verifiability = (
|
||||
"TESTABLE"
|
||||
if _re.search(
|
||||
r"(tests/[\w/.\-]+|test_[\w/.\-]+\.py|pytest|unittest|"
|
||||
r"run (the )?tests)",
|
||||
body or "",
|
||||
_re.IGNORECASE,
|
||||
)
|
||||
else "NONE"
|
||||
)
|
||||
if mechanism_uncertain and verifiability == "TESTABLE":
|
||||
verifiability = "MEASUREMENT_REQUIRED"
|
||||
|
||||
external = _detect_external_refs(body or "")
|
||||
scope_novel = "NEW_SUBSYSTEM" if any(m in (body or "").lower() for m in FLOOR_EXPLICIT_MARKERS) else "KNOWN_PATTERN"
|
||||
# LOCAL AUTHORITY grounding (Option C unified semantics):
|
||||
# - With a contract compiler (research path): evidence_classes == ["repo"].
|
||||
# - Without one (operator path): the ABSENCE of grounding-defeaters —
|
||||
# no external refs, no measurement language, no novelty markers —
|
||||
# IS the grounding evidence. The task text itself is the authority.
|
||||
if evidence_classes is not None:
|
||||
grounded = evidence_classes == ["repo"] and not mechanism_uncertain
|
||||
else:
|
||||
grounded = external == "NONE" and not mechanism_uncertain and scope_novel == "KNOWN_PATTERN"
|
||||
# LOCAL_FIX vs STANDARD_CHANGE discriminator (epic #284 doctrine):
|
||||
# LOCAL_FIX = reproducible local DEFECT (fix/bug/broken/regression
|
||||
# language); STANDARD_CHANGE = clear change, more than trivial
|
||||
# (add/implement/extend/refactor language). Observable, deterministic.
|
||||
is_defect = any(
|
||||
w in lower
|
||||
for w in ("fix", "bug", "broken", "regression", "wrong", "typo", "fails", "failing", "crash", "defect")
|
||||
)
|
||||
is_change = any(
|
||||
w in lower for w in ("add ", "implement", "extend", "refactor", "introduce", "support for", "new feature")
|
||||
)
|
||||
# Handoff §10: the ACTION VERB governs mutation-class, not object words.
|
||||
# "inspect the failing test" requests reading, even though "failing"
|
||||
# appears. A leading inspection verb makes the task read-only.
|
||||
first_words = lower.split()[:3]
|
||||
if any(
|
||||
w
|
||||
in (
|
||||
"inspect",
|
||||
"read",
|
||||
"review",
|
||||
"analyze",
|
||||
"analyse",
|
||||
"explain",
|
||||
"look",
|
||||
"list",
|
||||
"show",
|
||||
"describe",
|
||||
"check",
|
||||
"what",
|
||||
"why",
|
||||
"how",
|
||||
"where",
|
||||
"which",
|
||||
)
|
||||
for w in first_words
|
||||
):
|
||||
is_defect = False
|
||||
is_change = False
|
||||
return SufficiencyInputs(
|
||||
local_authority=("GROUNDED" if grounded else "NOT_GROUNDED"),
|
||||
external_dependency=external,
|
||||
version_freshness_risk=(
|
||||
"RISK"
|
||||
if (
|
||||
external == "PRESENT"
|
||||
and any(w in (body or "").lower() for w in ("version", "upgrade", "latest", "current behaviour"))
|
||||
)
|
||||
else "NONE"
|
||||
),
|
||||
verifiability=verifiability,
|
||||
scope_novelty=scope_novel,
|
||||
provenance={
|
||||
"local_authority": (
|
||||
"ISSUE contract evidence_classes (deterministic compiler) "
|
||||
"+ mechanism-uncertainty scan (measurement language)"
|
||||
),
|
||||
"mechanism_uncertain": str(mechanism_uncertain),
|
||||
"external_dependency": "URL/external-reference scan of issue body",
|
||||
"version_freshness_risk": "version-language scan, gated on external dependency",
|
||||
"verifiability": "test path/command reference; measurement language overrides",
|
||||
"scope_novelty": "explicit new-subsystem/adoption markers in issue body",
|
||||
"task_class_language": ("defect" if is_defect else "change" if is_change else "neutral"),
|
||||
"ambiguous": str(ambiguous),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _detect_external_refs(body: str) -> str:
|
||||
"""Observable external-reference detection. URL schemes and explicit
|
||||
API/SDK/service markers — what is present, not what is inferred."""
|
||||
lower = body.lower()
|
||||
if "http://" in lower or "https://" in lower:
|
||||
return "PRESENT"
|
||||
if any(m in lower for m in (" api ", "sdk", "library", "package", "service", "endpoint")):
|
||||
return "PRESENT"
|
||||
return "NONE"
|
||||
@@ -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]")
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""builder_bridge.py — MiMo builder bridge (thin adapter over Rune Core).
|
||||
|
||||
Requirement (handoff 2026-09-10 §6–§8): the MiMo builder (unlimited GLM)
|
||||
should benefit from Rune Core's growing cognition TODAY, through the exact
|
||||
same core the eventual Rune TUI uses — without turning MiMo into a second
|
||||
Rune and without a second model call just to build context.
|
||||
|
||||
Normal path (cheap — deterministic state lookup + selective retrieval):
|
||||
|
||||
MiMo coding request (task text)
|
||||
↓
|
||||
preflight(task_text) → compact BuilderCapsule (JSON, bounded)
|
||||
↓
|
||||
builder proceeds normally, optionally citing the capsule
|
||||
|
||||
The adapter contains NO cognition of its own: it calls the same Rune Core
|
||||
functions the operator path calls (derive_sufficiency, classify_route,
|
||||
memory_selector, AttentionCapsule). Same core, thin adapter.
|
||||
|
||||
Contents of the capsule (bounded, only what is useful):
|
||||
- task route (LOCAL/PROBE/DEEP) + floor flag + unknowns
|
||||
- memory selection summary (what the selector would inject, capped)
|
||||
- validated process lessons relevant to the task (PL1-validated only;
|
||||
until PL1 exists: none — raw SEED records are never injected)
|
||||
- constraints/authority hints from the route provenance
|
||||
|
||||
Overhead: one deterministic pass, zero model calls. Escalation happens
|
||||
only when the route says so (PROBE/DEEP RESEARCH), never by default.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
_PKG_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _PKG_DIR not in sys.path:
|
||||
sys.path.insert(0, _PKG_DIR)
|
||||
|
||||
try:
|
||||
from . import adaptive_router
|
||||
from .attention_capsule import AttentionCapsule
|
||||
except ImportError: # standalone script execution
|
||||
from rune import adaptive_router
|
||||
from rune.attention_capsule import AttentionCapsule
|
||||
|
||||
# Bounded: the capsule never carries more than this many memory lines or
|
||||
# lesson lines. The builder has its own context window; we are a garnish,
|
||||
# not the meal.
|
||||
MAX_MEMORY_LINES = 12
|
||||
MAX_LESSON_LINES = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuilderCapsule:
|
||||
"""Compact, builder-consumable Rune Core preflight result."""
|
||||
|
||||
task_route: str = ""
|
||||
floor: bool = False
|
||||
unknowns: list = field(default_factory=list)
|
||||
memory_lines: list = field(default_factory=list)
|
||||
lesson_lines: list = field(default_factory=list)
|
||||
constraints: list = field(default_factory=list)
|
||||
generated_at: str = ""
|
||||
core_version: str = "rune-core/operator-path"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"task_route": self.task_route,
|
||||
"floor": self.floor,
|
||||
"unknowns": self.unknowns,
|
||||
"memory_lines": self.memory_lines[:MAX_MEMORY_LINES],
|
||||
"lesson_lines": self.lesson_lines[:MAX_LESSON_LINES],
|
||||
"constraints": self.constraints,
|
||||
"generated_at": self.generated_at,
|
||||
"core_version": self.core_version,
|
||||
}
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""Compact rendering for injection into a builder prompt."""
|
||||
d = self.to_dict()
|
||||
if (
|
||||
not d["memory_lines"]
|
||||
and not d["lesson_lines"]
|
||||
and not d["unknowns"]
|
||||
and not d["constraints"]
|
||||
and d["task_route"] in ("LOCAL_FIX", "STANDARD_CHANGE")
|
||||
):
|
||||
return "" # nothing useful — inject nothing (handoff §5)
|
||||
lines = [f"### Rune Core preflight ({d['task_route']})"]
|
||||
if d["memory_lines"]:
|
||||
lines.append("**Relevant Rune memory:**")
|
||||
lines += [f"- {m}" for m in d["memory_lines"]]
|
||||
if d["lesson_lines"]:
|
||||
lines.append("**Validated process lessons:**")
|
||||
lines += [f"- {l}" for l in d["lesson_lines"]]
|
||||
if d["unknowns"]:
|
||||
lines.append(f"**Material unknowns:** {', '.join(d['unknowns'])}")
|
||||
if d["constraints"]:
|
||||
lines.append("**Constraints:**")
|
||||
lines += [f"- {c}" for c in d["constraints"]]
|
||||
if d["floor"]:
|
||||
lines.append("**Deep-research floor applies** — do not skip evidence.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _default_selector():
|
||||
"""The SAME MemorySelector the operator path uses (lazy, degrade-safe)."""
|
||||
try:
|
||||
from .context_broker import ContextBroker # noqa: F401
|
||||
from .memory_selector import MemorySelector
|
||||
|
||||
return MemorySelector()
|
||||
except ImportError:
|
||||
return None # core memory stack unavailable — bridge runs route-only
|
||||
|
||||
|
||||
def preflight(task_text: str, memory_selector=None) -> BuilderCapsule:
|
||||
"""Deterministic Rune Core preflight for a builder task.
|
||||
|
||||
Zero model calls. Uses the same core authority as the operator path:
|
||||
derive_sufficiency → classify_route → (optional) memory selection.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sufficiency = adaptive_router.derive_sufficiency(title=(task_text or "")[:200], body=task_text or "")
|
||||
decision = adaptive_router.classify_route(
|
||||
issue_body=task_text or "",
|
||||
has_local_repro=(sufficiency.local_authority == "GROUNDED"),
|
||||
owning_authority_known=True,
|
||||
testable_locally=(sufficiency.verifiability == "TESTABLE"),
|
||||
narrow_and_reversible=(sufficiency.scope_novelty == "KNOWN_PATTERN"),
|
||||
sufficiency=sufficiency,
|
||||
pipeline_sha="builder-bridge",
|
||||
)
|
||||
|
||||
capsule = BuilderCapsule(
|
||||
task_route=decision.route,
|
||||
floor=decision.floor,
|
||||
unknowns=list(decision.unknowns),
|
||||
generated_at=datetime.now(timezone.utc).isoformat(),
|
||||
)
|
||||
|
||||
# Memory retrieval through the SAME thresholded store authority the
|
||||
# operator's selector delegates to (memory_store.recall — the ONE
|
||||
# floor, same min_score). The bridge calls the store directly rather
|
||||
# than the full selector: query expansion (recall_uplift) alone costs
|
||||
# ~1.2s of unindexed LIKE scans per request (measured 2026-09-10),
|
||||
# which violates the §7 low-overhead requirement for the normal path.
|
||||
# The store's own gate (min_score=RECALL_MIN_SCORE) is the same
|
||||
# abstention authority; dense+lexical hybrid still applies.
|
||||
memory_lines = _cached_memory(task_text or "")
|
||||
capsule.memory_lines = memory_lines[:MAX_MEMORY_LINES]
|
||||
|
||||
# Constraints from route provenance (observable, bounded).
|
||||
prov = sufficiency.provenance or {}
|
||||
if decision.floor:
|
||||
capsule.constraints.append("Deep-research floor: " + ", ".join(decision.reasons)[:200])
|
||||
if sufficiency.local_authority == "NOT_GROUNDED":
|
||||
capsule.constraints.append("Local authority not established — verify against external/runtime evidence.")
|
||||
|
||||
# Lessons: PL1-VALIDATED ONLY. Until PL1 exists, this stays empty by
|
||||
# design (handoff §9: raw SEED records are outcome telemetry, not lessons).
|
||||
capsule.lesson_lines = _validated_lessons_for(task_text or "")
|
||||
|
||||
return capsule
|
||||
|
||||
|
||||
# --- Memory retrieval: same authority, bounded overhead (handoff §7/§8) ---
|
||||
#
|
||||
# The full selector path costs ~1.3s warm (measured 2026-09-10: the
|
||||
# recall_uplift store-derived pass runs ~4000 unindexed LIKE scans per
|
||||
# request against the 435MB store). Paying that on EVERY builder request
|
||||
# violates §7/§8. Design: TTL cache keyed by normalized task text.
|
||||
# - first request for a unique task: synchronous retrieval (correctness
|
||||
# first — the capsule must never be empty-wrong), ~1.3s
|
||||
# - repeats within TTL: instant cache hit
|
||||
# - the cache stores the RESULT, so identical glue-work tasks (the
|
||||
# common builder case) are near-zero overhead after the first hit.
|
||||
_CACHE: dict = {}
|
||||
_CACHE_TTL = 600.0 # seconds
|
||||
_CACHE_MAX = 128
|
||||
|
||||
|
||||
def _cache_key(task_text: str) -> str:
|
||||
import re as _re
|
||||
|
||||
return _re.sub(r"\s+", " ", (task_text or "").strip().lower())
|
||||
|
||||
|
||||
def _cached_memory(task_text: str) -> list:
|
||||
import time as _time
|
||||
|
||||
key = _cache_key(task_text)
|
||||
now = _time.monotonic()
|
||||
hit = _CACHE.get(key)
|
||||
if hit is not None and now - hit[0] < _CACHE_TTL:
|
||||
return hit[1]
|
||||
lines = _retrieve_memory(task_text)
|
||||
if len(_CACHE) >= _CACHE_MAX:
|
||||
# evict oldest
|
||||
oldest = min(_CACHE.items(), key=lambda kv: kv[1][0])
|
||||
_CACHE.pop(oldest[0], None)
|
||||
_CACHE[key] = (now, lines)
|
||||
return lines
|
||||
|
||||
|
||||
_SELECTOR = None
|
||||
|
||||
|
||||
def _get_selector():
|
||||
global _SELECTOR
|
||||
if _SELECTOR is None:
|
||||
_SELECTOR = _default_selector()
|
||||
return _SELECTOR
|
||||
|
||||
|
||||
def _retrieve_memory(task_text: str) -> list:
|
||||
"""The SAME selector the operator path uses — identical floor, identical
|
||||
sources. Any failure degrades to no-memory (observable: empty lines)."""
|
||||
try:
|
||||
selector = _get_selector()
|
||||
if selector is None:
|
||||
return []
|
||||
result = selector.select_for_task(task_text)
|
||||
lines = []
|
||||
for block in result.blocks[:MAX_MEMORY_LINES]:
|
||||
content = getattr(block, "content", "") or ""
|
||||
for line in content.splitlines()[:3]:
|
||||
if line.strip():
|
||||
lines.append(line.strip()[:200])
|
||||
if len(lines) >= MAX_MEMORY_LINES:
|
||||
break
|
||||
if len(lines) >= MAX_MEMORY_LINES:
|
||||
break
|
||||
return lines
|
||||
except Exception: # noqa: BLE001 — the bridge is an advisory
|
||||
# preflight, not the execution path: ANY selector failure degrades
|
||||
# to no-memory rather than blocking the builder. Observable: empty
|
||||
# lines while the route still records.
|
||||
return []
|
||||
|
||||
|
||||
def _validated_lessons_for(task_text: str) -> list:
|
||||
"""PL1-validated lessons relevant to this task. Empty until PL1 promotes
|
||||
its first lesson — the function exists so the bridge never needs a
|
||||
second integration when PL1 lands."""
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover — manual bridge invocation
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Rune Core preflight for the MiMo builder")
|
||||
ap.add_argument("task", help="Task text to preflight")
|
||||
ap.add_argument("--format", choices=("json", "md"), default="md")
|
||||
args = ap.parse_args()
|
||||
cap = preflight(args.task)
|
||||
if args.format == "json":
|
||||
print(json.dumps(cap.to_dict(), indent=2))
|
||||
else:
|
||||
print(cap.to_markdown())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""builder_bridge_cli.py — CLI wrapper around rune.builder_bridge.preflight.
|
||||
|
||||
Called by the MiMo builder-bridge hook (one subprocess per unique task text;
|
||||
the hook dedupes repeats within its TTL, and this CLI additionally benefits
|
||||
from the module's own TTL cache within a long-lived interpreter — here the
|
||||
process is fresh each call, so the hook-level dedupe is the effective one).
|
||||
|
||||
Usage: builder_bridge_cli.py "<task text>"
|
||||
Output: the capsule markdown (possibly empty — inject-nothing policy).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Rune Core bootstrap (Option C): the bridge lives in src/rune.
|
||||
_REPO = "/mnt/workdrive/issue161-harness"
|
||||
sys.path.insert(0, os.path.join(_REPO, "src"))
|
||||
sys.path.append(_REPO) # tests/ style imports if ever needed
|
||||
|
||||
# The store to read: the LIVE harness store (839+ entries), not the dev
|
||||
# checkout's empty DB. memory_env honours RUNE_MEMORY_DIR.
|
||||
os.environ.setdefault("RUNE_MEMORY_DIR", "/home/nick/.config/opencode/memory")
|
||||
|
||||
from rune.builder_bridge import preflight # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
task = " ".join(sys.argv[1:]).strip() if len(sys.argv) > 1 else ""
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--json":
|
||||
task = " ".join(sys.argv[2:]).strip()
|
||||
cap = preflight(task)
|
||||
print(json.dumps(cap.to_dict()))
|
||||
return 0
|
||||
cap = preflight(task)
|
||||
print(cap.to_markdown())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+31
-40
@@ -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.0"' 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")
|
||||
File renamed without changes.
+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")
|
||||
|
||||
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user