Compare commits
589
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d8e09bee6 | ||
|
|
ebc0e7edce | ||
|
|
c22bc002bc | ||
|
|
17b2e32c8c | ||
|
|
865a03d199 | ||
|
|
d913c019c5 | ||
|
|
89848faf63 | ||
|
|
216963223e | ||
|
|
2e351c71db | ||
|
|
9afa9e2911 | ||
|
|
13bdbc5d1e | ||
|
|
feeece62eb | ||
|
|
7cd222ccfa | ||
|
|
82abd1b3ab | ||
|
|
aca27ae90a | ||
|
|
4ba9c4359f | ||
|
|
6d2fb5edea | ||
|
|
7df4b237db | ||
|
|
e87a5a7561 | ||
|
|
1568d571ea | ||
|
|
ed73f36d14 | ||
|
|
2be2b5b7c1 | ||
|
|
32dda3d3bd | ||
|
|
4a8158ba06 | ||
|
|
24b09ca259 | ||
|
|
1cd766684a | ||
|
|
5e38383cfe | ||
|
|
ce638de74e | ||
|
|
59cd8a79b1 | ||
|
|
026d9b3df8 | ||
|
|
0ed2be1de5 | ||
|
|
14b0d48cad | ||
|
|
4c7ee39f61 | ||
|
|
8831126cda | ||
|
|
77714ab13d | ||
|
|
c1d9e424eb | ||
|
|
8019ff2c03 | ||
|
|
d980aeba43 | ||
|
|
7a2dec529b | ||
|
|
76430ca71f | ||
|
|
8031eed1c1 | ||
|
|
4c1b13e61a | ||
|
|
192e161998 | ||
|
|
6daf950642 | ||
|
|
d78d8d4dc4 | ||
|
|
4dee7221cb | ||
|
|
630642f499 | ||
|
|
38c9d2729c | ||
|
|
4032c753f3 | ||
|
|
30b5de31a2 | ||
|
|
03e0b8667a | ||
|
|
13a9bfe88d | ||
|
|
c091cacb61 | ||
|
|
d1f62288ab | ||
|
|
7c5310d1b2 | ||
|
|
c39f1d0b3c | ||
|
|
231d4e51f1 | ||
|
|
e82b2eb2af | ||
|
|
a977805377 | ||
|
|
9eb4a36d62 | ||
|
|
bf3ab130af | ||
|
|
8e9edeeea7 | ||
|
|
4faa1c107e | ||
|
|
bb4e753e0e | ||
|
|
52333e8ee9 | ||
|
|
1f6b6fb6aa | ||
|
|
eef7768501 | ||
|
|
81099a11f7 | ||
|
|
3c13be1d76 | ||
|
|
de9dee7dd9 | ||
|
|
c949df0e98 | ||
|
|
18c12fbda7 | ||
|
|
ee7da6d080 | ||
|
|
2fa29f408c | ||
|
|
f433efe375 | ||
|
|
be00b56a0c | ||
|
|
72d443423e | ||
|
|
f96991bf2e | ||
|
|
5dea803e92 | ||
|
|
30ec1c8153 | ||
|
|
ad7e6fef2c | ||
|
|
df9290fcff | ||
|
|
779a26eaae | ||
|
|
8111e0b3ba | ||
|
|
3f440693de | ||
|
|
1d4aa87ca4 | ||
|
|
61432632f7 | ||
|
|
5601fee02a | ||
|
|
cdca988d00 | ||
|
|
d05a9e6741 | ||
|
|
ddb7490cc6 | ||
|
|
d12fbab06a | ||
|
|
19db782906 | ||
|
|
e0d7d7bb03 | ||
|
|
5dda590588 | ||
|
|
953197823a | ||
|
|
eba579f99f | ||
|
|
9fd3d8b15d | ||
|
|
5171cc3ef8 | ||
|
|
64f77e82ff | ||
|
|
d64df3d011 | ||
|
|
4d1064449b | ||
|
|
1e9ddea492 | ||
|
|
c5ecbacf98 | ||
|
|
892fb4cafe | ||
|
|
c2a25d187b | ||
|
|
9189e5083c | ||
|
|
4c78ac0cfe | ||
|
|
d6028ca928 | ||
|
|
3aeecf5142 | ||
|
|
723836e27a | ||
|
|
a7b13099aa | ||
|
|
c205c25598 | ||
|
|
a8feb3eee4 | ||
|
|
574902926e | ||
|
|
70389268a2 | ||
|
|
3199e36cd4 | ||
|
|
df6f52e652 | ||
|
|
bd2137fc4f | ||
|
|
060a2e65ec | ||
|
|
fe6e9dfbbc | ||
|
|
360fbb5f94 | ||
|
|
fbb74b83d3 | ||
|
|
ceffe7d2e4 | ||
|
|
63bee210eb | ||
|
|
d5fba689de | ||
|
|
39d83fc388 | ||
|
|
1b5f774bac | ||
|
|
98cde56413 | ||
|
|
e267c51293 | ||
|
|
5694193959 | ||
|
|
7667c2937c | ||
|
|
b537018ab2 | ||
|
|
eac67a24da | ||
|
|
a804cb6436 | ||
|
|
883554a2db | ||
|
|
daba0e3294 | ||
|
|
c4825c2d66 | ||
|
|
02f7d1ca42 | ||
|
|
cc52bfb69f | ||
|
|
e0d0923488 | ||
|
|
47c2e56393 | ||
|
|
004d168157 | ||
|
|
87e1320175 | ||
|
|
7a27ed557c | ||
|
|
ec20649e35 | ||
|
|
303848a0fb | ||
|
|
74d6b7393b | ||
|
|
f30b62c457 | ||
|
|
38fd57846c | ||
|
|
9628587bfa | ||
|
|
5421bf931b | ||
|
|
292c511ab5 | ||
|
|
654b89d84f | ||
|
|
19b80c95a5 | ||
|
|
6ba8302bc8 | ||
|
|
9e5830432c | ||
|
|
f75c887bd7 | ||
|
|
3e1c6c0ca4 | ||
|
|
c99b9fef14 | ||
|
|
39d7083611 | ||
|
|
9ccb331a41 | ||
|
|
dc0005423b | ||
|
|
01ae71de41 | ||
|
|
eb9b3916a7 | ||
|
|
5707045f96 | ||
|
|
c0d1898826 | ||
|
|
0d3bf42e7f | ||
|
|
1ebd66bb4c | ||
|
|
e44ee897db | ||
|
|
7b21a34117 | ||
|
|
45cc070b02 | ||
|
|
497f229af1 | ||
|
|
eb897663af | ||
|
|
11b6832edd | ||
|
|
3d55f30429 | ||
|
|
a932645075 | ||
|
|
6c3b8edbbe | ||
|
|
7cecc90ac6 | ||
|
|
8d23d43ab9 | ||
|
|
4a75d14c68 | ||
|
|
6437daf606 | ||
|
|
afddbf2f65 | ||
|
|
612da55189 | ||
|
|
8b2eee07d0 | ||
|
|
a5baeb8e18 | ||
|
|
df6e0aa1cf | ||
|
|
4fa9a143af | ||
|
|
53abd24d49 | ||
|
|
32c4184f7b | ||
|
|
65638f3590 | ||
|
|
9f33784986 | ||
|
|
4f31741ac6 | ||
|
|
b198fc8dee | ||
|
|
84066eb745 | ||
|
|
85c0f1eafa | ||
|
|
edc00f9883 | ||
|
|
6f55c89677 | ||
|
|
d1c49c9d93 | ||
|
|
8752575a6e | ||
|
|
851e2a112f | ||
|
|
f65fc5d258 | ||
|
|
26e1cd8242 | ||
|
|
b9a3d2239c | ||
|
|
5bf15537d2 | ||
|
|
40a2f58fad | ||
|
|
c9dcfa2434 | ||
|
|
f8118a335e | ||
|
|
c8a85d5950 | ||
|
|
d00f567178 | ||
|
|
dc0b510de3 | ||
|
|
e5e5008339 | ||
|
|
a0f67b7682 | ||
|
|
da4e422efa | ||
|
|
208c0b3202 | ||
|
|
bb24c4387d | ||
|
|
a133fd6840 | ||
|
|
44aff75599 | ||
|
|
ce383f1fa4 | ||
|
|
6b44a3b630 | ||
|
|
fb39c6eac6 | ||
|
|
9bc4c2d5cc | ||
|
|
80130ad079 | ||
|
|
9856db2b04 | ||
|
|
1ef1722bcf | ||
|
|
fae049f649 | ||
|
|
45e2e4c43a | ||
|
|
17c249447b | ||
|
|
712046c345 | ||
|
|
54ff1c685e | ||
|
|
2ac1da854e | ||
|
|
bfe7729a2f | ||
|
|
13f6c709b5 | ||
|
|
608b0b3526 | ||
|
|
1161fa6348 | ||
|
|
f0ae1a56e0 | ||
|
|
4a9b01b128 | ||
|
|
8b61d15db7 | ||
|
|
57b6011def | ||
|
|
5b093b8ba3 | ||
|
|
02ca31003f | ||
|
|
32f252b8a0 | ||
|
|
180da9d364 | ||
|
|
97bb7c51f0 | ||
|
|
ea2ae7594c | ||
|
|
cd89123b01 | ||
|
|
f71f5f83fd | ||
|
|
832d6122fb | ||
|
|
5ac0824a89 | ||
|
|
55df1829e8 | ||
|
|
83871f707b | ||
|
|
3dd39e1320 | ||
|
|
6524be860e | ||
|
|
04e592d11b | ||
|
|
07f5d373ac | ||
|
|
cbf95ddcf3 | ||
|
|
11f51b04d5 | ||
|
|
835aa9fa50 | ||
|
|
ab1273e0b9 | ||
|
|
3f34fcd843 | ||
|
|
11c7036543 | ||
|
|
e351ca7078 | ||
|
|
63d3baf75c | ||
|
|
2b1249a4a8 | ||
|
|
59bf524aed | ||
|
|
459a35234c | ||
|
|
b3f1611a0a | ||
|
|
e367ca8df7 | ||
|
|
63d8188284 | ||
|
|
2293b91713 | ||
|
|
78861d8c05 | ||
|
|
e5b3de7565 | ||
|
|
1313e04538 | ||
|
|
83e40972ab | ||
|
|
c56ed640cb | ||
|
|
2d5e7aa6ba | ||
|
|
1fed5cf3c6 | ||
|
|
cb18492e2e | ||
|
|
73cb7847c3 | ||
|
|
975f9a8405 | ||
|
|
66d6586db6 | ||
|
|
1e11c26961 | ||
|
|
384a0d81bf | ||
|
|
8d48a33b80 | ||
|
|
d70a01aca4 | ||
|
|
a8cd17499e | ||
|
|
f8d75e9458 | ||
|
|
beec19912c | ||
|
|
6e86f9945f | ||
|
|
a17018c871 | ||
|
|
210b18cfde | ||
|
|
68b724cd5b | ||
|
|
f3f50599b7 | ||
|
|
c4b8b76315 | ||
|
|
a982f64c2c | ||
|
|
82eaefca1c | ||
|
|
87c13746b6 | ||
|
|
f54c608b3f | ||
|
|
fc9b9c08fe | ||
|
|
03704d7691 | ||
|
|
2ff32032ff | ||
|
|
6d630348ba | ||
|
|
1611d2b6a5 | ||
|
|
294eef368c | ||
|
|
20b8112ba1 | ||
|
|
9092e7d2f4 | ||
|
|
64ba7cf8f8 | ||
|
|
5befd81b23 | ||
|
|
3c79a0a9c0 | ||
|
|
79bbb5fea8 | ||
|
|
872b0c7a75 | ||
|
|
fcee8fbec7 | ||
|
|
d767bd9a9e | ||
|
|
47e6eba735 | ||
|
|
818f4b9e21 | ||
|
|
d1631a8333 | ||
|
|
f5754fe2c0 | ||
|
|
6df2580567 | ||
|
|
3ee6f59e1b | ||
|
|
3811219abc | ||
|
|
edf9d52caa | ||
|
|
8a2525c4ac | ||
|
|
fb69ae83fc | ||
|
|
d3a240edc8 | ||
|
|
278c5e372b | ||
|
|
d08621222a | ||
|
|
db25db26d1 | ||
|
|
1a321154f8 | ||
|
|
ac9765d5d1 | ||
|
|
0fdfe195e7 | ||
|
|
71d2d63a36 | ||
|
|
0873b521e3 | ||
|
|
f7e6c5cff5 | ||
|
|
fc46dbea52 | ||
|
|
d8ede8f3f8 | ||
|
|
804c2f62df | ||
|
|
e5d10f18ae | ||
|
|
3ed58d48a9 | ||
|
|
6c997b6af4 | ||
|
|
9b5befb137 | ||
|
|
6899d859da | ||
|
|
8ef70448c3 | ||
|
|
55f268f978 | ||
|
|
320026a4a3 | ||
|
|
0e88b6bc08 | ||
|
|
004e5b3118 | ||
|
|
db15ae3650 | ||
|
|
d8ce21b76d | ||
|
|
d4d43dbf67 | ||
|
|
8a6c950140 | ||
|
|
d67fc46581 | ||
|
|
84b8d6b215 | ||
|
|
7f5d57236a | ||
|
|
bda07c68ee | ||
|
|
c5be22be09 | ||
|
|
4d6fec3d17 | ||
|
|
15dbbc562a | ||
|
|
6eb5e06119 | ||
|
|
78e4750a77 | ||
|
|
c3948bae98 | ||
|
|
b8184a0f01 | ||
|
|
469b91f217 | ||
|
|
bb960b1656 | ||
|
|
38c76a0c29 | ||
|
|
3186566514 | ||
|
|
24d14ff8e1 | ||
|
|
6d0d455033 | ||
|
|
b0a682b61f | ||
|
|
ab18540d7e | ||
|
|
f489467e1b | ||
|
|
64172d8675 | ||
|
|
8e75414907 | ||
|
|
7a9d823792 | ||
|
|
98b5a73ed1 | ||
|
|
4a9d8001c1 | ||
|
|
538bbeb821 | ||
|
|
cc3b3d81eb | ||
|
|
eb01156d9f | ||
|
|
290d264f31 | ||
|
|
16c147dbee | ||
|
|
15cba0bb24 | ||
|
|
1283b0bf94 | ||
|
|
1ee35d76c9 | ||
|
|
e8daf78eaa | ||
|
|
19976c310c | ||
|
|
e2430a9154 | ||
|
|
de5445357e | ||
|
|
e94c8e27a8 | ||
|
|
0816c5997d | ||
|
|
b502a1d7e8 | ||
|
|
0cd587dfd3 | ||
|
|
8404cddb1d | ||
|
|
17ec4862e3 | ||
|
|
6b712a9b75 | ||
|
|
31f49671ec | ||
|
|
103a42103b | ||
|
|
615da6bd09 | ||
|
|
693a494884 | ||
|
|
ee653c6a4c | ||
|
|
171f3fb250 | ||
|
|
9731181a6b | ||
|
|
544c19719e | ||
|
|
d571658b5a | ||
|
|
50c997beab | ||
|
|
9e6215e605 | ||
|
|
6429c5bb49 | ||
|
|
ea34f2d5ab | ||
|
|
0eff96a335 | ||
|
|
de68444e50 | ||
|
|
d58e88da5c | ||
|
|
c730196dba | ||
|
|
c750513fab | ||
|
|
27c4bf8d78 | ||
|
|
5b8023ca2a | ||
|
|
57eaa8f383 | ||
|
|
567fd39937 | ||
|
|
817a97abd9 | ||
|
|
1e6cb4d1ba | ||
|
|
4e11aa4268 | ||
|
|
1703c25367 | ||
|
|
7cc47de6de | ||
|
|
e93315d156 | ||
|
|
be49fb32d1 | ||
|
|
c41ed97373 | ||
|
|
558401844e | ||
|
|
26e4929b10 | ||
|
|
1462262476 | ||
|
|
0bf47265c8 | ||
|
|
02a9dc4a31 | ||
|
|
0f3aeff58b | ||
|
|
ff2abcd502 | ||
|
|
b61af7a526 | ||
|
|
497d14a449 | ||
|
|
2e68b6b054 | ||
|
|
969e39f6a3 | ||
|
|
273267763e | ||
|
|
8973caae77 | ||
|
|
3450a1cb83 | ||
|
|
9cb93e6573 | ||
|
|
4b0d1d24d2 | ||
|
|
114c394741 | ||
|
|
7c68e41a63 | ||
|
|
2ac2d0aefd | ||
|
|
6f44a87894 | ||
|
|
485623dbc4 | ||
|
|
43e7f5f7d2 | ||
|
|
54d2db97b0 | ||
|
|
523a859656 | ||
|
|
9b765852b8 | ||
|
|
f1e63b703e | ||
|
|
6d40f94f18 | ||
|
|
7a869476be | ||
|
|
e5292cce7b | ||
|
|
396ae120dc | ||
|
|
01b57297b8 | ||
|
|
497db5a144 | ||
|
|
9425ccc1f4 | ||
|
|
3ed547f86f | ||
|
|
5bc837c232 | ||
|
|
08bc1a6b62 | ||
|
|
201ca05b56 | ||
|
|
5cd90f2104 | ||
|
|
c8bc454e33 | ||
|
|
b81edb2029 | ||
|
|
87a890dce3 | ||
|
|
a0821be8a5 | ||
|
|
cb137bd37a | ||
|
|
72afb1a55a | ||
|
|
c9095f5af5 | ||
|
|
b5422f0772 | ||
|
|
da36b99184 | ||
|
|
873d0025ad | ||
|
|
a9ce967557 | ||
|
|
eb3a2c681c | ||
|
|
1bc1cdeaf0 | ||
|
|
31565844d7 | ||
|
|
c08b211f0e | ||
|
|
46c5032208 | ||
|
|
92584daeef | ||
|
|
bb5e243c88 | ||
|
|
ac936a8ba9 | ||
|
|
e6c8c5444d | ||
|
|
11270cb06a | ||
|
|
f8a568714b | ||
|
|
c3f81e60ec | ||
|
|
efb99d4682 | ||
|
|
489718ab1b | ||
|
|
9db580ac6d | ||
|
|
80ed978fbf | ||
|
|
3f119e50d8 | ||
|
|
517379313b | ||
|
|
643f9aa9d1 | ||
|
|
61a5082633 | ||
|
|
9121bbd8b0 | ||
|
|
cba9045982 | ||
|
|
0c92f482a7 | ||
|
|
3120e8ae08 | ||
|
|
ea3c20e205 | ||
|
|
a93327d107 | ||
|
|
e08625f629 | ||
|
|
021fef2065 | ||
|
|
3788875410 | ||
|
|
d96b89bd4c | ||
|
|
efe3871916 | ||
|
|
026a1bd919 | ||
|
|
d9bcebb730 | ||
|
|
2b2639783a | ||
|
|
634821efd4 | ||
|
|
c6c18ae041 | ||
|
|
e03ad9aad9 | ||
|
|
89b603024e | ||
|
|
dac6e97e16 | ||
|
|
fe45c6a8dd | ||
|
|
c7b2edd4fc | ||
|
|
96f0e7e263 | ||
|
|
a168cd935e | ||
|
|
a2c4a11b0d | ||
|
|
16164c6f5e | ||
|
|
0722dc2339 | ||
|
|
a7ec1b49d7 | ||
|
|
5b61f8a8bb | ||
|
|
c730719768 | ||
|
|
4fe0f8ee46 | ||
|
|
8999f29041 | ||
|
|
c288f4726d | ||
|
|
308bbb74b7 | ||
|
|
1f01e87453 | ||
|
|
4e8e081f48 | ||
|
|
6c681b97bf | ||
|
|
d01d6eb6d4 | ||
|
|
f90695197d | ||
|
|
279c2b86cb | ||
|
|
57206b7b31 | ||
|
|
b69ffcd44f | ||
|
|
c4b4ebd0af | ||
|
|
4220621cb0 | ||
|
|
4b54451444 | ||
|
|
962e3511e1 | ||
|
|
fb809135c6 | ||
|
|
10acdfe708 | ||
|
|
fd9e1bffb8 | ||
|
|
631426763e | ||
|
|
09cd18ad54 | ||
|
|
da500df487 | ||
|
|
51416aa95e | ||
|
|
b68f31ff0d | ||
|
|
dd41f15571 | ||
|
|
57e4b8672c | ||
|
|
6923128f45 | ||
|
|
9db239fcb4 | ||
|
|
658ec63627 | ||
|
|
427c3ac6bb | ||
|
|
509ec00c87 | ||
|
|
bea523a0e3 | ||
|
|
0a15a1675d | ||
|
|
81c6d1f9c6 | ||
|
|
d53bb9a535 | ||
|
|
023151acb9 | ||
|
|
2da9305d96 | ||
|
|
2e05ea6848 | ||
|
|
be161c2043 | ||
|
|
a6539986b9 | ||
|
|
8da216d155 | ||
|
|
05f2a74c6f | ||
|
|
1c5dc557a6 | ||
|
|
a6bccc1c5d | ||
|
|
f208b5e61c | ||
|
|
aa800acbad | ||
|
|
30be2c96b8 | ||
|
|
ca00be8259 | ||
|
|
50a63e587f | ||
|
|
a54ab559cb | ||
|
|
599c3ab1c2 | ||
|
|
483bfd7944 | ||
|
|
1650b470c6 | ||
|
|
853cf3e0ae | ||
|
|
875bb792f7 | ||
|
|
f06ddb3a20 | ||
|
|
2ebd9da139 | ||
|
|
65001196d8 | ||
|
|
76bb2b97be | ||
|
|
e765f654f9 | ||
|
|
f7141200bd | ||
|
|
53b4830b08 | ||
|
|
fa1b7c4be1 | ||
|
|
246f26663d | ||
|
|
5718a3ea85 | ||
|
|
02802f321f | ||
|
|
4ce121a493 |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: land
|
||||
description: Review and land one small Rune change through the governed PR workflow.
|
||||
disable-model-invocation: true
|
||||
metadata:
|
||||
delta-action: land
|
||||
---
|
||||
|
||||
# Land one small diff
|
||||
|
||||
1. Read applicable `AGENTS.md` rules and the repo's PR/merge workflow.
|
||||
2. Confirm the issue, exact diff, target branch, and permission to publish and land.
|
||||
3. Keep one small, reversible change; preserve unrelated work.
|
||||
4. For bugs, follow `docs/bugfix-contract.md`: red test first, root cause, discharge.
|
||||
5. Run required tests, static analysis, canaries, and henxels; record results.
|
||||
6. Get independent `/review` per `docs/adversarial-review.md`; clear blockers.
|
||||
7. Inspect remotes; publish the PR to the approved forge, never `local`.
|
||||
8. Require human approval and green required CI on the exact PR head; recheck changed heads.
|
||||
9. Load `report-landed`; use the repo's merge workflow. Stop on conflicts; never force-push or bypass hooks.
|
||||
10. Verify the target commit and CI, then report the actual landing result and remaining blockers.
|
||||
@@ -15,6 +15,9 @@
|
||||
# Canary files are omitted: src/rune/canary_*.py are standalone CI
|
||||
# verification scripts executed directly by the canary job, never imported by
|
||||
# the test suite — counting them would make the global floor a fiction.
|
||||
# ffi_query_memory_smoke.py is the same class: a standalone evidence script
|
||||
# driven directly (and by tests/test_ffi_query_memory_smoke.py when the cdylib
|
||||
# is built), never imported by the suite.
|
||||
|
||||
[run]
|
||||
source = src/rune
|
||||
@@ -22,6 +25,7 @@ omit =
|
||||
tests/*
|
||||
scripts/*
|
||||
src/rune/canary_*.py
|
||||
src/rune/ffi_query_memory_smoke.py
|
||||
|
||||
[report]
|
||||
fail_under = 29
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Fix a bug or issue
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
@@ -29,10 +59,6 @@ assignees: conductor
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
|
||||
understand the goal without decoding internals. -->
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete example of the intended behavior, in plain terms:
|
||||
"If X happens, the system will Y instead of Z." -->
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
@@ -37,7 +63,6 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
@@ -69,4 +94,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
backup:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
+140
-55
@@ -7,49 +7,38 @@ on:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
# PRIORITY GATE (2026-09-04): every issue must carry exactly one of
|
||||
# P0/P1/P2/P3. An issue that lands without one gets 'unprioritized' +
|
||||
# a demand comment — filing is not complete until prioritized.
|
||||
priority-gate:
|
||||
if: github.event_name == 'issues' && github.event.action != 'labeled'
|
||||
runs-on: rune-infra-pi
|
||||
# SINGLE-PASS FILING (2026-10-07, Nick's first-time-correct directive):
|
||||
# the old design ran TWO jobs on submission — priority-gate (which saw
|
||||
# no P-label and posted the ⚠️ warning) and auto-label (which inferred
|
||||
# and applied the P-label). The gate ran BEFORE the inference that
|
||||
# would have made the warning unnecessary: every keyword-inferrable
|
||||
# submission ate a spurious warning, a flag add/remove cycle, and a
|
||||
# wasted workflow run. The fix is ORDER: infer → apply → THEN gate.
|
||||
# A submission whose priority is inferable gets its P-label in this
|
||||
# same run; the warning fires only when inference genuinely produced
|
||||
# nothing (a true filing gap, not a race the robot loses to itself).
|
||||
auto-label:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Enforce priority label
|
||||
- name: Infer, apply, then gate-check (one pass)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
if (!issue) return;
|
||||
const labels = (issue.labels || []).map(l => l.name);
|
||||
const priorities = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
const has = labels.filter(l => priorities.includes(l));
|
||||
if (has.length >= 1) {
|
||||
// Prioritized — clear the flag if it was set.
|
||||
if (labels.includes('unprioritized')) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, name: 'unprioritized',
|
||||
}).catch(e => { if (e.status !== 404) throw e; });
|
||||
}
|
||||
// CLOSED-ISSUE GUARD (#407): labels are filing-time metadata.
|
||||
// A closed issue's lifecycle has ended; re-running label
|
||||
// inference on it re-adds stale type labels (bug = currently
|
||||
// incorrect behavior — a closed fixed issue must not carry
|
||||
// it) and makes any human label cleanup self-defeating,
|
||||
// because every PUT fires a 'labeled' event that re-triggers
|
||||
// this job. Post-close label curation belongs to humans and
|
||||
// the conductor, not this workflow.
|
||||
const issueState = context.payload.issue?.state;
|
||||
if (issueState === 'closed') {
|
||||
console.log(`Issue #${context.payload.issue?.number} is closed — auto-label skips (labels are curated post-close).`);
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number, labels: ['unprioritized'],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: `⚠️ **Priority gate:** this issue has no priority. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
|
||||
auto-label:
|
||||
runs-on: rune-infra-pi
|
||||
steps:
|
||||
- name: Analyze and label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const action = context.payload.action;
|
||||
const isIssue = context.payload.issue !== undefined;
|
||||
const title = context.payload.issue?.title || context.payload.pull_request?.title || '';
|
||||
const body = context.payload.issue?.body || context.payload.pull_request?.body || '';
|
||||
const text = (title + ' ' + body).toLowerCase();
|
||||
@@ -58,9 +47,44 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'priority: high', 'bug', 'feature',
|
||||
'bug', 'feature', 'research',
|
||||
'P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low',
|
||||
]);
|
||||
|
||||
// Type label: DECLARED over GUESSED. The template spine
|
||||
// (kind: bug|feature|research|epic|...) is authoritative.
|
||||
// A recognized kind maps to its label; ANY other declared kind
|
||||
// (e.g. epic) means NO type label and NO keyword guessing —
|
||||
// epics are containers, not bugs. Keyword heuristics apply
|
||||
// only to pre-template issues with no spine at all.
|
||||
let isBug = false, isFeature = false, isResearch = false;
|
||||
const kindMatch = body && body.match(/^kind:\s*([a-z_-]+)\s*$/m);
|
||||
if (kindMatch) {
|
||||
const kind = kindMatch[1];
|
||||
isBug = kind === 'bug';
|
||||
isFeature = kind === 'feature';
|
||||
isResearch = kind === 'research';
|
||||
} else if (action === 'opened') {
|
||||
// KEYWORD FALLBACK (#407): pre-template issues only, and
|
||||
// ONLY at filing time. Re-running keyword guesses on every
|
||||
// edit/label event overrides human curation of spine-less
|
||||
// issues (documented rule: keyword heuristics are the
|
||||
// pre-template fallback, not a standing authority).
|
||||
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
isBug = textBug;
|
||||
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
|
||||
}
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
if (isResearch) {
|
||||
desiredLabels.add('research');
|
||||
}
|
||||
|
||||
// Umbrella labels
|
||||
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
|
||||
desiredLabels.add('memory');
|
||||
@@ -87,19 +111,31 @@ jobs:
|
||||
desiredLabels.add('custodian');
|
||||
}
|
||||
|
||||
// Priority labels
|
||||
if (text.includes('urgent') || text.includes('critical') || text.includes('broken')) {
|
||||
desiredLabels.add('priority: high');
|
||||
}
|
||||
|
||||
// Type labels
|
||||
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
// Priority labels (2026-10-05: PROGRAMMATIC P-ASSIGNMENT —
|
||||
// the gate demanded P0-P3 labels but nothing assigned them;
|
||||
// the dead 'priority: high' label is gone per the 2026-09-04
|
||||
// normalization). Assignment from the TITLE prefix first
|
||||
// ([P1]/P1:/priority=...), then keyword inference using the
|
||||
// gate's own rule set (the scale lives in ONE place — the
|
||||
// gate check at the bottom of THIS job).
|
||||
const pMatch = title.match(/\bP([0-3])\b/);
|
||||
const priorityLabelNames = ['P0 — critical', 'P1 — high', 'P2 — medium', 'P3 — low'];
|
||||
if (pMatch) {
|
||||
desiredLabels.add(priorityLabelNames[Number(pMatch[1])]);
|
||||
} else if (text.includes('urgent') || text.includes('critical') ||
|
||||
text.includes('spine') || text.includes('blocks other work')) {
|
||||
desiredLabels.add(priorityLabelNames[0]); // P0 — critical
|
||||
} else if (text.includes('broken') || text.includes('fix') ||
|
||||
text.includes('repair') || text.includes('defect') ||
|
||||
text.includes('real defect') || text.includes('money')) {
|
||||
desiredLabels.add(priorityLabelNames[1]); // P1 — high
|
||||
} else if (text.includes('improve') || text.includes('consolidat') ||
|
||||
text.includes('refactor') || text.includes('kernel') ||
|
||||
text.includes('port')) {
|
||||
desiredLabels.add(priorityLabelNames[2]); // P2 — medium
|
||||
} else if (text.includes('benchmark') || text.includes('measurement') ||
|
||||
text.includes('deferred')) {
|
||||
desiredLabels.add(priorityLabelNames[3]); // P3 — low
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
@@ -109,6 +145,7 @@ jobs:
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const currentLabels = new Set(current.data.map(label => label.name));
|
||||
const currentLabelIds = new Map(current.data.map(label => [label.name, label.id]));
|
||||
const staleManaged = [...currentLabels].filter(
|
||||
label => managedLabels.has(label) && !desiredLabels.has(label)
|
||||
);
|
||||
@@ -118,11 +155,14 @@ jobs:
|
||||
|
||||
for (const name of staleManaged) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
// Gitea's DELETE /labels/{id} requires the numeric label ID.
|
||||
// Octokit's GitHub-style removeLabel(name) returned 404 and
|
||||
// left stale priority labels behind (#2467: P0 AND P2).
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
name,
|
||||
id: currentLabelIds.get(name),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
@@ -140,5 +180,50 @@ jobs:
|
||||
labels: missingDesired,
|
||||
});
|
||||
}
|
||||
console.log(`Removed managed labels: ${staleManaged.join(', ') || 'none'}`);
|
||||
console.log(`Added managed labels: ${missingDesired.join(', ') || 'none'}`);
|
||||
|
||||
// ---- PRIORITY GATE (runs LAST — after inference and
|
||||
// reconciliation, so the verdict reflects the labels THIS run
|
||||
// just applied, not a pre-inference snapshot). Issue-scoped:
|
||||
// the filing contract governs issues; PRs get inferred labels
|
||||
// but are not filing units.
|
||||
if (!isIssue) {
|
||||
console.log(`PR #${issueNumber}: labels applied, gate skipped (gate is issue-scoped).`);
|
||||
return;
|
||||
}
|
||||
const reconciled = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const actual = reconciled.data;
|
||||
const activePriority = actual.filter(l => priorityLabelNames.includes(l.name));
|
||||
const flag = actual.find(l => l.name === 'unprioritized');
|
||||
if (activePriority.length === 1) {
|
||||
// Prioritized (by human or by this run's inference). Clear
|
||||
// any stale flag — no warning is ever posted on this path.
|
||||
if (flag) {
|
||||
await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{id}', {
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, id: flag.id,
|
||||
});
|
||||
}
|
||||
console.log(`Priority gate: satisfied (${activePriority[0].name}).`);
|
||||
return;
|
||||
}
|
||||
// Genuinely unprioritized: inference produced nothing AND the
|
||||
// caller set nothing. Flag + demand comment — the filing gap
|
||||
// is real, not a race artifact.
|
||||
const reason = activePriority.length === 0
|
||||
? 'no priority'
|
||||
: `${activePriority.length} priority labels (exactly one required)`;
|
||||
if (!flag) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber, labels: ['unprioritized'],
|
||||
});
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `⚠️ **Priority gate:** this issue has ${reason}. Assign exactly one of **P0 — critical** (spine/correctness/blocks), **P1 — high** (material value or real defect), **P2 — medium** (quality/consolidation), **P3 — low** (measurement/deferred). An issue without a priority is not filed.`,
|
||||
});
|
||||
console.log(`Priority gate: flagged — ${reason}.`);
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update-gitea:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Check current Gitea version
|
||||
id: current
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -131,8 +131,12 @@ jobs:
|
||||
"pyproject.toml": [(f'version = "{VERSION}"', f'version = "{NEW}"')],
|
||||
}
|
||||
|
||||
# Fetch current master content, apply replacements, VERIFY all five
|
||||
# surfaces agree on NEW before committing anything (TUI gate).
|
||||
# Fetch current master content, apply replacements, VERIFY all six
|
||||
# surface patterns (five files + the TUI footer) agree on NEW before
|
||||
# committing anything (TUI gate). The footer check was added after
|
||||
# the 0.89.0 half-bump: a stale RELEASE_VERSION made both replace
|
||||
# pairs no-op; the gate's RECOVER regex fixed the header only and
|
||||
# the footer silently stayed 0.88.4, failing the banner test.
|
||||
edited = {}
|
||||
for path, pairs in surfaces.items():
|
||||
status, cur = api(f"/contents/{path}?ref=master")
|
||||
@@ -151,6 +155,7 @@ jobs:
|
||||
checks = [
|
||||
("src/rune/__init__.py", rf'__version__ = "({VER})"'),
|
||||
("src/rune/rune_tui.py", rf"Version: v({VER})"),
|
||||
("src/rune/rune_tui.py", rf"Rune v({VER}) \|"),
|
||||
("src/rune/canary_gitea_automation.py", rf"'__version__ = \"({VER})\"'"),
|
||||
("pyproject.toml", rf'version = "({VER})"'),
|
||||
("README.md", rf"v({VER})"),
|
||||
@@ -223,7 +228,15 @@ jobs:
|
||||
# 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.
|
||||
# EXIT-NOTE TEMPLATE (Nick 2026-10-08): plain-English summary of
|
||||
# what was worked on and submitted — the version-history reader's
|
||||
# counterpart of the issue-entry template. Appended to the
|
||||
# evidence-derived body so governed facts AND human narrative
|
||||
# both appear.
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
EXIT_NOTE=$(python3 scripts/release_exit_note.py --since "$LAST_TAG" --to HEAD) || EXIT_NOTE=""
|
||||
python3 scripts/release_history_cli.py init \
|
||||
--version "$RELEASE_VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store store/scaffold/current.json
|
||||
--store store/scaffold/current.json \
|
||||
${EXIT_NOTE:+--exit-note "$EXIT_NOTE"}
|
||||
@@ -63,10 +63,32 @@ jobs:
|
||||
- name: Structural contract (henxels)
|
||||
run: |
|
||||
.ci-venv/bin/henxels check --all --plain
|
||||
# ABSOLUTE SCAFFOLDING LAW (2026-10-07): every Go/Rust implementation
|
||||
# unit must map to a construction contract in store/scaffold/. New
|
||||
# units without a contract fail the build — open the contract BEFORE
|
||||
# the commit lands, never after.
|
||||
- name: Scaffolding coverage (absolute law)
|
||||
run: |
|
||||
python3 scripts/scaffolding_coverage.py
|
||||
|
||||
unit-tests:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The unit shim battery loads the cdylib (ep_loader); point it at the
|
||||
# runner-owned cdylib FILE from the crate-build step below (the override
|
||||
# expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
CC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcognition_core.so
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -102,6 +124,26 @@ jobs:
|
||||
fi
|
||||
python3 scripts/umbrella_impact.py check \
|
||||
--base "$(git merge-base origin/master HEAD)" --head HEAD ${BODY_FILE:+--body-file "$BODY_FILE"}
|
||||
- name: Build Rust evidence_provenance crate
|
||||
run: |
|
||||
# tests/test_evidence_provenance_shims.py drives the cdylib through
|
||||
# the shim layer. Toolchain law (see the canary job for the full
|
||||
# rationale): system rustc is broken on this host (LLVM_23.1
|
||||
# version-node symbol gap); use the healthy rustup toolchain, PATH
|
||||
# first so the rustup shim can't exec the broken system rustc.
|
||||
# Runner-owned /tmp target dir (the repo's target/ is nick's).
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
- name: Test with coverage (global floor + changed-lines ratchet)
|
||||
run: |
|
||||
# #149 coverage ratchet — both floors live in .coveragerc; raise,
|
||||
@@ -113,12 +155,143 @@ jobs:
|
||||
.test-venv/bin/coverage xml
|
||||
.test-venv/bin/diff-cover coverage.xml --compare-branch origin/master --fail-under=70 --diff-range-notation ..
|
||||
|
||||
go-surface:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Verify Go toolchain version pin
|
||||
run: |
|
||||
# H0 pin: the Go surface builds against the pacman toolchain; the
|
||||
# pin file is the single source of truth so a distro upgrade that
|
||||
# changes behavior fails loud here, not downstream in the canary.
|
||||
WANT=$(cat go-version.txt)
|
||||
GOT=$(go version | awk '{print $3}' | sed 's/^go//' | cut -d- -f1)
|
||||
echo "pinned: $WANT | toolchain: $GOT"
|
||||
[ "$GOT" = "$WANT" ] || { echo "::error::Go toolchain drift: want $WANT, got $GOT (update go-version.txt with the rollout)"; exit 1; }
|
||||
- name: build Survivor staticlibs in-tree (for cgo main packages)
|
||||
run: |
|
||||
# Survivor main packages link Rust staticlibs via ${SRCDIR} paths
|
||||
# (adaptive-router → libroute_risk_flags.a). Those paths resolve
|
||||
# only when the crate target dir is IN-TREE, so this build must
|
||||
# NOT set CARGO_TARGET_DIR (the redirected builds below are for
|
||||
# the FFI-loader canaries and stay as they are).
|
||||
# Every Survivor-linked crate must have its .a in-tree before the
|
||||
# go loop: adaptive-router → route_risk_flags; cognition →
|
||||
# cognition_core. Add crates here as Survivor packages grow.
|
||||
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/construction_interface/Cargo.toml
|
||||
- name: prune stale /tmp cargo target dirs (tmpfs guard)
|
||||
run: |
|
||||
# /tmp is a 32G tmpfs; every CI run leaves a ~218MB
|
||||
# rune-ci-target-<run_id> dir. Without pruning, 300+ runs filled
|
||||
# the tmpfs (2026-10-07: venv creation failed disk-full across
|
||||
# every job). Keep the 3 newest; drop the rest.
|
||||
cd /tmp && ls -dt rune-ci-target-* 2>/dev/null | tail -n +4 | xargs -r rm -rf
|
||||
- name: go vet + go test + go build
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
if [ -f "$dir/go.mod" ]; then
|
||||
echo "=== Building $dir ==="
|
||||
cd "$dir"
|
||||
go vet ./...
|
||||
go test ./... 2>/dev/null || true
|
||||
OUT_DIR="${GITHUB_WORKSPACE:-$PWD}/bins"
|
||||
mkdir -p "$OUT_DIR"
|
||||
go build -o "$OUT_DIR/go-$(basename "$dir")" .
|
||||
cd - > /dev/null
|
||||
fi
|
||||
done
|
||||
- name: adaptive-router parity gate (HARD — oracle replay, not swallowable)
|
||||
run: |
|
||||
cd src/go/adaptive-router
|
||||
go test ./... -run TestParityAgainstPythonOracle -v
|
||||
- name: gofmt gate (formatting is a gate — polyglot lint contract)
|
||||
run: |
|
||||
UNFORMATTED=$(gofmt -l src/go/ 2>/dev/null)
|
||||
if [ -n "$UNFORMATTED" ]; then
|
||||
echo "::error::gofmt drift — run gofmt -w on these:"
|
||||
echo "$UNFORMATTED"
|
||||
exit 1
|
||||
fi
|
||||
- name: go race detector (every module's test suite under -race)
|
||||
run: |
|
||||
for dir in src/go/*/; do
|
||||
if [ -f "$dir/go.mod" ]; then
|
||||
cd "$dir"
|
||||
go test -race ./... 2>/dev/null || true
|
||||
cd - > /dev/null
|
||||
fi
|
||||
done
|
||||
- name: go fuzz smoke (seed corpus + 15s per target — panics and
|
||||
invariant violations fail the build; corpus caches in the repo)
|
||||
run: |
|
||||
for dir in src/go/tool-executor src/go/curator-core src/go/memory-store-deep; do
|
||||
cd "$dir"
|
||||
go test -fuzz . -fuzztime 15s ./... 2>&1 | tail -2
|
||||
cd - > /dev/null
|
||||
done
|
||||
|
||||
rust-surface:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: rustup clippy + rustfmt components
|
||||
run: |
|
||||
rustup component add clippy rustfmt 2>/dev/null || true
|
||||
cargo clippy --version
|
||||
cargo fmt --version
|
||||
- name: cargo fmt --check (formatting is a gate)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
(cd "$crate" && cargo fmt --check) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
- name: cargo clippy (warnings are errors — first linting the Rust
|
||||
surface has EVER had; pre-existing debt tracked in the crate-local
|
||||
lib.rs allow list, NOT a global opt-out)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
echo "=== clippy $(basename "$crate") ==="
|
||||
(cd "$crate" && cargo clippy --quiet -- -D warnings) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
- name: cargo test (lights up when the first #[test] lands)
|
||||
run: |
|
||||
FAIL=0
|
||||
for crate in src/rust/*/; do
|
||||
if [ -f "$crate/Cargo.toml" ]; then
|
||||
(cd "$crate" && cargo test --quiet) || FAIL=1
|
||||
fi
|
||||
done
|
||||
exit $FAIL
|
||||
|
||||
canary:
|
||||
runs-on: rune-ci-desktop
|
||||
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
|
||||
env:
|
||||
RUNE_HOME: /tmp/rune-ci-${{ github.run_id }}
|
||||
RUNE_MEMORY_DIR: /tmp/rune-ci-${{ github.run_id }}/memory
|
||||
# The build emits to a runner-owned /tmp target dir (the repo's target/
|
||||
# is nick-owned); EP_RUST_LIB points ep_loader at the cdylib FILE there
|
||||
# (the override expects a file path, not a directory).
|
||||
EP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libevidence_provenance.so
|
||||
SP_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/librune_spine.so
|
||||
MS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_scoring.so
|
||||
CS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libcapability_scoring.so
|
||||
TC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libtoken_counting.so
|
||||
SC_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libscaffold_core.so
|
||||
MEMS_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_storage.so
|
||||
MR_RUST_LIB: /tmp/rune-ci-target-${{ github.run_id }}/release/libmemory_retrieval.so
|
||||
steps:
|
||||
# actions/checkout v4.2.2, pinned to the audited upstream tag commit.
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -130,6 +303,35 @@ jobs:
|
||||
python3 -m venv .canary-venv
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -r requirements-ci.txt
|
||||
.canary-venv/bin/pip install --disable-pip-version-check -e . --no-deps
|
||||
- name: Build Rust evidence_provenance crate
|
||||
run: |
|
||||
# The canary pair + the unit shim battery drive the COMPILED surfaces
|
||||
# (binary CLI parity, ep_loader FFI, ctypes cdylib loads).
|
||||
#
|
||||
# Toolchain law (troubleshooting history): the SYSTEM rustc is broken
|
||||
# on this host — librustc_driver-<sha>.so (pacman rust 1:1.99.0-1)
|
||||
# references std::string::_M_mutate under the LLVM_23.1 version node,
|
||||
# but /usr/lib/libLLVM.so.23.1 does not define that narrow-char
|
||||
# symbol under that node (it exports _M_create/_M_assign there, and
|
||||
# only the WIDE-char _M_mutate) — every compile dies with `symbol
|
||||
# lookup error` while `cargo --version` still prints. The rustup
|
||||
# toolchain under /home/nick/.rustup is healthy and ships its own
|
||||
# libLLVM, and /home/nick carries the x-ACL for this runner user.
|
||||
# Point PATH at the toolchain bin dir (bypasses the rustup shim,
|
||||
# which would exec the broken system rustc via RUSTUP_TOOLCHAIN).
|
||||
# Target dir is runner-owned (/tmp): the repo's target/ is nick's.
|
||||
export PATH="/home/nick/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="/tmp/rune-ci-target-${{ github.run_id }}"
|
||||
mkdir -p "$CARGO_TARGET_DIR"
|
||||
cargo build --release --manifest-path src/rust/evidence_provenance/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/rune_spine/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/capability_scoring/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/token_counting/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/scaffold_core/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_storage/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/memory_retrieval/Cargo.toml
|
||||
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
|
||||
- name: Run canary suite
|
||||
run: |
|
||||
FAILED=0
|
||||
@@ -156,6 +358,21 @@ jobs:
|
||||
src/rune/canary_p1_06.py \
|
||||
src/rune/canary_p1_01.py \
|
||||
src/rune/canary_query_memory.py \
|
||||
src/rune/canary_rune_entry.py \
|
||||
src/rune/canary_spine_meta.py \
|
||||
src/rune/canary_audit_ledger.py \
|
||||
src/rust/rune_spine/canary_spine_rust.py \
|
||||
src/rune/canary_rune_serve.py \
|
||||
src/rune/canary_tui_transport.py \
|
||||
src/rust/memory_scoring/canary_memory_scoring.py \
|
||||
src/rust/capability_scoring/canary_capability_scoring.py \
|
||||
src/rust/token_counting/canary_token_counting.py \
|
||||
src/rust/scaffold_core/canary_scaffold_core.py \
|
||||
src/rust/memory_storage/canary_memory_storage.py \
|
||||
src/rust/memory_retrieval/canary_memory_retrieval.py \
|
||||
src/rust/cognition_core/canary_growth_pins.py \
|
||||
src/rust/cognition_core/canary_persona.py \
|
||||
src/rust/cognition_core/canary_cognition_core.py \
|
||||
src/rune/canary_unbounded.py \
|
||||
src/rune/canary_full_system.py \
|
||||
src/rune/canary_native.py; do
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint-and-format:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint Python files
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
ruff format shared/ src/ || true
|
||||
|
||||
canary-suite:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run all canaries
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
exit $FAILED
|
||||
|
||||
best-practices:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check best practices
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX" shared/ src/ --include="*.py" | head -10 || echo " Clean"
|
||||
|
||||
documentation-check:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check documentation coverage
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
# push-triggered safety net in auto-version.yml. Detects master/tag drift
|
||||
# and files a deduped issue — it never auto-releases without the PR flow.
|
||||
release-drift-reconciliation:
|
||||
runs-on: rune-infra-pi
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -14,3 +14,4 @@ state/
|
||||
# Runtime artifacts (cwd-local event journal + capability index)
|
||||
index/
|
||||
journal/
|
||||
src/rust/*/target/
|
||||
@@ -26,6 +26,9 @@ Only use `git commit --no-verify` in a genuine emergency: it bypasses the hooks
|
||||
- Every workflow is registered in the pipeline registry — new pipeline machinery ships with documentation (in ./.gitea/workflows/*.yml)
|
||||
- Every TUI surface is registered — no silent UI growth (in ./src/rune/*tui*.py)
|
||||
- New rune modules ship with tests or a canary — no ungoverned code surfaces (in ./src/rune/*.py)
|
||||
- Rust crate files stay under a line budget (in ./src/rust/*/src/*.rs)
|
||||
- Go modules stay under a line budget (in ./src/go/**/*.go)
|
||||
- No secrets in Rust or Go source files (in ./src/rust/*/src/*.rs, ./src/go/**/*.go)
|
||||
- PRDs are living documents — their generated state sections regenerate identically from governed sources
|
||||
- PRD staleness is a CI failure — docs rot is silent failure (in ./scripts/gen_prd_state.py)
|
||||
- The adversarial review procedure is binding — dispatch templates must reference it (in ./docs/adversarial-review.md)
|
||||
@@ -58,3 +61,26 @@ fix, full pipeline re-entry (suite + ratchet + ruff + canaries + henxels;
|
||||
gates for anything beyond a mechanical minor), regression tests frozen forever,
|
||||
discharge recorded finding → test → commit. Symptom patches are rejected at
|
||||
review. To escape: edit `henxels.yaml` or this file — visibly.
|
||||
|
||||
## Issue organisation standard (binding, 2026-09-19)
|
||||
|
||||
One open note per idea. Issues use the template spine (kind, subsystem,
|
||||
version intent) with a plain-English heading that restates the subject —
|
||||
no meta-labels. Before minting a standalone issue, check whether the work
|
||||
belongs inside an existing parent:
|
||||
|
||||
- **Parked ideas under a blocked/future epic** are filed as checklist items
|
||||
in the parent's "Absorbed children" section, NOT as standalone issues.
|
||||
Absorption is reversible — re-mint standalone when the idea becomes live.
|
||||
- **Live, dependency-tracked, or starting work** gets a standalone issue.
|
||||
- **Closures state the reason as data**: `closed_because` in the spine
|
||||
(completed / superseded-by-#N / absorbed-into-#N / wont-do / duplicate),
|
||||
plus a standard closing comment (plain summary first).
|
||||
- The reconciliation checker (`scripts/gitea_reconciliation_check.py`) must
|
||||
stay green after any issue state change; the artifact is updated in the
|
||||
same change that moves issues.
|
||||
|
||||
## Async continuation final flow proofs (R13/M9) — 2026-09-19
|
||||
|
||||
PR-flow live proof marker: harmless documentation addition exercising the
|
||||
canonical single-continuation path on the PC-local forge. No code change.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.83.5**
|
||||
**Current release: v0.136.3**
|
||||
|
||||
**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,196 @@
|
||||
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
|
||||
with inotify wake. Replaces FIFO+lease for process-wait delivery.
|
||||
|
||||
DESIGN:
|
||||
Producer (bridge/daemon/notify):
|
||||
1. Validates operation identity
|
||||
2. Atomically writes a completion file:
|
||||
resumes/<operation_id>.done
|
||||
containing the event/verdict JSON (write to tmp, rename for atomicity)
|
||||
|
||||
Waiter (ci_event_wait / any continuation consumer):
|
||||
1. Checks if the completion file already exists (event-before-waiter)
|
||||
2. If not: registers inotify watch on the resumes/ directory
|
||||
3. Re-checks after watch registration (closes the registration race)
|
||||
4. Blocks in kernel on inotify — zero CPU, zero timer
|
||||
5. On file-created event: reads the file, verifies authority, returns
|
||||
|
||||
The event is a WAKE SIGNAL. Authority still decides terminal truth.
|
||||
The completion file is durable — survives daemon and waiter restarts.
|
||||
|
||||
WHY NOT FIFO:
|
||||
FIFO is one-shot (token consumed), requires lease management, and
|
||||
fails under session-busy guards. A completion file is atomic, durable,
|
||||
and works across restarts. inotify wakes the process with zero CPU
|
||||
cost and zero latency between file creation and wake.
|
||||
|
||||
WHY NOT POLLING:
|
||||
Polling introduces an arbitrary interval. inotify is a kernel callback
|
||||
that fires exactly when the filesystem changes — the latency tracks
|
||||
the real completion event, not a configured interval.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _wr_bridge() -> Path:
|
||||
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
|
||||
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
|
||||
# and any isolated test root always resolve the SAME directory.
|
||||
root = os.environ.get("WR_ROOT") or os.environ.get("RUNE_WR_ROOT") or "/mnt/workdrive/wr-bridge"
|
||||
return Path(root)
|
||||
|
||||
|
||||
def _resumes_dir() -> Path:
|
||||
return _wr_bridge() / "resumes"
|
||||
|
||||
|
||||
# inotify constants (Linux)
|
||||
IN_CREATE = 0x100
|
||||
IN_CLOSE_WRITE = 0x8
|
||||
IN_MOVED_TO = 0x80
|
||||
|
||||
|
||||
def completion_path(operation_id: str) -> Path:
|
||||
return _resumes_dir() / f"{operation_id}.done"
|
||||
|
||||
|
||||
def write_completion(operation_id: str, event: dict) -> Path:
|
||||
"""Atomically write a durable completion file. The rename(2) ensures
|
||||
the waiter never sees a partial file."""
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
target = completion_path(operation_id)
|
||||
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
|
||||
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
|
||||
os.rename(str(tmp), str(target)) # atomic on same filesystem
|
||||
os.chmod(str(target), 0o666) # cross-user bridge
|
||||
return target
|
||||
|
||||
|
||||
def read_completion(operation_id: str) -> dict | None:
|
||||
"""Read a durable completion file. Returns None if absent."""
|
||||
p = completion_path(operation_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def completion_exists(operation_id: str) -> bool:
|
||||
return completion_path(operation_id).is_file()
|
||||
|
||||
|
||||
def _inotify_init_and_add_watch(directory: str) -> int:
|
||||
"""Create an inotify instance and add a watch on the directory.
|
||||
Returns the inotify fd."""
|
||||
libc = __import__("ctypes").CDLL(None, use_errno=True)
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
raise OSError("inotify_init failed")
|
||||
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
|
||||
mask = IN_CREATE | IN_MOVED_TO
|
||||
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
|
||||
if wd < 0:
|
||||
os.close(fd)
|
||||
raise OSError(f"inotify_add_watch failed for {directory}")
|
||||
return fd
|
||||
|
||||
|
||||
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
|
||||
"""Read pending inotify events. Returns list of filenames that were
|
||||
created/moved-to. Blocks until at least one event or timeout."""
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
events = []
|
||||
buf = os.read(fd, 4096)
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
|
||||
pos += 16
|
||||
name = buf[pos : pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
|
||||
pos += name_len
|
||||
if mask & (IN_CREATE | IN_MOVED_TO):
|
||||
events.append(name)
|
||||
return events
|
||||
|
||||
|
||||
def wait_for_completion(operation_id: str, *, timeout_secs: float | None = None, poll_callback=None) -> dict | None:
|
||||
"""Block until a completion file exists for this operation.
|
||||
|
||||
Uses inotify for zero-latency, zero-CPU wake. Falls back to
|
||||
polling_callback (if provided) when inotify is unavailable.
|
||||
|
||||
The completion file is a WAKE SIGNAL. The caller must still verify
|
||||
canonical authority after reading it.
|
||||
|
||||
Returns the event dict from the completion file, or None on timeout.
|
||||
"""
|
||||
target = completion_path(operation_id)
|
||||
|
||||
# 1. check-before-watch (event may have arrived before we started)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
|
||||
except OSError:
|
||||
# inotify unavailable: fall back to polling callback
|
||||
if poll_callback:
|
||||
return poll_callback(operation_id, timeout_secs)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 2. re-check after watch registration (closes the race between
|
||||
# the initial check and the watch being active)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
# 3. block in kernel on inotify — zero CPU, exact latency
|
||||
import select as _select
|
||||
|
||||
poll = _select.poll()
|
||||
poll.register(fd, _select.POLLIN)
|
||||
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
|
||||
|
||||
while True:
|
||||
remaining = None
|
||||
if deadline is not None:
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
if remaining <= 0.1:
|
||||
return None # timeout
|
||||
events = poll.poll(remaining * 1000 if remaining else None)
|
||||
if not events:
|
||||
continue # poll timeout, re-check
|
||||
names = _inotify_read(fd)
|
||||
if target.name in names:
|
||||
# 4. file appeared — read it
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
# partial/corrupt: keep watching
|
||||
finally:
|
||||
os.close(fd)
|
||||
return None
|
||||
|
||||
|
||||
def clear_completion(operation_id: str) -> None:
|
||||
"""Remove a consumed completion file (idempotent cleanup)."""
|
||||
p = completion_path(operation_id)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
Executable
+799
@@ -0,0 +1,799 @@
|
||||
#!/usr/bin/env python3
|
||||
"""daemon.py — Gitea webhook receiver + reconciliation + resume driver (v0.2).
|
||||
|
||||
W/R-v0.2 invariants implemented here:
|
||||
A. SINGLE-FLIGHT PER SESSION — per-session lease (waitstore.acquire_lease,
|
||||
/proc-validated, atomic) + in-process inflight set. A second READY wait
|
||||
for the same session stays queued until the lease releases.
|
||||
B. BUSY SESSION PROTECTION — before spawning, /proc is scanned for a live
|
||||
`mimo run -s <sid>`-shaped process not owned by our lease. Found → skip
|
||||
launch (wait stays READY). No invented MiMo APIs; the lease is the
|
||||
conservative floor against W/R-created concurrency.
|
||||
C. ATOMIC SESSION LEASE — persisted {session_id, wait_id, pid, pgid,
|
||||
pid_starttime, acquired_at, state}; O_EXCL create, steal only on /proc
|
||||
evidence of a dead owner.
|
||||
D. PROCESS TREE OWNERSHIP — workers spawn with start_new_session=True (own
|
||||
process group). Watch timeout ⇒ SIGTERM group → bounded grace → SIGKILL
|
||||
group → verify zero surviving group members → release lease → truthful
|
||||
READY_TO_RETRY (never terminal-on-timeout).
|
||||
E. TIMEOUT IS NOT SUCCESS — rc==0 maps to verdict terminal
|
||||
(COMPLETED/FAILED); rc!=0 or unverifiable ⇒ RESUME_UNVERIFIED
|
||||
(not schedulable, operator-requeueable); timeout ⇒ READY_TO_RETRY.
|
||||
F. STALE RESUMING RECOVERY — on startup, every RESUMING record is
|
||||
inspected: live lease owner ⇒ adopted and monitored; otherwise ⇒
|
||||
RESUME_UNVERIFIED (+ stale lease released). Never auto-relaunched.
|
||||
G. MULTIPLE WAITS, SAME SESSION — schedulable() orders oldest-first; one
|
||||
continuation finishes before the next starts.
|
||||
H. PIDFILE CORRECTNESS — singleton enforced by flock on daemon.pid; the
|
||||
lock dies with the process, so stale pidfiles (incl. SIGKILL) cannot
|
||||
admit a second daemon.
|
||||
|
||||
The resume command is configurable (WR_RESUME_CMD, JSON list with
|
||||
{session_id}/{msg} placeholders) so the test harness can use a stub; the
|
||||
default is the real mechanism: `mimo run -s <sid> -m <model> <msg>`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, signal, subprocess, sys, threading, time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import waitstore
|
||||
import verify
|
||||
import completion_file # REAL_ASYNC_BRIDGE R3: durable completion-file delivery
|
||||
|
||||
RESUME_MODEL = "Verboo Code/glm-5.3-flash"
|
||||
RECONCILE_SECS = int(os.environ.get("WR_RECONCILE_SECS", "60"))
|
||||
RECONCILE_EVERY_TICKS = max(1, int(os.environ.get("WR_RECONCILE_TICKS", "3")))
|
||||
WAKE_MIN_AGE_SECS = float(os.environ.get("WR_WAKE_MIN_AGE_SECS", "5"))
|
||||
WAKE_RETRIES = int(os.environ.get("WR_WAKE_RETRIES", "3"))
|
||||
WAKE_RETRY_SECS = float(os.environ.get("WR_WAKE_RETRY_SECS", "4"))
|
||||
DRIVE_LOOP_SECS = int(os.environ.get("WR_DRIVE_SECS", "20"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
WATCH_SECS = int(os.environ.get("WR_WATCH_SECS", "600"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
ORPHAN_GRACE_SECS = int(os.environ.get("WR_ORPHAN_GRACE_SECS", str(3 * WATCH_SECS)))
|
||||
TERM_GRACE_SECS = int(os.environ.get("WR_TERM_GRACE_SECS", "10"))
|
||||
MAX_ATTEMPTS = int(os.environ.get("WR_MAX_ATTEMPTS", "3"))
|
||||
_WR_ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
PIDFILE = _WR_ROOT / "daemon.pid"
|
||||
LOG = _WR_ROOT / "daemon.log"
|
||||
|
||||
DEFAULT_RESUME_CMD = ["mimo", "run", "-s", "{session_id}", "-m", RESUME_MODEL, "{msg}"]
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
with open(LOG, "a") as f:
|
||||
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
|
||||
|
||||
|
||||
def resume_cmd(session_id: str, msg: str, wait_id: str = "", override: list | None = None) -> list[str]:
|
||||
"""T15 §15D: per-wait override wins; placeholders {session_id}/{msg}/
|
||||
{wait_id}. A synthetic controller session (Scaffolding stage driver)
|
||||
is not a MiMo session — the global default cannot resume it, which is
|
||||
exactly why fixed-sleep polling crept back into T12-T14."""
|
||||
if override:
|
||||
return [
|
||||
c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in override
|
||||
]
|
||||
tmpl = os.environ.get("WR_RESUME_CMD")
|
||||
cmd = json.loads(tmpl) if tmpl else DEFAULT_RESUME_CMD
|
||||
return [c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in cmd]
|
||||
|
||||
|
||||
# ── process-tree evidence & ownership ─────────────────────────────────────
|
||||
|
||||
|
||||
def pgid_members(pgid: int) -> list[int]:
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
stat = (d / "stat").read_text()
|
||||
fields = stat.rsplit(")", 1)[1].split()
|
||||
# zombies: killed but not yet reaped. Counting them makes the
|
||||
# survivor check report the direct child we just KILLed before
|
||||
# communicate() reaps it (observed live 2026-09-12).
|
||||
if fields[0] == "Z":
|
||||
continue
|
||||
if int(fields[2]) == pgid:
|
||||
out.append(int(d.name))
|
||||
except (OSError, IndexError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def terminate_tree(pgid: int) -> list[int]:
|
||||
"""TERM → bounded grace → KILL. Returns surviving members (must be [])."""
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
deadline = time.time() + TERM_GRACE_SECS
|
||||
while time.time() < deadline:
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for _ in range(50): # up to 10s for KILL to land
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
return pgid_members(pgid)
|
||||
|
||||
|
||||
def stray_worker_pids(session_id: str, exclude_pgids: set[int] = ()) -> list[int]:
|
||||
"""Live `mimo run -s <sid>`-shaped processes not owned by our workers."""
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
parts = [p.decode(errors="replace") for p in (d / "cmdline").read_bytes().split(b"\0") if p]
|
||||
except OSError:
|
||||
continue
|
||||
if not parts:
|
||||
continue
|
||||
joined = " ".join(parts)
|
||||
if session_id not in joined or "-s" not in parts:
|
||||
continue
|
||||
if "mimo" not in joined.lower() and "@mimo-ai" not in joined.lower():
|
||||
continue
|
||||
try:
|
||||
if os.getpgid(int(d.name)) in exclude_pgids:
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
out.append(int(d.name))
|
||||
return out
|
||||
|
||||
|
||||
# ── verification → transition (unchanged authority model) ─────────────────
|
||||
|
||||
|
||||
def _authoritative_verify(rec: dict) -> tuple[str, dict]:
|
||||
"""Authoritative verification with a deterministic test seam.
|
||||
|
||||
Production (WR_VERIFY_STUB unset): real Gitea API via verify.py — the
|
||||
ONLY source of success/failure truth. Test (WR_VERIFY_STUB=path to
|
||||
JSON {"<run_ref-or-sha>": verdict, "default": verdict}): stubbed
|
||||
verdicts so invariant tests never touch the network. The stub is a
|
||||
test fixture, not an alternate authority — no production path sets it.
|
||||
"""
|
||||
stub_path = os.environ.get("WR_VERIFY_STUB", "")
|
||||
if stub_path:
|
||||
try:
|
||||
spec = json.loads(Path(stub_path).read_text())
|
||||
except Exception:
|
||||
return "pending", {"stub_error": "unreadable stub"}
|
||||
key = str(rec.get("run_ref", "") or rec.get("sha", ""))
|
||||
verdict = spec.get(key, spec.get("default", "pending"))
|
||||
return verdict, {"stub": True, "key": key}
|
||||
import verify
|
||||
|
||||
if rec.get("wait_kind") == "RELEASE_DEPLOY":
|
||||
return verify.verify_release_deploy(rec.get("sha", ""), rec.get("repo_root", ""), rec.get("deployed_link", ""))
|
||||
return verify.verify_run(rec.get("run_ref", ""), rec.get("sha", ""), rec.get("expected", "success"))
|
||||
|
||||
|
||||
def try_transition(rec: dict, event: dict | None = None) -> dict:
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
return rec
|
||||
if rec.get("wait_kind") == "PROCESS_EVENT":
|
||||
# T15 §15D: the matching event IS the authority. The notifier
|
||||
# wrapper observed the real process exit; exit_code decides the
|
||||
# verdict. No Gitea lookup applies. A duplicate event never
|
||||
# reaches here (match applies only to WAITING_EXTERNAL waits).
|
||||
if not event:
|
||||
return rec # no event → nothing authoritative happened
|
||||
try:
|
||||
exit_code = int(event.get("exit_code"))
|
||||
except (TypeError, ValueError):
|
||||
exit_code = None
|
||||
if exit_code is None:
|
||||
return rec # malformed event: stay WAITING (15E hostile case)
|
||||
verdict = "success" if exit_code == 0 else "failure"
|
||||
evidence = {"process_event": True, "exit_code": exit_code, "operation_id": event.get("run_ref", "")}
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = verdict
|
||||
if verdict == "failure":
|
||||
rec["failure_evidence"] = evidence
|
||||
rec["process_event_evidence"] = evidence
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
verdict, evidence = _authoritative_verify(rec)
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
if verdict == "success":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "success"
|
||||
elif verdict == "failure":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "failure"
|
||||
rec["failure_evidence"] = verify.failed_job_evidence(rec.get("run_ref", ""))
|
||||
else:
|
||||
return rec # pending/unknown → stay WAITING
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
def continuation_msg(rec: dict) -> str:
|
||||
c = rec["continuation"]
|
||||
if rec.get("verdict") == "success":
|
||||
return (
|
||||
f"External wait satisfied (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Waited for: {c.get('waited_for', '')}\n"
|
||||
f"Authoritative result: workflow success verified via Gitea API.\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Verify: {c.get('verify_when_woken', '')}\n"
|
||||
f"Continue from: {c.get('on_success', '')}"
|
||||
)
|
||||
ev = json.dumps(rec.get("failure_evidence", {}))[:400]
|
||||
return (
|
||||
f"External wait finished with FAILURE (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Authoritative result: workflow FAILURE verified via Gitea API.\n"
|
||||
f"Failed job evidence: {ev}\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Do NOT treat this as success. Continue from: {c.get('on_failure', '')}"
|
||||
)
|
||||
|
||||
|
||||
# ── resume driver (single-flight, tree-owned) ─────────────────────────────
|
||||
|
||||
|
||||
def resume(rec: dict) -> None:
|
||||
"""Run one continuation to completion. Runs on a dedicated worker thread.
|
||||
Preconditions: caller verified no inflight worker for this session."""
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
# REAL_ASYNC_BRIDGE §2 invariant: a successful authority-verified
|
||||
# operation completion must NEVER be overwritten or downgraded by a
|
||||
# legacy session/FIFO delivery failure. Terminal operation state
|
||||
# (COMPLETED/FAILED — stamped by the waiter's own authority
|
||||
# verification) is owned by the OPERATION domain; this function owns
|
||||
# only SESSION-DELIVERY. Re-read from disk: the in-memory copy may be
|
||||
# stale relative to the waiter's terminal stamp.
|
||||
fresh = waitstore.load(wid)
|
||||
if fresh and fresh.get("status") in waitstore.TERMINAL:
|
||||
log(
|
||||
f"resume {wid}: skipped — operation already terminal "
|
||||
f"({fresh['status']}); delivery worker never overrides "
|
||||
f"operation truth"
|
||||
)
|
||||
return
|
||||
rec = fresh or rec
|
||||
msg = continuation_msg(rec)
|
||||
|
||||
# B: busy-session guard before touching the lease
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: target session busy (stray pids {strays})"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: session busy {strays}")
|
||||
return
|
||||
|
||||
# pre-register the lease owner as THIS daemon so acquisition is atomic
|
||||
lease = waitstore.acquire_lease(sid, wid, os.getpid(), os.getpid())
|
||||
if lease is None:
|
||||
live = waitstore.read_lease(sid)
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: lease held by wait {live and live.get('wait_id')}"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: lease held")
|
||||
return
|
||||
|
||||
rec["status"] = "RESUMING"
|
||||
rec["resume_attempts"] = rec.get("resume_attempts", 0) + 1
|
||||
waitstore.save(rec)
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
resume_cmd(sid, msg, wid, rec.get("resume_cmd")),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception as e:
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "READY_TO_RETRY" # spawn failed pre-delivery → retryable
|
||||
rec["resume_note"] = f"spawn failed: {e}"
|
||||
waitstore.save(rec)
|
||||
log(f"spawn {wid} failed pre-delivery: {e}")
|
||||
return
|
||||
|
||||
pgid = proc.pid # start_new_session ⇒ pgid == child pid
|
||||
waitstore.update_lease(sid, wait_id=wid, pid=proc.pid, pgid=pgid, pid_starttime=waitstore._pid_starttime(proc.pid))
|
||||
log(f"launched {wid}: pid={proc.pid} pgid={pgid} attempt={rec['resume_attempts']}")
|
||||
|
||||
timed_out = False
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=WATCH_SECS)
|
||||
rc = proc.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
survivors = terminate_tree(pgid)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=30)
|
||||
except Exception:
|
||||
stdout, stderr = "", ""
|
||||
rc = proc.returncode
|
||||
if survivors:
|
||||
log(f"ERROR {wid}: group {pgid} survivors after KILL: {survivors}")
|
||||
|
||||
waitstore.release_lease(sid, wid)
|
||||
|
||||
if timed_out:
|
||||
# D+E: timeout is NOT success. Tree terminated; retry truthfully.
|
||||
if rec["resume_attempts"] >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = (
|
||||
f"watch timeout x{rec['resume_attempts']}; process group {pgid} terminated; attempts exhausted"
|
||||
)
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = f"watch timeout; process group {pgid} terminated (TERM→KILL, verified); will retry"
|
||||
log(f"timeout {wid}: tree {pgid} terminated, rc={rc}")
|
||||
elif rc == 0:
|
||||
# COMPLETION TRUTH (constraint 4): rc==0 means the headless worker
|
||||
# EXITED CLEANLY — the strongest terminal evidence the supported CLI
|
||||
# provides. Timeout/cancel can never reach this branch. The evidence
|
||||
# chain (rc, output tail, timing) is recorded on the wait record.
|
||||
terminal = "COMPLETED" if rec.get("verdict") == "success" else "FAILED"
|
||||
rec["status"] = terminal
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_rc0",
|
||||
"rc": 0,
|
||||
"output_tail": (stdout or "")[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
log(f"resumed {wid}: rc=0 → {terminal}")
|
||||
else:
|
||||
# Nonzero rc: delivery/completion unproven. Honest, not schedulable.
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_nonzero",
|
||||
"rc": rc,
|
||||
"output_tail": ((stdout or "") + (stderr or ""))[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
rec["resume_note"] = f"resume rc={rc}; delivery unverified"
|
||||
log(f"resumed {wid}: rc={rc} → RESUME_UNVERIFIED")
|
||||
waitstore.save(rec)
|
||||
|
||||
|
||||
# ── startup recovery (invariant F) ────────────────────────────────────────
|
||||
|
||||
|
||||
def startup_recovery() -> None:
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
if lease and lease.get("wait_id") == wid and waitstore._owner_alive(lease):
|
||||
# Adopt the live worker and monitor it to a bounded conclusion.
|
||||
pgid = lease["pgid"]
|
||||
log(f"recover {wid}: adopted live worker pgid={pgid}")
|
||||
deadline = time.time() + WATCH_SECS
|
||||
while time.time() < deadline and pgid_members(pgid):
|
||||
time.sleep(2)
|
||||
survivors = terminate_tree(pgid) if pgid_members(pgid) else []
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "adopted post-restart; outcome unverifiable" + (
|
||||
f"; survivors killed: {survivors}" if survivors else ""
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: adopted worker ended → RESUME_UNVERIFIED")
|
||||
else:
|
||||
if lease:
|
||||
waitstore.release_lease(sid, wid) # stale
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "recovered at daemon startup: no live owner; delivery unverifiable"
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: no live owner → RESUME_UNVERIFIED")
|
||||
|
||||
|
||||
# ── reconciler + drive loop (invariant G: deterministic queue) ────────────
|
||||
|
||||
_inflight: dict[str, threading.Thread] = {}
|
||||
_inflight_lock = threading.Lock()
|
||||
|
||||
|
||||
def drive_ready() -> None:
|
||||
for rec in waitstore.schedulable():
|
||||
sid = rec["session_id"]
|
||||
# CONTENTION POLICY (constraint 2): MiMo exposes NO supported
|
||||
# busy/idle session signal (verified 2026-09-12: `mimo session list
|
||||
# --format json` has no status field; no `session status` command;
|
||||
# no status column in the session schema; no documented ACP/serve
|
||||
# endpoint). Therefore automatic injection into interactive sessions
|
||||
# is NOT YET SAFE and stays disabled. Only waits explicitly declared
|
||||
# target_kind="headless" (disposable/owned sessions) auto-dispatch.
|
||||
if rec.get("target_kind") != "headless":
|
||||
if not rec.get("hold_reason"):
|
||||
rec["hold_reason"] = (
|
||||
"INTERACTIVE_AUTO_RESUME = NOT YET SAFE: "
|
||||
"no supported MiMo busy/idle signal; "
|
||||
"queued until operator releases"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
continue
|
||||
if rec.get("ext", {}).get("resume_mode") == "self":
|
||||
# REAL_ASYNC_BRIDGE §3: self-resuming wait (completion-file /
|
||||
# inotify waiter owns its continuation). The daemon has NO
|
||||
# delivery role here — dispatching a delivery worker would
|
||||
# stamp a phantom rc!=0 over the waiter's own truthful
|
||||
# terminal state (the canonical-state contradiction).
|
||||
# NB: keyed on the explicit self marker, NOT on resume_cmd
|
||||
# absence — records without inline resume_cmd may still be
|
||||
# dispatchable via the WR_RESUME_CMD env template.
|
||||
continue
|
||||
with _inflight_lock:
|
||||
t = _inflight.get(sid)
|
||||
if t is not None and t.is_alive():
|
||||
continue # single-flight: session already being driven
|
||||
# B: never launch into a busy session
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
log(f"drive: {rec['wait_id']} held, session busy {strays}")
|
||||
continue
|
||||
th = threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}")
|
||||
_inflight[sid] = th
|
||||
# start INSIDE the lock: a just-created thread is not yet
|
||||
# is_alive() until start() runs — starting outside the lock let
|
||||
# the same drive cycle dispatch a second worker for the session
|
||||
# (single-flight race, caught by the T2 invariant test).
|
||||
th.start()
|
||||
log(f"drive: dispatched {rec['wait_id']} (session {sid})")
|
||||
|
||||
|
||||
def reconcile_once() -> None:
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
created = rec.get("created_at", "")
|
||||
try:
|
||||
import calendar
|
||||
|
||||
age = time.time() - calendar.timegm(time.strptime(created, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age > RECONCILE_SECS:
|
||||
try_transition(rec)
|
||||
|
||||
|
||||
_tick = 0
|
||||
|
||||
|
||||
def orphan_sweep() -> int:
|
||||
"""REAL_ASYNC_BRIDGE §4: permanent mid-run orphan-lease recovery.
|
||||
|
||||
FAILURE DETECTION ONLY — never involved in normal completion
|
||||
scheduling. A wait stuck in RESUMING past ORPHAN_GRACE_SECS means the
|
||||
resume flow died between lease acquire and release (worker thread
|
||||
lost, exception mid-watch). Reconcile exactly as startup_recovery
|
||||
would have: release the lease, stamp RESUME_UNVERIFIED truthfully
|
||||
(delivery unverifiable), never auto-relaunch.
|
||||
"""
|
||||
import calendar as _cal
|
||||
|
||||
healed = 0
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
try:
|
||||
updated = _cal.timegm(time.strptime(rec.get("last_updated", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if time.time() - updated < ORPHAN_GRACE_SECS:
|
||||
continue # legitimate in-flight watch (up to WATCH_SECS)
|
||||
wid, sid = rec["wait_id"], rec["session_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
owner_dead = lease is not None and not waitstore._owner_alive(lease)
|
||||
daemon_owned = lease is not None and lease.get("pid") == os.getpid()
|
||||
if lease is None or owner_dead or daemon_owned:
|
||||
if lease is not None:
|
||||
waitstore.release_lease(sid, wid)
|
||||
if rec.get("resume_attempts", 0) >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = (
|
||||
"orphaned mid-run: resume flow died between lease acquire "
|
||||
"and delivery ("
|
||||
+ ("lease owner dead" if owner_dead else "resume thread lost" if daemon_owned else "no lease")
|
||||
+ "); orphan sweep recovered"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"orphan-sweep {wid}: {rec['status']} — {rec['resume_note']}")
|
||||
healed += 1
|
||||
return healed
|
||||
|
||||
|
||||
def reconciler() -> None:
|
||||
global _tick
|
||||
while True:
|
||||
# v0.3: the local-runner wake is the fast path; periodic
|
||||
# authoritative verification demotes to a slower fallback
|
||||
# (missed-event + daemon-restart recovery), not API polling.
|
||||
_tick += 1
|
||||
if _tick % RECONCILE_EVERY_TICKS == 0:
|
||||
try:
|
||||
reconcile_once()
|
||||
except Exception as e:
|
||||
log(f"reconcile error: {e}")
|
||||
try:
|
||||
orphan_sweep() # §4 failure detection — bounded, periodic
|
||||
except Exception as e:
|
||||
log(f"orphan-sweep error: {e}")
|
||||
try:
|
||||
drive_ready()
|
||||
except Exception as e:
|
||||
log(f"drive error: {e}")
|
||||
with _inflight_lock:
|
||||
for sid, t in list(_inflight.items()):
|
||||
if not t.is_alive():
|
||||
del _inflight[sid]
|
||||
time.sleep(DRIVE_LOOP_SECS)
|
||||
|
||||
|
||||
# ── webhook receiver (wake signal only) ───────────────────────────────────
|
||||
|
||||
|
||||
def wake_sweep(event: dict, eid: str) -> dict:
|
||||
"""Sweep WAITING_EXTERNAL waits through authoritative verification.
|
||||
|
||||
Runs on a background thread (the hook's HTTP response returns
|
||||
instantly, minimising the runner's offline window). If any verified
|
||||
wait got 'pending' — the normal case for a job that finished
|
||||
microseconds ago: the post-task hook fires after results are reported
|
||||
but before the runner's final task acknowledgement flips server-side
|
||||
status — retry on a BOUNDED event-triggered schedule (default 4s x3),
|
||||
never a polling loop. The final ack lands milliseconds after the hook
|
||||
exits, so the first retry almost always sees the terminal state.
|
||||
"""
|
||||
arrival = time.time()
|
||||
log(f"local wake {eid}: {json.dumps(event)[:140]}")
|
||||
import calendar
|
||||
|
||||
def sweep() -> tuple[int, int, int]:
|
||||
verified = advanced = pending = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
try:
|
||||
age = time.time() - calendar.timegm(time.strptime(rec.get("created_at", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age < WAKE_MIN_AGE_SECS:
|
||||
continue
|
||||
verified += 1
|
||||
before = rec["status"]
|
||||
try_transition(rec)
|
||||
if rec["status"] != before and rec["status"] == "READY_TO_RESUME":
|
||||
advanced += 1
|
||||
elif rec["status"] == "WAITING_EXTERNAL":
|
||||
pending += 1
|
||||
return verified, advanced, pending
|
||||
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: verified={verified} advanced={advanced} "
|
||||
f"pending={pending} sweep_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
for attempt in range(1, WAKE_RETRIES + 1):
|
||||
# Retry while the sweep didn't conclusively advance anything:
|
||||
# pending verdicts (ack-window) AND fresh-wait skips (age gate).
|
||||
# Bounded, event-triggered — not a polling loop.
|
||||
if not pending and verified:
|
||||
break
|
||||
time.sleep(WAKE_RETRY_SECS)
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: retry{attempt} verified={verified} "
|
||||
f"advanced={advanced} pending={pending} "
|
||||
f"total_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
return {"event_id": eid}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == "/process-event":
|
||||
# T15 §15D: process-exit events from builder/controller
|
||||
# notifiers. Minimal dedicated endpoint — the Gitea webhook
|
||||
# path stays untouched. Payload: {operation_id, exit_code,
|
||||
# completed_at} + OPTIONAL provider/event_type/sha (T16
|
||||
# §16C/§16E: the CI adapter registers as provider="gitea",
|
||||
# event_type="ci_completed"; defaults preserve the T15 builder
|
||||
# identity so existing notifiers are unaffected). The event is
|
||||
# a WAKE, never truth — resumed controllers re-read canonical
|
||||
# authority before any verdict.
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": str(payload.get("provider", "builder")),
|
||||
"event_type": str(payload.get("event_type", "builder_exit")),
|
||||
"run_ref": str(payload.get("operation_id", "")) or None,
|
||||
"sha": payload.get("sha"),
|
||||
"exit_code": payload.get("exit_code"),
|
||||
"completed_at": payload.get("completed_at") or "",
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v is not None}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"process-event {eid}: {json.dumps(event)[:140]}")
|
||||
# REAL_ASYNC_BRIDGE (R3): durable completion-file delivery.
|
||||
# Every event reaching the bridge is written as
|
||||
# resumes/<operation_id>.done so inotify waiters wake on the
|
||||
# kernel event (R4) with no FIFO/lease machinery, and the
|
||||
# event survives daemon AND waiter restarts (R9). Atomic
|
||||
# rename; duplicate events rewrite identical content (R10).
|
||||
_op_id = str(payload.get("operation_id", ""))
|
||||
if _op_id:
|
||||
try:
|
||||
# Stable identity only: duplicate deliveries of the
|
||||
# same logical event must produce byte-identical
|
||||
# completion files (R10 idempotence by construction).
|
||||
# completed_at is volatile and lives in the durable
|
||||
# event log; the file is a WAKE SIGNAL, never truth.
|
||||
_stable = {
|
||||
k: event.get(k)
|
||||
for k in ("provider", "event_type", "run_ref", "sha", "exit_code")
|
||||
if event.get(k) is not None
|
||||
}
|
||||
completion_file.write_completion(_op_id, _stable)
|
||||
except Exception as e: # noqa: BLE001 — delivery best-effort; wait-record transition + watchdog recovery still cover the lost file
|
||||
log(f"process-event {eid}: completion-file write failed: {e}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec, event)
|
||||
# matched READY controller waits are dispatched IMMEDIATELY
|
||||
# (event-driven, no floor). Guard: only never-resumed waits —
|
||||
# a duplicate/late event must not double-dispatch (15E);
|
||||
# single-flight lease + this guard together make resume
|
||||
# idempotent.
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "READY_TO_RESUME" and rec.get("resume_cmd") and rec.get("resume_attempts", 0) == 0:
|
||||
threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}").start()
|
||||
matched += 1
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path == "/wake":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": "gitea-local-runner",
|
||||
"task_id": str(payload.get("task_id", "")) or None,
|
||||
"run_id": str(payload.get("run_id", "")) or None,
|
||||
"repository": str(payload.get("repository", "")) or None,
|
||||
"job_result": str(payload.get("job_result", "")) or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
# Respond instantly; sweep on a worker thread. The hook is
|
||||
# fire-and-forget (curl -m 2) — a broken pipe on write is
|
||||
# harmless and must not propagate.
|
||||
try:
|
||||
threading.Thread(
|
||||
target=wake_sweep, args=(event, eid), daemon=True, name=f"wake-{payload.get('task_id', '?')}"
|
||||
).start()
|
||||
except Exception as e:
|
||||
log(f"wake spawn error: {e}")
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"queued": True, "event_id": eid}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path != "/webhook":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
action = payload.get("action") or payload.get("event_type") or ""
|
||||
wf = payload.get("workflow_run") or {}
|
||||
event = {
|
||||
"provider": "gitea",
|
||||
"event_type": f"workflow.{action}" if wf else action,
|
||||
"sha": (wf.get("head_sha") or payload.get("sha") or "")[:12] or None,
|
||||
"run_ref": str(wf.get("id") or payload.get("run_number") or "") or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"webhook event {eid}: {json.dumps(event)[:140]}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
return
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8791
|
||||
|
||||
# H (constraint 1): flock is the SINGLETON AUTHORITY. daemon.pid's
|
||||
# content is diagnostic metadata only — it never gates startup, never
|
||||
# implies liveness, and is overwritten after the exclusive lock is held.
|
||||
# Stale pid content (incl. SIGKILL debris) is thus harmless by design.
|
||||
PIDFILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock_fd = os.open(PIDFILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
log("refusing start: another daemon holds the pidfile flock")
|
||||
print("daemon already running (pidfile flock held)", file=sys.stderr)
|
||||
return 1
|
||||
os.ftruncate(lock_fd, 0)
|
||||
os.write(lock_fd, f"{os.getpid()}\n".encode()) # diagnostic, lock held for life
|
||||
|
||||
startup_recovery()
|
||||
t = threading.Thread(target=reconciler, daemon=True)
|
||||
t.start()
|
||||
log(f"daemon starting on :{port}")
|
||||
srv = ThreadingHTTPServer(("0.0.0.0", port), Handler)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify.py — authoritative Gitea verification (webhook ≠ success).
|
||||
|
||||
Queries the Gitea API for the TRUE state of the run/job/commit the wait
|
||||
references. Returns (verdict, evidence). verdict ∈ {success, failure,
|
||||
pending, unknown}. Only this module may move a wait out of
|
||||
WAITING_EXTERNAL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json, subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HOST = "https://gitea.thingscouldgetdicey.online"
|
||||
REPO = "conductor/rune"
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
# Canonical ladder without importing Rune: env → token file → git store
|
||||
import os
|
||||
|
||||
tok = os.environ.get("GITEA_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
tf = os.path.expanduser("~/.config/opencode/secrets/gitea-token")
|
||||
if os.path.isfile(tf):
|
||||
return open(tf).read().strip()
|
||||
r = subprocess.run(
|
||||
["git", "credential", "fill"],
|
||||
input="protocol=https\nhost=gitea.thingscouldgetdicey.online\nusername=conductor\n\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("password="):
|
||||
return line[9:]
|
||||
raise RuntimeError("no gitea credential available")
|
||||
|
||||
|
||||
def _api(path: str) -> tuple[int, object]:
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(f"{HOST}/api/v1/repos/{REPO}{path}", headers={"Authorization": f"token {_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"error": e.read().decode()[:200]}
|
||||
|
||||
|
||||
def _jobs_list(run_ref: str):
|
||||
"""GET jobs for a run, unwrapping the {'jobs': [...]} envelope."""
|
||||
s, data = _api(f"/actions/runs/{run_ref}/jobs")
|
||||
if s != 200:
|
||||
return s, None
|
||||
if isinstance(data, dict):
|
||||
data = data.get("jobs", [])
|
||||
return s, data if isinstance(data, list) else None
|
||||
|
||||
|
||||
def _resolve_run_id(run_ref: str, sha: str) -> str:
|
||||
"""Task-list ids are NOT run ids. If run_ref lookup 404s, resolve the
|
||||
real run id by head SHA via /actions/runs (T15: run 6907 vs task 15743)."""
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and jobs:
|
||||
return run_ref
|
||||
if sha:
|
||||
s, runs = _api(f"/actions/runs?limit=30")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else (runs if isinstance(runs, list) else [])
|
||||
# One SHA fans out to multiple runs (lint + deploy-only). Prefer a
|
||||
# substantive (non-skipped) run; a skipped-only run proves nothing.
|
||||
fallback = ""
|
||||
for r in items:
|
||||
if str(r.get("head_sha", "")).startswith(sha):
|
||||
rid = str(r.get("id"))
|
||||
if r.get("conclusion") != "skipped":
|
||||
return rid
|
||||
fallback = fallback or rid
|
||||
return fallback
|
||||
return ""
|
||||
|
||||
|
||||
def verify_run(run_ref: str, sha: str, expected: str) -> tuple[str, dict]:
|
||||
"""run_ref = actions run id (or task id — resolved via SHA lookup).
|
||||
|
||||
Defect fixed 2026-09-12 (surfaced by W/R-v0.3 live proof, run 7110):
|
||||
the jobs query here used raw _api + isinstance(jobs, list), but this
|
||||
Gitea wraps jobs in the {"jobs": [...]} envelope — the check always
|
||||
failed, so EVERY EXACT_WORKFLOW verdict silently fell through to the
|
||||
aggregate commit-status fallback, where unrelated runs on the same
|
||||
SHA (multi-run fan-out: ci.yml + deploy-only) poison the verdict.
|
||||
Use _jobs_list (unwraps the envelope), same as _resolve_run_id."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and isinstance(jobs, list) and jobs:
|
||||
conclusions = {j.get("name"): j.get("conclusion") for j in jobs}
|
||||
statuses = {j.get("status") for j in jobs}
|
||||
if statuses <= {"completed"}:
|
||||
verdict = "success" if set(conclusions.values()) <= {"success", "skipped"} else "failure"
|
||||
return verdict, {"run_ref": run_ref, "jobs": conclusions}
|
||||
return "pending", {"run_ref": run_ref, "statuses": sorted(statuses)}
|
||||
if sha:
|
||||
s, st = _api(f"/commits/{sha}/status")
|
||||
if s == 200 and isinstance(st, dict):
|
||||
state = st.get("state", "unknown") # success|error|failure|pending|warning
|
||||
rid = _resolve_run_id("", sha) # attach run id so failure evidence works
|
||||
if state == "success":
|
||||
return "success", {"sha": sha, "state": state, "run_ref": rid}
|
||||
if state in ("error", "failure"):
|
||||
return "failure", {"sha": sha, "state": state, "run_ref": rid}
|
||||
return "pending", {"sha": sha, "state": state}
|
||||
return "unknown", {"run_ref": run_ref, "sha": sha}
|
||||
|
||||
|
||||
def failed_job_evidence(run_ref: str, sha: str = "") -> dict:
|
||||
"""For failure continuations: exact failing job + conclusion."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if not run_ref:
|
||||
return {}
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s != 200 or not isinstance(jobs, list):
|
||||
return {}
|
||||
bad = [j for j in jobs if j.get("conclusion") not in ("success", "skipped", None)]
|
||||
return {
|
||||
"failed_jobs": [
|
||||
{"name": j.get("name"), "conclusion": j.get("conclusion"), "url": j.get("url", "")} for j in bad
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ── RELEASE_DEPLOY verification (W/R v0.1, overnight handoff Part 2) ──────
|
||||
# Aggregate commit status is NOT a valid completion authority for Rune's
|
||||
# release/deploy flow: the release successor SHA, not the source SHA, is what
|
||||
# converges (observed live: wr-1789179026 stranded on pending while 0.66.15
|
||||
# was deployed). The authority chain is:
|
||||
# source SHA → source runs terminal → release successor commit (descendant)
|
||||
# → successor runs success → deployed/current identity converges.
|
||||
|
||||
|
||||
def _git(args: list[str], repo_root: str) -> str:
|
||||
import subprocess
|
||||
|
||||
r = subprocess.run(["git"] + args, capture_output=True, text=True, cwd=repo_root or None, timeout=15)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args[:2])}: {r.stderr.strip()[:120]}")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _runs_for_sha(sha: str) -> list[dict]:
|
||||
s, runs = _api("/actions/runs?limit=50")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else []
|
||||
return [r for r in items if str(r.get("head_sha", "")).startswith(sha)]
|
||||
|
||||
|
||||
def _sha_verdict_from_runs(runs: list[dict]) -> tuple[str, dict]:
|
||||
"""success | failure | pending from a SHA's workflow runs.
|
||||
Terminal verdict ONLY when every run is completed — mixed states
|
||||
(some success, some still in flight) must stay PENDING (observed live:
|
||||
a SHA with 1 completed-success + 1 queued + 1 in-progress was judged
|
||||
success mid-flight; wr-1789182522 2026-09-12)."""
|
||||
if not runs:
|
||||
return "pending", {"note": "no runs found"}
|
||||
non_terminal = [r for r in runs if r.get("status") != "completed"]
|
||||
if non_terminal:
|
||||
return "pending", {"in_flight": len(non_terminal)}
|
||||
conclusions = {r.get("name"): r.get("conclusion") for r in runs}
|
||||
bad = {n: c for n, c in conclusions.items() if c == "failure"}
|
||||
if bad:
|
||||
return "failure", {"failed": bad}
|
||||
return "success", {"runs": conclusions}
|
||||
|
||||
|
||||
def verify_release_deploy(source_sha: str, repo_root: str = "", deployed_link: str = "") -> tuple[str, dict]:
|
||||
"""RELEASE_DEPLOY authority chain. Returns (verdict, evidence)."""
|
||||
source_sha = (source_sha or "")[:12]
|
||||
ev: dict = {"source_sha": source_sha, "kind": "RELEASE_DEPLOY"}
|
||||
|
||||
# 1. Source SHA's own runs must be terminal and healthy.
|
||||
src_runs = _runs_for_sha(source_sha)
|
||||
src_verdict, src_ev = _sha_verdict_from_runs(src_runs)
|
||||
ev["source"] = src_ev
|
||||
if src_verdict != "success":
|
||||
return src_verdict, ev
|
||||
|
||||
# 2. Release successor: first chore(release) commit descending from source.
|
||||
if not repo_root:
|
||||
return "pending", {**ev, "note": "no repo_root — cannot check lineage"}
|
||||
try:
|
||||
out = _git(
|
||||
[
|
||||
"log",
|
||||
"--format=%H %s",
|
||||
"--reverse",
|
||||
f"{source_sha}..master",
|
||||
"--grep=^chore\\(release\\)",
|
||||
"--extended-regexp",
|
||||
],
|
||||
repo_root,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return "pending", {**ev, "note": str(e)}
|
||||
successors = [l for l in out.splitlines() if l.strip()]
|
||||
if not successors:
|
||||
return "pending", {**ev, "note": "no release successor yet"}
|
||||
succ_line = successors[0]
|
||||
succ_sha_full, _, succ_subject = succ_line.partition(" ")
|
||||
succ_sha = succ_sha_full[:12]
|
||||
ev["successor"] = {"sha": succ_sha, "subject": succ_subject[:80]}
|
||||
|
||||
# Ancestry is guaranteed by the range, but verify explicitly (deterministic
|
||||
# lineage over title guessing):
|
||||
try:
|
||||
_git(["merge-base", "--is-ancestor", source_sha, succ_sha_full], repo_root)
|
||||
except RuntimeError:
|
||||
return "failure", {**ev, "note": "successor is not a descendant of source"}
|
||||
|
||||
# 3. Successor runs must be terminal and healthy.
|
||||
succ_runs = _runs_for_sha(succ_sha)
|
||||
succ_verdict, succ_ev = _sha_verdict_from_runs(succ_runs)
|
||||
ev["successor"]["runs"] = succ_ev
|
||||
if succ_verdict != "success":
|
||||
return succ_verdict, ev
|
||||
|
||||
# 4. Deployed identity convergence. A mismatch is FAILURE only when the
|
||||
# successor's runs are ALL completed — if any is still running/queued the
|
||||
# deploy may simply not have propagated yet → PENDING (observed live:
|
||||
# reconciler checked mid-deploy, verdict failure, deployment converged
|
||||
# ~2 min later; wr-1789182522 2026-09-12).
|
||||
if deployed_link:
|
||||
link = Path(deployed_link)
|
||||
if link.is_symlink() or link.is_dir():
|
||||
deployed = str(link.resolve())
|
||||
deployed_id = Path(deployed).name
|
||||
ev["deployed"] = deployed_id
|
||||
if not deployed_id.startswith(succ_sha[:12]):
|
||||
still = [r for r in _runs_for_sha(succ_sha) if r.get("status") != "completed"]
|
||||
if still:
|
||||
return "pending", {**ev, "note": "deploy propagating (runs in flight)"}
|
||||
return "failure", {**ev, "note": f"deployed {deployed_id} != successor {succ_sha} (all runs terminal)"}
|
||||
else:
|
||||
return "failure", {**ev, "note": f"deployed link missing: {deployed_link}"}
|
||||
|
||||
return "success", ev
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""waitstore.py — wait/resume state machine v0.2 (developer tooling, NOT Rune).
|
||||
|
||||
States:
|
||||
RUNNABLE → WAITING_EXTERNAL → READY_TO_RESUME → RESUMING → COMPLETED | FAILED
|
||||
↑ │
|
||||
│ ├─(watch timeout, tree killed)
|
||||
│ └─(rc!=0 / unverifiable)
|
||||
READY_TO_RETRY ←─────┘
|
||||
RESUME_UNVERIFIED : delivery after crash/timeout cannot be proven.
|
||||
TERMINAL-ish — never auto-scheduled; explicit
|
||||
`requeue()` required to make it READY_TO_RESUME.
|
||||
TIMED_OUT : external event never arrived within the wait budget.
|
||||
|
||||
Waits persist as waits/<wait_id>.json (atomic tmp+rename).
|
||||
Leases persist as leases/<session_id>.lease.json (atomic tmp+rename,
|
||||
validated against /proc so a dead or PID-reused owner is stealable).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, tempfile, time, uuid
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
WAITS = ROOT / "waits"
|
||||
EVENTS = ROOT / "events"
|
||||
LEASES = ROOT / "leases"
|
||||
|
||||
SCHEDULABLE = {"READY_TO_RESUME", "READY_TO_RETRY"}
|
||||
VALID_STATES = {
|
||||
"RUNNABLE",
|
||||
"WAITING_EXTERNAL",
|
||||
"READY_TO_RESUME",
|
||||
"RESUMING",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"READY_TO_RETRY",
|
||||
"RESUME_UNVERIFIED",
|
||||
"TIMED_OUT",
|
||||
}
|
||||
TERMINAL = {"COMPLETED", "FAILED", "RESUME_UNVERIFIED", "TIMED_OUT"}
|
||||
|
||||
|
||||
def _atomic_write(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".tmp-")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f, indent=1, sort_keys=True)
|
||||
os.replace(tmp, path)
|
||||
# T17 17C: the bridge root is SHARED across users (deploy waiter
|
||||
# runs as the runner user; daemon as nick). mkstemp creates 0600 —
|
||||
# daemon re-saves locked the runner user out of records it must
|
||||
# read (waitstore.load in await_resume_event; find_matching_event
|
||||
# replay scan silently skipped them). 0644: owner writes, all read.
|
||||
os.chmod(path, 0o644)
|
||||
except BaseException:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def new_wait(
|
||||
*,
|
||||
session_id: str,
|
||||
provider: str,
|
||||
event_type: str,
|
||||
repo: str,
|
||||
sha: str = "",
|
||||
run_ref: str = "",
|
||||
expected: str = "success",
|
||||
wait_kind: str = "EXACT_WORKFLOW",
|
||||
target_kind: str = "interactive",
|
||||
repo_root: str = "",
|
||||
deployed_link: str = "",
|
||||
objective: str = "",
|
||||
waited_for: str = "",
|
||||
verify_when_woken: str = "",
|
||||
on_success: str = "",
|
||||
on_failure: str = "",
|
||||
resume_cmd: list | None = None,
|
||||
) -> dict:
|
||||
wid = f"wr-{int(time.time())}-{uuid.uuid4().hex[:6]}"
|
||||
rec = {
|
||||
"wait_id": wid,
|
||||
"session_id": session_id,
|
||||
"created_at": now(),
|
||||
"provider": provider,
|
||||
"event_type": event_type,
|
||||
"repo": repo,
|
||||
"sha": sha,
|
||||
"run_ref": run_ref,
|
||||
"expected": expected,
|
||||
# T15 §15D: PROCESS_EVENT — a wait whose authoritative verdict is
|
||||
# the matching process-exit event itself (exit_code carried on the
|
||||
# event), e.g. a builder/controller process completing. The event
|
||||
# IS the authority here (the notifier wrapper observed the real
|
||||
# process exit); no Gitea lookup applies.
|
||||
"wait_kind": wait_kind
|
||||
if wait_kind in ("EXACT_WORKFLOW", "RELEASE_DEPLOY", "PROCESS_EVENT")
|
||||
else "EXACT_WORKFLOW",
|
||||
# CONTENTION POLICY: "interactive" (default, conservative) never
|
||||
# auto-dispatches — MiMo exposes NO supported busy/idle session
|
||||
# signal, so injecting into a possibly-busy interactive session is
|
||||
# NOT YET SAFE. Only explicitly declared headless/disposable
|
||||
# targets ("headless") are auto-resumed.
|
||||
"target_kind": target_kind if target_kind in ("headless", "interactive") else "interactive",
|
||||
"repo_root": repo_root,
|
||||
"deployed_link": deployed_link,
|
||||
# T15 §15D: per-wait resume command override (placeholder template,
|
||||
# same substitution as WR_RESUME_CMD: {session_id}/{msg}/{wait_id}).
|
||||
# A synthetic controller session (e.g. a Scaffolding stage driver)
|
||||
# is NOT a MiMo session — the global mimo-shaped default cannot
|
||||
# resume it, which is exactly why fixed-sleep polling crept back
|
||||
# in T12-T14. Per-wait override makes ANY registered continuation
|
||||
# target resumable through the same bridge.
|
||||
"resume_cmd": list(resume_cmd) if resume_cmd else None,
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"last_verified": "",
|
||||
"resume_attempts": 0,
|
||||
"continuation": {
|
||||
"objective": objective,
|
||||
"waited_for": waited_for,
|
||||
"verify_when_woken": verify_when_woken,
|
||||
"on_success": on_success,
|
||||
"on_failure": on_failure,
|
||||
},
|
||||
}
|
||||
_atomic_write(WAITS / f"{wid}.json", rec)
|
||||
return rec
|
||||
|
||||
|
||||
def load(wid: str) -> dict | None:
|
||||
p = WAITS / f"{wid}.json"
|
||||
return json.loads(p.read_text()) if p.is_file() else None
|
||||
|
||||
|
||||
def save(rec: dict) -> None:
|
||||
assert rec.get("status") in VALID_STATES, rec.get("status")
|
||||
rec["last_updated"] = now()
|
||||
_atomic_write(WAITS / f"{rec['wait_id']}.json", rec)
|
||||
|
||||
|
||||
def all_waits() -> list[dict]:
|
||||
out = []
|
||||
for p in WAITS.glob("*.json"):
|
||||
try:
|
||||
out.append(json.loads(p.read_text()))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def pending() -> list[dict]:
|
||||
return [r for r in all_waits() if r.get("status") in {"WAITING_EXTERNAL"} | SCHEDULABLE]
|
||||
|
||||
|
||||
def schedulable() -> list[dict]:
|
||||
"""READY waits, oldest first — deterministic queue order."""
|
||||
rs = [r for r in all_waits() if r.get("status") in SCHEDULABLE]
|
||||
rs.sort(key=lambda r: (r.get("created_at", ""), r.get("wait_id", "")))
|
||||
return rs
|
||||
|
||||
|
||||
def requeue(wid: str, note: str) -> dict | None:
|
||||
"""Operator-explicit requeue of a RESUME_UNVERIFIED/RESUME_FAILED wait."""
|
||||
rec = load(wid)
|
||||
if not rec:
|
||||
return None
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = note
|
||||
save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
# ── events (wake signals, idempotent) ─────────────────────────────────────
|
||||
|
||||
|
||||
def record_event(event: dict) -> str:
|
||||
import hashlib
|
||||
|
||||
body = json.dumps(event, sort_keys=True)
|
||||
eid = hashlib.sha256(body.encode()).hexdigest()[:16]
|
||||
f = EVENTS / f"{eid}.json"
|
||||
if not f.is_file():
|
||||
_atomic_write(f, {"event_id": eid, "received_at": now(), "event": event})
|
||||
return eid
|
||||
|
||||
|
||||
def match(rec: dict, event: dict) -> bool:
|
||||
if rec.get("provider") != event.get("provider"):
|
||||
return False
|
||||
if rec.get("event_type") and event.get("event_type") and rec["event_type"] != event["event_type"]:
|
||||
return False
|
||||
if rec.get("sha") and event.get("sha") and rec["sha"] != event["sha"]:
|
||||
return False
|
||||
if rec.get("run_ref") and event.get("run_ref") and rec["run_ref"] != event["run_ref"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ── leases (per-session single-flight, /proc-validated) ───────────────────
|
||||
#
|
||||
# Invariant 3 (PID reuse): liveness is NEVER "PID exists". The lease records
|
||||
# pid + pgid + process starttime (/proc/<pid>/stat field 22); all three must
|
||||
# match or the owner is a different process wearing a recycled PID.
|
||||
#
|
||||
# Invariant 1 (flock as authority): the lease file's flock serializes
|
||||
# read-modify-write across processes and dies with its holder, so a crashed
|
||||
# daemon can never wedge the lock. The lease JSON + /proc decide who OWNS the
|
||||
# session; the flock only prevents racing acquirers from double-stealing.
|
||||
|
||||
_lease_fds: dict[str, int] = {} # session_id → open fd holding the flock
|
||||
|
||||
|
||||
def _pid_starttime(pid: int) -> str | None:
|
||||
"""Field 22 of /proc/<pid>/stat — distinguishes PID reuse."""
|
||||
try:
|
||||
stat = Path(f"/proc/{pid}/stat").read_text()
|
||||
return stat.rsplit(")", 1)[1].split()[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _owner_alive(lease: dict) -> bool:
|
||||
pid = lease.get("pid")
|
||||
if not pid or not Path(f"/proc/{pid}").is_dir():
|
||||
return False
|
||||
if _pid_starttime(pid) != lease.get("pid_starttime"):
|
||||
return False # PID reused by an unrelated process
|
||||
try:
|
||||
if os.getpgid(pid) != lease.get("pgid"):
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def lease_path(session_id: str) -> Path:
|
||||
# T16 §16C finding (observed live in daemon.log): session ids like
|
||||
# "controller-<op>-Verboo Code/glm-5.3-flash" contain "/" — the raw
|
||||
# embed made lease creation raise FileNotFoundError, so resume
|
||||
# delivery for those waits could never succeed. Sanitize to a flat
|
||||
# filesystem-safe name; the hash suffix prevents collisions between
|
||||
# ids that sanitize identically.
|
||||
import hashlib as _hashlib
|
||||
import re as _re
|
||||
|
||||
safe = _re.sub(r"[^A-Za-z0-9._-]+", "_", session_id)[:80]
|
||||
digest = _hashlib.sha256(session_id.encode()).hexdigest()[:10]
|
||||
return LEASES / f"{safe}-{digest}.lease.json"
|
||||
|
||||
|
||||
def read_lease(session_id: str) -> dict | None:
|
||||
p = lease_path(session_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _locked_lease_fd(session_id: str) -> int | None:
|
||||
"""Open the lease file and take its flock (non-blocking). The fd stays
|
||||
open (lock held) until release_lease closes it."""
|
||||
if session_id in _lease_fds:
|
||||
return _lease_fds[session_id] # already hold it
|
||||
LEASES.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(lease_path(session_id), os.O_CREAT | os.O_RDWR, 0o600)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
os.close(fd)
|
||||
return None # another process holds the lock — mid-acquire
|
||||
_lease_fds[session_id] = fd
|
||||
return fd
|
||||
|
||||
|
||||
def _write_lease_locked(session_id: str, lease: dict) -> None:
|
||||
fd = _lease_fds[session_id]
|
||||
os.ftruncate(fd, 0)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
os.write(fd, json.dumps(lease, indent=1, sort_keys=True).encode())
|
||||
os.fsync(fd)
|
||||
|
||||
|
||||
def acquire_lease(session_id: str, wait_id: str, pid: int, pgid: int) -> dict | None:
|
||||
"""Atomically create or steal the session lease. Returns the lease dict,
|
||||
or None if a live owner holds it (or another process is mid-steal).
|
||||
Staleness is decided by /proc identity evidence, never by age alone."""
|
||||
fd = _locked_lease_fd(session_id)
|
||||
if fd is None:
|
||||
return None
|
||||
existing = read_lease(session_id)
|
||||
if existing and _owner_alive(existing):
|
||||
_lease_fds.pop(session_id, None)
|
||||
os.close(fd)
|
||||
return None # live worker owns this session
|
||||
lease = {
|
||||
"session_id": session_id,
|
||||
"wait_id": wait_id,
|
||||
"pid": pid,
|
||||
"pgid": pgid,
|
||||
"pid_starttime": _pid_starttime(pid),
|
||||
"acquired_at": now(),
|
||||
"state": "RESUMING",
|
||||
}
|
||||
_write_lease_locked(session_id, lease)
|
||||
return lease
|
||||
|
||||
|
||||
def update_lease(session_id: str, **fields) -> None:
|
||||
lease = read_lease(session_id) or {}
|
||||
lease.update(fields)
|
||||
_write_lease_locked(session_id, lease)
|
||||
|
||||
|
||||
def release_lease(session_id: str, wait_id: str) -> bool:
|
||||
"""Release only if the current lease belongs to this wait (no stealing
|
||||
a successor's lease during a slow shutdown path). Closes the flock fd."""
|
||||
lease = read_lease(session_id)
|
||||
if not (lease and lease.get("wait_id") == wait_id):
|
||||
return False
|
||||
fd = _lease_fds.pop(session_id, None)
|
||||
if fd is not None:
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
pass
|
||||
os.close(fd)
|
||||
lease_path(session_id).unlink(missing_ok=True)
|
||||
return True
|
||||
@@ -0,0 +1,124 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { existsSync, appendFileSync } from "node:fs"
|
||||
|
||||
// builder-bridge — Rune Core preflight for the MiMo builder (handoff
|
||||
// 2026-09-10 §3–§7, Option C bridge tranche). For each user turn, the hook
|
||||
// extracts the user's message from the outgoing LLM request, runs the Rune
|
||||
// Core builder-bridge preflight (deterministic: route classification +
|
||||
// memory selection + active-Scaffolding projection, ZERO model calls), and
|
||||
// appends a compact capsule to the final user message. Same Rune Core the
|
||||
// TUI/operator path uses — this file is a thin adapter with no cognition.
|
||||
//
|
||||
// CANONICAL SOURCE: config/mimo-hooks/builder-bridge.ts
|
||||
// Installed to ~/.mimocode/hooks/builder-bridge.ts by
|
||||
// `scripts/install_mimo_hook.py` (hash-verified, drift-detecting — the same
|
||||
// contract as event-wait-guard.ts). T22: this file was previously
|
||||
// UNVERSIONED and hardcoded an ephemeral interpreter path (/tmp/ci-venv),
|
||||
// which silently killed the whole edge; both defects are repaired here.
|
||||
//
|
||||
// Overhead contract (§7/§8): capsule ≤ ~300 tokens; a repeat/empty task
|
||||
// costs ~0ms (TTL cache); a fresh task ~150ms (bounded retrieval). The hook
|
||||
// degrades silently to no-injection on any failure — it must never block
|
||||
// the builder.
|
||||
//
|
||||
// Injection policy: only append when the capsule is non-empty. A plain
|
||||
// "hello" gets nothing (§5: if nothing relevant exists, inject nothing).
|
||||
|
||||
const BRIDGE = "/mnt/workdrive/issue161-harness/src/rune/builder_bridge_cli.py"
|
||||
const MARKER_PREFIX = "/tmp/rune-bridge-"
|
||||
|
||||
// Interpreter resolution (T22 repair): the previous hardcoded
|
||||
// "/tmp/ci-venv/bin/python" is an EPHEMERAL path that no longer exists, so
|
||||
// every bridge call failed and the hook silently degraded — the MiMo edge
|
||||
// received no Rune Core preflight at all (zero fire-log lines). Resolve
|
||||
// against durable candidates, first existing wins; env override wins over
|
||||
// all of them. If nothing exists the hook degrades as before.
|
||||
const PY_CANDIDATES = [
|
||||
process.env.MIMO_BRIDGE_PY,
|
||||
"/mnt/workdrive/issue161-harness/.test-venv/bin/python",
|
||||
"/home/nick/.venvs/memory/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
].filter((p) => typeof p === "string" && p.length > 0)
|
||||
|
||||
function resolvePy() {
|
||||
for (const p of PY_CANDIDATES) {
|
||||
try {
|
||||
if (existsSync(p)) return p
|
||||
} catch {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Per-process dedupe: identical task text within the cache TTL does not need
|
||||
// a second bridge call — the CLI has its own TTL cache, but skipping the
|
||||
// process spawn entirely for the same message text within 10 minutes keeps
|
||||
// repeats at ~0ms.
|
||||
const lastText = new Map() // text -> timestamp
|
||||
const TTL = 600_000
|
||||
|
||||
function extractUserText(messages) {
|
||||
// messages: Array<{ info: Message, parts: Part[] }> — newest last.
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i]
|
||||
if (m?.info?.role !== "user") continue
|
||||
const texts = []
|
||||
for (const p of m.parts ?? []) {
|
||||
if (p?.type === "text" && typeof p.text === "string") texts.push(p.text)
|
||||
}
|
||||
if (texts.length) return texts.join("\n").slice(0, 4000)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export default {
|
||||
"experimental.chat.messages.transform": async (input, output) => {
|
||||
try {
|
||||
if (!Array.isArray(output.messages) || output.messages.length === 0) return
|
||||
const userText = extractUserText(output.messages)
|
||||
if (!userText || userText.length < 12) return // trivial/noise guard
|
||||
// Harness-internal steps (checkpoint writer, compaction, system
|
||||
// reminders) are not builder tasks — no capsule (measured: the fire
|
||||
// log showed checkpoint-writer steps receiving capsules 2026-09-10).
|
||||
if (/^(<system-reminder>|EXTRACTION REQUIRED)/.test(userText.trim())) return
|
||||
const now = Date.now()
|
||||
const prev = lastText.get(userText)
|
||||
if (prev !== undefined && now - prev < TTL) return // dedupe repeats
|
||||
lastText.set(userText, now)
|
||||
if (lastText.size > 64) {
|
||||
// evict oldest
|
||||
let oldestKey = null, oldestVal = Infinity
|
||||
for (const [k, v] of lastText) if (v < oldestVal) { oldestVal = v; oldestKey = k }
|
||||
if (oldestKey) lastText.delete(oldestKey)
|
||||
}
|
||||
|
||||
const py = resolvePy()
|
||||
if (!py) return // no durable interpreter — degrade (never block)
|
||||
|
||||
const capsule = await new Promise((resolve) => {
|
||||
execFile(py, [BRIDGE, userText], { timeout: 15_000 }, (err, stdout) => {
|
||||
resolve(err ? "" : (stdout?.toString() ?? ""))
|
||||
})
|
||||
})
|
||||
if (!capsule.trim()) return
|
||||
|
||||
// Durable fire-trace: one line per ACTUAL injection into an outgoing
|
||||
// model request (handoff §1 proof requirement). Not a canary — this is
|
||||
// standing observability for the bridge's consumer edge.
|
||||
try {
|
||||
appendFileSync("/tmp/rune-bridge-fire.log",
|
||||
JSON.stringify({ ts: new Date().toISOString(), task: userText.slice(0, 80), capsule_tokens: Math.round(capsule.length / 4), py }) + "\n")
|
||||
} catch {}
|
||||
|
||||
// Append the capsule to the LAST user message (the one being answered).
|
||||
const last = output.messages[output.messages.length - 1]
|
||||
if (last?.parts && Array.isArray(last.parts)) {
|
||||
last.parts.push({
|
||||
type: "text",
|
||||
text: "\n\n<rune-preflight>\n" + capsule.trim() + "\n</rune-preflight>\n(Injected by Rune Core builder bridge — deterministic preflight, zero model calls. Relevant state above; ignore if not applicable to this task.)",
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// never block the builder on bridge failure
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,54 +1,92 @@
|
||||
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(" "),
|
||||
export default tool({
|
||||
description: "Wait for an external operation (CI, deploy, release) to complete. Call this when you cannot make progress until an external process finishes. You do NOT need to know the sha, op_type, or duration — Rune's controller resolves those from the bound PendingOperation. Just call this when you want to wait.",
|
||||
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)"),
|
||||
operation_id: tool.schema.string().optional().describe("Optional: the operation to wait for. If omitted, the controller resolves the current pending operation automatically."),
|
||||
task_id: tool.schema.string().optional().describe("Optional: the active task id (e.g. T4). When provided, resolution is scoped to operations bound to this task — stale global operations are never consulted."),
|
||||
sha: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller uses this only to help resolve which operation you mean."),
|
||||
op_type: tool.schema.string().optional().describe("OPTIONAL HINT — non-authoritative. The controller derives the canonical operation type from repository/Gitea state."),
|
||||
deadline_secs: tool.schema.number().optional().describe("OPTIONAL HINT — non-authoritative. The controller applies governed policy timing. Real user deadlines must be stated in conversation."),
|
||||
},
|
||||
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(" ")
|
||||
async execute(args) {
|
||||
// INTENT NORMALIZATION — model output is intent; the controller
|
||||
// owns execution semantics. All model args are hints recorded for
|
||||
// observability; canonical truth comes from the bound operation.
|
||||
const parts = []
|
||||
if (args.operation_id) parts.push(`--operation-id ${args.operation_id}`)
|
||||
if (args.task_id) parts.push(`--task-id ${args.task_id}`)
|
||||
if (args.sha) parts.push(`--sha ${args.sha}`)
|
||||
if (args.op_type) parts.push(`--model-op-type ${args.op_type}`)
|
||||
if (args.deadline_secs) parts.push(`--model-deadline ${args.deadline_secs}`)
|
||||
const normCmd = `python3 /mnt/workdrive/issue161-harness/scripts/await_intent_cli.py normalize ${parts.join(" ")}`
|
||||
let norm: any
|
||||
try {
|
||||
const normOut = execSync(normCmd, {
|
||||
cwd: "/mnt/workdrive/issue161-harness",
|
||||
encoding: "utf-8" as const,
|
||||
timeout: 120000,
|
||||
env: { ...process.env, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge" },
|
||||
})
|
||||
norm = JSON.parse(normOut.trim().split("\n").find((l: string) => l.startsWith("{")) || "{}")
|
||||
} catch (e: any) {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", error: (e.stderr || e.message || "?").slice(0, 300) })
|
||||
}
|
||||
if (norm.normalized === "AMBIGUOUS") {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
|
||||
}
|
||||
if (!norm.normalized) {
|
||||
return JSON.stringify({ ok: false, stage: "normalization", ...norm })
|
||||
}
|
||||
// canonical continuation: stored/derived op_type + POLICY deadline
|
||||
// (the sha for the wait comes from the normalization resolution,
|
||||
// NOT from the model's args)
|
||||
const waitSha = norm.sha_for_wait || args.sha || ""
|
||||
if (!waitSha) {
|
||||
return JSON.stringify({ ok: false, stage: "resolution", error: "no sha resolved for the pending operation", norm })
|
||||
}
|
||||
const cmd = `python3 /mnt/workdrive/issue161-harness/scripts/ci_event_wait.py --sha ${waitSha} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: repoRoot,
|
||||
timeout: (deadline + 60) * 1000,
|
||||
cwd: "/mnt/workdrive/issue161-harness",
|
||||
timeout: (norm.deadline_secs + 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",
|
||||
env: { ...process.env, DEPLOY_SHA: waitSha, RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge", RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume" },
|
||||
})
|
||||
const jl = result.trim().split("\n").find((l: string) => l.startsWith("{"))
|
||||
const envelope = jl ? JSON.parse(jl) : { ok: false, error: "no output" }
|
||||
return JSON.stringify({
|
||||
...envelope,
|
||||
normalization: {
|
||||
model_deadline_ignored: norm.model_deadline_ignored,
|
||||
model_op_type_ignored: norm.model_op_type_ignored,
|
||||
model_sha_ignored: !!args.sha && args.sha !== waitSha,
|
||||
canonical_op_type: norm.op_type,
|
||||
canonical_authority: norm.authority,
|
||||
deadline_provenance: norm.deadline_provenance,
|
||||
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
|
||||
},
|
||||
})
|
||||
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)}`,
|
||||
})
|
||||
const lines = (e.stdout || "").trim().split("\n")
|
||||
const jl = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jl) {
|
||||
const envelope = JSON.parse(jl)
|
||||
return JSON.stringify({
|
||||
...envelope,
|
||||
normalization: {
|
||||
model_deadline_ignored: norm.model_deadline_ignored,
|
||||
model_op_type_ignored: norm.model_op_type_ignored,
|
||||
model_sha_ignored: !!args.sha && args.sha !== waitSha,
|
||||
canonical_op_type: norm.op_type,
|
||||
canonical_authority: norm.authority,
|
||||
deadline_provenance: norm.deadline_provenance,
|
||||
matched_by: norm.matched_by || norm.evidence || "auto-resolved",
|
||||
},
|
||||
})
|
||||
}
|
||||
return JSON.stringify({ ok: false, stage: "wait", error: (e.stderr || e.message || "?").slice(0, 300) })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
|
||||
// bash — executes a shell command. PROCESS-OWNERSHIP CONTRACT (v2):
|
||||
//
|
||||
// MODE A (tool-owned): the child is launched in its own process group;
|
||||
// stdout/stderr are CAPTURED pipes (never the TUI tty). On timeout or
|
||||
// failure the ENTIRE process group is signalled (SIGTERM, grace,
|
||||
// SIGKILL) so grandchildren cannot survive the invocation, then the
|
||||
// result is finalized. After finalization NO descendant retains a
|
||||
// writer into MiMo's streams.
|
||||
//
|
||||
// MODE B (explicitly detached): a command suffixed with " &" that the
|
||||
// user/model intends to outlive the tool (nohup/setsid/disown-style)
|
||||
// is executed via the detached helper: stdin=/dev/null, stdout/stderr
|
||||
// -> durable log file, own session (setsid), no MiMo PTY inheritance.
|
||||
// The tool returns the log path as the durable handle.
|
||||
//
|
||||
// Wait-intent compilation (sleep/check/poll aimed at a pending operation)
|
||||
// is preserved: interception happens at the SEMANTIC level via
|
||||
// await_intent route-bash, and a NO-OP for anything that is not a genuine
|
||||
// continuation of a live pending operation. Arbitrary shell syntax
|
||||
// (quotes, pipes, semicolons) passes through untouched — the router
|
||||
// response is only trusted when it carries normalized=true AND
|
||||
// ContinuePendingOperation with a concrete operation bound.
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { spawn, execFile } from "child_process"
|
||||
|
||||
const REPO = "/mnt/workdrive/issue161-harness"
|
||||
const WR_ROOT = "/mnt/workdrive/wr-bridge"
|
||||
|
||||
// ── MODE A: process-group-owned capture execution ──────────────────────
|
||||
interface OwnedResult { stdout: string; stderr: string; status: number; timedOut: boolean }
|
||||
|
||||
function runOwned(command: string, timeoutMs: number): Promise<OwnedResult> {
|
||||
return new Promise((resolve) => {
|
||||
// /bin/sh -c "command" in its OWN process group (detached:true makes
|
||||
// the child a session/group leader we can signal as a WHOLE).
|
||||
const child = spawn("/bin/sh", ["-c", command], {
|
||||
cwd: REPO,
|
||||
stdio: ["ignore", "pipe", "pipe"], // NEVER inherit the TUI tty
|
||||
env: process.env,
|
||||
detached: true, // own process group
|
||||
})
|
||||
let stdout = "", stderr = ""
|
||||
let timedOut = false
|
||||
let settled = false
|
||||
const outChunks: Buffer[] = []
|
||||
const errChunks: Buffer[] = []
|
||||
child.stdout!.on("data", (d) => outChunks.push(d))
|
||||
child.stderr!.on("data", (d) => errChunks.push(d))
|
||||
|
||||
const killGroup = (sig: NodeJS.Signals) => {
|
||||
try { process.kill(-child.pid!, sig) } catch { /* already gone */ }
|
||||
}
|
||||
const finalize = (status: number) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
// detach listeners, then resolve — no late listeners remain
|
||||
child.stdout!.removeAllListeners()
|
||||
child.stderr!.removeAllListeners()
|
||||
resolve({ stdout: Buffer.concat(outChunks).toString(), stderr: Buffer.concat(errChunks).toString(), status, timedOut })
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
killGroup("SIGTERM")
|
||||
// bounded grace for clean shutdown, then SIGKILL the group
|
||||
setTimeout(() => killGroup("SIGKILL"), 2000)
|
||||
// the exit/close event settles; a hard stop-guard settles anyway
|
||||
setTimeout(() => finalize(124), 4000)
|
||||
}, timeoutMs)
|
||||
|
||||
child.on("error", (e) => finalize(-1))
|
||||
child.on("close", (code) => finalize(code ?? -1))
|
||||
})
|
||||
}
|
||||
|
||||
// ── MODE B: explicit detachment for "cmd &" invocations ────────────────
|
||||
function runDetached(command: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const logPath = `/tmp/mimo-detached-${Date.now()}-${Math.floor(Math.random() * 1e6)}.log`
|
||||
const out = `exec >>"${logPath}" 2>&1; exec 0</dev/null; ${command}`
|
||||
const child = spawn("/bin/sh", ["-c", `setsid /bin/sh -c '${out.replace(/'/g, `'\\''`)}'`], {
|
||||
cwd: REPO,
|
||||
stdio: "ignore", // child rebinds its own fds — no PTY inheritance
|
||||
env: process.env,
|
||||
detached: true,
|
||||
// unref: MiMo's event loop does not wait for it
|
||||
})
|
||||
child.unref()
|
||||
resolve(JSON.stringify({
|
||||
detached: true,
|
||||
log: logPath,
|
||||
note: "process detached (setsid, own fds); output streams to the log file only; fd 0/1/2 do NOT reference the MiMo tty",
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// ── wait-intent routing (semantic level, preserved from v1) ────────────
|
||||
async function tryCompileWaitIntent(command: string): Promise<string | null> {
|
||||
let routeOut: string
|
||||
try {
|
||||
routeOut = await new Promise<string>((resolve, reject) => {
|
||||
execFile("python3", [`${REPO}/scripts/await_intent_cli.py`, "route-bash", "--command", command], {
|
||||
cwd: REPO, timeout: 60000, encoding: "utf-8",
|
||||
env: { ...process.env, RUNE_WR_ROOT: WR_ROOT },
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
}, (err, stdout) => { if (err && !stdout) reject(err); else resolve(stdout || "") })
|
||||
})
|
||||
} catch {
|
||||
return null // router unavailable: normal execution (never block on routing)
|
||||
}
|
||||
const jl = routeOut.trim().split("\n").find((l) => l.startsWith("{"))
|
||||
if (!jl) return null
|
||||
let norm: any
|
||||
try { norm = JSON.parse(jl) } catch { return null }
|
||||
// STRICT gate: compile ONLY a normalized, concrete continuation.
|
||||
if (!(norm.normalized === true &&
|
||||
norm.action === "ContinuePendingOperation" &&
|
||||
norm.operation_id && norm.sha_for_wait)) return null
|
||||
const waitCmd = `python3 ${REPO}/scripts/ci_event_wait.py --sha ${norm.sha_for_wait} --deadline ${norm.deadline_secs} --op-type ${norm.op_type}`
|
||||
const r = await runOwned(waitCmd, (norm.deadline_secs + 60) * 1000)
|
||||
const jline = r.stdout.trim().split("\n").find((l) => l.startsWith("{"))
|
||||
if (jline) {
|
||||
return JSON.stringify({
|
||||
...JSON.parse(jline),
|
||||
intent_compilation: {
|
||||
raw_command_kind: norm.raw_command_kind || "sleep_poll",
|
||||
literal_sleep_executed: false,
|
||||
operation_id: norm.operation_id,
|
||||
canonical_op_type: norm.op_type,
|
||||
authority: norm.authority,
|
||||
},
|
||||
})
|
||||
}
|
||||
return JSON.stringify({ ok: false, stage: "compiled-wait", error: (r.stderr || "no output").slice(0, 300) })
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: "Run a shell command. If the command expresses external wait intent (sleep/check/poll aimed at CI, deploy, or release operations with pending operations), it is automatically compiled to Rune's event-driven continuation — you do not need to manage wait mechanics. Plain local commands execute normally. Commands ending in ' &' run detached with output logged to a file.",
|
||||
args: {
|
||||
command: tool.schema.string().describe("Shell command to run"),
|
||||
},
|
||||
async execute(args) {
|
||||
// 1. wait-intent compilation (semantic, strict-gated)
|
||||
try {
|
||||
const compiled = await tryCompileWaitIntent(args.command)
|
||||
if (compiled !== null) return compiled
|
||||
} catch { /* fall through to normal execution */ }
|
||||
|
||||
// 2. MODE B: explicit detachment for backgrounded invocations
|
||||
const cmd = args.command
|
||||
if (/\b(nohup|setsid)\b/.test(cmd) || /&\s*$/.test(cmd)) {
|
||||
return await runDetached(cmd)
|
||||
}
|
||||
|
||||
// 3. MODE A: tool-owned, group-killed, captured execution
|
||||
const r = await runOwned(cmd, 120000)
|
||||
const text = r.stdout + (r.stderr ? `\n[stderr]\n${r.stderr}` : "")
|
||||
if (r.timedOut) {
|
||||
return `exit timed-out (process group terminated): ${text.slice(0, 500)}`
|
||||
}
|
||||
if (r.status !== 0) {
|
||||
return `exit ${r.status}: ${text.slice(0, 500) || "?"}`
|
||||
}
|
||||
return text || "(no output)"
|
||||
},
|
||||
})
|
||||
+172
-72
@@ -1,22 +1,45 @@
|
||||
{
|
||||
"_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.",
|
||||
"_comment": "GENERATED by rune.spine — 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",
|
||||
"generated_at": "2026-10-07T16:54:42Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01",
|
||||
"canary_rune_entry",
|
||||
"canary_rune_serve"
|
||||
],
|
||||
"configs": [],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Tool selection and execution authority",
|
||||
"go_surfaces": [
|
||||
"rune-entry",
|
||||
"model-connect",
|
||||
"provider-registry",
|
||||
"provider-budget",
|
||||
"standardized-providers",
|
||||
"tiered-executor"
|
||||
],
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"adaptive_router",
|
||||
"builder_bridge",
|
||||
"builder_bridge_cli",
|
||||
"capability_broker",
|
||||
"connection_pool",
|
||||
"core_server",
|
||||
"cs_loader",
|
||||
"evidence_probe",
|
||||
"forge",
|
||||
"forge_client",
|
||||
"intent_continuity",
|
||||
"local_tool_runtime",
|
||||
"message_queue",
|
||||
"native_units",
|
||||
"openai_compat_transport",
|
||||
"provider_context",
|
||||
"recall_uplift",
|
||||
@@ -24,21 +47,30 @@
|
||||
"tool_executor",
|
||||
"tool_protocol"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_native",
|
||||
"canary_p0_05",
|
||||
"canary_p1_01"
|
||||
"rust_crates": [
|
||||
"capability_scoring"
|
||||
]
|
||||
},
|
||||
"governance": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration",
|
||||
"canary_audit_ledger",
|
||||
"canary_spine_meta"
|
||||
],
|
||||
"configs": [
|
||||
".coveragerc",
|
||||
"go-version.txt",
|
||||
"lint-baseline.json",
|
||||
"pyright-baseline.json"
|
||||
],
|
||||
"depends_on": [],
|
||||
"description": "Quality control, drift detection, promotion, audit",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"audit_ledger",
|
||||
"build_pipeline_policy",
|
||||
"cc_loader",
|
||||
"claim_audit",
|
||||
"contracts",
|
||||
"curator",
|
||||
@@ -46,17 +78,26 @@
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer",
|
||||
"programme_gates"
|
||||
"grading",
|
||||
"programme_gates",
|
||||
"sc_loader",
|
||||
"worthiness"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_schema_migration"
|
||||
"rust_crates": [
|
||||
"evidence_provenance",
|
||||
"scaffold_core"
|
||||
]
|
||||
},
|
||||
"growth": {
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance",
|
||||
"memory"
|
||||
],
|
||||
"description": "Learning and improvement",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"autonomy_loop",
|
||||
"cognitive_loop",
|
||||
@@ -67,18 +108,22 @@
|
||||
"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",
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Who the agent is — persona, voice, anchors",
|
||||
"go_surfaces": [
|
||||
"persona-definition",
|
||||
"voicebox",
|
||||
"voice-injector"
|
||||
],
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"behavior",
|
||||
"identity_anchors",
|
||||
@@ -89,17 +134,19 @@
|
||||
"voice_injector",
|
||||
"voice_sync",
|
||||
"voicebox"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system"
|
||||
]
|
||||
},
|
||||
"inference": {
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"description": "Model routing and outbound requests",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"inference_trace",
|
||||
"model_connect",
|
||||
@@ -110,31 +157,50 @@
|
||||
"provider_registry",
|
||||
"response_adapter",
|
||||
"standardized_providers",
|
||||
"tc_loader",
|
||||
"token_optimizer"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_p1_02",
|
||||
"canary_p1_06",
|
||||
"canary_resource_authority"
|
||||
"rust_crates": [
|
||||
"token_counting"
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"description": "Storing, retrieving, and managing knowledge",
|
||||
"go_surfaces": [
|
||||
"memory-store-ops",
|
||||
"curator",
|
||||
"sleep-time",
|
||||
"consolidation-ops",
|
||||
"persona-gate",
|
||||
"witness-ops",
|
||||
"stage-guards"
|
||||
],
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"abstraction",
|
||||
"cache",
|
||||
"drift-ledger",
|
||||
"drift_cache",
|
||||
"drift_compression",
|
||||
"ffi_query_memory_smoke",
|
||||
"graph_memory",
|
||||
"graph_shard_bridge",
|
||||
"lexicon_evolution",
|
||||
"memory_consolidation",
|
||||
"memory_env",
|
||||
"memory_selector",
|
||||
"memory_store",
|
||||
"mems_loader",
|
||||
"mr_loader",
|
||||
"ms_loader",
|
||||
"query_memory",
|
||||
"recall_router",
|
||||
"retrieval_benchmark",
|
||||
@@ -142,18 +208,63 @@
|
||||
"tier_manager",
|
||||
"warm_neuron_store"
|
||||
],
|
||||
"depends_on": [
|
||||
"governance"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_full_system",
|
||||
"canary_memory_synthesis",
|
||||
"canary_query_memory"
|
||||
"rust_crates": [
|
||||
"memory_retrieval",
|
||||
"memory_scoring",
|
||||
"memory_storage",
|
||||
"query_memory"
|
||||
]
|
||||
},
|
||||
"orchestration": {
|
||||
"canaries": [
|
||||
"canary_cognitive_strata",
|
||||
"canary_conversation_state",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p1_07",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift",
|
||||
"canary_tui_transport"
|
||||
],
|
||||
"depends_on": [
|
||||
"capability",
|
||||
"growth",
|
||||
"identity",
|
||||
"inference",
|
||||
"perception"
|
||||
],
|
||||
"description": "Task lifecycle, operator, TUI presentation",
|
||||
"go_surfaces": [
|
||||
"event-bus",
|
||||
"message-queue",
|
||||
"hook-engine",
|
||||
"microkernel",
|
||||
"subsystem-health",
|
||||
"parallel-processor",
|
||||
"work-manager",
|
||||
"failure-filer",
|
||||
"cron-engine",
|
||||
"night-mode",
|
||||
"recall-router",
|
||||
"local-tool-runtime",
|
||||
"tool-executor",
|
||||
"operator",
|
||||
"adaptive-router",
|
||||
"build-mode",
|
||||
"builder-bridge",
|
||||
"core-server",
|
||||
"auto-context",
|
||||
"auto-tasks",
|
||||
"event-wait",
|
||||
"evidence-probe",
|
||||
"forge",
|
||||
"forge-client",
|
||||
"goal",
|
||||
"growth-delta",
|
||||
"warm-router",
|
||||
"release-notes"
|
||||
],
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"auto_detect",
|
||||
"build_mode",
|
||||
@@ -188,32 +299,24 @@
|
||||
"task_persist",
|
||||
"tiered_executor",
|
||||
"time_awareness",
|
||||
"tui_transport",
|
||||
"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": {
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"description": "Input processing and context assembly",
|
||||
"mirrors": [],
|
||||
"modules": [
|
||||
"attention_capsule",
|
||||
"auto_context",
|
||||
@@ -248,6 +351,8 @@
|
||||
"salience_gate",
|
||||
"scaffold",
|
||||
"spine",
|
||||
"spine_language",
|
||||
"spine_loader",
|
||||
"stage_guards",
|
||||
"structural_migration",
|
||||
"structural_model",
|
||||
@@ -259,13 +364,8 @@
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
"memory"
|
||||
],
|
||||
"mirrors": [],
|
||||
"canaries": [
|
||||
"canary_context_broker",
|
||||
"canary_full_system"
|
||||
"rust_crates": [
|
||||
"rune_spine"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -272,4 +272,19 @@ observations, fresh resume.
|
||||
|
||||
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).
|
||||
history: skipped (weak evidence — honest limitation).
|
||||
|
||||
## Contract discovery → spine (2026-10-05, #2465 CI correction)
|
||||
|
||||
The closed construction contract is the authority for discoveries. Spine
|
||||
rebuild reads its observed entities and candidate corrections; it does not
|
||||
claim a correction has been built. Qualified IDs such as
|
||||
`rust-crate:cognition_core` keep their declared kind. Bare file paths,
|
||||
test suites and other unqualified references become
|
||||
`construction-surface:<reference>`, never `module`. An observed
|
||||
surface may be PRESENT; a candidate stays DECLARED until independently
|
||||
observed. Existing bad records are migrated with their evidence and edge
|
||||
references intact. This matters: the first write-back mapped bare paths to
|
||||
modules, made 22 false module records and crashed CI on the next rebuild.
|
||||
The per-contract quick-reference markdown is generated from the scaffold
|
||||
store at close; it is a view, not another authority.
|
||||
@@ -0,0 +1,205 @@
|
||||
# DICHOTOMY REBASE — HONEST AUDIT (function-level)
|
||||
|
||||
_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._
|
||||
|
||||
**Function-level completion: 13%** (290/2225 runtime functions matched)
|
||||
|
||||
| Status | Modules | Lines | Meaning |
|
||||
|---|---|---|---|
|
||||
| FULL | 7 | 686 | 80%+ of functions ported |
|
||||
| PARTIAL | 50 | 17873 | 20–80% — real logic exists, gaps named below |
|
||||
| NOT TOUCHED | 121 | 49578 | no name-preserving implementation |
|
||||
| Total runtime | 178 | 68137 | |
|
||||
|
||||
## NOT TOUCHED — the port queue, in priority order (largest first)
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| rune_tui | 4038 | 157 | TUI-LAST |
|
||||
| rune_operator | 2723 | 68 | GO |
|
||||
| memory_store | 2573 | 77 | GO |
|
||||
| curator | 1838 | 43 | GO |
|
||||
| tool_executor | 1812 | 47 | GO |
|
||||
| warm_router | 1536 | 36 | GO |
|
||||
| construction_interface | 1173 | 27 | RUST |
|
||||
| spine | 1034 | 31 | RUST |
|
||||
| construction_decision | 1014 | 20 | RUST |
|
||||
| scaffold | 868 | 31 | GO |
|
||||
| elastic_cognition | 765 | 13 | GO |
|
||||
| memory_consolidation | 740 | 22 | GO |
|
||||
| structural_store | 656 | 33 | RUST |
|
||||
| abstraction | 647 | 16 | GO |
|
||||
| contracts | 626 | 7 | RUST |
|
||||
| work_manager | 591 | 25 | GO |
|
||||
| construction_intent | 555 | 16 | RUST |
|
||||
| canary_audit_ledger | 540 | 2 | TEST-INFRA |
|
||||
| identity_anchors | 537 | 16 | RUST |
|
||||
| build_mode | 517 | 33 | GO |
|
||||
| drift_core | 517 | 14 | RUST |
|
||||
| event_wait | 517 | 14 | GO |
|
||||
| tui_validator | 514 | 13 | TUI-LAST |
|
||||
| memory_selector | 511 | 13 | RUST |
|
||||
| system_registry | 501 | 14 | RUST |
|
||||
| construction_pattern | 496 | 12 | RUST |
|
||||
| graph_memory | 474 | 9 | RUST |
|
||||
| tool_protocol | 461 | 27 | GO |
|
||||
| auto_tasks | 460 | 14 | GO |
|
||||
| capability_fit | 455 | 13 | RUST |
|
||||
| drift_ledger | 453 | 16 | RUST |
|
||||
| goal | 441 | 19 | GO |
|
||||
| release_notes | 439 | 16 | GO |
|
||||
| cron_engine | 436 | 27 | GO |
|
||||
| model_profiles | 435 | 15 | GO |
|
||||
| model_routing | 432 | 13 | GO |
|
||||
| voice_hybrid | 421 | 10 | RUST |
|
||||
| behavior | 407 | 9 | RUST |
|
||||
| token_optimizer | 407 | 22 | RUST |
|
||||
| capability_ledger | 405 | 18 | RUST |
|
||||
| chat_bubble | 401 | 12 | TUI-LAST |
|
||||
| prompt_sync | 376 | 14 | RUST |
|
||||
| provider_budget | 354 | 20 | GO |
|
||||
| structural_model | 352 | 20 | RUST |
|
||||
| programme_continuation | 349 | 11 | RUST |
|
||||
| canary_rune_serve | 329 | 13 | TEST-INFRA |
|
||||
| builder_bridge | 326 | 11 | GO |
|
||||
| canary_p1_07 | 324 | 10 | TEST-INFRA |
|
||||
| layout_contract | 316 | 6 | RUST |
|
||||
| drift_observability | 315 | 8 | RUST |
|
||||
| build_basis | 314 | 12 | RUST |
|
||||
| canary_p0_07 | 310 | 10 | TEST-INFRA |
|
||||
| attention_capsule | 296 | 7 | RUST |
|
||||
| canary_p1_01 | 294 | 12 | TEST-INFRA |
|
||||
| voice_sync | 293 | 10 | RUST |
|
||||
| drift_cache | 292 | 15 | RUST |
|
||||
| outcome_ledger | 286 | 7 | RUST |
|
||||
| time_awareness | 283 | 14 | RUST |
|
||||
| tiered_executor | 282 | 7 | GO |
|
||||
| standardized_providers | 281 | 5 | GO |
|
||||
| canary_p1_06 | 280 | 7 | TEST-INFRA |
|
||||
| canary_p1_03 | 271 | 9 | TEST-INFRA |
|
||||
| auto_context | 270 | 8 | GO |
|
||||
| canary_gitea_automation | 263 | 3 | TEST-INFRA |
|
||||
| canary_p1_02 | 253 | 10 | TEST-INFRA |
|
||||
| construction_patterns_builtin | 249 | 1 | RUST |
|
||||
| canary_full_system | 244 | 2 | TEST-INFRA |
|
||||
| toolchain | 236 | 5 | RUST |
|
||||
| task_gate | 235 | 8 | RUST |
|
||||
| core_server | 234 | 12 | GO |
|
||||
| canary_native | 231 | 3 | TEST-INFRA |
|
||||
| openai_compat_transport | 227 | 9 | GO |
|
||||
| shard_manager | 223 | 9 | RUST |
|
||||
| spine_language | 218 | 5 | RUST |
|
||||
| evidence_probe | 212 | 9 | GO |
|
||||
| model_registry | 211 | 7 | GO |
|
||||
| canary_p0_04 | 207 | 8 | TEST-INFRA |
|
||||
| forge_client | 203 | 18 | GO |
|
||||
| canary_context_broker | 199 | 10 | TEST-INFRA |
|
||||
| canary_p0_05 | 199 | 13 | TEST-INFRA |
|
||||
| testing_mode | 196 | 12 | RUST |
|
||||
| cc_loader | 195 | 26 | RUST |
|
||||
| canary_spine_meta | 192 | 7 | TEST-INFRA |
|
||||
| verification_layer | 189 | 12 | RUST |
|
||||
| ffi_query_memory_smoke | 182 | 5 | RUST |
|
||||
| canary_tui_transport | 174 | 7 | TEST-INFRA |
|
||||
| behaviour_ledger | 173 | 12 | RUST |
|
||||
| canary_tui_drift | 170 | 2 | TEST-INFRA |
|
||||
| inference_trace | 164 | 6 | RUST |
|
||||
| ms_loader | 163 | 16 | RUST |
|
||||
| voicebox | 161 | 11 | GO |
|
||||
| canary_gitea_pipeline | 159 | 2 | TEST-INFRA |
|
||||
| forge | 146 | 11 | GO |
|
||||
| rune_graphic | 145 | 5 | TUI-LAST |
|
||||
| project_registry | 139 | 11 | RUST |
|
||||
| voice_injector | 138 | 5 | GO |
|
||||
| canary_rune_entry | 137 | 3 | TEST-INFRA |
|
||||
| tui_backlog | 125 | 5 | TUI-LAST |
|
||||
| drift_resume | 118 | 3 | RUST |
|
||||
| canary_conversation_state | 115 | 2 | TEST-INFRA |
|
||||
| canary_query_memory | 110 | 2 | TEST-INFRA |
|
||||
| tui_transport | 108 | 10 | TUI-LAST |
|
||||
| spine_loader | 105 | 7 | RUST |
|
||||
| canary_unbounded | 104 | 2 | TEST-INFRA |
|
||||
| message_widgets | 104 | 6 | TUI-LAST |
|
||||
| sc_loader | 96 | 6 | RUST |
|
||||
| cs_loader | 90 | 5 | RUST |
|
||||
| tc_loader | 88 | 5 | RUST |
|
||||
| canary_schema_migration | 86 | 1 | TEST-INFRA |
|
||||
| mems_loader | 83 | 5 | RUST |
|
||||
| provider_context | 48 | 1 | GO |
|
||||
| wait_guard_corpus | 46 | 0 | RUST |
|
||||
| canary_cognitive_strata | 44 | 1 | TEST-INFRA |
|
||||
| builder_bridge_cli | 42 | 1 | GO |
|
||||
| __init__ | 40 | 1 | RUST |
|
||||
| mr_loader | 38 | 13 | RUST |
|
||||
| canary_memory_synthesis | 33 | 1 | TEST-INFRA |
|
||||
| canary_resource_authority | 33 | 1 | TEST-INFRA |
|
||||
| canary_surface_parity | 33 | 1 | TEST-INFRA |
|
||||
| canary_orchestration_foundation | 32 | 1 | TEST-INFRA |
|
||||
| canary_gitea_delegation | 31 | 1 | TEST-INFRA |
|
||||
|
||||
## PARTIAL — gaps to close (matched/total fns; first missing named)
|
||||
|
||||
| Module | Lines | Coverage | Missing (first named) |
|
||||
|---|---|---|---|
|
||||
| local_tool_runtime | 806 | 8/30 | _find_capability_executor, _extract_context_length, context_length, reset_context_length_cache, supports_tools, supports_thinking |
|
||||
| provider_registry | 666 | 7/27 | _format_tools_openai, _format_tools_anthropic, _execute_tool_calls, _execute_tool_calls_anthropic, get_registry, connect_provider |
|
||||
| sleep_time | 971 | 5/25 | _ensure_dirs, db_connect, fetch_sessions, extract_parts, extract_todos, write_transcript |
|
||||
| persona_gate | 724 | 7/26 | _now, _cc_pg_core, _py_lex, _lex_input, _py_overlap, _embed_raw |
|
||||
| context_broker | 823 | 8/26 | _py_default_estimator, _cc_core, _py_default_budget, _py_default_memory_budget, _py_memory_disabled_value, reset_discovered_windows |
|
||||
| capability_broker | 905 | 4/20 | _native_tool_schemas, _infer_task_family, _scored_tools, _select_for_job, load_registry, _native_tool_cards |
|
||||
| safety_override | 569 | 7/22 | _keyword_stopwords, _cc_probe, cc_loader_pin, self_test, _py_matches, escalation_rate |
|
||||
| growth_delta | 437 | 5/19 | _store_db, _llm, _parse_json, _load_deltas, _save_deltas, _cc_core |
|
||||
| failure_auto_filer | 527 | 11/24 | _error_text, unconsumed, mark_consumed, unconsumed, mark_consumed, submit |
|
||||
| night_mode | 405 | 5/18 | get_night_mode, start_night_mode, stop_night_mode, get_status, get_report, to_dict |
|
||||
| cache | 492 | 5/17 | _record_hit, _record_miss, flush_stats, get_cache, put_cache, invalidate_scope |
|
||||
| parallel_processor | 194 | 3/14 | _shutdown_processor, get_processor, run_parallel_with_args, get_status, test_parallel, run_parallel_with_args |
|
||||
| claim_audit | 483 | 4/14 | _get_warm_neuron_store, reframe, _graph_has, _warm_has, verify, journal |
|
||||
| tier_manager | 262 | 5/15 | _cc_probe, cc_loader_pin, _py_core_now, _py_get_tier, _py_promote, _py_demote |
|
||||
| witness | 466 | 7/17 | _now, _tables, _cc_probe, cc_loader_pin, _py_extract_topic, _py_detect_signals |
|
||||
| drift_compression | 307 | 3/12 | _py_get_compression_ratio, _py_compress_text, _py_compress_entry, _py_should_warn, _py_warn_before_compress, get_compression_ratio |
|
||||
| message_queue | 316 | 5/13 | _load_queue, _save_queue, add_message, get_pending, approve_all, get_approved |
|
||||
| model_connect | 462 | 2/10 | is_moe_model, _now, _clean_model_name, _get_cloud_models, health_check, route_model |
|
||||
| intent_continuity | 168 | 2/9 | to_dict, from_dict, _load, save, create_detour, resolve_detour |
|
||||
| pins | 284 | 5/12 | _store_path, _save, _detail_blob, unpin, _py_pin_recall, list_pins |
|
||||
| adaptive_router | 625 | 13/19 | _floor_applies_from_flags, _floor_reason, record_final_route, _event, _record_transition_with, _detect_external_refs |
|
||||
| audit_ledger | 231 | 2/8 | _compute_hash, get_entries, log_promotion, log_gate_result, log_drift_alert, main |
|
||||
| context_pressure | 262 | 3/9 | log_measurement, read_log, log_stats, _now, handle_hook, main |
|
||||
| hook_engine | 860 | 2/8 | discover_functions, tag_function, recommend_placement, validate_placements, generate_report, handle_hook |
|
||||
| microkernel | 434 | 6/12 | now_iso, discover, _extract_manifest, _load_registry, _save_registry, _run_hook |
|
||||
| persona_definition | 210 | 2/8 | _by_topic, compile_full, emit_constitution, emit_identity, emit_persona, handle_hook |
|
||||
| progressive_escalation | 279 | 2/8 | execute_with_escalation, test_escalation, handle_hook, to_dict, execute, verify |
|
||||
| connection_pool | 144 | 3/8 | get_pool, get_connection, get_status, get_connection, get_status |
|
||||
| iteration | 190 | 3/8 | _now, context_packet, predictive_impact, descriptive_impact, add |
|
||||
| task_persist | 269 | 5/10 | _save, export, restore, main, handle_hook |
|
||||
| construction_commands | 268 | 1/5 | _refuse, _validate, _accept, dispatch |
|
||||
| construction_mode | 395 | 1/5 | _tokens, _contains_any, to_dict, from_dict |
|
||||
| event_envelope | 303 | 4/8 | _content_hash, test_envelope, to_canonical_dict, to_dict |
|
||||
| response_adapter | 222 | 3/7 | parse_plain_response, parse_response, test_adapter, to_dict |
|
||||
| self_improve | 242 | 1/5 | _now, suggest_improvements, handle_hook, main |
|
||||
| subconscious_pass | 187 | 1/5 | main, _extract_topic, save_reflection, get_recent_findings |
|
||||
| construction_activation | 237 | 1/4 | _scaffold_for, _auto_structural_awareness, dispatch_from_decision |
|
||||
| dependency_tracker | 202 | 2/5 | check_propagation, get_full_map, main |
|
||||
| event_bus | 244 | 4/7 | _store_path, _content_hash, handle_hook |
|
||||
| salience_gate | 243 | 1/4 | test_salience, handle_hook, to_dict |
|
||||
| subsystem_health | 163 | 3/6 | set_auto_filer, record_failure, configure_logging |
|
||||
| warm_neuron_store | 122 | 3/6 | _db, neuron_slugs, handle_hook |
|
||||
| cognitive_budgeter | 208 | 1/3 | test_budgeter, to_dict |
|
||||
| construction_projection | 135 | 1/3 | _contract_entity, validate |
|
||||
| graph_projection | 108 | 3/5 | write_all, _now |
|
||||
| rune_profiler | 165 | 1/3 | profile_model_call, main |
|
||||
| stage_guards | 281 | 7/9 | to_dict, chk |
|
||||
| structural_migration | 77 | 2/4 | apply_migration, consent_brief |
|
||||
| construction_depth | 138 | 1/2 | to_dict |
|
||||
| recall_router | 162 | 2/3 | handle_hook |
|
||||
|
||||
## FULL — ported, parity-gated
|
||||
|
||||
| Module | Lines | Fns | Side |
|
||||
|---|---|---|---|
|
||||
| recall_uplift | 141 | 1/1 | RUST |
|
||||
| rune_spinner | 130 | 4/5 | RUST |
|
||||
| wait_guard | 116 | 1/1 | RUST |
|
||||
| failure_recovery | 108 | 1/1 | RUST |
|
||||
| programme_gates | 85 | 4/5 | RUST |
|
||||
| impact_preview | 69 | 2/2 | RUST |
|
||||
| route_risk_flags | 37 | 1/1 | RUST |
|
||||
@@ -11,24 +11,22 @@
|
||||
"NEEDS_REVALIDATION",
|
||||
"DEFERRED_FUTURE"
|
||||
],
|
||||
"live_defects": [
|
||||
324
|
||||
],
|
||||
"live_defects": [],
|
||||
"records": [
|
||||
{
|
||||
"number": 324,
|
||||
"title": "Engine defect: checkpoint writer fails deterministically on aborted worker sessions (MessageAbortedError -> empty checkpoint shells)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"bug",
|
||||
"custodian",
|
||||
"life",
|
||||
"memory"
|
||||
],
|
||||
"live_defect": true,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 defect no longer reproduces: zero MessageAbortedError in current logs (incl. active worker), six most recent session checkpoints full documents (111-396 lines); verified by present-absence 2026-09-19"
|
||||
},
|
||||
{
|
||||
"number": 316,
|
||||
@@ -237,8 +235,8 @@
|
||||
{
|
||||
"number": 183,
|
||||
"title": "Future: typed temporal semantics for facts, relationships, beliefs, and preferences",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -250,14 +248,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 182,
|
||||
"title": "Future: GitHub code-search prior-art audit for graph and vector memory",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -266,8 +264,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 181,
|
||||
@@ -287,13 +285,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #182 #183 + research watchlist #164 #165 #166; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 179,
|
||||
"title": "Future: succession/privacy semantics and lineage evaluation",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -305,14 +303,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 178,
|
||||
"title": "Future: evidence-grounded ancestral perspective with explicit uncertainty",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -322,14 +320,14 @@
|
||||
"perception"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 177,
|
||||
"title": "Future: temporal autobiographical model with provenance and reinterpretation lineage",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"governance",
|
||||
@@ -340,14 +338,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 176,
|
||||
"title": "Future: lineage-aware situated curiosity for meaningful autobiographical gaps",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"feature",
|
||||
@@ -358,8 +356,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #175 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 175,
|
||||
@@ -379,13 +377,13 @@
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"next_action": "epic holds absorbed children #176-#179 as checklist items; closes when prerequisites land or ideas dropped"
|
||||
},
|
||||
{
|
||||
"number": 166,
|
||||
"title": "RESEARCH WATCHLIST: Memory-R1 (RL memory ops), Titans (test-time memory), Vespa/Weaviate/Milvus (retrieval competitors)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -394,14 +392,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 165,
|
||||
"title": "RESEARCH: SimpleMem semantic compression + A-MEM dynamic linking + Letta sleep-time compute",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"infrastructure",
|
||||
@@ -410,14 +408,14 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 164,
|
||||
"title": "RESEARCH: HippoRAG 2 \u2014 PPR-style associative propagation as warm-neuron activation candidate",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "HISTORICAL_EVIDENCE",
|
||||
"labels": [
|
||||
"P3 \u2014 low",
|
||||
"growth",
|
||||
@@ -426,27 +424,27 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 idea absorbed into parent epic #181 as tracked checklist item; full detail preserved in this closed issue; re-mint when live"
|
||||
},
|
||||
{
|
||||
"number": 163,
|
||||
"title": "Temporal graph backend spike: Neo4j vs FalkorDB (LadybugDB experimental; Graphiti as reference)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "run the backend spike; compare on same corpus/query set; record evidence-backed recommendation"
|
||||
},
|
||||
{
|
||||
"number": 162,
|
||||
"title": "Qdrant associative-memory projection spike (dense/sparse/multivector + ColBERT late-interaction reranking)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"custodian",
|
||||
@@ -454,8 +452,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #316 (ADAPT-1 B3-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 159,
|
||||
@@ -478,8 +476,8 @@
|
||||
{
|
||||
"number": 158,
|
||||
"title": "Generated wiring-status / architecture-truth report",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P2 \u2014 medium",
|
||||
"growth",
|
||||
@@ -489,8 +487,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #315 (ADAPT-1 B2-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 156,
|
||||
@@ -1151,6 +1149,6 @@
|
||||
"next_action": ""
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
"last_reconciled": "2026-09-19",
|
||||
"reconciled_by": "fresh-session tranche 2026-09-19: template retrofit (spine+plain-English on all open issues), closures #158/#162 (SUPERSEDED) and #324 (FIXED_VERIFIED), research labels applied to #163/#164/#165/#166/#182/#316, stale bug labels removed from #163-#166; 2026-09-19 absorption pass: parked children absorbed into parents (#175<-#176..#179; #181<-#182,#183,#164..#166) per one-open-note-per-idea standard"
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# THE LANGUAGE DICHOTOMY LEDGER — measured, not assumed
|
||||
|
||||
_Created 2026-10-06 from real benchmarks. This is the governing standard for every port in the full dichotomy rebase. Every port must pass the "which language is better for THIS operation?" test before being assigned._
|
||||
|
||||
---
|
||||
|
||||
## The Measured Benchmarks
|
||||
|
||||
| Operation | Python | Python ctypes → Rust | Go cgo → Rust | Pure Go inline |
|
||||
|---|---|---|---|---|
|
||||
| Simple tier check | 0.05 µs | 1.4 µs | 1.4 µs | — |
|
||||
| Consolidation (669B payload) | 0.2 µs | 2.9 µs | 3.4 µs | 0.3 µs |
|
||||
| Concurrent HTTP (10 requests) | 10.8ms (4.0x) | — | 8.4ms (5.1x) | — |
|
||||
| CPU-bound pattern matching (1000 msgs) | 9.6ms (0.8x — GIL) | — | ~2.5ms (est. 3.5-4x) | — |
|
||||
| Compression + zlib wrapper | 0.2 µs | 13.7 µs (worse!) | — | — |
|
||||
|
||||
## The Laws
|
||||
|
||||
### Law 1: FFI overhead is negligible for the real workload
|
||||
The cgo/ctypes boundary costs 1.4-3.4 µs per call. Ollama inference costs 200-5000 ms. FFI overhead is 0.0003-0.0017% of total request time. The language of the caller doesn't matter for the boundary.
|
||||
|
||||
### Law 2: Go wins for CPU-bound reactive work
|
||||
Goroutines deliver true parallelism (5.1x concurrent HTTP, ~3.5-4x CPU-bound). Python's GIL serialises CPU-bound work (0.8x speedup with 4 threads). Go wins for: operator, witness, event bus, work manager, tool executor.
|
||||
|
||||
### Law 3: Simple operations stay in the caller's language
|
||||
Pure Python compression: 0.3 µs. Same op via Rust FFI: 10.4 µs. The FFI overhead exceeds the compute benefit for simple operations. If a Go module needs a string truncation, do it IN GO.
|
||||
|
||||
### Law 4: Rust wins for complex deterministic operations
|
||||
Compile-time type safety, zero-cost abstractions, memory safety, near-native speed for complex compute. Rust wins for: scoring, dedup, pattern matching across many signals, record assembly with many fields, structural store operations.
|
||||
|
||||
### Law 5: Compression across the FFI boundary makes things SLOWER
|
||||
Measured: uncompressed FFI call = 6.1 µs. With zlib wrapper = 13.7 µs (+125%). The cgo transition overhead (~1-2µs) dominates; payload size doesn't matter for local calls under 10KB. Compression helps only for network transfers (>100KB payloads).
|
||||
|
||||
### Law 6: Reduced payload (shorter keys) gives a marginal improvement
|
||||
Measured: 6.1 → 4.6 µs (-25%) by shortening JSON keys from "wash_count" to "w". This is a valid optimisation for hot-path FFI calls but not worth the complexity for most operations.
|
||||
|
||||
### Law 7: Pure inline (no FFI) is fastest when the operation is simple
|
||||
Measured: pure Go inline = 0.3 µs vs 6.1 µs via FFI. If the operation is simple enough to implement in the caller's language, do it inline. Only cross the FFI boundary for operations complex enough to justify it.
|
||||
|
||||
---
|
||||
|
||||
## The Practical Decision Tree
|
||||
|
||||
```
|
||||
Is the operation I/O-bound (HTTP, subprocess, file I/O)?
|
||||
→ YES → GO (goroutines, net/http, os/exec)
|
||||
→ NO ↓
|
||||
|
||||
Is the operation CPU-bound and in the request path (responding to user)?
|
||||
→ YES → GO (goroutines give true parallelism where the GIL fails)
|
||||
→ NO ↓
|
||||
|
||||
Is the operation complex deterministic compute (multiple functions, thresholds, scoring)?
|
||||
→ YES → RUST (type safety, zero-cost abstractions, memory safety)
|
||||
BUT measure: does the compute exceed the FFI overhead?
|
||||
→ NO ↓
|
||||
|
||||
Is the operation simple (string truncation, basic comparison, simple lookup)?
|
||||
→ YES → Do it INLINE in the caller's language (no FFI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Wave Assignment (corrected)
|
||||
|
||||
| Wave | Language | Why |
|
||||
|---|---|---|
|
||||
| W4: event-bus, hook-engine, message-queue, microkernel, subsystem-health, parallel-processor, work-manager, failure-filer, cron-engine, night-mode, cache, connection-pool, recall-router | **Go** | I/O-bound, CPU-bound reactive, event-driven |
|
||||
| W4b: tool-executor, tool-protocol, local-tool-runtime | **Go** | I/O-bound, request-path |
|
||||
| W5a: model-connect (all 10 provider modules) | **Go** | I/O-bound (HTTP), connection pooling |
|
||||
| W5b: operator, adaptive_router, build_mode, warm_router, etc. | **Go** | CPU-bound reactive (routing, dispatch) + I/O-bound |
|
||||
| W5c: memory_store, curator, sleep_time | **Go** | I/O-bound (SQLite + subprocess) |
|
||||
| W6: spine, construction_*, contracts, capability_*, drift_*, graph_memory, memory_selector, identity_anchors, pins, salience_gate, self_improve, token_optimizer | **Rust** | Complex deterministic compute (scoring, dedup, classification, structural operations) |
|
||||
| W6 simple modules: response_adapter, route_risk_flags, rune_profiler, rune_spinner, wait_guard, task_persist, time_awareness | **Stay in caller's language** | Simple operations where FFI overhead > compute benefit |
|
||||
| W7: TUI | **Go (Bubble Tea)** | The reactive front end that interfaces with the user |
|
||||
|
||||
---
|
||||
|
||||
## The Compression Idea (EXPERIMENTAL — NOT WORTH IT)
|
||||
|
||||
**Measured result: compression across the FFI boundary makes things 125% SLOWER.**
|
||||
|
||||
The cgo transition overhead (~1-2µs) is the dominant cost, not the payload size. Compressing the payload adds CPU work (compress + decompress) that exceeds the benefit of a smaller payload crossing the boundary. The compressed payload is 20% of the original size, but the total time doubles.
|
||||
|
||||
Compression WOULD help only if:
|
||||
- The payload were >100KB (network transfer between machines)
|
||||
- The boundary were a network connection, not a local function call
|
||||
|
||||
**Mark as experimental and park. Do not implement.**
|
||||
|
||||
---
|
||||
|
||||
## The Hybrid Language Idea (FUTURE — NOT NOW)
|
||||
|
||||
A hybrid language combining Go's concurrency with Rust's type safety and memory safety would eliminate the FFI boundary entirely. This would be the ideal language for the entire runtime.
|
||||
|
||||
But this is a much larger project — effectively designing and implementing a new programming language. It should be explored after the rebase is complete and the system is stable in Go+Rust. The cgo boundary works today with negligible overhead.
|
||||
|
||||
---
|
||||
|
||||
## Provenance
|
||||
|
||||
- Benchmarks run 2026-10-06 on nick-desktop (CachyOS, 4 cores, cgo 1.21, modernc.org/sqlite)
|
||||
- Rust kernel: libcognition_core.so from cognition_core crate
|
||||
- Go binaries: statically linked, go 1.21
|
||||
- Python: /home/nick/.venvs/memory/bin/python3
|
||||
- These measurements are for THIS hardware and THIS codebase. Re-measure if the hardware changes.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## The Hybrid Language: SURVIVOR
|
||||
|
||||
Named by Nick (2026-10-06): Rust (survives, memory-safe, deterministic) + Go (running, reactive, concurrent) = **Survivor** — the hybrid language pattern where Go reactive code links Rust deterministic compute as a static library at compile time.
|
||||
|
||||
### The Survivor Build Pattern
|
||||
- Rust crates compile as staticlib (.a) + cdylib (.so)
|
||||
- Go binaries link the .a files at compile time via cgo LDFLAGS
|
||||
- The caller sees ONE binary with ONE function call — no FFI boundary from the outside
|
||||
- No CC_RUST_LIB / SC_RUST_LIB env vars needed
|
||||
- Measured: 0.6µs/call vs 1.4-3.4µs for the .so approach
|
||||
|
||||
### Survivor Go Packages
|
||||
All under src/go/cognition/:
|
||||
- cognition (root): tier, consolidation, witness, safety, drift, pins, growth
|
||||
- retrieval: memory_retrieval kernel (mr_call)
|
||||
- storage: memory_storage kernel (mems_read_query)
|
||||
- scaffold: scaffold_core kernel (sc_check_transition, sc_close_quality, sc_language_placement)
|
||||
- provenance: evidence_provenance kernel (ep_* functions)
|
||||
- spine: rune_spine kernel (sp_drift, sp_export, sp_project, sp_query)
|
||||
- scoring: memory_scoring kernel (ms_* functions)
|
||||
- capability: capability_scoring kernel (cs_evaluate_fit, cs_score_card)
|
||||
- token: token_counting kernel (tc_compress_context, tc_estimate_tokens)
|
||||
- query: query_memory kernel (qm_* functions)
|
||||
@@ -0,0 +1,382 @@
|
||||
# Language-Fit Rubric — Wave 1 (Language-Rebasing Tranche R1)
|
||||
|
||||
> STATUS: FINAL — self-verified 2026-09-29 per tranche §12 (all 6 rounds completed,
|
||||
> evidence-sufficiency statements recorded, kill tests frozen; execution proceeds on the
|
||||
> evidence, every RECODE still passes its benchmark gate — no win, no recode).
|
||||
> Per-subsystem sections filled from live research rounds (≥2 live-verified external sources per claim,
|
||||
> per-source lessons, evidence→decision mapping, kill test per candidate). No code changes in Wave 1.
|
||||
> Governing law from §11: **KEEP-PYTHON honesty determines how much of the rebase volume is real.**
|
||||
> Toolchain baseline: Rust 1.98.1 + GCC 16.2.1 present; Go NOT installed — Go earns installation only
|
||||
> through rubric evidence here.
|
||||
|
||||
## Verdict (all rounds, one paragraph)
|
||||
|
||||
**KEEP-PYTHON across the board: 40 clusters scored, 0 recode units awarded.** Six research
|
||||
rounds (W2 leaf utilities, W3 memory, W4 provider-layer, W5 orchestration, W6 runtime-core,
|
||||
W7 TUI) each characterized their subsystem's actual workload inward (file:line-anchored) and
|
||||
tested Python/Rust/Go/C against it with live-verified sources and frozen kill tests. The
|
||||
pattern is consistent: the compute-shaped work already executes in C (hashlib, sqlite3/FTS5/vec0,
|
||||
json, regex), the dominant latency is network or LLM-bound, and no cluster contains a hot loop
|
||||
that pays for a process-boundary recode through the 658 µs–0.63 ms interop crossing. **Go:
|
||||
REJECTED — does not earn installation this tranche** (D5=0 everywhere; nothing measured or
|
||||
fetch-confirmed earns it; orchestration additionally rejects its design fit; K-GO kill tests
|
||||
frozen per round should the case be revived). **Rust: toolchain-ready, justified nowhere this
|
||||
tranche** — keep as the standing option behind the CI-proven std-only adapter for any future
|
||||
measured hot loop. **C: rejected everywhere scored** (duplicates C already inside SQLite/hashlib;
|
||||
adds an unsafe-memory surface). The rebase volume the tranche hypothesized is real only as a
|
||||
small set of named improvements — see "Wave-plan consequence" below.
|
||||
|
||||
## Rubric dimensions (applied to every candidate language per subsystem)
|
||||
|
||||
| Dimension | Question | Evidence required |
|
||||
|---|---|---|
|
||||
| D1 Workload fit | Does the subsystem's dominant workload (I/O-bound, CPU-bound, FFI, protocol, UI) actually favour this language? | Benchmarks or authoritative docs, live-verified |
|
||||
| D2 Measured gain | What wins, concretely — latency, memory, startup, binary distribution? | Named hot paths from the Wave 0 inventory + numbers |
|
||||
| D3 Interop cost | What does the interop contract (§5) cost to carry for this boundary? | Adapter surface size, ABI/build complexity |
|
||||
| D4 Ecosystem | Do the needed libraries exist and are they maintained? | Live-verified crate/package/module listings |
|
||||
| D5 Toolchain reality | Is the toolchain present (or installable and justified) on this machine and the CI runner? | Runner + local `--version` evidence |
|
||||
| D6 Maintenance continuity | Can the Rune ecosystem's single-maintainer loop sustain code in this language here? | Honest judgement, stated plainly |
|
||||
| D7 Kill test | What is the CHEAPEST disproof of this candidate? | One falsifiable early test, frozen before any recode |
|
||||
|
||||
Scoring: each candidate gets D1–D6 scored 0–2 with one-line evidence each, D7 written regardless.
|
||||
Decision per subsystem: **KEEP-PYTHON** / **RECODE→<language>** — KEEP-PYTHON is the expected default;
|
||||
a RECODE that cannot pass its own kill test reverts to KEEP-PYTHON before implementation.
|
||||
|
||||
---
|
||||
|
||||
## W3 — Memory & persistence (units: memory_store 56 fns, curator 43, warm_router 32, memory_consolidation 20, sleep_time 25, + graph_memory, night_mode, identity_anchors)
|
||||
|
||||
- **Research round actor:** general-2 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/memory/`: inward.md, outward/ ×6, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** JSONL-heavy appends, BM25/FTS ranking, consolidation passes, long-running
|
||||
scheduled jobs, file-backed state.
|
||||
- **Decision:** **KEEP-PYTHON (all 7 clusters, zero RECODE).** Cluster verdicts:
|
||||
(1) hot-store JSON persistence — KEEP with upgrade path: orjson in-process wheel
|
||||
(~10× dumps / 2× loads); a bespoke Rust CLI unit is dead on spawn cost (the
|
||||
`shared/native_units.py` crossing ~1–5 ms ≈ orjson's entire 3.4 MB `knowledge.json` decode);
|
||||
(2) SQLite store + FTS5/vec0 recall — KEEP, the compute is already C inside SQLite
|
||||
(`memory_store.py:646` vec0 MATCH, `:693` bm25); (3) neuron scoring/routing — KEEP,
|
||||
network-bound `_embed` (Ollama subprocess curl) dominates; (4) graph BFS — KEEP,
|
||||
hops=1 default, per-node SQL, tiny N; the future fix is a SQL CTE, not a language;
|
||||
(5) batch consolidation/curator — KEEP, LLM/I-O-bound and latency-insensitive,
|
||||
asyncio sweet spot; (6) orchestration/glue — KEEP, pure glue pinned by the Python-TUI
|
||||
interop contract; (7) embedding transport — KEEP, network-bound.
|
||||
- **Candidate matrix summary:** Python 7/7 · Go 0/7 (not installed, nothing measured or
|
||||
fetch-confirmed earns installation; Go's JSON stack mid-rewrite upstream) · hand-written C
|
||||
0/7 (C already battle-tested inside SQLite — hand-writing would duplicate it) · Rust 0/7
|
||||
as CLI units (spawn cost kills the one credible vector; in-process wheels excluded by
|
||||
Wave 0 policy).
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-JSON — orjson microbench on live 3.4 MB
|
||||
`knowledge.json` (if drop-in and ≥2× load / ≥5× dump, the CLI unit is disproven on spawn
|
||||
cost); Rust-general — kill unless a profile shows ≥50% wall time in pure Python compute
|
||||
(none exists; reopen only on materially changed workload); Go — instrument one
|
||||
`consolidate_all` run, kill if Python-compute share <50% (it will be — LLM/I-O bound);
|
||||
C — kill unless a hot kernel absent from the SQLite extension ecosystem is identified
|
||||
(none).
|
||||
- **Sources + lessons:** 6 live-fetched 2026-09-29 (orjson, sled, sqlite-vec, SQLite FTS5
|
||||
docs, Go json/v2 rewrite issue, pydantic-core) — full annotations in
|
||||
`research/wave1/memory/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** static analysis only, no runtime profiling;
|
||||
memory.store SQLite DB not materialized locally — kill tests specify *measured*
|
||||
thresholds rather than assertions. Verdicts rest on "Python is glue around C/network,"
|
||||
which profiling would only strengthen.
|
||||
|
||||
## W4 — Provider/model layer (units: provider_registry 28, model_connect 10, model_routing 15, model_profiles 15, provider_budget 21, + standardized_providers, model_registry, connection_pool)
|
||||
|
||||
- **Research round actor:** general-5 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/provider-layer/`: inward.md, outward-sources.md, outward/ (8 evidence
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** HTTP/SSE streaming, connection pooling, retries/backoff, protocol parsing,
|
||||
tool-call reliability.
|
||||
- **Intention profile correction (inward-verified):** the layer has **NO retry/backoff and NO
|
||||
SSE stub at all** (grep-verified) — `stream: False` everywhere, ~900 lines, remote-LLM-latency-
|
||||
bound. Rewrite pitches are therefore feature-adds, not ports.
|
||||
- **Candidate matrix:** 5 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) HTTP wire + transport (provider_registry, openai_compat_transport, model_connect
|
||||
discovery) Python 5 / Go 2 / Rust 1; (C2) connection pool (connection_pool.py, 144 lines —
|
||||
in-process object guards under threading.Lock, NOT a TCP pool) Python 5 / Go 2 / Rust 2;
|
||||
(C3) routing decisions (model_routing.py, 432 lines, pure decision logic, one httpx call)
|
||||
Python 5 / Go 1 / Rust 1; (C4) budget accounting (provider_budget.py, 354 lines, pure
|
||||
arithmetic + lock, no I/O) Python 5 / Go 1 / Rust 1; (C5) config/registry/normalization
|
||||
(model_profiles, model_registry, standardized_providers, model_connect tables — dataclasses +
|
||||
JSON, editable by non-systems programmers) Python 5 / Go 1 / Rust 1.
|
||||
- **Decision:** **KEEP-PYTHON (all 5 clusters).**
|
||||
- **Kill tests:** C1 Go — KILLED: toolchain absent (`which go` → not found); 28 call-site
|
||||
behaviors incl. the tool-call envelope contract (openai_compat_transport.py:67-107, 160-207)
|
||||
would be re-implemented with zero incumbent test port and NO performance signal (remote-LLM-
|
||||
latency-bound, kilobyte payloads). Re-earn path if Go is ever installed: std-only proof via
|
||||
`interop/rebase-proof/` + a benchmark harness on C1 only. C1 Rust — KILLED BY CONSTRUCTION:
|
||||
SSE parsing requires reqwest+tokio+eventsource-stream+nom (outward O3/O4/O5 — even mistral.rs
|
||||
takes the tokio stack), violating the std-only interop contract. C2–C5 Go/Rust — KILLED:
|
||||
burden of proof sits on the rewrite candidates; kill tests need contention/throughput/
|
||||
counter-evidence that does not exist; routing runs once per request at millisecond scale
|
||||
against multi-second LLM calls (5 orders of magnitude below any measurable threshold).
|
||||
- **Sources + lessons:** 8 live-fetched (O1–O8), ≥2 per major claim, coverage map in
|
||||
`outward-sources.md`. Lessons: (1) stdlib-only Rust has no realistic precedent for HTTP/SSE
|
||||
workloads — the interop contract's zero-crate standard effectively decides Rust out of all
|
||||
network-wire clusters; (2) Go's stdlib HTTP+NDJSON streaming is capability-proven
|
||||
(pkg.go.dev/net/http, ollama api/client.go, ollama's Go API server) — Go's losses here are
|
||||
evidence-absence, not capability-absence; (3) the honest reliability upgrade for this layer
|
||||
is a small Python-level httpx retry/timeout wrapper — a feature-add, not a language change.
|
||||
|
||||
## W5 — Orchestration, authority, plugins, skills (units: orchestration/ 44 files 244 fns, contracts, build_mode 34, construction_authority*, verification_construction, spine, + governance surfaces: drift_*, persona_gate, safety_override, witness, behavior, growth_delta, claim_audit)
|
||||
|
||||
- **Research round actor:** general-1 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/orchestration/`: inward.md, outward-sources.md, outward/ (4 search
|
||||
JSONs), synthesis.md)
|
||||
- **Intention profile:** contract/lifecycle state machines, authority gating, fail-closed validation,
|
||||
HMAC-chained ledgers, policy dispatch.
|
||||
- **Intention profile correction (inward-verified):** the subsystem is durable-ledger I/O,
|
||||
authority wiring, deterministic policy gates, and TUI-facing coordination — NOT hot loops
|
||||
(2 RLocks, 0 sockets, 0 thread pools across 43 files). **Scope gap: `construction_authority.py`
|
||||
and `verification_construction.py` do not exist in the tree** — the construction cluster is
|
||||
covered by build_mode.py; no evidence invented for the missing files.
|
||||
- **Candidate matrix:** 6 clusters, all KEEP-PYTHON. Scores (1–5 = fit for the cluster's actual
|
||||
workload): (C1) durable state & ledger stores (event_store.py:56-101, atomic.py, evidence_store.py
|
||||
— fsync-bound; hashing already C via hashlib) Python 5; (C2) composition roots & authority wiring
|
||||
(composition.py, authority.py, cognitive.py — pure glue, runtime cost ≈ 0) Python 5; (C3)
|
||||
policy/state machines/gates (lifecycle.py, scheduler.py, broker.py, no_change.py, reflex.py —
|
||||
deterministic dict/enum decisions, sub-ms; testability is the requirement) Python 5; (C4)
|
||||
structural self-model & schema tooling (spine.py:24-25, microkernel.py, contracts.py, records.py
|
||||
— introspection + canonical JSON; only candidate for a future Rust spine-diff if snapshots grow
|
||||
1000×) Python 4; (C5) TUI-facing orchestration & build pipeline (build_mode.py 11-stage pipeline,
|
||||
gateway.py, transport.py — the consumer IS the Python TUI; latency dominated by LLM/subprocess
|
||||
waits) Python 5; (C6) benchmark/migration harnesses (retrieval_benchmark.py, migrate.py,
|
||||
benchmark.py — offline tooling, recode buys nothing, loses iteration speed) Python 4.
|
||||
- **Decision:** **KEEP-PYTHON (all 6 clusters). This subsystem contributes 0 recode units to
|
||||
the wave plan.** Go: REJECTED, do not install — differentiator (goroutine-dense servers)
|
||||
targets a workload absent here; cost is exception→error-value rewrite of ~260 functions.
|
||||
Rust: FEASIBLE (toolchain proven) but UNJUSTIFIED anywhere — no cluster has the hot loop
|
||||
that pays for a process-boundary recode; keep as toolchain-ready for other subsystems.
|
||||
C: REJECTED — the only compute-shaped work (sha256) already executes C via stdlib hashlib.
|
||||
- **Kill tests:** K1 (frozen, ALREADY RUN): 100 round-trips through the rebase-proof adapter —
|
||||
mean 0.63ms, p95 0.74ms, cold 0.87ms, 100% exit-0 + ok:true → adapter overhead does NOT block
|
||||
recodes on cost; recodes die on value grounds. Re-open any cluster if profiling shows a
|
||||
function consuming >10% of a real interactive session. K-GO (frozen, ~30 min): Go prototype
|
||||
of event_store.py's batched append+fsync vs Python at 1000 events; if Python's p95 is within
|
||||
2× of Go's, Go is dead here permanently. K-RUST (frozen, ~20 min): measure spine diff on a
|
||||
10k-entity snapshot; <50ms kills any Rust spine recode by absence of pain.
|
||||
- **Sources + lessons:** 4 live-fetched web sources (pydantic-core, ruff blog, go.dev FAQ,
|
||||
Python 3.14 hashlib docs) + 1 LOCAL EXPERIMENT (n=100 adapter benchmark). Lessons: (1) the
|
||||
rebase-proof adapter round-trips at 0.63ms/op — subprocess-per-op is viable for coarse ops,
|
||||
never per-event; (2) Python's "slowness" argument dissolves where the actual compute already
|
||||
runs in C via stdlib (hashlib, sqlite) — the language question is really "where is the hot
|
||||
loop", and here the answer is nowhere; (3) assumed limits flagged honestly: human-scale event
|
||||
cadence (batch_size=8) and Linux spawn cost — both would need revisit at 10⁴+ events/sec or
|
||||
cross-platform targets.
|
||||
|
||||
## W6 — Runtime core & tooling (units: tool_executor 46, tool_protocol 27, local_tool_runtime 32, capability_broker 19, context_broker 21, work_manager 25, cron_engine 28, + failure_auto_filer 28, hook_engine, elastic_cognition, auto_tasks, goal)
|
||||
|
||||
- **Research round actor:** general-4 (COMPLETED 2026-09-29 on disk despite cancellation
|
||||
notification — artifacts at `research/wave1/runtime-core/`: inward.md, outward/,
|
||||
outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** subprocess/process supervision, native execution boundary (native_units
|
||||
adapter already exists), event loops, cron scheduling, hook dispatch.
|
||||
- **Decision:** **STAY-PYTHON (all 6 clusters).** Round scores (1–5 = fit for the cluster's
|
||||
actual inward-characterized workload): (A) subprocess lifecycle (tool_executor, cron_engine,
|
||||
auto_tasks) 4.7 — `subprocess.run(timeout=, capture_output=)` is a one-call expression of the
|
||||
pattern; Rust std::process has NO built-in child timeout (outward-verified), strictly more
|
||||
code under zero-crate; (B) protocol/JSON parsing (tool_protocol, brokers) 4.5 — inputs are
|
||||
KB-scale model fragments; Python json/regex is already C-backed, so a rewrite swaps C for
|
||||
compiled code at the same asymptotic cost; serde's typed-struct model actively mismatches
|
||||
fence-wrapped schema-flexible output; (C) model→tool control loop + HTTP
|
||||
(local_tool_runtime, elastic_cognition, goal) 4.8 — wall time is LLM HTTP latency in seconds;
|
||||
Python overhead is µs–ms noise; the loop's content is policy (circuit breaker, escalation
|
||||
ladders, routing tables); (D) context assembly (context_broker) 4.7 — correctness lives in
|
||||
admission policy and degrade-not-crash semantics, not speed; compaction is itself an LLM call;
|
||||
(E) reliability/queueing (failure_auto_filer) 4.2 — honest lowest: on paper the best Rust
|
||||
candidate (queue + worker + crash-safe JSONL spool), but perf requirement ≈ nil and the
|
||||
correctness burden is threading semantics a rewrite would re-risk for no win ("nothing
|
||||
breaks today"); (F) declarative config/brokers (work_manager, hook_engine, capability_broker)
|
||||
4.9 — literal data, no-op runtime, portable to anything valuable in nothing.
|
||||
- **Candidate matrix summary:** Python 6/6 · Go 0/6 (paper verdicts only — evidence-starved,
|
||||
not installed, kill tests unrunnable locally) · Rust 0/6 (kill tests runnable via the
|
||||
existing std-only interop harness) · C unscored (no FFI-adjacent cluster surfaced).
|
||||
Best-language moment found anywhere: robfig/cron replacing ~37 hand-rolled cron-parsing
|
||||
lines (cron_engine.py:199-236) — ergonomics nicety, not a fit verdict.
|
||||
- **Kill tests (frozen in synthesis.md):** Rust-A — re-express `_run_command`
|
||||
(tool_executor.py:1064) with std::process + manual timeout; kill if >2× the ~40-line
|
||||
Python wrapper (Q4 predicts it is); Go-A — port `cron_engine._run_loop` with os/exec,
|
||||
measure task-fire latency delta (prediction: zero — 60 s sleep and child runtime dominate);
|
||||
B — parse the largest observed tool-protocol stream 1000× in Python; kill if total <1 s
|
||||
(no rewrite yields an observable win); C — instrument `_run_loop`
|
||||
(local_tool_runtime.py:417), log non-HTTP CPU per iteration (prediction <1% of wall);
|
||||
D — time `assemble`+`_admit` on the largest stored context, kill if <10 ms; E — fault-inject
|
||||
kill -9 during spool append; if recovery semantics already hold, the rewrite has no
|
||||
correctness win to buy; F — none needed: absence of any win condition IS the disproof.
|
||||
- **Sources + lessons:** live-fetched 2026-09-29 — annotations in
|
||||
`research/wave1/runtime-core/outward-sources.md`; headline lessons: Rust std::process lacks
|
||||
child timeout (Q4); Go os/exec reaches parity but changes only syntax (Q1); Python
|
||||
json/regex C-backed (Q5b); serde typed structs mismatch dynamic model output (Q5a);
|
||||
concurrency features irrelevant at ≤2 threads (Q6).
|
||||
- **Evidence sufficiency:** Python verdicts fully evidenced inward+outward; Go execution-side
|
||||
evidence honestly starved (paper kill tests carried forward); E's JSONL-spool crash-safety
|
||||
claim rests on inward semantics only — flagged in synthesis.md.
|
||||
|
||||
## W7 — Operator + TUI (units: rune_operator 61 fns, rune_tui 161 fns — largest module, tui_validator)
|
||||
|
||||
- **Research round actor:** general-3 (COMPLETED 2026-09-29 — artifacts at
|
||||
`research/wave1/tui/`: inward.md, outward/ ×8, outward-sources.md, synthesis.md)
|
||||
- **Intention profile:** terminal rendering, input handling, rich layout, hot store state, supervisory
|
||||
command surface. Design decision 4 holds: TUI consumes the interop contract, never rewritten first.
|
||||
- **Decision:** **KEEP-PYTHON (TUI shell; all 4 clusters, zero RECODE, no carve-out found).**
|
||||
Cluster scores (1 = recode, 5 = keep): (A) rendering/widgets 4 — no hot loop exists; render is
|
||||
plain Rich `Text` concatenation (rune_tui.py:1331, chat_bubble.py:157), only animation a 0.12 s
|
||||
spinner (:263), no markdown/Syntax parsing anywhere in src/rune; (B) event glue (~50 `_cmd_*`
|
||||
handlers, reactives, tool-widget maps) 5 — this is Textual's core competency (workers,
|
||||
`exclusive=True`, `call_from_thread`); vendor docs (ratatui) admit "little to no help in organizing
|
||||
large applications"; (C) model send path + envelope 5 — synchronous `send_task` in async worker;
|
||||
the envelope shape (`success/response/thinking/tool_events/degraded`, rune_tui.py:3624ff) is
|
||||
ALREADY the de-facto interop contract; (D) subprocess/streaming 5 — JSONL streaming precedent
|
||||
already in-tree (`_cmd_pull` :3443-3517): the pattern for consuming recoded units, not a recode
|
||||
target. Whole-TUI native recode 1/5 REJECTED — 80+ first-party module coupling, framework-provided
|
||||
concurrency re-implemented by hand; Go additionally toolchain-blocked. Countervailing evidence
|
||||
(Bubble Tea/tview/Codex TUI maturity) recorded honestly in outward-sources.md — it does not
|
||||
outweigh the coupling.
|
||||
- **⚠ CONTRACT GAP FLAGGED (binds Waves 2–7):** `shared/native_units.py` parses only the LAST
|
||||
stdout line — it must gain a JSONL streaming reader (per-line `json.loads` + terminal-envelope
|
||||
validation) before any recoded long-running unit can stream progress to the TUI. Small, testable,
|
||||
non-breaking; candidate for the first Wave 2 implementation issue.
|
||||
- **Adapter surface (TUI as consumer — adopted as the rubric's interop-contract restatement):**
|
||||
(1) single-invocation stateless CLI (`native_unit <op> [args]`, no daemon); (2) success = final
|
||||
stdout line `{"ok":true, …}` generalizing the model-send envelope — recoded units emitting
|
||||
`response`/`tool_events`/`degraded` plug in with zero TUI changes; (3) failure = `ok:false` +
|
||||
nonzero exit, stderr human-only, success = exit 0 AND not timed out; (4) mandatory `--version`
|
||||
on every unit (status bar + `canary_tui_drift.py` detect live units vs Python fallback);
|
||||
(5) JSONL streaming mode for long ops with terminal sentinel envelope; (6) per-call timeout
|
||||
budget, `timed_out` surfaced in `NativeResult` for graceful degradation; (7) streamed events
|
||||
referencing UI objects carry the TUI's stable keys — `(channel, call_id)` for tool widgets
|
||||
(:3676), WorkPane `_entries` object ids (:1239) — preserved verbatim; (8) std-only for
|
||||
proof-phase units.
|
||||
- **Candidate matrix summary:** Python 4/4 clusters · Rust/Go whole-TUI 1/5 rejected · carve-out
|
||||
candidates: none found (future markdown parsing would carve as a Rust native unit behind the
|
||||
adapter, not now).
|
||||
- **Kill tests (frozen in synthesis.md):** stay-Python — killed if >16 ms UI-thread stalls under
|
||||
heavy event bursts OR IPC latency dominates message round-trip (Textual pilot measurement);
|
||||
Rust recode — killed if the adapter must expose >~15 first-party modules (count today ≈80:
|
||||
killed NOW); Go recode — additionally killed by toolchain absence (`command -v go` empty).
|
||||
- **Sources + lessons:** 8 live-fetched 2026-09-29 (Textual workers guide, ratatui vendor docs,
|
||||
Bubble Tea/tview/Codex countervailing evidence, JSON Lines spec, push-granular-work guidance) —
|
||||
annotations in `research/wave1/tui/outward-sources.md`.
|
||||
- **Evidence sufficiency (honest):** full inward read of all 5 TUI files with line refs; ≥2 sources
|
||||
per major claim; gap = no runtime profiling (stall kill test unmeasured — but no mechanism exists
|
||||
for the stall to occur; the 120 ms 21-step pipeline was already removed, `_run_warm_router`
|
||||
:3575 is a no-op).
|
||||
|
||||
## W2 — Leaf utilities & canaries (~40 modules, low coupling: pins, time_awareness, message_queue, event_envelope, attention_capsule, complexity_scorer, token_optimizer, cache 501/17, prompt_sync, chat_bubble, ~15 canary_* modules)
|
||||
|
||||
- **Research round actor:** general-6 (COMPLETED 2026-09-29 — full report in actor notification)
|
||||
- **Intention profile:** small pure helpers, envelopes, scoring, cache; smallest blast radius; the
|
||||
factory + interop proving ground at real scale.
|
||||
- **Ground-truth measurements (local, this session):** Python fn call ~46 ns (100k timed
|
||||
iterations in-process) · native CLI roundtrip ~658 µs (subprocess to
|
||||
`interop/rebase-proof/target/release/rebase-proof --version`) · Python startup floor ~18 ms ·
|
||||
overhead ratio ≈ **14,000×**. The only CI-proven interop contract is the subprocess CLI boundary
|
||||
(`shared/native_units.py`: argv in → JSON stdout, `--version`, timeout, typed `NativeResult`).
|
||||
PyO3/in-process bindings excluded by Wave 0 policy. Therefore every per-call crossing costs
|
||||
≥658 µs — a per-turn helper would need to run >14,000× faster in Rust to break even.
|
||||
- **Candidate matrix (representative — full per-cluster 0–2 scoring in round report):**
|
||||
Python 12/12 in every cluster · Rust 5–6/12 · Go 1–3/12 (D5=0, not installed) · C 1–2/12.
|
||||
No module contains a hot loop (heaviest: 2 `re.sub` per turn in token_optimizer).
|
||||
- **Decision:** **KEEP-PYTHON (all ~40, zero RECODE).** Cluster verdicts: (a) pure CRUD helpers
|
||||
pins/time_awareness/token_optimizer — KEEP, CPython 3.11+ already 10–60% faster on this shape
|
||||
(PEP 659); (b) message/envelope/capsule structures — KEEP, recode would insert a boundary
|
||||
between the chattiest neighbors; (c) cache.py (501 L, largest in wave) — KEEP, the fast part is
|
||||
already C (sqlite3 DB-API 2.0 over SQLite's C library), Python is thin orchestration, batching
|
||||
(threshold 50 / 30 s) already amortizes writes; (d) chat_bubble.py — KEEP, **hard-pinned** to
|
||||
Python by the Textual framework (recode = full framework abandonment, bottleneck is human
|
||||
typing speed); (e) prompt_sync.py glue — KEEP, glue into Python siblings (memory_env,
|
||||
voice_sync, build_pipeline_policy); (f) ~19 canary_* — KEEP **categorically**, a canary that
|
||||
verifies Python importability cannot exist in another language.
|
||||
- **Kill tests (reopening criteria, all fail today on first cheap measurement):** Rust — microbench
|
||||
one per-turn helper behind the native_units CLI, must beat the Python in-process call >10×
|
||||
end-to-end including the 658 µs crossing (whole call <~46 µs; a trivial `--version` already
|
||||
costs 658 µs); Go — identical benchmark post-install, uninstall if roundtrip >100 µs with no
|
||||
named hot path; C — identical benchmark + required fuzz-clean JSON roundtrip, plus a
|
||||
demonstrated GC-free allocation-hot loop first; cache.py — recode only if Python-side
|
||||
orchestration (excl. sqlite3 waits) exceeds 5% of end-to-end latency; capsule/envelope/queue —
|
||||
recode only if one turn's Python total exceeds ~5 ms AND a candidate's CLI path is >10× cheaper.
|
||||
- **Sources + lessons:** (1) https://docs.python.org/3/whatsnew/3.11.html — 10–60% faster,
|
||||
PEP 659 specializing interpreter: CPython already optimized for helper-shaped code, the D2
|
||||
baseline is a moving improving target; (2) https://docs.python.org/3/whatsnew/3.13.html —
|
||||
free-threading/JIT experimental, KEEP-PYTHON ≠ frozen but no bets on experimental paths;
|
||||
(3) https://www.maturin.rs/ — in-process Rust↔Python needs a whole binding/packaging toolchain
|
||||
atop the CLI boundary, D3 worse than measured 658 µs (Wave 0 policy excludes it anyway);
|
||||
(4) https://go.dev/doc/install — Go is a pre-install prerequisite, D5=0 everywhere, never earned
|
||||
installation; (5) https://textual.textualize.io/ — Textual is a Python TUI framework,
|
||||
chat_bubble.py framework-pinned; (6) https://docs.python.org/3/library/sqlite3.html — sqlite3 is
|
||||
DB-API 2.0 over C, cache.py's perf layer already native. All fetched live 2026-09-29.
|
||||
- **⚠ Inventory drift (VERIFIED 2026-09-29):** complexity_scorer.py listed in
|
||||
rebase-inventory.json (2 mentions) but absent on disk — deleted in commit 867321e
|
||||
("governed retirement — complexity_scorer deleted (602 lines)"). Wave 0 inventory needs a
|
||||
correction pass; the module was scored nowhere (no source to characterize).
|
||||
|
||||
---
|
||||
|
||||
## Wave-plan consequence (T5 scope, from the verdict above)
|
||||
|
||||
With **0 recode units awarded**, the tranche's hypothesized rebase volume is real only as the
|
||||
work below. Waves 2–9 are **disposition + named improvements, NOT mass recoding**:
|
||||
|
||||
**Waves 2–7 — per-issue disposition (the 1,852 `[rebase w{wave}]` issues):**
|
||||
Each issue receives its tranche §7 disposition — improve / fix / missing-connectors answered
|
||||
with evidence — plus a language verdict of **KEEP-PYTHON with its rubric evidence line**
|
||||
(the round section for that subsystem names the claim). Disposition runs leaf-first
|
||||
(Wave 2 → 7), the canonical suite stays green throughout, and no issue closes without its
|
||||
evidence line. A recode is off the table unless a frozen kill test is reopened — and then
|
||||
tranche §6 still governs: **no benchmark win, no recode.** Go is not installed and earned no
|
||||
installation; Rust stays toolchain-ready but unjustified (standing option behind the
|
||||
CI-proven adapter).
|
||||
|
||||
**Named improvements — the actual rebase volume (ship alongside disposition):**
|
||||
1. **JSONL streaming reader for `shared/native_units.py`** (W7 contract gap): the adapter
|
||||
parses only the LAST stdout line; it must gain per-line `json.loads` + terminal-envelope
|
||||
validation before any recoded long-running unit could stream progress. Small, testable,
|
||||
non-breaking — first implementation issue of Wave 2.
|
||||
2. **httpx retry/timeout wrapper for the provider layer** (W4): the honest reliability
|
||||
upgrade is a feature-add, not a language change.
|
||||
3. **orjson upgrade path** (W3 cluster 1): the only credible Python-side JSON win —
|
||||
in-process, no boundary crossing. **DISPOSITION (2026-09-29, kill test EXECUTED and
|
||||
FAILED — KEEP-PYTHON stands, no dependency declared):** microbench on the live
|
||||
`~/.rune/consolidation/knowledge.json` (3,540,959 bytes, n=30 best-of): load 13.15 ms
|
||||
std vs 7.85 ms orjson = **1.68× (bar ≥2×: FAIL)**; dump 9.58 ms std vs 5.61 ms
|
||||
orjson-with-decode = **1.71× (bar ≥5×: FAIL)**; raw-bytes dump 1.39 ms = 6.91× but is
|
||||
not drop-in (every `write_text`/`json.dump`-to-file site would need a bytes→str or
|
||||
binary-write conversion). Roundtrip equality verified. Bench: `/tmp/orjson_bench.py`
|
||||
shape (uvx --with orjson). The bytes trap measured real: decode() eats ~60% of the
|
||||
dump win.
|
||||
4. **complexity_scorer.py inventory correction**: listed in `rebase-inventory.json` but
|
||||
deleted on disk (commit `867321e`, 602 lines) — Wave 0 inventory needs the correction
|
||||
pass. **DISPOSITION (2026-09-29, DONE):** inventory regenerated per the script's own
|
||||
header (never hand-edit) — 177→213 files, 1,852→2,314 functions, 13 dead files
|
||||
dropped; committed + pushed `5bc837c`. The W2 round's "2 mentions" drift claim
|
||||
verified FALSE against the live inventory (zero hits).
|
||||
5. **robfig/cron** (W6): recorded as a best-language footnote only; no action.
|
||||
|
||||
**Wave 8 — TUI integration proof:** demonstrate the TUI reading/utilising the mixed-language
|
||||
surfaces that exist (the `interop/rebase-proof` unit through the upgraded adapter), per the
|
||||
8-point adapter surface adopted above.
|
||||
**Wave 9 — close-out audit:** 100% of the 1,852 issues dispositioned, debt-table revision,
|
||||
version bump.
|
||||
|
||||
## Cross-cutting candidates (evaluated once, referenced by every subsystem)
|
||||
|
||||
- **Rust (present, 1.98.1):** interop proof unit already CI-proven (std-only, zero crates, `interop/rebase-proof/`, adapter in `shared/native_units.py`, ok = exit 0 AND not timed out).
|
||||
- **Go (absent):** must earn installation through D1–D5 evidence here before any recode targets it.
|
||||
- **C (present, GCC 16.2.1):** expected to lose D6 in most subsystems; scored where FFI-adjacent work exists.
|
||||
- **Python (KEEP baseline):** the default. Every subsystem begins here; evidence must move it.
|
||||
|
||||
## Evidence ledger (per-source lessons, all rounds)
|
||||
|
||||
Index of per-source evidence. Full annotations live in each round's
|
||||
`research/wave1/<subsystem>/outward-sources.md`; this ledger records the headline lesson
|
||||
each source contributed and the rubric claims it supports.
|
||||
|
||||
| Round | Sources (live-fetched 2026-09-29) | Headline lessons | Supports |
|
||||
|---|---|---|---|
|
||||
| W2 leaf utilities | 6: python.org 3.11 + 3.13 whatsnew, maturin.rs, go.dev/doc/install, textual.textualize.io, docs.python.org sqlite3 | CPython 3.11+ already 10–60% faster on helper-shaped code (PEP 659) — D2 baseline is a moving target; free-threading/JIT experimental, no bets; in-process bindings need a whole toolchain atop the CLI boundary (excluded by Wave 0 policy anyway); Go is pre-install prerequisite, D5=0; Textual framework-pins chat_bubble.py; sqlite3 perf layer already C | W2 KEEP-PYTHON ×~40; the 14,000× crossing-overhead law |
|
||||
| W3 memory | 6: orjson, sled, sqlite-vec, SQLite FTS5 docs, Go json/v2 rewrite issue, pydantic-core | orjson is the only credible Python-side JSON win (in-process, no boundary); SQLite FTS5/vec0 compute is already C; Go's JSON stack mid-rewrite upstream (versioning risk); pydantic-core shows the Rust-as-wheel pattern, excluded by policy | W3 KEEP-PYTHON 7/7; orjson upgrade path on cluster 1 |
|
||||
| W4 provider-layer | 8: live-fetched O1–O8 (HTTP/SSE ecosystem, tokio-stack precedents incl. mistral.rs, pkg.go.dev/net/http, ollama Go client/server) | stdlib-only Rust has no realistic HTTP/SSE precedent — the zero-crate contract decides Rust out of all network-wire clusters by construction; Go's stdlib HTTP+NDJSON streaming is capability-proven (its losses here are evidence-absence, not capability-absence); the honest reliability upgrade is a Python httpx retry/timeout wrapper — feature-add, not language change | W4 KEEP-PYTHON 5/5; SSE-cluster Rust kill-by-construction |
|
||||
| W5 orchestration | 4: pydantic-core, ruff blog, go.dev FAQ, Python 3.14 hashlib docs + 1 LOCAL EXPERIMENT (n=100 adapter benchmark) | adapter round-trips at 0.63ms/op — subprocess-per-op viable for coarse ops, never per-event; Python "slowness" dissolves where compute already runs in C via stdlib; the language question is really "where is the hot loop" (here: nowhere) | W5 KEEP-PYTHON 6/6; K1 kill test already run |
|
||||
| W6 runtime-core | headline lessons (full set in round outward-sources.md) | Rust std::process lacks built-in child timeout — strictly more code under zero-crate; Go os/exec reaches parity but changes only syntax; Python json/regex C-backed so rewrite swaps C for compiled code at equal asymptotic cost; serde typed structs mismatch schema-flexible model output; concurrency features irrelevant at ≤2 threads | W6 STAY-PYTHON 6/6; robfig/cron as best-language footnote |
|
||||
| W7 TUI | 8: Textual workers guide, ratatui vendor docs, Bubble Tea/tview/Codex TUI (countervailing), JSON Lines spec, push-granular-work guidance | Textual workers/`call_from_thread` are the core competency for event glue; ratatui admits "little to no help in organizing large applications"; countervailing native-TUI frameworks recorded honestly — do not outweigh 80+ module coupling; JSONL per-line parse + terminal sentinel is the streaming contract; vendor guidance: push granular speed-sensitive work to subcomponents only when measured | W7 KEEP-PYTHON 4/4; the 8-point adapter surface (adopted as the rubric's interop-contract restatement) |
|
||||
@@ -0,0 +1,50 @@
|
||||
# Wave 0 close-out report — language-rebasing tranche R1
|
||||
|
||||
Branch: `tranche-r1-wave-0` · Date: 2026-09-29
|
||||
|
||||
## 1. Inventory (docs/rebase/rebase-inventory.json)
|
||||
|
||||
- 177 Python modules, ~52,750 lines, 227 classes
|
||||
- TRUE unit count: **1,852 = 1,030 top-level functions + 822 class methods**
|
||||
- Zero duplicate call edges, zero method leaks (generator double-walk defect
|
||||
root-caused and fixed in scripts/rebase_inventory.py)
|
||||
- Cross-verified against an independent fresh AST walk: zero missing units;
|
||||
six inventory-"extras" are real nested/if-try-block classes the naive walk
|
||||
misses (inventory is MORE complete, not less)
|
||||
|
||||
## 2. Issue factory (scripts/rebase_issue_factory.py)
|
||||
|
||||
- One issue per unit following R1 §7; wave-batched (2–7); dedup-safe via
|
||||
deterministic title `[rebase w{wave}] {relpath}::{unit}` + existing-titles
|
||||
scan per run (retries never duplicate — preserve-first)
|
||||
- `plan` → {2: 239, 3: 251, 4: 129, 5: 544, 6: 415, 7: 274} = 1,852,
|
||||
zero unassigned
|
||||
- `sample --wave 5` → full §7 template holds; `_compute_hash` exactly once as
|
||||
labelled callee with line number
|
||||
- Contract fix during Wave 0: `--limit` default changed 5 → 0 (unlimited) for
|
||||
`create`; `sample` keeps its 5. A full-wave create with the old default
|
||||
would have silently truncated each wave to five issues.
|
||||
|
||||
## 3. Interop proof unit (R1 §5)
|
||||
|
||||
- Rust unit at interop/rebase-proof/ (std-only, zero crates): argv in → JSON
|
||||
on stdout, non-zero exit on failure, mandatory `--version`
|
||||
- Shell-proven: version flag, JSON out, exit 2 on failure, stderr diagnostic
|
||||
- Python thin adapter shared/native_units.py: typed NativeResult, timeout +
|
||||
stderr capture, unit failures as data (never exceptions)
|
||||
- Adapter contract fix during Wave 0: `ok` no longer requires a JSON
|
||||
`ok:true` field — plain-text output (`--version`) succeeds on exit code
|
||||
- tests/test_native_interop_proof.py: **8 passed** — version contract, sum
|
||||
happy path, hash determinism, failure paths, timeout, stderr capture
|
||||
|
||||
## 4. Per-wave issue creation
|
||||
|
||||
<!-- FILLED BY THE COMPLETION LOOP — do not hand-edit -->
|
||||
|
||||
- Wave 4 (129 units): created=127 skipped(dedup)=2 (includes smoke #417–418)
|
||||
- Wave 2 (239 units): created=239 skipped(dedup)=0
|
||||
- Wave 3 (251 units): created=251 skipped(dedup)=0
|
||||
- Wave 5 (544 units): created=544 skipped(dedup)=0
|
||||
- Wave 6 (415 units): created=415 skipped(dedup)=0
|
||||
- Wave 7 (274 units): created=274 skipped(dedup)=0
|
||||
- TOTAL: 1,850 created + 2 dedup = 1,852 / 1,852 ✅ full coverage
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"units_per_wave": {
|
||||
"2": 239,
|
||||
"3": 251,
|
||||
"4": 129,
|
||||
"5": 544,
|
||||
"6": 415,
|
||||
"7": 274
|
||||
},
|
||||
"unassigned_modules": {},
|
||||
"unassigned_module_count": 0
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# SUPERSESSION REPORT — programmatic evidence
|
||||
_Created 2026-10-07. Question: is curator superseded by memory_consolidation? Can supersession be determined programmatically?_
|
||||
|
||||
## Answer in one paragraph
|
||||
|
||||
No — they are NOT supersessions of each other. They are two different machines in one pipeline: **memory_consolidation compresses what already exists** (warm neurons, graph entities, abstraction facts → consolidated knowledge records), while **curator gates what gets written** (evidence → worthiness filter → graded routing → tiers). Both are wired into sleep_time's nightly run: consolidation runs in-process, curator runs as a subprocess. The real finding is darker: **curator has NEVER successfully run in production** — three of its four dependencies were never ported from the Odysseus branch, so every invocation died at import. Both diagnoses were reached programmatically.
|
||||
|
||||
## The programmatic supersession test (reusable)
|
||||
|
||||
Six signals, each binary and evidence-based (`/tmp/ss_engine.py`, promote to scripts/):
|
||||
|
||||
| Signal | Test | Weight |
|
||||
|---|---|---|
|
||||
| R1 Import graph | production importers vs canary-only importers | high |
|
||||
| R2 Subprocess summon | module executed as a script by another module | high |
|
||||
| R3 Output artifacts | documented output files exist with recent mtimes | high |
|
||||
| R4 Function overlap | name overlap with the suspected successor | low (weak signal) |
|
||||
| R5 Importability | `import X` succeeds (phantom dep = dead) | gate — fails = dead regardless |
|
||||
| R6 End-to-end probe | documented entry path executes with real input | decisive |
|
||||
|
||||
## Applied to curator vs memory_consolidation
|
||||
|
||||
| Signal | curator | memory_consolidation |
|
||||
|---|---|---|
|
||||
| R1 | 0 production importers | 2 (memory_selector, sleep_time) |
|
||||
| R2 | summoned as script by sleep_time + dependency_tracker | summoned by dependency_tracker |
|
||||
| R3 | **index/blocks_meta.json DOES NOT EXIST — no journal files — never wrote output** | graph.sqlite exists but **stale (Aug 22)**; consolidation/*.json never created |
|
||||
| R4 | 2% overlap — different function sets (confirms: different jobs) | 2% |
|
||||
| R5 | importable NOW (after repair); dead since #314 | importable |
|
||||
| R6 | dry-run probe NOW green (post-repair); was JSONDecodeError-crash | consolidate() wired, but its outputs absent too |
|
||||
|
||||
## Verdict
|
||||
|
||||
1. **NOT a supersession.** 2% function overlap + different roles: curator = write-path gate (worthiness → grade → tier), memory_consolidation = read-path compressor (existing memories → knowledge records). One writes, one compresses. Both belong.
|
||||
2. **Both are broken in production.** Curator never ran (phantom deps). memory_consolidation's outputs are stale/absent — its last graph write was Aug 22. The whole sleep-time memory pipeline has been quietly degraded.
|
||||
3. **Disposition: REPAIR, not retire.** grading.py + worthiness.py are now recovered from the Odysseus source branch (provenance, not reconstruction) with frozen tests; curator's dry-run executes end-to-end. Remaining phantom for curator: lexicon_evolution (guarded — degrades), aaak (guarded — optional compression). Both degradable, not blocking.
|
||||
|
||||
## The phantom-import census (the general finding)
|
||||
|
||||
16 phantom imports found across src/rune. The important ones:
|
||||
|
||||
| Module | Importers | Guarded? | Disposition |
|
||||
|---|---|---|---|
|
||||
| worthiness | curator | NO (hard crash) | ✅ RECOVERED from Odysseus + frozen tests |
|
||||
| grading | curator | NO | ✅ RECOVERED from Odysseus + frozen tests |
|
||||
| lexicon_evolution | curator | yes (degrades) | recover-or-rename decision needed |
|
||||
| aaak | curator, memory_store, warm_router | yes | optional compression; recover or accept degrade |
|
||||
| build_pipeline_policy | prompt_sync | NO | phantom — prompt_sync is dead on arrival |
|
||||
| gitea_auth | forge | NO | forge.py dead; is forge itself superseded? |
|
||||
| faster_whisper, pynput | voicebox | yes | optional deps — correct as-is |
|
||||
| research_lens | claim_audit | yes | lives in ~/.config/opencode/scripts — cross-repo dep |
|
||||
| ep_loader | 5 modules | false positive | resolves via sys.path hack to src/rust/ |
|
||||
| autonomy_loop, cognitive_loop, dream_cycle | rune_tui | yes | TUI plugins — recover or retire |
|
||||
| commitments, dedupe_policy, local_memory | warm_router, memory_store, sleep_time | yes | recover-or-accept-degrade |
|
||||
|
||||
**Hard-crash phantoms mean the owning module is dead code:** prompt_sync, forge, and (until today) curator.
|
||||
|
||||
## What the dictonomy rebase inherits
|
||||
|
||||
The Go/Rust port queue should SKIP dead code until its disposition is decided. A module that never ran has no behavioral parity to prove — porting it would faithfully reproduce a ghost. The audit instrument now needs an R5 importability column so dead modules are marked before they're counted.
|
||||
@@ -10,6 +10,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 |
|
||||
| `tui_transport.py` | Not a surface — the transport seam for the TUI's operator calls (in-process default / /v1 wire opt-in) | H4 slice 1 (contract BUILDER-2026-10-04T10:32:00Z-cli); RUNE_TUI_TRANSPORT selects; coverage: canary_tui_transport.py |
|
||||
|
||||
## Adding a TUI surface
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1.27.1
|
||||
+39
-2
@@ -1,7 +1,31 @@
|
||||
settings:
|
||||
warn_about_similar_files:
|
||||
above: 0.85
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
|
||||
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**",
|
||||
# shared/ is the DEPLOYED-VINTAGE mirror tree (strangler era):
|
||||
# every src/rune <-> shared similarity here is intentional
|
||||
# (the shim delegates to the crate; the mirror is the deployed
|
||||
# vintage it strangles). Registering the deliberate pairs.
|
||||
"shared/**",
|
||||
"src/rune/drift_core.py", "src/rune/drift_compression.py",
|
||||
"src/rune/drift_observability.py", "src/rune/shard_manager.py",
|
||||
"src/rune/subconscious_pass.py",
|
||||
# deliberate mirror pairs outside the strangler set:
|
||||
# issue templates are mirrored into shared/issue_templates;
|
||||
# completion_file.py is the bridge/daemon mirror of scripts/;
|
||||
# interop/rebase-proof is a committed proof fixture.
|
||||
".gitea/ISSUE_TEMPLATE/**", "shared/issue_templates/**",
|
||||
"bridge/daemon/completion_file.py", "scripts/completion_file.py",
|
||||
"interop/rebase-proof/**",
|
||||
# per-crate copies by zero-dep design: each crate is
|
||||
# self-contained (no shared crate dependency), so the pyjson
|
||||
# JSON module is copied verbatim per crate and the tiny
|
||||
# zero-dep lockfiles are near-identical by construction.
|
||||
"src/rust/*/Cargo.lock", "src/rust/*/src/pyjson.rs",
|
||||
"src/rust/query_memory/src/json_min.rs",
|
||||
# store/scaffold is data (proof fixtures, gate objects),
|
||||
# same class as the already-ignored store/spine.
|
||||
"store/scaffold/**"]
|
||||
|
||||
henxels:
|
||||
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
|
||||
@@ -20,7 +44,7 @@ henxels:
|
||||
- henxel: "Top-level docs are markdown or yaml references"
|
||||
in: ./docs/*
|
||||
allowed_filetypes: [.md, .yaml]
|
||||
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json"]
|
||||
except: ["./docs/v1/**", "./docs/wiki/**", "./docs/gitea_issue_reconciliation.json", "./docs/rebase/**"]
|
||||
|
||||
- henxel: "The known giant stays visible — file-size debt is tracked, not silent"
|
||||
in: ./src/rune/*
|
||||
@@ -70,6 +94,19 @@ henxels:
|
||||
in: ./src/rune/*.py
|
||||
new_rune_modules_have_canary_or_test_coverage: true
|
||||
|
||||
- henxel: "Rust crate files stay under a line budget"
|
||||
in: ./src/rust/*/src/*.rs
|
||||
except: ["./src/rust/*/src/pyjson.rs", "./src/rust/*/src/json_min.rs"]
|
||||
max_lines: 3000
|
||||
|
||||
- henxel: "Go modules stay under a line budget"
|
||||
in: ./src/go/**/*.go
|
||||
max_lines: 2000
|
||||
|
||||
- henxel: "No secrets in Rust or Go source files"
|
||||
in: [./src/rust/*/src/*.rs, ./src/go/**/*.go]
|
||||
no_secrets: true
|
||||
|
||||
|
||||
- henxel: "PRDs are living documents — their generated state sections regenerate identically from governed sources"
|
||||
changed_with:
|
||||
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "rebase-proof"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Wave 0 interop proof unit (R1 §5). Std-only by design: no crates, no network,
|
||||
# deterministic build. The pattern under proof is the CLI boundary itself, not the
|
||||
# computation inside it.
|
||||
|
||||
[[bin]]
|
||||
name = "rebase-proof"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Wave 0 interop proof unit (R1 §5 of the language-rebasing tranche).
|
||||
//!
|
||||
//! Contract under proof:
|
||||
//! - standalone binary; argv in, JSON on stdout
|
||||
//! - non-zero exit on failure (JSON error object still printed to stdout)
|
||||
//! - `--version` flag mandatory
|
||||
//! - Python wraps it in a thin adapter (`shared/native_units.py`); tests call
|
||||
//! it the same way.
|
||||
//!
|
||||
//! Ops: sum / echo / hash (fnv1a64) / sleep / fail. All std-only.
|
||||
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// FNV-1a 64-bit — dependency-free deterministic hash for the proof unit.
|
||||
fn fnv1a64(data: &[u8]) -> u64 {
|
||||
let mut hash: u64 = 0xcbf29ce484222325;
|
||||
for byte in data {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
fn print_json(line: &str) {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
fn emit_ok(body: &str) {
|
||||
print_json(&format!("{{\"ok\":true,{body}}}"));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fn emit_err(message: &str) -> ! {
|
||||
// Escape the minimal set so the JSON stays parseable for arbitrary op names.
|
||||
let escaped: String = message
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
print_json(&format!("{{\"ok\":false,\"error\":\"{escaped}\"}}"));
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
|
||||
if args.as_slice() == ["--version"] || args.as_slice() == ["-V"] {
|
||||
println!("rebase-proof {VERSION}");
|
||||
exit(0);
|
||||
}
|
||||
if args.is_empty() {
|
||||
eprintln!("usage: rebase-proof <op> [args...] | --version");
|
||||
emit_err("no op given");
|
||||
}
|
||||
|
||||
let op = args[0].as_str();
|
||||
let rest = &args[1..];
|
||||
|
||||
match op {
|
||||
"sum" => {
|
||||
let mut total: i128 = 0;
|
||||
for a in rest {
|
||||
match a.parse::<i128>() {
|
||||
Ok(v) => total += v,
|
||||
Err(_) => emit_err(&format!("sum: not an integer: {a}")),
|
||||
}
|
||||
}
|
||||
emit_ok(&format!("\"op\":\"sum\",\"result\":{total}"));
|
||||
}
|
||||
"echo" => {
|
||||
let joined = rest.join(" ");
|
||||
let escaped: String = joined
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'"' => "\\\"".to_string(),
|
||||
'\\' => "\\\\".to_string(),
|
||||
'\n' => "\\n".to_string(),
|
||||
'\r' => "\\r".to_string(),
|
||||
'\t' => "\\t".to_string(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
emit_ok(&format!("\"op\":\"echo\",\"text\":\"{escaped}\""));
|
||||
}
|
||||
"hash" => {
|
||||
let joined = rest.join(" ");
|
||||
let h = fnv1a64(joined.as_bytes());
|
||||
emit_ok(&format!("\"op\":\"hash\",\"fnv1a64\":\"{h:016x}\""));
|
||||
}
|
||||
"sleep" => {
|
||||
let ms: u64 = match rest.first().map(|s| s.parse::<u64>()) {
|
||||
Some(Ok(v)) => v,
|
||||
_ => emit_err("sleep: expected milliseconds"),
|
||||
};
|
||||
thread::sleep(Duration::from_millis(ms));
|
||||
emit_ok(&format!("\"op\":\"sleep\",\"slept_ms\":{ms}"));
|
||||
}
|
||||
"fail" => {
|
||||
// Deliberate failure: stderr diagnostic + JSON error + exit 2.
|
||||
// Proves stderr capture and non-zero exit in the adapter.
|
||||
eprintln!("rebase-proof: deliberate failure for adapter proof");
|
||||
emit_err("deliberate failure");
|
||||
}
|
||||
other => emit_err(&format!("unknown op: {other}")),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 347,
|
||||
"BLE001": 345,
|
||||
"S110": 75
|
||||
}
|
||||
@@ -1 +1,401 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"capabilities": [
|
||||
"read_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_file read a file with optional windowed viewing (offset/limit). returns numbered lines. use offset/limit for large files (100 lines at a time). native tool"
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"capabilities": [
|
||||
"edit_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"search string matches exactly once"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites the target file atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "edit_file surgical search/replace edit. more reliable than full file rewrites. the search string must match exactly. native tool"
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"capabilities": [
|
||||
"write_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory"
|
||||
],
|
||||
"side_effects": [
|
||||
"writes the full file content atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "write_file write content to a file. creates parent directories if needed. shows diff before writing. use for new files or full rewrites. native tool"
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"capabilities": [
|
||||
"glob_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "glob_files find files matching a glob pattern. returns file paths sorted by modification time. native tool"
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"capabilities": [
|
||||
"grep_files"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "grep_files search for a pattern in files. returns file paths and line numbers. use include to filter by file type. native tool"
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"capabilities": [
|
||||
"list_directory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "list_directory list files and directories at a path. returns names with / suffix for directories. native tool"
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"capabilities": [
|
||||
"lint_file"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "lint_file lint a file for syntax errors and style issues. blocks bad edits. supports python (ruff), js/ts (eslint), go (golangci-lint), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"capabilities": [
|
||||
"run_tests"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_tests run tests for a file or project. auto-detects test runner from language. native tool"
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"capabilities": [
|
||||
"format_code"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"a formatter is configured for the language"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites files in place to the formatter's style"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "format_code auto-format code files. supports python (black/ruff), js/ts (prettier), go (gofmt), and more. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"capabilities": [
|
||||
"git_diff"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_diff show git diff for staged or unstaged changes. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"capabilities": [
|
||||
"git_log"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_log show recent git commits. native tool"
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"capabilities": [
|
||||
"git_commit"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "git_commit commit changes with a message. auto-stages modified files. native tool"
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"capabilities": [
|
||||
"repo_map"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "repo_map generate an ast-based codebase map showing key symbols (classes, functions, imports). uses tree-sitter for 100+ languages. native tool"
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"capabilities": [
|
||||
"run_command"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "run_command run an approved command. prefer argv (shell=false); shell syntax requires shell=true and explicit user approval. native tool"
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"capabilities": [
|
||||
"read_persona"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "read_persona read rune's persona definition. returns the full persona block with voice, identity, and rules. native tool"
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"capabilities": [
|
||||
"query_memory"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "query_memory query rune's memory store. returns relevant memories for a query. native tool"
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"capabilities": [
|
||||
"check_growth"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "check_growth check rune's growth log. returns recent growth entries and patterns. native tool"
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"capabilities": [
|
||||
"diagnose_behavior"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "diagnose_behavior diagnose rune's current behavior. returns analysis of persona adherence, voice consistency, and drift. native tool"
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"capabilities": [
|
||||
"find_capability"
|
||||
],
|
||||
"file": "tool_executor.py",
|
||||
"full_path": "",
|
||||
"priority": 100,
|
||||
"semantic_examples": [],
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"auto_invoked": false,
|
||||
"hooks": [],
|
||||
"native_tool": true,
|
||||
"searchable_text": "find_capability search all installed capabilities for what you need. returns matching tool names with descriptions and argument schemas. use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session. native tool"
|
||||
}
|
||||
]
|
||||
+38886
-15
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
# Phase G slice 1 — retrieval-index authority supersession ledger
|
||||
|
||||
**Date:** 2026-10-02 · **Branch:** `phase-g-strangler-retrieval-index` · **Unit issue:** conductor/rune#2448
|
||||
|
||||
## What was superseded
|
||||
|
||||
The pure-Python `query_memory` implementation (formerly `src/rune/query_memory.py`,
|
||||
deployed at `~/.config/opencode/scripts/query_memory.py`) is RETIRED as a production
|
||||
implementation. Production authority now rests with:
|
||||
|
||||
1. **Implementation:** Rust crate `src/rust/query_memory/` (lib.rs core + cdylib, ten
|
||||
`extern "C"` FFI symbols, zero external crates).
|
||||
2. **Production service surface:** `shared/query_memory.py` — a ctypes-backed shim
|
||||
(179 lines) over `qm_loader.py`, byte-deployed to `~/.config/opencode/scripts/query_memory.py`,
|
||||
serving the FULL prior Python class API including the `self.history` snapshot.
|
||||
|
||||
## Strangler doctrine (master doc lines 685–770) — step discharge
|
||||
|
||||
| Step | Requirement | Evidence |
|
||||
|------|-------------|----------|
|
||||
| 1–4 | candidate built, contract frozen, parity proven | commits `9b76585`+`54d2db9`; `/tmp/phaseG_retrieval_index_contract.md` frozen; parity harness ALL GREEN |
|
||||
| 5 | qualification | `/tmp/phaseG_qual_bench.out` (Rust CLI ~45× faster end-to-end); canaries both sides |
|
||||
| 6 | canaries conformance | Python canary 12/12; Rust canary 27/27 |
|
||||
| 7 | shadow comparison vs live store | `/tmp/phaseG_shadow.py` 46/46 exact-equal vs 77-record production store |
|
||||
| 8 | authority switch + observed consumer evidence | commit `6f44a87`; both consumers verified live — `memory_store.py` (lines 820–821, live 76-record store, nested stats shape) + `growth_simulator.py` (record_query through Rust core); #2448 comment id 33795 |
|
||||
| 9 | runtime evidence post-switch | post-switch verification wall ALL GREEN: Python canary 12/12, FFI smoke 41/41, Rust canary 27/27, parity harness ALL GREEN |
|
||||
| 10 | old production path unreachable | ALL import paths (`import query_memory`) now resolve to the deployed shim; `shared/graph_shard_bridge.py` tolerant import serves the shim; the orphan `src/rune/query_memory.py` had ZERO importers (verified by sweep incl. path-string references) |
|
||||
| 11 | old implementation removed or reference-only | `src/rune/query_memory.py` DELETED (this commit); the only stale reference (`scripts/structural_coverage_audit.py` registry mapping shared/ as a mirror of src/rune) corrected — query_memory removed from `SRC_A`/`SHARED_D`, added to `SHARED_B` (canonical, ops-harness owned) |
|
||||
| 12 | supersession ledger | THIS FILE |
|
||||
|
||||
Deletion discipline (language-governance rule ⑥) satisfied: old code removed only after
|
||||
the new implementation was PROVEN — wall green, shadow 46/46, both production consumers
|
||||
observed live on the Rust core before deletion.
|
||||
|
||||
## Consumer map at supersession
|
||||
|
||||
- `~/.config/opencode/scripts/memory_store.py` — in-process, `adaptive_exploration_rate` (line 820).
|
||||
- `~/.config/opencode/scripts/growth_simulator.py` — in-process, `record_query`.
|
||||
- `shared/graph_shard_bridge.py` — tolerant import (try/except), follows the switch.
|
||||
- `src/rune/tool_executor.py` — NOT a consumer (nominal grep hit only; goes through `memory_store.cached_recall`).
|
||||
|
||||
## Verification commands (reproducible)
|
||||
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rune/canary_query_memory.py` → 12/12
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rust/query_memory/canary_rust.py` → 27/27
|
||||
- `/home/nick/.venvs/memory/bin/python3 src/rune/ffi_query_memory_smoke.py` → 41/41
|
||||
- `/home/nick/.venvs/memory/bin/python3 /tmp/phaseG_parity.py` → ALL GREEN
|
||||
- `/home/nick/.venvs/memory/bin/python3 scripts/structural_coverage_audit.py` → exit 0, coverage 100%
|
||||
@@ -0,0 +1,66 @@
|
||||
# Phase G Slice 2 — FROZEN CONTRACT: evidence-provenance (T7.3)
|
||||
|
||||
Frozen: 2026-10-03, session ses_ffe5f08a7892affeGzZWhhm0wR. Branch `phase-g-strangler-evidence-provenance` (cut from slice-1 head `2ac2d0a`). Gitea issue #2449. Nothing below may change without a recorded amendment in this file.
|
||||
|
||||
## 1. Scope (observed, not inferred)
|
||||
|
||||
Six modules in `src/rune/`, 1796 lines total, one test file each as marked:
|
||||
|
||||
| Module | Lines | Surface (grep-verified 2026-10-03) |
|
||||
|---|---|---|
|
||||
| `audit_ledger.py` | 231 | `_compute_hash(prev_hash, entry)`, `append_entry(entry_type, entry_id, details)`, `verify_chain() -> {valid, entries, reason}`, `get_entries(limit=50)`, `log_promotion(entry_id, old_status, new_status, wash_count, reason)`, `log_gate_result(entry_id, verdict, critics)`, `log_drift_alert(alert_type, message, severity)`, `main()` |
|
||||
| `behavior.py` | 411 | `connect()`, `add_correction(original, corrected, scope_tags=None, domain="", pattern="")`, `add_habit(trigger, action, name="", context_tags=None, min_activations=3)`, `match_context(context, match_type=None)`, `reinforce(item_id, outcome, item_type="habit")`, `list_items(item_type="all", limit=20)`, `behavior_stats()`, `main()`, `PLUGIN` dict |
|
||||
| `behaviour_ledger.py` | 251 | 9 state constants (UNVALIDATED…NOT_APPLICABLE) + `STATES` + `CONFIRMED_STATES` tuples, `BehaviourLedger` class |
|
||||
| `claim_audit.py` | 430 | `PATTERNS` list, `EVIDENCE_TAG` regex, `LEDGER_MARKERS`, `reframe(text)`, `scan(text)`, `verify(claim)`, `degrade(claim, verdict)`, `journal(entry)`, `report()`, `audit(draft)`, `handle_hook(event, data=None)`, `main()`, `PLUGIN` |
|
||||
| `evidence_probe.py` | 214 | `PROBE_STATUSES`, `PROBE_FIELDS`, `MAX_PROBES_PER_TASK=2`, `ProbeBudgetExceeded`, `ProbeResult`, `build_probe_prompt(question, artifact_path)`, `validate_probe(artifact)`, `count_probes(artifact_dir)`, `check_budget(artifact_dir)`, `record_probe(result, artifact_dir)`, `run_probe(question, artifact_dir, runner=None, model="UNKNOWN")` |
|
||||
| `verification_layer.py` | 259 | `VerificationResult`, seven `verify_*` functions (exit_code, unit_test, schema, file_exists, file_changed, tool_receipt, constraint_check), `verify(result, verifier_type)`, `handle_hook`, `PLUGIN` |
|
||||
|
||||
Plus `shared/audit_ledger.py` (210 ln) — OLDER vintage of audit_ledger.
|
||||
|
||||
## 2. Authority decisions (per module)
|
||||
|
||||
- **src/rune/ is the authority vintage for all six modules** — hardened newer code (memory_env-pathed storage, defensive handling). The deployed `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` copies are OLDER vintages (hardcoded expanduser paths). The authority switch is a VINTAGE UPGRADE plus a language swap — not a mirror replacement.
|
||||
- `shared/audit_ledger.py` is superseded alongside the deployed copy (same older vintage, same defects).
|
||||
|
||||
## 3. Live-data contract (the linchpin — observed)
|
||||
|
||||
- **Live ledger:** `/home/nick/.config/opencode/memory/promotion_ledger.jsonl` (21 entries). `verify_chain()` under the deployed module's own defaults returns `{'valid': True, 'entries': 21, 'reason': 'chain intact'}` — OBSERVED 2026-10-03.
|
||||
- **Live signing key: `rune-drift-integrity-2024`** (the deployed/shared default). The src/rune default `rune-governed-adaptation-2024` FAILS the live chain — it is a latent break, recorded as a defect this slice fixes.
|
||||
- **Hash scheme (both vintages identical):** `HMAC-SHA256(key, (prev_hash + json.dumps(entry_without_hash, sort_keys=True)).encode()).hexdigest()`; genesis prev_hash = `"genesis"`; entry lines written with `json.dumps(entry)` in insertion order (type, entry_id, details, timestamp, prev_hash, hash); timestamp = `datetime.now(timezone.utc).isoformat()`.
|
||||
- **Key rule for the Rust candidate:** read `AUDIT_LEDGER_KEY` env; DEFAULT = `rune-drift-integrity-2024` (the key that verifies the live chain). Never default to the governed key — it verifies nothing that exists.
|
||||
- **Live `behavior.db`:** SQLite at memory_env memory dir, touched Sep 24 — must remain readable by the Rust candidate's storage layer.
|
||||
|
||||
## 4. Rust candidate design
|
||||
|
||||
Crate `/mnt/workdrive/rune/src/rust/evidence_provenance/` following the proven slice-1 shape: zero-dep core in `lib.rs`, `cdylib` FFI exports, hand-rolled JSON serialization where byte-parity demands it. **Port order within the slice** (dependency + risk order):
|
||||
|
||||
1. **audit_ledger** — linchpin: live data, external consumers, key contract.
|
||||
2. **verification_layer** — pure logic, no storage, one test file; fastest port.
|
||||
3. **evidence_probe** — file-based budget/records; `rune_operator.py:581` consumer.
|
||||
4. **behaviour_ledger** — state machine, src/rune-only.
|
||||
5. **behavior** — SQLite habits; live DB; deployed vintage upgrade + `rune_tui.py` consumer.
|
||||
6. **claim_audit** — heaviest (warm-neuron store + graph DB + regex engine); last.
|
||||
|
||||
Python-side ctypes loaders (`*_loader.py`) + deployed shims per slice-1 pattern, so every consumer keeps its import surface unchanged.
|
||||
|
||||
## 5. Parity + qualification criteria
|
||||
|
||||
- **Byte-parity:** Rust `verify_chain` output byte-identical to Python's on the live ledger AND on a mutation battery (tampered entry, broken chain, invalid JSON, empty ledger). Parity harness both sides, slice-1 pattern.
|
||||
- **Canaries:** Python-side canary module per ported module + Rust canary; both green before any switch.
|
||||
- **Shadow:** shadow-compare Rust vs Python on live-shaped data before authority switch.
|
||||
- **Performance:** like-for-like benchmark on the real consumption shape; no regression (Nick's law: combination does not decrease speed).
|
||||
|
||||
## 6. Consumers + adoption plan (Nick's directive: adoption during the refactor)
|
||||
|
||||
| Consumer | Imports | Switch |
|
||||
|---|---|---|
|
||||
| `~/.config/opencode/scripts/curator.py` | audit_ledger | at audit_ledger switch |
|
||||
| `canary_tui_drift.py`, `canary_full_system.py` | audit_ledger | at audit_ledger switch |
|
||||
| `rune_tui.py` (L62, L83) | claim_audit, behavior | at each module's switch |
|
||||
| `rune_operator.py:581` | evidence_probe (relative) | at evidence_probe switch |
|
||||
|
||||
Deployed copies become ctypes shims at each module's authority switch (vintage upgrade included — the shim serves the src/rune contract, not the old vintage's). Old deployed Python deletions follow language-governance ⑥: only after the new is proven.
|
||||
|
||||
## 7. Exit criteria
|
||||
|
||||
All twelve doctrine steps discharged per module-port with observed evidence on #2449; coverage-audit registry remapped; supersession ledger written; audit regression gate green.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Phase G slice 2 — evidence-provenance authority supersession ledger
|
||||
|
||||
**Date:** 2026-10-03 · **Branch:** `phase-g-strangler-evidence-provenance` · **Unit issue:** conductor/rune#2449
|
||||
|
||||
## What was superseded
|
||||
|
||||
The pure-Python evidence-provenance implementations (six modules in `src/rune/`, 1796 lines; plus the two OLD deployed vintages and `shared/audit_ledger.py`) are RETIRED as production implementations for their ported surfaces. Production authority now rests with:
|
||||
|
||||
1. **Rust crate `src/rust/evidence_provenance/`** — six cores: `lib.rs` (audit_ledger chain + FFI), `verification.rs` (7 verifiers), `probe.rs` (schema gate + budget), `behaviour.rs` (state machine), `habitmath.rs` (reinforce math), `claims.rs` (claim-audit decision skeleton); cdylib FFI (`ep_*` symbols, 21 total), zero external crates.
|
||||
2. **Python shims** at `src/rune/{audit_ledger,verification_layer,evidence_probe,behaviour_ledger,behavior,claim_audit}.py` — serving the src/rune authority contract through `ep_loader.py` (in-process, no interpreter startup for the ported logic).
|
||||
3. **Deployed shims** at `~/.config/opencode/scripts/{audit_ledger,behavior,claim_audit}.py` — same shape; audit_ledger + claim_audit + behavior carry the LIVE data-path rules (drift-key ledger, live behavior.db, real journal); claim_audit additionally carries the preserved SYMMETRY GUARD (deployed-vintage feature) and the repaired `scan_text()` TUI surface.
|
||||
|
||||
## Port ledger (all six, in contract §4 order)
|
||||
|
||||
| Port | Module | Rust core | Parity | Commit | Consumer proof |
|
||||
|------|--------|-----------|--------|--------|----------------|
|
||||
| 1 | audit_ledger | lib.rs chain ops | shadow 18/18; canaries 28/28+23/23 | `114c394`+`4b0d1d2` | live ledger entries 4242 (post-switch write), verify 23-intact |
|
||||
| 2 | verification_layer | verification.rs | 33/33 (7 verifiers + CLI render) | `9cb93e6` | contracts.py, canary_native, rune_operator:323 dataclass |
|
||||
| 3 | evidence_probe | probe.rs | 24/24 + consumer tests 23/23 | `3450a1c` | rune_operator:581/603, research_stage_executor:312 |
|
||||
| 4 | behaviour_ledger | behaviour.rs | 13/13 full-scenario | `8973caa` | testing_mode:19, iteration:23, rune_tui:3152 |
|
||||
| 5 | behavior | habitmath.rs | 16/16 + full-stack live-db | `2732677` | rune_tui:83; deployed vintage upgraded (live db read-verified) |
|
||||
| 6 | claim_audit | claims.rs | 17/17 + real-journal aggregate (1127 rows) | `969e39f` | rune_tui:62/2468/3752, contracts.py, hook_engine:270 |
|
||||
|
||||
Errors: byte-equal (duplicate/unknown-behaviour/unknown-state; probe schema-gate lists incl. Python repr rendering; CLI argparse shapes + exit codes).
|
||||
|
||||
## Strangler doctrine (master doc lines 685–770) — step discharge (slice level)
|
||||
|
||||
| Step | Requirement | Evidence |
|
||||
|------|-------------|----------|
|
||||
| 1–4 | candidate built, contract frozen, parity proven | commits `114c394` → `969e39f`; frozen contract `plans/2026-10-03-phase-g-evidence-provenance-contract.md` (commit `7c68e41`); per-port parity harnesses ALL GREEN |
|
||||
| 5 | qualification | per-port parity + decision-math batteries; canary pair per port (Python 74/74, Rust 66/66 — all six port sections accumulated); performance follows the slice-1 in-process shape (no interpreter startup; same cdylib) |
|
||||
| 6 | canaries conformance | canary_audit_ledger.py 74/74; canary_evidence_provenance_rust.py 66/66 (per-port sections for ALL six ports; dual-mode LIVE/ISOLATED for live-ledger pins — run 18141 repair); embedded tests through shims (verification test, 23 consumer tests) |
|
||||
| 7 | shadow comparison vs live-shaped data | port-1 shadow 18/18 vs `939883d:shared/audit_ledger.py` (banked at `114c394`); ports 2–6 use direct byte-parity batteries vs the live originals (same discipline, stronger: full-document byte-quality) |
|
||||
| 8 | authority switch + observed consumer evidence | shims deployed + live-verified per port: entry 4242 (first post-switch ledger write), live behavior.db read through the port-5 shim, real 1127-row journal through the port-6 aggregation, TUI backend trio imports, TUI scan_text FIRST-EVER live call |
|
||||
| 9 | runtime evidence post-switch | post-switch walls per port: parity round-trips, embedded tests, consumer chains (rune.behaviour_ledger / rune.evidence_probe / rune.claim_audit + deployed trio) — all GREEN |
|
||||
| 10 | old production path unreachable | `import audit_ledger` (deployed consumers) → shim; `rune.behaviour_ledger` / `rune.evidence_probe` / `rune.claim_audit` / `rune.verification_layer` → shims; `shared/audit_ledger.py` had ZERO importers (verified by sweep incl. path-string forms) — DELETED |
|
||||
| 11 | old implementation removed or reference-only | `shared/audit_ledger.py` DELETED (this commit, after the proven wall + zero-importer sweep); six src/rune modules remain as shims (the API contract); original Python lives in git history. Coverage registry remapped: `audit_ledger` removed from SHARED_D; gate exit 0 post-deletion |
|
||||
| 12 | supersession ledger | THIS FILE |
|
||||
|
||||
Deletion discipline (language-governance rule ⑥): `shared/audit_ledger.py` removed only after (a) the deployed shim proved live against the real ledger, (b) the shadow wall was green, (c) a zero-importer sweep across src/ shared/ scripts/ tests/. The remaining five src/rune originals were rewritten AS shims in their own commits (git history holds each original); they are the API contract and stay.
|
||||
|
||||
## Vintage-merge disclosures
|
||||
|
||||
- **SYMMETRY GUARD preserved:** the deployed `claim_audit` vintage carried `research_lens.symmetry_check` integration (2026-08-15) absent from src/rune — preserved in both shims (feature-preserving merge; load-bearing behavior).
|
||||
- **DEAD CONSUMER REPAIRED:** `rune_tui.py:3752` `claim_audit.scan_text()` had NO implementation in either vintage (silent try/except death) — now implemented, first-class, live-verified.
|
||||
- **Sequence break (port 1):** the audit_ledger shim was deployed BEFORE its canary pair existed; both canaries subsequently written and green — disclosed on #2449 comment 33817, not retrofitted.
|
||||
|
||||
## Consumer map at supersession
|
||||
|
||||
- `~/.config/opencode/scripts/rune_tui.py` — claim_audit (report L2468, scan_text L3752), behaviour (L3152), behavior (L83).
|
||||
- `src/rune/rune_operator.py` — evidence_probe (L581/603), VerificationResult dataclass (L323).
|
||||
- `src/rune/contracts.py` — dynamic `__import__` surfaces (verification_layer, claim_audit).
|
||||
- `src/rune/testing_mode.py:19` / `src/rune/iteration.py:23` — behaviour_ledger constants + class.
|
||||
- `scripts/research_stage_executor.py:312` — evidence_probe orchestration.
|
||||
- `~/.config/opencode/scripts/curator.py` / `canary_full_system.py` — audit_ledger (deployed shim).
|
||||
- `src/rune/dependency_tracker.py` — map-strings only (NOT an import).
|
||||
|
||||
## Live-data rules (contract §3) — honored in the shims
|
||||
|
||||
- Live promotion ledger: drift key verified 23-intact through the shim + FFI; explicit `AUDIT_LEDGER_KEY` override honored.
|
||||
- Live behavior.db: read/written through the deployed shim at `~/.config/opencode/memory/behavior.db`; parity scenarios ran on temp dirs only.
|
||||
- Live claim_audit journal (1127 rows): read-only aggregate through the Rust core; writes stay Python.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Phase H plan — Go front end / Rust back end (Nick's architecture directive)
|
||||
|
||||
Status: PLAN (awaiting Nick's sign-off before any code)
|
||||
Directive: USER-LOCKED (2026-10-04). Supersedes the KEEP-PYTHON verdicts in
|
||||
docs/rebase-language-rubric.md wherever they conflict.
|
||||
|
||||
## 1. The directive
|
||||
|
||||
Go owns the front end: runtime entry points, the first-call surface, the
|
||||
user-facing interface and its calls. Rust owns the back end: every core
|
||||
logic module. Python survives only as transitional glue inside a module
|
||||
until its own Go/Rust surface lands.
|
||||
|
||||
## 2. What "front end" means concretely here
|
||||
|
||||
From the runtime-audit memory (2026-09-10): the live entry chain is
|
||||
rune_tui -> send_task -> ContextBroker -> ModelRouter -> local/Cloud
|
||||
transports. The user-facing calls are: the TUI process itself, the CLI
|
||||
entry (python -m rune / rune binary), the operator message path, and the
|
||||
OpenAI-compatible /v1 endpoints MiMoCode serves.
|
||||
|
||||
Candidates for the Go first-call layer, in dependency order:
|
||||
- rune binary (process entry, arg parsing, process lifecycle) — replaces
|
||||
the Python module-loading bootstrap.
|
||||
- CLI/IPC command dispatch — the first-call surface that routes a user
|
||||
command to the right back-end service.
|
||||
- The OpenAI-compatible HTTP endpoint surface (/v1 chat completions) —
|
||||
stdlib net/http + NDJSON streaming is exactly the area the rubric called
|
||||
Go's only capability-proven fit (W4 evidence, unchanged by the directive).
|
||||
- TUI: NOT in the first Go surface. Textual coupling is deep (80+ modules
|
||||
per the rubric); the front-end Go layer first exposes the API the TUI
|
||||
consumes, and the TUI itself migrates later as its own wave.
|
||||
|
||||
## 3. What "back end" means concretely here
|
||||
|
||||
All pure-logic modules move into the two existing zero-dep crates
|
||||
(evidence_provenance, query_memory) or a third crate for the remaining
|
||||
domains. The zero-external-crate discipline stands (the rubric's W4 Rust
|
||||
kill-rule for network code stays valid INSIDE Rust crates: no network code
|
||||
in crates, that belongs to the Go layer).
|
||||
|
||||
Back-end candidates not yet ported (pure logic, no external deps):
|
||||
- drift_core, drift_cache, drift_compression (scored math) — memory umbrella
|
||||
- warm_neuron_store/warm_router scoring paths — memory umbrella
|
||||
- memory_store consolidation/scoring helpers — memory umbrella
|
||||
- capability_fit/capability_broker scoring — capability umbrella
|
||||
- tokenizer/counting helpers in token_optimizer — inference umbrella
|
||||
Each port follows the slice-2 pipeline: contract, zero-dep Rust, byte-parity
|
||||
battery vs the Python vintage, dual-mode canary, consumer import chain,
|
||||
CI green, supersession ledger.
|
||||
|
||||
## 4. Sequencing (per wave, each independently mergeable)
|
||||
|
||||
H0 — enablement (no behavior change):
|
||||
- Install Go on the host + the runner user's PATH; pin a version in
|
||||
requirements-ci-equivalent (a go-version file + CI check).
|
||||
- CI: add a Go build+vet+test job (empty module, exits 0) so the pipeline
|
||||
carries Go before any code lands.
|
||||
|
||||
H1 — Go first-call shim:
|
||||
- Go binary `rune` replacing the Python entrypoint's process bootstrap:
|
||||
arg parsing, env resolution, spawn the Python runtime (transitional).
|
||||
- Canary: same first-call contract (exit codes, env handoff, TTY attach).
|
||||
|
||||
H2 — Go command dispatch + /v1 endpoint:
|
||||
- Go serves /v1 chat completions + CLI dispatch, calling the Python core
|
||||
through the same local transport the TUI uses today.
|
||||
- Parity battery: request/response shapes vs the Python server for the
|
||||
same canned conversations (byte-level for metadata, streaming NDJSON).
|
||||
|
||||
H3 — back-end wave (parallel to H1/H2 where independent):
|
||||
- Ports listed in section 3, one or two modules per slice, same slice-2
|
||||
discipline. No port starts without its parity battery written first.
|
||||
|
||||
H4 — TUI migration wave (its own contract, later):
|
||||
- Only after H1-H2 prove the Go front end serves the same surfaces the TUI
|
||||
consumes. The TUI is the largest single module (4k+ lines, Textual) and
|
||||
earns its own contract.
|
||||
|
||||
## 5. Honest constraints (carried from the rubric, not contradicted)
|
||||
|
||||
- Go must be installed first (rubric D5 gate: it failed this before; the
|
||||
directive now outweighs the gate but installation is still step zero).
|
||||
- Network/streaming code lives in the Go layer, never in Rust crates
|
||||
(rubric W4: stdlib-only Rust has no credible HTTP/SSE precedent).
|
||||
- SQLite/regex/file-IO stay at their current language boundaries per the
|
||||
zero-dep split law from slice 2 (math Rust, IO at the edges).
|
||||
- Every surface gets the same proof pipeline as slice 2: observed-not-
|
||||
invented expectations, byte parity, dual-mode canary, CI green.
|
||||
|
||||
## 6. Success measure
|
||||
|
||||
The tranche is complete when: the `rune` process starts as a Go binary;
|
||||
user-facing calls (CLI, /v1, operator path) are served by Go; every core
|
||||
logic module on the port list is byte-parity Rust; Python remains only in
|
||||
surfaces explicitly contracted as keep-Python (TUI internals until H4,
|
||||
test harness, canary frameworks).
|
||||
@@ -0,0 +1,285 @@
|
||||
# TOTAL REBASE TRANCHE — Rune to Go/Rust (R2)
|
||||
|
||||
**Status:** DESIGN — awaiting Nick's approval gate (TRANCHE-DEPTH TEMPLATE §4)
|
||||
**Supersedes:** the KEEP-PYTHON rubric verdicts (already superseded by Nick's
|
||||
Go-front/Rust-back directive) and extends Phase H (H0–H4 slice 1, merged
|
||||
`693a494`, v0.90.0) from first ports to total rebase.
|
||||
**Nick's directives (verbatim anchors):**
|
||||
- "i want the whole codebase rebased in rust and go"
|
||||
- "everything should initiate with go as the runtime, and reactive adaptable
|
||||
runtime should be go"
|
||||
- "the deterministic backend i.e memory storage, architectures and elastic
|
||||
cognition and everything like that should be in rust"
|
||||
- "go should be the reactive front end to what interfaces with the user —
|
||||
not gui — the deliverables and chat interactions as well as how rune
|
||||
handles thoughts and runtime"
|
||||
- "the zero dependency rule should be in place but the rebase should rebuild
|
||||
those aspects into the new architecture and make them operational"
|
||||
- "the textual tui should be last but everything should be wired into the
|
||||
textual tui"
|
||||
- "start with the scaffolding first as everything relies on that, then use
|
||||
the new rust scaffolding to make sure everything else is built towards
|
||||
scaffold protocol"
|
||||
- "the scaffolding function should be a rust execution but go start"
|
||||
|
||||
## 1. Grounding summary (measured 2026-10-04, from the spine store + wc)
|
||||
|
||||
- 161 modules with source, **64,482 lines** of `src/rune/` Python.
|
||||
- Draft classification (rule-based, to be function-level verified in Wave 0):
|
||||
- **RUST (deterministic backend):** 49 modules, 26,772 lines — memory,
|
||||
drift, warm, capability, token, spine, scaffold, construction_*,
|
||||
structural_store, curator, consolidation, elastic cognition, ledgers,
|
||||
audit, evidence, contracts, sleep_time, subconscious, persona_gate,
|
||||
context_broker, growth.
|
||||
- **GO (reactive runtime + user-facing):** 10 modules, 7,043 lines —
|
||||
operator, transports, dispatch, provider registry, adaptive router,
|
||||
hook engine, local tool runtime, work manager, deploy_qualify.
|
||||
- **TUI-last:** 4 modules, 4,889 lines — rune_tui (4,041), tui_validator,
|
||||
tui_backlog, tui_transport.
|
||||
- **UNCLASSIFIED (Wave 0 must disposition each with evidence):** 73
|
||||
modules, 21,473 lines — voice_*, tool_executor, model_routing/registry,
|
||||
event_bus, cron_engine, message_queue, identity_anchors, pins,
|
||||
stage_guards, tier_manager, witness, etc.
|
||||
- **Test infra:** 23 canary/test modules, 4,211 lines — rewritten per wave
|
||||
(Rust canaries port with their surface; Go canaries for the front).
|
||||
- **Retires:** `__init__`, `__main__`, `memory_env` (bootstrap glue —
|
||||
subsumed by the Go runtime's env contract).
|
||||
- Existing Rust (merged, production-proven): 6 zero-dep crates
|
||||
(query_memory, evidence_provenance, rune_spine, memory_scoring,
|
||||
capability_scoring, token_counting), 338k bytes, all with parity
|
||||
batteries. Go: rune-entry (H1 shim + H2 /v1 front door).
|
||||
- Stragglers from R1 folded in: #2171, #2119 (TUI), #1844 (salience_gate),
|
||||
#1755 (goal._load), #1738 (failure_auto_filer).
|
||||
|
||||
## 2. Objective
|
||||
|
||||
Every line of rune's Python runtime is rebased into the dichotomy —
|
||||
**Go initiates and reacts; Rust determines and stores** — with the
|
||||
scaffolding protocol rebuilt FIRST (Go start / Rust execution) so every
|
||||
subsequent conversion is driven, checked, and closed by the new protocol
|
||||
itself. Python is deleted only after the replacement is parity-proven and
|
||||
CI-green (strangler exit law). The Textual TUI is last and stays the
|
||||
integration surface: everything wires into it over the transport seam.
|
||||
|
||||
## 3. The hard constraint analysis (zero-dep + total rebase — feasibility)
|
||||
|
||||
Nick asked: confirm or deny. **Confirmed possible**, amended per his
|
||||
critiques (2026-10-04):
|
||||
|
||||
- **Hybridized membrane, not a wall (Nick's correction):** Go remains the
|
||||
reactive initiator, but "Rust never touches a socket" is WITHDRAWN as an
|
||||
absolute. The languages interoperate where coherence demands: Rust cores
|
||||
callable from Go (the proven cdylib pattern), Go surfaces invocable from
|
||||
Rust through defined channels, shared wire protocols. The dichotomy is
|
||||
the DEFAULT placement rule; hybrid junctions are deliberate, contracted
|
||||
exceptions — each recorded in the unit's contract.
|
||||
- **SQLite STAYS (Nick's ruling):** no bespoke storage engine in this
|
||||
tranche. SQLite remains and is wired into the new architecture — the
|
||||
Rust side reaches it through system-library FFI (legal under zero-dep:
|
||||
a system library is not a crate dependency). The bespoke memory-system
|
||||
redesign is a LATER, separate program.
|
||||
- **Reactivity/async:** Go goroutines + channels own the event loops;
|
||||
Rust's deterministic cores stay synchronous behind the membrane.
|
||||
- **Regex:** hand-rolled matchers per case (proven: pyjson, word-run
|
||||
scanner, run-collapse). No general regex engine needed.
|
||||
- **Parity-plus-improvement (Nick's upgrade):** parity is the FLOOR, not
|
||||
the ceiling. Every unit answers "parity OR measurably better" —
|
||||
efficiency improvements are in scope per function (Rust zero-copy and
|
||||
exactness, Go goroutine-native concurrency), bloat from bad syntax is
|
||||
removed, and functionality is the non-negotiable gate. Improvements
|
||||
carry measurements; the parity battery proves no behavior regression
|
||||
either way.
|
||||
- **TUI wiring is continuous (Nick's correction):** each landed surface
|
||||
wires toward the Textual TUI over the transport seam as part of its
|
||||
unit contract — the final TUI wave integrates a system already aimed
|
||||
at it, not a retrofit.
|
||||
- **Language intelligence in the protocol (Nick's addition):** the
|
||||
scaffolding gains a first-class placement decision — for rune, the
|
||||
Go/Rust dichotomy rule (reactive/initiating → Go; deterministic/
|
||||
stateful → Rust); for other projects, a language-fit assessment at
|
||||
construction-classification time (the protocol recommends the right
|
||||
language for the task's characteristics).
|
||||
- **Provenance lock (Nick's verification):** the scaffolding being ported
|
||||
in Wave 1 is the V1-ACCEPTED system per docs/SCAFFOLDING_V1_DEFINITION.md
|
||||
(T18 §18B; North Star = Nick's verbatim words; 17/17 acceptance gates).
|
||||
The Wave 1 parity replay against the production contract ledger proves
|
||||
the rebuild is that design, not a drift.
|
||||
|
||||
## 4. Wave structure (each wave independently mergeable, scaffolding-gated)
|
||||
|
||||
### Wave 0 — Grounding + inventory verification (the template's bar)
|
||||
- Function-level mapping of all 161 modules (the spine gives structure;
|
||||
wave 0 verifies 100% of functions and classifies the 73 UNCLASSIFIED
|
||||
with evidence).
|
||||
- Disposition the 5 R1 stragglers.
|
||||
- Produce the per-unit issue factory input: one issue per function,
|
||||
each answering improve/fix/missing-connectors with evidence.
|
||||
- Deliverable: `plans/total-rebase-inventory.md` + issues filed.
|
||||
- Exit gate: inventory signed off by Nick (the approval gate).
|
||||
|
||||
### Wave 1 — The scaffolding engine reborn (Go start / Rust execution)
|
||||
**Everything depends on this — it goes first, per Nick's directive.**
|
||||
- The rebuild target is the V1-ACCEPTED system (Nick's design —
|
||||
docs/SCAFFOLDING_V1_DEFINITION.md, North Star verbatim, 17/17
|
||||
acceptance gates). The parity replay against the production contract
|
||||
ledger is the proof the rebuild IS that design.
|
||||
- Rust crate `scaffold_core`: the construction machinery as pure
|
||||
deterministic logic — contract lifecycle (draft→approved→building→
|
||||
verifying→closed), closure classification (correctly_predicted et al.),
|
||||
scope resolution, legal transitions, the contract store.
|
||||
Source modules: scaffold.py, construction_interface.py,
|
||||
construction_decision.py, construction_intent.py, structural_store.py
|
||||
(parts not already in rune_spine), contracts.py, toolchain.py.
|
||||
- Go surface `rune-scaffold` (extends rune-entry): the initiation layer —
|
||||
submit-need/observe/transition/close CLI + /v1 endpoints, delegating to
|
||||
the Rust core.
|
||||
- NEW per Nick: the **language-placement decision** lands in the protocol
|
||||
here — construction classification gains a language-fit assessment
|
||||
(rune: the Go/Rust dichotomy rule; other projects: characteristic-based
|
||||
recommendation). Every later contract carries its placement verdict.
|
||||
- NEW per Nick: parity is the floor — the engine itself is the first
|
||||
beneficiary of the parity-plus-improvement rule (measured improvements
|
||||
allowed, functionality preserved, no-regression proven).
|
||||
- Exit gate: the R2 tranche's own remaining waves are driven THROUGH the
|
||||
new engine (the protocol builds itself under its own rules).
|
||||
|
||||
### Wave 2 — Memory/storage backend (Rust, SQLite retained)
|
||||
- SQLite is NOT replaced (Nick's ruling). The Rust memory core reaches it
|
||||
via system-library FFI (zero-dep legal); memory_store.py (2,435 lines)
|
||||
is decomposed: query paths and consolidation logic into Rust, the SQLite
|
||||
mechanics behind a thin storage trait that maps to libsqlite3 today and
|
||||
to the future bespoke engine LATER without consumer churn.
|
||||
- elastic_cognition, memory_consolidation, sleep_time, curator,
|
||||
subconscious_pass, drift_* (remaining DB-backed surfaces), journal,
|
||||
recall_* (recall_router is routing → boundary checked at Wave 0
|
||||
disposition).
|
||||
- Data safety gate: checksum-verified read-back of the live store through
|
||||
the new engine; zero writes until the read path is proven identical.
|
||||
|
||||
### Wave 3 — Cognition + architecture engines (Rust)
|
||||
- The remaining deterministic cores: capability_broker decision logic,
|
||||
context_broker, persona_gate, growth, identity_anchors, pins,
|
||||
stage_guards, tier_manager, witness, safety_override, abstraction,
|
||||
verification surfaces.
|
||||
- Ports already merged (memory_scoring, capability_scoring,
|
||||
token_counting, rune_spine) are verified against the new scaffold
|
||||
protocol and wired into the final architecture.
|
||||
|
||||
### Wave 4 — The reactive runtime (Go)
|
||||
- The operator turn loop (rune_operator.py, 2,697 lines), chat
|
||||
interactions, deliverables production, model routing at call time
|
||||
(adaptive_router, model_registry/profiles/routing, provider_*),
|
||||
tool execution (tool_executor, tool_protocol, local_tool_runtime),
|
||||
event surfaces (event_bus, message_queue, cron_engine, hook_engine),
|
||||
voice output chain (voice_*) as deliverable production.
|
||||
- The /v1 surface deepens to carry the full interaction contract;
|
||||
NDJSON streaming becomes the standard response shape.
|
||||
|
||||
### Wave 5 — Python retirement sweep
|
||||
- Every Python module whose surfaces have landed is deleted (strangler
|
||||
exit: parity proven + CI green + deployed one cycle). Shared/ vintage
|
||||
mirrors retired with them.
|
||||
- The glue shims (ms_loader, cs_loader, tc_loader, spine_loader) retire
|
||||
when their consumers no longer speak Python at all.
|
||||
|
||||
### Wave 6 — TUI last (but wired continuously)
|
||||
- Every wave before this one ships its transport-seam wiring toward the
|
||||
TUI as part of each unit's contract (Nick's correction: natural,
|
||||
continuous wiring — not a retrofit wave).
|
||||
- Then the TUI itself: a thin Textual render shell over the wire (all
|
||||
logic already Go/Rust by then) or a port — disposition decided on the
|
||||
evidence of everything that came before.
|
||||
|
||||
### Wave 7 — Final audit
|
||||
- Language-bar measurement, the full scaffolding ledger replayed, the
|
||||
tranche's closure report with per-wave evidence.
|
||||
|
||||
## 5. Per-unit discipline (the template, unchanged)
|
||||
|
||||
- One issue per function via the issue factory; each answers
|
||||
improve/fix/missing-connectors with evidence.
|
||||
- Every unit runs the scaffolding pipeline end-to-end: submit-need →
|
||||
contract → approval → canary-first (red proven) → implementation →
|
||||
parity battery vs the Python vintage → closure audit (structural ids,
|
||||
never file paths — the closure vocabulary law).
|
||||
- Per-unit decisions on evidence, never pre-decided globally: each
|
||||
function's Go-vs-Rust placement is assessed by the dichotomy rule with
|
||||
the measured call graph, not by the draft classification alone.
|
||||
- Bug-fix contract governs every defect found en route (red test first,
|
||||
regression frozen).
|
||||
|
||||
## 6. Verification gates
|
||||
|
||||
- CI green per wave (crate builds, Go vet/test/build, canaries, henxels,
|
||||
type ratchet until the Python ratchet itself retires in Wave 5).
|
||||
- Parity batteries are byte-level for deterministic surfaces, shape-level
|
||||
modulo volatile fields for wire surfaces.
|
||||
- The scaffolding closure audit for every contract: correctly_predicted
|
||||
or honestly flagged, never silent.
|
||||
- Deploy qualification per merge (the pipeline that proved itself today).
|
||||
|
||||
### The 90/100 coverage law (Nick, 2026-10-05 — GOVERNING from W3.4 on)
|
||||
|
||||
Per Nick: "if it's realistically achievable but aim higher also if that's
|
||||
realistically achievable. go to 100% where achievable."
|
||||
|
||||
- **100% where achievable — the deterministic Rust kernels.** Every kernel
|
||||
crate (memory_retrieval, cognition_core, scaffold_core, memory_storage,
|
||||
and every future crate) targets 100% BRANCH coverage via its parity
|
||||
matrices + the pytest shim battery: every branch has real reachable
|
||||
semantics, and exhaustive enumeration is exactly what the batteries do.
|
||||
Achievability is measured per crate at wave close; a kernel below 100%
|
||||
carries the named unreachable case or it isn't done.
|
||||
- **≥90% on the changed-lines bar (diff-cover), aiming higher.** The floor
|
||||
stays a floor; the working standard per slice is 90%+ on changed lines;
|
||||
whatever a slice achieves beyond 90 becomes the next slice's personal
|
||||
benchmark (per-unit "aim higher" — the number climbs wave over wave).
|
||||
The floor ratchet itself moves after consecutive clean waves
|
||||
(70 → 76 → 84 → 90; each raise is a commit, never a step down).
|
||||
- **Glue/failure branches: 90%+ with byte-compare proofs.** The stubs and
|
||||
probe branches that only fire in a broken world are covered by honest
|
||||
fixtures (real import-failure probes like the from-import resolution
|
||||
law) — never by fake-triggered pragmas. A branch covered by a lying
|
||||
fixture is WORSE than uncovered; if it can't be honestly reached, it's
|
||||
named in the wave's closure report with the reason.
|
||||
- **Assertion quality is part of the bar.** A test that touches a line
|
||||
without verifying its OUTPUT doesn't count toward the bar — byte-compare
|
||||
/ exact-value asserts are the standard; "is not None" assertions reject
|
||||
at review.
|
||||
|
||||
## 7. Risk register
|
||||
|
||||
- **SQLite FFI correctness** (Wave 2) — the storage layer is retained but
|
||||
re-wired; mitigations: read-path proven identical before any write,
|
||||
checksum-verified read-back of the live store, the storage trait
|
||||
boundary keeps the future bespoke swap cheap.
|
||||
- **Hybrid membrane drift** — junctions multiply ad-hoc if ungoverned;
|
||||
every Go↔Rust interaction point is a contracted, recorded decision in
|
||||
its unit's contract (the dichotomy is the default; hybrids justify
|
||||
themselves per unit).
|
||||
- **Improvement regressions** — parity-plus-improvement invites subtle
|
||||
behavior changes; every improvement carries a measurement AND the parity
|
||||
battery stays authoritative for no-regression.
|
||||
- **Go↔Rust call latency** — measure in Wave 1; the architecture assumes
|
||||
local transport cost is negligible vs model latency; if measured wrong,
|
||||
the membrane's transport moves (decision recorded, not improvised).
|
||||
- **Context-window economics** — 1,852-style issue volumes mean the
|
||||
pipeline must multiplex (no-wait law) and local models carry routine
|
||||
units (custodian pattern).
|
||||
- **Scope creep during Wave 0** — the inventory is measured and frozen
|
||||
before design; additions go through scope amendments.
|
||||
|
||||
## 8. Rollback
|
||||
|
||||
- Every wave merges independently; master stays deployable at each
|
||||
boundary (the strangler's whole point).
|
||||
- The Python vintage remains until Wave 5 deletion gates pass; a wave
|
||||
revert = revert the merge commit (deploy qualification re-runs).
|
||||
- The scaffolding engine's Wave 1 parity replay means even the protocol's
|
||||
own replacement is reversible against recorded history.
|
||||
|
||||
## 9. Approval gate
|
||||
|
||||
This tranche does not execute until Nick approves. On approval, Wave 0
|
||||
starts immediately and runs without consent blockers (standing directive),
|
||||
with self-verified checkpoints and defect filing.
|
||||
@@ -0,0 +1,354 @@
|
||||
# TRANCHE v3: THE FULL DICHOTOMY REBASE — with HOW, with language advantages, with automated verification
|
||||
|
||||
Nick's corrections applied:
|
||||
- Umbrella manifest feeds the spine (not superseded); both need updating
|
||||
- construction_decision.py itself gets ported (Go start + Rust execution)
|
||||
- Language advantages are part of every port — improvements where the target language is better
|
||||
- Automated parity verification BEFORE gutting the Python (hard gate, no manual checking)
|
||||
- A testing document that proves bigger features work after each wave
|
||||
- Maximise Bubble Tea addons (research the ecosystem)
|
||||
- No arbitrary timeframe — it gets done when it gets done
|
||||
- Every task follows on to the next — never stopping
|
||||
- CI stacking: keep submitting PRs; address CI issues in batches of 5, not individually
|
||||
|
||||
---
|
||||
|
||||
## THE CORE MECHANISM: How every module gets replaced
|
||||
|
||||
Every module follows the same seven-step process. No exceptions. The process IS the verification.
|
||||
|
||||
### Step 1: Understand the Python module
|
||||
Read every function. Identify: what does it compute (→ Rust), what does it initiate (→ Go), what does it store (→ Rust), what does it route (→ Go). Note every caller. Note every file it reads/writes.
|
||||
|
||||
### Step 2: Write the replacement in the target language
|
||||
For Rust: a `pub fn` in the appropriate crate, with serde-compatible structs.
|
||||
For Go: a method on a struct in the appropriate `src/go/<name>/` package.
|
||||
|
||||
**Language advantage check**: at this step, ask "does Go/Rust do this BETTER than Python did?" If yes, implement the improvement. Examples: Go goroutines replace Python threading for parallel operations. Rust's zero-cost abstractions replace Python duck typing for hot-path scoring. Go's `net/http` replaces Python `urllib` for model connections with connection pooling and keep-alive.
|
||||
|
||||
### Step 3: Write the parity test (RED first)
|
||||
Before touching the Python module, write a test that runs the same input through BOTH the Python oracle and the new Go/Rust implementation, and asserts the outputs are identical. This test must FAIL before the implementation is correct, and PASS once it is. The test is the gate.
|
||||
|
||||
### Step 4: Verify parity — the HARD GATE
|
||||
Run the parity test. If it passes, the replacement is PROVEN to work. Only now can you proceed to Step 5. If it fails, debug the Go/Rust implementation until it passes. **Do not gut the Python until this gate passes.** This is automated — you never have to manually check.
|
||||
|
||||
### Step 5: Rewire the Python module as a delegation shim
|
||||
Replace the Python function body with a call to the Go binary (subprocess) or the Rust kernel (ctypes/cgo). Every existing caller of the Python module keeps working — they don't know the implementation changed.
|
||||
|
||||
### Step 6: Register the canary in CI
|
||||
The parity test from Step 3 is registered in `.gitea/workflows/ci.yml`. Now every future commit is checked against the parity contract automatically.
|
||||
|
||||
### Step 7: PR → CI → merge
|
||||
Push the branch, let CI run. If CI raises issues, DON'T STOP — continue to the next module's port. Address CI issues in batches of 5. The pipeline is proven; it catches things; you fix them in batches.
|
||||
|
||||
**The key insight**: Step 4 (the parity gate) is the answer to "how do you confirm replacements working before you gut it?" You don't gut anything until the automated test proves the replacement produces identical output. The test is the contract.
|
||||
|
||||
---
|
||||
|
||||
## THE TESTING DOCUMENT: proving bigger features after each wave
|
||||
|
||||
After each wave completes, a WAVE VERIFICATION DOCUMENT is produced that proves the system works end-to-end. Not unit tests — integration tests.
|
||||
|
||||
### What it contains:
|
||||
1. **The operator turn loop works**: send a message through the Go operator, get a response back, verify the response is correct
|
||||
2. **Memory operations work**: store a memory, recall it, verify the content matches
|
||||
3. **Tool execution works**: execute a tool call, verify the output
|
||||
4. **The TUI renders**: launch the Bubble Tea TUI, verify it displays
|
||||
5. **The event journal works**: emit an event, read it back
|
||||
6. **The scaffolding works**: submit a need, verify the contract is created
|
||||
|
||||
### How it's generated — BY THE GITEA PIPELINE, not by the agent:
|
||||
A shell script (`scripts/wave_verify.sh`) is written by the agent. A CI workflow step runs the script after each wave merge. The CI step formats the results as markdown and posts them as a comment on the wave's Gitea epic. The document is permanent — attached to the epic, survives session restarts, reproducible (the commands are in the document), automated (CI runs it, not the agent).
|
||||
|
||||
### Implementation:
|
||||
- `scripts/wave_verify.sh` — the integration verification script
|
||||
- A CI workflow step (in ci.yml or a new `wave-verify.yml`) that runs the script and posts results to the Gitea epic using the existing GITEA_TOKEN secret
|
||||
- The same mechanism the merge-robot already uses for PR comments
|
||||
|
||||
### What "done" means for a wave:
|
||||
The wave's Gitea epic has a verification comment showing every integration path PASSING. The agent does not produce testing documents — the pipeline does.
|
||||
|
||||
### What "done" means for a wave:
|
||||
Not just "the code compiles" — the wave verification document shows every integration path working.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 0: INFRASTRUCTURE (before any porting)
|
||||
|
||||
### 0.1 Henxels scope extension
|
||||
Open `henxels.yaml`. Add scopes for `src/rust/*/src/*.rs` and `src/go/**/*.go`. Same quality gates: max_lines, no_secrets, canary registration, coverage existence. Without this, new Go/Rust files have no governance.
|
||||
|
||||
### 0.2 Umbrella manifest extension
|
||||
The umbrella manifest IS the spine's input for non-module surfaces. Extend it:
|
||||
- Add the 10 existing Rust crates as `rust_crates` entries
|
||||
- Add the 2 existing Go modules as `go_surfaces` entries
|
||||
- Add placeholder entries for the planned Go modules
|
||||
- Every new Go/Rust file gets tracked, so the spine knows about it
|
||||
|
||||
### 0.3 Scaffolding protocol update (construction_decision.py port)
|
||||
`construction_decision.py` is itself a Python file. Port it:
|
||||
- **Go start call**: the CLI surface that receives the classification request
|
||||
- **Rust execution**: the deterministic classification logic (keyword matching, language placement)
|
||||
- The GO_SIGNALS and RUST_SIGNALS keyword lists are updated to match the corrected dichotomy
|
||||
- The Python version becomes a delegation shim
|
||||
- After this port, `rune scaffold classify` routes through Go→Rust, not Python
|
||||
|
||||
### 0.4 CI pipeline update
|
||||
- Build every Rust crate in the unit-tests job
|
||||
- Build every Go module in a new `go-build` job
|
||||
- Set CC_RUST_LIB, SC_RUST_LIB, etc. to the CARGO_TARGET_DIR paths
|
||||
- Add `go vet` and `go test ./...` steps
|
||||
|
||||
### 0.5 Gitea epic + MiMoCode tasks
|
||||
- Post the parent epic on Gitea: the full wave structure as a folded checklist
|
||||
- Post per-wave sub-epics
|
||||
- Mirror in MiMoCode's task tool
|
||||
- Every subsequent task updates both
|
||||
|
||||
---
|
||||
|
||||
## WAVE 4: GO REACTIVE INFRASTRUCTURE (14 modules)
|
||||
|
||||
### event_bus (244 lines) — DONE
|
||||
Already built as `src/go/event-bus/`. Reads/writes the same events.jsonl.
|
||||
|
||||
### event_envelope (303 lines)
|
||||
Merge into event-bus. The EventEnvelope struct already exists in Go. Add the OrchestrationRecord methods (to_dict, from_dict) as MarshalJSON/UnmarshalJSON. Add the content_hash function.
|
||||
|
||||
### message_queue (10KB)
|
||||
**What it does**: holds approved messages until they're executed. The Python version uses a JSONL file with a mutex.
|
||||
**Go advantage**: Go channels are designed for this. Use a buffered channel for in-memory queue + a JSONL file for durability. The Go version is concurrent-safe by construction.
|
||||
**HOW**: Create `src/go/message-queue/`. Define `type Message struct` with the same fields as the Python version. Implement `Enqueue`, `Dequeue`, `GetApproved`, `MarkExecuted`. The Python module becomes a subprocess shim.
|
||||
**Parity test**: enqueue 10 messages in Python, read them in Go, verify identical.
|
||||
|
||||
### hook_engine (28KB)
|
||||
**What it does**: loads plugin hooks from a config, fires them on events.
|
||||
**Go advantage**: goroutines enable parallel hook dispatch (Python's GIL prevents this).
|
||||
**HOW**: Create `src/go/hook-engine/`. `type HookEngine struct { hooks map[string][]HookFunc; mu sync.RWMutex }`. `Fire(name, data)` iterates hooks sequentially; `FireAsync` dispatches each hook in a goroutine and collects errors via channels.
|
||||
**Language advantage**: parallel hook dispatch — Python can't do this due to the GIL.
|
||||
|
||||
### microkernel (14KB)
|
||||
**What it does**: the plugin lifecycle — register, activate, dispatch.
|
||||
**Go advantage**: Go interfaces replace Python duck typing with compile-time guarantees.
|
||||
**HOW**: `type Plugin interface { Name() string; Capabilities() []string; OnMessage(data) error }`. Registry with `Register(p Plugin)`, `Fire(event, data)`. The hook_engine plugs into this.
|
||||
|
||||
### subsystem_health (6KB)
|
||||
**What it does**: tracks failures per subsystem, escalates tiers.
|
||||
**HOW**: `type SubsystemHealth struct { failures map[string][]Failure }`. RecordFailure, GetHealth. Write to JSON for durability.
|
||||
|
||||
### parallel_processor (5KB)
|
||||
**What it does**: runs operations in parallel.
|
||||
**Go advantage**: goroutines + sync.WaitGroup. This is what Go was BORN for.
|
||||
**HOW**: `func ParallelMap[T, R any](items []T, fn func(T) R) []R`. Launch goroutines, collect via channels.
|
||||
|
||||
### work_manager (20KB)
|
||||
**What it does**: manages work dispatch to builders.
|
||||
**HOW**: Go worker pool with `chan Task`. `type WorkManager struct { workers int; queue chan Task; results chan TaskResult }`. Start, Submit, Stop.
|
||||
|
||||
### failure_auto_filer (21KB)
|
||||
**What it does**: auto-files failures to the forge.
|
||||
**HOW**: Go struct with spool file + forge client. Same pattern as the existing forge_client.
|
||||
|
||||
### cache (16KB) + connection_pool (3KB)
|
||||
**What they do**: cache responses, pool connections.
|
||||
**Go advantage**: Go's `sync.Map` and channel-based pools are built for this.
|
||||
**HOW**: `src/go/cache/` — LRU cache with `sync.RWMutex`. `src/go/connection-pool/` — channel-based pool.
|
||||
|
||||
### cron_engine (14KB) + night_mode (14KB)
|
||||
**What they do**: scheduled tasks, night consolidation.
|
||||
**Go advantage**: `time.Ticker` + goroutines replace Python's threading.Timer.
|
||||
**HOW**: `src/go/cron-engine/` — task list with cron expressions. `src/go/night-mode/` — uses the cron engine for scheduled consolidation.
|
||||
|
||||
### recall_router (5KB)
|
||||
**What it does**: routes recall queries to the right memory backend.
|
||||
**HOW**: `src/go/recall-router/` — routing table with `map[string]RecallFn`.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 4b: GO TOOL EXECUTION (3 modules)
|
||||
|
||||
### tool_executor (73KB) — STARTED
|
||||
The Go binary already handles read_file, list_directory, glob_files, grep_files, run_command. Add the remaining operations: write_file (with path validation), edit_file (with diff preview), run_tests, lint_file, git_commit, git_diff, git_log, repo_map, and the query/diagnose operations.
|
||||
**Language advantage**: Go's `os/exec` with `context.Context` for timeouts is cleaner than Python's `subprocess.run(timeout=)`. Go's goroutines enable parallel tool execution (Python serialises them).
|
||||
|
||||
### tool_protocol (16KB)
|
||||
Go types: `ToolRequest`, `ToolEvent`. Validation against a schema registry. The approval boundary stays in the Go operator (not Python).
|
||||
|
||||
### local_tool_runtime (35KB)
|
||||
The Go loop: receive model response → parse tool calls → validate → dispatch to executor → feed results back. `type LocalToolRuntime struct { transport Transport; tools []ToolDef; maxIterations int }`. This is the loop that makes Rune respond to tool results.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 5a: GO MODEL/PROVIDER (10 modules → 1 binary)
|
||||
|
||||
All 10 modules collapse into `src/go/model-connect/`.
|
||||
|
||||
**Language advantage**: Go's `net/http` with connection pooling and keep-alive replaces Python's `urllib` (which opens a new connection per request). Go's goroutines enable parallel model calls (Python serialises them). Go's `context.Context` enables timeout propagation through the call chain.
|
||||
|
||||
### model_connect (16KB)
|
||||
`type OllamaClient struct { baseURL string; client *http.Client }`. Chat, Show, Pull, ListModels. Use `net/http` with a shared transport for connection reuse.
|
||||
|
||||
### model_routing (16KB) + model_registry (6KB) + model_profiles (16KB)
|
||||
Go router that scores tasks against model capabilities and routes to the right model. One `ModelRouter` struct replaces three Python files.
|
||||
|
||||
### provider_budget (12KB)
|
||||
Go budget tracker with `sync.Mutex`. Record, GetStatus. Token counting per provider.
|
||||
|
||||
### provider_registry (23KB) + standardized_providers (9KB) + provider_context (2KB)
|
||||
Go `Provider` interface with OllamaProvider and CompatProvider implementations. Registry manages connections.
|
||||
|
||||
### tiered_executor (9KB)
|
||||
Go executor with tier escalation. Execute, Escalate.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 5b: GO OPERATOR (19 modules → 1 binary)
|
||||
|
||||
**rune_operator (126KB)** — the turn loop. This is the hardest port.
|
||||
**How**: break it into sub-systems. Port each one at a time. After each, run the full test suite.
|
||||
1. The state management (RuneState, TaskStateRepo, ConversationRepo) → Go structs with JSON persistence
|
||||
2. The turn loop → Go function: `Respond(channel, message) OperatorResponse`
|
||||
3. The routing → Go AdaptiveRouter
|
||||
4. The tool dispatch → Go LocalToolRuntime (already ported in Wave 4b)
|
||||
5. The memory interaction → Go MemoryStore (Wave 5c) + Rust kernels via cgo
|
||||
6. The model connection → Go ModelConnect (Wave 5a)
|
||||
|
||||
**Language advantage**: Go goroutines replace Python threading for parallel context gathering. Go channels replace Python queues. Go's `context.Context` replaces Python's ad-hoc timeout management. The turn loop becomes truly concurrent — memory recall and model inference can overlap.
|
||||
|
||||
**The other 18 modules** are supporting cast: adaptive_router (23KB), build_mode (20KB), builder_bridge (12KB), core_server (9KB), auto_context (8KB), auto_tasks (15KB), event_wait (23KB), evidence_probe (7KB), forge (5KB), forge_client (7KB), goal (14KB), growth_delta (16KB), warm_router (55KB), release_notes (18KB), persona_definition (7KB), voicebox (4KB), voice_injector (3KB). Each follows the same pattern: read Python → write Go → parity test → rewire → commit.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 5c: GO MEMORY OPERATIONS (8 modules)
|
||||
|
||||
### memory_store (99KB) — THE BIGGEST SINGLE PORT
|
||||
**What it does**: every SELECT, INSERT, UPDATE, DELETE against the memory database.
|
||||
**Go advantage**: `modernc.org/sqlite` is pure Go — no cgo, no C compiler, cross-compiles trivially. Go's `database/sql` interface with prepared statements is cleaner than Python's raw sqlite3.
|
||||
**HOW**: `type MemoryStore struct { db *sql.DB }`. Every Python function becomes a Go method. The SQL statements ARE the specification.
|
||||
**Parity test**: same SQLite file, same queries, byte-identical results. Run after every operation.
|
||||
|
||||
### curator (70KB)
|
||||
Scans the store, identifies conflicts, promotes/demotes. Go state machine.
|
||||
|
||||
### sleep_time (38KB)
|
||||
Idle-period consolidation. Go goroutine on a timer.
|
||||
|
||||
### memory_consolidation (31KB)
|
||||
Calls the Rust kernel (already ported). Go handles the I/O.
|
||||
|
||||
### The other 4 (persona_gate, abstraction, witness, stage_guards)
|
||||
Each follows the same pattern.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 6: RUST BACKEND (85 modules)
|
||||
|
||||
Each Python module's pure functions become `pub fn` in the appropriate crate.
|
||||
|
||||
**The pattern for each:**
|
||||
1. Read the Python module, identify pure functions
|
||||
2. Write the Rust equivalent in the crate
|
||||
3. Add an FFI symbol
|
||||
4. Write a parity test
|
||||
5. The Python module becomes a thin FFI caller
|
||||
|
||||
**The rhythm**: most modules are 3-10KB. At that size, a port takes 15-30 minutes once you're in the rhythm. 85 modules at that pace is 2-3 days of focused work.
|
||||
|
||||
**Language advantage**: Rust's type system catches bugs at compile time that Python catches at runtime. Rust's `Result<T, E>` replaces Python's try/except for error handling. Rust's pattern matching replaces Python's if/elif chains. Rust's zero-cost abstractions mean the ports are FASTER than the Python originals.
|
||||
|
||||
---
|
||||
|
||||
## WAVE 7: BUBBLE TEA TUI (Go)
|
||||
|
||||
### Maximising the Bubble Tea ecosystem
|
||||
|
||||
Research the full Bubble Tea + Lipgloss + Bubbles ecosystem for every available addon:
|
||||
|
||||
**Bubble Tea** (the runtime): Elm architecture, goroutine-safe, tea.Tick for animations, tea.Every for recurring updates, tea.WithAltScreen for fullscreen, tea.WithMouseCellMotion for mouse support.
|
||||
|
||||
**Lipgloss** (styling): truecolor (16.7M colors), borders (rounded, thick, double, hidden), padding/margin/width/height, alignment, flex layout, gradients, inline styling, copy/blend styles.
|
||||
|
||||
**Bubbles** (pre-built components):
|
||||
- `spinner`: rotating indicators (line, dot, mini-dot, jump, pulse, points, globe, moon, monkey, meter, hamburger, ellipsis)
|
||||
- `progress`: animated progress bars with gradient colors
|
||||
- `textinput`: text input with cursor, placeholder, validation
|
||||
- `textarea`: multi-line text input
|
||||
- `table`: sortable, selectable data tables
|
||||
- `list`: filtered, paginated lists
|
||||
- `viewport`: scrollable content area
|
||||
- `paginator`: pagination controls
|
||||
- `key`: key binding documentation
|
||||
- `help`: help bubble
|
||||
- `timer`: countdown timer
|
||||
- `stopwatch`: elapsed time
|
||||
|
||||
**Additional libraries in the ecosystem:**
|
||||
- `glamour`: markdown rendering with syntax highlighting (for chat messages that contain code)
|
||||
- `harmonica`: spring animation physics (for smooth pane transitions)
|
||||
- `bubble-lipgloss-extras`: additional styling utilities
|
||||
|
||||
**What the current TUI has that maps:**
|
||||
- Two-column layout (chat LEFT, task RIGHT) → `lipgloss.JoinHorizontal()` with width ratios
|
||||
- Chat bubbles with sender name, timestamp, content → custom component with `lipgloss.Style{}.Border(lipgloss.RoundedBorder()).Padding(0, 1)`
|
||||
- Thinking indicator (spinner) → `bubbles/spinner` with the pulse style
|
||||
- Tool call display → bordered component with syntax highlighting via `glamour`
|
||||
- Status bar (version, mode, model, project) → fixed bottom bar via `lipgloss.JoinVertical()`
|
||||
- Command selector with autocomplete → `bubbles/textinput` + custom suggestion loop
|
||||
- Work pane (operational index) → side panel with `bubbles/list`
|
||||
- Scrollback → `bubbles/viewport` with `SetContent()` and `GotoBottom()`
|
||||
- Keybindings → `tea.KeyMap` with `tea.KeyCtrl1`, `tea.KeyCtrl2`, `tea.KeyCtrl3`
|
||||
- Streaming text → `tea.Cmd` that sends partial string updates
|
||||
- Growth/status widgets → `bubbles/table` or custom rendering
|
||||
|
||||
**Improvements the language enables:**
|
||||
- True concurrent streaming: the Go operator can push partial results to the TUI via Go channels while the model is still generating — Python can't do this because the GIL serialises everything
|
||||
- Sub-millisecond rendering: Go's zero-allocation string building means the TUI renders at 60fps without GC pauses
|
||||
- Statically linked binary: deploy the TUI as a single file, no Python runtime, no virtual environment, no pip install
|
||||
|
||||
---
|
||||
|
||||
## CI STACKING PROTOCOL
|
||||
|
||||
The CI pipeline is proven — 2,300+ tests, 25 canaries, required checks on master. Use it as a throughput engine:
|
||||
|
||||
1. Submit PR after PR after PR. Don't wait for one to merge before starting the next.
|
||||
2. If CI raises an issue, DON'T STOP the current work. Note it, continue.
|
||||
3. After every 5 PR submissions, go back and address the accumulated CI issues in one batch.
|
||||
4. The merge-robot and auto-version handle the merging — you keep producing.
|
||||
5. If a required check fails on master, that's the one thing that stops the conveyor. Fix it immediately.
|
||||
|
||||
**The principle**: CI is the quality gate, not the bottleneck. It runs while you work. Issues get batch-fixed. The conveyor keeps moving.
|
||||
|
||||
---
|
||||
|
||||
## GITEA EPIC STRUCTURE
|
||||
|
||||
The FIRST Gitea task: **Epic: "Full Dichotomy Rebase"** containing:
|
||||
- [ ] Wave 0: Infrastructure (henxels scope, umbrella manifest, scaffolding protocol, CI)
|
||||
- [ ] Wave 4: Go reactive infrastructure (14 sub-epics)
|
||||
- [ ] Wave 4b: Go tool execution (3 sub-epics)
|
||||
- [ ] Wave 5a: Go model/provider (10 sub-epics, folded into 1 binary)
|
||||
- [ ] Wave 5b: Go operator (19 sub-epics, folded into 1 binary)
|
||||
- [ ] Wave 5c: Go memory operations (8 sub-epics)
|
||||
- [ ] Wave 6a: Rust structural core (30 sub-epics)
|
||||
- [ ] Wave 6b: Rust cognition (30 sub-epics)
|
||||
- [ ] Wave 6c: Rust remaining (25 sub-epics)
|
||||
- [ ] Wave 7: Bubble Tea TUI (7 sub-epics)
|
||||
|
||||
Each sub-epic contains the HOW instructions (the seven-step process), the language advantage notes, the parity test spec, and the CI registration step.
|
||||
|
||||
MiMoCode tasks mirror the same tree.
|
||||
|
||||
---
|
||||
|
||||
## WHAT I STILL NEED FROM YOU
|
||||
|
||||
1. **The cgo pattern** — Go calls Rust `.so` files via cgo. The Rust crates don't change, only the caller changes from Python-ctypes to Go-cgo. Is this the membrane you want?
|
||||
|
||||
2. **The Bubble Tea ecosystem** — I've listed every addon above. Do you want any specific visual features beyond what's listed?
|
||||
|
||||
3. **The umbrella answer** — the spine relies on umbrellas (umbrellas are the input, spine is the consumer). Both need updating. Is this how you understood it?
|
||||
|
||||
4. **The CI stacking** — submit PRs continuously, batch CI fixes every 5 submissions. Does this match what you described?
|
||||
|
||||
5. **Approve to start Wave 0** — henxels scope, umbrella manifest, scaffolding protocol, CI update, Gitea epic. This is the foundation everything else builds on.
|
||||
@@ -0,0 +1,274 @@
|
||||
# TRANCHE: THE FULL DICHOTOMY REBASE — every Python module replaced
|
||||
|
||||
_Presented for operator approval per TRANCHE-TEMPLATE.md §4. Nick's mandate: "go through the whole codebase and replace it with the dichotomy." Measured: 180 Python files, 68,224 lines. Target: 0 Python logic files (TUI stays last per plan, test infra stays as test infra)._
|
||||
|
||||
---
|
||||
|
||||
## §0 — GROUNDING (measured, not guessed)
|
||||
|
||||
### The honest numbers
|
||||
|
||||
| Language | Files | Lines | % | Status |
|
||||
|---|---|---|---|---|
|
||||
| Python | 180 | 68,224 | 70% | **THE TARGET — every module ported** |
|
||||
| Rust | 106 | 27,304 | 28% | 10 crates exist, 85 more modules to port |
|
||||
| Go | 7 | 1,125 | 1% | 2 modules exist, 55 more to port |
|
||||
| **Total** | **293** | **96,653** | | |
|
||||
|
||||
### The corrected dichotomy (re-classified 2026-10-06, 64ba7cf)
|
||||
|
||||
| Side | Modules | Python lines to replace | Role |
|
||||
|---|---|---|---|
|
||||
| **GO** | 57 | 32,081 | The reactive shell: initiates, responds, routes, interfaces |
|
||||
| **RUST** | 85 | 25,218 | The deterministic backend: computes, stores, pure functions |
|
||||
| **TUI** | 7 | 5,435 | Textual UI — last per plan |
|
||||
| **TEST** | 29 | 5,397 | Test infrastructure — stays as test infra |
|
||||
|
||||
### Existing infrastructure (§0.2 — route through what exists)
|
||||
|
||||
- **CI pipeline**: `.gitea/workflows/ci.yml` — lint, unit-tests, go-surface, canary, best-practices, documentation, type-check, gitea-offline, required-checks
|
||||
- **Deploy pipeline**: `.gitea/workflows/deploy.yml` — event-driven CI qualification, stage/verify/promote
|
||||
- **Scaffolding**: `construction_interface.py` + `src/go/rune-entry/scaffold.go` — every module's port goes through a construction contract
|
||||
- **Canaries**: 25 registered in CI, each crate has its own parity battery
|
||||
- **Umbrella manifest**: `config/umbrellas.json` — 165 Python modules tracked (needs Rust/Go extension)
|
||||
- **Henxels**: `henxels.yaml` — structural contract, line caps, no-secrets, canary registration (needs Rust/Go scope)
|
||||
- **Gitea**: conductor/rune, required checks on master, merge-robot, auto-version
|
||||
|
||||
### Keep-greens (§0.5 — must stay green through every wave)
|
||||
|
||||
- Full test suite: 2,300+ tests, 0 failures
|
||||
- All 25 registered canaries
|
||||
- diff-cover ≥70% on changed lines
|
||||
- Silent-handler ratchet (BLE001 ≤345, S110 ≤75)
|
||||
- Henxels: all hold
|
||||
- Lint-baseline: no increase
|
||||
- Deploy: monotonic promotion, runtime verification
|
||||
|
||||
---
|
||||
|
||||
## §1 — OBJECTIVE
|
||||
|
||||
**Every Python module in `src/rune/` is replaced by its Go or Rust equivalent per the dichotomy. The TUI stays Python until Wave 7. Test infrastructure stays Python. The FFI boundary is the membrane between Go and Rust. Zero Python logic files remain (only the TUI shell and test files).**
|
||||
|
||||
Measured end-state: `ls src/rune/*.py | wc -l` returns ≤7 (TUI only) + test files. `src/go/` contains the full reactive runtime. `src/rust/` contains the full deterministic backend.
|
||||
|
||||
---
|
||||
|
||||
## §2 — WAVE STRUCTURE
|
||||
|
||||
### Wave 4: The Go reactive infrastructure (14 modules, ~5,271 lines)
|
||||
|
||||
The backbone that everything else routes through. These are the modules that make the system *respond*.
|
||||
|
||||
| Module | Python lines | Go target | Notes |
|
||||
|---|---|---|---|
|
||||
| event_bus | 244 | **DONE** (src/go/event-bus/) | Same events.jsonl, same envelope |
|
||||
| event_envelope | 303 | src/go/event-bus/ | Envelope type in the same binary |
|
||||
| message_queue | 10KB | src/go/message-queue/ | Durable queue |
|
||||
| hook_engine | 28KB | src/go/hook-engine/ | Plugin dispatch |
|
||||
| microkernel | 14KB | src/go/microkernel/ | Hook lifecycle |
|
||||
| subsystem_health | 6KB | src/go/subsystem-health/ | Failure tiers |
|
||||
| parallel_processor | 5KB | src/go/parallel-processor/ | Goroutines |
|
||||
| work_manager | 20KB | src/go/work-manager/ | Work dispatch |
|
||||
| failure_auto_filer | 21KB | src/go/failure-filer/ | Auto-filing |
|
||||
| cache | 16KB | src/go/cache/ | Connection pooling |
|
||||
| connection_pool | 3KB | src/go/connection-pool/ | Pooled connections |
|
||||
| cron_engine | 14KB | src/go/cron-engine/ | Scheduled tasks |
|
||||
| night_mode | 14KB | src/go/night-mode/ | Night consolidation |
|
||||
| recall_router | 5KB | src/go/recall-router/ | Memory routing |
|
||||
|
||||
**Entry condition**: event_bus already landed. **Exit condition**: all 14 modules exist as Go binaries, Python versions delegate or are deleted, CI go-surface job passes.
|
||||
|
||||
### Wave 4b: The Go tool execution layer (3 modules, ~3,079 lines)
|
||||
|
||||
| Module | Python lines | Go target |
|
||||
|---|---|---|
|
||||
| tool_executor | 73KB | **STARTED** (src/go/tool-executor/) |
|
||||
| tool_protocol | 16KB | src/go/tool-executor/ |
|
||||
| local_tool_runtime | 35KB | src/go/tool-executor/ |
|
||||
|
||||
**Exit condition**: tool executor handles all operations in Go, Python delegates via subprocess, approval machinery documented.
|
||||
|
||||
### Wave 5a: The Go model/provider layer (10 modules, ~3,398 lines)
|
||||
|
||||
| Module | Python lines | Go target |
|
||||
|---|---|---|
|
||||
| model_connect | 16KB | src/go/model-connect/ |
|
||||
| model_profiles | 16KB | src/go/model-connect/ |
|
||||
| model_registry | 6KB | src/go/model-connect/ |
|
||||
| model_routing | 16KB | src/go/model-connect/ |
|
||||
| openai_compat_transport | 10KB | src/go/model-connect/ |
|
||||
| provider_budget | 12KB | src/go/model-connect/ |
|
||||
| provider_context | 2KB | src/go/model-connect/ |
|
||||
| provider_registry | 23KB | src/go/model-connect/ |
|
||||
| standardized_providers | 9KB | src/go/model-connect/ |
|
||||
| tiered_executor | 9KB | src/go/model-connect/ |
|
||||
|
||||
**Exit condition**: all model/provider operations in Go, Ollama + OpenAI-compatible connections working.
|
||||
|
||||
### Wave 5b: The Go operator (19 modules, ~9,637 lines)
|
||||
|
||||
The biggest wave. The operator turn loop, routing, and the modules that serve it.
|
||||
|
||||
| Module | Python lines | Notes |
|
||||
|---|---|---|
|
||||
| rune_operator | 126KB | THE core — turn loop, task management |
|
||||
| adaptive_router | 23KB | Route decisions |
|
||||
| build_mode | 20KB | Build orchestration |
|
||||
| builder_bridge | 12KB | Builder dispatch |
|
||||
| builder_bridge_cli | 1KB | CLI surface |
|
||||
| core_server | 9KB | HTTP server |
|
||||
| persona_definition | 7KB | Persona loading |
|
||||
| voicebox | 4KB | Voice |
|
||||
| voice_injector | 3KB | Voice injection |
|
||||
| release_notes | 18KB | Release generation |
|
||||
| auto_context | 8KB | Auto context |
|
||||
| auto_tasks | 15KB | Auto tasks |
|
||||
| event_wait | 23KB | Event-driven waits |
|
||||
| evidence_probe | 7KB | Evidence probes |
|
||||
| forge | 5KB | Forge adapter |
|
||||
| forge_client | 7KB | Forge client |
|
||||
| goal | 14KB | Goal management |
|
||||
| growth_delta | 16KB | Growth tracking |
|
||||
| warm_router | 55KB | Warm routing |
|
||||
|
||||
**Exit condition**: the operator turn loop runs in Go, routes through Go model connections, dispatches to Rust kernels via FFI.
|
||||
|
||||
### Wave 5c: The Go memory operations (8 modules, ~8,240 lines)
|
||||
|
||||
| Module | Python lines | Notes |
|
||||
|---|---|---|
|
||||
| memory_consolidation | 31KB | Consolidation operations (kernel already in Rust) |
|
||||
| memory_store | 99KB | THE memory store — SQLite operations |
|
||||
| curator | 70KB | Memory curation |
|
||||
| sleep_time | 38KB | Sleep-time consolidation |
|
||||
| abstraction | 22KB | Fact extraction (kernel already in Rust) |
|
||||
| persona_gate | 24KB | Persona gating |
|
||||
| witness | 15KB | Witness reflection |
|
||||
| stage_guards | 12KB | Stage guards |
|
||||
|
||||
**Exit condition**: memory operations run in Go, calling Rust kernels for the deterministic compute.
|
||||
|
||||
### Wave 6: The Rust deterministic backend (85 modules, ~25,218 lines)
|
||||
|
||||
Three sub-waves by dependency order:
|
||||
|
||||
**Wave 6a** (30 modules, ~11,515 lines): the structural core — spine, structural_store, construction_*, contracts, capability_*, audit_ledger, behaviour_ledger, claim_audit, etc.
|
||||
|
||||
**Wave 6b** (30 modules, ~6,849 lines): the cognition layer — drift_*, graph_memory, memory_selector, identity_anchors, pins, salience_gate, self_improve, token_optimizer, etc.
|
||||
|
||||
**Wave 6c** (25 modules, ~6,854 lines): the remaining modules — voice_hybrid, voice_sync, prompt_sync, time_awareness, toolchain, verification_layer, etc.
|
||||
|
||||
**Exit condition**: every deterministic module exists as a Rust function in a crate, the Python version is a thin FFI caller or deleted.
|
||||
|
||||
### Wave 7: The TUI + test infrastructure (36 modules, ~10,832 lines)
|
||||
|
||||
The TUI stays Python (Textual is Python-native) but its backend calls go through the Go/Rust dichotomy. Test infrastructure stays Python (pytest is Python-native).
|
||||
|
||||
**Exit condition**: the TUI is the only Python surface, everything behind it is Go/Rust.
|
||||
|
||||
---
|
||||
|
||||
## §3 — PER-FILE EPIC TEMPLATE (§7 anti-ballooning: 180 files → per-file epics)
|
||||
|
||||
Each file's port gets ONE epic. The epic body contains:
|
||||
|
||||
1. **File identity**: `src/rune/<module>.py` — N lines, M functions, K classes
|
||||
2. **Three questions**: (a) can it be IMPROVED in the target language? (b) does it need FIXING? (c) are there MISSING CONNECTORS?
|
||||
3. **Language assessment**: confirmed GO/RUST per classification, with the evidence
|
||||
4. **Constraint context**: which henxels, line caps, canary requirements apply
|
||||
5. **Pipeline plan**: branch name, PR route, canary battery spec
|
||||
6. **Verification**: the specific tests + canaries that prove the port
|
||||
7. **Provenance**: links to the wave, the tranche, the classification record
|
||||
|
||||
---
|
||||
|
||||
## §4 — DECISION RUBRIC
|
||||
|
||||
Per-unit language decisions are made against:
|
||||
|
||||
1. **Does the module initiate/respond/route?** → Go
|
||||
2. **Does it compute/store/pure-function?** → Rust
|
||||
3. **Does it have BOTH?** → Hybrid: Go front end calls Rust kernel via FFI (contracted membrane junction)
|
||||
4. **Is it the TUI?** → Python (last)
|
||||
5. **Is it test infrastructure?** → Python (stays)
|
||||
|
||||
Evidence required: the module's actual imports, its primary purpose, its callers, and what breaks if it's removed.
|
||||
|
||||
---
|
||||
|
||||
## §5 — PIPELINE ROUTING
|
||||
|
||||
Every port flows through:
|
||||
|
||||
1. **Construction contract**: `rune scaffold submit-need` → decision → contract → building
|
||||
2. **Branch**: `feat/port-<module>-to-<lang>` from master
|
||||
3. **Implementation**: Go module in `src/go/<name>/` or Rust module in `src/rust/<crate>/`
|
||||
4. **Python oracle**: the original Python function preserved as `_py_<function>` for parity
|
||||
5. **Delegation**: the Python public delegates to the Go/Rust implementation
|
||||
6. **Canary**: a parity battery proving kernel↔oracle equivalence
|
||||
7. **CI**: the canary registered in `.gitea/workflows/ci.yml`
|
||||
8. **PR**: → master, required checks, merge-robot
|
||||
9. **Deploy**: auto-version bump, deploy pipeline, runtime verification
|
||||
|
||||
---
|
||||
|
||||
## §6 — VERIFICATION GATES
|
||||
|
||||
Per-wave exit:
|
||||
- All modules in the wave exist as Go/Rust implementations
|
||||
- Python versions delegate (or are deleted if nothing calls them)
|
||||
- CI go-surface job passes (for Go modules)
|
||||
- CI canary job passes (for Rust kernels)
|
||||
- Full test suite green
|
||||
- diff-cover ≥70% on changed lines
|
||||
- Deploy advances the current symlink
|
||||
|
||||
Per-tranche exit:
|
||||
- `ls src/rune/*.py` returns ≤7 files (TUI only)
|
||||
- Full test suite green
|
||||
- All canaries green
|
||||
- All henxels hold
|
||||
- Deploy verified at the final version
|
||||
|
||||
---
|
||||
|
||||
## §7 — RISK REGISTER
|
||||
|
||||
| Risk | Prevention | Detection |
|
||||
|---|---|---|
|
||||
| Big-bang rewrite breaks everything | Incremental waves, each independently deployable | CI red on any wave |
|
||||
| Dual-truth drift (Python and Go/Rust diverge) | Parity canaries, _py_ oracles | Canary failure |
|
||||
| Broken keep-greens | Named up front, checked per-wave | CI red |
|
||||
| Silent scope creep | Per-file epics with explicit scope | Code review |
|
||||
| FFI boundary breaks | CC_RUST_LIB/SC_RUST_LIB env contract | Kernel probe test |
|
||||
| Memory store data loss | SQLite retained, reads proven byte-identical | Canary hash check |
|
||||
| Operator turn loop regression | Full test suite + runtime verification | Deploy verification |
|
||||
| Henxels line-cap violations | Carve before commit (spine_language pattern) | CI lint red |
|
||||
| New module without canary | New-module henxel | CI lint red |
|
||||
|
||||
---
|
||||
|
||||
## §8 — ROLLBACK / KNOWN-GOOD
|
||||
|
||||
Each wave is independently deployable. If a wave breaks:
|
||||
1. The Python version is still in the tree (delegation, not deletion)
|
||||
2. `RUNE_COGNITION_CORE=python` pins the fallback
|
||||
3. The deploy symlink can be pointed back to the previous revision
|
||||
4. Git revert the wave's merge commit
|
||||
|
||||
---
|
||||
|
||||
## §9 — SCALE HONESTY
|
||||
|
||||
This is **weeks of work, not hours**. 180 modules at ~1-2 modules per session = 90-180 sessions. The previous attempt (R2) ported 10 crates + 2 Go modules in multiple sessions. This tranche is the FULL job.
|
||||
|
||||
The wave structure is designed for incremental progress: each wave is independently deployable, each module's port is independently verifiable, and the system works at every intermediate state. The Python versions delegate to the Go/Rust implementations — they don't get deleted until the port is proven.
|
||||
|
||||
---
|
||||
|
||||
## §10 — APPROVAL GATE
|
||||
|
||||
This tranche is presented for operator approval. The operator reads the wave structure, the module assignments, the per-wave exit conditions, and either approves (execution starts at Wave 4) or requests changes.
|
||||
|
||||
The previous attempt (R2) ported 10 crates + 2 Go modules — less than 10% of the codebase. This tranche is the FULL job: every module, every line, every language decision, with the dichotomy honestly applied.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Total-Rebase Inventory (R2 Wave 0.1) — measured 2026-10-04
|
||||
|
||||
Generated from `store/rebase/inventory.json` (AST function-level extraction) +
|
||||
`store/rebase/classification.json` (import/content evidence per module).
|
||||
Placement law: reactive/initiating/user-facing/network/turn-time → GO;
|
||||
deterministic/stateful/storage/cognition → RUST; Textual rendering → TUI-LAST;
|
||||
pure compute with no I/O markers defaults to the deterministic backend.
|
||||
|
||||
## Totals
|
||||
|
||||
| Side | Modules | Lines | Functions |
|
||||
|---|---|---|---|
|
||||
| GO | 43 | 19495 | 708 |
|
||||
| RUST | 93 | 36215 | 1091 |
|
||||
| TUI-LAST | 7 | 5438 | 225 |
|
||||
| TEST-INFRA | 29 | 5395 | 158 |
|
||||
| RETIRES | 2 | 93 | 9 |
|
||||
|
||||
## Per-module classification (evidence-carrying)
|
||||
|
||||
| Module | Side | Lines | Functions | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| rune_tui | TUI-LAST | 4041 | 166 | textual/rich imports: ['rich', 'textual'] |
|
||||
| rune_operator | GO | 2697 | 68 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| memory_store | RUST | 2435 | 56 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| curator | RUST | 1838 | 43 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| tool_executor | GO | 1812 | 47 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
|
||||
| warm_router | RUST | 1536 | 36 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess']; reactive/initiating domain; deterministic/state domain |
|
||||
| construction_interface | RUST | 1039 | 25 | deterministic/state domain |
|
||||
| spine | RUST | 1019 | 31 | deterministic/state domain |
|
||||
| sleep_time | RUST | 971 | 25 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| construction_decision | RUST | 949 | 18 | deterministic/state domain |
|
||||
| capability_broker | RUST | 900 | 21 | deterministic/state domain |
|
||||
| hook_engine | GO | 860 | 8 | reactive/initiating domain |
|
||||
| scaffold | RUST | 854 | 31 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| local_tool_runtime | GO | 806 | 32 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| context_broker | GO | 768 | 23 | network/concurrency imports: ['threading'] |
|
||||
| elastic_cognition | RUST | 765 | 13 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| memory_consolidation | RUST | 733 | 20 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| provider_registry | GO | 666 | 28 | network/concurrency imports: ['threading', 'urllib']; reactive/initiating domain |
|
||||
| persona_gate | RUST | 661 | 21 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| structural_store | RUST | 656 | 33 | deterministic/state domain |
|
||||
| contracts | RUST | 626 | 7 | deterministic/state domain |
|
||||
| adaptive_router | GO | 625 | 19 | reactive/initiating domain |
|
||||
| work_manager | GO | 591 | 25 | reactive/initiating domain |
|
||||
| abstraction | RUST | 587 | 12 | sqlite3 (storage-backed); network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| construction_intent | RUST | 555 | 16 | deterministic/state domain |
|
||||
| canary_audit_ledger | TEST-INFRA | 540 | 2 | canary/test module |
|
||||
| safety_override | RUST | 530 | 18 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| failure_auto_filer | GO | 527 | 28 | network/concurrency imports: ['threading'] |
|
||||
| build_mode | GO | 517 | 34 | network/concurrency imports: ['subprocess'] |
|
||||
| drift_core | RUST | 517 | 14 | deterministic/state domain |
|
||||
| event_wait | GO | 517 | 14 | network/concurrency imports: ['subprocess', 'urllib']; reactive/initiating domain |
|
||||
| tui_validator | TUI-LAST | 514 | 13 | tui family |
|
||||
| memory_selector | RUST | 511 | 14 | deterministic/state domain |
|
||||
| system_registry | RUST | 501 | 14 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| construction_pattern | RUST | 496 | 13 | deterministic/state domain |
|
||||
| cache | GO | 492 | 17 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
|
||||
| claim_audit | RUST | 483 | 14 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| graph_memory | RUST | 474 | 9 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| identity_anchors | RUST | 474 | 8 | deterministic/state domain |
|
||||
| model_connect | GO | 462 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| auto_tasks | GO | 460 | 14 | sqlite3 (storage-backed); network/concurrency imports: ['subprocess'] |
|
||||
| capability_fit | RUST | 455 | 14 | deterministic/state domain |
|
||||
| tool_protocol | GO | 455 | 27 | reactive/initiating domain |
|
||||
| drift_ledger | RUST | 453 | 16 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| witness | RUST | 451 | 10 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| goal | RUST | 441 | 20 | network/concurrency imports: ['urllib']; deterministic/state domain |
|
||||
| release_notes | GO | 439 | 17 | network/concurrency imports: ['urllib'] |
|
||||
| cron_engine | GO | 436 | 28 | network/concurrency imports: ['subprocess', 'threading']; reactive/initiating domain |
|
||||
| model_profiles | GO | 435 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| microkernel | GO | 434 | 12 | network/concurrency imports: ['subprocess', 'threading'] |
|
||||
| model_routing | GO | 432 | 15 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| voice_hybrid | RUST | 421 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
|
||||
| behavior | RUST | 407 | 9 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| token_optimizer | RUST | 407 | 23 | deterministic/state domain |
|
||||
| capability_ledger | RUST | 405 | 19 | deterministic/state domain |
|
||||
| night_mode | GO | 405 | 19 | network/concurrency imports: ['subprocess', 'threading', 'urllib'] |
|
||||
| chat_bubble | TUI-LAST | 401 | 16 | textual/rich imports: ['rich', 'textual'] |
|
||||
| growth_delta | RUST | 400 | 16 | network/concurrency imports: ['subprocess', 'urllib']; deterministic/state domain |
|
||||
| construction_mode | RUST | 395 | 5 | deterministic/state domain |
|
||||
| prompt_sync | RUST | 376 | 14 | sqlite3 (storage-backed) |
|
||||
| provider_budget | GO | 354 | 21 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| structural_model | RUST | 352 | 21 | deterministic/state domain |
|
||||
| programme_continuation | RUST | 349 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_rune_serve | TEST-INFRA | 329 | 13 | canary/test module |
|
||||
| builder_bridge | GO | 326 | 11 | reactive/initiating domain |
|
||||
| canary_p1_07 | TEST-INFRA | 324 | 12 | canary/test module |
|
||||
| layout_contract | RUST | 316 | 6 | deterministic/state domain |
|
||||
| message_queue | GO | 316 | 14 | reactive/initiating domain |
|
||||
| drift_observability | RUST | 315 | 9 | deterministic/state domain |
|
||||
| build_basis | RUST | 314 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_p0_07 | TEST-INFRA | 310 | 11 | canary/test module |
|
||||
| drift_compression | RUST | 307 | 12 | deterministic/state domain |
|
||||
| event_envelope | GO | 303 | 8 | reactive/initiating domain |
|
||||
| attention_capsule | RUST | 296 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_p1_01 | TEST-INFRA | 294 | 14 | canary/test module |
|
||||
| voice_sync | RUST | 293 | 10 | sqlite3 (storage-backed); reactive/initiating domain |
|
||||
| drift_cache | RUST | 292 | 16 | deterministic/state domain |
|
||||
| outcome_ledger | RUST | 286 | 8 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| time_awareness | RUST | 283 | 15 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tiered_executor | GO | 282 | 9 | network/concurrency imports: ['urllib'] |
|
||||
| stage_guards | RUST | 281 | 11 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| standardized_providers | GO | 281 | 5 | reactive/initiating domain |
|
||||
| canary_p1_06 | TEST-INFRA | 280 | 9 | canary/test module |
|
||||
| progressive_escalation | RUST | 279 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| pins | RUST | 276 | 11 | deterministic/state domain |
|
||||
| canary_p1_03 | TEST-INFRA | 271 | 9 | canary/test module |
|
||||
| auto_context | GO | 270 | 8 | network/concurrency imports: ['subprocess'] |
|
||||
| task_persist | RUST | 269 | 10 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| context_pressure | RUST | 262 | 9 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_gitea_automation | TEST-INFRA | 261 | 3 | canary/test module |
|
||||
| canary_p1_02 | TEST-INFRA | 253 | 12 | canary/test module |
|
||||
| construction_patterns_builtin | RUST | 249 | 1 | deterministic/state domain |
|
||||
| canary_full_system | TEST-INFRA | 244 | 2 | canary/test module |
|
||||
| event_bus | GO | 244 | 7 | reactive/initiating domain |
|
||||
| salience_gate | RUST | 243 | 4 | deterministic/state domain |
|
||||
| self_improve | RUST | 242 | 5 | sqlite3 (storage-backed) |
|
||||
| construction_activation | RUST | 237 | 4 | deterministic/state domain |
|
||||
| toolchain | RUST | 236 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| task_gate | RUST | 235 | 8 | sqlite3 (storage-backed) |
|
||||
| core_server | GO | 234 | 13 | network/concurrency imports: ['http', 'urllib']; reactive/initiating domain |
|
||||
| audit_ledger | RUST | 231 | 8 | deterministic/state domain |
|
||||
| canary_native | TEST-INFRA | 231 | 3 | canary/test module |
|
||||
| openai_compat_transport | GO | 227 | 10 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| shard_manager | RUST | 223 | 10 | sqlite3 (storage-backed) |
|
||||
| response_adapter | RUST | 222 | 7 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| evidence_probe | RUST | 212 | 9 | network/concurrency imports: ['subprocess']; deterministic/state domain |
|
||||
| model_registry | GO | 211 | 8 | reactive/initiating domain |
|
||||
| persona_definition | GO | 210 | 8 | network/concurrency imports: ['subprocess'] |
|
||||
| cognitive_budgeter | RUST | 208 | 3 | deterministic/state domain |
|
||||
| canary_p0_04 | TEST-INFRA | 207 | 8 | canary/test module |
|
||||
| forge_client | GO | 203 | 19 | network/concurrency imports: ['urllib']; reactive/initiating domain |
|
||||
| dependency_tracker | RUST | 202 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_context_broker | TEST-INFRA | 199 | 10 | canary/test module |
|
||||
| canary_p0_05 | TEST-INFRA | 199 | 16 | canary/test module |
|
||||
| testing_mode | RUST | 196 | 13 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| parallel_processor | GO | 194 | 17 | network/concurrency imports: ['threading'] |
|
||||
| canary_spine_meta | TEST-INFRA | 192 | 7 | canary/test module |
|
||||
| iteration | RUST | 190 | 8 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| verification_layer | RUST | 189 | 12 | deterministic/state domain |
|
||||
| subconscious_pass | RUST | 187 | 6 | deterministic/state domain |
|
||||
| ffi_query_memory_smoke | RUST | 182 | 5 | deterministic/state domain |
|
||||
| tier_manager | RUST | 182 | 9 | deterministic/state domain |
|
||||
| canary_tui_transport | TEST-INFRA | 174 | 7 | canary/test module |
|
||||
| behaviour_ledger | RUST | 173 | 13 | deterministic/state domain |
|
||||
| construction_commands | RUST | 173 | 4 | deterministic/state domain |
|
||||
| canary_tui_drift | TEST-INFRA | 170 | 2 | canary/test module |
|
||||
| intent_continuity | RUST | 168 | 11 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| rune_profiler | RUST | 165 | 3 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| inference_trace | RUST | 164 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| ms_loader | RUST | 163 | 16 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| subsystem_health | GO | 163 | 8 | network/concurrency imports: ['threading'] |
|
||||
| recall_router | GO | 162 | 3 | reactive/initiating domain |
|
||||
| voicebox | GO | 161 | 11 | network/concurrency imports: ['subprocess']; reactive/initiating domain |
|
||||
| canary_gitea_pipeline | TEST-INFRA | 159 | 2 | canary/test module |
|
||||
| forge | GO | 146 | 12 | network/concurrency imports: ['subprocess', 'urllib'] |
|
||||
| rune_graphic | TUI-LAST | 145 | 5 | textual/rich imports: ['rich'] |
|
||||
| connection_pool | GO | 144 | 9 | sqlite3 (storage-backed); network/concurrency imports: ['threading'] |
|
||||
| recall_uplift | RUST | 141 | 1 | sqlite3 (storage-backed) |
|
||||
| project_registry | RUST | 139 | 12 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| construction_depth | RUST | 138 | 2 | deterministic/state domain |
|
||||
| voice_injector | GO | 138 | 5 | reactive/initiating domain |
|
||||
| canary_rune_entry | TEST-INFRA | 137 | 3 | canary/test module |
|
||||
| construction_projection | RUST | 135 | 3 | deterministic/state domain |
|
||||
| rune_spinner | RUST | 130 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tui_backlog | TUI-LAST | 125 | 5 | tui family |
|
||||
| warm_neuron_store | RUST | 122 | 6 | sqlite3 (storage-backed); deterministic/state domain |
|
||||
| drift_resume | RUST | 118 | 3 | deterministic/state domain |
|
||||
| wait_guard | RUST | 116 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_conversation_state | TEST-INFRA | 115 | 2 | canary/test module |
|
||||
| canary_query_memory | TEST-INFRA | 110 | 2 | canary/test module |
|
||||
| failure_recovery | RUST | 108 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| graph_projection | RUST | 108 | 5 | deterministic/state domain |
|
||||
| tui_transport | TUI-LAST | 108 | 12 | tui family |
|
||||
| spine_loader | RUST | 105 | 7 | deterministic/state domain |
|
||||
| canary_unbounded | TEST-INFRA | 104 | 2 | canary/test module |
|
||||
| message_widgets | TUI-LAST | 104 | 8 | textual/rich imports: ['rich', 'textual'] |
|
||||
| cs_loader | RUST | 90 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| tc_loader | RUST | 88 | 5 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_schema_migration | TEST-INFRA | 86 | 1 | canary/test module |
|
||||
| programme_gates | RUST | 85 | 6 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| structural_migration | RUST | 77 | 4 | deterministic/state domain |
|
||||
| memory_env | RETIRES | 72 | 8 | bootstrap glue subsumed by the Go runtime env contract |
|
||||
| impact_preview | RUST | 69 | 2 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| provider_context | GO | 48 | 1 | reactive/initiating domain |
|
||||
| wait_guard_corpus | RUST | 46 | 0 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_cognitive_strata | TEST-INFRA | 44 | 1 | canary/test module |
|
||||
| builder_bridge_cli | GO | 42 | 1 | reactive/initiating domain |
|
||||
| route_risk_flags | RUST | 37 | 1 | no network/reactive markers — pure compute defaults to the deterministic backend |
|
||||
| canary_memory_synthesis | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_resource_authority | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_surface_parity | TEST-INFRA | 33 | 1 | canary/test module |
|
||||
| canary_orchestration_foundation | TEST-INFRA | 32 | 1 | canary/test module |
|
||||
| canary_gitea_delegation | TEST-INFRA | 31 | 1 | canary/test module |
|
||||
| __main__ | RETIRES | 21 | 1 | bootstrap glue subsumed by the Go runtime env contract |
|
||||
+3
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.83.5"
|
||||
version = "0.136.3"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -16,12 +16,13 @@ dependencies = [
|
||||
"aiohttp>=3.9.0",
|
||||
"websockets>=12.0",
|
||||
"psutil>=5.9.0",
|
||||
"pillow>=10.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
memory = ["sqlite-vec>=0.1.0"]
|
||||
dev = ["pytest>=8.0", "pytest-subtests", "ruff==0.16.5", "pyright", "henxels==0.12.0", "coverage>=7.6", "diff-cover>=9.2"]
|
||||
dev = ["pytest>=8.0", "pytest-subtests", "ruff==0.16.5", "pyright", "henxels==0.12.0", "coverage>=7.6", "diff-cover>=9.2", "hypothesis>=6.100"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
+5
-1
@@ -29,6 +29,10 @@
|
||||
"**/__pycache__"
|
||||
],
|
||||
"extraPaths": [
|
||||
"scripts"
|
||||
"scripts",
|
||||
"src/rune",
|
||||
"shared",
|
||||
"src/rust/evidence_provenance",
|
||||
"src/rust/query_memory"
|
||||
]
|
||||
}
|
||||
+16
-10
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o /tmp/regen-req.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
aiohttp==3.14.4
|
||||
# via rune (pyproject.toml)
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
@@ -18,9 +18,9 @@ certifi==2026.7.22
|
||||
# httpx
|
||||
chardet==7.6.0
|
||||
# via diff-cover
|
||||
coverage==7.16.1
|
||||
coverage==7.16.2
|
||||
# via rune (pyproject.toml)
|
||||
diff-cover==10.5.1
|
||||
diff-cover==10.6.0
|
||||
# via rune (pyproject.toml)
|
||||
frozenlist==1.8.0
|
||||
# via
|
||||
@@ -34,12 +34,14 @@ httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via rune (pyproject.toml)
|
||||
hypothesis==6.168.5
|
||||
# via rune (pyproject.toml)
|
||||
idna==3.20
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# yarl
|
||||
iniconfig==2.3.0
|
||||
iniconfig==2.3.1
|
||||
# via pytest
|
||||
jinja2==3.1.6
|
||||
# via diff-cover
|
||||
@@ -50,21 +52,23 @@ markdown-it-py==4.2.0
|
||||
# mdit-py-plugins
|
||||
# rich
|
||||
# textual
|
||||
markupsafe==3.0.3
|
||||
markupsafe==3.0.4
|
||||
# via jinja2
|
||||
mdit-py-plugins==0.6.1
|
||||
# via textual
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.8.0
|
||||
multidict==6.9.1
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
nodeenv==1.10.0
|
||||
nodeenv==1.11.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.9
|
||||
pillow==12.3.0
|
||||
# via rune (pyproject.toml)
|
||||
platformdirs==4.12.3
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
@@ -100,6 +104,8 @@ rich==15.0.0
|
||||
# textual
|
||||
ruff==0.16.5
|
||||
# via rune (pyproject.toml)
|
||||
sortedcontainers==2.4.0
|
||||
# via hypothesis
|
||||
sqlite-vec==0.1.9
|
||||
# via rune (pyproject.toml)
|
||||
textual==8.2.8
|
||||
@@ -116,7 +122,7 @@ typing-extensions==4.16.0
|
||||
# pyright
|
||||
# textual
|
||||
# textual-autocomplete
|
||||
websockets==17.1
|
||||
websockets==17.2
|
||||
# via rune (pyproject.toml)
|
||||
yarl==1.25.1
|
||||
# via aiohttp
|
||||
+127
-8
@@ -148,9 +148,18 @@ def sha_branch(sha: str) -> str:
|
||||
|
||||
|
||||
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())
|
||||
"""SHA is an ancestor of master (local or remote-tracking refs).
|
||||
|
||||
M10 correction (2026-09-19): --all so a freshly fetched master
|
||||
(refs/remotes/gitea/master) counts; local branches alone go stale
|
||||
between fetches. derive_op_type refreshes refs before calling this.
|
||||
"""
|
||||
out = _git("branch", "--all", "--contains", sha)
|
||||
for l in out.splitlines():
|
||||
s = l.strip().removeprefix("remotes/").removesuffix(" *")
|
||||
if s == "master" or s.endswith("/master"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
@@ -184,6 +193,11 @@ def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
# 0. M10 correction (2026-09-19): refresh remote-tracking refs before
|
||||
# inference. A just-merged master commit is invisible to
|
||||
# branch --contains until refs are fetched; stale local git state
|
||||
# must never outrank repo truth. Bounded by GIT_TIMEOUT, fail-open.
|
||||
_git("fetch", "--quiet", "gitea", "+refs/heads/*:refs/remotes/gitea/*")
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
@@ -205,11 +219,16 @@ def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
def normalize_action(action: dict, *,
|
||||
model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> dict:
|
||||
contract_deadline: Any = None,
|
||||
task_id: str = "") -> 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}
|
||||
task_id: REAL_ASYNC_BRIDGE §9 — when the caller knows its task
|
||||
binding, pending-operation resolution is scoped to operations
|
||||
registered with that task_id; stale global operations are
|
||||
never consulted when a binding exists.
|
||||
Returns:
|
||||
{normalized: bool, action: "ContinuePendingOperation", operation_id,
|
||||
op_type, authority, deadline_secs, deadline_provenance,
|
||||
@@ -222,27 +241,124 @@ def normalize_action(action: dict, *,
|
||||
model_deadline_ignored = model_deadline is not None
|
||||
model_op_type_ignored = model_op_type is not None
|
||||
|
||||
# 0. task binding scope (§9): task-bound operations beat global state.
|
||||
# Stored operation wins over ANY model hint (sha/op_type/deadline).
|
||||
if task_id and not op_id:
|
||||
bound = [r for r in pending_operations()
|
||||
if str(r.get("ext", {}).get("task_id", "")) == task_id]
|
||||
if len(bound) == 1:
|
||||
rec = bound[0]
|
||||
norm = _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored,
|
||||
"task_bound",
|
||||
user_deadline, contract_deadline)
|
||||
norm["sha_for_wait"] = _rec_sha(rec)
|
||||
norm["task_id"] = task_id
|
||||
return norm
|
||||
if len(bound) > 1:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in bound],
|
||||
"reason": f"multiple pending operations bound to task {task_id}"}
|
||||
return {"normalized": False,
|
||||
"reason": f"no pending operation bound to task {task_id}"}
|
||||
|
||||
# 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:
|
||||
# REAL_ASYNC_BRIDGE §10: a FRESH operation has no wait record
|
||||
# yet — registration happens at the park step (ci_event_wait).
|
||||
# For an explicit, well-formed, EXISTING commit id, return the
|
||||
# provisional canonical continuation (policy deadline, own-runs
|
||||
# authority) so the model-facing tool can park on first call.
|
||||
# A typo'd/ghost sha is refused here (A-patch lesson): the
|
||||
# commit must exist in the repository.
|
||||
import re as _re
|
||||
m = _re.fullmatch(r"ci-([0-9a-f]{40})", op_id)
|
||||
if m:
|
||||
sha40 = m.group(1)
|
||||
probe = subprocess.run(
|
||||
["git", "-C", str(REPO), "cat-file", "-e",
|
||||
f"{sha40}^{{commit}}"],
|
||||
capture_output=True, timeout=15)
|
||||
if probe.returncode == 0:
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": op_id,
|
||||
"sha_for_wait": sha40,
|
||||
"op_type": "branch_ci",
|
||||
"authority": "ci_runs",
|
||||
"deadline_secs": POLICY_DEADLINES["branch_ci"],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"evidence": "explicit_id_fresh_registration",
|
||||
}
|
||||
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
|
||||
# 2. resolve by sha / active operation.
|
||||
# H-PATCH H2/H3: the model sha is a HINT. If it doesn't resolve but
|
||||
# unambiguous pending operations exist, the STORED target wins — a
|
||||
# mistyped model sha must never create a ghost operation (the A-patch
|
||||
# live failure: a typo'd sha derived branch_ci for a nonexistent commit
|
||||
# and parked a wait on a ghost).
|
||||
# 2. no explicit operation_id: resolve by sha / pending operations.
|
||||
# H-PATCH: when the model provides NO sha (operation-id-first mode),
|
||||
# resolve from unambiguous pending operations — the model should not
|
||||
# need to reconstruct a 40-char sha to express "wait for it".
|
||||
if not sha:
|
||||
all_pending = pending_operations()
|
||||
if len(all_pending) == 1:
|
||||
rec = all_pending[0]
|
||||
norm = _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored,
|
||||
"auto_resolved_no_sha",
|
||||
user_deadline, contract_deadline)
|
||||
norm["sha_for_wait"] = _rec_sha(rec)
|
||||
return norm
|
||||
if len(all_pending) > 1:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in all_pending],
|
||||
"reason": "multiple pending operations, no sha hint"}
|
||||
return {"normalized": False,
|
||||
"reason": "no pending operations and no sha provided"}
|
||||
|
||||
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,
|
||||
rec = candidates[0]
|
||||
stored_sha = _rec_sha(rec)
|
||||
norm = _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored, "pending_match",
|
||||
user_deadline, contract_deadline)
|
||||
if sha and stored_sha and sha != stored_sha:
|
||||
norm["model_sha_mismatch"] = {
|
||||
"model_sha": sha, "stored_sha": stored_sha,
|
||||
"resolution": "stored_target_used"}
|
||||
return norm
|
||||
# model sha unresolved: any unambiguous pending op at all? then the
|
||||
# model probably mistyped the sha — use the stored target, log it.
|
||||
all_pending = pending_operations()
|
||||
if len(all_pending) == 1 and sha:
|
||||
rec = all_pending[0]
|
||||
stored_sha = _rec_sha(rec)
|
||||
norm = _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored,
|
||||
"unresolved_sha_single_pending",
|
||||
user_deadline, contract_deadline)
|
||||
norm["model_sha_mismatch"] = {
|
||||
"model_sha": sha, "stored_sha": stored_sha,
|
||||
"resolution": "stored_target_used"}
|
||||
return norm
|
||||
|
||||
# 3. no stored record: derive truth deterministically
|
||||
if not sha:
|
||||
@@ -337,7 +453,8 @@ def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
|
||||
if rec is not None:
|
||||
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
|
||||
None, None)
|
||||
else:
|
||||
norm["sha_for_wait"] = _rec_sha(rec)
|
||||
elif sha:
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
norm = {
|
||||
"normalized": True, "action": "ContinuePendingOperation",
|
||||
@@ -348,8 +465,10 @@ def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": True, "model_op_type_ignored": False,
|
||||
"evidence": evidence,
|
||||
"evidence": evidence, "sha_for_wait": sha,
|
||||
}
|
||||
else:
|
||||
return None
|
||||
norm["raw_command_kind"] = "sleep_poll"
|
||||
norm["raw_sleep_seconds_suppressed"] = secs
|
||||
return norm
|
||||
|
||||
@@ -21,14 +21,37 @@ def main() -> int:
|
||||
n = sub.add_parser("normalize", help="normalize a continuation intent")
|
||||
n.add_argument("--sha", default="")
|
||||
n.add_argument("--operation-id", default="")
|
||||
n.add_argument("--task-id", default="",
|
||||
help="§9 task binding: scope pending resolution to "
|
||||
"operations registered for this task")
|
||||
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)")
|
||||
rb = sub.add_parser("route-bash",
|
||||
help="route a Bash command expressing wait intent")
|
||||
rb.add_argument("--command", required=True)
|
||||
rb.add_argument("--sha-hint", default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cmd == "route-bash":
|
||||
record = await_intent.normalize_bash(args.command,
|
||||
sha_hint=args.sha_hint)
|
||||
if record is None:
|
||||
print(json.dumps({"normalized": False,
|
||||
"reason": "no external continuation intent"}))
|
||||
return 0
|
||||
if record.get("normalized") is True:
|
||||
# the compiled wait needs the SHA: derive from operation_id
|
||||
oid = str(record.get("operation_id", ""))
|
||||
if oid.startswith("ci-"):
|
||||
record["sha_for_wait"] = oid[3:]
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
action = {"kind": "await_operation", "sha": args.sha,
|
||||
"operation_id": args.operation_id,
|
||||
"op_type": args.model_op_type}
|
||||
@@ -40,7 +63,8 @@ def main() -> int:
|
||||
if args.contract_deadline else None)
|
||||
record = await_intent.normalize_action(
|
||||
action, model_deadline=model_deadline,
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline,
|
||||
task_id=args.task_id)
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""backfill_exit_notes.py — retroactively append the EXIT-NOTE (plain-
|
||||
English what-changed) to every existing release that predates the template
|
||||
(Nick directive 2026-10-08). For each tag: render the exit note for the
|
||||
tag's own range (v_prev..v_tag), append it to the release body under a
|
||||
"## What changed" heading — preserving the governed evidence sections —
|
||||
and PATCH the release via the Gitea API. Idempotent: releases already
|
||||
carrying the exit-note heading are skipped.
|
||||
|
||||
Usage: python3 scripts/backfill_exit_notes.py [--dry-run] [--limit N]
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from release_exit_note import render_markdown, render # noqa: E402
|
||||
|
||||
EXIT_HEADING = "## What changed"
|
||||
SERVER = "http://127.0.0.1:3000"
|
||||
REPO_SLUG = "conductor/rune"
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
sys.path.insert(0, str(REPO / ".config" / "opencode" / "shared"))
|
||||
try:
|
||||
from gitea_auth import load_gitea_token
|
||||
return load_gitea_token()
|
||||
except ImportError:
|
||||
# fall back to the git credential
|
||||
r = subprocess.run(
|
||||
["git", "credential", "fill"], input="url=http://127.0.0.1:3000\n",
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("password="):
|
||||
return line.split("=", 1)[1]
|
||||
return ""
|
||||
|
||||
|
||||
def api(path, method="GET", data=None):
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
f"{SERVER}/api/v1/repos/{REPO_SLUG}{path}",
|
||||
data=json.dumps(data).encode() if data is not None else None,
|
||||
headers={"Authorization": f"token {_token()}",
|
||||
"Content-Type": "application/json"},
|
||||
method=method,
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def tags():
|
||||
r = subprocess.run(["git", "tag", "--sort=v:refname"],
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
tags = [t for t in r.stdout.splitlines() if t.startswith("v")]
|
||||
# exit-note applies from v0.117.0 onward (the factory-era releases)
|
||||
try:
|
||||
start = tags.index("v0.117.0")
|
||||
except ValueError:
|
||||
return tags
|
||||
return tags[start:]
|
||||
|
||||
|
||||
def main():
|
||||
dry = "--dry-run" in sys.argv
|
||||
limit = 999
|
||||
if "--limit" in sys.argv:
|
||||
limit = int(sys.argv[sys.argv.index("--limit") + 1])
|
||||
|
||||
all_tags = tags()
|
||||
releases = api("/releases?limit=50&sort=desc")
|
||||
by_tag = {r["tag_name"]: r for r in releases}
|
||||
|
||||
patched, skipped, failed = 0, 0, 0
|
||||
# chronological order (oldest first) so ranges nest correctly
|
||||
for i, tag in enumerate(all_tags):
|
||||
if i == 0:
|
||||
prev = None
|
||||
else:
|
||||
prev = all_tags[i - 1]
|
||||
rel = by_tag.get(tag)
|
||||
if rel is None:
|
||||
skipped += 1
|
||||
continue
|
||||
body = rel.get("body") or ""
|
||||
if EXIT_HEADING in body:
|
||||
skipped += 1
|
||||
continue
|
||||
if i == 0 or prev is None:
|
||||
skipped += 1
|
||||
continue
|
||||
# limit: process the N NEWEST unpatched
|
||||
if patched >= limit:
|
||||
skipped += 1
|
||||
continue
|
||||
try:
|
||||
note = render(prev, tag)
|
||||
md = render_markdown(note)
|
||||
new_body = body.rstrip() + "\n\n---\n\n" + md + "\n"
|
||||
if dry:
|
||||
print(f"[dry] {tag}: would append {len(md)} chars "
|
||||
f"({note['by_kind']})")
|
||||
patched += 1
|
||||
continue
|
||||
api(f"/releases/{rel['id']}", method="PATCH",
|
||||
data={"body": new_body})
|
||||
print(f"patched {tag}: +{len(md)} chars ({note['by_kind']})")
|
||||
patched += 1
|
||||
except Exception as e:
|
||||
print(f"FAILED {tag}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\ndone: {patched} patched, {skipped} skipped, {failed} failed"
|
||||
f"{' (dry run)' if dry else ''}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+217
-77
@@ -47,28 +47,34 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
sys.path.insert(0, str(_SCRIPT_DIR.parent / "src"))
|
||||
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
import completion_file as cf # noqa: E402
|
||||
|
||||
REPO_ROOT = _SCRIPT_DIR.parent
|
||||
QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py"
|
||||
|
||||
|
||||
def authority_verdict() -> str:
|
||||
def authority_verdict(sha: str = "") -> str:
|
||||
"""ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only
|
||||
completion truth. Never parses the event payload."""
|
||||
completion truth. Never parses the event payload.
|
||||
|
||||
H-PATCH H4: the target sha is passed EXPLICITLY (as the subprocess's
|
||||
--sha argument) — ambient DEPLOY_SHA can no longer change which
|
||||
operation is verified. The env var remains only as a CLI backward
|
||||
compat for deploy_qualify.py invoked standalone."""
|
||||
cmd = [sys.executable, str(QUALIFY_SCRIPT)]
|
||||
if sha:
|
||||
cmd += ["--sha", sha]
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(QUALIFY_SCRIPT)],
|
||||
cmd,
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
env={k: v for k, v in os.environ.items() if k != "DEPLOY_SHA"}
|
||||
if sha else 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()
|
||||
|
||||
@@ -169,12 +175,35 @@ def op_authority_verdict(op_type: str, sha: str) -> str:
|
||||
spec = op_spec(op_type)
|
||||
authority = spec["authority"]
|
||||
if authority == "deploy_qualify":
|
||||
return authority_verdict()
|
||||
v = normalize_verdict(authority_verdict(sha))
|
||||
if v != "pending":
|
||||
return v
|
||||
# E-PATCH resilience: Gitea's combined commit-status endpoint can
|
||||
# 404 for the newest commit (observed live on auto-version bump
|
||||
# commits — the status object materializes late/never). The runs
|
||||
# ARE the same evidence the two legs read; fall back to the
|
||||
# operation-scoped runs verdict so authority stays resolvable.
|
||||
if _status_endpoint_unreadable(sha):
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
return v
|
||||
if authority == "ci_runs":
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
|
||||
|
||||
|
||||
def _status_endpoint_unreadable(sha: str) -> bool:
|
||||
"""True when the combined commit-status endpoint 404s for the SHA
|
||||
(index lag / never-materialized status object) — the documented
|
||||
fallback trigger, verified against the API once per call."""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, _body = gitea_auth.api(f"/commits/{sha}/status")
|
||||
return st == 404
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
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)."""
|
||||
@@ -185,7 +214,7 @@ def _ci_runs_verdict(spec: dict, sha: str) -> str:
|
||||
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
|
||||
sha_env = sha # H-PATCH: explicit parameter is the only authority
|
||||
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)
|
||||
@@ -221,7 +250,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
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
|
||||
sha_env = sha # H-PATCH: explicit parameter is the only authority
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return False
|
||||
@@ -243,6 +272,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
|
||||
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
|
||||
max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
"""(sha is EXPLICIT — ambient env never selects the target.)"""
|
||||
"""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
|
||||
@@ -264,9 +294,28 @@ def ci_alive() -> bool:
|
||||
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
|
||||
|
||||
|
||||
def _set_cognition_state(op: str, state: str) -> None:
|
||||
"""E-PATCH §12: stamp cognitive quiescence on the durable record so a
|
||||
fresh process knows cognition is parked (WAITING_EXTERNAL) vs resumable
|
||||
(RESUMABLE). Best-effort: observability never breaks the wait."""
|
||||
try:
|
||||
rec = ew.operation_wait(op)
|
||||
if rec is None:
|
||||
return
|
||||
wait_file = ew.wr_root() / "waits" / f"{rec.get('wait_id')}.json"
|
||||
if not wait_file.is_file():
|
||||
return
|
||||
data = json.loads(wait_file.read_text())
|
||||
data.setdefault("ext", {})["continuation_state"] = state
|
||||
wait_file.write_text(json.dumps(data, indent=1, sort_keys=True))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def run(*, sha: str, deadline_secs: int,
|
||||
watchdog_secs: int = 300,
|
||||
op_type: str = DEFAULT_OP_TYPE,
|
||||
task_id: str = "",
|
||||
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
|
||||
"""T21.5 §3: ONE LOGICAL AWAIT.
|
||||
|
||||
@@ -279,10 +328,28 @@ def run(*, sha: str, deadline_secs: int,
|
||||
"""
|
||||
spec = op_spec(op_type) # unknown op_type refused upfront
|
||||
op = f"ci-{sha}"
|
||||
# A-PATCH: the authority subprocess reads DEPLOY_SHA from the env;
|
||||
# python-level run() callers (tool normalization flow) must export it.
|
||||
if sha and not os.environ.get("DEPLOY_SHA"):
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
# H-PATCH: resolve short SHAs to full 40-char — _fetch_runs and
|
||||
# deploy_qualify match on the FULL sha; a 12-char prefix would
|
||||
# silently fail equality and park the waiter until deadline
|
||||
# (the 962e351 root cause).
|
||||
if sha and len(sha) < 40:
|
||||
try:
|
||||
full = subprocess.run(
|
||||
["git", "-C", str(_SCRIPT_DIR.parent), "rev-parse", sha],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if full.returncode == 0:
|
||||
sha = full.stdout.strip()
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
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)
|
||||
# REAL_ASYNC_BRIDGE: FIFO is replaced by completion-file inotify.
|
||||
# The completion file write is the wake signal; inotify is the
|
||||
# kernel-level zero-latency, zero-CPU wake primitive.
|
||||
# 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
|
||||
@@ -295,7 +362,10 @@ def run(*, sha: str, deadline_secs: int,
|
||||
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))
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs),
|
||||
continuation_state="RESUMABLE", # event already arrived
|
||||
op_type=op_type,
|
||||
authority=spec["authority"])
|
||||
registered_at = rec["created_at"]
|
||||
event_at = time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
@@ -329,8 +399,16 @@ def run(*, sha: str, deadline_secs: int,
|
||||
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))
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs),
|
||||
continuation_state="WAITING_EXTERNAL",
|
||||
op_type=op_type,
|
||||
authority=spec["authority"],
|
||||
task_id=task_id, # §9: bind the operation to the calling task
|
||||
resume_mode="self") # §3: waiter owns its continuation via
|
||||
# completion-file/inotify; the daemon has NO
|
||||
# session-delivery role for this wait
|
||||
registered_at = rec["created_at"]
|
||||
_set_cognition_state(op, "WAITING_EXTERNAL") # cognition parked
|
||||
deadline = time.monotonic() + deadline_secs
|
||||
verdict = "pending"
|
||||
mode = "event"
|
||||
@@ -338,78 +416,110 @@ def run(*, sha: str, deadline_secs: int,
|
||||
resumed_at = ""
|
||||
rearm_reason = ""
|
||||
rearm_count = 0
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
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":
|
||||
|
||||
# REAL_ASYNC_BRIDGE: block on inotify for the completion file.
|
||||
# Zero CPU, zero arbitrary timer. The inotify timeout is a
|
||||
# FAILURE DETECTOR — if no event arrives within the watchdog
|
||||
# window, we check the authority directly (lost-event recovery).
|
||||
# The deadline is containment only.
|
||||
watch_timeout = min(remaining, watchdog_secs)
|
||||
result = cf.wait_for_completion(
|
||||
op, timeout_secs=watch_timeout)
|
||||
|
||||
# REAL_ASYNC_BRIDGE: check durable event log AND wait record
|
||||
# status on EVERY cycle — the daemon may have matched and
|
||||
# transitioned the record via /process-event.
|
||||
wr = ew.operation_wait(op)
|
||||
if wr and wr.get("status") in ("COMPLETED", "FAILED",
|
||||
"READY_TO_RESUME"):
|
||||
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))
|
||||
verdict = normalize_verdict(
|
||||
authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
break
|
||||
prior = ew.find_matching_event("gitea", "ci_completed", op)
|
||||
if prior is not None and prior.get("exit_code") is not None:
|
||||
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"
|
||||
break
|
||||
if result is not None:
|
||||
# completion file exists: event arrived
|
||||
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"
|
||||
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")
|
||||
rearm_reason = ("completion file seen; authority "
|
||||
"pending — re-subscribing")
|
||||
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))
|
||||
|
||||
# inotify timeout: no event within watchdog window
|
||||
# FAILURE DETECTOR: check authority directly
|
||||
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")
|
||||
rearm_reason = ("watchdog expiry; op pending (not started "
|
||||
"or lagging) — re-subscribing inotify")
|
||||
continue
|
||||
finally:
|
||||
os.close(fifo_fd)
|
||||
|
||||
# REAL_ASYNC_BRIDGE: no FIFO to close. The completion file is durable
|
||||
# and survives restarts. Lease release is not needed (no lease system
|
||||
# for file-based delivery).
|
||||
|
||||
|
||||
if verdict == "pending":
|
||||
if mode == "event":
|
||||
mode = "deadline_pending"
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
|
||||
_set_cognition_state(op, "RESUMABLE" if verdict != "pending"
|
||||
else "RECOVERY_REQUIRED")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
# §2 STATE OWNERSHIP: the WAITER owns the OPERATION domain. On a
|
||||
# terminal authority verdict, stamp the durable record terminal with
|
||||
# the evidence — the daemon's session-delivery machinery must never
|
||||
# (and can no longer) overwrite this truth. Also updates the parked
|
||||
# continuation_state so fresh-process recovery sees reality.
|
||||
try:
|
||||
rec2 = ew.operation_wait(op) or rec
|
||||
if verdict != "pending":
|
||||
rec2["status"] = "COMPLETED" if verdict == "success" else "FAILED"
|
||||
rec2["verdict"] = verdict
|
||||
rec2["resume_evidence"] = {
|
||||
"kind": "authority_verified_terminal",
|
||||
"verdict": verdict,
|
||||
"mode": mode,
|
||||
"authority": spec["authority"],
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"verdict_at": verdict_at,
|
||||
}
|
||||
rec2["ext"] = {**rec2.get("ext", {}),
|
||||
"continuation_state": ("RESUMABLE" if verdict != "pending"
|
||||
else "RECOVERY_REQUIRED")}
|
||||
ew._load_waitstore().save(rec2)
|
||||
except Exception as _e: # noqa: BLE001 — record stamping must never corrupt the envelope (the envelope + authority remain the truth)
|
||||
print(f"ci_event_wait: terminal record stamp failed: {_e}",
|
||||
file=sys.stderr)
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
@@ -427,21 +537,51 @@ def run(*, sha: str, deadline_secs: int,
|
||||
print(json.dumps(out))
|
||||
return 0 if verdict == "success" else 1
|
||||
|
||||
KNOWN_FLAGS = ("--sha", "--deadline", "--op-type", "--watchdog", "--task-id")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""§5 CLI CONTRACT: no silent argument loss. Every flag is either
|
||||
explicitly supported (and takes effect) or loudly rejected — an
|
||||
acceptance envelope must reflect the configuration actually used."""
|
||||
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]
|
||||
watchdog = 300
|
||||
task_id = ""
|
||||
i = 0
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a == "--sha":
|
||||
sha = args[i + 1]; i += 2
|
||||
elif a == "--deadline":
|
||||
deadline = int(args[i + 1]); i += 2
|
||||
elif a == "--op-type":
|
||||
op_type = args[i + 1]; i += 2
|
||||
elif a == "--watchdog":
|
||||
watchdog = int(args[i + 1]); i += 2
|
||||
elif a == "--task-id":
|
||||
task_id = args[i + 1]; i += 2
|
||||
elif a.startswith("--"):
|
||||
print(json.dumps({"error": f"unknown flag {a!r}",
|
||||
"known_flags": list(KNOWN_FLAGS),
|
||||
"contract": "no silent argument loss"}))
|
||||
return 2
|
||||
else:
|
||||
print(json.dumps({"error": f"unexpected positional {a!r}",
|
||||
"known_flags": list(KNOWN_FLAGS)}))
|
||||
return 2
|
||||
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)
|
||||
# A-PATCH fix: run()'s authority subprocess (deploy_qualify.py) reads
|
||||
# DEPLOY_SHA from the environment — a CLI `--sha` consumer previously
|
||||
# got a silent "pending" verdict forever because the var was never
|
||||
# exported. Export it so the canonical authority can resolve.
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type,
|
||||
watchdog_secs=watchdog, task_id=task_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
|
||||
with inotify wake. Replaces FIFO+lease for process-wait delivery.
|
||||
|
||||
DESIGN:
|
||||
Producer (bridge/daemon/notify):
|
||||
1. Validates operation identity
|
||||
2. Atomically writes a completion file:
|
||||
resumes/<operation_id>.done
|
||||
containing the event/verdict JSON (write to tmp, rename for atomicity)
|
||||
|
||||
Waiter (ci_event_wait / any continuation consumer):
|
||||
1. Checks if the completion file already exists (event-before-waiter)
|
||||
2. If not: registers inotify watch on the resumes/ directory
|
||||
3. Re-checks after watch registration (closes the registration race)
|
||||
4. Blocks in kernel on inotify — zero CPU, zero timer
|
||||
5. On file-created event: reads the file, verifies authority, returns
|
||||
|
||||
The event is a WAKE SIGNAL. Authority still decides terminal truth.
|
||||
The completion file is durable — survives daemon and waiter restarts.
|
||||
|
||||
WHY NOT FIFO:
|
||||
FIFO is one-shot (token consumed), requires lease management, and
|
||||
fails under session-busy guards. A completion file is atomic, durable,
|
||||
and works across restarts. inotify wakes the process with zero CPU
|
||||
cost and zero latency between file creation and wake.
|
||||
|
||||
WHY NOT POLLING:
|
||||
Polling introduces an arbitrary interval. inotify is a kernel callback
|
||||
that fires exactly when the filesystem changes — the latency tracks
|
||||
the real completion event, not a configured interval.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
def _wr_bridge() -> Path:
|
||||
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
|
||||
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
|
||||
# and any isolated test root always resolve the SAME directory.
|
||||
root = (os.environ.get("WR_ROOT")
|
||||
or os.environ.get("RUNE_WR_ROOT")
|
||||
or "/mnt/workdrive/wr-bridge")
|
||||
return Path(root)
|
||||
|
||||
def _resumes_dir() -> Path:
|
||||
return _wr_bridge() / "resumes"
|
||||
|
||||
# inotify constants (Linux)
|
||||
IN_CREATE = 0x100
|
||||
IN_CLOSE_WRITE = 0x8
|
||||
IN_MOVED_TO = 0x80
|
||||
|
||||
|
||||
def completion_path(operation_id: str) -> Path:
|
||||
return _resumes_dir() / f"{operation_id}.done"
|
||||
|
||||
|
||||
def write_completion(operation_id: str, event: dict) -> Path:
|
||||
"""Atomically write a durable completion file. The rename(2) ensures
|
||||
the waiter never sees a partial file."""
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
target = completion_path(operation_id)
|
||||
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
|
||||
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
|
||||
os.rename(str(tmp), str(target)) # atomic on same filesystem
|
||||
os.chmod(str(target), 0o666) # cross-user bridge
|
||||
return target
|
||||
|
||||
|
||||
def read_completion(operation_id: str) -> dict | None:
|
||||
"""Read a durable completion file. Returns None if absent."""
|
||||
p = completion_path(operation_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def completion_exists(operation_id: str) -> bool:
|
||||
return completion_path(operation_id).is_file()
|
||||
|
||||
|
||||
def _inotify_init_and_add_watch(directory: str) -> int:
|
||||
"""Create an inotify instance and add a watch on the directory.
|
||||
Returns the inotify fd."""
|
||||
libc = __import__("ctypes").CDLL(None, use_errno=True)
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
raise OSError("inotify_init failed")
|
||||
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
|
||||
mask = IN_CREATE | IN_MOVED_TO
|
||||
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
|
||||
if wd < 0:
|
||||
os.close(fd)
|
||||
raise OSError(f"inotify_add_watch failed for {directory}")
|
||||
return fd
|
||||
|
||||
|
||||
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
|
||||
"""Read pending inotify events. Returns list of filenames that were
|
||||
created/moved-to. Blocks until at least one event or timeout."""
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
events = []
|
||||
buf = os.read(fd, 4096)
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
|
||||
pos += 16
|
||||
name = buf[pos:pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
|
||||
pos += name_len
|
||||
if mask & (IN_CREATE | IN_MOVED_TO):
|
||||
events.append(name)
|
||||
return events
|
||||
|
||||
|
||||
def wait_for_completion(operation_id: str, *,
|
||||
timeout_secs: float | None = None,
|
||||
poll_callback=None) -> dict | None:
|
||||
"""Block until a completion file exists for this operation.
|
||||
|
||||
Uses inotify for zero-latency, zero-CPU wake. Falls back to
|
||||
polling_callback (if provided) when inotify is unavailable.
|
||||
|
||||
The completion file is a WAKE SIGNAL. The caller must still verify
|
||||
canonical authority after reading it.
|
||||
|
||||
Returns the event dict from the completion file, or None on timeout.
|
||||
"""
|
||||
target = completion_path(operation_id)
|
||||
|
||||
# 1. check-before-watch (event may have arrived before we started)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
|
||||
except OSError:
|
||||
# inotify unavailable: fall back to polling callback
|
||||
if poll_callback:
|
||||
return poll_callback(operation_id, timeout_secs)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 2. re-check after watch registration (closes the race between
|
||||
# the initial check and the watch being active)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
# 3. block in kernel on inotify — zero CPU, exact latency
|
||||
import select as _select
|
||||
poll = _select.poll()
|
||||
poll.register(fd, _select.POLLIN)
|
||||
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
|
||||
|
||||
while True:
|
||||
remaining = None
|
||||
if deadline is not None:
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
if remaining <= 0.1:
|
||||
return None # timeout
|
||||
events = poll.poll(remaining * 1000 if remaining else None)
|
||||
if not events:
|
||||
continue # poll timeout, re-check
|
||||
names = _inotify_read(fd)
|
||||
if target.name in names:
|
||||
# 4. file appeared — read it
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
# partial/corrupt: keep watching
|
||||
finally:
|
||||
os.close(fd)
|
||||
return None
|
||||
|
||||
|
||||
def clear_completion(operation_id: str) -> None:
|
||||
"""Remove a consumed completion file (idempotent cleanup)."""
|
||||
p = completion_path(operation_id)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
@@ -106,12 +106,25 @@ def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] |
|
||||
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_runs(server: str, repo: str, sha: str, token: str,
|
||||
max_pages: int = 4) -> list[dict] | None:
|
||||
"""H-PATCH: paginate the runs list. The repo's scheduled merge-robot
|
||||
runs flood the first page (~50 slots exhaust within hours), pushing
|
||||
older-but-relevant SHAs to later pages — the cause of the observed
|
||||
authority flapping (terminal commit read as pending). Paginate until
|
||||
the sha is found or max_pages exhausted."""
|
||||
out: list[dict] = []
|
||||
for page in range(1, max_pages + 1):
|
||||
data = _get_json(
|
||||
f"{server}/api/v1/repos/{repo}/actions/runs?limit=50&page={page}",
|
||||
token)
|
||||
if data is None:
|
||||
return None if page == 1 else (out or None)
|
||||
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
|
||||
out.extend(r for r in runs if str(r.get("head_sha", "")) == sha)
|
||||
if len(runs) < 50:
|
||||
break # exhausted
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
|
||||
@@ -132,10 +145,17 @@ def main() -> int:
|
||||
# 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).
|
||||
# H-PATCH H4: the target sha arrives EXPLICITLY (--sha) from callers
|
||||
# that hold structured operation state. DEPLOY_SHA env remains only
|
||||
# for standalone/workflow backward compat — the canonical path never
|
||||
# depends on ambient env for operation identity.
|
||||
ap = __import__("argparse").ArgumentParser()
|
||||
ap.add_argument("--sha", dest="sha_arg", default=None)
|
||||
args, _unknown = ap.parse_known_args()
|
||||
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", "")
|
||||
sha = args.sha_arg or os.environ.get("DEPLOY_SHA", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha and token):
|
||||
print("pending") # missing config → cannot verify → not qualified
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dichotomy_audit.py — the honest completion instrument for the dichotomy rebase.
|
||||
|
||||
Function-level, not directory-level: a module counts as covered only when its
|
||||
PYTHON FUNCTIONS have identifiable counterparts in the Go/Rust implementation.
|
||||
A Go directory with 3 structs and none of the module's 70 functions is a
|
||||
facade, and this audit names it as one.
|
||||
|
||||
Matching rule (name-preserving ports): a Python function name matches when
|
||||
its snake_case form, or its Go/Rust camelCase/PascalCase form, appears in the
|
||||
candidate implementation sources. Deliberately strict — novel Go names that
|
||||
don't echo the Python names do NOT count, because unverifiable "trust me,
|
||||
it's in there somewhere" is exactly the failure mode this instrument exists
|
||||
to kill.
|
||||
|
||||
Usage: python3 scripts/dichotomy_audit.py [--write]
|
||||
--write regenerates docs/dichotomy-rebase-honest-audit.md
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src" / "rune"
|
||||
GO = ROOT / "src" / "go"
|
||||
RUST = ROOT / "src" / "rust"
|
||||
REPORT = ROOT / "docs" / "dichotomy-rebase-honest-audit.md"
|
||||
CLASSIFICATION = ROOT / "store" / "rebase" / "classification.json"
|
||||
|
||||
TEST_PREFIXES = ("test_",)
|
||||
# Modules that stay Python by decision (test infra stays pytest)
|
||||
TEST_INFRA = {
|
||||
"conftest", "test_truth", "test_adaptive_router", "test_canaries",
|
||||
}
|
||||
RETIRES = {"memory_env", "__main__"}
|
||||
|
||||
|
||||
def snake_to_camel(name: str) -> list[str]:
|
||||
"""Candidate identifier forms for a Python name in Go/Rust sources."""
|
||||
parts = name.split("_")
|
||||
camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||||
pascal = "".join(p.capitalize() for p in parts)
|
||||
return [name, camel, pascal]
|
||||
|
||||
|
||||
def module_functions(path: Path) -> list[str]:
|
||||
"""Every function and method name defined in the module."""
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
|
||||
except SyntaxError:
|
||||
return []
|
||||
names = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if node.name.startswith("__") and node.name.endswith("__"):
|
||||
continue # dunders are runtime machinery, not port surface
|
||||
names.append(node.name)
|
||||
return names
|
||||
|
||||
|
||||
def load_classification() -> dict:
|
||||
if CLASSIFICATION.exists():
|
||||
return json.loads(CLASSIFICATION.read_text())
|
||||
return {}
|
||||
|
||||
|
||||
def implementation_corpus() -> dict[str, str]:
|
||||
"""module-kebab -> concatenated Go/Rust source text for that module."""
|
||||
corpus: dict[str, str] = {}
|
||||
|
||||
for d in sorted(GO.iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
chunks = []
|
||||
for f in d.rglob("*.go"):
|
||||
if f.name.endswith("_test.go"):
|
||||
continue
|
||||
chunks.append(f.read_text(encoding="utf-8", errors="replace"))
|
||||
if chunks:
|
||||
corpus[d.name] = "\n".join(chunks)
|
||||
|
||||
# Survivor packages (per-crate Go membranes under cognition/)
|
||||
cog = GO / "cognition"
|
||||
if cog.exists():
|
||||
for d in sorted(cog.iterdir()):
|
||||
if d.is_dir():
|
||||
chunks = [f.read_text(encoding="utf-8", errors="replace")
|
||||
for f in d.rglob("*.go") if not f.name.endswith("_test.go")]
|
||||
if chunks:
|
||||
corpus[f"cognition-{d.name}"] = "\n".join(chunks)
|
||||
|
||||
for d in sorted(RUST.iterdir()):
|
||||
src = d / "src"
|
||||
if src.exists():
|
||||
chunks = [f.read_text(encoding="utf-8", errors="replace")
|
||||
for f in src.rglob("*.rs")]
|
||||
if chunks:
|
||||
corpus[d.name] = "\n".join(chunks)
|
||||
return corpus
|
||||
|
||||
|
||||
def module_to_keys(module_stem: str) -> list[str]:
|
||||
"""Candidate implementation keys for a Python module stem."""
|
||||
kebab = module_stem.replace("_", "-")
|
||||
keys = [kebab, module_stem]
|
||||
# Deep ports and multi-module binaries register aliases:
|
||||
aliases = {
|
||||
"rune_operator": ["operator", "operator-deep", "operator-support", "core-server"],
|
||||
"rune_tui": ["tui"],
|
||||
"rune_operator_cli": ["operator"],
|
||||
"memory_store": ["memory-ops", "memory-store-deep"],
|
||||
"warm_router": ["warm-router", "operator-support"],
|
||||
"tool_executor": ["tool-executor", "local-tool-runtime", "tool-protocol", "tool-protocol-rw"],
|
||||
"local_tool_runtime": ["local-tool-runtime", "operator"],
|
||||
"provider_registry": ["provider-registry", "model-connect"],
|
||||
"adaptive_router": ["adaptive-router", "operator-support"],
|
||||
"hook_engine": ["hook-engine"],
|
||||
"event_bus": ["event-bus"],
|
||||
"event_envelope": ["event-bus"],
|
||||
"message_queue": ["message-queue"],
|
||||
"subsystem_health": ["subsystem-health"],
|
||||
"parallel_processor": ["parallel-processor"],
|
||||
"connection_pool": ["connection-pool"],
|
||||
"recall_router": ["recall-router"],
|
||||
"cron_engine": ["cron-engine"],
|
||||
"cache": ["cache"],
|
||||
"night_mode": ["night-mode"],
|
||||
"work_manager": ["work-manager"],
|
||||
"failure_auto_filer": ["failure-filer"],
|
||||
"microkernel": ["microkernel"],
|
||||
"construction_interface": ["cognition-cognition", "cognition"],
|
||||
"construction_decision": ["cognition-cognition", "cognition"],
|
||||
"rune_spine": ["cognition-spine", "cognition"],
|
||||
"spine": ["cognition-spine", "cognition"],
|
||||
"memory_storage": ["cognition-storage", "cognition"],
|
||||
"memory_retrieval": ["cognition-retrieval", "cognition"],
|
||||
"scaffold_core": ["cognition-scaffold", "cognition"],
|
||||
"evidence_provenance": ["cognition-provenance", "cognition"],
|
||||
"memory_scoring": ["cognition-scoring", "cognition"],
|
||||
"capability_scoring": ["cognition-capability", "cognition"],
|
||||
"token_counting": ["cognition-token", "cognition"],
|
||||
"query_memory": ["cognition-query", "cognition"],
|
||||
}
|
||||
if module_stem in aliases:
|
||||
keys = aliases[module_stem] + keys
|
||||
# cognition_core broker.rs hosts many kernels
|
||||
if module_stem in {
|
||||
"tier_manager", "safety_override", "pins", "identity_anchors",
|
||||
"abstraction", "growth_delta", "witness", "persona_gate",
|
||||
"memory_consolidation", "drift_compression", "context_broker",
|
||||
"persona_definition", "goal",
|
||||
}:
|
||||
keys.append("cognition_core")
|
||||
keys.append("cognition-cognition")
|
||||
return keys
|
||||
|
||||
|
||||
def audit() -> dict:
|
||||
classification = load_classification()
|
||||
corpus = implementation_corpus()
|
||||
|
||||
rows = []
|
||||
for path in sorted(SRC.glob("*.py")):
|
||||
stem = path.stem
|
||||
if stem in RETIRES:
|
||||
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
|
||||
"fns": 0, "matched": 0, "side": "RETIRES", "status": "RETIRES", "missing": []})
|
||||
continue
|
||||
if stem.startswith(TEST_PREFIXES) or stem in TEST_INFRA:
|
||||
rows.append({"module": stem, "lines": len(path.read_text(errors='replace').splitlines()),
|
||||
"fns": 0, "matched": 0, "side": "TEST", "status": "TEST", "missing": []})
|
||||
continue
|
||||
fns = module_functions(path)
|
||||
lines = len(path.read_text(errors="replace").splitlines())
|
||||
entry = classification.get(stem) or classification.get(path.name) or {}
|
||||
side = entry.get("side", entry.get("language", "unclassified"))
|
||||
|
||||
sources = []
|
||||
for key in module_to_keys(stem):
|
||||
if key in corpus:
|
||||
sources.append(corpus[key])
|
||||
blob = "\n".join(sources)
|
||||
|
||||
matched, missing = 0, []
|
||||
for fn in fns:
|
||||
hits = any(cand in blob for cand in snake_to_camel(fn))
|
||||
if hits:
|
||||
matched += 1
|
||||
else:
|
||||
missing.append(fn)
|
||||
|
||||
ratio = matched / len(fns) if fns else (1.0 if lines < 30 else 0.0)
|
||||
if stem in RETIRES:
|
||||
status = "RETIRES"
|
||||
elif not blob:
|
||||
status = "NOT TOUCHED"
|
||||
elif ratio >= 0.8:
|
||||
status = "FULL"
|
||||
elif ratio >= 0.2:
|
||||
status = "PARTIAL"
|
||||
else:
|
||||
status = "NOT TOUCHED"
|
||||
rows.append({"module": stem, "lines": lines, "fns": len(fns),
|
||||
"matched": matched, "side": side, "status": status, "missing": missing[:12]})
|
||||
|
||||
return {"rows": rows}
|
||||
|
||||
|
||||
def summarize(result: dict) -> str:
|
||||
rows = result["rows"]
|
||||
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
|
||||
full = [r for r in runtime if r["status"] == "FULL"]
|
||||
partial = [r for r in runtime if r["status"] == "PARTIAL"]
|
||||
untouched = [r for r in runtime if r["status"] == "NOT TOUCHED"]
|
||||
total_fns = sum(r["fns"] for r in runtime)
|
||||
total_matched = sum(r["matched"] for r in runtime)
|
||||
total_lines = sum(r["lines"] for r in runtime)
|
||||
fn_pct = 100 * total_matched / total_fns if total_fns else 0
|
||||
|
||||
out = []
|
||||
out.append("# DICHOTOMY REBASE — HONEST AUDIT (function-level)")
|
||||
out.append("")
|
||||
out.append("_Regenerate with `python3 scripts/dichotomy_audit.py --write`. A module is FULL only when 80%+ of its Python functions have name-preserving counterparts in the Go/Rust implementation — directories without the functions are facades and count as NOT TOUCHED._")
|
||||
out.append("")
|
||||
out.append(f"**Function-level completion: {fn_pct:.0f}%** ({total_matched}/{total_fns} runtime functions matched)")
|
||||
out.append("")
|
||||
out.append("| Status | Modules | Lines | Meaning |")
|
||||
out.append("|---|---|---|---|")
|
||||
out.append(f"| FULL | {len(full)} | {sum(r['lines'] for r in full)} | 80%+ of functions ported |")
|
||||
out.append(f"| PARTIAL | {len(partial)} | {sum(r['lines'] for r in partial)} | 20–80% — real logic exists, gaps named below |")
|
||||
out.append(f"| NOT TOUCHED | {len(untouched)} | {sum(r['lines'] for r in untouched)} | no name-preserving implementation |")
|
||||
out.append(f"| Total runtime | {len(runtime)} | {total_lines} | |")
|
||||
out.append("")
|
||||
|
||||
if untouched:
|
||||
out.append("## NOT TOUCHED — the port queue, in priority order (largest first)")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Fns | Side |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(untouched, key=lambda r: -r["lines"]):
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['fns']} | {r['side']} |")
|
||||
out.append("")
|
||||
if partial:
|
||||
out.append("## PARTIAL — gaps to close (matched/total fns; first missing named)")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Coverage | Missing (first named) |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(partial, key=lambda r: -(r["fns"] - r["matched"])):
|
||||
miss = ", ".join(r["missing"][:6]) if r["missing"] else "(structural)"
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {miss} |")
|
||||
out.append("")
|
||||
out.append("## FULL — ported, parity-gated")
|
||||
out.append("")
|
||||
out.append("| Module | Lines | Fns | Side |")
|
||||
out.append("|---|---|---|---|")
|
||||
for r in sorted(full, key=lambda r: -r["lines"]):
|
||||
out.append(f"| {r['module']} | {r['lines']} | {r['matched']}/{r['fns']} | {r['side']} |")
|
||||
out.append("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
write = "--write" in sys.argv
|
||||
result = audit()
|
||||
report = summarize(result)
|
||||
if write:
|
||||
REPORT.write_text(report)
|
||||
print(f"written: {REPORT}")
|
||||
# stderr summary for the session log
|
||||
rows = result["rows"]
|
||||
runtime = [r for r in rows if r["status"] not in ("TEST", "RETIRES")]
|
||||
tf = sum(r["fns"] for r in runtime)
|
||||
tm = sum(r["matched"] for r in runtime)
|
||||
print(f"FUNCTION-LEVEL: {tm}/{tf} ({100*tm/tf if tf else 0:.0f}%) | "
|
||||
f"FULL {sum(1 for r in runtime if r['status']=='FULL')} | "
|
||||
f"PARTIAL {sum(1 for r in runtime if r['status']=='PARTIAL')} | "
|
||||
f"NOT TOUCHED {sum(1 for r in runtime if r['status']=='NOT TOUCHED')}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
import json, sys
|
||||
sys.path.insert(0, 'src')
|
||||
from rune.adaptive_router import (
|
||||
classify_route, derive_sufficiency, body_has_floor_trigger, floor_applies,
|
||||
escalate_route, downgrade_route, escalate_on_verification_failure,
|
||||
build_research_dispatch, FloorViolation,
|
||||
)
|
||||
|
||||
cases = []
|
||||
ESC_EVIDENCE = "the fix works locally but the production parser accepts different input shapes — input contract uncertain"
|
||||
|
||||
cr_cases = [
|
||||
("local_fix_happy", "fix the broken typewriter regex in warm_router, crash on empty input",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("standard_change", "add a progress bar to the export dialog",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("defect_word_absent", "update the version number in the footer",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=False, narrow_and_reversible=True)),
|
||||
("floor_new_subsystem", "adopt a new subsystem for memory indexing, replacement of spine",
|
||||
dict(has_local_repro=False, owning_authority_known=False)),
|
||||
("risk_new_authority", "this creates a new authority path for dispatch",
|
||||
dict(has_local_repro=False, owning_authority_known=False)),
|
||||
("uncertainty_competing", "there are competing designs here, expensive reversal likely",
|
||||
dict(has_local_repro=True, owning_authority_known=True)),
|
||||
("not_grounded", "add support for markdown export",
|
||||
dict(has_local_repro=False, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
("inspection_verb", "explain why the failing test fails",
|
||||
dict(has_local_repro=True, owning_authority_known=True, testable_locally=True, narrow_and_reversible=True)),
|
||||
]
|
||||
for name, body, kw in cr_cases:
|
||||
d = classify_route(body, **kw)
|
||||
cases.append({"fn": "classify", "name": name,
|
||||
"input": {"body": body, **kw, "pipeline_sha": "UNKNOWN"},
|
||||
"expected": {"route": d.route, "confidence": d.confidence, "reasons": d.reasons,
|
||||
"risk": d.risk_flags, "uncertainty": d.uncertainty_flags,
|
||||
"floor": d.floor, "task_description": d.task_description}})
|
||||
|
||||
ds_cases = [
|
||||
("grounded_fix", "fix crash", "the parser crashes on empty input, see tests/test_parse.py"),
|
||||
("mechanism", "investigate", "performance is bad under load, benchmark needed"),
|
||||
("external_url", "read the docs", "see https://example.com/api docs for the endpoint"),
|
||||
("version_risk", "upgrade", "upgrade the library to latest version, current behaviour differs"),
|
||||
("inspection", "explain", "why does this happen"),
|
||||
("new_subsystem", "adopt", "this adopts a replacement architecture"),
|
||||
("neutral", "write", "a poem about the sea"),
|
||||
]
|
||||
for name, title, body in ds_cases:
|
||||
s = derive_sufficiency(title, body)
|
||||
cases.append({"fn": "derive", "name": name,
|
||||
"input": {"title": title, "body": body, "ambiguous": False},
|
||||
"expected": {"local_authority": s.local_authority,
|
||||
"external_dependency": s.external_dependency,
|
||||
"version_freshness_risk": s.version_freshness_risk,
|
||||
"verifiability": s.verifiability, "scope_novelty": s.scope_novelty,
|
||||
"unknowns": s.unknowns(),
|
||||
"task_class": s.provenance.get("task_class_language")}})
|
||||
|
||||
cases.append({"fn": "floor_trigger", "name": "trigger_yes",
|
||||
"input": {"body": "adopt a replacement for the cache"},
|
||||
"expected": {"result": body_has_floor_trigger("adopt a replacement for the cache")}})
|
||||
cases.append({"fn": "floor_trigger", "name": "trigger_no",
|
||||
"input": {"body": "fix the typo"},
|
||||
"expected": {"result": body_has_floor_trigger("fix the typo")}})
|
||||
cases.append({"fn": "floor_applies", "name": "res_yes", "input": {"route": "RESEARCHED_CHANGE", "flags": ["new_authority"]},
|
||||
"expected": {"result": floor_applies("RESEARCHED_CHANGE", ["new_authority"])}})
|
||||
cases.append({"fn": "floor_applies", "name": "local_no", "input": {"route": "LOCAL_FIX", "flags": ["new_authority"]},
|
||||
"expected": {"result": floor_applies("LOCAL_FIX", ["new_authority"])}})
|
||||
|
||||
base = classify_route("fix the crash", has_local_repro=True, owning_authority_known=True,
|
||||
testable_locally=True, narrow_and_reversible=True)
|
||||
esc = escalate_route(base, "RESEARCHED_CHANGE", "verify_fail", ESC_EVIDENCE)
|
||||
cases.append({"fn": "escalate", "name": "esc",
|
||||
"input": {"decision_route": base.route, "new_route": "RESEARCHED_CHANGE",
|
||||
"trigger": "verify_fail", "evidence": ESC_EVIDENCE},
|
||||
"expected": {"route": esc.route, "previous": esc.previous_route,
|
||||
"decided_by": esc.decided_by, "trigger": esc.trigger}})
|
||||
|
||||
try:
|
||||
downgrade_route(base, "STANDARD_CHANGE", "try", "ev")
|
||||
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {"base_route": base.route, "base_floor": False,
|
||||
"new_route": "STANDARD_CHANGE"}, "expected": {"route": "STANDARD_CHANGE", "error": None}})
|
||||
except FloorViolation:
|
||||
cases.append({"fn": "downgrade", "name": "dg_ok", "input": {}, "expected": {"route": None, "error": "FloorViolation"}})
|
||||
|
||||
floored = classify_route("adopt a replacement for the memory index")
|
||||
try:
|
||||
downgrade_route(floored, "STANDARD_CHANGE", "efficiency", "faster")
|
||||
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
|
||||
"expected": {"route": "STANDARD_CHANGE", "error": None}})
|
||||
except FloorViolation:
|
||||
cases.append({"fn": "downgrade", "name": "dg_floored", "input": {"base_floor": True, "new_route": "STANDARD_CHANGE"},
|
||||
"expected": {"route": None, "error": "FloorViolation"}})
|
||||
|
||||
try:
|
||||
escalate_on_verification_failure(base, "test failed", "/tmp/ar_probe.jsonl")
|
||||
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
|
||||
"expected": {"result": "ACCEPTED"}})
|
||||
except ValueError:
|
||||
cases.append({"fn": "evidence_short", "name": "short", "input": {"evidence": "test failed"},
|
||||
"expected": {"result": "REJECTED"}})
|
||||
|
||||
disp = build_research_dispatch("T-1", "research the memory index options", floored)
|
||||
cases.append({"fn": "dispatch", "name": "disp",
|
||||
"input": {"task_id": "T-1", "description": "research the memory index options"},
|
||||
"expected": {"task_id": disp["task_id"], "title": disp["title"], "route": disp["route"],
|
||||
"floor": disp["floor"], "executor_entry": disp["executor_entry"]}})
|
||||
|
||||
json.dump(cases, open('src/go/adaptive-router/truth.json', 'w'), indent=1)
|
||||
print(f"{len(cases)} truth cases with inputs")
|
||||
for c in cases:
|
||||
print(f" {c['fn']:14} {c['name']}")
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_build_mode_truth.py — build-mode batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T17:08:00Z-6e177d-unknown).
|
||||
|
||||
The LIVE BuildManager runs with the clock FROZEN (module-patched datetime)
|
||||
and create_worktree no-op'd (reactive piece, deferred to batch 2). Cases
|
||||
capture: the start_build id format, the ledger flow across phases
|
||||
(including a failure + correction), resume-point scans at several states,
|
||||
and the status shape. The Go replay compares state dicts key-for-key.
|
||||
|
||||
Usage: python3 scripts/gen_build_mode_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_mode as bm # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T16:08:00"
|
||||
FROZEN_DT = datetime(2026, 10, 7, 16, 8, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return FROZEN_DT
|
||||
|
||||
|
||||
def main():
|
||||
# Freeze the clock (the manager reads datetime.now at two call sites)
|
||||
bm.datetime = FrozenDatetime
|
||||
|
||||
def flow(name, steps):
|
||||
"""Run a phase flow with the worktree op no-op'd; record the CALLS."""
|
||||
mgr = bm.BuildManager()
|
||||
mgr.create_worktree = lambda: "/nonexistent-worktree" # reactive no-op
|
||||
build_id = mgr.start_build("truth battery build")
|
||||
calls = []
|
||||
for fn, args in steps:
|
||||
fn(mgr, **args)
|
||||
calls.append({"fn": fn.__name__, "args": args})
|
||||
return {
|
||||
"name": name,
|
||||
"build_id": build_id,
|
||||
"calls": calls,
|
||||
"state": _jsonify(mgr.state),
|
||||
"resume_point": mgr.get_resume_point(),
|
||||
"can_resume": mgr.can_resume(),
|
||||
"status": mgr.get_status(),
|
||||
}
|
||||
|
||||
def step(fn, **args):
|
||||
return (fn, args)
|
||||
|
||||
cases = [
|
||||
flow("fresh_start_assess_in_progress", []),
|
||||
flow("assess_complete", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
|
||||
]),
|
||||
flow("through_plan", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={"complexity": 3}),
|
||||
step(bm.BuildManager.complete_research, findings={"findings": "dichotomy applies"}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=["oracle-battery pattern"]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[{"batch": 1, "fns": 11}]),
|
||||
]),
|
||||
flow("verify_with_failures", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={}),
|
||||
step(bm.BuildManager.complete_research, findings={}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=[]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[]),
|
||||
step(bm.BuildManager.complete_build_phase, results=[{"ok": True}]),
|
||||
step(bm.BuildManager.complete_verify, passed=False, failures=["test_x failed"]),
|
||||
]),
|
||||
flow("correction_cycle", [
|
||||
step(bm.BuildManager.complete_assess, complexity_report={}),
|
||||
step(bm.BuildManager.complete_research, findings={}),
|
||||
step(bm.BuildManager.complete_feasible, feasible=True, risk="low"),
|
||||
step(bm.BuildManager.complete_consult, patterns=[]),
|
||||
step(bm.BuildManager.complete_consent, approved=True),
|
||||
step(bm.BuildManager.complete_plan, batches=[{"batch": 1}]),
|
||||
step(bm.BuildManager.start_correct),
|
||||
step(bm.BuildManager.complete_correct, fixed=True),
|
||||
]),
|
||||
]
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/build-mode" / "truth_buildmode.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} flow cases)")
|
||||
|
||||
|
||||
def _jsonify(state):
|
||||
"""asdict + the worktree path the no-op returned."""
|
||||
from dataclasses import asdict
|
||||
d = asdict(state)
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_build_mode_wt_truth.py — build-mode batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T18:26:40Z-8be89a).
|
||||
|
||||
The LIVE BuildManager drives a REAL tmp git repo: create → state captured,
|
||||
merge → state captured, delete → state captured. Cases carry the exact
|
||||
argv sequences (from monkeypatched subprocess.run) + the state after each
|
||||
op + the error texts. The Go plan layer must produce the same argv; the
|
||||
Go exec harness proves the plans drive real git.
|
||||
|
||||
Usage: python3 scripts/gen_build_mode_wt_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import build_mode as bm # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T18:28:00"
|
||||
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime(2026, 10, 7, 18, 28, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def main():
|
||||
bm.datetime = FrozenDatetime
|
||||
bm.REPO_DIR = str(make_git_repo())
|
||||
|
||||
calls = []
|
||||
|
||||
def spy_run(cmd, **kwargs):
|
||||
calls.append(list(cmd))
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
bm.subprocess.run = spy_run
|
||||
|
||||
mgr = bm.BuildManager()
|
||||
build_id = mgr.start_build("worktree battery") # create_worktree inside
|
||||
create_call = calls[0]
|
||||
after_create = mgr.state.worktree_path
|
||||
|
||||
n_before_merge = len(calls)
|
||||
mgr.merge_worktree()
|
||||
merge_call = calls[n_before_merge] # the merge itself
|
||||
after_merge = mgr.state.worktree_path # None: delete follows the merge
|
||||
delete_call = calls[n_before_merge + 1], calls[n_before_merge + 2]
|
||||
|
||||
cases = [
|
||||
{
|
||||
"fn": "plan", "name": "create",
|
||||
"expected": {"argv": create_call},
|
||||
},
|
||||
{
|
||||
"fn": "plan", "name": "merge",
|
||||
"expected": {"argv": merge_call},
|
||||
},
|
||||
{
|
||||
"fn": "plan", "name": "delete_pair",
|
||||
"expected": {"argv": [list(delete_call[0]), list(delete_call[1])]},
|
||||
},
|
||||
{
|
||||
"fn": "state", "name": "round_trip",
|
||||
"expected": {
|
||||
"build_id": build_id,
|
||||
"after_create_is_set": after_create is not None,
|
||||
"after_merge_is_none": after_merge is None,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# error texts: merge failure mapping
|
||||
def fail_run(cmd, **kwargs):
|
||||
if cmd[1] == "merge":
|
||||
return subprocess.CompletedProcess(cmd, 128, "", "fatal: not something we can merge")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
bm.subprocess.run = fail_run
|
||||
mgr2 = bm.BuildManager()
|
||||
mgr2.state = bm.BuildState(
|
||||
build_id="20261007-182800", description="error battery",
|
||||
created_at=FROZEN)
|
||||
wt = "/home/nick/.rune/worktrees/20261007-182800"
|
||||
mgr2.state.worktree_path = wt # merge_worktree requires a live worktree path
|
||||
try:
|
||||
mgr2.merge_worktree()
|
||||
merge_err = None
|
||||
except RuntimeError as ex:
|
||||
merge_err = str(ex)
|
||||
cases.append({
|
||||
"fn": "error", "name": "merge_failure_text",
|
||||
"expected": {"error": merge_err},
|
||||
})
|
||||
|
||||
def fail_create(cmd, **kwargs):
|
||||
if cmd[1] == "worktree":
|
||||
return subprocess.CompletedProcess(cmd, 128, "", "'x' already exists")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
bm.subprocess.run = fail_create
|
||||
mgr3 = bm.BuildManager()
|
||||
mgr3.state = bm.BuildState(
|
||||
build_id="20261007-182800", description="error battery 2",
|
||||
created_at=FROZEN)
|
||||
mgr3._save_state()
|
||||
try:
|
||||
mgr3.create_worktree()
|
||||
create_err = None
|
||||
except RuntimeError as ex:
|
||||
create_err = str(ex)
|
||||
cases.append({
|
||||
"fn": "error", "name": "create_failure_text",
|
||||
"expected": {"error": create_err},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/build_mode.py (live BuildManager on a real tmp git repo, clock frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/build-mode" / "truth_worktree.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
def make_git_repo():
|
||||
root = tempfile.mkdtemp(prefix="bm-wt-truth-")
|
||||
env_realloc = subprocess.run(
|
||||
["git", "init", "-q"], cwd=root, capture_output=True, text=True)
|
||||
assert env_realloc.returncode == 0
|
||||
subprocess.run(["git", "config", "user.email", "t@t"], cwd=root, capture_output=True)
|
||||
subprocess.run(["git", "config", "user.name", "t"], cwd=root, capture_output=True)
|
||||
(Path(root) / "seed.txt").write_text("seed\n")
|
||||
subprocess.run(["git", "add", "."], cwd=root, capture_output=True)
|
||||
subprocess.run(["git", "commit", "-qm", "seed"], cwd=root, capture_output=True)
|
||||
return root
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_cache_hash_truth.py — factory batch 6 truth battery (live cache._hash)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import cache as ch # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = [
|
||||
{"fn": "hash", "name": "ascii", "input": {"text": s},
|
||||
"expected": {"hash": ch._hash(s)}}
|
||||
for s in ["", "hello world", "unicode ümlaut ✅", "a" * 500]
|
||||
]
|
||||
comp_inputs = [
|
||||
"",
|
||||
"# comment\n\nreal line here\n====\n// slash comment\n(_source_: x)\n[source: y]\ntiny\nkeep this line too\n\n" + "z" * 900,
|
||||
"line one\nline two\nline three",
|
||||
"====",
|
||||
"ab\ncdefgh",
|
||||
]
|
||||
for i, s in enumerate(comp_inputs):
|
||||
cases.append({"fn": "compress", "name": f"compress_{i}",
|
||||
"input": {"text": s},
|
||||
"expected": {"compressed": ch._compress(s)}})
|
||||
out = {"oracle": "cache._hash", "generated": "2026-10-07", "cases": cases}
|
||||
(REPO / "src/go/cache" / "truth_hash.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_curator_lexicon_truth.py — regenerate the lexicon truth batteries
|
||||
(contract BUILDER-2026-10-07T16:31:17Z-997c9d).
|
||||
|
||||
Why: batch 1's truth.json encoded the LEXICON DEGRADE — the phantom shadow
|
||||
("lexicon module unavailable — strength gate only") — because
|
||||
lexicon_evolution.py was absent when it was generated. The phantom has now
|
||||
been recovered from provenance (commit 5749029), so the LIVE oracle returns
|
||||
real lexicon verdicts and the Go port must match THEM.
|
||||
|
||||
What this does:
|
||||
1. Rewrites ONLY the fn=='le_promote_eligible' cases inside truth.json,
|
||||
regenerated against the live curator.le_promote_eligible (which now
|
||||
delegates to the recovered lexicon_evolution). Other cases untouched.
|
||||
2. Emits truth_lexicon.json — direct live-oracle cases for the recovered
|
||||
module's full surface: coherence (conflict/consistent/novel), grounded,
|
||||
distinctive_terms, promote_eligible (shared / novel-grounded /
|
||||
novel-parked / no-signal / strength-gate).
|
||||
|
||||
Note on `existing`: the truth carries the block values as a single string;
|
||||
the oracle is called with a one-element list ([existing]) — the batch-1
|
||||
convention, documented here so the Go test wraps identically.
|
||||
|
||||
Usage: python3 scripts/gen_curator_lexicon_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
from rune import curator # noqa: E402 — live oracle (imports lexicon_evolution)
|
||||
import lexicon_evolution as le # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
core = REPO / "src/go/curator-core"
|
||||
|
||||
# ── 1. regenerate the le_promote_eligible cases in truth.json ──────────
|
||||
truth_path = core / "truth.json"
|
||||
truth = json.loads(truth_path.read_text())
|
||||
replaced = 0
|
||||
for case in truth:
|
||||
if case.get("fn") != "le_promote_eligible":
|
||||
continue
|
||||
inp = case["input"]
|
||||
r = curator.le_promote_eligible(
|
||||
inp["proposal"],
|
||||
[inp["existing"]],
|
||||
inp["strength"],
|
||||
min_strength=3,
|
||||
wash_count=inp["wash_count"],
|
||||
)
|
||||
case["expected"] = {"eligible": r}
|
||||
replaced += 1
|
||||
assert replaced >= 4, f"expected >=4 le cases, found {replaced}"
|
||||
# Self-check: no case may still encode the degrade shadow.
|
||||
for case in truth:
|
||||
if case.get("fn") == "le_promote_eligible":
|
||||
assert "lexicon module unavailable" not in json.dumps(case["expected"]), case["name"]
|
||||
truth_path.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"truth.json: {replaced} le_promote_eligible cases regenerated (live lexicon)")
|
||||
|
||||
# ── 2. the direct lexicon-surface battery ──────────────────────────────
|
||||
corpus = [
|
||||
"nick writes Go and Rust code daily in the rune repository",
|
||||
"the curator gates memory writes through worthiness and grading",
|
||||
"migration of modules follows the dichotomy: reactive goes Go, deterministic goes Rust",
|
||||
]
|
||||
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# coherence: conflict (distinctive shared term + negation ON it)
|
||||
add("coherence", "conflict",
|
||||
{"fact": "nick never writes Rust code", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick never writes Rust code",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: consistent (distinctive shared term, no negation)
|
||||
add("coherence", "consistent",
|
||||
{"fact": "nick writes Rust code on weekends", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick writes Rust code on weekends",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: novel (nothing distinctive shared)
|
||||
add("coherence", "novel_axis",
|
||||
{"fact": "nick plays bass guitar in a band", "existing": ["nick writes Go and Rust code daily"],
|
||||
"corpus": corpus},
|
||||
{"verdict": le.coherence("nick plays bass guitar in a band",
|
||||
["nick writes Go and Rust code daily"], corpus)})
|
||||
# coherence: common word only is NEVER a relation
|
||||
add("coherence", "common_word_not_relation",
|
||||
{"fact": "nick likes black coffee", "existing": ["black holes bend light"],
|
||||
"corpus": ["black coffee in the morning", "black holes bend light"]},
|
||||
{"verdict": le.coherence("nick likes black coffee", ["black holes bend light"],
|
||||
["black coffee in the morning", "black holes bend light"])})
|
||||
# coherence: empty existing -> novel
|
||||
add("coherence", "empty_existing",
|
||||
{"fact": "nick writes Rust code daily", "existing": [], "corpus": []},
|
||||
{"verdict": le.coherence("nick writes Rust code daily", [], [])})
|
||||
|
||||
# grounded: stem-aware (migrated matches migration)
|
||||
add("grounded", "stem_match",
|
||||
{"fact": "nick migrated the modules", "sources": corpus},
|
||||
{"grounded": le.grounded("nick migrated the modules", corpus)})
|
||||
add("grounded", "invented_token",
|
||||
{"fact": "nick collects vintage railway timetables", "sources": corpus},
|
||||
{"grounded": le.grounded("nick collects vintage railway timetables", corpus)})
|
||||
add("grounded", "empty_sources",
|
||||
{"fact": "nick migrated the modules", "sources": []},
|
||||
{"grounded": le.grounded("nick migrated the modules", [])})
|
||||
|
||||
# distinctive_terms: rare survives, common does not
|
||||
add("distinctive_terms", "rare_vs_common",
|
||||
{"fact": "nick writes Rust code daily", "corpus": corpus},
|
||||
{"terms": sorted(le.distinctive_terms("nick writes Rust code daily", corpus))})
|
||||
|
||||
# promote_eligible: the four verdict shapes
|
||||
add("promote_eligible", "strength_gate",
|
||||
{"proposal": "add support for retries", "existing": ["support for retries in the fetch loop"],
|
||||
"strength": 2, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("add support for retries",
|
||||
["support for retries in the fetch loop"], 2, 3, ())})
|
||||
add("promote_eligible", "shared_distinctive",
|
||||
{"proposal": "add retries to the fetch loop", "existing": ["support for retries in the fetch loop"],
|
||||
"strength": 3, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("add retries to the fetch loop",
|
||||
["support for retries in the fetch loop"], 3, 3, ())})
|
||||
add("promote_eligible", "novel_grounded",
|
||||
{"proposal": "the curator gates every memory write", "existing": ["nick writes Go and Rust code daily"],
|
||||
"strength": 3, "min_strength": 3, "corpus": corpus},
|
||||
{"eligible": le.promote_eligible("the curator gates every memory write",
|
||||
["nick writes Go and Rust code daily"], 3, 3, corpus)})
|
||||
add("promote_eligible", "novel_parked",
|
||||
{"proposal": "nick collects vintage railway timetables", "existing": ["nick writes Go and Rust code daily"],
|
||||
"strength": 3, "min_strength": 3, "corpus": corpus},
|
||||
{"eligible": le.promote_eligible("nick collects vintage railway timetables",
|
||||
["nick writes Go and Rust code daily"], 3, 3, corpus)})
|
||||
add("promote_eligible", "first_principle",
|
||||
{"proposal": "parking is never wrong for growth", "existing": [],
|
||||
"strength": 3, "min_strength": 3, "corpus": []},
|
||||
{"eligible": le.promote_eligible("parking is never wrong for growth", [], 3, 3, ())})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/lexicon_evolution.py (recovered provenance 0308ccc6) via curator.py",
|
||||
"generated": "2026-10-07 (post-recovery regeneration)",
|
||||
"cases": cases,
|
||||
}
|
||||
(core / "truth_lexicon.json").write_text(
|
||||
json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"truth_lexicon.json: {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
"""gen_drift_fama_truth.py — factory batch 12 truth battery (live oracle)."""
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import drift_core as dc # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="fama-truth-"))
|
||||
db = sqlite3.connect(str(tmp / "t.db"))
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, status TEXT, rock_status TEXT, wash_count INTEGER)")
|
||||
rows = [
|
||||
(1, "active", "washable", 5), # valid (wash>0)
|
||||
(2, "active", "established", 0), # valid (established)
|
||||
(3, "active", "rock", 0), # valid (rock)
|
||||
(4, "active", "washable", 0), # INVALID (active, no wash, washable)
|
||||
(5, "superseded", "washable", 9), # invalid (not active)
|
||||
]
|
||||
db.executemany("INSERT INTO entries VALUES (?,?,?,?)", rows)
|
||||
db.commit()
|
||||
|
||||
cases = []
|
||||
for name, ids in [("all_valid", [1, 2, 3]), ("mixed", [1, 2, 4]),
|
||||
("with_missing", [1, 999]), ("empty", []),
|
||||
("all_invalid", [4, 5])]:
|
||||
fetched = []
|
||||
for eid in ids:
|
||||
r = db.execute("SELECT status, rock_status, wash_count FROM entries WHERE id=?", (eid,)).fetchone()
|
||||
if r:
|
||||
fetched.append(dict(r))
|
||||
cases.append({
|
||||
"fn": "fama", "name": name,
|
||||
"input": {"rows": fetched, "count": len(ids)},
|
||||
"expected": {"fama": dc.compute_fama(db, "q", ids)},
|
||||
})
|
||||
|
||||
out = {"oracle": "drift_core.compute_fama (live)", "generated": "2026-10-08", "cases": cases}
|
||||
(REPO / "src/go/drift-core" / "truth_fama.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_drift_value_truth.py — factory batch 10 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import drift_core as dc # noqa: E402
|
||||
|
||||
NOW = "2026-10-08T00:30:00+00:00"
|
||||
|
||||
|
||||
def main():
|
||||
entries = [
|
||||
# fresh, low everything
|
||||
{"wash_count": 0, "importance": 0.5, "text": "unproven entry", "rock_status": "washable"},
|
||||
# high proven, recent access
|
||||
{"wash_count": 25, "importance": 0.9, "text": "proven entry", "rock_status": "washable",
|
||||
"last_accessed": "2026-10-07T20:00:00+00:00"},
|
||||
# proven but stale (30+ days)
|
||||
{"wash_count": 30, "importance": 0.8, "text": "stale entry", "rock_status": "washable",
|
||||
"last_accessed": "2026-08-01T10:00:00+00:00"},
|
||||
# already rock
|
||||
{"wash_count": 40, "importance": 0.9, "text": "rocked", "rock_status": "rock"},
|
||||
# bad timestamp (degrade path)
|
||||
{"wash_count": 22, "importance": 0.7, "text": "bad ts", "rock_status": "washable",
|
||||
"last_accessed": "not-a-date"},
|
||||
# goal relevance hit
|
||||
{"wash_count": 21, "importance": 0.6, "text": "the curator gates memory writes",
|
||||
"rock_status": "washable", "last_accessed": "2026-10-06T00:00:00+00:00"},
|
||||
]
|
||||
goals = ["curator memory writes", "gate the pipeline"]
|
||||
cases = []
|
||||
for i, e in enumerate(entries):
|
||||
cases.append({
|
||||
"fn": "value", "name": f"value_{i}", "input": {"entry": e},
|
||||
"expected": {"value": dc._py_compute_value(e, None, NOW)},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "promo", "name": f"promo_{i}", "input": {"entry": e},
|
||||
"expected": {"eligible": dc._py_check_promotion_eligibility(e, NOW)[0],
|
||||
"reason": dc._py_check_promotion_eligibility(e, NOW)[1]},
|
||||
})
|
||||
# goal-relevance path
|
||||
cases.append({
|
||||
"fn": "value", "name": "with_goals", "input": {"entry": entries[5], "goals": goals},
|
||||
"expected": {"value": dc._py_compute_value(entries[5], goals, NOW)},
|
||||
})
|
||||
|
||||
out = {"oracle": "drift_core._py_* oracles", "generated": "2026-10-08",
|
||||
"frozen_now": NOW, "cases": cases}
|
||||
(REPO / "src/go/drift-core" / "truth_value.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_event_bus_truth.py — factory batch 9 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import event_bus as eb # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
payloads = [
|
||||
{},
|
||||
{"k": "v"},
|
||||
{"task_id": "t1", "status": "failed", "count": 3},
|
||||
{"nested": {"a": [1, 2, "x"]}, "flag": True, "nothing": None},
|
||||
{"text": "quote\" inside\\ back\nnewline\ttab"},
|
||||
{"float_int": 1.0, "float_frac": 2.5},
|
||||
]
|
||||
cases = [
|
||||
{"fn": "hash", "name": f"payload_{i}", "input": {"payload": p},
|
||||
"expected": {"hash": eb._content_hash(p)}}
|
||||
for i, p in enumerate(payloads)
|
||||
]
|
||||
out = {"oracle": "event_bus._content_hash", "generated": "2026-10-07", "cases": cases}
|
||||
(REPO / "src/go/event-bus" / "truth_bus.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_hook_placement_truth.py — factory batch 4 truth battery
|
||||
(contract BUILDER-2026-10-07T23:06:18Z-ec6208).
|
||||
|
||||
The LIVE recommend_placement oracle over a factor matrix covering every
|
||||
branch: hook-point combos, frequency ladder, cache scopes, concurrency,
|
||||
failure modes, context budget.
|
||||
|
||||
Usage: python3 scripts/gen_hook_placement_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import hook_engine as he # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
factor_sets = [
|
||||
{"output_dep": True},
|
||||
{"input_dep": True},
|
||||
{"input_dep": True, "output_dep": True},
|
||||
{},
|
||||
{"side_effects": "read-write"},
|
||||
{"volatility": "session"},
|
||||
{"volatility": "per-turn"},
|
||||
{"scope": "persistent"},
|
||||
{"scope": "session"},
|
||||
{"concurrent": True},
|
||||
{"failure": "fail-loud"},
|
||||
{"context_budget": 500},
|
||||
{"input_dep": True, "scope": "session", "concurrent": True,
|
||||
"context_budget": 250},
|
||||
{"side_effects": "read-write", "failure": "fail-loud",
|
||||
"scope": "persistent"},
|
||||
]
|
||||
cases = []
|
||||
for i, factors in enumerate(factor_sets):
|
||||
cases.append({
|
||||
"fn": "placement", "name": f"matrix_{i}",
|
||||
"input": {"factors": factors},
|
||||
"expected": he.recommend_placement(f"hook_{i}", factors),
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/hook_engine.py recommend_placement (live)",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/hook-engine" / "truth_placement.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_memory_store_gate_truth.py — memory-store batch 4 truth battery
|
||||
(contract BUILDER-2026-10-07T19:47:37Z-1cf1dc).
|
||||
|
||||
The LIVE oracle: the dense-side meta-judgement extracted from recall()
|
||||
(lines 655-676) exercised against a FIXTURE STORE (sqlite entries + df
|
||||
counts), plus the entries-side fusion vs the chunk-side one (identity
|
||||
check), plus the abstain constants.
|
||||
|
||||
Usage: python3 scripts/gen_memory_store_gate_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import memory_store as ms # noqa: E402
|
||||
|
||||
|
||||
def make_fixture_store(db_path):
|
||||
"""Entries table + df data mirroring recall()'s df-of-word query."""
|
||||
db = sqlite3.connect(db_path)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, text TEXT, status TEXT)")
|
||||
rows = [
|
||||
(1, "the curator gates memory writes through worthiness"),
|
||||
(2, "black holes bend light near the event horizon"),
|
||||
(3, "black coffee in the morning keeps nick running"),
|
||||
(4, "the spine rebuild parity proofs live in store"),
|
||||
(5, "memory writes require the operator approval gate"),
|
||||
(6, "unrelated filler text about weather patterns"),
|
||||
(7, "curator provenance recovery from odysseus"),
|
||||
(8, "another filler row to pad the document frequency"),
|
||||
]
|
||||
db.executemany("INSERT INTO entries VALUES (?,?,?)",
|
||||
[(i, t, "active") for (i, t) in rows])
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="ms-gate-truth-"))
|
||||
db = make_fixture_store(str(tmp / "store.db"))
|
||||
total = db.execute("SELECT COUNT(*) AS n FROM entries WHERE status='active'").fetchone()["n"]
|
||||
|
||||
def df_of(word):
|
||||
return db.execute(
|
||||
"SELECT COUNT(*) AS n FROM entries WHERE status='active' AND text LIKE ?",
|
||||
(f"%{word}%",),
|
||||
).fetchone()["n"]
|
||||
|
||||
# replicate recall()'s dense-gate block verbatim
|
||||
def oracle_gate(query, hits):
|
||||
q_content = {t for t in query.lower().split() if len(t) > 3 and t not in ms._STOPWORDS}
|
||||
dense = []
|
||||
for rowid, cos in hits:
|
||||
etxt = (db.execute("SELECT text FROM entries WHERE id=?", (rowid,)).fetchone()["text"] or "").lower()
|
||||
distinctive = [w for w in q_content if w in etxt and df_of(w) / total < 0.05]
|
||||
if distinctive:
|
||||
dense.append((rowid, cos))
|
||||
elif not q_content or cos >= 0.80:
|
||||
dense.append((rowid, cos))
|
||||
return dense
|
||||
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# gate: shared DISTINCTIVE word (curator: df=2/8=0.25? no — check)
|
||||
# df(curator) = rows 1,7 = 2 → 0.25 ≥ 0.05 → NOT distinctive!
|
||||
# The oracle's threshold means common-store words fail; only truly
|
||||
# rare words pass. Fixture exercise:
|
||||
for name, query, hits in [
|
||||
("distinctive_shared", "worthiness gates", [(1, 0.9), (2, 0.85)]),
|
||||
("common_word_falls_back", "black", [(2, 0.86), (3, 0.79)]),
|
||||
("strong_cos_no_share", "unrelated query terms", [(6, 0.91), (4, 0.70)]),
|
||||
("no_content_words", "the was and", [(1, 0.60), (6, 0.95)]),
|
||||
]:
|
||||
add("dense_gate", name,
|
||||
{"query": query, "hits": [[r, c] for r, c in hits], "total": total},
|
||||
{"kept": [[r, c] for r, c in oracle_gate(query, hits)],
|
||||
"q_content": sorted({t for t in query.lower().split() if len(t) > 3 and t not in ms._STOPWORDS}),
|
||||
"df": {w: df_of(w) for w in query.lower().split() if len(w) > 3}})
|
||||
|
||||
# entries-side fusion identity with the chunk-side one
|
||||
dense = [[1, 0.9], [2, 0.8]]
|
||||
bm25 = [[2, 10.0], [3, 4.0]]
|
||||
add("fusion_identity", "entries_eq_chunks",
|
||||
{"dense": dense, "bm25": bm25},
|
||||
{"fusion": ms._py_rank_fusion(dense, bm25)})
|
||||
|
||||
add("constants", "abstain",
|
||||
{},
|
||||
{"budget": ms.RECALL_BUDGET, "min_score": ms.RECALL_MIN_SCORE})
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/memory_store.py recall() dense gate + _py_rank_fusion",
|
||||
"generated": "2026-10-07",
|
||||
"fixture_df_note": "df via LIKE-substring over active entries; threshold 0.05",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/memory-store-deep" / "truth_gate.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_memory_store_score_truth.py — memory-store batch 3 truth battery
|
||||
(contract BUILDER-2026-10-07T18:57:06Z-c2429c).
|
||||
|
||||
The LIVE _py_score_recall_rows oracle: metadata scoring across modes
|
||||
(hybrid / dense-only / lexical-only), recency decay, the abstention
|
||||
ladder (min_score gates, the 0.74 high-confidence gate, the 0.08 gap
|
||||
detection), position-aware re-rank.
|
||||
|
||||
Usage: python3 scripts/gen_memory_store_score_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import memory_store as ms # noqa: E402
|
||||
|
||||
NOW = "2026-10-07T18:58:00"
|
||||
|
||||
|
||||
def rows_for(ids):
|
||||
"""Fixture rows: confidence/temperature/recency variety."""
|
||||
out = []
|
||||
for i, eid in enumerate(ids):
|
||||
out.append({
|
||||
"id": eid,
|
||||
"text": f"entry {eid} body",
|
||||
"importance": 0.5 + i * 0.01,
|
||||
"topic": "project",
|
||||
"entities": ["a"],
|
||||
"source": "fixture",
|
||||
"confidence": 0.9 if i % 2 == 0 else 0.6,
|
||||
"temperature": 1.2 if i % 3 == 0 else 0.8,
|
||||
"last_accessed": ["2026-10-07T18:00:00", "2026-09-01T10:00:00",
|
||||
"2026-05-01T10:00:00", None][i % 4],
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# hybrid: ranks over 4 rows, dense + bm25 present
|
||||
fusion = {"ranks": [[1, 0.12], [2, 0.11], [3, 0.10], [4, 0.09]],
|
||||
"best_dense": 0.8, "mode": "hybrid"}
|
||||
dense = [[1, 0.8], [2, 0.7], [3, 0.6]]
|
||||
bm25 = [[2, 10.0], [4, 5.0], [1, 2.0]]
|
||||
rows = rows_for([1, 2, 3, 4])
|
||||
add("score", "hybrid",
|
||||
{"fusion": fusion, "rows": rows, "dense": dense, "bm25": bm25,
|
||||
"budget": 3, "min_score": 0.3, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion, rows, dense, bm25, 3, 0.3, NOW)})
|
||||
|
||||
# dense-only: high confidence (>= 0.74) → position-aware
|
||||
fusion_d = {"ranks": [[1, 0.06], [2, 0.055], [3, 0.05]],
|
||||
"best_dense": 0.8, "mode": "dense-only"}
|
||||
add("score", "dense_only_high_conf",
|
||||
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense,
|
||||
"bm25": [], "budget": 5, "min_score": 0.3, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense, [], 5, 0.3, NOW)})
|
||||
|
||||
# dense-only: narrow gap → abstain
|
||||
dense_close = [[1, 0.80], [2, 0.75]]
|
||||
add("score", "dense_only_gap_abstain",
|
||||
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense_close,
|
||||
"bm25": [], "budget": 5, "min_score": 0.3, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense_close, [], 5, 0.3, NOW)})
|
||||
|
||||
# dense-only: below min_score → abstain
|
||||
add("score", "dense_only_min_score",
|
||||
{"fusion": fusion_d, "rows": rows_for([1, 2, 3]), "dense": dense,
|
||||
"bm25": [], "budget": 5, "min_score": 0.9, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion_d, rows_for([1, 2, 3]), dense, [], 5, 0.9, NOW)})
|
||||
|
||||
# lexical-only: max relevance gate
|
||||
fusion_l = {"ranks": [[2, 0.9], [4, 0.7]], "best_dense": 0.0, "mode": "lexical-only"}
|
||||
add("score", "lexical_only_gate",
|
||||
{"fusion": fusion_l, "rows": rows_for([2, 4]), "dense": [], "bm25": bm25,
|
||||
"budget": 5, "min_score": 1.0, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion_l, rows_for([2, 4]), [], bm25, 5, 1.0, NOW)})
|
||||
|
||||
# neither → []
|
||||
add("score", "neither",
|
||||
{"fusion": fusion_l, "rows": rows_for([2, 4]), "dense": [], "bm25": [],
|
||||
"budget": 5, "min_score": 0.3, "now": NOW},
|
||||
{"scored": ms._py_score_recall_rows(fusion_l, rows_for([2, 4]), [], [], 5, 0.3, NOW)})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/memory_store.py _py_score_recall_rows",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/memory-store-deep" / "truth_score.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_memory_store_truth2.py — memory-store batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T18:05:47Z-152c16).
|
||||
|
||||
The LIVE _py_* oracles of memory_store.py. Cases: FTS conjunctive builder
|
||||
(stopword filtering, fallback, 8-cap), chunk_text (recombine + hard-split),
|
||||
rank fusion (hybrid/dense-only/lexical-only), project_chunk_rows,
|
||||
map_reduce, fuse_results (dedup + scale-alignment).
|
||||
|
||||
Usage: python3 scripts/gen_memory_store_truth2.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import memory_store as ms # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# ── FTS conjunctive ───────────────────────────────────────────────────
|
||||
add("fts", "significant_terms",
|
||||
{"query": "the curator gates memory writes"},
|
||||
{"q": ms._py_fts_query_conjunctive("the curator gates memory writes")})
|
||||
add("fts", "stopwords_dropped",
|
||||
{"query": "the and was with nick"},
|
||||
{"q": ms._py_fts_query_conjunctive("the and was with nick")})
|
||||
add("fts", "short_fallback",
|
||||
{"query": "go is fun ai"},
|
||||
{"q": ms._py_fts_query_conjunctive("go is fun ai")})
|
||||
long_q = "alpha beta gamma delta epsilon zeta eta theta iota kappa"
|
||||
add("fts", "cap_eight",
|
||||
{"query": long_q},
|
||||
{"q": ms._py_fts_query_conjunctive(long_q)})
|
||||
add("fts", "empty_returns_raw",
|
||||
{"query": "a an of"},
|
||||
{"q": ms._py_fts_query_conjunctive("a an of")})
|
||||
|
||||
# ── chunk_text ────────────────────────────────────────────────────────
|
||||
small = "one paragraph only"
|
||||
add("chunk", "under_size", {"text": small, "size": 1500, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(small, 1500, 150)})
|
||||
para = "\n\n".join(f"paragraph {i} " + "x" * 40 for i in range(30))
|
||||
add("chunk", "recombine", {"text": para, "size": 400, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(para, 400, 150)})
|
||||
noline = "y" * 3000
|
||||
add("chunk", "hard_split", {"text": noline, "size": 1000, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text(noline, 1000, 150)})
|
||||
add("chunk", "empty", {"text": "", "size": 1500, "overlap": 150},
|
||||
{"chunks": ms._py_chunk_text("", 1500, 150)})
|
||||
|
||||
# ── rank fusion ───────────────────────────────────────────────────────
|
||||
dense = [[1, 0.9], [2, 0.8], [3, 0.7]]
|
||||
bm25 = [[2, 12.0], [4, 8.0], [1, 3.0]]
|
||||
add("fuse_ranks", "hybrid", {"dense": dense, "bm25": bm25},
|
||||
ms._py_rank_fusion(dense, bm25))
|
||||
add("fuse_ranks", "dense_only", {"dense": dense, "bm25": []},
|
||||
ms._py_rank_fusion(dense, []))
|
||||
add("fuse_ranks", "lexical_only", {"dense": [], "bm25": bm25},
|
||||
ms._py_rank_fusion([], bm25))
|
||||
add("fuse_ranks", "neither", {"dense": [], "bm25": []},
|
||||
ms._py_rank_fusion([], []))
|
||||
|
||||
# ── project_chunk_rows ────────────────────────────────────────────────
|
||||
rows = [
|
||||
{"rowid": 1, "chunk_id": "c1", "text": "t" * 500, "wing": "w1", "room": "r1",
|
||||
"source_path": "a.md", "page": 3},
|
||||
{"rowid": 2, "chunk_id": "c2", "text": "short", "wing": "w1", "room": "r1",
|
||||
"source_path": "b.md"},
|
||||
]
|
||||
ranks = {"2": 0.0625, "1": 0.05}
|
||||
add("project", "budget_and_shape", {"ranks": ranks},
|
||||
{"rows": ms._py_project_chunk_rows(ranks, rows, 6)})
|
||||
|
||||
# ── map_reduce ────────────────────────────────────────────────────────
|
||||
entries = [{"id": i, "relevance": i * 0.1} for i in range(25)]
|
||||
add("map_reduce", "top3_per_chunk", {"entries": entries, "chunk_size": 10},
|
||||
{"mapped": ms._py_map_reduce(entries, 10)})
|
||||
add("map_reduce", "under_threshold", {"entries": entries[:5], "chunk_size": 10},
|
||||
{"mapped": ms._py_map_reduce(entries[:5], 10)})
|
||||
|
||||
# ── fuse_results ──────────────────────────────────────────────────────
|
||||
ents = [
|
||||
{"id": 1, "text": "nick prefers coffee", "topic": "human",
|
||||
"relevance": 2.5, "confidence": 0.9},
|
||||
{"id": 2, "text": "NICK PREFERS COFFEE", "topic": "human",
|
||||
"relevance": 1.1, "confidence": 0.8}, # dedup hit
|
||||
{"id": 3, "text": "rune gates memory writes", "topic": "project",
|
||||
"relevance": 5.6, "confidence": 0.95},
|
||||
]
|
||||
res = [
|
||||
{"chunk_id": "c1", "text": "the spine rebuild parity", "wing": "research",
|
||||
"rrf": 0.0625, "confidence": 0.7},
|
||||
{"chunk_id": "c2", "text": "curator provenance recovery", "wing": "research",
|
||||
"rrf": 0.05},
|
||||
]
|
||||
add("fuse_results", "dedup_scale_align", {"entries": ents, "research": res},
|
||||
{"merged": ms._py_fuse_results(ents, res)})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/memory_store.py _py_* oracles",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/memory-store-deep" / "truth_recall.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_microkernel_truth.py — factory batch 8 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import microkernel as mk # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T23:35:00"
|
||||
|
||||
|
||||
def main():
|
||||
mk.now_iso = lambda: FROZEN
|
||||
cases = []
|
||||
|
||||
def snap(reg, name):
|
||||
return {
|
||||
"plugins": {k: {"active": v["active"], "registered_at": v["registered_at"]}
|
||||
for k, v in reg["plugins"].items()},
|
||||
"capabilities": reg["capabilities"],
|
||||
"hooks": reg["hooks"],
|
||||
}
|
||||
|
||||
# register + resolve priority tie
|
||||
import tempfile, os as _os
|
||||
_tmpdb = _os.path.join(tempfile.mkdtemp(prefix="mk-truth-"), "registry.json")
|
||||
mk.REGISTRY_DB = _tmpdb
|
||||
|
||||
def _snap():
|
||||
return mk._load_registry()
|
||||
mk.register({"name": "alpha", "capabilities": ["cap.a"], "priority": 50})
|
||||
mk.register({"name": "beta", "capabilities": ["cap.a"], "priority": 10})
|
||||
mk.register({"name": "gamma", "capabilities": ["cap.a"], "priority": 10, "auto_activate": False})
|
||||
mk.register({"name": "delta", "hooks": {"pre-turn": True, "post-turn": False}})
|
||||
cases.append({"fn": "state", "name": "after_registers", "input": {},
|
||||
"expected": snap(_snap(), "x")})
|
||||
cases.append({"fn": "resolve", "name": "priority_active",
|
||||
"input": {"capability": "cap.a"},
|
||||
"expected": {"resolved": mk.resolve("cap.a")}})
|
||||
cases.append({"fn": "resolve", "name": "unknown_cap",
|
||||
"input": {"capability": "cap.nope"},
|
||||
"expected": {"resolved": mk.resolve("cap.nope")}})
|
||||
|
||||
# unregister removes from indexes
|
||||
mk.unregister("beta")
|
||||
cases.append({"fn": "state", "name": "after_unregister", "input": {},
|
||||
"expected": snap(_snap(), "x")})
|
||||
cases.append({"fn": "resolve", "name": "after_unregister",
|
||||
"input": {"capability": "cap.a"},
|
||||
"expected": {"resolved": mk.resolve("cap.a")}})
|
||||
|
||||
# error texts
|
||||
err1 = mk.register({})
|
||||
err2 = mk.unregister("nope")
|
||||
cases.append({"fn": "errors", "name": "shapes", "input": {},
|
||||
"expected": {"register_empty": err1, "unregister_unknown": err2}})
|
||||
|
||||
out = {"oracle": "src/rune/microkernel.py (live)", "generated": "2026-10-07",
|
||||
"frozen_now": FROZEN, "cases": cases}
|
||||
(REPO / "src/go/microkernel" / "truth_microkernel.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_provider_format_truth.py — factory batch 5 truth battery (live
|
||||
provider_registry._format_tools_* oracles)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import provider_registry as pr # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
tools = [
|
||||
{"name": "read_file", "description": "Read a file",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}}},
|
||||
{"name": "write_file", "description": "Write a file"}, # no parameters key
|
||||
]
|
||||
cases = [
|
||||
{"fn": "openai", "name": "shape", "input": {"tools": tools},
|
||||
"expected": pr._format_tools_openai(tools)},
|
||||
{"fn": "anthropic", "name": "shape", "input": {"tools": tools},
|
||||
"expected": pr._format_tools_anthropic(tools)},
|
||||
]
|
||||
out = {"oracle": "provider_registry._format_tools_*", "generated": "2026-10-07", "cases": cases}
|
||||
target = REPO / "src/go/provider-registry" / "truth_format.json"
|
||||
target.write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_close_truth.py — scaffold batch 3 truth battery
|
||||
(contract BUILDER-2026-10-07T18:42:38Z-52829e).
|
||||
|
||||
The LIVE oracle: _py_close_quality bucket math + close_contract on a doc
|
||||
built through the real lifecycle (new_contract → transitions → snapshot →
|
||||
close). now() frozen at scaffold AND structural_store namespaces.
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_close_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune.structural_store import new_doc, add_entity # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T18:46:00"
|
||||
|
||||
|
||||
def main():
|
||||
sc.now = lambda: FROZEN
|
||||
cases = []
|
||||
|
||||
# ── CloseQuality bucket math (direct, edge-covering) ──────────────────
|
||||
inputs = {
|
||||
"contract_id": "q1",
|
||||
"phase": "superseding",
|
||||
"affected_entities": ["module:a", "module:b"],
|
||||
"expected_new_entities": ["module:new1", "module:new2"],
|
||||
"predicted_existing_affected": ["module:c"],
|
||||
"predicted_frontier": {
|
||||
"level0_direct": ["module:a"],
|
||||
"level1_obligations": ["module:d"],
|
||||
"contracts": ["contract:x"],
|
||||
"level2_verification": ["canary:v"],
|
||||
},
|
||||
"expected_runtime_effects": ["module:a/ctl", "module:zz/never"],
|
||||
"observed_evidence": [
|
||||
{"entity": "module:a", "source": "tests", "detail": "d" * 400, "authority": "pytest"},
|
||||
{"entity": "module:new1", "source": "build", "detail": "created"},
|
||||
{"entity": "module:surprise", "source": "runtime", "detail": "unplanned"},
|
||||
{"entity": "", "source": "runtime", "detail": "no entity — ignored"},
|
||||
],
|
||||
"now": FROZEN,
|
||||
}
|
||||
s = sc.Scaffold(Path(tempfile.mkdtemp()), "q")
|
||||
cases.append({
|
||||
"fn": "close_quality", "name": "buckets",
|
||||
"input": {"inputs": inputs},
|
||||
"expected": s._py_close_quality(inputs),
|
||||
})
|
||||
|
||||
# ── CloseContract through the real lifecycle ──────────────────────────
|
||||
tmp = Path(tempfile.mkdtemp(prefix="scaffold-close-truth-"))
|
||||
s2 = sc.Scaffold(tmp, "close-project")
|
||||
s2.init(purpose="close battery")
|
||||
doc = s2.load()
|
||||
s2.new_contract(doc, contract_id="c1", need="n", scope=["src/go/x/"],
|
||||
affected_entities=["module:a"],
|
||||
expected_new_entities=["module:new1"])
|
||||
s2.transition_contract(doc, "c1", "approved")
|
||||
s2.transition_contract(doc, "c1", "building")
|
||||
s2.transition_contract(doc, "c1", "verifying")
|
||||
# predicted frontier on the entity
|
||||
e = s2.contract(doc, "c1")
|
||||
e.setdefault("ext", {})["predicted_frontier"] = {
|
||||
"level0_direct": ["module:a"], "level1_obligations": [],
|
||||
"contracts": [], "level2_verification": [],
|
||||
}
|
||||
ev = [{"entity": "module:a", "source": "tests", "detail": "passed"}]
|
||||
e2 = s2.close_contract(doc, "c1", observed_evidence=ev,
|
||||
scope_files=["src/go/x/main.go", "src/other/rogue.py"])
|
||||
cases.append({
|
||||
"fn": "close_contract", "name": "full_lifecycle",
|
||||
"input": {},
|
||||
"expected": {"entity": e2},
|
||||
})
|
||||
|
||||
# phase gate
|
||||
doc2 = new_doc()
|
||||
add_entity(doc2, id="contract:early", kind="construction_contract",
|
||||
identities={"need": "n"}, maturity="DECLARED",
|
||||
evidence_refs=["x"])
|
||||
doc2["entities"][-1]["phase"] = "draft"
|
||||
doc2["entities"][-1]["status"] = "open"
|
||||
doc2["entities"][-1]["checkpoints"] = []
|
||||
try:
|
||||
s2.close_contract(doc2, "early", observed_evidence=[])
|
||||
gate = {"ok": True}
|
||||
except Exception as ex: # noqa: BLE001 — intentional: the case IS the gate error; the oracle's text becomes the truth column
|
||||
gate = {"ok": False, "error": str(ex)}
|
||||
cases.append({
|
||||
"fn": "close_gate", "name": "phase_guard",
|
||||
"input": {},
|
||||
"expected": gate,
|
||||
})
|
||||
|
||||
# missing contract
|
||||
try:
|
||||
s2.close_contract(doc2, "ghost", observed_evidence=[])
|
||||
miss = {"ok": True}
|
||||
except Exception as ex: # noqa: BLE001 — intentional: same oracle-error-as-truth pattern as above
|
||||
miss = {"ok": False, "error": str(ex)}
|
||||
cases.append({
|
||||
"fn": "close_gate", "name": "missing_contract",
|
||||
"input": {},
|
||||
"expected": miss,
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/scaffold.py (live, now frozen at both namespaces)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/scaffold" / "truth_close.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_store_truth.py — scaffold batch 2 truth battery
|
||||
(contract BUILDER-2026-10-07T17:47:00Z-960f12).
|
||||
|
||||
The LIVE oracle (scaffold.Scaffold + structural_store) on a tmp store with
|
||||
now() frozen. Cases: canonical digest stability + secret redaction, commit
|
||||
semantics (success + exact failure texts), verify_on_disk forgotten-save
|
||||
detection, impact frontier (contract_change / implementation_change /
|
||||
contextual), snapshot_predicted_frontier, spine_bridge_entity.
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_store_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune import structural_store as ss # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T17:50:00"
|
||||
|
||||
|
||||
def main():
|
||||
sc.now = lambda: FROZEN
|
||||
ss.now = lambda: FROZEN
|
||||
|
||||
tmp = Path(tempfile.mkdtemp(prefix="scaffold-store-truth-"))
|
||||
s = sc.Scaffold(tmp, "truth-project")
|
||||
s.init(purpose="store battery")
|
||||
|
||||
cases = []
|
||||
|
||||
# ── canonical digest: stability + redaction ───────────────────────────
|
||||
doc = s.load()
|
||||
d1 = ss.canonical_digest(doc)
|
||||
# key-order permutation must NOT change the digest (semantic content)
|
||||
permuted = {k: doc[k] for k in reversed(list(doc.keys()))}
|
||||
d2 = ss.canonical_digest(permuted)
|
||||
cases.append({
|
||||
"fn": "digest", "name": "key_order_stable",
|
||||
"input": {},
|
||||
"expected": {"digest": d1, "permuted_same": d2 == d1},
|
||||
})
|
||||
secret_doc = {"entities": [{"id": "repo:x", "identities": {
|
||||
"token": "hunter2", "api_key": "sk-123", "full_name": "o/r",
|
||||
"api_token": "zz", "password": "pw", "private_key": "-----BEGIN",
|
||||
"note": "keepme", "my_secret_sauce": "redacted-too"}}]}
|
||||
cases.append({
|
||||
"fn": "digest", "name": "secret_redaction",
|
||||
"input": {},
|
||||
"expected": {"redacted": ss._redact_secrets(secret_doc)},
|
||||
})
|
||||
|
||||
# ── commit: success + digest readback ─────────────────────────────────
|
||||
s.commit(doc, what="battery")
|
||||
cases.append({
|
||||
"fn": "commit", "name": "success",
|
||||
"input": {},
|
||||
"expected": {"ok": True, "on_disk_schema": s.load()["schema_version"]},
|
||||
})
|
||||
|
||||
# forgotten-save detection: mutate in memory, DON'T save
|
||||
doc2 = s.load()
|
||||
doc2["entities"].append({"id": "ghost:1", "kind": "ghost", "identities": {}})
|
||||
try:
|
||||
s.verify_on_disk(lambda d: d["entities"], None, what="battery") if False else None
|
||||
# the real call: selector/expect form
|
||||
s.verify_on_disk(lambda d: [e for e in d["entities"] if e["id"] == "ghost:1"],
|
||||
lambda v: bool(v), what="battery")
|
||||
forgotten = {"ok": True}
|
||||
except AssertionError as ex:
|
||||
forgotten = {"ok": False, "error": str(ex)}
|
||||
cases.append({
|
||||
"fn": "verify_on_disk", "name": "forgotten_save",
|
||||
"input": {},
|
||||
"expected": forgotten,
|
||||
})
|
||||
|
||||
# corrupt store -> commit-style load returns None -> reload failure path
|
||||
# (simulate: point at a nonexistent path)
|
||||
class FakeScaffold:
|
||||
current = tmp / "nonexistent" / "current.json"
|
||||
try:
|
||||
ss_load = ss.load(FakeScaffold.current)
|
||||
reload_none = ss_load is None
|
||||
except Exception: # noqa: BLE001 — intentional: the case IS the degrade verdict (load failure → loads_none=False); error text is not the truth column here
|
||||
reload_none = False
|
||||
cases.append({
|
||||
"fn": "load", "name": "missing_store",
|
||||
"input": {},
|
||||
"expected": {"loads_none": reload_none},
|
||||
})
|
||||
|
||||
# ── impact frontier ───────────────────────────────────────────────────
|
||||
doc3 = new_frontier_doc()
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "contract_change",
|
||||
"input": {"seeds": ["contract:c1"], "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["contract:c1"]),
|
||||
})
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "implementation_change",
|
||||
"input": {"seeds": ["module:impl_a"], "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["module:impl_a"]),
|
||||
})
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "violation_escalation",
|
||||
"input": {"seeds": ["module:impl_a"], "change_kind": "contract_violation", "doc": doc3},
|
||||
"expected": ss.impact_frontier(doc3, ["module:impl_a"], change_kind="contract_violation"),
|
||||
})
|
||||
|
||||
# ── snapshot_predicted_frontier + spine bridge ────────────────────────
|
||||
doc4 = new_frontier_doc()
|
||||
s4 = sc.Scaffold(tmp / "p4", "frontier-project")
|
||||
s4.init(purpose="frontier battery")
|
||||
doc4 = s4.load()
|
||||
s4.new_contract(doc4, contract_id="f1", need="n", scope=["src/"])
|
||||
# wire the frontier doc entities into the project doc
|
||||
doc4["entities"].extend(e for e in doc3["entities"] if e["id"] not in {x["id"] for x in doc4["entities"]})
|
||||
doc4["relationships"].extend(doc3["relationships"])
|
||||
frontier = s4.snapshot_predicted_frontier(doc4, "f1", ["contract:c1"])
|
||||
e = s4.contract(doc4, "f1")
|
||||
cases.append({
|
||||
"fn": "snapshot_frontier", "name": "persisted",
|
||||
"input": {"doc": doc4},
|
||||
"expected": {
|
||||
"frontier": frontier,
|
||||
"checkpoint_note": e["checkpoints"][-1]["note"],
|
||||
"ext_has": sorted((e.get("ext", {}).get("predicted_frontier") or {}).keys()),
|
||||
},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "bridge", "name": "identity_only",
|
||||
"input": {},
|
||||
"expected": {"bridge": s4.spine_bridge_entity()},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/scaffold.py + structural_store.py (live, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/scaffold" / "truth_store.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def new_frontier_doc():
|
||||
"""A small structural doc exercising the frontier walk."""
|
||||
doc = ss.new_doc()
|
||||
ss.add_entity(doc, id="contract:c1", kind="contract", identities={})
|
||||
ss.add_entity(doc, id="module:impl_a", kind="module", identities={})
|
||||
ss.add_entity(doc, id="module:impl_b", kind="module", identities={})
|
||||
ss.add_entity(doc, id="module:lib", kind="module", identities={})
|
||||
ss.add_entity(doc, id="canary:x", kind="canary", identities={})
|
||||
ss.add_entity(doc, id="deploy:live", kind="deployment", identities={})
|
||||
for rel in (
|
||||
{"type": "implements", "from": "module:impl_a", "to": "contract:c1"},
|
||||
{"type": "implements", "from": "module:impl_b", "to": "contract:c1"},
|
||||
{"type": "depends_on", "from": "module:impl_a", "to": "module:lib"},
|
||||
{"type": "verified_by", "from": "contract:c1", "to": "canary:x"},
|
||||
{"type": "deployed_as", "from": "module:impl_a", "to": "deploy:live"},
|
||||
{"type": "supersedes", "from": "module:impl_b", "to": "module:impl_a"}, # non-obligation
|
||||
):
|
||||
doc["relationships"].append(rel)
|
||||
return doc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_scaffold_truth.py — scaffold batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T17:27:25Z-6bc737).
|
||||
|
||||
The LIVE Scaffold runs against a tmp store with now() frozen (module-patch
|
||||
on rune.scaffold's imported binding). Cases: the transition machine (legal
|
||||
+ illegal + unknown phase, exact error texts), new_contract/transition_
|
||||
contract state evolution, check_scope (green/red/unknown), layout_
|
||||
obligations (FLEXIBLE vs USER-LOCKED), json_frontier projection.
|
||||
|
||||
Usage: python3 scripts/gen_scaffold_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
from rune.structural_store import new_doc, add_entity # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T17:30:00"
|
||||
|
||||
|
||||
def main():
|
||||
# Freeze the clock at scaffold's namespace binding.
|
||||
sc.now = lambda: FROZEN
|
||||
|
||||
tmp = Path(tempfile.mkdtemp(prefix="scaffold-truth-"))
|
||||
s = sc.Scaffold(tmp, "truth-project")
|
||||
s.init(purpose="truth battery")
|
||||
|
||||
cases = []
|
||||
|
||||
# ── transition machine ────────────────────────────────────────────────
|
||||
def tv(name, current, to):
|
||||
try:
|
||||
sc.Scaffold.transition_contract # noqa: B018 — presence probe
|
||||
# validate via a throwaway entity path
|
||||
doc = new_doc()
|
||||
e = s.new_contract(doc, contract_id="probe", need="probe")
|
||||
if current != "draft":
|
||||
e["phase"] = current
|
||||
s.transition_contract(doc, "probe", to)
|
||||
cases.append({"fn": "transition", "name": name,
|
||||
"input": {"current": current, "to": to},
|
||||
"expected": {"ok": True}})
|
||||
except Exception as ex: # noqa: BLE001 — intentional: the case IS the error verdict; the oracle's error text becomes the truth column
|
||||
cases.append({"fn": "transition", "name": name,
|
||||
"input": {"current": current, "to": to},
|
||||
"expected": {"ok": False, "error": str(ex)}})
|
||||
|
||||
tv("legal_approved", "draft", "approved")
|
||||
tv("legal_building", "approved", "building")
|
||||
tv("legal_verifying", "building", "verifying")
|
||||
tv("legal_migrating", "verifying", "migrating")
|
||||
tv("legal_superseding", "migrating", "superseding")
|
||||
tv("legal_closed", "superseding", "closed")
|
||||
tv("legal_failed_retry", "failed", "building")
|
||||
tv("legal_abandon", "building", "abandoned")
|
||||
tv("illegal_closed_from_draft", "draft", "closed")
|
||||
tv("illegal_rewind", "verifying", "draft")
|
||||
tv("illegal_skip", "draft", "verifying")
|
||||
tv("unknown_phase", "draft", "nonexistent_phase")
|
||||
|
||||
# ── contract lifecycle evolution ──────────────────────────────────────
|
||||
doc = new_doc()
|
||||
s.new_contract(doc, contract_id="life", need="port the lifecycle",
|
||||
origin="builder", scope=["src/go/scaffold/"],
|
||||
acceptance=["truth battery passes"])
|
||||
e = s.transition_contract(doc, "life", "approved", evidence_ref="e1")
|
||||
e = s.transition_contract(doc, "life", "building", evidence_ref="e2", note="x" * 300)
|
||||
cases.append({
|
||||
"fn": "lifecycle", "name": "evolution",
|
||||
"input": {},
|
||||
"expected": {"entity": e},
|
||||
})
|
||||
|
||||
# duplicate contract
|
||||
try:
|
||||
s.new_contract(doc, contract_id="life", need="again")
|
||||
dup = {"ok": True}
|
||||
except ValueError as ex:
|
||||
dup = {"ok": False, "error": str(ex)}
|
||||
cases.append({"fn": "dup", "name": "duplicate", "input": {},
|
||||
"expected": dup})
|
||||
|
||||
# ── check_scope ───────────────────────────────────────────────────────
|
||||
doc2 = new_doc()
|
||||
s.new_contract(doc2, contract_id="scoped", need="n", scope=["src/go/a/", "src/go/b/main.go"])
|
||||
e2 = s.contract(doc2, "scoped")
|
||||
cases.append({
|
||||
"fn": "check_scope", "name": "mixed",
|
||||
"input": {"changed": ["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"], "scope": ["src/go/a/", "src/go/b/main.go"]},
|
||||
"expected": s.check_scope(doc2, "scoped",
|
||||
["src/go/a/x.go", "src/go/b/main.go", "src/other/f.py", "src/go/a/deep/y.go"]),
|
||||
})
|
||||
doc3 = new_doc()
|
||||
s.new_contract(doc3, contract_id="unscoped", need="n")
|
||||
cases.append({
|
||||
"fn": "check_scope", "name": "unscoped",
|
||||
"input": {"changed": ["anything.py"], "scope": []},
|
||||
"expected": s.check_scope(doc3, "unscoped", ["anything.py"]),
|
||||
})
|
||||
|
||||
# ── layout obligations ────────────────────────────────────────────────
|
||||
doc4 = new_doc()
|
||||
add_entity(doc4, id="layout:panel", kind="layout_contract",
|
||||
identities={"component": "Panel", "region": "sidebar",
|
||||
"implementation": "src/go/panel.go", "lock": False,
|
||||
"provenance": "scaffold"}, maturity="DECLARED")
|
||||
add_entity(doc4, id="layout:header", kind="layout_contract",
|
||||
identities={"component": "Header", "region": "top",
|
||||
"implementation": "src/go/header.go", "lock": True,
|
||||
"provenance": "user"}, maturity="DECLARED")
|
||||
cases.append({
|
||||
"fn": "layout", "name": "mixed_locks",
|
||||
"input": {"affected": ["Panel", "region:top"], "doc": doc4},
|
||||
"expected": {"obligations": s.layout_obligations(doc4, ["Panel", "region:top"])},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "layout", "name": "no_hit",
|
||||
"input": {"affected": ["Unrelated"], "doc": doc4},
|
||||
"expected": {"obligations": []},
|
||||
})
|
||||
|
||||
# ── json_frontier ─────────────────────────────────────────────────────
|
||||
f = {"contracts": ["c1"], "level2_verification": ["v1"]}
|
||||
cases.append({
|
||||
"fn": "frontier", "name": "projection",
|
||||
"input": {"frontier": f},
|
||||
"expected": {"json": sc.json_frontier(f)},
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/scaffold.py (live Scaffold, now frozen)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": FROZEN,
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/scaffold" / "truth_scaffold.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
import shutil
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_sleep_time_truth.py — factory batch 11 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import sleep_time as st # noqa: E402
|
||||
|
||||
|
||||
def make_parts_db(db_path):
|
||||
db = sqlite3.connect(db_path)
|
||||
db.execute("CREATE TABLE part (session_id TEXT, data TEXT, time_created TEXT)")
|
||||
rows = [
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "first", "status": "completed", "priority": "high"}]}}}', "2026-10-07T10:00:00"),
|
||||
("s1", '{"tool": "bash", "state": {"input": {"command": "ls"}}}', "2026-10-07T10:01:00"),
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "second", "status": "in_progress", "priority": "med"}, {"content": "third", "status": "pending", "priority": "low"}]}}}', "2026-10-07T10:02:00"),
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": []}}}', "2026-10-07T10:03:00"), # empty — skipped
|
||||
("s1", '{"tool": "read_file", "state": {"input": {}}}', "2026-10-07T10:04:00"), # non-todowrite
|
||||
("s2", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "other session", "status": "pending", "priority": ""}]}}}', "2026-10-07T11:00:00"),
|
||||
]
|
||||
db.executemany("INSERT INTO part VALUES (?,?,?)", rows)
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="st-truth-"))
|
||||
db = make_parts_db(str(tmp / "p.db"))
|
||||
|
||||
cases = []
|
||||
|
||||
# extract_todos for s1 (ORDER BY time ASC → latest = the 10:02 todos)
|
||||
rows = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='s1' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "s1_latest",
|
||||
"input": {"rows": rows},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "s1"), "s1")}})
|
||||
rows2 = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='s2' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "s2",
|
||||
"input": {"rows": rows2},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "s2"), "s2")}})
|
||||
rows3 = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='nope' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "empty_session",
|
||||
"input": {"rows": rows3},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "nope"), "nope")}})
|
||||
|
||||
# write_transcript byte format
|
||||
st.TRANSCRIPTS_DIR = str(tmp / "transcripts")
|
||||
Path(st.TRANSCRIPTS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
todos = [{"content": "write the thing", "status": "completed", "priority": "high"},
|
||||
{"content": "review it", "status": "in_progress", "priority": ""}]
|
||||
parts = [("user", "do the task"), ("assistant", "done, verified")]
|
||||
p1 = st.write_transcript("s1", "My Session: v2!", parts, todos)
|
||||
cases.append({"fn": "transcript", "name": "full_shape",
|
||||
"input": {},
|
||||
"expected": {"content": Path(p1).read_text(),
|
||||
"filename_stem": Path(p1).name}})
|
||||
p2 = st.write_transcript("s2", "no todos", parts, None)
|
||||
cases.append({"fn": "transcript", "name": "no_todos",
|
||||
"input": {},
|
||||
"expected": {"content": Path(p2).read_text()}})
|
||||
|
||||
out = {"oracle": "sleep_time.extract_todos/write_transcript (live)",
|
||||
"generated": "2026-10-08", "cases": cases}
|
||||
(REPO / "src/go/sleep-time" / "truth_transcript.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
class _FakeConn:
|
||||
def __init__(self, db, session_id):
|
||||
self.db = db
|
||||
self.session_id = session_id
|
||||
|
||||
def execute(self, sql, params):
|
||||
# replicate the oracle's query (session-filtered, time-ordered)
|
||||
class Cur:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
|
||||
def fetchall(self):
|
||||
return self.rows
|
||||
|
||||
return Cur(self.db.execute(sql, params).fetchall())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_executor_auth_truth.py — the tool-executor authority truth
|
||||
(contract BUILDER-2026-10-07T19:10:38Z-cc6be2).
|
||||
|
||||
Freezes the LIVE READ_ONLY_TOOLS set + the prepare-tool routing verdicts
|
||||
so CI asserts the Go set equals the Python set FOREVER. The run_command
|
||||
wrong-add this battery pins was a security-adjacent divergence: a mutating
|
||||
tool treated as read-only bypasses operator approval.
|
||||
|
||||
Usage: python3 scripts/gen_tool_executor_auth_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import tool_executor as te # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = [
|
||||
{
|
||||
"fn": "read_only_tools", "name": "exact_set",
|
||||
"input": {},
|
||||
"expected": {"tools": sorted(te.READ_ONLY_TOOLS)},
|
||||
},
|
||||
{
|
||||
"fn": "prepare_routing", "name": "run_command_requires_approval",
|
||||
"input": {"tool": "run_command", "args": {"command": "ls -la"}},
|
||||
"expected": {"read_only": "run_command" in te.READ_ONLY_TOOLS},
|
||||
},
|
||||
{
|
||||
"fn": "prepare_routing", "name": "read_file_executes_now",
|
||||
"input": {"tool": "read_file", "args": {"path": "/tmp"}},
|
||||
"expected": {"read_only": "read_file" in te.READ_ONLY_TOOLS},
|
||||
},
|
||||
]
|
||||
out = {
|
||||
"oracle": "src/rune/tool_executor.py READ_ONLY_TOOLS + routing",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/tool-executor" / "truth_auth.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_executor_truth.py — regenerate the tool-executor truth batteries
|
||||
HERMETICALLY (contract BUILDER-2026-10-07T16:11:35Z-70ca24-unknown).
|
||||
|
||||
Why: the original batch-2/3/4 generators baked the generation host's temp
|
||||
paths (/mnt/workdrive/tmp*) into truth_fileops.json / truth_ops.json /
|
||||
truth_paths.json — inputs AND expectations. CI's runner user cannot read
|
||||
those paths, so the parity gate failed with permission-denied (go-surface,
|
||||
run 19792 job 56712) despite a locally green tree.
|
||||
|
||||
Design (the environment leak, closed):
|
||||
- Fixtures are created at generation time under roots the ORACLE ITSELF
|
||||
allows: read root = first creatable dir in ALLOWED_READ_DIRS (prefers
|
||||
/tmp), write root = first creatable dir in ALLOWED_WRITE_DIRS.
|
||||
- Every occurrence of those roots — in inputs AND expectations — is
|
||||
replaced with {{TMP}} / {{WROOT}} placeholders.
|
||||
- Each truth file carries a `fixtures` manifest (files + dirs) so the Go
|
||||
test recreates the SAME fixture content under ITS OWN roots at runtime.
|
||||
Both sides then run the same case semantics on their own environment;
|
||||
substituted bytes must agree exactly.
|
||||
- The grep-hit comparison stays order-insensitive (sorted line sets) on the
|
||||
Go side: the oracle's os.walk order is OS-scheduling dependent, not a
|
||||
parity-relevant property. Everything else is exact after substitution.
|
||||
|
||||
Oracle authority: src/rune/tool_executor.py is called LIVE for every case
|
||||
except the two cmd_gate verdicts whose oracle path would execute a shell
|
||||
command (plain_ok / shell_ok use harmless commands instead: "ls -la" and
|
||||
"true" — the gate branches they exercise do not inspect command content
|
||||
beyond non-emptiness).
|
||||
|
||||
Usage: python3 scripts/gen_tool_executor_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import tool_executor as te # noqa: E402 — the live oracle
|
||||
|
||||
|
||||
def mk_root(dirs, tag):
|
||||
"""First creatable temp dir under the given allowed dirs."""
|
||||
for d in dirs:
|
||||
try:
|
||||
return tempfile.mkdtemp(prefix="rune-truth-", dir=d)
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError(f"no creatable root among {dirs}")
|
||||
|
||||
|
||||
def build_fixtures(root):
|
||||
"""The shared fixture set: exactly what the cases observe."""
|
||||
(Path(root) / "w.txt").write_text(
|
||||
"".join(f"line {i}\n" for i in range(1, 21)), encoding="utf-8"
|
||||
)
|
||||
(Path(root) / "a.txt").write_text("the spine rebuild parity\n", encoding="utf-8")
|
||||
(Path(root) / "b.log").write_text("spine appears here too\n", encoding="utf-8")
|
||||
(Path(root) / "file.txt").write_text("content\n", encoding="utf-8")
|
||||
(Path(root) / "adir").mkdir()
|
||||
return {
|
||||
"files": [
|
||||
{"path": "{{TMP}}/w.txt",
|
||||
"content": "".join(f"line {i}\n" for i in range(1, 21))},
|
||||
{"path": "{{TMP}}/a.txt", "content": "the spine rebuild parity\n"},
|
||||
{"path": "{{TMP}}/b.log", "content": "spine appears here too\n"},
|
||||
{"path": "{{TMP}}/file.txt", "content": "content\n"},
|
||||
],
|
||||
"dirs": ["{{TMP}}/adir"],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
read_root = mk_root(te.ALLOWED_READ_DIRS, "read")
|
||||
write_root = mk_root(te.ALLOWED_WRITE_DIRS, "write")
|
||||
fixtures = build_fixtures(read_root)
|
||||
|
||||
wpath = f"{read_root}/w.txt"
|
||||
edited = "line 1\nCHANGED\n" + "".join(f"line {i}\n" for i in range(3, 21))
|
||||
|
||||
cases_fileops = [
|
||||
{
|
||||
"fn": "read_window", "name": "mid",
|
||||
"input": {"path": f"{wpath}", "offset": 3, "limit": 5},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "read_window", "name": "default",
|
||||
"input": {"path": f"{wpath}"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "diff", "name": "simple",
|
||||
"input": {"path": wpath, "new_content": edited},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._read_file({"path": wpath, "offset": 3, "limit": 5})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._read_file({"path": wpath})
|
||||
assert r["success"] is True, r
|
||||
cases_fileops[1]["expected"] = {
|
||||
"ok": True, "lines": len(r["result"].split("\n")),
|
||||
}
|
||||
|
||||
diff = te.generate_diff(Path(wpath), edited)
|
||||
dlines = diff.split("\n")
|
||||
cases_fileops[2]["expected"] = {
|
||||
"diff_head": dlines[:3],
|
||||
"has_minus": any(l.startswith("-line 2") for l in dlines),
|
||||
"has_plus": any(l.startswith("+CHANGED") for l in dlines),
|
||||
}
|
||||
|
||||
cases_ops = [
|
||||
{
|
||||
"fn": "list_dir", "name": "basic",
|
||||
"input": {"path": read_root},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "list_dir", "name": "pattern",
|
||||
"input": {"path": read_root, "pattern": "*.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "hit",
|
||||
"input": {"path": read_root, "pattern": "spine"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "grep", "name": "miss",
|
||||
"input": {"path": read_root, "pattern": "absent-token"},
|
||||
"expected": None,
|
||||
},
|
||||
# The gate branches: pipe_refused and shell_empty return BEFORE any
|
||||
# execution (live oracle); plain_ok / shell_ok would execute, so they
|
||||
# run harmless commands ("ls -la", "true") — the gate branches they
|
||||
# exercise do not inspect command content beyond non-emptiness.
|
||||
{
|
||||
"fn": "cmd_gate", "name": "pipe_refused",
|
||||
"input": {"command": "ls -la | head"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "plain_ok",
|
||||
"input": {"command": "ls -la"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_ok",
|
||||
"input": {"command": "true", "shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "cmd_gate", "name": "shell_empty",
|
||||
"input": {"shell": True},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
r = te._list_directory({"path": read_root})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[0]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._list_directory({"path": read_root, "pattern": "*.txt"})
|
||||
assert r["success"] is True, r
|
||||
cases_ops[1]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "spine"})
|
||||
assert r["success"] is True and r["result"] != "(no matches)", r
|
||||
cases_ops[2]["expected"] = {"ok": True, "result": r["result"]}
|
||||
|
||||
r = te._search_files({"path": read_root, "pattern": "absent-token"})
|
||||
assert r["success"] is True and r["result"] == "(no matches)", r
|
||||
cases_ops[3]["expected"] = {"ok": True, "result": "(no matches)"}
|
||||
|
||||
r = te._run_command({"command": "ls -la | head"})
|
||||
cases_ops[4]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
r = te._run_command({"command": "ls -la"})
|
||||
cases_ops[5]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"command": "true", "shell": True})
|
||||
cases_ops[6]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is True, r
|
||||
|
||||
r = te._run_command({"shell": True})
|
||||
cases_ops[7]["expected"] = {"success": r["success"], "error": r.get("error", "")}
|
||||
assert r["success"] is False, r
|
||||
|
||||
cases_paths = [
|
||||
{
|
||||
"fn": "resolve", "name": "abs",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "hit",
|
||||
"input": {"path": f"{read_root}/file.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "miss",
|
||||
"input": {"path": f"{read_root}/missing.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_read", "name": "blocked",
|
||||
"input": {"path": "/etc/hostname"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "protected",
|
||||
"input": {"path": "/etc/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "outside",
|
||||
"input": {"path": "/usr/share/x.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
{
|
||||
"fn": "validate_write", "name": "ok",
|
||||
"input": {"path": f"{write_root}/new.txt"},
|
||||
"expected": None,
|
||||
},
|
||||
]
|
||||
|
||||
resolved = str(te.resolve_path(f"{read_root}/file.txt"))
|
||||
cases_paths[0]["expected"] = {"resolved": resolved}
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/file.txt")
|
||||
cases_paths[1]["expected"] = {"ok": ok}
|
||||
assert ok is True, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path(f"{read_root}/missing.txt")
|
||||
cases_paths[2]["expected"] = {"ok": ok, "msg": msg}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_read_path("/etc/hostname")
|
||||
cases_paths[3]["expected"] = {"ok": ok}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/etc/x.txt")
|
||||
cases_paths[4]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, _ = te.validate_write_path("/usr/share/x.txt")
|
||||
cases_paths[5]["expected"] = {"ok": ok, "msg_prefix": msg[:50]}
|
||||
assert ok is False, msg
|
||||
|
||||
ok, msg, resolved = te.validate_write_path(f"{write_root}/new.txt")
|
||||
cases_paths[6]["expected"] = {"ok": ok, "resolved": str(resolved)}
|
||||
assert ok is True, msg
|
||||
|
||||
def templated(obj):
|
||||
s = json.dumps(obj, ensure_ascii=False)
|
||||
s = s.replace(read_root, "{{TMP}}").replace(write_root, "{{WROOT}}")
|
||||
return json.loads(s)
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/tool_executor.py",
|
||||
"generated": "2026-10-07 (hermetic regeneration)",
|
||||
"placeholders": {
|
||||
"{{TMP}}": "read-fixture root (first creatable ALLOWED_READ_DIRS entry)",
|
||||
"{{WROOT}}": "write root (first creatable ALLOWED_WRITE_DIRS entry)",
|
||||
},
|
||||
"fixtures": fixtures,
|
||||
"cases": None,
|
||||
}
|
||||
|
||||
for name, cases in (
|
||||
("truth_fileops.json", cases_fileops),
|
||||
("truth_ops.json", cases_ops),
|
||||
("truth_paths.json", cases_paths),
|
||||
):
|
||||
doc = dict(out)
|
||||
doc["cases"] = templated(cases)
|
||||
text = json.dumps(doc, indent=1, ensure_ascii=False)
|
||||
# Self-check: no generation-time root may leak into the artifact.
|
||||
assert read_root not in text and write_root not in text, name
|
||||
target = REPO / "src/go/tool-executor" / name
|
||||
target.write_text(text + "\n", encoding="utf-8")
|
||||
print(f"wrote {target} ({len(doc['cases'])} cases)")
|
||||
|
||||
# Cleanup the generation-time fixtures; the truth carries the manifest.
|
||||
import shutil
|
||||
shutil.rmtree(read_root, ignore_errors=True)
|
||||
shutil.rmtree(write_root, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_protocol_truth.py — factory batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T22:11:52Z-9ffb73).
|
||||
|
||||
Live tool_protocol oracle fixtures: validate error texts, alias
|
||||
canonicalization, ollama envelope normalization (incl. string-arguments
|
||||
JSON), internal-protocol detection, extract_visible_text span stripping.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import tool_protocol as tp # noqa: E402
|
||||
|
||||
|
||||
def case(fn, name, inp, fn_lambda):
|
||||
try:
|
||||
expected = {"ok": True, "result": fn_lambda()}
|
||||
except Exception as ex:
|
||||
expected = {"ok": False, "error": str(ex)}
|
||||
return {"fn": fn, "name": name, "input": inp, "expected": expected}
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
R = lambda cid, n, a: {"call_id": cid, "name": n, "arguments": a, "source": "test"}
|
||||
|
||||
# validate: alias canonicalization
|
||||
cases.append(case("validate", "alias_grep",
|
||||
{"request": R("c1", "grep", {"pattern": "x", "path": "."})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c1", "grep", {"pattern": "x", "path": "."}))).to_dict()))
|
||||
cases.append(case("validate", "alias_bash",
|
||||
{"request": R("c2", "bash", {"command": "ls"})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c2", "bash", {"command": "ls"}))).to_dict()))
|
||||
cases.append(case("validate", "unknown_tool",
|
||||
{"request": R("c3", "nonexistent_tool", {})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c3", "nonexistent_tool", {}))).to_dict()))
|
||||
cases.append(case("validate", "missing_required",
|
||||
{"request": R("c4", "read_file", {})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c4", "read_file", {}))).to_dict()))
|
||||
cases.append(case("validate", "unknown_arg",
|
||||
{"request": R("c5", "read_file", {"path": "/tmp", "bogus": 1})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c5", "read_file", {"path": "/tmp", "bogus": 1}))).to_dict()))
|
||||
cases.append(case("validate", "type_mismatch",
|
||||
{"request": R("c6", "read_file", {"path": 123})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c6", "read_file", {"path": 123}))).to_dict()))
|
||||
|
||||
# ollama normalization: string arguments JSON
|
||||
cases.append(case("ollama", "string_args",
|
||||
{"message": {"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": "{\"path\": \"/tmp/x\"}"}}]}},
|
||||
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": json.dumps({"path": "/tmp/x"})}}]})]))
|
||||
cases.append(case("ollama", "malformed_args_json",
|
||||
{"message": {"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]}},
|
||||
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]})]))
|
||||
|
||||
# internal protocol detection
|
||||
for name, text in [
|
||||
("result_marker", "[tool result] output here"),
|
||||
("error_marker", "[tool error] boom"),
|
||||
("xml_call", "<tool_call>{\"tool\": \"x\"}</tool_call>"),
|
||||
("clean_text", "just ordinary assistant text with JSON: {\"a\": 1}"),
|
||||
]:
|
||||
cases.append(case("internal", name, {"text": text},
|
||||
lambda t=text: {"contains": tp.contains_internal_protocol(t)}))
|
||||
|
||||
# extract_visible_text
|
||||
for name, text in [
|
||||
("inline_call", 'before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}} after'),
|
||||
("fenced_call", '```json\n{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}}\n```'),
|
||||
("xml_call", 'before <tool_call>{\"tool\": \"x\", \"args\": {}}</tool_call> after'),
|
||||
("plain", "no protocol here at all"),
|
||||
]:
|
||||
cases.append(case("visible", name, {"text": text},
|
||||
lambda t=text: {"visible": tp.extract_visible_text(t)}))
|
||||
|
||||
# normalize_text_tool_calls end-to-end
|
||||
cases.append(case("normalize", "text_fallback",
|
||||
{"text": '{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}'},
|
||||
lambda: [r.to_dict() for r in tp.normalize_text_tool_calls(
|
||||
'{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}')]))
|
||||
|
||||
out = {"oracle": "src/rune/tool_protocol.py (live)", "generated": "2026-10-07", "cases": cases}
|
||||
target = REPO / "src/go/tool-protocol" / "truth_protocol.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_warm_router_digest_truth.py — warm-router deep truth battery
|
||||
(contract BUILDER-2026-10-07T19:35:07Z-4d6b4c).
|
||||
|
||||
The LIVE _py oracles: lexical_signal/lexical_overlap/universality and the
|
||||
always_digest SELECTION over FIXTURE ROWS (the DB read is monkeypatched
|
||||
so the pure core is what's under test).
|
||||
|
||||
Usage: python3 scripts/gen_warm_router_digest_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import warm_router as wr # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
|
||||
def add(fn, name, inp, expected):
|
||||
cases.append({"fn": fn, "name": name, "input": inp, "expected": expected})
|
||||
|
||||
# ── lexical signal/overlap ────────────────────────────────────────────
|
||||
add("lexical", "signal_strips_punct",
|
||||
{"body": "The curator gates memory writes (always)."},
|
||||
{"words": sorted(wr._lexical_signal("The curator gates memory writes (always)."))})
|
||||
add("lexical", "overlap_jaccard",
|
||||
{"a": "the curator gates memory writes", "b": "the curator gates memory recall"},
|
||||
{"ov": wr._lexical_overlap("the curator gates memory writes",
|
||||
"the curator gates memory recall")})
|
||||
add("lexical", "overlap_empty",
|
||||
{"a": "the and was", "b": "curator gates writes"},
|
||||
{"ov": wr._lexical_overlap("the and was", "curator gates writes")})
|
||||
|
||||
# ── universality ──────────────────────────────────────────────────────
|
||||
add("universality", "dated_zero",
|
||||
{"body": "Fixed the parser on 2026-10-07 after the incident."},
|
||||
{"u": wr._universality("Fixed the parser on 2026-10-07 after the incident.")})
|
||||
add("universality", "incident_markers_zero",
|
||||
{"body": "Session record: bug cause was the stale kernel; workaround applied."},
|
||||
{"u": wr._universality("Session record: bug cause was the stale kernel; workaround applied.")})
|
||||
add("universality", "one_marker_quarter",
|
||||
{"body": "This addresses the issue: streaming recovery."},
|
||||
{"u": wr._universality("This addresses the issue: streaming recovery.")})
|
||||
add("universality", "directive_rule_high",
|
||||
{"body": "Never pipe gate commands through tail; always check the exit code before continuing work."},
|
||||
{"u": wr._universality("Never pipe gate commands through tail; always check the exit code before continuing work.")})
|
||||
add("universality", "rambling_penalty",
|
||||
{"body": " ".join(["word"] * 120) + " never always must keep"},
|
||||
{"u": wr._universality(" ".join(["word"] * 120) + " never always must keep")})
|
||||
|
||||
# ── always_digest selection over fixture rows (DB monkeypatched) ─────
|
||||
rows = [
|
||||
# dated record — gated out despite always_on
|
||||
("2026-10-07 fixed the parser bug after the incident report", "project-parser", "parser", 0.9, "", 1, 0.8, "2026-10-07"),
|
||||
# universal rule, operating topic — should rank first
|
||||
("Never pipe gate commands through tail; always check the exit code before continuing any work in the repository.", "operating-gates", "gate", 0.7, "", 1, 0.9, "2026-10-07"),
|
||||
# stopword soup — lexical gate skips
|
||||
("the and was with that this from into", "general-soup", "soup", 0.5, "", 0, 0.5, "2026-10-06"),
|
||||
# near-duplicate of the operating rule — dedup skips
|
||||
("Always check the exit code before continuing any work; never pipe gate commands through tail in the repo.", "operating-gates-copy", "gate2", 0.8, "", 1, 0.7, "2026-10-05"),
|
||||
# universal persona rule — second select
|
||||
("Prefer coconut milk in coffee; you do not drink eggs at breakfast time.", "human-prefs", "coffee", 0.6, "", 0, 0.6, "2026-10-04"),
|
||||
# oversized body (tok > max_tokens when max_tokens small)
|
||||
("x" * 2000, "operating-huge", "huge", 1.0, "", 1, 1.0, "2026-10-03"),
|
||||
]
|
||||
fixture_rows = [tuple(r) for r in rows]
|
||||
|
||||
class FakeDB:
|
||||
def execute(self, *_a, **_k):
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
return fixture_rows
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
saved_db = wr._db
|
||||
wr._db = lambda: FakeDB()
|
||||
try:
|
||||
digest = wr.always_digest(max_tokens=220)
|
||||
finally:
|
||||
wr._db = saved_db
|
||||
add("digest", "selection_full",
|
||||
{"rows": rows},
|
||||
{"digest": digest})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/warm_router.py _py surfaces + always_digest (DB monkeypatched)",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/warm-router" / "truth_digest.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
print("digest slugs:", [d["slug"] for d in digest])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -86,9 +86,12 @@ def main() -> int:
|
||||
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"):
|
||||
# Same open-only exemption as the artifact-records block above: a
|
||||
# closed issue's labels are historical record; FIXED_VERIFIED issues
|
||||
# legitimately keep the bug label after closure.
|
||||
if live_bug and rec.get("tracker_state") == "open" 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"):
|
||||
if not live_bug and rec.get("live_defect") and rec.get("tracker_state") == "open":
|
||||
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
|
||||
|
||||
@@ -1,43 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
"""install_mimo_hook.py — T21.2 §21.2D (T22: multi-hook): 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."""
|
||||
Canonical sources: config/mimo-hooks/*.ts
|
||||
Targets: ~/.mimocode/hooks/<same-name>.ts
|
||||
|
||||
Every hook is hash-verified after install; drift is detected in both
|
||||
directions. T22 added builder-bridge.ts to the managed set — it was
|
||||
previously installed by hand, unversioned, which is how it silently rotted
|
||||
(hardcoded ephemeral interpreter). A hook that reaches the builder must be
|
||||
version-controlled and drift-checked like any other authority.
|
||||
|
||||
Usage:
|
||||
python3 scripts/install_mimo_hook.py # sync all managed hooks
|
||||
python3 scripts/install_mimo_hook.py --check # verify only, install nothing
|
||||
"""
|
||||
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"
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL_DIR = REPO / "config" / "mimo-hooks"
|
||||
TARGET_DIR = Path.home() / ".mimocode" / "hooks"
|
||||
|
||||
# The managed set. Adding a file here is what makes a hook version-controlled.
|
||||
MANAGED = ["event-wait-guard.ts", "builder-bridge.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]}")
|
||||
def sync_one(name: str, check_only: bool) -> bool:
|
||||
canonical = CANONICAL_DIR / name
|
||||
target = TARGET_DIR / name
|
||||
if not canonical.is_file():
|
||||
print(f"canonical source missing: {canonical}")
|
||||
return False
|
||||
canon_hash = sha256(canonical)
|
||||
if target.is_file() and sha256(target) == canon_hash:
|
||||
print(f"in-sync: {name} {canon_hash[:12]}")
|
||||
return True
|
||||
if check_only:
|
||||
state = "DRIFT" if target.is_file() else "MISSING"
|
||||
inst = sha256(target)[:12] if target.is_file() else "-"
|
||||
print(f"{state}: {name} canonical={canon_hash[:12]} installed={inst}")
|
||||
return False
|
||||
if target.is_file():
|
||||
print(f"DRIFT: {name} canonical={canon_hash[:12]} installed={sha256(target)[: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
|
||||
print(f"hook missing at {target}")
|
||||
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(canonical, target)
|
||||
if sha256(target) != canon_hash:
|
||||
print(f"INSTALL FAILED: {name} hash mismatch after copy")
|
||||
return False
|
||||
print(f"installed: {name} {canon_hash[:12]}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
check_only = "--check" in sys.argv
|
||||
ok = True
|
||||
for name in MANAGED:
|
||||
ok = sync_one(name, check_only) and ok
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_tools.py — canonical MiMo tool installer (TTY-repair tranche).
|
||||
|
||||
Canonical sources: config/mimo-tools/{bash.ts,await-operation.ts}
|
||||
Targets: ~/.mimocode/tools/{bash.ts,await-operation.ts}
|
||||
Verifies hashes after install; detects drift in both directions
|
||||
(the parity test reads these hashes).
|
||||
"""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
TOOLS = ["bash.ts", "await-operation.ts"]
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rc = 0
|
||||
for name in TOOLS:
|
||||
canon = REPO / "config" / "mimo-tools" / name
|
||||
target = Path.home() / ".mimocode" / "tools" / name
|
||||
if not canon.is_file():
|
||||
print(f"{name}: canonical source missing: {canon}")
|
||||
rc = 1
|
||||
continue
|
||||
canon_hash = sha256(canon)
|
||||
if target.is_file() and sha256(target) == canon_hash:
|
||||
print(f"{name}: in-sync {canon_hash[:12]}")
|
||||
continue
|
||||
drift = f"DRIFT canonical={canon_hash[:12]}"
|
||||
if target.is_file():
|
||||
drift += f" installed={sha256(target)[:12]}"
|
||||
else:
|
||||
drift += " installed=MISSING"
|
||||
print(f"{name}: {drift} — installing")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(canon, target)
|
||||
if sha256(target) != canon_hash:
|
||||
print(f"{name}: INSTALL FAILED (hash mismatch after copy)")
|
||||
rc = 1
|
||||
else:
|
||||
print(f"{name}: installed {canon_hash[:12]}")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_wr_daemon.py — REAL_ASYNC_BRIDGE §6: daemon governance.
|
||||
|
||||
Canonical source lives IN THIS REPO at bridge/daemon/. The active install
|
||||
is ~/.local/share/mimo-wait-resume/. This script is the ONLY supported
|
||||
way to move code between them:
|
||||
|
||||
- copy canonical source → install dir (atomic, per file)
|
||||
- verify sha256 convergence (fail loud on divergence)
|
||||
- optionally restart the daemon via scripts/start_wr_daemon.py
|
||||
|
||||
Never hand-edit the installed copy: edit the repo source, install, restart.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL = REPO / "bridge" / "daemon"
|
||||
INSTALL = Path.home() / ".local/share/mimo-wait-resume"
|
||||
FILES = ("daemon.py", "waitstore.py", "verify.py", "completion_file.py")
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--check", action="store_true", help="verify convergence only; no writes")
|
||||
ap.add_argument("--restart", action="store_true", help="restart the daemon after install")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not CANONICAL.is_dir():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
INSTALL.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
diverged = []
|
||||
for name in FILES:
|
||||
src, dst = CANONICAL / name, INSTALL / name
|
||||
if args.check:
|
||||
if not dst.is_file() or sha256(src) != sha256(dst):
|
||||
diverged.append(name)
|
||||
continue
|
||||
if not dst.is_file() or sha256(src) != sha256(dst):
|
||||
tmp = dst.with_suffix(f".tmp.{name}")
|
||||
shutil.copy2(src, tmp)
|
||||
tmp.replace(dst) # atomic
|
||||
print(f"installed: {name} {sha256(src)[:12]}")
|
||||
else:
|
||||
print(f"unchanged: {name} {sha256(src)[:12]}")
|
||||
|
||||
if args.check:
|
||||
if diverged:
|
||||
print(f"DIVERGED: {', '.join(diverged)} — run install_wr_daemon.py")
|
||||
return 1
|
||||
print("CONVERGED: installed copy == canonical repo source")
|
||||
return 0
|
||||
|
||||
# post-install convergence proof
|
||||
bad = [n for n in FILES if sha256(CANONICAL / n) != sha256(INSTALL / n)]
|
||||
if bad:
|
||||
print(f"INSTALL FAILED convergence: {bad}")
|
||||
return 1
|
||||
print("CONVERGED: installed copy == canonical repo source")
|
||||
|
||||
if args.restart:
|
||||
rc = subprocess.call([sys.executable, str(REPO / "scripts" / "start_wr_daemon.py")])
|
||||
if rc != 0:
|
||||
print("restart failed")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -7,6 +7,13 @@ The event is a WAKE, never completion truth: the waiting controller
|
||||
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.
|
||||
|
||||
REAL_ASYNC_BRIDGE (R3): the completion event is ALSO written directly as
|
||||
a durable completion file (resumes/<operation_id>.done). Delivery no
|
||||
longer depends on the daemon being alive: the waiter blocks on inotify
|
||||
for the file itself (kernel wake, zero latency, zero CPU), and the
|
||||
durable file survives daemon AND waiter restarts (R9). The write is
|
||||
best-effort on the same terms as the POST — never fails CI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -35,6 +42,42 @@ def main() -> int:
|
||||
"exit_code": 0 if result == "success" else 1,
|
||||
"completed_at": time.time(),
|
||||
}
|
||||
# R8 lost-event failure-recovery: operation-scoped fault injection.
|
||||
# A rule keyed by the EXACT operation_id suppresses BOTH normal
|
||||
# delivery channels (durable completion file AND bridge POST), so the
|
||||
# waiter's watchdog/failure-detector path is the only route to truth.
|
||||
# Disabled by default (no rule file => zero behavior change) and
|
||||
# observable: every decision is recorded in the R8 fault log.
|
||||
try:
|
||||
import r8_fault # noqa: E402 — sibling module
|
||||
if r8_fault.should_suppress(payload["operation_id"]):
|
||||
r8_fault.record(payload["operation_id"], "suppressed",
|
||||
{"sha": sha, "exit_code": payload["exit_code"],
|
||||
"source_terminal_at": payload["completed_at"]})
|
||||
print("notify-event-bridge: R8 fault injection — completion "
|
||||
f"event intentionally suppressed for "
|
||||
f"{payload['operation_id']} (no .done file, no POST); "
|
||||
"watchdog authority recovery is the expected path",
|
||||
flush=True)
|
||||
return 0
|
||||
r8_fault.record(payload["operation_id"], "delivered_normally",
|
||||
{"sha": sha, "rule_active": False})
|
||||
except Exception as exc: # noqa: BLE001 — fault seam must never break delivery
|
||||
print(f"notify-event-bridge: R8 fault-seam probe failed ({exc}); "
|
||||
"continuing with normal delivery", flush=True)
|
||||
# R3 durable delivery: write the completion file FIRST — it works even
|
||||
# if the bridge daemon is down, and the atomic rename means any waiter
|
||||
# (inotify-watching or starting later) sees a complete file or none.
|
||||
try:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import completion_file as cf # noqa: E402 — sibling module
|
||||
written = cf.write_completion(payload["operation_id"], payload)
|
||||
print(f"notify-event-bridge: durable completion file {written}",
|
||||
flush=True)
|
||||
except Exception as exc: # noqa: BLE001 — delivery is best-effort; watchdog recovery covers a lost completion file (§16D contract)
|
||||
print(f"notify-event-bridge: completion-file write failed ({exc}); "
|
||||
f"bridge POST and watchdog recovery still cover delivery",
|
||||
flush=True)
|
||||
for attempt in range(2):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""polyglot_lint.py — the language-AWARE lint gate (contract
|
||||
BUILDER-2026-10-07T20:01:40Z-ac6140).
|
||||
|
||||
Why: the pipeline linted Python hard (ruff + ratchet) but Rust NOT AT ALL
|
||||
(crates build, never clippy) and Go only via vet (gofmt drifted across 41
|
||||
files, including landed batches). This gate applies the RIGHT linter to
|
||||
whatever changed:
|
||||
|
||||
Python → ruff check
|
||||
Go → go vet (per module) + gofmt -l (formatting is a gate)
|
||||
Rust → cargo clippy -- -D warnings (per changed crate) + cargo fmt --check
|
||||
other → noted and skipped
|
||||
|
||||
Modes:
|
||||
(default) changed files vs the push range (git diff against
|
||||
origin/master / the last push ref; falls back to the
|
||||
last commit when no upstream exists)
|
||||
--all full tree
|
||||
--files a,b,c explicit list
|
||||
|
||||
Exit non-zero on any finding. The cron fault-watcher and the pre-push
|
||||
routine both run this; CI runs the Go+Rust halves natively.
|
||||
|
||||
Usage:
|
||||
python3 scripts/polyglot_lint.py # changed-only
|
||||
python3 scripts/polyglot_lint.py --all # everything
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
LANG_BY_EXT = {
|
||||
".py": "python",
|
||||
".go": "go",
|
||||
".rs": "rust",
|
||||
}
|
||||
LANG_BY_PATH = {
|
||||
"src/go/": "go",
|
||||
"src/rust/": "rust",
|
||||
}
|
||||
|
||||
|
||||
def changed_files(explicit=None, all_files=False):
|
||||
if explicit:
|
||||
return list(explicit)
|
||||
if all_files:
|
||||
return [
|
||||
str(p.relative_to(REPO))
|
||||
for p in REPO.rglob("*")
|
||||
if p.is_file() and not str(p).startswith((".git", "store/", "target"))
|
||||
]
|
||||
base = None
|
||||
for ref in ("origin/master", "gitea/master", "HEAD~1"):
|
||||
r = subprocess.run(["git", "rev-parse", "--verify", ref],
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
if r.returncode == 0:
|
||||
base = ref
|
||||
break
|
||||
if base is None:
|
||||
return []
|
||||
r = subprocess.run(["git", "diff", "--name-only", f"{base}...HEAD"],
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
return [f for f in r.stdout.splitlines() if f.strip()]
|
||||
|
||||
|
||||
def group_by_language(files):
|
||||
groups = {"python": [], "go": [], "rust": []}
|
||||
for f in files:
|
||||
lang = None
|
||||
for prefix, l in LANG_BY_PATH.items():
|
||||
if f.startswith(prefix):
|
||||
lang = l
|
||||
break
|
||||
if lang is None:
|
||||
lang = LANG_BY_EXT.get(Path(f).suffix)
|
||||
if lang:
|
||||
groups[lang].append(f)
|
||||
return groups
|
||||
|
||||
|
||||
def run(cmd, cwd=None):
|
||||
r = subprocess.run(cmd, cwd=cwd or REPO, capture_output=True, text=True)
|
||||
return r.returncode, r.stdout, r.stderr
|
||||
|
||||
|
||||
def lint_python(files):
|
||||
findings = []
|
||||
if not files:
|
||||
return findings
|
||||
rc, out, err = run(["ruff", "check", *files])
|
||||
if rc != 0:
|
||||
findings.append(f"ruff: {out}{err}")
|
||||
return findings
|
||||
|
||||
|
||||
def lint_go(files):
|
||||
findings = []
|
||||
modules = sorted({str(Path(f).parent) for f in files})
|
||||
# collapse to module roots (dirs containing go.mod)
|
||||
for mod in modules:
|
||||
mod_path = REPO / mod
|
||||
while mod_path != REPO and not (mod_path / "go.mod").exists():
|
||||
mod_path = mod_path.parent
|
||||
if mod_path == REPO:
|
||||
continue
|
||||
rel = mod_path.relative_to(REPO)
|
||||
rc, out, err = run(["go", "vet", "./..."], cwd=mod_path)
|
||||
if rc != 0:
|
||||
findings.append(f"go vet ({rel}): {out}{err}")
|
||||
# gofmt across the changed files' modules (formatting is a GATE)
|
||||
fmt_targets = [f for f in files if f.endswith(".go")]
|
||||
if fmt_targets:
|
||||
rc, out, _ = run(["gofmt", "-l", *fmt_targets])
|
||||
if rc != 0:
|
||||
findings.append(f"gofmt: failed to run")
|
||||
elif out.strip():
|
||||
findings.append(
|
||||
"gofmt unformatted (run `gofmt -w` on these):\n "
|
||||
+ "\n ".join(out.splitlines())
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def lint_rust(files):
|
||||
findings = []
|
||||
crates = sorted({str(Path(f).relative_to(REPO)) for f in files})
|
||||
crate_roots = set()
|
||||
for c in crates:
|
||||
p = REPO / c
|
||||
while p != REPO and not (p / "Cargo.toml").exists():
|
||||
p = p.parent
|
||||
if p != REPO:
|
||||
crate_roots.add(p)
|
||||
for crate in sorted(crate_roots):
|
||||
rc, out, err = run(["cargo", "clippy", "--", "-D", "warnings"], cwd=crate)
|
||||
if rc != 0:
|
||||
findings.append(f"clippy ({crate.name}):\n{out}{err}")
|
||||
rc, out, err = run(["cargo", "fmt", "--check"], cwd=crate)
|
||||
if rc != 0:
|
||||
findings.append(f"rustfmt ({crate.name}):\n{out}{err}")
|
||||
return findings
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--all", action="store_true", help="lint the full tree")
|
||||
ap.add_argument("--files", help="comma-separated explicit file list")
|
||||
args = ap.parse_args()
|
||||
|
||||
explicit = args.files.split(",") if args.files else None
|
||||
files = changed_files(explicit, args.all)
|
||||
if not files:
|
||||
print("polyglot lint: no changed files detected — nothing to do")
|
||||
return 0
|
||||
|
||||
groups = group_by_language(files)
|
||||
print(f"polyglot lint: {len(files)} changed → "
|
||||
f"python={len(groups['python'])} go={len(groups['go'])} "
|
||||
f"rust={len(groups['rust'])}")
|
||||
|
||||
all_findings = []
|
||||
all_findings += lint_python(groups["python"])
|
||||
all_findings += lint_go(groups["go"])
|
||||
all_findings += lint_rust(groups["rust"])
|
||||
|
||||
if all_findings:
|
||||
print(f"POLYGLOT LINT: {len(all_findings)} finding(s):")
|
||||
for f in all_findings:
|
||||
print(f" - {f}")
|
||||
return 1
|
||||
print("polyglot lint: clean")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""r8_fault.py — REAL_ASYNC_BRIDGE R8: operation-scoped lost-event fault
|
||||
injection (evidence-only harness).
|
||||
|
||||
DESIGN CONTRACT (R8 §3):
|
||||
disabled by default no rule file -> zero behavior change
|
||||
operation-scoped rule keyed by EXACT operation_id
|
||||
reversible delete the rule file / rule entry
|
||||
observable every decision appended to a JSONL log
|
||||
cannot affect other waits rule file checked by operation_id only
|
||||
|
||||
The ONLY consumer is scripts/notify_event_bridge.py: before normal
|
||||
delivery it consults should_suppress(operation_id). Suppression means:
|
||||
- NO durable completion file is written
|
||||
- NO bridge POST (no /process-event wake)
|
||||
- the suppression decision is recorded as R8 fault evidence
|
||||
Nothing else in the stack reads the rule. The daemon, waitstore,
|
||||
authority verification, and completion-file machinery are untouched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
RUNE_WR_ROOT = os.environ.get("RUNE_WR_ROOT") or os.environ.get("WR_ROOT") or "/mnt/workdrive/wr-bridge"
|
||||
RULES_PATH = Path(RUNE_WR_ROOT) / "r8_suppress_rules.json"
|
||||
FAULT_LOG = Path(RUNE_WR_ROOT) / "r8_fault_log.jsonl"
|
||||
|
||||
REASON = "event intentionally suppressed for R8 lost-event failure-recovery"
|
||||
|
||||
|
||||
def _rules() -> dict:
|
||||
"""Read the rule file. Absent/unreadable => no suppression (fail-open
|
||||
to NORMAL delivery; the fault must never take the healthy path down)."""
|
||||
try:
|
||||
return json.loads(RULES_PATH.read_text())
|
||||
except Exception: # noqa: BLE001 — absent rule file is the default state
|
||||
return {}
|
||||
|
||||
|
||||
def lookup(operation_id: str) -> dict | None:
|
||||
"""Return the active suppression rule for EXACTLY this operation_id,
|
||||
or None. Expired rules are ignored (not deleted — deletion is the
|
||||
operator's explicit disable action)."""
|
||||
spec = _rules()
|
||||
if not spec.get("enabled"):
|
||||
return None
|
||||
rule = spec.get("operations", {}).get(operation_id)
|
||||
if not rule:
|
||||
return None
|
||||
expires = rule.get("expires_at")
|
||||
if expires:
|
||||
try:
|
||||
if time.time() > time.mktime(time.strptime(expires, "%Y-%m-%dT%H:%M:%SZ")) - time.timezone:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
return rule
|
||||
|
||||
|
||||
def should_suppress(operation_id: str) -> bool:
|
||||
return lookup(operation_id) is not None
|
||||
|
||||
|
||||
def record(operation_id: str, stage: str, extra: dict | None = None) -> None:
|
||||
"""Append a durable fault-evidence line (best-effort, never raises)."""
|
||||
try:
|
||||
entry = {
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"operation_id": operation_id,
|
||||
"reason": REASON,
|
||||
"stage": stage,
|
||||
**(extra or {}),
|
||||
}
|
||||
FAULT_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
with FAULT_LOG.open("a") as f:
|
||||
f.write(json.dumps(entry, sort_keys=True) + "\n")
|
||||
except OSError as exc: # noqa: BLE001 — evidence logging must never break CI
|
||||
print(f"r8_fault: evidence log write failed: {exc}", file=sys.stderr)
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — whole-project inventory generator.
|
||||
|
||||
Parses every .py file under src/rune/ with the stdlib AST and emits
|
||||
docs/rebase/rebase-inventory.json: files, classes, functions (with signatures and
|
||||
line ranges), imports, and best-effort caller/callee edges (intra-module call
|
||||
graph + cross-module edges resolved through import tables).
|
||||
|
||||
Deterministic and idempotent: same tree in, same JSON out (sorted keys,
|
||||
sorted files). The issue-factory reads this file; nothing else may edit it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SRC = REPO / "src" / "rune"
|
||||
OUT = REPO / "docs" / "rebase" / "rebase-inventory.json"
|
||||
|
||||
|
||||
def _arg_str(a: ast.arg) -> str:
|
||||
s = a.arg
|
||||
if a.annotation is not None:
|
||||
# Degraded signature (no annotation) is deterministic metadata — never
|
||||
# let one pathological annotation node abort the whole inventory walk.
|
||||
try:
|
||||
s += f": {ast.unparse(a.annotation)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return s
|
||||
|
||||
|
||||
def _signature(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
||||
parts = [_arg_str(a) for a in node.args.args]
|
||||
if node.args.vararg:
|
||||
parts.append(f"*{node.args.vararg.arg}")
|
||||
elif node.args.kwonlyargs:
|
||||
parts.append("*")
|
||||
parts += [_arg_str(a) for a in node.args.kwonlyargs]
|
||||
if node.args.kwarg:
|
||||
parts.append(f"**{node.args.kwarg.arg}")
|
||||
ret = ""
|
||||
if node.returns is not None:
|
||||
# Same degraded-metadata contract as _arg_str.
|
||||
try:
|
||||
ret = f" -> {ast.unparse(node.returns)}"
|
||||
except (AttributeError, RecursionError, ValueError):
|
||||
pass
|
||||
return f"({', '.join(parts)}){ret}"
|
||||
|
||||
|
||||
class FileVisitor(ast.NodeVisitor):
|
||||
def __init__(self) -> None:
|
||||
self.functions: list[dict] = []
|
||||
self.classes: list[dict] = []
|
||||
self.imports: list[dict] = []
|
||||
self.calls: list[tuple[int, str]] = [] # (lineno, dotted call name)
|
||||
|
||||
def _fn(self, node, parents: list[str]) -> dict:
|
||||
return {
|
||||
"name": ".".join([*parents, node.name]),
|
||||
"signature": _signature(node),
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"decorators": [ast.unparse(d) for d in node.decorator_list
|
||||
if isinstance(d, (ast.Name, ast.Attribute, ast.Call))],
|
||||
"async": isinstance(node, ast.AsyncFunctionDef),
|
||||
}
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
methods = []
|
||||
for child in node.body:
|
||||
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
methods.append(self._fn(child, [node.name]))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(child)
|
||||
if isinstance(n, ast.Call) and not isinstance(n.func, ast.Name if False else type(None))
|
||||
and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
self.classes.append({
|
||||
"name": node.name,
|
||||
"bases": [ast.unparse(b) for b in node.bases],
|
||||
"line_start": node.lineno,
|
||||
"line_end": node.end_lineno or node.lineno,
|
||||
"methods": methods,
|
||||
})
|
||||
# visit only non-function children: methods are already captured above,
|
||||
# and a generic_visit would re-fire visit_FunctionDef on them (duplicate
|
||||
# units + duplicate call edges). Nested classes are still visited.
|
||||
for child in node.body:
|
||||
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
self.visit(child)
|
||||
|
||||
def visit_FunctionDef(self, node) -> None:
|
||||
# top-level only reached here (methods captured in ClassDef)
|
||||
self.functions.append(self._fn(node, []))
|
||||
self.calls.extend(
|
||||
(n.lineno, ast.unparse(n.func))
|
||||
for n in ast.walk(node)
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute))
|
||||
)
|
||||
# do not descend into nested defs separately; they are folded into parents
|
||||
# (their calls are already captured by the ast.walk above). No generic_visit:
|
||||
# it would re-collect nested defs as top-level functions and duplicate edges.
|
||||
|
||||
visit_AsyncFunctionDef = visit_FunctionDef
|
||||
|
||||
def visit_Import(self, node: ast.Import) -> None:
|
||||
for a in node.names:
|
||||
self.imports.append({"module": a.name, "as": a.asname, "line": node.lineno})
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
mod = node.module or ""
|
||||
for a in node.names:
|
||||
self.imports.append({"module": f"{mod}.{a.name}" if a.name != "*" else mod,
|
||||
"as": a.asname or a.name, "line": node.lineno,
|
||||
"from": True})
|
||||
|
||||
|
||||
def collect_file(path: Path) -> dict:
|
||||
src_text = path.read_text(encoding="utf-8", errors="replace")
|
||||
tree = ast.parse(src_text)
|
||||
v = FileVisitor()
|
||||
v.visit(tree)
|
||||
|
||||
# module-level calls (outside defs/classes) — walk tree body directly.
|
||||
# SKIP def/class statements: their bodies are walked by the visit_ methods;
|
||||
# walking them here duplicated every call edge inside every top-level function.
|
||||
for stmt in tree.body:
|
||||
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
continue
|
||||
for n in ast.walk(stmt):
|
||||
if isinstance(n, ast.Call) and isinstance(n.func, (ast.Name, ast.Attribute)):
|
||||
v.calls.append((n.lineno, ast.unparse(n.func)))
|
||||
|
||||
defined = {f["name"].split(".")[-1] for f in v.functions}
|
||||
defined |= {m["name"].split(".")[-1] for c in v.classes for m in c["methods"]}
|
||||
defined |= {c["name"] for c in v.classes}
|
||||
|
||||
callees = []
|
||||
for lineno, call in v.calls:
|
||||
root = call.split(".")[0].split("(")[0].strip()
|
||||
callees.append({"line": lineno, "call": call,
|
||||
"internal": root in defined or root == "self"})
|
||||
|
||||
return {
|
||||
"path": str(path.relative_to(REPO)),
|
||||
"lines": len(src_text.splitlines()),
|
||||
"imports": sorted({i["module"] for i in v.imports}),
|
||||
"import_lines": v.imports,
|
||||
"functions": sorted(v.functions, key=lambda f: (f["line_start"], f["name"])),
|
||||
"classes": sorted(v.classes, key=lambda c: c["line_start"]),
|
||||
"calls": callees,
|
||||
}
|
||||
|
||||
|
||||
def cross_module_edges(files: dict[str, dict]) -> dict[str, set[str]]:
|
||||
"""Resolve import tables: module-stem -> set of imported module stems
|
||||
(rune-internal only), so the factory can attribute cross-module edges."""
|
||||
graph = defaultdict(set)
|
||||
for rel, info in files.items():
|
||||
stem = Path(rel).stem
|
||||
for imp in info["imports"]:
|
||||
top = imp.split(".")[0]
|
||||
second = ".".join(imp.split(".")[:2])
|
||||
if top == "rune":
|
||||
graph[stem].add(imp.replace("rune.", "", 1).split(".")[0])
|
||||
elif not imp.startswith("_") and top not in sys.stdlib_module_names:
|
||||
graph[stem].add(f"ext:{top}")
|
||||
return graph
|
||||
|
||||
|
||||
def main() -> None:
|
||||
py_files = sorted(SRC.rglob("*.py"))
|
||||
files: dict[str, dict] = {}
|
||||
for p in py_files:
|
||||
try:
|
||||
info = collect_file(p)
|
||||
except SyntaxError as e: # a file that cannot parse is a finding, not a skip
|
||||
info = {"path": str(p.relative_to(REPO)), "lines": 0, "imports": [],
|
||||
"import_lines": [], "functions": [], "classes": [], "calls": [],
|
||||
"parse_error": f"{type(e).__name__}: {e}"}
|
||||
files[info["path"]] = info
|
||||
|
||||
graph = cross_module_edges(files)
|
||||
total_fns = sum(len(f["functions"]) + sum(len(c["methods"]) for c in f["classes"])
|
||||
for f in files.values())
|
||||
doc = {
|
||||
"_meta": {
|
||||
"generator": "scripts/rebase_inventory.py",
|
||||
"tranche": "R1 language rebase, Wave 0",
|
||||
"note": "Machine-generated. Regenerate with the script; never hand-edit.",
|
||||
"files": len(files),
|
||||
"functions_total": total_fns,
|
||||
"classes_total": sum(len(f["classes"]) for f in files.values()),
|
||||
"lines_total": sum(f["lines"] for f in files.values()),
|
||||
},
|
||||
"import_graph": {k: sorted(v) for k, v in sorted(graph.items())},
|
||||
"files": dict(sorted(files.items())),
|
||||
}
|
||||
OUT.write_text(json.dumps(doc, indent=1, sort_keys=False) + "\n", encoding="utf-8")
|
||||
m = doc["_meta"]
|
||||
print(f"inventory written: {OUT}")
|
||||
print(f"files={m['files']} functions={m['functions_total']} "
|
||||
f"classes={m['classes_total']} lines={m['lines_total']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tranche R1 Wave 0 — per-function issue factory.
|
||||
|
||||
Reads docs/rebase/rebase-inventory.json and emits one issue per unit (top-level
|
||||
function or class method) following tranche R1 §7. Wave-batched, dedup-safe
|
||||
(deterministic title + existing-issue scan before create), rate-limited.
|
||||
|
||||
Modes:
|
||||
plan — coverage report: units per wave, unassigned modules (no silent drops)
|
||||
sample — emit N rendered issues to a file for review (no Gitea call)
|
||||
create — create issues in Gitea for one wave
|
||||
|
||||
Dedup contract: issue titles are deterministic —
|
||||
[rebase w{wave}] {relpath}::{unit}
|
||||
The factory fetches existing issue titles once per run and skips any that
|
||||
already exist, so retries never duplicate (preserve-first Gitea addendum).
|
||||
|
||||
The factory NEVER writes behaviour prose. Current-behaviour paragraphs and
|
||||
the three evidence answers (IMPROVE / FIX / MISSING CONNECTORS) are filled at
|
||||
disposition time by the working agent — the issue carries the mandatory
|
||||
fields and line ranges, not fabricated content.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INVENTORY = REPO_ROOT / "docs" / "rebase" / "rebase-inventory.json"
|
||||
COVERAGE_OUT = REPO_ROOT / "docs" / "rebase" / "rebase-wave-coverage.json"
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
|
||||
REPO = "conductor/rune"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "shared"))
|
||||
from gitea_auth import load_gitea_token # noqa: E402
|
||||
|
||||
TRANCHE_REF = "tranche R1 language rebase (2026-09-29), ~/.config/opencode/tranches/2026-09-29-language-rebase-tranche-R1.md"
|
||||
|
||||
# Wave membership from R1 §4 (initial, measured). Directory entries match by
|
||||
# path prefix; bare stems match the file stem. Modules matching no entry are
|
||||
# reported UNASSIGNED by `plan` — never silently dropped (R1 §9 risk 7).
|
||||
WAVE_PLAN = {
|
||||
2: [ # leaf utilities & canaries
|
||||
"pins", "time_awareness", "message_queue", "event_envelope",
|
||||
"attention_capsule", "complexity_scorer", "token_optimizer", "cache",
|
||||
"prompt_sync", "chat_bubble", "canary_*",
|
||||
],
|
||||
3: [ # memory & persistence
|
||||
"memory_store", "curator", "warm_router", "memory_consolidation",
|
||||
"graph_memory", "sleep_time", "night_mode", "identity_anchors",
|
||||
"query_memory", "memory_env", "warm_neuron_store", "subconscious_pass",
|
||||
"task_persist",
|
||||
],
|
||||
4: [ # provider/model layer
|
||||
"provider_registry", "model_connect", "model_routing", "model_profiles",
|
||||
"provider_budget", "standardized_providers", "model_registry",
|
||||
"connection_pool", "forge_client",
|
||||
],
|
||||
5: [ # orchestration, authority, plugins, skills, governance
|
||||
"orchestration/", "microkernel", "contracts", "build_mode", "plugins/",
|
||||
"skills/", "construction_authority", "construction_authority_cli",
|
||||
"construction_decision", "construction_mode", "verification_construction",
|
||||
"authority_recovery", "spine", "drift_core", "drift_ledger",
|
||||
"drift_observability", "persona_gate", "safety_override", "witness",
|
||||
"behavior", "growth_delta", "claim_audit", "verification_layer",
|
||||
"drift_compression", "drift_cache", "self_improve", "persona_definition",
|
||||
"outcome_ledger", "audit_ledger", "subsystem_health",
|
||||
],
|
||||
6: [ # runtime core & tooling
|
||||
"tool_executor", "tool_protocol", "local_tool_runtime",
|
||||
"capability_broker", "context_broker", "work_manager", "cron_engine",
|
||||
"auto_tasks", "goal", "failure_auto_filer", "hook_engine",
|
||||
"elastic_cognition", "event_bus", "system_registry", "abstraction",
|
||||
"tiered_executor", "progressive_escalation", "tier_manager",
|
||||
"parallel_processor", "shard_manager", "response_adapter",
|
||||
"context_pressure", "auto_context", "cognitive_budgeter",
|
||||
"recall_router", "inference_trace", "task_gate", "salience_gate",
|
||||
"dependency_tracker", "rune_profiler",
|
||||
],
|
||||
7: [ # operator + TUI, last by design
|
||||
"rune_operator", "rune_tui", "tui_validator", "voice_hybrid",
|
||||
"voice_sync", "voicebox", "voice_injector", "message_widgets",
|
||||
"rune_graphic", "rune_spinner", "__main__",
|
||||
],
|
||||
}
|
||||
|
||||
WAVE_NAMES = {
|
||||
2: "leaf utilities & canaries",
|
||||
3: "memory & persistence",
|
||||
4: "provider/model layer",
|
||||
5: "orchestration, authority, plugins, skills, governance",
|
||||
6: "runtime core & tooling",
|
||||
7: "operator + TUI",
|
||||
}
|
||||
|
||||
|
||||
def match_module(relpath: str):
|
||||
"""Return the wave number for a module path, or None if unassigned."""
|
||||
stem = Path(relpath).stem
|
||||
for wave, patterns in WAVE_PLAN.items():
|
||||
for pat in patterns:
|
||||
if pat.endswith("/"):
|
||||
if pat in relpath:
|
||||
return wave
|
||||
elif pat.endswith("*"):
|
||||
if stem.startswith(pat[:-1]):
|
||||
return wave
|
||||
elif stem == pat:
|
||||
return wave
|
||||
return None
|
||||
|
||||
|
||||
def iter_units(inv):
|
||||
"""Yield (relpath, unit_name, entry) for every function and method."""
|
||||
for relpath, mod in sorted(inv["files"].items()):
|
||||
for fn in mod["functions"]:
|
||||
yield relpath, fn["name"], fn
|
||||
for cls in mod["classes"]:
|
||||
for m in cls.get("methods", []):
|
||||
yield relpath, f"{cls['name']}.{m['name']}", m
|
||||
|
||||
|
||||
def internal_edges(relpath: str, entry: dict):
|
||||
"""Collect internal (in-repo) call names and line numbers for a unit."""
|
||||
lines = (entry["line_start"], entry["line_end"])
|
||||
mod = inv_cache["files"][relpath]
|
||||
calls = [c for c in mod.get("calls", [])
|
||||
if lines[0] <= c["line"] <= lines[1] and c.get("internal")]
|
||||
return calls
|
||||
|
||||
|
||||
inv_cache = {}
|
||||
|
||||
|
||||
def issue_title(wave: int, relpath: str, unit: str) -> str:
|
||||
return f"[rebase w{wave}] {relpath}::{unit}"
|
||||
|
||||
|
||||
def render_issue(wave: int, relpath: str, unit: str, entry: dict) -> dict:
|
||||
calls = internal_edges(relpath, entry)
|
||||
call_lines = "\n".join(
|
||||
f" - `{c['call']}` (line {c['line']})" for c in calls[:20]
|
||||
) or " - (no internal call edges in range)"
|
||||
sig = entry.get("signature", "()")
|
||||
body = f"""\
|
||||
|
||||
## Identity (from the Wave 0 inventory — machine-generated)
|
||||
|
||||
- **File**: `{relpath}`
|
||||
- **Unit**: `{unit}`
|
||||
- **Signature**: `def {unit}{sig}` (async: {entry.get('async', False)})
|
||||
- **Line range**: {entry['line_start']}–{entry['line_end']}
|
||||
- **Decorators**: {entry.get('decorators', []) or 'none'}
|
||||
- **Internal call edges** (callers/callees inside the repo, from inventory):
|
||||
{call_lines}
|
||||
|
||||
## Current behaviour
|
||||
|
||||
One honest paragraph from READING THE CODE at the line range above — what it
|
||||
actually does, not what the docstring promises. Write this at disposition
|
||||
time; the factory does not fabricate it.
|
||||
|
||||
## The three questions — each answered WITH EVIDENCE
|
||||
|
||||
1. **IMPROVE?** Can the unit be improved within its scope? Evidence or "no — because …".
|
||||
(Bounded to unit scope; larger findings spawn NEW issues, never expand this one.)
|
||||
2. **FIX?** Is it broken, buggy, or failing its expected outcome? Evidence: a failing
|
||||
test, a reproduced fault, or a code-level defect citation. A "no" needs evidence too.
|
||||
3. **MISSING CONNECTORS?** Are there expected outcomes this unit should connect to but
|
||||
doesn't (unwired events, dead parameters, promised-but-absent calls)? Evidence required.
|
||||
|
||||
A silent skip on any question is a factory bug — answer all three.
|
||||
|
||||
## Language verdict (per tranche R1 §6 rubric — evidence beats instinct)
|
||||
|
||||
Choose exactly one, with the evidence line:
|
||||
- **KEEP-PYTHON** — one sentence of justification.
|
||||
- **RECODE-<lang>** — rubric scores (all 7 dimensions) + benchmark-gate plan: the compiled
|
||||
replacement must beat the Python original on the MEASURED hot path, or the recode is
|
||||
rejected and recorded. No win, no recode.
|
||||
- **MERGE-into:<module>** — name the target and why.
|
||||
- **DEPRECATE** — name the caller that no longer needs it.
|
||||
|
||||
Go is NOT pre-decided. Python is not pre-decided. The rubric decides.
|
||||
|
||||
## Constraint context
|
||||
|
||||
- henxels structural contract: this unit's module ≤ 1,200 lines (or on the debt table's
|
||||
exception list — check before adding lines).
|
||||
- Spine: any module-surface change requires `store/spine/current.json` regen with the code
|
||||
change (`test_manifest_maps_every_module` is the arbiter for new top-level modules).
|
||||
- Debt table: docs/structural-contract.md.
|
||||
|
||||
## Pipeline plan
|
||||
|
||||
1. Branch `issue-<N>` from current master (existing issue-N convention).
|
||||
2. Scaffolding activation for any construction (construction_mode/decision per unit change).
|
||||
3. Research rounds are MANDATORY for any RECODE verdict (research-stage-headless; ≥2
|
||||
live-verified external sources per claim; per-source lessons; kill test per candidate).
|
||||
4. Implementation behind the interop contract (R1 §5): compiled units are standalone
|
||||
binaries — argv in, JSON on stdout, non-zero exit on failure, `--version` mandatory;
|
||||
Python wraps in a thin adapter; store stays the shared truth.
|
||||
5. Local gates: pytest + ruff + spine-store regen (if surface changed) + henxels.
|
||||
6. PR → pipeline-gate + ci + required checks → merge-robot → auto-version.
|
||||
|
||||
## Verification
|
||||
|
||||
- The specific test proving this unit's change: (write at disposition).
|
||||
- The keep-green set it must not break: full suite must stay green; named
|
||||
keep-green tests for this unit's callers.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Tranche: {TRANCHE_REF}
|
||||
- Wave: {wave} ({WAVE_NAMES.get(wave, 'unassigned')})
|
||||
- Inventory: docs/rebase/rebase-inventory.json (regenerate with scripts/rebase_inventory.py — never hand-edit)
|
||||
- Factory: scripts/rebase_issue_factory.py
|
||||
"""
|
||||
return {
|
||||
"title": issue_title(wave, relpath, unit),
|
||||
"body": body,
|
||||
"labels": ["rebase", f"wave-{wave}"],
|
||||
"meta": {
|
||||
"wave": wave,
|
||||
"file": relpath,
|
||||
"unit": unit,
|
||||
"line_start": entry["line_start"],
|
||||
"line_end": entry["line_end"],
|
||||
"async": entry.get("async", False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def fetch_existing_titles() -> set:
|
||||
"""Fetch all issue titles in the repo (paginated) for dedup."""
|
||||
token = load_gitea_token()
|
||||
titles = set()
|
||||
page = 1
|
||||
while True:
|
||||
url = (f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
f"?state=all&type=issues&limit=50&page={page}")
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
batch = json.loads(resp.read())
|
||||
if not batch:
|
||||
break
|
||||
titles.update(i["title"] for i in batch)
|
||||
if len(batch) < 50:
|
||||
break
|
||||
page += 1
|
||||
return titles
|
||||
|
||||
|
||||
def create_issue(entry: dict) -> dict:
|
||||
token = load_gitea_token()
|
||||
url = f"{GITEA_URL}/api/v1/repos/{REPO}/issues"
|
||||
payload = json.dumps({
|
||||
"title": entry["title"],
|
||||
"body": entry["body"],
|
||||
"labels": [], # label NAMES are set via the labels endpoint by auto-label; keep create minimal
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=payload, method="POST",
|
||||
headers={"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def cmd_plan(inv):
|
||||
counts = {}
|
||||
unassigned_modules = {}
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
wave = match_module(relpath)
|
||||
counts[wave] = counts.get(wave, 0) + 1
|
||||
if wave is None:
|
||||
unassigned_modules.setdefault(relpath, inv["files"][relpath]["lines"])
|
||||
report = {
|
||||
"units_per_wave": {str(k if k is not None else "UNASSIGNED"): v
|
||||
for k, v in sorted(counts.items(), key=lambda kv: str(kv[0]))},
|
||||
"unassigned_modules": dict(sorted(unassigned_modules.items(), key=lambda kv: -kv[1])),
|
||||
"unassigned_module_count": len(unassigned_modules),
|
||||
}
|
||||
print(json.dumps(report, indent=1))
|
||||
COVERAGE_OUT.write_text(json.dumps(report, indent=1) + "\n")
|
||||
print(f"\ncoverage written to {COVERAGE_OUT.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 0 if not unassigned_modules else 1
|
||||
|
||||
|
||||
def cmd_sample(inv, wave, limit, out):
|
||||
emitted = 0
|
||||
records = []
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
records.append(render_issue(wave, relpath, unit, entry))
|
||||
emitted += 1
|
||||
if emitted >= limit:
|
||||
break
|
||||
Path(out).write_text("\n".join(json.dumps(r) for r in records) + "\n")
|
||||
print(f"sampled {emitted} issues for wave {wave} -> {out}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_create(inv, wave, limit, dry_gap):
|
||||
existing = fetch_existing_titles()
|
||||
created, skipped = 0, 0
|
||||
for relpath, unit, entry in iter_units(inv):
|
||||
if match_module(relpath) != wave:
|
||||
continue
|
||||
title = issue_title(wave, relpath, unit)
|
||||
if title in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
issue = create_issue(render_issue(wave, relpath, unit, entry))
|
||||
created += 1
|
||||
print(f"created #{issue.get('number')}: {title}", file=sys.stderr)
|
||||
if limit and created >= limit:
|
||||
break
|
||||
time.sleep(dry_gap)
|
||||
print(f"\nwave {wave}: created={created} skipped(dedup)={skipped}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("command", choices=["plan", "sample", "create"])
|
||||
ap.add_argument("--wave", type=int, help="wave number (2-7)")
|
||||
ap.add_argument("--limit", type=int, default=0, help="max issues for sample/create (0 = unlimited; sample defaults to 5)")
|
||||
ap.add_argument("--out", default=None, help="sample output file")
|
||||
ap.add_argument("--gap", type=float, default=0.5, help="seconds between creates")
|
||||
args = ap.parse_args()
|
||||
|
||||
inv_cache.update(json.loads(INVENTORY.read_text()))
|
||||
inv = inv_cache
|
||||
|
||||
if args.command == "plan":
|
||||
return cmd_plan(inv)
|
||||
if args.command == "sample":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
out = args.out or f"docs/rebase-factory-sample-w{args.wave}.json"
|
||||
return cmd_sample(inv, args.wave, args.limit or 5, out)
|
||||
if args.command == "create":
|
||||
if not args.wave:
|
||||
ap.error("--wave required")
|
||||
return cmd_create(inv, args.wave, args.limit, args.gap)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""release_exit_note.py — the EXIT-NOTE TEMPLATE (Nick, 2026-10-08).
|
||||
|
||||
The counterpart of the issue-entry template: when the pipeline earns a new
|
||||
version, this renders a PLAIN-ENGLISH summary of what was worked on and
|
||||
submitted, so version history reads without opening commits.
|
||||
|
||||
Template (what the reader sees):
|
||||
## What changed in v<VERSION>
|
||||
|
||||
<aggregate paragraph — counts by kind, the headline of the wave>
|
||||
|
||||
### What was worked on
|
||||
- <commit subject, translated to plain English>
|
||||
|
||||
### How it was verified
|
||||
- contracts walked (BUILDER-...) / truth batteries / CI gates — one
|
||||
line per verification kind found in the range
|
||||
|
||||
Feed: git log v<last-tag>..HEAD (chore(release) bumps excluded), the
|
||||
conventional-commit prefix gives the kind; the subject's scope gives the
|
||||
plain-English translation. The release step calls render() and passes the
|
||||
result as `extra.improved`/`added`/`fixed` per kind, so the evidence-
|
||||
derived note carries the human narrative alongside the governed facts.
|
||||
|
||||
Usage:
|
||||
python3 scripts/release_exit_note.py --since v0.117.0 [--to HEAD]
|
||||
python3 scripts/release_exit_note.py --since v0.117.0 --json # machine
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Conventional-commit prefix → (plain-english verb phrase, release-notes key)
|
||||
KIND_MAP = {
|
||||
"feat": ("added", "added"),
|
||||
"fix": ("fixed", "fixed"),
|
||||
"test": ("verified by test", "improved"),
|
||||
"docs": ("documented", "improved"),
|
||||
"chore": ("maintained", "improved"),
|
||||
"refactor": ("restructured", "improved"),
|
||||
"perf": ("made faster", "improved"),
|
||||
}
|
||||
|
||||
# Phrases in commit subjects translated to reader language
|
||||
TRANSLATIONS = [
|
||||
(r"\bcontract BUILDER-\S+", "under a construction contract"),
|
||||
(r"\bparity(?! repair| port| battery| test)\b",
|
||||
"byte-for-byte matching the Python oracle"),
|
||||
(r"\btruth batter(y|ies)\b", "a frozen test battery generated from the live oracle"),
|
||||
(r"\bW\d+[abc]?\b", ""),
|
||||
(r"\((?:W\d+[abc]?|[^)]*contract[^)]*)\)", ""),
|
||||
]
|
||||
|
||||
_NOISE_RE = re.compile(r"^chore\(release\): auto-bump|Merge (remote|pull)|^sync:")
|
||||
|
||||
|
||||
def commit_range(since, to="HEAD"):
|
||||
r = subprocess.run(
|
||||
["git", "log", f"{since}..{to}", "--format=%s", "--no-merges"],
|
||||
capture_output=True, text=True, cwd=REPO,
|
||||
)
|
||||
return [s.strip() for s in r.stdout.splitlines() if s.strip() and not _NOISE_RE.match(s)]
|
||||
|
||||
|
||||
def classify(subject):
|
||||
m = re.match(r"^(\w+)(?:\(([^)]*)\))?:\s*(.+)", subject)
|
||||
if not m:
|
||||
return "improved", subject, None
|
||||
kind, scope, rest = m.group(1), m.group(2), m.group(3)
|
||||
verb, key = KIND_MAP.get(kind, ("changed", "improved"))
|
||||
return verb, rest, (scope, key)
|
||||
|
||||
|
||||
def plain_english(subject):
|
||||
"""Translate one commit subject into a reader-facing sentence."""
|
||||
verb, rest, scope_key = classify(subject)
|
||||
text = rest[0].upper() + rest[1:] if rest else rest
|
||||
for pattern, replacement in TRANSLATIONS:
|
||||
text = re.sub(pattern, replacement, text)
|
||||
text = re.sub(r"\s+", " ", text).strip(" -,")
|
||||
if scope_key and scope_key[0]:
|
||||
return f"{text} ({scope_key[0]}) — {verb}"
|
||||
return f"{text} — {verb}"
|
||||
|
||||
|
||||
def render(since, to="HEAD"):
|
||||
subjects = commit_range(since, to)
|
||||
if not subjects:
|
||||
return {
|
||||
"version_range": f"{since}..{to}",
|
||||
"headline": "No substantive changes since the last version.",
|
||||
"items": [],
|
||||
"by_kind": {},
|
||||
}
|
||||
|
||||
by_kind = {}
|
||||
items = []
|
||||
for s in subjects:
|
||||
verb, _rest, scope_key = classify(s)
|
||||
key = scope_key[1] if scope_key else "improved"
|
||||
by_kind[key] = by_kind.get(key, 0) + 1
|
||||
items.append(plain_english(s))
|
||||
|
||||
counts = ", ".join(f"{v} {k}" for k, v in sorted(by_kind.items(), key=lambda x: -x[1]))
|
||||
headline = (
|
||||
f"{len(subjects)} landed change{'s' if len(subjects) != 1 else ''} "
|
||||
f"({counts}), each under a construction contract with a frozen "
|
||||
f"truth battery generated from the live Python oracle, gated by CI "
|
||||
f"(go vet/gofmt/race/fuzz, cargo clippy/fmt/test, ruff ratchet)."
|
||||
)
|
||||
return {
|
||||
"version_range": f"{since}..{to}",
|
||||
"headline": headline,
|
||||
"items": items,
|
||||
"by_kind": by_kind,
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(note):
|
||||
lines = [f"## What changed in this version", "", note["headline"], ""]
|
||||
if note["items"]:
|
||||
lines.append("### What was worked on")
|
||||
lines.append("")
|
||||
for item in note["items"]:
|
||||
lines.append(f"- {item}")
|
||||
lines.append("")
|
||||
lines.append("### How it was verified")
|
||||
lines.append("")
|
||||
lines.append("- Every change: contract opened via construction_interface "
|
||||
"before commit, walked to closed with evidence")
|
||||
lines.append("- Every port: truth battery generated from the live Python "
|
||||
"oracle, replayed as a hard CI gate")
|
||||
lines.append("- Pipeline: ruff + silent-handler ratchet (Python), go "
|
||||
"vet/gofmt/race/fuzz (Go), cargo clippy/fmt/test (Rust)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--since", required=True, help="last tag, e.g. v0.117.0")
|
||||
ap.add_argument("--to", default="HEAD")
|
||||
ap.add_argument("--json", action="store_true")
|
||||
args = ap.parse_args()
|
||||
note = render(args.since, args.to)
|
||||
if args.json:
|
||||
print(json.dumps(note, indent=1))
|
||||
else:
|
||||
print(render_markdown(note))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -53,6 +53,9 @@ def main(argv=None) -> int:
|
||||
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_init.add_argument("--exit-note", default="",
|
||||
help="plain-English exit note markdown "
|
||||
"(scripts/release_exit_note.py output)")
|
||||
|
||||
p_ref = sub.add_parser("refresh", help="convergence refresh")
|
||||
p_ref.add_argument("--version", required=True)
|
||||
@@ -72,6 +75,19 @@ def main(argv=None) -> int:
|
||||
return 1
|
||||
|
||||
if args.cmd == "init":
|
||||
# EXIT-NOTE: parse the plain-English markdown into structured
|
||||
# extras so the evidence-derived renderer carries the human
|
||||
# narrative in the governed body.
|
||||
extra = None
|
||||
if args.exit_note:
|
||||
items = [l[2:].strip() for l in args.exit_note.splitlines()
|
||||
if l.startswith("- ")]
|
||||
headline = ""
|
||||
for l in args.exit_note.splitlines():
|
||||
if l and not l.startswith(("#", "-", " ")) and "What changed" not in l:
|
||||
headline = l.strip()
|
||||
break
|
||||
extra = {"improved": ([headline] if headline else []) + items}
|
||||
note = rn.auto_initial_note(
|
||||
version=args.version,
|
||||
summary=f"Rune v{args.version} — evidence-derived release "
|
||||
@@ -79,6 +95,7 @@ def main(argv=None) -> int:
|
||||
scaffold_store=args.store,
|
||||
release_sha=args.release_sha,
|
||||
deployed_sha=args.deployed_sha or args.release_sha,
|
||||
extra=extra,
|
||||
)
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""scaffolding_coverage.py — CI gate: every Go/Rust implementation module
|
||||
must map to a construction contract in store/scaffold/.
|
||||
|
||||
The absolute-scaffolding law (Nick, 2026-10-07): scaffolding is not optional
|
||||
process, it is the record. A Go/Rust unit without a contract whose
|
||||
affected_surface covers it is ungoverned code — this gate fails the build.
|
||||
|
||||
Rules:
|
||||
* Every directory under src/go/ (and every crate under src/rust/) must
|
||||
appear in at least one contract's scope.
|
||||
* Contracts are read from store/scaffold/ (the durable trace).
|
||||
* EXEMPT: crates that pre-date the pipeline (the 10 original kernels) are
|
||||
mapped via the legacy bootstrap contract; test fixtures and _test.go are
|
||||
not units.
|
||||
|
||||
Exit 0 = all covered. Exit 1 = ungoverned units (listed).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCAFFOLD = ROOT / "store" / "scaffold"
|
||||
GO = ROOT / "src" / "go"
|
||||
RUST = ROOT / "src" / "rust"
|
||||
|
||||
# Units predating the absolute law, retro-adopted 2026-10-07 (contracts
|
||||
# BUILDER-2026-10-07T11:32-33Z-*). The map is the adoption record; new units
|
||||
# MUST get their contract BEFORE their first commit lands.
|
||||
RETRO_ADOPTED = {
|
||||
"adaptive-router", "memory-store-deep", "operator-deep", "curator-core",
|
||||
# Original kernels + infra from the pre-pipeline era (bootstrap contract):
|
||||
"event-bus", "message-queue", "subsystem-health", "parallel-processor",
|
||||
"connection-pool", "recall-router", "cron-engine", "cache", "night-mode",
|
||||
"work-manager", "failure-filer", "microkernel", "tool-executor",
|
||||
"rune-entry", "hook-engine", "model-connect", "operator", "operator-support",
|
||||
"memory-ops", "sleep-time", "core-server", "context-broker", "stage-guards",
|
||||
"tool-protocol", "tool-protocol-rw", "witness", "curator", "elastic-cognition",
|
||||
"persona-gate", "scaffold", "abstraction", "tui", "warm-router",
|
||||
"provider-registry", "local-tool-runtime", "memory-store-deep",
|
||||
"cognition",
|
||||
# Rust kernels (the 10 originals + W6 wave — bootstrap-era + parity ports)
|
||||
"route_risk_flags", "impact_preview", "programme_gates", "structural_migration",
|
||||
"rune_spinner", "warm_neuron_store", "graph_projection", "failure_recovery",
|
||||
"construction_depth", "intent_continuity", "wait_guard", "task_persist",
|
||||
"response_adapter", "salience_gate", "time_awareness", "recall_uplift",
|
||||
"verification_layer", "project_registry", "behaviour_ledger", "audit_ledger",
|
||||
"cognitive_budgeter", "progressive_escalation", "shard_manager", "task_gate",
|
||||
"drift_core", "self_improve", "token_optimizer", "dependency_tracker",
|
||||
"context_pressure", "inference_trace", "wait_guard_corpus", "rune_profiler",
|
||||
"construction_projection", "outcome_ledger", "construction_mode",
|
||||
"drift_resume", "toolchain", "construction_commands", "construction_activation",
|
||||
"subconscious_pass", "attention_capsule", "contracts", "capability_fit",
|
||||
"capability_ledger", "memory_selector", "graph_memory", "claim_audit",
|
||||
"build_basis", "prompt_sync", "voice_sync", "layout_contract",
|
||||
"system_registry", "voice_hybrid", "capability_broker", "structural_store",
|
||||
"spine", "behavior", "construction_intent", "construction_decision",
|
||||
"construction_pattern", "structural_model", "programme_continuation",
|
||||
"cognition_core", "memory_retrieval", "memory_storage", "scaffold_core",
|
||||
"evidence_provenance", "rune_spine", "memory_scoring", "capability_scoring",
|
||||
"token_counting", "query_memory",
|
||||
"drift_observability", "iteration", "spine_loader", "testing_mode",
|
||||
"construction_interface", "construction_patterns_builtin", "drift_cache",
|
||||
"drift_ledger", "scaffold-membrane",
|
||||
}
|
||||
|
||||
|
||||
def contract_scopes() -> set[str]:
|
||||
"""Every surface named in any persisted contract under store/scaffold/."""
|
||||
scopes: set[str] = set()
|
||||
if not SCAFFOLD.exists():
|
||||
return scopes
|
||||
for f in SCAFFOLD.rglob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
blobs = _iter_dicts(data)
|
||||
for d in blobs:
|
||||
for key in ("scope_files", "affected_surface", "surfaces", "scope"):
|
||||
v = d.get(key)
|
||||
if isinstance(v, list):
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
scopes.add(item)
|
||||
scopes.add(item.rstrip("/").split("/")[-1])
|
||||
elif isinstance(v, str):
|
||||
scopes.add(v)
|
||||
scopes.add(v.rstrip("/").split("/")[-1])
|
||||
return scopes
|
||||
|
||||
|
||||
def _iter_dicts(obj):
|
||||
if isinstance(obj, dict):
|
||||
yield obj
|
||||
for v in obj.values():
|
||||
yield from _iter_dicts(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
yield from _iter_dicts(v)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
scopes = contract_scopes()
|
||||
ungoverned = []
|
||||
|
||||
if GO.exists():
|
||||
for d in sorted(GO.iterdir()):
|
||||
if d.is_dir() and (d / "go.mod").exists():
|
||||
if d.name not in scopes and d.name not in RETRO_ADOPTED:
|
||||
ungoverned.append(f"src/go/{d.name}/")
|
||||
|
||||
if RUST.exists():
|
||||
for d in sorted(RUST.iterdir()):
|
||||
if (d / "Cargo.toml").exists():
|
||||
if d.name not in scopes and d.name not in RETRO_ADOPTED:
|
||||
ungoverned.append(f"src/rust/{d.name}/")
|
||||
|
||||
if ungoverned:
|
||||
print("UNGOVERNED UNITS — no construction contract covers them")
|
||||
print("(absolute-scaffolding law 2026-10-07: open a contract via")
|
||||
print(" construction_interface.submit_need BEFORE the next commit):")
|
||||
for u in ungoverned:
|
||||
print(f" {u}")
|
||||
return 1
|
||||
|
||||
print(f"scaffolding coverage: all Go/Rust units governed "
|
||||
f"({len(RETRO_ADOPTED)} retro-adopted/bootstrap, "
|
||||
f"{len(scopes)} contract scopes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -50,7 +50,7 @@ SRC_A = {
|
||||
"memory_store": "memory", "memory_selector": "memory",
|
||||
"memory_consolidation": "memory", "memory_env": "memory",
|
||||
"shard_manager": "memory", "tier_manager": "memory",
|
||||
"graph_memory": "memory", "query_memory": "memory",
|
||||
"graph_memory": "memory",
|
||||
"curator": "memory", "sleep_time": "memory",
|
||||
"subconscious_pass": "memory", "warm_neuron_store": "memory",
|
||||
"warm_router": "memory", "abstraction": "memory",
|
||||
@@ -166,14 +166,12 @@ 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",
|
||||
@@ -182,7 +180,8 @@ 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"}
|
||||
"graph_shard_bridge", "growth_simulator", "query_memory",
|
||||
"umbrella_update"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -47,7 +47,19 @@ def main() -> int:
|
||||
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"],
|
||||
# §16V AMBIENT-INTERPRETER CLOSURE (T7.3 slice-2 CI repair, 2026-10-03):
|
||||
# venv-signal stripping fixed ACTIVATION drift, but dependency
|
||||
# resolution still followed the AMBIENT host interpreter — runner
|
||||
# (py311, no rich/Pillow) vs desktop (3.14 system site with both)
|
||||
# produced 460 vs 454 on the identical tree, and any import-level
|
||||
# ignore flips to reportUnnecessaryTypeIgnoreComment in the other
|
||||
# environment. Pin --pythonpath to the INVOKING interpreter (the
|
||||
# venv running this script) so dependency resolution follows the
|
||||
# installed lock, not the host: CI's .type-venv has the locked deps;
|
||||
# local preflight must run the ratchet from a venv built from
|
||||
# requirements-ci.txt (same lock) — that is now the contract.
|
||||
cmd + [str(ROOT / "src" / "rune"), "--outputjson",
|
||||
"--pythonpath", sys.executable],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""audit_ledger.py — HMAC-chained tamper-evident promotion ledger.
|
||||
|
||||
Every promotion decision is recorded in a hash-chained ledger.
|
||||
Tampering with any entry breaks the chain.
|
||||
|
||||
Research basis:
|
||||
- PoEM (arXiv:2608.16032): HMAC-chained tamper-evident execution ledger
|
||||
- Autogenesis (arXiv:2604.15034): versioned interfaces, auditable lineage
|
||||
|
||||
Run under ~/.venvs/memory/bin/python3.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
LEDGER_FILE = os.path.expanduser("~/.config/opencode/memory/promotion_ledger.jsonl")
|
||||
SECRET_KEY = os.environ.get("AUDIT_LEDGER_KEY", "rune-drift-integrity-2024")
|
||||
|
||||
|
||||
def _compute_hash(prev_hash: str, entry: dict) -> str:
|
||||
"""Compute HMAC chain hash for a ledger entry."""
|
||||
payload = json.dumps(entry, sort_keys=True).encode()
|
||||
msg = (prev_hash + payload.decode()).encode()
|
||||
return hmac.new(SECRET_KEY.encode(), msg, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def append_entry(entry_type: str, entry_id: int, details: dict) -> dict:
|
||||
"""Append an entry to the promotion ledger.
|
||||
|
||||
Returns the ledger entry with chain hash.
|
||||
"""
|
||||
# Read last hash
|
||||
prev_hash = "genesis"
|
||||
if os.path.exists(LEDGER_FILE):
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
try:
|
||||
last = json.loads(lines[-1])
|
||||
prev_hash = last.get("hash", "genesis")
|
||||
except json.JSONDecodeError:
|
||||
prev_hash = "genesis"
|
||||
|
||||
entry = {
|
||||
"type": entry_type,
|
||||
"entry_id": entry_id,
|
||||
"details": details,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"prev_hash": prev_hash,
|
||||
}
|
||||
|
||||
# Compute chain hash
|
||||
entry["hash"] = _compute_hash(prev_hash, entry)
|
||||
|
||||
# Append to ledger
|
||||
os.makedirs(os.path.dirname(LEDGER_FILE), exist_ok=True)
|
||||
with open(LEDGER_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def verify_chain() -> dict:
|
||||
"""Verify the integrity of the entire ledger chain.
|
||||
|
||||
Returns dict with verification result.
|
||||
"""
|
||||
if not os.path.exists(LEDGER_FILE):
|
||||
return {"valid": True, "entries": 0, "reason": "empty ledger"}
|
||||
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
prev_hash = "genesis"
|
||||
for i, line in enumerate(lines):
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
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')}"
|
||||
}
|
||||
|
||||
# Verify HMAC
|
||||
stored_hash = entry.get("hash")
|
||||
computed = _compute_hash(prev_hash, {k: v for k, v in entry.items() if k != "hash"})
|
||||
if stored_hash != computed:
|
||||
return {
|
||||
"valid": False,
|
||||
"entries": i,
|
||||
"reason": f"HMAC mismatch at line {i+1}: stored={stored_hash[:16]}..., computed={computed[:16]}..."
|
||||
}
|
||||
|
||||
prev_hash = stored_hash
|
||||
|
||||
return {"valid": True, "entries": len(lines), "reason": "chain intact"}
|
||||
|
||||
|
||||
def get_entries(limit: int = 50) -> List[dict]:
|
||||
"""Get recent ledger entries."""
|
||||
if not os.path.exists(LEDGER_FILE):
|
||||
return []
|
||||
|
||||
with open(LEDGER_FILE, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
entries = []
|
||||
for line in lines[-limit:]:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Promotion audit ledger")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
# verify
|
||||
sub.add_parser("verify", help="Verify ledger chain integrity")
|
||||
|
||||
# list [--limit N]
|
||||
p_list = sub.add_parser("list", help="Show recent ledger entries")
|
||||
p_list.add_argument("--limit", type=int, default=20)
|
||||
|
||||
# log-promotion <entry_id> <old> <new> <wash_count> <reason>
|
||||
p_log = sub.add_parser("log-promotion", help="Log a promotion")
|
||||
p_log.add_argument("entry_id", type=int)
|
||||
p_log.add_argument("old_status")
|
||||
p_log.add_argument("new_status")
|
||||
p_log.add_argument("wash_count", type=int)
|
||||
p_log.add_argument("reason")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.cmd:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if args.cmd == "verify":
|
||||
result = verify_chain()
|
||||
print(f"Chain valid: {result['valid']}")
|
||||
print(f"Entries: {result['entries']}")
|
||||
print(f"Reason: {result['reason']}")
|
||||
|
||||
elif args.cmd == "list":
|
||||
entries = get_entries(args.limit)
|
||||
if entries:
|
||||
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':
|
||||
print(f" verdict={details.get('verdict')}")
|
||||
else:
|
||||
print("No ledger entries.")
|
||||
|
||||
elif args.cmd == "log-promotion":
|
||||
entry = log_promotion(args.entry_id, args.old_status, args.new_status, args.wash_count, args.reason)
|
||||
print(f"Logged: {entry['hash'][:16]}...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+88
-12
@@ -33,7 +33,7 @@ SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
|
||||
SHARED_DIR = os.path.expanduser("~/.config/opencode/shared")
|
||||
CUSTODIAN_STATE_FILE = os.path.expanduser("~/.config/opencode/memory/custodian_state.json")
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://gitea.thingscouldgetdicey.online")
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000") # gitea.internal (PC); external = https://gitea.thingscouldgetdicey.online via Pi Caddy
|
||||
GITEA_TOKEN = load_gitea_token()
|
||||
REPO = "conductor/rune"
|
||||
TUI_DIR = "/mnt/workdrive/rune"
|
||||
@@ -54,6 +54,15 @@ UMBRELLA_TAGS = {
|
||||
}
|
||||
|
||||
|
||||
# Issue template directory — canonical copies live beside this script
|
||||
# (mirrored to .gitea/ISSUE_TEMPLATE/ for the web-UI submission path).
|
||||
ISSUE_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "issue_templates")
|
||||
|
||||
# kind -> Gitea label that MUST be present on the issue
|
||||
KIND_LABEL = {"feature": "feature", "bug": "bug", "research": "research"}
|
||||
VERSION_INTENTS = ("major", "minor", "patch", "none")
|
||||
|
||||
|
||||
class Custodian:
|
||||
"""The Custodian agent for Gitea-based self-improvement."""
|
||||
|
||||
@@ -105,23 +114,90 @@ class Custodian:
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:200]}
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: List[str] = None) -> dict:
|
||||
"""Create a Gitea issue with plain-language description.
|
||||
def _ensure_label(self, name: str, description: str = "", color: str = "#6f42c1") -> bool:
|
||||
"""Make sure a label exists on the repo; return its id or None."""
|
||||
labels = self._gitea_api("labels")
|
||||
if isinstance(labels, list):
|
||||
for l in labels:
|
||||
if l["name"] == name:
|
||||
return l["id"]
|
||||
data = {"name": name, "description": description or name, "color": color}
|
||||
created = self._gitea_api("labels", method="POST", data=data)
|
||||
return created.get("id") if isinstance(created, dict) else None
|
||||
|
||||
Labels should be umbrella names (memory, governance, etc).
|
||||
They get converted to IDs automatically.
|
||||
def _load_issue_template(self, kind: str) -> str:
|
||||
"""Load the canonical issue template for a kind, WITHOUT the web-UI
|
||||
frontmatter (name/about/title/labels — that metadata belongs to the
|
||||
submission form, not the issue body). Empty string if absent."""
|
||||
path = os.path.join(ISSUE_TEMPLATE_DIR, f"{kind}.md")
|
||||
try:
|
||||
with open(path) as f:
|
||||
text = f.read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
text = text[end + 4:]
|
||||
return text.lstrip("\n")
|
||||
|
||||
def create_issue(
|
||||
self,
|
||||
title: str,
|
||||
body: str,
|
||||
labels: List[str] = None,
|
||||
kind: str = "feature",
|
||||
version_intent: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a Gitea issue assembled from the canonical template.
|
||||
|
||||
kind: feature | bug | research — selects the template and forces the
|
||||
matching label (the research label is auto-created if missing).
|
||||
version_intent: major | minor | patch | none — pre-ticks the matching
|
||||
box in the template's Version impact section. The pipeline
|
||||
verifies the real bump at merge and flags mismatches.
|
||||
labels: umbrella names (memory, governance, ...); converted to IDs.
|
||||
|
||||
The caller's body replaces the SUBMITTED_BODY marker inside the
|
||||
template's Plain English section — plain terms first, always.
|
||||
|
||||
ORGANISATION STANDARD (one open note per idea, 2026-09-19): before
|
||||
minting a standalone issue, check whether the work belongs inside an
|
||||
existing parent. Ideas parked under a blocked/future epic are filed
|
||||
as checklist items in the parent's "Absorbed children" section, NOT
|
||||
as standalone issues — the board stays uncluttered and every idea
|
||||
has exactly one home. Mint a standalone issue only when the work is
|
||||
live, dependency-tracked, or about to start. Absorbed ideas are
|
||||
reversible: re-mint standalone when they become live.
|
||||
"""
|
||||
# Convert label names to IDs
|
||||
# 1. assemble body from template (graceful: unformatted if template missing)
|
||||
template = self._load_issue_template(kind)
|
||||
text = body.strip()
|
||||
if template and "<!-- SUBMITTED_BODY -->" in template:
|
||||
text = template.replace("<!-- SUBMITTED_BODY -->", text, 1)
|
||||
if version_intent in VERSION_INTENTS:
|
||||
text = text.replace(f"- [ ] {version_intent} ", f"- [x] {version_intent} ", 1)
|
||||
|
||||
# 2. labels: umbrella names + mandatory kind label
|
||||
labels = list(labels or [])
|
||||
kind_label = KIND_LABEL.get(kind)
|
||||
if kind_label and kind_label not in labels:
|
||||
labels.append(kind_label)
|
||||
|
||||
all_labels = self._gitea_api("labels")
|
||||
label_map = {l["name"]: l["id"] for l in all_labels} if isinstance(all_labels, list) else {}
|
||||
label_ids = []
|
||||
if labels:
|
||||
all_labels = self._gitea_api("labels")
|
||||
if isinstance(all_labels, list):
|
||||
label_map = {l["name"]: l["id"] for l in all_labels}
|
||||
label_ids = [label_map[l] for l in labels if l in label_map]
|
||||
for l in labels:
|
||||
if l in label_map:
|
||||
label_ids.append(label_map[l])
|
||||
else:
|
||||
lid = self._ensure_label(l)
|
||||
if lid:
|
||||
label_ids.append(lid)
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"body": text,
|
||||
"labels": label_ids,
|
||||
}
|
||||
result = self._gitea_api("issues", method="POST", data=data)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
|
||||
**WHAT (bug):**
|
||||
**WHERE:**
|
||||
**EVIDENCE:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every fix must clear these before the issue closes. -->
|
||||
- [ ] TDD: regression test written first, red → green
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
WHY — design-spec reference (S#) or reason.
|
||||
FILES — affected components.
|
||||
ACCEPTANCE — frozen-test file + test count + canary name.
|
||||
DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
|
||||
**WHAT:**
|
||||
**WHY:**
|
||||
**FILES:**
|
||||
**ACCEPTANCE:**
|
||||
**DEPENDENCIES:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every stage must clear these before the issue closes. -->
|
||||
- [ ] TDD: frozen tests written first, red → green (N passed)
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
<restate the issue title here in everyday words — this heading IS the
|
||||
plain-English summary; no meta-labels like "plain English" in it>
|
||||
<!-- The heading above carries the summary itself — a scan of issue
|
||||
headings alone should tell the whole story of the board. TOP PRIORITY:
|
||||
if a technical detail below contradicts this section, the issue is
|
||||
buggy — fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
Loaded 100 of 823 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user