Compare commits
214
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f86b2d1eec | ||
|
|
307b0bf814 | ||
|
|
d776f286de | ||
|
|
621f88c425 | ||
|
|
ef6811ff58 | ||
|
|
a4f02f227c | ||
|
|
5b315ac7c7 | ||
|
|
90bd2b3285 | ||
|
|
1bb4ea86b8 | ||
|
|
f98416e94a | ||
|
|
b3465176f8 | ||
|
|
aab5159ccc | ||
|
|
80f10b9be7 | ||
|
|
9d75e51b22 | ||
|
|
b6113fad7b | ||
|
|
f765365d03 | ||
|
|
b94a3aef61 | ||
|
|
8ca4628694 | ||
|
|
535c290917 | ||
|
|
6bc31c513e | ||
|
|
386137dc14 | ||
|
|
f87adc0d18 | ||
|
|
ed3a73a0a9 | ||
|
|
600008482d | ||
|
|
4bfa4f9b0a | ||
|
|
1d6213cd74 | ||
|
|
9c3f5f0a1d | ||
|
|
c0594ce192 | ||
|
|
dc0ac7b92e | ||
|
|
5e0a2def9d | ||
|
|
049a8dbd86 | ||
|
|
0a9b0648cd | ||
|
|
da2ec4df0e | ||
|
|
dcf992bb2f | ||
|
|
3dadf2d3f5 | ||
|
|
c8b317de53 | ||
|
|
ce330d3ac0 | ||
|
|
16b90fd8be | ||
|
|
c7a28e1db3 | ||
|
|
2731db03f4 | ||
|
|
eb2f1a3e0e | ||
|
|
e3b459dfdf | ||
|
|
b8d030e6c4 | ||
|
|
20a47fac86 | ||
|
|
36ee604646 | ||
|
|
20f493596d | ||
|
|
faf3575fb3 | ||
|
|
2e16fc4c89 | ||
|
|
12303bbd20 | ||
|
|
de30cd958e | ||
|
|
fd6e9b4bca | ||
|
|
24413e4228 | ||
|
|
407c23a689 | ||
|
|
982a9b07d5 | ||
|
|
ff506ebe55 | ||
|
|
2032819d99 | ||
|
|
e2464ae9d6 | ||
|
|
d7e04f1098 | ||
|
|
e639383c49 | ||
|
|
57a274d97d | ||
|
|
aac5295c24 | ||
|
|
9563148f9d | ||
|
|
76984c6362 | ||
|
|
4072d36b1a | ||
|
|
6d871abc9c | ||
|
|
ce40acbb1f | ||
|
|
1de28b55a0 | ||
|
|
75d6d7d62a | ||
|
|
7f47969128 | ||
|
|
a65e5d44de | ||
|
|
26a040892d | ||
|
|
1497c85cd4 | ||
|
|
a2463ad80b | ||
|
|
6965fc7375 | ||
|
|
0daad967c1 | ||
|
|
502317d8b6 | ||
|
|
07cdc2fc56 | ||
|
|
b4161a4034 | ||
|
|
e036f5f545 | ||
|
|
ab6c4f4e38 | ||
|
|
c520b024d1 | ||
|
|
1c73d809b9 | ||
|
|
34fc38d4ed | ||
|
|
a67a9c8520 | ||
|
|
b0efaffca1 | ||
|
|
a087a0b5e7 | ||
|
|
acb3ce0157 | ||
|
|
58af0b0f78 | ||
|
|
95f9e15ed9 | ||
|
|
b083b0c4cf | ||
|
|
46e74110ad | ||
|
|
c669e37a3d | ||
|
|
7f4a6262ae | ||
|
|
34d5ab74b7 | ||
|
|
7b268e17fe | ||
|
|
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 | ||
|
|
fdc9e6566f | ||
|
|
1d2e5a678f | ||
|
|
0562499951 | ||
|
|
6dc52e073f | ||
|
|
bd9e564cfe | ||
|
|
1df2972b58 |
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
|
||||
|
||||
+35
-8
@@ -286,12 +286,39 @@ jobs:
|
||||
run: |
|
||||
NEW="$(echo "${{ gitea.event.pull_request.head.ref }}" | sed 's|chore/auto-bump-||')"
|
||||
python3 scripts/check_version_surfaces.py check --root . --version "$NEW"
|
||||
- name: Publish protected-branch gate (full pipeline path)
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
- name: Publish protected-branch gate (full pipeline path — all needs green)
|
||||
# RELEASE-SAFETY FIX (2026-09-13): the previous version of this step
|
||||
# read /tmp/need_* files that no step ever writes and echoed success
|
||||
# UNCONDITIONALLY — required-checks published green while type-check
|
||||
# and unit-tests were red, and deploy promoted the red commits
|
||||
# (observed live: 24413e4, fd6e9b4, de30cd9 promoted 0.66.28/29 with
|
||||
# failing type-check). The verdict now comes from the needs context
|
||||
# itself: this step runs ONLY when every required job succeeded.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs['unit-tests'].result == 'success'
|
||||
&& needs.canary.result == 'success'
|
||||
&& needs['best-practices'].result == 'success'
|
||||
&& needs.documentation.result == 'success'
|
||||
&& needs['type-check'].result == 'success'
|
||||
&& needs['gitea-offline'].result == 'success' }}
|
||||
run: echo "All required CI checks passed"
|
||||
- name: Refuse promotion when any required check failed (fail-closed)
|
||||
# Companion to the step above: if anything is not exactly success
|
||||
# (failure, cancelled, skipped on a non-bump path), the required-checks
|
||||
# context reports FAILURE — deploy_qualify then refuses promotion.
|
||||
# Fail-closed: not verified → not deployable.
|
||||
if: >-
|
||||
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
|
||||
&& (needs.lint.result != 'success'
|
||||
|| needs['unit-tests'].result != 'success'
|
||||
|| needs.canary.result != 'success'
|
||||
|| needs['best-practices'].result != 'success'
|
||||
|| needs.documentation.result != 'success'
|
||||
|| needs['type-check'].result != 'success'
|
||||
|| needs['gitea-offline'].result != 'success') }}
|
||||
run: |
|
||||
# All needs must have succeeded (skipped deps = this is a bump PR, handled above)
|
||||
FAILED=""
|
||||
for job in lint unit-tests canary best-practices documentation type-check gitea-offline; do
|
||||
RESULT=$(cat /tmp/need_$job 2>/dev/null || echo "")
|
||||
done
|
||||
echo "All required CI checks passed"
|
||||
echo "::error::Required CI checks not green — refusing to publish required-checks success"
|
||||
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
|
||||
exit 1
|
||||
@@ -0,0 +1,85 @@
|
||||
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 (monotonic)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
- name: Promotion truthfulness check
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
|
||||
if [ "$CURRENT_TARGET" = "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
|
||||
echo "PROMOTED: current == candidate ($SHA)"
|
||||
else
|
||||
# Not promoted. deploy_stage exit 0 + non-matching current =
|
||||
# NOOP_ALREADY_CURRENT or STALE_SUPERSEDED — truthful non-error
|
||||
# terminal states (a newer build already won). Do NOT claim
|
||||
# promotion; the runtime-verification step below only runs for
|
||||
# real promotions.
|
||||
echo "NOT PROMOTED: current -> $CURRENT_TARGET (superseded/noop — current is authoritative)"
|
||||
fi
|
||||
|
||||
- name: Post-deploy runtime verification (only for real promotions)
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
CURRENT_TARGET=$(readlink /mnt/workdrive/rune-code/current || true)
|
||||
if [ "$CURRENT_TARGET" != "/mnt/workdrive/rune-code/${SHA:0:12}" ]; then
|
||||
echo "SKIP runtime verification: this job did not promote (superseded/noop)"
|
||||
exit 0
|
||||
fi
|
||||
# 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.65.0**
|
||||
**Current release: v0.66.44**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
+244
-215
@@ -1,217 +1,246 @@
|
||||
{
|
||||
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
|
||||
"umbrellas": {
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"memory_store",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"shard_manager",
|
||||
"query_memory",
|
||||
"graph_memory",
|
||||
"warm_neuron_store",
|
||||
"abstraction",
|
||||
"cache",
|
||||
"recall_router",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"tier_manager",
|
||||
"graph_shard_bridge",
|
||||
"memory_selector",
|
||||
"retrieval_benchmark"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_query_memory",
|
||||
"canary_memory_synthesis",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"drift_core",
|
||||
"drift_observability",
|
||||
"drift_ledger",
|
||||
"audit_ledger",
|
||||
"curator",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"build_pipeline_policy",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": []
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"identity_anchors",
|
||||
"voice_hybrid",
|
||||
"voice_sync",
|
||||
"voicebox",
|
||||
"voice_injector",
|
||||
"behavior",
|
||||
"prompt_sync"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"self_improve",
|
||||
"subconscious_pass",
|
||||
"goal",
|
||||
"elastic_cognition",
|
||||
"growth_delta",
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"growth_simulator"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory",
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"witness",
|
||||
"salience_gate",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"capability_broker",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
"canary_p0_05",
|
||||
"canary_native"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"model_routing",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_connect",
|
||||
"provider_registry",
|
||||
"provider_budget",
|
||||
"standardized_providers",
|
||||
"inference_trace",
|
||||
"response_adapter",
|
||||
"token_optimizer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_06",
|
||||
"canary_p1_02",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"rune_operator",
|
||||
"rune_tui",
|
||||
"rune_graphic",
|
||||
"rune_spinner",
|
||||
"rune_profiler",
|
||||
"chat_bubble",
|
||||
"message_widgets",
|
||||
"tui_validator",
|
||||
"microkernel",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"hook_engine",
|
||||
"system_registry",
|
||||
"cron_engine",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"work_manager",
|
||||
"parallel_processor",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"night_mode",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"verification_layer",
|
||||
"outcome_ledger",
|
||||
"warm_router",
|
||||
"subsystem_health",
|
||||
"build_mode",
|
||||
"dependency_tracker",
|
||||
"auto_detect",
|
||||
"build_orchestrator",
|
||||
"custodian",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"umbrella_update"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_07",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_tui_drift",
|
||||
"canary_cognitive_strata",
|
||||
"canary_surface_parity"
|
||||
],
|
||||
"depends_on": [
|
||||
"perception",
|
||||
"capability",
|
||||
"inference",
|
||||
"identity",
|
||||
"growth"
|
||||
]
|
||||
}
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-13T16:15:07Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"evidence_probe",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"openai_compat_transport",
|
||||
"recall_uplift",
|
||||
"route_risk_flags",
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"elastic_cognition",
|
||||
"goal",
|
||||
"growth_delta",
|
||||
"growth_simulator",
|
||||
"self_improve",
|
||||
"subconscious_pass"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"prompt_sync",
|
||||
"voice_hybrid",
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_routing",
|
||||
"provider_budget",
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"token_optimizer"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"cache",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
"memory_store",
|
||||
"query_memory",
|
||||
"recall_router",
|
||||
"retrieval_benchmark",
|
||||
"shard_manager",
|
||||
"tier_manager",
|
||||
"warm_neuron_store"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
"build_orchestrator",
|
||||
"chat_bubble",
|
||||
"cron_engine",
|
||||
"custodian",
|
||||
"dependency_tracker",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"hook_engine",
|
||||
"message_widgets",
|
||||
"microkernel",
|
||||
"night_mode",
|
||||
"outcome_ledger",
|
||||
"parallel_processor",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_profiler",
|
||||
"rune_spinner",
|
||||
"rune_tui",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"subsystem_health",
|
||||
"system_registry",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tui_validator",
|
||||
"umbrella_update",
|
||||
"verification_layer",
|
||||
"warm_router",
|
||||
"work_manager"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"capability_fit",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_decision",
|
||||
"construction_interface",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"layout_contract",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"structural_store",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
|
||||
"umbrellas": {
|
||||
"memory": {
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"modules": [
|
||||
"memory_store",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"shard_manager",
|
||||
"query_memory",
|
||||
"graph_memory",
|
||||
"warm_neuron_store",
|
||||
"abstraction",
|
||||
"cache",
|
||||
"recall_router",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"tier_manager",
|
||||
"graph_shard_bridge",
|
||||
"memory_selector",
|
||||
"retrieval_benchmark"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_query_memory",
|
||||
"canary_memory_synthesis",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"modules": [
|
||||
"drift_core",
|
||||
"drift_observability",
|
||||
"drift_ledger",
|
||||
"audit_ledger",
|
||||
"curator",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"build_pipeline_policy",
|
||||
"failure_auto_filer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
],
|
||||
"depends_on": []
|
||||
},
|
||||
"identity": {
|
||||
"description": "Who the agent is \u2014 persona, voice, anchors",
|
||||
"modules": [
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"identity_anchors",
|
||||
"voice_hybrid",
|
||||
"voice_sync",
|
||||
"voicebox",
|
||||
"voice_injector",
|
||||
"behavior",
|
||||
"prompt_sync"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"description": "Learning and improvement",
|
||||
"modules": [
|
||||
"self_improve",
|
||||
"subconscious_pass",
|
||||
"goal",
|
||||
"elastic_cognition",
|
||||
"growth_delta",
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
"dream_cycle",
|
||||
"growth_simulator"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory",
|
||||
"governance"
|
||||
]
|
||||
},
|
||||
"perception": {
|
||||
"description": "Input processing and context assembly",
|
||||
"modules": [
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
"auto_tasks",
|
||||
"witness",
|
||||
"salience_gate",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"capability_broker",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"intent_continuity",
|
||||
"openai_compat_transport",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
"connection_pool",
|
||||
"adaptive_router",
|
||||
"route_risk_flags",
|
||||
"evidence_probe",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"recall_uplift"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_01",
|
||||
"canary_p0_05",
|
||||
"canary_native"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"description": "Model routing and outbound requests",
|
||||
"modules": [
|
||||
"model_routing",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"model_connect",
|
||||
"provider_registry",
|
||||
"provider_budget",
|
||||
"standardized_providers",
|
||||
"inference_trace",
|
||||
"response_adapter",
|
||||
"token_optimizer"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_06",
|
||||
"canary_p1_02",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"modules": [
|
||||
"rune_operator",
|
||||
"rune_tui",
|
||||
"rune_graphic",
|
||||
"rune_spinner",
|
||||
"rune_profiler",
|
||||
"chat_bubble",
|
||||
"message_widgets",
|
||||
"tui_validator",
|
||||
"microkernel",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"hook_engine",
|
||||
"system_registry",
|
||||
"cron_engine",
|
||||
"task_gate",
|
||||
"task_persist",
|
||||
"work_manager",
|
||||
"parallel_processor",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"night_mode",
|
||||
"pins",
|
||||
"progressive_escalation",
|
||||
"safety_override",
|
||||
"sleep_time",
|
||||
"verification_layer",
|
||||
"outcome_ledger",
|
||||
"warm_router",
|
||||
"subsystem_health",
|
||||
"build_mode",
|
||||
"dependency_tracker",
|
||||
"auto_detect",
|
||||
"build_orchestrator",
|
||||
"custodian",
|
||||
"gitea_auth",
|
||||
"gitea_pipeline",
|
||||
"gitea_provision",
|
||||
"umbrella_update"
|
||||
],
|
||||
"canaries": [
|
||||
"canary_p1_07",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_tui_drift",
|
||||
"canary_cognitive_strata",
|
||||
"canary_surface_parity"
|
||||
],
|
||||
"depends_on": [
|
||||
"perception",
|
||||
"capability",
|
||||
"inference",
|
||||
"identity",
|
||||
"growth"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
# 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).
|
||||
|
||||
|
||||
## PL1 held-out era reconciliation (2026-09-12)
|
||||
|
||||
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
|
||||
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
|
||||
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
|
||||
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
|
||||
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
|
||||
backlog was never in the ADAPT-LIVE tranche scope).
|
||||
|
||||
| # | state | evidence / basis |
|
||||
|---|---|---|
|
||||
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
|
||||
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
|
||||
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
|
||||
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
|
||||
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
|
||||
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
|
||||
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
|
||||
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
|
||||
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
|
||||
|
||||
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
|
||||
(72 records, 100 live issues in window) after these updates.
|
||||
@@ -0,0 +1,189 @@
|
||||
# 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 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). 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.
|
||||
|
||||
## Local CI tooling (developer-side, NOT Rune runtime): W/R-v0.3 reactive wake (2026-09-12)
|
||||
|
||||
The desktop gitea-runner (v3.3.0) now runs `host.post_task_script` = `/usr/local/bin/gitea-wr-wake.sh`
|
||||
after every job: a sub-second fire-and-forget POST to the local W/R daemon (`127.0.0.1:8791/wake`),
|
||||
which sweeps waiting external waits through AUTHORITATIVE Gitea verification (bounded retry for the
|
||||
reporter-ack window; 60s reconciler demoted to fallback). Runner event ≠ success proof. Config:
|
||||
`/etc/gitea-desktop-runner/config.yaml` (backup: config.yaml.pre-wr03). Measured: terminal→verified
|
||||
wait transition ~2s (was 0-30s polling). INTERACTIVE_AUTO_RESUME remains NOT YET SAFE (no supported
|
||||
MiMo busy/idle signal) — headless-only dispatch, unchanged.
|
||||
@@ -0,0 +1,228 @@
|
||||
# Scaffolding V1 — Tranche 1
|
||||
|
||||
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
|
||||
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
|
||||
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
|
||||
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
|
||||
built every time Rune changes._
|
||||
|
||||
## Tranche 1 contract
|
||||
|
||||
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
|
||||
never a new one:
|
||||
|
||||
| question | authority projected |
|
||||
|---|---|
|
||||
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
|
||||
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
|
||||
| who consumes X? | reverse import edges |
|
||||
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
|
||||
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
|
||||
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
|
||||
|
||||
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
|
||||
`all [outfile]`. If the projection disagrees with an authority, fix the
|
||||
authority — never the projection.
|
||||
|
||||
## Mechanism audit (2026-09-12, deployed 0.66.22)
|
||||
|
||||
| mechanism | verdict | basis |
|
||||
|---|---|---|
|
||||
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
|
||||
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
|
||||
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
|
||||
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
|
||||
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
|
||||
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
|
||||
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
|
||||
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
|
||||
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
|
||||
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
|
||||
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
|
||||
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
|
||||
|
||||
## Real-change proof (this tranche)
|
||||
|
||||
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
|
||||
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
|
||||
for a 131072-window model (observed live in every PL1 Task A/B run).
|
||||
|
||||
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
|
||||
derived — subsystem `capability`; consumers `rune_operator`; gates:
|
||||
full suite, lint ratchet, henxels, inference+orchestration canaries;
|
||||
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
|
||||
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
|
||||
authority in `openai_compat_transport.py`; consumed by context discovery
|
||||
(new) and transport selection (deduped); red-first regression tests.
|
||||
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
|
||||
· the projection itself caught two gaps mid-flight (relative imports
|
||||
hidden consumers; a broad except biting the lint ratchet) — both fixed at
|
||||
the source the projection pointed to.
|
||||
4. Deploy: normal release chain → deployed identity verified.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Projection engine: **UNIT-PROVEN** + proven on one real change.
|
||||
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
|
||||
capability discovery / gap recognition / reuse-before-build / external
|
||||
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
|
||||
does not block them; the projection source-list is the extension surface).
|
||||
- Deferred with reasons recorded: none blocked by Tranche 1.
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding V1 — Tranche 2: Construction Substrate (2026-09-13)
|
||||
|
||||
_Tranche 2 (contract SCAFFOLD-TRANCHE-001) built the construction substrate
|
||||
itself: a shared structural-store primitive, the Project Scaffold layer over
|
||||
it, and durable Construction Contracts — Rune's first build tracked by the
|
||||
system it builds._
|
||||
|
||||
## Architecture
|
||||
|
||||
src/rune/structural_store.py shared generic primitive — NO Rune constants
|
||||
↓ ↓
|
||||
src/rune/spine.py src/rune/scaffold.py
|
||||
(Rune Spine — canonical (Project Scaffold — canonical
|
||||
structural self-model) construction/structural truth
|
||||
about EXTERNAL projects)
|
||||
|
||||
Authority map (no duplicates):
|
||||
|
||||
| fact | authority | others |
|
||||
|---|---|---|
|
||||
| Rune's structure | `spine.py` + `store/spine/current.json` | scaffold only references |
|
||||
| external project structure | `scaffold.py` store | Spine holds bridge facts only |
|
||||
| content history / rollback | Git | both reference commits |
|
||||
| coordination (issues/PRs/CI) | Gitea | scaffold stores observations |
|
||||
| why work exists | Task/Intent | contracts reference task ids |
|
||||
|
||||
Construction Contract = ONE durable object (`contract:<id>`, kind
|
||||
`construction_contract`) with plain `phase`/`status` fields governed by a
|
||||
small legal-transition map (draft → approved → building → verifying →
|
||||
migrating → superseding → closed; failed/abandoned terminal-or-retry). No
|
||||
workflow engine. Resumability = store durability, proven across real
|
||||
process boundaries.
|
||||
|
||||
## Mechanics
|
||||
|
||||
- **Repo binding** (`bind_repo`): forge/owner/name/branch/root/placement as
|
||||
scaffold DATA. No secrets — schema rejects secret-named fields AND every
|
||||
`save()` recursively redacts credential-named keys (defense in depth in
|
||||
the shared core, so Spine inherits it too).
|
||||
- **Scope check** (`check_scope`): changed files vs approved scope paths →
|
||||
green / red / **unknown** (an unscoped contract is never silently green).
|
||||
Out-of-scope change = RED until explicitly amended; amendments recorded,
|
||||
original RED preserved in history.
|
||||
- **Predicted vs observed**: frontier snapshot at approval; observed impact
|
||||
captured at closure from INDEPENDENT evidence (git, tests) — never from
|
||||
re-walking the prediction's own graph. Missed entities become CANDIDATE
|
||||
corrections awaiting evidence.
|
||||
- **Drift**: authority-owned facts that fail re-observation go STALE —
|
||||
never silently fresh, never force-reverted. Reality wins.
|
||||
- **Bridge** (`spine_bridge_entity`): identity + repo surface only. Project
|
||||
internals NEVER leak into Rune Spine (test-enforced). Contract evidence
|
||||
may cite `spine://` URIs — citations are inert on the referenced
|
||||
authority.
|
||||
|
||||
## Real proof (this tranche on itself)
|
||||
|
||||
SCAFFOLD-TRANCHE-001 migrated from its pre-scaffold JSON into the new
|
||||
store, advanced through phases with evidence checkpoints, closed with
|
||||
predicted-vs-observed from git/test evidence, scope check caught the
|
||||
store's own runtime artifacts (RED → explicit amendment → green, history
|
||||
kept), and construction provenance attached into Rune Spine without
|
||||
duplicating authority. A fresh interpreter answers what/why/state/
|
||||
evidence/changed/next from the store alone — no transcripts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
| component | state |
|
||||
|---|---|
|
||||
| structural_store core | EXERCISED (unit-proven + real use) |
|
||||
| Rune Spine after refactor | LIVE_PROVEN behavior-neutral (68 tests + identical rebuild) |
|
||||
| Project Scaffold | EXERCISED |
|
||||
| Construction Contract | EXERCISED (resumability proven cross-process) |
|
||||
| repo binding (identity model) | UNIT-PROVEN (local git fixture) |
|
||||
| external-repo live proof | BLOCKED ON USER REPO SELECTION |
|
||||
| Scaffolding overall | substrate only — capability-fit discovery, gap decisions, toolchain selection, patterns, maintenance TUI all NOT BUILT (data seams exist) |
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 4 — Live Need → Decision → Construction Contract (2026-09-13)
|
||||
|
||||
_The adoption tranche: a real Need now flows automatically through the
|
||||
Tranche 2/3 mechanisms and produces a governed Construction Contract. The
|
||||
model no longer has to remember the procedure — it is durable Scaffolding._
|
||||
|
||||
## Live flow
|
||||
|
||||
NEED (reference to Task/Intent — pointer, never a copy)
|
||||
↓ normalization (gaps recorded, never invented)
|
||||
↓ structural discovery + fit + resolution (Spine + Project Scaffold)
|
||||
↓ toolchain decision (existing project toolchain wins)
|
||||
↓ layout obligations (USER-LOCKED authority inherited)
|
||||
↓ user-approval gate (material changes require approval; approved
|
||||
bounded work never re-asks)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generated Construction Contract → existing build/verify pipeline
|
||||
|
||||
## Authority map (unchanged — the path CONNECTS, never duplicates)
|
||||
|
||||
| authority | owns |
|
||||
|---|---|
|
||||
| Task/Intent | why work exists |
|
||||
| Rune Spine | Rune structural truth |
|
||||
| Project Scaffold | external-project structural/construction truth |
|
||||
| Layout Contract | desired interface constraints (USER-LOCKED = user authority) |
|
||||
| Construction Contract | governed build obligations/history |
|
||||
| Git / Gitea | history / execution |
|
||||
|
||||
## Boundary honesty
|
||||
|
||||
- **ADAPT/research**: the live path records a research ROUTE
|
||||
(TARGETED_EVIDENCE_PROBE / FULL_RESEARCH_PIPELINE) when evidence is
|
||||
insufficient — a seam, NOT an executed pipeline call. No fake
|
||||
integration claimed.
|
||||
- **BUILD_NEW** is never a fallback: insufficient evidence blocks it;
|
||||
normalization gaps block it; merge precedence across authorities
|
||||
requires every consulted authority to agree.
|
||||
- **No second task engine**: one decision function + one trace object.
|
||||
|
||||
## Contract-quality repair (Tranche 3 defects)
|
||||
|
||||
Closure now distinguishes:
|
||||
|
||||
predicted_existing_affected planned changes to existing entities
|
||||
expected_new entities the build PLANNED to create
|
||||
correctly_predicted predicted AND independently observed
|
||||
predicted_but_unaffected predicted, NOT observed
|
||||
expected_new_observed planned creation happened
|
||||
expected_new_missing planned creation did NOT happen
|
||||
genuinely_unexpected_new observed, planned by neither list
|
||||
|
||||
Prediction cannot self-certify observation: the observed set comes ONLY
|
||||
from caller evidence (git/tests/build/deploy). Legacy keys (missed/
|
||||
needless/observed) preserved — old contracts readable without migration.
|
||||
|
||||
## Layout-obligation inheritance
|
||||
|
||||
A change touching a UI component/region/implementation inherits its Layout
|
||||
Contract as a build obligation. USER-LOCKED obligations state that changing
|
||||
placement requires explicit user authorization. Unrelated backend changes
|
||||
inherit nothing. USER-LOCKED drift is never reconciled by editing the
|
||||
contract — code adapts.
|
||||
|
||||
## Maturity (honest)
|
||||
|
||||
- Need intake: EXERCISED (real live proof)
|
||||
- decision orchestration: LIVE-PROVEN for the bounded live flow
|
||||
- capability discovery adoption: EXERCISED
|
||||
- fit/gap adoption: EXERCISED
|
||||
- toolchain adoption: EXERCISED
|
||||
- contract generation: LIVE-PROVEN (one real generated contract)
|
||||
- contract-quality prediction: EXERCISED (repaired semantics, one live closure)
|
||||
- layout-obligation inheritance: UNIT-PROVEN (fixtures) + real TUI binding
|
||||
- Scaffolding overall: adoption path live for one bounded flow — NOT
|
||||
general autonomous construction, NOT universal discovery/toolchain
|
||||
intelligence, NOT Scaffolding complete.
|
||||
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 |
|
||||
|
||||
@@ -9,6 +9,7 @@ new surface._
|
||||
|---|---|---|
|
||||
| `rune_tui.py` | The main Textual application — chat pane, work pane, status bar (version label + refresh), footer, dialogs | The known giant (file-size debt tracked in henxels); composition root for all UI |
|
||||
| `tui_validator.py` | Import-allowlist validation for TUI code | Drift defect tracked in #139 (allowlist vs `__future__`/relative imports) |
|
||||
| `tui_backlog.py` | Not a surface — the durable TUI Upgrade Backlog ledger (outstanding projection/control work per backend feature) | SCAFFOLD-TRANCHE-007; ledger only, never structural authority |
|
||||
|
||||
## Adding a TUI surface
|
||||
|
||||
|
||||
@@ -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
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
settings:
|
||||
warn_about_similar_files:
|
||||
above: 0.85
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**"]
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
|
||||
|
||||
henxels:
|
||||
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
|
||||
@@ -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.65.0"
|
||||
version = "0.66.44"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"errorCount": 120,
|
||||
"warningCount": 465,
|
||||
"informationCount": 0
|
||||
"errorCount": 117,
|
||||
"warningCount": 443,
|
||||
"informationCount": 0
|
||||
}
|
||||
@@ -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.*
|
||||
Whitespace-only changes.
@@ -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).
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""construction_guards.py — deterministic guards for observed process
|
||||
defects (SCAFFOLD-TRANCHE-006 §21-22).
|
||||
|
||||
Two real defect classes from recent sessions:
|
||||
|
||||
A. DURABLE-STORE MUTATION WITHOUT PERSISTENCE (Tranche 5: closure script
|
||||
mutated the contract in memory, never called save(); the next read saw
|
||||
stale state). Guard: a durable-boundary operation verifies by
|
||||
RELOAD/READBACK, not by successful in-memory mutation.
|
||||
|
||||
B. BLIND SOURCE-EDIT NO-OP (multiple sessions: python str.replace() on
|
||||
ruff-reformatted files silently matched nothing; the script reported
|
||||
success while changing nothing). Guard: an automated edit verifies its
|
||||
POSTCONDITION — expected text present / old text absent — and a
|
||||
zero-match replacement is an explicit, truthful no-op, never success.
|
||||
|
||||
Not a generic editor framework. Two small, deterministic postcondition
|
||||
checks at the boundaries where the defects actually occurred.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["verify_persisted", "assert_persisted", "apply_source_edit", "EditNoOp", "EditPostconditionFailed"]
|
||||
|
||||
|
||||
class EditNoOp(Exception):
|
||||
"""The edit matched zero targets — an explicit, truthful no-op."""
|
||||
|
||||
|
||||
class EditPostconditionFailed(Exception):
|
||||
"""The edit claimed success but the expected postcondition is absent."""
|
||||
|
||||
|
||||
# ── Guard A: durable mutation verified by readback ──────────────────────────
|
||||
|
||||
|
||||
def verify_persisted(path: Path | str, selector, expect) -> bool:
|
||||
"""Reload the durable store from disk and check the expectation against
|
||||
the RELOADED state. `selector(reloaded_doc) -> value`; pass == value
|
||||
equals `expect` (or expect(value) is truthy when callable).
|
||||
|
||||
The reloaded document is parsed as JSON when the file is JSON-shaped;
|
||||
otherwise the raw text is passed to the selector (plain-text stores).
|
||||
|
||||
This is the Tranche 5 defect guard: an in-memory mutation that was
|
||||
never save()d fails here, because the disk state is what gets read.
|
||||
"""
|
||||
raw = Path(path).read_text(encoding="utf-8")
|
||||
try:
|
||||
doc = json.loads(raw)
|
||||
except ValueError:
|
||||
doc = raw
|
||||
value = selector(doc)
|
||||
if callable(expect):
|
||||
return bool(expect(value))
|
||||
return value == expect
|
||||
|
||||
|
||||
def assert_persisted(path: Path | str, selector, expect, *, what: str = "durable state") -> None:
|
||||
"""verify_persisted that raises on mismatch — for use right after a
|
||||
save() at durable boundaries."""
|
||||
if not verify_persisted(path, selector, expect):
|
||||
raise AssertionError(
|
||||
f"persistence verification FAILED for {what}: reloaded state "
|
||||
f"does not match expectation — the mutation may not have been "
|
||||
f"saved (Tranche 5 defect class A)"
|
||||
)
|
||||
|
||||
|
||||
# ── Guard B: source edit with postcondition ─────────────────────────────────
|
||||
|
||||
|
||||
def apply_source_edit(
|
||||
text: str,
|
||||
old: str,
|
||||
new: str,
|
||||
*,
|
||||
expected_count: int | None = None,
|
||||
postcondition: str | None = None,
|
||||
allow_noop: bool = False,
|
||||
) -> str:
|
||||
"""Apply a source edit WITH postcondition verification (Guard B).
|
||||
|
||||
- zero matches: raises EditNoOp (truthful no-op) unless allow_noop=True
|
||||
- expected_count given: match count must equal it
|
||||
- postcondition given: the RESULT must contain it, else
|
||||
EditPostconditionFailed
|
||||
Returns the new text. Never claims success for a change that did not
|
||||
happen."""
|
||||
count = text.count(old)
|
||||
if count == 0:
|
||||
if allow_noop:
|
||||
return text # truthful, explicit no-op — caller records it
|
||||
raise EditNoOp(
|
||||
f"edit matched 0 targets — nothing changed (Tranche 5 defect "
|
||||
f"class B: str.replace silently no-ops on reformatted files)"
|
||||
)
|
||||
if expected_count is not None and count != expected_count:
|
||||
raise EditPostconditionFailed(f"edit matched {count} targets, expected {expected_count}")
|
||||
result = text.replace(old, new)
|
||||
if postcondition and postcondition not in result:
|
||||
raise EditPostconditionFailed(
|
||||
"postcondition text absent from result — edit did not achieve its expected effect"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def apply_source_edit_file(
|
||||
path: Path | str, old: str, new: str, *, expected_count: int | None = None, postcondition: str | None = None
|
||||
) -> int:
|
||||
"""File-level convenience: read, apply_source_edit, write back, then
|
||||
VERIFY BY READBACK (both guards composed). Returns match count."""
|
||||
p = Path(path)
|
||||
text = p.read_text(encoding="utf-8")
|
||||
result = apply_source_edit(text, old, new, expected_count=expected_count, postcondition=postcondition)
|
||||
p.write_text(result, encoding="utf-8")
|
||||
# Guard A composed: the written file must contain the postcondition
|
||||
if postcondition:
|
||||
assert_persisted(p, lambda t: t, lambda t: postcondition in t, what=f"edit of {p.name}")
|
||||
return text.count(old)
|
||||
@@ -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,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy_promotion.py — monotonic promotion authority for Rune deploys.
|
||||
|
||||
SCAFFOLD contract DEPLOY-MONOTONIC-001. Root cause of the 2026-09-13
|
||||
deploy-order race (deploys 7355/7359): deploy_stage.py promoted
|
||||
unconditionally in job-finish order — no comparison against the currently
|
||||
deployed commit, no lock, no truthful outcome for a stale candidate.
|
||||
|
||||
This module owns the PROMOTION decision. The invariant:
|
||||
|
||||
current never regresses along the canonical release branch.
|
||||
|
||||
Ordering authority: GIT ANCESTRY on the canonical release branch
|
||||
(refs/heads/master). Rune's release flow is linear history — every deploy
|
||||
candidate is a master push (tranche commit or its auto-bump descendant).
|
||||
candidate == deployed commit -> NOOP_ALREADY_CURRENT (idempotent)
|
||||
candidate is DESCENDANT of it -> allowed (normal forward deploy)
|
||||
candidate is ANCESTOR of it -> STALE_SUPERSEDED (refuse; truthful)
|
||||
no ancestry relationship -> REFUSED_DIVERGENT (fail closed)
|
||||
deployed commit unknown/unreadable-> REFUSED_UNKNOWN_CURRENT (fail closed)
|
||||
|
||||
Mechanism (smallest that provides the invariant, per brief §4-5):
|
||||
1. promotion-time stale check — ancestry comparison against the commit
|
||||
current ACTUALLY points to at promotion time (not at job start);
|
||||
2. an flock'd critical section around the read-compare-write of
|
||||
current/last-good so two concurrent promotions cannot interleave
|
||||
between the ancestry check and the symlink swap.
|
||||
Workflow-level concurrency is NOT relied on (brief §5): the fence lives at
|
||||
the promotion operation itself.
|
||||
|
||||
Truthful outcomes (brief §8) — build/stage success != promotion success:
|
||||
PROMOTED candidate became current (verified below)
|
||||
NOOP_ALREADY_CURRENT same commit already deployed; nothing mutated
|
||||
STALE_SUPERSEDED a newer commit is already current; nothing mutated
|
||||
REFUSED_DIVERGENT ordering unprovable; fail closed
|
||||
REFUSED_UNKNOWN_CURRENT deployed commit unreadable; fail closed
|
||||
FAILED_* verification/staging failures (in deploy_stage)
|
||||
|
||||
A stale or refused outcome is a NON-ERROR terminal state for the release
|
||||
pipeline (the newer build simply won) but MUST NOT print "PROMOTED" and
|
||||
MUST NOT mutate current/last-good. deploy_stage's exit code and the
|
||||
workflow's post-deploy runtime verification enforce this: the workflow
|
||||
asserts current == candidate after a PROMOTED outcome, so a stale outcome
|
||||
makes the job report honestly rather than masquerade.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
RELEASE_BRANCH = "refs/heads/master"
|
||||
|
||||
OUTCOME_PROMOTED = "PROMOTED"
|
||||
OUTCOME_NOOP = "NOOP_ALREADY_CURRENT"
|
||||
OUTCOME_STALE = "STALE_SUPERSEDED"
|
||||
OUTCOME_DIVERGENT = "REFUSED_DIVERGENT"
|
||||
OUTCOME_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
|
||||
|
||||
|
||||
# ── pure decision logic (unit-tested; no I/O) ──────────────────────────────
|
||||
|
||||
|
||||
def decide_promotion(candidate_sha: str, deployed_sha: str, ancestry: str) -> str:
|
||||
"""Pure promotion decision from a precomputed ancestry relationship.
|
||||
ancestry is one of: 'same' | 'descendant' | 'ancestor' | 'divergent' |
|
||||
'unknown'. Fail-closed: anything not provably allowed is refused."""
|
||||
if candidate_sha == deployed_sha or ancestry == "same":
|
||||
return OUTCOME_NOOP
|
||||
if ancestry == "descendant":
|
||||
return OUTCOME_PROMOTED
|
||||
if ancestry == "ancestor":
|
||||
return OUTCOME_STALE
|
||||
if ancestry == "divergent":
|
||||
return OUTCOME_DIVERGENT
|
||||
return OUTCOME_UNKNOWN_CURRENT
|
||||
|
||||
|
||||
def ancestry_of(candidate_sha: str, deployed_sha: str, repo: Path, fetch_on_divergent: bool = True) -> str:
|
||||
"""Classify candidate vs deployed using git merge-base --is-ancestor on
|
||||
the canonical release branch's repository. Returns 'unknown' on any git
|
||||
failure (fail-closed callers treat that as refusal).
|
||||
|
||||
Observed live 2026-09-13 (deploy 7449, SHA 535c2909): a DELAYED older
|
||||
candidate's checkout cannot contain commits newer than itself — its
|
||||
local object store lacks the deployed commit entirely, so ancestry
|
||||
classifies 'divergent' and fail-closed refuses. Correct refusal, wrong
|
||||
reason: the candidate IS an ancestor. When the local classification is
|
||||
divergent, fetch origin's canonical branch first (best-effort; the
|
||||
deploy workflow's checkout has credentials configured) and re-classify.
|
||||
If still divergent afterwards, the refusal stands with better
|
||||
evidence."""
|
||||
|
||||
def _is_ancestor(a: str, b: str) -> bool:
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "merge-base", "--is-ancestor", a, b],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return r.returncode == 0
|
||||
|
||||
if candidate_sha == deployed_sha:
|
||||
return "same"
|
||||
if _is_ancestor(candidate_sha, deployed_sha):
|
||||
return "ancestor" # candidate is OLDER than what's deployed
|
||||
if _is_ancestor(deployed_sha, candidate_sha):
|
||||
return "descendant" # candidate is NEWER (normal forward deploy)
|
||||
if fetch_on_divergent:
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "-c", f"safe.directory={repo}", "fetch", "origin", "master", "--quiet"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass # best-effort: classification below still fails closed
|
||||
if _is_ancestor(candidate_sha, deployed_sha):
|
||||
return "ancestor"
|
||||
if _is_ancestor(deployed_sha, candidate_sha):
|
||||
return "descendant"
|
||||
return "divergent"
|
||||
|
||||
|
||||
def read_deployed_commit(deploy_root: Path = DEPLOY_ROOT) -> str:
|
||||
"""Read the commit the `current` symlink ACTUALLY points to right now —
|
||||
from the deployed build identity, the deployment authority (never from
|
||||
a stale job-start artifact). Empty string if unreadable."""
|
||||
current = deploy_root / "current"
|
||||
try:
|
||||
target = Path(os.readlink(current))
|
||||
except OSError:
|
||||
return ""
|
||||
identity = target / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||
return str(data.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
# ── the fenced promotion operation ──────────────────────────────────────────
|
||||
|
||||
|
||||
def promote(
|
||||
candidate_sha: str, stage: Path, tmp_stage: Path, repo: Path, deploy_root: Path = DEPLOY_ROOT
|
||||
) -> tuple[str, str]:
|
||||
"""Atomically decide + promote under an exclusive lock.
|
||||
|
||||
Returns (outcome, detail). ONLY the PROMOTED outcome mutates
|
||||
current/last-good, and it does so inside the same critical section as
|
||||
the decision — no interleaving window between check and swap.
|
||||
|
||||
Crash-safety ordering (brief §16): the stage rename happens BEFORE the
|
||||
lock (a staged-but-unpromoted tree is inert), the symlink swap is a
|
||||
single rename(2) via a temporary link, so current is never absent and
|
||||
never half-updated. last-good is refreshed BEFORE the swap only from a
|
||||
value that was genuinely current a moment ago.
|
||||
"""
|
||||
deploy_root.mkdir(parents=True, exist_ok=True)
|
||||
lock_path = deploy_root / ".promotion.lock"
|
||||
with open(lock_path, "w") as lock_fd:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
deployed = read_deployed_commit(deploy_root)
|
||||
if not deployed:
|
||||
return (OUTCOME_UNKNOWN_CURRENT, "deployed commit unreadable — fail closed")
|
||||
rel = ancestry_of(candidate_sha, deployed, repo)
|
||||
outcome = decide_promotion(candidate_sha, deployed, rel)
|
||||
if outcome != OUTCOME_PROMOTED:
|
||||
return (outcome, f"deployed={deployed[:12]} ancestry={rel}")
|
||||
|
||||
# --- mutate (still inside the lock) ---
|
||||
current = deploy_root / "current"
|
||||
last_good = deploy_root / "last-good"
|
||||
prev_target = os.readlink(current) if current.is_symlink() else ""
|
||||
if prev_target and (deploy_root / "last-good").is_symlink():
|
||||
(deploy_root / "last-good").unlink()
|
||||
if prev_target:
|
||||
_atomic_symlink(deploy_root, "last-good", prev_target)
|
||||
if stage.exists():
|
||||
# re-deploying an already-staged SHA: restage over it
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(stage)
|
||||
os.rename(tmp_stage, stage)
|
||||
_atomic_symlink_target(deploy_root, "current", stage)
|
||||
return (OUTCOME_PROMOTED, f"current -> {stage.name}")
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _atomic_symlink(deploy_root: Path, name: str, target: str) -> None:
|
||||
"""Symlink swap that never leaves `name` absent: write temp link, rename
|
||||
over the final name (rename(2) is atomic on the same filesystem)."""
|
||||
_atomic_symlink_target(deploy_root, name, Path(target))
|
||||
|
||||
|
||||
def _atomic_symlink_target(deploy_root: Path, name: str, target: Path) -> None:
|
||||
tmp = deploy_root / f".{name}.swap-{os.getpid()}"
|
||||
if tmp.is_symlink() or tmp.exists():
|
||||
tmp.unlink()
|
||||
tmp.symlink_to(target)
|
||||
os.replace(tmp, deploy_root / name)
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/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).
|
||||
|
||||
RELEASE-SAFETY HARDENING (2026-09-13, after red-CI promotions 0.66.28/29):
|
||||
the verdict is the AND of two independent legs —
|
||||
1. required-checks context (newest observation for this exact SHA);
|
||||
2. the latest COMPLETED ci.yml@refs/heads/master run for this exact SHA,
|
||||
every job concluded success (branch+tag fan-out resolved by path;
|
||||
jobs envelope unwrapped — no aggregate-commit-status fallback, the
|
||||
exact bug class verify.py hit in W/R-v0.3).
|
||||
Any disagreement, pending, or missing evidence → pending/failure.
|
||||
FAIL-CLOSED: not verified → not deployable.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
REQUIRED_CONTEXT = "CI Pipeline / required-checks"
|
||||
CI_WORKFLOW_PATH = "ci.yml@refs/heads/master"
|
||||
|
||||
|
||||
# ── pure verdict logic (unit-tested; fixtures from the real incidents) ────
|
||||
|
||||
def required_check_verdict(statuses: list[dict]) -> str:
|
||||
"""Newest non-skipped required-checks observation for the SHA.
|
||||
Missing → pending. Newest wins (stale success never overrides a newer
|
||||
failure, and vice versa)."""
|
||||
candidates = [
|
||||
s for s in statuses
|
||||
if str(s.get("context", "")).startswith(REQUIRED_CONTEXT)
|
||||
and s.get("status") != "skipped"
|
||||
]
|
||||
if not candidates:
|
||||
return "pending"
|
||||
newest = max(candidates, key=lambda s: str(s.get("created_at", "")))
|
||||
return str(newest.get("status", "pending"))
|
||||
|
||||
|
||||
def independent_jobs_verdict(runs: list[dict], jobs_fetch, sha: str = "") -> str:
|
||||
"""Latest COMPLETED ci.yml@refs/heads/master run for the exact SHA:
|
||||
every job must conclude success. Fan-out-safe (tag runs and sibling
|
||||
workflows are excluded by path); the run is resolved by head_sha, never
|
||||
by 'latest branch state'. Missing run → pending. Any non-success
|
||||
conclusion → failure."""
|
||||
mine = [r for r in runs if r.get("path") == CI_WORKFLOW_PATH]
|
||||
if sha:
|
||||
mine = [r for r in mine if str(r.get("head_sha", "")) == sha]
|
||||
completed = [r for r in mine if r.get("status") == "completed"]
|
||||
if not completed:
|
||||
return "pending"
|
||||
latest = max(completed, key=lambda r: str(r.get("created_at", "") or r.get("run_started_at", "")))
|
||||
jobs = jobs_fetch(latest.get("id"))
|
||||
if jobs is None:
|
||||
return "pending" # jobs probe failed → not verified
|
||||
if not jobs:
|
||||
return "pending" # no jobs visible → cannot verify
|
||||
for j in jobs:
|
||||
if j.get("conclusion") != "success":
|
||||
return "failure"
|
||||
return "success"
|
||||
|
||||
|
||||
def combine(a: str, b: str) -> str:
|
||||
"""Fail-closed conjunction: any failure → failure; any pending → pending;
|
||||
success only when BOTH legs are success."""
|
||||
if "failure" in (a, b):
|
||||
return "failure"
|
||||
if "pending" in (a, b):
|
||||
return "pending"
|
||||
return "success"
|
||||
|
||||
|
||||
def qualify(statuses: list[dict], runs: list[dict], jobs_fetch, sha: str = "") -> str:
|
||||
return combine(required_check_verdict(statuses),
|
||||
independent_jobs_verdict(runs, jobs_fetch, sha=sha))
|
||||
|
||||
|
||||
# ── API fetches (thin; injectable in tests) ───────────────────────────────
|
||||
|
||||
def _get_json(url: str, token: str, timeout: int = 30):
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/commits/{sha}/status", token)
|
||||
return None if data is None else data.get("statuses", [])
|
||||
|
||||
|
||||
def _fetch_runs(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs?limit=50", token)
|
||||
if data is None:
|
||||
return None
|
||||
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
|
||||
return [r for r in runs if str(r.get("head_sha", "")) == sha]
|
||||
|
||||
|
||||
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs/{run_id}/jobs", token)
|
||||
if data is None:
|
||||
return None
|
||||
# jobs envelope unwrap (this Gitea wraps in {"jobs": [...]}; some
|
||||
# versions return a bare list) — never fall back to aggregate status
|
||||
if isinstance(data, dict):
|
||||
return data.get("jobs", [])
|
||||
return data if isinstance(data, list) else None
|
||||
|
||||
|
||||
# ── CLI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
|
||||
statuses = _fetch_status(server, repo, sha, token)
|
||||
runs = _fetch_runs(server, repo, sha, token)
|
||||
if statuses is None or runs is None:
|
||||
# transient API/network failure → keep polling
|
||||
print("pending (probe failed)")
|
||||
return 0
|
||||
jobs_fetch = lambda run_id: _fetch_jobs(server, repo, run_id, token) # noqa: E731
|
||||
verdict = qualify(statuses, runs, jobs_fetch, sha=sha)
|
||||
print(verdict)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/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 promotes
|
||||
through the MONOTONIC promotion authority (deploy_promotion.py): an older
|
||||
candidate finishing after a newer promoted one is fenced out (STALE_SUPERSEDED)
|
||||
and reports truthfully — build/stage success != promotion success.
|
||||
|
||||
Exit 0 = PROMOTED and verified, or NOOP_ALREADY_CURRENT, or a truthful
|
||||
STALE_SUPERSEDED (newer build already current; nothing mutated).
|
||||
Non-zero = deployment refused (divergent/unknown ordering, verification
|
||||
failure); 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: FENCED through the promotion authority -----------------
|
||||
# SCAFFOLD contract DEPLOY-MONOTONIC-001: promotion is monotonic. The
|
||||
# ancestry check + lock live in deploy_promotion.py; this script only
|
||||
# handles the truthful outcome reporting. A stale candidate must NOT
|
||||
# claim promotion success — build/stage success != promotion success.
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import deploy_promotion # noqa: E402
|
||||
|
||||
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
print(f"PROMOTED: {short}")
|
||||
print("current ->", os.readlink(DEPLOY_ROOT / "current"))
|
||||
if (DEPLOY_ROOT / "last-good").is_symlink():
|
||||
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
|
||||
return 0
|
||||
if outcome == deploy_promotion.OUTCOME_NOOP:
|
||||
print(f"NOOP_ALREADY_CURRENT: {short} is the deployed commit")
|
||||
print(detail)
|
||||
return 0
|
||||
if outcome == deploy_promotion.OUTCOME_STALE:
|
||||
print(f"STALE_SUPERSEDED: {short} is older than the deployed commit — promotion refused (monotonic promotion)")
|
||||
print(detail)
|
||||
print("current unchanged:", os.readlink(DEPLOY_ROOT / "current"))
|
||||
return 0 # truthful non-error terminal state: the newer build won
|
||||
# divergent / unknown-current: fail closed
|
||||
print(f"REFUSED ({outcome}): {short} — {detail}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
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,16 +50,44 @@ 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]
|
||||
|
||||
|
||||
def _load_manifest() -> dict:
|
||||
"""Structural facts from SPINE (the single structural authority).
|
||||
|
||||
Phase D: reads the Spine-generated compatibility view (same schema as
|
||||
the old manual manifest). The view is OUTPUT — the authority is
|
||||
store/spine/current.json; direct edits to the view are flagged by
|
||||
`spine drift`. If a manual manifest is present (Spine not yet seeded
|
||||
in this checkout), it degrades gracefully to that legacy truth."""
|
||||
if os.path.exists(UMBRELLA_MANIFEST):
|
||||
with open(UMBRELLA_MANIFEST) as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
return data # generated view or manual manifest — both work
|
||||
return {"umbrellas": {}}
|
||||
|
||||
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
|
||||
|
||||
Answers, per component/module:
|
||||
what subsystem owns X? → SPINE store (structural authority)
|
||||
what does X depend on? → SPINE relationships (ingested from AST)
|
||||
who consumes X? → SPINE relationships (ingested from AST)
|
||||
what changes if X changes? → mirrors + dependent subsystems + consumers (SPINE)
|
||||
what proof obligations arise? → unit test, canary, ownership, umbrella gates
|
||||
which obligations are complete? → evidence check (test file exists, canary registered)
|
||||
|
||||
SPINE INTEGRATION: structural facts come from the Spine store — the single
|
||||
structural authority (ingested once from the umbrella manifest + AST).
|
||||
This script adds change-impact and proof-obligation reasoning on top; it
|
||||
no longer re-derives structure. No store → builds one (deterministic).
|
||||
|
||||
Commands:
|
||||
component <module> full card for one src/rune module
|
||||
impact <module> expected impact + obligations BEFORE a change
|
||||
obligations <module> obligation checklist with satisfied/pending state
|
||||
all [outfile] write the full projection JSON (default docs/)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import ast, json, sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src" / "rune"
|
||||
TESTS = ROOT / "tests"
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
from rune import spine # noqa: E402 — Spine IS the structural authority
|
||||
|
||||
|
||||
def load_spine() -> dict:
|
||||
"""Structural state from the Spine store (built on first use)."""
|
||||
doc = spine.load()
|
||||
if doc is None:
|
||||
doc = spine.build()
|
||||
spine.save(doc)
|
||||
return doc
|
||||
|
||||
|
||||
def module_imports(module: str) -> set[str]:
|
||||
"""Module names imported by src/rune/<module>.py (intra-package).
|
||||
|
||||
Includes relative imports (`from .x import y` — the dual-mode lazy
|
||||
pattern used across the runtime; missing these would hide real
|
||||
consumers, observed first run: rune_operator ↔ local_tool_runtime)."""
|
||||
path = SRC / f"{module}.py"
|
||||
if not path.is_file():
|
||||
return set()
|
||||
tree = ast.parse(path.read_text())
|
||||
names: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
names.add(a.name.split(".")[0])
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
names.add(node.module.split(".")[0])
|
||||
return names
|
||||
|
||||
|
||||
def import_graph() -> dict[str, set[str]]:
|
||||
return {p.stem: module_imports(p.stem) for p in SRC.glob("*.py")}
|
||||
|
||||
|
||||
def consumers_of(module: str, graph: dict[str, set[str]]) -> set[str]:
|
||||
return {m for m, deps in graph.items() if module in deps and m != module}
|
||||
|
||||
|
||||
def subsystem_for(doc: dict, module: str) -> str:
|
||||
rels = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
||||
return rels[0]["to"].split(":", 1)[1] if rels else ""
|
||||
|
||||
|
||||
def dependent_subsystems(doc: dict, subsystem: str) -> list[str]:
|
||||
return [r["from"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, to_id=f"subsystem:{subsystem}", type="depends_on")]
|
||||
|
||||
|
||||
def mirrors_for(doc: dict, module: str) -> list[str]:
|
||||
subs = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
|
||||
out = []
|
||||
for s in subs:
|
||||
out.extend(r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=s["to"], type="mirrors"))
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def canary_registered(module: str) -> bool:
|
||||
try:
|
||||
sys.path.insert(0, str(SRC))
|
||||
from rune import canary_native
|
||||
return module in canary_native.MODULES
|
||||
except ImportError:
|
||||
# canary_native pulls optional deps (sqlite_vec etc.); the projection
|
||||
# degrades to "not registered" rather than failing the audit.
|
||||
return False
|
||||
|
||||
|
||||
def obligations(module: str, doc: dict) -> list[dict]:
|
||||
"""Proof obligations for changing this module, with evidence state.
|
||||
Structural facts sourced from Spine."""
|
||||
umb = subsystem_for(doc, module)
|
||||
test_file = TESTS / f"test_{module}.py"
|
||||
obs = [
|
||||
{"obligation": "unit_test", "satisfied": test_file.is_file(),
|
||||
"evidence": f"tests/test_{module}.py" + ("" if test_file.is_file() else " (MISSING)")},
|
||||
{"obligation": "canary", "satisfied": canary_registered(module),
|
||||
"evidence": "canary_native.MODULES" if canary_registered(module)
|
||||
else "not in canary registry (informational — not every module needs one)"},
|
||||
{"obligation": "umbrella_manifest", "satisfied": bool(umb),
|
||||
"evidence": f"umbrella '{umb}'" if umb else "NOT in any umbrella — manifest gap"},
|
||||
{"obligation": "full_suite", "satisfied": None,
|
||||
"evidence": "run: pytest tests/ -q (runtime evidence, not static)"},
|
||||
{"obligation": "lint_ratchet", "satisfied": None,
|
||||
"evidence": "run: scripts/lint_ratchet.py"},
|
||||
{"obligation": "henxels", "satisfied": None,
|
||||
"evidence": "run: henxels check --all"},
|
||||
]
|
||||
if umb:
|
||||
deps = dependent_subsystems(doc, umb)
|
||||
if deps:
|
||||
obs.append({"obligation": "dependent_canaries", "satisfied": None,
|
||||
"evidence": f"subsystems depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
|
||||
mir = mirrors_for(doc, module)
|
||||
if mir:
|
||||
obs.append({"obligation": "mirrors_travel", "satisfied": None,
|
||||
"evidence": f"files that must travel together: {', '.join(mir)}"})
|
||||
return obs
|
||||
|
||||
|
||||
def component_card(module: str, doc: dict) -> dict:
|
||||
"""Structural card from SPINE (owner/deps/consumers/mirrors) + obligations."""
|
||||
deps = [r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=f"module:{module}", type="depends_on")]
|
||||
consumers = [r["to"].split(":", 1)[1] for r in
|
||||
spine.relationships(doc, from_id=f"module:{module}", type="consumed_by")]
|
||||
e = spine.entity(doc, f"module:{module}")
|
||||
card = {
|
||||
"component": module,
|
||||
"subsystem": subsystem_for(doc, module),
|
||||
"dependencies": sorted(set(deps)),
|
||||
"consumers": sorted(set(consumers)),
|
||||
"mirrors": mirrors_for(doc, module),
|
||||
"obligations": obligations(module, doc),
|
||||
}
|
||||
if e:
|
||||
card["spine_maturity"] = e.get("maturity")
|
||||
card["spine_evidence"] = e.get("evidence_refs", [])
|
||||
return card
|
||||
|
||||
|
||||
def impact(module: str) -> dict:
|
||||
doc = load_spine()
|
||||
card = component_card(module, doc)
|
||||
umb = card["subsystem"]
|
||||
card["expected_impact"] = {
|
||||
"direct_consumers": card["consumers"],
|
||||
"subsystem_dependents": dependent_subsystems(doc, umb) if umb else [],
|
||||
"must_travel": card["mirrors"],
|
||||
"gates": ["full_suite", "lint_ratchet", "henxels", "umbrella_gates"]
|
||||
+ (["canary"] if card and any(o["obligation"] == "canary" and o["satisfied"]
|
||||
for o in card["obligations"]) else []),
|
||||
}
|
||||
return card
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print(__doc__)
|
||||
return 2
|
||||
cmd = args[0]
|
||||
if cmd in ("component", "impact", "obligations") and len(args) < 2:
|
||||
print(f"usage: {cmd} <module>", file=sys.stderr)
|
||||
return 2
|
||||
doc = load_spine()
|
||||
if cmd == "impact":
|
||||
print(json.dumps(impact(args[1]), indent=1))
|
||||
elif cmd == "component":
|
||||
print(json.dumps(component_card(args[1], doc), indent=1))
|
||||
elif cmd == "obligations":
|
||||
card = component_card(args[1], doc)
|
||||
print(json.dumps(card["obligations"], indent=1))
|
||||
elif cmd == "all":
|
||||
out = Path(args[1]) if len(args) > 1 else ROOT / "docs" / "scaffold_projection.json"
|
||||
cards = {p.stem: component_card(p.stem, doc)
|
||||
for p in sorted(SRC.glob("*.py"))
|
||||
if spine.entity(doc, f"module:{p.stem}")}
|
||||
out.write_text(json.dumps({
|
||||
"generated_by": "scripts/scaffold_projection.py (projection — no new authority)",
|
||||
"sources": ["rune.spine store (structural authority)", "src/rune/canary_native.py",
|
||||
"tests/ filesystem"],
|
||||
"components": cards,
|
||||
}, indent=1) + "\n")
|
||||
print(f"projection written: {out} ({len(cards)} components)")
|
||||
else:
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+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.65.0"
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.44"
|
||||
|
||||
# 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.65.0"' in rune_version,
|
||||
'__version__ = "0.66.44"' 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.
|
||||
@@ -96,6 +93,31 @@ def main() -> int:
|
||||
"if: always()" in required_gate and "needs: [lint, unit-tests, canary" in required_gate,
|
||||
failures,
|
||||
)
|
||||
# RELEASE-SAFETY (2026-09-13, after 0.66.28/29 red promotions): the
|
||||
# required-checks publish step previously echoed success UNCONDITIONALLY
|
||||
# (read /tmp/need_* files that no step wrote). The verdict MUST come
|
||||
# from the needs context — this canary locks that class shut.
|
||||
publish_section = required_gate.split("Publish protected-branch gate", 1)[-1]
|
||||
check(
|
||||
"required gate verdict evaluates needs results (not unconditional echo)",
|
||||
"needs.lint.result == 'success'" in publish_section
|
||||
and "needs['type-check'].result == 'success'" in publish_section
|
||||
and "needs['unit-tests'].result == 'success'" in publish_section,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"required gate fails closed when any check is not success",
|
||||
"!= 'success'" in publish_section and "exit 1" in publish_section,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"no unconditional success echo without a needs-conditioned if",
|
||||
not (
|
||||
"All required CI checks passed" in publish_section
|
||||
and "needs.lint.result == 'success'" not in publish_section
|
||||
),
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"required gate depends on unit tests and verification jobs",
|
||||
"needs: [lint, unit-tests, canary" in text,
|
||||
@@ -113,8 +135,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 +146,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,
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
"""capability_fit.py — Requirement-fit capability discovery + gap decision.
|
||||
|
||||
SCAFFOLD-TRANCHE-003. Layer over the Tranche 2 substrate (structural_store,
|
||||
scaffold, spine). NOT a second graph: discovery queries existing structural
|
||||
authorities (Rune Spine / Project Scaffold stores) and produces candidates;
|
||||
fit and resolution are derived verdicts with recorded evidence.
|
||||
|
||||
Architecture:
|
||||
|
||||
CapabilityRequirement (open-world object)
|
||||
↓
|
||||
discover_candidates(store_doc) DETERMINISTIC — structural only
|
||||
↓
|
||||
evaluate_fit(requirement, candidate) DETERMINISTIC first;
|
||||
bounded LLM seam recorded, never
|
||||
authoritative alone
|
||||
↓
|
||||
decide(requirement, fits) resolution surface:
|
||||
SATISFIED / REUSE / CONFIGURE /
|
||||
COMPOSE / ADAPT_EXISTING /
|
||||
ACQUIRE_EXTERNAL / BUILD_NEW /
|
||||
INSUFFICIENT_EVIDENCE
|
||||
|
||||
Core invariants (tranche brief §5, §27):
|
||||
- BUILD_NEW is NEVER the default and is never silently chosen from
|
||||
insufficient evidence.
|
||||
- Structural discovery produces CANDIDATES; names alone are never fit.
|
||||
- A stale/superseded/degraded candidate is not silently reusable.
|
||||
- BUILD BUT DON'T BREAK: adaptation selected only when existing consumers
|
||||
are provably unaffected; brittle coupling forces BUILD_NEW (with
|
||||
rationale) or COMPOSE.
|
||||
- When an LLM contributes interpretation, provider/model/reason/evidence/
|
||||
confidence are RECORDED; its prose alone never establishes authority.
|
||||
- Memory is never a structural authority here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune.structural_store import entity, relationships, resolve_status
|
||||
|
||||
__all__ = [
|
||||
"CapabilityRequirement",
|
||||
"RESOLUTIONS",
|
||||
"discover_candidates",
|
||||
"evaluate_fit",
|
||||
"decide",
|
||||
"record_semantic_fit",
|
||||
]
|
||||
|
||||
RESOLUTIONS = (
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"CONFIGURE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"ACQUIRE_EXTERNAL",
|
||||
"BUILD_NEW",
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
)
|
||||
|
||||
# relationship types treated as "candidate implements/exposes this contract"
|
||||
_PROVIDES = {"implements", "exposes", "provides", "serves"}
|
||||
_CONSUMES = {"consumes", "depends_on"}
|
||||
# entity kinds eligible as capability candidates (open-world: unknown kinds
|
||||
# with providing relationships are also eligible — see discover_candidates)
|
||||
_BASE_KINDS = {"capability", "module", "component", "service", "cli", "tool", "workflow"}
|
||||
# maturity rank for reuse-worthiness (shared ladder from structural_store)
|
||||
_MATURITY_ORDER = ["DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN"]
|
||||
_MATURITY_RANK = {n: i for i, n in enumerate(_MATURITY_ORDER)}
|
||||
|
||||
|
||||
# ── Capability Requirement ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class CapabilityRequirement:
|
||||
"""Open-world structured requirement. No fixed ontology: `props` carries
|
||||
any constraint the need expresses; a small set of FIRST-CLASS keys are
|
||||
interpreted deterministically (contracts, environment, mandatory) and
|
||||
everything else roundtrips as data."""
|
||||
|
||||
FIRST_CLASS = (
|
||||
"objective",
|
||||
"contracts",
|
||||
"environment",
|
||||
"mandatory",
|
||||
"optional",
|
||||
"target_project",
|
||||
"inputs",
|
||||
"outputs",
|
||||
"performance",
|
||||
"reliability",
|
||||
"deployment",
|
||||
"integration",
|
||||
"user_constraints",
|
||||
"evidence_refs",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
objective: str,
|
||||
*,
|
||||
contracts: list[str] | None = None,
|
||||
environment: dict | None = None,
|
||||
mandatory: list[str] | None = None,
|
||||
optional: list[str] | None = None,
|
||||
**props,
|
||||
):
|
||||
self.objective = objective
|
||||
self.contracts = sorted(contracts or []) # required interfaces
|
||||
self.environment = dict(environment or {}) # e.g. {"os": "linux"}
|
||||
self.mandatory = sorted(mandatory or [])
|
||||
self.optional = sorted(optional or [])
|
||||
self.props = dict(props) # open-world
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
"objective": self.objective,
|
||||
"contracts": self.contracts,
|
||||
"environment": self.environment,
|
||||
"mandatory": self.mandatory,
|
||||
"optional": self.optional,
|
||||
}
|
||||
d.update(self.props)
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "CapabilityRequirement":
|
||||
known = {k: d.pop(k) for k in cls.FIRST_CLASS if k in d}
|
||||
return cls(known.pop("objective"), **{**known, **d})
|
||||
|
||||
|
||||
# ── semantic-fit seam (recorded, never authoritative alone) ───────────────
|
||||
|
||||
|
||||
def record_semantic_fit(
|
||||
provider: str, model: str, reason: str, evidence: list[str], confidence: float, interpretation: str
|
||||
) -> dict:
|
||||
"""Record a bounded LLM semantic interpretation contributing to fit.
|
||||
The record is EVIDENCE, not authority: decide() treats it as supporting
|
||||
context that must agree with deterministic facts. Never silent."""
|
||||
if not provider or not model:
|
||||
raise ValueError("semantic fit record requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
raise ValueError("confidence must be in [0,1]")
|
||||
return {
|
||||
"kind": "semantic_fit",
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"reason": reason,
|
||||
"evidence": sorted(evidence),
|
||||
"confidence": confidence,
|
||||
"interpretation": interpretation[:500],
|
||||
}
|
||||
|
||||
|
||||
# ── structural discovery (deterministic) ──────────────────────────────────
|
||||
|
||||
|
||||
def _provides_contracts(doc: dict, eid: str) -> set[str]:
|
||||
out = set()
|
||||
for r in relationships(doc, from_id=eid):
|
||||
if r["type"] in _PROVIDES:
|
||||
out.add(r["to"])
|
||||
return out
|
||||
|
||||
|
||||
def _consumers(doc: dict, eid: str) -> set[str]:
|
||||
out = set()
|
||||
for r in relationships(doc, to_id=eid):
|
||||
if r["type"] in {"consumes", "consumed_by", "depends_on"}:
|
||||
out.add(r["from"])
|
||||
return out
|
||||
|
||||
|
||||
def discover_candidates(doc: dict, req: CapabilityRequirement) -> list[dict]:
|
||||
"""Deterministic candidate discovery over ONE structural store doc.
|
||||
A candidate is any entity that (a) is a known capability-ish kind, OR
|
||||
(b) has providing relationships (open-world kinds stay discoverable).
|
||||
Returns candidates with their structural evidence attached. Names are
|
||||
NOT fit — every candidate must still pass evaluate_fit."""
|
||||
candidates = []
|
||||
for e in doc["entities"]:
|
||||
eid = e["id"]
|
||||
provides = _provides_contracts(doc, eid)
|
||||
eligible = e["kind"] in _BASE_KINDS or bool(provides)
|
||||
if not eligible:
|
||||
continue
|
||||
# environment filter: candidate must not CONTRADICT a stated
|
||||
# environment constraint (missing info is not contradiction)
|
||||
env = e.get("identities", {}).get("environment", {})
|
||||
if isinstance(env, dict) and req.environment:
|
||||
contradicts = any(k in env and req.environment.get(k) not in (None, env[k]) for k in req.environment)
|
||||
if contradicts:
|
||||
continue
|
||||
candidates.append(
|
||||
{
|
||||
"id": eid,
|
||||
"kind": e["kind"],
|
||||
"identities": e.get("identities", {}),
|
||||
"maturity": e.get("maturity", "DECLARED"),
|
||||
"status": resolve_status(e),
|
||||
"superseded_by": e.get("superseded_by"),
|
||||
"provides": sorted(provides),
|
||||
"consumers": sorted(_consumers(doc, eid)),
|
||||
"evidence_refs": e.get("evidence_refs", []),
|
||||
}
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
# ── fit evaluation ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def evaluate_fit(req: CapabilityRequirement, candidate: dict) -> dict:
|
||||
"""DETERMINISTIC fit of one candidate against a requirement.
|
||||
fit: EXACT | PARTIAL | INCOMPATIBLE | UNKNOWN.
|
||||
Names are ignored; contracts + environment + health decide."""
|
||||
missing_contracts = sorted(set(req.contracts) - set(candidate["provides"]))
|
||||
covered = len(req.contracts) - len(missing_contracts)
|
||||
|
||||
# health: a superseded or stale candidate cannot be reused as-is
|
||||
status = candidate["status"]
|
||||
unhealthy = status in {"SUPERSEDED", "STALE"} or bool(candidate.get("superseded_by"))
|
||||
|
||||
# maturity: DECLARED-only candidates have no usage evidence
|
||||
maturity = _MATURITY_RANK.get(candidate.get("maturity", "DECLARED"), 0)
|
||||
|
||||
if unhealthy:
|
||||
return {
|
||||
"candidate": candidate["id"],
|
||||
"fit": "INCOMPATIBLE",
|
||||
"reason": f"candidate status {status} — not reusable as-is",
|
||||
"missing_contracts": missing_contracts,
|
||||
"covered": covered,
|
||||
}
|
||||
if req.contracts and not candidate["provides"]:
|
||||
# candidate provides nothing verifiable → cannot establish fit
|
||||
return {
|
||||
"candidate": candidate["id"],
|
||||
"fit": "UNKNOWN",
|
||||
"reason": "candidate exposes no contracts to check",
|
||||
"missing_contracts": missing_contracts,
|
||||
"covered": 0,
|
||||
}
|
||||
if not missing_contracts:
|
||||
fit = "EXACT"
|
||||
reason = "all required contracts provided"
|
||||
elif covered > 0:
|
||||
fit = "PARTIAL"
|
||||
reason = f"covers {covered}/{len(req.contracts)} required contracts"
|
||||
else:
|
||||
fit = "INCOMPATIBLE"
|
||||
reason = "provides none of the required contracts"
|
||||
return {
|
||||
"candidate": candidate["id"],
|
||||
"fit": fit,
|
||||
"reason": reason,
|
||||
"missing_contracts": missing_contracts,
|
||||
"covered": covered,
|
||||
"maturity_rank": maturity,
|
||||
"consumers": candidate["consumers"],
|
||||
}
|
||||
|
||||
|
||||
# ── gap / resolution decision ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def _adaptation_brittle(doc: dict, candidate_id: str, missing_contracts: list[str]) -> tuple[bool, str]:
|
||||
"""BUILD BUT DON'T BREAK: adaptation is brittle when the contracts the
|
||||
candidate is missing are load-bearing elsewhere (other entities consume
|
||||
or provide them), i.e. adapting means touching a shared surface."""
|
||||
for c in missing_contracts:
|
||||
ce = entity(doc, c)
|
||||
if ce is not None:
|
||||
consumers = _consumers(doc, c)
|
||||
consumers.discard(candidate_id)
|
||||
if consumers:
|
||||
return True, (f"missing contract {c} is load-bearing (consumed by {sorted(consumers)})")
|
||||
for r in relationships(doc, to_id=c):
|
||||
if r["type"] in _PROVIDES and r["from"] != candidate_id:
|
||||
return True, (f"missing contract {c} is already provided by {r['from']} — adapting would fork it")
|
||||
return False, ""
|
||||
|
||||
|
||||
def decide(
|
||||
doc: dict,
|
||||
req: CapabilityRequirement,
|
||||
fits: list[dict],
|
||||
*,
|
||||
external_evidence: list[dict] | None = None,
|
||||
semantic_fits: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""Resolution decision from deterministic fits. Evidence-backed:
|
||||
every verdict carries the fits/rationale that produced it."""
|
||||
external_evidence = list(external_evidence or [])
|
||||
semantic_fits = list(semantic_fits or [])
|
||||
exact = [f for f in fits if f["fit"] == "EXACT"]
|
||||
partial = [f for f in fits if f["fit"] == "PARTIAL"]
|
||||
unknown = [f for f in fits if f["fit"] == "UNKNOWN"]
|
||||
incompatible = [f for f in fits if f["fit"] == "INCOMPATIBLE"]
|
||||
|
||||
def _result(resolution, rationale, **kw):
|
||||
base = {
|
||||
"resolution": resolution,
|
||||
"rationale": rationale,
|
||||
"considered": [f["candidate"] for f in fits],
|
||||
"semantic_fits": semantic_fits,
|
||||
}
|
||||
base.update(kw)
|
||||
assert resolution in RESOLUTIONS
|
||||
return base
|
||||
|
||||
# INSUFFICIENT EVIDENCE dominates when fit cannot be established
|
||||
if unknown and not exact and not partial:
|
||||
return _result(
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"candidates exist but expose no checkable contracts; no deterministic or recorded semantic evidence",
|
||||
unknown_candidates=[f["candidate"] for f in unknown],
|
||||
)
|
||||
if not fits:
|
||||
# nothing structural at all: external evidence decides the route
|
||||
if external_evidence:
|
||||
return _result(
|
||||
"ACQUIRE_EXTERNAL",
|
||||
"no structural candidates; external evidence suggests acquisition investigation",
|
||||
external=external_evidence,
|
||||
)
|
||||
return _result(
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"no candidates discovered and no external evidence — BUILD_NEW not silently chosen (§27 J)",
|
||||
)
|
||||
|
||||
# EXACT: already satisfied vs reuse
|
||||
if exact:
|
||||
best = max(exact, key=lambda f: f.get("maturity_rank", 0))
|
||||
if req.props.get("already_consumed_by") and req.props["already_consumed_by"] in best.get("consumers", []):
|
||||
return _result(
|
||||
"SATISFIED",
|
||||
f"{best['candidate']} exactly matches and the requirement's context already consumes it",
|
||||
selected=best["candidate"],
|
||||
fits=exact,
|
||||
)
|
||||
return _result(
|
||||
"REUSE",
|
||||
f"{best['candidate']} exactly matches all required contracts with health evidence",
|
||||
selected=best["candidate"],
|
||||
fits=exact,
|
||||
)
|
||||
|
||||
# PARTIAL: compose, adapt (clean), or build
|
||||
if partial:
|
||||
# COMPOSITION: union of ≥2 partials covers the requirement
|
||||
if len(partial) >= 2:
|
||||
union: set[str] = set()
|
||||
chosen: list[str] = []
|
||||
for f in sorted(partial, key=lambda x: -x["covered"]):
|
||||
new = set(req.contracts) - union
|
||||
if new & set(req.contracts) and not (set(f["missing_contracts"]) >= set(req.contracts)):
|
||||
add = [c for c in req.contracts if c not in union and c not in f["missing_contracts"]]
|
||||
if add:
|
||||
chosen.append(f["candidate"])
|
||||
union.update(add)
|
||||
if union >= set(req.contracts):
|
||||
break
|
||||
if union >= set(req.contracts):
|
||||
return _result(
|
||||
"COMPOSE",
|
||||
f"composition {chosen} jointly covers all required contracts; none covers alone",
|
||||
components=chosen,
|
||||
permanence="temporary (user-controlled)",
|
||||
)
|
||||
# ADAPT: best partial, if adaptation is clean
|
||||
best = max(partial, key=lambda f: f["covered"])
|
||||
brittle, why = _adaptation_brittle(doc, best["candidate"], best["missing_contracts"])
|
||||
if not brittle:
|
||||
return _result(
|
||||
"ADAPT_EXISTING",
|
||||
f"{best['candidate']} covers {best['covered']}/"
|
||||
f"{len(req.contracts)}; adaptation clean ({best['reason']})",
|
||||
selected=best["candidate"],
|
||||
missing=best["missing_contracts"],
|
||||
obligations={"new_behavior_proof": True, "regression_proof": True},
|
||||
)
|
||||
if external_evidence:
|
||||
return _result(
|
||||
"ACQUIRE_EXTERNAL",
|
||||
f"best partial ({best['candidate']}) cannot be "
|
||||
f"adapted without brittle coupling ({why}); "
|
||||
f"external evidence available",
|
||||
external=external_evidence,
|
||||
)
|
||||
return _result(
|
||||
"BUILD_NEW",
|
||||
f"best partial ({best['candidate']}) would create brittle "
|
||||
f"coupling: {why}; composition insufficient; building is the "
|
||||
f"safer path — REUSE-WHEN-CLEAN, BUILD-WHEN-FORCED-REUSE-"
|
||||
f"WOULD-BRITTLE (§5)",
|
||||
selected=None,
|
||||
rejected_partial=best["candidate"],
|
||||
brittle=why,
|
||||
obligations={"new_behavior_proof": True},
|
||||
)
|
||||
|
||||
# everything incompatible
|
||||
if external_evidence:
|
||||
return _result(
|
||||
"ACQUIRE_EXTERNAL", "all candidates incompatible; external evidence available", external=external_evidence
|
||||
)
|
||||
if incompatible:
|
||||
return _result(
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"all candidates incompatible with requirement "
|
||||
"constraints; affirmative gap evidence required "
|
||||
"before BUILD_NEW",
|
||||
incompatible=[f["candidate"] for f in incompatible],
|
||||
)
|
||||
return _result("INSUFFICIENT_EVIDENCE", "no usable fit evidence")
|
||||
|
||||
|
||||
def resolve(
|
||||
doc: dict,
|
||||
req: CapabilityRequirement,
|
||||
*,
|
||||
external_evidence: list[dict] | None = None,
|
||||
semantic_fits: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""One-call surface: discover → fit → decide."""
|
||||
fits = [evaluate_fit(req, c) for c in discover_candidates(doc, req)]
|
||||
return decide(doc, req, fits, external_evidence=external_evidence, semantic_fits=semantic_fits)
|
||||
+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,
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
"""construction_decision.py — live Need → Decision → Construction Contract.
|
||||
|
||||
SCAFFOLD-TRANCHE-004: the adoption layer that turns the Tranche 2/3
|
||||
mechanisms into a durable procedure Rune no longer has to remember.
|
||||
|
||||
NEED (references Task/Intent — never duplicates them)
|
||||
↓ normalize_need() missing info is NOT invented
|
||||
↓ discover + fit per authority (deterministic; Spine + Project Scaffold)
|
||||
↓ resolve per authority + merge (capability_fit resolution surface)
|
||||
↓ resolve_toolchain() (existing project toolchain wins)
|
||||
↓ layout obligations (LOCKED authority inherited)
|
||||
↓ approval gate (material changes → user)
|
||||
↓ DecisionTrace (durable, model-independent)
|
||||
↓ generate_contract() (populated from the trace)
|
||||
|
||||
Boundaries (brief §3, §32, §33):
|
||||
- NOT a task engine: Need carries a reference to the existing Task/Intent
|
||||
authority; Scaffolding never becomes the "why" authority.
|
||||
- NOT a workflow engine: one decision function, one trace object.
|
||||
- NOT a research pipeline: insufficient evidence routes to a recorded
|
||||
research path (seam) — the live ADAPT wiring is a recorded route, not
|
||||
an executed pipeline call (honest seam, §6).
|
||||
- NOT a structural authority: it reads Spine/Project Scaffold and
|
||||
WRITES only Construction Contracts + decision traces.
|
||||
- BUILD_NEW is never a fallback (§8): merge precedence and the
|
||||
insufficient-evidence invariant of capability_fit enforce it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune import capability_fit as cf
|
||||
from rune import toolchain as tc
|
||||
from rune.structural_store import relationships
|
||||
|
||||
__all__ = ["Need", "make_decision", "generate_contract", "merge_resolutions"]
|
||||
|
||||
|
||||
# ── Need intake (§4) ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Need:
|
||||
"""Construction-decision input. References — does not copy — the
|
||||
existing Task/Intent authority (`task_ref` is a pointer, not a copy).
|
||||
Carries no task state of its own: it is WHY-adjacent input to Scaffolding,
|
||||
not a backlog entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
need_id: str,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "user",
|
||||
project: str = "rune",
|
||||
task_ref: str = "",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
requested_permanence: str = "",
|
||||
):
|
||||
if not need_id:
|
||||
raise ValueError("need_id required")
|
||||
if not objective:
|
||||
raise ValueError("objective (required effect) required — do not normalize a need we cannot state")
|
||||
self.need_id = need_id
|
||||
self.objective = objective
|
||||
self.origin = origin
|
||||
self.project = project
|
||||
self.task_ref = task_ref
|
||||
self.constraints = dict(constraints or {})
|
||||
self.affected_surface = list(affected_surface or [])
|
||||
self.evidence_refs = list(evidence_refs or [])
|
||||
self.approval_context = approval_context
|
||||
self.requested_permanence = requested_permanence
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"need_id": self.need_id,
|
||||
"objective": self.objective,
|
||||
"origin": self.origin,
|
||||
"project": self.project,
|
||||
"task_ref": self.task_ref,
|
||||
"constraints": dict(self.constraints),
|
||||
"affected_surface": list(self.affected_surface),
|
||||
"evidence_refs": list(self.evidence_refs),
|
||||
"approval_context": self.approval_context,
|
||||
"requested_permanence": self.requested_permanence,
|
||||
}
|
||||
|
||||
|
||||
# ── requirement normalization (§5) ─────────────────────────────────────────
|
||||
|
||||
|
||||
def normalize_need(need: Need) -> tuple[cf.CapabilityRequirement, list[str]]:
|
||||
"""Derive the CapabilityRequirement WITHOUT inventing facts. Contracts
|
||||
may come from explicit constraints (`contracts` key) or from contract-
|
||||
shaped entries on the affected surface (`contract:x` ids). Everything
|
||||
not determinable lands in `missing` — the caller records it and the
|
||||
resolution degrades to INSUFFICIENT_EVIDENCE rather than guessing."""
|
||||
contracts = sorted(need.constraints.get("contracts", []))
|
||||
contracts += sorted(a for a in need.affected_surface if a.startswith("contract:"))
|
||||
missing = []
|
||||
if need.constraints.get("environment"):
|
||||
pass # carried through
|
||||
else:
|
||||
missing.append("environment")
|
||||
if not need.evidence_refs:
|
||||
missing.append("evidence_refs")
|
||||
req = cf.CapabilityRequirement(
|
||||
need.objective,
|
||||
contracts=contracts,
|
||||
environment=need.constraints.get("environment", {}),
|
||||
target_project=need.project,
|
||||
source_need=need.need_id,
|
||||
task_ref=need.task_ref,
|
||||
requested_permanence=need.requested_permanence,
|
||||
**{k: v for k, v in need.constraints.items() if k not in ("contracts", "environment")},
|
||||
)
|
||||
return req, missing
|
||||
|
||||
|
||||
# ── affected-surface classification + resolution (Tranche 5 §3-5) ──────────
|
||||
|
||||
_SURFACE_UI = "ui:"
|
||||
_SURFACE_REGION = "region:"
|
||||
_SURFACE_CONTRACT = "contract:"
|
||||
_SURFACE_MODULE = "module:"
|
||||
|
||||
|
||||
def classify_surface(surface: str) -> str:
|
||||
"""Classify one affected-surface entry (open-world; Tranche 5 §3).
|
||||
Kinds: structural_entity | source_path | workflow_config | ui_component |
|
||||
layout_region | unresolved. Never drops an entry — unresolved is a
|
||||
first-class kind."""
|
||||
if surface.startswith((_SURFACE_UI,)):
|
||||
return "ui_component"
|
||||
if surface.startswith((_SURFACE_REGION,)):
|
||||
return "layout_region"
|
||||
if surface.startswith(
|
||||
(
|
||||
_SURFACE_CONTRACT,
|
||||
_SURFACE_MODULE,
|
||||
_SURFACE_CAPABILITY,
|
||||
)
|
||||
):
|
||||
return "structural_entity"
|
||||
if surface.endswith(".yml") or surface.endswith(".yaml") or surface.endswith(".json") or surface.endswith(".toml"):
|
||||
return "workflow_config"
|
||||
if (
|
||||
surface.endswith(".py")
|
||||
or "/" in surface
|
||||
or surface.endswith(".md")
|
||||
or surface.endswith(".rs")
|
||||
or surface.endswith(".ts")
|
||||
):
|
||||
return "source_path"
|
||||
return "unresolved"
|
||||
|
||||
|
||||
_SURFACE_CAPABILITY = "capability:"
|
||||
|
||||
|
||||
def resolve_surface(surface: str, kind: str, authorities: dict[str, dict]) -> dict:
|
||||
"""Deterministically resolve one surface against the structural
|
||||
authorities (Tranche 5 §5). No Memory, no LLM archaeology: identity
|
||||
lookups and recorded path->module mappings only.
|
||||
|
||||
Returns {surface, kind, resolved: [...], unresolved_reason: ""}.
|
||||
resolved entries: {entity_id, authority, via} where via records HOW the
|
||||
mapping was established (identity | path_attribute | provides)."""
|
||||
out = {"surface": surface, "kind": kind, "resolved": [], "unresolved_reason": ""}
|
||||
if kind == "structural_entity":
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, surface)
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": surface, "authority": name, "via": "identity"})
|
||||
break
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "entity id not found in any authority"
|
||||
elif kind == "source_path":
|
||||
# path -> module via recorded identities.path in any authority
|
||||
for name, doc in authorities.items():
|
||||
for e in doc["entities"]:
|
||||
p = e.get("identities", {}).get("path", "")
|
||||
if p and (p == surface or p.endswith("/" + surface) or surface.endswith(p)):
|
||||
out["resolved"].append({"entity_id": e["id"], "authority": name, "via": "path_attribute"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "no authority records this path as an entity (path remains file-scope only)"
|
||||
elif kind == "ui_component":
|
||||
for name, doc in authorities.items():
|
||||
cid = surface if surface.startswith("ui:") else f"ui:{surface}"
|
||||
e = ss_entity(doc, f"layout:{cid}")
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": f"layout:{cid}", "authority": name, "via": "layout_contract"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "no Layout Contract bound for component"
|
||||
elif kind == "layout_region":
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, surface if surface.startswith("region:") else f"region:{surface}")
|
||||
if e is not None:
|
||||
out["resolved"].append({"entity_id": e["id"], "authority": name, "via": "identity"})
|
||||
if not out["resolved"]:
|
||||
out["unresolved_reason"] = "region not defined in any authority"
|
||||
else:
|
||||
out["unresolved_reason"] = f"kind {kind} has no deterministic resolver"
|
||||
return out
|
||||
|
||||
|
||||
def ss_entity(doc: dict, eid: str):
|
||||
from rune.structural_store import entity as _e
|
||||
|
||||
return _e(doc, eid)
|
||||
|
||||
|
||||
def implemented_contracts(doc: dict, entity_id: str) -> list[str]:
|
||||
"""Contracts an entity implements/exposes/provides (structural fact)."""
|
||||
out = []
|
||||
for r in relationships(doc, from_id=entity_id):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"}:
|
||||
out.append(r["to"])
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def resolve_affected_surface(need: Need, authorities: dict[str, dict]) -> dict:
|
||||
"""Classify + resolve the WHOLE affected surface (Tranche 5 §3-5, §13).
|
||||
Produces the propagated representation the generated contract carries:
|
||||
affected_existing_entities structural ids (resolved)
|
||||
affected_paths file-scope entries (mechanical scope)
|
||||
affected_ui_surfaces ui components / regions found
|
||||
unresolved_surfaces retained, never dropped
|
||||
File scope != structural impact: a path stays a path for scope checking
|
||||
even when it also resolves to a module entity (no authority duplication)."""
|
||||
result = {
|
||||
"affected_existing_entities": [],
|
||||
"affected_paths": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"affected_regions": [],
|
||||
"workflow_config_surfaces": [],
|
||||
"unresolved_surfaces": [],
|
||||
"resolutions": [],
|
||||
}
|
||||
for surface in need.affected_surface:
|
||||
kind = classify_surface(surface)
|
||||
res = resolve_surface(surface, kind, authorities)
|
||||
result["resolutions"].append(res)
|
||||
if kind == "source_path":
|
||||
result["affected_paths"].append(surface)
|
||||
elif kind == "workflow_config":
|
||||
result["workflow_config_surfaces"].append(surface)
|
||||
elif kind == "ui_component":
|
||||
result["affected_ui_surfaces"].append(surface)
|
||||
elif kind == "layout_region":
|
||||
result["affected_regions"].append(surface)
|
||||
for r in res["resolved"]:
|
||||
eid = r["entity_id"]
|
||||
if eid not in result["affected_existing_entities"] and not eid.startswith("layout:"):
|
||||
result["affected_existing_entities"].append(eid)
|
||||
if res["unresolved_reason"] and not res["resolved"]:
|
||||
result["unresolved_surfaces"].append({"surface": surface, "kind": kind, "reason": res["unresolved_reason"]})
|
||||
for k in (
|
||||
"affected_existing_entities",
|
||||
"affected_paths",
|
||||
"affected_ui_surfaces",
|
||||
"affected_regions",
|
||||
"workflow_config_surfaces",
|
||||
):
|
||||
result[k] = sorted(set(result[k]))
|
||||
|
||||
# §9/§17 DERIVED EXPECTATION (structural consequence, not invention): a
|
||||
# Rune test path (tests/test_<name>.py) affected by the work implies the
|
||||
# "test:<name>" surface participates in the build. Derived only when not
|
||||
# already an existing entity nor explicitly declared; provenance records
|
||||
# the derivation for closure accounting.
|
||||
derived = []
|
||||
for surface in need.affected_surface:
|
||||
if surface.startswith("tests/test_") and surface.endswith(".py"):
|
||||
stem = surface[len("tests/test_") : -len(".py")]
|
||||
test_entity = f"test:{stem}"
|
||||
exists = any(ss_entity(doc, test_entity) is not None for doc in authorities.values())
|
||||
if test_entity not in need.constraints.get("expected_new_entities", []) and not exists:
|
||||
derived.append(
|
||||
{
|
||||
"entity": test_entity,
|
||||
"derived_from": surface,
|
||||
"rule": "Rune test-path convention: tests/test_<name>.py implies "
|
||||
"the test:<name> surface participates in the build",
|
||||
"evidence": f"affected_surface entry {surface}",
|
||||
}
|
||||
)
|
||||
result["derived_expected_new"] = derived
|
||||
return result
|
||||
|
||||
|
||||
# ── requirement completeness gate (Tranche 5 §6-9) ──────────────────────────
|
||||
|
||||
COMPLETE_ENOUGH = "COMPLETE_ENOUGH"
|
||||
MATERIAL_GAP = "MATERIAL_GAP"
|
||||
COMPLETENESS_UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
def check_completeness(
|
||||
req: cf.CapabilityRequirement, need: Need, authorities: dict[str, dict], surface_result: dict
|
||||
) -> dict:
|
||||
"""Bounded, structure-grounded completeness check run BEFORE fit.
|
||||
|
||||
Rule (deterministic, no model vibes): every affected-surface entry that
|
||||
resolves to a structural entity contributes that entity's IMPLEMENTED
|
||||
contracts to the requirement's material scope. If an affected entity
|
||||
implements contracts NOT represented in the normalized requirement, the
|
||||
requirement is materially incomplete — the need touches a component
|
||||
whose obligations it has not stated. That is exactly the deploy-incident
|
||||
shape: the affected promotion implementation carried an unrepresented
|
||||
ordering contract while the requirement named only qualification.
|
||||
|
||||
Verdicts (Tranche 5 §8):
|
||||
COMPLETE_ENOUGH all affected entities' contracts are represented
|
||||
(or none implement contracts) — fit may proceed
|
||||
MATERIAL_GAP uncovered implemented contracts exist on affected
|
||||
entities — named with provenance, routed to probe
|
||||
UNKNOWN no surface resolved — completeness undeterminable;
|
||||
never silently COMPLETE (§8)
|
||||
|
||||
Derived requirements (§9) are structural consequences, not inventions:
|
||||
each uncovered contract carries derived_from (the entity that
|
||||
implements it) + evidence (the implements relationship) + rule.
|
||||
"""
|
||||
gaps = []
|
||||
resolved_any = False
|
||||
for res in surface_result["resolutions"]:
|
||||
for r in res["resolved"]:
|
||||
resolved_any = True
|
||||
for name, doc in authorities.items():
|
||||
e = ss_entity(doc, r["entity_id"])
|
||||
if e is None:
|
||||
continue
|
||||
for contract in implemented_contracts(doc, r["entity_id"]):
|
||||
if contract not in req.contracts:
|
||||
gaps.append(
|
||||
{
|
||||
"missing_contract": contract,
|
||||
"derived_from": r["entity_id"],
|
||||
"surface": res["surface"],
|
||||
"authority": name,
|
||||
"rule": "affected-surface entity implements a "
|
||||
"contract not represented in the "
|
||||
"normalized requirement",
|
||||
"evidence": f"{r['entity_id']} implements {contract} (authority: {name})",
|
||||
}
|
||||
)
|
||||
break # entity found in one authority is enough
|
||||
if gaps:
|
||||
# dedupe by (missing_contract, derived_from)
|
||||
seen = set()
|
||||
unique = []
|
||||
for g in gaps:
|
||||
key = (g["missing_contract"], g["derived_from"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(g)
|
||||
return {
|
||||
"verdict": MATERIAL_GAP,
|
||||
"gaps": sorted(unique, key=lambda g: (g["missing_contract"], g["derived_from"])),
|
||||
"probe_questions": [
|
||||
f"does the need's stated failure implicate {g['derived_from']}'s {g['missing_contract']} obligation?"
|
||||
for g in unique[:5]
|
||||
],
|
||||
"note": "MATERIAL_GAP: affected surface carries obligations "
|
||||
"the requirement does not represent — refine before "
|
||||
"trusting an apparently-complete fit",
|
||||
}
|
||||
if resolved_any:
|
||||
return {
|
||||
"verdict": COMPLETE_ENOUGH,
|
||||
"gaps": [],
|
||||
"probe_questions": [],
|
||||
"note": "all resolved affected entities' implemented contracts are represented in the requirement",
|
||||
}
|
||||
return {
|
||||
"verdict": COMPLETENESS_UNKNOWN,
|
||||
"gaps": [],
|
||||
"probe_questions": [f"which structural entities does '{s}' map to?" for s in need.affected_surface[:5]],
|
||||
"note": "no affected surface resolved — completeness undeterminable; UNKNOWN never silently becomes COMPLETE",
|
||||
}
|
||||
|
||||
|
||||
def record_proposed_requirement(
|
||||
contract_id: str, *, provider: str, model: str, evidence: list[str], confidence: float
|
||||
) -> dict:
|
||||
"""Record a MODEL-PROPOSED requirement addition (Tranche 5 §12). It is
|
||||
evidence with a verification state — never structural authority by
|
||||
itself. It becomes accepted only via existing authority, research
|
||||
evidence, implementation evidence, or explicit user decision; until
|
||||
then normalize_need excludes it from the requirement's contract set."""
|
||||
if not provider or not model:
|
||||
raise ValueError("model-proposed requirement requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
raise ValueError("confidence must be in [0,1]")
|
||||
return {
|
||||
"kind": "model_proposed_requirement",
|
||||
"contract": contract_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted(evidence),
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}
|
||||
|
||||
|
||||
# ── research routing seam (§6 — honest: recorded route, not execution) ─────
|
||||
|
||||
|
||||
def research_route(req: cf.CapabilityRequirement, decision: dict) -> dict:
|
||||
"""Record which evidence path the decision needs. This is a SEAM: the
|
||||
live ADAPT interface is not yet wired for direct invocation, so the
|
||||
route is recorded on the trace for the orchestrating session to act on.
|
||||
NOT claimed as executed research."""
|
||||
if decision["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
return {
|
||||
"route": "TARGETED_EVIDENCE_PROBE",
|
||||
"question": f"what satisfies: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
if decision["resolution"] == "ACQUIRE_EXTERNAL":
|
||||
return {
|
||||
"route": "FULL_RESEARCH_PIPELINE",
|
||||
"question": f"external acquisition options for: {req.objective[:120]}",
|
||||
"status": "RECORDED_NOT_EXECUTED",
|
||||
}
|
||||
return {"route": "NONE", "status": "LOCAL_SOLVE"}
|
||||
|
||||
|
||||
# ── resolution merge across authorities ─────────────────────────────────────
|
||||
|
||||
_PRECEDENCE = [
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
"ACQUIRE_EXTERNAL",
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"BUILD_NEW",
|
||||
]
|
||||
|
||||
|
||||
def merge_resolutions(results: list[dict]) -> dict:
|
||||
"""Merge per-authority resolution results. Precedence: a concrete
|
||||
reuse/compose/adapt verdict from any authority wins over
|
||||
INSUFFICIENT_EVIDENCE from another; BUILD_NEW can only result when NO
|
||||
authority produced a better verdict AND positive gap evidence exists —
|
||||
it is never a fallback across merges (§8)."""
|
||||
if not results:
|
||||
return {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authority consulted"}
|
||||
best = min(results, key=lambda r: _PRECEDENCE.index(r["resolution"]))
|
||||
merged = dict(best)
|
||||
merged["per_authority"] = [{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results]
|
||||
if best["resolution"] == "BUILD_NEW" and len(results) > 1:
|
||||
# only honest if every authority independently agrees
|
||||
if any(r["resolution"] != "BUILD_NEW" for r in results):
|
||||
weaker = [r for r in results if r["resolution"] == "INSUFFICIENT_EVIDENCE"]
|
||||
if weaker:
|
||||
merged = dict(weaker[0])
|
||||
merged["per_authority"] = [
|
||||
{"authority": r.get("authority", ""), "resolution": r["resolution"]} for r in results
|
||||
]
|
||||
merged["rationale"] = (
|
||||
"BUILD_NEW rejected: at least one authority returned "
|
||||
"INSUFFICIENT_EVIDENCE — positive gap evidence required "
|
||||
"from all consulted authorities (§8)"
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
# ── user-approval gate (§13) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def needs_user_approval(
|
||||
resolution: str, toolchain_decision: tc.DecisionRecord, layout_obligations: list[dict], need: Need
|
||||
) -> dict:
|
||||
"""Classify whether decision→contract requires explicit user approval.
|
||||
Small bounded work inside an already-approved contract does NOT re-ask
|
||||
(need.approval_context covers it)."""
|
||||
reasons = []
|
||||
if toolchain_decision.user_approval_required:
|
||||
reasons.append("toolchain change (CHANGE_MAJOR)")
|
||||
if resolution == "BUILD_NEW" and need.requested_permanence != "temporary":
|
||||
reasons.append("new capability construction")
|
||||
if any(o["lock"] for o in layout_obligations):
|
||||
reasons.append("USER-LOCKED layout surface affected — placement changes require user authorization")
|
||||
if need.requested_permanence == "permanent":
|
||||
reasons.append("permanent promotion requested (user-controlled)")
|
||||
if need.constraints.get("new_repository") or need.constraints.get("new_application"):
|
||||
reasons.append("new repository/application")
|
||||
covered = bool(need.approval_context) and not reasons
|
||||
if reasons and need.approval_context and need.constraints.get("approval_covers") == "all":
|
||||
covered = True
|
||||
reasons = [f"{r} — covered by: {need.approval_context}" for r in reasons]
|
||||
return {
|
||||
"required": bool(reasons) and not covered,
|
||||
"reasons": reasons,
|
||||
"covered_by": need.approval_context if covered else "",
|
||||
}
|
||||
|
||||
|
||||
# ── the live decision path ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def make_decision(
|
||||
need: Need,
|
||||
*,
|
||||
authorities: dict[str, dict],
|
||||
toolchain_constraints: tc.ToolchainConstraints | None = None,
|
||||
project_toolchain: dict | None = None,
|
||||
prior_iterations: list[dict] | None = None,
|
||||
extra_patterns: list | None = None,
|
||||
) -> dict:
|
||||
"""Run NEED → decision trace. `authorities` maps authority name → store
|
||||
doc (e.g. {"rune_spine": spine_doc, "project_scaffold": scaffold_doc}).
|
||||
`project_toolchain` seeds the Toolchain Resolver's existing-project
|
||||
constraint (existing toolchain wins — §12).
|
||||
`prior_iterations` (Tranche 5 §11): earlier decision iterations being
|
||||
refined — retained in the trace with why-it-changed provenance; the
|
||||
final resolution stays singular.
|
||||
|
||||
Order of operations (Tranche 5 §6): affected-surface resolution →
|
||||
REQUIREMENT COMPLETENESS GATE → discovery/fit → merge. A MATERIAL_GAP
|
||||
downgrades premature concrete verdicts (REUSE/SATISFIED/COMPOSE/
|
||||
ADAPT_EXISTING) to INSUFFICIENT_EVIDENCE with the gap recorded — the
|
||||
fit engine was right GIVEN its requirement; the requirement was too
|
||||
narrow."""
|
||||
req, missing = normalize_need(need)
|
||||
trace: dict = {
|
||||
"need": need.to_dict(),
|
||||
"requirement": req.to_dict(),
|
||||
"normalization_missing": missing,
|
||||
"candidates": [],
|
||||
"fits": [],
|
||||
"per_authority_results": [],
|
||||
"layout_obligations": [],
|
||||
"research": {},
|
||||
"toolchain": {},
|
||||
"approval": {},
|
||||
"resolution": "",
|
||||
"rationale": "",
|
||||
"iterations": list(prior_iterations or []),
|
||||
}
|
||||
|
||||
# affected-surface classification + resolution (Tranche 5 §3-5)
|
||||
surface_result = resolve_affected_surface(need, authorities)
|
||||
trace["affected_surfaces"] = surface_result
|
||||
|
||||
# REQUIREMENT COMPLETENESS GATE (Tranche 5 §6-8) — before fit
|
||||
completeness = check_completeness(req, need, authorities, surface_result)
|
||||
trace["completeness"] = completeness
|
||||
|
||||
# live discovery + fit + resolution per authority (§7-8)
|
||||
results = []
|
||||
all_fits = []
|
||||
for name, doc in authorities.items():
|
||||
candidates = cf.discover_candidates(doc, req)
|
||||
fits = [cf.evaluate_fit(req, c) for c in candidates]
|
||||
for f, c in zip(fits, candidates):
|
||||
f["authority"] = name
|
||||
c["authority"] = name
|
||||
all_fits.extend(fits)
|
||||
trace["candidates"].extend(
|
||||
{k: c[k] for k in ("id", "kind", "maturity", "status", "authority", "provides", "consumers")}
|
||||
for c in candidates
|
||||
)
|
||||
res = cf.decide(doc, req, fits)
|
||||
res["authority"] = name
|
||||
results.append(res)
|
||||
trace["fits"] = all_fits
|
||||
if results:
|
||||
merged = merge_resolutions(results)
|
||||
else:
|
||||
merged = {"resolution": "INSUFFICIENT_EVIDENCE", "rationale": "no authorities provided"}
|
||||
if missing and merged["resolution"] in ("BUILD_NEW",):
|
||||
# normalization gaps block BUILD_NEW too (§5: do not invent)
|
||||
merged = {
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
"rationale": f"requirement normalization incomplete: {missing}; refusing to build on guesses",
|
||||
}
|
||||
if completeness["verdict"] == MATERIAL_GAP and merged["resolution"] in (
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
):
|
||||
# PREMATURE-FIT GUARD (Tranche 5 §6-7): the fit engine was correct
|
||||
# GIVEN its requirement — but the requirement is materially
|
||||
# incomplete (affected surface carries unrepresented obligations).
|
||||
# A concrete verdict on an incomplete requirement is not trustworthy.
|
||||
merged = {
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
"rationale": (
|
||||
"premature fit blocked: requirement is materially incomplete "
|
||||
f"({len(completeness['gaps'])} unrepresented obligation(s) on "
|
||||
"affected surfaces); refine the requirement before trusting "
|
||||
"an apparently-complete fit"
|
||||
),
|
||||
"premature_fit_blocked": {
|
||||
"would_have_been": merged["resolution"],
|
||||
"gaps": completeness["gaps"],
|
||||
},
|
||||
}
|
||||
trace["resolution"] = merged["resolution"]
|
||||
trace["rationale"] = merged.get("rationale", "")
|
||||
if missing and trace["resolution"] == "INSUFFICIENT_EVIDENCE":
|
||||
trace["rationale"] += f"; normalization gaps recorded, not invented: {missing}"
|
||||
trace["per_authority_results"] = merged.get("per_authority", results)
|
||||
trace["_merged"] = merged
|
||||
|
||||
# toolchain (§12): existing project toolchain wins
|
||||
constraints = toolchain_constraints or tc.ToolchainConstraints(**(project_toolchain or {}))
|
||||
tdec = tc.resolve_toolchain(constraints)
|
||||
trace["toolchain"] = tdec.to_dict()
|
||||
|
||||
# layout obligations (§19-20): affected surface + candidate impls
|
||||
affected = list(need.affected_surface)
|
||||
for c in trace["candidates"]:
|
||||
affected.append(c["id"])
|
||||
lay = []
|
||||
for name, doc in authorities.items():
|
||||
lay.extend(layout_obligations_for(doc, affected))
|
||||
trace["layout_obligations"] = lay
|
||||
|
||||
# research routing (§6, honest seam)
|
||||
trace["research"] = research_route(req, merged)
|
||||
|
||||
# PATTERN SELECTION (Tranche 6 §4-7): evidence-backed applicability over
|
||||
# the builtin registry + caller-supplied patterns; obligations filtered
|
||||
# per project evidence; conflicts surfaced, never silently resolved.
|
||||
# Project evidence > pattern assumption (§3): a pattern that contradicts
|
||||
# recorded project exclusions becomes DOES_NOT_APPLY.
|
||||
from rune.construction_pattern import ( # noqa: E402 — local: pattern layer
|
||||
combine_pattern_obligations, derive_pattern_obligations,
|
||||
evaluate_applicability)
|
||||
from rune.construction_patterns_builtin import BUILTIN_PATTERNS # noqa: E402
|
||||
|
||||
pattern_candidates = list(BUILTIN_PATTERNS) + list(
|
||||
extra_patterns or [])
|
||||
patterns_considered = []
|
||||
derivations = []
|
||||
for pattern in pattern_candidates:
|
||||
app = evaluate_applicability(pattern, need, surface_result, authorities)
|
||||
patterns_considered.append(app)
|
||||
# APPLIES and PARTIAL both reach obligation derivation: applicability
|
||||
# gates whether the pattern is CONSULTED; the per-obligation `when`
|
||||
# conditions do the precision filtering (Tranche 6 §7). The
|
||||
# applicability verdict is recorded on every derived obligation so
|
||||
# weaker evidence stays visible.
|
||||
if app["verdict"] in ("APPLIES", "PARTIAL"):
|
||||
d = derive_pattern_obligations(
|
||||
pattern, need, surface_result, authorities,
|
||||
project_exceptions=need.constraints.get(
|
||||
"pattern_exceptions", []))
|
||||
for entry in d["accepted"] + d["rejected"] + d["unresolved"]:
|
||||
entry["pattern_applicability"] = app["verdict"]
|
||||
derivations.append(d)
|
||||
combined = combine_pattern_obligations(derivations) if derivations else {
|
||||
"obligations": [], "conflicts": []}
|
||||
trace["patterns_considered"] = patterns_considered
|
||||
trace["pattern_obligations"] = combined
|
||||
|
||||
# approval gate (§13)
|
||||
trace["approval"] = needs_user_approval(merged["resolution"], tdec, lay, need)
|
||||
trace["obligations"] = obligations_from_trace(trace, req)
|
||||
# pattern-derived obligations join the contract's verification set (§19):
|
||||
# provenance stays PATTERN (distinct from STRUCTURAL derivations in
|
||||
# obligations_from_trace — §8 provenance separation)
|
||||
trace["obligations"]["pattern"] = combined["obligations"]
|
||||
trace["obligations"]["pattern_conflicts"] = combined["conflicts"]
|
||||
for po in combined["obligations"]:
|
||||
trace["obligations"]["verification"].append(
|
||||
f"[pattern:{po['pattern']}@r{po['revision']}] {po['obligation']}")
|
||||
return trace
|
||||
|
||||
|
||||
def layout_obligations_for(doc: dict, affected: list[str]) -> list[dict]:
|
||||
"""Layout obligations from a store doc without a Scaffold instance
|
||||
(delegates to the same authority logic via a lightweight query)."""
|
||||
obligations = []
|
||||
affected_set = set(affected)
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "layout_contract" or e.get("status") == "RETIRED":
|
||||
continue
|
||||
ident = e["identities"]
|
||||
comp, region, impl = (ident.get("component", ""), ident.get("region", ""), ident.get("implementation", ""))
|
||||
hit = (
|
||||
comp in affected
|
||||
or f"region:{region}" in affected
|
||||
or (impl and any(a in impl for a in affected if a.endswith(".py")))
|
||||
)
|
||||
if not hit:
|
||||
continue
|
||||
locked = bool(ident.get("lock"))
|
||||
obligations.append(
|
||||
{
|
||||
"component": comp,
|
||||
"region": region,
|
||||
"lock": locked,
|
||||
"provenance": ident.get("provenance", "scaffold"),
|
||||
"obligation": (
|
||||
"USER-LOCKED: preserve user placement in region "
|
||||
"'%s'; changes require explicit user authorization" % region
|
||||
)
|
||||
if locked
|
||||
else (
|
||||
"preserve/verify %s placement in region '%s'; movable "
|
||||
"only via authorized layout amendment" % (comp, region)
|
||||
),
|
||||
"verification": ("layout_contract_check must PASS; USER-LOCKED placement unchanged")
|
||||
if locked
|
||||
else "layout_contract_check must PASS",
|
||||
"status": e.get("status", "ACTIVE"),
|
||||
}
|
||||
)
|
||||
return obligations
|
||||
|
||||
|
||||
def obligations_from_trace(trace: dict, req: cf.CapabilityRequirement) -> dict:
|
||||
"""BUILD-BUT-DON'T-BREAK (§9) + composition/acquire obligations (§10-11)
|
||||
derived automatically from the trace — the builder does not memorize the
|
||||
rule; the path produces it."""
|
||||
resolution = trace["resolution"]
|
||||
obligations: dict = {
|
||||
"new_behavior": [],
|
||||
"regression": [],
|
||||
"composition": [],
|
||||
"acquisition": [],
|
||||
"layout": trace["layout_obligations"],
|
||||
"verification": [],
|
||||
}
|
||||
merged = trace.get("_merged", {})
|
||||
selected = merged.get("selected") or merged.get("components") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
if resolution in ("ADAPT_EXISTING", "REUSE", "SATISFIED") and selected:
|
||||
# regression obligations from existing consumers + contracts (§9)
|
||||
for c in trace["candidates"]:
|
||||
if c["id"] in selected and c.get("consumers"):
|
||||
obligations["regression"].append(
|
||||
f"preserve existing behavior of {c['id']} for consumers: {', '.join(c['consumers'])}"
|
||||
)
|
||||
obligations["verification"].append("new behavior proven by tests before closure")
|
||||
if resolution == "ADAPT_EXISTING":
|
||||
obligations["verification"].append("regression proof: original capability's consumers still work")
|
||||
if resolution == "COMPOSE":
|
||||
obligations["composition"] = [
|
||||
{
|
||||
"components": merged.get("components", selected),
|
||||
"joining_contracts": req.contracts,
|
||||
"permanence": merged.get("permanence", "temporary (user-controlled)"),
|
||||
}
|
||||
]
|
||||
obligations["verification"].append("composition boundary behavior verified end-to-end")
|
||||
if resolution == "ACQUIRE_EXTERNAL":
|
||||
obligations["acquisition"] = merged.get("external", [])
|
||||
obligations["verification"].append("external acquisition requires research/approval step before install")
|
||||
if resolution == "BUILD_NEW":
|
||||
obligations["verification"].append("positive gap evidence recorded in contract")
|
||||
for lo in trace["layout_obligations"]:
|
||||
obligations["verification"].append(lo["verification"])
|
||||
return obligations
|
||||
|
||||
|
||||
# ── contract generation (§15) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_contract(
|
||||
s, doc: dict, trace: dict, *, contract_id: str, scope: list[str] | None = None, proposer_note: str = ""
|
||||
) -> dict:
|
||||
"""Populate a Construction Contract from the decision trace (Tranche 5
|
||||
§13-17 IMPACT-AWARE generation). The Need's affected surface is never
|
||||
lost — it is classified and propagated into distinct surfaces:
|
||||
|
||||
affected_entities structural entity ids (existing)
|
||||
expected_new_entities planned new structural entities
|
||||
ext.affected_paths file-scope entries (mechanical scope check)
|
||||
ext.affected_ui_surfaces ui components (layout obligations follow)
|
||||
ext.unresolved_surfaces retained, never dropped
|
||||
ext.expected_runtime_effects operational effects (deployment:current
|
||||
advances etc.) — accounting metadata, NOT a
|
||||
workflow engine; closure requires independent
|
||||
observation before marking them observed.
|
||||
|
||||
File scope: generated from affected paths + selected capability
|
||||
implementation paths + explicit expected-new paths — NOT unrestricted
|
||||
repo scope. `scope` param may still extend it; generated paths are
|
||||
recorded as such (provenance: affected-surface).
|
||||
"""
|
||||
merged = trace["_merged"]
|
||||
req = trace["requirement"]
|
||||
surfaces = trace.get("affected_surfaces", {})
|
||||
selected = merged.get("selected") or []
|
||||
if isinstance(selected, str):
|
||||
selected = [selected]
|
||||
|
||||
# structural affected: selected candidates + requirement contracts +
|
||||
# resolved surface entities — the union the old generator partially lost
|
||||
affected = {c["id"] for c in trace["candidates"] if c["id"] in selected}
|
||||
affected |= set(trace["requirement"].get("contracts", []))
|
||||
affected |= set(surfaces.get("affected_existing_entities", []))
|
||||
|
||||
# file scope: generated (affected paths + selected impl paths + expected
|
||||
# new paths) UNION caller-supplied additions, provenance-tracked
|
||||
affected_paths = list(surfaces.get("affected_paths", []))
|
||||
impl_paths = []
|
||||
for c in trace["candidates"]:
|
||||
if c["id"] in selected:
|
||||
p = c.get("identities", {}).get("path", "")
|
||||
if p:
|
||||
impl_paths.append(p)
|
||||
expected_new_paths = trace["need"]["constraints"].get("expected_new_source_paths", [])
|
||||
generated_scope = sorted(
|
||||
set(affected_paths)
|
||||
| set(surfaces.get("workflow_config_surfaces", []))
|
||||
| set(impl_paths)
|
||||
| set(expected_new_paths)
|
||||
)
|
||||
caller_scope = sorted(set(scope or []) - set(generated_scope))
|
||||
final_scope = sorted(set(generated_scope) | set(caller_scope))
|
||||
|
||||
expected_runtime = sorted(trace["need"]["constraints"].get("expected_runtime_effects", []))
|
||||
|
||||
obl = trace["obligations"]
|
||||
s.new_contract(
|
||||
doc,
|
||||
contract_id=contract_id,
|
||||
need=trace["need"]["objective"],
|
||||
origin=trace["need"]["origin"],
|
||||
scope=final_scope,
|
||||
requirement=req.get("objective", ""),
|
||||
gap_evidence=merged.get("rationale", ""),
|
||||
research_refs=(
|
||||
trace["need"]["evidence_refs"]
|
||||
+ ([trace["research"]["route"]] if trace["research"]["route"] != "NONE" else [])
|
||||
),
|
||||
resolution=trace["resolution"],
|
||||
toolchain=trace["toolchain"],
|
||||
affected_entities=sorted(affected),
|
||||
expected_new_entities=sorted(set(
|
||||
trace["need"]["constraints"].get("expected_new_entities", [])
|
||||
) | {d["entity"] for d in surfaces.get("derived_expected_new", [])}),
|
||||
expected_consumers=[],
|
||||
regression_obligations=obl["regression"],
|
||||
verification_obligations=sorted(set(obl["verification"])),
|
||||
rollback_ref=trace["need"]["constraints"].get("rollback_ref", "git revert"),
|
||||
acceptance=[f"resolution {trace['resolution']} verified end-to-end"],
|
||||
non_goals=trace["need"]["constraints"].get("non_goals", []),
|
||||
)
|
||||
e = s.contract(doc, contract_id)
|
||||
e.setdefault("ext", {}).update(
|
||||
{
|
||||
"decision_trace": {
|
||||
k: trace[k]
|
||||
for k in (
|
||||
"need",
|
||||
"requirement",
|
||||
"candidates",
|
||||
"fits",
|
||||
"resolution",
|
||||
"rationale",
|
||||
"research",
|
||||
"toolchain",
|
||||
"approval",
|
||||
"obligations",
|
||||
"layout_obligations",
|
||||
"normalization_missing",
|
||||
"completeness",
|
||||
"affected_surfaces",
|
||||
"iterations",
|
||||
"patterns_considered",
|
||||
"pattern_obligations",
|
||||
)
|
||||
},
|
||||
# distinct impact surfaces (Tranche 5 §13) — file scope != structural
|
||||
# impact != expected new != runtime effect
|
||||
"affected_paths": affected_paths,
|
||||
"affected_ui_surfaces": surfaces.get("affected_ui_surfaces", []),
|
||||
"affected_regions": surfaces.get("affected_regions", []),
|
||||
"workflow_config_surfaces": surfaces.get("workflow_config_surfaces", []),
|
||||
"unresolved_surfaces": surfaces.get("unresolved_surfaces", []),
|
||||
"expected_runtime_effects": expected_runtime,
|
||||
"scope_provenance": {"generated_from_affected_surface": generated_scope, "caller_added": caller_scope},
|
||||
}
|
||||
)
|
||||
if proposer_note:
|
||||
e["ext"]["decision_trace"]["proposer_note"] = proposer_note
|
||||
return e
|
||||
@@ -0,0 +1,767 @@
|
||||
"""construction_interface.py — the stable builder-facing Construction
|
||||
Interface (SCAFFOLD-TRANCHE-007).
|
||||
|
||||
ONE shared boundary for every builder — MiMo, Kimi, GLM, future models,
|
||||
deterministic scripts — and for both modes:
|
||||
|
||||
Rune-internal mode authority = Rune Spine + Rune's Project Scaffold
|
||||
external-project mode authority = that project's Project Scaffold
|
||||
|
||||
The builder must NOT need to know construction_decision.py, scaffold.py,
|
||||
structural_store.py, Layout Contract internals, or store paths. Those are
|
||||
implementation details behind this adapter.
|
||||
|
||||
Authority enforcement (§14) — the adapter is NOT a bypass:
|
||||
- builders may not rewrite Rune Spine or Project Scaffold truth
|
||||
- builders may not amend USER-LOCKED layout
|
||||
- builders may not silently broaden scope
|
||||
- builders may not self-certify completion ("done=true" is not a thing)
|
||||
- closing remains governed by Scaffolding (phase map + gates)
|
||||
- builder identity is RECORDED for audit but never changes semantics
|
||||
(§17): identical structured input → identical behavior, any builder.
|
||||
|
||||
Machine-readable output (§12): every operation returns a stable envelope —
|
||||
{schema, operation, success, ids, state, evidence, errors, warnings}
|
||||
Errors are explicit states (§13): NEED_INCOMPLETE, INSUFFICIENT_EVIDENCE,
|
||||
APPROVAL_REQUIRED, UNKNOWN_CONTRACT, SCOPE_VIOLATION, VERIFICATION_FAILED,
|
||||
STORE_WRITE_FAILED, STALE_STATE — never prose-only exit-1.
|
||||
|
||||
Transport (§3): typed Python API (this module) + JSON CLI (module main,
|
||||
same convention as rune-spine). No network service.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from rune import structural_store as ss
|
||||
from rune import scaffold as sc
|
||||
from rune import spine as spine_mod
|
||||
from rune import construction_decision as cd
|
||||
|
||||
__all__ = ["ConstructionInterface", "INTERFACE_SCHEMA_VERSION", "Errors"]
|
||||
|
||||
INTERFACE_SCHEMA_VERSION = 1
|
||||
|
||||
# ── explicit error states (§13) ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class Errors:
|
||||
NEED_INCOMPLETE = "NEED_INCOMPLETE"
|
||||
INSUFFICIENT_EVIDENCE = "INSUFFICIENT_EVIDENCE"
|
||||
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
||||
UNKNOWN_CONTRACT = "UNKNOWN_CONTRACT"
|
||||
SCOPE_VIOLATION = "SCOPE_VIOLATION"
|
||||
VERIFICATION_FAILED = "VERIFICATION_FAILED"
|
||||
LAYOUT_VIOLATION = "LAYOUT_VIOLATION"
|
||||
PATTERN_CONFLICT = "PATTERN_CONFLICT"
|
||||
STORE_WRITE_FAILED = "STORE_WRITE_FAILED"
|
||||
STALE_STATE = "STALE_STATE"
|
||||
|
||||
|
||||
def _envelope(
|
||||
operation: str,
|
||||
*,
|
||||
success: bool,
|
||||
ids: dict | None = None,
|
||||
state: dict | None = None,
|
||||
evidence: list | None = None,
|
||||
errors: list | None = None,
|
||||
warnings: list | None = None,
|
||||
**extra,
|
||||
) -> dict:
|
||||
env = {
|
||||
"schema": INTERFACE_SCHEMA_VERSION,
|
||||
"operation": operation,
|
||||
"success": success,
|
||||
"ids": ids or {},
|
||||
"state": state or {},
|
||||
"evidence": evidence or [],
|
||||
"errors": errors or [],
|
||||
"warnings": warnings or [],
|
||||
}
|
||||
env.update(extra)
|
||||
return env
|
||||
|
||||
|
||||
class ConstructionInterface:
|
||||
"""The builder-facing construction lifecycle for ONE project target.
|
||||
|
||||
Rune-internal mode (default):
|
||||
ConstructionInterface(builder="mimo")
|
||||
External-project mode:
|
||||
ConstructionInterface(builder="mimo",
|
||||
project_store="/path/to/.rune/scaffold",
|
||||
project_id="that-project")
|
||||
|
||||
`builder` is recorded for audit (session record, §20) and NEVER changes
|
||||
construction semantics (§17).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
builder: str = "unknown",
|
||||
*,
|
||||
project_store: str | None = None,
|
||||
project_id: str | None = None,
|
||||
spine_store: str = "store/spine/current.json",
|
||||
):
|
||||
if not builder:
|
||||
raise ValueError("builder identity required (recorded for audit)")
|
||||
self.builder = builder
|
||||
self.spine_store = spine_store
|
||||
if project_store:
|
||||
self.scaffold = sc.Scaffold(project_store, project_id or Path(project_store).name)
|
||||
self.mode = "external_project"
|
||||
else:
|
||||
self.scaffold = sc.Scaffold("store/scaffold", "issue161-harness")
|
||||
self.mode = "rune_internal"
|
||||
|
||||
# -- authority access (read-only projections for builders) ----------------
|
||||
|
||||
def _authorities(self) -> dict[str, dict]:
|
||||
"""The structural authorities for THIS mode. Rune-internal consults
|
||||
the real Spine store; external mode consults only the project
|
||||
scaffold (a Rune-independent project has no Rune Spine)."""
|
||||
auth: dict[str, dict] = {"project_scaffold": self.scaffold.load()}
|
||||
if self.mode == "rune_internal":
|
||||
sd = spine_mod.load(Path(self.spine_store))
|
||||
if sd is not None:
|
||||
auth["rune_spine"] = sd
|
||||
return auth
|
||||
|
||||
def _record_session(self, doc: dict, operation: str, detail: str) -> None:
|
||||
"""Builder session record (§20): compact, no transcripts."""
|
||||
doc.setdefault("observations", []).append(
|
||||
{
|
||||
"kind": "builder_session",
|
||||
"builder": self.builder,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"detail": detail[:200],
|
||||
"observed_at": ss.now(),
|
||||
}
|
||||
)
|
||||
|
||||
# ── §5 submit_need ────────────────────────────────────────────────────────
|
||||
|
||||
def capabilities(self) -> dict:
|
||||
"""§28 schema negotiation: builders detect the supported interface
|
||||
schema and operation set BEFORE calling operations. Also exposes the
|
||||
error-state vocabulary so builders can switch on states without
|
||||
parsing prose. Read-only; no contract required."""
|
||||
return _envelope(
|
||||
"capabilities",
|
||||
success=True,
|
||||
state={
|
||||
"schema_version": INTERFACE_SCHEMA_VERSION,
|
||||
"mode": self.mode,
|
||||
"project": self.scaffold.project_id,
|
||||
"operations": [
|
||||
"submit_need",
|
||||
"inspect_decision",
|
||||
"get_scope",
|
||||
"get_obligations",
|
||||
"report_observation",
|
||||
"verify",
|
||||
"transition",
|
||||
"close",
|
||||
"resume",
|
||||
"amend_scope",
|
||||
"capabilities",
|
||||
],
|
||||
"error_states": sorted(v for k, v in vars(Errors).items() if not k.startswith("_")),
|
||||
},
|
||||
)
|
||||
|
||||
def submit_need(
|
||||
self,
|
||||
objective: str,
|
||||
*,
|
||||
origin: str = "builder",
|
||||
constraints: dict | None = None,
|
||||
affected_surface: list[str] | None = None,
|
||||
evidence_refs: list[str] | None = None,
|
||||
approval_context: str = "",
|
||||
task_ref: str = "",
|
||||
generate: bool = True,
|
||||
contract_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Submit a structured Need; run the full live decision path;
|
||||
optionally generate the Construction Contract. Returns decision
|
||||
summary + contract ref + outstanding questions. Does NOT decide
|
||||
approval — that surfaces via state.approval."""
|
||||
if not objective or not objective.strip():
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "objective is required"}],
|
||||
)
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-{self.builder}",
|
||||
objective=objective,
|
||||
origin=origin,
|
||||
project=self.scaffold.project_id,
|
||||
task_ref=task_ref,
|
||||
constraints=dict(constraints or {}),
|
||||
affected_surface=list(affected_surface or []),
|
||||
evidence_refs=list(evidence_refs or []),
|
||||
approval_context=approval_context,
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
authorities = self._authorities()
|
||||
trace = cd.make_decision(need, authorities=authorities, project_toolchain={"existing_language": "python"})
|
||||
ids: dict = {"need_id": need.need_id}
|
||||
state: dict = {
|
||||
"resolution": trace["resolution"],
|
||||
"completeness": trace["completeness"]["verdict"],
|
||||
"research": trace["research"],
|
||||
"approval": trace["approval"],
|
||||
"builder": self.builder,
|
||||
}
|
||||
errors, warnings = [], []
|
||||
if trace["completeness"]["verdict"] == cd.MATERIAL_GAP:
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.INSUFFICIENT_EVIDENCE,
|
||||
"detail": "material requirement gap",
|
||||
"gaps": trace["completeness"]["gaps"],
|
||||
"probe_questions": trace["completeness"]["probe_questions"],
|
||||
}
|
||||
)
|
||||
if trace["obligations"].get("pattern_conflicts"):
|
||||
errors.append(
|
||||
{
|
||||
"error": Errors.PATTERN_CONFLICT,
|
||||
"detail": "pattern obligations conflict",
|
||||
"conflicts": trace["obligations"]["pattern_conflicts"],
|
||||
}
|
||||
)
|
||||
if trace["approval"]["required"]:
|
||||
errors.append({"error": Errors.APPROVAL_REQUIRED, "reasons": trace["approval"]["reasons"]})
|
||||
if trace["resolution"] == "INSUFFICIENT_EVIDENCE" and not errors:
|
||||
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
|
||||
contract = None
|
||||
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
|
||||
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
|
||||
cd.generate_contract(
|
||||
self.scaffold,
|
||||
doc,
|
||||
trace,
|
||||
contract_id=cid,
|
||||
scope=None,
|
||||
proposer_note=f"via Construction Interface by {self.builder}",
|
||||
)
|
||||
ids["contract_id"] = cid
|
||||
state["contract_phase"] = "draft"
|
||||
state["next_action"] = "review for approval"
|
||||
warnings.append({"warning": "contract generated in draft; requires approval transition before building"})
|
||||
self._record_session(doc, "submit_need", f"{need.need_id} → {trace['resolution']}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"submit_need",
|
||||
success=False,
|
||||
ids=ids,
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("submit_need", success=not errors, ids=ids, state=state, errors=errors, warnings=warnings)
|
||||
|
||||
# ── §6 inspect_decision ───────────────────────────────────────────────────
|
||||
|
||||
def inspect_decision(self, contract_id: str) -> dict:
|
||||
"""Read-only projection of the durable decision trace (§6)."""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
return _envelope(
|
||||
"inspect_decision",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"requirement": t.get("requirement", {}),
|
||||
"completeness": t.get("completeness", {}),
|
||||
"candidates": t.get("candidates", []),
|
||||
"fits": [f for f in t.get("fits", []) if f["fit"] != "UNKNOWN"],
|
||||
"resolution": t.get("resolution", ""),
|
||||
"rationale": t.get("rationale", ""),
|
||||
"toolchain": t.get("toolchain", {}),
|
||||
"patterns_considered": t.get("patterns_considered", []),
|
||||
"structural_obligations": {
|
||||
k: t.get("obligations", {}).get(k, [])
|
||||
for k in ("regression", "verification", "composition", "acquisition")
|
||||
},
|
||||
"pattern_obligations": t.get("obligations", {}).get("pattern", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"iterations": t.get("iterations", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
# ── §7 get_scope ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_scope(self, contract_id: str) -> dict:
|
||||
"""Mechanical builder-facing scope (§7): what may I touch?"""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
return _envelope(
|
||||
"get_scope",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"allowed_paths": c["identities"]["scope"],
|
||||
"affected_structural_entities": c["identities"]["affected_entities"],
|
||||
"expected_new_entities": c["identities"].get("expected_new_entities", []),
|
||||
"affected_paths": ext.get("affected_paths", []),
|
||||
"affected_ui_surfaces": ext.get("affected_ui_surfaces", []),
|
||||
"workflow_config_surfaces": ext.get("workflow_config_surfaces", []),
|
||||
"unresolved_surfaces": ext.get("unresolved_surfaces", []),
|
||||
"expected_runtime_effects": ext.get("expected_runtime_effects", []),
|
||||
"layout_obligations": t_layout(ext),
|
||||
"scope_amendments": ext.get("scope_amendments", []),
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
def get_obligations(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
return _envelope(
|
||||
"get_obligations",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={"obligations": _obligations_projection(c), "builder": self.builder},
|
||||
)
|
||||
|
||||
def report_observation(self, contract_id: str, observations: Any) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
if not isinstance(observations, list) or not observations:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "observations list required"}],
|
||||
)
|
||||
items = []
|
||||
for o in observations:
|
||||
if not isinstance(o, dict) or not o.get("entity"):
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.NEED_INCOMPLETE, "detail": "each observation needs an entity"}],
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"entity": o["entity"],
|
||||
"source": o.get("source", f"builder:{self.builder}"),
|
||||
"detail": str(o.get("detail", ""))[:300],
|
||||
}
|
||||
)
|
||||
ss.record_observed_impact(doc, contract_id, items)
|
||||
self._record_session(doc, "report_observation", f"{len(items)} observation(s) on {contract_id}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope(
|
||||
"report_observation",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"recorded": len(items),
|
||||
"note": "evidence recorded; satisfaction is determined by Scaffolding gates, not builder claims",
|
||||
},
|
||||
)
|
||||
|
||||
def verify(
|
||||
self, contract_id: str, *, changed_files: list[str] | None = None, layout_observations: dict | None = None
|
||||
) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"verify",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
state: dict = {"phase": c.get("phase"), "status": c.get("status")}
|
||||
errors, warnings = [], []
|
||||
if changed_files is not None:
|
||||
scope = self.scaffold.check_scope(doc, contract_id, changed_files)
|
||||
state["scope_check"] = scope
|
||||
if scope["verdict"] == "red":
|
||||
errors.append({"error": Errors.SCOPE_VIOLATION, "out_of_scope": scope["out_of_scope"]})
|
||||
elif scope["verdict"] == "unknown":
|
||||
warnings.append({"warning": "scope UNKNOWN — contract has no approved scope paths"})
|
||||
if layout_observations:
|
||||
from rune import layout_contract as lc
|
||||
|
||||
gate = lc.check_layout(doc, layout_observations)
|
||||
state["layout_gate"] = gate["verdict"]
|
||||
if gate["verdict"] == "FAIL":
|
||||
errors.append(
|
||||
{"error": Errors.LAYOUT_VIOLATION, "checks": [x for x in gate["checks"] if x["verdict"] == "FAIL"]}
|
||||
)
|
||||
elif gate["verdict"] == "UNKNOWN":
|
||||
warnings.append({"warning": "layout UNKNOWN — never PASS"})
|
||||
obligations = _obligations_projection(c)
|
||||
q = c.get("ext", {}).get("contract_quality")
|
||||
if q:
|
||||
state["contract_quality"] = {
|
||||
k: q[k]
|
||||
for k in (
|
||||
"correctly_predicted",
|
||||
"expected_new_observed",
|
||||
"expected_runtime_observed",
|
||||
"genuinely_unexpected_new",
|
||||
"scope_check",
|
||||
)
|
||||
if k in q
|
||||
}
|
||||
state["obligations_total"] = len(obligations)
|
||||
state["remaining"] = (
|
||||
"closure requires phase transitions through verifying with evidence; builder cannot self-certify"
|
||||
)
|
||||
return _envelope(
|
||||
"verify",
|
||||
success=not errors,
|
||||
ids={"contract_id": contract_id},
|
||||
state=state,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.transition_contract(
|
||||
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
|
||||
)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "transition", f"{contract_id} → {phase}")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
return _envelope("transition", success=True, ids={"contract_id": contract_id}, state={"phase": phase})
|
||||
|
||||
def close(self, contract_id: str, *, observed_evidence: list[dict], scope_files: list[str] | None = None) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
try:
|
||||
self.scaffold.close_contract(doc, contract_id, observed_evidence=observed_evidence, scope_files=scope_files)
|
||||
except sc.IllegalPhaseTransition as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED, "detail": str(e)}],
|
||||
)
|
||||
except ValueError as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": str(e)}],
|
||||
)
|
||||
self._record_session(doc, "close", f"{contract_id} closed")
|
||||
try:
|
||||
self.scaffold.save(doc)
|
||||
except (OSError, ValueError) as e:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": str(e)[:200]}],
|
||||
)
|
||||
closed = self.scaffold.contract(doc, contract_id)
|
||||
if closed is None:
|
||||
return _envelope(
|
||||
"close",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": "contract vanished after close"}],
|
||||
)
|
||||
q = closed["ext"]["contract_quality"]
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
if k in q
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def resume(self, contract_id: str) -> dict:
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
ext = c.get("ext", {})
|
||||
t = ext.get("decision_trace", {})
|
||||
return _envelope(
|
||||
"resume",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
|
||||
state={
|
||||
"mode": self.mode,
|
||||
"need": t.get("need", {}),
|
||||
"decision": {
|
||||
"resolution": t.get("resolution"),
|
||||
"completeness": t.get("completeness", {}).get("verdict"),
|
||||
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
|
||||
},
|
||||
"phase": c.get("phase"),
|
||||
"status": c.get("status"),
|
||||
"scope": c["identities"]["scope"],
|
||||
"obligations": _obligations_projection(c),
|
||||
"patterns": c.get("ext", {}).get("pattern_refs", []),
|
||||
"evidence": [cp.get("evidence", "")[:80] for cp in c.get("checkpoints", [])],
|
||||
"unresolved": ext.get("unresolved_surfaces", []),
|
||||
"approval": t.get("approval", {}),
|
||||
"next_action": c["identities"].get("next_action", ""),
|
||||
"last_builder_observations": [
|
||||
o
|
||||
for o in doc.get("observations", [])
|
||||
if o.get("kind") == "observed_consequence" and o.get("task") == contract_id
|
||||
][-5:],
|
||||
"builder": self.builder,
|
||||
},
|
||||
)
|
||||
|
||||
def amend_scope(self, contract_id: str, add_paths: list[str], *, reason: str, authority: str = "user") -> dict:
|
||||
if authority != "user":
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[
|
||||
{
|
||||
"error": Errors.APPROVAL_REQUIRED,
|
||||
"detail": "scope expansion requires user authority — builders cannot broaden their own scope",
|
||||
}
|
||||
],
|
||||
)
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"amend_scope",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
c["identities"]["scope"] = sorted(set(c["identities"]["scope"]) | set(add_paths))
|
||||
c.setdefault("ext", {}).setdefault("scope_amendments", []).append(
|
||||
{"added": sorted(add_paths), "reason": reason[:200], "authorized_by": "user"}
|
||||
)
|
||||
self._record_session(doc, "amend_scope", f"{contract_id} +{len(add_paths)} paths (user)")
|
||||
self.scaffold.save(doc)
|
||||
return _envelope(
|
||||
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
|
||||
)
|
||||
|
||||
# explicit method binding (readable, debuggable — no locals() tricks)
|
||||
|
||||
|
||||
def t_layout(ext: dict) -> list[dict]:
|
||||
"""Layout obligations from a contract's decision trace (helper)."""
|
||||
return ext.get("decision_trace", {}).get("layout_obligations", [])
|
||||
|
||||
|
||||
# ── §8 get_obligations / §9 report_observation / §10 verification ───────────
|
||||
|
||||
|
||||
def _obligations_projection(c: dict) -> list[dict]:
|
||||
t = c.get("ext", {}).get("decision_trace", {})
|
||||
obl = t.get("obligations", {})
|
||||
out = []
|
||||
for kind in ("regression", "verification", "composition", "acquisition"):
|
||||
for item in obl.get(kind, []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"{kind}:{abs(hash(str(item))) % 10**8}",
|
||||
"description": str(item)[:200],
|
||||
"provenance": "STRUCTURAL",
|
||||
"kind": kind,
|
||||
"status": "REQUIRED",
|
||||
}
|
||||
)
|
||||
for p in obl.get("pattern", []):
|
||||
out.append(
|
||||
{
|
||||
"id": p["id"],
|
||||
"description": p["obligation"],
|
||||
"provenance": "PATTERN",
|
||||
"pattern": f"{p['pattern']}@r{p['revision']}",
|
||||
"pattern_applicability": p.get("pattern_applicability", ""),
|
||||
"evidence": p.get("reason", ""),
|
||||
"status": p.get("verdict", "REQUIRED"),
|
||||
}
|
||||
)
|
||||
for lo in t.get("layout_obligations", []):
|
||||
out.append(
|
||||
{
|
||||
"id": f"layout:{lo['component']}",
|
||||
"description": lo["obligation"],
|
||||
"provenance": "LAYOUT_CONTRACT",
|
||||
"status": "REQUIRED",
|
||||
"verification": lo["verification"],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _obligation_scope_files(obligations: list[dict]) -> list[str]:
|
||||
"""Obligation ids referenced by closure scope checks."""
|
||||
return []
|
||||
|
||||
|
||||
# ── CLI transport (§3/§12/§27) ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
"""JSON CLI — same convention as rune-spine/rune-scaffold mains. Every
|
||||
operation prints one JSON envelope; exit 0 = success, 1 = operation
|
||||
failure (the envelope carries the explicit error state)."""
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd = argv[0]
|
||||
args = argv[1:]
|
||||
builder = "cli"
|
||||
project_store = None
|
||||
project_id = None
|
||||
# --builder / --project-store / --project-id flags
|
||||
for flag, key in (("--builder", "b"), ("--project-store", "s"), ("--project-id", "p")):
|
||||
while flag in args:
|
||||
i = args.index(flag)
|
||||
val = args[i + 1]
|
||||
del args[i : i + 2]
|
||||
if flag == "--builder":
|
||||
builder = val
|
||||
elif flag == "--project-store":
|
||||
project_store = val
|
||||
else:
|
||||
project_id = val
|
||||
|
||||
iface = ConstructionInterface(builder=builder, project_store=project_store, project_id=project_id)
|
||||
|
||||
def _out(env: dict) -> int:
|
||||
print(json.dumps(env, indent=1, default=str))
|
||||
return 0 if env["success"] else 1
|
||||
|
||||
if cmd == "capabilities":
|
||||
return _out(iface.capabilities())
|
||||
if cmd == "submit-need":
|
||||
payload = json.loads(args[0]) if args else {}
|
||||
return _out(
|
||||
iface.submit_need(
|
||||
payload.get("objective", ""),
|
||||
origin=payload.get("origin", "builder"),
|
||||
constraints=payload.get("constraints"),
|
||||
affected_surface=payload.get("affected_surface"),
|
||||
evidence_refs=payload.get("evidence_refs"),
|
||||
approval_context=payload.get("approval_context", ""),
|
||||
generate=payload.get("generate", True),
|
||||
)
|
||||
)
|
||||
if cmd == "decision":
|
||||
return _out(iface.inspect_decision(args[0]))
|
||||
if cmd == "scope":
|
||||
return _out(iface.get_scope(args[0]))
|
||||
if cmd == "obligations":
|
||||
return _out(iface.get_obligations(args[0]))
|
||||
if cmd == "observe":
|
||||
return _out(iface.report_observation(args[0], json.loads(args[1])))
|
||||
if cmd == "verify":
|
||||
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
|
||||
if cmd == "transition":
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
|
||||
if cmd == "close":
|
||||
return _out(
|
||||
iface.close(
|
||||
args[0],
|
||||
observed_evidence=json.loads(args[1]),
|
||||
scope_files=(json.loads(args[2]) if len(args) > 2 else None),
|
||||
)
|
||||
)
|
||||
if cmd == "resume":
|
||||
return _out(iface.resume(args[0]))
|
||||
return _out(
|
||||
_envelope(cmd, success=False, errors=[{"error": "UNKNOWN_OPERATION", "detail": f"unknown command: {cmd}"}])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,496 @@
|
||||
"""construction_pattern.py — reusable evidence about HOW a class of thing
|
||||
is normally built.
|
||||
|
||||
SCAFFOLD-TRANCHE-006. Canonical distinction (tranche brief):
|
||||
|
||||
Construction Pattern = reusable knowledge for a CLASS of construction
|
||||
Project Scaffold = structural truth for THIS project
|
||||
Construction Contract = obligations for THIS change
|
||||
|
||||
A pattern is GUIDANCE, not authority (§3): project evidence always wins.
|
||||
A pattern must never overwrite contradictory project evidence — if the
|
||||
pattern assumes HTTP services and the project evidence says message-queue-
|
||||
only, the pattern adapts or becomes non-applicable.
|
||||
|
||||
Applicability (§4-5) is EVIDENCE-BACKED and deterministic: signals are
|
||||
checked against the affected surface + structural authorities. Verdicts:
|
||||
APPLIES / PARTIAL / DOES_NOT_APPLY / UNKNOWN. UNKNOWN never becomes APPLIES
|
||||
because a name sounds similar; name similarity establishes nothing. Model
|
||||
reasoning may PROPOSE a pattern (recorded, PENDING) — it may not certify
|
||||
applicability.
|
||||
|
||||
Obligation derivation (§6-7) is FILTERED by this project: every pattern
|
||||
obligation carries a `when` condition evaluated against project evidence;
|
||||
verdicts REQUIRED / APPLICABLE / NOT_APPLICABLE / UNKNOWN with the reason.
|
||||
Patterns REDUCE false-positive obligation expansion, never amplify it.
|
||||
|
||||
Provenance (§16): every rule answers "why is this here" with real Rune
|
||||
evidence — incidents, contracts, tests, user requirements. Patterns
|
||||
accumulate verified engineering knowledge, not model folklore.
|
||||
|
||||
Revision (§17): lightweight revision identity; contracts record the
|
||||
revision consumed so "which builds used the old rule" stays answerable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune import structural_store as ss
|
||||
|
||||
__all__ = [
|
||||
"ConstructionPattern",
|
||||
"evaluate_applicability",
|
||||
"derive_pattern_obligations",
|
||||
"record_pattern_proposal",
|
||||
"APPLIES",
|
||||
"PARTIAL",
|
||||
"DOES_NOT_APPLY",
|
||||
"UNKNOWN",
|
||||
"OBL_REQUIRED",
|
||||
"OBL_APPLICABLE",
|
||||
"OBL_NOT_APPLICABLE",
|
||||
"OBL_UNKNOWN",
|
||||
]
|
||||
|
||||
APPLIES = "APPLIES"
|
||||
PARTIAL = "PARTIAL"
|
||||
DOES_NOT_APPLY = "DOES_NOT_APPLY"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
OBL_REQUIRED = "REQUIRED"
|
||||
OBL_APPLICABLE = "APPLICABLE"
|
||||
OBL_NOT_APPLICABLE = "NOT_APPLICABLE"
|
||||
OBL_UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
# ── pattern object (open-world) ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class ConstructionPattern:
|
||||
"""One reusable construction pattern. Open-world: `ext` carries anything
|
||||
the schema above does not name. NOT structural truth — guidance."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pattern_id: str,
|
||||
purpose: str,
|
||||
*,
|
||||
revision: int = 1,
|
||||
maturity: str = "CANDIDATE",
|
||||
signals: dict | None = None,
|
||||
obligations: list[dict] | None = None,
|
||||
provenance: list[str] | None = None,
|
||||
exclusions: list[str] | None = None,
|
||||
toolchain_notes: list[str] | None = None,
|
||||
runtime_effects: list[str] | None = None,
|
||||
**ext,
|
||||
):
|
||||
if not pattern_id:
|
||||
raise ValueError("pattern_id required")
|
||||
if not purpose:
|
||||
raise ValueError("purpose required")
|
||||
for o in obligations or []:
|
||||
for key in ("id", "obligation", "when"):
|
||||
if key not in o:
|
||||
raise ValueError(f"obligation missing {key!r}: {o}")
|
||||
self.pattern_id = pattern_id
|
||||
self.purpose = purpose
|
||||
self.revision = revision
|
||||
self.maturity = maturity
|
||||
self.signals = dict(signals or {})
|
||||
self.obligations = list(obligations or [])
|
||||
self.provenance = sorted(provenance or [])
|
||||
self.exclusions = sorted(exclusions or [])
|
||||
self.toolchain_notes = sorted(toolchain_notes or [])
|
||||
self.runtime_effects = sorted(runtime_effects or [])
|
||||
self.ext = dict(ext)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"pattern_id": self.pattern_id,
|
||||
"purpose": self.purpose,
|
||||
"revision": self.revision,
|
||||
"maturity": self.maturity,
|
||||
"signals": dict(self.signals),
|
||||
"obligations": [dict(o) for o in self.obligations],
|
||||
"provenance": list(self.provenance),
|
||||
"exclusions": list(self.exclusions),
|
||||
"toolchain_notes": list(self.toolchain_notes),
|
||||
"runtime_effects": list(self.runtime_effects),
|
||||
**self.ext,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ConstructionPattern":
|
||||
src = dict(d) # never mutate the caller's dict
|
||||
known = {
|
||||
k: src.pop(k)
|
||||
for k in (
|
||||
"pattern_id",
|
||||
"purpose",
|
||||
"revision",
|
||||
"maturity",
|
||||
"signals",
|
||||
"obligations",
|
||||
"provenance",
|
||||
"exclusions",
|
||||
"toolchain_notes",
|
||||
"runtime_effects",
|
||||
)
|
||||
if k in src
|
||||
}
|
||||
return cls(**known, **src)
|
||||
|
||||
def revise(self, *, reason: str, evidence: list[str], changes: dict | None = None) -> "ConstructionPattern":
|
||||
"""Revision with provenance (§17): prior meaning not invisibly
|
||||
mutated — returns a NEW pattern at revision+1 recording the change."""
|
||||
d = self.to_dict()
|
||||
d.pop("pattern_id")
|
||||
rev = {**d, **(changes or {})}
|
||||
rev["revision"] = self.revision + 1
|
||||
rev.setdefault("revision_history", []).append(
|
||||
{
|
||||
"prior_revision": self.revision,
|
||||
"reason": reason[:200],
|
||||
"evidence": sorted(evidence),
|
||||
"changed": sorted((changes or {}).keys()),
|
||||
}
|
||||
)
|
||||
return ConstructionPattern(self.pattern_id, **rev)
|
||||
|
||||
|
||||
# ── model proposal seam (§5 — recorded, never authoritative) ────────────────
|
||||
|
||||
|
||||
def record_pattern_proposal(
|
||||
pattern_id: str, *, provider: str, model: str, evidence: list[str], confidence: float
|
||||
) -> dict:
|
||||
"""A model may PROPOSE a pattern; it may not certify applicability.
|
||||
Recorded with verification state PENDING until deterministic project
|
||||
evidence, research evidence, proven construction evidence, or an
|
||||
explicit user decision accepts it."""
|
||||
if not provider or not model:
|
||||
raise ValueError("pattern proposal requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
raise ValueError("confidence must be in [0,1]")
|
||||
return {
|
||||
"kind": "pattern_proposal",
|
||||
"pattern_id": pattern_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted(evidence),
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}
|
||||
|
||||
|
||||
# ── applicability (§4 — deterministic signals only) ─────────────────────────
|
||||
|
||||
|
||||
def _surface_text(need, surface_result: dict) -> set[str]:
|
||||
"""Everything the affected surface offers as evidence: paths, entities,
|
||||
ui surfaces, regions, workflow configs, the objective text tokens."""
|
||||
tokens = set()
|
||||
for k in (
|
||||
"affected_paths",
|
||||
"affected_existing_entities",
|
||||
"affected_ui_surfaces",
|
||||
"affected_regions",
|
||||
"workflow_config_surfaces",
|
||||
):
|
||||
tokens.update(surface_result.get(k, []))
|
||||
tokens.update(need.affected_surface)
|
||||
# objective word tokens (deterministic split — not semantics)
|
||||
tokens.update(w.lower() for w in need.objective.replace("/", " ").replace("-", " ").split())
|
||||
return tokens
|
||||
|
||||
|
||||
# ── shared boundary-aware matchers (Tranche 7 §0) ──────────────────────────
|
||||
|
||||
def _path_matches(signal: str, path: str) -> bool:
|
||||
"""Boundary-aware path-signal matching: short (<4 char) signals match
|
||||
path TOKENS only (the 'ui'-in-'builtin' false-positive class); longer
|
||||
signals may use containment."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if len(s) >= 4:
|
||||
return s in path.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9]+", path.lower()))
|
||||
|
||||
|
||||
def _entity_matches(signal: str, entity_id: str) -> bool:
|
||||
"""Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
'ui:', 'region:') match the id's namespace prefix — structural kinds
|
||||
carry meaning in their prefix, and prefix equality is exact, not a
|
||||
substring accident."""
|
||||
import re as _re
|
||||
|
||||
s = signal.lower()
|
||||
if s.endswith(":"):
|
||||
return entity_id.lower().startswith(s)
|
||||
if len(s) >= 4:
|
||||
return s in entity_id.lower()
|
||||
return s in set(_re.split(r"[^a-z0-9:]+", entity_id.lower()))
|
||||
|
||||
|
||||
def evaluate_applicability(
|
||||
pattern: ConstructionPattern, need, surface_result: dict, authorities: dict[str, dict]
|
||||
) -> dict:
|
||||
"""Deterministic applicability verdict from the pattern's declared
|
||||
signals (§4). Signals (all optional; evidence-checked against the
|
||||
affected surface and structural authorities):
|
||||
surface_kinds subset of {source_path, workflow_config,
|
||||
ui_component, layout_region,
|
||||
structural_entity}
|
||||
path_contains any-of substrings expected in affected paths
|
||||
entities_match any-of entity-id prefixes/substrings
|
||||
objective_any any-of lowercase words expected in the objective
|
||||
contracts_match any-of contract ids implemented by affected entities
|
||||
|
||||
Verdict: count how many DECLARED signal groups produce evidence.
|
||||
all declared groups hit -> APPLIES
|
||||
some hit -> PARTIAL (or DOES_NOT_APPLY when the
|
||||
pattern's `strong` groups all miss)
|
||||
none / no signals -> UNKNOWN (never APPLIES by name)
|
||||
Project evidence overrides (§3): exclusions evaluated first — if the
|
||||
need explicitly declares the pattern non-applicable, DOES_NOT_APPLY
|
||||
with the exclusion recorded.
|
||||
"""
|
||||
# §3/§18: PROJECT evidence overrides the pattern. Exclusions may be
|
||||
# declared by the project/need (pattern_exclusions constraint) or by the
|
||||
# pattern itself (non-applicability domains).
|
||||
declared_exclusions = list(pattern.exclusions) + list(need.constraints.get("pattern_exclusions", []))
|
||||
for ex in declared_exclusions:
|
||||
if ex.lower() in need.objective.lower() or ex in need.constraints.get("pattern_exclusions", []):
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": DOES_NOT_APPLY,
|
||||
"reason": f"declared exclusion matched: {ex}",
|
||||
"evidence": {"exclusion": ex, "source": "project/need declaration"},
|
||||
}
|
||||
|
||||
signals = pattern.signals
|
||||
if not signals:
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
}
|
||||
tokens = _surface_text(need, surface_result)
|
||||
|
||||
groups_hit, groups_declared = [], 0
|
||||
evidence: dict = {}
|
||||
|
||||
def _check(group: str, candidates: list[str], present) -> None:
|
||||
nonlocal groups_declared
|
||||
if not candidates:
|
||||
return
|
||||
groups_declared += 1
|
||||
hits = [c for c in candidates if present(c)]
|
||||
if hits:
|
||||
groups_hit.append(group)
|
||||
evidence[group] = hits[:5]
|
||||
|
||||
_check(
|
||||
"surface_kinds",
|
||||
signals.get("surface_kinds", []),
|
||||
lambda c: (
|
||||
bool(
|
||||
surface_result.get(
|
||||
{
|
||||
"source_path": "affected_paths",
|
||||
"workflow_config": "workflow_config_surfaces",
|
||||
"ui_component": "affected_ui_surfaces",
|
||||
"layout_region": "affected_regions",
|
||||
}.get(c, ""),
|
||||
[],
|
||||
)
|
||||
)
|
||||
or any(r["kind"] == c for r in surface_result.get("resolutions", []))
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"path_contains",
|
||||
[p for p in signals.get("path_contains", [])],
|
||||
lambda c: any(
|
||||
_path_matches(c, p)
|
||||
for p in surface_result.get("affected_paths", []) + surface_result.get("workflow_config_surfaces", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"entities_match",
|
||||
[e for e in signals.get("entities_match", [])],
|
||||
lambda c: any(
|
||||
_entity_matches(c, e) for e in surface_result.get("affected_existing_entities", [])
|
||||
),
|
||||
)
|
||||
_check(
|
||||
"objective_any",
|
||||
[w.lower() for w in signals.get("objective_any", [])],
|
||||
lambda c: c in {w.lower() for w in need.objective.replace("/", " ").split()},
|
||||
)
|
||||
contracts_match = signals.get("contracts_match", [])
|
||||
if contracts_match:
|
||||
groups_declared += 1
|
||||
hits = []
|
||||
for name, doc in authorities.items():
|
||||
for e in surface_result.get("affected_existing_entities", []):
|
||||
for r in ss.relationships(doc, from_id=e):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"} and r["to"] in contracts_match:
|
||||
hits.append(f"{e}->{r['to']}")
|
||||
if hits:
|
||||
groups_hit.append("contracts_match")
|
||||
evidence["contracts_match"] = hits[:5]
|
||||
|
||||
if groups_declared == 0:
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "no applicable signal groups declared",
|
||||
"evidence": {},
|
||||
}
|
||||
if len(groups_hit) == groups_declared:
|
||||
verdict = APPLIES
|
||||
elif groups_hit:
|
||||
verdict = PARTIAL
|
||||
else:
|
||||
verdict = DOES_NOT_APPLY
|
||||
return {
|
||||
"pattern": pattern.pattern_id,
|
||||
"verdict": verdict,
|
||||
"reason": f"{len(groups_hit)}/{groups_declared} signal groups "
|
||||
f"produced evidence: {sorted(groups_hit) or 'none'}",
|
||||
"evidence": evidence,
|
||||
}
|
||||
|
||||
|
||||
# ── obligation derivation (§6-7 — filtered by project evidence) ─────────────
|
||||
|
||||
|
||||
def _obligation_when_hit(obligation: dict, need, surface_result: dict, authorities: dict[str, dict]) -> tuple[str, str]:
|
||||
"""Evaluate one obligation's `when` condition against project evidence.
|
||||
Returns (verdict, reason). `when` shapes (all deterministic):
|
||||
path_contains: [substrings] — any affected/config path matches
|
||||
entities_match: [substrings] — any affected entity matches
|
||||
always: true — applies whenever the pattern applies
|
||||
contracts_touched: [ids] — contract in requirement or implemented
|
||||
by affected entities
|
||||
An obligation with no recognized `when` is UNKNOWN (never silently
|
||||
required)."""
|
||||
when = obligation.get("when")
|
||||
paths = [p for p in surface_result.get("affected_paths", [])] + [
|
||||
p for p in surface_result.get("workflow_config_surfaces", [])
|
||||
]
|
||||
entities = (
|
||||
[e for e in surface_result.get("affected_existing_entities", [])]
|
||||
+ [u for u in surface_result.get("affected_ui_surfaces", [])]
|
||||
+ [r for r in surface_result.get("affected_regions", [])]
|
||||
)
|
||||
if when is None:
|
||||
return OBL_UNKNOWN, "obligation declares no applicability condition"
|
||||
if when.get("always") is True:
|
||||
return OBL_REQUIRED, "pattern-level obligation (always applies when the pattern applies)"
|
||||
if "path_contains" in when:
|
||||
hits = [p for sub in when["path_contains"] for p in paths if _path_matches(sub, p)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected path(s): {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected path matches " + str(when["path_contains"]))
|
||||
if "entities_match" in when:
|
||||
hits = [e for sub in when["entities_match"] for e in entities if _entity_matches(sub, e)]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"affected entity/entities: {hits[:3]}"
|
||||
return OBL_NOT_APPLICABLE, ("no affected entity matches " + str(when["entities_match"]))
|
||||
if "contracts_touched" in when:
|
||||
touched = set(need.constraints.get("contracts", []))
|
||||
for name, doc in authorities.items():
|
||||
for e in entities:
|
||||
for r in ss.relationships(doc, from_id=e):
|
||||
if r["type"] in {"implements", "exposes", "provides", "serves"}:
|
||||
touched.add(r["to"])
|
||||
hits = [c for c in when["contracts_touched"] if c in touched]
|
||||
if hits:
|
||||
return OBL_APPLICABLE, f"contract(s) touched: {hits}"
|
||||
return OBL_NOT_APPLICABLE, ("touched contracts do not include " + str(when["contracts_touched"]))
|
||||
return OBL_UNKNOWN, f"unrecognized when-condition shape: {sorted(when)}"
|
||||
|
||||
|
||||
def derive_pattern_obligations(
|
||||
pattern: ConstructionPattern,
|
||||
need,
|
||||
surface_result: dict,
|
||||
authorities: dict[str, dict],
|
||||
project_exceptions: list[dict] | None = None,
|
||||
) -> dict:
|
||||
"""Derive this pattern's obligations FILTERED by project evidence (§7).
|
||||
Every returned obligation carries: pattern, revision, obligation text,
|
||||
verdict, reason/evidence, and provenance kind PATTERN (distinct from
|
||||
Tranche 5's STRUCTURAL derivation — §8 provenance separation).
|
||||
Project exceptions (§18) suppress with provenance retained."""
|
||||
project_exceptions = list(project_exceptions or [])
|
||||
out = {
|
||||
"pattern": pattern.pattern_id,
|
||||
"revision": pattern.revision,
|
||||
"accepted": [],
|
||||
"rejected": [],
|
||||
"unresolved": [],
|
||||
}
|
||||
for o in pattern.obligations:
|
||||
entry = {
|
||||
"id": o["id"],
|
||||
"obligation": o["obligation"],
|
||||
"pattern": pattern.pattern_id,
|
||||
"revision": pattern.revision,
|
||||
"provenance": "PATTERN",
|
||||
"pattern_provenance": list(pattern.provenance),
|
||||
}
|
||||
# project exception check first (§18) — suppress with provenance
|
||||
exc = next((x for x in project_exceptions if x.get("obligation") == o["id"]), None)
|
||||
if exc:
|
||||
entry.update(
|
||||
{
|
||||
"verdict": "EXCEPTED",
|
||||
"reason": exc.get("reason", ""),
|
||||
"exception_evidence": exc.get("evidence", ""),
|
||||
"exception_authority": exc.get("authority", "user"),
|
||||
}
|
||||
)
|
||||
out["rejected"].append(entry)
|
||||
continue
|
||||
verdict, reason = _obligation_when_hit(o, need, surface_result, authorities)
|
||||
entry["verdict"] = verdict
|
||||
entry["reason"] = reason
|
||||
if verdict in (OBL_REQUIRED, OBL_APPLICABLE):
|
||||
out["accepted"].append(entry)
|
||||
elif verdict == OBL_NOT_APPLICABLE:
|
||||
out["rejected"].append(entry)
|
||||
else:
|
||||
out["unresolved"].append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def combine_pattern_obligations(derivations: list[dict]) -> dict:
|
||||
"""Combine multiple patterns' derivations (§26-27): duplicates of the
|
||||
same obligation id collapse (provenance lists merge); conflicting
|
||||
obligations (same id, contradictory verdicts across patterns) surface
|
||||
as conflicts requiring evidence/user decision — never silently chosen."""
|
||||
merged: dict[str, dict] = {}
|
||||
conflicts: list[dict] = []
|
||||
for d in derivations:
|
||||
for entry in d["accepted"]:
|
||||
oid = entry["id"]
|
||||
if oid in merged:
|
||||
prev = merged[oid]
|
||||
if prev["obligation"] != entry["obligation"]:
|
||||
conflicts.append(
|
||||
{
|
||||
"obligation_id": oid,
|
||||
"conflict": "same id, different text",
|
||||
"sources": [prev["pattern"], entry["pattern"]],
|
||||
}
|
||||
)
|
||||
continue
|
||||
prev.setdefault("also_from", []).append(entry["pattern"])
|
||||
continue
|
||||
merged[oid] = dict(entry)
|
||||
return {"obligations": sorted(merged.values(), key=lambda x: x["id"]), "conflicts": conflicts}
|
||||
@@ -0,0 +1,249 @@
|
||||
"""construction_patterns_builtin.py — the initial evidence-grounded pattern
|
||||
set (SCAFFOLD-TRANCHE-006 §10-14: 4 patterns, each derived from REAL Rune
|
||||
evidence, each appropriately immature).
|
||||
|
||||
This module is DATA, not a framework: pattern provenance answers "why is
|
||||
this rule here" with real incidents/contracts/tests. Maturity stays
|
||||
CANDIDATE/UNIT-PROVEN — existence is not trust (§15).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rune.construction_pattern import ConstructionPattern
|
||||
|
||||
__all__ = ["BUILTIN_PATTERNS", "get_pattern", "PATTERN_REGISTRY_REVISION"]
|
||||
|
||||
# Bumped when any builtin pattern's obligations change meaningfully.
|
||||
PATTERN_REGISTRY_REVISION = 1
|
||||
|
||||
CAPABILITY_ADAPTATION = ConstructionPattern(
|
||||
"capability-module-adaptation",
|
||||
"Adapting or extending an existing capability/module: prove the NEW "
|
||||
"behavior, preserve the OLD behavior (BUILD BUT DON'T BREAK).",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"entities_match": ["module:", "capability:"],
|
||||
"path_contains": ["src/"],
|
||||
"objective_any": ["adapt", "extend", "modify", "preserve", "existing", "capability", "repair"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "adapt-new-behavior-proof",
|
||||
"obligation": "new behavior proven by tests before closure",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "adapt-old-behavior-preserved",
|
||||
"obligation": "existing capability behavior preserved; original contracts remain valid",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "adapt-consumers-verified",
|
||||
"obligation": "existing consumers of the adapted capability verified against their contracts",
|
||||
"when": {"entities_match": ["module:", "capability:"]},
|
||||
},
|
||||
{
|
||||
"id": "adapt-migration-detected",
|
||||
"obligation": "migration obligations detected and recorded if any consumer-visible interface changes",
|
||||
"when": {"path_contains": ["src/"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"SCAFFOLD-TRANCHE-001 (build-but-don't-break established)",
|
||||
"SCAFFOLD-TRANCHE-003 (adaptation two-proof rule)",
|
||||
"construction_decision.obligations_from_trace (auto-derived since T4)",
|
||||
"LIVE-CONTRACT-QUALITY-001 (adaptation resolved + closed live)",
|
||||
],
|
||||
)
|
||||
|
||||
RELEASE_DEPLOYMENT = ConstructionPattern(
|
||||
"release-deployment-change",
|
||||
"Changes to the release/deployment path: fail-closed qualification, "
|
||||
"exact candidate identity, staging != promotion, truthful promotion "
|
||||
"outcomes, deployed-identity verification, negative + forward proofs.",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"path_contains": [
|
||||
"deploy",
|
||||
"promotion",
|
||||
"qualify",
|
||||
"release.yml",
|
||||
"ci.yml",
|
||||
"deploy_qualify",
|
||||
"deploy_stage",
|
||||
"deploy_promotion",
|
||||
],
|
||||
"objective_any": ["deploy", "release", "promot", "qualif", "candidate", "current", "monotonic"],
|
||||
"workflow_config_signals": True, # informational; surface kinds carry it
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "rel-fail-closed-qualification",
|
||||
"obligation": "CI qualification remains fail-closed (exact SHA, "
|
||||
"two-leg verdict); red candidates never deploy",
|
||||
"when": {"path_contains": ["deploy", "release", "qualify", "ci.yml"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-exact-candidate-identity",
|
||||
"obligation": "deployment addresses the EXACT pushed revision, never whatever HEAD is at deploy time",
|
||||
"when": {"path_contains": ["deploy", "release"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-staging-promotion-distinction",
|
||||
"obligation": "staged candidate != promoted candidate; promotion outcome must be independently verifiable",
|
||||
"when": {"path_contains": ["deploy", "promotion", "stage"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-promotion-truthfulness",
|
||||
"obligation": "promotion outcomes are truthful (PROMOTED / "
|
||||
"NOOP_ALREADY_CURRENT / STALE_SUPERSEDED / REFUSED_*); "
|
||||
"stale candidates never claim success",
|
||||
"when": {"path_contains": ["promotion", "deploy_stage", "deploy_promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-monotonic-promotion",
|
||||
"obligation": "promotion ordering: an older candidate finishing "
|
||||
"later cannot regress current; ancestry check + "
|
||||
"fenced critical section",
|
||||
"when": {"path_contains": ["deploy_promotion", "deploy_stage", "promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-negative-candidate-proof",
|
||||
"obligation": "negative proof: red/unqualified candidate is refused; stale candidate cannot promote",
|
||||
"when": {"path_contains": ["deploy", "promotion", "qualify"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-forward-candidate-proof",
|
||||
"obligation": "forward proof: a green newer candidate still promotes normally",
|
||||
"when": {"path_contains": ["deploy", "promotion"]},
|
||||
},
|
||||
{
|
||||
"id": "rel-deployed-identity-verification",
|
||||
"obligation": "deployed identity verified from the deployment "
|
||||
"authority (current symlink + build identity), not "
|
||||
"from job artifacts alone",
|
||||
"when": {"always": True},
|
||||
},
|
||||
{
|
||||
"id": "rel-release-regression-verification",
|
||||
"obligation": "existing release-safety guarantees re-proven (the "
|
||||
"guarantees that already exist must not silently "
|
||||
"weaken)",
|
||||
"when": {"always": True},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"maintenance:deploy-order-race (live incident: deploys 7355/7359)",
|
||||
"DEPLOY-MONOTONIC-001 (monotonic promotion + truthful outcomes, live-proven)",
|
||||
"release-safety repair 2026-09-13 (red-promotion incidents 0.66.28/29: required-checks aggregator lied)",
|
||||
"scripts/deploy_promotion.py (ancestry + fenced critical section)",
|
||||
"tests/test_deploy_stage_race.py (22-test matrix incl. two-process live race)",
|
||||
],
|
||||
)
|
||||
|
||||
UI_TUI_STRUCTURAL = ConstructionPattern(
|
||||
"ui-tui-structural-change",
|
||||
"UI/TUI structural changes: component identity, Layout Contract "
|
||||
"inheritance, USER-LOCKED preservation, deterministic verification.",
|
||||
maturity="UNIT-PROVEN",
|
||||
signals={
|
||||
"surface_kinds": ["ui_component", "layout_region"],
|
||||
"path_contains": ["tui", "ui", "widget"],
|
||||
"entities_match": ["ui:", "region:", "layout:"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "ui-layout-contract-inherited",
|
||||
"obligation": "affected UI components inherit their Layout Contract "
|
||||
"obligations; layout_contract_check must PASS",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-user-locked-preserved",
|
||||
"obligation": "USER-LOCKED placement is user authority: preserved "
|
||||
"exactly; changes require explicit user authorization",
|
||||
"when": {"entities_match": ["ui:", "region:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-unknown-never-pass",
|
||||
"obligation": "layout verification UNKNOWN (no adapter evidence) must never be reported as PASS",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
{
|
||||
"id": "ui-component-identity-stable",
|
||||
"obligation": "ui:<component> identity survives implementation-path "
|
||||
"changes; authority never keyed by filename alone",
|
||||
"when": {"entities_match": ["ui:", "region:", "layout:"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"issue #105 + plans/2026-09-03-tui-footer.md (version display)",
|
||||
"Tranche 3 real TUI proof (gate PASS -> violation FAIL -> restore)",
|
||||
"tests/test_layout_contract.py + tests/test_layout_contract_real_proof.py",
|
||||
"maintenance:version-display-user-lock (USER-LOCK upgrade pending "
|
||||
"user confirmation — provenance rule honored, not fabricated)",
|
||||
],
|
||||
)
|
||||
|
||||
STORE_SCHEMA_EVOLUTION = ConstructionPattern(
|
||||
"store-schema-evolution",
|
||||
"Durable store/schema changes: backward readability, authority "
|
||||
"preservation, deterministic roundtrip, migration only when necessary.",
|
||||
maturity="CANDIDATE",
|
||||
signals={
|
||||
"path_contains": ["store", "schema", "structural_store", "scaffold", "spine"],
|
||||
"entities_match": ["module:structural_store", "module:scaffold", "module:spine"],
|
||||
"objective_any": ["store", "schema", "record", "persist", "field", "migration"],
|
||||
},
|
||||
obligations=[
|
||||
{
|
||||
"id": "store-backward-readability",
|
||||
"obligation": "old records without new fields read as empty/unknown, never crash; legacy keys preserved",
|
||||
"when": {"path_contains": ["store", "structural_store", "scaffold", "spine", "schema"]},
|
||||
},
|
||||
{
|
||||
"id": "store-roundtrip",
|
||||
"obligation": "deterministic save/load roundtrip incl. unknown entity/relation kinds and open-world fields",
|
||||
"when": {"path_contains": ["store", "structural_store", "scaffold", "schema"]},
|
||||
},
|
||||
{
|
||||
"id": "store-authority-preservation",
|
||||
"obligation": "structural facts only change through their owning "
|
||||
"projector/authority; canonical rebuild after "
|
||||
"registration (no hand-edited structural facts)",
|
||||
"when": {"path_contains": ["spine", "scaffold", "store/"]},
|
||||
},
|
||||
{
|
||||
"id": "store-migration-only-if-needed",
|
||||
"obligation": "no mandatory migration unless semantics truly change; "
|
||||
"ext/open-world fields preferred over schema churn",
|
||||
"when": {"path_contains": ["schema", "store", "structural_store"]},
|
||||
},
|
||||
],
|
||||
provenance=[
|
||||
"Tranche 2 structural-store extraction (behavior-neutrality proof)",
|
||||
"Tranche 4 contract-quality repair (legacy keys preserved, old contracts readable — test_h)",
|
||||
"Tranche 5 expected-runtime-effects extension (old contracts close without new fields)",
|
||||
"Tranche 2 CI lesson: hand-edited spine store diverged from canonical rebuild (deploy-identity edges)",
|
||||
],
|
||||
)
|
||||
|
||||
BUILTIN_PATTERNS = [CAPABILITY_ADAPTATION, RELEASE_DEPLOYMENT, UI_TUI_STRUCTURAL, STORE_SCHEMA_EVOLUTION]
|
||||
|
||||
_REGISTRY: dict[str, ConstructionPattern] = {p.pattern_id: p for p in BUILTIN_PATTERNS}
|
||||
|
||||
|
||||
def get_pattern(pattern_id: str, revision: int | None = None) -> ConstructionPattern | None:
|
||||
"""Fetch a builtin pattern; a specific revision raises if the registry
|
||||
no longer carries it (contracts reference the revision they consumed —
|
||||
§17)."""
|
||||
p = _REGISTRY.get(pattern_id)
|
||||
if p is None:
|
||||
return None
|
||||
if revision is not None and p.revision != revision:
|
||||
raise ValueError(
|
||||
f"pattern {pattern_id} revision {revision} not in registry "
|
||||
f"(current: {p.revision}) — the consuming contract records which "
|
||||
f"revision it used"
|
||||
)
|
||||
return p
|
||||
@@ -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
@@ -1,8 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dependency_tracker.py — Auto-propagation system for umbrella dependencies.
|
||||
"""dependency_tracker.py — Auto-propagation for the OpenCode HARNESS scripts.
|
||||
|
||||
SCOPE CLASSIFICATION (Spine V1 pre-Memory closure, 2026-09-12): the
|
||||
DEPENDENCY_GRAPH below describes ~/.config/opencode/scripts/*.py — the
|
||||
developer HARNESS (verified: 20/23 names collide with src/rune modules but
|
||||
all exist in the scripts dir; propagate() resolves against SCRIPTS_DIR).
|
||||
This is NOT Rune architecture and NOT a competing structural authority:
|
||||
Rune's structural truth lives in the Spine store (store/spine/current.json,
|
||||
consumed via rune.spine / scripts/scaffold_projection.py). The name
|
||||
collisions with src/rune modules are coincidental — do not read this map
|
||||
as Rune structure.
|
||||
|
||||
When a component changes, automatically updates dependent components.
|
||||
Maps the dependency graph between umbrella systems and fires
|
||||
Maps the dependency graph between harness systems and fires
|
||||
propagation rules when changes are detected.
|
||||
|
||||
Usage:
|
||||
@@ -28,81 +38,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 +124,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 +148,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 +174,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,
|
||||
}
|
||||
Loaded 100 of 292 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user