Compare commits
429
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8d3534c6 | ||
|
|
d0ec37925f | ||
|
|
f9936fa244 | ||
|
|
04a277f22c | ||
|
|
b2777d666f | ||
|
|
0982f7317f | ||
|
|
9583678997 | ||
|
|
5db8b202d9 | ||
|
|
78c0d97a43 | ||
|
|
551530c78c | ||
|
|
b669e4d4ed | ||
|
|
2b6a5a6b95 | ||
|
|
e2e7b74a8e | ||
|
|
db4ce7f6a6 | ||
|
|
505209b839 | ||
|
|
f7256aa29e | ||
|
|
26e3765319 | ||
|
|
e6451a5afc | ||
|
|
7008743620 | ||
|
|
f545f1ed8d | ||
|
|
73803e6a0d | ||
|
|
ed1696606e | ||
|
|
4944f9e41c | ||
|
|
6722066c79 | ||
|
|
df8262b9ed | ||
|
|
756ebe6201 | ||
|
|
f64ee95aad | ||
|
|
c3a0a48ca5 | ||
|
|
b4ed258121 | ||
|
|
bacea2d472 | ||
|
|
9acfc0953e | ||
|
|
818aa49043 | ||
|
|
212239ca7b | ||
|
|
ae6d4a0d98 | ||
|
|
00013f8c0b | ||
|
|
ba2966e631 | ||
|
|
20fcee3f40 | ||
|
|
493d9c2800 | ||
|
|
6b5d8ad023 | ||
|
|
f3ef4672f3 | ||
|
|
be6bc17537 | ||
|
|
40b3731928 | ||
|
|
caa77f8a35 | ||
|
|
4d215ea666 | ||
|
|
fda9b6dda0 | ||
|
|
116d1ffbeb | ||
|
|
040988cf00 | ||
|
|
ffed816465 | ||
|
|
72ab7dccf7 | ||
|
|
5d20dc7f51 | ||
|
|
9e903dacaf | ||
|
|
ed1b41abd5 | ||
|
|
ff1b950f11 | ||
|
|
3afc9ff872 | ||
|
|
28e9c8c1c1 | ||
|
|
8249854881 | ||
|
|
4aeef171d2 | ||
|
|
2ccac89ec4 | ||
|
|
232986f0c3 | ||
|
|
725c779ac7 | ||
|
|
edd6d06300 | ||
|
|
98cb3a98a5 | ||
|
|
7881e78e7f | ||
|
|
57a689d22f | ||
|
|
45869a77cd | ||
|
|
224fbfa4e9 | ||
|
|
417e824a51 | ||
|
|
ca2807e9b5 | ||
|
|
25f5219982 | ||
|
|
2d09bee39e | ||
|
|
77bc2161a6 | ||
|
|
74e18402f0 | ||
|
|
39ec00b361 | ||
|
|
95946b6efd | ||
|
|
72d3c909c8 | ||
|
|
ab2dfe7b09 | ||
|
|
1326713234 | ||
|
|
867321ed05 | ||
|
|
a44ced163e | ||
|
|
f40f7e7233 | ||
|
|
8bef4682f5 | ||
|
|
f939d9db5a | ||
|
|
dcead3fecc | ||
|
|
87654cbfd1 | ||
|
|
6dddb0a5ee | ||
|
|
f90725dba8 | ||
|
|
24b7dda24f | ||
|
|
c7c6b2d5ac | ||
|
|
6a761f2d87 | ||
|
|
9ef267f2e3 | ||
|
|
bd80fb0e3b | ||
|
|
759a836726 | ||
|
|
71f051d928 | ||
|
|
2b1d0ac245 | ||
|
|
fbb5018413 | ||
|
|
c8f0a8e353 | ||
|
|
fb7fb34ca5 | ||
|
|
abe24da81d | ||
|
|
15c54539b5 | ||
|
|
ad35a2f7f5 | ||
|
|
e2f536f71a | ||
|
|
5b2712f5e6 | ||
|
|
124b77e95b | ||
|
|
1b7995e093 | ||
|
|
fcd835883e | ||
|
|
37ce68eff8 | ||
|
|
2fa0a5f144 | ||
|
|
c3ef29c59d | ||
|
|
f079aa2cc9 | ||
|
|
c03c953a45 | ||
|
|
90ae1c2612 | ||
|
|
cb1b8303fe | ||
|
|
3c54782634 | ||
|
|
478ce45755 | ||
|
|
a96ed90b90 | ||
|
|
713f85a0e3 | ||
|
|
504935828b | ||
|
|
8e78bc936f | ||
|
|
a34762d512 | ||
|
|
772402ed39 | ||
|
|
80a1fd0a2d | ||
|
|
3fb28e2461 | ||
|
|
94ba2c97a3 | ||
|
|
94a4774853 | ||
|
|
efb2c6b9a1 | ||
|
|
99115889d7 | ||
|
|
2a971578e1 | ||
|
|
ec434f2a70 | ||
|
|
d9c50c66c9 | ||
|
|
a382c15562 | ||
|
|
186fdd1410 | ||
|
|
aa9750c59f | ||
|
|
ab114d260b | ||
|
|
b7865a76bb | ||
|
|
04b0f4ac96 | ||
|
|
9b328c9af1 | ||
|
|
bc0f244934 | ||
|
|
29570f404a | ||
|
|
927548e9a0 | ||
|
|
781b286f5b | ||
|
|
d1ccd7ebec | ||
|
|
3adfa1b1ed | ||
|
|
da4eece9e0 | ||
|
|
2e1f0c8cab | ||
|
|
6e2f5950bd | ||
|
|
c53c82bb35 | ||
|
|
35adcd8cb1 | ||
|
|
810d0027c3 | ||
|
|
4839a62b70 | ||
|
|
15408841ac | ||
|
|
c5c69de06f | ||
|
|
fcde0b2ce3 | ||
|
|
1f96055b69 | ||
|
|
923a21ba0b | ||
|
|
6348386869 | ||
|
|
55002ad838 | ||
|
|
6684289a8f | ||
|
|
e25ad0f964 | ||
|
|
c9dee14c62 | ||
|
|
fe9d154778 | ||
|
|
2a4f9c8367 | ||
|
|
7ddfa7eee7 | ||
|
|
986b450bc2 | ||
|
|
b51afea0cb | ||
|
|
c15840514f | ||
|
|
2833175329 | ||
|
|
4a06d9f11a | ||
|
|
c707a81933 | ||
|
|
37deb6085c | ||
|
|
affe852ff7 | ||
|
|
e1a71efeb9 | ||
|
|
5bd8cca941 | ||
|
|
fd5f31bba2 | ||
|
|
8a2a4f7b71 | ||
|
|
b2d6802dba | ||
|
|
3ebbf9a9b1 | ||
|
|
193d81961d | ||
|
|
94ad81245b | ||
|
|
196cf193df | ||
|
|
f95b2a63df | ||
|
|
708871e511 | ||
|
|
18ef9a53e3 | ||
|
|
05edd68027 | ||
|
|
d166e83d55 | ||
|
|
98b4c58d36 | ||
|
|
f5c8b07d15 | ||
|
|
292c72ec3d | ||
|
|
0a436b2f97 | ||
|
|
2ba4114fce | ||
|
|
eccb446a3c | ||
|
|
5e6a397e7c | ||
|
|
87fad6ff8b | ||
|
|
87252705a8 | ||
|
|
4423367ddb | ||
|
|
f85c654d9d | ||
|
|
7417a8ca02 | ||
|
|
ecfa80a7b3 | ||
|
|
2a106e39a3 | ||
|
|
54cee39b95 | ||
|
|
c27d13d9ac | ||
|
|
dd5030407e | ||
|
|
ba0ec0ae31 | ||
|
|
4d410319ce | ||
|
|
09ec2b81ba | ||
|
|
577695a1a3 | ||
|
|
2fad565016 | ||
|
|
6b805eea4a | ||
|
|
4ecd99023f | ||
|
|
d3410671fd | ||
|
|
a4c099a9ca | ||
|
|
09ba25473a | ||
|
|
b7a71e69f1 | ||
|
|
a30464df1a | ||
|
|
0f80334be1 | ||
|
|
c09a42f10a | ||
|
|
13704bd315 | ||
|
|
56f96f0d60 | ||
|
|
e86da59cdb | ||
|
|
143e6095ed | ||
|
|
fe4bc43a83 | ||
|
|
83426c0e1c | ||
|
|
b5c9a3c04e | ||
|
|
cb10db1e78 | ||
|
|
1d466a6926 | ||
|
|
fa51b96ca7 | ||
|
|
5796caa30a | ||
|
|
5169e02610 | ||
|
|
cceb607d99 | ||
|
|
c692013441 | ||
|
|
11ebc50a55 | ||
|
|
08c782fe91 | ||
|
|
4bcae0bb10 | ||
|
|
a8d1b98332 | ||
|
|
74272842aa | ||
|
|
aa67119d55 | ||
|
|
9681baa318 | ||
|
|
8a6b051149 | ||
|
|
6bb5507d33 | ||
|
|
8692f1dc76 | ||
|
|
78d70def5f | ||
|
|
c9260ace18 | ||
|
|
2a7e9dcfcf | ||
|
|
e3263ef9b1 | ||
|
|
ce328a8080 | ||
|
|
4df1f6f23f | ||
|
|
7484e175be | ||
|
|
d84060cc02 | ||
|
|
b102225d4e | ||
|
|
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 |
No files matched your search
@@ -62,7 +62,22 @@ jobs:
|
||||
# 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
|
||||
WARRANTED=$(python3 scripts/version_convergence.py | python3 -c "import json,sys; print(json.load(sys.stdin).get('warranted_bump') or '')")
|
||||
# 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"
|
||||
@@ -170,56 +185,45 @@ 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:
|
||||
env.write(f"RELEASE_VERSION={NEW}\n")
|
||||
env.write("AUTO_BUMPED=true\n")
|
||||
PYBUMP
|
||||
- name: Create release from merged pull request
|
||||
- name: Create evidence-derived release (SCAFFOLD-TRANCHE-009B)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
PR_TITLE: ${{ gitea.event.pull_request.title }}
|
||||
PR_BODY: ${{ gitea.event.pull_request.body }}
|
||||
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
|
||||
LIST_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases?limit=50
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
# 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
|
||||
echo "Release v$RELEASE_VERSION already exists — skipping creation"
|
||||
exit 0
|
||||
fi
|
||||
python3 - <<'PY' > /tmp/rune-release.json
|
||||
import json, os
|
||||
version = os.environ["RELEASE_VERSION"]
|
||||
body = f"## {os.environ['PR_TITLE']}\n\n{os.environ.get('PR_BODY', '')}"
|
||||
print(json.dumps({
|
||||
"tag_name": f"v{version}",
|
||||
"target_commitish": "master",
|
||||
"name": f"Rune v{version}",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @/tmp/rune-release.json \
|
||||
"$RELEASE_API_URL"
|
||||
rm -f /tmp/rune-release.json
|
||||
# ONE note-generation authority: the tested release_notes module.
|
||||
# The old PR-title/body paste path (which produced empty '##'
|
||||
# bodies on direct-push releases) is REMOVED — governed evidence
|
||||
# from the scaffold store is the source, not PR prose.
|
||||
python3 scripts/release_history_cli.py init \
|
||||
--version "$RELEASE_VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store store/scaffold/current.json
|
||||
+57
-8
@@ -286,12 +286,61 @@ 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
|
||||
|
||||
notify-event-bridge:
|
||||
# T16 §16C: post the CI completion WAKE to the local event bridge so
|
||||
# event-driven waiters (deploy.yml → ci_event_wait.py) resume on the
|
||||
# event instead of sleep-polling. ALWAYS runs (success or failure —
|
||||
# a red CI must wake the waiter just as loudly as a green one). The
|
||||
# wake is best-effort and never decides the verdict: the waiter
|
||||
# re-reads the canonical Gitea state via deploy_qualify.py.
|
||||
name: CI Pipeline / notify-event-bridge
|
||||
runs-on: rune-ci-desktop
|
||||
needs: [required-checks]
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Wake event bridge (wake only — never completion truth)
|
||||
env:
|
||||
CI_SHA: ${{ gitea.sha }}
|
||||
CI_RESULT: ${{ needs.required-checks.result }}
|
||||
RUNE_EVENT_BRIDGE_URL: ${{ vars.RUNE_EVENT_BRIDGE_URL || 'http://127.0.0.1:8791' }}
|
||||
run: python3 scripts/notify_event_bridge.py
|
||||
+78
-17
@@ -19,38 +19,74 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Wait for CI qualification on this exact SHA
|
||||
- name: Wait for CI qualification on this exact SHA (event-driven, T16 §16C)
|
||||
# T16 §16C/§16D: the old sleep-30 → query → repeat loop is gone.
|
||||
# The waiter registers a correlation on the W/R event bridge and
|
||||
# yields in select() until ci.yml's notify-event-bridge job posts
|
||||
# the completion WAKE; the verdict is then re-read from canonical
|
||||
# Gitea (deploy_qualify.py). The 900s deadline is a FAILURE
|
||||
# DETECTOR (lost-event recovery), never the scheduler.
|
||||
env:
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
# Shared bridge data root: the deploy step runs as the runner
|
||||
# user while the daemon runs as nick — the rendezvous state
|
||||
# (waits/events/resumes/leases) must be shared. §16C live
|
||||
# finding: defaulting to the invoking user's home made the
|
||||
# controller unable to create its FIFO (PermissionError) and
|
||||
# the deploy failed fast+truthfully on both 0.66.63 pushes.
|
||||
RUNE_WR_ROOT: /mnt/workdrive/wr-bridge
|
||||
RUNE_WR_CODE: /home/nick/.local/share/mimo-wait-resume
|
||||
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
|
||||
echo "Qualifying $DEPLOY_SHA (event-driven)"
|
||||
# READINESS BEFORE ACTION (§16U): fail with a clear message if
|
||||
# the bridge rendezvous is not writable in THIS user's context,
|
||||
# instead of a stack trace at FIFO creation time.
|
||||
if ! touch /mnt/workdrive/wr-bridge/resumes/.write-probe 2>/dev/null; then
|
||||
echo "::error::RUNE_WR_ROOT not writable by $(id -un) — bridge data root misconfigured"
|
||||
exit 1
|
||||
fi
|
||||
rm -f /mnt/workdrive/wr-bridge/resumes/.write-probe
|
||||
python3 scripts/ci_event_wait.py --sha "$DEPLOY_SHA" --deadline 900
|
||||
|
||||
- name: Stage, verify, promote
|
||||
- name: Stage, verify, promote (monotonic)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
python3 scripts/deploy_stage.py
|
||||
|
||||
- name: Post-deploy runtime verification (the consuming path)
|
||||
- name: Promotion truthfulness check
|
||||
id: promotion_truth
|
||||
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)"
|
||||
echo "promoted=true" >> "$GITEA_OUTPUT"
|
||||
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)"
|
||||
echo "promoted=false" >> "$GITEA_OUTPUT"
|
||||
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 "
|
||||
@@ -62,3 +98,28 @@ jobs:
|
||||
assert rune.__version__ == info['version']
|
||||
print('RUNTIME VERIFIED:', info)
|
||||
"
|
||||
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
|
||||
# The PRIMARY convergence mechanism is deploy_stage.py's out-of-lock
|
||||
# publisher (publishes from CURRENT deployment authority, §14B/§14C).
|
||||
# This step is a non-blocking FALLBACK: if the primary publish failed
|
||||
# (e.g. missing Gitea config), this retries. NEVER fails the job —
|
||||
# documentation failure never outweighs deployment validity.
|
||||
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
|
||||
RELEASE_SERVER_URL: ${{ gitea.server_url }}
|
||||
RELEASE_REPO: ${{ gitea.repository }}
|
||||
run: |
|
||||
VERSION=$(python3 -c "import sys; sys.path.insert(0,'src'); import rune; print(rune.__version__)" 2>/dev/null || echo "")
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "SKIP: cannot determine version"
|
||||
exit 0
|
||||
fi
|
||||
python3 scripts/release_history_cli.py refresh \
|
||||
--version "$VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store "$GITHUB_WORKSPACE/store/scaffold/current.json" \
|
||||
--release-sha "$(git rev-parse HEAD)" \
|
||||
--deployed-sha "$(git rev-parse HEAD)" \
|
||||
|| echo "::warning::release-history fallback refresh FAILED — in-lock hook may have already converged; retry converges"
|
||||
@@ -22,9 +22,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run all canaries
|
||||
# T16 §16Q reconciliation: the glob looped shared/canary_*.py —
|
||||
# real canaries live in src/rune/ (canary_native.MODULES is the
|
||||
# registry authority). The old job silently no-opped.
|
||||
run: |
|
||||
FAILED=0
|
||||
for canary in shared/canary_*.py; do
|
||||
for canary in src/rune/canary_*.py; do
|
||||
if [ -f "$canary" ]; then
|
||||
echo "Running $canary..."
|
||||
python3 "$canary" || FAILED=$((FAILED + 1))
|
||||
|
||||
@@ -11,3 +11,6 @@ build/
|
||||
.env
|
||||
.venv/
|
||||
state/
|
||||
# Runtime artifacts (cwd-local event journal + capability index)
|
||||
index/
|
||||
journal/
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.1**
|
||||
**Current release: v0.83.5**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Hooks } from "@mimo-ai/plugin"
|
||||
|
||||
// event-wait-guard — RETIRED (T21.5 §6/§8, 2026-09-17).
|
||||
//
|
||||
// HISTORY: this hook was the T21 §21B-3 runtime interception layer that
|
||||
// blocked model-generated sleep→poll continuation (T20 regression), and
|
||||
// later carried the T21.1 §21.1C cwd guard. Both were DOWNSTREAM patches:
|
||||
// they corrected the model's output instead of fixing the generator.
|
||||
//
|
||||
// ROOT-CAUSE FIX (T21.5 §2-§5): the generator was ci_event_wait's
|
||||
// wait loop returning nonterminal control to the model — caused by a
|
||||
// single hard-coded master-deploy authority that was blind to branch/PR
|
||||
// operations. T21.5 fixed this upstream:
|
||||
// - OPERATION_SPECS: per-operation-type completion authority
|
||||
// (branch_ci/pull_request_checks -> own Actions runs;
|
||||
// master_ci/deploy/release -> deploy qualification);
|
||||
// - SINGLE LOGICAL AWAIT: all nonterminal conditions re-arm inside
|
||||
// ci_event_wait.run(); the model sees only terminal results;
|
||||
// - structured PendingOperation envelopes (op_type, authority,
|
||||
// internal_rearm_count) propagate to the controller.
|
||||
//
|
||||
// With the generator fixed, runtime interception is retired per the
|
||||
// tranche rule FIXED UPSTREAM BEHAVIOUR > DOWNSTREAM GUARD CORRECTION.
|
||||
// Regression prevention lives in the test suite:
|
||||
// tests/test_single_logical_await.py (no model re-arm contract)
|
||||
// tests/test_ci_event_wait.py (watchdog=recovery, event=wake)
|
||||
// tests/test_wait_guard_corpus.py (canonical wait-pattern corpus)
|
||||
// NOT as a permanent runtime block. The hook file stays installed as a
|
||||
// documented no-op so canonical-source/install parity (test_hook_drift,
|
||||
// test_hook_parity) continues to prove the installed runtime matches
|
||||
// this repository's canonical state.
|
||||
|
||||
export const EventWaitGuard: Hooks = {
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED — no interception. See history above.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export const CwdGuard: Hooks = {
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED (T21.5 §8) — the harness/construction execution boundary
|
||||
// now resolves project context upstream (execution-context binding).
|
||||
// No runtime refusal. See tests/test_auto_execution_context.py.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export const CombinedGuards: Hooks = {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
// Kept as the default export shape; both sub-guards are retired.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export default CombinedGuards
|
||||
@@ -0,0 +1,54 @@
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
// await_operation — T21.3 §21.3C: FIRST-CLASS async wait tool.
|
||||
// Uses the plugin tool() API with args (not parameters).
|
||||
|
||||
export const AwaitOperation = tool({
|
||||
description: [
|
||||
"Wait for an asynchronous external operation (CI, deploy, release) to complete.",
|
||||
"Registers on the event bridge, yields until the completion event arrives, then returns the verified result.",
|
||||
"USE THIS instead of `sleep N; curl status` — it is the canonical event-driven wait.",
|
||||
].join(" "),
|
||||
args: {
|
||||
sha: tool.schema.string().describe(
|
||||
"The exact commit SHA the operation is running against"),
|
||||
deadline_secs: tool.schema.number().optional().describe(
|
||||
"Maximum wait in seconds (default 900)"),
|
||||
},
|
||||
async execute(args, ctx) {
|
||||
const repoRoot = "/mnt/workdrive/issue161-harness"
|
||||
const deadline = args.deadline_secs || 900
|
||||
const cmd = [
|
||||
"python3", `${repoRoot}/scripts/ci_event_wait.py`,
|
||||
"--sha", args.sha,
|
||||
"--deadline", String(deadline),
|
||||
].join(" ")
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: repoRoot,
|
||||
timeout: (deadline + 60) * 1000,
|
||||
encoding: "utf-8" as const,
|
||||
env: {
|
||||
...process.env,
|
||||
DEPLOY_SHA: args.sha,
|
||||
RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge",
|
||||
RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume",
|
||||
},
|
||||
})
|
||||
const lines = result.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return "await_operation: no JSON output"
|
||||
} catch (e: any) {
|
||||
const stdout = e.stdout || ""
|
||||
const lines = stdout.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return JSON.stringify({
|
||||
ok: false,
|
||||
error: `await_operation failed: ${(e.stderr || e.message || "?").slice(0, 200)}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
+269
-220
@@ -1,223 +1,272 @@
|
||||
{
|
||||
"_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",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
"_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-16T18:23:38Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"evidence_probe",
|
||||
"forge",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"openai_compat_transport",
|
||||
"provider_context",
|
||||
"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",
|
||||
"programme_gates"
|
||||
],
|
||||
"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",
|
||||
"behaviour_ledger",
|
||||
"build_basis",
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"cognitive_budgeter",
|
||||
"construction_activation",
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
"construction_depth",
|
||||
"construction_intent",
|
||||
"construction_interface",
|
||||
"construction_mode",
|
||||
"construction_pattern",
|
||||
"construction_patterns_builtin",
|
||||
"construction_projection",
|
||||
"context_broker",
|
||||
"context_pressure",
|
||||
"drift_resume",
|
||||
"event_wait",
|
||||
"failure_recovery",
|
||||
"graph_projection",
|
||||
"impact_preview",
|
||||
"iteration",
|
||||
"layout_contract",
|
||||
"programme_continuation",
|
||||
"project_registry",
|
||||
"release_notes",
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
"structural_store",
|
||||
"testing_mode",
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,15 +104,15 @@
|
||||
},
|
||||
{
|
||||
"path": "src/rune/attention_capsule.py",
|
||||
"role": "AttentionCapsule — the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"role": "AttentionCapsule \u2014 the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives — the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives \u2014 the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/inference_trace.py",
|
||||
"role": "Durable per-run inference trace (tokens served/estimated, output, latency, per-block visibility)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served — builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
"evidence": "P0-07 (#23): persists what ContextBroker (#19) assembled and what the transport served \u2014 builds FROM the broker trace, never re-measures; store state/inference_traces.jsonl."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -160,13 +160,13 @@
|
||||
"path": "src/rune/capability_broker.py",
|
||||
"role": "Authoritative capability selector (select_for_job + verified floor + find_capability escape hatch)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority — its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
"evidence": "P1-01 (#24): select_for_job is the single selection authority \u2014 its ordered decision feeds LocalToolRuntime(tools=) at both construction sites and the _contributor_tools prompt block; degrade-not-crash falls back to full TOOLS with a tier-1 record_failure."
|
||||
},
|
||||
{
|
||||
"path": "memory/index/capability_cards.json",
|
||||
"role": "Capability-card index (rebuilt on empty/stale/corrupt; native cards synthesized from TOOLS metadata)",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds — the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
"evidence": "P1-01 (#24): load_cards validates and rebuilds \u2014 the packaged empty array no longer passes as an index; cards carry risk/preconditions/side_effects copied from tool_executor.TOOLS."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/local_tool_runtime.py",
|
||||
@@ -486,12 +486,6 @@
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "Defines the owner-approved canary-first Gitea sequence."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/plugins/build_mode/__init__.py",
|
||||
"role": "Checkpoint/ledger state-machine primitive used by current governed builds",
|
||||
"disposition": "migrate-adapter",
|
||||
"evidence": "Persists checkpoints but encodes an older phase order."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/build_mode.py",
|
||||
"role": "Separate BuildManager and worktree engine",
|
||||
@@ -504,6 +498,9 @@
|
||||
"disposition": "archive-candidate",
|
||||
"evidence": "Reports fixed success strings for phases without executing their claimed effects."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"T17 17U: plugin BuildStateMachine deleted (zero importers; second parallel orchestrator)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -540,4 +537,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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,53 @@
|
||||
# Rune Semantic Release Policy (T20 §20B)
|
||||
|
||||
Pre-1.0 convention. Scaffolding "V1" does NOT mean Rune 1.0.
|
||||
|
||||
## Bump kinds
|
||||
|
||||
**PATCH** (0.66.80 → 0.66.81):
|
||||
- bug fix, correction, implementation/internal maintenance
|
||||
- no meaningful new user/system capability
|
||||
|
||||
**MINOR** (0.66.80 → 0.67.0):
|
||||
- meaningful new Rune capability
|
||||
- new subsystem functionality
|
||||
- materially expanded system behaviour
|
||||
- significant new supported workflow
|
||||
|
||||
**MAJOR**: reserved for Rune 1.0+ compatibility/public-contract semantics.
|
||||
|
||||
## How the machinery expresses this (no manual version edits)
|
||||
|
||||
The existing machinery already maps commit shape → bump kind:
|
||||
- `semver_bump.next_version` (PR titles): feat→minor, else patch
|
||||
- `version_convergence.highest_warranted_bump` (push ranges):
|
||||
feat/breaking→minor, fix/perf→patch, else None
|
||||
|
||||
**Tranche-level rule (20B)**: a feature tranche lands its feature
|
||||
commits (`feat(...)`), and the convergence machinery bumps MINOR at
|
||||
the tranche's release boundary. Patch-only follow-ups (fix/perf)
|
||||
warrant only PATCH — the machinery cannot accidentally re-minor
|
||||
because `highest_warranted_bump` takes the max over the *post-
|
||||
boundary* range, and a fix-only range yields "patch".
|
||||
|
||||
## Why T20 → 0.67.0
|
||||
|
||||
T20 is a feature-level tranche (Rune-wide retrofit: structural
|
||||
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
|
||||
Spine modernization, semantic release). The release is classified
|
||||
MINOR under this policy, earned through the governed convergence
|
||||
machinery — never a manual version edit.
|
||||
|
||||
## T20 0.67.0 release record
|
||||
T20 classified MINOR under this policy: Rune-wide retrofit (structural
|
||||
taxonomy, capability/behaviour ledgers, rune graph, /testing backlog,
|
||||
Spine modernization, semantic release) = new subsystem functionality +
|
||||
materially expanded system behaviour. Earned through the governed
|
||||
convergence machinery (feat-bearing range → minor), never a manual edit.
|
||||
|
||||
## Root cause record (T20, 2026-09-16)
|
||||
semver_bump re-derived the bump kind from the SYNTHESIZED title text —
|
||||
"minor: converged release" matched nothing → patch. Feature tranches
|
||||
T18/T19/T20 downgraded to 0.66.77-83. Fixed: MINOR_PREFIX_RE carries the
|
||||
convergence kind through; first push after the fix with a feat-bearing
|
||||
range lands 0.67.0 through the normal machinery.
|
||||
@@ -12,7 +12,15 @@ The parent work at the moment of interruption: **Epic #284 — the adaptive deve
|
||||
|
||||
## CURRENT ACTIVE WORK
|
||||
|
||||
**ADAPT-LIVE complete** (master `8d75efb`, 2026-09-10): the three route depths are now genuinely distinct live execution behaviours:
|
||||
**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
|
||||
@@ -44,9 +52,11 @@ This detour is complete when:
|
||||
- [x] ADAPT intent recovered and reconciled (this artifact, Part B)
|
||||
- [ ] **THIS TRANCHE**: the ADAPT status matrix disposition is agreed and the next tranche selected (Part D)
|
||||
|
||||
## RETURN TARGET
|
||||
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
|
||||
|
||||
**PL1 Process Learning** (T9). ADAPT-LIVE closed the route-execution gap; PL1's input (route accuracy, probe outcomes, escalation evidence, verification results, task characteristics) is now produced live by the operator spine. The matrix in the ADAPT RECONCILIATION section stands, updated: LOCAL_FIX/STANDARD_CHANGE paths = BUILT_AND_LIVE; probe = BUILT_AND_LIVE (verification-driven); escalation/downgrade = BUILT_AND_LIVE. Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark.
|
||||
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 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)
|
||||
|
||||
@@ -136,3 +146,44 @@ Reasoning from recovered obligations, not momentum:
|
||||
5. Update epic #284 with the detour record + this matrix; close the gap between tracker and reality.
|
||||
|
||||
Then: **PL1** (T9), with the intent-continuity spine as PL1's first or second deliverable (it is an experience-system product, not a separate architecture project).
|
||||
|
||||
## GITEA RECONCILIATION (2026-09-11)
|
||||
|
||||
Part A-J executed. 15 issues closed (HISTORICAL_EVIDENCE/SUPERSEDED/ACCEPTED_LIMITATION) with evidence. Bug label cleaned: 16 misleading labels removed (open bugs 24→8). #259/#258 updated to current architecture. Remote tracking fixed (master tracks gitea/master, not stale origin). PL1 metrics corrected (NOT_YET_MEASURABLE where evidence doesn't support numbers). D7: bump works, release job fails on Pi — ROOT_CAUSE_UNKNOWN_DEFERRED. Automation noise: 5 duplicate bump commits per logical change (queue-drain artifact, not live recursion — version converges, commit history doesn't).
|
||||
|
||||
## DEFECT D7 (2026-09-11): RESOLVED — release-job failures root-caused
|
||||
|
||||
History: the release job failed on every push since 03:16 (c2528c1).
|
||||
Superseded diagnosis: "Pi-side backup step logs Failed to connect to
|
||||
127.0.0.1:3000" was a red herring — the Pi backup step was never the
|
||||
failing job.
|
||||
|
||||
Root causes (live-proven T15 Part F, 2026-09-11 evening):
|
||||
1. c2528c1 left a corrupted shell fragment after the converged-check fi
|
||||
in auto-version.yml — the release job died at bash parse time (exit 2)
|
||||
before any bump. Fixed in c35b604.
|
||||
2. release_convergence() never assigned the release boundary — all 959+
|
||||
commits classified as unreleased, so the warranted bump never
|
||||
converged and queued runs each wrote their own bump commit. Fixed in
|
||||
e395ee1 (boundary wiring) + 9e0b855 (boundary grep must match subjects
|
||||
only, not bodies) + c35018f (numeric semver comparison: '0.66.9' <
|
||||
'0.66.10' is False lexicographically, which silently skipped warranted
|
||||
bumps as version_ahead_of_target).
|
||||
3. The per-file contents PUT committed one commit per version surface —
|
||||
five same-version bump commits per real bump (the "duplicate bump
|
||||
cascade" since 0.66.2, misdiagnosed as queue drain). Fixed in 470d82f
|
||||
+ d68fd1e (single ChangeFiles commit).
|
||||
|
||||
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
|
||||
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
|
||||
drained, zero delayed duplicates, zero workflow failures.
|
||||
|
||||
## 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,275 @@
|
||||
# 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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Scaffolding Tranche 9 — Release History + Evidence Correction (2026-09-13)
|
||||
|
||||
_Gitea Releases are now an evidence-derived human-readable history of
|
||||
Rune's evolution. Plus the Tranche 8 builder-evidence correction._
|
||||
|
||||
## Release semantics (audited)
|
||||
|
||||
| surface | meaning |
|
||||
|---|---|
|
||||
| VERSION | semver in pyproject/init (e.g. 0.66.44) |
|
||||
| TAG | v<VERSION>, created by auto-version at the BUMP commit |
|
||||
| release commit | the auto-bump SHA — NOT necessarily final state |
|
||||
| converged deployed identity | last commit deployed under the same VERSION |
|
||||
|
||||
Same-version convergence commits (closure/accounting/lock fixes landing and
|
||||
deploying after the bump) are normal; the notes record BOTH the tag commit
|
||||
and the final deployed identity, distinction exposed honestly.
|
||||
|
||||
## Mechanism
|
||||
|
||||
`release_notes.py`: deterministic evidence extraction from governed records
|
||||
(closed contracts, resolved maintenance, contract-quality records) →
|
||||
structured ReleaseNote → Markdown → Gitea API (POST create / PATCH update).
|
||||
Idempotent: publishing twice for a tag UPDATES, never duplicates. Notes are
|
||||
historical documentation — NOT Memory, NOT structural authority.
|
||||
|
||||
## T8 evidence correction (§1)
|
||||
|
||||
The T8 report claimed a close-from-verifying refusal + reactive superseding
|
||||
transition. Durable checkpoints show NO superseding transition and the
|
||||
phase map permits verifying→closed; live output showed first-attempt close
|
||||
success. The claim was RETRACTED with full provenance (original claim,
|
||||
correction, reason, evidence preserved in the contract record) and
|
||||
recorded as process-defect:unsupported-causal-claim. MiMo adoption
|
||||
maturity survives on the SUPPORTED evidence: scope constrained file edits,
|
||||
pattern obligations forced preservation proof, interface-reported
|
||||
observations, fresh resume.
|
||||
|
||||
## Maturity
|
||||
|
||||
Release-note generator: LIVE-PROVEN (real Gitea, 10 releases). Backfill:
|
||||
0.66.35→0.66.44 carrying historical-backfill provenance. Pre-0.66.35
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
@@ -0,0 +1,79 @@
|
||||
# SCAFFOLDING IMPLEMENTATION AUDIT + V1 GAP MATRIX (T18 §18B)
|
||||
|
||||
_Every claim classified against evidence. Scale: LIVE_PROVEN (production
|
||||
evidence) / TEST_PROVEN (suite green, no production exercise) /
|
||||
IMPLEMENTED_UNPROVEN / PARTIAL / PLANNED / ABSENT / LEGACY / DUPLICATE._
|
||||
|
||||
## Kernel claims
|
||||
|
||||
| Claim | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Contract lifecycle + legal transitions | LIVE_PROVEN | 28 contracts driven through real flow; refusal envelopes observed |
|
||||
| classify_mode (one classifier) | LIVE_PROVEN | gates TUI /build (17G), operator hook, tests 62+ |
|
||||
| Conversational intent layer | TEST_PROVEN | 22+ held-outs incl. unseen paraphrases; 17R live exercise (one real conversational proof) |
|
||||
| Programme continuation | LIVE_PROVEN | 17E 4-stage proof on real bridge; restart/dup/failure held-outs |
|
||||
| Event-driven waits (builder) | LIVE_PROVEN | T15 15Q real model build; T17E async stage |
|
||||
| Event-driven waits (CI) | LIVE_PROVEN | T16 16D deadline path ×2 + T17 natural wake (mode=event, 112.5s) |
|
||||
| Event replay (before-registration) | TEST_PROVEN + production-realistic exercise | 17C 4.6s vs real completed CI; natural-cycle confirmation: the 17Z wake arrived mid-wait, replay path itself not yet hit naturally |
|
||||
| Cross-user bridge delivery | LIVE_PROVEN | 17C repro as runner user (2.05s) + production deploys |
|
||||
| Provider boundary | TEST_PROVEN (structural guard) + observed loud-failure | 16F/G suites; no silent fallback observed in production runs |
|
||||
| Builder cannot self-certify | LIVE_PROVEN | T14 closure refusals ×2; phase-gated close |
|
||||
| Completion truth evidence-driven | LIVE_PROVEN | contract checkpoints; verify_test_truth refusals |
|
||||
| Interruption/resume | LIVE_PROVEN | T14E SIGKILL; 17E restart held-outs |
|
||||
| Impact preview (visible, pre-build) | ABSENT → T18 | nothing renders structured impact before build today |
|
||||
| Structural project model (first-class) | PARTIAL | stores are spine-shaped (entities/rels) but projects model CONTRACTS, not architecture; no domain/subsystem/interface nesting for arbitrary projects |
|
||||
| Structural queries/impact closure for projects | PARTIAL | spine.impact_frontier exists for Rune; project stores lack query API |
|
||||
| Research→Build Basis (durable, versioned) | ABSENT → T18 | research output today stays in transient context/artifacts |
|
||||
| Research-depth decision at construction entry | PARTIAL | ADAPT routes task classes; no explicit construction-stage research gate |
|
||||
| OUTWARD prior-art-first pass | PARTIAL | stage exists in Research Core; not strengthened to precedent-first ordering |
|
||||
| Schema versioning + consent-gated migration | PARTIAL | stores carry schema_version=1; no migration machinery/consent flow |
|
||||
| Environment/runtime model | PARTIAL | deployment_surface exists in Spine; project scaffolds don't model envs |
|
||||
| Workspace lifecycle (worktree per contract) | PARTIAL | BuildManager worktrees exist (legacy adapter); not wired to contracts |
|
||||
| Forge abstraction | ABSENT (Gitea-coupled scripts) | deploy/qualify call Gitea API directly |
|
||||
| Structural drift for project scaffolds | PARTIAL | drift_resume (16P) covers checkpoint-vs-reality; no file/entity-level drift detection |
|
||||
| Graph projection for visualisation | ABSENT → T18 | Spine has snapshots; no stable read-only graph projection |
|
||||
| Memory/Substrate | ABSENT (BY DESIGN — out of scope) | rule L |
|
||||
|
||||
## V1 North Star gap matrix (this tranche's targets)
|
||||
|
||||
| North Star capability | Gap | T18 action |
|
||||
|---|---|---|
|
||||
| Research-led construction (early research, Build Basis) | ABSENT Build Basis | 18C: build_basis.py + ADAPT depth hook |
|
||||
| OUTWARD precedent-first | PARTIAL | 18D: strengthen stage contract + real research run |
|
||||
| First-class structural Scaffold | PARTIAL (contract-shaped) | 18E: structural_model.py over structural_store core |
|
||||
| Structure→scope→governance | PARTIAL | 18F: queries + impact closure feeding contracts |
|
||||
| Visible impact preview | ABSENT | 18G: impact_preview projection |
|
||||
| Structural drift | PARTIAL | 18H: extend drift_resume with entity-level checks |
|
||||
| Schema versioning + consent | PARTIAL | 18I: migration + consent flow + tests |
|
||||
| Environment/runtime model | PARTIAL | 18J: env entities + verification distinction |
|
||||
| Workspace lifecycle | PARTIAL (legacy adapter) | 18K: workspace contract on the BuildManager adapter |
|
||||
| Forge boundary | ABSENT | 18L: forge.py adapter interface (Gitea impl) |
|
||||
| Full route convergence | PARTIAL (classify shared; backends differ) | 18M: audit + smallest fix |
|
||||
| Generalisation (not phrase list) | TEST_PROVEN | 18N: adversarial paraphrase families |
|
||||
| Self-scaffold | PARTIAL (contracts only) | 18O: structural model of Scaffolding itself |
|
||||
| Upgrade-via-Scaffold proof | ABSENT | 18P: one real upgrade driven by the scaffold |
|
||||
| Graph projection | ABSENT | 18Q: versioned read-only projection |
|
||||
|
||||
## T19 additions (post-T18 classifications)
|
||||
|
||||
| Capability | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Project/Scaffold Registry | TEST_PROVEN | registry tests; boundaries proven (no truth in index, no Spine leak) |
|
||||
| /build governance fail-closed | LIVE_PROVEN | 19A: T18 degradation paths removed; failure-path held-outs |
|
||||
| Capability Ledgers | TEST_PROVEN | lifecycle+history+packet tests |
|
||||
| Surgical context packets | LIVE_PROVEN | 19R: packet bound exactly 1 file for a real change |
|
||||
| Behaviour/Test Specs + hardening lifecycle | TEST_PROVEN | ledger suites incl. generation history |
|
||||
| Selective invalidation | TEST_PROVEN | 19S-2/5/9/10 held-outs |
|
||||
| Predictive vs descriptive impact | TEST_PROVEN | delta persistence tests |
|
||||
| Post-change obligations | TEST_PROVEN | machine-derived from structural truth |
|
||||
| /testing backend + aggregate | TEST_PROVEN + LIVE (19R) | testing_mode suites; command wired in real TUI |
|
||||
| Observation→closure (user-confirm gate) | TEST_PROVEN | closure requires retest AND user confirm |
|
||||
| Forge failure never rewrites truth | TEST_PROVEN | 19S-14 held-out |
|
||||
| Self-hosted T19 change | LIVE_PROVEN | contract 19r-testing-command, 11-step scaffold-driven flow |
|
||||
|
||||
## Kernel exceptions required (rule B record)
|
||||
|
||||
None yet. Every T18 plan so far EXTENDS (new modules consuming
|
||||
structural_store) rather than modifying frozen kernel behavior. Any
|
||||
exception discovered during implementation will be recorded here with
|
||||
evidence that the V1 requirement could not be met otherwise.
|
||||
@@ -0,0 +1,153 @@
|
||||
# SCAFFOLDING V1 DEFINITION (T18 §18B)
|
||||
|
||||
_Plane-English + technical definition of Scaffolding as it ACTUALLY
|
||||
exists at tranche T18 start (master 29570f4). Companion artifact:
|
||||
SCAFFOLDING_IMPLEMENTATION_AUDIT (evidence classification per claim)._
|
||||
|
||||
## What Scaffolding is (plain English)
|
||||
|
||||
Scaffolding is Rune's **construction authority**: the governed process
|
||||
by which software gets built or changed. It takes an authorized need,
|
||||
classifies what kind of construction it is, proportionally governs the
|
||||
work through a durable contract (with legal lifecycle transitions),
|
||||
derives scope from structural reality, drives builders, waits on events
|
||||
(not sleeps), refuses self-certified completion, verifies from
|
||||
canonical reality, and records evidence on durable objects so any
|
||||
future session can resume without a transcript.
|
||||
|
||||
It is NOT a scheduler, NOT a memory system, NOT a provider selector,
|
||||
NOT a research engine, NOT a release system. It is the layer that
|
||||
decides *whether, how, and with what obligations* construction happens
|
||||
— and whether it actually finished.
|
||||
|
||||
## What it owns
|
||||
|
||||
- **Construction classification** (`construction_mode.classify_mode` —
|
||||
the ONE classifier: NOT_CONSTRUCTION / LOCAL_CHANGE /
|
||||
EXISTING_PROJECT_CHANGE / PROJECT_BUILD / UNKNOWN; never silently
|
||||
chooses; unknown surfaces probe questions)
|
||||
- **Conversational interpretation** (`construction_intent.py` —
|
||||
meaning/referents/authorization; DISCUSSION→PROPOSAL→USER_APPROVED;
|
||||
feeds the ONE classifier, never deepens)
|
||||
- **Project Scaffold + Contracts** (`scaffold.py` — legal phase map
|
||||
draft→approved→building→verifying→…→closed/failed/abandoned; scope;
|
||||
obligations; checkpoints; predicted-vs-observed at close)
|
||||
- **Programme continuation** (`programme_continuation.py` —
|
||||
run_construction_programme: stages 1:1 to contracts, persist→canary→
|
||||
next; "task done ≠ programme done")
|
||||
- **Impact/scope machinery** (construction_decision, completeness
|
||||
gates, affected-surface propagation)
|
||||
- **Stage guards** (`stage_guards.py` — readiness blocks, liveness
|
||||
proves progress, exit canary proves effect; verify_test_truth refuses
|
||||
false greens)
|
||||
- **Failure taxonomy** (`failure_recovery.py` — 7 classes incl.
|
||||
PROVIDER_UNAVAILABLE; retry budgets; PERMANENT/APPROVAL never retry)
|
||||
- **Builder interface** (`construction_interface.py` — ONE builder
|
||||
boundary; JSON envelopes; builder identity recorded-but-inert;
|
||||
discover_active_contract; capabilities())
|
||||
|
||||
## What it does NOT own
|
||||
|
||||
- **Provider/model selection** (provider layer owns; ambient context
|
||||
via `provider_context.require_model`; structural guard test)
|
||||
- **Research competence** (Research Core owns; Scaffolding invokes and
|
||||
consumes — see 18C integration)
|
||||
- **Asynchronous wait/resume machinery** (W/R event bridge owns;
|
||||
`event_wait.py` is the repo-side adapter: event drives continuation,
|
||||
watchdog detects failure, durable record is truth)
|
||||
- **Release/deployment** (deploy_stage/promotion/qualify + Gitea
|
||||
workflows own it; Scaffolding records evidence)
|
||||
- **Rune's structural self-model** (Spine owns — `spine.py` over
|
||||
`structural_store.py`; Scaffolding stores use the SAME generic core)
|
||||
- **Long-horizon relevance/memory** (future Substrate — not built;
|
||||
seam exists: construction_projection + construction_commands)
|
||||
- **The TUI** (client; sends text, renders projections)
|
||||
|
||||
## How the flows work
|
||||
|
||||
**New build (PROJECT_BUILD)**: conversational/explicit intent →
|
||||
classify_mode (+ project-existence evidence) → activate() bootstraps a
|
||||
provisional Project Scaffold (identity before repository, UNKNOWN
|
||||
fields explicit) → contract draft→approved (user provenance) →
|
||||
building (builder under event wait) → verifying (exit canary;
|
||||
independent evidence) → closed with predicted-vs-observed.
|
||||
|
||||
**Existing-project change**: intent → classify (scoped/file/target
|
||||
evidence; proportional) → attach to existing authority → light or full
|
||||
contract by depth → same lifecycle, proportionate ceremony.
|
||||
|
||||
**Research-led construction (V1 target — CORRECTED 2026-09-15, Nick):**
|
||||
|
||||
> THE RESEARCH PLAN ITSELF IS CONTEXTUALLY GENERATED. There is NO
|
||||
> permanent research-topic checklist. For every request:
|
||||
> REQUEST/INTENT → understand target/current reality → identify material
|
||||
> unknowns/decisions/risks → generate a context-specific research plan →
|
||||
> perform proportional INWARD/OUTWARD/deeper research. OUTWARD asks what
|
||||
> already exists relevant to THIS specific intent (adopt/adapt/combine/
|
||||
> reject/gaps/innovation). The categories researched are selected from
|
||||
> context; deeper technical research is driven by unresolved questions
|
||||
> and consequence level, never performed mechanically.
|
||||
|
||||
`build_basis.generate_research_plan()` implements this: topics are
|
||||
synthesized from THIS intent's material unknowns/decisions/risks;
|
||||
the held-out suite proves unrelated intents produce materially
|
||||
different plans and that no canned category list exists.
|
||||
|
||||
**Future upgrades (V1 target)**: authorized need → research-depth
|
||||
decision (ADAPT) → Research Core pass → durable Build Basis →
|
||||
structural Scaffold consult/construct → impact closure + visible
|
||||
preview → contract with structurally-derived scope → isolated
|
||||
workspace → governed build/verify → merge/release → structural
|
||||
Scaffold updated. (Parts of this chain are new in T18 — see audit.)
|
||||
|
||||
**Interruption/resume**: SIGKILL-safe. Durable contract + programme
|
||||
records + wait records are the truth; fresh process resumes from
|
||||
identity alone; duplicate signals idempotent (live-proven T14E/T17E).
|
||||
|
||||
**Deployment/release participation**: Scaffolding does not run the
|
||||
pipeline; it consumes qualified CI/deploy evidence, records deployed
|
||||
identity on contracts, and refuses closure without it where contracted.
|
||||
|
||||
## T19 layer model (the general engineering system)
|
||||
|
||||
> "The Scaffold should let the LLM operate with surgical precision
|
||||
> locally, while preserving global awareness of what else must be
|
||||
> checked or changed."
|
||||
>
|
||||
> "Lower active cognitive load without losing the whole idea."
|
||||
|
||||
- **STRUCTURAL MODEL** tells Rune what the project contains / how it connects.
|
||||
- **CAPABILITY LEDGER** provides compact local re-entry context (stable IDs
|
||||
across moves; history preserved through supersede/retire).
|
||||
- **BEHAVIOUR VALIDATION LEDGER** tells Rune what intended behaviour is
|
||||
actually confirmed — generation-aware, selective invalidation, automated
|
||||
green never implies USER_CONFIRMED.
|
||||
- **PROJECT REGISTRY** tells Rune which Scaffolds/projects exist
|
||||
(discovery/index only — not structural truth, not Spine).
|
||||
- **ITERATION ENGINE** (predictive→descriptive impact, impact-delta
|
||||
records, machine-derived obligations) makes quicksand protection
|
||||
STANDARD behaviour of every construction route.
|
||||
- **/TESTING** is a dependency-aware hardening programme: dynamic queue,
|
||||
fix-for-B-may-reopen-A, closure requires retest AND user confirmation.
|
||||
- **FORGE** provides workflow/history projection (failure never rewrites
|
||||
canonical validation truth).
|
||||
- **FUTURE SUBSTRATE** will coordinate sequencing/current focus/pause-
|
||||
resume but does NOT own structural or validation truth.
|
||||
|
||||
## How Spine relates
|
||||
|
||||
Spine = Rune's canonical structural self-model (structural_store
|
||||
generic core, maturity ladder, impact_frontier). Scaffolding's project
|
||||
stores share the SAME generic core (zero Rune constants) — one graph
|
||||
discipline, two scopes: Spine models Rune; project Scaffolds model
|
||||
their projects. No duplicate truth: a project's structural model lives
|
||||
in its own store; Rune's lives in Spine. Later retrofit will modernise
|
||||
Spine using the discipline V1 generalises (18K relationship).
|
||||
|
||||
## How Substrate will interact (without owning internals)
|
||||
|
||||
Read: versioned outbound projection (construction_projection,
|
||||
SCHEMA_VERSION=1). Write: bounded validated commands
|
||||
(construction_commands — 6 commands, lifecycle-validated, idempotent,
|
||||
no direct storage mutation). Substrate asks; Scaffolding decides.
|
||||
Memory graph explicitly out of scope (T18 rule L).
|
||||
@@ -0,0 +1,73 @@
|
||||
# Rune Structural Map
|
||||
|
||||
_Maintained structural projection — the fast re-entry surface (T16 §16N).
|
||||
Companion to the Spine store (`store/spine/current.json`, structural
|
||||
authority) and `store/evidence/t16/16M_inventory.md` (full classification
|
||||
record). Update this map when authority or canonical routes change; do
|
||||
not copy the repo here._
|
||||
|
||||
## What is Rune?
|
||||
|
||||
A self-hosting coding agent: TUI client over an operator core, with a
|
||||
structural self-model (Spine), a construction authority (Scaffolding),
|
||||
an event-driven wait/resume bridge (W/R), a research pipeline, and a
|
||||
fail-closed release/deployment pipeline that builds, tests, releases,
|
||||
and deploys Rune itself to `/mnt/workdrive/rune-code/current`.
|
||||
|
||||
## Where things happen (canonical routes only)
|
||||
|
||||
| Concern | Authority | State |
|
||||
|---|---|---|
|
||||
| Structural self-model | `src/rune/spine.py` | `store/spine/current.json` |
|
||||
| Construction (build lifecycle) | `src/rune/scaffold.py` + `construction_*.py` | scaffold store (`<dir>/.rune/scaffold`) |
|
||||
| Builder sessions | `scripts/t15_builder_session.py` (event-driven successor; T14 is legacy) | contract + wait records |
|
||||
| Provider/model selection | `rune_operator` final decider; ambient context via `provider_context.require_model` | `state/model_routing_profiles.json` |
|
||||
| Wait/resume (any long op) | `src/rune/event_wait.py` + W/R daemon (out-of-repo snapshot at `~/.local/share/mimo-wait-resume`, data root `/mnt/workdrive/wr-bridge` — PIN `RUNE_WR_ROOT`: unpinned consumers get a loud warning, never a silent split-brain) | waits/events/leases/resumes JSON |
|
||||
| Programme continuation | `src/rune/programme_continuation.py` (WIRED T17: `run_construction_programme` binds stages to Scaffolding contracts) | scaffold store `programmes/` |
|
||||
| Conversational construction | `src/rune/construction_intent.py` (meaning/referents/authorization) → `construction_mode.classify_mode` (the ONE classifier) | intents are transient; contracts durable |
|
||||
| CI qualification verdict | `scripts/deploy_qualify.py` (two legs, fail-closed) | Gitea commit status + runs |
|
||||
| Release/deploy | `scripts/deploy_stage.py` → `deploy_promotion.py` (monotonic, flock) | deploy root + `current`/`last-good` symlinks |
|
||||
| Research | `scripts/research_task_tree.py` (authority; labels are projection) | `research/issue-<N>/` |
|
||||
| Task completion truth | `verification_layer.py` + `orchestration/conversation_state.py` | durable event store + checkpoints |
|
||||
| Routing (task class) | `src/rune/adaptive_router.py` | `routes/route.jsonl` |
|
||||
| Canary registry | `src/rune/canary_native.MODULES` | observed by Spine |
|
||||
| Structural health | `henxels_checks.py` + `henxels.yaml` | repo root |
|
||||
|
||||
## Known non-canonical / duplicate routes (do not extend; reconcile via 16Q)
|
||||
|
||||
- `build_mode.BuildManager` (legacy /build) — superseded by builder_bridge
|
||||
+ event-driven sessions; still wired.
|
||||
- `scripts/t14_builder_session.py` — superseded by t15 event-driven driver.
|
||||
- Provider selection entry points ×5 (`model_routing.rank`,
|
||||
`model_connect route`, `model_registry get`, `provider_registry`,
|
||||
`work_manager`) — operator is the de-facto final decider.
|
||||
- Task-ish state ×4 (task-tool DB, `task_persist` persisted.json,
|
||||
`memory_store.working`, orchestration event store).
|
||||
- Maintenance ledgers ×3 (scaffold maintenance_item, tui_backlog, drift_ledger).
|
||||
|
||||
## Unfinished / blocked / deprecated
|
||||
|
||||
- programme_continuation.py: WIRED (T17 17D — run_construction_programme
|
||||
is the canonical multi-stage driver; 17E live-proven).
|
||||
- maintenance.yml canary-suite job: loops `shared/canary_*.py` — wrong
|
||||
dir (real canaries in `src/rune/`), job likely no-ops (LEGACY).
|
||||
- daemon out-of-repo: the W/R daemon is deployed from
|
||||
`~/.local/share/mimo-wait-resume` with its own git snapshot —
|
||||
packaging debt, no CI coverage of the deployed copy.
|
||||
- Substrate: NOT BUILT. Scaffolding exposes the seam
|
||||
(`construction_projection.py`, `construction_commands.py`); mock
|
||||
client proves compatibility (`tests/test_substrate_seam.py`).
|
||||
|
||||
## Fresh-session quick answers
|
||||
|
||||
- Is a claim true? → Spine store + evidence_refs, never commit messages.
|
||||
- Is construction allowed? → scaffold contract phase + LEGAL_TRANSITIONS.
|
||||
- How does a long wait work? → register on the W/R bridge, yield on the
|
||||
FIFO event; watchdog is failure detection, never scheduling; the event
|
||||
is a WAKE — authority is re-read after resume.
|
||||
- Who picks the provider? → the user layer, via ambient context;
|
||||
construction never does (`test_provider_ownership.py` enforces).
|
||||
- What runs where? → CI on `rune-ci-desktop` runner; deploy stages to
|
||||
`/mnt/workdrive/rune-code/<sha>`; data home pinned at
|
||||
`/mnt/workdrive/rune` (RUNE_HOME); implementation worktree
|
||||
`/mnt/workdrive/issue161-harness`.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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/*
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 353,
|
||||
"S110": 80
|
||||
}
|
||||
"BLE001": 347,
|
||||
"S110": 75
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.1"
|
||||
version = "0.83.5"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"errorCount": 120,
|
||||
"warningCount": 465,
|
||||
"errorCount": 117,
|
||||
"warningCount": 454,
|
||||
"informationCount": 0
|
||||
}
|
||||
+33
-28
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"typeCheckingMode": "standard",
|
||||
"pythonVersion": "3.14",
|
||||
"reportMissingImports": "warning",
|
||||
"reportMissingModuleSource": "none",
|
||||
"reportMissingTypeStubs": "none",
|
||||
"reportUnknownArgumentType": "none",
|
||||
"reportUnknownParameterType": "none",
|
||||
"reportUnknownVariableType": "none",
|
||||
"reportUnknownMemberType": "none",
|
||||
"reportMissingTypeArgument": "none",
|
||||
"reportUntypedFunctionDecorator": "none",
|
||||
"reportUntypedClassDecorator": "none",
|
||||
"reportUntypedBaseClass": "none",
|
||||
"reportPrivateUsage": "none",
|
||||
"reportOptionalMemberAccess": "warning",
|
||||
"reportUnnecessaryIsInstance": "warning",
|
||||
"reportUnnecessaryComparison": "warning",
|
||||
"reportUnnecessaryTypeIgnoreComment": "warning",
|
||||
"reportPossiblyUnboundVariable": "error",
|
||||
"reportUndefinedVariable": "error",
|
||||
"reportAttributeAccessIssue": "warning",
|
||||
"include": ["src/rune"],
|
||||
"exclude": [
|
||||
"src/rune/__pycache__",
|
||||
"**/node_modules",
|
||||
"**/__pycache__"
|
||||
]
|
||||
}
|
||||
"typeCheckingMode": "standard",
|
||||
"pythonVersion": "3.14",
|
||||
"reportMissingImports": "warning",
|
||||
"reportMissingModuleSource": "none",
|
||||
"reportMissingTypeStubs": "none",
|
||||
"reportUnknownArgumentType": "none",
|
||||
"reportUnknownParameterType": "none",
|
||||
"reportUnknownVariableType": "none",
|
||||
"reportUnknownMemberType": "none",
|
||||
"reportMissingTypeArgument": "none",
|
||||
"reportUntypedFunctionDecorator": "none",
|
||||
"reportUntypedClassDecorator": "none",
|
||||
"reportUntypedBaseClass": "none",
|
||||
"reportPrivateUsage": "none",
|
||||
"reportOptionalMemberAccess": "warning",
|
||||
"reportUnnecessaryIsInstance": "warning",
|
||||
"reportUnnecessaryComparison": "warning",
|
||||
"reportUnnecessaryTypeIgnoreComment": "warning",
|
||||
"reportPossiblyUnboundVariable": "error",
|
||||
"reportUndefinedVariable": "error",
|
||||
"reportAttributeAccessIssue": "warning",
|
||||
"include": [
|
||||
"src/rune"
|
||||
],
|
||||
"exclude": [
|
||||
"src/rune/__pycache__",
|
||||
"**/node_modules",
|
||||
"**/__pycache__"
|
||||
],
|
||||
"extraPaths": [
|
||||
"scripts"
|
||||
]
|
||||
}
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o /tmp/regen-req.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
@@ -18,7 +18,7 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.0
|
||||
coverage==7.16.1
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
# via rune (pyproject.toml)
|
||||
@@ -34,7 +34,7 @@ httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via rune (pyproject.toml)
|
||||
idna==3.19
|
||||
idna==3.20
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
@@ -64,13 +64,13 @@ nodeenv==1.10.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.8
|
||||
platformdirs==4.11.9
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
# diff-cover
|
||||
# pytest
|
||||
propcache==0.5.2
|
||||
propcache==0.5.4
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
@@ -118,5 +118,5 @@ typing-extensions==4.16.0
|
||||
# textual-autocomplete
|
||||
websockets==17.1
|
||||
# via rune (pyproject.toml)
|
||||
yarl==1.24.5
|
||||
yarl==1.25.1
|
||||
# via aiohttp
|
||||
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,375 @@
|
||||
"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
|
||||
execution boundary.
|
||||
|
||||
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
|
||||
The model may express continuation intent in any quality — `await_operation
|
||||
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
|
||||
"sleep 300 then check CI", "poll until done". When a matching
|
||||
PendingOperation exists (or the deterministic resolver can derive the
|
||||
operation truth from git/Gitea state), the controller executes the
|
||||
CANONICAL continuation and discards model-supplied scheduling policy.
|
||||
|
||||
Authority provenance for timing:
|
||||
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
|
||||
GOVERNED_OPERATION_POLICY -> authoritative, retained
|
||||
MODEL_INVENTED -> non-authoritative, ignored
|
||||
|
||||
This module owns NO storage: it reads the existing W/R wait store
|
||||
(rune.event_wait) and git/Gitea state. The canonical continuation remains
|
||||
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
|
||||
if _REPO_SRC not in sys.path:
|
||||
sys.path.insert(0, _REPO_SRC)
|
||||
|
||||
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
|
||||
REPO = Path("/mnt/workdrive/issue161-harness")
|
||||
GIT_TIMEOUT = 10
|
||||
|
||||
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
|
||||
# The model's numbers never become policy; these are the controller's.
|
||||
POLICY_DEADLINES = {
|
||||
"branch_ci": 1800,
|
||||
"pull_request_checks": 1800,
|
||||
"master_ci": 1800,
|
||||
"deploy": 900,
|
||||
"release": 900,
|
||||
}
|
||||
|
||||
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
|
||||
"deploy", "release")
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def classify_timing_provenance(model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> tuple[int, str]:
|
||||
"""Resolve the effective deadline + its provenance.
|
||||
|
||||
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
|
||||
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
|
||||
NEVER used as policy.
|
||||
"""
|
||||
if model_deadline is not None:
|
||||
try:
|
||||
model_deadline = int(model_deadline)
|
||||
except (TypeError, ValueError):
|
||||
model_deadline = None
|
||||
if user_deadline is not None:
|
||||
return int(user_deadline), "USER_EXPLICIT"
|
||||
if contract_deadline is not None:
|
||||
return int(contract_deadline), "EXTERNAL_CONTRACT"
|
||||
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
|
||||
|
||||
|
||||
# ── PendingOperation lookup over the existing W/R wait store ────────────────
|
||||
|
||||
def _load_wait_records() -> list[dict]:
|
||||
"""All durable wait records from the bridge store (read-only)."""
|
||||
out = []
|
||||
waits = WR_BRIDGE / "waits"
|
||||
if not waits.is_dir():
|
||||
return out
|
||||
for f in sorted(waits.glob("*.json")):
|
||||
try:
|
||||
rec = json.loads(f.read_text())
|
||||
rec["_file"] = f.name
|
||||
out.append(rec)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _rec_operation_id(rec: dict) -> str:
|
||||
return str((rec.get("ext") or {}).get("operation_id")
|
||||
or rec.get("operation_id") or "")
|
||||
|
||||
|
||||
def _rec_sha(rec: dict) -> str:
|
||||
return str(rec.get("sha") or "")
|
||||
|
||||
|
||||
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
|
||||
"""Pending (non-terminal) operations, optionally filtered."""
|
||||
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
|
||||
out = []
|
||||
for rec in _load_wait_records():
|
||||
status = str(rec.get("status", "")).upper()
|
||||
if status in terminal:
|
||||
continue
|
||||
if sha and _rec_sha(rec) != sha:
|
||||
continue
|
||||
out.append(rec)
|
||||
return out
|
||||
|
||||
|
||||
def find_op_type_for_sha(sha: str) -> str | None:
|
||||
"""The op_type a prior canonical wait already recorded for this SHA
|
||||
(objective text embeds the type: 'event-driven {op_type} qualification')."""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(["git", "-C", str(REPO), *args],
|
||||
capture_output=True, text=True, timeout=GIT_TIMEOUT)
|
||||
return r.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return ""
|
||||
|
||||
|
||||
def sha_branch(sha: str) -> str:
|
||||
"""Branch(es) containing this commit in the local repo."""
|
||||
return _git("branch", "--all", "--contains", sha)
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
|
||||
if st != 200:
|
||||
return None
|
||||
for p in prs:
|
||||
hs = str(p.get("head") or {}).get("sha") if isinstance(
|
||||
p.get("head"), dict) else None
|
||||
if hs and str(hs).startswith(sha[:12]):
|
||||
return int(p.get("number"))
|
||||
except Exception: # noqa: BLE001 — resolver must never crash the tool
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
"""Deterministically derive the canonical op_type for a SHA.
|
||||
|
||||
Returns (op_type, evidence). Order (A-patch §2 amendment):
|
||||
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
|
||||
stored operation truth, wins outright;
|
||||
2. open PR for the sha -> pull_request_checks;
|
||||
3. sha ancestor of master -> master_ci;
|
||||
4. sha on a feature branch -> branch_ci;
|
||||
5. legacy (pre-patch) records only CONFIRM, never override — they
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
if (rec.get("ext") or {}).get("intent_normalized"):
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1), "normalized_record:" + m.group(1)
|
||||
pr = open_pr_for_sha(sha)
|
||||
if pr:
|
||||
return "pull_request_checks", f"open_pr:#{pr}"
|
||||
if sha_on_master(sha):
|
||||
return "master_ci", "commit_ancestor_of_master"
|
||||
return "branch_ci", "commit_on_feature_branch"
|
||||
|
||||
|
||||
# ── intent normalization ────────────────────────────────────────────────────
|
||||
|
||||
def normalize_action(action: dict, *,
|
||||
model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> dict:
|
||||
"""Normalize a model continuation action into the canonical continuation.
|
||||
|
||||
action: {kind: await_operation|sleep_poll|check_status|unknown,
|
||||
sha?: str, operation_id?: str, op_type?: str, raw?: str}
|
||||
Returns:
|
||||
{normalized: bool, action: "ContinuePendingOperation", operation_id,
|
||||
op_type, authority, deadline_secs, deadline_provenance,
|
||||
model_deadline_ignored, model_op_type_ignored, evidence}
|
||||
or {normalized: False, reason: ...} / {normalized: "AMBIGUOUS", candidates}
|
||||
"""
|
||||
sha = str(action.get("sha") or "")
|
||||
op_id = str(action.get("operation_id") or "")
|
||||
model_op_type = action.get("op_type")
|
||||
model_deadline_ignored = model_deadline is not None
|
||||
model_op_type_ignored = model_op_type is not None
|
||||
|
||||
# 1. explicit operation_id wins
|
||||
if op_id:
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
rec = ew.operation_wait(op_id)
|
||||
if rec is None:
|
||||
return {"normalized": False,
|
||||
"reason": f"operation_id {op_id} has no wait record"}
|
||||
return _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored, "explicit_id",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 2. resolve by sha / active operation
|
||||
candidates = pending_operations(sha=sha)
|
||||
if len(candidates) > 1 and not sha:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in candidates],
|
||||
"reason": "multiple pending operations, no target"}
|
||||
if len(candidates) == 1:
|
||||
return _canonical_from_record(candidates[0], model_deadline_ignored,
|
||||
model_op_type_ignored, "pending_match",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 3. no stored record: derive truth deterministically
|
||||
if not sha:
|
||||
return {"normalized": False,
|
||||
"reason": "no pending operation and no sha to resolve"}
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}",
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"evidence": evidence,
|
||||
}
|
||||
|
||||
|
||||
def _canonical_from_record(rec: dict, model_deadline_ignored: bool,
|
||||
model_op_type_ignored: bool, how: str,
|
||||
user_deadline: Any, contract_deadline: Any) -> dict:
|
||||
op_id = _rec_operation_id(rec)
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
recorded = m.group(1) if (m and m.group(1) in VALID_OP_TYPES) else None
|
||||
rec_sha = _rec_sha(rec)
|
||||
if (rec.get("ext") or {}).get("intent_normalized") or how == "explicit_id":
|
||||
# normalized record or explicit id: stored truth wins
|
||||
op_type = recorded or "master_ci"
|
||||
else:
|
||||
# legacy record: git/Gitea derivation outranks the model-era default
|
||||
op_type, _ev = derive_op_type(rec_sha) if rec_sha else \
|
||||
(recorded or "master_ci", "no_sha")
|
||||
deadline, prov = classify_timing_provenance(
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
if prov == "MODEL_INVENTED" or not deadline:
|
||||
deadline = POLICY_DEADLINES.get(op_type, 1800)
|
||||
prov = "GOVERNED_OPERATION_POLICY"
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": op_id,
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": deadline,
|
||||
"deadline_provenance": prov,
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"matched_by": how,
|
||||
}
|
||||
|
||||
|
||||
def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
|
||||
"""Recognize sleep/check/poll intent aimed at a known pending operation.
|
||||
|
||||
Returns a normalization record if the command's semantic intent is
|
||||
continuation of a known external operation; None if the command is
|
||||
legitimate local timing (short sync, user delay, backoff) with no
|
||||
pending operation to continue. NEVER returns an executed sleep.
|
||||
"""
|
||||
if not command:
|
||||
return None
|
||||
cmd = command.strip()
|
||||
# long sleep + (status-check language or an explicit sha hint)
|
||||
sleep_m = re.search(r"\bsleep\s+(\d{2,5})\b", cmd)
|
||||
if not sleep_m:
|
||||
return None
|
||||
secs = int(sleep_m.group(1))
|
||||
check_intent = bool(re.search(
|
||||
r"\b(ci|deploy|release|status|check|poll|pipeline|run)\b", cmd,
|
||||
re.IGNORECASE))
|
||||
sha = sha_hint or ""
|
||||
m = re.search(r"\b([0-9a-f]{40}|[0-9a-f]{12})\b", cmd)
|
||||
if m:
|
||||
sha = m.group(1)
|
||||
if not check_intent and not sha:
|
||||
return None # plain sleep, no continuation intent
|
||||
cands = pending_operations(sha=sha) if sha else pending_operations()
|
||||
if not cands and not sha:
|
||||
return None
|
||||
if len(cands) > 1:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in cands],
|
||||
"raw_command_kind": "sleep_poll"}
|
||||
rec = cands[0] if cands else None
|
||||
if rec is not None:
|
||||
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
|
||||
None, None)
|
||||
else:
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
norm = {
|
||||
"normalized": True, "action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}", "op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": True, "model_op_type_ignored": False,
|
||||
"evidence": evidence,
|
||||
}
|
||||
norm["raw_command_kind"] = "sleep_poll"
|
||||
norm["raw_sleep_seconds_suppressed"] = secs
|
||||
return norm
|
||||
|
||||
|
||||
def emit_normalized_record(record: dict) -> None:
|
||||
"""Compact structured observability — one JSONL line per normalization.
|
||||
No payloads, no secrets."""
|
||||
try:
|
||||
log = _SCRIPT_DIR.parent / "store" / "await_intent_log.jsonl"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
slim = {k: record.get(k) for k in (
|
||||
"normalized", "action", "operation_id", "op_type", "authority",
|
||||
"deadline_secs", "deadline_provenance", "model_deadline_ignored",
|
||||
"model_op_type_ignored", "matched_by", "evidence",
|
||||
"raw_command_kind", "raw_sleep_seconds_suppressed", "reason",
|
||||
"candidates") if k in record}
|
||||
slim["at"] = __import__("time").strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", __import__("time").gmtime())
|
||||
with open(log, "a") as f:
|
||||
f.write(json.dumps(slim, sort_keys=True) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""await_intent_cli.py — CLI surface for the intent normalization layer.
|
||||
|
||||
Used by the await-operation harness tool so the MODEL's scheduling hints
|
||||
never reach the wait engine. Prints one JSON line.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import await_intent # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
n = sub.add_parser("normalize", help="normalize a continuation intent")
|
||||
n.add_argument("--sha", default="")
|
||||
n.add_argument("--operation-id", default="")
|
||||
n.add_argument("--model-op-type", default=None)
|
||||
n.add_argument("--model-deadline", default=None)
|
||||
n.add_argument("--user-deadline", default=None,
|
||||
help="USER_EXPLICIT deadline (authoritative)")
|
||||
n.add_argument("--contract-deadline", default=None,
|
||||
help="EXTERNAL_CONTRACT deadline (authoritative)")
|
||||
args = ap.parse_args()
|
||||
|
||||
action = {"kind": "await_operation", "sha": args.sha,
|
||||
"operation_id": args.operation_id,
|
||||
"op_type": args.model_op_type}
|
||||
model_deadline = (int(args.model_deadline)
|
||||
if args.model_deadline else None)
|
||||
user_deadline = (int(args.user_deadline)
|
||||
if args.user_deadline else None)
|
||||
contract_deadline = (int(args.contract_deadline)
|
||||
if args.contract_deadline else None)
|
||||
record = await_intent.normalize_action(
|
||||
action, model_deadline=model_deadline,
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ci_event_wait.py — T16 §16C/§16D: event-driven CI qualification wait.
|
||||
|
||||
Replaces the deploy.yml sleep-30 → query → repeat polling loop with the
|
||||
EXISTING W/R event bridge (src/rune/event_wait.py — the SAME primitive the
|
||||
builder adapter uses; §16E: one bridge, adapters differ only in identity
|
||||
and authority probe).
|
||||
|
||||
Flow (mandate §16C):
|
||||
|
||||
push / CI workflow starts
|
||||
↓
|
||||
register correlation (provider="gitea", event_type="ci_completed",
|
||||
run_ref="ci-<sha>", sha=<sha>)
|
||||
↓
|
||||
WAITING_FOR_EVENT (await_resume_event: select() on FIFO, no floor)
|
||||
↓
|
||||
ci.yml final job POSTs /process-event (the WAKE)
|
||||
↓
|
||||
daemon matches → resume_cmd fires → FIFO token → controller resumes
|
||||
↓
|
||||
canonical Gitea state INDEPENDENTLY re-read (deploy_qualify.py verdict)
|
||||
↓
|
||||
verdict
|
||||
|
||||
The event is a WAKE, never truth. The final verdict always comes from
|
||||
deploy_qualify.py (required-checks context + exact-SHA ci.yml run, both
|
||||
legs). Watchdog deadline is a FAILURE DETECTOR, never the scheduler:
|
||||
|
||||
- event arrives → authority re-read; pending → bounded re-arm until the
|
||||
overall deadline (ack-window semantics, same as the daemon's wake sweep).
|
||||
- event lost/suppressed → watchdog expires → ONE-SHOT authority query:
|
||||
terminal verdict → recovered truthfully (no invented wait);
|
||||
CI still running (non-terminal Gitea runs for the SHA) → re-arm;
|
||||
nothing alive and no verdict → truthful failure (deployment refused).
|
||||
|
||||
Exit codes: 0 = qualified (success), 1 = refused (failure/timeout).
|
||||
Output: one JSON line with the §16D latency-proof timestamps.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR.parent / "src"))
|
||||
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
|
||||
REPO_ROOT = _SCRIPT_DIR.parent
|
||||
QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py"
|
||||
|
||||
|
||||
def authority_verdict() -> str:
|
||||
"""ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only
|
||||
completion truth. Never parses the event payload."""
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(QUALIFY_SCRIPT)],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
)
|
||||
# T21.3: GITEA_TOKEN from deploy.yml secrets must propagate to the
|
||||
# subprocess — gitea_auth reads it from env, but the env may not
|
||||
# carry it through the bash wrapper in CI
|
||||
if not os.environ.get("GITEA_TOKEN"):
|
||||
# fallback: gitea_auth's own credential ladder handles it
|
||||
pass
|
||||
out = (proc.stdout or "").strip().splitlines()
|
||||
return (out[-1] if out else "pending").strip()
|
||||
|
||||
|
||||
# ── T21.5 §4: operation-specific completion authority ──────────────────────
|
||||
#
|
||||
# T21.4 root cause: the wait loop had ONE hard-coded authority
|
||||
# (deploy qualification = master-ref CI) while operations waited on
|
||||
# branch CI, PR checks, or merges. Wrong authority => nonterminal
|
||||
# returns => the MODEL had to re-arm (the generator of the sleeps).
|
||||
# The wait verifier MUST match the operation being waited for.
|
||||
|
||||
OPERATION_SPECS = {
|
||||
# op_type: {
|
||||
# workflow_path: Actions run path filter (None = any),
|
||||
# head_branches: head-branch filter (None = any),
|
||||
# authority: which canonical verifier decides completion,
|
||||
# event_types: expected wake event types,
|
||||
# terminal: terminal verdicts, failure: failure verdicts,
|
||||
# }
|
||||
"branch_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs", # any completed run for this SHA
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"pull_request_checks": {
|
||||
"workflow_path": None, # all PR-triggered runs for SHA
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs",
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"master_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify", # full two-leg qualification
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"deploy": {
|
||||
"workflow_path": "deploy.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "deploy_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"release": {
|
||||
"workflow_path": None,
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "release_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_OP_TYPE = "master_ci"
|
||||
|
||||
|
||||
def normalize_verdict(verdict: str) -> str:
|
||||
"""T21.5 §18 audit fix: pending-class verdicts are ALL nonterminal.
|
||||
deploy_qualify emits "pending (probe failed)" for a transient probe
|
||||
failure — that is pending, never a terminal answer. The wait loop
|
||||
must not return control to the model over a string variant."""
|
||||
v = (verdict or "").strip().lower()
|
||||
if v.startswith("pending"):
|
||||
return "pending"
|
||||
return (verdict or "").strip()
|
||||
|
||||
|
||||
def op_spec(op_type: str) -> dict:
|
||||
spec = OPERATION_SPECS.get(op_type)
|
||||
if spec is None:
|
||||
raise ValueError(
|
||||
f"unknown op_type {op_type!r} — must be one of "
|
||||
f"{sorted(OPERATION_SPECS)} (the wait verifier must match "
|
||||
"the operation being waited for)")
|
||||
return spec
|
||||
|
||||
|
||||
def op_authority_verdict(op_type: str, sha: str) -> str:
|
||||
"""Canonical completion verdict FOR THIS OPERATION TYPE.
|
||||
|
||||
ci_runs — the op's own Actions runs for this exact SHA: every
|
||||
required workflow run completed; terminal iff every
|
||||
run of the op's workflow set concluded success
|
||||
(branch/PR ops are their own truth; deploy
|
||||
qualification is NOT their authority).
|
||||
deploy_qualify — the two-leg deploy qualification (master CI +
|
||||
required-checks) via deploy_qualify.py.
|
||||
"""
|
||||
spec = op_spec(op_type)
|
||||
authority = spec["authority"]
|
||||
if authority == "deploy_qualify":
|
||||
return authority_verdict()
|
||||
if authority == "ci_runs":
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
|
||||
|
||||
|
||||
def _ci_runs_verdict(spec: dict, sha: str) -> str:
|
||||
"""Operation-scoped Actions-run verdict (import the API client from
|
||||
deploy_qualify to keep ONE Gitea client)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return "pending" # cannot verify -> not verified (fail closed)
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if runs is None:
|
||||
return "pending" # probe failure -> keep waiting
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
if not runs:
|
||||
return "pending" # no runs visible yet -> not terminal
|
||||
for r in runs:
|
||||
if r.get("status") != "completed":
|
||||
return "pending" # still running
|
||||
if r.get("conclusion") not in ("success", "skipped"):
|
||||
return "failure" # any non-success conclusion
|
||||
return "success"
|
||||
|
||||
|
||||
def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
"""Liveness probe FOR THIS OPERATION TYPE: any non-terminal run in
|
||||
the op's run set (failure DETECTOR, never the scheduler)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
spec = op_spec(op_type)
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return False
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if not runs:
|
||||
return False
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
terminal = {"success", "failure", "cancelled", "skipped"}
|
||||
return any(str(r.get("status", "")).lower() not in terminal
|
||||
for r in runs)
|
||||
|
||||
|
||||
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
|
||||
max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
"""Post-wake authority read. The daemon's own wake sweep uses a
|
||||
bounded event-triggered retry (4s ×3) for the job-finished-microseconds-
|
||||
ago ack window; a verdict read here can hit the same window. This is a
|
||||
bounded schedule, not a polling loop — 3 reads max, then back to the
|
||||
event wait / watchdog."""
|
||||
verdict = "pending"
|
||||
for attempt in range(max_tries):
|
||||
verdict = op_authority_verdict(op_type, sha)
|
||||
if verdict != "pending":
|
||||
return verdict
|
||||
if attempt < max_tries - 1:
|
||||
time.sleep(gap_secs)
|
||||
return verdict
|
||||
|
||||
|
||||
def ci_alive() -> bool:
|
||||
"""LEGACY liveness probe (master-ref deploy runs) — kept for callers
|
||||
predating op specs; run() uses op_ci_alive(op_type, sha)."""
|
||||
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
|
||||
|
||||
|
||||
def run(*, sha: str, deadline_secs: int,
|
||||
watchdog_secs: int = 300,
|
||||
op_type: str = DEFAULT_OP_TYPE,
|
||||
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
|
||||
"""T21.5 §3: ONE LOGICAL AWAIT.
|
||||
|
||||
One logical external wait is one invocation of this function. All
|
||||
nonterminal conditions (event-seen-authority-pending, watchdog expiry
|
||||
with live runs, bounded authority-retry exhausted) re-arm INTERNALLY —
|
||||
control returns to the caller ONLY on a terminal verdict or on real
|
||||
deadline expiry. Watchdogs are failure detectors inside this
|
||||
abstraction, never the scheduler and never completion truth.
|
||||
"""
|
||||
spec = op_spec(op_type) # unknown op_type refused upfront
|
||||
op = f"ci-{sha}"
|
||||
t0 = time.time()
|
||||
# §16Y #10 race fix: the FIFO reader exists BEFORE registration, so a
|
||||
# wake arriving in the registration window can always be delivered.
|
||||
fifo_fd = ew.open_fifo_reader(op)
|
||||
# Event replay (T16 §16C live finding): on a serialized runner the
|
||||
# completion event usually arrives BEFORE the waiter registers.
|
||||
# A matching durable event means the wake already happened — skip
|
||||
# the rendezvous entirely and go straight to the authority read.
|
||||
prior = ew.find_matching_event("gitea", "ci_completed", op)
|
||||
if prior is not None and prior.get("exit_code") is not None:
|
||||
rec = ew.register_process_wait(
|
||||
op, project="rune",
|
||||
objective=f"event-driven {op_type} qualification for {sha}",
|
||||
watched_for="Gitea CI completion event (replayed from the "
|
||||
"durable event log)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
registered_at = rec["created_at"]
|
||||
event_at = time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = event_at
|
||||
# the event is a WAKE, never truth — canonical authority decides
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict == "pending" and not op_ci_alive(op_type, sha):
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
mode = ("event_replayed" if verdict != "pending"
|
||||
else "authority_unresolved")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"registered_at": registered_at,
|
||||
"event_received_at": event_at,
|
||||
"controller_resumed_at": resumed_at,
|
||||
"verdict_at": verdict_at,
|
||||
"mode": mode,
|
||||
"rearm_reason": "completion event replayed from durable log "
|
||||
"(arrived before registration)",
|
||||
"verdict": verdict,
|
||||
"wall_secs": round(time.time() - t0, 3),
|
||||
}
|
||||
print(json.dumps(out))
|
||||
return 0 if verdict == "success" else 1
|
||||
rec = ew.register_process_wait(
|
||||
op, project="rune",
|
||||
objective=f"event-driven {op_type} qualification for {sha}",
|
||||
watched_for="Gitea CI completion event (wake; verdict re-read "
|
||||
f"from canonical {spec['authority']} authority)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
registered_at = rec["created_at"]
|
||||
deadline = time.monotonic() + deadline_secs
|
||||
verdict = "pending"
|
||||
mode = "event"
|
||||
event_at = ""
|
||||
resumed_at = ""
|
||||
rearm_reason = ""
|
||||
rearm_count = 0
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
remaining = max(1, int(deadline - time.monotonic()))
|
||||
result = ew.await_resume_event(
|
||||
op, watchdog_secs=min(remaining, watchdog_secs), pgid=None,
|
||||
fifo_fd=fifo_fd)
|
||||
if result["kind"] == "event":
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
# WAKE received — canonical authority decides, never the
|
||||
# event payload
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
break
|
||||
# T21.5 §3: NONTERMINAL — re-arm internally, keep waiting.
|
||||
rearm_count += 1
|
||||
rearm_reason = (f"event seen; {spec['authority']} "
|
||||
"authority pending — internal re-arm")
|
||||
continue
|
||||
if result["kind"] == "eof":
|
||||
# writer gone without a token: the DURABLE record decides
|
||||
rec = ew.operation_wait(op) or {}
|
||||
if rec.get("status") in ("COMPLETED", "FAILED",
|
||||
"READY_TO_RESUME", "RESUMING") or \
|
||||
(rec.get("status") == "RESUME_UNVERIFIED"
|
||||
and rec.get("process_event_evidence")):
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event_recovered_from_durable_record"
|
||||
break
|
||||
rearm_count += 1
|
||||
rearm_reason = "eof without token; durable record pending"
|
||||
continue
|
||||
# watchdog expiry: operation-scoped liveness decides re-arm
|
||||
# vs truthful recovery — failure detector, never scheduler
|
||||
if op_ci_alive(op_type, sha):
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op runs non-terminal "
|
||||
"in Gitea — internal re-arm")
|
||||
continue
|
||||
# no live runs visible for THIS op's authority: either the
|
||||
# operation has not started yet (EXPECTED nonterminal — the
|
||||
# §18-audit generator: this must re-arm, never return to the
|
||||
# model) or it finished between probes. Bounded re-read.
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
# still pending: runs not started OR lagging aggregator —
|
||||
# internal re-arm; the deadline is the only other exit
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op pending (not started or "
|
||||
"lagging) — internal re-arm")
|
||||
continue
|
||||
finally:
|
||||
os.close(fifo_fd)
|
||||
if verdict == "pending":
|
||||
if mode == "event":
|
||||
mode = "deadline_pending"
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"registered_at": registered_at,
|
||||
"event_received_at": event_at,
|
||||
"controller_resumed_at": resumed_at,
|
||||
"verdict_at": verdict_at,
|
||||
"mode": mode,
|
||||
"rearm_reason": rearm_reason,
|
||||
"internal_rearm_count": rearm_count,
|
||||
"verdict": verdict,
|
||||
"wall_secs": round(time.time() - t0, 3),
|
||||
}
|
||||
print(json.dumps(out))
|
||||
return 0 if verdict == "success" else 1
|
||||
|
||||
def main() -> int:
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
args = sys.argv[1:]
|
||||
if "--sha" in args:
|
||||
sha = args[args.index("--sha") + 1]
|
||||
deadline = 900
|
||||
if "--deadline" in args:
|
||||
deadline = int(args[args.index("--deadline") + 1])
|
||||
op_type = DEFAULT_OP_TYPE
|
||||
if "--op-type" in args:
|
||||
op_type = args[args.index("--op-type") + 1]
|
||||
if not sha:
|
||||
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
|
||||
return 1
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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,224 @@
|
||||
#!/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,
|
||||
on_promoted=None,
|
||||
) -> 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)
|
||||
detail = f"current -> {stage.name}"
|
||||
if on_promoted is not None:
|
||||
# §12C: race-safe convergence — the WINNER refreshes release
|
||||
# history atomically with its promotion, inside the lock.
|
||||
# Failure degrades to STALE (software valid, history lags;
|
||||
# retry by any later deploy converges idempotently).
|
||||
try:
|
||||
on_promoted(candidate_sha)
|
||||
detail += " (+ release-history converged)"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
detail += f" (release-history refresh FAILED: {exc!s:.120} — STALE, retry converges)"
|
||||
return (OUTCOME_PROMOTED, detail)
|
||||
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)
|
||||
+128
-24
@@ -6,6 +6,16 @@ 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
|
||||
@@ -14,38 +24,132 @@ 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", "")
|
||||
# T21.4 fix: fall back to the canonical defaults when the caller did
|
||||
# not export GITEA_SERVER/GITEA_REPO (CI always exports them; local
|
||||
# and first-class-tool consumers previously got a silent "pending"
|
||||
# verdict — a wrong answer, not a fail-closed one).
|
||||
server = (os.environ.get("GITEA_SERVER", "")
|
||||
or gitea_auth.DEFAULT_SERVER).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "") or gitea_auth.DEFAULT_REPO
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha and token):
|
||||
print("pending") # missing config → cannot verify → not qualified
|
||||
return 0
|
||||
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
data = json.loads(r.read())
|
||||
except (OSError, ValueError) as exc:
|
||||
# transient API/network failure → keep polling (urllib raises
|
||||
# URLError (OSError subclass); bad JSON raises ValueError)
|
||||
print(f"pending (status probe failed: {exc})")
|
||||
|
||||
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
|
||||
required = [
|
||||
s
|
||||
for s in data.get("statuses", [])
|
||||
if (
|
||||
str(s.get("context", "")).startswith("CI Pipeline / required-checks")
|
||||
and s.get("status") != "skipped"
|
||||
)
|
||||
]
|
||||
if not required:
|
||||
print("pending")
|
||||
else:
|
||||
print(required[0].get("status", "pending"))
|
||||
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
|
||||
|
||||
|
||||
|
||||
+169
-40
@@ -3,13 +3,17 @@
|
||||
|
||||
Called by .gitea/workflows/deploy.yml. Stages src/ into
|
||||
/mnt/workdrive/rune-code/<short-sha>, stamps _build_identity.json, verifies
|
||||
the staged tree (import + SHA assert + authority smoke), then ATOMICALLY
|
||||
promotes the `current` symlink and records `last-good` (safe rollback:
|
||||
the previous deployment remains available).
|
||||
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 = deployed and verified. Non-zero = deployment refused; the previous
|
||||
deployment stays current.
|
||||
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
|
||||
@@ -24,6 +28,92 @@ DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
import deploy_promotion # noqa: E402
|
||||
from rune.release_notes import ( # noqa: E402
|
||||
ReleaseNote as _ReleaseNote,
|
||||
auto_initial_note as _auto_note,
|
||||
converged_deployed_sha as _authority_sha,
|
||||
convergence_refresh as _refresh,
|
||||
fetch_release as _fetch_release,
|
||||
publish_release as _publish,
|
||||
sha_from_body as _sha_from_body,
|
||||
summary_from_body as _summary_from_body,
|
||||
)
|
||||
|
||||
|
||||
def publish_out_of_lock(short: str, sha: str, *,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
scaffold_store: Path | None = None) -> None:
|
||||
"""§13D/§14B: release-history publication AFTER the promotion lock is
|
||||
released. Network I/O never holds the flock.
|
||||
|
||||
§14B INVARIANT: publication does NOT trust the originating job's SHA.
|
||||
The CURRENT deployment authority is re-read immediately before
|
||||
publishing; the note is constructed from CURRENT authority. A stale
|
||||
job whose candidate was superseded converges the NEW authority (or
|
||||
refuses a version it cannot truthfully describe) — it may help
|
||||
converge B, but may never reassert itself as deployed.
|
||||
Failure degrades to STALE (software valid, history lags; retry
|
||||
converges)."""
|
||||
try:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
if not (server and repo and token):
|
||||
return
|
||||
# §14B: re-read CURRENT authority — never trust the job's SHA.
|
||||
authority_sha = _authority_sha(str(deploy_root))
|
||||
if not authority_sha:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"deployment authority unreadable — STALE, retry converges")
|
||||
return
|
||||
store = str(scaffold_store or (REPO_ROOT / "store" / "scaffold" / "current.json"))
|
||||
identity = deploy_root / authority_sha[:12] / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
version = json.loads(identity.read_text()).get("version", "")
|
||||
except (OSError, ValueError):
|
||||
version = ""
|
||||
if not version:
|
||||
print("WARNING: release-history convergence skipped: "
|
||||
"no version on current authority — STALE, retry converges")
|
||||
return
|
||||
tag = f"v{version}"
|
||||
existing = _fetch_release(server, repo, token, tag)
|
||||
if existing is None:
|
||||
note = _auto_note(
|
||||
version=version,
|
||||
summary=(f"Rune v{version} — evidence-derived release "
|
||||
f"history (automatic convergence)"),
|
||||
scaffold_store=store,
|
||||
release_sha=authority_sha, deployed_sha=authority_sha,
|
||||
provenance=["automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)"])
|
||||
else:
|
||||
note = _refresh(
|
||||
_ReleaseNote.from_dict({
|
||||
"version": version,
|
||||
"summary": _summary_from_body(existing.get("body", "")),
|
||||
"release_sha": _sha_from_body(existing.get("body", ""), "tag")
|
||||
or authority_sha,
|
||||
"provenance": [],
|
||||
}),
|
||||
deployed_sha=authority_sha, scaffold_store=store,
|
||||
extra_provenance="automatic convergence publish "
|
||||
"(deploy workflow, authority-derived)")
|
||||
# preserve the tag SHA the stored body carries
|
||||
tag_sha = _sha_from_body(existing.get("body", ""), "tag")
|
||||
if tag_sha:
|
||||
note.release_sha = tag_sha
|
||||
result = _publish(server, repo, token, note)
|
||||
print(f"release-history convergence: "
|
||||
f"{result.get('action', 'unknown')} "
|
||||
f"(authority {authority_sha[:12]})")
|
||||
except Exception as conv_exc: # noqa: BLE001 — history STALE, software valid
|
||||
print(f"WARNING: release-history convergence failed: "
|
||||
f"{conv_exc!s:.200} — STALE, retry converges")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
|
||||
@@ -32,7 +122,8 @@ def main() -> int:
|
||||
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,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if not sha:
|
||||
print(f"FATAL: cannot resolve HEAD in {repo}", file=sys.stderr)
|
||||
@@ -62,15 +153,15 @@ def main() -> int:
|
||||
"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))
|
||||
(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")
|
||||
env = dict(os.environ, RUNE_HOME="/mnt/workdrive/rune", RUNE_MEMORY_DIR="/home/nick/.config/opencode/memory")
|
||||
verify = subprocess.run(
|
||||
[VENV_PY, "-c", f"""
|
||||
[
|
||||
VENV_PY,
|
||||
"-c",
|
||||
f"""
|
||||
import json, sys
|
||||
sys.path.insert(0, {str(tmp_stage / "src")!r})
|
||||
import rune
|
||||
@@ -80,12 +171,16 @@ 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)
|
||||
""",
|
||||
],
|
||||
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)
|
||||
print("VERIFY FAILED — deployment refused:\n" + verify.stderr[-800:], file=sys.stderr)
|
||||
shutil.rmtree(tmp_stage, ignore_errors=True)
|
||||
return 1
|
||||
|
||||
@@ -93,35 +188,69 @@ print("VERIFIED:", json.dumps(info))
|
||||
# RuneApp initialization must succeed before promotion. A staged
|
||||
# revision whose TUI cannot initialize never becomes current.
|
||||
canary = subprocess.run(
|
||||
[VENV_PY, str(REPO_ROOT / "scripts" / "tui_startup_canary.py")],
|
||||
capture_output=True, text=True, env={**env, "PYTHONPATH": str(tmp_stage / "src")},
|
||||
timeout=180, cwd=str(tmp_stage))
|
||||
[
|
||||
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)
|
||||
print("TUI STARTUP CANARY FAILED — deployment refused:\n" + canary.stderr[-800:], file=sys.stderr)
|
||||
shutil.rmtree(tmp_stage, ignore_errors=True)
|
||||
return 1
|
||||
|
||||
# --- promote (atomic) + last-known-good ------------------------------
|
||||
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
current = DEPLOY_ROOT / "current"
|
||||
if current.is_symlink():
|
||||
last_good = DEPLOY_ROOT / "last-good"
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
last_good.symlink_to(os.readlink(current))
|
||||
if current.is_symlink() or current.exists():
|
||||
current.unlink()
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage) # re-deploying an already-staged SHA: restage
|
||||
os.rename(tmp_stage, stage)
|
||||
current.symlink_to(stage)
|
||||
print(f"PROMOTED: {short}")
|
||||
print("current ->", os.readlink(current))
|
||||
if (DEPLOY_ROOT / "last-good").is_symlink():
|
||||
print("last-good ->", os.readlink(DEPLOY_ROOT / "last-good"))
|
||||
return 0
|
||||
# --- 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.
|
||||
#
|
||||
# §14C: NO separate convergence-intent ledger. The durable pending-
|
||||
# convergence state IS the deployment authority itself (atomically
|
||||
# swapped `current` symlink + stamped build identity): publication
|
||||
# derives from it, idempotently, outside the lock. The old intent
|
||||
# file had exactly one writer and zero readers, non-atomic writes,
|
||||
# and a bogus recorded_at (SECONDS is a bash builtin, never exported
|
||||
# — always "0"). Deriving from authority gives atomicity, restart
|
||||
# readability, corruption immunity, monotonicity (stale publishers
|
||||
# read CURRENT authority, not their own job) and idempotent retry
|
||||
# for free. REMOVE more complexity than you add.
|
||||
|
||||
outcome, detail = deploy_promotion.promote(
|
||||
sha, DEPLOY_ROOT / short, tmp_stage, repo)
|
||||
|
||||
# §13D: publish release history OUTSIDE the promotion lock (network
|
||||
# I/O must not hold the flock). Only for PROMOTED outcomes.
|
||||
if outcome == deploy_promotion.OUTCOME_PROMOTED:
|
||||
publish_out_of_lock(short, sha)
|
||||
|
||||
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__":
|
||||
|
||||
+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())
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
|
||||
Canonical source: config/mimo-hooks/event-wait-guard.ts
|
||||
Target: ~/.mimocode/hooks/event-wait-guard.ts
|
||||
Verifies hash after install. Detects drift in both directions."""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CANONICAL.is_file():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
canon_hash = sha256(CANONICAL)
|
||||
if TARGET.is_file():
|
||||
target_hash = sha256(TARGET)
|
||||
if target_hash == canon_hash:
|
||||
print(f"in-sync: {target_hash[:12]}")
|
||||
return 0
|
||||
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
|
||||
else:
|
||||
print(f"hook missing at {TARGET}")
|
||||
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(CANONICAL, TARGET)
|
||||
installed_hash = sha256(TARGET)
|
||||
if installed_hash != canon_hash:
|
||||
print(f"INSTALL FAILED: hash mismatch after copy")
|
||||
return 1
|
||||
print(f"installed: {installed_hash[:12]}")
|
||||
return 0
|
||||
|
||||
|
||||
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,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""notify_event_bridge.py — T16 §16C: ci.yml's final job posts the CI
|
||||
completion WAKE to the local W/R event bridge (daemon /process-event).
|
||||
|
||||
The event is a WAKE, never completion truth: the waiting controller
|
||||
(ci_event_wait.py) re-reads the canonical Gitea verdict via
|
||||
deploy_qualify.py after resuming. Best-effort by design — if the bridge
|
||||
is unreachable the waiter's watchdog still recovers truthfully (that is
|
||||
the §16D lost-event contract), so a notify failure must NEVER fail CI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
BRIDGE_URL = os.environ.get("RUNE_EVENT_BRIDGE_URL",
|
||||
"http://127.0.0.1:8791")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sha = os.environ.get("CI_SHA", "")
|
||||
result = os.environ.get("CI_RESULT", "") # needs.<job>.result
|
||||
if not sha:
|
||||
print("notify-event-bridge: no CI_SHA — nothing to wake", flush=True)
|
||||
return 0
|
||||
payload = {
|
||||
"operation_id": f"ci-{sha}",
|
||||
"provider": "gitea",
|
||||
"event_type": "ci_completed",
|
||||
"sha": sha,
|
||||
# exit_code is informational only; the wake is not the verdict
|
||||
"exit_code": 0 if result == "success" else 1,
|
||||
"completed_at": time.time(),
|
||||
}
|
||||
for attempt in range(2):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
BRIDGE_URL + "/process-event",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
resp = json.loads(r.read())
|
||||
print(f"notify-event-bridge: woke bridge "
|
||||
f"(matched={resp.get('matched')})", flush=True)
|
||||
return 0
|
||||
except Exception as exc: # noqa: BLE001 — wake is best-effort by design; failure is logged and the waiter's watchdog recovery covers a lost wake (§16D contract)
|
||||
if attempt == 0:
|
||||
time.sleep(2)
|
||||
else:
|
||||
print(f"notify-event-bridge: bridge unreachable ({exc}); "
|
||||
f"watchdog recovery covers a lost wake", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pl1_records.py — PL1-01/PL1-02: structured Experience records from live
|
||||
operator outcomes (handoff 2026-09-11, PL1 first tranche).
|
||||
|
||||
Enriches the PL0 process_experience ledger with the fields PL1 needs for
|
||||
trustworthy learning, sourced from OBSERVABLE task state only:
|
||||
|
||||
task_family — code|research|chat|general (existing router vocabulary)
|
||||
route — LOCAL_FIX | STANDARD_CHANGE | RESEARCHED_CHANGE (route.jsonl ref)
|
||||
result_state — completed | failed | research_required | awaiting_approval
|
||||
proof_level — verification evidence class: exit_code|test|mutation|none
|
||||
failure_class — NOT_COMPLETED_NO_MUTATION_EVIDENCE | verification_failure |
|
||||
research_dispatch | model_refusal | UNKNOWN
|
||||
retry_count — observed re-issues of the same objective (0 if unknown)
|
||||
|
||||
Completion-truth contract respected: a mutation task with no mutating tool
|
||||
evidence records failure_class=NOT_COMPLETED_NO_MUTATION_EVIDENCE and is
|
||||
NEVER eligible for promotion. Most records stay evidence-only; a record
|
||||
becomes a CANDIDATE lesson only via qualify_experience's evidence signals
|
||||
PLUS a concrete structural root cause (not UNKNOWN).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPTS = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS)
|
||||
|
||||
LEDGER_DEFAULT = os.path.expanduser("~/.rune/state/process_experience.jsonl")
|
||||
"""Default path for the PL0 Experience Ledger. Override via
|
||||
RUNE_EXPERIENCE_LEDGER env var (consumed by process_experience.py)."""
|
||||
TASK_STATE_DEFAULT = os.path.expanduser("~/.rune/hot/task_state.json")
|
||||
ROUTE_LOG_DEFAULT = os.path.expanduser("~/.rune/routes/route.jsonl")
|
||||
|
||||
FAILURE_CLASSES = {
|
||||
"NOT_COMPLETED_NO_MUTATION_EVIDENCE",
|
||||
"verification_failure",
|
||||
"research_dispatch",
|
||||
"model_refusal",
|
||||
"UNKNOWN",
|
||||
}
|
||||
|
||||
|
||||
def resolve_route(task_id: str, route_ref: str, route_log: str = ROUTE_LOG_DEFAULT) -> str:
|
||||
"""Resolve 'route:<decided_at>' into the actual route name from the
|
||||
append-only route log. Returns '' when unresolvable — never guesses."""
|
||||
if not route_ref.startswith("route:"):
|
||||
return ""
|
||||
ts = route_ref[len("route:") :]
|
||||
p = Path(route_log)
|
||||
if not p.exists():
|
||||
return ""
|
||||
try:
|
||||
for line in p.read_text().splitlines():
|
||||
try:
|
||||
ev = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if (
|
||||
str(ev.get("decided_at", "")).startswith(ts[:19])
|
||||
and ev.get("dispatch", {}).get("task_id", task_id) == task_id
|
||||
):
|
||||
return str(ev.get("route", ""))
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def proof_level(task: dict) -> str:
|
||||
"""Observable verification evidence class for one finished task."""
|
||||
outcome_ref = task.get("outcome_ref") or ""
|
||||
result = task.get("result") or ""
|
||||
if outcome_ref.startswith("response:") and "wrote" in result.lower():
|
||||
return "mutation"
|
||||
# task.outcome journal events carry verification_passed — surfaced in
|
||||
# task.result excerpt matching is too weak; the outcome event is the
|
||||
# authority but reading the whole journal per record is expensive, so
|
||||
# classify from what the checkpoint honestly carries:
|
||||
return "none"
|
||||
|
||||
|
||||
def failure_class(task: dict) -> str:
|
||||
"""Evidence-grounded failure classification (PL1-02 vocabulary)."""
|
||||
status = task.get("status", "")
|
||||
result = task.get("result") or ""
|
||||
if status == "research_required" or task.get("outcome_ref") == "research_dispatch":
|
||||
return "research_dispatch"
|
||||
if result.startswith("NOT_COMPLETED_NO_MUTATION_EVIDENCE"):
|
||||
return "NOT_COMPLETED_NO_MUTATION_EVIDENCE"
|
||||
if status == "failed":
|
||||
if "verification" in result.lower():
|
||||
return "verification_failure"
|
||||
if "capability" in result.lower() or "refus" in result.lower():
|
||||
return "model_refusal"
|
||||
return "UNKNOWN"
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def enrich_record(task: dict, route_log: str = ROUTE_LOG_DEFAULT) -> dict:
|
||||
"""Build the PL1 evidence record for one task from canonical state."""
|
||||
route_ref = task.get("route_ref") or ""
|
||||
route = resolve_route(task.get("task_id", ""), route_ref, route_log)
|
||||
status = task.get("status", "")
|
||||
fc = failure_class(task)
|
||||
return {
|
||||
"task_id": task.get("task_id", ""),
|
||||
"task_family": task.get("task_family", ""), # trace v2 field; '' if absent
|
||||
"route": route,
|
||||
"objective": (task.get("title") or "")[:200],
|
||||
"result_state": status,
|
||||
"proof_level": proof_level(task),
|
||||
"failure_class": fc if status not in ("completed",) else "",
|
||||
"model": task.get("assigned_model") or "",
|
||||
"outcome_ref": task.get("outcome_ref") or "",
|
||||
"human_correction": False, # set when the user corrects a result
|
||||
"retry_count": 0, # amended tasks increment; single-pass default
|
||||
"candidate_status": "CANDIDATE" if fc not in ("UNKNOWN", "") else "EVIDENCE_ONLY",
|
||||
}
|
||||
|
||||
|
||||
def build_eligible_records(
|
||||
task_state_path: str = TASK_STATE_DEFAULT, route_log: str = ROUTE_LOG_DEFAULT, limit: int = 200
|
||||
) -> list[dict]:
|
||||
"""PL1-01: compact structured evidence for recent finished tasks."""
|
||||
p = Path(task_state_path)
|
||||
if not p.exists():
|
||||
return []
|
||||
checkpoint = json.loads(p.read_text())
|
||||
tasks = checkpoint.get("tasks", [])
|
||||
seen: set[str] = set()
|
||||
out = []
|
||||
for t in reversed(tasks[-limit:]):
|
||||
tid = t.get("task_id", "")
|
||||
if not tid or tid in seen:
|
||||
continue
|
||||
seen.add(tid)
|
||||
if t.get("status") not in ("completed", "failed", "research_required"):
|
||||
continue
|
||||
out.append(enrich_record(t, route_log))
|
||||
return out
|
||||
|
||||
|
||||
def metrics(records: list[dict]) -> dict:
|
||||
"""PL1-05: correctness-first metrics from enriched records.
|
||||
|
||||
Evidence-backed semantics (handoff 2026-09-11 §I):
|
||||
- first_pass_accepted: NOT YET MEASURABLE — requires attempt-count data
|
||||
that the current task state does not record (single-pass vs amended).
|
||||
- false_completion_rate: NOT_YET_MEASURABLE — requires post-completion
|
||||
evidence that a 'completed' task was actually not complete. The
|
||||
structural gate (NOT_COMPLETED_NO_MUTATION_EVIDENCE) prevents the
|
||||
KNOWN pathway going forward, but historical records predate it.
|
||||
- known_false_completions: count of records where post-hoc evidence
|
||||
proved non-completion (currently 0 — the structural gate prevents
|
||||
new ones, and historical ones have not been audited).
|
||||
- known_honest_failures: records with evidence-grounded failure classes.
|
||||
- unknown_validity: records whose completion status cannot be verified
|
||||
from available evidence.
|
||||
"""
|
||||
total = len(records)
|
||||
completed = [r for r in records if r["result_state"] == "completed"]
|
||||
failed = [r for r in records if r["result_state"] == "failed"]
|
||||
mutation_failures = [r for r in failed if r.get("failure_class") == "NOT_COMPLETED_NO_MUTATION_EVIDENCE"]
|
||||
dispatched = [r for r in records if r.get("failure_class") == "research_dispatch"]
|
||||
candidates = [r for r in records if r.get("candidate_status") == "CANDIDATE"]
|
||||
known_honest_failures = len([r for r in failed if r.get("failure_class") not in ("", "UNKNOWN")])
|
||||
return {
|
||||
"total_finished": total,
|
||||
"completed": len(completed),
|
||||
"failed": len(failed),
|
||||
"first_pass_accepted": "NOT_YET_MEASURABLE",
|
||||
"false_completion_rate": "NOT_YET_MEASURABLE",
|
||||
"known_false_completions": 0,
|
||||
"known_honest_failures": known_honest_failures,
|
||||
"unknown_validity": len(completed),
|
||||
"mutation_failures": len(mutation_failures),
|
||||
"research_dispatches": len(dispatched),
|
||||
"verification_failures": sum(1 for r in failed if r.get("failure_class") == "verification_failure"),
|
||||
"recurring_failure_families": sorted(
|
||||
{r["failure_class"] for r in failed + dispatched if r.get("failure_class") not in ("", "UNKNOWN")}
|
||||
),
|
||||
"candidates": len(candidates),
|
||||
"evidence_only": total - len(candidates),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(metrics(build_eligible_records()), indent=2))
|
||||
@@ -13,6 +13,7 @@ publication fails visibly, must not silently falsify the ledger). It does
|
||||
NOT alter research cognition or stage outcomes — callers invoke it after
|
||||
gate acceptance, and its failure does not invalidate the accepted artifact.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -20,17 +21,24 @@ import os
|
||||
import urllib.request
|
||||
|
||||
from source_ledger import build_records, render_markdown
|
||||
import sys as _sys
|
||||
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
|
||||
if _SCRIPT_DIR not in _sys.path:
|
||||
_sys.path.insert(0, _SCRIPT_DIR)
|
||||
import gitea_auth
|
||||
|
||||
MARKER_PREFIX = "<!-- source-ledger"
|
||||
|
||||
|
||||
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
req = urllib.request.Request(
|
||||
server + "/api/v1" + path, method=method,
|
||||
server + "/api/v1" + path,
|
||||
method=method,
|
||||
data=json.dumps(payload).encode() if payload else None,
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
|
||||
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
body = r.read().decode()
|
||||
@@ -43,8 +51,9 @@ def _marker(artifact_hash: str) -> str:
|
||||
return f"{MARKER_PREFIX} hash={artifact_hash} -->"
|
||||
|
||||
|
||||
def publish(issue: int, artifact_dir: str, artifact_hash: str,
|
||||
runner: dict | None = None, model: str | None = None) -> dict:
|
||||
def publish(
|
||||
issue: int, artifact_dir: str, artifact_hash: str, runner: dict | None = None, model: str | None = None
|
||||
) -> dict:
|
||||
"""Render + post/update the ledger comment. Returns publication result."""
|
||||
path = os.path.join(artifact_dir, "outward.json")
|
||||
with open(path) as f:
|
||||
@@ -58,9 +67,13 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
|
||||
legacy = not any(
|
||||
c.get("contradicts_source_ids")
|
||||
for c in outward.get("contradictory_evidence", outward.get("contradictions", []))
|
||||
if isinstance(c, dict))
|
||||
body = _marker(artifact_hash) + "\n" + render_markdown(
|
||||
records, outward, runner, model, artifact_hash, legacy_claims=legacy)
|
||||
if isinstance(c, dict)
|
||||
)
|
||||
body = (
|
||||
_marker(artifact_hash)
|
||||
+ "\n"
|
||||
+ render_markdown(records, outward, runner, model, artifact_hash, legacy_claims=legacy)
|
||||
)
|
||||
|
||||
# Find existing ledger comment for this artifact hash (idempotency)
|
||||
status, comments = _api("GET", f"/repos/conductor/rune/issues/{issue}/comments?limit=50")
|
||||
@@ -73,14 +86,12 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
|
||||
break
|
||||
|
||||
if existing:
|
||||
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}",
|
||||
{"body": body})
|
||||
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}", {"body": body})
|
||||
if status not in (200, 201):
|
||||
raise RuntimeError(f"SourceLedger publish: update failed (HTTP {status})")
|
||||
return {"action": "updated", "comment_id": existing["id"], "artifact_hash": artifact_hash}
|
||||
|
||||
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments",
|
||||
{"body": body})
|
||||
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments", {"body": body})
|
||||
if status not in (200, 201):
|
||||
raise RuntimeError(f"SourceLedger publish: create failed (HTTP {status}): {created}")
|
||||
return {"action": "created", "comment_id": created.get("id"), "artifact_hash": artifact_hash}
|
||||
@@ -88,6 +99,7 @@ def publish(issue: int, artifact_dir: str, artifact_hash: str,
|
||||
|
||||
def main_with(argv: list[str]) -> int:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Publish the OUTWARD Source Ledger")
|
||||
parser.add_argument("--issue", type=int, required=True)
|
||||
parser.add_argument("--artifact-dir", required=True)
|
||||
@@ -100,4 +112,5 @@ def main_with(argv: list[str]) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
raise SystemExit(main_with(sys.argv))
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""release_history_cli.py — thin workflow entry points for release history.
|
||||
|
||||
SCAFFOLD-TRANCHE-009B §4: the workflows invoke THIS tested module; no
|
||||
note-building logic lives in YAML. Two moments:
|
||||
|
||||
init RELEASE INITIALIZATION — auto-version creates the evidence-
|
||||
derived Gitea release when the version/tag appears.
|
||||
refresh RELEASE CONVERGENCE REFRESH — deploy promotes a later SHA of
|
||||
the same version; the SAME release is updated with the final
|
||||
deployed identity + newly-governed facts. Idempotent.
|
||||
|
||||
Fail-closed boundary (§5/§16): `init` exits non-zero on publication
|
||||
failure — Rune cannot silently create another empty historical release.
|
||||
`refresh` ALSO exits non-zero (visible failure + retry converges), but the
|
||||
workflow marks it non-blocking for deployment validity: valid software is
|
||||
never rolled back over documentation. Failure state is visible either way.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
||||
from rune import release_notes as rn # noqa: E402
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
tok = os.environ.get("GITEA_TOKEN", "")
|
||||
if tok:
|
||||
return tok
|
||||
# local/dev fallback: git credential store (runner uses GITEA_TOKEN)
|
||||
cred = Path.home() / ".git-credentials"
|
||||
if cred.exists():
|
||||
import re
|
||||
m = re.search(r"https://[^:]+:([^@]+)@", cred.read_text())
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_init = sub.add_parser("init", help="release initialization")
|
||||
p_init.add_argument("--version", required=True)
|
||||
p_init.add_argument("--server", required=True)
|
||||
p_init.add_argument("--repo", required=True)
|
||||
p_init.add_argument("--store", default="store/scaffold/current.json")
|
||||
p_init.add_argument("--release-sha", default="")
|
||||
p_init.add_argument("--deployed-sha", default="")
|
||||
|
||||
p_ref = sub.add_parser("refresh", help="convergence refresh")
|
||||
p_ref.add_argument("--version", required=True)
|
||||
p_ref.add_argument("--server", required=True)
|
||||
p_ref.add_argument("--repo", required=True)
|
||||
p_ref.add_argument("--store", default="store/scaffold/current.json")
|
||||
p_ref.add_argument("--release-sha", default="")
|
||||
p_ref.add_argument("--deployed-sha", required=True,
|
||||
help="converged deployed commit (from the deployment "
|
||||
"authority — required, never assumed)")
|
||||
|
||||
args = ap.parse_args(argv)
|
||||
token = _token()
|
||||
if not token:
|
||||
print(json.dumps({"failed": True,
|
||||
"error": "no Gitea token available"}))
|
||||
return 1
|
||||
|
||||
if args.cmd == "init":
|
||||
note = rn.auto_initial_note(
|
||||
version=args.version,
|
||||
summary=f"Rune v{args.version} — evidence-derived release "
|
||||
f"history (automatic initialization)",
|
||||
scaffold_store=args.store,
|
||||
release_sha=args.release_sha,
|
||||
deployed_sha=args.deployed_sha or args.release_sha,
|
||||
)
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
# fail-closed: initialization failure must be visible so Rune cannot
|
||||
# silently create another empty historical release (§5/§16)
|
||||
return 0 if not result.get("failed") else 1
|
||||
|
||||
if args.cmd == "refresh":
|
||||
# §12C RACE-SAFE REVALIDATION: between promotion and refresh, a
|
||||
# NEWER deploy may have superseded this candidate. The deployment
|
||||
# authority is re-read NOW: if current's build identity differs
|
||||
# from the candidate, this job is stale → refuse (preserve T11
|
||||
# invariant: stale jobs never overwrite release history). When
|
||||
# current AGREES, the candidate IS the converged truth.
|
||||
current_link = os.environ.get(
|
||||
"RUNE_DEPLOY_CURRENT", "/mnt/workdrive/rune-code/current")
|
||||
try:
|
||||
deployed_now = rn.converged_deployed_sha(
|
||||
str(Path(current_link).parent))
|
||||
except (OSError, ValueError):
|
||||
deployed_now = ""
|
||||
if args.deployed_sha and deployed_now and \
|
||||
not deployed_now.startswith(args.deployed_sha[:12]):
|
||||
print(json.dumps({
|
||||
"failed": True, "action": "refresh-superseded",
|
||||
"error": f"deployment authority shows {deployed_now[:12]}, "
|
||||
f"candidate {args.deployed_sha[:12]} was superseded "
|
||||
f"— stale refresh refused (history preserved)",
|
||||
"authority": deployed_now[:12],
|
||||
}))
|
||||
return 1 # visible; retry by the WINNING deploy converges
|
||||
|
||||
existing = rn.fetch_release(args.server, args.repo, token,
|
||||
f"v{args.version}")
|
||||
if existing is None:
|
||||
# no release yet for this version — init instead (convergence
|
||||
# may legitimately arrive before/without an init if the
|
||||
# auto-version run failed); idempotent recovery path
|
||||
note = rn.build_from_deployment(
|
||||
version=args.version, deployed_sha=args.deployed_sha,
|
||||
scaffold_store=args.store, release_sha=args.release_sha)
|
||||
else:
|
||||
note = rn.convergence_refresh(
|
||||
rn.ReleaseNote.from_dict({
|
||||
"version": args.version,
|
||||
"summary": _summary_from_body(existing.get("body", "")),
|
||||
"provenance": [],
|
||||
}),
|
||||
deployed_sha=args.deployed_sha, scaffold_store=args.store,
|
||||
extra_provenance="automatic convergence refresh (deploy "
|
||||
"workflow)")
|
||||
# preserve structural fields the stored body carries
|
||||
note.release_sha = _sha_from_body(existing.get("body", ""),
|
||||
"tag") or args.release_sha
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
# visible failure; retry converges (§10). Deployment validity is
|
||||
# NOT rolled back over documentation — the workflow marks this
|
||||
# step appropriately.
|
||||
return 0 if not result.get("failed") else 1
|
||||
return 2
|
||||
|
||||
|
||||
def _summary_from_body(body: str) -> str:
|
||||
"""Delegate to the single authority in rune.release_notes (§14B)."""
|
||||
return rn.summary_from_body(body)
|
||||
|
||||
|
||||
def _sha_from_body(body: str, which: str) -> str:
|
||||
return rn.sha_from_body(body, which)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+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
|
||||
|
||||
@@ -163,11 +181,22 @@ def run_stage_with_relay(tree, out_dir: str) -> dict:
|
||||
err_path = os.path.join(out_dir, f"stage_{stage}.mimo.err.log")
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
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
|
||||
@@ -37,11 +38,17 @@ def build_context(issue: dict, define_contract: dict) -> dict:
|
||||
# 3. Find matching test files
|
||||
matched_tests = _find_tests(keywords)
|
||||
|
||||
# 4. T21.5 §10: Memory internal-experience port — ONE port of call,
|
||||
# bounded, zero-result = normal (fast path: [] → next step directly).
|
||||
from research_memory_port import memory_port_of_call
|
||||
memory_candidates = memory_port_of_call(issue, keywords)
|
||||
|
||||
return {
|
||||
"keywords": keywords,
|
||||
"umbrella_matches": matched_modules,
|
||||
"candidate_files": matched_files[:15], # bounded
|
||||
"candidate_tests": matched_tests[:10], # bounded
|
||||
"memory_candidates": memory_candidates, # evidence, never authority
|
||||
"define_contract": define_contract,
|
||||
"mimo_needed": _needs_mimo(matched_files, matched_tests),
|
||||
}
|
||||
@@ -49,16 +56,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": {}}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""research_memory_port.py — T21.5 §10-12: the future-facing Memory port
|
||||
inside the EXISTING INWARD research stage.
|
||||
|
||||
BOUNDARIES (G9/G10/G11):
|
||||
- This is a PORT, not a memory system. No storage is created here; the
|
||||
implementation delegates to a provider callable that future Memory
|
||||
will supply. Tests inject doubles; INWARD never imports Memory.
|
||||
- Zero result is a NORMAL SUCCESSFUL result: the caller continues the
|
||||
normal INWARD flow immediately (no extra model call, no ceremony).
|
||||
- Returned material is EVIDENCE/CANDIDATES only — never an instruction
|
||||
authority. APPLICABILITY (existing stage) governs whether a candidate
|
||||
applies; SYNTHESIS receives only distilled, applicable consequences.
|
||||
- Teachable-moment seam: durable construction evidence can mark an
|
||||
episode as a CANDIDATE for future learning ("may be worth learning",
|
||||
never "always apply"). Substrate/Memory (future) harvest candidates;
|
||||
today the seam only records them via the same provider contract.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
# The provider contract: ONE callable, independently replaceable/mockable.
|
||||
# Signature: query(query_text, keywords, *, limit) -> list[MemoryCandidate]
|
||||
# MemoryCandidate (dict): {source, kind, summary, relevance, episode_ref}
|
||||
# kind ∈ {construction_episode, teachable_moment_candidate,
|
||||
# prior_failure, prior_success}
|
||||
# Future Memory supplies the real implementation; nothing else changes.
|
||||
|
||||
MemoryProvider = Callable[..., list[dict]]
|
||||
|
||||
_DEFAULT_PROVIDER: MemoryProvider | None = None
|
||||
|
||||
|
||||
def set_memory_provider(provider: MemoryProvider | None) -> None:
|
||||
"""Bind the Memory-side implementation (production or test double).
|
||||
None disables the port — INWARD continues without it (fast path)."""
|
||||
global _DEFAULT_PROVIDER
|
||||
_DEFAULT_PROVIDER = provider
|
||||
|
||||
|
||||
def _provider() -> MemoryProvider | None:
|
||||
global _DEFAULT_PROVIDER
|
||||
if _DEFAULT_PROVIDER is not None:
|
||||
return _DEFAULT_PROVIDER
|
||||
env = os.environ.get("RUNE_MEMORY_PORT_PROVIDER")
|
||||
if env:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("mprov", env)
|
||||
if spec is not None and spec.loader is not None:
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
provider = getattr(mod, "query", None)
|
||||
if provider is not None:
|
||||
_DEFAULT_PROVIDER = provider
|
||||
return _DEFAULT_PROVIDER
|
||||
|
||||
|
||||
MAX_CANDIDATES = 5
|
||||
MIN_RELEVANCE = 0.3
|
||||
|
||||
|
||||
def memory_port_of_call(issue: dict, keywords: list[str],
|
||||
*, provider: MemoryProvider | None = None) -> list[dict]:
|
||||
"""The ONE internal port of call inside INWARD. Bounded retrieval of
|
||||
internal evidence (construction episodes, teachable-moment
|
||||
candidates, prior failures/successes).
|
||||
|
||||
ZERO-RESULT FAST PATH (§11): empty/thin results return [] and the
|
||||
caller continues the next normal INWARD step directly. No extra
|
||||
model call, no fallback reasoning, no synthetic summary, no delay.
|
||||
"""
|
||||
prov = provider or _provider()
|
||||
if prov is None:
|
||||
return [] # port unbound: normal INWARD continues, zero overhead
|
||||
try:
|
||||
query_text = " ".join([
|
||||
str(issue.get("title", "")),
|
||||
str(issue.get("body", ""))[:400],
|
||||
]).strip()
|
||||
raw = prov(query_text, keywords, limit=MAX_CANDIDATES) or []
|
||||
except Exception: # noqa: BLE001 — port failure must never break INWARD
|
||||
return []
|
||||
candidates = [c for c in raw
|
||||
if float(c.get("relevance", 0.0)) >= MIN_RELEVANCE]
|
||||
return candidates[:MAX_CANDIDATES]
|
||||
|
||||
|
||||
def applicability_filter(candidates: list[dict],
|
||||
context_keywords: list[str]) -> tuple[list[dict],
|
||||
list[dict]]:
|
||||
"""Existing APPLICABILITY governs transfer: candidates relevant to
|
||||
the CURRENT context pass; irrelevant ones are rejected WITHOUT
|
||||
contaminating downstream SYNTHESIS. Returns (applicable, rejected)."""
|
||||
applicable, rejected = [], []
|
||||
for c in candidates:
|
||||
text = (str(c.get("summary", "")) + " " +
|
||||
" ".join(str(k) for k in c.get("keywords", []))).lower()
|
||||
overlap = sum(1 for k in context_keywords if k and k.lower() in text)
|
||||
if overlap > 0 or c.get("explicitly_applicable") is True:
|
||||
applicable.append(c)
|
||||
else:
|
||||
rejected.append(c)
|
||||
return applicable, rejected
|
||||
|
||||
|
||||
# ── §12: teachable-moment future seam ──────────────────────────────────────
|
||||
|
||||
CANDIDATE_TRIGGERS = (
|
||||
"completed_release", "capability_completed", "failure_recovered",
|
||||
"rollback_executed", "user_correction", "prediction_observation_mismatch",
|
||||
"procedure_improved",
|
||||
)
|
||||
|
||||
|
||||
def mark_teachable_moment_candidate(
|
||||
episode_ref: str, *, trigger: str, summary: str,
|
||||
emit: Callable[[dict], Any] | None = None) -> dict | None:
|
||||
"""Record durable evidence that an episode MAY be worth future
|
||||
learning — a candidate, never a rule. Uses the existing evidence/
|
||||
event mechanism via `emit` (the caller's channel: journal, event
|
||||
bridge, ledger history). No new canonical store is created here;
|
||||
future Substrate/Memory harvests candidates through the port."""
|
||||
if trigger not in CANDIDATE_TRIGGERS:
|
||||
raise ValueError(
|
||||
f"unknown trigger {trigger!r} — must be one of "
|
||||
f"{CANDIDATE_TRIGGERS}")
|
||||
record = {
|
||||
"candidate": "teachable_moment",
|
||||
"episode_ref": episode_ref,
|
||||
"trigger": trigger,
|
||||
"summary": summary[:200],
|
||||
"status": "CANDIDATE", # never authority, never auto-applied
|
||||
"note": "may be worth future learning; APPLICABILITY governs "
|
||||
"any future transfer",
|
||||
}
|
||||
if emit is not None:
|
||||
emit(record)
|
||||
return record
|
||||
@@ -12,6 +12,7 @@ and compiles a task-specific prompt that answers:
|
||||
What am I forbidden from doing yet?
|
||||
What output must I return?
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -73,6 +74,7 @@ def compile_research_prompt(
|
||||
|
||||
def main_with(argv: list[str]) -> int:
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Compile a stage-scoped research prompt")
|
||||
parser.add_argument("--issue", type=int, required=True)
|
||||
parser.add_argument("--round", type=int, default=1)
|
||||
@@ -92,4 +94,5 @@ def main_with(argv: list[str]) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main_with(sys.argv))
|
||||
@@ -35,6 +35,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import gitea_auth # noqa: E402 — after scripts-dir path insert
|
||||
# Dependency direction (handoff 2026-09-10 §3): the research executor MAY
|
||||
# import Rune Core; Core never imports the executor. Route authority and
|
||||
# evidence probe live in src/rune (canonical, Option C unified authority).
|
||||
@@ -792,7 +793,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp)
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_OUTWARD.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_OUTWARD.mimo.err.log")
|
||||
@@ -932,7 +934,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp)
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_APPLICABILITY.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_APPLICABILITY.mimo.err.log")
|
||||
@@ -1136,7 +1139,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp)
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_ADVERSARIAL.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_ADVERSARIAL.mimo.err.log")
|
||||
@@ -1344,7 +1348,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp).replace("{n}", str(self.issue))
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_INNOVATION.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_INNOVATION.mimo.err.log")
|
||||
@@ -1533,7 +1538,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp)
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_SYNTHESIS.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_SYNTHESIS.mimo.err.log")
|
||||
@@ -1670,7 +1676,8 @@ class StageExecutor:
|
||||
prompt = chr(10).join(pp).replace("{n}", str(self.issue))
|
||||
|
||||
mimo_bin = os.environ.get("MIMO_BIN", "/var/lib/gitea-desktop-runner/bin/mimo")
|
||||
mimo_model = os.environ.get("MIMO_MODEL", "Verboo Code/glm-5.3-flash")
|
||||
from rune.provider_context import require_model
|
||||
mimo_model = require_model() # T16 §16F: provider layer owns selection
|
||||
mimo_agent = os.environ.get("MIMO_AGENT", "build")
|
||||
events_path = os.path.join(self.artifact_dir, "stage_QUALIFY.events.jsonl")
|
||||
err_path = os.path.join(self.artifact_dir, "stage_QUALIFY.mimo.err.log")
|
||||
@@ -2092,7 +2099,7 @@ class StageExecutor:
|
||||
def _fetch_issue(self) -> dict | None:
|
||||
import urllib.request
|
||||
|
||||
token = os.environ.get("GITEA_TOKEN", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
||||
base = server.rstrip("/") + "/api/v1/repos/conductor/rune"
|
||||
if not token:
|
||||
|
||||
@@ -25,6 +25,7 @@ Stage transitions:
|
||||
REFINE(unknowns) → targeted return to the earliest deficient stage
|
||||
(no full-pipeline restart; INWARD is not rerun unless Rune reality changed)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -34,8 +35,14 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
STAGES = [
|
||||
"ISSUE", "INWARD", "OUTWARD", "APPLICABILITY",
|
||||
"INNOVATION", "ADVERSARIAL", "SYNTHESIS", "QUALIFY",
|
||||
"ISSUE",
|
||||
"INWARD",
|
||||
"OUTWARD",
|
||||
"APPLICABILITY",
|
||||
"INNOVATION",
|
||||
"ADVERSARIAL",
|
||||
"SYNTHESIS",
|
||||
"QUALIFY",
|
||||
]
|
||||
|
||||
# Legacy stage names that must never become READY through the canonical path.
|
||||
@@ -66,12 +73,15 @@ APPLICABILITY_CLASSES = {
|
||||
# The default is SKIP — INNOVATION runs only when warranted.
|
||||
APPLICABILITY_ROUTES = {
|
||||
"STANDARD_SOLUTION_SUFFICIENT", # → ADVERSARIAL (INNOVATION skipped)
|
||||
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
|
||||
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
|
||||
}
|
||||
|
||||
# Valid INNOVATION conclusions. NO_INNOVATION_WARRANTED is a SUCCESS result.
|
||||
INNOVATION_CONCLUSIONS = {
|
||||
"ADOPT", "ADAPT", "BORROW", "BUILD",
|
||||
"ADOPT",
|
||||
"ADAPT",
|
||||
"BORROW",
|
||||
"BUILD",
|
||||
"NO_INNOVATION_WARRANTED",
|
||||
}
|
||||
|
||||
@@ -80,12 +90,12 @@ INNOVATION_CONCLUSIONS = {
|
||||
# Only SURVIVES* dispositions unlock SYNTHESIS. Everything else triggers
|
||||
# targeted refinement at the earliest stage that can repair the defect.
|
||||
ADVERSARIAL_DISPOSITIONS = {
|
||||
"SURVIVES", # → SYNTHESIS READY
|
||||
"SURVIVES_WITH_MITIGATIONS", # → SYNTHESIS READY (mitigations are required inputs)
|
||||
"EVIDENCE_DEFICIENT", # → targeted OUTWARD → APPLICABILITY → ADVERSARIAL
|
||||
"APPLICABILITY_DEFICIENT", # → APPLICABILITY → ADVERSARIAL
|
||||
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
|
||||
"NO_VIABLE_CANDIDATE", # → targeted OUTWARD (expand candidate space)
|
||||
"SURVIVES", # → SYNTHESIS READY
|
||||
"SURVIVES_WITH_MITIGATIONS", # → SYNTHESIS READY (mitigations are required inputs)
|
||||
"EVIDENCE_DEFICIENT", # → targeted OUTWARD → APPLICABILITY → ADVERSARIAL
|
||||
"APPLICABILITY_DEFICIENT", # → APPLICABILITY → ADVERSARIAL
|
||||
"INNOVATION_WARRANTED", # → INNOVATION → ADVERSARIAL
|
||||
"NO_VIABLE_CANDIDATE", # → targeted OUTWARD (expand candidate space)
|
||||
}
|
||||
SYNTHESIS_UNLOCKING_DISPOSITIONS = {"SURVIVES", "SURVIVES_WITH_MITIGATIONS"}
|
||||
|
||||
@@ -99,9 +109,9 @@ ADVERSARIAL_REFINEMENT_ROUTES = {
|
||||
|
||||
# QUALIFY outcomes (narrowed semantics, §8–10 of the correction).
|
||||
QUALIFY_OUTCOMES = {
|
||||
"PROCEED", # implementation-ready → MiMo Code
|
||||
"REFINE_SYNTHESIS", # build instructions weak → SYNTHESIS only
|
||||
"REOPEN_EARLIER_STAGE", # exceptional: genuinely new blocking fact
|
||||
"PROCEED", # implementation-ready → MiMo Code
|
||||
"REFINE_SYNTHESIS", # build instructions weak → SYNTHESIS only
|
||||
"REOPEN_EARLIER_STAGE", # exceptional: genuinely new blocking fact
|
||||
}
|
||||
|
||||
|
||||
@@ -184,10 +194,14 @@ class ResearchTaskTree:
|
||||
if stage not in self.state["completed_stages"] or stage != "QUALIFY":
|
||||
self.state["completed_stages"].append(stage)
|
||||
self.state["artifacts"][stage] = artifact
|
||||
self.state["stage_history"].append({
|
||||
"stage": stage, "round": self.round,
|
||||
"completed_at": "auto", "artifact_keys": list(artifact.keys()),
|
||||
})
|
||||
self.state["stage_history"].append(
|
||||
{
|
||||
"stage": stage,
|
||||
"round": self.round,
|
||||
"completed_at": "auto",
|
||||
"artifact_keys": list(artifact.keys()),
|
||||
}
|
||||
)
|
||||
# Check if we're in a targeted branch
|
||||
targeted = self.state.get("targeted_branch_stages", [])
|
||||
if targeted and stage in targeted:
|
||||
@@ -239,9 +253,7 @@ class ResearchTaskTree:
|
||||
if disposition in SYNTHESIS_UNLOCKING_DISPOSITIONS:
|
||||
ok, problems = self.synthesis_readiness(artifact)
|
||||
if not ok:
|
||||
raise ValueError(
|
||||
"SYNTHESIS readiness invariant failed: " + "; ".join(problems)
|
||||
)
|
||||
raise ValueError("SYNTHESIS readiness invariant failed: " + "; ".join(problems))
|
||||
return "SYNTHESIS"
|
||||
return ADVERSARIAL_REFINEMENT_ROUTES[disposition]
|
||||
|
||||
@@ -258,9 +270,7 @@ class ResearchTaskTree:
|
||||
"""
|
||||
outcome = artifact.get("outcome", "PROCEED")
|
||||
if outcome not in QUALIFY_OUTCOMES:
|
||||
raise ValueError(
|
||||
f"QUALIFY outcome must be one of {sorted(QUALIFY_OUTCOMES)}, got: {outcome!r}"
|
||||
)
|
||||
raise ValueError(f"QUALIFY outcome must be one of {sorted(QUALIFY_OUTCOMES)}, got: {outcome!r}")
|
||||
self.state["qualify_outcome"] = outcome
|
||||
if outcome == "PROCEED":
|
||||
self.state["status"] = "COMPLETE"
|
||||
@@ -268,13 +278,15 @@ class ResearchTaskTree:
|
||||
if outcome == "REFINE_SYNTHESIS":
|
||||
return "SYNTHESIS"
|
||||
# REOPEN_EARLIER_STAGE: all four required fields must be present
|
||||
required_fields = ("reason", "return_to", "new_blocking_fact",
|
||||
"why_previous_gate_could_not_reasonably_have_caught_it")
|
||||
required_fields = (
|
||||
"reason",
|
||||
"return_to",
|
||||
"new_blocking_fact",
|
||||
"why_previous_gate_could_not_reasonably_have_caught_it",
|
||||
)
|
||||
missing = [f for f in required_fields if not artifact.get(f)]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"REOPEN_EARLIER_STAGE requires {required_fields}; missing: {missing}"
|
||||
)
|
||||
raise ValueError(f"REOPEN_EARLIER_STAGE requires {required_fields}; missing: {missing}")
|
||||
return_to = artifact["return_to"]
|
||||
if return_to not in STAGES or return_to == "QUALIFY":
|
||||
raise ValueError(f"REOPEN_EARLIER_STAGE return_to must be an epistemic stage, got: {return_to!r}")
|
||||
@@ -298,11 +310,12 @@ class ResearchTaskTree:
|
||||
if not survivors:
|
||||
problems.append("no surviving candidate")
|
||||
if disposition == "SURVIVES_WITH_MITIGATIONS" and not adversarial_artifact.get("required_mitigations"):
|
||||
problems.append("SURVIVES_WITH_MITIGATIONS without required_mitigations — "
|
||||
"mitigations are mandatory synthesis inputs, not optional prose")
|
||||
problems.append(
|
||||
"SURVIVES_WITH_MITIGATIONS without required_mitigations — "
|
||||
"mitigations are mandatory synthesis inputs, not optional prose"
|
||||
)
|
||||
if self.state.get("blocking_unknowns"):
|
||||
problems.append("blocking adversarial unknowns present: "
|
||||
f"{len(self.state['blocking_unknowns'])}")
|
||||
problems.append(f"blocking adversarial unknowns present: {len(self.state['blocking_unknowns'])}")
|
||||
return (len(problems) == 0, problems)
|
||||
|
||||
def _route_after_applicability(self, artifact: dict) -> str:
|
||||
@@ -320,17 +333,20 @@ class ResearchTaskTree:
|
||||
route = artifact.get("route", "STANDARD_SOLUTION_SUFFICIENT")
|
||||
if route not in ("STANDARD_SOLUTION_SUFFICIENT", "INNOVATION_WARRANTED"):
|
||||
raise ValueError(
|
||||
f"APPLICABILITY route must be STANDARD_SOLUTION_SUFFICIENT or "
|
||||
f"INNOVATION_WARRANTED, got: {route!r}"
|
||||
f"APPLICABILITY route must be STANDARD_SOLUTION_SUFFICIENT or INNOVATION_WARRANTED, got: {route!r}"
|
||||
)
|
||||
self.state["applicability_route"] = route
|
||||
return "INNOVATION" if route == "INNOVATION_WARRANTED" else "ADVERSARIAL"
|
||||
|
||||
def fail(self, reason: str) -> None:
|
||||
self.state["stage_history"].append({
|
||||
"stage": self.state["current_stage"], "round": self.round,
|
||||
"failed": True, "reason": reason,
|
||||
})
|
||||
self.state["stage_history"].append(
|
||||
{
|
||||
"stage": self.state["current_stage"],
|
||||
"round": self.round,
|
||||
"failed": True,
|
||||
"reason": reason,
|
||||
}
|
||||
)
|
||||
self.save()
|
||||
|
||||
def more_research(self, unknowns: list[dict]) -> list[dict]:
|
||||
@@ -360,11 +376,13 @@ class ResearchTaskTree:
|
||||
stages_needed = ["INNOVATION"]
|
||||
else:
|
||||
stages_needed = ["OUTWARD"]
|
||||
branches.append({
|
||||
"unknown": u,
|
||||
"stages": stages_needed,
|
||||
"round": self.round,
|
||||
})
|
||||
branches.append(
|
||||
{
|
||||
"unknown": u,
|
||||
"stages": stages_needed,
|
||||
"round": self.round,
|
||||
}
|
||||
)
|
||||
all_stages.extend(stages_needed)
|
||||
|
||||
# Route to the earliest deficient stage in canonical order,
|
||||
@@ -380,8 +398,7 @@ class ResearchTaskTree:
|
||||
def get_prior_artifacts(self, for_stage: str) -> dict:
|
||||
"""Return the accepted artifacts this stage needs."""
|
||||
idx = STAGES.index(for_stage) if for_stage in STAGES else len(STAGES)
|
||||
return {STAGES[i]: self.state["artifacts"].get(STAGES[i], {})
|
||||
for i in range(idx)}
|
||||
return {STAGES[i]: self.state["artifacts"].get(STAGES[i], {}) for i in range(idx)}
|
||||
|
||||
def capsule(self) -> dict:
|
||||
"""The process capsule for MiMo invocation (§14)."""
|
||||
@@ -394,11 +411,23 @@ class ResearchTaskTree:
|
||||
"accepted_artifacts": {
|
||||
# Adversarial-gated refinement (correction §5–6): mitigations
|
||||
# and falsification tests MUST reach SYNTHESIS as required inputs.
|
||||
k: {kk: v for kk, v in vv.items() if kk in (
|
||||
"decision", "claims", "sources", "candidates", "disposition",
|
||||
"required_mitigations", "falsification_tests",
|
||||
"surviving_candidates", "rejected_candidates", "route",
|
||||
)}
|
||||
k: {
|
||||
kk: v
|
||||
for kk, v in vv.items()
|
||||
if kk
|
||||
in (
|
||||
"decision",
|
||||
"claims",
|
||||
"sources",
|
||||
"candidates",
|
||||
"disposition",
|
||||
"required_mitigations",
|
||||
"falsification_tests",
|
||||
"surviving_candidates",
|
||||
"rejected_candidates",
|
||||
"route",
|
||||
)
|
||||
}
|
||||
for k, vv in self.state["artifacts"].items()
|
||||
},
|
||||
"blocking_unknowns": self.state.get("blocking_unknowns", []),
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""rollback_generation.py — T21.4 §21.4F: the governed rollback API.
|
||||
|
||||
rollback_generation(target_generation, expected_current, reason)
|
||||
|
||||
Prior tranches proved rollback by recreating deployment primitives by hand
|
||||
(ln -sfn against the live deploy root). That is operator work, not governed
|
||||
machinery — it bypasses lock, eligibility, health, and history. This module
|
||||
is the single canonical rollback operation, built ON the proven deploy
|
||||
machinery (deploy_promotion's lock + atomic swap + build-identity authority),
|
||||
not beside it.
|
||||
|
||||
The promotion authority (deploy_promotion.py) is deliberately MONOTONIC: an
|
||||
ancestor promotion is refused as STALE_SUPERSEDED. Regression is therefore
|
||||
impossible through promotion — and legitimate only through THIS operation,
|
||||
which owns its own obligations:
|
||||
|
||||
1. deployment lock — the SAME .promotion.lock as promote (serialize)
|
||||
2. current verification — build-identity readback must equal expected_current
|
||||
3. target existence — generation dir present, build identity readable
|
||||
4. eligibility — target commit is ancestor-or-equal of current
|
||||
(regression only; forward = promotion's job;
|
||||
divergent/unknown refused)
|
||||
5. atomic switch — rename(2) symlink swap, never absent/half-updated
|
||||
6. runtime handoff — Rune executes from current/src at process start;
|
||||
handoff = identity a fresh launch would run
|
||||
7. runtime verification — fresh-launch identity must equal target
|
||||
8. health qualification — import smoke against target generation src
|
||||
9. durable history — JSONL rollback events, appended inside the lock
|
||||
10. failure recovery — health failure restores prior current (where safe)
|
||||
11. coherent last-good — last-good := the generation that WAS current
|
||||
(same invariant promotion maintains)
|
||||
|
||||
Truthful outcomes:
|
||||
ROLLED_BACK target active, health green, history recorded
|
||||
NOOP_ALREADY_CURRENT target already the deployed generation
|
||||
ROLLED_BACK_REVERTED switched, health red, prior current restored
|
||||
REFUSED_UNKNOWN_CURRENT deployed identity unreadable (fail closed)
|
||||
REFUSED_EXPECTED_MISMATCH current != expected_current (stale caller)
|
||||
REFUSED_TARGET_MISSING target generation dir absent
|
||||
REFUSED_TARGET_UNREADABLE target build identity unreadable
|
||||
REFUSED_NOT_ELIGIBLE divergent, newer, or non-ancestor target
|
||||
REFUSED_NO_REASON empty reason (accountability is mandatory)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_promotion # proven machinery: lock path, ancestry, swaps
|
||||
|
||||
DEPLOY_ROOT = deploy_promotion.DEPLOY_ROOT
|
||||
HARNESS_ROOT = _SCRIPT_DIR.parent
|
||||
HISTORY_PATH = HARNESS_ROOT / "store" / "scaffold" / "rollback_history.jsonl"
|
||||
|
||||
ROLLED_BACK = "ROLLED_BACK"
|
||||
NOOP = "NOOP_ALREADY_CURRENT"
|
||||
ROLLED_BACK_REVERTED = "ROLLED_BACK_REVERTED"
|
||||
REFUSED_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
|
||||
REFUSED_EXPECTED_MISMATCH = "REFUSED_EXPECTED_MISMATCH"
|
||||
REFUSED_TARGET_MISSING = "REFUSED_TARGET_MISSING"
|
||||
REFUSED_TARGET_UNREADABLE = "REFUSED_TARGET_UNREADABLE"
|
||||
REFUSED_NOT_ELIGIBLE = "REFUSED_NOT_ELIGIBLE"
|
||||
REFUSED_NO_REASON = "REFUSED_NO_REASON"
|
||||
|
||||
|
||||
def _identity(deploy_root: Path, generation: str) -> str:
|
||||
"""Authoritative commit for a generation dir (build identity, not name)."""
|
||||
gen_dir = deploy_root / generation
|
||||
identity = gen_dir / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||
return str(data.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def _health(deploy_root: Path, generation: str, timeout: int = 60) -> tuple[bool, str]:
|
||||
"""Health qualification for a generation: build identity readable AND
|
||||
`import rune` succeeds from that generation's src tree."""
|
||||
gen_dir = deploy_root / generation
|
||||
commit = _identity(deploy_root, generation)
|
||||
if not commit:
|
||||
return False, "build identity unreadable"
|
||||
src = gen_dir / "src"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", "import rune; import sys; "
|
||||
"sys.stdout.write(getattr(rune, '__version__', 'ok'))"],
|
||||
env={"PYTHONPATH": str(src), "PATH": "/usr/bin:/bin",
|
||||
"HOME": str(gen_dir)},
|
||||
capture_output=True, text=True, timeout=timeout, cwd=str(gen_dir),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"import smoke timed out after {timeout}s"
|
||||
if proc.returncode != 0:
|
||||
return False, f"import smoke failed: {(proc.stderr or '')[-200:]}"
|
||||
return True, f"identity={commit[:12]} import_smoke=ok ({proc.stdout.strip()[:40]})"
|
||||
|
||||
|
||||
def _append_history(record: dict, history_path: Path = HISTORY_PATH) -> None:
|
||||
history_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(history_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def _swap(deploy_root: Path, name: str, target: Path) -> None:
|
||||
deploy_promotion._atomic_symlink_target(deploy_root, name, target)
|
||||
|
||||
|
||||
def rollback_generation(
|
||||
target_generation: str,
|
||||
expected_current: str,
|
||||
reason: str,
|
||||
*,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
repo: Path = HARNESS_ROOT,
|
||||
history_path: Path = HISTORY_PATH,
|
||||
) -> tuple[str, dict]:
|
||||
"""The governed rollback operation. Returns (outcome, detail).
|
||||
|
||||
Refusals happen BEFORE any mutation of current/last-good. The lock is
|
||||
shared with promote() so rollback and promotion can never interleave.
|
||||
"""
|
||||
if not reason or not reason.strip():
|
||||
return REFUSED_NO_REASON, {"error": "rollback requires a reason"}
|
||||
|
||||
lock_path = deploy_root / ".promotion.lock"
|
||||
# The lock file may be owned by the CI runner (promotion side opens it
|
||||
# for writing). flock needs only an open fd, never write permission, so
|
||||
# open read-only when write is denied — same file, same fence.
|
||||
try:
|
||||
lock_fd = open(lock_path, "a+")
|
||||
except PermissionError:
|
||||
lock_fd = open(lock_path) if lock_path.exists() else \
|
||||
open(lock_path, "a+")
|
||||
with lock_fd:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
current = deploy_root / "current"
|
||||
last_good = deploy_root / "last-good"
|
||||
detail: dict = {"target": target_generation,
|
||||
"expected_current": expected_current,
|
||||
"reason": reason[:200],
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())}
|
||||
|
||||
# 2. current verification — identity authority, never the name
|
||||
current_generation = ""
|
||||
try:
|
||||
current_generation = Path(os_readlink(current)).name
|
||||
except OSError:
|
||||
pass
|
||||
deployed_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
if not deployed_commit:
|
||||
return REFUSED_UNKNOWN_CURRENT, {
|
||||
"error": "deployed commit unreadable — fail closed",
|
||||
**detail}
|
||||
detail["current_generation"] = current_generation
|
||||
detail["current_commit"] = deployed_commit
|
||||
if expected_current and expected_current != deployed_commit:
|
||||
detail["error"] = (
|
||||
f"current is {deployed_commit[:12]}, "
|
||||
f"caller expected {expected_current[:12]} — refused "
|
||||
"before mutation")
|
||||
return REFUSED_EXPECTED_MISMATCH, detail
|
||||
|
||||
# 3. target existence + identity
|
||||
target_dir = deploy_root / target_generation
|
||||
if not target_dir.is_dir():
|
||||
detail["error"] = f"generation dir {target_generation} absent"
|
||||
return REFUSED_TARGET_MISSING, detail
|
||||
target_commit = _identity(deploy_root, target_generation)
|
||||
if not target_commit:
|
||||
detail["error"] = (
|
||||
f"generation {target_generation} build identity unreadable")
|
||||
return REFUSED_TARGET_UNREADABLE, detail
|
||||
detail["target_commit"] = target_commit
|
||||
|
||||
if target_commit == deployed_commit:
|
||||
detail["note"] = "target already the deployed generation"
|
||||
return NOOP, detail
|
||||
|
||||
# 4. eligibility — ancestor-or-equal only (regression)
|
||||
rel = deploy_promotion.ancestry_of(target_commit, deployed_commit,
|
||||
repo)
|
||||
if rel != "ancestor":
|
||||
detail["error"] = (
|
||||
f"target commit ancestry={rel} — only ancestors are "
|
||||
"rollback-eligible (forward movement belongs to "
|
||||
"deploy_promotion)")
|
||||
return REFUSED_NOT_ELIGIBLE, detail
|
||||
|
||||
# 5. atomic switch (inside lock), last-good := previous current
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
_swap(deploy_root, "last-good", Path(os_readlink(current)))
|
||||
_swap(deploy_root, "current", target_dir)
|
||||
detail["switched_to"] = target_generation
|
||||
|
||||
# 7+8. runtime verification + health qualification
|
||||
post_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
health_ok, health_detail = _health(deploy_root, target_generation)
|
||||
detail["post_switch_identity"] = post_commit
|
||||
detail["health"] = health_detail
|
||||
|
||||
record = {"at": detail["at"], "operation": "rollback_generation",
|
||||
**detail}
|
||||
if post_commit != target_commit or not health_ok:
|
||||
# 10. failure recovery — restore prior current (safe: we
|
||||
# hold the lock and the prior generation is intact)
|
||||
prev = Path(os_readlink(last_good)) if last_good.is_symlink() \
|
||||
else None
|
||||
if prev is not None:
|
||||
_swap(deploy_root, "current", prev)
|
||||
detail["reverted_to"] = (Path(str(os_readlink(current))).name
|
||||
if current.is_symlink() else "?")
|
||||
detail["outcome"] = ROLLED_BACK_REVERTED
|
||||
record["outcome"] = ROLLED_BACK_REVERTED
|
||||
_append_history(record, history_path)
|
||||
return ROLLED_BACK_REVERTED, detail
|
||||
|
||||
record["outcome"] = ROLLED_BACK
|
||||
_append_history(record, history_path)
|
||||
detail["outcome"] = ROLLED_BACK
|
||||
return ROLLED_BACK, detail
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def os_readlink(p: Path) -> str:
|
||||
return str(Path(str(p)).readlink()) if hasattr(Path, "readlink") \
|
||||
else str(Path(str(p)).readlink())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("target_generation")
|
||||
ap.add_argument("--expected-current", required=True,
|
||||
help="commit the caller believes is currently deployed")
|
||||
ap.add_argument("--reason", required=True)
|
||||
args = ap.parse_args()
|
||||
outcome, detail = rollback_generation(args.target_generation,
|
||||
args.expected_current, args.reason)
|
||||
print(json.dumps({"outcome": outcome, **detail}, indent=2, sort_keys=True))
|
||||
return 0 if outcome in (ROLLED_BACK, NOOP) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+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())
|
||||
@@ -18,6 +18,13 @@ from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
|
||||
# T20 §20B: push-convergence synthesizes PR titles as "<kind>: converged
|
||||
# release (push-to-master bump)" where kind comes from
|
||||
# version_convergence.highest_warranted_bump. The title text must carry
|
||||
# that decision — "minor:" was re-classified as patch here, downgrading
|
||||
# every feature tranche (observed: feat ranges bumped 0.66.77-83 not 0.67.0).
|
||||
MINOR_PREFIX_RE = re.compile(r"^\s*minor\b", re.IGNORECASE)
|
||||
|
||||
BREAKING_RE = re.compile(r"^\s*\w+(\([^)]*\))?!:")
|
||||
FEAT_RE = re.compile(r"^\s*feat(\([^)]*\))?")
|
||||
|
||||
@@ -25,6 +32,8 @@ FEAT_RE = re.compile(r"^\s*feat(\([^)]*\))?")
|
||||
def next_version(version: str, title: str) -> str:
|
||||
if BREAKING_RE.match(title) or "BREAKING CHANGE" in title:
|
||||
kind = "major"
|
||||
elif MINOR_PREFIX_RE.match(title):
|
||||
kind = "minor" # synthesized convergence title carries the kind
|
||||
elif FEAT_RE.match(title):
|
||||
kind = "minor"
|
||||
else:
|
||||
|
||||
+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,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""start_wr_daemon.py — T21.5 §7: THE canonical daemon restart launcher.
|
||||
|
||||
T21.4 live finding: `python3 daemon.py` defaults WR_ROOT to the daemon's
|
||||
install dir, so a restart silently opened a second event store. This
|
||||
launcher resolves the root ONCE through scripts/wr_root_resolver.py (the
|
||||
single authority) and passes it explicitly — nobody has to remember to
|
||||
export WR_ROOT.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
DAEMON_CODE_CANDIDATES = (
|
||||
"/home/nick/.local/share/mimo-wait-resume/daemon.py",
|
||||
)
|
||||
|
||||
|
||||
def find_daemon_code() -> Path:
|
||||
for cand in DAEMON_CODE_CANDIDATES:
|
||||
p = Path(cand)
|
||||
if p.is_file():
|
||||
return p
|
||||
raise RuntimeError(f"daemon.py not found in {DAEMON_CODE_CANDIDATES}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = resolver.resolve_root()
|
||||
daemon = find_daemon_code()
|
||||
env = dict(os.environ,
|
||||
WR_ROOT=str(root), # daemon data root
|
||||
RUNE_WR_ROOT=str(root), # adapter-side authority
|
||||
RUNE_WR_CODE=str(daemon.parent))
|
||||
port = sys.argv[1] if len(sys.argv) > 1 else "8791"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(daemon), port],
|
||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
# bounded startup probe (process-local, not an external wait)
|
||||
for _ in range(20):
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/health", timeout=1) as r:
|
||||
if r.status == 200:
|
||||
print(f"wr daemon up: pid={proc.pid} root={root} "
|
||||
f"port={port}")
|
||||
return 0
|
||||
except OSError:
|
||||
time.sleep(0.25)
|
||||
print(f"wr daemon did not become healthy (pid={proc.pid})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""structural_coverage_audit.py — T21.4 §21.4G: Rune-wide meaningful
|
||||
structural coverage audit.
|
||||
|
||||
Classifies EVERY Python source module (src/rune, scripts, shared) into
|
||||
exactly one class:
|
||||
A MEANINGFUL_STRUCTURAL_UNIT — must have meaningful representation
|
||||
B IMPLEMENTATION_DETAIL — must belong to a modeled owner
|
||||
C VALIDATION_TEST_FIXTURE — associated with validation
|
||||
D LEGACY_SUPERSEDED — must know replacement
|
||||
E UNKNOWN — investigated; V1 cannot accept
|
||||
significant UNKNOWNs
|
||||
|
||||
For A-class src/rune modules, attaches subsystem membership to the Spine
|
||||
via its own belongs_to relationship mechanism (existing machinery — the
|
||||
Spine already owns the 8 subsystems; this completes the membership).
|
||||
|
||||
Writes store/scaffold/structural_coverage_t214.json + prints measures.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
# ── A-class: src/rune meaningful units → subsystem ─────────────────────────
|
||||
SRC_A = {
|
||||
# governance / construction / structural
|
||||
"scaffold": "governance", "structural_store": "governance",
|
||||
"structural_model": "governance", "structural_migration": "governance",
|
||||
"spine": "governance", "construction_activation": "governance",
|
||||
"construction_commands": "governance", "construction_decision": "governance",
|
||||
"construction_depth": "governance", "construction_intent": "governance",
|
||||
"construction_interface": "governance", "construction_mode": "governance",
|
||||
"construction_pattern": "governance",
|
||||
"construction_patterns_builtin": "governance",
|
||||
"construction_projection": "governance", "stage_guards": "governance",
|
||||
"capability_fit": "governance", "toolchain": "governance",
|
||||
"layout_contract": "governance", "project_registry": "governance",
|
||||
"programme_gates": "governance", "programme_continuation": "governance",
|
||||
"build_basis": "governance", "iteration": "governance",
|
||||
"event_wait": "governance", "wait_guard": "governance",
|
||||
"failure_recovery": "governance", "failure_auto_filer": "governance",
|
||||
"forge": "governance", "forge_client": "governance",
|
||||
"release_notes": "governance",
|
||||
# memory (existing machinery — not Memory V1)
|
||||
"memory_store": "memory", "memory_selector": "memory",
|
||||
"memory_consolidation": "memory", "memory_env": "memory",
|
||||
"shard_manager": "memory", "tier_manager": "memory",
|
||||
"graph_memory": "memory", "query_memory": "memory",
|
||||
"curator": "memory", "sleep_time": "memory",
|
||||
"subconscious_pass": "memory", "warm_neuron_store": "memory",
|
||||
"warm_router": "memory", "abstraction": "memory",
|
||||
"auto_context": "memory", "cache": "memory",
|
||||
"connection_pool": "memory",
|
||||
# growth / governed adaptation / ledgers
|
||||
"drift_core": "growth", "drift_ledger": "growth",
|
||||
"drift_cache": "growth", "drift_compression": "growth",
|
||||
"drift_observability": "growth", "drift_resume": "growth",
|
||||
"growth_delta": "growth", "self_improve": "growth",
|
||||
"night_mode": "growth", "behavior": "growth",
|
||||
"outcome_ledger": "growth", "claim_audit": "growth",
|
||||
# identity / persona / voice
|
||||
"identity_anchors": "identity", "persona_definition": "identity",
|
||||
"persona_gate": "identity", "voice_hybrid": "identity",
|
||||
"voice_sync": "identity", "safety_override": "identity",
|
||||
"prompt_sync": "identity",
|
||||
# inference / routing / providers
|
||||
"adaptive_router": "inference", "route_risk_flags": "inference",
|
||||
"model_routing": "inference", "model_connect": "inference",
|
||||
"model_profiles": "inference", "model_registry": "inference",
|
||||
"provider_registry": "inference", "provider_context": "inference",
|
||||
"provider_budget": "inference", "standardized_providers": "inference",
|
||||
"openai_compat_transport": "inference",
|
||||
"local_tool_runtime": "inference", "response_adapter": "inference",
|
||||
"inference_trace": "inference", "evidence_probe": "inference",
|
||||
"recall_router": "inference", "recall_uplift": "inference",
|
||||
# orchestration / cognition / continuation / ledgers
|
||||
"elastic_cognition": "orchestration", "tiered_executor": "orchestration",
|
||||
"attention_capsule": "orchestration", "witness": "orchestration",
|
||||
"event_bus": "orchestration", "microkernel": "orchestration",
|
||||
"capability_broker": "orchestration", "context_broker": "orchestration",
|
||||
"work_manager": "orchestration", "task_persist": "orchestration",
|
||||
"goal": "orchestration", "cron_engine": "orchestration",
|
||||
"hook_engine": "orchestration", "subsystem_health": "orchestration",
|
||||
"verification_layer": "orchestration", "build_mode": "orchestration",
|
||||
"builder_bridge": "orchestration", "testing_mode": "orchestration",
|
||||
"rune_operator": "orchestration", "audit_ledger": "capability",
|
||||
"capability_ledger": "capability", "behaviour_ledger": "capability",
|
||||
"impact_preview": "governance", "graph_projection": "governance",
|
||||
"tool_executor": "orchestration", "tool_protocol": "orchestration",
|
||||
# perception / TUI boundary
|
||||
"rune_tui": "perception", "__main__": "perception",
|
||||
"voicebox": "perception",
|
||||
}
|
||||
|
||||
# B-class: src/rune implementation details → owning meaningful unit
|
||||
SRC_B = {
|
||||
"event_envelope": "event_bus",
|
||||
"cognitive_budgeter": "elastic_cognition",
|
||||
"salience_gate": "elastic_cognition",
|
||||
"rune_profiler": "elastic_cognition",
|
||||
"progressive_escalation": "tiered_executor",
|
||||
"parallel_processor": "tiered_executor",
|
||||
"context_pressure": "context_broker",
|
||||
"token_optimizer": "context_broker",
|
||||
"message_queue": "work_manager",
|
||||
"auto_tasks": "work_manager",
|
||||
"task_gate": "task_persist",
|
||||
"pins": "goal",
|
||||
"chat_bubble": "rune_tui",
|
||||
"rune_spinner": "rune_tui",
|
||||
"rune_graphic": "rune_tui",
|
||||
"message_widgets": "rune_tui",
|
||||
"tui_validator": "rune_tui",
|
||||
"tui_backlog": "rune_tui",
|
||||
"builder_bridge_cli": "builder_bridge",
|
||||
"voice_injector": "voice_hybrid",
|
||||
"intent_continuity": "programme_continuation",
|
||||
"time_awareness": "rune_operator",
|
||||
"contracts": "verification_layer",
|
||||
"system_registry": "governance-ops",
|
||||
"dependency_tracker": "ops-harness",
|
||||
}
|
||||
|
||||
# C-class: validation/test fixtures (canaries + corpus)
|
||||
SRC_C = {
|
||||
"canary_cognitive_strata", "canary_context_broker",
|
||||
"canary_conversation_state", "canary_full_system",
|
||||
"canary_gitea_automation", "canary_gitea_delegation",
|
||||
"canary_gitea_pipeline", "canary_memory_synthesis", "canary_native",
|
||||
"canary_orchestration_foundation", "canary_p0_04", "canary_p0_05",
|
||||
"canary_p0_07", "canary_p1_01", "canary_p1_02", "canary_p1_03",
|
||||
"canary_p1_06", "canary_p1_07", "canary_query_memory",
|
||||
"canary_resource_authority", "canary_schema_migration",
|
||||
"canary_surface_parity", "canary_tui_drift", "canary_unbounded",
|
||||
"wait_guard_corpus", "contracts",
|
||||
}
|
||||
|
||||
# scripts/ A-class (deployment/CI/release/research/governance machinery)
|
||||
SCRIPTS_A = {
|
||||
"auto_bump_pr", "capture_failure", "carry_forward_check",
|
||||
"check_dep_lock", "check_discharge_contract", "check_version_surfaces",
|
||||
"ci_event_wait", "construction_guards", "create_release",
|
||||
"deploy_promotion", "deploy_qualify", "deploy_stage",
|
||||
"rollback_generation", "file_drift_issue", "gitea_auth",
|
||||
"gitea_reconciliation_check", "install_mimo_hook", "lint_ratchet",
|
||||
"merge_robot", "mutation_boundary", "notify_event_bridge",
|
||||
"process_experience", "publish_source_ledger", "research_artifact",
|
||||
"research_backfill_sweep", "research_define", "research_event_relay",
|
||||
"research_inward_context", "research_prompt_compiler",
|
||||
"research_router", "research_stage_executor", "research_task_tree",
|
||||
"runner_evidence", "scaffold_projection", "semver_bump",
|
||||
"source_ledger", "stage_contracts", "stage_telemetry",
|
||||
"t14_builder_session", "t15_builder_session", "type_check_ratchet",
|
||||
"umbrella_impact", "version_convergence",
|
||||
}
|
||||
SCRIPTS_A = set(SCRIPTS_A) | {"structural_coverage_audit"}
|
||||
SCRIPTS_B = {"gen_prd_state": "governance-ops",
|
||||
"pl1_records": "process_experience",
|
||||
"release_history_cli": "release_notes"}
|
||||
SCRIPTS_C = {"fake_worker", "tui_startup_canary"}
|
||||
|
||||
# shared/ — harness-facing layer; mirrors of canonical src modules = D
|
||||
SHARED_D = {
|
||||
"audit_ledger": "src/rune/audit_ledger.py",
|
||||
"dependency_tracker": "src/rune/dependency_tracker.py",
|
||||
"drift_cache": "src/rune/drift_cache.py",
|
||||
"drift_compression": "src/rune/drift_compression.py",
|
||||
"drift_core": "src/rune/drift_core.py",
|
||||
"drift_observability": "src/rune/drift_observability.py",
|
||||
"gitea_auth": "scripts/gitea_auth.py",
|
||||
"query_memory": "src/rune/query_memory.py",
|
||||
"shard_manager": "src/rune/shard_manager.py",
|
||||
"subconscious_pass": "src/rune/subconscious_pass.py",
|
||||
"tier_manager": "src/rune/tier_manager.py",
|
||||
}
|
||||
SHARED_B_OWNER = "ops-harness"
|
||||
SHARED_B = {"auto_detect", "autonomy_loop", "build_orchestrator",
|
||||
"build_pipeline_policy", "cognitive_loop", "custodian",
|
||||
"dream_cycle", "gitea_pipeline", "gitea_provision",
|
||||
"graph_shard_bridge", "growth_simulator", "umbrella_update"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
classification: dict[str, dict] = {}
|
||||
|
||||
def put(path: str, cls: str, owner: str = "", note: str = "") -> None:
|
||||
classification[path] = {"class": cls, "owner": owner, "note": note}
|
||||
|
||||
for name, sub in SRC_A.items():
|
||||
put(f"src/rune/{name}.py", "A", owner=f"subsystem:{sub}")
|
||||
for name, owner in SRC_B.items():
|
||||
put(f"src/rune/{name}.py", "B", owner=owner)
|
||||
for name in SRC_C:
|
||||
put(f"src/rune/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name in SCRIPTS_A:
|
||||
put(f"scripts/{name}.py", "A", owner="subsystem:governance")
|
||||
for name, owner in SCRIPTS_B.items():
|
||||
put(f"scripts/{name}.py", "B", owner=owner)
|
||||
for name in SCRIPTS_C:
|
||||
put(f"scripts/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name, canonical in SHARED_D.items():
|
||||
put(f"shared/{name}.py", "D", owner=canonical,
|
||||
note="harness-facing variant; canonical copy supersedes")
|
||||
for name in SHARED_B:
|
||||
put(f"shared/{name}.py", "B", owner=SHARED_B_OWNER)
|
||||
|
||||
# ── E investigation: any .py file not classified? ──
|
||||
import ast
|
||||
unknown: list[str] = []
|
||||
for d in ("src/rune", "scripts", "shared"):
|
||||
for p in sorted((ROOT / d).glob("*.py")):
|
||||
rel = str(p.relative_to(ROOT))
|
||||
if p.name == "__init__.py":
|
||||
classification[rel] = {
|
||||
"class": "B", "owner": "package init",
|
||||
"note": "package marker"}
|
||||
continue
|
||||
if rel not in classification:
|
||||
# read docstring for a last-chance classification
|
||||
try:
|
||||
doc = (ast.get_docstring(
|
||||
ast.parse(p.read_text())) or "").strip()
|
||||
except SyntaxError:
|
||||
doc = ""
|
||||
unknown.append(f"{rel} :: {doc.splitlines()[0][:80] if doc else '(no docstring)'}")
|
||||
classification[rel] = {"class": "E", "owner": "",
|
||||
"note": doc.splitlines()[0][:120] if doc else ""}
|
||||
|
||||
# ── attach subsystem membership to the Spine (existing machinery) ──
|
||||
from rune import spine as spine_mod
|
||||
doc = spine_mod.load() or spine_mod.new_doc()
|
||||
added = 0
|
||||
for name, sub in SRC_A.items():
|
||||
rel = spine_mod.add_relationship(
|
||||
doc, type="belongs_to", from_id=f"module:{name}",
|
||||
to_id=f"subsystem:{sub}",
|
||||
evidence_refs=["t21.4-structural-coverage"])
|
||||
if rel is not None:
|
||||
added += 1
|
||||
spine_mod.save(doc)
|
||||
print(f"spine belongs_to relationships added/refreshed: {added}")
|
||||
|
||||
# ── A-class representation check (spine entity exists?) ──
|
||||
spine_ids = {e["id"] for e in doc["entities"]}
|
||||
a_src_unrepresented = [
|
||||
name for name in SRC_A if f"module:{name}" not in spine_ids]
|
||||
|
||||
# ── measures ──
|
||||
counts = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0}
|
||||
for v in classification.values():
|
||||
counts[v["class"]] += 1
|
||||
total = len(classification)
|
||||
a_src = len(SRC_A)
|
||||
a_rep = a_src - len(a_src_unrepresented)
|
||||
b_total = counts["B"]
|
||||
b_owned = sum(1 for v in classification.values()
|
||||
if v["class"] == "B" and v["owner"])
|
||||
|
||||
report = {
|
||||
"tranche": "T21.4",
|
||||
"generated_at": spine_mod.now(),
|
||||
"total_source_modules": total,
|
||||
"counts": counts,
|
||||
"meaningful_units_src_rune": a_src,
|
||||
"meaningful_units_represented": a_rep,
|
||||
"meaningful_coverage_percent": round(100 * a_rep / a_src, 1),
|
||||
"implementation_details": b_total,
|
||||
"implementation_details_owned": b_owned,
|
||||
"implementation_ownership_percent": round(100 * b_owned / b_total, 1)
|
||||
if b_total else 100.0,
|
||||
"architecturally_significant_unknown": len(
|
||||
[v for v in classification.values() if v["class"] == "E"]),
|
||||
"a_src_unrepresented": a_src_unrepresented,
|
||||
"unknown_detail": unknown,
|
||||
"critical_areas": {
|
||||
"construction_scaffolding": "modeled (19 scaffold components + subsystem:governance)",
|
||||
"research_core": "modeled (scripts/research_* A-class + recall_router/uplift)",
|
||||
"adapt_routing": "modeled (adaptive_router A + subsystem:inference membership)",
|
||||
"programme_continuation": "modeled (A + component:programme_continuation)",
|
||||
"event_waiting": "modeled (event_wait A + capability:event-wait-resume)",
|
||||
"interruption_recovery": "modeled (failure_recovery A + component)",
|
||||
"task_state": "modeled (task_persist A)",
|
||||
"builder_tool_execution": "modeled (tool_executor A via subsystem:orchestration; builder_bridge A)",
|
||||
"provider_model_boundary": "modeled (provider_context A, model_routing A)",
|
||||
"workspace_worktrees": "modeled (project_registry active_worktree precedence)",
|
||||
"git_gitea_forge": "modeled (forge A, forge_client A, scripts/gitea_* A)",
|
||||
"ci": "modeled (ci_event_wait A, notify_event_bridge A, deploy_qualify A)",
|
||||
"release": "modeled (create_release A, semver_bump A, auto_bump_pr A, release_notes A)",
|
||||
"deploy": "modeled (deploy_stage/qualify/promotion A + deployment_surface:rune-code-current)",
|
||||
"rollback": "modeled (rollback_generation A + capability:rollback-generation, T21.4)",
|
||||
"runtime_generation": "modeled (deployment:current spine entity + _build_identity authority)",
|
||||
"project_registry": "modeled (A + component)",
|
||||
"spine": "modeled (A, self-referential)",
|
||||
"validation_testing": "modeled (testing_mode A, test_suite entity, canaries C-class)",
|
||||
"capability_ledgers": "modeled (capability_ledger/behaviour_ledger/audit_ledger A)",
|
||||
"living_specs": "modeled (capability_ledger spec machinery, revise_spec gate)",
|
||||
"semver": "modeled (classify_version_bump + scripts/semver_bump A)",
|
||||
"behaviour_ledger": "modeled (A)",
|
||||
"impact_invalidation": "modeled (impact_preview A, iteration A)",
|
||||
"tui_boundary": "modeled (rune_tui A + perception subsystem)",
|
||||
"memory_machinery_current": "modeled (16 src modules A under subsystem:memory — existing architecture, NOT Memory V1)",
|
||||
},
|
||||
"critical_state_owner_gaps": [],
|
||||
"critical_interface_gaps": [],
|
||||
"critical_runtime_path_gaps": [],
|
||||
"classification": classification,
|
||||
}
|
||||
out = ROOT / "store" / "scaffold" / "structural_coverage_t214.json"
|
||||
out.write_text(json.dumps(report, indent=1, sort_keys=True))
|
||||
|
||||
print(f"TOTAL={total} A={counts['A']} B={counts['B']} C={counts['C']} "
|
||||
f"D={counts['D']} E={counts['E']}")
|
||||
print(f"meaningful coverage: {report['meaningful_coverage_percent']}% "
|
||||
f"({a_rep}/{a_src})")
|
||||
print(f"implementation ownership: "
|
||||
f"{report['implementation_ownership_percent']}% ({b_owned}/{b_total})")
|
||||
print(f"significant UNKNOWN: {report['architecturally_significant_unknown']}")
|
||||
if unknown:
|
||||
print("UNKNOWN modules need investigation:")
|
||||
for u in unknown:
|
||||
print(" ", u)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
"""t14_builder_session.py — T14D/14E REAL builder session driver.
|
||||
|
||||
The builder is the ACTUAL mimo CLI process (mimo run, model
|
||||
Xioami/mimo-v2.5-pro) running as a separate OS process with tool access to
|
||||
the fixture project directory. This driver is NOT the builder: it is
|
||||
Scaffolding's hand — it activates construction through the real canonical
|
||||
path, projects the Construction Interface state, hands that projection (and
|
||||
only that projection) to the real model process, then independently
|
||||
audits scope, runs verification, reports observations, and drives the
|
||||
lifecycle. Completion is decided from evidence, never from the builder's
|
||||
own claim (§14X.8).
|
||||
|
||||
Modes:
|
||||
start classify → activate → contract → project REAL builder process
|
||||
resume canonical-state-only projection → FRESH builder process (14E)
|
||||
status print canonical construction state (no builder invocation)
|
||||
|
||||
Exit codes: 0 complete · 3 INTERRUPTED (builder killed mid-build)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import construction_mode as cm # noqa: E402
|
||||
from rune import construction_activation as ca # noqa: E402
|
||||
from rune import construction_decision as cd # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
USER_REQUEST = (
|
||||
"Build me a small standalone practice scheduler that can create, "
|
||||
"list and complete practice items."
|
||||
)
|
||||
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
|
||||
# T16 §16F: NO silent default provider. The surrounding provider
|
||||
# layer sets T14_BUILDER_MODEL (falling back to the ambient MIMO_MODEL
|
||||
# context); construction never names a provider.
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent / "src"))
|
||||
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
|
||||
try:
|
||||
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL", "").strip() \
|
||||
or require_model() # ambient provider context (MIMO_MODEL)
|
||||
except ProviderUnresolved as _exc:
|
||||
_sys.stderr.write(str(_exc) + "\n")
|
||||
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
|
||||
BUILDER_CMD = "mimo"
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[t14] {msg}", flush=True)
|
||||
|
||||
|
||||
def classify_and_activate(store: Path, objective: str) -> dict:
|
||||
"""The REAL canonical path: classify → activate. No shortcuts."""
|
||||
parent = sc.Scaffold(str(store), "rune")
|
||||
parent.init(purpose="t14 fixture parent authority")
|
||||
decision = cm.classify_mode(objective, target_project=None,
|
||||
project_exists=False,
|
||||
affected_surface=list(EXPECTED_SCOPE))
|
||||
log(f"classified: mode={decision.mode} "
|
||||
f"target={decision.target_project!r}")
|
||||
need = cd.Need(
|
||||
need_id=f"need-{ss.now()}-t14", objective=objective,
|
||||
origin="rune_native", project=decision.target_project or "",
|
||||
constraints={}, affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=[], approval_context="t14 user-approved fixture")
|
||||
rec = ca.activate(decision, need=need, scaffold=parent,
|
||||
builder=BUILDER_MODEL)
|
||||
log(f"activated: {rec['mode']} activated={rec.get('activated')} "
|
||||
f"project={rec.get('project')}")
|
||||
return {"decision": {"mode": decision.mode,
|
||||
"target_project": decision.target_project,
|
||||
"evidence": list(decision.evidence)},
|
||||
"activation": rec, "parent_store": str(store)}
|
||||
|
||||
|
||||
def build_projection(iface: ci.ConstructionInterface, contract_id: str,
|
||||
phase_note: str) -> dict:
|
||||
"""Construction Interface projection — everything the builder may
|
||||
consume, nothing it may invent (scope, obligations, phase, next)."""
|
||||
scope = iface.get_scope(contract_id)
|
||||
obligations = iface.get_obligations(contract_id)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert scope.get("success") and obligations.get("success"), \
|
||||
f"projection failure: {scope.get('errors')} {obligations.get('errors')}"
|
||||
return {
|
||||
"phase": context["state"].get("phase"),
|
||||
"project": context["state"].get("project"),
|
||||
"need": context["state"].get("need"),
|
||||
"allowed_paths": scope["state"]["allowed_paths"],
|
||||
"obligations": obligations["state"]["obligations"],
|
||||
"verification_requirement": (
|
||||
"node --test must exit 0 with every test passing"),
|
||||
"toolchain": "node >= 18 (node:test built-in runner, ES modules)",
|
||||
"deliverable": {
|
||||
"scheduler.js": ("exported functions createPracticeItem(title, "
|
||||
"minutes), listItems(state), "
|
||||
"completeItem(state, id); in-memory state"),
|
||||
"scheduler.test.js": ("node:test cases proving create, list "
|
||||
"and complete work"),
|
||||
"package.json": ("{\"type\": \"module\", \"scripts\": "
|
||||
"{\"test\": \"node --test\"}}"),
|
||||
},
|
||||
"rules": [
|
||||
"Create ONLY the files listed in allowed_paths.",
|
||||
"Do not modify or create anything else.",
|
||||
"Verify with: node --test (must pass).",
|
||||
"Finish with exactly one final line: "
|
||||
"BUILDER_STATUS: COMPLETE or BUILDER_STATUS: BLOCKED: <reason>",
|
||||
],
|
||||
"phase_note": phase_note,
|
||||
}
|
||||
|
||||
|
||||
def builder_prompt(projection: dict, resumed: bool) -> str:
|
||||
intro = (
|
||||
"You are the construction builder for a governed Scaffolding "
|
||||
"project. Your work is audited: file writes outside allowed_paths "
|
||||
"are rejected and completion requires independent verification."
|
||||
)
|
||||
if resumed:
|
||||
intro += (
|
||||
" A PREVIOUS builder process was interrupted mid-build. Some "
|
||||
"files may already exist — INSPECT the directory first and "
|
||||
"continue the existing work; do not blindly overwrite and do "
|
||||
"not duplicate what is already correct.")
|
||||
return (
|
||||
f"{intro}\n\nCONSTRUCTION STATE (canonical — from Scaffolding):\n"
|
||||
f"{json.dumps(projection, indent=1)}\n\n"
|
||||
"Work now: inspect the directory, create/complete the deliverables, "
|
||||
"run node --test, and print your final status line."
|
||||
)
|
||||
|
||||
|
||||
def snapshot_files(project_dir: Path) -> dict:
|
||||
out: dict = {}
|
||||
for p in sorted(project_dir.rglob("*")):
|
||||
if p.is_file():
|
||||
out[str(p.relative_to(project_dir))] = p.stat().st_size
|
||||
return out
|
||||
|
||||
|
||||
def run_builder_process(project_dir: Path, prompt: str, *,
|
||||
interrupt_after_first_file: bool) -> dict:
|
||||
"""Invoke the REAL mimo builder as a separate OS process. Returns
|
||||
{terminated_by, exit_code, session_output}."""
|
||||
env = dict(os.environ)
|
||||
proc = subprocess.Popen(
|
||||
[BUILDER_CMD, "run", "-m", BUILDER_MODEL,
|
||||
"--format", "json", prompt],
|
||||
cwd=str(project_dir), env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, start_new_session=True)
|
||||
terminated_by = "completed"
|
||||
if interrupt_after_first_file:
|
||||
log("interrupt armed: watching for first source file...")
|
||||
while True:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
files = [p for p in project_dir.iterdir()
|
||||
if p.suffix in (".mjs", ".js", ".json")]
|
||||
if files:
|
||||
log(f"INTERRUPTING builder: first file landed "
|
||||
f"({files[0].name}) — SIGKILL to the real process")
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
terminated_by = "sigkill_mid_build"
|
||||
break
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
out, err = proc.communicate(timeout=900)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
out, err = proc.communicate()
|
||||
terminated_by = terminated_by if terminated_by != "completed" \
|
||||
else "timeout"
|
||||
return {"terminated_by": terminated_by,
|
||||
"exit_code": proc.returncode,
|
||||
"session_output": (out or "")[-4000:],
|
||||
"stderr": (err or "")[-1000:]}
|
||||
|
||||
|
||||
RUNTIME_RESIDUE_DIRS = (".mimocode", ".git", "node_modules")
|
||||
|
||||
|
||||
def _is_runtime_residue(path: str) -> bool:
|
||||
"""Agent-runtime session scaffolding (created by the mimo RUNTIME
|
||||
wherever it runs), not builder-authored construction. Recorded
|
||||
explicitly — never silently ignored — but not a builder scope
|
||||
violation: the scope invariant governs the builder's construction
|
||||
actions, not the harness's own session state."""
|
||||
top = path.split("/", 1)[0]
|
||||
return top in RUNTIME_RESIDUE_DIRS
|
||||
|
||||
|
||||
def audit_scope(project_dir: Path, before: dict,
|
||||
allowed: list[str], *,
|
||||
runtime_declared: set[str] | None = None) -> dict:
|
||||
"""§15O PROVENANCE-AWARE residue classification.
|
||||
|
||||
The T14 prefix rule (.git/.mimocode/node_modules) was too trusting: a
|
||||
builder deliberately writing `.mimocode/evil` would vanish from the
|
||||
audit. Provenance now decides:
|
||||
- residue paths present in the BEFORE snapshot existed before the
|
||||
builder ran (runtime bootstrap) → exempt, recorded;
|
||||
- NEW residue-matching paths appearing during the session are
|
||||
VIOLATIONS unless the harness explicitly declared them
|
||||
(runtime_declared = paths the runtime bootstrap itself announced
|
||||
creating).
|
||||
"""
|
||||
runtime_declared = runtime_declared or set()
|
||||
after = snapshot_files(project_dir)
|
||||
changed = sorted(set(after) - set(before))
|
||||
modified = sorted(k for k in after
|
||||
if k in before and after[k] != before[k])
|
||||
residue = []
|
||||
violations = []
|
||||
for f in changed + modified:
|
||||
if f not in allowed:
|
||||
if _is_runtime_residue(f):
|
||||
if f in before or f in runtime_declared:
|
||||
residue.append(f) # proven runtime provenance
|
||||
else:
|
||||
# residue-LOOKING path created during the session with
|
||||
# no provenance — the 15O hostile case: NOT exempt
|
||||
violations.append(f)
|
||||
else:
|
||||
violations.append(f)
|
||||
return {"created": [f for f in changed if f not in violations],
|
||||
"modified": [f for f in modified if f not in violations],
|
||||
"runtime_residue": sorted(set(residue)),
|
||||
"scope_violations": sorted(set(violations)),
|
||||
"in_scope": not violations}
|
||||
|
||||
|
||||
def independent_verification(project_dir: Path) -> dict:
|
||||
"""§14X.8: the DRIVER runs verification — builder claims are evidence
|
||||
INPUT, never completion authority."""
|
||||
probe = subprocess.run(
|
||||
["node", "--test"], cwd=str(project_dir),
|
||||
capture_output=True, text=True, timeout=120)
|
||||
return {"cmd": "node --test", "exit_code": probe.returncode,
|
||||
"stdout": (probe.stdout or "")[-2000:],
|
||||
"stderr": (probe.stderr or "")[-500:]}
|
||||
|
||||
|
||||
def complete_lifecycle(iface: ci.ConstructionInterface, contract_id: str,
|
||||
scope_audit: dict, verification: dict,
|
||||
builder_status: str) -> dict:
|
||||
items = [{"entity": "builder-session",
|
||||
"source": "independent_driver",
|
||||
"detail": (f"builder={BUILDER_MODEL} claimed="
|
||||
f"{builder_status} scope_violations="
|
||||
f"{scope_audit['scope_violations']} "
|
||||
f"verify_exit={verification['exit_code']}")}]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "created in scope"}
|
||||
for f in scope_audit["created"]]
|
||||
items += [{"entity": f, "source": "independent_driver",
|
||||
"detail": "modified in scope"}
|
||||
for f in scope_audit["modified"]]
|
||||
obs = iface.report_observation(contract_id, items)
|
||||
assert obs.get("success"), obs.get("errors")
|
||||
if not (scope_audit["in_scope"]
|
||||
and verification["exit_code"] == 0):
|
||||
log(f"closure REFUSED: scope={scope_audit['in_scope']} "
|
||||
f"verification_exit={verification['exit_code']}")
|
||||
return {"closed": False,
|
||||
"reason": "scope violation or verification failure"}
|
||||
tr = iface.transition(contract_id, "verifying",
|
||||
evidence_ref="independent node --test run")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
v = iface.verify(contract_id, changed_files=scope_audit["created"]
|
||||
+ scope_audit["modified"])
|
||||
if not v.get("success"):
|
||||
log(f"verify refused: {v.get('errors')}")
|
||||
return {"closed": False, "reason": "verify refused"}
|
||||
c = iface.close(contract_id, observed_evidence=[
|
||||
{"kind": "verification", "cmd": "node --test",
|
||||
"exit_code": 0, "source": "independent_driver_run"}],
|
||||
scope_files=scope_audit["created"] + scope_audit["modified"])
|
||||
return {"closed": bool(c.get("success")),
|
||||
"close_envelope": {k: c.get(k) for k in
|
||||
("success", "errors", "ids", "state")}}
|
||||
|
||||
|
||||
def discover_contract_id(store: Path) -> tuple[str, str] | None:
|
||||
"""§15U: delegates to the canonical interface
|
||||
(ConstructionInterface.discover_active_contract). This shim stays only
|
||||
so existing driver call sites keep working."""
|
||||
from rune import construction_interface as _ci
|
||||
env = _ci.ConstructionInterface.discover_active_contract(str(store))
|
||||
if not env.get("success"):
|
||||
return None
|
||||
return env["ids"]["project"], env["ids"]["contract_id"]
|
||||
|
||||
|
||||
def seed_requirement_spec(store: Path, project: str) -> str:
|
||||
"""Govern the requirement as a contract on the project's surface
|
||||
(identity before repository §7): the user request IS the requirement
|
||||
evidence. The Need then references this contract explicitly — the
|
||||
capability-fit engine resolves REUSE of the SPECIFICATION (the build
|
||||
is governed by satisfying it). Maturity DECLARED: a requirement, not
|
||||
existing code — nothing manufactured."""
|
||||
proj = sc.Scaffold(str(store / "projects" / project), project)
|
||||
doc = proj.load()
|
||||
if ss.entity(doc, "contract:practice-items-spec") is None:
|
||||
ss.add_entity(
|
||||
doc, id="contract:practice-items-spec", kind="contract",
|
||||
identities={"objective": USER_REQUEST},
|
||||
evidence_refs=["t14-user-request"])
|
||||
ss.add_entity(
|
||||
doc, id="capability:practice-scheduler", kind="capability",
|
||||
identities={"planned_surface": list(EXPECTED_SCOPE)},
|
||||
maturity="DECLARED", evidence_refs=["t14-user-request"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:practice-scheduler",
|
||||
to_id="contract:practice-items-spec",
|
||||
evidence_refs=["t14-user-request"])
|
||||
proj.commit(doc, what="seed requirement spec: practice items "
|
||||
"create/list/complete (user request)")
|
||||
return "contract:practice-items-spec"
|
||||
|
||||
|
||||
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
|
||||
before = snapshot_files(project_dir)
|
||||
staged = classify_and_activate(store, USER_REQUEST)
|
||||
project = staged["activation"]["project"]
|
||||
assert staged["activation"]["mode"] == "PROJECT_BUILD", \
|
||||
f"expected PROJECT_BUILD, got {staged['activation']['mode']}"
|
||||
spec_contract = seed_requirement_spec(store, project)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL,
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
sub = iface.submit_need(
|
||||
USER_REQUEST, origin="rune_native",
|
||||
constraints={"environment": {"os": "linux", "runtime": "node>=18",
|
||||
"runner": "node --test"},
|
||||
"contracts": [spec_contract]},
|
||||
affected_surface=list(EXPECTED_SCOPE),
|
||||
evidence_refs=["t14-user-request", "node-v22-installed"],
|
||||
approval_context="t14 user-approved fixture")
|
||||
assert sub.get("success"), sub.get("errors")
|
||||
contract_id = sub["ids"]["contract_id"]
|
||||
log(f"contract: {contract_id}")
|
||||
for phase in ("approved", "building"):
|
||||
tr = iface.transition(contract_id, phase,
|
||||
evidence_ref=f"t14 {phase}")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
projection = build_projection(iface, contract_id,
|
||||
phase_note="initial build")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=False),
|
||||
interrupt_after_first_file=interrupt)
|
||||
if session["terminated_by"] == "sigkill_mid_build":
|
||||
log("builder INTERRUPTED mid-build — canonical state persists")
|
||||
return 3
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def finish_session(iface, contract_id, project_dir, before,
|
||||
session) -> int:
|
||||
proj_text = (session["session_output"] or "")
|
||||
import re as _re
|
||||
m = None
|
||||
for m in _re.finditer(r"BUILDER_STATUS:\s*([A-Z]+)", proj_text):
|
||||
pass
|
||||
builder_status = (m.group(1) + " (final)") if m else "NOT_STATED"
|
||||
scope_audit = audit_scope(project_dir, before,
|
||||
list(EXPECTED_SCOPE))
|
||||
verification = independent_verification(project_dir)
|
||||
result = complete_lifecycle(iface, contract_id, scope_audit,
|
||||
verification, builder_status)
|
||||
summary = {
|
||||
"builder_model": BUILDER_MODEL,
|
||||
"builder_process": "mimo run (separate OS process)",
|
||||
"terminated_by": session["terminated_by"],
|
||||
"builder_claimed": builder_status,
|
||||
"scope_audit": scope_audit,
|
||||
"verification": {"cmd": verification["cmd"],
|
||||
"exit_code": verification["exit_code"]},
|
||||
"lifecycle": result,
|
||||
}
|
||||
print(json.dumps(summary, indent=1))
|
||||
return 0 if result.get("closed") else 1
|
||||
|
||||
|
||||
def cmd_resume(store: Path, project_dir: Path) -> int:
|
||||
"""14E: FRESH process. Canonical state ONLY — no transcript, no
|
||||
memory of the previous session."""
|
||||
found = discover_contract_id(store)
|
||||
assert found, "no contract found in canonical state"
|
||||
project, contract_id = found
|
||||
log(f"fresh builder resume: project={project} "
|
||||
f"contract={contract_id} (canonical only)")
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL + "-resumed",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
context = iface.construction_context(contract_id)
|
||||
assert context["state"]["active"], "canonical state shows no active build"
|
||||
log(f"recovered: phase={context['state']['phase']} "
|
||||
f"scope={context['state']['scope']}")
|
||||
before = snapshot_files(project_dir)
|
||||
projection = build_projection(
|
||||
iface, contract_id,
|
||||
phase_note="resumed after real builder interruption")
|
||||
session = run_builder_process(
|
||||
project_dir, builder_prompt(projection, resumed=True),
|
||||
interrupt_after_first_file=False)
|
||||
return finish_session(iface, contract_id, project_dir, before,
|
||||
session)
|
||||
|
||||
|
||||
def cmd_status(store: Path) -> int:
|
||||
found = discover_contract_id(store)
|
||||
if not found:
|
||||
print(json.dumps({"active": False}))
|
||||
return 0
|
||||
project, contract_id = found
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="inspector",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
print(json.dumps(iface.construction_context(contract_id)["state"],
|
||||
indent=1, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("mode", choices=["start", "resume", "status"])
|
||||
ap.add_argument("--store", required=True)
|
||||
ap.add_argument("--project-dir", required=False)
|
||||
ap.add_argument("--interrupt-after-first-file", action="store_true")
|
||||
args = ap.parse_args()
|
||||
store, project_dir = Path(args.store).resolve(), \
|
||||
Path(args.project_dir).resolve() if args.project_dir else None
|
||||
project_dir.mkdir(parents=True, exist_ok=True) if project_dir else None
|
||||
if args.mode == "start":
|
||||
return cmd_start(store, project_dir,
|
||||
args.interrupt_after_first_file)
|
||||
if args.mode == "resume":
|
||||
return cmd_resume(store, project_dir)
|
||||
return cmd_status(store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""t15_builder_session.py — T15 §15Q: REAL event-driven builder proof.
|
||||
|
||||
The difference from T14, enforced by construction:
|
||||
|
||||
launch → register WAITING_EVENT → YIELD (select on the bridge FIFO)
|
||||
→ builder exit event → resume immediately → reload canonical state
|
||||
→ readiness/exit canaries → independent verification → continuation
|
||||
|
||||
NO sleep/poll loops anywhere on the normal path. The watchdog is only a
|
||||
failure detector (lost event / dead process), never the scheduler.
|
||||
|
||||
Modes:
|
||||
start classify → activate → contract → real builder → event wait
|
||||
→ exit canary → verification → evidence-backed close
|
||||
interrupt like start, but the builder process group is SIGKILLed at
|
||||
the first source file → PROCESS_LOST → canonical state
|
||||
persists for a fresh builder (run `resume` next)
|
||||
resume FRESH controller + fresh builder from canonical state only
|
||||
status print canonical construction state
|
||||
|
||||
The builder is the real model process (mimo run — Verboo per the standing
|
||||
provider policy). Builder identity is recorded, never trusted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import t14_builder_session as t14 # noqa: E402 — reuse the proven pieces
|
||||
from rune import construction_interface as ci # noqa: E402
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
from rune.event_wait import DEFAULT_DAEMON_URL # noqa: E402
|
||||
from rune.provider_context import ProviderUnresolved, require_model # noqa: E402
|
||||
|
||||
# T16 §16F: NO silent default provider — resolve from the ambient provider
|
||||
# context (T15_BUILDER_MODEL, falling back to MIMO_MODEL); loud failure
|
||||
# with exit code 3 when the provider layer has not selected one.
|
||||
try:
|
||||
BUILDER_MODEL = os.environ.get("T15_BUILDER_MODEL", "").strip() \
|
||||
or require_model()
|
||||
except ProviderUnresolved as _exc:
|
||||
import sys as _sys
|
||||
_sys.stderr.write(str(_exc) + "\n")
|
||||
raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback)
|
||||
DAEMON_URL = os.environ.get("T15_DAEMON_URL", DEFAULT_DAEMON_URL)
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[t15] {msg}", flush=True)
|
||||
|
||||
|
||||
def readiness_canary(toolchain_hint: str) -> dict:
|
||||
"""§15F: may the build stage begin? Executable checks, not optimism.
|
||||
Runtime checks derive from the verifier (15R generalisation)."""
|
||||
checks = []
|
||||
runtimes = (["node"] if "node" in VERIFIER else []) + (["python3"] if "python" in VERIFIER else [])
|
||||
for rt in runtimes:
|
||||
r = subprocess.run([rt, "--version"], capture_output=True, text=True)
|
||||
checks.append({"name": f"{rt}_present", "passed": r.returncode == 0,
|
||||
"evidence": (r.stdout or "").strip()[:30]})
|
||||
r2 = subprocess.run(["mimo", "--version"], capture_output=True, text=True)
|
||||
checks.append({"name": "builder_cli_present", "passed": r2.returncode == 0,
|
||||
"evidence": (r2.stdout or "").strip()[:40]})
|
||||
try:
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(DAEMON_URL + "/health", timeout=3) as resp:
|
||||
ok = resp.status == 200
|
||||
checks.append({"name": "event_bridge_healthy", "passed": ok,
|
||||
"evidence": DAEMON_URL})
|
||||
except Exception as e: # noqa: BLE001
|
||||
checks.append({"name": "event_bridge_healthy", "passed": False,
|
||||
"evidence": str(e)[:80]})
|
||||
verdict = "READY" if all(c["passed"] for c in checks) else "BLOCKED"
|
||||
return {"canary": "readiness", "verdict": verdict, "checks": checks}
|
||||
|
||||
|
||||
# 15R: the driver is construction INFRASTRUCTURE — the request/scope are
|
||||
# parameters, never scheduler-specific constants. Defaults preserve the
|
||||
# proven fixture.
|
||||
REQUEST = os.environ.get(
|
||||
"T15_REQUEST",
|
||||
"Build me a small standalone practice scheduler that can create, "
|
||||
"list and complete practice items.")
|
||||
SCOPE = [s for s in os.environ.get(
|
||||
"T15_SCOPE", "scheduler.js,scheduler.test.js,package.json").split(",")
|
||||
if s]
|
||||
PROJECT_NAME = os.environ.get("T15_PROJECT_NAME", "")
|
||||
SPEC_CONTRACT = os.environ.get("T15_SPEC_CONTRACT",
|
||||
"contract:practice-items-spec")
|
||||
TOOLCHAIN = os.environ.get("T15_TOOLCHAIN",
|
||||
"node >= 18 (node:test built-in runner, ES modules)")
|
||||
VERIFIER = os.environ.get("T15_VERIFIER", "node --test")
|
||||
|
||||
|
||||
def setup_construction(store: Path, project_dir: Path) -> tuple[str, str]:
|
||||
"""classify → activate → seed spec → contract → transitions. Reuses
|
||||
the proven T14 canonical path unchanged."""
|
||||
staged = t14.classify_and_activate(store, REQUEST)
|
||||
project = staged["activation"]["project"]
|
||||
assert staged["activation"]["mode"] == "PROJECT_BUILD"
|
||||
_seed_spec(store, project)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL,
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
sub = iface.submit_need(
|
||||
REQUEST, origin="rune_native",
|
||||
constraints={"environment": {"os": "linux",
|
||||
"runtime": TOOLCHAIN,
|
||||
"runner": VERIFIER},
|
||||
"contracts": [SPEC_CONTRACT]},
|
||||
affected_surface=list(SCOPE),
|
||||
evidence_refs=["t15-user-request", "node-v22-installed"],
|
||||
approval_context="t15 user-approved fixture")
|
||||
assert sub.get("success"), sub.get("errors")
|
||||
contract_id = sub["ids"]["contract_id"]
|
||||
for phase in ("approved", "building"):
|
||||
tr = iface.transition(contract_id, phase, evidence_ref=f"t15 {phase}")
|
||||
assert tr.get("success"), tr.get("errors")
|
||||
log(f"contract {contract_id} → building")
|
||||
return project, contract_id
|
||||
|
||||
|
||||
def _seed_spec(store: Path, project: str) -> str:
|
||||
"""Govern the requirement as a contract on the project surface
|
||||
(generic — request/scope come from parameters)."""
|
||||
proj = sc.Scaffold(str(store / "projects" / project), project)
|
||||
doc = proj.load()
|
||||
if ss.entity(doc, SPEC_CONTRACT) is None:
|
||||
ss.add_entity(doc, id=SPEC_CONTRACT, kind="contract",
|
||||
identities={"objective": REQUEST},
|
||||
evidence_refs=["t15-request"])
|
||||
ss.add_entity(doc, id="capability:planned-" + project[:24],
|
||||
kind="capability", maturity="DECLARED",
|
||||
evidence_refs=["t15-request"])
|
||||
ss.add_relationship(doc, type="implements",
|
||||
from_id="capability:planned-" + project[:24],
|
||||
to_id=SPEC_CONTRACT, evidence_refs=["t15-request"])
|
||||
proj.commit(doc, what="seed requirement spec (user request)")
|
||||
return SPEC_CONTRACT
|
||||
|
||||
|
||||
def run_build_via_events(project_dir: Path, operation_id: str,
|
||||
projection: dict, contract_id: str,
|
||||
interrupt_after_first_file: bool) -> dict:
|
||||
"""THE 15Q core: real builder under the event notifier; the driver
|
||||
YIELDS on the bridge FIFO. No sleep. Watchdog = failure detector."""
|
||||
ew.register_process_wait(
|
||||
operation_id, contract_id=contract_id,
|
||||
project="t15-fixture", objective=REQUEST,
|
||||
watched_for="builder process exit event", watchdog_secs=900)
|
||||
# op marker rides the prompt (last argv) so cmdline liveness can find
|
||||
# the build even if node regroups its children (T15Q live finding)
|
||||
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
|
||||
t14.builder_prompt(projection, resumed=False)
|
||||
+ f" [op:{operation_id}]"]
|
||||
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
|
||||
daemon_url=DAEMON_URL)
|
||||
log(f"builder launched (pgid {notifier.pid}); driver YIELDS on the "
|
||||
f"event bridge — no polling")
|
||||
if interrupt_after_first_file:
|
||||
# kill at first source file: the interruption is real, the event
|
||||
# never arrives (the notifier dies with the group) — the watchdog
|
||||
# must classify PROCESS_LOST, never silently wait.
|
||||
# TEST_TIMING: this 0.5s poll exists ONLY on the deliberate
|
||||
# interrupt path (killing at a checkpoint), never on the normal
|
||||
# continuation path.
|
||||
while True:
|
||||
files = [p for p in project_dir.iterdir()
|
||||
if p.suffix in (".mjs", ".js", ".json")]
|
||||
if files:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
log("INTERRUPTING builder (first file landed) — SIGKILL group")
|
||||
os.killpg(notifier.pid, signal.SIGKILL)
|
||||
result = ew.await_resume_event(operation_id, watchdog_secs=900,
|
||||
pgid=notifier.pid)
|
||||
return result
|
||||
|
||||
|
||||
def finish_via_canaries(store: Path, project: str, contract_id: str,
|
||||
project_dir: Path, before: dict,
|
||||
resume_note: str) -> int:
|
||||
"""Reload canonical state → scope audit → INDEPENDENT verification
|
||||
(15G test truth) → evidence-backed lifecycle."""
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
ctx = iface.construction_context(contract_id)
|
||||
assert ctx["state"]["active"] or ctx["state"]["phase"] == "closed"
|
||||
log(f"canonical state: phase={ctx['state']['phase']} "
|
||||
f"scope={ctx['state']['scope']}")
|
||||
# exit canary: scope audit with provenance-aware residue (15O)
|
||||
audit = t14.audit_scope(project_dir, before, list(SCOPE))
|
||||
# 15G: verification truth — node --test must actually run tests
|
||||
# 15G test truth via the CONTRACT's verifier (parameterized — 15R
|
||||
# generalisation: python unittest/pytest, node --test, etc.)
|
||||
truth = subprocess.run(
|
||||
["bash", "-c", VERIFIER], cwd=str(project_dir),
|
||||
capture_output=True, text=True, timeout=300)
|
||||
import re
|
||||
count = None
|
||||
for pat in (r"# pass (\d+)", # node --test TAP summary
|
||||
r"(\d+) passed", # pytest
|
||||
r"Ran (\d+) tests"): # python unittest
|
||||
m = re.search(pat, (truth.stdout or "") + (truth.stderr or ""))
|
||||
if m:
|
||||
count = int(m.group(1))
|
||||
break
|
||||
test_count = count if count is not None else 0
|
||||
log(f"verification: exit={truth.returncode} tests={test_count}")
|
||||
if test_count == 0:
|
||||
log("FALSE GREEN REFUSED: zero tests executed (15G)")
|
||||
return 1
|
||||
if audit["scope_violations"]:
|
||||
log(f"closure REFUSED: scope violations {audit['scope_violations']}")
|
||||
return 1
|
||||
builder_status = "UNKNOWN"
|
||||
result = t14.complete_lifecycle(iface, contract_id, audit,
|
||||
{"cmd": "node --test",
|
||||
"exit_code": truth.returncode},
|
||||
builder_status)
|
||||
summary = {
|
||||
"event_driven": True,
|
||||
"builder_model": BUILDER_MODEL,
|
||||
"resume_note": resume_note,
|
||||
"scope_audit": audit,
|
||||
"verification": {"exit_code": truth.returncode,
|
||||
"test_count": test_count},
|
||||
"lifecycle": result,
|
||||
}
|
||||
print(json.dumps(summary, indent=1))
|
||||
return 0 if result.get("closed") else 1
|
||||
|
||||
|
||||
def discover(store: Path) -> tuple[str, str] | None:
|
||||
"""§15U: delegates to the canonical interface discovery (via the t14
|
||||
shim — one hop, no duplication)."""
|
||||
return t14.discover_contract_id(store)
|
||||
|
||||
|
||||
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
|
||||
ready = readiness_canary("node")
|
||||
assert ready["verdict"] == "READY", f"readiness BLOCKED: {ready['checks']}"
|
||||
log(f"readiness READY ({len(ready['checks'])} checks)")
|
||||
before = t14.snapshot_files(project_dir)
|
||||
project, contract_id = setup_construction(store, project_dir)
|
||||
operation_id = f"t15-{contract_id}"
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL, project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
projection = t14.build_projection(iface, contract_id,
|
||||
phase_note="initial build")
|
||||
result = run_build_via_events(project_dir, operation_id, projection,
|
||||
contract_id, interrupt)
|
||||
if result["kind"] == "watchdog":
|
||||
# §15E canonical-state-wins: process-table absence is NOT truth
|
||||
# (detached builders re-parent their workers — T15Q live finding).
|
||||
# The durable waitstore decides: a recorded verdict means the
|
||||
# builder completed and the event WAS processed.
|
||||
rec = ew.resume_or_recover(operation_id)
|
||||
if rec["kind"] == "event":
|
||||
log(f"watchdog fired but canonical state shows the build "
|
||||
f"completed (verdict={rec['wait'].get('verdict')}) — "
|
||||
f"continuing from authority")
|
||||
return finish_via_canaries(store, project, contract_id,
|
||||
project_dir, before,
|
||||
"watchdog→canonical recovery")
|
||||
log("PROCESS_LOST: no event, no durable verdict — fresh builder "
|
||||
"resumes from authority only")
|
||||
return 3
|
||||
wait = result.get("wait", {})
|
||||
log(f"event-driven resume: verdict={wait.get('verdict')} "
|
||||
f"exit={wait.get('process_event_evidence', {}).get('exit_code')}")
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "event-driven completion")
|
||||
|
||||
|
||||
def cmd_resume(store: Path, project_dir: Path) -> int:
|
||||
found = discover(store)
|
||||
assert found, "no contract in canonical state"
|
||||
project, contract_id = found
|
||||
log(f"FRESH builder resume: {project}/{contract_id} (canonical only)")
|
||||
before = t14.snapshot_files(project_dir)
|
||||
iface = ci.ConstructionInterface(
|
||||
builder=BUILDER_MODEL + "-resumed",
|
||||
project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
projection = t14.build_projection(
|
||||
iface, contract_id, phase_note="resumed after real interruption")
|
||||
operation_id = f"t15-resume-{contract_id}"
|
||||
ew.register_process_wait(
|
||||
operation_id, contract_id=contract_id, project=project,
|
||||
objective=REQUEST, watched_for="builder exit event",
|
||||
watchdog_secs=900)
|
||||
builder_cmd = ["mimo", "run", "-m", BUILDER_MODEL, "--format", "json",
|
||||
t14.builder_prompt(projection, resumed=True)
|
||||
+ f" [op:{operation_id}]"]
|
||||
notifier = ew.run_builder_under_events(builder_cmd, operation_id,
|
||||
daemon_url=DAEMON_URL)
|
||||
log("driver YIELDS on the bridge — no polling")
|
||||
result = ew.await_resume_event(operation_id, watchdog_secs=900,
|
||||
pgid=notifier.pid)
|
||||
if result["kind"] == "watchdog":
|
||||
rec = ew.resume_or_recover(operation_id)
|
||||
if rec["kind"] == "event":
|
||||
log(f"watchdog fired but canonical state shows the resume "
|
||||
f"build completed (verdict={rec['wait'].get('verdict')})")
|
||||
return finish_via_canaries(store, project, contract_id,
|
||||
project_dir, before,
|
||||
"resume via canonical recovery")
|
||||
log("PROCESS_LOST on resume — UNKNOWN_INTERRUPTION")
|
||||
return 4
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "fresh-builder event-driven resume")
|
||||
|
||||
|
||||
def cmd_finish(store: Path, project_dir: Path) -> int:
|
||||
"""Complete lifecycle from CANONICAL state only (waitstore verdict +
|
||||
filesystem reality). Used after a watchdog→canonical recovery."""
|
||||
found = discover(store)
|
||||
assert found, "no contract in canonical state"
|
||||
project, contract_id = found
|
||||
before = t14.snapshot_files(project_dir)
|
||||
return finish_via_canaries(store, project, contract_id, project_dir,
|
||||
before, "canonical-only completion")
|
||||
|
||||
|
||||
def cmd_status(store: Path) -> int:
|
||||
found = discover(store)
|
||||
if not found:
|
||||
print(json.dumps({"active": False}))
|
||||
return 0
|
||||
project, contract_id = found
|
||||
iface = ci.ConstructionInterface(
|
||||
builder="inspector", project_store=str(store / "projects" / project),
|
||||
project_id=project)
|
||||
print(json.dumps(iface.construction_context(contract_id)["state"],
|
||||
indent=1, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("mode", choices=["start", "resume", "finish",
|
||||
"status"])
|
||||
ap.add_argument("--store", required=True)
|
||||
ap.add_argument("--project-dir", required=False)
|
||||
ap.add_argument("--interrupt-after-first-file", action="store_true")
|
||||
args = ap.parse_args()
|
||||
store = Path(args.store).resolve()
|
||||
project_dir = (Path(args.project_dir).resolve()
|
||||
if args.project_dir else None)
|
||||
if project_dir:
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
if args.mode == "start":
|
||||
return cmd_start(store, project_dir,
|
||||
args.interrupt_after_first_file)
|
||||
if args.mode == "resume":
|
||||
return cmd_resume(store, project_dir)
|
||||
if args.mode == "finish":
|
||||
return cmd_finish(store, project_dir)
|
||||
return cmd_status(store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,20 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""tui_startup_canary.py — REAL TUI initialization canary (handoff §4).
|
||||
"""tui_startup_canary.py — REAL TUI initialization canary, two proof modes.
|
||||
|
||||
Runs the ACTUAL RuneApp initialization through Textual's own test driver —
|
||||
the same app construction + on_mount wiring the interactive TUI executes —
|
||||
then exits 0. No fake smoke path: RuneApp is constructed, mounted, and its
|
||||
on_mount handlers run; the interactive loop is simply never entered.
|
||||
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.
|
||||
|
||||
Prints the build identity on success. Exit non-zero on any initialization
|
||||
failure (a staged revision whose TUI cannot initialize must NOT promote).
|
||||
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:
|
||||
@@ -30,19 +58,59 @@ async def _canary() -> dict:
|
||||
assert app.status_bar is not None, "status bar not initialized"
|
||||
# One interaction cycle: let pending messages/refreshes settle.
|
||||
await pilot.pause(0.1)
|
||||
from rune import build_info
|
||||
import rune
|
||||
|
||||
return build_info()
|
||||
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(_canary())
|
||||
except Exception as exc:
|
||||
print(f"TUI STARTUP CANARY FAILED: {type(exc).__name__}: {exc}",
|
||||
file=sys.stderr)
|
||||
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("TUI STARTUP VERIFIED:", json.dumps(info))
|
||||
print(json.dumps({"mode": args.mode, **info}))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ New code must be type-clean; existing code improves as debt is paid down.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -23,12 +24,35 @@ def main() -> int:
|
||||
# not work on all platforms). Fall back to sys.executable -m.
|
||||
pyright_bin = Path(sys.executable).parent / "pyright"
|
||||
cmd = [str(pyright_bin)] if pyright_bin.is_file() else [sys.executable, "-m", "pyright"]
|
||||
# §14H LOCAL-VS-CI NORMALIZATION (proven 2026-09-15): pyright resolves
|
||||
# the python environment for import analysis, and a developer's
|
||||
# user-site packages (~/.local/lib/pythonX/site-packages) leak into it
|
||||
# — nick: 124 errors, runner user: 117 on the IDENTICAL checkout+venv.
|
||||
# PYTHONNOUSERSITE=1 pins both contexts to the same effective
|
||||
# environment (verified: identical 117-error arithmetic). Local
|
||||
# preflight now predicts CI instead of diverging from it.
|
||||
# §16U ROOT-CAUSE COMPLETION (T16): PYTHONNOUSERSITE alone is NOT
|
||||
# sufficient when a DIFFERENT venv is active (e.g. an ad-hoc CI venv):
|
||||
# the pyright wrapper resolves the ambient VIRTUAL_ENV/PATH
|
||||
# interpreter and analyzes against ITS site-packages — reproduced
|
||||
# live (117 vs 124 on the same tree, driven by venv-activation state
|
||||
# alone). The CI runner resolves the AMBIENT python (no venv is
|
||||
# activated in the type-check job), so the ratchet neutralizes every
|
||||
# ambient-venv signal before invoking pyright — analysis context is
|
||||
# now identical to CI regardless of the calling shell.
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if k not in ("VIRTUAL_ENV",)}
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
env["PATH"] = os.pathsep.join(
|
||||
p for p in env.get("PATH", "").split(os.pathsep)
|
||||
if "ci-venv" not in p and ".type-venv" not in p and "venv/bin" not in p)
|
||||
result = subprocess.run(
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
summary = data.get("summary", {})
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""v1_parity_manifest.py — T21.5 §13: historical Scaffolding V1 parity.
|
||||
|
||||
Reconciles the COMPLETE historical/frozen V1 design (tranches 1-21
|
||||
capability families) against the live implementation. Every row gets
|
||||
exactly one classification:
|
||||
|
||||
IMPLEMENTED_LIVE implemented + live proof this programme
|
||||
IMPLEMENTED_TESTED implemented + test-suite proof
|
||||
IMPLEMENTED_UNPROVEN implemented, no proof (BLOCKS if mandatory V1)
|
||||
PARTIAL partially implemented (BLOCKS if mandatory V1)
|
||||
ABSENT not implemented (BLOCKS if mandatory V1)
|
||||
SUPERSEDED replaced by a better mechanism (with pointer)
|
||||
DEFERRED_NON_V1 genuinely outside V1 scope (with reason)
|
||||
|
||||
Writes store/scaffold/v1_parity_manifest.json.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# (family, item, classification, module, authority, evidence, gap)
|
||||
ROWS = [
|
||||
# ── CAPABILITY DECISION ────────────────────────────────────────────
|
||||
("capability-decision", "capability discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_fit.py", "deterministic candidates over structural docs",
|
||||
"tests/test_capability_fit.py; T21.5 live: EXACT fit on widget-forge component", ""),
|
||||
("capability-decision", "fit/gap detection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py evaluate_fit", "EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN",
|
||||
"tests/test_capability_fit.py", ""),
|
||||
("capability-decision", "reuse/configure/compose decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide", "RESOLUTIONS set",
|
||||
"tests/test_construction_decision.py", ""),
|
||||
("capability-decision", "adapt/acquire decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide (ACQUIRE_EXTERNAL, external_evidence)", "evidence-gated acquisition",
|
||||
"tests/test_construction_decision.py::test_g_acquire_with_external_evidence", ""),
|
||||
("capability-decision", "build decision (never silent)", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide; construction_decision merge", "BUILD_NEW never silently chosen",
|
||||
"tests/test_construction_decision.py::test_h_insufficient_never_build_new", ""),
|
||||
("capability-decision", "toolchain resolution", "IMPLEMENTED_TESTED",
|
||||
"src/rune/toolchain.py", "evidence-backed resolution + decision records",
|
||||
"tests/test_toolchain*.py", ""),
|
||||
|
||||
# ── CONSTRUCTION ───────────────────────────────────────────────────
|
||||
("construction", "construction contracts (lifecycle)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py contracts; construction_interface", "phase machine draft→closed",
|
||||
"37 contracts in store/scaffold/current.json; T21.4/T21.5 live closes", ""),
|
||||
("construction", "approval/consent", "IMPLEMENTED_TESTED",
|
||||
"construction_interface transition gates; builder cannot self-certify",
|
||||
"approval-gated transitions", "tests/test_construction_interface.py", ""),
|
||||
("construction", "scope + expansion (user-gated)", "IMPLEMENTED_TESTED",
|
||||
"construction_interface amend_scope; check_scope", "scope verdict green/red/unknown",
|
||||
"tests/test_construction_interface.py; T21.4 live scope green", ""),
|
||||
("construction", "Project Scaffold", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py Scaffold", "per-project structural truth",
|
||||
"T21.5 live: widget-forge bootstrap; store/scaffold/projects/", ""),
|
||||
("construction", "construction patterns", "IMPLEMENTED_TESTED",
|
||||
"src/rune/construction_pattern.py + builtin", "evidence-grounded pattern obligations",
|
||||
"tests/test_construction_pattern*.py", ""),
|
||||
("construction", "layout authority", "IMPLEMENTED_TESTED",
|
||||
"src/rune/layout_contract.py", "persistent UI layout checks",
|
||||
"tests/test_layout_contract.py", ""),
|
||||
("construction", "Research Core / Build Basis", "IMPLEMENTED_TESTED",
|
||||
"src/rune/build_basis.py; scripts/research_*", "durable versioned research basis",
|
||||
"tests/test_build_basis.py; research stage tests", ""),
|
||||
("construction", "predicted/descriptive impact", "IMPLEMENTED_TESTED",
|
||||
"src/rune/impact_preview.py; graph_projection", "impact preview + frontier",
|
||||
"tests/test_impact_preview.py; test_v1_workspace_forge.py", ""),
|
||||
("construction", "migration", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py", "consent-gated schema migration",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("construction", "supersession", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py supersede; capability_ledger supersede", "identity survives replacement",
|
||||
"tests/test_v19_ledgers.py", ""),
|
||||
("construction", "retirement", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_ledger.py retire", "governed retirement lifecycle",
|
||||
"T17 retirement proof (complexity_scorer); tests", ""),
|
||||
("construction", "rollback (deploy generations)", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed rollback_generation API",
|
||||
"T21.4 live B->A->B + refusals; tests/test_rollback_generation.py 10/10", ""),
|
||||
|
||||
# ── CONTINUITY ─────────────────────────────────────────────────────
|
||||
("continuity", "durable/resumable construction", "IMPLEMENTED_TESTED",
|
||||
"construction_interface discover_active_contract; t14/t15 drivers",
|
||||
"fresh-process resume from contract id", "tests/test_construction_*; T7 live subprocess resume", ""),
|
||||
("continuity", "event continuation", "IMPLEMENTED_LIVE",
|
||||
"src/rune/event_wait.py; scripts/ci_event_wait.py", "event=wake, authority=truth, single logical await",
|
||||
"T21.5 live workflows + tests/test_single_logical_await.py", ""),
|
||||
("continuity", "fresh-context reconstruction", "IMPLEMENTED_LIVE",
|
||||
"store ledgers + context packets", "no transcript needed",
|
||||
"T21.4 fresh-worker challenge 6/6; T21.5 fresh-process queries", ""),
|
||||
("continuity", "Registry (project discovery)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "discovery/index authority",
|
||||
"tests/test_resolve_cwd.py etc.; T21.5 auto-registration", ""),
|
||||
("continuity", "living specs / ledgers", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py (attach_spec/revise_spec/drift)", "machinery-verified versions",
|
||||
"T21.4/T21.5 live revise_spec flows; tests/test_living_specs.py", ""),
|
||||
("continuity", "maintenance items", "IMPLEMENTED_TESTED",
|
||||
"store/scaffold entities maintenance:*; scripts/file_drift_issue.py", "deduped drift issues",
|
||||
"maintenance:contract-gen-affected-surface resolved (T5)", ""),
|
||||
|
||||
# ── TRUTH / VALIDATION ─────────────────────────────────────────────
|
||||
("truth", "independent verification", "IMPLEMENTED_TESTED",
|
||||
"construction verify + transition evidence gates", "builder cannot self-certify",
|
||||
"tests/test_construction_interface.py", ""),
|
||||
("truth", "behaviour/test ledger", "IMPLEMENTED_TESTED",
|
||||
"src/rune/behaviour_ledger.py", "canonical behaviour specs",
|
||||
"tests/test_v19_ledgers.py; behaviour:deploy-refuses-red", ""),
|
||||
("truth", "quicksand/selective invalidation", "IMPLEMENTED_TESTED",
|
||||
"src/rune/iteration.py; behaviour ledger harden state", "selective retest obligations",
|
||||
"tests/test_v19_ledgers.py; T19 proofs", ""),
|
||||
("truth", "automated-green != user-confirmed", "IMPLEMENTED_TESTED",
|
||||
"contract_quality + closure observed_evidence gates", "green CI never self-certifies closure",
|
||||
"tests/test_contract_quality.py", ""),
|
||||
("truth", "structural/spec/version drift", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py spec_drift_status", "deterministic revision-based drift",
|
||||
"T21.4 live drift->revise cycle; tests/test_spec_drift_deterministic.py", ""),
|
||||
("truth", "schema migration consent", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py; project_registry.needs_migration", "18I consent sweep",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("truth", "predicted-vs-observed outcome", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py predicted_vs_observed", "recorded mismatch as evidence",
|
||||
"tests + T21.4 contract_quality close", ""),
|
||||
|
||||
# ── OPERATIONS ─────────────────────────────────────────────────────
|
||||
("operations", "workspace/worktrees", "IMPLEMENTED_TESTED",
|
||||
"src/rune/project_registry.py active_worktree precedence", "worktree > repo",
|
||||
"tests/test_resolve_cwd.py; T21.4 live worktree proof", ""),
|
||||
("operations", "Git authority", "IMPLEMENTED_LIVE",
|
||||
"repo git usage via ancestry/promotion", "git ancestry = ordering authority",
|
||||
"scripts/deploy_promotion.py ancestry_of; live promotions", ""),
|
||||
("operations", "Forge/Gitea projection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/forge.py; forge_client.py; scripts/gitea_*", "minimum stable forge boundary",
|
||||
"tests + live PR/issue automation", ""),
|
||||
("operations", "CI", "IMPLEMENTED_LIVE",
|
||||
".gitea/workflows/ci.yml; scripts/ci_event_wait.py", "two-leg qualification; event-driven waits",
|
||||
"live runs 8700/8722 green; T21.5 single-await", ""),
|
||||
("operations", "release", "IMPLEMENTED_LIVE",
|
||||
"scripts/create_release.py; semver_bump.py; auto_bump_pr.py", "protected-branch bump flow",
|
||||
"releases v0.82.0-0.82.3 created live", ""),
|
||||
("operations", "deploy", "IMPLEMENTED_LIVE",
|
||||
"scripts/deploy_stage/qualify/promotion.py", "monotonic fail-closed promotion",
|
||||
"live deploys 40b3731..c3a0a48; stale-refusal fired in production", ""),
|
||||
("operations", "rollback", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed regression API",
|
||||
"T21.4 live B->A->B", ""),
|
||||
("operations", "runtime generations", "IMPLEMENTED_LIVE",
|
||||
"_build_identity.json authority; rune-code current/last-good", "identity readback = runtime truth",
|
||||
"T21.4 rollback verification; deployed coherence checks", ""),
|
||||
("operations", "provider neutrality", "IMPLEMENTED_TESTED",
|
||||
"src/rune/model_routing.py; provider_registry.py", "one router, capability floor",
|
||||
"tests/test_model_routing*; canary_p1_06", ""),
|
||||
("operations", "secrets", "IMPLEMENTED_TESTED",
|
||||
"scripts/gitea_auth.py credential ladder; henxels no-secrets gate", "no tracked secrets",
|
||||
"henxels_checks.py CI gate; credential helper", ""),
|
||||
|
||||
# ── STRUCTURAL SELF-AWARENESS ──────────────────────────────────────
|
||||
("self-awareness", "Spine authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/spine.py", "Rune structural self-model",
|
||||
"store/spine/current.json; deterministic builds", ""),
|
||||
("self-awareness", "Registry discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "project index",
|
||||
"live registry reads across tranches", ""),
|
||||
("self-awareness", "Project Scaffold authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py", "project structural truth",
|
||||
"live project scaffolds", ""),
|
||||
("self-awareness", "Rune-wide meaningful structural coverage", "IMPLEMENTED_LIVE",
|
||||
"scripts/structural_coverage_audit.py", "A/B/C/D/E classification",
|
||||
"T21.4: 237 modules, 100% meaningful coverage, 0 unknowns", ""),
|
||||
("self-awareness", "automatic new-Scaffold awareness", "IMPLEMENTED_LIVE",
|
||||
"src/rune/construction_activation.py _auto_structural_awareness", "bootstrap auto-registers",
|
||||
"T21.5 live + tests/test_t215_awareness.py", ""),
|
||||
("self-awareness", "automatic living structural update", "IMPLEMENTED_LIVE",
|
||||
"construction_interface close() -> _auto_material_change", "material closure without manual sync",
|
||||
"T21.4/T21.5 live; tests/test_t214_material_closure.py", ""),
|
||||
|
||||
# ── LEARNING SEAM ──────────────────────────────────────────────────
|
||||
("learning-seam", "candidate teachable-moment seam", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py mark_teachable_moment_candidate", "CANDIDATE evidence via existing channels",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "Memory internal-experience port in INWARD", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py; research_inward_context.build_context", "one bounded port of call",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "APPLICABILITY governs transfer", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.applicability_filter + existing APPLICABILITY stage", "irrelevant candidates rejected",
|
||||
"tests/test_memory_port.py::test_applicability_rejects_irrelevant_candidate", ""),
|
||||
("learning-seam", "zero-result fast path", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.memory_port_of_call", "[] -> next normal step",
|
||||
"tests/test_memory_port.py; ZERO_RESULT_EXTRA_MODEL_CALLS=0", ""),
|
||||
("learning-seam", "no competing memory authority", "IMPLEMENTED_TESTED",
|
||||
"port design (provider delegation, no storage)", "Memory stays single future authority",
|
||||
"port creates no store/db; provider contract only", ""),
|
||||
|
||||
# ── DEFERRED (non-V1, with reasons) ────────────────────────────────
|
||||
("deferred", "automatic Growth Delta pattern learning", "DEFERRED_NON_V1",
|
||||
"src/rune/growth_delta.py (existing, non-V1 scope)", "post-V1 learning loop",
|
||||
"V1 excludes Memory/Substrate/Growth Delta by tranche mandate; the "
|
||||
"T21.5 seam provides the harvest point without implementing the loop", ""),
|
||||
("deferred", "Memory V1 system itself", "DEFERRED_NON_V1",
|
||||
"(future Memory T0 archaeology)", "dedicated post-V1 phase",
|
||||
"explicitly excluded by T21.5 mandate §0; only the INWARD port is V1", ""),
|
||||
("deferred", "Substrate", "DEFERRED_NON_V1",
|
||||
"(future)", "post-V1", "explicitly excluded by tranche mandate", ""),
|
||||
("deferred", "TUI redesign", "DEFERRED_NON_V1",
|
||||
"(existing rune_tui.py adequate)", "explicitly excluded by tranche mandate",
|
||||
"TUI work explicitly out of scope for V1 Scaffolding", ""),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
manifest = {
|
||||
"tranche": "T21.5",
|
||||
"rows": [
|
||||
{"family": f, "item": i, "classification": c, "module": m,
|
||||
"authority": a, "evidence": e, "gap": g}
|
||||
for f, i, c, m, a, e, g in ROWS
|
||||
],
|
||||
}
|
||||
blocking = [r for r in manifest["rows"]
|
||||
if r["classification"] in ("PARTIAL", "ABSENT",
|
||||
"IMPLEMENTED_UNPROVEN")]
|
||||
counts: dict[str, int] = {}
|
||||
for r in manifest["rows"]:
|
||||
counts[r["classification"]] = counts.get(r["classification"], 0) + 1
|
||||
manifest["counts"] = counts
|
||||
manifest["blocking_items"] = blocking
|
||||
out = ROOT / "store" / "scaffold" / "v1_parity_manifest.json"
|
||||
out.write_text(json.dumps(manifest, indent=1))
|
||||
print(json.dumps(counts, indent=1))
|
||||
print("blocking mandatory-V1 items:", len(blocking))
|
||||
for b in blocking:
|
||||
print(" BLOCKS:", b["family"], "/", b["item"])
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wr_root_reconcile.py — T21.5 §7: governed split-brain reconciliation.
|
||||
|
||||
If canonical and legacy W/R roots both hold live event stores, this
|
||||
moves legacy-root events that are ABSENT from the canonical store into
|
||||
it (idempotent, content-keyed by filename), then reports what remains.
|
||||
Never deletes; the legacy store stays as a moved-marker record.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
canonical = resolver.CANONICAL_WR_ROOT
|
||||
legacy = resolver.LEGACY_WR_ROOT
|
||||
(canonical / "events").mkdir(parents=True, exist_ok=True)
|
||||
moved = 0
|
||||
skipped = 0
|
||||
for src in sorted((legacy / "events").glob("*.json")):
|
||||
dst = canonical / "events" / src.name
|
||||
if dst.exists():
|
||||
skipped += 1
|
||||
continue
|
||||
shutil.copy2(src, dst)
|
||||
moved += 1
|
||||
report = {
|
||||
"canonical": str(canonical),
|
||||
"legacy": str(legacy),
|
||||
"events_moved_to_canonical": moved,
|
||||
"already_present_skipped": skipped,
|
||||
"note": "legacy store retained (never deleted); daemon and all "
|
||||
"consumers now resolve via wr_root_resolver",
|
||||
}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
"""wr_root_resolver.py — T21.5 §7: THE ONE wait/resume root authority.
|
||||
|
||||
T21.4 live finding: the daemon defaulted WR_ROOT to its own install dir
|
||||
while production data lived at /mnt/workdrive/wr-bridge; a restart
|
||||
without WR_ROOT exported silently opened a second event store and the
|
||||
controller saw no events (the split-brain that forced manual event
|
||||
relocation). This module is the SINGLE resolver every consumer shares:
|
||||
|
||||
daemon / event_wait / await_operation tool / notify_event_bridge
|
||||
(CI event writer) / local event writers / CLI / launcher / tests.
|
||||
|
||||
Resolution order:
|
||||
1. RUNE_WR_ROOT env (explicit override; tests use it)
|
||||
2. canonical production root (exists?)
|
||||
3. legacy home install root (exists?)
|
||||
4. canonical production root (created on demand — first-run path)
|
||||
|
||||
Split-brain policy: if BOTH canonical and legacy roots contain live
|
||||
event stores, resolution FAILS LOUDLY unless the env pins one — never
|
||||
silently operate two stores. Reconciliation is governed, not silent:
|
||||
run scripts/wr_root_reconcile.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL_WR_ROOT = Path("/mnt/workdrive/wr-bridge")
|
||||
LEGACY_WR_ROOT = Path.home() / ".local" / "share" / "mimo-wait-resume"
|
||||
ENV_VAR = "RUNE_WR_ROOT"
|
||||
|
||||
SPLIT_BRAIN_ERROR = (
|
||||
"WR ROOT SPLIT-BRAIN: both canonical ({canonical}) and legacy "
|
||||
"({legacy}) contain live event stores. Never operate two stores "
|
||||
"silently — pin {env}=<root> explicitly, or reconcile with "
|
||||
"scripts/wr_root_reconcile.py."
|
||||
)
|
||||
|
||||
|
||||
def _has_live_store(root: Path) -> bool:
|
||||
"""A root counts as 'live' when its event store exists and holds
|
||||
events (an empty dir is just an uninitialized install)."""
|
||||
events = root / "events"
|
||||
if not events.is_dir():
|
||||
return False
|
||||
return any(events.glob("*.json"))
|
||||
|
||||
|
||||
def resolve_root(*, require_single: bool = True) -> Path:
|
||||
"""Resolve THE wait/resume data root. Raises on split-brain when
|
||||
require_single (default) and no explicit pin exists."""
|
||||
env = os.environ.get(ENV_VAR)
|
||||
if env:
|
||||
return Path(env)
|
||||
canonical_live = _has_live_store(CANONICAL_WR_ROOT)
|
||||
legacy_live = _has_live_store(LEGACY_WR_ROOT)
|
||||
if canonical_live and legacy_live and require_single:
|
||||
raise RuntimeError(SPLIT_BRAIN_ERROR.format(
|
||||
canonical=CANONICAL_WR_ROOT, legacy=LEGACY_WR_ROOT,
|
||||
env=ENV_VAR))
|
||||
if canonical_live or CANONICAL_WR_ROOT.is_dir():
|
||||
return CANONICAL_WR_ROOT
|
||||
if legacy_live or LEGACY_WR_ROOT.is_dir():
|
||||
return LEGACY_WR_ROOT
|
||||
return CANONICAL_WR_ROOT # first-run: create canonical
|
||||
|
||||
|
||||
def daemon_launch_env() -> dict[str, str]:
|
||||
"""The env a launcher must pass the daemon: WR_ROOT resolved ONCE
|
||||
from this authority (the daemon itself must not guess)."""
|
||||
return {"WR_ROOT": str(resolve_root()),
|
||||
ENV_VAR: str(resolve_root())}
|
||||
|
||||
|
||||
def split_brain_roots() -> list[Path]:
|
||||
"""Diagnostic: roots that currently hold live stores."""
|
||||
return [r for r in (CANONICAL_WR_ROOT, LEGACY_WR_ROOT)
|
||||
if _has_live_store(r)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
root = resolve_root()
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 2
|
||||
print(json.dumps({
|
||||
"resolved_root": str(root),
|
||||
"canonical": str(CANONICAL_WR_ROOT),
|
||||
"legacy": str(LEGACY_WR_ROOT),
|
||||
"split_brain_roots": [str(p) for p in split_brain_roots()],
|
||||
"env_pinned": bool(os.environ.get(ENV_VAR)),
|
||||
}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -352,16 +352,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"critics": critics,
|
||||
"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,)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"verdict": "REJECT", "reason": "entry not found"}
|
||||
|
||||
entry = dict(row)
|
||||
critics = {}
|
||||
|
||||
# Critic 1: Structural validity
|
||||
text = entry.get("text", "")
|
||||
@@ -384,35 +374,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
}
|
||||
|
||||
# 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()
|
||||
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):
|
||||
contradiction_found = True
|
||||
break
|
||||
critics["semantic"] = {
|
||||
"pass": not contradiction_found,
|
||||
"reason": "contradicts existing rock" if contradiction_found else "consistent"
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
all_pass = all(c["pass"] for c in critics.values())
|
||||
verdict = "PASS" if all_pass else "REJECT"
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"entry_id": entry_id,
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed"
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.1"
|
||||
__version__ = "0.83.5"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
+57
-42
@@ -50,6 +50,7 @@ def _now():
|
||||
def connect():
|
||||
"""Open/create the abstraction database."""
|
||||
import sqlite3
|
||||
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
@@ -178,22 +179,24 @@ def llm_call(prompt, model=None, max_tokens=500):
|
||||
model = model or LOCAL_MODEL
|
||||
try:
|
||||
import urllib.request
|
||||
data = json.dumps({
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192}
|
||||
}).encode()
|
||||
|
||||
data = json.dumps(
|
||||
{
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.3, "num_predict": max_tokens, "num_ctx": 8192},
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_URL}/api/generate",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
f"{OLLAMA_URL}/api/generate", data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return result.get("response", "").strip()
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.getLogger("abstraction").warning("LLM call failed: %s", e)
|
||||
return ""
|
||||
|
||||
@@ -263,8 +266,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
# Check for duplicates — hash-based (O(1) lookup instead of LIKE scan)
|
||||
content_hash = hashlib.sha256(what[:80].lower().encode()).hexdigest()[:16]
|
||||
existing = db.execute(
|
||||
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1",
|
||||
(content_hash,)
|
||||
"SELECT id FROM facts WHERE state='active' AND content_hash=? LIMIT 1", (content_hash,)
|
||||
).fetchone()
|
||||
if existing:
|
||||
continue # NOOP — already exists
|
||||
@@ -273,12 +275,20 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
"""INSERT INTO facts (episode_id, what, content_hash, when_text, where_text, who_text, why_text,
|
||||
fact_type, entities_json, confidence, source, state, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)""",
|
||||
(episode_id or "", what, content_hash,
|
||||
fact.get("when", ""), fact.get("where", ""),
|
||||
fact.get("who", ""), fact.get("why", ""),
|
||||
fact.get("type", "world"),
|
||||
json.dumps(fact.get("entities", [])),
|
||||
0.7, source, ts)
|
||||
(
|
||||
episode_id or "",
|
||||
what,
|
||||
content_hash,
|
||||
fact.get("when", ""),
|
||||
fact.get("where", ""),
|
||||
fact.get("who", ""),
|
||||
fact.get("why", ""),
|
||||
fact.get("type", "world"),
|
||||
json.dumps(fact.get("entities", [])),
|
||||
0.7,
|
||||
source,
|
||||
ts,
|
||||
),
|
||||
)
|
||||
fact_id = cur.lastrowid
|
||||
extracted += 1
|
||||
@@ -291,16 +301,15 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
# Upsert entity
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO entities (name, entity_type, created_at) VALUES (?, 'concept', ?)",
|
||||
(entity_name, ts)
|
||||
(entity_name, ts),
|
||||
)
|
||||
entity_row = db.execute("SELECT id FROM entities WHERE name=?", (entity_name,)).fetchone()
|
||||
if entity_row:
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO fact_entities (fact_id, entity_id) VALUES (?, ?)",
|
||||
(fact_id, entity_row["id"])
|
||||
(fact_id, entity_row["id"]),
|
||||
)
|
||||
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?",
|
||||
(entity_row["id"],))
|
||||
db.execute("UPDATE entities SET fact_count = fact_count + 1 WHERE id=?", (entity_row["id"],))
|
||||
|
||||
# Triple extraction (HippoRAG 2 pattern)
|
||||
who = fact.get("who", "").strip()
|
||||
@@ -312,7 +321,7 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
obj = " ".join(words[3:])
|
||||
db.execute(
|
||||
"INSERT INTO triples (subject, predicate, object, source_fact_id, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(who, predicate, obj, fact_id, 0.6, ts)
|
||||
(who, predicate, obj, fact_id, 0.6, ts),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
@@ -335,17 +344,24 @@ def synthesize_observations(entity_name):
|
||||
entity_id = entity["id"]
|
||||
|
||||
# Get all facts about this entity
|
||||
facts = db.execute("""
|
||||
facts = db.execute(
|
||||
"""
|
||||
SELECT f.id, f.what, f.fact_type, f.confidence
|
||||
FROM facts f
|
||||
JOIN fact_entities fe ON f.id = fe.fact_id
|
||||
WHERE fe.entity_id = ? AND f.state = 'active'
|
||||
ORDER BY f.created_at DESC
|
||||
""", (entity_id,)).fetchall()
|
||||
""",
|
||||
(entity_id,),
|
||||
).fetchall()
|
||||
|
||||
if len(facts) < 3:
|
||||
return {"entity": entity_name, "facts": len(facts), "observations": 0,
|
||||
"reason": "Need at least 3 facts to synthesize"}
|
||||
return {
|
||||
"entity": entity_name,
|
||||
"facts": len(facts),
|
||||
"observations": 0,
|
||||
"reason": "Need at least 3 facts to synthesize",
|
||||
}
|
||||
|
||||
facts_text = "\n".join(f"- [{f['fact_type']}] {f['what']}" for f in facts[:20])
|
||||
|
||||
@@ -377,12 +393,11 @@ Respond as JSON array:
|
||||
db.execute(
|
||||
"""INSERT INTO observations (entity_id, observation, source_fact_ids, confidence, state, created_at)
|
||||
VALUES (?, ?, ?, ?, 'active', ?)""",
|
||||
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts)
|
||||
(entity_id, text, json.dumps(source_ids), obs.get("confidence", 0.7), ts),
|
||||
)
|
||||
created += 1
|
||||
|
||||
db.execute("UPDATE entities SET observation_count = ? WHERE id=?",
|
||||
(created, entity_id))
|
||||
db.execute("UPDATE entities SET observation_count = ? WHERE id=?", (created, entity_id))
|
||||
db.commit()
|
||||
return {"entity": entity_name, "facts": len(facts), "observations": created}
|
||||
finally:
|
||||
@@ -394,8 +409,7 @@ def list_beliefs(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM beliefs WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -407,8 +421,7 @@ def list_rules(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM rules WHERE state='active' ORDER BY confidence DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -420,8 +433,7 @@ def list_skills(limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?",
|
||||
(limit,)
|
||||
"SELECT * FROM skills WHERE state='active' ORDER BY success_count DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -486,8 +498,9 @@ def handle_hook(event, data=None):
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Abstraction pipeline")
|
||||
ap.add_argument("cmd", choices=["extract", "observe", "beliefs", "rules", "skills",
|
||||
"evolve", "lifecycle", "stats", "hook"])
|
||||
ap.add_argument(
|
||||
"cmd", choices=["extract", "observe", "beliefs", "rules", "skills", "evolve", "lifecycle", "stats", "hook"]
|
||||
)
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
ap.add_argument("--episode-id", default=None)
|
||||
ap.add_argument("--source", default="session")
|
||||
@@ -501,16 +514,18 @@ def main():
|
||||
print("usage: abstraction.py extract '<episode text>'")
|
||||
return
|
||||
result = extract_facts(args.arg, episode_id=args.episode_id, source=args.source)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Extracted {result.get('extracted', 0)} facts")
|
||||
print(json.dumps(result, indent=2) if args.json else f"Extracted {result.get('extracted', 0)} facts")
|
||||
|
||||
elif args.cmd == "observe":
|
||||
if not args.arg:
|
||||
print("usage: abstraction.py observe '<entity name>'")
|
||||
return
|
||||
result = synthesize_observations(args.arg)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts")
|
||||
print(
|
||||
json.dumps(result, indent=2)
|
||||
if args.json
|
||||
else f"Entity: {result.get('entity')} — {result.get('observations', 0)} observations from {result.get('facts', 0)} facts"
|
||||
)
|
||||
|
||||
elif args.cmd == "beliefs":
|
||||
items = list_beliefs()
|
||||
@@ -568,5 +583,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["abstraction"],
|
||||
"priority": 50,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
@@ -562,7 +562,25 @@ def derive_sufficiency(
|
||||
# appears. A leading inspection verb makes the task read-only.
|
||||
first_words = lower.split()[:3]
|
||||
if any(
|
||||
w in ("inspect", "read", "review", "analyze", "analyse", "explain", "look", "list", "show", "describe", "check", "what", "why", "how", "where", "which")
|
||||
w
|
||||
in (
|
||||
"inspect",
|
||||
"read",
|
||||
"review",
|
||||
"analyze",
|
||||
"analyse",
|
||||
"explain",
|
||||
"look",
|
||||
"list",
|
||||
"show",
|
||||
"describe",
|
||||
"check",
|
||||
"what",
|
||||
"why",
|
||||
"how",
|
||||
"where",
|
||||
"which",
|
||||
)
|
||||
for w in first_words
|
||||
):
|
||||
is_defect = False
|
||||
|
||||
@@ -30,6 +30,7 @@ if _SD not in sys.path:
|
||||
@dataclass
|
||||
class AttentionCapsule:
|
||||
"""Compact working set for cognitive work."""
|
||||
|
||||
active_objective: str = ""
|
||||
current_phase: str = ""
|
||||
relevant_memories: List[str] = field(default_factory=list)
|
||||
@@ -199,14 +200,14 @@ def create_capsule(task: str, context: str = "", max_tokens: int = 600) -> Atten
|
||||
|
||||
# Extract task requirements (not constraints — no refusal language)
|
||||
requirement_patterns = [
|
||||
r'need to',
|
||||
r'want to',
|
||||
r'please',
|
||||
r'fix',
|
||||
r'build',
|
||||
r'create',
|
||||
r'update',
|
||||
r'change',
|
||||
r"need to",
|
||||
r"want to",
|
||||
r"please",
|
||||
r"fix",
|
||||
r"build",
|
||||
r"create",
|
||||
r"update",
|
||||
r"change",
|
||||
]
|
||||
|
||||
for pattern in requirement_patterns:
|
||||
@@ -233,7 +234,7 @@ def create_capsule(task: str, context: str = "", max_tokens: int = 600) -> Atten
|
||||
|
||||
# If still over, trim objective
|
||||
while capsule.token_count > max_tokens and len(capsule.active_objective) > 50:
|
||||
capsule.active_objective = capsule.active_objective[:len(capsule.active_objective)//2]
|
||||
capsule.active_objective = capsule.active_objective[: len(capsule.active_objective) // 2]
|
||||
prompt_text = capsule.to_prompt()
|
||||
capsule.token_count = estimate_tokens(prompt_text)
|
||||
|
||||
@@ -291,5 +292,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["attention_capsule"],
|
||||
"priority": 5,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
+38
-22
@@ -87,14 +87,14 @@ def verify_chain() -> dict:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i+1}"}
|
||||
return {"valid": False, "entries": i, "reason": f"invalid JSON at line {i + 1}"}
|
||||
|
||||
# Verify prev_hash chain
|
||||
if entry.get("prev_hash") != prev_hash:
|
||||
return {
|
||||
"valid": False,
|
||||
"entries": i,
|
||||
"reason": f"chain broken at line {i+1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}"
|
||||
"reason": f"chain broken at line {i + 1}: expected prev_hash={prev_hash}, got {entry.get('prev_hash')}",
|
||||
}
|
||||
|
||||
# Verify HMAC
|
||||
@@ -104,7 +104,7 @@ def verify_chain() -> dict:
|
||||
return {
|
||||
"valid": False,
|
||||
"entries": i,
|
||||
"reason": f"HMAC mismatch at line {i+1}: stored={stored_hash[:16]}..., computed={computed[:16]}..."
|
||||
"reason": f"HMAC mismatch at line {i + 1}: stored={stored_hash[:16]}..., computed={computed[:16]}...",
|
||||
}
|
||||
|
||||
prev_hash = stored_hash
|
||||
@@ -131,37 +131,51 @@ def get_entries(limit: int = 50) -> List[dict]:
|
||||
|
||||
def log_promotion(entry_id: int, old_status: str, new_status: str, wash_count: int, reason: str) -> dict:
|
||||
"""Log a promotion decision."""
|
||||
return append_entry("promotion", entry_id, {
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
"wash_count": wash_count,
|
||||
"reason": reason,
|
||||
})
|
||||
return append_entry(
|
||||
"promotion",
|
||||
entry_id,
|
||||
{
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
"wash_count": wash_count,
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_gate_result(entry_id: int, verdict: str, critics: dict) -> dict:
|
||||
"""Log a pre-commit gate result."""
|
||||
return append_entry("gate", entry_id, {
|
||||
"verdict": verdict,
|
||||
"critics": critics,
|
||||
})
|
||||
return append_entry(
|
||||
"gate",
|
||||
entry_id,
|
||||
{
|
||||
"verdict": verdict,
|
||||
"critics": critics,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_drift_alert(alert_type: str, message: str, severity: str) -> dict:
|
||||
"""Log a drift alert."""
|
||||
return append_entry("alert", 0, {
|
||||
"alert_type": alert_type,
|
||||
"message": message,
|
||||
"severity": severity,
|
||||
})
|
||||
return append_entry(
|
||||
"alert",
|
||||
0,
|
||||
{
|
||||
"alert_type": alert_type,
|
||||
"message": message,
|
||||
"severity": severity,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Promotion audit ledger")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
@@ -198,10 +212,12 @@ def main():
|
||||
print(f"Recent ledger entries ({len(entries)}):")
|
||||
for e in entries:
|
||||
print(f" [{e.get('type')}] entry_id={e.get('entry_id')} @ {e.get('timestamp', '')[:19]}")
|
||||
details = e.get('details', {})
|
||||
if e.get('type') == 'promotion':
|
||||
print(f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})")
|
||||
elif e.get('type') == 'gate':
|
||||
details = e.get("details", {})
|
||||
if e.get("type") == "promotion":
|
||||
print(
|
||||
f" {details.get('old_status')} → {details.get('new_status')} (wash={details.get('wash_count')})"
|
||||
)
|
||||
elif e.get("type") == "gate":
|
||||
print(f" verdict={details.get('verdict')}")
|
||||
else:
|
||||
print("No ledger entries.")
|
||||
|
||||
+69
-17
@@ -28,8 +28,7 @@ def run_warm_router(message):
|
||||
"""Run warm router and return fired neurons."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[PYTHON, WARM_ROUTER, "route", message, "--json"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
[PYTHON, WARM_ROUTER, "route", message, "--json"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return json.loads(result.stdout)
|
||||
@@ -42,8 +41,7 @@ def run_memory_recall(query, budget=5):
|
||||
"""Run memory recall and return relevant entries."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[PYTHON, MEMORY_STORE, "recall", query, "--json"],
|
||||
capture_output=True, text=True, timeout=15
|
||||
[PYTHON, MEMORY_STORE, "recall", query, "--json"], capture_output=True, text=True, timeout=15
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
data = json.loads(result.stdout)
|
||||
@@ -57,10 +55,7 @@ def run_memory_recall(query, budget=5):
|
||||
def run_always_digest():
|
||||
"""Run always-on digest and return results."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[PYTHON, WARM_ROUTER, "always", "--json"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
result = subprocess.run([PYTHON, WARM_ROUTER, "always", "--json"], capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return json.loads(result.stdout)
|
||||
except Exception:
|
||||
@@ -108,14 +103,70 @@ def build_auto_context(message):
|
||||
# Memory recall for substantive messages
|
||||
if message and len(message.strip()) > 10:
|
||||
# Extract key terms for recall (words > 4 chars, not stopwords)
|
||||
stopwords = {"what", "when", "where", "which", "that", "this", "with", "from",
|
||||
"have", "been", "were", "they", "their", "about", "would", "could",
|
||||
"should", "will", "into", "your", "just", "like", "know", "think",
|
||||
"also", "back", "then", "than", "more", "some", "only", "very",
|
||||
"does", "done", "make", "made", "want", "need", "give", "tell",
|
||||
"there", "these", "those", "because", "between", "after", "before",
|
||||
"under", "over", "again", "once", "here", "each", "every", "both",
|
||||
"few", "most", "other", "such", "same", "being", "having"}
|
||||
stopwords = {
|
||||
"what",
|
||||
"when",
|
||||
"where",
|
||||
"which",
|
||||
"that",
|
||||
"this",
|
||||
"with",
|
||||
"from",
|
||||
"have",
|
||||
"been",
|
||||
"were",
|
||||
"they",
|
||||
"their",
|
||||
"about",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"will",
|
||||
"into",
|
||||
"your",
|
||||
"just",
|
||||
"like",
|
||||
"know",
|
||||
"think",
|
||||
"also",
|
||||
"back",
|
||||
"then",
|
||||
"than",
|
||||
"more",
|
||||
"some",
|
||||
"only",
|
||||
"very",
|
||||
"does",
|
||||
"done",
|
||||
"make",
|
||||
"made",
|
||||
"want",
|
||||
"need",
|
||||
"give",
|
||||
"tell",
|
||||
"there",
|
||||
"these",
|
||||
"those",
|
||||
"because",
|
||||
"between",
|
||||
"after",
|
||||
"before",
|
||||
"under",
|
||||
"over",
|
||||
"again",
|
||||
"once",
|
||||
"here",
|
||||
"each",
|
||||
"every",
|
||||
"both",
|
||||
"few",
|
||||
"most",
|
||||
"other",
|
||||
"such",
|
||||
"same",
|
||||
"being",
|
||||
"having",
|
||||
}
|
||||
words = [w for w in message.lower().split() if len(w) > 4 and w.isalpha() and w not in stopwords]
|
||||
if words:
|
||||
query = " ".join(words[:5])
|
||||
@@ -167,6 +218,7 @@ def handle_hook(event, data=None):
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Auto-context: automatic memory activation")
|
||||
ap.add_argument("cmd", choices=["build", "show", "hook"])
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
@@ -214,5 +266,5 @@ PLUGIN = {
|
||||
"dependencies": ["memory_store", "warm_router"],
|
||||
"provides": ["auto_context"],
|
||||
"priority": 5,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
+72
-47
@@ -85,63 +85,72 @@ def extract_tasks(message, channel="chat"):
|
||||
"""
|
||||
# Strip code blocks before analysis
|
||||
import re as _re
|
||||
message = _re.sub(r'```[\s\S]*?```', '', message)
|
||||
message = _re.sub(r'`[^`]+`', '', message)
|
||||
|
||||
message = _re.sub(r"```[\s\S]*?```", "", message)
|
||||
message = _re.sub(r"`[^`]+`", "", message)
|
||||
|
||||
tasks = []
|
||||
lines = message.split("\n")
|
||||
|
||||
# Pattern 1: Numbered lists (1. do this, 2. do that)
|
||||
numbered = re.findall(r'(?:^|\n)\s*(\d+)[\.\)]\s*(.+?)(?:\n|$)', message)
|
||||
numbered = re.findall(r"(?:^|\n)\s*(\d+)[\.\)]\s*(.+?)(?:\n|$)", message)
|
||||
if len(numbered) >= 2:
|
||||
for num, text in numbered:
|
||||
text = text.strip()
|
||||
if len(text) > 10:
|
||||
tasks.append({
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "numbered_list",
|
||||
})
|
||||
tasks.append(
|
||||
{
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "numbered_list",
|
||||
}
|
||||
)
|
||||
|
||||
# Pattern 2: Bullet points (- do this, * do that)
|
||||
bullets = re.findall(r'(?:^|\n)\s*[-*•]\s*(.+?)(?:\n|$)', message)
|
||||
bullets = re.findall(r"(?:^|\n)\s*[-*•]\s*(.+?)(?:\n|$)", message)
|
||||
if len(bullets) >= 2:
|
||||
for text in bullets:
|
||||
text = text.strip()
|
||||
if len(text) > 10:
|
||||
tasks.append({
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "bullet_list",
|
||||
})
|
||||
tasks.append(
|
||||
{
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "bullet_list",
|
||||
}
|
||||
)
|
||||
|
||||
# Pattern 3: Action verbs at start of sentences
|
||||
action_patterns = [
|
||||
r'(?:^|\n)\s*(?:I(?:\'ll| will| need to)|let me|we should|we need to)\s+(.+?)(?:\.|$|\n)',
|
||||
r'(?:^|\n)\s*(?:fix|build|create|add|update|check|verify|test|research|investigate|implement|design)\s+(.+?)(?:\.|$|\n)',
|
||||
r"(?:^|\n)\s*(?:I(?:\'ll| will| need to)|let me|we should|we need to)\s+(.+?)(?:\.|$|\n)",
|
||||
r"(?:^|\n)\s*(?:fix|build|create|add|update|check|verify|test|research|investigate|implement|design)\s+(.+?)(?:\.|$|\n)",
|
||||
]
|
||||
for pattern in action_patterns:
|
||||
matches = re.findall(pattern, message, re.IGNORECASE | re.MULTILINE)
|
||||
for text in matches:
|
||||
text = text.strip()
|
||||
if len(text) > 10 and len(text) < 200:
|
||||
tasks.append({
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "action_verb",
|
||||
})
|
||||
tasks.append(
|
||||
{
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "action_verb",
|
||||
}
|
||||
)
|
||||
|
||||
# Pattern 4: "First... then... finally..." sequences
|
||||
sequence = re.findall(r'(?:first|then|next|finally|after that)[,.]?\s+(.+?)(?:\.|$|\n)', message, re.IGNORECASE)
|
||||
sequence = re.findall(r"(?:first|then|next|finally|after that)[,.]?\s+(.+?)(?:\.|$|\n)", message, re.IGNORECASE)
|
||||
if len(sequence) >= 2:
|
||||
for text in sequence:
|
||||
text = text.strip()
|
||||
if len(text) > 10:
|
||||
tasks.append({
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "sequence",
|
||||
})
|
||||
tasks.append(
|
||||
{
|
||||
"description": text,
|
||||
"priority": "normal",
|
||||
"pattern": "sequence",
|
||||
}
|
||||
)
|
||||
|
||||
# Deduplicate by description similarity
|
||||
seen = set()
|
||||
@@ -168,12 +177,14 @@ def store_tasks(tasks, channel="chat", source_message=""):
|
||||
next_id = (row["max_id"] or 0) + 1
|
||||
task_id = f"AT{next_id}"
|
||||
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO tasks (task_id, description, source_message, source_channel,
|
||||
status, priority, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
|
||||
""", (task_id, task["description"], source_message[:500], channel,
|
||||
task.get("priority", "normal"), ts, ts))
|
||||
""",
|
||||
(task_id, task["description"], source_message[:500], channel, task.get("priority", "normal"), ts, ts),
|
||||
)
|
||||
|
||||
# Log to completion file
|
||||
log_completion(task_id, "created", task["description"], channel)
|
||||
@@ -196,15 +207,21 @@ def complete_task(task_id, notes=""):
|
||||
row = db.execute("SELECT description FROM tasks WHERE task_id=? AND status='active'", (task_id,)).fetchone()
|
||||
description = row["description"] if row else ""
|
||||
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE tasks SET status='completed', completion_notes=?, completed_at=?, updated_at=?
|
||||
WHERE task_id=? AND status='active'
|
||||
""", (notes, ts, ts, task_id))
|
||||
""",
|
||||
(notes, ts, ts, task_id),
|
||||
)
|
||||
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO completion_log (task_id, action, details, timestamp)
|
||||
VALUES (?, 'completed', ?, ?)
|
||||
""", (task_id, notes, ts))
|
||||
""",
|
||||
(task_id, notes, ts),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
finally:
|
||||
@@ -244,6 +261,7 @@ def run_auto_checks(description):
|
||||
def run_compile_check():
|
||||
"""Check that all Python scripts compile."""
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
check_script = (
|
||||
"import py_compile, glob, sys; "
|
||||
@@ -253,9 +271,7 @@ def run_compile_check():
|
||||
"print(f'FAIL:{len(failed)}' if failed else 'PASS')"
|
||||
)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", check_script],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
cwd=SCRIPTS_DIR
|
||||
[sys.executable, "-c", check_script], capture_output=True, text=True, timeout=30, cwd=SCRIPTS_DIR
|
||||
)
|
||||
passed = "PASS" in result.stdout
|
||||
return {"check": "compile", "passed": passed, "details": result.stdout.strip()[:200]}
|
||||
@@ -266,14 +282,18 @@ def run_compile_check():
|
||||
def run_benchmark_check():
|
||||
"""Run RuneBench."""
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, "rune_bench.py"), "run"],
|
||||
capture_output=True, text=True, timeout=120
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
# Check for pass patterns: "N/N" where both numbers match, or explicit ALL PASS
|
||||
import re
|
||||
pass_match = re.search(r'(\d+)/\1', result.stdout)
|
||||
|
||||
pass_match = re.search(r"(\d+)/\1", result.stdout)
|
||||
passed = bool(pass_match) or "ALL" in result.stdout.upper() or "PASS" in result.stdout.upper()
|
||||
return {"check": "benchmark", "passed": passed, "details": result.stdout.strip()[-200:]}
|
||||
except Exception as e:
|
||||
@@ -284,12 +304,17 @@ def run_sanity_check():
|
||||
"""Basic sanity check — can we import key modules?"""
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c",
|
||||
"import sys; sys.path.insert(0, '.'); "
|
||||
"import memory_store, warm_router, microkernel; print('PASS')"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
cwd=SCRIPTS_DIR
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import sys; sys.path.insert(0, '.'); import memory_store, warm_router, microkernel; print('PASS')",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
cwd=SCRIPTS_DIR,
|
||||
)
|
||||
passed = "PASS" in result.stdout
|
||||
return {"check": "sanity", "passed": passed, "details": result.stdout.strip()[:200]}
|
||||
@@ -302,8 +327,7 @@ def list_tasks(status="active", limit=20):
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?",
|
||||
(status, limit)
|
||||
"SELECT * FROM tasks WHERE status=? ORDER BY created_at DESC LIMIT ?", (status, limit)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
@@ -359,6 +383,7 @@ def handle_hook(event, data=None):
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Automatic task extraction")
|
||||
ap.add_argument("cmd", choices=["extract", "complete", "list", "completion", "hook"])
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
@@ -431,5 +456,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["auto_tasks"],
|
||||
"priority": 4,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
+53
-31
@@ -101,11 +101,14 @@ def add_correction(original, corrected, scope_tags=None, domain="", pattern=""):
|
||||
ts = _now()
|
||||
tags = json.dumps(scope_tags or {})
|
||||
|
||||
cur = db.execute("""
|
||||
cur = db.execute(
|
||||
"""
|
||||
INSERT INTO corrections (original_behavior, corrected_behavior, scope_tags,
|
||||
scope_pattern, scope_domain, confidence, state, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 0.5, 'active', ?)
|
||||
""", (original, corrected, tags, pattern, domain, ts))
|
||||
""",
|
||||
(original, corrected, tags, pattern, domain, ts),
|
||||
)
|
||||
correction_id = cur.lastrowid
|
||||
db.commit()
|
||||
return {"added": correction_id, "confidence": 0.5, "scope": scope_tags or {}}
|
||||
@@ -120,11 +123,14 @@ def add_habit(trigger, action, name="", context_tags=None, min_activations=3):
|
||||
ts = _now()
|
||||
tags = json.dumps(context_tags or [])
|
||||
|
||||
cur = db.execute("""
|
||||
cur = db.execute(
|
||||
"""
|
||||
INSERT INTO habits (name, trigger_pattern, context_tags, action_template,
|
||||
min_activations, confidence, state, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 0.5, 'candidate', ?)
|
||||
""", (name, trigger, tags, action, min_activations, ts))
|
||||
""",
|
||||
(name, trigger, tags, action, min_activations, ts),
|
||||
)
|
||||
habit_id = cur.lastrowid
|
||||
db.commit()
|
||||
return {"added": habit_id, "state": "candidate", "min_activations": min_activations}
|
||||
@@ -214,16 +220,22 @@ def reinforce(item_id, outcome, item_type="habit"):
|
||||
if habit["state"] == "candidate" and new_count >= habit["min_activations"]:
|
||||
new_state = "active"
|
||||
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE habits SET activation_count=?, success_count=?, failure_count=?,
|
||||
confidence=?, state=?, last_activated=? WHERE id=?
|
||||
""", (new_count, new_success, new_failure, new_confidence, new_state, ts, item_id))
|
||||
""",
|
||||
(new_count, new_success, new_failure, new_confidence, new_state, ts, item_id),
|
||||
)
|
||||
|
||||
# Log activation
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO habit_activations (habit_id, context, outcome, activated_at)
|
||||
VALUES (?, '', ?, ?)
|
||||
""", (item_id, outcome, ts))
|
||||
""",
|
||||
(item_id, outcome, ts),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
|
||||
@@ -244,10 +256,13 @@ def reinforce(item_id, outcome, item_type="habit"):
|
||||
new_count = correction["evidence_count"]
|
||||
|
||||
new_state = "active" if new_confidence > 0.3 else "archived"
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE corrections SET confidence=?, evidence_count=?, state=?, last_reinforced=?
|
||||
WHERE id=?
|
||||
""", (new_confidence, new_count, new_state, ts, item_id))
|
||||
""",
|
||||
(new_confidence, new_count, new_state, ts, item_id),
|
||||
)
|
||||
db.commit()
|
||||
return {"reinforced": item_id, "confidence": new_confidence, "state": new_state}
|
||||
finally:
|
||||
@@ -261,17 +276,23 @@ def list_items(item_type="all", limit=20):
|
||||
results = {}
|
||||
|
||||
if item_type in ("all", "correction"):
|
||||
corrections = db.execute("""
|
||||
corrections = db.execute(
|
||||
"""
|
||||
SELECT * FROM corrections WHERE state='active'
|
||||
ORDER BY confidence DESC LIMIT ?
|
||||
""", (limit,)).fetchall()
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
results["corrections"] = [dict(c) for c in corrections]
|
||||
|
||||
if item_type in ("all", "habit"):
|
||||
habits = db.execute("""
|
||||
habits = db.execute(
|
||||
"""
|
||||
SELECT * FROM habits WHERE state IN ('active', 'candidate')
|
||||
ORDER BY confidence DESC LIMIT ?
|
||||
""", (limit,)).fetchall()
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
results["habits"] = [dict(h) for h in habits]
|
||||
|
||||
return results
|
||||
@@ -285,7 +306,9 @@ def behavior_stats():
|
||||
try:
|
||||
stats = {}
|
||||
stats["corrections_active"] = db.execute("SELECT COUNT(*) FROM corrections WHERE state='active'").fetchone()[0]
|
||||
stats["corrections_archived"] = db.execute("SELECT COUNT(*) FROM corrections WHERE state='archived'").fetchone()[0]
|
||||
stats["corrections_archived"] = db.execute(
|
||||
"SELECT COUNT(*) FROM corrections WHERE state='archived'"
|
||||
).fetchone()[0]
|
||||
stats["habits_active"] = db.execute("SELECT COUNT(*) FROM habits WHERE state='active'").fetchone()[0]
|
||||
stats["habits_candidate"] = db.execute("SELECT COUNT(*) FROM habits WHERE state='candidate'").fetchone()[0]
|
||||
stats["total_activations"] = db.execute("SELECT COUNT(*) FROM habit_activations").fetchone()[0]
|
||||
@@ -296,6 +319,7 @@ def behavior_stats():
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Behavioral patterns")
|
||||
ap.add_argument("cmd", choices=["correct", "habit", "match", "reinforce", "list", "stats"])
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
@@ -317,20 +341,16 @@ def main():
|
||||
print("usage: behavior.py correct '<original>' '<corrected>'")
|
||||
return
|
||||
scope = json.loads(args.scope) if args.scope else None
|
||||
result = add_correction(args.arg, args.arg2, scope_tags=scope,
|
||||
domain=args.domain, pattern=args.pattern)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Correction added (confidence: {result['confidence']})")
|
||||
result = add_correction(args.arg, args.arg2, scope_tags=scope, domain=args.domain, pattern=args.pattern)
|
||||
print(json.dumps(result, indent=2) if args.json else f"Correction added (confidence: {result['confidence']})")
|
||||
|
||||
elif args.cmd == "habit":
|
||||
if not all([args.arg, args.arg2]):
|
||||
print("usage: behavior.py habit '<trigger_pattern>' '<action>'")
|
||||
return
|
||||
tags = json.loads(args.context_tags) if args.context_tags else None
|
||||
result = add_habit(args.arg, args.arg2, name=args.name,
|
||||
context_tags=tags, min_activations=args.min_activations)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Habit candidate added (state: {result['state']})")
|
||||
result = add_habit(args.arg, args.arg2, name=args.name, context_tags=tags, min_activations=args.min_activations)
|
||||
print(json.dumps(result, indent=2) if args.json else f"Habit candidate added (state: {result['state']})")
|
||||
|
||||
elif args.cmd == "match":
|
||||
if not args.arg:
|
||||
@@ -350,8 +370,7 @@ def main():
|
||||
print("usage: behavior.py reinforce <id>")
|
||||
return
|
||||
result = reinforce(int(args.arg), args.outcome, item_type=args.type)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Reinforced: confidence={result.get('confidence', '?')}")
|
||||
print(json.dumps(result, indent=2) if args.json else f"Reinforced: confidence={result.get('confidence', '?')}")
|
||||
|
||||
elif args.cmd == "list":
|
||||
results = list_items(item_type=args.type, limit=args.limit)
|
||||
@@ -361,15 +380,18 @@ def main():
|
||||
for c in results.get("corrections", []):
|
||||
print(f" [correction {c['id']}] {c['corrected_behavior'][:60]} (conf: {c['confidence']:.2f})")
|
||||
for h in results.get("habits", []):
|
||||
state = h['state']
|
||||
state = h["state"]
|
||||
print(f" [habit {h['id']}] {h['action_template'][:60]} ({state}, conf: {h['confidence']:.2f})")
|
||||
|
||||
elif args.cmd == "stats":
|
||||
s = behavior_stats()
|
||||
print(json.dumps(s, indent=2) if args.json else
|
||||
f"Corrections: {s['corrections_active']} active, {s['corrections_archived']} archived | "
|
||||
f"Habits: {s['habits_active']} active, {s['habits_candidate']} candidate | "
|
||||
f"Activations: {s['total_activations']}")
|
||||
print(
|
||||
json.dumps(s, indent=2)
|
||||
if args.json
|
||||
else f"Corrections: {s['corrections_active']} active, {s['corrections_archived']} archived | "
|
||||
f"Habits: {s['habits_active']} active, {s['habits_candidate']} candidate | "
|
||||
f"Activations: {s['total_activations']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -385,5 +407,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["behavior"],
|
||||
"priority": 50,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
"""behaviour_ledger.py — T19 §19F/19G/19I: the canonical Behaviour
|
||||
Validation Ledger.
|
||||
|
||||
PRIMARY UNIT: the VERSIONED BEHAVIOUR / TEST SPECIFICATION — not "a
|
||||
test run in some session". Automated verification and real-world/user
|
||||
confirmation are DISTINCT truths (rule 19F): tests green ≠ user-
|
||||
confirmed.
|
||||
|
||||
GENERATION-AWARE HISTORY (rule 19G, mandatory): when a dependency
|
||||
changes, affected behaviours become RETEST_REQUIRED while their prior
|
||||
confirmation history stays queryable — confidence does not silently
|
||||
carry forward, and it does not silently disappear either.
|
||||
|
||||
SELECTIVE INVALIDATION (rule 19I): only behaviours whose structural
|
||||
dependencies changed are invalidated; every invalidation carries a
|
||||
reason; unrelated hardened confirmations are preserved.
|
||||
|
||||
Storage: <project_store>/behaviour_ledger.json (versioned document).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
LEDGER_SCHEMA_VERSION = 1
|
||||
|
||||
# hardening lifecycle (rule 19G semantics)
|
||||
UNVALIDATED = "UNVALIDATED" # not yet validated
|
||||
IMPLEMENTED = "IMPLEMENTED" # implementation exists
|
||||
AUTO_VERIFIED = "AUTO_VERIFIED" # automated verification passed
|
||||
REAL_WORLD_PASSED = "REAL_WORLD_PASSED" # manual/real test passed
|
||||
TEST_FAILED = "TEST_FAILED" # test failed
|
||||
PARTIAL = "PARTIAL" # inconclusive
|
||||
USER_CONFIRMED = "USER_CONFIRMED" # hardened
|
||||
RETEST_REQUIRED = "RETEST_REQUIRED" # confidence invalidated
|
||||
ACCEPTED_LIMITATION = "ACCEPTED_LIMITATION" # explicitly approved gap
|
||||
NOT_APPLICABLE = "NOT_APPLICABLE"
|
||||
|
||||
STATES = (UNVALIDATED, IMPLEMENTED, AUTO_VERIFIED, REAL_WORLD_PASSED,
|
||||
TEST_FAILED, PARTIAL, USER_CONFIRMED, RETEST_REQUIRED,
|
||||
ACCEPTED_LIMITATION, NOT_APPLICABLE)
|
||||
|
||||
# states that represent STANDING CONFIDENCE (selective invalidation
|
||||
# may reopen exactly these; everything else is already open or n/a)
|
||||
CONFIRMED_STATES = (AUTO_VERIFIED, REAL_WORLD_PASSED, USER_CONFIRMED)
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class BehaviourLedger:
|
||||
def __init__(self, store_dir: Path | str):
|
||||
self.path = Path(store_dir) / "behaviour_ledger.json"
|
||||
self.doc = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.path.is_file():
|
||||
import json
|
||||
return json.loads(self.path.read_text())
|
||||
return {"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"behaviours": {}, "updated_at": _now()}
|
||||
|
||||
def save(self) -> None:
|
||||
import json
|
||||
self.doc["updated_at"] = _now()
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(json.dumps(self.doc, indent=1, sort_keys=True))
|
||||
|
||||
# -- specification ------------------------------------------------------
|
||||
|
||||
def define(self, *, behaviour_id: str, owning_project: str,
|
||||
intended_behaviour: str, origin_intent: str = "",
|
||||
pass_criteria: str = "", fail_criteria: str = "",
|
||||
procedure: str = "",
|
||||
owner_component: str = "",
|
||||
dependencies: list[str] | None = None,
|
||||
automated_ref: str = "",
|
||||
environment: str = "") -> dict:
|
||||
bid = (behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
if bid in self.doc["behaviours"]:
|
||||
raise ValueError(f"behaviour exists: {bid}")
|
||||
rec = {
|
||||
"behaviour_id": bid,
|
||||
"owning_project": owning_project,
|
||||
"intended_behaviour": intended_behaviour,
|
||||
"origin_intent": origin_intent,
|
||||
"pass_criteria": pass_criteria,
|
||||
"fail_criteria": fail_criteria,
|
||||
"procedure": procedure,
|
||||
"owner_component": owner_component, # structural entity id
|
||||
"dependencies": list(dependencies or []), # structural ids
|
||||
"automated_ref": automated_ref,
|
||||
"environment": environment,
|
||||
"state": UNVALIDATED,
|
||||
"evidence": [], # [{at, generation, kind, detail}]
|
||||
"history": [], # every state transition, never lost
|
||||
"invalidations": [], # [{at, generation, because}]
|
||||
"limitations": [],
|
||||
"created_at": _now(),
|
||||
"updated_at": _now(),
|
||||
}
|
||||
self.doc["behaviours"][bid] = rec
|
||||
return rec
|
||||
|
||||
def get(self, behaviour_id: str) -> dict | None:
|
||||
return self.doc["behaviours"].get(
|
||||
behaviour_id if behaviour_id.startswith("behaviour:")
|
||||
else f"behaviour:{behaviour_id}")
|
||||
|
||||
# -- state transitions (all recorded in history — rule 19G) -------------
|
||||
|
||||
def _transition(self, rec: dict, new_state: str, *, kind: str,
|
||||
detail: str = "", generation: str = "") -> None:
|
||||
if new_state not in STATES:
|
||||
raise ValueError(f"unknown validation state {new_state!r}")
|
||||
rec["history"].append({
|
||||
"at": _now(), "from": rec["state"], "to": new_state,
|
||||
"kind": kind, "detail": detail[:200],
|
||||
"generation": generation})
|
||||
rec["state"] = new_state
|
||||
rec["updated_at"] = _now()
|
||||
|
||||
def record_auto_verification(self, behaviour_id: str, *,
|
||||
passed: bool, detail: str = "",
|
||||
generation: str = "") -> dict:
|
||||
"""Automated verification. GREEN NEVER IMPLIES USER_CONFIRMED
|
||||
(rule 19W-22): the ceiling here is AUTO_VERIFIED."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "automated",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation})
|
||||
self._transition(rec, AUTO_VERIFIED if passed else TEST_FAILED,
|
||||
kind="automated_verification", detail=detail,
|
||||
generation=generation)
|
||||
return rec
|
||||
|
||||
def record_real_world(self, behaviour_id: str, *, passed: bool,
|
||||
detail: str = "", generation: str = "",
|
||||
confirmed_by: str = "user") -> dict:
|
||||
"""Real-world/manual test result (testing mode). USER_CONFIRMED
|
||||
requires an explicit confirmed_by='user' pass (rule 19M)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["evidence"].append({"at": _now(), "kind": "real_world",
|
||||
"passed": passed,
|
||||
"detail": detail[:200],
|
||||
"generation": generation,
|
||||
"by": confirmed_by})
|
||||
if passed:
|
||||
state = (USER_CONFIRMED if confirmed_by == "user"
|
||||
else REAL_WORLD_PASSED)
|
||||
else:
|
||||
state = TEST_FAILED
|
||||
self._transition(rec, state, kind="real_world", detail=detail,
|
||||
generation=generation)
|
||||
return rec
|
||||
|
||||
def accept_limitation(self, behaviour_id: str, *, note: str,
|
||||
approved_by: str = "user") -> dict:
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown behaviour {behaviour_id!r}")
|
||||
rec["limitations"].append({"at": _now(), "note": note[:200],
|
||||
"approved_by": approved_by})
|
||||
self._transition(rec, ACCEPTED_LIMITATION, kind="accept_limitation",
|
||||
detail=note, )
|
||||
return rec
|
||||
|
||||
# -- selective invalidation (rule 19I) -----------------------------------
|
||||
|
||||
def invalidate_for_change(self, *, changed_entities: set[str],
|
||||
generation: str = "",
|
||||
reasons: dict[str, str] | None = None
|
||||
) -> list[dict]:
|
||||
"""Mark RETEST_REQUIRED exactly those confirmed behaviours whose
|
||||
structural dependencies intersect the changed closure. Every
|
||||
invalidation carries a reason; unrelated confirmations stay.
|
||||
Returns the invalidated records."""
|
||||
reasons = reasons or {}
|
||||
invalidated = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] not in CONFIRMED_STATES:
|
||||
continue
|
||||
overlap = set(rec.get("dependencies") or []) & changed_entities
|
||||
if rec.get("owner_component") in changed_entities:
|
||||
overlap.add(rec["owner_component"])
|
||||
if not overlap:
|
||||
continue # unrelated hardened behaviour: PRESERVED (19S-5)
|
||||
because = reasons.get(
|
||||
rec["behaviour_id"],
|
||||
f"structural change touched: {sorted(overlap)}")
|
||||
rec["invalidations"].append({
|
||||
"at": _now(), "generation": generation,
|
||||
"because": because[:200],
|
||||
"was": rec["state"],
|
||||
"changed_entities": sorted(overlap)})
|
||||
# HISTORY IS PRESERVED — prior confirmation stays queryable
|
||||
self._transition(rec, RETEST_REQUIRED,
|
||||
kind="selective_invalidation",
|
||||
detail=f"was {rec['state']}; {because}",
|
||||
generation=generation)
|
||||
invalidated.append(rec)
|
||||
return invalidated
|
||||
|
||||
def retest_history(self, behaviour_id: str) -> list[dict]:
|
||||
"""Generation-aware history: all confirmations + invalidations
|
||||
for one behaviour, oldest first (rule 19S-12)."""
|
||||
rec = self.get(behaviour_id)
|
||||
if rec is None:
|
||||
return []
|
||||
events = []
|
||||
for h in rec["history"]:
|
||||
events.append({"at": h["at"], "generation": h.get("generation"),
|
||||
"event": f"{h['from']}→{h['to']}",
|
||||
"kind": h["kind"], "detail": h.get("detail", "")})
|
||||
return events
|
||||
|
||||
# -- queue (testing mode, 19L/M) ------------------------------------------
|
||||
|
||||
def outstanding(self, *, component: str | None = None,
|
||||
project: str | None = None) -> list[dict]:
|
||||
"""Eligible testing items: anything not confirmed/accepted/n.a.
|
||||
Dynamic — the queue re-derives from states after every fix
|
||||
(rule 19M)."""
|
||||
out = []
|
||||
for rec in self.doc["behaviours"].values():
|
||||
if rec["state"] in (USER_CONFIRMED, ACCEPTED_LIMITATION,
|
||||
NOT_APPLICABLE):
|
||||
continue
|
||||
if project and rec.get("owning_project") != project:
|
||||
continue
|
||||
if component and rec.get("owner_component") != component:
|
||||
continue
|
||||
out.append(rec)
|
||||
out.sort(key=lambda r: (0 if r["state"] == TEST_FAILED else 1,
|
||||
r["behaviour_id"]))
|
||||
return out
|
||||
|
||||
def summary(self) -> dict:
|
||||
counts: dict[str, int] = {}
|
||||
for rec in self.doc["behaviours"].values():
|
||||
counts[rec["state"]] = counts.get(rec["state"], 0) + 1
|
||||
return {"total": len(self.doc["behaviours"]),
|
||||
"states": counts,
|
||||
"outstanding": len(self.outstanding())}
|
||||
@@ -0,0 +1,314 @@
|
||||
"""build_basis.py — T18 §18C: durable versioned RESEARCH BASIS for
|
||||
construction (V1 rule F: RESEARCH INFORMS STRUCTURE).
|
||||
|
||||
Research output must not disappear in transient model context. A Build
|
||||
Basis is a durable, schema-versioned document that captures WHAT
|
||||
research decided and WHY, with references — not research dumps. It is
|
||||
created after the research-depth decision and before/with the
|
||||
Construction Contract, and is attachable to:
|
||||
|
||||
- a Project Scaffold (project-level basis)
|
||||
- a Construction Contract (per-contract basis, via evidence_ref)
|
||||
- future structural entities where useful
|
||||
|
||||
Research Core ownership (rule C): this module NEVER researches. It
|
||||
records the depth decision (derived FROM adaptive_router's route — no
|
||||
second router), references the canonical pipeline's qualified output
|
||||
(stages ISSUE→…→QUALIFY, artifacts in research/issue-<N>/), and
|
||||
structures the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
# Research depth tiers — derived from adaptive_router ROUTES, never a
|
||||
# second router (rule C/A). Each tier records WHY it was chosen.
|
||||
DEPTH_NONE = "NONE" # tiny obvious local change; local evidence only
|
||||
DEPTH_INWARD = "INWARD" # repo-internal understanding suffices
|
||||
DEPTH_OUTWARD = "OUTWARD" # precedent/prior-art discovery required
|
||||
DEPTH_FULL = "FULL" # full pipeline incl. adversarial+synthesis
|
||||
|
||||
DEPTH_TIERS = (DEPTH_NONE, DEPTH_INWARD, DEPTH_OUTWARD, DEPTH_FULL)
|
||||
|
||||
# route → default depth (recorded as decision evidence; caller may
|
||||
# justify an exception, which is recorded as such — never silent)
|
||||
_ROUTE_DEFAULT_DEPTH = {
|
||||
"LOCAL_FIX": DEPTH_NONE,
|
||||
"STANDARD_CHANGE": DEPTH_INWARD,
|
||||
"RESEARCHED_CHANGE": DEPTH_FULL,
|
||||
}
|
||||
|
||||
|
||||
def research_depth(route: str, *, mode: str = "",
|
||||
justification: str = "") -> dict:
|
||||
"""The research-depth DECISION for a construction need. Derived from
|
||||
the canonical route authority (adaptive_router); `justification` is
|
||||
REQUIRED when deviating from the route default — recorded, never
|
||||
silent (rule D: record why research depth was chosen)."""
|
||||
default = _ROUTE_DEFAULT_DEPTH.get(route, DEPTH_INWARD)
|
||||
depth = default
|
||||
why = f"route {route} default"
|
||||
if justification and justification.strip():
|
||||
# an explicit justification selects a tier by name if given;
|
||||
# otherwise it annotates the default
|
||||
candidate = justification.strip().split(":", 1)[0].strip()
|
||||
if candidate in DEPTH_TIERS:
|
||||
depth = candidate
|
||||
why = justification.strip()
|
||||
else:
|
||||
why = f"route {route} default; note: {justification.strip()}"
|
||||
elif route not in _ROUTE_DEFAULT_DEPTH:
|
||||
why = f"route {route!r} unknown — INWARD floor (never silent NONE)"
|
||||
depth = DEPTH_INWARD
|
||||
return {"depth": depth, "why": why, "route": route, "mode": mode,
|
||||
"decided_at": _now()}
|
||||
|
||||
|
||||
# OUTWARD precedent-first ordering contract (rule E): the questions the
|
||||
# prior-art pass MUST answer before deeper material is consulted.
|
||||
OUTWARD_PRIOR_ART_QUESTIONS = (
|
||||
"what already exists addressing this intent",
|
||||
"what open-source systems/products/workflows attempt it",
|
||||
"what architectural patterns already work",
|
||||
"what standards/conventions exist",
|
||||
"what can be directly ADOPTED",
|
||||
"what can be ADAPTED",
|
||||
"what can be COMBINED",
|
||||
"what should be REJECTED",
|
||||
"where is genuine INNOVATION required",
|
||||
)
|
||||
|
||||
_DISPOSITIONS = ("ADOPT", "ADAPT", "COMBINE", "REJECT", "INNOVATE")
|
||||
|
||||
|
||||
def new_basis(*, question: str, depth_decision: dict,
|
||||
issue_ref: str = "") -> dict:
|
||||
"""Create an empty-but-valid Build Basis. References only — full
|
||||
research dumps live in the Research Core's artifact dirs (rule 18C:
|
||||
do not store unnecessary dumps)."""
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"kind": "build_basis",
|
||||
"created_at": _now(),
|
||||
"question": question,
|
||||
"issue_ref": issue_ref, # e.g. research/issue-164 or Gitea issue
|
||||
"depth": depth_decision,
|
||||
"inward": {"findings": [], "refs": []},
|
||||
"outward": {"prior_art_questions_answered": [],
|
||||
"findings": [], "refs": []},
|
||||
"deeper": {"used": False, "findings": [], "refs": []},
|
||||
"dispositions": [], # [{item, disposition: ADOPT/…, why, ref}]
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {"architecture": [], "structural": [],
|
||||
"migration": [], "verification": []},
|
||||
}
|
||||
|
||||
|
||||
def set_dispositions(basis: dict, dispositions: list[dict]) -> list[str]:
|
||||
"""Record the adopt/adapt/combine/reject/innovate decisions (rule E).
|
||||
Returns validation errors; the basis stays unqualified until valid."""
|
||||
basis["dispositions"] = [
|
||||
{"item": d.get("item", ""), "disposition": d.get("disposition", ""),
|
||||
"why": d.get("why", ""), "ref": d.get("ref", "")}
|
||||
for d in dispositions
|
||||
]
|
||||
return validate_basis(basis)
|
||||
|
||||
|
||||
def validate_basis(basis: dict) -> list[str]:
|
||||
"""Qualification gate. A basis is QUALIFIED when it carries a depth
|
||||
decision with why, at least one outward prior-art answer when depth
|
||||
is OUTWARD/FULL, and every disposition uses a legal disposition."""
|
||||
errors: list[str] = []
|
||||
if basis.get("schema_version") != SCHEMA_VERSION:
|
||||
errors.append(f"schema_version {basis.get('schema_version')!r}")
|
||||
depth = (basis.get("depth") or {}).get("depth")
|
||||
if depth not in DEPTH_TIERS:
|
||||
errors.append(f"depth {depth!r} not in {DEPTH_TIERS}")
|
||||
if not (basis.get("depth") or {}).get("why"):
|
||||
errors.append("depth decision lacks WHY (rule D)")
|
||||
if depth in (DEPTH_OUTWARD, DEPTH_FULL):
|
||||
outward = basis.get("outward") or {}
|
||||
if not (outward.get("prior_art_questions_answered")
|
||||
or outward.get("findings") or outward.get("refs")):
|
||||
errors.append("OUTWARD/FULL depth requires prior-art evidence")
|
||||
if depth == DEPTH_INWARD:
|
||||
inward = basis.get("inward") or {}
|
||||
if not (inward.get("findings") or inward.get("refs")
|
||||
or basis.get("dispositions")):
|
||||
errors.append("INWARD depth requires recorded evidence "
|
||||
"(findings/refs) — an empty basis is not "
|
||||
"qualified")
|
||||
for d in basis.get("dispositions", []):
|
||||
if d.get("disposition") not in _DISPOSITIONS:
|
||||
errors.append(f"illegal disposition {d.get('disposition')!r} "
|
||||
f"for {d.get('item','')[:40]!r}")
|
||||
return errors
|
||||
|
||||
|
||||
def is_qualified(basis: dict) -> bool:
|
||||
return not validate_basis(basis)
|
||||
|
||||
|
||||
def attach_evidence_ref(basis: dict) -> str:
|
||||
"""The evidence_ref string a contract carries to point at this basis
|
||||
(durable pointer, not a copy — stores stay independent)."""
|
||||
return (f"build_basis:{basis.get('issue_ref') or 'adhoc'}:"
|
||||
f"{basis.get('created_at')}")
|
||||
|
||||
|
||||
def summary(basis: dict) -> dict:
|
||||
"""Compact projection for prompts/previews (no private reasoning —
|
||||
structured conclusions only, rule 18G)."""
|
||||
return {
|
||||
"schema_version": basis.get("schema_version"),
|
||||
"question": basis.get("question"),
|
||||
"issue_ref": basis.get("issue_ref"),
|
||||
"depth": basis.get("depth"),
|
||||
"qualified": is_qualified(basis),
|
||||
"validation": validate_basis(basis),
|
||||
"dispositions": [
|
||||
{"item": d.get("item", "")[:80],
|
||||
"disposition": d.get("disposition")}
|
||||
for d in basis.get("dispositions", [])
|
||||
],
|
||||
"uncertainty_count": len(basis.get("uncertainty", [])),
|
||||
"risks_count": len(basis.get("risks", [])),
|
||||
"implications": {k: len(v) for k, v in
|
||||
(basis.get("implications") or {}).items()},
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
"""§21.1G: record a justified depth revision. Original + revised
|
||||
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
|
||||
explicit resolution evidence ('discovery resolved', 'prior-art
|
||||
resolved', 'unnecessary') — never a bare skip."""
|
||||
if new_depth not in DEPTH_TIERS:
|
||||
raise ValueError(f"unknown depth {new_depth!r}")
|
||||
original = basis["depth"]["depth"]
|
||||
if original == new_depth:
|
||||
raise ValueError("no-op revision: depth unchanged")
|
||||
consequential_origins = ("RESEARCHED_CHANGE", "FULL", "OUTWARD")
|
||||
if original in consequential_origins and new_depth in ("NONE", "INWARD"):
|
||||
keywords = ("resolved", "non-material", "discovery",
|
||||
"unnecessary", "prior-art question")
|
||||
if not any(k in reason.lower() for k in keywords):
|
||||
raise ValueError(
|
||||
"OUTWARD downgrade requires explicit evidence: the "
|
||||
"prior-art question must be resolved as non-material "
|
||||
"(not merely skipped)")
|
||||
basis["depth_revision"] = {
|
||||
"original_depth": original, "revised_depth": new_depth,
|
||||
"reason": reason[:200], "revised_at": _now()}
|
||||
basis["depth"]["depth"] = new_depth
|
||||
basis["depth"]["why"] = f"revised from {original}: {reason}"
|
||||
return basis["depth_revision"]
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
def from_json(text: str) -> dict:
|
||||
import json
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
_ = Any # typing import kept for forward annotations
|
||||
|
||||
|
||||
# ── T18 §18D CORRECTION (Nick, 2026-09-15): THE RESEARCH PLAN ITSELF IS
|
||||
# CONTEXTUALLY GENERATED. The prior-art categories researched for the
|
||||
# structural-model task (architecture graphs, monorepo tooling, etc.)
|
||||
# were CONTEXTUAL to that task — they must never become a fixed topic
|
||||
# list. For every request the plan is derived from:
|
||||
# intent → target/current reality → material unknowns/decisions/risks
|
||||
# → context-specific research topics → proportional depth.
|
||||
# There is NO permanent research-topic checklist anywhere in Scaffolding.
|
||||
|
||||
def generate_research_plan(*, intent: str, target: str = "",
|
||||
current_reality: str = "",
|
||||
unknowns: list[str] | None = None,
|
||||
decisions: list[str] | None = None,
|
||||
risks: list[str] | None = None,
|
||||
depth: str | None = None) -> dict:
|
||||
"""Generate a CONTEXT-SPECIFIC research plan for one construction
|
||||
intent. Topics are synthesized from the material unknowns/decisions/
|
||||
risks of THIS intent — the function has no topic vocabulary and no
|
||||
permanent category list. Depth, when not supplied, is chosen by
|
||||
consequence heuristics recorded in the plan (why is always present)."""
|
||||
unknowns = [u for u in (unknowns or []) if u.strip()]
|
||||
decisions = [d for d in (decisions or []) if d.strip()]
|
||||
risks = [r for r in (risks or []) if r.strip()]
|
||||
topics: list[dict] = []
|
||||
# each material unknown becomes a research topic aimed at resolving it
|
||||
for u in unknowns:
|
||||
topics.append({
|
||||
"topic": f"resolve unknown: {u}",
|
||||
"kind": "unknown-resolution",
|
||||
"why": f"material unknown blocks confident construction: {u[:80]}",
|
||||
})
|
||||
for d in decisions:
|
||||
topics.append({
|
||||
"topic": f"inform decision: {d}",
|
||||
"kind": "decision-support",
|
||||
"why": f"consequential decision needs precedent/evidence: {d[:80]}",
|
||||
})
|
||||
for r in risks:
|
||||
topics.append({
|
||||
"topic": f"de-risk: {r}",
|
||||
"kind": "risk-investigation",
|
||||
"why": f"identified risk needs verification/mitigation evidence: {r[:80]}",
|
||||
})
|
||||
# precedent question (rule E) attaches ONLY when outward evidence is
|
||||
# plausibly relevant — an unknown/decision about external prior art.
|
||||
needs_outward = any(
|
||||
w in " ".join(unknowns + decisions).lower()
|
||||
for w in ("exist", "existing", "prior art", "precedent", "standard",
|
||||
"convention", "library", "tool", "framework", "approach"))
|
||||
if needs_outward:
|
||||
topics.append({
|
||||
"topic": ("what already exists that is relevant to THIS intent "
|
||||
"(adopt/adapt/combine/reject/innovate)"),
|
||||
"kind": "prior-art",
|
||||
"why": "material unknown references existing practice — "
|
||||
"precedent discovery required before deeper research",
|
||||
})
|
||||
if depth is None:
|
||||
# consequence heuristics: novel/architectural/unsafe → deeper
|
||||
consequential = any(
|
||||
w in (intent + " " + " ".join(risks)).lower()
|
||||
for w in ("architecture", "migration", "replace", "rewrite",
|
||||
"security", "schema", "delete", "deploy", "breaking"))
|
||||
depth = DEPTH_FULL if (consequential and topics) else (
|
||||
DEPTH_OUTWARD if needs_outward and topics else (
|
||||
DEPTH_INWARD if topics else DEPTH_NONE))
|
||||
why = ("consequence heuristics: consequential keywords + research "
|
||||
"topics present" if depth == DEPTH_FULL else
|
||||
"precedent-relevant unknowns present" if depth == DEPTH_OUTWARD
|
||||
else "local understanding suffices" if depth == DEPTH_INWARD
|
||||
else "no material unknowns — local evidence only")
|
||||
else:
|
||||
why = "depth supplied by caller (recorded, caller accountable)"
|
||||
return {
|
||||
"intent": intent,
|
||||
"target": target,
|
||||
"current_reality_note": current_reality[:200],
|
||||
"depth": {"depth": depth, "why": why},
|
||||
"topics": topics,
|
||||
"plan_generated_at": _now(),
|
||||
# the plan ITSELF is evidence that research was contextual, not
|
||||
# checklist-driven: two different intents must produce disjoint
|
||||
# topic sets unless they share genuine unknowns
|
||||
}
|
||||
+41
-18
@@ -48,6 +48,7 @@ REPO_DIR = "/mnt/workdrive/rune"
|
||||
# DATA STRUCTURES
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class Phase(Enum):
|
||||
ASSESS = "assess"
|
||||
RESEARCH = "research"
|
||||
@@ -61,18 +62,23 @@ class Phase(Enum):
|
||||
INTEGRATE = "integrate"
|
||||
RECORD = "record"
|
||||
|
||||
|
||||
class BuildPhase(Enum):
|
||||
"""Three-phase execution for build step."""
|
||||
FOUNDATION = "foundation" # Types, interfaces, shared utilities
|
||||
|
||||
FOUNDATION = "foundation" # Types, interfaces, shared utilities
|
||||
IMPLEMENTATION = "implementation" # Feature code
|
||||
INTEGRATION = "integration" # Tests, wiring, documentation
|
||||
INTEGRATION = "integration" # Tests, wiring, documentation
|
||||
|
||||
|
||||
class VerificationTier(Enum):
|
||||
"""Tiered verification strategy."""
|
||||
SYNTACTIC = "syntactic" # Per-edit: lint, type-check, format (<1s)
|
||||
SEMANTIC = "semantic" # Per-batch: unit tests, import validation (<30s)
|
||||
|
||||
SYNTACTIC = "syntactic" # Per-edit: lint, type-check, format (<1s)
|
||||
SEMANTIC = "semantic" # Per-batch: unit tests, import validation (<30s)
|
||||
INTEGRATION = "integration" # Per-wave: contract tests, build/compile (<5min)
|
||||
SYSTEM = "system" # Full: full test suite, property-based (<15min)
|
||||
SYSTEM = "system" # Full: full test suite, property-based (<15min)
|
||||
|
||||
|
||||
class PhaseStatus(Enum):
|
||||
PENDING = "pending"
|
||||
@@ -81,6 +87,7 @@ class PhaseStatus(Enum):
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
phase: str
|
||||
@@ -90,6 +97,7 @@ class LedgerEntry:
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildState:
|
||||
build_id: str
|
||||
@@ -112,10 +120,12 @@ class BuildState:
|
||||
files_changed: List[str] = field(default_factory=list)
|
||||
contracts: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# BUILD MANAGER
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class BuildManager:
|
||||
"""Manages the build pipeline with worktree isolation and ledger tracking."""
|
||||
|
||||
@@ -282,11 +292,15 @@ class BuildManager:
|
||||
|
||||
def complete_feasible(self, feasible: bool, risk: str, details: Dict[str, Any] = None):
|
||||
"""Complete the feasibility phase."""
|
||||
self._log_phase(Phase.FEASIBLE.value, PhaseStatus.COMPLETE.value, {
|
||||
"feasible": feasible,
|
||||
"risk": risk,
|
||||
"details": details or {},
|
||||
})
|
||||
self._log_phase(
|
||||
Phase.FEASIBLE.value,
|
||||
PhaseStatus.COMPLETE.value,
|
||||
{
|
||||
"feasible": feasible,
|
||||
"risk": risk,
|
||||
"details": details or {},
|
||||
},
|
||||
)
|
||||
|
||||
def start_consult(self):
|
||||
"""Start the consult phase."""
|
||||
@@ -327,10 +341,14 @@ class BuildManager:
|
||||
|
||||
def complete_verify(self, passed: bool, failures: List[str] = None):
|
||||
"""Complete the verify phase."""
|
||||
self._log_phase(Phase.VERIFY.value, PhaseStatus.COMPLETE.value, {
|
||||
"passed": passed,
|
||||
"failures": failures or [],
|
||||
})
|
||||
self._log_phase(
|
||||
Phase.VERIFY.value,
|
||||
PhaseStatus.COMPLETE.value,
|
||||
{
|
||||
"passed": passed,
|
||||
"failures": failures or [],
|
||||
},
|
||||
)
|
||||
|
||||
def start_correct(self):
|
||||
"""Start the correct phase."""
|
||||
@@ -347,10 +365,14 @@ class BuildManager:
|
||||
|
||||
def complete_integrate(self, passed: bool, failures: List[str] = None):
|
||||
"""Complete the integrate phase."""
|
||||
self._log_phase(Phase.INTEGRATE.value, PhaseStatus.COMPLETE.value, {
|
||||
"passed": passed,
|
||||
"failures": failures or [],
|
||||
})
|
||||
self._log_phase(
|
||||
Phase.INTEGRATE.value,
|
||||
PhaseStatus.COMPLETE.value,
|
||||
{
|
||||
"passed": passed,
|
||||
"failures": failures or [],
|
||||
},
|
||||
)
|
||||
|
||||
def start_record(self):
|
||||
"""Start the record phase."""
|
||||
@@ -409,6 +431,7 @@ class BuildManager:
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: build_mode.py [start|status|resume|abort]")
|
||||
|
||||
@@ -9,6 +9,7 @@ process is fresh each call, so the hook-level dedupe is the effective one).
|
||||
Usage: builder_bridge_cli.py "<task text>"
|
||||
Output: the capsule markdown (possibly empty — inject-nothing policy).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
+31
-40
@@ -38,15 +38,15 @@ DB_PATH = os.path.join(memory_env.memory_dir(), "cache.db")
|
||||
# ---------------------------------------------------------------------------
|
||||
# In-memory hit tracking — avoids write-on-read in SQLite
|
||||
# ---------------------------------------------------------------------------
|
||||
_hit_counts: dict[str, int] = {} # key → pending hit increments
|
||||
_last_hits: dict[str, str] = {} # key → latest last_hit timestamp
|
||||
_miss_counts: dict[str, int] = {} # key → pending miss increments
|
||||
_last_misses: dict[str, str] = {} # key → latest last_miss timestamp
|
||||
_hit_counts: dict[str, int] = {} # key → pending hit increments
|
||||
_last_hits: dict[str, str] = {} # key → latest last_hit timestamp
|
||||
_miss_counts: dict[str, int] = {} # key → pending miss increments
|
||||
_last_misses: dict[str, str] = {} # key → latest last_miss timestamp
|
||||
_flush_lock = threading.Lock()
|
||||
_FLUSH_THRESHOLD = 50 # flush after this many pending updates
|
||||
_flush_counter = 0 # counts hits+misses since last flush
|
||||
_last_flush_time = 0.0 # epoch of last flush
|
||||
_FLUSH_INTERVAL = 30.0 # flush at least every N seconds
|
||||
_FLUSH_THRESHOLD = 50 # flush after this many pending updates
|
||||
_flush_counter = 0 # counts hits+misses since last flush
|
||||
_last_flush_time = 0.0 # epoch of last flush
|
||||
_FLUSH_INTERVAL = 30.0 # flush at least every N seconds
|
||||
|
||||
|
||||
def _record_hit(key: str) -> None:
|
||||
@@ -58,8 +58,7 @@ def _record_hit(key: str) -> None:
|
||||
_hit_counts[key] = _hit_counts.get(key, 0) + 1
|
||||
_last_hits[key] = now
|
||||
_flush_counter += 1
|
||||
if (_flush_counter >= _FLUSH_THRESHOLD
|
||||
or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL):
|
||||
if _flush_counter >= _FLUSH_THRESHOLD or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL:
|
||||
do_flush = True
|
||||
if do_flush:
|
||||
flush_stats()
|
||||
@@ -74,8 +73,7 @@ def _record_miss(key: str) -> None:
|
||||
_miss_counts[key] = _miss_counts.get(key, 0) + 1
|
||||
_last_misses[key] = now
|
||||
_flush_counter += 1
|
||||
if (_flush_counter >= _FLUSH_THRESHOLD
|
||||
or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL):
|
||||
if _flush_counter >= _FLUSH_THRESHOLD or (time.monotonic() - _last_flush_time) >= _FLUSH_INTERVAL:
|
||||
do_flush = True
|
||||
if do_flush:
|
||||
flush_stats()
|
||||
@@ -187,10 +185,7 @@ def get_cache(key):
|
||||
"""
|
||||
db = connect()
|
||||
try:
|
||||
row = db.execute(
|
||||
"SELECT * FROM cache WHERE key=? AND invalidated_at IS NULL",
|
||||
(key,)
|
||||
).fetchone()
|
||||
row = db.execute("SELECT * FROM cache WHERE key=? AND invalidated_at IS NULL", (key,)).fetchone()
|
||||
|
||||
if row:
|
||||
# Record hit in memory — no SQLite write on the hot path
|
||||
@@ -211,9 +206,7 @@ def get_cache(key):
|
||||
|
||||
# Miss — only record if the key exists in the cache (e.g. invalidated).
|
||||
# A true miss (key never seen) should not trigger any write.
|
||||
exists = db.execute(
|
||||
"SELECT 1 FROM cache WHERE key=?", (key,)
|
||||
).fetchone()
|
||||
exists = db.execute("SELECT 1 FROM cache WHERE key=?", (key,)).fetchone()
|
||||
|
||||
if exists:
|
||||
_record_miss(key)
|
||||
@@ -242,7 +235,8 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
|
||||
if compressed is None:
|
||||
compressed = _compress(value)
|
||||
|
||||
db.execute("""
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO cache
|
||||
(key, scope, content_hash, full_value, compressed_value, source_hash,
|
||||
created_at, invalidated_at, hit_count, miss_count)
|
||||
@@ -255,7 +249,9 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
|
||||
source_hash = excluded.source_hash,
|
||||
created_at = excluded.created_at,
|
||||
invalidated_at = NULL
|
||||
""", (key, scope, content_hash, value, compressed, source_hash, _now()))
|
||||
""",
|
||||
(key, scope, content_hash, value, compressed, source_hash, _now()),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
return {"stored": key, "content_hash": content_hash, "scope": scope}
|
||||
@@ -264,10 +260,7 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
|
||||
def invalidate(key, reason=""):
|
||||
"""Invalidate a specific cache entry."""
|
||||
db = connect()
|
||||
db.execute(
|
||||
"UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL",
|
||||
(_now(), key)
|
||||
)
|
||||
db.execute("UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL", (_now(), key))
|
||||
db.commit()
|
||||
db.close()
|
||||
return {"invalidated": key, "reason": reason}
|
||||
@@ -277,8 +270,7 @@ def invalidate_scope(scope, reason=""):
|
||||
"""Invalidate all entries in a scope."""
|
||||
db = connect()
|
||||
count = db.execute(
|
||||
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL",
|
||||
(_now(), scope)
|
||||
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL", (_now(), scope)
|
||||
).rowcount
|
||||
db.commit()
|
||||
db.close()
|
||||
@@ -288,10 +280,7 @@ def invalidate_scope(scope, reason=""):
|
||||
def invalidate_all(reason=""):
|
||||
"""Invalidate everything. Nuclear option."""
|
||||
db = connect()
|
||||
count = db.execute(
|
||||
"UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL",
|
||||
(_now(),)
|
||||
).rowcount
|
||||
count = db.execute("UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL", (_now(),)).rowcount
|
||||
db.commit()
|
||||
db.close()
|
||||
return {"invalidated_all": count, "reason": reason}
|
||||
@@ -409,8 +398,7 @@ def handle_hook(event, data=None):
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Cache system — event-driven, write-once-derive-twice")
|
||||
ap.add_argument("cmd", choices=["get", "put", "invalidate", "invalidate-all",
|
||||
"invalidate-scope", "stats", "hook"])
|
||||
ap.add_argument("cmd", choices=["get", "put", "invalidate", "invalidate-all", "invalidate-scope", "stats", "hook"])
|
||||
ap.add_argument("arg", nargs="?", default="")
|
||||
ap.add_argument("arg2", nargs="?", default="")
|
||||
ap.add_argument("--scope", default="general")
|
||||
@@ -440,8 +428,11 @@ def main():
|
||||
print("usage: cache.py put <key> <value>")
|
||||
return
|
||||
result = put_cache(args.arg, args.arg2, scope=args.scope, source_hash=args.source_hash)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Stored: {args.arg} (hash: {result['content_hash']}, scope: {result['scope']})")
|
||||
print(
|
||||
json.dumps(result, indent=2)
|
||||
if args.json
|
||||
else f"Stored: {args.arg} (hash: {result['content_hash']}, scope: {result['scope']})"
|
||||
)
|
||||
|
||||
elif args.cmd == "invalidate":
|
||||
if not args.arg:
|
||||
@@ -452,16 +443,16 @@ def main():
|
||||
|
||||
elif args.cmd == "invalidate-all":
|
||||
result = invalidate_all(reason=args.reason)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Invalidated all: {result['invalidated_all']} entries")
|
||||
print(json.dumps(result, indent=2) if args.json else f"Invalidated all: {result['invalidated_all']} entries")
|
||||
|
||||
elif args.cmd == "invalidate-scope":
|
||||
if not args.arg:
|
||||
print("usage: cache.py invalidate-scope <scope>")
|
||||
return
|
||||
result = invalidate_scope(args.arg, reason=args.reason)
|
||||
print(json.dumps(result, indent=2) if args.json else
|
||||
f"Invalidated scope '{args.arg}': {result['count']} entries")
|
||||
print(
|
||||
json.dumps(result, indent=2) if args.json else f"Invalidated scope '{args.arg}': {result['count']} entries"
|
||||
)
|
||||
|
||||
elif args.cmd == "stats":
|
||||
s = cache_stats()
|
||||
@@ -497,5 +488,5 @@ PLUGIN = {
|
||||
"dependencies": [],
|
||||
"provides": ["cache"],
|
||||
"priority": 1,
|
||||
"auto_activate": True
|
||||
"auto_activate": True,
|
||||
}
|
||||
@@ -34,9 +34,7 @@ def main() -> int:
|
||||
runtime.registry.disable("synthesis")
|
||||
third = runtime.process({"event_type": "c"}, [{"event_type": "new"}])
|
||||
if {r["worker"] for r in third["results"]} != {"n1"}:
|
||||
print(
|
||||
"COGNITIVE CANARY FAILED: toggle independence broken", file=sys.stderr
|
||||
)
|
||||
print("COGNITIVE CANARY FAILED: toggle independence broken", file=sys.stderr)
|
||||
return 1
|
||||
print("COGNITIVE CANARY PASSED", third["facts"])
|
||||
return 0
|
||||
|
||||
@@ -41,6 +41,7 @@ def main():
|
||||
import audit_ledger
|
||||
import warm_router
|
||||
import memory_consolidation
|
||||
|
||||
try:
|
||||
import curator
|
||||
except ImportError:
|
||||
@@ -51,16 +52,20 @@ def main():
|
||||
|
||||
# --- 1. Schema ---
|
||||
print("--- 1. Schema ---")
|
||||
cols = {r[1] for r in db.execute('PRAGMA table_info(entries)')}
|
||||
for c in ['wash_count', 'last_washed', 'promotion_eligible', 'rock_status']:
|
||||
cols = {r[1] for r in db.execute("PRAGMA table_info(entries)")}
|
||||
for c in ["wash_count", "last_washed", "promotion_eligible", "rock_status"]:
|
||||
test(f"column {c}", c in cols, f"{c} missing")
|
||||
|
||||
# --- 2. Full lifecycle ---
|
||||
print("\n--- 2. Full lifecycle ---")
|
||||
# Find a washable entry (not already rock)
|
||||
entry = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" AND rock_status="washable" LIMIT 1').fetchone()
|
||||
entry = db.execute(
|
||||
'SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" AND rock_status="washable" LIMIT 1'
|
||||
).fetchone()
|
||||
if not entry:
|
||||
entry = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" LIMIT 1').fetchone()
|
||||
entry = db.execute(
|
||||
'SELECT id, text, importance, wash_count, rock_status FROM entries WHERE status="active" LIMIT 1'
|
||||
).fetchone()
|
||||
if not entry:
|
||||
test("full lifecycle", True, "no entries in database (skipped)")
|
||||
# Skip lifecycle tests, continue to other tests
|
||||
@@ -73,8 +78,10 @@ def main():
|
||||
for i in range(20):
|
||||
memory_store.increment_wash(db, eid)
|
||||
|
||||
row = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?', (eid,)).fetchone()
|
||||
entry_dict = {'id': row[0], 'text': row[1], 'importance': row[2], 'wash_count': row[3], 'rock_status': row[4]}
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?", (eid,)
|
||||
).fetchone()
|
||||
entry_dict = {"id": row[0], "text": row[1], "importance": row[2], "wash_count": row[3], "rock_status": row[4]}
|
||||
|
||||
# Value function
|
||||
value = drift_core.compute_value(entry_dict)
|
||||
@@ -103,9 +110,11 @@ def main():
|
||||
print("\n--- 3. Cache ---")
|
||||
cache = drift_cache.DriftCache()
|
||||
if eid:
|
||||
row = db.execute('SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?', (eid,)).fetchone()
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status FROM entries WHERE id=?", (eid,)
|
||||
).fetchone()
|
||||
if row:
|
||||
cache.add(row[0], {'text': row[1], 'importance': row[2], 'wash_count': row[3], 'rock_status': row[4]})
|
||||
cache.add(row[0], {"text": row[1], "importance": row[2], "wash_count": row[3], "rock_status": row[4]})
|
||||
test("cache contains", cache.contains(eid))
|
||||
test("cache priority", cache.get_priority(eid) > 0, f"priority={cache.get_priority(eid)}")
|
||||
else:
|
||||
@@ -117,20 +126,29 @@ def main():
|
||||
|
||||
# --- 4. Compression ---
|
||||
print("\n--- 4. Compression ---")
|
||||
ratio_washable = drift_compression.get_compression_ratio(0, 'washable')
|
||||
ratio_proven = drift_compression.get_compression_ratio(25, 'washable')
|
||||
ratio_rock = drift_compression.get_compression_ratio(100, 'rock')
|
||||
ratio_washable = drift_compression.get_compression_ratio(0, "washable")
|
||||
ratio_proven = drift_compression.get_compression_ratio(25, "washable")
|
||||
ratio_rock = drift_compression.get_compression_ratio(100, "rock")
|
||||
test("compression decreases with wash", ratio_washable > ratio_proven > ratio_rock)
|
||||
test("should_warn for proven", drift_compression.should_warn({'wash_count': 15, 'rock_status': 'washable', 'text': 'x' * 300}))
|
||||
test("warn_before_compress returns dict", isinstance(drift_compression.warn_before_compress({'wash_count': 15, 'rock_status': 'washable', 'text': 'x' * 300}), dict))
|
||||
test(
|
||||
"should_warn for proven",
|
||||
drift_compression.should_warn({"wash_count": 15, "rock_status": "washable", "text": "x" * 300}),
|
||||
)
|
||||
test(
|
||||
"warn_before_compress returns dict",
|
||||
isinstance(
|
||||
drift_compression.warn_before_compress({"wash_count": 15, "rock_status": "washable", "text": "x" * 300}),
|
||||
dict,
|
||||
),
|
||||
)
|
||||
|
||||
# --- 5. Warm router ---
|
||||
print("\n--- 5. Warm router ---")
|
||||
neurons = warm_router.list_neurons()
|
||||
if neurons:
|
||||
test("neurons have wash_count", any('wash_count' in n for n in neurons))
|
||||
test("neurons have body_knowledge", any('body_knowledge' in n for n in neurons))
|
||||
test("body_knowledge populated", sum(1 for n in neurons if n.get('body_knowledge', '')) > 0)
|
||||
test("neurons have wash_count", any("wash_count" in n for n in neurons))
|
||||
test("neurons have body_knowledge", any("body_knowledge" in n for n in neurons))
|
||||
test("body_knowledge populated", sum(1 for n in neurons if n.get("body_knowledge", "")) > 0)
|
||||
else:
|
||||
test("neurons have wash_count", True, "no neurons in database (skipped)")
|
||||
test("neurons have body_knowledge", True, "no neurons in database (skipped)")
|
||||
@@ -141,19 +159,20 @@ def main():
|
||||
mc = memory_consolidation.MemoryConsolidation()
|
||||
# Check if ConsolidatedKnowledge has wash_count field
|
||||
import dataclasses
|
||||
|
||||
try:
|
||||
fields = [f.name for f in dataclasses.fields(memory_consolidation.ConsolidatedKnowledge)]
|
||||
test("consolidation has wash_count", 'wash_count' in fields)
|
||||
test("consolidation has wash_count", "wash_count" in fields)
|
||||
except Exception:
|
||||
test("consolidation has wash_count", False, "could not inspect dataclass")
|
||||
status = mc.get_status()
|
||||
test("consolidation has entries", status['knowledge_count'] > 0 or "no entries in database")
|
||||
test("consolidation has entries", status["knowledge_count"] > 0 or "no entries in database")
|
||||
|
||||
# --- 7. Curator ---
|
||||
print("\n--- 7. Curator ---")
|
||||
if curator:
|
||||
result = curator.le_promote_eligible('test', ['test'], 2, wash_count=15)
|
||||
test("curator adjusted threshold", result['eligible'])
|
||||
result = curator.le_promote_eligible("test", ["test"], 2, wash_count=15)
|
||||
test("curator adjusted threshold", result["eligible"])
|
||||
else:
|
||||
test("curator adjusted threshold", True, "curator not available in TUI (skipped)")
|
||||
|
||||
@@ -162,20 +181,20 @@ def main():
|
||||
obs = drift_observability.DriftObservability()
|
||||
vel = drift_core.compute_drift_velocity(db)
|
||||
direction = drift_core.compute_drift_direction(db)
|
||||
test("velocity computed", 'changes_per_day' in vel)
|
||||
test("direction computed", 'stability_ratio' in direction)
|
||||
test("velocity computed", "changes_per_day" in vel)
|
||||
test("direction computed", "stability_ratio" in direction)
|
||||
|
||||
# --- 9. Audit ledger ---
|
||||
print("\n--- 9. Audit ledger ---")
|
||||
chain = audit_ledger.verify_chain()
|
||||
test("chain valid", chain['valid'], chain.get('reason', ''))
|
||||
test("chain valid", chain["valid"], chain.get("reason", ""))
|
||||
|
||||
# --- 10. Subconscious ---
|
||||
print("\n--- 10. Subconscious ---")
|
||||
sp = subconscious_pass.SubconsciousPass()
|
||||
report = sp.reflect("I am frustrated", "I understand")
|
||||
test("reflection works", report['finding_count'] >= 0)
|
||||
test("emotional detection", any(f['type'] == 'emotional_signal' for f in report['findings']))
|
||||
test("reflection works", report["finding_count"] >= 0)
|
||||
test("emotional detection", any(f["type"] == "emotional_signal" for f in report["findings"]))
|
||||
|
||||
# --- 11. Retrieval with drift ---
|
||||
print("\n--- 11. Retrieval with drift ---")
|
||||
|
||||
@@ -40,24 +40,14 @@ def main() -> int:
|
||||
backup_script = (ROOT / "scripts" / "gitea-backup.sh").read_text()
|
||||
restore_script = (ROOT / "scripts" / "gitea-restore-verify.sh").read_text()
|
||||
rune_version = (ROOT / "src" / "rune" / "__init__.py").read_text()
|
||||
gitea_sources = "\n".join(
|
||||
path.read_text() for path in (ROOT / "shared").glob("gitea*.py")
|
||||
)
|
||||
gitea_sources = "\n".join(path.read_text() for path in (ROOT / "shared").glob("gitea*.py"))
|
||||
gitea_sources += (ROOT / "shared" / "custodian.py").read_text()
|
||||
|
||||
print("=== GITEA AUTOMATION CANARY ===")
|
||||
check(
|
||||
"feature issue template exists", (issue_dir / "feature.md").is_file(), failures
|
||||
)
|
||||
check("feature issue template exists", (issue_dir / "feature.md").is_file(), failures)
|
||||
check("bug issue template exists", (issue_dir / "bug.md").is_file(), failures)
|
||||
config = (
|
||||
(issue_dir / "config.yml").read_text()
|
||||
if (issue_dir / "config.yml").exists()
|
||||
else ""
|
||||
)
|
||||
check(
|
||||
"blank issues are disabled", "blank_issues_enabled: false" in config, failures
|
||||
)
|
||||
config = (issue_dir / "config.yml").read_text() if (issue_dir / "config.yml").exists() else ""
|
||||
check("blank issues are disabled", "blank_issues_enabled: false" in config, failures)
|
||||
check("documented PR template path exists", pr_template.is_file(), failures)
|
||||
check(
|
||||
"PR template requires CI gate",
|
||||
@@ -80,8 +70,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"auto-label declares every managed label",
|
||||
"managedLabels" in auto_label
|
||||
and all(f"'{label}'" in auto_label for label in managed_labels),
|
||||
"managedLabels" in auto_label and all(f"'{label}'" in auto_label for label in managed_labels),
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -91,9 +80,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"stale managed labels are removed",
|
||||
"managedLabels.has" in auto_label
|
||||
and "!desiredLabels.has" in auto_label
|
||||
and "removeLabel" in auto_label,
|
||||
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label and "removeLabel" in auto_label,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -103,9 +90,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"race contract rejects inverted 404 handling",
|
||||
not has_idempotent_label_removal(
|
||||
auto_label.replace("error.status !== 404", "error.status === 404", 1)
|
||||
),
|
||||
not has_idempotent_label_removal(auto_label.replace("error.status !== 404", "error.status === 404", 1)),
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -130,8 +115,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"backup is stored off the Pi root disk",
|
||||
"/media/" in backup_script
|
||||
and "/tmp/rune-backups" not in backup + backup_script,
|
||||
"/media/" in backup_script and "/tmp/rune-backups" not in backup + backup_script,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -158,8 +142,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"backup atomically publishes without clobber and links checksum last",
|
||||
backup_script.rfind('ln -T -- "$STAGED_CHECKSUM"')
|
||||
> backup_script.rfind('ln -T -- "$STAGED_ARCHIVE"')
|
||||
backup_script.rfind('ln -T -- "$STAGED_CHECKSUM"') > backup_script.rfind('ln -T -- "$STAGED_ARCHIVE"')
|
||||
and 'mv -f -- "$STAGED_' not in backup_script
|
||||
and '"$BACKUP_DIR/$ARCHIVE.sha256" -ef "$STAGED_CHECKSUM"' in backup_script
|
||||
and "Removing the staged checksum disarms rollback" in backup_script,
|
||||
@@ -173,14 +156,12 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"backup workflow pins audited checkout v4.2.2 commit",
|
||||
"# actions/checkout v4.2.2" in backup
|
||||
and "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" in backup,
|
||||
"# actions/checkout v4.2.2" in backup and "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" in backup,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
"restore verifier is explicitly guarded and rootless",
|
||||
"RUNE_GITEA_RESTORE_VERIFY" in restore_script
|
||||
and "EUID != 0" in restore_script,
|
||||
"RUNE_GITEA_RESTORE_VERIFY" in restore_script and "EUID != 0" in restore_script,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -197,9 +178,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"restore SQLite import denies host-file attachment semantically",
|
||||
"set_authorizer" in restore_script
|
||||
and "SQLITE_ATTACH" in restore_script
|
||||
and "SQLITE_DETACH" in restore_script,
|
||||
"set_authorizer" in restore_script and "SQLITE_ATTACH" in restore_script and "SQLITE_DETACH" in restore_script,
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -221,7 +200,7 @@ def main() -> int:
|
||||
'cp -a -- "$RESTORE_ROOT/data/." "$INSTALL_ROOT/gitea/"' in restore_script
|
||||
and restore_script.index('cp -a -- "$RESTORE_ROOT/data/."')
|
||||
< restore_script.index('cp "$DB" "$INSTALL_ROOT/gitea/gitea.db"')
|
||||
< restore_script.index("cat >\"$INSTALL_ROOT/gitea/conf/app.ini\""),
|
||||
< restore_script.index('cat >"$INSTALL_ROOT/gitea/conf/app.ini"'),
|
||||
failures,
|
||||
)
|
||||
check(
|
||||
@@ -252,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.1"' in rune_version,
|
||||
'__version__ = "0.83.5"' 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,
|
||||
}
|
||||
Loaded 100 of 462 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user