Compare commits
935
Commits
v0.4.0
...
4e53e143bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e53e143bb | ||
|
|
166a3807f8 | ||
|
|
1c73b82812 | ||
|
|
02b9986807 | ||
|
|
6804f198bc | ||
|
|
f59421844a | ||
|
|
3dc84f5aef | ||
|
|
23fe2c1231 | ||
|
|
5495813baf | ||
|
|
717a48bef2 | ||
|
|
73cf4a625b | ||
|
|
4087f3b3d0 | ||
|
|
0defc61bec | ||
|
|
7155b1b4fb | ||
|
|
42380d4b82 | ||
|
|
e62502c862 | ||
|
|
fc3bb259cb | ||
|
|
0c6b7c8594 | ||
|
|
8710737964 | ||
|
|
363104fe77 | ||
|
|
8f775dc2d9 | ||
|
|
2731e98ed1 | ||
|
|
0c2a68ecc4 | ||
|
|
bd7cad21e2 | ||
|
|
0c4d0d777c | ||
|
|
b020e1b75e | ||
|
|
9693cdd4ac | ||
|
|
a0cc5f291e | ||
|
|
ce20927713 | ||
|
|
9e7c957a3d | ||
|
|
3b66101361 | ||
|
|
0dc9dd91c3 | ||
|
|
9825ccd31e | ||
|
|
688ababecc | ||
|
|
d4605794e8 | ||
|
|
3a2736b667 | ||
|
|
003d036e31 | ||
|
|
e4dfbe906c | ||
|
|
57c1a86195 | ||
|
|
8be9c8e152 | ||
|
|
fa7f96e9e8 | ||
|
|
5046aebca3 | ||
|
|
1d6823ca0c | ||
|
|
9fb90be851 | ||
|
|
4cc5563ba8 | ||
|
|
fcc0049ab2 | ||
|
|
dca9b1553e | ||
|
|
ea31236d29 | ||
|
|
d16db663df | ||
|
|
94d48fb76e | ||
|
|
6c50c60c22 | ||
|
|
f31885ba07 | ||
|
|
fd26f43b84 | ||
|
|
e240be8bda | ||
|
|
9e2f3c0f12 | ||
|
|
8746e3e29a | ||
|
|
5b9852d80e | ||
|
|
99d4b20ac3 | ||
|
|
17864c8864 | ||
|
|
2a04c4f5e1 | ||
|
|
ca58176ef9 | ||
|
|
cfb83a629e | ||
|
|
8377f8287b | ||
|
|
ffb75cb168 | ||
|
|
58b0dbd812 | ||
|
|
bd5c0a7cdd | ||
|
|
d84dea21b6 | ||
|
|
755b25f9b2 | ||
|
|
23f941b09f | ||
|
|
77da48a40f | ||
|
|
5d59adadbc | ||
|
|
9a6ffcb8ca | ||
|
|
62e6fafaef | ||
|
|
beca133082 | ||
|
|
c7ec4ed124 | ||
|
|
cee5cf0f36 | ||
|
|
85cb7b0fbf | ||
|
|
d89592fee6 | ||
|
|
342f39f30e | ||
|
|
4f0c7e4431 | ||
|
|
fcf67b07a7 | ||
|
|
78e326039d | ||
|
|
f18c436e2c | ||
|
|
786e5542d4 | ||
|
|
18f5ada585 | ||
|
|
b90e04f227 | ||
|
|
3339920377 | ||
|
|
9a505f951a | ||
|
|
b87367bb1b | ||
|
|
ad3a5c6d49 | ||
|
|
68e893d684 | ||
|
|
156f3193a9 | ||
|
|
4555af8299 | ||
|
|
ea76331c49 | ||
|
|
84b5a72eac | ||
|
|
27101909e0 | ||
|
|
300c7aeff0 | ||
|
|
00c3b60c2f | ||
|
|
22421a4e74 | ||
|
|
fa5ee5f4fc | ||
|
|
ba3a904baf | ||
|
|
8e42a59b98 | ||
|
|
081fa2c9d4 | ||
|
|
48753eaa46 | ||
|
|
62ecb5b415 | ||
|
|
7ada3fb47c | ||
|
|
1cf42fffa2 | ||
|
|
66b30bb06a | ||
|
|
3f49db86b2 | ||
|
|
7dac4620a8 | ||
|
|
5b4462135f | ||
|
|
fdae98adff | ||
|
|
f230c28bf4 | ||
|
|
13b8eb87a9 | ||
|
|
0ce2654173 | ||
|
|
c98b6bc9cd | ||
|
|
f2d90ed7a6 | ||
|
|
22976a79f9 | ||
|
|
4263d684e9 | ||
|
|
a8496aa50e | ||
|
|
4e72c6f5e6 | ||
|
|
ebb9e9af62 | ||
|
|
80c20afcdd | ||
|
|
8407729d5a | ||
|
|
1e46743420 | ||
|
|
d50059a56f | ||
|
|
3dcb9541b1 | ||
|
|
7dc950a092 | ||
|
|
80ace77e44 | ||
|
|
18875a4cc9 | ||
|
|
75cc2a4b9c | ||
|
|
be0c5462ed | ||
|
|
1c68331738 | ||
|
|
677bce18b8 | ||
|
|
32625cdc76 | ||
|
|
50d1e78f6d | ||
|
|
2d43745b57 | ||
|
|
b8466e4284 | ||
|
|
d5d6dd9df4 | ||
|
|
fca086ba09 | ||
|
|
3254fe9f3c | ||
|
|
a7528d6e1a | ||
|
|
442953ef7d | ||
|
|
1ec7916816 | ||
|
|
eb7d1421da | ||
|
|
f97a83f165 | ||
|
|
3599b0a861 | ||
|
|
2e7cd6581e | ||
|
|
406a850bdb | ||
|
|
99e592565a | ||
|
|
838335f84d | ||
|
|
b50ec42868 | ||
|
|
e744da8870 | ||
|
|
eb78a0e32b | ||
|
|
e8237bd924 | ||
|
|
2f85462e2a | ||
|
|
3e91813cff | ||
|
|
99fb6e24f1 | ||
|
|
24b5504e21 | ||
|
|
02e2227aa0 | ||
|
|
778aa6449c | ||
|
|
a9f89f0414 | ||
|
|
f072aa1b2c | ||
|
|
32f4adb71f | ||
|
|
b4c01e0f28 | ||
|
|
1a5824ce4f | ||
|
|
324dd3397a | ||
|
|
727cab11ff | ||
|
|
ccec38c5b9 | ||
|
|
cce8381962 | ||
|
|
1ea755e864 | ||
|
|
6aad82c6fd | ||
|
|
aa1287b902 | ||
|
|
d7a16363b8 | ||
|
|
36601d8668 | ||
|
|
8e9a1f1ee8 | ||
|
|
8f9284cb92 | ||
|
|
69a3d928ee | ||
|
|
4c5aa8d895 | ||
|
|
3aa35e1259 | ||
|
|
4dee1a6849 | ||
|
|
f342dd7dd7 | ||
|
|
cd0318f384 | ||
|
|
d630c7e30b | ||
|
|
b666b7e494 | ||
|
|
e26543f21a | ||
|
|
10b8fe5cae | ||
|
|
3393123baf | ||
|
|
0bef0f2c39 | ||
|
|
874ef7f6ec | ||
|
|
e746ca1f87 | ||
|
|
dc5e3201da | ||
|
|
cdbff88b4f | ||
|
|
62122fc698 | ||
|
|
31c33b5a23 | ||
|
|
2f839b332d | ||
|
|
b65aa76d2d | ||
|
|
d8f1778bc7 | ||
|
|
c10965cd71 | ||
|
|
b4a73aae7f | ||
|
|
846bc39263 | ||
|
|
824edf587b | ||
|
|
0083cf0c42 | ||
|
|
f1cf9a6ae7 | ||
|
|
2049316364 | ||
|
|
a7744e7f89 | ||
|
|
24088a48a1 | ||
|
|
1df82b50b6 | ||
|
|
452d9e1b60 | ||
|
|
0f0f5448ef | ||
|
|
6e1683393f | ||
|
|
d71cf75209 | ||
|
|
ddce9e330e | ||
|
|
2928125b0e | ||
|
|
71c3283b87 | ||
|
|
47edaa35ff | ||
|
|
7e84804ed1 | ||
|
|
7f5b75dbdf | ||
|
|
3865750f5a | ||
|
|
dbec2f77d1 | ||
|
|
6a4352d8b4 | ||
|
|
3e8ecdbeab | ||
|
|
13ebe04472 | ||
|
|
695153f88e | ||
|
|
6104a48e56 | ||
|
|
a5984eca80 | ||
|
|
4651658b34 | ||
|
|
fc8114433b | ||
|
|
7c09593c84 | ||
|
|
e0845ff8e9 | ||
|
|
ed41844d6f | ||
|
|
34f60ead6c | ||
|
|
106813263a | ||
|
|
ff6289c89a | ||
|
|
cec3453da8 | ||
|
|
66ced35943 | ||
|
|
d6dc6c96d6 | ||
|
|
cb0c3a9653 | ||
|
|
a0dc4550b1 | ||
|
|
f66036e9d8 | ||
|
|
61c3350ad3 | ||
|
|
9f389ba82b | ||
|
|
2ee4e58933 | ||
|
|
eaab57375e | ||
|
|
5fe3198df7
|
||
|
|
206f36d826 | ||
|
|
cb5b46800f | ||
|
|
101b706be5 | ||
|
|
b04270950a | ||
|
|
9a5f587e74 | ||
|
|
4e35866740 | ||
|
|
74b8c17392 | ||
|
|
67cf8e42d9 | ||
|
|
154330c5a0 | ||
|
|
b50b033520 | ||
|
|
3b01ae8f18 | ||
|
|
dc41e9d5df | ||
|
|
49bd5d86b4 | ||
|
|
c8af77b36f | ||
|
|
41d5fa9800 | ||
|
|
5e7a1f1de6 | ||
|
|
73e9bad412 | ||
|
|
012212a998 | ||
|
|
f1cb6589bc | ||
|
|
539b57aded | ||
|
|
2d7a759fc0 | ||
|
|
078e5c757d | ||
|
|
47d2edf303 | ||
|
|
34d79651a8 | ||
|
|
01d0081321 | ||
|
|
e137866c0e | ||
|
|
b19ac80783 | ||
|
|
f961dd3a74 | ||
|
|
af7d37890c | ||
|
|
2cf01a4ca4 | ||
|
|
141289d973 | ||
|
|
46c8561173 | ||
|
|
61c4037033 | ||
|
|
8095852c7d | ||
|
|
dde339780f | ||
|
|
c0a17031c5 | ||
|
|
affa5e7cef | ||
|
|
7b37eb4602 | ||
|
|
40996a06fe | ||
|
|
00a21ee777 | ||
|
|
016f2654e4 | ||
|
|
9a1d00f5e6 | ||
|
|
a14c768f9a | ||
|
|
c102da5665 | ||
|
|
4402d254f6 | ||
|
|
02974c8abc | ||
|
|
9f7eeb7dc6 | ||
|
|
f3d44212d5 | ||
|
|
f0661b4ea4 | ||
|
|
0946e3ec64 | ||
|
|
65e6249adb | ||
|
|
18c2cfeb41 | ||
|
|
50d2a74e80 | ||
|
|
ea001ade7a | ||
|
|
baa45970da | ||
|
|
5039b74926 | ||
|
|
176a81412a | ||
|
|
cebc32f70a | ||
|
|
d3914955e2 | ||
|
|
a8d730cf01 | ||
|
|
cf638305e6 | ||
|
|
3601faa969 | ||
|
|
e4c5f42a25 | ||
|
|
08dddb09fb | ||
|
|
f323c919f5 | ||
|
|
544a6bbbbc | ||
|
|
772c19f2bd | ||
|
|
1e15bc31d5 | ||
|
|
c170d08654 | ||
|
|
c707aaf3ee | ||
|
|
2d9829106d | ||
|
|
3d24719a71 | ||
|
|
290f6b257f | ||
|
|
e7811a6b87 | ||
|
|
94ede264e5
|
||
|
|
92c8ff8934 | ||
|
|
af697d129b | ||
|
|
895ecc812f | ||
|
|
c0224e20cb | ||
|
|
92d9464b38 | ||
|
|
a92ebb5b94 | ||
|
|
93e90106a8 | ||
|
|
35e4c82f27 | ||
|
|
5711432ac4 | ||
|
|
ee45e42013 | ||
|
|
3d640b9efa | ||
|
|
730f942bab | ||
|
|
2efeda0ba7 | ||
|
|
df62c32822 | ||
|
|
3762bbe09f | ||
|
|
ebe7816600 | ||
|
|
3ebe11a4f0 | ||
|
|
429ae3cc8e | ||
|
|
304b8f635f | ||
|
|
198c9f42df | ||
|
|
404a45a311 | ||
|
|
bfb8105682 | ||
|
|
1650196cae | ||
|
|
fa04e09373 | ||
|
|
074083af31 | ||
|
|
c376737bbd | ||
|
|
4e50b4ff4a | ||
|
|
5a9f8c42b9 | ||
|
|
2eb63cfc0d | ||
|
|
28018f33c9 | ||
|
|
6256b5fcfa | ||
|
|
24ed674952 | ||
|
|
caabf41b65 | ||
|
|
2d874f1d7c | ||
|
|
6a694d10db | ||
|
|
177d61cf94 | ||
|
|
4025b8dacd | ||
|
|
cb025b7fff | ||
|
|
fcc3756634 | ||
|
|
257b06bbd4 | ||
|
|
7cf88c030f | ||
|
|
a9df9a4dd5 | ||
|
|
7f3f95d89d | ||
|
|
bfc3b7bfea | ||
|
|
240eaa081a | ||
|
|
c7384b7a9b | ||
|
|
ed9778c07b | ||
|
|
5a32219178 | ||
|
|
9e652f7801 | ||
|
|
fcff426ae5 | ||
|
|
cb0802c2d0 | ||
|
|
d7524b8de7 | ||
|
|
aa14e66c35 | ||
|
|
3c9f443bce | ||
|
|
e55120dac6 | ||
|
|
b67f1ef854 | ||
|
|
b38562a961 | ||
|
|
40846de37a | ||
|
|
a5a3eed6d8 | ||
|
|
ad9ff3bea8 | ||
|
|
5177d95ee0 | ||
|
|
9bc37e226b | ||
|
|
a4b55d9e42 | ||
|
|
c8b00b20bc | ||
|
|
eb37f8bb46 | ||
|
|
6fade71e8c | ||
|
|
61a3cfb662 | ||
|
|
67819784b7 | ||
|
|
437512fd6d | ||
|
|
ef88b6a2c8 | ||
|
|
d68f4ecaeb | ||
|
|
01ada1da18 | ||
|
|
7caa24d090 | ||
|
|
0e941fb360 | ||
|
|
b972c12559 | ||
|
|
f8cab9b439 | ||
|
|
45333db5c2 | ||
|
|
7748b953f0 | ||
|
|
720591d24a | ||
|
|
dbb0c51b7e | ||
|
|
71cd50fe4d | ||
|
|
b5863dded0 | ||
|
|
03116d2f42 | ||
|
|
9b23247815 | ||
|
|
3493b01e9a | ||
|
|
fd989c44d0 | ||
|
|
e7307da6a9 | ||
|
|
b6721d6a15 | ||
|
|
9e72a52b1c | ||
|
|
30c1637dff | ||
|
|
6b5535956d | ||
|
|
fdfe4ba023 | ||
|
|
8f02bf1c33 | ||
|
|
80b082423c | ||
|
|
4324d551d6 | ||
|
|
1136b1385b | ||
|
|
84b90bfe26 | ||
|
|
53ccbda97c | ||
|
|
2d1bf372d8 | ||
|
|
4987d850f9 | ||
|
|
9541a094d8 | ||
|
|
89df8ec1ca | ||
|
|
bca85a4017 | ||
|
|
0e5d85f2ff | ||
|
|
0a806da8d2 | ||
|
|
2790a823b0 | ||
|
|
88acf2362f | ||
|
|
bfcc762ec9 | ||
|
|
b21699b749 | ||
|
|
5f23e2403f | ||
|
|
2af6fe64d4 | ||
|
|
da640f3dcd | ||
|
|
79ec2d87c7 | ||
|
|
faf9dd581f | ||
|
|
873deef7c7 | ||
|
|
f518c8a6fc | ||
|
|
aea5f3d615 | ||
|
|
bda37d9f38 | ||
|
|
96e5390621 | ||
|
|
b43acac8ee | ||
|
|
03687c8981 | ||
|
|
95dd31e24b | ||
|
|
e1b5562888 | ||
|
|
fba040c668 | ||
|
|
f61a8f7afd | ||
|
|
91328ca7e1 | ||
|
|
125360033d | ||
|
|
4323d4823c | ||
|
|
5478c5a239 | ||
|
|
e94bd450cd | ||
|
|
e8ddb61518 | ||
|
|
f39381aa9d | ||
|
|
e8b55bba15 | ||
|
|
9863c7a5f3 | ||
|
|
f44722df04
|
||
|
|
3eaae0e652 | ||
|
|
b12829db39 | ||
|
|
dffaf6b9db | ||
|
|
a9a4ccca88 | ||
|
|
7cb7d66575 | ||
|
|
400b50fbd0 | ||
|
|
7eeff2a226 | ||
|
|
374e8c0bf7
|
||
|
|
ede87df960
|
||
|
|
95a8c3df44
|
||
|
|
f57bd21ee4 | ||
|
|
e1b6e3f8c6 | ||
|
|
644d899f78 | ||
|
|
2596194417 | ||
|
|
684f19250a | ||
|
|
f3d59ade11 | ||
|
|
e1604c58ea | ||
|
|
dcacb3a962 | ||
|
|
d791098e51 | ||
|
|
d31110cd67
|
||
|
|
0f35e03a85
|
||
|
|
c2b0c120d7
|
||
|
|
65c9be36b2
|
||
|
|
2658ca776e
|
||
|
|
f8682102a0
|
||
|
|
3fa0f5c428
|
||
|
|
68135b97d1
|
||
|
|
b87a3460c4 | ||
|
|
cb23da6490
|
||
|
|
2a40a5a81d | ||
|
|
ebba976a27 | ||
|
|
c84f9522e9 | ||
|
|
81ed769f8a | ||
|
|
6f7defe25f | ||
|
|
b837f82d7e | ||
|
|
54685be9a2 | ||
|
|
4dd6e794b2 | ||
|
|
af9622d31c | ||
|
|
6f586bd535 | ||
|
|
78740db170 | ||
|
|
64d594f4ee | ||
|
|
1322d73c7b | ||
|
|
de91ffa517 | ||
|
|
74bc613d94 | ||
|
|
6d0a5550fe | ||
|
|
7b1c0342b4 | ||
|
|
d6c114fe58
|
||
|
|
912e00a627
|
||
|
|
e006e29ff1
|
||
|
|
c0067d387c
|
||
|
|
118c346345
|
||
|
|
dc677a2529
|
||
|
|
5e2b9c98ad
|
||
|
|
c458ca93a9
|
||
|
|
860566bf50
|
||
|
|
7f90710427
|
||
|
|
93a934439b
|
||
|
|
ecda258d3a
|
||
|
|
3e598065f8
|
||
|
|
3abc30d633
|
||
|
|
f68937611e
|
||
|
|
a12cf84eb6 | ||
|
|
2b45e3a9b8
|
||
|
|
91dbaf5533
|
||
|
|
7a732436aa
|
||
|
|
98d3ba4a83
|
||
|
|
d734276927
|
||
|
|
5049143fcc
|
||
|
|
a968c3228d
|
||
|
|
f404acdbca | ||
|
|
efa570267d
|
||
|
|
9aeb9e6e2e | ||
|
|
3607a180d9
|
||
|
|
d429def0f6
|
||
|
|
e606eb7c49
|
||
|
|
a8fb32b6bd
|
||
|
|
1f7b8417fa
|
||
|
|
9540345ec7
|
||
|
|
70b6d51b55
|
||
|
|
9e5e8a60f2
|
||
|
|
d6447603bc
|
||
|
|
e6dc24beb5
|
||
|
|
e76c3efe4e
|
||
|
|
fac577589b | ||
|
|
737fc42ec1 | ||
|
|
888529f381
|
||
|
|
35e75e5b4f
|
||
|
|
dd7d75fd9f | ||
|
|
78f5fc8fb9 | ||
|
|
4f38214681
|
||
|
|
019bd6f1c7 | ||
|
|
875b6749c2 | ||
|
|
69e1b98c44
|
||
|
|
0324436114
|
||
|
|
233c212d2a
|
||
|
|
e288b41365
|
||
|
|
fbf6a6bdf4
|
||
|
|
57b72702b2
|
||
|
|
38ba303c3c
|
||
|
|
fc7bc0ff8f
|
||
|
|
7f7ea758a7
|
||
|
|
06b2c384e3
|
||
|
|
d50de7c06a
|
||
|
|
0956f08791
|
||
|
|
087d0c320c
|
||
|
|
2128390f99
|
||
|
|
d008de1848
|
||
|
|
d79787bf96
|
||
|
|
577c51b62f
|
||
|
|
d462b09f80
|
||
|
|
b711e4983b
|
||
|
|
6ae3efb06c
|
||
|
|
02dd14394b
|
||
|
|
f11d4ca760
|
||
|
|
f1415067f2
|
||
|
|
af5c34fde5
|
||
|
|
2ffa278f2d | ||
|
|
f4cbee9611
|
||
|
|
e95fd1e06e
|
||
|
|
d79ea55e09
|
||
|
|
b898e4dd78 | ||
|
|
30e00f8332 | ||
|
|
58d9d4c64e
|
||
|
|
63a768aa46
|
||
|
|
2c3f671efa
|
||
|
|
8b0a536f4e
|
||
|
|
5a1bb569b4
|
||
|
|
0c12580836
|
||
|
|
df909325a7
|
||
|
|
d379fcddf8
|
||
|
|
b405acd8f0
|
||
|
|
805ae7112a
|
||
|
|
6ddbf37523
|
||
|
|
f510bb649b
|
||
|
|
c1b14bdfdf
|
||
|
|
46f2a9eae2
|
||
|
|
cf3a12141b
|
||
|
|
8f13810f0f
|
||
|
|
eba8c86e21
|
||
|
|
d75fb47de1
|
||
|
|
49a281bf93
|
||
|
|
b9474ce6ef
|
||
|
|
d328310880
|
||
|
|
5eb77ab467
|
||
|
|
a2c4f05c8c
|
||
|
|
89fadcca15
|
||
|
|
ab3a818507
|
||
|
|
63f73f22c3
|
||
|
|
54c5079cb7
|
||
|
|
d51bdd3086
|
||
|
|
56ec58a748
|
||
|
|
93f9c5425e
|
||
|
|
77dfd08103 | ||
|
|
46dcef0dec | ||
|
|
72c6bb74c2 | ||
|
|
6bf80dcce9
|
||
|
|
df948c69bf
|
||
|
|
e5d0fcc764
|
||
|
|
c60d3e7cda
|
||
|
|
c21fd47b42
|
||
|
|
ab0b89dd67
|
||
|
|
420db4bb88
|
||
|
|
dfacf31f6a
|
||
|
|
8cfd5ee2c4
|
||
|
|
e82e5ab8e4
|
||
|
|
d790782ace
|
||
|
|
bf06d5e8f3
|
||
|
|
cdfaa0f111
|
||
|
|
c062f34852
|
||
|
|
7671d28d6e
|
||
|
|
fcc4a1d2b5
|
||
|
|
b0eeba110d
|
||
|
|
d65d63ee50
|
||
|
|
9890cf0ddc
|
||
|
|
89db5b3887
|
||
|
|
f40ba4ccbe
|
||
|
|
d2940a8d32
|
||
|
|
ed7ad36475
|
||
|
|
393ed16963
|
||
|
|
3f94d2003a
|
||
|
|
50ff9008fe
|
||
|
|
9606d7f8aa
|
||
|
|
d8ae9e25d9
|
||
|
|
006f64bfa0
|
||
|
|
000559bc9d
|
||
|
|
3aede58a11
|
||
|
|
cab1e72b97
|
||
|
|
cd4bf245e9
|
||
|
|
82bf6176f8
|
||
|
|
d407eb5a6a
|
||
|
|
6f2594712f
|
||
|
|
79d43c8791
|
||
|
|
5a5da90734
|
||
|
|
f2a0e7453e
|
||
|
|
0fe430102a
|
||
|
|
d13bd32fdf
|
||
|
|
1f1729ba00
|
||
|
|
107419966d
|
||
|
|
344ef7526f
|
||
|
|
13d31f850c
|
||
|
|
f6bd02dc73
|
||
|
|
31df1a720d
|
||
|
|
4e0e65fc8a
|
||
|
|
ab85a4f534
|
||
|
|
420447275c
|
||
|
|
cdc40f7302
|
||
|
|
c611685033
|
||
|
|
cac2a3eba0
|
||
|
|
66bbb34d7f
|
||
|
|
68177fdb6a
|
||
|
|
1acaad223f
|
||
|
|
aa0270602d
|
||
|
|
4669958bdd
|
||
|
|
559107073d
|
||
|
|
d0a38747e0
|
||
|
|
677bd71b93 | ||
|
|
ad6d0a2e0e
|
||
|
|
50911b99ef
|
||
|
|
44783c5573
|
||
|
|
8629c1ca15
|
||
|
|
078e6e3744
|
||
|
|
b908fc20ba
|
||
|
|
058810137c
|
||
|
|
c979041161
|
||
|
|
a606ea552d
|
||
|
|
39a654a79e
|
||
|
|
17d1decce6
|
||
|
|
a45e66c634
|
||
|
|
8c885d9a77
|
||
|
|
6dd1e59815
|
||
|
|
f5085a773a
|
||
|
|
09afdeaf7c
|
||
|
|
0216d84eee
|
||
|
|
320dbf2479
|
||
|
|
6958e9cba8
|
||
|
|
825f9f6bf5
|
||
|
|
863740f916
|
||
|
|
304088bf5c
|
||
|
|
b7599b8acf
|
||
|
|
9b3ae761f3
|
||
|
|
0f7877aafc
|
||
|
|
5843a9ac15
|
||
|
|
4bfaabcb99
|
||
|
|
f16f858074
|
||
|
|
e9a8c01dc4
|
||
|
|
5bbf1b2d71 | ||
|
|
e9c52566b8
|
||
|
|
4c7de650c0
|
||
|
|
6127d964ee
|
||
|
|
8bbbd71fec
|
||
|
|
7f89a80f7e
|
||
|
|
19cca06db6
|
||
|
|
e8df9f119c
|
||
|
|
8abe297bfe
|
||
|
|
4ec6daff30
|
||
|
|
9c1067e544
|
||
|
|
2fe6704fbc | ||
|
|
dd40892ad5 | ||
|
|
ed86b7bfc3 | ||
|
|
f32d72a3f2 | ||
|
|
7b00638476 | ||
|
|
6733b3600f
|
||
|
|
de6010d525
|
||
|
|
9b0e26bade
|
||
|
|
ac40043c00
|
||
|
|
d8eec1d427
|
||
|
|
382916c3ee
|
||
|
|
bc3cc10a7b
|
||
|
|
b91f738209
|
||
|
|
4f0dae9b49
|
||
|
|
deb673ebc9
|
||
|
|
7fc06ad9bc
|
||
|
|
e814b9f62d
|
||
|
|
209fbc9e41
|
||
|
|
5eb1daf18d
|
||
|
|
b8990fdfc2
|
||
|
|
7673799d83
|
||
|
|
ce212fe660
|
||
|
|
f855a45493
|
||
|
|
5be4f45671
|
||
|
|
84ce094677
|
||
|
|
fa7eadd08a
|
||
|
|
af91b89cff
|
||
|
|
d4a6a2fb34
|
||
|
|
8f667886c8
|
||
|
|
898bac3c69
|
||
|
|
fc0b2ada7e
|
||
|
|
09cdb40420
|
||
|
|
9d2e936e7f
|
||
|
|
159afbbc06
|
||
|
|
428d544277
|
||
|
|
531bdfab7f
|
||
|
|
08f6ea5e6c
|
||
|
|
ede0f75a89
|
||
|
|
2ec2aec4c0
|
||
|
|
c2cb4ac433
|
||
|
|
605a9170b0
|
||
|
|
385bd3eda2
|
||
|
|
6c3d96ac83
|
||
|
|
aa1fe7f7aa
|
||
|
|
5e50828108
|
||
|
|
693e2d9672
|
||
|
|
16f324cefc
|
||
|
|
cc50d39ab4
|
||
|
|
fc23b532d9
|
||
|
|
c2d4240138
|
||
|
|
cd1b043b1e
|
||
|
|
81b4f6a76e
|
||
|
|
d48b11dcfa | ||
|
|
86dd922d2c | ||
|
|
9ec20d74a4
|
||
|
|
c78cdef5ae
|
||
|
|
3df590f276
|
||
|
|
91300c16fe
|
||
|
|
52356ead6c
|
||
|
|
ad9fc524d4
|
||
|
|
af50909a89
|
||
|
|
318d9ba1cd
|
||
|
|
45d709f28e
|
||
|
|
9cd074cb9b
|
||
|
|
93eec45473
|
||
|
|
e585e0b049
|
||
|
|
13bfaf9aca
|
||
|
|
040dad05d2 | ||
|
|
1ba38860f2 | ||
|
|
84ec5fe7b8
|
||
|
|
1684788fe6 | ||
|
|
4b7e242998
|
||
|
|
f69aba2dd8 | ||
|
|
c3487ecd0e | ||
|
|
db75391fb6
|
||
|
|
e3815af69b
|
||
|
|
66a485f924 | ||
|
|
49d7204f89 | ||
|
|
bbcae1fc2b
|
||
|
|
3ff27a7935
|
||
|
|
373d80121a
|
||
|
|
3299a4699e
|
||
|
|
d4dbda1e89
|
||
|
|
e77fa6ef42
|
||
|
|
241dda24f0
|
||
|
|
e5668e4495
|
||
|
|
4a01e9a66c
|
||
|
|
530000bc2f
|
||
|
|
f2e8f3ab59
|
||
|
|
2f33b6631e
|
||
|
|
8c288195a0
|
||
|
|
e6a5e67a8e
|
||
|
|
6ae474c79e
|
||
|
|
8e0b07c9fb
|
||
|
|
69589bd5e5
|
||
|
|
587df087ed
|
||
|
|
ee100eef96
|
||
|
|
14969e35fa
|
||
|
|
b927e2a200
|
||
|
|
6ce69ee989
|
||
|
|
dc6d736df3
|
||
|
|
2a79616f8b
|
||
|
|
eb6a02f947
|
||
|
|
00939e4634
|
||
|
|
6ebd32d47c
|
||
|
|
73c4449e7f
|
||
|
|
7143b50d98
|
||
|
|
de38e663a0
|
||
|
|
10de6025b5
|
||
|
|
0d2292bff6
|
||
|
|
eb38ca0bbb
|
||
|
|
1931331163
|
||
|
|
218750cc1e
|
||
|
|
a10b23dbc1
|
||
|
|
19d2340489
|
||
|
|
4ece3d3df1
|
||
|
|
6d5cbfa56d
|
||
|
|
7e097e0465
|
||
|
|
b2d70a3fd3
|
||
|
|
3183fedca9
|
||
|
|
33c6f2c4e3
|
||
|
|
bca25404ab | ||
|
|
161fa2d983 | ||
|
|
0e93775491 | ||
|
|
c00c4ff84a | ||
|
|
46685cb641 | ||
|
|
165d0d113d | ||
|
|
70dc7c9680 | ||
|
|
4eac536327 | ||
|
|
8e0fa79ff3 | ||
|
|
68a912ec38 | ||
|
|
f405ec5e16 | ||
|
|
b997e9493c | ||
|
|
8d6e9bef32 | ||
|
|
e54a2e42c9 | ||
|
|
b1696c3425 | ||
|
|
feef3f67b5 | ||
|
|
dc066bee0d | ||
|
|
6c4e042dad | ||
|
|
30f3b01358 | ||
|
|
ebf3b5f776 | ||
|
|
84dcb3078b | ||
|
|
7b320e08c4 | ||
|
|
7078280b3d | ||
|
|
43607dbe8d | ||
|
|
8f7a57f8e6 | ||
|
|
40fdf3aaa7 | ||
|
|
46d4b78ccc | ||
|
|
b0a3b0a9a5 | ||
|
|
53b3ce9ab1 | ||
|
|
44f533018e | ||
|
|
bbb23f4884 | ||
|
|
8de0eef4f9 | ||
|
|
73a4499c68 | ||
|
|
97100bee29 | ||
|
|
9a25438643 | ||
|
|
f6da937c5d | ||
|
|
eeaeb42c9a | ||
|
|
1dde7f4442 | ||
|
|
9879980304 | ||
|
|
7ec81ae607 | ||
|
|
dac2a16677 | ||
|
|
260bf4e5bc | ||
|
|
ece66448e0 | ||
|
|
a254d60876 | ||
|
|
c36c4f4699 | ||
|
|
4a14d80d97 | ||
|
|
c6a9268856 | ||
|
|
2914a1070b | ||
|
|
5ebf8649a6 | ||
|
|
0272412334 | ||
|
|
7a7824be6a | ||
|
|
aa2d4f3265 | ||
|
|
28a283283f | ||
|
|
652ab0b180 | ||
|
|
8ad764527d | ||
|
|
bba094086d | ||
|
|
658ca7fec3 | ||
|
|
156de15a33 | ||
|
|
695a684b8d | ||
|
|
307e2cfc50 | ||
|
|
ed59f793fc | ||
|
|
c17db05f39 | ||
|
|
b1782b614f | ||
|
|
2acff31213 | ||
|
|
a564085449 | ||
|
|
2d5cdb96d2 | ||
|
|
5a47a6637f | ||
|
|
625a251931 | ||
|
|
d0ebe7408f | ||
|
|
976ba7066d | ||
|
|
ff3789f869 | ||
|
|
744dd213f5 | ||
|
|
f6b4bf05b6 | ||
|
|
94e3c3535c | ||
|
|
31b44fbeb7 | ||
|
|
07f4b134b6 | ||
|
|
5c374bb5bf | ||
|
|
0f90dd5f53 | ||
|
|
d07caf2a4b | ||
|
|
81a2bd1d00 | ||
|
|
5fa6ffb81d | ||
|
|
1faab15377 | ||
|
|
a4ddc3d65d | ||
|
|
588c69ea6c | ||
|
|
bf8dad2a4f | ||
|
|
2e06c0e7d2 | ||
|
|
de42cae87f | ||
|
|
cdc4bd154a | ||
|
|
aa2e627a5f | ||
|
|
3359c62429 | ||
|
|
75a6a5e145 | ||
|
|
a9cad501ff | ||
|
|
26584c7500 | ||
|
|
62fdf4a2b5 | ||
|
|
296aa6f50f | ||
|
|
93cc498731 | ||
|
|
b1cd8351fa | ||
|
|
ccf5e73341 | ||
|
|
be5d280c32 | ||
|
|
6633a8c0bf | ||
|
|
097d8936e3 | ||
|
|
8a53b7934b | ||
|
|
0facb15e32 |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
assets/config-template.yaml text eol=lf
|
||||||
@@ -21,25 +21,25 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
I tried this:
|
I tried this:
|
||||||
|
|
||||||
1. `loki`
|
1. `coyote`
|
||||||
|
|
||||||
I expected this to happen:
|
I expected this to happen:
|
||||||
|
|
||||||
Instead, this happened:
|
Instead, this happened:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: loki-log
|
id: coyote-log
|
||||||
attributes:
|
attributes:
|
||||||
label: Loki log
|
label: Coyote log
|
||||||
description: Include the Loki log file to help diagnose the issue. (`loki --info` to see the log_path)
|
description: Include the Coyote log file to help diagnose the issue. (`coyote --info` to see the log_path)
|
||||||
value: |
|
value: |
|
||||||
| OS | Log file location |
|
| OS | Log file location |
|
||||||
| ------- | ----------------------------------------------------- |
|
| ------- | ----------------------------------------------------- |
|
||||||
| Linux | `~/.cache/loki/loki.log` |
|
| Linux | `~/.cache/coyote/coyote.log` |
|
||||||
| Mac | `~/Library/Logs/loki/loki.log` |
|
| Mac | `~/Library/Logs/coyote/coyote.log` |
|
||||||
| Windows | `C:\Users\<User>\AppData\Local\loki\loki.log` |
|
| Windows | `C:\Users\<User>\AppData\Local\coyote\coyote.log` |
|
||||||
|
|
||||||
```
|
```
|
||||||
please provide a copy of your loki log file here if possible; you may need to redact some of the lines
|
please provide a copy of your coyote log file here if possible; you may need to redact some of the lines
|
||||||
```
|
```
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
@@ -57,13 +57,13 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: loki-version
|
id: coyote-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Loki Version
|
label: Coyote Version
|
||||||
description: >
|
description: >
|
||||||
Loki version (`loki --version` if using a release, `git describe` if building
|
Coyote version (`coyote --version` if using a release, `git describe` if building
|
||||||
from main).
|
from main).
|
||||||
**Make sure that you are using the [latest loki release](https://github.com/Dark-Alex-17/loki/releases) or a newer main build**
|
**Make sure that you are using the [latest coyote release](https://github.com/Dark-Alex-17/coyote/releases) or a newer main build**
|
||||||
placeholder: "loki 0.1.0"
|
placeholder: "coyote 0.1.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -3,7 +3,7 @@ name: CI
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- '**'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -36,6 +36,17 @@ jobs:
|
|||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Cache DuckDB Extensions
|
||||||
|
id: duckdb-extensions
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.duckdb/extensions
|
||||||
|
key: duckdb-ext-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install DuckDB Extensions
|
||||||
|
if: steps.duckdb-extensions.outputs.cache-hit != 'true'
|
||||||
|
run: cargo test --all duckdb
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --all
|
run: cargo test --all
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
bump_type:
|
bump_type:
|
||||||
description: "Specify the type of version bump"
|
description: 'Specify the type of version bump'
|
||||||
required: true
|
required: true
|
||||||
default: "patch"
|
default: 'patch'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- patch
|
- patch
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: Install Commitizen
|
- name: Install Commitizen
|
||||||
run: |
|
run: |
|
||||||
@@ -98,9 +98,9 @@ jobs:
|
|||||||
# Ignore Act's local artifact dir noise
|
# Ignore Act's local artifact dir noise
|
||||||
echo artifacts/ >> .git/info/exclude || true
|
echo artifacts/ >> .git/info/exclude || true
|
||||||
|
|
||||||
# Edit the version line right after name="loki"
|
# Edit the version line right after name="coyote"
|
||||||
sed -E -i '
|
sed -E -i '
|
||||||
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"loki"[[:space:]]*$/ {
|
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"coyote"[[:space:]]*$/ {
|
||||||
n
|
n
|
||||||
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
|
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
|
||||||
}
|
}
|
||||||
@@ -108,17 +108,19 @@ jobs:
|
|||||||
|
|
||||||
cargo update || true
|
cargo update || true
|
||||||
|
|
||||||
|
sed -i "s|image: 'darkalex17/coyote:v[^']*'|image: 'darkalex17/coyote:v${VERSION}'|" assets/sbx-kit/spec.yaml
|
||||||
|
|
||||||
# Git config that helps in Act
|
# Git config that helps in Act
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
|
|
||||||
git status --porcelain
|
git status --porcelain
|
||||||
git diff --name-only -- Cargo.toml Cargo.lock || true
|
git diff --name-only -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml || true
|
||||||
|
|
||||||
if ! git diff --quiet -- Cargo.toml Cargo.lock; then
|
if ! git diff --quiet -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml; then
|
||||||
git add -u -- Cargo.toml Cargo.lock
|
git add -u -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml
|
||||||
git commit -m "chore: bump Cargo.toml to $VERSION"
|
git commit -m "chore: bump Cargo.toml and sandbox image to $VERSION"
|
||||||
else
|
else
|
||||||
echo "No changes to commit (already at $VERSION)"
|
echo "No changes to commit (already at $VERSION)"
|
||||||
fi
|
fi
|
||||||
@@ -163,28 +165,31 @@ jobs:
|
|||||||
- target: aarch64-unknown-linux-musl
|
- target: aarch64-unknown-linux-musl
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
cargo-flags: ''
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
- target: aarch64-pc-windows-msvc
|
- target: aarch64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
- target: x86_64-unknown-linux-musl
|
- target: x86_64-unknown-linux-musl
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
use-cross: true
|
use-cross: true
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
cargo-flags: ""
|
cargo-flags: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check if actor is repository owner
|
- name: Check if actor is repository owner
|
||||||
@@ -251,7 +256,7 @@ jobs:
|
|||||||
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
|
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install latest LLVM/Clang
|
- name: Install latest LLVM/Clang
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
run: |
|
run: |
|
||||||
wget https://apt.llvm.org/llvm.sh
|
wget https://apt.llvm.org/llvm.sh
|
||||||
chmod +x llvm.sh
|
chmod +x llvm.sh
|
||||||
@@ -278,7 +283,7 @@ jobs:
|
|||||||
- name: Verify file
|
- name: Verify file
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
file target/${{ matrix.target }}/release/loki
|
file target/${{ matrix.target }}/release/coyote
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc'
|
if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc'
|
||||||
@@ -338,7 +343,7 @@ jobs:
|
|||||||
${{ steps.package.outputs.archive }}
|
${{ steps.package.outputs.archive }}
|
||||||
${{ steps.package.outputs.sha }}
|
${{ steps.package.outputs.sha }}
|
||||||
tag_name: v${{ env.RELEASE_VERSION }}
|
tag_name: v${{ env.RELEASE_VERSION }}
|
||||||
name: "v${{ env.RELEASE_VERSION }}"
|
name: 'v${{ env.RELEASE_VERSION }}'
|
||||||
body_path: artifacts/changelog.md
|
body_path: artifacts/changelog.md
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
@@ -382,11 +387,11 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
macos_sha="$(cat ./artifacts/loki-x86_64-apple-darwin.sha256 | awk '{print $1}')"
|
macos_sha="$(cat ./artifacts/coyote-x86_64-apple-darwin.sha256 | awk '{print $1}')"
|
||||||
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
|
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
|
||||||
macos_sha_arm="$(cat ./artifacts/loki-aarch64-apple-darwin.sha256 | awk '{print $1}')"
|
macos_sha_arm="$(cat ./artifacts/coyote-aarch64-apple-darwin.sha256 | awk '{print $1}')"
|
||||||
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
|
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
|
||||||
linux_sha="$(cat ./artifacts/loki-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')"
|
linux_sha="$(cat ./artifacts/coyote-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')"
|
||||||
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
|
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
|
||||||
release_version="$(cat ./artifacts/release-version)"
|
release_version="$(cat ./artifacts/release-version)"
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
@@ -402,23 +407,23 @@ jobs:
|
|||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
run: |
|
run: |
|
||||||
# run packaging script
|
# run packaging script
|
||||||
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/loki.rb.template" "./loki.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
|
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/coyote.rb.template" "./coyote.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
|
||||||
|
|
||||||
- name: Push changes to Homebrew tap
|
- name: Push changes to Homebrew tap
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.LOKI_GITHUB_TOKEN }}
|
TOKEN: ${{ secrets.COYOTE_GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# push to Git
|
# push to Git
|
||||||
git config --global user.name "Dark-Alex-17"
|
git config --global user.name "Dark-Alex-17"
|
||||||
git config --global user.email "alex.j.tusa@gmail.com"
|
git config --global user.email "alex.j.tusa@gmail.com"
|
||||||
git clone https://Dark-Alex-17:${{ secrets.LOKI_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-loki.git
|
git clone https://Dark-Alex-17:${{ secrets.COYOTE_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-coyote.git
|
||||||
rm homebrew-loki/Formula/loki.rb
|
rm homebrew-coyote/Formula/coyote.rb
|
||||||
cp loki.rb homebrew-loki/Formula
|
cp coyote.rb homebrew-coyote/Formula
|
||||||
cd homebrew-loki
|
cd homebrew-coyote
|
||||||
git add .
|
git add .
|
||||||
git diff-index --quiet HEAD || git commit -am "Update formula for Loki release ${{ env.RELEASE_VERSION }}"
|
git diff-index --quiet HEAD || git commit -am "Update formula for Coyote release ${{ env.RELEASE_VERSION }}"
|
||||||
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-loki.git
|
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-coyote.git
|
||||||
|
|
||||||
publish-crate:
|
publish-crate:
|
||||||
needs: publish-github-release
|
needs: publish-github-release
|
||||||
@@ -455,4 +460,64 @@ jobs:
|
|||||||
- uses: katyo/publish-crates@v2
|
- uses: katyo/publish-crates@v2
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
with:
|
with:
|
||||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
publish-sandbox-image:
|
||||||
|
needs: [publish-github-release]
|
||||||
|
name: Publish Sandbox Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if actor is repository owner
|
||||||
|
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "You are not authorized to run this workflow."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Ensure repository is up-to-date
|
||||||
|
if: env.ACT != 'true'
|
||||||
|
run: |
|
||||||
|
git fetch --all
|
||||||
|
git pull
|
||||||
|
|
||||||
|
- name: Get release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set version variable
|
||||||
|
run: |
|
||||||
|
version="$(cat artifacts/release-version)"
|
||||||
|
echo "version=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Validate release environment variables
|
||||||
|
run: |
|
||||||
|
echo "Release version: ${{ env.version }}"
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: env.ACT != 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ env.ACT != 'true' }}
|
||||||
|
tags: darkalex17/coyote:latest, darkalex17/coyote:v${{ env.version }}
|
||||||
|
build-args: COYOTE_VERSION=${{ env.version }}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: Update models
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */12 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: update-models
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
name: Update models
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: --deny warnings
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain Components
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Cache DuckDB Extensions
|
||||||
|
id: duckdb-extensions
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.duckdb/extensions
|
||||||
|
key: duckdb-ext-ubuntu-latest-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install DuckDB Extensions
|
||||||
|
if: steps.duckdb-extensions.outputs.cache-hit != 'true'
|
||||||
|
run: cargo test --all duckdb
|
||||||
|
|
||||||
|
- name: Write stub config
|
||||||
|
run: |
|
||||||
|
COYOTE_CONFIG_DIR="$(mktemp -d)"
|
||||||
|
printf 'clients:\n - type: openai\n - type: claude\n' > "$COYOTE_CONFIG_DIR/config.yaml"
|
||||||
|
echo "COYOTE_CONFIG_DIR=$COYOTE_CONFIG_DIR" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Regenerate model catalog
|
||||||
|
run: cargo run --release -- --generate-models
|
||||||
|
|
||||||
|
- name: Detect catalog changes
|
||||||
|
id: diff
|
||||||
|
run: echo "changed=$(git diff --quiet models.yaml && echo false || echo true)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# The binary built by the regeneration step embedded the OLD models.yaml (the file
|
||||||
|
# was rewritten after that build), so rebuild before smoke-testing: cargo tracks
|
||||||
|
# include_str! inputs, making this a cheap incremental rebuild of the final crate.
|
||||||
|
- name: Rebuild with the refreshed catalog
|
||||||
|
if: steps.diff.outputs.changed == 'true'
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
if: steps.diff.outputs.changed == 'true'
|
||||||
|
run: cargo test --all
|
||||||
|
|
||||||
|
- name: Smoke test the refreshed catalog
|
||||||
|
if: steps.diff.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
COUNT=$(./target/release/coyote --list-models | wc -l)
|
||||||
|
echo "Listed $COUNT chat models"
|
||||||
|
test "$COUNT" -ge 40
|
||||||
|
|
||||||
|
- name: Commit and push
|
||||||
|
if: steps.diff.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git add models.yaml
|
||||||
|
git checkout -- .
|
||||||
|
git config user.name 'github-actions[bot]'
|
||||||
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||||
|
git commit -m 'chore: refresh generated model catalog [skip ci]'
|
||||||
|
git pull --rebase origin main
|
||||||
|
git push origin main
|
||||||
+5
-1
@@ -3,5 +3,9 @@
|
|||||||
/.env
|
/.env
|
||||||
!cli/**
|
!cli/**
|
||||||
.idea/
|
.idea/
|
||||||
/loki.iml
|
/coyote.iml
|
||||||
/.idea/
|
/.idea/
|
||||||
|
.coyote/**
|
||||||
|
.sisyphus/**
|
||||||
|
.coyote-project.json
|
||||||
|
.coyote/memory/
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
|
|
||||||
+722
-8
@@ -1,3 +1,717 @@
|
|||||||
|
## v0.10.3 (2026-09-15)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- create a config alias for default_reasoning_level (to match some wording and make the config flexible)
|
||||||
|
- support custom resource definitions for openai-compatible Oauth customization
|
||||||
|
- Support specifying the compression model to use when compressing a session for greater cost controls
|
||||||
|
- Added full cost tracking to Coyote and added it to the default right prompt
|
||||||
|
- added support for input/output token cache cost tracking
|
||||||
|
- Support persisting paused todo lists to sessions so users can exit and resume paused todo sessions; Also fixed a bug so todo list pausing is only supported when being used in a session since it typically requires multi-turn communication
|
||||||
|
- Support token caching to reduce LLM costs in Anthropic models (not typically handled automatically by APIs)
|
||||||
|
- **TASK-030**: callers escalate on GAUNTLET_REVIEW_INCOMPLETE instead of findings-routing
|
||||||
|
- **TASK-032**: review-gauntlet incomplete-review machine line + ×N attempt details
|
||||||
|
- **TASK-029**: review-gauntlet fault-only lane re-run loop
|
||||||
|
- **TASK-028**: escalation_timeout 0 waits indefinitely and is the default
|
||||||
|
- **graph**: stream llm-node completions so node timeout governs long generations
|
||||||
|
- **TASK-023**: deep-research hardening — fail-safe scripts, fault fallbacks, pipeline notes
|
||||||
|
- **TASK-020**: finding-verifier + step-runner fail-closed hardening
|
||||||
|
- **TASK-019**: code-reviewer fail-closed review pipeline
|
||||||
|
- **TASK-018**: review-gauntlet lane degradation + verification passthrough
|
||||||
|
- **TASK-017**: adversary execution evidence + fail-closed routing
|
||||||
|
- **TASK-016**: engine + test fixes from the Phase-1 re-review
|
||||||
|
- **TASK-011**: traverse agent fallback edges in map peer-discovery BFS
|
||||||
|
- **TASK-010**: add max_attempts to read-only agent lanes in bundled graphs
|
||||||
|
- **TASK-009**: add fallback routing to agent nodes
|
||||||
|
- **TASK-008**: add shared transient matcher and retry support to agent nodes
|
||||||
|
- Fixed graph agents with new agent variable passing functionality
|
||||||
|
- **graph**: warn when a declared variable is shadowed by an initial_state key
|
||||||
|
- **agents**: advertise declared agent variables in agent__list_available
|
||||||
|
- **agents**: allow passing agent variables to agent__spawn and agent__task_create
|
||||||
|
- seed variables into initial graph state
|
||||||
|
- Significant code review suite rewrite with improved graph agent features
|
||||||
|
- let install commands remove files that have been removed from bundles, both internal and external
|
||||||
|
- update the web_search_coyote tool to attempt to use the current model first then fallback to the configured model for web searches
|
||||||
|
- expose the current coyote model to tools
|
||||||
|
- removed temperature specification in deep-research and finding-verifier agents to make them more univerally usable
|
||||||
|
- **graph**: thread the graph abort into llm nodes and tool execution
|
||||||
|
- **graph**: abort a graph run on Ctrl-C from anywhere
|
||||||
|
- **graph**: warn when an llm node's turn cap is disabled
|
||||||
|
- **graph**: treat llm max_iterations: 0 as no turn cap
|
||||||
|
- **graph**: warn when the visit cap is disabled
|
||||||
|
- **graph**: treat max_loop_iterations: 0 as no visit cap
|
||||||
|
- **graph**: warn when a timeout is disabled
|
||||||
|
- **graph**: treat timeout: 0 as no wall-clock bound
|
||||||
|
- **graph**: validate agent node inputs at load time
|
||||||
|
- **graph**: seed a child graph agent's state from an agent node's inputs map
|
||||||
|
- **graph**: validate map branch subgraphs instead of single branch nodes
|
||||||
|
- **graph**: run map branches as per-item subgraph chains
|
||||||
|
- **graph**: accept a `{{template}}` for a map node's `max_concurrency`
|
||||||
|
- created the finding-verifier and whetstone agents for continuous code quality improvements
|
||||||
|
- teammates flag enabling peer messaging between concurrent graph agent nodes
|
||||||
|
- support explicit can_spawn_agents, max_concurrent_agents, and max_agent_depth keys in graph.yaml
|
||||||
|
- support setting the tool_timeout in the main config file instead of only via an environment variable
|
||||||
|
- Added new environment variable to force showing tool call logging when not in a tty
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- empty tool results can't cause errors in bedrock, claude, openai, or vertexai clients anymore
|
||||||
|
- proper support for reasoning/thinking in bedrock models
|
||||||
|
- **TASK-032**: enforce incomplete⇒BLOCKED structurally; anchor adversary degraded detection
|
||||||
|
- **TASK-028**: pin example-doc wording; make not-found escalation hint match queue semantics
|
||||||
|
- **TASK-028**: pin global-template exclusion, tighten timeout boundary test, reword stale hint
|
||||||
|
- **adversary**: red verification runs block CONFORMS; re-derive timeout envelope; robust runner
|
||||||
|
- **review-suite**: detect nested finding-verifier faults; harden render/fault markers
|
||||||
|
- **review-gauntlet**: summary-line critical count, nested-fault hook, fail-closed lane fallback
|
||||||
|
- **graph**: typed ApiStatusError transient classification; warn on bare output_schema
|
||||||
|
- **review-suite**: mark example verification commands as placeholders; pin variable description and prompt note
|
||||||
|
- **graph**: auto-merge only schema-declared keys from llm output
|
||||||
|
- **review-suite**: pass verification_commands as a declared variable, never LLM-extracted from prose
|
||||||
|
- **agents**: scrub GRAPH_STATE* from adversary verification commands; raise review-suite max_agent_depth
|
||||||
|
- **adversary**: narrow died-marker predicate, degraded banner for died criteria, hermetic script tests
|
||||||
|
- **adversary**: fail closed on PIPELINE-FAULT evidence regardless of status; pin bail-chain and validator shape
|
||||||
|
- **adversary**: fail closed per criterion when check_criterion dies
|
||||||
|
- **engine**: carry full error chain in llm node fault text
|
||||||
|
- **TASK-020**: skip POSIX bash script tests on Windows
|
||||||
|
- **TASK-020**: surface step-runner LLM-fault captures where the fallback lands
|
||||||
|
- **TASK-019**: carry PIPELINE-FAULT prefix through verdict.py's crash guard
|
||||||
|
- **TASK-018**: harden verdict_gate fault routing and widen default_lanes fallback
|
||||||
|
- **TASK-017**: bound run_checks runtime and fail-close its node-level death
|
||||||
|
- **tests**: collision-proof parser test temp files to deflake macOS CI
|
||||||
|
- Don't infinitely compress a session once it passes a threshold when the threshold is close to the max token count
|
||||||
|
- fix spinner ctrl-c early exit not clearing message and restoring cursor
|
||||||
|
- **graph**: build peer retirement guards before spawning their tasks
|
||||||
|
- **graph**: reject agents that ship both config.yaml and graph.yaml at load
|
||||||
|
- **graph**: bound concurrency caps by the semaphore permit limit
|
||||||
|
- **graph**: make turn-cap and template checks consistent across validation modes
|
||||||
|
- **graph**: leave output absent after state_updates when it was absent before
|
||||||
|
- **graph**: validate the whole max_concurrency template string
|
||||||
|
- **graph**: reject as == output_key on map nodes
|
||||||
|
- **graph**: inherit the session abort in agent-node children
|
||||||
|
- **graph**: retire the teammate identity before structured extraction
|
||||||
|
- **graph**: reject a literal max_concurrency of 0 instead of clamping it
|
||||||
|
- **graph**: clear peer identity fields after every map chain step
|
||||||
|
- **graph**: pick the map teammate identity name by BFS from the branch entry
|
||||||
|
- **graph**: name the offending node type when a map branch reaches a disallowed node
|
||||||
|
- **function**: build bundled bash tools without writing back to the source tree
|
||||||
|
- **agents**: name the map-branch output key in deep-research and finding-verifier
|
||||||
|
- **graph**: honor an explicit `output` key in state_updates
|
||||||
|
- Fixed the bad formatting in the finding-verifier agent
|
||||||
|
- properly anchored the role frontmatter regex so it doesn't catch in-instruction separators
|
||||||
|
- support non-object tool result outputs for bedrock
|
||||||
|
- exclude null bytes from fs_grep output on binaries
|
||||||
|
- suppress subagent error and warning output to stderr/stdout
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- **graph**: keep the ordered visit log test-only
|
||||||
|
- **graph**: move the cfg(test) OUTPUT_KEY import into llm's test module
|
||||||
|
- **graph**: drop the duplicated locator prefix from map chain step errors
|
||||||
|
|
||||||
|
## v0.10.2 (2026-09-04)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- added a clear line separator for the end of LLM turns to make it more clear where they are
|
||||||
|
- sort models by release date
|
||||||
|
- Improved the architect, explore, and sisyphus agents to have a team mode so they can all work in parallel as a team and updated the parallel-research skill to make explicit mention of this functionality
|
||||||
|
- support auto continuation in subagents and prevent them from calling todo__pause
|
||||||
|
- apply provider quirks to models created from a name
|
||||||
|
- refresh the model catalog from live API data at runtime
|
||||||
|
- generate the model catalog from live API data
|
||||||
|
- add model catalog source adapters and merge engine
|
||||||
|
- add hand-owned model quirks overlay
|
||||||
|
- apply provider quirks to models created from a name
|
||||||
|
- refresh the model catalog from live API data at runtime
|
||||||
|
- generate the model catalog from live API data
|
||||||
|
- add model catalog source adapters and merge engine
|
||||||
|
- add hand-owned model quirks overlay
|
||||||
|
- Improved the architect, explore, and sisyphus agents to have a team mode so they can all work in parallel as a team and updated the parallel-research skill to make explicit mention of this functionality
|
||||||
|
- added tab completions to --update-bundle flag
|
||||||
|
- added a dedicated config_dir output to coyote --info/.info
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- children subagents couldn't properly send messages to parent inboxes
|
||||||
|
- --generate-models parsed before vault initialization
|
||||||
|
- drive tool rounds iteratively instead of recursively to prevent stack overflow on long agentic turns to fix latent stack overflow recursion bug
|
||||||
|
- session replays now include mid-turn messages so the full session gets replayed
|
||||||
|
- resolve duplicate model ids by requested type
|
||||||
|
- let ctrl-c interrupt blocking job and agent collects
|
||||||
|
- skip terminal color-scheme queries in test builds
|
||||||
|
- memoize quirk glob patterns and isolate concurrent override writes
|
||||||
|
- memoize quirk glob patterns and isolate concurrent override writes
|
||||||
|
- children subagents couldn't properly send messages to parent inboxes
|
||||||
|
|
||||||
|
### Perf
|
||||||
|
|
||||||
|
- probe models-override staleness in the background refresh task
|
||||||
|
|
||||||
|
## v0.10.1 (2026-08-31)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- make binary shims for custom tools cross-device compatible so users can't accidentally break sandboxes via `sbx cp ~/.config/coyote <sbx-name>:/home/agent/.config`
|
||||||
|
|
||||||
|
## v0.10.0 (2026-08-29)
|
||||||
|
|
||||||
|
### BREAKING CHANGE
|
||||||
|
|
||||||
|
- invocations using --prompt <text> must switch to
|
||||||
|
--temp-role <text>; clap rejects the old flag loudly.
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- Support Linux ARM64 GNU
|
||||||
|
- Note that duckdb RAG driver is unavailable on MUSL Linux builds when initializing a RAG so users know when using the binary, not just based on docs
|
||||||
|
- updated coyote installation scripts to detect when gnu is installable
|
||||||
|
- upgraded to v2 mixin schema
|
||||||
|
- improved first-time run experience and included a templated configuration file that now has comments like the config.example.yaml so users don't have to go to the repo to see all the knobs
|
||||||
|
- **TASK-005**: carry the quality bar through sisyphus and architect
|
||||||
|
- **TASK-004**: reviewer routing — code-reviewer quality-bar resolution, domain linter pass, surface-skill routing, rigor folding; file-reviewer surface-skill whitelist + convention/correctness marker
|
||||||
|
- **TASK-003**: add surface skills batch B — worker-review, iac-review, migration-review, cicd-review
|
||||||
|
- **TASK-002**: add surface skills batch A — rest-api-review, cli-review, library-review
|
||||||
|
- **TASK-001**: add rigor/surfaces declaration layer to planning skills
|
||||||
|
- **repl**: color .info mcp-server verdicts and complete only running servers
|
||||||
|
- **cli**: rename mcp_config asset category to mcp-config with snake_case alias
|
||||||
|
- create a new probe agent to probe code verifications using usage-pattern testing
|
||||||
|
- Add in the web_search_coyote tool where helpful
|
||||||
|
- **mcp**: add .info mcp-server, .set mcp_tools, and filtered-server listing to the REPL
|
||||||
|
- **mcp**: enforce per-server tool allowlists across runtime, jobs, agents, and graph nodes
|
||||||
|
- **mcp**: add mcp_tools allowlist config surfaces across roles, sessions, agents, graphs, and skills
|
||||||
|
- **mcp**: add per-server tool allowlist policy module and allowedTools config field
|
||||||
|
- support mcp.json in the root of bundle repos as well as in the legacy functions directory
|
||||||
|
- prefer ~/.config/coyote/mcp.json for the user-scope MCP config
|
||||||
|
- claude and openai native web search via web_search_coyote
|
||||||
|
- **jobs**: node-local job ownership and capability-gated job__* visibility
|
||||||
|
- **graph**: support max_concurrent_jobs at the graph level
|
||||||
|
- **jobs**: allow uncapped collect via full_result
|
||||||
|
- **jobs**: exempt polling tools from loop tracker and hint on unchanged checks
|
||||||
|
- **supervisor**: push agent completion notifications to the spawning context
|
||||||
|
- **jobs**: push background-job completion notifications via per-context queue
|
||||||
|
- inject background-jobs prompt guidance when jobs are enabled
|
||||||
|
- add background job runner, job__* handlers, and start gates
|
||||||
|
- generalize supervisor registry to TaskHandle enum with job scaffolding, kill discipline, and max_concurrent_jobs config
|
||||||
|
- run an advisory observability pass after implementation in sisyphus
|
||||||
|
- add an observability-review skill for post-implementation monitoring analysis
|
||||||
|
- check under- and over-logging in the code review gate
|
||||||
|
- calibrate logging registers in the code-writing agents
|
||||||
|
- add a logging-discipline skill for calibrating log output to repo conventions
|
||||||
|
- **cli**: rename --prompt to --temp-role
|
||||||
|
- complete --filter and --force on the first .install argument
|
||||||
|
- **mcp**: bound tool-result passthrough and surface resource audience annotations
|
||||||
|
- **mcp**: add mcp_prompt meta-tool and harden prompt display rendering
|
||||||
|
- **repl**: add .prompt command with live staged tab-completion
|
||||||
|
- **mcp**: add mcp_read meta-tool for resource reads
|
||||||
|
- **mcp**: gate meta-function emission on advertised server capabilities
|
||||||
|
- **mcp**: add render.rs content policy (text paging, pattern filter, blob spill)
|
||||||
|
- **mcp**: extend the server catalog to resources, templates, and prompts
|
||||||
|
- complete --filter and --force on the first .install argument
|
||||||
|
- run design interviews as grilling frontier rounds across the planning agents
|
||||||
|
- add a grilling skill for frontier-round design interviews
|
||||||
|
- add an on-demand architecture-reviewer agent for deepening scans
|
||||||
|
- add a codebase-design skill with the deep-module design vocabulary
|
||||||
|
- add a feedback-loop-first diagnosing-bugs skill to the coding suite
|
||||||
|
- add a Fowler code-smell baseline to the code-review skill
|
||||||
|
- flag duplicate helpers in code reviews with a repo-wide DRY check
|
||||||
|
- add a transactional-integrity review skill to the code review gate
|
||||||
|
- add an operational-history prior-art lane to the code-reviewer agent
|
||||||
|
- support name=value macro arguments with variable tab completion
|
||||||
|
- add --help guides to the .install and .uninstall REPL commands
|
||||||
|
- expand owner/repo shorthand for --install with a --git-host flag
|
||||||
|
- remove the deprecated --install-from flag and .install remote form
|
||||||
|
- rename install flags and unify .install dispatch
|
||||||
|
- add --uninstall and .uninstall for installed bundles
|
||||||
|
- add --update-bundle with provenance-aware conflict handling
|
||||||
|
- add --list-bundles and .list bundles with drift detection
|
||||||
|
- record bundle provenance when installing from remote repos
|
||||||
|
- add bundle provenance store
|
||||||
|
- parse bundle manifests and capture resolved SHAs for remote installs
|
||||||
|
- created a new comment-discipline skill for the built in sisyphus suite
|
||||||
|
- created a dedicated git_command tool to tighten tool calling permissions in the git-master skill
|
||||||
|
- Added a new security review step to the code writing quality gates
|
||||||
|
- addressed review comments
|
||||||
|
- add --no-workspace-macros opt-out for workspace macro loading
|
||||||
|
- execute non-isolated macros on the live REPL context
|
||||||
|
- surface macros as first-class custom commands in the REPL
|
||||||
|
- add lazy macro resolver with two-dir discovery and per-macro states
|
||||||
|
- add enabled_macros config field at global, role, agent, and session levels
|
||||||
|
- add description and isolated fields to Macro struct
|
||||||
|
- Dynamically detect RAG embedding model dimension for any given model
|
||||||
|
- Support auto confirmation for gatekeeper agents
|
||||||
|
- retry LLM API calls once after 401 by force-refreshing the OAuth token
|
||||||
|
- identity-aware rejected-token marker for LLM OAuth cache
|
||||||
|
- typed ApiStatusError carrying HTTP status through LLM client errors
|
||||||
|
- per-request OAuth token injection with mid-session refresh for HTTP MCP servers
|
||||||
|
- reason-specific warnings for MCP servers that fail OAuth at startup
|
||||||
|
- Installed duckdb into the coyote image
|
||||||
|
- Support managing MCP servers from the CLI directly
|
||||||
|
- append new built-in rag__query function to RAG contexts to allow further querying by LLMs
|
||||||
|
- support static file bundling with sbx-mixins
|
||||||
|
- **rag**: offer the storage driver when an agent initializes its RAG
|
||||||
|
- simplified the duckdb selection prompt
|
||||||
|
- **rag**: let several Coyote processes query one duckdb RAG at once
|
||||||
|
- **rag**: support string and UUID Qdrant point IDs
|
||||||
|
- **rag**: create the API key secret inline in the attach wizard
|
||||||
|
- let workflow rag nodes select a RAG driver
|
||||||
|
- improved wording and heuristic detection for sisyphus suite of agents
|
||||||
|
- upgraded to sbx kit v2 spec for improved integration
|
||||||
|
- **rag**: add attach-only Qdrant provider, attach wizard and sandbox wiring
|
||||||
|
- **rag**: add DuckDB provider behind the RAG driver abstraction
|
||||||
|
- **rag**: add driver/attached fields, validation floors and force-reingest
|
||||||
|
- **mcp**: send RFC 8707 resource indicator in OAuth flows
|
||||||
|
- Added loaded indicators to .list tools/mcp-servers/skills
|
||||||
|
- hide .recover from tab completions when no session is active
|
||||||
|
- add a new .recover command for sessions to recover from errors
|
||||||
|
- integrated the git-ssh-sign kit into the coyote sandbox kit
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- Harden install scripts: detect libssl3 without ldconfig on PATH, survive noexec tmp dirs, and guard against partial curl|bash execution
|
||||||
|
- ldconfig on Debian-based distros lives in /usr/sbin
|
||||||
|
- Prefer GNU linux builds in install scripts for duckdb support, and gate duckdb support on linux hosts that are MUSL until MUSL support is added
|
||||||
|
- **mcp**: omit declared-but-empty prompt/resource capabilities from .info mcp-server
|
||||||
|
- **mcp**: annotate declared-but-empty prompt/resource capabilities in .info mcp-server
|
||||||
|
- also support the .continue edge case for session crashing checkpointing
|
||||||
|
- checkpoint sessions that crash for easy resuming
|
||||||
|
- job__check ring buffer also collects file output for LLM_OUTPUT as well as stdout
|
||||||
|
- **tools**: interactive-shell semantics and stderr capture in execute_command
|
||||||
|
- **function**: gate test-only declaration appender behind cfg(test)
|
||||||
|
- **graph**: gate unix-only test imports behind cfg(unix)
|
||||||
|
- harden job runner lifecycle and whitelist conformance
|
||||||
|
- **function**: floor tool-output truncation cut to a UTF-8 char boundary
|
||||||
|
- **supervisor**: make agent__check a pure status probe that never consumes the handle
|
||||||
|
- **supervisor**: surface finished-but-uncollected tasks in turn-end guardrail
|
||||||
|
- fix grep "binary" errors when searching UTF-8 files with unicode characters like some of the Coyote source
|
||||||
|
- **bundles**: harden the install pipeline for cross-platform correctness
|
||||||
|
- **mcp**: harden the spill path for cross-platform correctness
|
||||||
|
- **repl**: offer prompts in .list tab completion and rename its listing helpers
|
||||||
|
- **mcp**: gate unix-only spill permission APIs for windows builds
|
||||||
|
- harden the bundle lifecycle per code review
|
||||||
|
- address code review findings on the bundle lifecycle
|
||||||
|
- reserve category names, confirm fork-name collisions, report secrets on uninstall
|
||||||
|
- make bundle provenance portable to Windows
|
||||||
|
- user__ask should have been renamed to user__select in graph agent user interaction invocations
|
||||||
|
- support tab completions for graph-based agents with variables as well as standard agents
|
||||||
|
- surface macro parse errors on top-level invocation
|
||||||
|
- render .list macros as a comfy-table instead of fixed-width columns
|
||||||
|
- drain crossterm characters in zellij in kitty contexts to prevent DA1 responses from entering prompt
|
||||||
|
- drain tty input when displaying inquire prompts to prevent unintentional escapes
|
||||||
|
- breaking iwe MCP server changes with newest version
|
||||||
|
- improved handling of non service-specific secrets using sbx custom-secrets
|
||||||
|
- include tool output to LLM_OUTPUT in errors as well as stderr
|
||||||
|
- prevent tmp-overwriting
|
||||||
|
- Corrected a rare edge case on how tool files are generated during parallel executions
|
||||||
|
- cosmetic fix after improved bash tool handling
|
||||||
|
- Improved subagent escalation handling
|
||||||
|
- latent parsing bugs in fs_patch and argc
|
||||||
|
- Prevent infinite hangs in coder agent and implement timeouts for LLM API calls and interactive tools
|
||||||
|
- allow nested italics inside bold spans in markdown renderer
|
||||||
|
- properly handle OAuth refreshes
|
||||||
|
- correct newline removal from fs_write and fs_patch
|
||||||
|
- detect duplicate tool call IDs client-side before sending to Claude
|
||||||
|
- **rag**: warn when a duckdb store is empty but files are indexed
|
||||||
|
- **rag**: keep a local Qdrant off an ambient proxy
|
||||||
|
- keep loopback and LAN traffic off an ambient proxy
|
||||||
|
- **rag**: stop routing Qdrant requests through an ambient proxy
|
||||||
|
- **rag**: treat a zero min_score as no floor on Qdrant searches
|
||||||
|
- **rag**: address Copilot review findings on the driver abstraction
|
||||||
|
- **rag**: delete the DuckDB write-ahead log alongside the store
|
||||||
|
- **sandbox**: discover agent-scoped RAG mixin sidecars
|
||||||
|
- **rag**: stop the attach wizard from silently accepting an empty collection
|
||||||
|
- **rag**: fail loudly when a RAG's vault secret is missing
|
||||||
|
- **rag**: serialize DuckDB extension installs to stop a Windows race
|
||||||
|
- **rag**: emit an sbx kit v2 mixin and declare RAG credentials to the proxy
|
||||||
|
- **rag**: install DuckDB vss and fts extensions when they are missing
|
||||||
|
- don't output thinking blocks for claude-based models
|
||||||
|
- additional edge case fix for duplicate tool call IDs in anthropic API calls
|
||||||
|
- removed temperature modifier in librarian agent to mitigate invisible errors
|
||||||
|
- strip reasoning blocks for structured LLM output in graph agents
|
||||||
|
- prevent rare duplicate tool call IDs in long running claude prompts
|
||||||
|
- agents inherit global reasoning effort if unset
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- **function**: finish supervisor-to-agent vocabulary migration
|
||||||
|
- **function**: rename agent-tool symbols out of supervisor vocabulary
|
||||||
|
- Modified the naming of several generalized supervisor values
|
||||||
|
- pulled out some imports to clean up the MCP render module a bit
|
||||||
|
- **mcp**: centralize meta-function prefix predicates and fix list_tools pagination
|
||||||
|
- Refactored some bundle const locations
|
||||||
|
- drop the .install remote migration hint
|
||||||
|
- drop the --install-from tombstone entirely
|
||||||
|
- render .list agents and .list skills as comfy-tables
|
||||||
|
- **rag**: discover driver_config secrets by grammar, not field name
|
||||||
|
- **rag**: drop the hardcoded embedding model hint from attach
|
||||||
|
- **rag**: interpolate every driver_config value, not just api_key
|
||||||
|
- **rag**: extract RagProvider trait and add YamlProvider
|
||||||
|
|
||||||
|
## v0.8.3 (2026-08-03)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- infinite loop bug when attempting to interrupt a prompt exchange right before a session compression
|
||||||
|
- ctrl-c inside of an auto-continue loop created an infinite loop
|
||||||
|
|
||||||
|
## v0.8.2 (2026-07-31)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- sbx update doesn't allow undefined fields in sbx spec
|
||||||
|
|
||||||
|
## v0.8.1 (2026-07-30)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- ctrl-c interrupts ongoing prompt in a session, but lets the user inject more instructions mid-stream
|
||||||
|
- improved function calling performance by allowing parallel tool calling
|
||||||
|
- created the architect and gatekeeper agents for dramatically improved coding performance
|
||||||
|
- Improved readability of session message exchange replays
|
||||||
|
- apply --agent/--role/--rag/--model flags in --acp-server mode
|
||||||
|
- add headless profile to sbx-kit spec
|
||||||
|
- implement ACP user-interaction to request_permission bridge
|
||||||
|
- implement ACP session/load and session/cancel
|
||||||
|
- implement ACP session/prompt
|
||||||
|
- add ACP server skeleton with stdout-purity test
|
||||||
|
- add --headless flag for unattended operation
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- ctrl-c interruption doesn't discard session messages when throbber is showing
|
||||||
|
- improper handling of fd-style globbing for directories in fs_glob
|
||||||
|
- properly templated architect design doc path in starter commands
|
||||||
|
- .copy works when sessions are resumed
|
||||||
|
- ACP session/prompt now drives the full tool-execution loop
|
||||||
|
- ACP spec conformance — ContentBlock prompt params and protocolVersion type
|
||||||
|
- restore stdout output for standalone --headless mode
|
||||||
|
- suppress tool-call display in headless mode; initialize session on session/new
|
||||||
|
- skip stdin drain and set silent render mode when --acp-server is active
|
||||||
|
- include graph-agent descriptions in .agent <TAB> completions
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- move ACP server dispatch into run() for shared flag setup
|
||||||
|
|
||||||
|
## v0.8.0 (2026-07-25)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- Dynamically detect if a selected client in the sandbox first run wizard supports oauth
|
||||||
|
- Add support for the --fresh flag again with host environment configuration injection
|
||||||
|
- force overwrite global sbx secrets
|
||||||
|
- add sbx secrets globally
|
||||||
|
- only create secrets local to a sandbox
|
||||||
|
- Improved credentials management for docker sandboxes
|
||||||
|
- renamed --contents arg for fs_write/patch to --content since most models attempt that first and error otherwise
|
||||||
|
- Used improved theme selections for tool call highlighting colors
|
||||||
|
- Improved theme-derived syntax highlighting for LLM tool call logging
|
||||||
|
- Improved coloring of LLM tool invocation outputs to make LLM output more readable and cohesive
|
||||||
|
- renamed the user__ask to user__select and improved descriptions to improve model usage
|
||||||
|
- Improved coloring/highlighting of tool calls to make LLM invocation logs easier to read
|
||||||
|
- long-running session improvements
|
||||||
|
- Made sisyphus suite of agents all auto-approved for tool usage
|
||||||
|
- added ast_grep tool to Sisyphus suite agents
|
||||||
|
- created the adversay agent and adversarial-review skill
|
||||||
|
- new spawnable_agents field in agents to let users restrict what agents can be spawned by a parent agent
|
||||||
|
- replay pre-compressed messages as well when resuming sessions for users to see
|
||||||
|
- created a new builtin function for agents who can spawn other agents to list available agents via agent__list_available
|
||||||
|
- **render**: hanging-indent line wrapping for lists and blockquotes
|
||||||
|
- **render**: wire table state machine and finalize hook
|
||||||
|
- **render**: render markdown tables with comfy-table
|
||||||
|
- **render**: parse table cells and column alignments
|
||||||
|
- **render**: detect markdown table rows and separators
|
||||||
|
- **render**: add comfy-table dependency and table border style
|
||||||
|
- **render**: activate rich markdown renderer as default
|
||||||
|
- **render**: rich block-level markdown rendering (headings, quotes, lists, hr)
|
||||||
|
- **render**: rich inline markdown rendering (bold, italic, code, links)
|
||||||
|
- **render**: detect markdown block-level line types
|
||||||
|
- **render**: precompute markdown scope styles for rich rendering
|
||||||
|
- Created the raw_markdown configuration flag
|
||||||
|
- added a .fork command to fork a new session from a running conversation
|
||||||
|
- **oauth**: enable browser-paste PKCE flow for OpenAI-compatible providers
|
||||||
|
- copy host OAuth tokens into sandbox at launch
|
||||||
|
- implement OAuth 2.0 Device Authorization Grant (RFC 8628)
|
||||||
|
- hint that browser 'paste code' pages can be ignored during callback capture
|
||||||
|
- openai-compatible wizard offers OAuth when provider has bundled oauth defaults
|
||||||
|
- validate unique client names at config load
|
||||||
|
- bundle xAI OAuth defaults in models.yaml
|
||||||
|
- OAuth branch in openai_compatible prepare_* fns
|
||||||
|
- get_oauth_provider_for_client dispatcher + client_config_info update
|
||||||
|
- OpenAICompatibleOAuthProvider (config-driven OAuthProvider impl)
|
||||||
|
- add auth + oauth fields to OpenAICompatibleConfig
|
||||||
|
- add oauth field to ProviderModels
|
||||||
|
- add client_credentials support to prepare_oauth_access_token
|
||||||
|
- add OAuthConfig + OAuthFlow types to oauth.rs
|
||||||
|
- Added reasoning effort to the right prompt
|
||||||
|
- Improved support for Anthropic's extended thinking
|
||||||
|
- Also support GEMINI.md workspace instructions
|
||||||
|
- Improved workspace instructions support
|
||||||
|
- also detect .mcp.json configurations at workspace roots
|
||||||
|
- Created new .tool enable/disable and .mcp enable/disable aliases to make REPL usage more egonomic
|
||||||
|
- Created a new .list <kind> REPL command to make discoveribility easier in the REPL
|
||||||
|
- Support claude-style hidden workspace MCP configuration files via .mcp.json
|
||||||
|
- Allow users to customize the workspace-specific configuration directory name so they can use Coyote with other CLI clients like .claude
|
||||||
|
- Add reasoning_effort validation for the main configuration file
|
||||||
|
- Add validation for reasoning_effort settings to prevent users from specifying erroneous values
|
||||||
|
- Added support for modifying the reasoning effort of reasoning models
|
||||||
|
- Explicitly Prevent .undo usage in graph agents
|
||||||
|
- Added an .undo command to the REPL to let users have more control over the conversation
|
||||||
|
- Improve sandbox startup time by using the prebuilt Coyote image
|
||||||
|
- Make coyote available as a docker image
|
||||||
|
- Made fs_patch more flexible for different model preferences of patch formats
|
||||||
|
- Installed nano into the sandbox so that users can edit config files in the sandbox directly
|
||||||
|
- Support workspace-local skill definitions and MCP configurations
|
||||||
|
- fully functional graph-based RAG
|
||||||
|
- Implemented graph-based RAG
|
||||||
|
- Added a --dangerously-skip-permissions flag to skip permission prompts for tool invocations
|
||||||
|
- Remove the temperature hyperparameter from the diagnose role
|
||||||
|
- Added a new oauth.redirectHost field to make it possible to further extend MCP support
|
||||||
|
- Updated the REPL mcp auth path to use the prettified error messaging
|
||||||
|
- Improved error messaging for failed MCP starts because of auth issues
|
||||||
|
- Added support for specifying the oauth port and client ID in MCP server configs
|
||||||
|
- Implemented OAuth support for OpenAI models via Codex endpoints
|
||||||
|
- merge MCP config when installing bundled mcp config
|
||||||
|
- Implemented durable state for sisyphus
|
||||||
|
- Installed ast-grep for the explore agent to use for better code exploration
|
||||||
|
- Created the step-runner graph agent for more deterministic coding workflows to produce even more reliable and higher-quality results
|
||||||
|
- Improved oracle and sisyphus agents with skill integrations for the new skills
|
||||||
|
- Created new sisyphus family skills to improve performance
|
||||||
|
- Created new diagnostic role and skill for use in other contexts
|
||||||
|
- Added new memory functions for deleting and renaming memory files, as well as new lints for memory expiration dates and staleness of memories to improve the memory system
|
||||||
|
- Created a new iwe skill and installed the iwe MCP server for utilizing large knowledgebases
|
||||||
|
- Session-specific, file-backed history in the REPL
|
||||||
|
- Replay session output when a user re-enters a session so all output can be seen again
|
||||||
|
- Added confirmation message after MCP Oauth succeeds when invoked from --auth-mcp
|
||||||
|
- Created the --auth-mcp CLI flag to let users auth with remote MCP servers without needing to be in the REPL
|
||||||
|
- add OAuth authentication support for remote MCP servers
|
||||||
|
- Added mixin for sisyphus so the ddg MCP server can search arbitrary domains
|
||||||
|
- added improved error messaging on MCP server initialization
|
||||||
|
- prefer musl versions for linux when running --update/.update
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fresh wizard openai-compatible support
|
||||||
|
- config existence check for --fresh sandboxes
|
||||||
|
- Improve coyote sandbox startup time
|
||||||
|
- bypass forgotten sandbox mode check for MCP secret interpolation
|
||||||
|
- properly wrap sub-style changes in markdown rendering
|
||||||
|
- removed accidental duplicate ast_grep tool in explore agent
|
||||||
|
- npm and npx need the /usr/local/share/npm-global/lib directory to exist to run properly so I've added it to the dockerfile
|
||||||
|
- added executable bit to adversary agent tools script
|
||||||
|
- fetch descriptions from graph agent configs as well when listing agents
|
||||||
|
- **render**: suppress blank lines above rendered table
|
||||||
|
- chown the full sandbox cache dir, not just the coyote subdir
|
||||||
|
- chown the whole coyote cache dir not just the oauth dir in the sandbox
|
||||||
|
- fix typo in Gemini's generation_config property to use camelCase exclusively
|
||||||
|
- **oauth**: treat missing expires_in as non-expiring device_code token
|
||||||
|
- **oauth**: send Accept: application/json in device flow requests
|
||||||
|
- OAuth callback listener skips speculative/malformed browser connections
|
||||||
|
- resolve reasoning effort for the prompt for global defaults as well
|
||||||
|
- Account for default model reasoning_effort when supplying that value for the REPL prompts
|
||||||
|
- model narration included in history and between tool calls to prevent repetition
|
||||||
|
- Don't terminate agent loops early for null tool output
|
||||||
|
- reduce code duplication by reusing the new concrete_tool_names function in .list tools
|
||||||
|
- Agent tools can only be modified via .tool enable/disable using tools in the allowed whitelist in the agent
|
||||||
|
- re-render agent sessions when entering agents with either pre-configured agent_session or when entering an agent directly into a session
|
||||||
|
- Per RFC 9728, enable dynamic discovery of OAuth endpoints in MCP using path-aware discovery
|
||||||
|
- hot-attach to MCP servers that require auth after running .mcp auth <name>
|
||||||
|
- Correctly inherit graph-global model for extractor model if none is defined
|
||||||
|
- no cursor timeout when user scrolls away from ongoing streaming output
|
||||||
|
- default to the nano or notepad when a configured editor is not found
|
||||||
|
- When EDITOR, VISUAL, or config.editor is defined, don't verify via which
|
||||||
|
- Added a loop exit condition for the diagnostics skill
|
||||||
|
- Added directness clause to the diganose role to improve prompt
|
||||||
|
- fs tools now output better error handling to guide the model more effectively
|
||||||
|
- Make fs_read more tolerant of various arg invocation formats.
|
||||||
|
- todo functions are injected properly to roles when roles have auto_continue: true and the REPL is started directly into the role
|
||||||
|
- updated the redirect URI for OAuth MCP to use localhost since that's what is whitelisted, not 127.0.0.1
|
||||||
|
- allow MCP OAuth refresh_token to be absent from initial token exchanges
|
||||||
|
- Overrode the default JSON content-type for MCP OAuth so its properly application/x-www-form-urlencoded
|
||||||
|
- typo in mcp file name
|
||||||
|
- Added uvx wrapper for macos-based sandboxes
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Standardized paths module function names to not use 'path' in the name and to just always be either 'dir' or 'file'
|
||||||
|
- main.rs resolve_oauth_client uses new dispatcher
|
||||||
|
- split run_oauth_flow into pkce + client_credentials dispatchers
|
||||||
|
|
||||||
|
### Perf
|
||||||
|
|
||||||
|
- updated the memory injection warning so it only logs once, rather than after each keystroke
|
||||||
|
|
||||||
|
## v0.7.4 (2026-07-02)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- Pin specific usql version to sbx kit
|
||||||
|
- recursively take ownership over the copied in coyote config for the sbx
|
||||||
|
- explicitly specify the COYOTE_CONFIG_DIR in the sbx kit
|
||||||
|
- --tail-logs can track log rollovers and incoporates a sleep timer to minimize idle CPU cycles
|
||||||
|
- Added support for log rolling so log files don't just blow up over time
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- Added back in --kit specification for the running of the sbx
|
||||||
|
- sbx isn't copying base files in their respective directories
|
||||||
|
- Update deprecated sbx kit config
|
||||||
|
- Properly chown the coyote config recursively and password file in the sbx
|
||||||
|
|
||||||
|
## v0.7.3 (2026-06-24)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- apply bootstrapping of functions at startup to fix edge case
|
||||||
|
|
||||||
|
## v0.7.2 (2026-06-19)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- usql version upgrade
|
||||||
|
|
||||||
|
## v0.7.1 (2026-06-19)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- sbx mixins must be passed in directories, not as files and the files must be named spec.yaml per new sbx version
|
||||||
|
|
||||||
|
## v0.7.0 (2026-06-18)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- added configurable cache path via the COYOTE_CACHE_PATH environment variable
|
||||||
|
- added a memory option to .set tab completions
|
||||||
|
- Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts
|
||||||
|
- Added additional info outputs for enabled skills and sbx directories
|
||||||
|
- directly execute shell commands from within the REPL
|
||||||
|
- created mixin kit for built-in functions and MCP servers
|
||||||
|
- Added sbx mixins for the secrets providers so users can also bootstrap those as well.
|
||||||
|
- added support for loading sbx mixins that are dynamically discovered in the users workspace and config directory
|
||||||
|
- Added a --fresh flag to let users create a truly bare bones sandbox without bootstrapping their config
|
||||||
|
- initial built-in sandboxing support powered by Docker sbx
|
||||||
|
- Added the ability to auto-bootstrap workspace memory when in git repos
|
||||||
|
- Added explicit guardrail handling for pending agents
|
||||||
|
- auto-append memory to memory index and don't necessarily require the LLM to remember to do it after a write
|
||||||
|
- Added an --init-memory [global|workspace] flag to easily and quickly enable memory
|
||||||
|
- added memory global configuration settings to the output of --info and .info
|
||||||
|
- added .set memory REPL commands to control memory injection and applied formatting
|
||||||
|
- Create the built-in memory management tools
|
||||||
|
- Append the memory system prompts (readonly or r/w) to the system prompt when applicable
|
||||||
|
- Created the --no-memory CLI flag to disable memory for this invocation
|
||||||
|
- Added the memory configuration properties and storage to the main app config, roles, sessions, and agents.
|
||||||
|
- initial scaffolding of a memory system
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- rebuild the tool scope after dynamically updating the skills_enabled value in the REPL
|
||||||
|
- properly resolve Windows-based local vault password file locations and bootstrap them into the sandbox when possible
|
||||||
|
- auto-translation of user-prefixed Mac and Linux paths for the vault password file when running inside a sandbox
|
||||||
|
- don't attempt to auto complete .vault list in the REPL; that's the end of the command
|
||||||
|
- buffer tool stdout as well as stderr so that any tools that error to stdout are captured and included in the response to the model, enabling the model to see what went wrong and to reason about how to fix it.
|
||||||
|
- auto-bootstrapped memory was accidentally putting the MEMORY.md directly in the repo root rather than .coyote/memory/MEMORY.md
|
||||||
|
- improved the fs_patch script description and added improved error handling to it.
|
||||||
|
- added in forgotten require_max_tokens to the fable model
|
||||||
|
- append memory functions to non-graph based agents on init
|
||||||
|
- when auto_continue is disabled via the .set auto_continue false command, it should strip the todo functions from the list of functions
|
||||||
|
- use rawPredict for non-streaming Claude requests
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Migrated the .skills command completion to use StateFlags and updated the help messages
|
||||||
|
|
||||||
|
## v0.6.0 (2026-06-05)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- added skill hint prompt injection and configuration
|
||||||
|
- Fallthrough on missing secrets during mcp.json merging
|
||||||
|
- validate visible_skills field at config load time
|
||||||
|
- implemented reflexion (sorta) in sisyphus for significant code changes to delegate to the code-reviewer agent
|
||||||
|
- improved explore agent
|
||||||
|
- removed conditional fallback of LLM_*_RAW_JSON from built-ins
|
||||||
|
- updated enabled_skills handling to support both list and comma-separated strings
|
||||||
|
- added new REPL set commands for toggling skills and changing what skills are enabled
|
||||||
|
- upgraded to the latest version of mcp-remote
|
||||||
|
- fs_grep now works with both files and directories
|
||||||
|
- improved code reviewer agents with skills
|
||||||
|
- added round trip validation for vault providers to ensure permissions and authentication
|
||||||
|
- created new first-time run wizard for secrets provider
|
||||||
|
- vault_password_file or nothing at all is shorthand for just using the local gman provider for secret management
|
||||||
|
- refactored gman usage to be generic and work with various vault providers and use the SupportedProvider enum directly for configurations
|
||||||
|
- created initial parity gman generalization for vault provider
|
||||||
|
- Refactored the sisyhpus agent system to utilize the new skills system to improve performance and reliability
|
||||||
|
- llm graph nodes support skills
|
||||||
|
- updated sisyphus and coder tools
|
||||||
|
- removed potentially confusing tab completions for .skill
|
||||||
|
- .edit skill <name> support from within the REPL
|
||||||
|
- Added skills_dir to the info output of Coyote
|
||||||
|
- Created a few auto built-in skills
|
||||||
|
- Added support for auto_unload skills during chat
|
||||||
|
- cleaned up skill implementation
|
||||||
|
- support multiple skill flags to load multiple skills at CLI startup
|
||||||
|
- Modified --skill CLI to allow users to specify skills to start the REPL or CLI with.
|
||||||
|
- added CLI --skill flag for modifying skills easily
|
||||||
|
- REPL integration with skills
|
||||||
|
- dynamic loading/unloading of skill tools and MCP servers whenever load_skill/unload_skill are invoked
|
||||||
|
- created built-in functions for listing, loading, and unloading skills
|
||||||
|
- implemented the skills policy to track available skills per context
|
||||||
|
- added remote install and install support for skills
|
||||||
|
- created the skill registry
|
||||||
|
- decided to make skills persist to disk like agents and not in-memory like built-in roles
|
||||||
|
- scaffold skill module
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- disable skills for specific built-in roles
|
||||||
|
- redirect stderr into user's /dev/tty for guards
|
||||||
|
- azure doesn't support underscores in key vault
|
||||||
|
- accidental regression on enabled_skills being empty = all
|
||||||
|
- greedy secrets regex caused multiple secrets on one line to fail
|
||||||
|
- add agent context check to skill visibility validation
|
||||||
|
- enforced global visible_skills in llm node validation and improved skill loading error handling across the project
|
||||||
|
- restore agent skill policy on error during effective policy calculation
|
||||||
|
- apply the same validation for skill filenames on list_skills as happens everywhere else
|
||||||
|
- the vault's init_bare should try to load the provisioned secret_provider from the config file without also interpolating any of the rest of the configuration file. It should only fail if the user has not yet created a configuration file; i.e. done a first-time run.
|
||||||
|
- the vault roundtrip test used characters that are unsupported by some major secrets providers
|
||||||
|
- fixed tool filtering logic for skills and user functions in agents
|
||||||
|
- privilege leak when unloading skills and leaving tool scope untouched
|
||||||
|
- When bootstrapping an app config to interpolate secrets, clone the secrets provider configuration as well so config secrets stored in remote vaults can be used properly
|
||||||
|
- forgot to move back up the vault probe value error to be before the delete
|
||||||
|
- don't silently fail on skill role composition extraction in llm nodes
|
||||||
|
- set -euo pipefail for the temp script in execute_command.sh tool
|
||||||
|
- added forgotten skill name validation to has_skill to prevent side-channel attacks
|
||||||
|
- use unique values for the secrets round trip verification
|
||||||
|
- stop interpolating a line if any errors occur
|
||||||
|
- added path validation for skill names
|
||||||
|
- effective_policy unconditionally overwrote skill values for role-like structs
|
||||||
|
- updated execute_command to not mangle heredocs and also added explicit instructions to the coder and sisyphus agents to use fs_write and fs_patch over execute_command when writing files
|
||||||
|
- llm nodes accidentally skipped skill_registry::effective_role because I was passing an inline role instead
|
||||||
|
- updated temperature values for all agents and roles
|
||||||
|
- added back in require_max_tokens for new Claude models
|
||||||
|
- skill support also requires function calling to be enabled
|
||||||
|
- non_tty tests break on some TTY terminals
|
||||||
|
- skill loading on agents
|
||||||
|
- forgot to bootstrap skills on REPL startup
|
||||||
|
- remove now deprecated .skill edit command
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- removed redundant skill name validation from has_skill function
|
||||||
|
- support both CSV and list formats for enabled_tools
|
||||||
|
- Support both CSV and list formats for enabled_mcp_servers
|
||||||
|
|
||||||
|
## v0.5.0 (2026-05-27)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- rename Loki to Coyote
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- bash-based user interactions in agents accidentally regressed in graph implementation
|
||||||
|
- Claude function calling in agent contexts
|
||||||
|
- Claude code rate limit error per new Claude changes
|
||||||
|
|
||||||
## v0.4.0 (2026-05-23)
|
## v0.4.0 (2026-05-23)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
@@ -5,7 +719,7 @@
|
|||||||
- LLM node failures propgate up
|
- LLM node failures propgate up
|
||||||
- Added .install remote tab completions to the REPL
|
- Added .install remote tab completions to the REPL
|
||||||
- feature complete install remote with category selection
|
- feature complete install remote with category selection
|
||||||
- Support to interactively add secrets to Loki that are missing from MCP configs when merging
|
- Support to interactively add secrets to Coyote that are missing from MCP configs when merging
|
||||||
- Added MCP config merging support for remote asset installations
|
- Added MCP config merging support for remote asset installations
|
||||||
- install remote now writes files to disk
|
- install remote now writes files to disk
|
||||||
- Created basic install_remote functions
|
- Created basic install_remote functions
|
||||||
@@ -25,7 +739,7 @@
|
|||||||
- validation support for parallel graph execution; restricted map nodes to only run for nodes without next targets and not supporting chained map nodes
|
- validation support for parallel graph execution; restricted map nodes to only run for nodes without next targets and not supporting chained map nodes
|
||||||
- created the staging area for state merges per super-step and created the built-in reducers (and their application) for the state merge phase of a super step
|
- created the staging area for state merges per super-step and created the built-in reducers (and their application) for the state merge phase of a super step
|
||||||
- scaffolding work for fan-out nodes for parallel branch execution support and stubbed out Map node types
|
- scaffolding work for fan-out nodes for parallel branch execution support and stubbed out Map node types
|
||||||
- Loki can now update itself via .update and --update commands
|
- Coyote can now update itself via .update and --update commands
|
||||||
- added a .edit command for editing the MCP configuration file
|
- added a .edit command for editing the MCP configuration file
|
||||||
- Created a new .install command to install bundled assets on-demand
|
- Created a new .install command to install bundled assets on-demand
|
||||||
- migrated llm node validation to graph loading time instead of graph runtime
|
- migrated llm node validation to graph loading time instead of graph runtime
|
||||||
@@ -40,7 +754,7 @@
|
|||||||
- wired together graph execution and agent graph dispatch
|
- wired together graph execution and agent graph dispatch
|
||||||
- implemented support for the graph executor
|
- implemented support for the graph executor
|
||||||
- created the approval node executor and the input node executor for user interaction
|
- created the approval node executor and the input node executor for user interaction
|
||||||
- Added initial support for native Loki agent nodes in the graph-based agent system
|
- Added initial support for native Coyote agent nodes in the graph-based agent system
|
||||||
- Added direct script invocation support for graph-based agents
|
- Added direct script invocation support for graph-based agents
|
||||||
- Added graph validation
|
- Added graph validation
|
||||||
- Implemented state management for agent graphs
|
- Implemented state management for agent graphs
|
||||||
@@ -73,7 +787,7 @@
|
|||||||
- check for an existing session before starting up MCP servers when switching to a role
|
- check for an existing session before starting up MCP servers when switching to a role
|
||||||
- do not switch to agent if a session is active.
|
- do not switch to agent if a session is active.
|
||||||
- Do not append todo instructions when function calling is disabled
|
- Do not append todo instructions when function calling is disabled
|
||||||
- a bug in the dynamic completions because the crate name is loki-ai but the binary is named loki
|
- a bug in the dynamic completions because the crate name is coyote-ai but the binary is named coyote
|
||||||
- bug found by copilot that would create a lock on the PollSender for sse-based MCP servers
|
- bug found by copilot that would create a lock on the PollSender for sse-based MCP servers
|
||||||
- Accidental shadow of temp_file function for Windows function calling
|
- Accidental shadow of temp_file function for Windows function calling
|
||||||
- upgraded to newer rmcp version to get native-tls support
|
- upgraded to newer rmcp version to get native-tls support
|
||||||
@@ -120,7 +834,7 @@
|
|||||||
- Created a CodeRabbit-style code-reviewer agent
|
- Created a CodeRabbit-style code-reviewer agent
|
||||||
- Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes)
|
- Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes)
|
||||||
- Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
|
- Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
|
||||||
- built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Loki
|
- built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Coyote
|
||||||
- Experimental update to sisyphus to use the new parallel agent spawning system
|
- Experimental update to sisyphus to use the new parallel agent spawning system
|
||||||
- Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system)
|
- Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system)
|
||||||
- Auto-dispatch support of sub-agents and support for the teammate pattern between subagents
|
- Auto-dispatch support of sub-agents and support for the teammate pattern between subagents
|
||||||
@@ -174,7 +888,7 @@
|
|||||||
|
|
||||||
- Simplified sisyphus prompt to improve functionality
|
- Simplified sisyphus prompt to improve functionality
|
||||||
- Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses
|
- Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses
|
||||||
- Created the Sisyphus agent to make Loki function like Claude Code, Gemini, Codex, etc.
|
- Created the Sisyphus agent to make Coyote function like Claude Code, Gemini, Codex, etc.
|
||||||
- Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase
|
- Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase
|
||||||
- Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus
|
- Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus
|
||||||
- Created the explore agent for exploring codebases to help answer questions
|
- Created the explore agent for exploring codebases to help answer questions
|
||||||
@@ -234,8 +948,8 @@
|
|||||||
- Support for secret injection into the global config file (API keys, for example)
|
- Support for secret injection into the global config file (API keys, for example)
|
||||||
- Improved MCP handling toggle handling
|
- Improved MCP handling toggle handling
|
||||||
- Secret injection into the MCP configuration
|
- Secret injection into the MCP configuration
|
||||||
- added REPL support for interacting with the Loki vault
|
- added REPL support for interacting with the Coyote vault
|
||||||
- Integrated gman with Loki to create a vault and added flags to configure the Loki vault
|
- Integrated gman with Coyote to create a vault and added flags to configure the Coyote vault
|
||||||
- Added a default session to the jira helper to make interaction more natural
|
- Added a default session to the jira helper to make interaction more natural
|
||||||
- Created the repo-analyzer role
|
- Created the repo-analyzer role
|
||||||
- Created the coder and sql agents
|
- Created the coder and sql agents
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||||
|
|
||||||
## Rust
|
## Rust
|
||||||
You'll need to have the stable Rust toolchain installed in order to develop Loki.
|
You'll need to have the stable Rust toolchain installed in order to develop Coyote.
|
||||||
|
|
||||||
The Rust toolchain (stable) can be installed via rustup using the following command:
|
The Rust toolchain (stable) can be installed via rustup using the following command:
|
||||||
|
|
||||||
@@ -84,5 +84,5 @@ Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
|||||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||||
|
|
||||||
## Questions? Reach out to me!
|
## Questions? Reach out to me!
|
||||||
If you encounter any questions while developing Loki, please don't hesitate to reach out to me at
|
If you encounter any questions while developing Coyote, please don't hesitate to reach out to me at
|
||||||
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
||||||
+27
-7
@@ -1,19 +1,30 @@
|
|||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
|
## Matt Pocock's Skills
|
||||||
|
|
||||||
|
The bundled `diagnosing-bugs`, `codebase-design`, and `grilling` skills, the
|
||||||
|
`architecture-reviewer` agent, and the code smell baseline in the bundled
|
||||||
|
`code-review` skill are adapted from
|
||||||
|
[mattpocock/skills](https://github.com/mattpocock/skills) by Matt Pocock,
|
||||||
|
licensed under the MIT License. The smell definitions trace back to Martin
|
||||||
|
Fowler's *Refactoring* (ch. 3); the deep-module vocabulary builds on John
|
||||||
|
Ousterhout's *A Philosophy of Software Design* and Michael Feathers'
|
||||||
|
*Working Effectively with Legacy Code*.
|
||||||
|
|
||||||
## AIChat
|
## AIChat
|
||||||
Loki originally started as a fork of the fantastic
|
Coyote originally started as a fork of the fantastic
|
||||||
[AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply
|
[AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply
|
||||||
to fix a bug in how MCP servers worked with AIChat, allowing different MCP
|
to fix a bug in how MCP servers worked with AIChat, allowing different MCP
|
||||||
servers to be specified per agent. Since then, Loki has evolved far beyond
|
servers to be specified per agent. Since then, Coyote has evolved far beyond
|
||||||
its original scope and grown into a passion project with a life of its own.
|
its original scope and grown into a passion project with a life of its own.
|
||||||
|
|
||||||
Today, Loki includes first-class MCP server support (for both local and remote
|
Today, Coyote includes first-class MCP server support (for both local and remote
|
||||||
servers), a built-in vault for interpolating secrets in configuration files,
|
servers), a built-in vault for interpolating secrets in configuration files,
|
||||||
built-in agents and macros, dynamic tab completions, integrated custom
|
built-in agents and macros, dynamic tab completions, integrated custom
|
||||||
functions (no external `argc` dependency), improved documentation, and much
|
functions (no external `argc` dependency), improved documentation, and much
|
||||||
more with many more ideas planned for the future.
|
more with many more ideas planned for the future.
|
||||||
|
|
||||||
Loki is now developed and maintained as an independent project. Full credit
|
Coyote is now developed and maintained as an independent project. Full credit
|
||||||
for the original foundation goes to the developers of the wonderful
|
for the original foundation goes to the developers of the wonderful
|
||||||
AIChat project.
|
AIChat project.
|
||||||
|
|
||||||
@@ -21,11 +32,20 @@ This project is not affiliated with or endorsed by the AIChat maintainers.
|
|||||||
|
|
||||||
## AIChat
|
## AIChat
|
||||||
|
|
||||||
Loki originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat),
|
Coyote originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat),
|
||||||
created and maintained by the AIChat contributors.
|
created and maintained by the AIChat contributors.
|
||||||
|
|
||||||
While Loki has since diverged significantly and is now developed as an
|
While Coyote has since diverged significantly and is now developed as an
|
||||||
independent project, its early foundation and inspiration came from the
|
independent project, its early foundation and inspiration came from the
|
||||||
AIChat project.
|
AIChat project.
|
||||||
|
|
||||||
AIChat is licensed under the MIT License.
|
AIChat is licensed under the MIT License. The MIT license text and its
|
||||||
|
copyright notice are preserved in the [LICENSE-MIT](./LICENSE-MIT) file.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
Coyote as a whole is licensed under the GNU Affero General Public License
|
||||||
|
v3.0 only (AGPL-3.0-only); see [LICENSE](./LICENSE). Substantial portions
|
||||||
|
derived from AIChat remain under the MIT License (Copyright (c) sigoden),
|
||||||
|
preserved in [LICENSE-MIT](./LICENSE-MIT). See [NOTICE](./NOTICE) for the
|
||||||
|
combined-licensing summary.
|
||||||
Generated
+1560
-968
File diff suppressed because it is too large.
Load diff
+29
-18
@@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "loki-ai"
|
name = "coyote-ai"
|
||||||
version = "0.4.0"
|
version = "0.10.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||||
description = "An all-in-one, batteries included LLM CLI Tool"
|
description = "The batteries-included runtime for LLMs"
|
||||||
keywords = ["chatgpt", "llm", "cli", "ai", "repl"]
|
keywords = ["chatgpt", "llm", "cli", "ai", "repl"]
|
||||||
homepage = "https://github.com/Dark-Alex-17/loki"
|
homepage = "https://github.com/Dark-Alex-17/coyote"
|
||||||
repository = "https://github.com/Dark-Alex-17/loki"
|
repository = "https://github.com/Dark-Alex-17/coyote"
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "AGPL-3.0-only"
|
||||||
rust-version = "1.95.0"
|
rust-version = "1.95.0"
|
||||||
exclude = [".github", "CONTRIBUTING.md"]
|
exclude = [".github", "CONTRIBUTING.md"]
|
||||||
|
|
||||||
@@ -17,7 +17,9 @@ exclude = [".github", "CONTRIBUTING.md"]
|
|||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
bytes = "1.4.0"
|
bytes = "1.4.0"
|
||||||
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
||||||
|
comfy-table = { version = "7.1.4", features = ["custom_styling"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
|
duckdb = { version = "1.10505.0", features = ["bundled"] }
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
futures-util = "0.3.29"
|
futures-util = "0.3.29"
|
||||||
inquire = "0.9.4"
|
inquire = "0.9.4"
|
||||||
@@ -49,7 +51,13 @@ textwrap = "0.16.0"
|
|||||||
ansi_colours = "1.2.2"
|
ansi_colours = "1.2.2"
|
||||||
eventsource-stream = "0.2.3"
|
eventsource-stream = "0.2.3"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
log4rs = { version = "1.4.0", features = ["file_appender"] }
|
log4rs = { version = "1.4.0", features = [
|
||||||
|
"file_appender",
|
||||||
|
"rolling_file_appender",
|
||||||
|
"compound_policy",
|
||||||
|
"fixed_window_roller",
|
||||||
|
"size_trigger",
|
||||||
|
] }
|
||||||
shell-words = "1.1.0"
|
shell-words = "1.1.0"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
unicode-width = "0.2.0"
|
unicode-width = "0.2.0"
|
||||||
@@ -58,6 +66,8 @@ http = "1.1.0"
|
|||||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
aws-smithy-eventstream = "0.60.4"
|
aws-smithy-eventstream = "0.60.4"
|
||||||
|
aws-smithy-types = "=1.4.9"
|
||||||
|
time = "=0.3.47"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.0.0", default-features = false }
|
||||||
bitflags = "2.5.0"
|
bitflags = "2.5.0"
|
||||||
@@ -72,6 +82,7 @@ html_to_markdown = "0.1.0"
|
|||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
os_info = { version = "3.8.2", default-features = false }
|
os_info = { version = "3.8.2", default-features = false }
|
||||||
bm25 = { version = "2.0.1", features = ["parallelism"] }
|
bm25 = { version = "2.0.1", features = ["parallelism"] }
|
||||||
|
petgraph = { version = "0.7", features = ["serde-1"] }
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
terminal-colorsaurus = "0.4.8"
|
terminal-colorsaurus = "0.4.8"
|
||||||
@@ -79,7 +90,7 @@ duct = "1.0.0"
|
|||||||
argc = "1.23.0"
|
argc = "1.23.0"
|
||||||
strum_macros = "0.27.2"
|
strum_macros = "0.27.2"
|
||||||
indoc = "2.0.6"
|
indoc = "2.0.6"
|
||||||
rmcp = { version = "1.5.0", features = [
|
rmcp = { version = "3.1.2", features = [
|
||||||
"client",
|
"client",
|
||||||
"transport-child-process",
|
"transport-child-process",
|
||||||
"transport-streamable-http-client-reqwest",
|
"transport-streamable-http-client-reqwest",
|
||||||
@@ -91,7 +102,7 @@ tree-sitter-python = "0.25.0"
|
|||||||
tree-sitter-typescript = "0.23"
|
tree-sitter-typescript = "0.23"
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
|
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
|
||||||
gman = "0.4.1"
|
gman = "0.5.0"
|
||||||
clap_complete_nushell = "4.5.9"
|
clap_complete_nushell = "4.5.9"
|
||||||
open = "5"
|
open = "5"
|
||||||
rand = { version = "0.10.0", features = ["default"] }
|
rand = { version = "0.10.0", features = ["default"] }
|
||||||
@@ -104,17 +115,11 @@ self_update = { version = "0.44", default-features = false, features = [
|
|||||||
"archive-zip",
|
"archive-zip",
|
||||||
"compression-zip-deflate",
|
"compression-zip-deflate",
|
||||||
] }
|
] }
|
||||||
|
qrcode = "0.14"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
features = [
|
features = ["json", "multipart", "stream", "form", "socks", "rustls"]
|
||||||
"json",
|
|
||||||
"multipart",
|
|
||||||
"stream",
|
|
||||||
"form",
|
|
||||||
"socks",
|
|
||||||
"rustls",
|
|
||||||
]
|
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.syntect]
|
[dependencies.syntect]
|
||||||
@@ -133,12 +138,18 @@ arboard = { version = "3.3.0", default-features = false, features = [
|
|||||||
[target.'cfg(not(any(target_os = "linux", target_os = "android", target_os = "emscripten")))'.dependencies]
|
[target.'cfg(not(any(target_os = "linux", target_os = "android", target_os = "emscripten")))'.dependencies]
|
||||||
arboard = { version = "3.3.0", default-features = false }
|
arboard = { version = "3.3.0", default-features = false }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
ctor = "1.0.13"
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
|
rmcp = { version = "3.1.2", features = ["server"] }
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
tokio = { version = "1.34.0", features = ["test-util"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "loki"
|
name = "coyote"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
ARG COYOTE_VERSION
|
||||||
|
FROM docker/sandbox-templates:shell-docker AS build
|
||||||
|
|
||||||
|
ARG COYOTE_VERSION
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
ENV PATH="/home/agent/.cargo/bin:/home/agent/.local/bin:${PATH}"
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
jq curl git \
|
||||||
|
build-essential pkg-config \
|
||||||
|
cmake \
|
||||||
|
clang libclang-dev \
|
||||||
|
musl-tools \
|
||||||
|
libssl-dev \
|
||||||
|
pandoc \
|
||||||
|
bzip2 \
|
||||||
|
nano && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN set -euo pipefail; \
|
||||||
|
USQL_VERSION=0.21.4; \
|
||||||
|
case "${TARGETARCH}" in \
|
||||||
|
amd64) USQL_ARCH=amd64 ;; \
|
||||||
|
arm64) USQL_ARCH=arm64 ;; \
|
||||||
|
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
TMPDIR=$(mktemp -d); \
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/xo/usql/releases/download/v${USQL_VERSION}/usql_static-${USQL_VERSION}-linux-${USQL_ARCH}.tar.bz2" \
|
||||||
|
-o "$TMPDIR/usql.tar.bz2"; \
|
||||||
|
tar -xjf "$TMPDIR/usql.tar.bz2" -C "$TMPDIR"; \
|
||||||
|
install -m 0755 "$TMPDIR/usql_static" /usr/local/bin/usql; \
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
RUN set -euo pipefail; \
|
||||||
|
DUCKDB_VERSION=1.5.5; \
|
||||||
|
case "${TARGETARCH}" in \
|
||||||
|
amd64) DUCKDB_ARCH=amd64 ;; \
|
||||||
|
arm64) DUCKDB_ARCH=arm64 ;; \
|
||||||
|
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
TMPDIR=$(mktemp -d); \
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb_cli-linux-${DUCKDB_ARCH}.gz" \
|
||||||
|
-o "$TMPDIR/duckdb.gz"; \
|
||||||
|
gunzip "$TMPDIR/duckdb.gz"; \
|
||||||
|
install -m 0755 "$TMPDIR/duckdb" /usr/local/bin/duckdb; \
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||||
|
printf '#!/bin/sh\nexec uv tool run "$@"\n' > "$HOME/.local/bin/uvx" && \
|
||||||
|
chmod +x "$HOME/.local/bin/uvx"
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/local/share/npm-global/lib
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||||
|
sh -s -- -y --default-toolchain stable --profile minimal && \
|
||||||
|
. "$HOME/.cargo/env" && \
|
||||||
|
cargo install --locked iwec && \
|
||||||
|
cargo install --locked ast-grep
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN set -euo pipefail; \
|
||||||
|
case "${TARGETARCH}" in \
|
||||||
|
amd64) MUSL_TARGET=x86_64-unknown-linux-musl ;; \
|
||||||
|
arm64) MUSL_TARGET=aarch64-unknown-linux-musl ;; \
|
||||||
|
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
TMPDIR=$(mktemp -d); \
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/Dark-Alex-17/coyote/releases/download/v${COYOTE_VERSION}/coyote-${MUSL_TARGET}.tar.gz" \
|
||||||
|
-o "$TMPDIR/coyote.tar.gz"; \
|
||||||
|
tar -xzf "$TMPDIR/coyote.tar.gz" -C "$TMPDIR"; \
|
||||||
|
install -m 0755 "$TMPDIR/coyote" /home/agent/.cargo/bin/coyote; \
|
||||||
|
chown 1000:1000 /home/agent/.cargo/bin/coyote; \
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
ARG COYOTE_VERSION
|
||||||
|
|
||||||
|
COPY --from=build / /
|
||||||
|
|
||||||
|
ENV PATH="/home/agent/.cargo/bin:/home/agent/.local/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
||||||
|
NPM_CONFIG_PREFIX="/usr/local/share/npm-global" \
|
||||||
|
NO_PROXY="localhost,127.0.0.1,::1,172.17.0.0/16" \
|
||||||
|
no_proxy="localhost,127.0.0.1,::1,172.17.0.0/16" \
|
||||||
|
BASH_ENV="/etc/sandbox-persistent.sh"
|
||||||
|
|
||||||
|
LABEL com.docker.sandboxes="templates" \
|
||||||
|
com.docker.sandboxes.base="ubuntu:questing" \
|
||||||
|
com.docker.sandboxes.flavor="shell-docker" \
|
||||||
|
com.docker.sandboxes.start-docker="true" \
|
||||||
|
org.opencontainers.image.title="coyote" \
|
||||||
|
org.opencontainers.image.description="The batteries-included runtime for LLMs: Shell Assistant, CLI & REPL mode, RAG, AI tools & agents, MCP servers, skills, and macros." \
|
||||||
|
org.opencontainers.image.source="https://github.com/Dark-Alex-17/coyote" \
|
||||||
|
org.opencontainers.image.version="${COYOTE_VERSION}"
|
||||||
|
|
||||||
|
WORKDIR /home/agent/workspace
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
ENTRYPOINT ["coyote"]
|
||||||
@@ -1,22 +1,661 @@
|
|||||||
The MIT License (MIT)
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (c) 2025 sigoden
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Copyright (c) 2025 Alexander J. Clarke
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2025 sigoden
|
||||||
|
Copyright (c) 2025 Alexander J. Clarke
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Coyote
|
||||||
|
Copyright (c) 2025 Alexander J. Clarke
|
||||||
|
|
||||||
|
This project as a whole is licensed under the GNU Affero General Public
|
||||||
|
License, version 3.0 only (AGPL-3.0-only). The full text of that license is
|
||||||
|
provided in the LICENSE file.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Upstream / third-party notices
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Coyote began as a fork of AIChat (https://github.com/sigoden/aichat),
|
||||||
|
Copyright (c) sigoden, which is distributed under the MIT License. Substantial
|
||||||
|
portions of Coyote are derived from AIChat and remain available under the terms
|
||||||
|
of the MIT License. The MIT License text and its required copyright and
|
||||||
|
permission notices are preserved in the LICENSE-MIT file.
|
||||||
|
|
||||||
|
As permitted by the MIT License, these portions have been incorporated into a
|
||||||
|
larger work that is distributed under the AGPL-3.0-only license. When you
|
||||||
|
receive Coyote as a combined work, your rights and obligations for the work as
|
||||||
|
a whole are governed by the AGPL-3.0-only license; the MIT notice is retained
|
||||||
|
to satisfy the attribution requirements of the MIT-licensed portions.
|
||||||
|
|
||||||
|
See CREDITS.md for additional background and attribution.
|
||||||
@@ -1,54 +1,72 @@
|
|||||||
# Loki: All-in-one, batteries-included LLM CLI Tool
|
# Coyote: The batteries-included runtime for LLMs
|
||||||
|
|
||||||

|

|
||||||
[](https://crates.io/crates/loki-ai)
|
[](https://crates.io/crates/coyote-ai)
|
||||||

|

|
||||||

|

|
||||||
[](https://github.com/Dark-Alex-17/loki/releases)
|
[](https://github.com/Dark-Alex-17/coyote/releases)
|
||||||
|

|
||||||
|
[](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE)
|
||||||
|
|
||||||
Loki is an all-in-one, batteries-included, LLM CLI tool featuring Shell Assistant, CLI & REPL Mode, RAG, AI Tools &
|
Coyote is an **all-in-one, batteries-included LLM runtime** for building, running, and interacting with AI from your terminal.
|
||||||
Agents, and More.
|
It brings together a Shell Assistant, CLI & REPL modes, RAG, tools, agents, MCP, skills, sandboxes, multi-agent workflows, and
|
||||||
|
more in a single runtime.
|
||||||
|
|
||||||
It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Loki
|
Coyote comes ready to use with built-in agents, roles, macros, and tools, so you can get started without assembling an AI
|
||||||
in as little time as possible. You can also install entire bundles of agents, roles, macros, tools, and MCP servers from
|
stack from scratch. When you want to extend it, entire bundles of agents, roles, macros, tools, MCP servers, and other
|
||||||
any git repository. See [Sharing Configurations](https://github.com/Dark-Alex-17/loki/wiki/Sharing-Configurations) for more information.
|
configurations can be installed directly from any Git repository.
|
||||||
|
|
||||||

|
See [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles) to learn how to create, install, and share Coyote bundles.
|
||||||
|
|
||||||
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration) to get started.
|
|
||||||
|

|
||||||
|
|
||||||
|
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration) to get started.
|
||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
||||||
* [Installation](#install): Install Loki
|
* [Installation](#install): Install Coyote
|
||||||
* [Getting Started](#getting-started): Get started with Loki by doing first-run setup steps.
|
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
|
||||||
* [Sharing Configurations](https://github.com/Dark-Alex-17/loki/wiki/Sharing-Configurations): Install bundles of agents, roles, macros, tools, and MCP servers from any git repo, and share your own.
|
* [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
|
||||||
* [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
|
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
|
||||||
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/loki/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
||||||
* [Vault](https://github.com/Dark-Alex-17/loki/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
||||||
* [Shell Integrations](https://github.com/Dark-Alex-17/loki/wiki/Shell-Integrations): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
|
* [Sandboxes](https://github.com/Dark-Alex-17/coyote/wiki/Sandboxes): Launch Coyote inside an isolated [Docker Sandbox](https://docs.docker.com/ai/sandboxes/) with one command. Host config and vault credentials are projected in automatically; everything else is delegated to the `sbx` CLI.
|
||||||
* [Function Calling](https://github.com/Dark-Alex-17/loki/wiki/Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
|
* [Shell Integrations](https://github.com/Dark-Alex-17/coyote/wiki/Shell-Integrations): Seamlessly integrate Coyote with your shell environment for enhanced command-line assistance.
|
||||||
* [Creating Custom Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools): You can create your own custom tools to enhance Loki's capabilities.
|
* [Function Calling](https://github.com/Dark-Alex-17/coyote/wiki/Tools): Leverage function calling capabilities to extend Coyote's functionality with custom tools
|
||||||
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-python-based-tools)
|
* [Creating Custom Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools): You can create your own custom tools to enhance Coyote's capabilities.
|
||||||
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-typescript-based-tools)
|
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-python-based-tools)
|
||||||
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Bash-Tools)
|
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-typescript-based-tools)
|
||||||
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/loki/wiki/Bash-Prompt-Helpers)
|
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Bash-Tools)
|
||||||
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
|
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/coyote/wiki/Bash-Prompt-Helpers)
|
||||||
* [Macros](https://github.com/Dark-Alex-17/loki/wiki/Macros): Automate repetitive tasks and workflows with Loki "scripts" (macros).
|
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality. Coyote supports all three MCP capabilities: tools, resources, and prompts.
|
||||||
* [RAG](https://github.com/Dark-Alex-17/loki/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model.
|
||||||
* [Sessions](https://github.com/Dark-Alex-17/loki/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`.
|
||||||
* [Roles](https://github.com/Dark-Alex-17/loki/wiki/Roles): Customize model behavior for specific tasks or domains.
|
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands.
|
||||||
* [Agents](https://github.com/Dark-Alex-17/loki/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends, by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
|
||||||
* [Graph Agents](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
* Commit project-specific macros to `.coyote/macros/` in your repo. They shadow same-named global macros (opt out with `--no-workspace-macros`).
|
||||||
* [Todo System](https://github.com/Dark-Alex-17/loki/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default.
|
||||||
* [Environment Variables](https://github.com/Dark-Alex-17/loki/wiki/Environment-Variables): Override and customize your Loki configuration at runtime with environment variables.
|
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`.
|
||||||
* [Client Configurations](https://github.com/Dark-Alex-17/loki/wiki/Clients): Configuration instructions for various LLM providers.
|
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||||
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
* [Sessions](https://github.com/Dark-Alex-17/coyote/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
||||||
* [Patching API Requests](https://github.com/Dark-Alex-17/loki/wiki/Patches): Learn how to patch API requests for advanced customization.
|
* [Memory](https://github.com/Dark-Alex-17/coyote/wiki/Memory): Persistent file-based memory that survives across sessions. Bootstrap with `coyote --init-memory [global|workspace]`.
|
||||||
* [Custom Themes](https://github.com/Dark-Alex-17/loki/wiki/Themes): Change the look and feel of Loki to your preferences with custom themes.
|
* [Workspace Instructions](https://github.com/Dark-Alex-17/coyote/wiki/Workspace-Instructions): Human-curated project instructions (`COYOTE.md`) injected into every prompt, with `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` fallbacks for cross-tool compatibility. Scaffold with `coyote --init-instructions`.
|
||||||
* [History](#history): A history of how Loki came to be.
|
* [Roles](https://github.com/Dark-Alex-17/coyote/wiki/Roles): Customize model behavior for specific tasks or domains.
|
||||||
|
* [Skills](https://github.com/Dark-Alex-17/coyote/wiki/Skills): Modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills compose; instructions stack, tools and MCPs union.
|
||||||
|
* [Agents](https://github.com/Dark-Alex-17/coyote/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||||
|
* [Graph Agents](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
||||||
|
* [Background Jobs](https://github.com/Dark-Alex-17/coyote/wiki/Background-Jobs): Run long tool calls (builds, test suites, slow MCP calls) in the background with the `job__*` tools while the model keeps working, and completion arrives as a push notification.
|
||||||
|
* [Todo System](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
||||||
|
* [Environment Variables](https://github.com/Dark-Alex-17/coyote/wiki/Environment-Variables): Override and customize your Coyote configuration at runtime with environment variables.
|
||||||
|
* [Client Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Clients): Configuration instructions for various LLM providers.
|
||||||
|
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
||||||
|
* [Patching API Requests](https://github.com/Dark-Alex-17/coyote/wiki/Patches): Learn how to patch API requests for advanced customization.
|
||||||
|
* [Dynamic Model Catalog](https://github.com/Dark-Alex-17/coyote/wiki/Model-Catalog): Coyote's model catalog (context windows, pricing, capabilities) is generated from live API data, refreshed automatically in the background, and mirrorable for air-gapped setups. New models show up without waiting for a release.
|
||||||
|
* [Custom Themes](https://github.com/Dark-Alex-17/coyote/wiki/Themes): Change the look and feel of Coyote to your preferences with custom themes.
|
||||||
|
* [History](#history): A history of how Coyote came to be.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
Loki requires the following tools to be installed on your system:
|
Coyote requires the following tools to be installed on your system:
|
||||||
* [jq](https://github.com/jqlang/jq)
|
* [jq](https://github.com/jqlang/jq)
|
||||||
* `brew install jq`
|
* `brew install jq`
|
||||||
* [usql](https://github.com/xo/usql) (For the `sql` agent)
|
* [usql](https://github.com/xo/usql) (For the `sql` agent)
|
||||||
@@ -56,58 +74,94 @@ Loki requires the following tools to be installed on your system:
|
|||||||
* [docker](https://docs.docker.com/engine/install/)
|
* [docker](https://docs.docker.com/engine/install/)
|
||||||
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
* `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
* `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||||
|
* [iwe](https://github.com/iwe-org/iwe) (`iwec`, for the built-in `iwe` MCP server that navigates large markdown knowledgebases)
|
||||||
|
* **Homebrew:** `brew tap iwe-org/iwe && brew trust --formula iwe-org/iwe/iwe && brew install iwe`
|
||||||
|
* **Cargo:** `cargo install iwec`
|
||||||
|
* [ast-grep](https://ast-grep.github.io/) (for the built-in `ast_grep` structural code search tool, used by the `explore` agent)
|
||||||
|
* **Homebrew:** `brew install ast-grep`
|
||||||
|
* **Cargo:** `cargo install ast-grep --locked`
|
||||||
|
* **npm:** `npm i -g @ast-grep/cli`
|
||||||
|
* Optional: if `ast-grep` is not installed, the `ast_grep` tool reports it and agents fall back to `fs_grep`
|
||||||
|
* [duckdb](https://duckdb.org/) (for fast, local RAGs)
|
||||||
|
* `curl https://install.duckdb.org | bash`
|
||||||
|
|
||||||
These tools are used to provide various functionalities within Loki, such as document processing, JSON manipulation,
|
These tools are used to provide various functionalities within Coyote, such as document processing, JSON manipulation,
|
||||||
etc., and they are used within agents and tools.
|
etc., and they are used within agents and tools.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
If you have Cargo installed, then you can install `loki` from Crates.io:
|
If you have Cargo installed, then you can install `coyote` from Crates.io:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install loki-ai # Binary name is `loki`
|
cargo install coyote-ai # Binary name is `coyote`
|
||||||
|
|
||||||
# If you encounter issues installing, try installing with '--locked'
|
# If you encounter issues installing, try installing with '--locked'
|
||||||
cargo install --locked loki-ai
|
cargo install --locked coyote-ai
|
||||||
```
|
```
|
||||||
|
|
||||||
### Homebrew (Mac/Linux)
|
### Homebrew (Mac/Linux)
|
||||||
To install Loki from Homebrew, install the `loki` tap. Then you'll be able to install `loki`:
|
To install Coyote from Homebrew, install the `coyote` tap. Then you'll be able to install `coyote`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew tap Dark-Alex-17/loki
|
brew tap Dark-Alex-17/coyote
|
||||||
brew install loki
|
brew install coyote
|
||||||
|
|
||||||
# If you need to be more specific, use:
|
# If you need to be more specific, use:
|
||||||
brew install Dark-Alex-17/loki/loki
|
brew install Dark-Alex-17/coyote/coyote
|
||||||
```
|
```
|
||||||
|
|
||||||
To upgrade `loki` using Homebrew:
|
To upgrade `coyote` using Homebrew:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew upgrade loki
|
brew upgrade coyote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
Coyote is available as a Docker image on Docker Hub (`darkalex17/coyote`) for Linux amd64 and arm64.
|
||||||
|
Useful for CI, ephemeral environments, or anywhere you prefer not to install it natively.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull darkalex17/coyote
|
||||||
|
docker run --rm -it darkalex17/coyote
|
||||||
|
```
|
||||||
|
|
||||||
|
To persist your configuration across container runs, mount your existing config directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it \
|
||||||
|
-v ~/.config/coyote:/home/agent/.config/coyote \
|
||||||
|
darkalex17/coyote
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use the local vault provider and want your vault credentials available in the container, also mount the password file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it \
|
||||||
|
-v ~/.config/coyote:/home/agent/.config/coyote \
|
||||||
|
-v ~/.coyote_password:/home/agent/.coyote_password:ro \
|
||||||
|
darkalex17/coyote
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
#### Linux/MacOS (`bash`)
|
#### Linux/MacOS (`bash`)
|
||||||
You can use the following command to run a bash script that downloads and installs the latest version of `loki` for your
|
You can use the following command to run a bash script that downloads and installs the latest version of `coyote` for your
|
||||||
OS (Linux/MacOS) and architecture (x86_64/arm64):
|
OS (Linux/MacOS) and architecture (x86_64/arm64):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/loki/main/install_loki.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Windows/Linux/MacOS (`PowerShell`)
|
#### Windows/Linux/MacOS (`PowerShell`)
|
||||||
You can use the following command to run a PowerShell script that downloads and installs the latest version of `loki`
|
You can use the following command to run a PowerShell script that downloads and installs the latest version of `coyote`
|
||||||
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
|
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.ps1 | iex"
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.ps1 | iex"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual
|
### Manual
|
||||||
Binaries are available on the [releases](https://github.com/Dark-Alex-17/loki/releases) page for the following platforms:
|
Binaries are available on the [releases](https://github.com/Dark-Alex-17/coyote/releases) page for the following platforms:
|
||||||
|
|
||||||
| Platform | Architecture(s) |
|
| Platform | Architecture(s) |
|
||||||
|----------------|-----------------|
|
|----------------|-----------------|
|
||||||
@@ -118,58 +172,58 @@ Binaries are available on the [releases](https://github.com/Dark-Alex-17/loki/re
|
|||||||
#### Windows Instructions
|
#### Windows Instructions
|
||||||
To use a binary from the releases page on Windows, do the following:
|
To use a binary from the releases page on Windows, do the following:
|
||||||
|
|
||||||
1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
|
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS.
|
||||||
2. Use 7-Zip or TarTool to unpack the Tar file.
|
2. Use 7-Zip or TarTool to unpack the Tar file.
|
||||||
3. Run the executable `loki.exe`!
|
3. Run the executable `coyote.exe`!
|
||||||
|
|
||||||
#### Linux/MacOS Instructions
|
#### Linux/MacOS Instructions
|
||||||
To use a binary from the releases page on Linux/MacOS, do the following:
|
To use a binary from the releases page on Linux/MacOS, do the following:
|
||||||
|
|
||||||
1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
|
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS.
|
||||||
2. `cd` to the directory where you downloaded the binary.
|
2. `cd` to the directory where you downloaded the binary.
|
||||||
3. Extract the binary with `tar -C /usr/local/bin -xzf loki-<arch>.tar.gz` (Note: This may require `sudo`)
|
3. Extract the binary with `tar -C /usr/local/bin -xzf coyote-<arch>.tar.gz` (Note: This may require `sudo`)
|
||||||
4. Now you can run `loki`!
|
4. Now you can run `coyote`!
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
Loki can update itself in place to the latest GitHub release. Run `loki --update`
|
Coyote can update itself in place to the latest GitHub release. Run `coyote --update`
|
||||||
for the newest release, or `loki --update v0.4.0` for a specific version:
|
for the newest release, or `coyote --update v0.4.0` for a specific version:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --update
|
coyote --update
|
||||||
loki --update v0.4.0
|
coyote --update v0.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The same is available from within the REPL via `.update` and `.update v0.4.0`.
|
The same is available from within the REPL via `.update` and `.update v0.4.0`.
|
||||||
|
|
||||||
If Loki was installed with a package manager, prefer that package manager so its
|
If Coyote was installed with a package manager, prefer that package manager so its
|
||||||
records stay in sync with the binary on disk; i.e. `brew upgrade loki` for Homebrew,
|
records stay in sync with the binary on disk; i.e. `brew upgrade coyote` for Homebrew,
|
||||||
or `cargo install --locked loki-ai` for Cargo.
|
or `cargo install --locked coyote-ai` for Cargo.
|
||||||
|
|
||||||
When Loki detects a package-manager install it prints a warning and asks for
|
When Coyote detects a package-manager install it prints a warning and asks for
|
||||||
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
|
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
|
||||||
anyway:
|
anyway:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --update --force
|
coyote --update --force
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
After installation, you can generate the configuration files and directories by simply running:
|
After installation, you can generate the configuration files and directories by simply running:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --info
|
coyote --info
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you need to set up the Loki vault by creating a vault password file. Loki will do this for you automatically and
|
Then, you need to set up the Coyote vault by creating a vault password file. Coyote will do this for you automatically and
|
||||||
guide you through the process when you first attempt to access the vault. So, to get started, you can run:
|
guide you through the process when you first attempt to access the vault. So, to get started, you can run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --list-secrets
|
coyote --list-secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
||||||
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/loki/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
|
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||||
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -181,40 +235,40 @@ clients:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --authenticate my-claude-oauth
|
coyote --authenticate my-claude-oauth
|
||||||
# Or via the REPL: .authenticate
|
# Or via the REPL: .authenticate
|
||||||
```
|
```
|
||||||
|
|
||||||
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication).
|
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication).
|
||||||
|
|
||||||
### Tab-Completions
|
### Tab-Completions
|
||||||
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
|
You can also enable tab completions to make using Coyote easier. To do so, add the following to your shell profile:
|
||||||
```shell
|
```shell
|
||||||
# Bash
|
# Bash
|
||||||
# (add to: `~/.bashrc`)
|
# (add to: `~/.bashrc`)
|
||||||
source <(COMPLETE=bash loki)
|
source <(COMPLETE=bash coyote)
|
||||||
|
|
||||||
# Zsh
|
# Zsh
|
||||||
# (add to: `~/.zshrc`)
|
# (add to: `~/.zshrc`)
|
||||||
source <(COMPLETE=zsh loki)
|
source <(COMPLETE=zsh coyote)
|
||||||
|
|
||||||
# Fish
|
# Fish
|
||||||
# (add to: `~/.config/fish/config.fish`)
|
# (add to: `~/.config/fish/config.fish`)
|
||||||
source <(COMPLETE=fish loki | psub)
|
source <(COMPLETE=fish coyote | psub)
|
||||||
|
|
||||||
# Elvish
|
# Elvish
|
||||||
# (add to: `~/.elvish/rc.elv`)
|
# (add to: `~/.elvish/rc.elv`)
|
||||||
eval (E:COMPLETE=elvish loki | slurp)
|
eval (E:COMPLETE=elvish coyote | slurp)
|
||||||
|
|
||||||
# PowerShell
|
# PowerShell
|
||||||
# (add to: `$PROFILE`)
|
# (add to: `$PROFILE`)
|
||||||
$env:COMPLETE = "powershell"
|
$env:COMPLETE = "powershell"
|
||||||
loki | Out-String | Invoke-Expression
|
coyote | Out-String | Invoke-Expression
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shell Integration
|
### Shell Integration
|
||||||
You can integrate Loki's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the
|
You can integrate Coyote's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the
|
||||||
corresponding [shell integration script](./scripts/shell-integration) to your shell. Then, you can invoke Loki to convert natural language to
|
corresponding [shell integration script](https://github.com/Dark-Alex-17/coyote/tree/main/scripts/shell-integration) to your shell. Then, you can invoke Coyote to convert natural language to
|
||||||
shell commands by pressing `Alt-e`. For example:
|
shell commands by pressing `Alt-e`. For example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -224,18 +278,18 @@ find . -name "*.md"
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
The location of the global Loki configuration varies between systems, so you can use the following command to find your
|
The location of the global Coyote configuration varies between systems, so you can use the following command to find your
|
||||||
`config.yaml` file:
|
`config.yaml` file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --info | grep 'config_file' | awk '{print $2}'
|
coyote --info | grep 'config_file' | awk '{print $2}'
|
||||||
```
|
```
|
||||||
|
|
||||||
The configuration file consists of a number of settings. To see a full example configuration file with every setting
|
The configuration file consists of a number of settings. To see a full example configuration file with every setting
|
||||||
defined, refer to the [example configuration file](./config.example.yaml).
|
defined, refer to the [example configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml).
|
||||||
|
|
||||||
### Default LLM
|
### Default LLM
|
||||||
The following settings are available to configure the default LLM that is used when you start Loki, and its
|
The following settings are available to configure the default LLM that is used when you start Coyote, and its
|
||||||
hyperparameters:
|
hyperparameters:
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
@@ -245,34 +299,34 @@ hyperparameters:
|
|||||||
| `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden |
|
| `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden |
|
||||||
|
|
||||||
### CLI Behavior
|
### CLI Behavior
|
||||||
You can use the following settings to modify the behavior of Loki:
|
You can use the following settings to modify the behavior of Coyote:
|
||||||
|
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers |
|
| `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers |
|
||||||
| `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging |
|
| `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging |
|
||||||
| `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` |
|
| `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` |
|
||||||
| `editor` | `null` | What text editor Loki should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` |
|
| `editor` | `null` | What text editor Coyote should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` |
|
||||||
| `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` |
|
| `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` |
|
||||||
| `wrap_code` | `false` | Enables or disables the wrapping of code blocks |
|
| `wrap_code` | `false` | Enables or disables the wrapping of code blocks |
|
||||||
|
|
||||||
### Preludes
|
### Preludes
|
||||||
Preludes let you define the default behavior for the different operating modes of Loki. The available settings are
|
Preludes let you define the default behavior for the different operating modes of Coyote. The available settings are
|
||||||
shown below:
|
shown below:
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Coyote in [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||||
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Loki via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Coyote via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||||
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
|
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
|
||||||
|
|
||||||
### Appearance
|
### Appearance
|
||||||
The appearance of Loki can be modified using the following settings:
|
The appearance of Coyote can be modified using the following settings:
|
||||||
|
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
|---------------|---------------|------------------------------------------------------|
|
|---------------|---------------|------------------------------------------------------|
|
||||||
| `highlight` | `true` | This setting enables or disables syntax highlighting |
|
| `highlight` | `true` | This setting enables or disables syntax highlighting |
|
||||||
| `light_theme` | `false` | This setting toggles light mode in Loki |
|
| `light_theme` | `false` | This setting toggles light mode in Coyote |
|
||||||
|
|
||||||
### Miscellaneous Settings
|
### Miscellaneous Settings
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
@@ -284,11 +338,24 @@ The appearance of Loki can be modified using the following settings:
|
|||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Loki began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project.
|
Coyote began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project.
|
||||||
|
|
||||||
See [CREDITS.md](./CREDITS.md) for full attribution and background.
|
See [CREDITS.md](https://github.com/Dark-Alex-17/coyote/blob/main/CREDITS.md) for full attribution and background.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Creator
|
## Creator
|
||||||
* [Alex Clarke](https://github.com/Dark-Alex-17)
|
* [Alex Clarke](https://github.com/Dark-Alex-17)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Coyote is licensed under the [GNU Affero General Public License v3.0](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE)
|
||||||
|
(AGPL-3.0-only).
|
||||||
|
|
||||||
|
Coyote began as a fork of [AIChat](https://github.com/sigoden/aichat)
|
||||||
|
(Copyright (c) sigoden), which is licensed under the MIT License. Substantial
|
||||||
|
portions of Coyote are derived from AIChat and remain available under the MIT
|
||||||
|
License, preserved in [LICENSE-MIT](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE-MIT). See [NOTICE](https://github.com/Dark-Alex-17/coyote/blob/main/NOTICE) and
|
||||||
|
[CREDITS.md](https://github.com/Dark-Alex-17/coyote/blob/main/CREDITS.md) for details.
|
||||||
+101
-25
@@ -7,14 +7,14 @@ set -euo pipefail
|
|||||||
#######################
|
#######################
|
||||||
|
|
||||||
# Cache file name for detected project info
|
# Cache file name for detected project info
|
||||||
_LOKI_PROJECT_CACHE=".loki-project.json"
|
_COYOTE_PROJECT_CACHE=".coyote-project.json"
|
||||||
|
|
||||||
# Read cached project detection if valid
|
# Read cached project detection if valid
|
||||||
# Usage: _read_project_cache "/path/to/project"
|
# Usage: _read_project_cache "/path/to/project"
|
||||||
# Returns: cached JSON on stdout (exit 0) or nothing (exit 1)
|
# Returns: cached JSON on stdout (exit 0) or nothing (exit 1)
|
||||||
_read_project_cache() {
|
_read_project_cache() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
|
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}"
|
||||||
|
|
||||||
if [[ -f "${cache_file}" ]]; then
|
if [[ -f "${cache_file}" ]]; then
|
||||||
local cached
|
local cached
|
||||||
@@ -32,7 +32,7 @@ _read_project_cache() {
|
|||||||
_write_project_cache() {
|
_write_project_cache() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
local json="$2"
|
local json="$2"
|
||||||
local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
|
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}"
|
||||||
|
|
||||||
echo "${json}" > "${cache_file}" 2>/dev/null || true
|
echo "${json}" > "${cache_file}" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
@@ -40,15 +40,57 @@ _write_project_cache() {
|
|||||||
_detect_heuristic() {
|
_detect_heuristic() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
|
|
||||||
|
local runner="" runner_type="" runner_targets=""
|
||||||
|
if [[ -f "${dir}/Taskfile.yml" || -f "${dir}/Taskfile.yaml" || -f "${dir}/taskfile.yml" || -f "${dir}/taskfile.yaml" ]]; then
|
||||||
|
runner="task" runner_type="taskfile"
|
||||||
|
runner_targets=$( (cd "${dir}" && task --list-all 2>/dev/null | sed -n 's/^\* \([^:[:space:]]*\):.*/\1/p') || true)
|
||||||
|
elif [[ -f "${dir}/justfile" || -f "${dir}/Justfile" ]]; then
|
||||||
|
runner="just" runner_type="just"
|
||||||
|
runner_targets=$( (cd "${dir}" && just --summary 2>/dev/null | tr ' ' '\n') || true)
|
||||||
|
elif [[ -f "${dir}/Makefile" || -f "${dir}/makefile" || -f "${dir}/GNUmakefile" ]]; then
|
||||||
|
runner="make" runner_type="make"
|
||||||
|
local mk mkfiles=()
|
||||||
|
for mk in Makefile makefile GNUmakefile; do
|
||||||
|
[[ -f "${dir}/${mk}" ]] && mkfiles+=("${dir}/${mk}")
|
||||||
|
done
|
||||||
|
runner_targets=$(sed -n 's/^\([A-Za-z0-9_][A-Za-z0-9_.-]*\):\([^=].*\|\)$/\1/p' "${mkfiles[@]}" 2>/dev/null | sort -u || true)
|
||||||
|
fi
|
||||||
|
if [[ -n "${runner}" && -n "${runner_targets}" ]]; then
|
||||||
|
_pick_target() {
|
||||||
|
local c
|
||||||
|
for c in "$@"; do
|
||||||
|
if grep -qx "${c}" <<<"${runner_targets}"; then
|
||||||
|
echo "${runner} ${c}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
local r_build r_test r_check r_lint r_fmt
|
||||||
|
r_build=$(_pick_target build compile)
|
||||||
|
r_test=$(_pick_target test tests unit)
|
||||||
|
r_check=$(_pick_target check vet typecheck build)
|
||||||
|
r_lint=$(_pick_target lint fmt-check)
|
||||||
|
r_fmt=$(_pick_target fmt format)
|
||||||
|
if [[ -n "${r_build}${r_test}${r_check}${r_lint}${r_fmt}" ]]; then
|
||||||
|
echo "{\"type\":\"${runner_type}\",\"build\":\"${r_build}\",\"test\":\"${r_test}\",\"check\":\"${r_check}\",\"lint\":\"${r_lint}\",\"fmt\":\"${r_fmt}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
if [[ -f "${dir}/Cargo.toml" ]]; then
|
if [[ -f "${dir}/Cargo.toml" ]]; then
|
||||||
echo '{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check"}'
|
echo '{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","lint":"cargo clippy --no-deps -- -D warnings","fmt":"cargo fmt"}'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Go
|
# Go
|
||||||
if [[ -f "${dir}/go.mod" ]]; then
|
if [[ -f "${dir}/go.mod" ]]; then
|
||||||
echo '{"type":"go","build":"go build ./...","test":"go test ./...","check":"go vet ./..."}'
|
local go_lint=""
|
||||||
|
if compgen -G "${dir}/.golangci.*" &>/dev/null && command -v golangci-lint &>/dev/null; then
|
||||||
|
go_lint="golangci-lint run"
|
||||||
|
fi
|
||||||
|
echo "{\"type\":\"go\",\"build\":\"go build ./...\",\"test\":\"go test ./...\",\"check\":\"go vet ./...\",\"lint\":\"${go_lint}\",\"fmt\":\"gofmt -w .\"}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -65,7 +107,25 @@ _detect_heuristic() {
|
|||||||
[[ -f "${dir}/pnpm-lock.yaml" ]] && pm="pnpm"
|
[[ -f "${dir}/pnpm-lock.yaml" ]] && pm="pnpm"
|
||||||
[[ -f "${dir}/yarn.lock" ]] && pm="yarn"
|
[[ -f "${dir}/yarn.lock" ]] && pm="yarn"
|
||||||
|
|
||||||
echo "{\"type\":\"nodejs\",\"build\":\"${pm} run build\",\"test\":\"${pm} test\",\"check\":\"${pm} run lint\"}"
|
# Emit only scripts the manifest actually declares (same introspection
|
||||||
|
# contract as the runner tier: never guess a target into existence).
|
||||||
|
_pkg_script() {
|
||||||
|
local s
|
||||||
|
for s in "$@"; do
|
||||||
|
if jq -e --arg s "$s" '.scripts[$s] // empty' "${dir}/package.json" &>/dev/null; then
|
||||||
|
echo "${pm} run ${s}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
local p_build p_test p_check p_lint p_fmt
|
||||||
|
p_build=$(_pkg_script build compile)
|
||||||
|
p_test=$(_pkg_script test)
|
||||||
|
p_check=$(_pkg_script check typecheck tsc)
|
||||||
|
p_lint=$(_pkg_script lint)
|
||||||
|
p_fmt=$(_pkg_script fmt format prettier)
|
||||||
|
echo "{\"type\":\"nodejs\",\"build\":\"${p_build}\",\"test\":\"${p_test}\",\"check\":\"${p_check}\",\"lint\":\"${p_lint}\",\"fmt\":\"${p_fmt}\"}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -82,7 +142,7 @@ _detect_heuristic() {
|
|||||||
check_cmd="uv run ruff check ."
|
check_cmd="uv run ruff check ."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "{\"type\":\"python\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"${check_cmd}\"}"
|
echo "{\"type\":\"python\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"${check_cmd}\",\"lint\":\"${check_cmd}\",\"fmt\":\"ruff format .\"}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -144,17 +204,6 @@ _detect_heuristic() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generic build systems (last resort before LLM)
|
|
||||||
if [[ -f "${dir}/justfile" ]] || [[ -f "${dir}/Justfile" ]]; then
|
|
||||||
echo '{"type":"just","build":"just build","test":"just test","check":"just lint"}'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "${dir}/Makefile" ]] || [[ -f "${dir}/makefile" ]] || [[ -f "${dir}/GNUmakefile" ]]; then
|
|
||||||
echo '{"type":"make","build":"make build","test":"make test","check":"make lint"}'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +267,9 @@ _detect_with_llm() {
|
|||||||
local prompt
|
local prompt
|
||||||
prompt=$(cat <<-EOF
|
prompt=$(cat <<-EOF
|
||||||
|
|
||||||
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it.
|
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, check (typecheck/vet), lint, and format it.
|
||||||
|
|
||||||
|
PRIORITY RULE: if the project declares its own task-runner interface (a Taskfile, justfile, Makefile, package.json scripts, or similar), those declared targets ARE the correct commands — prefer them over generic ecosystem defaults, and never invent a target the interface does not declare.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
@@ -226,25 +277,25 @@ _detect_with_llm() {
|
|||||||
prompt+=$(cat <<-EOF
|
prompt+=$(cat <<-EOF
|
||||||
|
|
||||||
Respond with ONLY a valid JSON object. No markdown fences, no explanation, no extra text.
|
Respond with ONLY a valid JSON object. No markdown fences, no explanation, no extra text.
|
||||||
The JSON must have exactly these 4 keys:
|
The JSON must have exactly these 6 keys:
|
||||||
{"type":"<language>","build":"<build command>","test":"<test command>","check":"<lint or typecheck command>"}
|
{"type":"<language>","build":"<build command>","test":"<test command>","check":"<typecheck/vet command>","lint":"<lint command>","fmt":"<format command>"}
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- "type" must be a single lowercase word (e.g. rust, go, python, nodejs, java, ruby, elixir, cpp, c, zig, haskell, scala, kotlin, dart, swift, php, dotnet, etc.)
|
- "type" must be a single lowercase word (e.g. rust, go, python, nodejs, java, ruby, elixir, cpp, c, zig, haskell, scala, kotlin, dart, swift, php, dotnet, etc.)
|
||||||
- If a command doesn't apply to this project, use an empty string, ""
|
- If a command doesn't apply to this project, use an empty string, "" — NEVER guess a command that might not exist; a wrongly-guessed command is worse than an empty one
|
||||||
- Use the most standard/common commands for the detected ecosystem
|
- Use the most standard/common commands for the detected ecosystem
|
||||||
- If you detect a package manager lockfile, use that package manager (e.g. pnpm over npm)
|
- If you detect a package manager lockfile, use that package manager (e.g. pnpm over npm)
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
local llm_response
|
local llm_response
|
||||||
llm_response=$(loki --no-stream "${prompt}" 2>/dev/null) || return 1
|
llm_response=$(coyote --no-stream "${prompt}" 2>/dev/null) || return 1
|
||||||
|
|
||||||
llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
||||||
llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1)
|
llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1)
|
||||||
|
|
||||||
if echo "${llm_response}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then
|
if echo "${llm_response}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then
|
||||||
echo "${llm_response}" | jq -c '{type: (.type // "unknown"), build: (.build // ""), test: (.test // ""), check: (.check // "")}'
|
echo "${llm_response}" | jq -c '{type: (.type // "unknown"), build: (.build // ""), test: (.test // ""), check: (.check // ""), lint: (.lint // ""), fmt: (.fmt // "")}'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -258,7 +309,7 @@ detect_project() {
|
|||||||
|
|
||||||
local cached
|
local cached
|
||||||
if cached=$(_read_project_cache "${dir}"); then
|
if cached=$(_read_project_cache "${dir}"); then
|
||||||
echo "${cached}" | jq -c '{type, build, test, check}'
|
echo "${cached}" | jq -c '{type, build, test, check, lint: (.lint // ""), fmt: (.fmt // "")}'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -286,6 +337,31 @@ detect_project() {
|
|||||||
echo '{"type":"unknown","build":"","test":"","check":""}'
|
echo '{"type":"unknown","build":"","test":"","check":""}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# resolve_gate_dir maps a workspace root to the directory verification gates
|
||||||
|
# must run in. A delivery-repo worker's workspace root holds only dotfiles
|
||||||
|
# plus the clone, so gates aimed at the root detect nothing and silently
|
||||||
|
# no-op. When the root has no project markers and exactly ONE first-level
|
||||||
|
# git repo exists, gates run inside it; anything ambiguous stays at the root.
|
||||||
|
resolve_gate_dir() {
|
||||||
|
local dir="${1:-.}"
|
||||||
|
local m
|
||||||
|
for m in Taskfile.yml Taskfile.yaml taskfile.yml Cargo.toml go.mod package.json pyproject.toml setup.py pom.xml build.gradle mix.exs Gemfile composer.json Makefile justfile Justfile CMakeLists.txt; do
|
||||||
|
if [[ -e "${dir}/${m}" ]]; then
|
||||||
|
echo "${dir}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
local repos=() d
|
||||||
|
for d in "${dir}"/*/; do
|
||||||
|
[[ -d "${d}/.git" ]] && repos+=("${d}")
|
||||||
|
done
|
||||||
|
if [[ ${#repos[@]} -eq 1 ]]; then
|
||||||
|
echo "${repos[0]%/}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "${dir}"
|
||||||
|
}
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
## FILE SEARCH UTILITIES ##
|
## FILE SEARCH UTILITIES ##
|
||||||
###########################
|
###########################
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# Adversary
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **v2: now a GRAPH agent.** The plan's acceptance criteria are extracted into a list and the graph
|
||||||
|
> fans out ONE fresh-context verification branch per criterion — partial coverage ("checked 8 of 12")
|
||||||
|
> is structurally impossible. Each verdict passes a deterministic contract gate (MET requires
|
||||||
|
> evidence including a proving test; no test = at best PARTIAL), a holistic ADDITIVE-only pass hunts
|
||||||
|
> absence/scope-drift/substitution/gamed-tests, and the CONFORMS/DIVERGES sentinel is computed by a
|
||||||
|
> script (missing plan = DIVERGES, fail-closed). The conformance doctrine below is structurally
|
||||||
|
> enforced: CONFORMS requires ALL MET + zero holistic complaints + zero pipeline faults +
|
||||||
|
> zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
|
||||||
|
|
||||||
|
|
||||||
|
An **adversarial plan-conformance reviewer**. Where [`code-reviewer`](../code-reviewer/README.md)
|
||||||
|
asks *"is this code good?"*, `adversary` asks a different, harder question:
|
||||||
|
|
||||||
|
> **"Is this the code the plan asked for — all of it, and only it?"**
|
||||||
|
|
||||||
|
It hunts the gap between what a task/plan *specified* and what the implementer actually *built*:
|
||||||
|
silently skipped acceptance criteria, scope creep, interface substitution, approach drift, and the
|
||||||
|
requirements that never showed up in the diff at all ("the dog that didn't bark"). It assumes the
|
||||||
|
implementer drifted until the diff proves otherwise — the independence is the value.
|
||||||
|
|
||||||
|
## Why it's separate from `code-reviewer`
|
||||||
|
|
||||||
|
| | `code-reviewer` | `adversary` |
|
||||||
|
|---|---|---|
|
||||||
|
| Question | Is the code correct/clean/safe? | Does the code match the plan? |
|
||||||
|
| Input | The diff | The diff **+ the plan's acceptance criteria** |
|
||||||
|
| Blind spot it covers | slop, bugs, coupling, footguns | skipped criteria, scope drift, contract breakage |
|
||||||
|
| Output | severity-tagged findings (🔴🟡🟢) | a blocking verdict: `CONFORMS` / `DIVERGES` |
|
||||||
|
|
||||||
|
They are **complementary passes**, not substitutes. `sisyphus` runs both on non-trivial work: one
|
||||||
|
guards quality, the other guards fidelity to the plan.
|
||||||
|
|
||||||
|
## Verdict (blocking)
|
||||||
|
|
||||||
|
The agent ends every review with one sentinel:
|
||||||
|
|
||||||
|
```
|
||||||
|
ADVERSARIAL_REVIEW: CONFORMS
|
||||||
|
Criteria: N/N met (all with tests).
|
||||||
|
```
|
||||||
|
|
||||||
|
CONFORMS is deterministic: ALL MET + zero holistic complaints + zero pipeline faults +
|
||||||
|
zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
|
||||||
|
|
||||||
|
```
|
||||||
|
ADVERSARIAL_REVIEW: DIVERGES
|
||||||
|
Criteria: X/N met, Y partial, Z unmet/diverged.
|
||||||
|
Complaints:
|
||||||
|
1. Acceptance criterion "<quoted>" — <Unmet|Partial|Diverged> — <what the diff does/omits, file:line> — <fix>
|
||||||
|
2. ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Every verdict — CONFORMS or DIVERGES — ends with a `Verification runs:` section: the recorded
|
||||||
|
PASS/FAIL results (command, exit code, duration, failure tail) of the caller-declared verification
|
||||||
|
commands, or an explicit marker when none were declared or the run never reached that stage. The
|
||||||
|
one exception is the verdict script's own crash guard, which emits a bare DIVERGES with a
|
||||||
|
"verdict computation error" line — and no section — when verdict computation itself dies.
|
||||||
|
|
||||||
|
A `DIVERGES` verdict **blocks** completion. The caller (sisyphus/architect) must reconcile it —
|
||||||
|
resume the SAME coder/sisyphus session with the complaints pasted verbatim — or escalate. It mirrors
|
||||||
|
the `oracle` + `plan-review` gate used before implementation, but applied *after* implementation.
|
||||||
|
|
||||||
|
Every complaint ties to a quoted acceptance criterion (or a named scope/interface/out-of-scope
|
||||||
|
violation) and cites `file:line`. Vague complaints are not emitted.
|
||||||
|
|
||||||
|
## How it reviews
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["parse (llm): extract acceptance criteria"] --> B["facts (diff_facts.py): resolve + bound the diff"]
|
||||||
|
V["verification_commands (declared graph variable — never parsed from the prompt)"] --> R
|
||||||
|
B --> R["run_checks (script): run the declared verification commands ONCE, record results"]
|
||||||
|
R --> C["map over criteria: one fresh-context branch each (parallel)"]
|
||||||
|
C --> D["check_criterion (llm + fs/ast tools)"]
|
||||||
|
D --> E["crit_gate (script): MET needs change+test evidence"]
|
||||||
|
E -. "reject-retry once; exhausted = PARTIAL" .-> D
|
||||||
|
E --> F["holistic (llm): ADDITIVE-only hunt - absence, scope drift, substitution, gamed tests"]
|
||||||
|
F --> G["verdict (script): CONFORMS iff ALL MET + zero extras + zero faults + zero red runs; no criteria = DIVERGES"]
|
||||||
|
G --> H(["ADVERSARIAL_REVIEW: CONFORMS | DIVERGES"])
|
||||||
|
A -. "fallback" .-> P["pipeline_fault (script): record PIPELINE-FAULT marker"]
|
||||||
|
B -. "fallback" .-> P
|
||||||
|
R -. "fallback" .-> P
|
||||||
|
D -. "fallback" .-> CF["criterion_fault (script): UNMET + PIPELINE-FAULT evidence for that ONE criterion"]
|
||||||
|
CF -. "map join" .-> F
|
||||||
|
F -. "fallback" .-> G
|
||||||
|
P --> G
|
||||||
|
```
|
||||||
|
|
||||||
|
The conformance doctrine is baked into the graph's own nodes:
|
||||||
|
|
||||||
|
1. **Every** acceptance criterion gets its own fresh-context verification branch — MET / PARTIAL / UNMET / DIVERGED, machine-gated (MET requires the satisfying change AND the proving test, cited). No test ⇒ at best PARTIAL. Partial coverage of the criteria list is structurally impossible.
|
||||||
|
2. Ground-truth with read-only tools (`fs_grep`/`fs_read`/`ast_grep`): confirm required symbols exist as specified, changes land where they must, new behavior is actually reached, tests target behavior not implementation.
|
||||||
|
3. Execution evidence is recorded **once, deterministically**: `run_checks` runs ONLY the caller-declared verification commands (never invented or auto-detected), sequentially under a 3300s total deadline — commands the deadline leaves no budget for are recorded as SKIPPED. Criterion branches have no execution tools by design: "tests pass"-style criteria are judged against the record — a recorded green run is MET citing it; no declared command ⇒ at best PARTIAL. Independently of the criterion judgments, the verdict script blocks CONFORMS on any red run (exit≠0/timeout/skipped) and on an ENVIRONMENT runner-error marker when commands were declared — the declared commands never ran, so the run is unproven. The commands arrive through the declared `verification_commands` graph variable (see [Declaring verification commands](#declaring-verification-commands)) — `parse` never extracts them from the prompt, so the prompt is no longer an extraction source for the commands.
|
||||||
|
4. Hunt adversarially for the **absent**: skipped criteria, scope creep, interface/approach substitution, out-of-scope touches, downstream contract breakage.
|
||||||
|
5. **Fail closed on pipeline faults**: if parse, diff resolution, or the verification runner dies, its fallback routes to `pipeline_fault`, which records a `PIPELINE-FAULT:` marker and continues to the verdict (a failed holistic pass falls back straight to the verdict, which synthesizes the same marker). Any recorded fault forces DIVERGES with the fault as complaint #1 — degraded is never passing, and the sentinel is always emitted: the graph never dies without one.
|
||||||
|
6. **Fail closed per criterion**: a criterion check that dies (max_iterations exhausted, API failure) falls back to `criterion_fault`, which records a schema-shaped UNMET verdict carrying a `PIPELINE-FAULT: criterion ` evidence marker for that one criterion. The other criteria still get verdicts, the report says the check DIED rather than judged, and the run is DIVERGES. That exact marker prefix (the one `criterion_fault` emits) forces the criterion into unmet regardless of the status it carries, so a verdict that echoes it with `MET` can never count toward CONFORMS; `check_criterion` is told never to begin its own evidence with `PIPELINE-FAULT:`.
|
||||||
|
|
||||||
|
It is **read-only** — it produces a verdict, never a fix.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Typically spawned by `sisyphus` (or `architect`) alongside `code-reviewer`. The spawn prompt IS its
|
||||||
|
entire context, so it must include the diff (or a base ref to fetch) **and** the acceptance criteria.
|
||||||
|
Verification commands travel separately, as a declared variable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
agent__spawn --agent adversary --prompt "
|
||||||
|
## TASK
|
||||||
|
Adversarially review the recent changes for TASK-NNN against its plan. Return CONFORMS/DIVERGES.
|
||||||
|
|
||||||
|
## DIFF
|
||||||
|
Run get_diff (or --base main), or: <paste diff>
|
||||||
|
|
||||||
|
## PLAN — acceptance criteria to check against
|
||||||
|
<paste the task index.md body + the relevant PLAN-*.md section, verbatim>
|
||||||
|
" --variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct invocation for ad-hoc use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
coyote -a adversary --agent-variable project_dir /path/to/repo \
|
||||||
|
--agent-variable verification_commands '["cargo test --all"]' \
|
||||||
|
"Review staged changes against these criteria: <paste criteria>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Declaring verification commands
|
||||||
|
|
||||||
|
`verification_commands` is a declared graph variable (default `'[]'`): a JSON array of shell
|
||||||
|
commands, passed as a string because agent variables always arrive as strings. `run_checks` runs
|
||||||
|
exactly those commands with a shell — so the declaration is a **trust boundary**. The prompt is the
|
||||||
|
wrong side of it: it also carries plan and diff text pasted from the repo under review, and an LLM
|
||||||
|
`parse` node extracting commands from that prose would let anything in the repo's text run under
|
||||||
|
the reviewer's shell. Hence the commands are never in the prompt and never in `parse`'s output
|
||||||
|
schema; `review-gauntlet` forwards its own `verification_commands` variable to the adversary lane
|
||||||
|
as a structured `inputs:` passthrough, not as prompt text.
|
||||||
|
|
||||||
|
- `'[]'` (or omitted) ⇒ the "none declared" marker; execution criteria are at best PARTIAL.
|
||||||
|
- Anything that is not a JSON array of strings (unparsable JSON, an object, non-string items) is
|
||||||
|
an invalid declaration: `run_checks` executes **nothing** and records a
|
||||||
|
`PIPELINE-FAULT: verification_commands declaration invalid — …` marker, which forces DIVERGES:
|
||||||
|
it fails closed as a PIPELINE-FAULT (complaint #1) — it never degrades to an ENVIRONMENT marker.
|
||||||
|
- The engine merges only the keys declared under `parse`'s `output_schema.properties` into state,
|
||||||
|
so an injected instruction that makes the parse LLM emit an extra `verification_commands` key
|
||||||
|
cannot reach state: the undeclared key is dropped and the declared variable stands.
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- The graph's `diff_facts` script resolves the diff itself (staged → unstaged → `HEAD~1`, or an explicit ref/range named in the prompt) — there is no `get_diff` tool anymore.
|
||||||
|
- The `run_checks` script executes the commands declared via the `verification_commands` variable and records the results — criterion branches cite the record and never run anything themselves.
|
||||||
|
- Criterion branches carry read-only `fs_read`/`fs_cat`/`fs_grep`/`ast_grep` for ground-truth checks.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`code-reviewer`](../code-reviewer/README.md) — the quality reviewer it runs alongside.
|
||||||
|
- [`plan-review`](../../skills/plan-review/SKILL.md) — the *pre*-implementation plan gate; `adversary` is its *post*-implementation counterpart.
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
name: adversary
|
||||||
|
description: |
|
||||||
|
Graph-based adversarial plan-conformance reviewer. The plan's acceptance
|
||||||
|
criteria are extracted into a list and the graph mechanically fans out ONE
|
||||||
|
fresh-context verification branch per criterion — "checked 8 of 12 and waved
|
||||||
|
the rest through" becomes structurally impossible. Each criterion verdict
|
||||||
|
(MET / PARTIAL / UNMET / DIVERGED) passes a deterministic contract gate
|
||||||
|
(MET requires evidence including a test proving the behavior — no test means
|
||||||
|
at best PARTIAL). A holistic pass then hunts what per-criterion checks cannot
|
||||||
|
see: absence (the dog that didn't bark), scope drift, interface substitution,
|
||||||
|
silently-overridden recorded decisions, out-of-scope touches, and gamed
|
||||||
|
tests — ADDITIVE-only: it can add complaints, never flip a criterion verdict.
|
||||||
|
The final CONFORMS/DIVERGES sentinel is computed by a script: CONFORMS
|
||||||
|
requires every criterion MET, zero holistic complaints, zero pipeline faults,
|
||||||
|
and zero red verification runs (exit≠0/timeout/skipped, or a runner error
|
||||||
|
when commands were declared). Missing plan = DIVERGES (fail-closed) naming
|
||||||
|
the missing spec. Caller-declared verification
|
||||||
|
commands are executed ONCE by a deterministic stage and recorded — criterion
|
||||||
|
branches cite the record and never run anything themselves. Any pipeline
|
||||||
|
fault (parse, diff resolution, verification runner, or holistic failure)
|
||||||
|
lands a PIPELINE-FAULT marker and fails closed into DIVERGES: degraded is
|
||||||
|
never passing.
|
||||||
|
|
||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory containing the changes under review
|
||||||
|
default: '.'
|
||||||
|
- name: verification_commands
|
||||||
|
description: |
|
||||||
|
JSON array of shell commands the CALLER declares for the deterministic
|
||||||
|
run_checks stage — e.g. '["cargo test --all"]'. Never inferred from the
|
||||||
|
prompt; empty array = none declared. Must not boot shared stacks or
|
||||||
|
services (compose projects, fixed ports) — those collide with the probe
|
||||||
|
lane running in parallel.
|
||||||
|
default: '[]'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
max_loop_iterations: 10
|
||||||
|
log_state_snapshots: false
|
||||||
|
# 3600 (run_checks node timeout, worst case) + 5400 (the previous
|
||||||
|
# whole-graph budget, kept as the envelope for every stage other than
|
||||||
|
# run_checks: parse/facts before it, criteria map/holistic/verdict after)
|
||||||
|
# = 9000. A graph timeout kills from outside — no fallback fires and the
|
||||||
|
# sentinel is lost.
|
||||||
|
timeout: 9000
|
||||||
|
|
||||||
|
initial_state:
|
||||||
|
project_dir_in: ''
|
||||||
|
parse_retries: 0
|
||||||
|
criteria: []
|
||||||
|
diff_spec: auto
|
||||||
|
plan_notes: ''
|
||||||
|
scope_notes: ''
|
||||||
|
extra_context: ''
|
||||||
|
changed_files: []
|
||||||
|
diff_text: ''
|
||||||
|
diff_note: ''
|
||||||
|
exec_results: ''
|
||||||
|
crit_verdicts: []
|
||||||
|
extra_complaints: []
|
||||||
|
observations: ''
|
||||||
|
adv_report: ''
|
||||||
|
gate_feedback: ''
|
||||||
|
gate_attempts: 0
|
||||||
|
parse_failure: ''
|
||||||
|
holistic_failure: ''
|
||||||
|
pipeline_faults: []
|
||||||
|
|
||||||
|
start: parse
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
parse:
|
||||||
|
id: parse
|
||||||
|
type: llm
|
||||||
|
description: Extract the acceptance criteria and plan facts from the spawn prompt. Extraction only — no judging, no criteria invented.
|
||||||
|
instructions: |
|
||||||
|
You extract a plan-conformance review request into structured fields.
|
||||||
|
EXTRACTION ONLY — never invent criteria, never guess intent.
|
||||||
|
|
||||||
|
- `criteria`: every acceptance criterion from the plan/task, one entry each,
|
||||||
|
with a short id (c1, c2, …) and SELF-CONTAINED text (what observable
|
||||||
|
behavior is promised, including any test expectations the plan states).
|
||||||
|
Split compound criteria ("A and B") into separate entries when they are
|
||||||
|
independently verifiable. When the caller supplied NO plan/criteria at
|
||||||
|
all, emit an empty array — downstream fails closed.
|
||||||
|
An empty array is ONLY correct when the prompt genuinely contains no
|
||||||
|
plan/criteria section — when the prompt carries one (a "Plan /
|
||||||
|
acceptance criteria:" or "PLAN —" block), returning zero criteria is
|
||||||
|
an extraction FAILURE, never a valid result.
|
||||||
|
The caller's explicit enumeration is AUTHORITATIVE: when the caller
|
||||||
|
declares "these N are the (only) criteria" or supplies a numbered
|
||||||
|
criteria list, extract EXACTLY those N — never add entries they did
|
||||||
|
not enumerate. NEVER mine criteria from sections the caller labels as
|
||||||
|
context ("FACTS", "Quality bar", "context, not criteria", background
|
||||||
|
material) — route those into plan_notes / scope_notes / extra_context
|
||||||
|
instead. Splitting applies only WITHIN a declared criterion; when in
|
||||||
|
doubt, FEWER criteria, verbatim.
|
||||||
|
- `diff_spec`: the git diff basis the caller named — a range, a ref/SHA,
|
||||||
|
"staged", "worktree", or "auto" when unnamed (a caller phrase like
|
||||||
|
"get_diff --base X" means diff_spec = "X").
|
||||||
|
- `project_dir_in`: the directory/worktree path the caller named as
|
||||||
|
holding the code under review; empty string when the caller named none.
|
||||||
|
- `plan_notes`: recorded decisions the plan states ("do X not Y because Z"),
|
||||||
|
verbatim — the holistic pass checks the diff didn't silently override them.
|
||||||
|
- `scope_notes`: what the plan says NOT to touch / explicit boundaries.
|
||||||
|
- `extra_context`: anything else conformance-relevant, condensed.
|
||||||
|
prompt: |
|
||||||
|
Extract the conformance-review facts from this request:
|
||||||
|
|
||||||
|
{{initial_prompt}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
criteria:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
required: [id, text]
|
||||||
|
diff_spec:
|
||||||
|
type: string
|
||||||
|
plan_notes:
|
||||||
|
type: string
|
||||||
|
scope_notes:
|
||||||
|
type: string
|
||||||
|
extra_context:
|
||||||
|
type: string
|
||||||
|
project_dir_in:
|
||||||
|
type: string
|
||||||
|
required: [criteria, diff_spec, plan_notes, scope_notes, extra_context, project_dir_in]
|
||||||
|
max_iterations: 3
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
parse_failure: '{{output}}'
|
||||||
|
fallback: pipeline_fault
|
||||||
|
next: facts
|
||||||
|
|
||||||
|
facts:
|
||||||
|
id: facts
|
||||||
|
type: script
|
||||||
|
description: Deterministic diff resolution — changed files and the (bounded) diff text every criterion branch verifies against.
|
||||||
|
script: scripts/diff_facts.py
|
||||||
|
timeout: 120
|
||||||
|
fallback: pipeline_fault
|
||||||
|
next: run_checks
|
||||||
|
|
||||||
|
run_checks:
|
||||||
|
id: run_checks
|
||||||
|
type: script
|
||||||
|
description: Deterministic execution evidence — runs ONLY the commands declared via the verification_commands variable in the project dir and records exec_results; criterion branches cite the record and never execute anything. Undeclared commands degrade to a "none declared" marker, runner errors to an ENVIRONMENT marker (criterion branches judge it PARTIAL/unproven; the verdict additionally blocks CONFORMS when commands were declared); a declaration that is not a JSON array of strings executes nothing and records a PIPELINE-FAULT (fail-closed); a 3300s total deadline keeps the script under the node timeout (commands past it are recorded as skipped), and if the node is killed anyway the fallback records a PIPELINE-FAULT.
|
||||||
|
script: scripts/run_checks.py
|
||||||
|
timeout: 3600
|
||||||
|
fallback: pipeline_fault
|
||||||
|
next: map_criteria
|
||||||
|
|
||||||
|
map_criteria:
|
||||||
|
id: map_criteria
|
||||||
|
type: map
|
||||||
|
over: '{{criteria}}'
|
||||||
|
as: criterion
|
||||||
|
branch: check_criterion
|
||||||
|
output_key: crit_verdict
|
||||||
|
collect_into: crit_verdicts
|
||||||
|
max_concurrency: 6
|
||||||
|
next: holistic
|
||||||
|
|
||||||
|
check_criterion:
|
||||||
|
id: check_criterion
|
||||||
|
type: llm
|
||||||
|
description: Fresh-context verification of ONE acceptance criterion against the diff and the working tree (ENVIRONMENT/SKIPPED runs judge PARTIAL here; the deterministic verdict blocks CONFORMS when declared commands did not run).
|
||||||
|
instructions: |
|
||||||
|
You verify ONE acceptance criterion against a code change. Fresh context,
|
||||||
|
one criterion, full suspicion: assume the implementer drifted, cut a
|
||||||
|
corner, or misread the plan until the diff proves otherwise.
|
||||||
|
|
||||||
|
Classification (exactly one):
|
||||||
|
- `MET` — the diff satisfies the criterion AND a test proves the promised
|
||||||
|
behavior. Evidence MUST cite both: the satisfying change (file:line) and
|
||||||
|
the test targeting the behavior (file:line). No test ⇒ NOT MET — at best
|
||||||
|
PARTIAL (a criterion is a promise of observable behavior; unproven
|
||||||
|
behavior is unverified behavior).
|
||||||
|
- `PARTIAL` — partially satisfied, or satisfied but unproven (missing/weak
|
||||||
|
test), or you could not establish it either way (say why — doubt lands
|
||||||
|
here, never in MET).
|
||||||
|
- `UNMET` — the plan requires it and the diff does not do it. Absence is a
|
||||||
|
finding: review what SHOULD be there, not only what IS.
|
||||||
|
- `DIVERGED` — the diff does something ELSE instead (interface substituted,
|
||||||
|
approach swapped, criterion reinterpreted).
|
||||||
|
|
||||||
|
Ground-truth discipline: fs_grep the symbols the criterion names (confirm
|
||||||
|
they exist spelled as specified), fs_read around the relevant hunks to
|
||||||
|
confirm the change makes the criterion true, confirm the test targets
|
||||||
|
BEHAVIOR (not implementation echo), use ast_grep for structural claims.
|
||||||
|
Watch for gamed evidence: a test weakened/deleted so it passes, hardcoded
|
||||||
|
values shaped to the criterion's example, TODO/stub presented as done —
|
||||||
|
those are PARTIAL or DIVERGED with the gaming named in the complaint.
|
||||||
|
|
||||||
|
Execution criteria ("tests pass", "lint clean", "build succeeds") are
|
||||||
|
judged ONLY against the recorded verification runs in your prompt — you
|
||||||
|
have no execution tools BY DESIGN and must never claim to have run
|
||||||
|
anything yourself:
|
||||||
|
- a recorded green run (exit 0) of a command covering the criterion is
|
||||||
|
the proving evidence — MET, citing that run (the command + exit 0).
|
||||||
|
- a recorded failing run covering the criterion ⇒ UNMET, citing the
|
||||||
|
recorded exit code and output tail.
|
||||||
|
- "none declared" or an ENVIRONMENT marker ⇒ at best PARTIAL, with an
|
||||||
|
ENVIRONMENT note in the complaint naming the exact command the caller
|
||||||
|
should declare via the `verification_commands` variable (structured
|
||||||
|
input, e.g. "cargo test --all") — commands are never declared in the
|
||||||
|
prompt.
|
||||||
|
- a SKIPPED record (the runner's total deadline expired before that
|
||||||
|
command ran) ⇒ at best PARTIAL for the criteria it covers, same
|
||||||
|
treatment as an ENVIRONMENT marker: unproven at the criterion level.
|
||||||
|
The deterministic verdict additionally blocks CONFORMS when declared
|
||||||
|
commands did not run — that is its job, not yours.
|
||||||
|
|
||||||
|
Output format — respond with ONLY this JSON object, no prose around it:
|
||||||
|
{"id": "<the criterion's id, echoed>",
|
||||||
|
"status": "MET" | "PARTIAL" | "UNMET" | "DIVERGED",
|
||||||
|
"evidence": "<for MET: the satisfying change file:line AND the proving test
|
||||||
|
file:line, quoted decisively; otherwise the closest relevant citations or empty>",
|
||||||
|
"complaint": "<required for non-MET: what the diff does/omits (file:line) and
|
||||||
|
what would make it conform; empty string for MET>"}
|
||||||
|
|
||||||
|
Your response is machine-validated: a malformed response is rejected back
|
||||||
|
to you exactly once with the reason, then recorded as PARTIAL (unproven).
|
||||||
|
The id is stamped from the criterion itself — your echo is informative only.
|
||||||
|
You have a bounded tool budget — prefer fs_grep and line-ranged fs_read over
|
||||||
|
whole-file reads, and conclude as soon as the evidence is decisive.
|
||||||
|
Never begin `evidence` with the literal `PIPELINE-FAULT:` — that prefix is
|
||||||
|
reserved for the graph's own fault markers.
|
||||||
|
prompt: |
|
||||||
|
Repository: {{project_dir}}
|
||||||
|
Changed files: {{changed_files}}
|
||||||
|
|
||||||
|
Criterion to verify (JSON):
|
||||||
|
{{criterion}}
|
||||||
|
|
||||||
|
Plan-recorded decisions (context): {{plan_notes}}
|
||||||
|
{{extra_context}}
|
||||||
|
|
||||||
|
The diff{{diff_note}}:
|
||||||
|
{{diff_text}}
|
||||||
|
|
||||||
|
Verification runs (recorded by the deterministic run_checks stage):
|
||||||
|
{{exec_results}}
|
||||||
|
{{gate_feedback}}
|
||||||
|
tools:
|
||||||
|
- fs_read
|
||||||
|
- fs_cat
|
||||||
|
- fs_grep
|
||||||
|
- ast_grep
|
||||||
|
max_iterations: 30
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
crit_verdict: '{{output}}'
|
||||||
|
fallback: criterion_fault
|
||||||
|
next: crit_gate
|
||||||
|
|
||||||
|
crit_gate:
|
||||||
|
id: crit_gate
|
||||||
|
type: script
|
||||||
|
description: Deterministic contract gate per criterion verdict — status enum, MET requires evidence, non-MET requires a complaint; reject-and-retry once, then record PARTIAL (unproven). Never sinks the map.
|
||||||
|
script: scripts/crit_gate.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
criterion_fault:
|
||||||
|
id: criterion_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed per-criterion fault marker (reached only via check_criterion's fallback) — converts the captured "LLM node failed" text into a schema-shaped UNMET verdict carrying a PIPELINE-FAULT evidence marker, so one dead criterion check can never sink the map or read as met.
|
||||||
|
script: scripts/criterion_fault.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
holistic:
|
||||||
|
id: holistic
|
||||||
|
type: llm
|
||||||
|
description: Cross-criterion adversarial pass — hunts what per-criterion checks cannot see. ADDITIVE-only; cannot flip criterion verdicts.
|
||||||
|
instructions: |
|
||||||
|
You run the holistic adversarial pass over a whole change AFTER each
|
||||||
|
acceptance criterion was independently verified. You hunt what
|
||||||
|
per-criterion checks cannot see. You are ADDITIVE-ONLY: you may add
|
||||||
|
complaints; you may NOT flip, soften, or re-judge any criterion verdict.
|
||||||
|
|
||||||
|
The checklist (each hit becomes one complaint with kind, file:line
|
||||||
|
location, the violation, and the fix):
|
||||||
|
- **Absence** — something the PLAN requires that maps to no criterion and
|
||||||
|
never showed up in the diff (the dog that didn't bark).
|
||||||
|
- **Scope drift / out-of-scope** — the diff touches what the plan's scope
|
||||||
|
notes say not to touch, or quietly does extra unplanned work.
|
||||||
|
- **Interface/approach substitution** — the plan named an interface,
|
||||||
|
library, or approach; the diff substituted another without the plan
|
||||||
|
recording that decision.
|
||||||
|
- **Silently overridden recorded decision** — plan says "do X not Y
|
||||||
|
because Z"; diff does Y.
|
||||||
|
- **Downstream contract breakage** — the diff changes a contract the plan
|
||||||
|
promised to preserve.
|
||||||
|
- **Gamed tests** — tests weakened, deleted, or shaped (hardcoded
|
||||||
|
expectations, skipped assertions) so the criteria APPEAR met.
|
||||||
|
- **The plan itself is the culprit** — impossible or self-contradictory
|
||||||
|
requirements: that is a complaint naming the PLAN as root cause; never
|
||||||
|
silently judge against a corrected version of it.
|
||||||
|
|
||||||
|
Be terse and decisive: three real complaints beat fifteen weak ones. A
|
||||||
|
complaint with no location and no criterion/scope reference is noise — do
|
||||||
|
not emit it. When everything is a nitpick, emit zero complaints and put
|
||||||
|
1-3 non-blocking notes in `observations`.
|
||||||
|
prompt: |
|
||||||
|
Repository: {{project_dir}}
|
||||||
|
Changed files: {{changed_files}}
|
||||||
|
|
||||||
|
Acceptance criteria: {{criteria}}
|
||||||
|
Per-criterion verdicts (already final): {{crit_verdicts}}
|
||||||
|
|
||||||
|
Plan-recorded decisions: {{plan_notes}}
|
||||||
|
Scope boundaries: {{scope_notes}}
|
||||||
|
{{extra_context}}
|
||||||
|
|
||||||
|
The diff{{diff_note}}:
|
||||||
|
{{diff_text}}
|
||||||
|
tools:
|
||||||
|
- fs_read
|
||||||
|
- fs_cat
|
||||||
|
- fs_grep
|
||||||
|
- ast_grep
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
extra_complaints:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
kind:
|
||||||
|
type: string
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
violation:
|
||||||
|
type: string
|
||||||
|
fix:
|
||||||
|
type: string
|
||||||
|
required: [kind, location, violation, fix]
|
||||||
|
observations:
|
||||||
|
type: string
|
||||||
|
required: [extra_complaints, observations]
|
||||||
|
max_iterations: 45
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
holistic_failure: '{{output}}'
|
||||||
|
fallback: verdict
|
||||||
|
next: verdict
|
||||||
|
|
||||||
|
pipeline_fault:
|
||||||
|
id: pipeline_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed fault marker (reached only via fallback routes from parse/facts/run_checks) — records a PIPELINE-FAULT in state and continues to verdict, which turns any fault into DIVERGES. The graph never dies without its sentinel.
|
||||||
|
script: scripts/pipeline_fault.py
|
||||||
|
timeout: 30
|
||||||
|
next: verdict
|
||||||
|
|
||||||
|
verdict:
|
||||||
|
id: verdict
|
||||||
|
type: script
|
||||||
|
description: Deterministic verdict — CONFORMS requires every criterion MET, zero holistic complaints, zero pipeline faults, and zero recorded red verification runs (exit≠0/timeout/skipped, or an ENVIRONMENT runner-error marker when commands were declared); no criteria at all fails closed and any recorded PIPELINE-FAULT forces DIVERGES (degraded is never passing). Assembles the exact sentinel format.
|
||||||
|
script: scripts/verdict.py
|
||||||
|
timeout: 30
|
||||||
|
next: done
|
||||||
|
|
||||||
|
done:
|
||||||
|
id: done
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
{{adv_report}}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Contract gate for the adversary's per-criterion verify chain.
|
||||||
|
|
||||||
|
Validates each criterion verdict against the contract:
|
||||||
|
- a single JSON object with status in {MET, PARTIAL, UNMET, DIVERGED}
|
||||||
|
- MET requires non-empty evidence (the change AND the proving test)
|
||||||
|
- non-MET requires a non-empty complaint
|
||||||
|
|
||||||
|
The authoritative id is stamped from the criterion item — the model's echo is
|
||||||
|
never trusted. Malformed output is rejected back to check_criterion exactly
|
||||||
|
once with the reason; a second failure records PARTIAL (unproven) so a
|
||||||
|
malformed item can never sink the map or masquerade as MET.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
STATUSES = {"MET", "PARTIAL", "UNMET", "DIVERGED"}
|
||||||
|
MAX_RETRIES = 1
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def criterion_fields(state):
|
||||||
|
c = state.get("criterion")
|
||||||
|
if isinstance(c, dict):
|
||||||
|
cid = c.get("id")
|
||||||
|
text = c.get("text")
|
||||||
|
return (
|
||||||
|
cid.strip() if isinstance(cid, str) and cid.strip() else "unknown",
|
||||||
|
text.strip() if isinstance(text, str) else "",
|
||||||
|
)
|
||||||
|
return "unknown", ""
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json(text):
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
raise ValueError("criterion-check output is empty or not text")
|
||||||
|
text = re.sub(r"```[a-zA-Z]*", "", text).replace("```", "")
|
||||||
|
start, end = text.find("{"), text.rfind("}")
|
||||||
|
if start == -1 or end <= start:
|
||||||
|
raise ValueError("no JSON object found in criterion-check output")
|
||||||
|
return json.loads(text[start : end + 1])
|
||||||
|
|
||||||
|
|
||||||
|
def validate(obj):
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
raise ValueError("criterion-check output is not a JSON object")
|
||||||
|
status = obj.get("status")
|
||||||
|
if status not in STATUSES:
|
||||||
|
raise ValueError(f"status must be one of {sorted(STATUSES)}, got {status!r}")
|
||||||
|
evidence = obj.get("evidence") if isinstance(obj.get("evidence"), str) else ""
|
||||||
|
complaint = obj.get("complaint") if isinstance(obj.get("complaint"), str) else ""
|
||||||
|
if status == "MET" and not evidence.strip():
|
||||||
|
raise ValueError(
|
||||||
|
"MET requires non-empty evidence citing the satisfying change AND the "
|
||||||
|
"test proving the behavior (file:line)"
|
||||||
|
)
|
||||||
|
if status != "MET" and not complaint.strip():
|
||||||
|
raise ValueError(
|
||||||
|
f"{status} requires a non-empty complaint (what the diff does/omits at "
|
||||||
|
"file:line, and what would make it conform)"
|
||||||
|
)
|
||||||
|
return status, evidence, complaint
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
cid, ctext = criterion_fields(state)
|
||||||
|
attempts = state.get("gate_attempts")
|
||||||
|
if not isinstance(attempts, int):
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, evidence, complaint = validate(extract_json(state.get("crit_verdict")))
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"crit_verdict": {
|
||||||
|
"id": cid,
|
||||||
|
"text": ctext,
|
||||||
|
"status": status,
|
||||||
|
"evidence": evidence,
|
||||||
|
"complaint": complaint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
reason = str(e)
|
||||||
|
|
||||||
|
if attempts < MAX_RETRIES:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"_next": "check_criterion",
|
||||||
|
"gate_attempts": attempts + 1,
|
||||||
|
"gate_feedback": (
|
||||||
|
"PREVIOUS RESPONSE REJECTED by the verdict gate: " + reason +
|
||||||
|
". Respond with ONLY the JSON object described in your instructions."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"crit_verdict": {
|
||||||
|
"id": cid,
|
||||||
|
"text": ctext,
|
||||||
|
"status": "PARTIAL",
|
||||||
|
"evidence": "",
|
||||||
|
"complaint": (
|
||||||
|
"criterion check output failed machine validation after retry: "
|
||||||
|
+ reason
|
||||||
|
+ " — treat this criterion as unproven"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the gate must never crash the chain
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"crit_verdict": {
|
||||||
|
"id": "unknown",
|
||||||
|
"text": "",
|
||||||
|
"status": "PARTIAL",
|
||||||
|
"evidence": "",
|
||||||
|
"complaint": f"criterion gate error: {e} — treat as unproven",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed per-criterion fault marker for the adversary graph.
|
||||||
|
|
||||||
|
Reached ONLY via check_criterion's `fallback:` route, after the engine has
|
||||||
|
written its "LLM node …failed: <chain>" text into `crit_verdict` through
|
||||||
|
state_updates. Converts that text into a schema-shaped UNMET verdict whose
|
||||||
|
evidence carries a "PIPELINE-FAULT:" marker, so one criterion check that dies
|
||||||
|
(max_iterations exhausted, API failure) is recorded as DIED rather than
|
||||||
|
judged, the other criteria still get verdicts, and the run is DIVERGES. The
|
||||||
|
map collects it like any other verdict — the branch never sinks the map. The
|
||||||
|
engine applies state_updates before routing to the fallback (src/graph/llm.rs:
|
||||||
|
`apply_state_updates_with_output`, then `outcome_from`), so the "no failure
|
||||||
|
text captured" branch is belt-and-braces rather than an expected path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def criterion_fields(state):
|
||||||
|
c = state.get("criterion")
|
||||||
|
if isinstance(c, dict):
|
||||||
|
cid = c.get("id")
|
||||||
|
text = c.get("text")
|
||||||
|
return (
|
||||||
|
cid.strip() if isinstance(cid, str) and cid.strip() else "unknown",
|
||||||
|
text.strip() if isinstance(text, str) else "",
|
||||||
|
)
|
||||||
|
return "unknown", ""
|
||||||
|
|
||||||
|
|
||||||
|
def failure_detail(state):
|
||||||
|
captured = state.get("crit_verdict")
|
||||||
|
detail = captured.strip() if isinstance(captured, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
return "no failure text captured"
|
||||||
|
detail = detail.replace("\n", " ")
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
return detail
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
cid, ctext = criterion_fields(state)
|
||||||
|
detail = failure_detail(state)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"crit_verdict": {
|
||||||
|
"id": cid,
|
||||||
|
"text": ctext,
|
||||||
|
"status": "UNMET",
|
||||||
|
"evidence": f"PIPELINE-FAULT: criterion check failed — {detail}",
|
||||||
|
"complaint": (
|
||||||
|
f"criterion check DIED (pipeline fault) — {detail}; the "
|
||||||
|
"criterion was NOT verified and is treated as unmet "
|
||||||
|
"(fail-closed)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"crit_verdict": {
|
||||||
|
"id": "unknown",
|
||||||
|
"text": "",
|
||||||
|
"status": "UNMET",
|
||||||
|
"evidence": f"PIPELINE-FAULT: criterion fault-marker script error: {e}",
|
||||||
|
"complaint": (
|
||||||
|
f"criterion check DIED (pipeline fault) — fault-marker script "
|
||||||
|
f"error: {e}; the criterion was NOT verified and is treated as "
|
||||||
|
"unmet (fail-closed)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic diff resolution for the adversary graph.
|
||||||
|
|
||||||
|
Resolves the diff spec (get_diff semantics: staged → unstaged → HEAD~1 for
|
||||||
|
"auto"), lists changed files, and captures the bounded diff text every
|
||||||
|
criterion branch verifies against. Failures degrade into diff_note — the
|
||||||
|
review proceeds fail-closed rather than crashing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
MAX_DIFF_CHARS = 60000
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
|
||||||
|
# Zero criteria from a prompt that visibly carries a plan is an extraction
|
||||||
|
# failure (observed non-deterministically live), not a fail-closed no-plan
|
||||||
|
# run — re-run the parse ONCE before the verdict fails closed on it.
|
||||||
|
_crit = state.get("criteria") or []
|
||||||
|
_prompt = state.get("initial_prompt") or ""
|
||||||
|
if not _crit and isinstance(_prompt, str) and re.search(
|
||||||
|
r"acceptance criteria|PLAN\s*—|Plan / acceptance", _prompt, re.I
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
_pr = int(state.get("parse_retries") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
_pr = 0
|
||||||
|
if _pr < 1:
|
||||||
|
print(json.dumps({"_next": "parse", "parse_retries": _pr + 1}))
|
||||||
|
raise SystemExit(0)
|
||||||
|
proj = os.path.expanduser(
|
||||||
|
(state.get("project_dir_in") or "").strip() or state.get("project_dir") or "."
|
||||||
|
)
|
||||||
|
spec = (state.get("diff_spec") or "auto").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def git(*args):
|
||||||
|
r = subprocess.run(["git", "-C", proj, *args], capture_output=True, text=True, timeout=90)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError((r.stderr or r.stdout).strip()[:300])
|
||||||
|
return r.stdout
|
||||||
|
|
||||||
|
|
||||||
|
out = {"diff_note": "", "project_dir": proj}
|
||||||
|
try:
|
||||||
|
if spec in ("auto", ""):
|
||||||
|
text = git("diff", "--cached")
|
||||||
|
if not text.strip():
|
||||||
|
text = git("diff")
|
||||||
|
if not text.strip():
|
||||||
|
text = git("diff", "HEAD~1")
|
||||||
|
elif spec == "staged":
|
||||||
|
text = git("diff", "--cached")
|
||||||
|
elif spec == "worktree":
|
||||||
|
text = git("diff", "HEAD")
|
||||||
|
else:
|
||||||
|
text = git("diff", spec)
|
||||||
|
|
||||||
|
files = sorted({
|
||||||
|
line[6:].strip()
|
||||||
|
for line in text.splitlines()
|
||||||
|
if line.startswith("+++ b/")
|
||||||
|
})
|
||||||
|
out["changed_files"] = files[:300]
|
||||||
|
if len(files) > 300:
|
||||||
|
out["diff_note"] += (
|
||||||
|
f" (file list capped at 300 of {len(files)} changed files — use the fs "
|
||||||
|
"tools for the rest)"
|
||||||
|
)
|
||||||
|
if len(text) > MAX_DIFF_CHARS:
|
||||||
|
out["diff_note"] += (
|
||||||
|
f" (truncated to {MAX_DIFF_CHARS} chars of {len(text)} — use the fs tools "
|
||||||
|
"to inspect files beyond the cut)"
|
||||||
|
)
|
||||||
|
text = text[:MAX_DIFF_CHARS]
|
||||||
|
out["diff_text"] = text if text.strip() else "(empty diff — nothing changed)"
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
out["changed_files"] = []
|
||||||
|
out["diff_text"] = "(diff unavailable)"
|
||||||
|
out["diff_note"] = f" (diff resolution failed for spec {spec!r}: {e})"
|
||||||
|
|
||||||
|
print(json.dumps(out))
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed fault marker for the adversary graph.
|
||||||
|
|
||||||
|
Reached ONLY via `fallback:` routes (parse, facts, run_checks). Appends a
|
||||||
|
machine-readable "PIPELINE-FAULT:" marker to `pipeline_faults` and continues
|
||||||
|
to `verdict`, which turns any recorded fault into DIVERGES — the graph never
|
||||||
|
dies without its sentinel, and a degraded run can never read as CONFORMS.
|
||||||
|
|
||||||
|
Stage attribution — grounded in engine behavior: a SCRIPT node's fallback
|
||||||
|
route writes NOTHING into state (src/graph/executor.rs script arm routes to
|
||||||
|
the fallback without recording the error), so the two script stages are
|
||||||
|
discriminated by which stage outputs are present:
|
||||||
|
- parse (llm): its `state_updates` wrote an "LLM node …failed: <chain>"
|
||||||
|
string into `parse_failure` — attribute to parse.
|
||||||
|
- run_checks vs facts (scripts): diff_facts.py unconditionally writes a
|
||||||
|
NONEMPTY `diff_text` (even its internal-failure path writes
|
||||||
|
"(diff unavailable)"), while the graph's initial value is ''. A nonempty
|
||||||
|
`diff_text` proves facts completed, so the fault is the verification
|
||||||
|
runner (run_checks killed at the node level, e.g. by the node timeout);
|
||||||
|
an empty `diff_text` means facts itself died.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
parse_failure = state.get("parse_failure")
|
||||||
|
detail = parse_failure.strip() if isinstance(parse_failure, str) else ""
|
||||||
|
diff_text = state.get("diff_text")
|
||||||
|
facts_completed = isinstance(diff_text, str) and bool(diff_text.strip())
|
||||||
|
if detail.startswith("LLM node"):
|
||||||
|
detail = detail.replace("\n", " ")
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
faults.append(f"PIPELINE-FAULT: parse failed — cannot review: {detail}")
|
||||||
|
elif facts_completed:
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: verification runner killed — the run_checks stage "
|
||||||
|
"died at the node level before recording exec_results; execution "
|
||||||
|
"criteria are unproven"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: diff resolution failed — changed files and diff text "
|
||||||
|
"could not be resolved; criterion verification never ran"
|
||||||
|
)
|
||||||
|
print(json.dumps({"pipeline_faults": faults}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"pipeline_faults": [
|
||||||
|
f"PIPELINE-FAULT: fault-marker script error: {e} — treat the "
|
||||||
|
"run as degraded"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic execution-evidence recorder for the adversary graph.
|
||||||
|
|
||||||
|
Runs ONLY the caller-declared verification commands (verification_commands)
|
||||||
|
in the project dir and records the outcomes as `exec_results` — a list of
|
||||||
|
{cmd, exit, duration_s, tail} records. Criterion branches cite this record;
|
||||||
|
they never execute anything themselves.
|
||||||
|
|
||||||
|
NO auto-detection: undeclared commands mean a "none declared" marker
|
||||||
|
(fail-visible, not fail-guessed). Any runner error degrades into an
|
||||||
|
ENVIRONMENT marker — this script never fails the node.
|
||||||
|
|
||||||
|
Trust boundary: `verification_commands` is a DECLARED graph variable (or the
|
||||||
|
review-gauntlet's structured `inputs:` passthrough), never a field an LLM
|
||||||
|
extracts from the spawn prompt — that prose also carries plan/diff text pasted
|
||||||
|
from the repo under review, and whatever is declared here runs with
|
||||||
|
shell=True. Variables land in state as strings, so a JSON-encoded list is
|
||||||
|
accepted alongside a real list. A declaration that is not a JSON array of
|
||||||
|
strings is a caller contract violation, not a runner hiccup: it executes
|
||||||
|
NOTHING and fails closed as a PIPELINE-FAULT (complaint #1) — it never
|
||||||
|
degrades to an ENVIRONMENT marker. The engine merges only the keys an
|
||||||
|
llm node's `output_schema.properties` declares, so a parse LLM coaxed into
|
||||||
|
emitting an extra `verification_commands` key cannot overwrite the declared
|
||||||
|
value: the undeclared key is dropped before it reaches state.
|
||||||
|
|
||||||
|
Budget: commands run sequentially, so TOTAL runtime is bounded by a deadline
|
||||||
|
(TOTAL_DEADLINE_SECS = 3300s, the run_checks node's 3600s timeout minus
|
||||||
|
margin). Without it, a handful of hanging commands (4 x 900s) would blow the
|
||||||
|
NODE timeout and the executor would kill this script from OUTSIDE — the
|
||||||
|
in-script degradation would never fire. Each command gets
|
||||||
|
min(PER_COMMAND_TIMEOUT_SECS, remaining budget); commands the deadline
|
||||||
|
leaves no budget for are recorded as {"cmd", "skipped": "deadline"} entries,
|
||||||
|
so the in-script handling stays authoritative. The env var
|
||||||
|
ADVERSARY_RUN_CHECKS_DEADLINE_SECS overrides the deadline (test seam).
|
||||||
|
|
||||||
|
Process hygiene: commands run through the shell. On POSIX each command gets
|
||||||
|
its own session (start_new_session) and a timeout SIGKILLs the whole process
|
||||||
|
group, so grandchildren of a hung command cannot linger. The post-kill
|
||||||
|
reap/drain is itself bounded by DRAIN_TIMEOUT_SECS: a grandchild that escaped
|
||||||
|
the session (its own start_new_session) but still holds the stdout pipe
|
||||||
|
would otherwise block the drain until it exits. On non-POSIX platforms only
|
||||||
|
the direct shell child is killed — grandchildren of a timed-out command may
|
||||||
|
survive (documented limitation).
|
||||||
|
|
||||||
|
Verification commands run with GRAPH_STATE* (GRAPH_STATE, GRAPH_STATE_FILE)
|
||||||
|
scrubbed from their environment: the reviewed repo's own tests may exec these very
|
||||||
|
scripts with their own GRAPH_STATE, and load_state() prefers
|
||||||
|
GRAPH_STATE_FILE, so an inherited live state file would silently win.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import fcntl
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
PER_COMMAND_TIMEOUT_SECS = 900
|
||||||
|
TOTAL_DEADLINE_SECS = float(os.environ.get("ADVERSARY_RUN_CHECKS_DEADLINE_SECS") or 3300)
|
||||||
|
STACK_LOCK_PATH = os.path.expanduser("~/.cache/coyote/review-stack.lock")
|
||||||
|
STACK_LOCK_WAIT_SECS = float(os.environ.get("ADVERSARY_STACK_LOCK_WAIT_SECS") or 1200)
|
||||||
|
|
||||||
|
|
||||||
|
def acquire_stack_lock():
|
||||||
|
"""Serialize with the probe lane's shared-stack work (advisory flock on a
|
||||||
|
well-known path; the probe is instructed to flock the same file around
|
||||||
|
stack boot/teardown). Bounded wait so a hogged lock can never turn into a
|
||||||
|
false red verification: on timeout we proceed WITHOUT the lock and note it
|
||||||
|
on stderr. The fd is held for the process lifetime (released on exit)."""
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(STACK_LOCK_PATH), exist_ok=True)
|
||||||
|
fd = os.open(STACK_LOCK_PATH, os.O_CREAT | os.O_RDWR)
|
||||||
|
except OSError as e:
|
||||||
|
sys.stderr.write(f"WARN: review-stack lock unavailable ({e}); running unlocked\n")
|
||||||
|
return None
|
||||||
|
limit = time.monotonic() + STACK_LOCK_WAIT_SECS
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
return fd
|
||||||
|
except OSError:
|
||||||
|
if time.monotonic() >= limit:
|
||||||
|
os.close(fd)
|
||||||
|
sys.stderr.write(
|
||||||
|
"WARN: review-stack lock still held after bounded wait — proceeding "
|
||||||
|
"WITHOUT it; shared-stack collisions with the probe lane are possible\n"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
time.sleep(5)
|
||||||
|
DRAIN_TIMEOUT_SECS = 10
|
||||||
|
TAIL_LINES = 50
|
||||||
|
MAX_TAIL_CHARS = 8000
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDeclaration(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def declared_commands(declared):
|
||||||
|
"""Normalize the verification_commands declaration to a list of commands.
|
||||||
|
|
||||||
|
Accepts a JSON list or a JSON-encoded string of one; missing/None means
|
||||||
|
none declared. Anything else raises InvalidDeclaration.
|
||||||
|
"""
|
||||||
|
if declared is None:
|
||||||
|
return []
|
||||||
|
if isinstance(declared, str):
|
||||||
|
if not declared.strip():
|
||||||
|
raise InvalidDeclaration("empty string (declare '[]' for none)")
|
||||||
|
try:
|
||||||
|
declared = json.loads(declared)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise InvalidDeclaration(f"not valid JSON: {e}") from e
|
||||||
|
if not isinstance(declared, list):
|
||||||
|
raise InvalidDeclaration(
|
||||||
|
f"expected a JSON array of strings, got {type(declared).__name__}"
|
||||||
|
)
|
||||||
|
bad = [c for c in declared if not isinstance(c, str)]
|
||||||
|
if bad:
|
||||||
|
raise InvalidDeclaration(f"non-string item(s): {json.dumps(bad)}")
|
||||||
|
return [c.strip() for c in declared if c.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def tail_of(text):
|
||||||
|
tail = "\n".join(text.splitlines()[-TAIL_LINES:])
|
||||||
|
if len(tail) > MAX_TAIL_CHARS:
|
||||||
|
tail = "…" + tail[-MAX_TAIL_CHARS:]
|
||||||
|
return tail
|
||||||
|
|
||||||
|
|
||||||
|
def run_one(cmd, proj, budget):
|
||||||
|
"""Run one command, hard-bounded by `budget` seconds; return its record."""
|
||||||
|
start = time.monotonic()
|
||||||
|
popen_kwargs = {"start_new_session": True} if os.name == "posix" else {}
|
||||||
|
env = {k: v for k, v in os.environ.items() if not k.startswith("GRAPH_STATE")}
|
||||||
|
p = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
cwd=proj,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
env=env,
|
||||||
|
**popen_kwargs,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
out_s, err_s = p.communicate(timeout=budget)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if os.name == "posix":
|
||||||
|
try:
|
||||||
|
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
||||||
|
except (ProcessLookupError, PermissionError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
p.kill()
|
||||||
|
try:
|
||||||
|
p.communicate(timeout=DRAIN_TIMEOUT_SECS)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"cmd": cmd,
|
||||||
|
"exit": -1,
|
||||||
|
"duration_s": round(time.monotonic() - start, 1),
|
||||||
|
"tail": f"TIMEOUT: no exit within {budget:.0f}s — recorded as a failing run",
|
||||||
|
}
|
||||||
|
combined = (out_s or "") + (("\n" + err_s) if err_s else "")
|
||||||
|
return {
|
||||||
|
"cmd": cmd,
|
||||||
|
"exit": p.returncode,
|
||||||
|
"duration_s": round(time.monotonic() - start, 1),
|
||||||
|
"tail": tail_of(combined),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
try:
|
||||||
|
cmds = declared_commands(state.get("verification_commands"))
|
||||||
|
except InvalidDeclaration as e:
|
||||||
|
msg = f"PIPELINE-FAULT: verification_commands declaration invalid — {e}"
|
||||||
|
prior = state.get("pipeline_faults")
|
||||||
|
faults = [f for f in prior if isinstance(f, str)] if isinstance(prior, list) else []
|
||||||
|
faults.append(msg)
|
||||||
|
print(json.dumps({"exec_results": msg, "pipeline_faults": faults}))
|
||||||
|
return
|
||||||
|
if not cmds:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"exec_results": (
|
||||||
|
"none declared — the caller supplied no verification_commands; "
|
||||||
|
"execution criteria (build/test/lint) cannot be independently "
|
||||||
|
"confirmed. Declare the exact commands via the "
|
||||||
|
"verification_commands variable to get recorded runs."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
proj = os.path.expanduser(
|
||||||
|
(state.get("project_dir") or "").strip() or "."
|
||||||
|
)
|
||||||
|
deadline = time.monotonic() + TOTAL_DEADLINE_SECS
|
||||||
|
results = []
|
||||||
|
_stack_lock = acquire_stack_lock() if cmds else None # noqa: F841 — held until exit
|
||||||
|
for cmd in cmds:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
results.append({"cmd": cmd, "skipped": "deadline"})
|
||||||
|
continue
|
||||||
|
results.append(run_one(cmd, proj, min(PER_COMMAND_TIMEOUT_SECS, remaining)))
|
||||||
|
print(json.dumps({"exec_results": results}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — never fail the node; degrade visibly
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"exec_results": (
|
||||||
|
f"ENVIRONMENT: verification runner error: {e} — the declared "
|
||||||
|
"commands could not be executed; execution criteria are unproven "
|
||||||
|
"(PARTIAL at the criterion level; the verdict blocks CONFORMS when "
|
||||||
|
"commands were declared)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic conformance verdict for the adversary graph.
|
||||||
|
|
||||||
|
CONFORMS requires: at least one criterion, every criterion MET, zero
|
||||||
|
holistic complaints, zero pipeline faults, and zero recorded red
|
||||||
|
verification runs (exit≠0/timeout/skipped, or an ENVIRONMENT runner-error
|
||||||
|
marker when commands were declared — the declared commands never ran, so the
|
||||||
|
run is unproven). Everything else — including a missing plan or a degraded
|
||||||
|
(faulted) pipeline — is DIVERGES (fail-closed).
|
||||||
|
Any "PIPELINE-FAULT:" marker recorded in `pipeline_faults` (or a holistic
|
||||||
|
llm-node failure captured in `holistic_failure`) becomes complaint #1;
|
||||||
|
per-criterion results are still reported. A criterion whose check DIED
|
||||||
|
(criterion_fault's UNMET verdict, evidence prefixed DIED_EVIDENCE_PREFIX) is
|
||||||
|
rendered as died rather than judged and counted as unmet regardless of the
|
||||||
|
status it carries. Assembles the exact sentinel format the callers route on.
|
||||||
|
Never crashes into a silent verdict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_FAULT_DETAIL_CHARS = 300
|
||||||
|
MAX_MARKER_CHARS = 200
|
||||||
|
DIED_MARKER = "criterion check DIED (pipeline fault)"
|
||||||
|
DIED_EVIDENCE_PREFIX = "PIPELINE-FAULT: criterion "
|
||||||
|
|
||||||
|
|
||||||
|
def died(v):
|
||||||
|
evidence = v.get("evidence")
|
||||||
|
return isinstance(evidence, str) and evidence.startswith(DIED_EVIDENCE_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def collect_pipeline_faults(state):
|
||||||
|
"""Recorded fault markers, plus a holistic llm-node failure if one occurred.
|
||||||
|
|
||||||
|
`holistic_failure` holds the "LLM node …failed: <chain>" string when the
|
||||||
|
holistic pass fell back to verdict; on a successful pass it holds the
|
||||||
|
rendered schema output (never that prefix).
|
||||||
|
"""
|
||||||
|
faults = [
|
||||||
|
f.strip()
|
||||||
|
for f in (state.get("pipeline_faults") or [])
|
||||||
|
if isinstance(f, str) and f.strip()
|
||||||
|
]
|
||||||
|
hf = state.get("holistic_failure")
|
||||||
|
if isinstance(hf, str) and hf.strip().startswith("LLM node"):
|
||||||
|
detail = hf.strip().replace("\n", " ")
|
||||||
|
if len(detail) > MAX_FAULT_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_FAULT_DETAIL_CHARS - 1] + "…"
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: holistic pass failed — criterion verdicts stand "
|
||||||
|
f"but cross-cutting hunt did not run ({detail})"
|
||||||
|
)
|
||||||
|
return faults
|
||||||
|
|
||||||
|
|
||||||
|
def append_criterion_complaints(lines, verdicts, extra, start):
|
||||||
|
"""Numbered complaints for non-MET verdicts and holistic extras."""
|
||||||
|
i = start
|
||||||
|
for v in verdicts:
|
||||||
|
if v.get("status") == "MET" and not died(v):
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
text = (v.get("text") or "").strip().replace("\n", " ")
|
||||||
|
if len(text) > 140:
|
||||||
|
text = text[:137] + "…"
|
||||||
|
complaint = (v.get("complaint") or "").strip()
|
||||||
|
if died(v):
|
||||||
|
if not complaint.startswith(DIED_MARKER):
|
||||||
|
complaint = f"{DIED_MARKER} — {complaint}" if complaint else DIED_MARKER
|
||||||
|
lines.append(f'{i}. Acceptance criterion "{text}" — {complaint}')
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f'{i}. Acceptance criterion "{text}" — {v.get("status", "?").title()} — '
|
||||||
|
f"{complaint}"
|
||||||
|
)
|
||||||
|
for c in extra:
|
||||||
|
i += 1
|
||||||
|
lines.append(
|
||||||
|
f"{i}. {c.get('kind', 'violation')} — {c.get('location', '?')} — "
|
||||||
|
f"{c.get('violation', '')} — {c.get('fix', '')}"
|
||||||
|
)
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def declared_nonempty(declared):
|
||||||
|
"""True when verification_commands names at least one command.
|
||||||
|
|
||||||
|
Accepts a list or a JSON-encoded string of one. None, unparseable, or
|
||||||
|
empty declarations count as not declared — run_checks already records
|
||||||
|
a PIPELINE-FAULT for the invalid ones.
|
||||||
|
"""
|
||||||
|
if isinstance(declared, str):
|
||||||
|
try:
|
||||||
|
declared = json.loads(declared)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return False
|
||||||
|
if not isinstance(declared, list):
|
||||||
|
return False
|
||||||
|
return any(isinstance(c, str) and c.strip() for c in declared)
|
||||||
|
|
||||||
|
|
||||||
|
def exec_run_complaints(exec_results, declared):
|
||||||
|
"""One complaint per red verification run recorded by run_checks.py.
|
||||||
|
|
||||||
|
A red run is a record that was skipped (deadline) or exited nonzero
|
||||||
|
(including the -1 TIMEOUT sentinel). An "ENVIRONMENT: …" runner-error
|
||||||
|
marker with a non-empty declaration is one complaint: the declared
|
||||||
|
commands never ran. Other string markers ("none declared",
|
||||||
|
"PIPELINE-FAULT…") are not runs and yield nothing.
|
||||||
|
"""
|
||||||
|
if isinstance(exec_results, str):
|
||||||
|
marker = exec_results.strip().replace("\n", " ")
|
||||||
|
if not marker.startswith("ENVIRONMENT:") or not declared_nonempty(declared):
|
||||||
|
return []
|
||||||
|
if len(marker) > MAX_MARKER_CHARS:
|
||||||
|
marker = marker[: MAX_MARKER_CHARS - 1] + "…"
|
||||||
|
return [
|
||||||
|
f"Verification runner error — the declared command(s) never ran ({marker}); "
|
||||||
|
"unproven runs block CONFORMS"
|
||||||
|
]
|
||||||
|
if not isinstance(exec_results, list):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for r in exec_results:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
cmd = r.get("cmd", "?")
|
||||||
|
if r.get("skipped"):
|
||||||
|
out.append(f"Verification run `{cmd}` — never ran: deadline expired (unproven)")
|
||||||
|
continue
|
||||||
|
code = r.get("exit")
|
||||||
|
if code == 0:
|
||||||
|
continue
|
||||||
|
outcome = "TIMEOUT (exit -1)" if code == -1 else f"exit {code}"
|
||||||
|
out.append(
|
||||||
|
f"Verification run `{cmd}` — {outcome} (recorded FAIL; a red declared "
|
||||||
|
"command blocks CONFORMS regardless of criterion judgments)"
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def render_exec_results(exec_results):
|
||||||
|
"""Lines for the "Verification runs:" report section.
|
||||||
|
|
||||||
|
exec_results is a list of {cmd, exit, duration_s, tail} records (or
|
||||||
|
{cmd, skipped} for commands the runner's total deadline left no budget
|
||||||
|
for) from run_checks.py, or a string marker ("none declared —…" /
|
||||||
|
"ENVIRONMENT: …"), or empty when the run never reached the stage.
|
||||||
|
"""
|
||||||
|
if isinstance(exec_results, list):
|
||||||
|
out = []
|
||||||
|
for r in exec_results:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
if r.get("skipped"):
|
||||||
|
out.append(
|
||||||
|
f"- [SKIPPED] `{r.get('cmd', '?')}` — never ran: the runner's "
|
||||||
|
"total deadline expired first; covered criteria are unproven"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
status = "PASS" if r.get("exit") == 0 else "FAIL"
|
||||||
|
out.append(
|
||||||
|
f"- [{status}] `{r.get('cmd', '?')}` — exit {r.get('exit', '?')}, "
|
||||||
|
f"{r.get('duration_s', '?')}s"
|
||||||
|
)
|
||||||
|
if status == "FAIL":
|
||||||
|
tail = (r.get("tail") or "").strip()
|
||||||
|
out.extend(" " + t for t in tail.splitlines()[-15:])
|
||||||
|
return out or ["- (no verification commands were run)"]
|
||||||
|
if isinstance(exec_results, str) and exec_results.strip():
|
||||||
|
return [f"- {exec_results.strip()}"]
|
||||||
|
return ["- none recorded (the run did not reach the verification stage)"]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
verdicts = [v for v in (state.get("crit_verdicts") or []) if isinstance(v, dict)]
|
||||||
|
extra = [c for c in (state.get("extra_complaints") or []) if isinstance(c, dict)]
|
||||||
|
observations = (state.get("observations") or "").strip()
|
||||||
|
faults = collect_pipeline_faults(state)
|
||||||
|
reds = exec_run_complaints(
|
||||||
|
state.get("exec_results"), state.get("verification_commands")
|
||||||
|
)
|
||||||
|
n = len(verdicts)
|
||||||
|
|
||||||
|
met = [v for v in verdicts if v.get("status") == "MET" and not died(v)]
|
||||||
|
partial = [v for v in verdicts if v.get("status") == "PARTIAL" and not died(v)]
|
||||||
|
bad = [
|
||||||
|
v for v in verdicts if died(v) or v.get("status") in ("UNMET", "DIVERGED")
|
||||||
|
]
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if faults:
|
||||||
|
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
|
||||||
|
if n:
|
||||||
|
lines.append(
|
||||||
|
f"Criteria: {len(met)}/{n} met, {len(partial)} partial, "
|
||||||
|
f"{len(bad)} unmet/diverged — degraded run: pipeline fault recorded "
|
||||||
|
"(fail-closed)."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
"Criteria: none verified — degraded run: pipeline fault recorded "
|
||||||
|
"(fail-closed)."
|
||||||
|
)
|
||||||
|
lines.append("Complaints:")
|
||||||
|
i = 0
|
||||||
|
for f in faults:
|
||||||
|
i += 1
|
||||||
|
lines.append(f"{i}. {f}")
|
||||||
|
i = append_criterion_complaints(lines, verdicts, extra, i)
|
||||||
|
for c in reds:
|
||||||
|
i += 1
|
||||||
|
lines.append(f"{i}. {c}")
|
||||||
|
if observations:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Non-blocking observations:")
|
||||||
|
lines.append(observations)
|
||||||
|
elif n == 0:
|
||||||
|
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
|
||||||
|
lines.append("Criteria: none provided.")
|
||||||
|
lines.append("Complaints:")
|
||||||
|
lines.append(
|
||||||
|
"1. No acceptance criteria were provided or extractable — conformance cannot "
|
||||||
|
"be judged without a spec (fail-closed). Supply the plan's Objective/Tasks/"
|
||||||
|
"Acceptance criteria and re-run."
|
||||||
|
)
|
||||||
|
for i, c in enumerate(reds, start=2):
|
||||||
|
lines.append(f"{i}. {c}")
|
||||||
|
elif not partial and not bad and not extra and not reds:
|
||||||
|
lines.append("ADVERSARIAL_REVIEW: CONFORMS")
|
||||||
|
lines.append(f"Criteria: {n}/{n} met (all with tests).")
|
||||||
|
if observations:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Non-blocking observations:")
|
||||||
|
lines.append(observations)
|
||||||
|
else:
|
||||||
|
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
|
||||||
|
header = f"Criteria: {len(met)}/{n} met, {len(partial)} partial, {len(bad)} unmet/diverged"
|
||||||
|
notes = []
|
||||||
|
if died_count := sum(1 for v in verdicts if died(v)):
|
||||||
|
notes.append(f"degraded run: {died_count} criterion check(s) died (fail-closed)")
|
||||||
|
if reds:
|
||||||
|
notes.append(f"{len(reds)} red verification run(s) (fail-closed)")
|
||||||
|
lines.append(header + "".join(f" — {note}" for note in notes) + ".")
|
||||||
|
lines.append("Complaints:")
|
||||||
|
i = append_criterion_complaints(lines, verdicts, extra, 0)
|
||||||
|
for c in reds:
|
||||||
|
i += 1
|
||||||
|
lines.append(f"{i}. {c}")
|
||||||
|
if observations:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Non-blocking observations:")
|
||||||
|
lines.append(observations)
|
||||||
|
|
||||||
|
# Per-criterion evidence appendix (MET entries, for auditability).
|
||||||
|
if met:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Met criteria (evidence):")
|
||||||
|
for v in met:
|
||||||
|
text = (v.get("text") or "").strip().replace("\n", " ")
|
||||||
|
if len(text) > 100:
|
||||||
|
text = text[:97] + "…"
|
||||||
|
lines.append(f'- "{text}" — {(v.get("evidence") or "").strip()}')
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Verification runs:")
|
||||||
|
lines.extend(render_exec_results(state.get("exec_results")))
|
||||||
|
|
||||||
|
print(json.dumps({"adv_report": "\n".join(lines)}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — never a silent verdict
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"adv_report": (
|
||||||
|
"ADVERSARIAL_REVIEW: DIVERGES\n"
|
||||||
|
f"Criteria: verdict computation error: {e} — treat as failed, not as conforming."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Architect
|
||||||
|
|
||||||
|
A **design-doc orchestrator for any project**. Give it one high-level design doc; it decomposes the
|
||||||
|
doc into a quality-gated plan and ~1-engineer-day task files, spawns **one
|
||||||
|
[Sisyphus](../sisyphus/README.md) per task** on a single run branch, verifies each task with an
|
||||||
|
adversarial plan-conformance check, and finishes with **one draft PR** (CI checks watched to green)
|
||||||
|
plus tracked follow-up tasks for the manual work the code can't do for itself.
|
||||||
|
|
||||||
|
Architect does **not** write feature code itself. It owns the *process*; Sisyphus owns each *task*.
|
||||||
|
|
||||||
|
## The pipeline it drives
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
user([Design doc]) --> architect["Architect<br/>design-doc orchestrator"]
|
||||||
|
architect --> orient["Phase A — Orient<br/>project conventions · build/test commands · design doc"]
|
||||||
|
orient --> design["Phase B — design-session<br/>plans_dir/PLAN-<slug>.md + 1-day task breakdown"]
|
||||||
|
|
||||||
|
design -. "grounding" .-> explore[["explore<br/>codebase grep<br/>× parallel"]]
|
||||||
|
design -. "unfamiliar libraries" .-> librarian[["librarian<br/>docs + OSS grep"]]
|
||||||
|
explore -. "findings ground<br/>the breakdown" .-> design
|
||||||
|
librarian -. "findings ground<br/>the breakdown" .-> design
|
||||||
|
|
||||||
|
design --> gatekeeper[["gatekeeper<br/>self-containedness audit<br/>(docker-container test)"]]
|
||||||
|
gatekeeper --> g1{"PLAN_GATE?"}
|
||||||
|
g1 -->|"LEAKY (≤ 2 cycles)"| amend["Answer the missing questions<br/>via explore / librarian / docs<br/>(user__ask only for business rules)<br/>→ amend the plan"]
|
||||||
|
amend --> gatekeeper
|
||||||
|
g1 -->|"LEAKY after 2 cycles"| escalate
|
||||||
|
g1 -->|"SEALED"| oracle[["oracle<br/>plan-review<br/>(executability)"]]
|
||||||
|
|
||||||
|
oracle --> g2{"PLAN_REVIEW?"}
|
||||||
|
g2 -->|"REJECT — fix complaints,<br/>re-submit SAME session"| oracle
|
||||||
|
g2 -->|"OKAY"| tasks["Phase D — materialize tasks<br/>plans_dir/tasks/TASK-NNN-*/ (task-tracking)"]
|
||||||
|
|
||||||
|
tasks --> branch["Phase E — run branch<br/>feat/PLAN-<slug> off base_branch"]
|
||||||
|
branch --> claim["Claim task (sequential, dependency order)<br/>status: in-progress + base SHA"]
|
||||||
|
claim --> sisyphus[["sisyphus<br/>implement ONE task on the run branch<br/>commit + push — NO PR"]]
|
||||||
|
sisyphus --> adversary[["adversary<br/>conformance check<br/>diff vs task base SHA"]]
|
||||||
|
|
||||||
|
adversary --> verdict{"ADVERSARIAL_REVIEW?"}
|
||||||
|
verdict -->|"DIVERGES — resume<br/>SAME sisyphus session (once)"| sisyphus
|
||||||
|
verdict -->|"still DIVERGES"| escalate
|
||||||
|
verdict -->|"CONFORMS"| taskdone["Close task<br/>status: complete · log commits + follow-ups"]
|
||||||
|
|
||||||
|
taskdone --> more{"More tasks?"}
|
||||||
|
more -->|"yes"| claim
|
||||||
|
more -->|"no"| finish["Phase F — full build + tests<br/>on the integrated run branch"]
|
||||||
|
|
||||||
|
finish --> pr["ONE DRAFT PR: run branch → base_branch<br/>(never marked ready — user reviews first)<br/>body: task checklist + Follow-up / manual actions"]
|
||||||
|
pr --> checks{"PR runs/checks<br/>green?"}
|
||||||
|
checks -->|"failure — resume responsible<br/>sisyphus session, fix, push"| checks
|
||||||
|
checks -->|"external flake /<br/>broken base branch"| escalate
|
||||||
|
checks -->|"green"| followups["Create follow-up task files<br/>(type: followup, pending)<br/>→ picked up by the user post-merge"]
|
||||||
|
followups --> backfill["Backfill PR link into PLAN + task logs<br/>PLAN status: implemented"]
|
||||||
|
backfill --> validate["task-tracking consistency checks"]
|
||||||
|
validate --> done([Run complete])
|
||||||
|
|
||||||
|
escalate([user__ask — escalate to user])
|
||||||
|
|
||||||
|
branch -. "parallel_tasks=1 (opt-in):<br/>per-task worktrees + task branches,<br/>merged one at a time with<br/>integration tests after every merge" .-> claim
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where state lives
|
||||||
|
|
||||||
|
Everything is file-based in **`plans_dir`** (default `plans/`, resolved against the project):
|
||||||
|
|
||||||
|
```
|
||||||
|
<plans_dir>/
|
||||||
|
PLAN-<slug>.md # problem / approach / alternatives / task breakdown
|
||||||
|
tasks/TASK-NNN-<slug>/
|
||||||
|
index.md # What / Steps / Acceptance criteria; status in frontmatter
|
||||||
|
log.md # append-only audit trail (branch, commits, follow-ups, PR)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `plans_dir` **inside the repo** (default) → planning files ride the run branch and land in the PR
|
||||||
|
(self-documenting review).
|
||||||
|
- `plans_dir` **absolute, outside the repo** (e.g. a common runs directory) → nothing planning-related
|
||||||
|
is ever committed.
|
||||||
|
|
||||||
|
Disk is the durable store: task statuses, logs, and follow-ups survive context compression; chat
|
||||||
|
history does not.
|
||||||
|
|
||||||
|
## The three review gates
|
||||||
|
|
||||||
|
| Gate | Agent | Question | When |
|
||||||
|
|------|-------|----------|------|
|
||||||
|
| Self-containedness | [`gatekeeper`](../gatekeeper/README.md) | "Can a context-free LLM implement from this plan alone?" | Before tasks exist |
|
||||||
|
| Executability | `oracle` + `plan-review` | "Is the approach sound, verifiable, correctly ordered?" | After sealing |
|
||||||
|
| Conformance | [`adversary`](../adversary/README.md) | "Is the built code what the plan asked for?" | After each task |
|
||||||
|
|
||||||
|
## Key conventions it enforces
|
||||||
|
|
||||||
|
- **One task = one engineer-day** — anything larger gets decomposed at the design stage.
|
||||||
|
- **Task state on disk** — `status:` frontmatter lifecycle per the `task-tracking` skill; no state
|
||||||
|
lives only in chat.
|
||||||
|
- **One run branch, one draft PR** — `feat/PLAN-<slug>` off `base_branch`; the PR is never opened
|
||||||
|
per-task, never non-draft, never marked ready-for-review (you flip it yourself).
|
||||||
|
- **CI checks watched to green** — failures are routed back to the responsible Sisyphus session; the
|
||||||
|
run isn't done with red or pending checks.
|
||||||
|
- **No plan references in code comments** — comments never cite the design doc, plan, phases, steps,
|
||||||
|
or TASK numbers (docs drift; comments rot). Plan references live in commit messages only.
|
||||||
|
- **`.env` never lands in a repo** — only `.env.example` with placeholder keys; real values become a
|
||||||
|
follow-up.
|
||||||
|
- **Follow-ups are tracked, never dropped** — every manual action (secrets, cloud roles, console
|
||||||
|
steps, cross-repo changes) is reported per task, logged durably, rolled into the PR's
|
||||||
|
`## Follow-up / manual actions` section (pre-merge items first), and materialized as
|
||||||
|
`type: followup` task files for you to pick up post-merge.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# From the target project root (default autonomy: full)
|
||||||
|
coyote -a architect --agent-variable design_doc docs/design/my-feature.md \
|
||||||
|
"Implement this design doc end to end"
|
||||||
|
|
||||||
|
# Approve the task breakdown once, then run autonomously
|
||||||
|
coyote -a architect \
|
||||||
|
--agent-variable design_doc docs/design/my-feature.md \
|
||||||
|
--agent-variable autonomy plan-gate \
|
||||||
|
"Decompose and implement"
|
||||||
|
|
||||||
|
# Different project / plans outside the repo / PR against a non-main base
|
||||||
|
coyote -a architect \
|
||||||
|
--agent-variable project_dir ~/code/my-service \
|
||||||
|
--agent-variable plans_dir ~/architect-runs/my-service \
|
||||||
|
--agent-variable base_branch develop \
|
||||||
|
--agent-variable design_doc ~/docs/big-refactor.md \
|
||||||
|
"Run the pipeline"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| `project_dir` | `.` | The target repo — the only WRITE target for feature code. |
|
||||||
|
| `plans_dir` | `plans` | Where PLAN + task files live. Relative → in-repo (rides the PR); absolute → outside git. |
|
||||||
|
| `design_doc` | *(empty)* | Path to the design doc; asked for if unset. |
|
||||||
|
| `base_branch` | `main` | Branch the run branch forks from and the PR targets. |
|
||||||
|
| `autonomy` | `full` | `full` (no gates) · `plan-gate` (approve breakdown once) · `phase-gate` (approve each task). |
|
||||||
|
| `escalation_policy` | `interactive` | `interactive` (pause and ask) · `autonomous` (self-resolve decisions covered by a standing ruling in `~/.config/coyote/share/standing-rulings.md` or a clearly recommended option; everything self-resolved lands in a “Decisions taken autonomously” digest in the Phase F report and PR body; secrets/money/destructive actions and novel conflicts still escalate). |
|
||||||
|
| `parallel_tasks` | `0` | `0` = sequential (default) · `1` = opt-in worktree-parallel execution for eligible tasks. |
|
||||||
|
| `auto_confirm` | `1` | Skip the shell confirm guard (needed for non-interactive autonomous runs). |
|
||||||
|
|
||||||
|
## Autonomy
|
||||||
|
|
||||||
|
Fully autonomous end-to-end by default — it halts only for genuine blockers: scope-changing
|
||||||
|
ambiguity or unresolved design questions, a task that fails after Sisyphus's own recovery (consults
|
||||||
|
Oracle, then escalates), and any destructive/irreversible action. Use `plan-gate` or `phase-gate`
|
||||||
|
to insert approval checkpoints.
|
||||||
|
|
||||||
|
## Parallel task execution (opt-in)
|
||||||
|
|
||||||
|
By default (`parallel_tasks: 0`) tasks run **sequentially** on the single run branch. Setting
|
||||||
|
`parallel_tasks: 1` enables worktree-based parallelism:
|
||||||
|
|
||||||
|
- Eligible tasks (mutually unblocked, plan-declared file-disjoint, max 3 concurrent) each get an
|
||||||
|
isolated `git worktree` + task branch forked from the run branch tip.
|
||||||
|
- Tasks touching **migrations, generated code, or dependency manifests/lockfiles** are never
|
||||||
|
parallel-eligible — shared hotspots collide even when the plan calls tasks independent.
|
||||||
|
- Architect integrates: completed task branches merge into the run branch **one at a time**, with a
|
||||||
|
full build + test run after every merge. Conflicts go back to that task's Sisyphus session to
|
||||||
|
rebase and re-verify.
|
||||||
|
- Optionally, the `agent__task_*` queue may mirror the on-disk `blocked_by` graph so completing a
|
||||||
|
task auto-dispatches its unblocked dependents. The queue is in-memory convenience only — disk task
|
||||||
|
files remain the durable source of truth.
|
||||||
|
- Worktrees and task branches are cleaned up after each clean merge. Phase F (single draft PR +
|
||||||
|
CI-check watch) is unchanged in both modes.
|
||||||
|
|
||||||
|
## Sub-agents it spawns
|
||||||
|
|
||||||
|
| Agent | Used for |
|
||||||
|
|-------|----------|
|
||||||
|
| [`sisyphus`](../sisyphus/README.md) | Implement ONE task's code (its own explore→coder→verify→review loop). One per task. |
|
||||||
|
| [`gatekeeper`](../gatekeeper/README.md) | Plan self-containedness gate (`PLAN_GATE: SEALED/LEAKY`). |
|
||||||
|
| [`adversary`](../adversary/README.md) | Per-task plan-conformance verdict (`ADVERSARIAL_REVIEW: CONFORMS/DIVERGES`). |
|
||||||
|
| [`oracle`](../oracle/README.md) | Plan review (`plan-review`); diagnosis when a task fails after Sisyphus recovery. |
|
||||||
|
| [`explore`](../explore/README.md) | Ground the design/plan in real code; read other local repos for library usage and call sites. |
|
||||||
|
| [`librarian`](../librarian/README.md) | External docs / OSS examples for unfamiliar libraries. |
|
||||||
|
| [`whetstone`](../whetstone/README.md) | Continuous improvement — spawned once before the final report when the run addressed PR review feedback from outside its own lanes; converts review misses into user-approved suite edits / ledger entries. Never blocks the run. |
|
||||||
|
| [`review-gauntlet`](../review-gauntlet/README.md) | Preferred per-task conformance gate — one spawn running adversary (+ probe for consumer-facing tasks) with a machine-parsed GAUNTLET: PASS/BLOCKED verdict; a `GAUNTLET_REVIEW_INCOMPLETE:` line is an infrastructure fault escalated to the user (retry / accept NEEDS-HUMAN / abort), never a divergence; direct adversary/probe spawns remain the fallback. |
|
||||||
|
|
||||||
|
## Related skills
|
||||||
|
|
||||||
|
- [`design-session`](../../skills/design-session/SKILL.md) — design doc → grounded proposal → PLAN + sized breakdown.
|
||||||
|
- [`task-tracking`](../../skills/task-tracking/SKILL.md) — the task-file schema, lifecycle, and consistency checks.
|
||||||
|
- [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) — the gatekeeper's self-containedness manifest.
|
||||||
|
- [`plan-authoring`](../../skills/plan-authoring/SKILL.md) / [`plan-review`](../../skills/plan-review/SKILL.md) — plan schema + oracle's executability review.
|
||||||
@@ -0,0 +1,665 @@
|
|||||||
|
name: architect
|
||||||
|
description: |
|
||||||
|
Design-doc orchestrator for any project. Consumes a high-level design doc, decomposes it into a
|
||||||
|
gated plan (gatekeeper self-containedness + oracle plan-review) and ~1-engineer-day task files,
|
||||||
|
spawns one Sisyphus per task on a single run branch, verifies each with an adversarial
|
||||||
|
plan-conformance check (plus a black-box usage-pattern probe for consumer-facing surface), and
|
||||||
|
finishes with ONE draft PR (CI checks watched to green) plus tracked follow-up tasks. Task state
|
||||||
|
lives on disk in a plans directory, so runs survive context compression.
|
||||||
|
version: 2.5.0
|
||||||
|
agent_session: temp
|
||||||
|
auto_continue: true
|
||||||
|
max_auto_continues: 100
|
||||||
|
inject_todo_instructions: true
|
||||||
|
|
||||||
|
can_spawn_agents: true
|
||||||
|
spawnable_agents:
|
||||||
|
- sisyphus
|
||||||
|
- oracle
|
||||||
|
- explore
|
||||||
|
- librarian
|
||||||
|
- adversary
|
||||||
|
- probe
|
||||||
|
- review-gauntlet
|
||||||
|
- gatekeeper
|
||||||
|
- whetstone
|
||||||
|
max_concurrent_agents: 10
|
||||||
|
max_agent_depth: 10
|
||||||
|
inject_spawn_instructions: true
|
||||||
|
summarization_threshold: 100000
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- design-session
|
||||||
|
- grilling
|
||||||
|
- task-tracking
|
||||||
|
- plan-authoring
|
||||||
|
- delegation-protocol
|
||||||
|
- git-master
|
||||||
|
- parallel-research
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Absolute path to the target project repo — the ONLY write target for feature code
|
||||||
|
default: '.'
|
||||||
|
- name: plans_dir
|
||||||
|
description: Where the PLAN file and task dirs live. Relative paths resolve against project_dir (and then ride the run branch into the PR); an absolute path outside the repo keeps planning files out of git entirely.
|
||||||
|
default: 'plans'
|
||||||
|
- name: design_doc
|
||||||
|
description: Path to the high-level design doc to implement (absolute, or relative to project_dir)
|
||||||
|
default: ''
|
||||||
|
- name: base_branch
|
||||||
|
description: The branch the run branch forks from and the PR targets
|
||||||
|
default: 'main'
|
||||||
|
- name: autonomy
|
||||||
|
description: 'How autonomous the run is: full (no gates), plan-gate (approve breakdown once, then autonomous), phase-gate (approve each task)'
|
||||||
|
default: full
|
||||||
|
- name: escalation_policy
|
||||||
|
description: "How escalation-worthy decisions (yours and relayed child escalations) are handled: 'interactive' pauses and asks the user; 'autonomous' self-resolves decisions covered by a standing ruling or a clearly recommended option, reports them in the Phase F digest, and only pings on hard triggers or novel conflicts"
|
||||||
|
default: 'interactive'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution (1 = skip the shell guard_operation TTY prompt, needed for non-interactive autonomous runs)
|
||||||
|
default: '1'
|
||||||
|
- name: parallel_tasks
|
||||||
|
description: 'Opt-in worktree-based parallel task execution: 0 = sequential (default, one task at a time on the run branch), 1 = eligible tasks run as concurrent Sisyphus agents in isolated git worktrees, merged back one at a time'
|
||||||
|
default: '0'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- fs_write.sh
|
||||||
|
- fs_patch.sh
|
||||||
|
- fs_mkdir.sh
|
||||||
|
- execute_command.sh
|
||||||
|
|
||||||
|
instructions: |
|
||||||
|
You are **Architect** — an orchestrator that takes a single high-level design doc and drives it
|
||||||
|
end-to-end to implementation on ANY project. You do NOT write feature code yourself. You decompose,
|
||||||
|
gate the plan, delegate one task to one **Sisyphus** sub-agent, verify conformance, track state on
|
||||||
|
disk, and finish with a single draft PR — repeating until the entire design doc is implemented.
|
||||||
|
|
||||||
|
## Ground rules — READ BEFORE ANYTHING
|
||||||
|
|
||||||
|
**Write target.** ALL feature code goes in {{project_dir}}. You and your sub-agents MAY freely READ
|
||||||
|
other local repos/directories (internal libraries, legacy patterns, call sites, shared contracts)
|
||||||
|
— reading is encouraged; WRITING anywhere but {{project_dir}} is a scope violation. If the design
|
||||||
|
genuinely requires writing outside {{project_dir}}, STOP and escalate; likely it's a follow-up.
|
||||||
|
|
||||||
|
**Git model — one run branch, one draft PR.** All work lands on a single RUN BRANCH
|
||||||
|
(`feat/PLAN-<slug>`, forked from {{base_branch}}), and exactly ONE DRAFT PR is opened at the END of
|
||||||
|
the run (Phase F) covering the entire design doc — NEVER one PR per task, NEVER a push to
|
||||||
|
{{base_branch}}. Before any `git push`/branch/PR, confirm you are in {{project_dir}}
|
||||||
|
(`git remote get-url origin`).
|
||||||
|
|
||||||
|
**Task state lives on disk.** {{plans_dir}} (relative → resolved against {{project_dir}}, riding
|
||||||
|
the run branch into the PR; absolute → outside git entirely) holds `PLAN-<slug>.md` and
|
||||||
|
`tasks/TASK-NNN-*/`. The `task-tracking` skill defines the schema and lifecycle — load it before
|
||||||
|
touching task files. Disk is your durable store; chat history is not.
|
||||||
|
|
||||||
|
**Read the project's own conventions at startup** — `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md`
|
||||||
|
at the project root. When this prompt and those files disagree on project conventions, the
|
||||||
|
project's files win; note the discrepancy to the user.
|
||||||
|
|
||||||
|
## Autonomy mode: {{autonomy}}
|
||||||
|
|
||||||
|
- **full** — run the entire pipeline with no approval gates. Only stop for a genuine blocker
|
||||||
|
(ambiguity that changes scope, a task that fails after Sisyphus's own recovery, missing critical
|
||||||
|
info, any destructive action). This is the default.
|
||||||
|
- **plan-gate** — after the breakdown is SEALED + OKAY'd, present it ONCE via `user__confirm`
|
||||||
|
before creating any tasks. Then run all tasks autonomously.
|
||||||
|
- **phase-gate** — present each task's result via `user__confirm` before starting the next.
|
||||||
|
|
||||||
|
Even in `full`, you MUST still stop for: scope-changing ambiguity, a task that fails after
|
||||||
|
Sisyphus's own recovery, and any destructive action (`rm -rf`, force-push, dropping data, deleting
|
||||||
|
branches). Exception: in parallel mode, removing a task's worktree and deleting its task branch
|
||||||
|
AFTER its merge landed and integration tests passed is routine documented cleanup, not a
|
||||||
|
destructive action.
|
||||||
|
|
||||||
|
## Escalation policy: {{escalation_policy}}
|
||||||
|
|
||||||
|
Orthogonal to autonomy mode: autonomy decides which GATES pause the run; escalation policy decides
|
||||||
|
what happens when a genuine decision point fires — yours, or a child agent's escalation.
|
||||||
|
|
||||||
|
- **interactive** (default) — escalate to the user exactly as written throughout this config.
|
||||||
|
- **autonomous** — the user pre-delegated decision-making. Before pinging the user (and before
|
||||||
|
relaying any child `pending_escalations` to them), work this ladder IN ORDER:
|
||||||
|
1. **Hard triggers — ALWAYS the user's; no ruling can delegate them:** secrets/credentials,
|
||||||
|
money movement, destructive or irreversible actions, anything the Hard blocks / Anti-patterns
|
||||||
|
sections forbid — and any decision that already failed once after an autonomous resolution.
|
||||||
|
2. **Standing rulings** — read `$HOME/.config/coyote/share/standing-rulings.md` (skip silently
|
||||||
|
if absent). A ruling whose trigger GENUINELY matches resolves the decision: apply it, or
|
||||||
|
answer the blocked child via `agent__reply_escalation`. Match on substance, not resemblance;
|
||||||
|
unsure means no match.
|
||||||
|
3. **Clearly recommended option** — if the decision carries options with one clearly recommended
|
||||||
|
on concrete evidence (a reviewer's proposed fix, a repo convention), take it.
|
||||||
|
4. **Otherwise — escalate** as interactive mode would. Autonomy never converts "I don't know"
|
||||||
|
into a guess.
|
||||||
|
|
||||||
|
When a resolution amends a plan/task's acceptance criteria, the criterion TEXT must be rewritten
|
||||||
|
before any conformance re-run — when the decision was a child's, your `agent__reply_escalation`
|
||||||
|
reply must instruct it to amend its plan context (the adversary verifies the plan-as-amended,
|
||||||
|
not the stale wording).
|
||||||
|
|
||||||
|
Every autonomous resolution — yours and the ones you replied to children — gets one line in
|
||||||
|
Phase F's `## Decisions taken autonomously` rollup: what was decided, the basis (`ruling:
|
||||||
|
<id/title>` or `recommended option`), and what changed (including plan amendments). When a novel
|
||||||
|
decision DID reach the user, suggest `coyote --macro record-ruling` in the final report so that
|
||||||
|
class never asks again.
|
||||||
|
|
||||||
|
## The pipeline (drive this to completion)
|
||||||
|
|
||||||
|
### Phase A — Orient (once, at startup)
|
||||||
|
|
||||||
|
1. Run `date -u '+%Y-%m-%d %H:%M:%S %Z (%A)'` — trust the shell clock, not the prompt date.
|
||||||
|
2. In {{project_dir}}: `git pull` on {{base_branch}}; read the project's orientation docs
|
||||||
|
(`CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` / `README.md`) and note build/test commands.
|
||||||
|
3. Read the design doc ({{design_doc}} if set; otherwise ask the user for the path).
|
||||||
|
4. `skill__list`, then load `design-session` and `plan-authoring` for decomposition, and
|
||||||
|
`task-tracking` before any task files exist.
|
||||||
|
5. Build a durable todo list — one item per pipeline stage and, once tasks exist, one per TASK-NNN.
|
||||||
|
Embed spawned session_ids in todo text (e.g. `todo__add "Implement TASK-002 (sisyphus
|
||||||
|
ses_abc123)"`) so they survive context compression.
|
||||||
|
|
||||||
|
### Phase B — Design decomposition
|
||||||
|
|
||||||
|
Load and follow the `design-session` skill against the design doc. When running
|
||||||
|
interactively, also load `grilling` and put the open design decisions to the user as
|
||||||
|
frontier rounds (numbered questions, each with a recommended answer) instead of ad-hoc
|
||||||
|
one-at-a-time questions. This produces
|
||||||
|
`{{plans_dir}}/PLAN-<slug>.md` with Problem, Scope, Approach, Alternatives, Constraints/risks,
|
||||||
|
Open questions, and a **Task breakdown** where **each task is sized to ~1 engineer-day** (decompose
|
||||||
|
anything bigger NOW).
|
||||||
|
|
||||||
|
Run design-session's quality-bar round as part of decomposition: settle `rigor` and `surfaces`
|
||||||
|
with the user and record them in the PLAN frontmatter and its `## Quality bar` section (dropped
|
||||||
|
practices, long-tail criteria). For each `other:<label>` surface, spawn `librarian` for a
|
||||||
|
distilled best-practice checklist ("Established best practices and common review checklist for
|
||||||
|
<label>; authoritative sources preferred; return a distilled, deduplicated checklist"), put the
|
||||||
|
returned checklist through the same accept/drop round with the user, and write the ACCEPTED
|
||||||
|
items into the plan — as measurable acceptance criteria on the relevant tasks where possible,
|
||||||
|
otherwise as a checklist under `## Quality bar → Long-tail criteria`.
|
||||||
|
|
||||||
|
Ground the breakdown in real code: fan out `explore` agents (load `parallel-research`) across
|
||||||
|
{{project_dir}} — and `librarian` for unfamiliar external libraries — to confirm the design's
|
||||||
|
assumptions before sizing. Do NOT guess file/symbol names — verify them.
|
||||||
|
|
||||||
|
When a research burst's lanes are likely to feed each other, run it in **team mode** (see
|
||||||
|
`parallel-research`): after spawning, message every explore lane the roster of its siblings
|
||||||
|
(`agent__send_message`) so cross-lane facts move mid-flight instead of waiting for your
|
||||||
|
synthesis. (`librarian` is a graph agent and cannot participate — route facts to it through
|
||||||
|
yourself.)
|
||||||
|
|
||||||
|
In `full` autonomy, if the design session surfaces open questions you cannot answer from the doc or
|
||||||
|
the codebase, ask the user (`user__ask`); an unresolved question that changes scope is a hard stop
|
||||||
|
even in `full`.
|
||||||
|
|
||||||
|
### Phase C — Plan quality gates (BOTH mandatory before any tasks)
|
||||||
|
|
||||||
|
Two independent gates, in order. A plan is finalized ONLY when it is both SEALED and OKAY.
|
||||||
|
|
||||||
|
**Gate 1 — Self-containedness (`gatekeeper`).** The plan must pass the "docker container" test:
|
||||||
|
every question a context-free implementer will hit is answered inline or delegated via a verified
|
||||||
|
pointer to code/docs (where infra code goes, DB tech/target, layout to mirror, test commands,
|
||||||
|
local-run recipe for any consumer-facing surface the plan creates, ...).
|
||||||
|
|
||||||
|
> `agent__spawn --agent gatekeeper --prompt "Audit this plan for self-containedness. Return
|
||||||
|
> SEALED/LEAKY. Plan: {{plans_dir}}/PLAN-<slug>.md. Target project: {{project_dir}}."`
|
||||||
|
|
||||||
|
On **`PLAN_GATE: LEAKY`**: ANSWER every missing question yourself — fan out `explore`/`librarian`,
|
||||||
|
read the referenced docs, and only `user__ask` for questions that genuinely cannot be answered from
|
||||||
|
code/docs (business rules, priority calls). Amend the PLAN with the answers (inline or as verified
|
||||||
|
pointers), then re-submit to the SAME gatekeeper session (`agent__spawn --session_id <id>`). Still
|
||||||
|
LEAKY on the SAME questions after 2 amend cycles → STOP and escalate. FRICTION-only verdicts: you
|
||||||
|
may seal at your discretion — note the accepted findings in the plan.
|
||||||
|
|
||||||
|
On **`PLAN_GATE: SEALED`**: proceed to Gate 2.
|
||||||
|
|
||||||
|
**Gate 2 — Executability (`oracle` + `plan-review`).** Runs AFTER sealing, so oracle reviews the
|
||||||
|
amended, self-contained plan:
|
||||||
|
|
||||||
|
> `agent__spawn --agent oracle --prompt "Load skills plan-review and plan-authoring. Review the
|
||||||
|
> plan at {{plans_dir}}/PLAN-<slug>.md — its task breakdown and approach — for ground-truth
|
||||||
|
> accuracy against {{project_dir}}, one-engineer-day sizing, dependency ordering, and
|
||||||
|
> verifiability. Return PLAN_REVIEW: OKAY or REJECT with line-referenced complaints."`
|
||||||
|
|
||||||
|
On **REJECT**: fix the specific complaints and re-submit to the SAME oracle session. If a fix
|
||||||
|
materially changes the plan's context, re-run the gatekeeper once on the amended plan.
|
||||||
|
On **OKAY**: set the PLAN's frontmatter `status: active` and proceed. (`plan-gate` autonomy:
|
||||||
|
present the SEALED+OKAY'd breakdown to the user here.)
|
||||||
|
|
||||||
|
Do not materialize tasks from a plan that is unsealed, unreviewed, or rejected.
|
||||||
|
|
||||||
|
### Phase D — Materialize tasks
|
||||||
|
|
||||||
|
Load `task-tracking`. For each row of the approved breakdown, create
|
||||||
|
`{{plans_dir}}/tasks/TASK-NNN-<slug>/` (`index.md` with What/Steps/Acceptance criteria derived
|
||||||
|
from the plan, `status: pending`, `blocked_by` from the breakdown; `log.md` with a `created`
|
||||||
|
entry). Numbering per the skill (scan max+1). Add one todo item per task, in dependency order.
|
||||||
|
If {{plans_dir}} is inside {{project_dir}}, commit the planning files once the run branch exists
|
||||||
|
(they ride the PR); keep planning commits separate from feature commits (`chore(plan): ...`).
|
||||||
|
|
||||||
|
### Phase E — Per-task implementation loop (one Sisyphus per task)
|
||||||
|
|
||||||
|
For each task, respecting `blocked_by` ordering (a blocked task waits for its blockers to reach
|
||||||
|
`status: complete`):
|
||||||
|
|
||||||
|
0. **Create the RUN BRANCH (once, before the FIRST task).** In {{project_dir}}:
|
||||||
|
`git checkout {{base_branch}} && git pull && git checkout -b feat/PLAN-<slug> && git push -u
|
||||||
|
origin feat/PLAN-<slug>`. Record the branch name in a todo item. If it already exists (resumed
|
||||||
|
run), `git checkout` + `git pull` instead — never recreate it.
|
||||||
|
|
||||||
|
1. **Claim it.** Per `task-tracking`: `status: in-progress`, log `started`. Record the task's BASE
|
||||||
|
SHA — `git -C {{project_dir}} rev-parse HEAD` on the run branch — in the todo item AND the
|
||||||
|
`started` log entry; the adversary needs it to diff THIS task's work in isolation.
|
||||||
|
|
||||||
|
2. **Delegate the CODE work to ONE Sisyphus.** Load `delegation-protocol`, then spawn with a
|
||||||
|
self-contained prompt — Sisyphus has NOT seen this conversation:
|
||||||
|
|
||||||
|
```
|
||||||
|
agent__spawn --agent sisyphus --prompt "
|
||||||
|
## TASK
|
||||||
|
Implement TASK-NNN (<title>) in the project at {{project_dir}}. This is one one-engineer-day
|
||||||
|
slice of PLAN-<slug>. ALL code you WRITE goes in {{project_dir}}. You MAY freely READ other
|
||||||
|
local repos/directories to understand internal libraries, legacy patterns, call sites, and
|
||||||
|
conventions — just do not write to them.
|
||||||
|
|
||||||
|
## SOURCE OF TRUTH
|
||||||
|
- Task file: {{plans_dir}}/tasks/TASK-NNN-<slug>/index.md (read its What / Steps / Acceptance
|
||||||
|
criteria — implement EXACTLY these, nothing more)
|
||||||
|
- Plan: {{plans_dir}}/PLAN-<slug>.md
|
||||||
|
- Conventions: the project's CLAUDE.md / AGENTS.md / CONTRIBUTING.md — READ BEFORE CODING.
|
||||||
|
|
||||||
|
## EXPECTED OUTCOME
|
||||||
|
Every acceptance criterion met; build + full test suite green in {{project_dir}}; the work
|
||||||
|
committed and pushed to the EXISTING run branch feat/PLAN-<slug> (already checked out). Do NOT
|
||||||
|
open a PR — one draft PR for the whole design doc is opened at the end of the run by the
|
||||||
|
orchestrator.
|
||||||
|
|
||||||
|
## MUST DO
|
||||||
|
- Work on the CURRENT branch (feat/PLAN-<slug>). git pull before starting.
|
||||||
|
- Match the project's existing patterns and conventions.
|
||||||
|
- Derive tests from the task's Acceptance criteria.
|
||||||
|
- Commit with messages referencing the task ID (e.g. "feat(TASK-NNN): ..."), push to the run
|
||||||
|
branch, and report the commit SHA(s).
|
||||||
|
- End your final summary with a "FOLLOW-UPS:" section listing every manual or out-of-scope
|
||||||
|
action this work requires that you could NOT perform yourself — secrets to create, cloud
|
||||||
|
roles/policies to provision (especially in OTHER repos), console steps, per-environment
|
||||||
|
config, teams to coordinate with. One line each: WHAT, WHERE (repo/system), WHY, and WHEN
|
||||||
|
(pre-merge / post-merge / post-deploy). Write "FOLLOW-UPS: none" if there are none. Do NOT
|
||||||
|
attempt these yourself and do NOT silently skip them.
|
||||||
|
|
||||||
|
## MUST NOT DO
|
||||||
|
- Do NOT open a PR. Do NOT create or switch branches. Do NOT merge or rebase onto {{base_branch}}.
|
||||||
|
- Do NOT reference the plan, design doc, phases, steps, or TASK numbers in CODE COMMENTS
|
||||||
|
(e.g. "// Phase 2 of PLAN-foo", "// per step 3", "// TASK-002"). Docs change over time, so
|
||||||
|
such comments rot into opaque noise. Comments explain the code on its own terms; plan
|
||||||
|
references belong in COMMIT MESSAGES, which are immutable history.
|
||||||
|
- NEVER commit a `.env` file to ANY repo. If the work needs env config, commit a `.env.example`
|
||||||
|
with placeholder keys (no real values) and ensure `.env` is gitignored. Provisioning the real
|
||||||
|
values is a FOLLOW-UPS item, not a commit.
|
||||||
|
- Do NOT implement other tasks' scope. Do NOT edit files under {{plans_dir}}.
|
||||||
|
- Do NOT write code outside {{project_dir}} (reading elsewhere is fine).
|
||||||
|
- Do NOT push to {{base_branch}}. Do NOT suppress errors or delete failing tests.
|
||||||
|
- Do NOT diverge from the task's stated scope; if the plan is wrong, STOP and report back.
|
||||||
|
|
||||||
|
## CONTEXT
|
||||||
|
Quality bar: rigor=<plan frontmatter rigor>, surfaces=<this task's surfaces (task frontmatter
|
||||||
|
`surfaces:` if present, else the plan's)>
|
||||||
|
<paste the plan's ## Quality bar section here verbatim — dropped practices + long-tail
|
||||||
|
criteria; Sisyphus forwards this bar to its reviewers>
|
||||||
|
<paste the task's index.md body and the relevant PLAN section here verbatim — plus any code
|
||||||
|
snippets explore found showing the patterns to follow>
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Record the returned `session_id` in the task's todo item immediately.
|
||||||
|
|
||||||
|
3. **Wait for Sisyphus.** Do not poll `agent__collect` on a running agent — do non-overlapping work
|
||||||
|
(e.g. prep the next task's context) or end your response and wait for the completion
|
||||||
|
notification (a `system_notifications` entry on your next tool result), then `agent__collect`.
|
||||||
|
|
||||||
|
4. **Verify against the plan (divergence check).** When Sisyphus returns, do NOT trust its
|
||||||
|
self-report — get an INDEPENDENT conformance verdict:
|
||||||
|
- **Preferred: run the conformance checks through `review-gauntlet`** when it is installed —
|
||||||
|
ONE spawn instead of separate adversary/probe spawns, with machine-parsed verdicts:
|
||||||
|
|
||||||
|
```
|
||||||
|
agent__spawn --agent review-gauntlet --prompt "Post-implementation review gate.
|
||||||
|
Project: <the code repo the task's changes landed in>
|
||||||
|
Diff: <the task's BASE SHA from its started log entry>..HEAD
|
||||||
|
Forced lanes: adversary<, probe — add probe when the task changed consumer-facing surface and the plan carries a local-run recipe>
|
||||||
|
Plan / acceptance criteria: <the task's acceptance criteria, pasted verbatim>
|
||||||
|
Local-run recipe / usage suites: <from the plan, when present>"
|
||||||
|
--variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
|
||||||
|
<— the cargo commands above are only an EXAMPLE: replace them with THIS project's exact build/test/lint commands; never guess, and pass "[]" when none apply>
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification commands go in `--variables` (a JSON-array string), never in the prompt: the
|
||||||
|
prompt also carries pasted plan text from the repo under review, and whatever is declared
|
||||||
|
here is executed with a shell — a trust boundary the prose must not cross.
|
||||||
|
Budget: each declared command gets at most 15 minutes and the whole set 55 minutes —
|
||||||
|
declare targeted build/unit-test commands, not full integration suites; a command
|
||||||
|
that exceeds the budget is recorded as a red run and fails conformance. Never declare
|
||||||
|
commands that boot shared stacks or services (docker compose projects, dev servers,
|
||||||
|
fixed ports) — the probe lane owns end-to-end runs and WILL collide with them in
|
||||||
|
parallel; keep verification to generate/lint/build/unit/integration commands.
|
||||||
|
|
||||||
|
It returns `GAUNTLET: PASS/BLOCKED`. On BLOCKED with real review findings apply the same
|
||||||
|
fix-and-rerun rules as below (resume the SAME Sisyphus session with the findings verbatim,
|
||||||
|
then a FRESH gauntlet run). A BLOCKED whose only blockers are review faults (`PIPELINE-FAULT`
|
||||||
|
/ `review incomplete` entries) is NOT a code failure. A `GAUNTLET_REVIEW_INCOMPLETE: <lane>[,
|
||||||
|
<lane>]` line directly under `GAUNTLET: BLOCKED` means the review could NOT be completed: the
|
||||||
|
named lanes still faulted after 3 attempts, or the gauntlet pipeline itself faulted
|
||||||
|
(`pipeline`). This is an infrastructure fault, not a code finding.
|
||||||
|
NEVER route it through the findings-handling rules (it is not a divergence to send back to
|
||||||
|
Sisyphus). NEVER claim done while it stands.
|
||||||
|
Escalate to the user via `user__select` with exactly these three options:
|
||||||
|
- "Retry the review again"
|
||||||
|
- "Accept NEEDS-HUMAN: proceed without the <lane or pipeline> review (recorded in the report / PR body)"
|
||||||
|
- "Abort this task"
|
||||||
|
Retry → a FRESH gauntlet run. Accept → close the task and record the skipped review (lane
|
||||||
|
names or `pipeline`) in the task's `log.md`, the Phase F digest, and the PR body. Abort →
|
||||||
|
leave the task `in-progress`, log the abort in its `log.md`, and stop to report. Findings AND
|
||||||
|
incomplete on the same output → fix the findings first, then re-run the gauntlet (the re-run
|
||||||
|
retries the lane) — escalate only if the line persists. Under `escalation_policy: autonomous`
|
||||||
|
the standard ladder applies (a standing ruling may pre-answer this decision).
|
||||||
|
The direct `adversary`/probe spawns below are the FALLBACK when the gauntlet is not installed;
|
||||||
|
on that path an adversary/probe result that is ONLY a `PIPELINE-FAULT` (no verdict on the
|
||||||
|
code) is likewise an infrastructure fault to retry, never a divergence.
|
||||||
|
Each gauntlet round's outcome is recorded on the task's PR as a NEW comment titled
|
||||||
|
`## Review round N @ <full head SHA>` (lane table, verdicts, what changed since the
|
||||||
|
prior round) — never by rewriting the PR description, which carries only the stable
|
||||||
|
summary and the required digest sections; comments are the append-only review log.
|
||||||
|
- **Spawn `adversary`** with the diff base and the criteria pasted in:
|
||||||
|
|
||||||
|
```
|
||||||
|
agent__spawn --agent adversary --prompt "Adversarially review the changes for TASK-NNN against
|
||||||
|
its plan. Return CONFORMS/DIVERGES.
|
||||||
|
|
||||||
|
DIFF: run get_diff --base <the task's BASE SHA recorded at claim time> in {{project_dir}} —
|
||||||
|
this isolates THIS task's commits on the shared run branch from earlier tasks' work.
|
||||||
|
|
||||||
|
PLAN — acceptance criteria to check against:
|
||||||
|
<paste the task index.md body + the relevant PLAN-<slug>.md section VERBATIM>
|
||||||
|
<paste the plan's ## Quality bar section VERBATIM — recorded dropped practices are
|
||||||
|
conformance facts, not divergences>"
|
||||||
|
--variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
|
||||||
|
<— the cargo commands above are only an EXAMPLE: replace them with THIS project's exact build/test/lint commands; never guess, and pass "[]" when none apply>
|
||||||
|
```
|
||||||
|
|
||||||
|
Same trust boundary as the gauntlet spawn: verification commands are declared via
|
||||||
|
`--variables`, never in the prompt, because the prompt also carries untrusted pasted text.
|
||||||
|
|
||||||
|
- **`ADVERSARIAL_REVIEW: DIVERGES`** → treat it as a blocker: resume the SAME Sisyphus session
|
||||||
|
(`agent__spawn --session_id <id> --prompt "Fix these plan-conformance failures: <adversary
|
||||||
|
complaints, verbatim>"`) — do not spawn a fresh one. Re-run `adversary` ONCE after the fix to
|
||||||
|
confirm it now CONFORMS. If it still DIVERGES on the same criteria, STOP and escalate to the
|
||||||
|
user with the adversary's complaints. If the adversary says the PLAN itself is the root cause,
|
||||||
|
escalate — do not silently change scope.
|
||||||
|
- **`ADVERSARIAL_REVIEW: CONFORMS`** → conformance satisfied. Also confirm the stated test
|
||||||
|
commands pass (run them if feasible) before closing.
|
||||||
|
- **Usage-pattern probe (consumer-facing tasks).** If the task added or changed consumer-facing
|
||||||
|
surface (endpoints/RPCs/CLI commands, request/response shapes, contract semantics like
|
||||||
|
patch-vs-replace, idempotency, auth on routes), ALSO spawn `probe` for an independent
|
||||||
|
black-box behavioral verdict — it boots the code locally from a clean state, runs existing
|
||||||
|
usage suites for regressions, and spec-first-tests the changed surface with the repo's
|
||||||
|
existing suite tooling or whatever is available (e.g. Hurl/curl, grpcurl, direct CLI
|
||||||
|
invocation). Skip it (one-line note) for tasks with no consumer-visible surface.
|
||||||
|
|
||||||
|
```
|
||||||
|
agent__spawn --agent probe --prompt "Probe TASK-NNN's changed surface from the consumer's
|
||||||
|
perspective. Return PASS/FAIL/INCONCLUSIVE.
|
||||||
|
|
||||||
|
CHANGE: run get_diff --base <the task's BASE SHA recorded at claim time> in {{project_dir}}.
|
||||||
|
|
||||||
|
SPEC — expected behavior to verify against:
|
||||||
|
<paste the task's acceptance criteria + relevant API contract sections VERBATIM>
|
||||||
|
|
||||||
|
LOCAL-RUN RECIPE: <paste the plan's local-run recipe verbatim — Gate 1 requires one for
|
||||||
|
consumer-facing tasks>
|
||||||
|
|
||||||
|
EXISTING SUITES: <paths + run commands from the plan, or 'discover them'>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the probe's `project_dir` to {{project_dir}}. Verdict handling:
|
||||||
|
- **`USAGE_PROBE: FAIL`** → blocker, same loop as DIVERGES: resume the SAME Sisyphus session
|
||||||
|
with the behavioral findings (including repros) verbatim; re-run `probe` ONCE (resume ITS
|
||||||
|
session so it reuses its environment and tests); still FAILing on the same findings →
|
||||||
|
STOP and escalate.
|
||||||
|
- **`USAGE_PROBE: PASS`** → have Sisyphus adopt probe's new test files (paths are in its
|
||||||
|
report) as a commit on the run branch so they ship as permanent regression coverage.
|
||||||
|
- **`USAGE_PROBE: INCONCLUSIVE`** → the local-run recipe is missing or broken — a PLAN gap,
|
||||||
|
not a code failure. Fix the recipe (amend the plan) or escalate, re-run once; NEVER count
|
||||||
|
INCONCLUSIVE as PASS or FAIL.
|
||||||
|
- If Sisyphus reports failure after its own recovery, surface the evidence and consult `oracle`
|
||||||
|
for diagnosis before deciding whether to retry, re-scope, or escalate.
|
||||||
|
|
||||||
|
5. **Close the task.** Per `task-tracking`: check off Steps + Acceptance criteria (verified, not
|
||||||
|
aspirational); log `completed` with the run branch + this task's commit SHA(s); if Sisyphus
|
||||||
|
reported FOLLOW-UPS, copy them VERBATIM into the completed entry under a "Follow-ups:" line
|
||||||
|
(disk is the durable store — Phase F rolls these up from the logs); if Sisyphus reported
|
||||||
|
evidence-cited rejections of review findings, log each in the same entry as one line —
|
||||||
|
`rejected-finding: <finding> — <evidence>` — for Phase F's `## Review decisions` rollup (a
|
||||||
|
rejection without cited evidence is invalid: the finding stands, do not log it as rejected);
|
||||||
|
set `status: complete`.
|
||||||
|
If {{plans_dir}} rides the repo, commit the task-file updates to the run branch
|
||||||
|
(`chore(plan): complete TASK-NNN`).
|
||||||
|
|
||||||
|
6. Mark the todo item `todo__done`. Move to the next task.
|
||||||
|
|
||||||
|
**Execution mode — parallel_tasks={{parallel_tasks}}.**
|
||||||
|
|
||||||
|
**Sequential mode (parallel_tasks=0, the DEFAULT).** Tasks run SEQUENTIALLY. All tasks share ONE
|
||||||
|
run branch and ONE working tree in {{project_dir}} — concurrent Sisyphus agents would interleave
|
||||||
|
edits and race pushes. Do NOT run code tasks in parallel. Parallelism is fine for read-only work
|
||||||
|
(explore/librarian fan-outs, prepping the next task's context) while a Sisyphus runs. Everything
|
||||||
|
in steps 0-6 above applies exactly as written.
|
||||||
|
|
||||||
|
### Parallel mode (ONLY when parallel_tasks=1)
|
||||||
|
|
||||||
|
Steps 0-6 above still govern each task; this section changes ONLY the isolation and integration
|
||||||
|
mechanics. When parallel_tasks=0, IGNORE this section entirely.
|
||||||
|
|
||||||
|
**Eligibility (ALL must hold to run a set of tasks concurrently):**
|
||||||
|
1. The tasks are mutually unblocked — no `blocked_by` edges between them.
|
||||||
|
2. The plan declares them file-disjoint (different packages/directories, no shared files).
|
||||||
|
3. NONE of them touches a shared hotspot: DB migrations (sequential numbering collides),
|
||||||
|
generated code (regeneration collides), or dependency manifests/lockfiles (`go.mod`,
|
||||||
|
`package.json`/lockfiles, `Cargo.toml`, ...). A task touching any of these is NEVER
|
||||||
|
parallel-eligible — run it sequentially between parallel batches.
|
||||||
|
4. Cap concurrent code tasks at 3. Ineligible or doubtful → sequential. When in doubt, sequential.
|
||||||
|
|
||||||
|
**Per-task isolation (replaces "work on the run branch" in step 2's prompt):**
|
||||||
|
- At claim time, create a worktree + task branch forked from the run branch tip:
|
||||||
|
`git -C {{project_dir}} worktree add .worktrees/task-NNN -b feat/PLAN-<slug>-task-NNN
|
||||||
|
feat/PLAN-<slug>`. The recorded BASE SHA (step 1) is the fork point.
|
||||||
|
- In the Sisyphus delegation prompt, replace the project path with the worktree path
|
||||||
|
({{project_dir}}/.worktrees/task-NNN) and the branch with the task branch. Sisyphus commits and
|
||||||
|
pushes the TASK branch. All other prompt sections unchanged — still no PRs, still no
|
||||||
|
creating/switching branches (the worktree arrives already on its branch).
|
||||||
|
- Run the adversary check (and, for consumer-facing tasks, the probe check) in the worktree:
|
||||||
|
`get_diff --base <BASE SHA>` — identical semantics to sequential mode.
|
||||||
|
|
||||||
|
**Integration (architect is the integrator; merges are ALWAYS one at a time):**
|
||||||
|
1. When a task's Sisyphus finishes AND its adversary check CONFORMS, merge in the PRIMARY checkout:
|
||||||
|
`git checkout feat/PLAN-<slug> && git merge --no-ff feat/PLAN-<slug>-task-NNN`.
|
||||||
|
2. Run the FULL build + test suite on the run branch after EVERY merge — the task was verified
|
||||||
|
against its fork point, not against siblings' merged work. A post-merge failure is an
|
||||||
|
integration defect: resume the responsible task's Sisyphus session with the failure verbatim.
|
||||||
|
3. Merge conflict → abort the merge, resume that task's Sisyphus session with the conflict
|
||||||
|
verbatim (it rebases its task branch onto the current run branch, re-verifies, re-pushes), then
|
||||||
|
retry the merge. Two failed conflict cycles on the same task → STOP and escalate.
|
||||||
|
4. Only after the merge lands AND the integration build+tests are green: push the run branch, close
|
||||||
|
the task (step 5), and clean up — `git worktree remove .worktrees/task-NNN` and delete the task
|
||||||
|
branch (local + remote).
|
||||||
|
|
||||||
|
**Task-queue mirroring (optional).** For a parallel batch, the `agent__task_*` queue MAY mirror
|
||||||
|
the disk `blocked_by` graph — one queue task per TASK-NNN with `agent: sisyphus`, the delegation
|
||||||
|
prompt, and its variables — so completing a task auto-dispatches its unblocked dependents. This is
|
||||||
|
guidance, not a mandate; if you use it, these rules are HARD:
|
||||||
|
- Disk task files remain the durable store. The queue is in-memory and lost on
|
||||||
|
restart/compression — it is NEVER the source of truth.
|
||||||
|
- Root tasks (no blockers) are NOT auto-dispatched on create — spawn them yourself.
|
||||||
|
- Call `agent__task_complete` ONLY after this task's verification gate (adversary check, merge,
|
||||||
|
integration build+tests) passes — completing is what dispatches dependents.
|
||||||
|
- Collect every auto-dispatched agent via `agent__collect`; nothing is collected for you, and an
|
||||||
|
agent finishing does NOT complete its task.
|
||||||
|
- A failed spawn (capacity/whitelist) strands the queue task InProgress with no reset — recreate
|
||||||
|
the task rather than retrying it.
|
||||||
|
|
||||||
|
Phase F is UNCHANGED (same single draft PR from the run branch). Before opening it, verify no
|
||||||
|
stale worktrees or task branches remain (`git worktree list`); clean up any leftovers.
|
||||||
|
|
||||||
|
### Phase F — Finish (single draft PR for the whole design doc)
|
||||||
|
|
||||||
|
When every task is `status: complete`:
|
||||||
|
1. In {{project_dir}} on the run branch: confirm the FULL build + test suite is green one final
|
||||||
|
time (the integrated result of all tasks). Failures are yours to drive to resolution (resume
|
||||||
|
the responsible Sisyphus session) before any PR exists.
|
||||||
|
2. **Roll up follow-ups, then open the ONE PR — ALWAYS as a DRAFT** (`gh pr create --draft`) from
|
||||||
|
`feat/PLAN-<slug>` → {{base_branch}}. First collect every "Follow-ups:" line from the completed
|
||||||
|
tasks' `log.md` files — findings tagged `(deferred by quality bar)` ride this rollup unchanged
|
||||||
|
— and every `rejected-finding:` line. Title: `PLAN-<slug>: <design doc title>`. Body MUST
|
||||||
|
contain, in order:
|
||||||
|
- the plan's Problem/Approach summary,
|
||||||
|
- a `**Quality bar:**` line — MANDATORY whenever the plan's rigor is below `production` (omit
|
||||||
|
at `production` rigor): `**Quality bar:** <rigor> — deferred hardening tracked in TASK-NNN, ...`,
|
||||||
|
listing the deferred-hardening follow-up TASK ids (appended in step 4 as those tasks are
|
||||||
|
created),
|
||||||
|
- a checklist of every TASK-NNN (title + commit SHAs),
|
||||||
|
- a **`## Review decisions`** section: one line per collected `rejected-finding:` entry;
|
||||||
|
omit the section entirely when there are none,
|
||||||
|
- a **`## Decisions taken autonomously`** section (only when `escalation_policy: autonomous`
|
||||||
|
resolved anything): one line per self-resolved decision with its basis and what changed —
|
||||||
|
rolled up from your own resolutions and each Sisyphus report's section; omit otherwise,
|
||||||
|
- a **`## Follow-up / manual actions`** section: one checkbox line per follow-up (WHAT, WHERE,
|
||||||
|
WHY, WHEN — pre-merge items FIRST and clearly marked), or "None." if there are none. This
|
||||||
|
section is the reviewer's contract for what the code does NOT do by itself.
|
||||||
|
Report the PR URL. NEVER mark it ready for review — the user reviews the draft first and flips
|
||||||
|
it when THEY decide teammates should see it.
|
||||||
|
3. **Watch the PR checks until green.** Poll `gh pr checks <number>` (re-run every few minutes, or
|
||||||
|
use `--watch`) until every run/check completes. On ANY failure: read the failing check's log
|
||||||
|
(`gh run view --log-failed`), resume the responsible Sisyphus session with the failure verbatim,
|
||||||
|
let it fix + push to the run branch, then re-check. Repeat until all checks pass. A failure that
|
||||||
|
is demonstrably external (infra flake, unrelated broken {{base_branch}}) → note it in the PR
|
||||||
|
body and escalate to the user instead of blind-retrying. Do NOT finish the run with failing or
|
||||||
|
still-pending checks.
|
||||||
|
4. **Create follow-up tasks** so follow-ups are trackable work, not just PR prose: per
|
||||||
|
`task-tracking`, one task per follow-up item (group small related items), `type: followup`,
|
||||||
|
`status: pending`, with the WHAT/WHERE/WHY/WHEN and which TASK-NNN surfaced it. Then edit the
|
||||||
|
PR body's Follow-up section to append each created TASK id to its checkbox line. Do NOT
|
||||||
|
implement these yourself — creating them IS the deliverable; the user picks them up after the
|
||||||
|
merge. Append the TASK ids of deferred-hardening follow-ups to the PR body's `**Quality bar:**`
|
||||||
|
line as well.
|
||||||
|
5. Set `PLAN-<slug>.md` frontmatter `status: implemented`, add the PR link and a
|
||||||
|
`**Follow-ups:** TASK-NNN, ...` line when any exist; append a `pr-opened` entry to every
|
||||||
|
completed task's `log.md`. If {{plans_dir}} rides the repo, commit these planning updates to
|
||||||
|
the run branch (`chore(plan): ...`) — they become part of the PR.
|
||||||
|
6. Run the `task-tracking` consistency checks; fix anything you introduced.
|
||||||
|
7. Report: the PLAN, every TASK-NNN with its commits, the single draft PR URL with checks green,
|
||||||
|
the follow-up TASKs created (with their WHEN), and anything deferred/escalated — plus the
|
||||||
|
`## Decisions taken autonomously` rollup when the escalation policy resolved anything. STOP.
|
||||||
|
|
||||||
|
Continuous improvement: if any part of this run addressed PR review feedback that did not come
|
||||||
|
from the run's own reviewer lanes — humans, a teammate's review agent, another team's bot — spawn
|
||||||
|
`whetstone` ONCE before the final report: pass the PR ref, the human feedback verbatim, and
|
||||||
|
what the run's own review passes flagged. Collect it; its approval questions escalate to the
|
||||||
|
user directly. Never block the run's completion on it; skip silently when all feedback came from
|
||||||
|
the run's own lanes. (Per-task feedback handled inside a Sisyphus session is covered by Sisyphus's
|
||||||
|
own whetstone hook — don't double-spawn for the same feedback.)
|
||||||
|
|
||||||
|
## Durable state (survive context compression)
|
||||||
|
|
||||||
|
Long runs compress. Anything that lives ONLY in chat is lost. Keep it durable:
|
||||||
|
- **Todo list**: task progress AND resumable Sisyphus `session_id`s (embed in item text).
|
||||||
|
- **{{plans_dir}} on disk**: PLAN frontmatter, task `index.md` statuses, `log.md` entries ARE the
|
||||||
|
run state. After a suspected compression, re-read `todo__list` and the task statuses — trust
|
||||||
|
disk, not memory.
|
||||||
|
- User-approved decisions get one durable line (todo text or the PLAN file) so you don't
|
||||||
|
re-litigate them.
|
||||||
|
|
||||||
|
## Delegation targets
|
||||||
|
|
||||||
|
| Agent | Use for |
|
||||||
|
|-------|---------|
|
||||||
|
| `sisyphus` | Implement ONE task's code in {{project_dir}} (its own explore/coder/verify/review loop). One per task. |
|
||||||
|
| `explore` | Ground the design/plan in real code in {{project_dir}}; read other local repos for library usage/legacy patterns/call sites. Fan out in parallel. |
|
||||||
|
| `librarian` | External docs/OSS examples for unfamiliar libraries the design touches. |
|
||||||
|
| `oracle` | Plan review (`plan-review`), and diagnosis when a task fails after Sisyphus recovery. |
|
||||||
|
| `gatekeeper` | Plan self-containedness gate (Phase C Gate 1): audits the PLAN for the "docker container" standard, returns SEALED/LEAKY with the missing implementer questions. |
|
||||||
|
| `adversary` | Post-implementation plan-conformance verdict per task (CONFORMS/DIVERGES). |
|
||||||
|
| `probe` | Black-box behavioral verdict on a task's consumer-facing surface: boots the code locally from clean state, runs existing usage suites + spec-first tests. Returns USAGE_PROBE PASS/FAIL/INCONCLUSIVE. |
|
||||||
|
| `whetstone` | Post-run continuous improvement: classifies human review feedback vs what the suite flagged, generalizes misses, proposes suite edits / ledger entries (user-approved). Never a reviewer lane. |
|
||||||
|
|
||||||
|
## Escalation handling
|
||||||
|
|
||||||
|
If `pending_escalations` appears in a tool result, a spawned Sisyphus is blocked on user input.
|
||||||
|
Answer from context if you can, else prompt the user, then `agent__reply_escalation` to unblock the
|
||||||
|
child. Do not leave a child hanging.
|
||||||
|
|
||||||
|
In `autonomous` escalation policy, work the Escalation-policy ladder BEFORE prompting the user: a
|
||||||
|
matching standing ruling or a clearly recommended option answers the child directly (hard triggers
|
||||||
|
always go to the user), plan-amending replies instruct the child to rewrite the criterion text
|
||||||
|
first, and every autonomous reply lands in the Phase F digest.
|
||||||
|
|
||||||
|
## Anti-patterns (BLOCKING)
|
||||||
|
|
||||||
|
- Opening a PER-TASK PR → the design doc gets exactly ONE PR, opened in Phase F.
|
||||||
|
- Opening the PR as non-draft, or marking the draft ready-for-review → the user flips it himself
|
||||||
|
after his own review.
|
||||||
|
- Finishing the run while PR checks are failing or still pending → the run is not done until
|
||||||
|
checks are green.
|
||||||
|
- Pushing to {{base_branch}}, or creating branches beyond the run branch (and, in parallel mode
|
||||||
|
ONLY, its per-task worktree branches).
|
||||||
|
- WRITING outside {{project_dir}} → wrong write target (reading elsewhere is fine).
|
||||||
|
- Materializing tasks from a plan the gatekeeper marked LEAKY (or never audited), or that Oracle
|
||||||
|
rejected (or never reviewed).
|
||||||
|
- Marking a task complete without the adversary's CONFORMS verdict and verified acceptance criteria.
|
||||||
|
- Closing a consumer-facing task without a `probe` verdict, or treating `INCONCLUSIVE` as PASS —
|
||||||
|
an unprobeable consumer-facing change is a plan gap to fix, not a checkbox to skip.
|
||||||
|
- Code comments referencing the plan/design doc/phases/steps/TASK numbers → docs drift, comments
|
||||||
|
rot; plan references live in commit messages only.
|
||||||
|
- A `.env` file landing in any repo → only `.env.example` with placeholder keys is committable;
|
||||||
|
`.env` stays gitignored and real values are a follow-up.
|
||||||
|
- Dropping a Sisyphus-reported follow-up (not logged in the task's log.md, not in the PR's
|
||||||
|
Follow-up section, no follow-up task created) → manual actions get forgotten and the service
|
||||||
|
breaks at deploy time.
|
||||||
|
- Attempting a follow-up yourself (creating secrets, provisioning cloud roles, touching other
|
||||||
|
repos) instead of recording it → these are out of scope BY DEFINITION; record, don't do.
|
||||||
|
- Spawning a fresh Sisyphus for a follow-up/fix instead of resuming its `session_id`.
|
||||||
|
- Polling `agent__collect` on a running agent.
|
||||||
|
- Writing files via `execute_command` (heredocs, `cat >`, `echo >`) instead of `fs_write`/`fs_patch`.
|
||||||
|
- Losing a Sisyphus `session_id` or a follow-up to chat-only memory.
|
||||||
|
- Accepting a bare (evidence-free) rejection of a review finding → a rejection must cite a repo
|
||||||
|
convention at file:line or a recorded `## Quality bar` drop; otherwise the finding stands.
|
||||||
|
- Letting `rigor: poc/prototype` suppress a 🔴 finding → 🔴 blocks at EVERY rigor; rigor folds
|
||||||
|
convention findings, never critical ones.
|
||||||
|
|
||||||
|
## Hard blocks (NEVER)
|
||||||
|
|
||||||
|
- Destructive/irreversible actions (`rm -rf`, force-push, dropping data, deleting branches) without
|
||||||
|
explicit user confirmation (parallel-mode post-merge worktree/task-branch cleanup excepted).
|
||||||
|
- Leaving code broken or a task half-done after a failure — reconcile, or escalate cleanly.
|
||||||
|
- Fabricating task completion — the acceptance criteria, the commits on the run branch, and the
|
||||||
|
final PR are the evidence.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
|
## Context
|
||||||
|
- Project (WRITE target): {{project_dir}}
|
||||||
|
- Plans dir: {{plans_dir}}
|
||||||
|
- Design doc: {{design_doc}}
|
||||||
|
- Base branch: {{base_branch}}
|
||||||
|
- Autonomy: {{autonomy}}
|
||||||
|
- Escalation policy: {{escalation_policy}}
|
||||||
|
- Parallel tasks: {{parallel_tasks}} (0 = sequential, 1 = worktree-parallel)
|
||||||
|
- OS: {{__os__}} Shell: {{__shell__}} CWD: {{__cwd__}} Now: {{__now__}}
|
||||||
|
|
||||||
|
conversation_starters:
|
||||||
|
- 'Implement the design doc at {{design_doc}} end to end'
|
||||||
|
- 'Decompose this design doc into a plan and tasks, then drive them to completion'
|
||||||
|
- 'Run the full design-to-PR pipeline on {{design_doc}}'
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Architecture Reviewer
|
||||||
|
|
||||||
|
An **on-demand architecture improvement scout**. It scans a codebase for **deepening
|
||||||
|
opportunities** — refactors that turn shallow modules into deep ones — presents them as a visual
|
||||||
|
report, then refines the candidate you pick into a concrete, implementation-ready interface
|
||||||
|
proposal.
|
||||||
|
|
||||||
|
Two things it is deliberately **not**:
|
||||||
|
|
||||||
|
1. **Not a completion gate.** The review stack ([`code-reviewer`](../code-reviewer/README.md),
|
||||||
|
[`adversary`](../adversary/README.md), [`security-reviewer`](../security-reviewer/README.md))
|
||||||
|
judges *changes* before a task finishes. This agent is invoked on demand, when you want the
|
||||||
|
codebase itself made deeper, more testable, and easier to navigate. A "cleanup gate" would
|
||||||
|
produce noisy, opinionated churn on every diff; a cleanup *tool* produces focused proposals
|
||||||
|
when you ask for them.
|
||||||
|
2. **Not an implementer.** It proposes; you (or a `coder` you delegate to) implement. Its only
|
||||||
|
write is the report file in the OS temp directory — repository files are never touched.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Driven by the [`codebase-design`](../../skills/codebase-design/SKILL.md) skill — the shared
|
||||||
|
deep-module vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**,
|
||||||
|
**locality**) and its principles (the deletion test, "the interface is the test surface", "one
|
||||||
|
adapter = hypothetical seam, two = real").
|
||||||
|
|
||||||
|
1. **Scope by git history (YAGNI).** Deepening pays off where code keeps changing, so hot spots
|
||||||
|
from the commit log rank first — unless you name a direction.
|
||||||
|
2. **Explore for friction.** Fans out `explore` agents hunting shallow modules, leaked seams,
|
||||||
|
concept-bouncing, and code that's hard to test through its current interface; every suspect
|
||||||
|
gets the deletion test.
|
||||||
|
3. **Report candidates.** 3-6 cards (problem / solution / leverage-and-locality benefits /
|
||||||
|
before-after visual / `Strong`-`Worth exploring`-`Speculative` badge), as a self-contained
|
||||||
|
Tailwind+Mermaid HTML file in your temp dir (default) or inline markdown
|
||||||
|
(`report_format: markdown`). Ends with a top recommendation, then stops and asks which
|
||||||
|
candidate to pursue.
|
||||||
|
4. **Refine via design-it-twice.** For the chosen candidate: frame the constraints and dependency
|
||||||
|
categories, produce 2-3 radically different interface designs (optionally spawning `oracle`
|
||||||
|
for an independent alternative), compare on depth/locality/seam placement, and hand off ONE
|
||||||
|
opinionated, implementation-ready proposal including the testing strategy ("replace, don't
|
||||||
|
layer").
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Scan the current repo, HTML report
|
||||||
|
coyote -a architecture-reviewer "Find deepening opportunities"
|
||||||
|
|
||||||
|
# Aim it at a pain point, inline report
|
||||||
|
coyote -a architecture-reviewer --agent-variable report_format markdown \
|
||||||
|
"The billing/entitlements code is painful to test - what should be deepened?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Also spawnable from `sisyphus` when a request is explicitly architecture-scale ("improve the
|
||||||
|
architecture of X", "make this module easier to test").
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`codebase-design`](../../skills/codebase-design/SKILL.md) — the vocabulary and principles it runs on.
|
||||||
|
- [`oracle`](../oracle/README.md) — advisory design review; also loads `codebase-design` for the shared vocabulary.
|
||||||
|
- [`explore`](../explore/README.md) — the codebase walkers it fans out.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Adapted from the `codebase-design` and `improve-codebase-architecture` skills in
|
||||||
|
[mattpocock/skills](https://github.com/mattpocock/skills) (MIT), which build on ideas from John
|
||||||
|
Ousterhout's *A Philosophy of Software Design* and Michael Feathers' *Working Effectively with
|
||||||
|
Legacy Code*.
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
name: architecture-reviewer
|
||||||
|
description: On-demand architecture improvement scout - scans a codebase for deepening opportunities (shallow modules, leaked seams, missing locality) weighted by git-history hot spots, presents candidates as a visual report, then refines the chosen candidate into a concrete interface proposal via design-it-twice. Proposes, never implements. NOT a completion gate - invoke it when you want the codebase made deeper, more testable, and easier to navigate.
|
||||||
|
version: 1.1.0
|
||||||
|
|
||||||
|
agent_session: temp
|
||||||
|
auto_continue: true
|
||||||
|
max_auto_continues: 20
|
||||||
|
inject_todo_instructions: true
|
||||||
|
|
||||||
|
can_spawn_agents: true
|
||||||
|
spawnable_agents:
|
||||||
|
- explore
|
||||||
|
- oracle
|
||||||
|
max_concurrent_agents: 4
|
||||||
|
# The limit is consulted by the SPAWNER against the child's absolute depth
|
||||||
|
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
|
||||||
|
# architecture-reviewer(2) → {explore | oracle}(3). This agent spawns depth-3
|
||||||
|
# children; 4 = one level of headroom.
|
||||||
|
max_agent_depth: 4
|
||||||
|
inject_spawn_instructions: true
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- codebase-design
|
||||||
|
- delegation-protocol
|
||||||
|
- grilling
|
||||||
|
- parallel-research
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory to scan
|
||||||
|
default: '.'
|
||||||
|
- name: report_format
|
||||||
|
description: Candidate report format - 'html' (self-contained file in the OS temp dir, opened for the user) or 'markdown' (inline in chat)
|
||||||
|
default: html
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- fs_write.sh
|
||||||
|
- execute_command.sh
|
||||||
|
|
||||||
|
instructions: |
|
||||||
|
You are an architecture improvement scout. You surface **deepening opportunities** — refactors
|
||||||
|
that turn shallow modules into deep ones — and refine the one the user picks into a concrete
|
||||||
|
interface proposal. The aim is testability, locality, and AI-navigability.
|
||||||
|
|
||||||
|
Two things you are NOT:
|
||||||
|
1. **Not a completion gate.** The review stack (`code-reviewer`/`adversary`/`security-reviewer`)
|
||||||
|
judges changes; you are invoked on demand to improve what already exists.
|
||||||
|
2. **Not an implementer.** You produce candidates and interface proposals; the user (or a coder
|
||||||
|
they delegate to) owns the code change. You never modify repository files — your only writes
|
||||||
|
are the report file in the OS temp directory.
|
||||||
|
|
||||||
|
## Step 0: Load the skill
|
||||||
|
|
||||||
|
Before anything else, `skill__load` `codebase-design`. It is your source of truth for the
|
||||||
|
vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**,
|
||||||
|
**locality**), the principles (the deletion test, "the interface is the test surface", "one
|
||||||
|
adapter = hypothetical seam, two = real"), the dependency categories for safe deepening, and the
|
||||||
|
design-it-twice pattern. Use those terms EXACTLY in every finding — no "component", "service",
|
||||||
|
or "boundary". Load `delegation-protocol` and `parallel-research` before spawning sub-agents.
|
||||||
|
|
||||||
|
## Phase 1: Scope, then explore
|
||||||
|
|
||||||
|
**Scope before you scan — YAGNI.** Deepening pays off where code keeps changing:
|
||||||
|
|
||||||
|
- If the user named a direction (a module, subsystem, or pain point), take it and skip inference.
|
||||||
|
- Otherwise mine the history for hot spots:
|
||||||
|
`execute_command --command "git -C {{project_dir}} log --oneline --name-only -100"` (or
|
||||||
|
similar) and let the files that keep recurring pull your attention. Scattered changes with no
|
||||||
|
hot spot → widen the net.
|
||||||
|
|
||||||
|
Read the workspace instructions (`COYOTE.md`/`AGENTS.md`) if present — documented conventions and
|
||||||
|
recorded decisions are constraints, not candidates; don't re-litigate them.
|
||||||
|
|
||||||
|
Then spawn 1-3 `explore` agents (per `delegation-protocol`, in parallel per `parallel-research`)
|
||||||
|
to walk the scoped area. Brief them to report friction, not metrics:
|
||||||
|
|
||||||
|
- Where does understanding one concept require bouncing between many small modules?
|
||||||
|
- Where are modules shallow — an interface nearly as complex as the implementation?
|
||||||
|
- Where were pure functions extracted "for testability" while the real bugs hide in how they're
|
||||||
|
called (no locality)?
|
||||||
|
- Where do tightly-coupled modules leak across their seams?
|
||||||
|
- What is untested, or hard to test through its current interface?
|
||||||
|
|
||||||
|
Apply the **deletion test** yourself to every suspect the explorers return: would deleting it
|
||||||
|
concentrate complexity (real candidate) or just move it (pass-through)?
|
||||||
|
|
||||||
|
## Phase 2: Present candidates
|
||||||
|
|
||||||
|
Produce 3-6 candidates, each with:
|
||||||
|
|
||||||
|
- **Files**: the modules involved
|
||||||
|
- **Problem**: the friction the current shape causes, in skill vocabulary
|
||||||
|
- **Solution**: plain-English description of the deepening (no interface design yet)
|
||||||
|
- **Benefits**: stated as leverage and locality gains, and how tests improve
|
||||||
|
- **Recommendation strength**: `Strong` / `Worth exploring` / `Speculative`
|
||||||
|
- **Before/after sketch**: for `html`, a visual per candidate; for `markdown`, a compact
|
||||||
|
ASCII/mermaid sketch
|
||||||
|
|
||||||
|
**Report delivery** (per `report_format`, currently: {{report_format}}):
|
||||||
|
|
||||||
|
- `html` — write ONE self-contained file to the OS temp dir (`$TMPDIR`, falling back to `/tmp`)
|
||||||
|
named `architecture-review-<timestamp>.html`. Use Tailwind via CDN for layout and Mermaid via
|
||||||
|
CDN for graph-shaped structure (call graphs, dependencies); hand-built divs/SVG for editorial
|
||||||
|
visuals (mass diagrams, collapse animations). One card per candidate with a side-by-side
|
||||||
|
before/after diagram. Open it for the user (`open` on macOS, `xdg-open` on Linux, `start` on
|
||||||
|
Windows) and print the absolute path. Nothing lands in the repo.
|
||||||
|
- `markdown` — render the same cards inline in your response.
|
||||||
|
|
||||||
|
End the report with a **Top recommendation**: which candidate you'd tackle first and why.
|
||||||
|
|
||||||
|
Then STOP and ask which candidate to explore. Do NOT propose interfaces yet.
|
||||||
|
|
||||||
|
## Phase 3: Refine the chosen candidate
|
||||||
|
|
||||||
|
1. **Frame the problem space**: the constraints any new interface must satisfy, the dependencies
|
||||||
|
and their category (in-process / local-substitutable / remote-but-owned / true external, per
|
||||||
|
the skill), and a rough illustrative sketch to make the constraints concrete. Show the user.
|
||||||
|
When the candidate carries open decisions (what sits behind the seam, which callers to
|
||||||
|
optimise for, what tests must survive), load `grilling` and walk them as frontier rounds —
|
||||||
|
recommended answer per question, facts fetched by you, decisions made by the user.
|
||||||
|
2. **Design it twice**: produce 2-3 radically different interface designs per the skill's
|
||||||
|
pattern (different constraint each: minimal interface / maximal flexibility / optimise the
|
||||||
|
common caller). For a candidate worth the budget, spawn `oracle` to independently design or
|
||||||
|
critique one alternative. Each design: interface (with invariants, ordering, error modes),
|
||||||
|
caller example, what hides behind the seam, adapter strategy, trade-offs.
|
||||||
|
3. **Compare and recommend**: contrast on depth, locality, and seam placement; give ONE
|
||||||
|
opinionated recommendation or a justified hybrid.
|
||||||
|
4. **Hand off**: summarize the chosen design as an implementation-ready proposal — files to
|
||||||
|
change, the target interface, the testing strategy ("replace, don't layer": new tests at the
|
||||||
|
deepened interface, old shallow-module tests deleted). Note that implementation belongs to
|
||||||
|
the caller, not you.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Never modify repository files.** The temp-dir report is your only write.
|
||||||
|
2. **Skill vocabulary, exactly.** Findings that say "service" or "boundary" get rewritten.
|
||||||
|
3. **Friction over dogma.** A shallow module that never changes and confuses no one is not a
|
||||||
|
candidate. Recent-change hot spots rank first.
|
||||||
|
4. **Candidates are judgment calls.** Frame every problem as observed friction with evidence
|
||||||
|
(file:line, test absence, change-history churn), not as rule violations.
|
||||||
|
5. **Respect recorded decisions.** If a candidate contradicts a documented convention or
|
||||||
|
decision, surface it only when the friction justifies revisiting — and mark the conflict
|
||||||
|
clearly in the card.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
- Project: {{project_dir}}
|
||||||
|
- Report format: {{report_format}}
|
||||||
|
- CWD: {{__cwd__}}
|
||||||
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
{{__tools__}}
|
||||||
@@ -1,5 +1,58 @@
|
|||||||
# Code Reviewer
|
# Code Reviewer
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **v3: now a GRAPH agent.** The orchestration shell is deterministic — a script resolves the
|
||||||
|
> diff/quality bar/ledger/history, domain linters run unconditionally, changed files are grouped
|
||||||
|
> into DOMAIN slices (deterministic proposal → LLM refinement → exact-cover gate) reviewed in
|
||||||
|
> parallel by [`domain-reviewer`](../domain-reviewer/README.md) leaders (peer messaging between
|
||||||
|
> leaders; per-file depth via `file-reviewer`), every leader report passes a completeness gate,
|
||||||
|
> findings are verified by `finding-verifier`, and the rigor folding + `MERGE-READY`/`NEEDS-HUMAN`
|
||||||
|
> verdict are computed BY A SCRIPT. Judgment stays in the agents; discipline lives in the graph.
|
||||||
|
> The review semantics below (severity mapping, folding rules, verdict rules, report format) are
|
||||||
|
> unchanged — they are now structurally enforced instead of prompt-enforced.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["parse (llm)"] --> B["facts (script): diff, quality bar, ledger, history, trigger signals"]
|
||||||
|
A -. "fallback" .-> PF["parse_fault (script): PIPELINE-FAULT verdict"]
|
||||||
|
PF --> L
|
||||||
|
B --> C["linters (script): tflint / hadolint / actionlint / buf breaking"]
|
||||||
|
C --> D1["group_propose (script): deterministic domain proposal"]
|
||||||
|
D1 --> D2["refine_groups (llm)"]
|
||||||
|
D2 --> D3["cover_gate (script: exact cover)"]
|
||||||
|
D2 -. "fallback" .-> D3
|
||||||
|
D3 --> E["map over domain slices (parallel): domain-reviewer leaders, teammates on, max_attempts: 2"]
|
||||||
|
D3 --> F["aux_lanes (llm): org-context + prior-art spawns"]
|
||||||
|
D3 --> G["downstream_sweep (script)"]
|
||||||
|
E --> H["completeness_gate (script): required sections even when clean"]
|
||||||
|
E -. "fallback" .-> DF["domain_fault (script): ⚠️ PIPELINE-FAULT slice banner"]
|
||||||
|
H -. "reject-retry once" .-> E
|
||||||
|
H --> I["synthesize (llm: dedup + prose, NO verdict authority)"]
|
||||||
|
F --> I
|
||||||
|
F -. "fallback" .-> AF["aux_fault (script): PIPELINE-FAULT lane-skip note"]
|
||||||
|
AF --> I
|
||||||
|
G --> I
|
||||||
|
I --> J["verify: finding-verifier (agent node, unskippable, max_attempts: 2)"]
|
||||||
|
I -. "fallback" .-> K
|
||||||
|
J --> K["verdict (script): FALSE-drops, rigor folding, fault accounting, MERGE-READY / NEEDS-HUMAN arithmetic"]
|
||||||
|
J -. "fallback" .-> K
|
||||||
|
K --> L["render (script): the standard report"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Failure fails closed.** Every LLM/agent node retries once (`max_attempts: 2` — the
|
||||||
|
domain-reviewer lanes and the verifier included), then routes to a `fallback:` instead of killing
|
||||||
|
the graph, and every fallback that loses review coverage leaves a `PIPELINE-FAULT` marker: a dead
|
||||||
|
`parse` emits a complete fault verdict and jumps straight to render; a dead domain lane becomes a
|
||||||
|
`> ⚠️ PIPELINE-FAULT` banner where its slice report would be; a dead `refine_groups` falls
|
||||||
|
through to the cover gate, which keeps the deterministic proposal; a dead synthesis or verifier
|
||||||
|
falls through to the verdict script. The verdict script counts every fault and forces
|
||||||
|
`NEEDS-HUMAN` — a degraded run never reads `MERGE-READY`. The aux context lanes are the one
|
||||||
|
exception: they are enrichment, so their fault (`aux_fault`) is surfaced as a lane-skip note but
|
||||||
|
never blocks. Fault detection is prefix-anchored (a report that merely *quotes* a marker doesn't
|
||||||
|
trip it), and the report is always emitted: the verdict script's own crash guard emits a
|
||||||
|
`PIPELINE-FAULT` NEEDS-HUMAN, and render bypasses its "No changes to review." stub for fault
|
||||||
|
verdicts — a fault renders as a full report, never silence.
|
||||||
|
|
||||||
A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified
|
A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified
|
||||||
report.
|
report.
|
||||||
|
|
||||||
@@ -12,11 +65,42 @@ agents while handling coordination and final reporting.
|
|||||||
- 🔄 **Cross-File Context**: Broadcasts sibling rosters so reviewers can alert each other about cross-cutting changes.
|
- 🔄 **Cross-File Context**: Broadcasts sibling rosters so reviewers can alert each other about cross-cutting changes.
|
||||||
- 📊 **Unified Reporting**: Synthesizes findings into a structured, easy-to-read summary with severity levels.
|
- 📊 **Unified Reporting**: Synthesizes findings into a structured, easy-to-read summary with severity levels.
|
||||||
- ⚡ **Parallel Execution**: Runs reviews concurrently for maximum speed.
|
- ⚡ **Parallel Execution**: Runs reviews concurrently for maximum speed.
|
||||||
|
- 🚨 **Operational History (optional)**: Checks the change against past production incidents via the [`incident-prior-art`](../../skills/incident-prior-art/SKILL.md) skill.
|
||||||
|
- 📦 **Context pack**: Every file-reviewer gets the same assembled inputs — change intent, repo convention docs, and matching entries from the review-miss ledger — so reviews judge against the house rules, not generic taste.
|
||||||
|
- 🏛️ **Org context (optional)**: When the diff touches decision-laden surface (public API shapes, money/auth logic), the agent named by the `org_context_agent` variable is spawned to find recorded decisions the diff must honor — contradicting one is a 🔴 finding. Disabled by default (`org_context_agent: ''`).
|
||||||
|
- 📚 **Review-miss ledger**: Loads the [`review-misses`](../../skills/review-misses/SKILL.md) skill — defect classes that previously ESCAPED review run as first-class checks; recurrences default to 🔴.
|
||||||
|
- ✅ **Finding Verifier**: Before the report posts, every 🔴/🟡 is re-validated against the working tree with quoted evidence; provably-false findings are dropped (and tallied) — false positives kill trust faster than missed bugs. Verification is delegated to the [`finding-verifier`](../finding-verifier/README.md) graph agent (one map branch per finding — structurally unskippable, fresh-context independent), with an inline fallback when it isn't installed.
|
||||||
|
- 🌐 **Downstream consumers (optional)**: When the diff changes consumer-facing contract surface (routes, protos, exported API, CLI flags), sibling checkouts under `consumers_root` and a `github_org` code search are grepped for consumers of the changed elements — a consumer matching a removed/changed contract is a 🔴 finding naming repo + file:line. Public-library consumers are unenumerable; the check degrades to semver/changelog discipline. Both probes disabled by default (`consumers_root: ''`, `github_org: ''`).
|
||||||
|
- 🚦 **Review Verdict**: The report opens with `MERGE-READY` or `NEEDS-HUMAN` (+ the 1-3 items a human must judge). Always-human triggers (irreversible migrations, authn/authz, money movement, secrets/crypto, incident-linked guard deletion) force `NEEDS-HUMAN` regardless of findings. Routing signal only — merging stays a human act.
|
||||||
|
|
||||||
|
## Operational History Lane
|
||||||
|
|
||||||
|
Code review answers "is this code good?" — this lane answers "did we already get burned by this?"
|
||||||
|
When the diff touches operationally-relevant surface (error handling, retries, timeouts, alerting,
|
||||||
|
config controlling any of these), the orchestrator:
|
||||||
|
|
||||||
|
1. **Git archaeology** (always available): blames the lines the diff deletes or weakens. A guard
|
||||||
|
that originated in an incident-fix commit and is being removed is a 🔴 CRITICAL finding — the
|
||||||
|
change reintroduces a known production failure mode.
|
||||||
|
2. **Prior-art delegation** (opt-in): if the `prior_art_agent` variable names an agent that can
|
||||||
|
search your incident record (Slack, Jira, postmortems, handoff docs), it is spawned in REVIEW
|
||||||
|
MODE with symptom-vocabulary search keys extracted from the diff (error strings, metric/alert
|
||||||
|
names, config keys — the vocabulary operators actually use).
|
||||||
|
|
||||||
|
The lane is disabled by default (`prior_art_agent: ''`) and findings fold into the standard
|
||||||
|
severity taxonomy under an "Operational history" report section — no separate verdict. Wire it up
|
||||||
|
in a bundle or your local config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
variables:
|
||||||
|
- name: prior_art_agent
|
||||||
|
default: 'oncall-historian' # any spawnable agent that can search your incident record
|
||||||
|
```
|
||||||
|
|
||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
name: code-reviewer
|
|
||||||
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
|
|
||||||
version: 1.0.0
|
|
||||||
temperature: 0.1
|
|
||||||
|
|
||||||
auto_continue: true
|
|
||||||
max_auto_continues: 20
|
|
||||||
inject_todo_instructions: true
|
|
||||||
|
|
||||||
can_spawn_agents: true
|
|
||||||
max_concurrent_agents: 10
|
|
||||||
max_agent_depth: 2
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- name: project_dir
|
|
||||||
description: Project directory to review
|
|
||||||
default: '.'
|
|
||||||
|
|
||||||
global_tools:
|
|
||||||
- fs_read.sh
|
|
||||||
- fs_grep.sh
|
|
||||||
- fs_glob.sh
|
|
||||||
- execute_command.sh
|
|
||||||
|
|
||||||
instructions: |
|
|
||||||
You are a code review orchestrator, similar to CodeRabbit. You coordinate per-file reviews and produce a unified report.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. **Get the diff:** Run `get_diff` to get the git diff (defaults to staged changes, falls back to unstaged)
|
|
||||||
2. **Parse changed files:** Extract the list of files from the diff
|
|
||||||
3. **Create todos:** One todo per phase (get diff, spawn reviewers, collect results, synthesize report)
|
|
||||||
4. **Spawn file-reviewers:** One `file-reviewer` agent per changed file, in parallel
|
|
||||||
5. **Broadcast sibling roster:** Send each file-reviewer a message with all sibling IDs and their file assignments
|
|
||||||
6. **Collect all results:** Wait for each file-reviewer to complete
|
|
||||||
7. **Synthesize:** Combine all findings into a CodeRabbit-style report
|
|
||||||
|
|
||||||
## Spawning File Reviewers
|
|
||||||
|
|
||||||
For each changed file, spawn a file-reviewer with a prompt containing:
|
|
||||||
- The file path
|
|
||||||
- The relevant diff hunk(s) for that file
|
|
||||||
- Instructions to review it
|
|
||||||
|
|
||||||
```
|
|
||||||
agent__spawn --agent file-reviewer --prompt "Review the following diff for <file_path>:
|
|
||||||
|
|
||||||
<diff content for this file>
|
|
||||||
|
|
||||||
Focus on bugs, security issues, logic errors, and style. Use the severity format (🔴🟡🟢💡).
|
|
||||||
End with REVIEW_COMPLETE."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sibling Roster Broadcast
|
|
||||||
|
|
||||||
After spawning ALL file-reviewers (collecting their IDs), send each one a message with the roster:
|
|
||||||
|
|
||||||
```
|
|
||||||
agent__send_message --to <agent_id> --message "SIBLING_ROSTER:
|
|
||||||
- <agent_id_1>: reviewing <file_1>
|
|
||||||
- <agent_id_2>: reviewing <file_2>
|
|
||||||
...
|
|
||||||
Send cross-cutting alerts to relevant siblings if your changes affect their files."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Diff Parsing
|
|
||||||
|
|
||||||
Split the diff by file. Each file's diff starts with `diff --git a/<path> b/<path>`. Extract:
|
|
||||||
- The file path (from the `+++ b/<path>` line)
|
|
||||||
- All hunks for that file (from `@@` markers to the next `diff --git` or end)
|
|
||||||
|
|
||||||
Skip binary files and files with only whitespace changes.
|
|
||||||
|
|
||||||
## Final Report Format
|
|
||||||
|
|
||||||
After collecting all file-reviewer results, synthesize into:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Code Review Summary
|
|
||||||
|
|
||||||
## Walkthrough
|
|
||||||
<2-3 sentence overview of what the changes do as a whole>
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
| File | Changes | Findings |
|
|
||||||
|------|---------|----------|
|
|
||||||
| `path/to/file1.rs` | <brief description> | 🔴 1 🟡 2 🟢 1 |
|
|
||||||
| `path/to/file2.rs` | <brief description> | 🟢 2 💡 1 |
|
|
||||||
|
|
||||||
## Detailed Findings
|
|
||||||
|
|
||||||
### `path/to/file1.rs`
|
|
||||||
<paste file-reviewer's findings here, cleaned up>
|
|
||||||
|
|
||||||
### `path/to/file2.rs`
|
|
||||||
<paste file-reviewer's findings here, cleaned up>
|
|
||||||
|
|
||||||
## Cross-File Concerns
|
|
||||||
<any cross-cutting issues identified by the teammate pattern>
|
|
||||||
|
|
||||||
---
|
|
||||||
*Reviewed N files, found X critical, Y warnings, Z suggestions, W nitpicks*
|
|
||||||
```
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
|
|
||||||
- **Single file changed:** Still spawn one file-reviewer (for consistency), skip roster broadcast
|
|
||||||
- **Too many files (>10):** Group small files (< 20 lines changed) and review them together
|
|
||||||
- **No changes found:** Report "No changes to review" and exit
|
|
||||||
- **Binary files:** Skip with a note in the summary
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
1. **Always use `get_diff` first:** Don't assume what changed
|
|
||||||
2. **Spawn in parallel:** All file-reviewers should be spawned before collecting any
|
|
||||||
3. **Don't review code yourself:** Delegate ALL review work to file-reviewers
|
|
||||||
4. **Preserve severity tags:** Don't downgrade or remove severity from file-reviewer findings
|
|
||||||
5. **Include ALL findings:** Don't summarize away specific issues
|
|
||||||
|
|
||||||
## Context
|
|
||||||
- Project: {{project_dir}}
|
|
||||||
- CWD: {{__cwd__}}
|
|
||||||
- Shell: {{__shell__}}
|
|
||||||
|
|
||||||
## Available Tools:
|
|
||||||
{{__tools__}}
|
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
name: code-reviewer
|
||||||
|
description: |
|
||||||
|
Graph-based review orchestrator — the deterministic shell around LLM review
|
||||||
|
judgment. A script resolves the diff, quality bar, conventions, review-miss
|
||||||
|
ledger patterns, and repo-history signals; domain linters run unconditionally;
|
||||||
|
changed files are grouped into DOMAIN slices (deterministic proposal, LLM
|
||||||
|
refinement, exact-cover gate) and each slice is reviewed by a domain-reviewer
|
||||||
|
leader — in parallel, with peer messaging between leaders — which delegates
|
||||||
|
per-file depth to file-reviewer. Every leader's report passes a completeness
|
||||||
|
gate (mandatory sections present even when clean, reject-and-retry once).
|
||||||
|
Findings are re-validated by the finding-verifier graph, folded by rigor and
|
||||||
|
scored into the MERGE-READY / NEEDS-HUMAN verdict BY A SCRIPT — the model
|
||||||
|
that wrote the findings never grades its own homework. Judgment stays in the
|
||||||
|
agents (per-file review, synthesis, dedup); discipline lives in the graph
|
||||||
|
(unskippable steps, identical context to every reviewer, machine-parsed
|
||||||
|
verdict math).
|
||||||
|
|
||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
can_spawn_agents: true
|
||||||
|
# The limit is consulted by the SPAWNER against the child's absolute depth
|
||||||
|
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
|
||||||
|
# review-gauntlet | step-runner(2) → code-reviewer(3) →
|
||||||
|
# {domain-reviewer(4) → file-reviewer(5) | finding-verifier(4)}.
|
||||||
|
# This agent spawns depth-4 children; 5 = one level of headroom.
|
||||||
|
max_agent_depth: 5
|
||||||
|
max_concurrent_agents: 16
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory containing the changes to review
|
||||||
|
default: '.'
|
||||||
|
- name: rigor
|
||||||
|
description: Quality bar rigor (poc|prototype|production); empty = resolve from plan frontmatter or default to production
|
||||||
|
default: ''
|
||||||
|
- name: surfaces
|
||||||
|
description: Comma-separated surface list governing skill routing; empty = detect from the diff
|
||||||
|
default: ''
|
||||||
|
- name: prior_art_agent
|
||||||
|
description: Agent for the operational-history lane (incident prior art); empty = skip the agent lane
|
||||||
|
default: ''
|
||||||
|
- name: org_context_agent
|
||||||
|
description: Agent for the org-context lane (recorded decisions, PRDs); empty = skip the lane
|
||||||
|
default: ''
|
||||||
|
- name: consumers_root
|
||||||
|
description: Directory of sibling consumer checkouts for the downstream sweep; empty = skip the local probe
|
||||||
|
default: ''
|
||||||
|
- name: github_org
|
||||||
|
description: GitHub org for the downstream code-search probe; empty = skip it
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
settings:
|
||||||
|
max_loop_iterations: 12
|
||||||
|
log_state_snapshots: false
|
||||||
|
timeout: 10800
|
||||||
|
|
||||||
|
initial_state:
|
||||||
|
project_dir_in: ''
|
||||||
|
verify_retries: 0
|
||||||
|
diff_spec: auto
|
||||||
|
intent: ''
|
||||||
|
rigor_in: ''
|
||||||
|
surfaces_in: ''
|
||||||
|
extra_context: ''
|
||||||
|
resolved_rigor: production
|
||||||
|
resolved_surfaces: ''
|
||||||
|
bar_provenance: detected-default
|
||||||
|
resolved_diff_spec: ''
|
||||||
|
changed_files: []
|
||||||
|
file_count: 0
|
||||||
|
facts_note: ''
|
||||||
|
conventions: ''
|
||||||
|
ledger_matches: ''
|
||||||
|
history_notes: ''
|
||||||
|
touches_auth: false
|
||||||
|
destructive_migration: false
|
||||||
|
consumer_surface: false
|
||||||
|
linter_notes: ''
|
||||||
|
proposed_groups: []
|
||||||
|
groups_note: ''
|
||||||
|
group_items: []
|
||||||
|
domain_reports: []
|
||||||
|
org_context_notes: ''
|
||||||
|
prior_art_notes: ''
|
||||||
|
aux_note: ''
|
||||||
|
downstream_notes: ''
|
||||||
|
walkthrough: ''
|
||||||
|
changes_rows: []
|
||||||
|
findings: []
|
||||||
|
cross_file: ''
|
||||||
|
operational_history: ''
|
||||||
|
org_context_section: ''
|
||||||
|
attention_flags: []
|
||||||
|
placement_notes: ''
|
||||||
|
verifier_output: ''
|
||||||
|
verdict_out: {}
|
||||||
|
final_report: ''
|
||||||
|
gate_feedback: ''
|
||||||
|
gate_attempts: 0
|
||||||
|
parse_failure: ''
|
||||||
|
refine_failure: ''
|
||||||
|
aux_failure: ''
|
||||||
|
synth_failure: ''
|
||||||
|
|
||||||
|
start: parse
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
parse:
|
||||||
|
id: parse
|
||||||
|
type: llm
|
||||||
|
description: Extract the review request's facts from the spawn prompt. Extraction only — no judging.
|
||||||
|
instructions: |
|
||||||
|
You extract the facts of a code-review request. EXTRACTION ONLY.
|
||||||
|
|
||||||
|
- `diff_spec`: the git diff basis the caller named — a range ("main...HEAD"),
|
||||||
|
a ref/SHA, "staged", "worktree" (uncommitted vs HEAD), or "auto" when the
|
||||||
|
caller did not name one (auto = staged, falling back to unstaged, falling
|
||||||
|
back to HEAD~1 — the classic get_diff semantics; a caller phrase like
|
||||||
|
"run get_diff --base X" means diff_spec = "X").
|
||||||
|
- `intent`: the PR description / ticket / task intent text, condensed but
|
||||||
|
specific. Empty string when the caller gave none.
|
||||||
|
- `rigor_in`: ONLY when the caller explicitly set a rigor (poc|prototype|production),
|
||||||
|
e.g. via a "Quality bar:" line; else empty string.
|
||||||
|
- `surfaces_in`: ONLY when the caller explicitly declared surfaces; else empty.
|
||||||
|
- `project_dir_in`: the directory/worktree path the caller named as
|
||||||
|
holding the code under review (e.g. "in /tmp/pr-review-x", "the repo
|
||||||
|
at ~/code/foo"); empty string when the caller named none.
|
||||||
|
- `extra_context`: any remaining reviewer-relevant caller context, condensed.
|
||||||
|
prompt: |
|
||||||
|
Extract the review-request facts from this request:
|
||||||
|
|
||||||
|
{{initial_prompt}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
diff_spec:
|
||||||
|
type: string
|
||||||
|
intent:
|
||||||
|
type: string
|
||||||
|
rigor_in:
|
||||||
|
type: string
|
||||||
|
surfaces_in:
|
||||||
|
type: string
|
||||||
|
project_dir_in:
|
||||||
|
type: string
|
||||||
|
extra_context:
|
||||||
|
type: string
|
||||||
|
required: [diff_spec, intent, rigor_in, surfaces_in, project_dir_in, extra_context]
|
||||||
|
max_iterations: 6
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
parse_failure: '{{output}}'
|
||||||
|
fallback: parse_fault
|
||||||
|
next: facts
|
||||||
|
|
||||||
|
parse_fault:
|
||||||
|
id: parse_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — emits a complete PIPELINE-FAULT verdict_out and jumps straight to render; a review that could not parse its request never reads as MERGE-READY.
|
||||||
|
script: scripts/parse_fault.py
|
||||||
|
timeout: 30
|
||||||
|
next: render
|
||||||
|
|
||||||
|
facts:
|
||||||
|
id: facts
|
||||||
|
type: script
|
||||||
|
description: Deterministic facts — diff resolution, changed files, quality-bar resolution (passed → plan frontmatter → detected), conventions pointer, review-miss ledger matches, repo-history sweep, verdict-trigger signals. Pure git/gh/filesystem; no LLM.
|
||||||
|
script: scripts/facts.py
|
||||||
|
timeout: 240
|
||||||
|
next: linters
|
||||||
|
|
||||||
|
linters:
|
||||||
|
id: linters
|
||||||
|
type: script
|
||||||
|
description: Domain linter pass — tflint/hadolint/actionlint on matching changed files, buf breaking when buf-configured protos changed. Only tools already on PATH; never installs; degrades silently per tool.
|
||||||
|
script: scripts/linters.py
|
||||||
|
timeout: 420
|
||||||
|
next: group_propose
|
||||||
|
|
||||||
|
group_propose:
|
||||||
|
id: group_propose
|
||||||
|
type: script
|
||||||
|
description: Deterministic domain grouping of changed files by surface-trigger path patterns.
|
||||||
|
script: scripts/group_domains.py
|
||||||
|
timeout: 30
|
||||||
|
next: refine_groups
|
||||||
|
|
||||||
|
refine_groups:
|
||||||
|
id: refine_groups
|
||||||
|
type: llm
|
||||||
|
description: Judgment refinement of the proposed domain groups — may rebalance, never drop or invent files (the cover gate enforces it).
|
||||||
|
instructions: |
|
||||||
|
You refine a deterministic domain grouping of changed files. You may:
|
||||||
|
- move a file whose CONTENT belongs to a different domain than its path suggests;
|
||||||
|
- split a group larger than ~8 files into coherent subgroups (name them
|
||||||
|
"<domain>-1", "<domain>-2" or by subsystem);
|
||||||
|
- merge groups that are really one change (a migration and its models);
|
||||||
|
- rename a domain for clarity.
|
||||||
|
|
||||||
|
You MUST include every changed file in exactly one group — no drops, no
|
||||||
|
inventions, no duplicates. A downstream gate validates the cover and falls
|
||||||
|
back to the deterministic proposal if you violate it.
|
||||||
|
prompt: |
|
||||||
|
Proposed groups (deterministic):
|
||||||
|
{{proposed_groups}}
|
||||||
|
|
||||||
|
All changed files (every one must appear in exactly one group):
|
||||||
|
{{changed_files}}
|
||||||
|
|
||||||
|
Change intent: {{intent}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
groups:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
files:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
rationale:
|
||||||
|
type: string
|
||||||
|
required: [domain, files]
|
||||||
|
required: [groups]
|
||||||
|
max_iterations: 6
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
refine_failure: '{{output}}'
|
||||||
|
fallback: cover_gate
|
||||||
|
next: cover_gate
|
||||||
|
|
||||||
|
cover_gate:
|
||||||
|
id: cover_gate
|
||||||
|
type: script
|
||||||
|
description: Exact-cover validation of the refined groups — every changed file in exactly one group, no unknown files. Violations fall back to the deterministic proposal (noted). Emits the map items.
|
||||||
|
script: scripts/cover_gate.py
|
||||||
|
timeout: 30
|
||||||
|
next: [map_domains, aux_lanes, downstream_sweep]
|
||||||
|
|
||||||
|
map_domains:
|
||||||
|
id: map_domains
|
||||||
|
type: map
|
||||||
|
over: '{{group_items}}'
|
||||||
|
as: domain_group
|
||||||
|
branch: review_domain
|
||||||
|
output_key: domain_report
|
||||||
|
collect_into: domain_reports
|
||||||
|
max_concurrency: 4
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
review_domain:
|
||||||
|
id: review_domain
|
||||||
|
type: agent
|
||||||
|
description: One domain-reviewer leader per domain slice — reviews the slice as a unit, delegates per-file depth to file-reviewer, messages the other leaders.
|
||||||
|
agent: domain-reviewer
|
||||||
|
teammates: true
|
||||||
|
timeout: 7200
|
||||||
|
prompt: |
|
||||||
|
## TASK
|
||||||
|
Review the changes in your assigned domain slice as ONE coherent unit and
|
||||||
|
produce a DOMAIN_REVIEW_COMPLETE-terminated report per your output format.
|
||||||
|
|
||||||
|
## ASSIGNMENT (JSON)
|
||||||
|
{{domain_group}}
|
||||||
|
|
||||||
|
## MUST DO
|
||||||
|
- Fetch your slice's diff yourself: `get_diff --files <your files, comma-separated>`
|
||||||
|
(diff spec: {{resolved_diff_spec}} — pass it as `--base` when it is a ref or range;
|
||||||
|
omit `--base` for staged/unstaged specs)
|
||||||
|
- Apply your domain charter: per-file review depth per your sizing rules, PLUS the
|
||||||
|
domain-level pass (guard symmetry across the slice, literal/enum drift,
|
||||||
|
schema↔usage mismatch, slice-level blast radius / test adequacy / removed guards)
|
||||||
|
- Report the mandatory checks EVEN WHEN CLEAN
|
||||||
|
- Message the other domain leaders when your slice changes something their domain consumes
|
||||||
|
{{gate_feedback}}
|
||||||
|
|
||||||
|
## MUST NOT DO
|
||||||
|
- No file modification; no findings omitted; never rewrite severities
|
||||||
|
|
||||||
|
## CONTEXT
|
||||||
|
Project: {{project_dir}}
|
||||||
|
Rigor: {{resolved_rigor}} (provenance: {{bar_provenance}})
|
||||||
|
Surfaces: {{resolved_surfaces}}
|
||||||
|
Intent: {{intent}}
|
||||||
|
Conventions: {{conventions}}
|
||||||
|
Review-miss patterns (run as FIRST-CLASS checks; cite MISS-NNN in the finding title when one fires):
|
||||||
|
{{ledger_matches}}
|
||||||
|
Repo-history signals:
|
||||||
|
{{history_notes}}
|
||||||
|
Linter output (extract the lines for your files):
|
||||||
|
{{linter_notes}}
|
||||||
|
{{extra_context}}
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
domain_report: '{{output}}'
|
||||||
|
fallback: domain_fault
|
||||||
|
next: completeness_gate
|
||||||
|
|
||||||
|
completeness_gate:
|
||||||
|
id: completeness_gate
|
||||||
|
type: script
|
||||||
|
description: Deterministic report-contract gate — required sections present (even when clean) and the completion sentinel. Rejects back to the leader exactly once with the missing sections, then keeps the report banner-flagged. Never sinks the map.
|
||||||
|
script: scripts/completeness_gate.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
domain_fault:
|
||||||
|
id: domain_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed domain lane fault marker (reached only via review_domain's fallback route inside the map branch). Normalizes the engine's "Agent node failed" text in domain_report into a > ⚠️ PIPELINE-FAULT banner the map collects; verdict.py counts any faulted lane and forces NEEDS-HUMAN — a dead domain lane never reads as a clean slice.
|
||||||
|
script: scripts/domain_fault.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
aux_lanes:
|
||||||
|
id: aux_lanes
|
||||||
|
type: llm
|
||||||
|
description: Conditional context lanes — org-context and operational-history/prior-art agents, spawned only when configured AND triggered. Additive context; never blocks.
|
||||||
|
instructions: |
|
||||||
|
You run the review's conditional context lanes by spawning agents (you have the
|
||||||
|
agent__spawn / agent__collect tools). Evaluate each lane's trigger against the
|
||||||
|
change facts; spawn the applicable ones IN ONE BURST, collect, and distill.
|
||||||
|
|
||||||
|
**Org-context lane** — only if `{{org_context_agent}}` is a non-empty agent name AND
|
||||||
|
the diff touches surface where a recorded decision plausibly exists (public API
|
||||||
|
shapes, money/entitlement logic, auth flows, data retention/deletion, domain state
|
||||||
|
machines and lifecycle semantics) OR the diff's scope visibly mismatches the
|
||||||
|
ticket/intent it cites. Spawn it with ONE question: "Any recorded decisions, PRDs,
|
||||||
|
or incident-driven constraints about <changed area>? Return quoted decisions with
|
||||||
|
dates/sources." (For a ticket mismatch: "which recorded ticket/PRD does THIS change
|
||||||
|
actually implement?") Distill the answer into `org_context_notes` — quoted decisions
|
||||||
|
with dates, and whether the diff appears to honor them.
|
||||||
|
|
||||||
|
**Operational-history lane** — only if `{{prior_art_agent}}` is a non-empty agent
|
||||||
|
name AND the diff touches operationally relevant surface (workers/queues,
|
||||||
|
migrations, infra, auth, retry/timeout logic, external calls, feature flags).
|
||||||
|
Spawn it in REVIEW MODE: "REVIEW MODE: incident prior art for <the changed
|
||||||
|
paths/surfaces>. Return incidents, postmortems, alert history, and prior-PR
|
||||||
|
lessons tied to these paths, with dates and links — especially any GUARD present
|
||||||
|
in the past that this diff deletes or weakens." Distill into `prior_art_notes`;
|
||||||
|
quote anything tying a DELETED guard to a past incident verbatim (the verdict
|
||||||
|
treats reintroduced failure modes as blocking).
|
||||||
|
|
||||||
|
A lane whose trigger does not match, whose agent name is empty, or whose agent
|
||||||
|
fails: skip it and say why in `aux_note` (one line each). Never invent lane
|
||||||
|
content; empty results are valid results.
|
||||||
|
prompt: |
|
||||||
|
Change facts:
|
||||||
|
- files: {{changed_files}}
|
||||||
|
- intent: {{intent}}
|
||||||
|
- auth surface: {{touches_auth}}; migrations: {{destructive_migration}}; consumer surface: {{consumer_surface}}
|
||||||
|
- resolved surfaces: {{resolved_surfaces}}
|
||||||
|
- extra: {{extra_context}}
|
||||||
|
|
||||||
|
Configured lane agents: org-context='{{org_context_agent}}', prior-art='{{prior_art_agent}}'.
|
||||||
|
Project: {{project_dir}}
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
org_context_notes:
|
||||||
|
type: string
|
||||||
|
prior_art_notes:
|
||||||
|
type: string
|
||||||
|
aux_note:
|
||||||
|
type: string
|
||||||
|
required: [org_context_notes, prior_art_notes, aux_note]
|
||||||
|
max_iterations: 10
|
||||||
|
max_attempts: 2
|
||||||
|
timeout: 3600
|
||||||
|
state_updates:
|
||||||
|
aux_failure: '{{output}}'
|
||||||
|
fallback: aux_fault
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
aux_fault:
|
||||||
|
id: aux_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for dead aux context lanes (reached only via aux_lanes' fallback) — records the failure as a PIPELINE-FAULT in aux_note, which synthesize surfaces as a lane skip. Aux lanes are enrichment; the degradation is visible but never blocks.
|
||||||
|
script: scripts/aux_fault.py
|
||||||
|
timeout: 30
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
downstream_sweep:
|
||||||
|
id: downstream_sweep
|
||||||
|
type: script
|
||||||
|
description: Downstream-consumers sweep — deterministic contract-token extraction from the diff, grep of sibling checkouts, bounded org code search. Runs only when consumer-facing surface changed; degrades silently.
|
||||||
|
script: scripts/downstream_sweep.py
|
||||||
|
timeout: 240
|
||||||
|
state_updates:
|
||||||
|
downstream_notes: '{{downstream_notes}}'
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
synthesize:
|
||||||
|
id: synthesize
|
||||||
|
type: llm
|
||||||
|
description: The judgment core — dedup, cross-file/cross-domain reasoning, prose. Emits STRUCTURED findings for the deterministic verdict; has NO verdict authority and cannot drop findings.
|
||||||
|
instructions: |
|
||||||
|
You synthesize domain review reports into the structured inputs of a code-review
|
||||||
|
report. You have NO verdict authority: the verdict, rigor folding, and severity
|
||||||
|
math happen downstream in a script, and finding verification happens in a separate
|
||||||
|
fresh-context agent. Your job is judgment, not grading.
|
||||||
|
|
||||||
|
Rules (non-negotiable):
|
||||||
|
- **Include ALL findings** from every domain report — never summarize one away.
|
||||||
|
Preserve each severity tag and any `[convention]`/`[correctness]` title marker
|
||||||
|
VERBATIM (marker goes in the `marker` field: "", "convention", or "correctness").
|
||||||
|
- **Dedup**: an identical finding reported twice (e.g. a surface skill and an
|
||||||
|
aspect skill like transactional-integrity) → keep the aspect skill's copy, drop
|
||||||
|
the duplicate. Dedup is the ONLY permitted removal.
|
||||||
|
- **Lane-derived findings you MUST add** when present in the lane notes:
|
||||||
|
- a diff that contradicts a recorded org decision (org_context_notes) → a
|
||||||
|
🔴 CRITICAL finding quoting the decision;
|
||||||
|
- a downstream consumer hit on a changed/removed contract element
|
||||||
|
(downstream_notes) → a 🔴 CRITICAL finding naming the consuming repo + file:line;
|
||||||
|
- repo-level linter residue that maps to no single file → a finding under the
|
||||||
|
owning surface (severity per the linter's own level; buf breaking = 🔴).
|
||||||
|
- **Every finding needs**: id (f1, f2, …), severity (icon + word verbatim, e.g.
|
||||||
|
"🔴 CRITICAL"), marker, file, lines, title, and `block` = the finding's full
|
||||||
|
markdown block exactly as it will appear in the report (#### title + Severity /
|
||||||
|
Lines / Description / Suggestion bullets).
|
||||||
|
- **attention_flags** (ADDITIVE-ONLY human-attention triggers; scripts add the
|
||||||
|
deterministic ones — migrations, auth — separately): money movement or
|
||||||
|
billing-amount computation changes; secrets/cryptography/key handling; a deleted
|
||||||
|
guard the operational-history lane tied to a past incident; a reintroduced
|
||||||
|
incident failure mode; a contradicted recorded decision. One short line each.
|
||||||
|
- **The placement question**: when the diff adds tolerance for a state its own
|
||||||
|
comments/errors call impossible ("should never happen", "defensive"), name the
|
||||||
|
PRODUCER of that state at path:line and put the question in `placement_notes` —
|
||||||
|
it is design intent, never auto-resolved.
|
||||||
|
- Prose sections: `walkthrough` (2-3 sentences on the change as a whole),
|
||||||
|
`changes_rows` (one {file, desc} per changed file, brief), `cross_file`
|
||||||
|
(cross-domain concerns from the leaders' reports + teammate alerts, or "None"),
|
||||||
|
`operational_history` (from prior_art_notes; "" when the lane didn't run),
|
||||||
|
`org_context_section` (from org_context_notes; "" when the lane didn't run).
|
||||||
|
Prior review decisions supplied in the caller context are SETTLED: never
|
||||||
|
invert or re-litigate one on the same line without NEW evidence (the line
|
||||||
|
changed, a new failure appeared). If you believe a settled decision is
|
||||||
|
wrong, do not re-flag it as a finding — surface it in `cross_file` as an
|
||||||
|
explicit conflict citing the prior decision, so the human sees a
|
||||||
|
disagreement, not a flip-flop.
|
||||||
|
|
||||||
|
prompt: |
|
||||||
|
Domain reports ({{file_count}} changed files, grouped into the domain slices below):
|
||||||
|
{{domain_reports}}
|
||||||
|
|
||||||
|
Lane notes:
|
||||||
|
- org context: {{org_context_notes}}
|
||||||
|
- operational history / prior art: {{prior_art_notes}}
|
||||||
|
- downstream consumers: {{downstream_notes}}
|
||||||
|
- lane skips: {{aux_note}}
|
||||||
|
- linter summary: {{linter_notes}}
|
||||||
|
- facts notes: {{facts_note}} {{groups_note}}
|
||||||
|
|
||||||
|
Intent: {{intent}}
|
||||||
|
Rigor: {{resolved_rigor}}; surfaces: {{resolved_surfaces}}
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
walkthrough:
|
||||||
|
type: string
|
||||||
|
changes_rows:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
desc:
|
||||||
|
type: string
|
||||||
|
required: [file, desc]
|
||||||
|
findings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
marker:
|
||||||
|
type: string
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
lines:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
block:
|
||||||
|
type: string
|
||||||
|
required: [id, severity, marker, file, lines, title, block]
|
||||||
|
cross_file:
|
||||||
|
type: string
|
||||||
|
operational_history:
|
||||||
|
type: string
|
||||||
|
org_context_section:
|
||||||
|
type: string
|
||||||
|
attention_flags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
placement_notes:
|
||||||
|
type: string
|
||||||
|
required: [walkthrough, changes_rows, findings, cross_file, operational_history, org_context_section, attention_flags, placement_notes]
|
||||||
|
max_iterations: 6
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
synth_failure: '{{output}}'
|
||||||
|
fallback: verdict
|
||||||
|
next: verify
|
||||||
|
|
||||||
|
verify:
|
||||||
|
id: verify
|
||||||
|
type: agent
|
||||||
|
description: Independent fresh-context verification of every 🔴/🟡 finding — structurally on the path to the report; cannot be skipped.
|
||||||
|
agent: finding-verifier
|
||||||
|
timeout: 3600
|
||||||
|
prompt: |
|
||||||
|
Verify these code-review findings against the working tree.
|
||||||
|
|
||||||
|
Project: {{project_dir}}
|
||||||
|
|
||||||
|
Findings (JSON; verify every 🔴 and 🟡 entry — id, severity, path, lines, claim
|
||||||
|
are the fields; `title` + `block` together are the claim):
|
||||||
|
{{findings}}
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
verifier_output: '{{output}}'
|
||||||
|
fallback: verdict
|
||||||
|
next: verify_gate
|
||||||
|
|
||||||
|
verify_gate:
|
||||||
|
id: verify_gate
|
||||||
|
type: script
|
||||||
|
description: Local verifier-fault retry — when the verifier completed but its output carries PIPELINE-FAULT markers (or the verifier died), re-run the cheap finding-verifier ONCE instead of letting the fault escalate into a full lane re-run at the gauntlet. Falls through to verdict when clean or out of retries; a gate crash falls forward on the declared edge.
|
||||||
|
script: scripts/verify_gate.py
|
||||||
|
timeout: 60
|
||||||
|
next: verdict
|
||||||
|
|
||||||
|
verdict:
|
||||||
|
id: verdict
|
||||||
|
type: script
|
||||||
|
description: Deterministic verdict math — applies verifier drops (FALSE, tallied), attaches evidence, folds by rigor, computes MERGE-READY/NEEDS-HUMAN from severity counts + always-human triggers. The model never grades its own homework.
|
||||||
|
script: scripts/verdict.py
|
||||||
|
timeout: 60
|
||||||
|
next: render
|
||||||
|
|
||||||
|
render:
|
||||||
|
id: render
|
||||||
|
type: script
|
||||||
|
description: Deterministic report assembly in the standard format from the structured findings and prose sections.
|
||||||
|
script: scripts/render.py
|
||||||
|
timeout: 30
|
||||||
|
next: done
|
||||||
|
|
||||||
|
done:
|
||||||
|
id: done
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
{{final_report}}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for dead aux context lanes of the code-reviewer graph.
|
||||||
|
|
||||||
|
Reached ONLY via aux_lanes' `fallback:` route. aux_lanes' state_updates wrote
|
||||||
|
an "LLM node …failed: <chain>" string into `aux_failure` before routing here;
|
||||||
|
this node records the failure as a PIPELINE-FAULT in `aux_note`, which flows
|
||||||
|
into synthesize's "lane skips" prompt line. Aux lanes are ENRICHMENT, not
|
||||||
|
review coverage: the degradation is made VISIBLE in the report's inputs but
|
||||||
|
never blocks — verdict.py deliberately does NOT scan aux_note for faults.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("aux_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"aux_note": f"PIPELINE-FAULT: aux context lanes failed after retries — {detail}"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"aux_note": "PIPELINE-FAULT: aux context lanes failed after retries — "
|
||||||
|
f"fault-marker script error: {e}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Report-contract gate for each domain-reviewer's output.
|
||||||
|
|
||||||
|
"Silence reads as not done": the mandatory sections must be PRESENT even when
|
||||||
|
clean. A report missing required sections is rejected back to the leader
|
||||||
|
exactly once with the missing list; a second failure keeps the report,
|
||||||
|
banner-flagged, so the synthesis (and the human) can see the gap — a
|
||||||
|
malformed report never sinks the whole review and never silently passes as
|
||||||
|
complete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
REQUIRED = [
|
||||||
|
("## Domain:", "the `## Domain:` header"),
|
||||||
|
("### Findings", "the `### Findings` section"),
|
||||||
|
("### Mandatory checks", "the `### Mandatory checks` section (report blast radius / test adequacy / guard symmetry even when clean)"),
|
||||||
|
("DOMAIN_REVIEW_COMPLETE", "the `DOMAIN_REVIEW_COMPLETE` sentinel"),
|
||||||
|
]
|
||||||
|
MAX_RETRIES = 1
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
report = state.get("domain_report")
|
||||||
|
report = report if isinstance(report, str) else ""
|
||||||
|
attempts = state.get("gate_attempts")
|
||||||
|
if not isinstance(attempts, int):
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
missing = [desc for marker, desc in REQUIRED if marker not in report]
|
||||||
|
if not missing:
|
||||||
|
print(json.dumps({}))
|
||||||
|
return
|
||||||
|
|
||||||
|
if attempts < MAX_RETRIES:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"_next": "review_domain",
|
||||||
|
"gate_attempts": attempts + 1,
|
||||||
|
"gate_feedback": (
|
||||||
|
"- COMPLETENESS GATE REJECTED your previous report — missing: "
|
||||||
|
+ "; ".join(missing)
|
||||||
|
+ ". Re-emit the FULL report with every required section "
|
||||||
|
"(mandatory checks are reported even when clean) and end with "
|
||||||
|
"DOMAIN_REVIEW_COMPLETE."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
group = state.get("domain_group")
|
||||||
|
domain = group.get("domain", "?") if isinstance(group, dict) else "?"
|
||||||
|
banner = (
|
||||||
|
f"> ⚠️ COMPLETENESS GATE: this domain report ({domain}) is missing required "
|
||||||
|
f"sections after one retry: {'; '.join(missing)}. Treat the missing checks as "
|
||||||
|
"NOT DONE, not as clean.\n\n"
|
||||||
|
)
|
||||||
|
print(json.dumps({"domain_report": banner + report}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a gate crash must never sink the map
|
||||||
|
print(json.dumps({"domain_report": f"> ⚠️ COMPLETENESS GATE ERROR: {e}\n\n(report unavailable)"}))
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exact-cover gate for the refined domain groups.
|
||||||
|
|
||||||
|
The LLM refinement may rebalance the deterministic proposal, but it may not
|
||||||
|
lose, invent, or duplicate files. This gate verifies the refined groups are an
|
||||||
|
EXACT COVER of the changed-file set; any violation falls back to the
|
||||||
|
deterministic proposal with a note. The LLM can shape the grouping, never
|
||||||
|
shrink it. Emits `group_items` — the map node's item list.
|
||||||
|
|
||||||
|
Crash-safe: this node has a `next:` edge, so an unguarded crash would
|
||||||
|
tolerant-fail ONWARD with an empty `group_items` — an empty review that could
|
||||||
|
masquerade as MERGE-READY. On any internal error it therefore emits one
|
||||||
|
catch-all group so the review still runs (and verdict.py's pipeline tripwire
|
||||||
|
catches the empty-report case independently).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
changed = state.get("changed_files") or []
|
||||||
|
proposed = state.get("proposed_groups") or []
|
||||||
|
refined = state.get("groups") or []
|
||||||
|
|
||||||
|
note = ""
|
||||||
|
chosen = None
|
||||||
|
if isinstance(refined, list) and refined:
|
||||||
|
seen = []
|
||||||
|
valid = True
|
||||||
|
for g in refined:
|
||||||
|
if not isinstance(g, dict) or not isinstance(g.get("files"), list) or not g.get("domain"):
|
||||||
|
valid = False
|
||||||
|
note = "refined groups malformed — fell back to the deterministic grouping"
|
||||||
|
break
|
||||||
|
seen.extend(g["files"])
|
||||||
|
if valid:
|
||||||
|
if sorted(seen) != sorted(changed):
|
||||||
|
missing = sorted(set(changed) - set(seen))
|
||||||
|
extra = sorted(set(seen) - set(changed))
|
||||||
|
dupes = sorted({f for f in seen if seen.count(f) > 1})
|
||||||
|
note = (
|
||||||
|
"refined groups failed the exact-cover check "
|
||||||
|
f"(missing={missing[:5]} extra={extra[:5]} duplicated={dupes[:5]}) — "
|
||||||
|
"fell back to the deterministic grouping"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
chosen = [{"domain": g["domain"], "files": g["files"]} for g in refined]
|
||||||
|
if chosen is None:
|
||||||
|
# refine_groups' fallback lands here with `groups` never set; make the
|
||||||
|
# degradation visible in the synthesis prompt via groups_note.
|
||||||
|
refine_failure = state.get("refine_failure")
|
||||||
|
if not refined and isinstance(refine_failure, str) and refine_failure.startswith("LLM node"):
|
||||||
|
note = "refinement lane failed after retries — fell back to the deterministic grouping"
|
||||||
|
chosen = [{"domain": g["domain"], "files": g["files"]} for g in proposed if isinstance(g, dict)]
|
||||||
|
|
||||||
|
print(json.dumps({"group_items": chosen, "groups_note": note}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crash must never tolerant-fail into an empty review
|
||||||
|
try:
|
||||||
|
files = load_state().get("changed_files") or []
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
files = []
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"group_items": [{"domain": "all-changes", "files": files}],
|
||||||
|
"groups_note": f"cover gate error: {e} — fell back to a single catch-all group",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed domain lane fault marker for the code-reviewer graph.
|
||||||
|
|
||||||
|
Reached ONLY via review_domain's `fallback:` route inside the map branch.
|
||||||
|
Grounded in engine behavior: on agent-node failure the engine writes
|
||||||
|
"Agent node failed: {e:#}" into `domain_report` via the node's state_updates
|
||||||
|
BEFORE routing to the fallback, so the raw failure text is already in state.
|
||||||
|
This node normalizes it into a "> ⚠️ PIPELINE-FAULT: domain review lane
|
||||||
|
failed after retries — …" banner the map collects as the lane's report;
|
||||||
|
verdict.py counts any banner-prefixed report as a fault and forces
|
||||||
|
NEEDS-HUMAN — a dead domain lane never reads as a clean slice.
|
||||||
|
|
||||||
|
Slice identity comes from `domain_group` (the map item cover_gate emitted,
|
||||||
|
which carries `domain` and `files`); a missing/malformed item degrades to
|
||||||
|
the failure detail alone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
FAULT_PREFIX = "> ⚠️ PIPELINE-FAULT: domain review lane failed after retries — "
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
slice_desc = ""
|
||||||
|
group = state.get("domain_group")
|
||||||
|
if isinstance(group, dict):
|
||||||
|
domain = group.get("domain")
|
||||||
|
files = [f for f in (group.get("files") or []) if isinstance(f, str)]
|
||||||
|
if isinstance(domain, str) and domain.strip():
|
||||||
|
slice_desc = f"domain '{domain.strip()}' (files: {', '.join(files)}): "
|
||||||
|
raw = state.get("domain_report")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(json.dumps({"domain_report": f"{FAULT_PREFIX}{slice_desc}{detail}"}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"domain_report": "> ⚠️ PIPELINE-FAULT: domain review lane failed "
|
||||||
|
f"after retries — fault-marker script error: {e}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Downstream-consumers sweep for the code-reviewer graph.
|
||||||
|
|
||||||
|
Runs only when consumer-facing contract surface changed. Extracts the most
|
||||||
|
specific changed contract tokens from the diff deterministically (route path
|
||||||
|
literals, proto message/service/rpc names, exported symbols), greps sibling
|
||||||
|
checkouts under `consumers_root`, and runs a bounded org code search via gh.
|
||||||
|
Hits are reported for the synthesis to raise as 🔴 findings naming the
|
||||||
|
consuming repo + file:line. Degrades silently when unconfigured/offline;
|
||||||
|
public-library caveats are noted, not guessed at.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
proj = os.path.abspath(state.get("project_dir") or ".")
|
||||||
|
spec = state.get("resolved_diff_spec") or "worktree"
|
||||||
|
consumers_root = os.path.expanduser((state.get("consumers_root") or "").strip())
|
||||||
|
github_org = (state.get("github_org") or "").strip()
|
||||||
|
|
||||||
|
if not state.get("consumer_surface"):
|
||||||
|
print(json.dumps({"downstream_notes": "(skipped — no consumer-facing contract surface in the diff)"}))
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def diff_text():
|
||||||
|
args = {"staged": ["--cached"], "worktree": ["HEAD"], "HEAD~1": ["HEAD~1"]}.get(spec, [spec])
|
||||||
|
r = subprocess.run(["git", "-C", proj, "diff", *args], capture_output=True, text=True, timeout=90)
|
||||||
|
return r.stdout if r.returncode == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
changed = [
|
||||||
|
l[1:] for l in diff_text().splitlines()
|
||||||
|
if (l.startswith("+") or l.startswith("-")) and not l.startswith(("+++", "---"))
|
||||||
|
]
|
||||||
|
text = "\n".join(changed)
|
||||||
|
tokens = set()
|
||||||
|
tokens.update(re.findall(r'["\'](/[a-z0-9][a-z0-9/_{}.:-]{5,})["\']', text)) # route paths
|
||||||
|
tokens.update(re.findall(r"^\s*(?:message|service|rpc)\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M)) # proto names
|
||||||
|
tokens.update(re.findall(r"^\s*(?:pub\s+fn|pub\s+struct|func|export\s+(?:function|const|class))\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M))
|
||||||
|
tokens = sorted(tokens, key=len, reverse=True)[:5]
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
if not tokens:
|
||||||
|
notes.append("(no specific contract tokens extractable from the diff — sweep skipped)")
|
||||||
|
else:
|
||||||
|
notes.append(f"contract tokens probed: {tokens}")
|
||||||
|
if consumers_root and os.path.isdir(consumers_root):
|
||||||
|
self_name = os.path.basename(proj)
|
||||||
|
for tok in tokens:
|
||||||
|
r = subprocess.run(
|
||||||
|
["grep", "-rIl", "--exclude-dir=.git", "--exclude-dir=node_modules",
|
||||||
|
"--exclude-dir=target", "--exclude-dir=vendor", tok, consumers_root],
|
||||||
|
capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
hits = [
|
||||||
|
h for h in r.stdout.splitlines()
|
||||||
|
if h.strip() and f"/{self_name}/" not in h and not h.startswith(proj)
|
||||||
|
][:4]
|
||||||
|
for h in hits:
|
||||||
|
notes.append(f"CONSUMER HIT (sibling checkout): {h} references '{tok}' — "
|
||||||
|
"a changed/removed contract element with a live consumer is a 🔴 finding")
|
||||||
|
elif consumers_root:
|
||||||
|
notes.append(f"(consumers_root '{consumers_root}' not found — local probe skipped)")
|
||||||
|
else:
|
||||||
|
notes.append("(consumers_root unset — local probe skipped)")
|
||||||
|
|
||||||
|
if github_org and shutil.which("gh"):
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.pop("CLICOLOR_FORCE", None)
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
|
env["GH_NO_UPDATE_NOTIFIER"] = "1"
|
||||||
|
for tok in tokens:
|
||||||
|
r = subprocess.run(
|
||||||
|
["gh", "api", "search/code", "-f", f'q=org:{github_org} "{tok}"',
|
||||||
|
"--jq", r'.items[:4][] | "\(.repository.full_name): \(.path)"'],
|
||||||
|
capture_output=True, text=True, timeout=30, env=env,
|
||||||
|
)
|
||||||
|
if r.returncode == 0 and r.stdout.strip():
|
||||||
|
for line in r.stdout.strip().splitlines():
|
||||||
|
notes.append(f"CONSUMER HIT (org search): {line} references '{tok}'")
|
||||||
|
elif github_org:
|
||||||
|
notes.append("(gh unavailable — org code search skipped)")
|
||||||
|
else:
|
||||||
|
notes.append("(github_org unset — org code search skipped)")
|
||||||
|
notes.append("Public-library caveat: consumers of a PUBLIC library are unenumerable — "
|
||||||
|
"for exported-API changes the check degrades to semver/changelog discipline.")
|
||||||
|
print(json.dumps({"downstream_notes": "\n".join(notes)}))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(json.dumps({"downstream_notes": f"(sweep degraded: {e})"}))
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic review facts for the code-reviewer graph.
|
||||||
|
|
||||||
|
Resolves the diff, the changed-file list, the quality bar (caller-passed →
|
||||||
|
plan frontmatter → detected default), the conventions pointer, review-miss
|
||||||
|
ledger matches, the repo-history sweep (fix/revert clusters, bounded gh
|
||||||
|
PR-comment pull), and the deterministic verdict-trigger signals. Pure
|
||||||
|
git/gh/filesystem — no LLM. Failures degrade into `facts_note`; they never
|
||||||
|
sink the review.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
proj = os.path.expanduser(
|
||||||
|
(state.get("project_dir_in") or "").strip() or state.get("project_dir") or "."
|
||||||
|
)
|
||||||
|
spec = (state.get("diff_spec") or "auto").strip()
|
||||||
|
notes = []
|
||||||
|
out = {"project_dir": proj}
|
||||||
|
|
||||||
|
|
||||||
|
def git(*args, timeout=60):
|
||||||
|
r = subprocess.run(["git", "-C", proj, *args], capture_output=True, text=True, timeout=timeout)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError((r.stderr or r.stdout).strip()[:300])
|
||||||
|
return r.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def scrubbed_env():
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.pop("CLICOLOR_FORCE", None)
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
|
env["GH_NO_UPDATE_NOTIFIER"] = "1"
|
||||||
|
env["NO_COLOR"] = "1"
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
# --- diff resolution (get_diff semantics) -----------------------------------
|
||||||
|
resolved = spec
|
||||||
|
files = []
|
||||||
|
try:
|
||||||
|
if spec in ("auto", ""):
|
||||||
|
names = git("diff", "--cached", "--numstat")
|
||||||
|
resolved = "staged"
|
||||||
|
if not names.strip():
|
||||||
|
names = git("diff", "--numstat")
|
||||||
|
resolved = "worktree"
|
||||||
|
if not names.strip():
|
||||||
|
names = git("diff", "HEAD~1", "--numstat")
|
||||||
|
resolved = "HEAD~1"
|
||||||
|
elif spec == "staged":
|
||||||
|
names = git("diff", "--cached", "--numstat")
|
||||||
|
elif spec == "worktree":
|
||||||
|
names = git("diff", "HEAD", "--numstat")
|
||||||
|
else:
|
||||||
|
names = git("diff", spec, "--numstat")
|
||||||
|
for line in names.splitlines():
|
||||||
|
parts = line.split("\t")
|
||||||
|
if len(parts) == 3:
|
||||||
|
added, deleted, path = parts
|
||||||
|
if added == "-" and deleted == "-":
|
||||||
|
notes.append(f"binary file skipped: {path}")
|
||||||
|
continue
|
||||||
|
files.append(path.strip())
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
notes.append(f"diff resolution failed for spec {spec!r}: {e}")
|
||||||
|
|
||||||
|
out["resolved_diff_spec"] = resolved
|
||||||
|
out["changed_files"] = files[:300]
|
||||||
|
out["file_count"] = len(files)
|
||||||
|
if len(files) > 300:
|
||||||
|
notes.append(
|
||||||
|
f"file list truncated: {len(files)} changed files, only the first 300 are "
|
||||||
|
"grouped and reviewed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def diff_text():
|
||||||
|
try:
|
||||||
|
if resolved == "staged":
|
||||||
|
return git("diff", "--cached", timeout=90)
|
||||||
|
if resolved == "worktree":
|
||||||
|
return git("diff", "HEAD", timeout=90) if spec == "worktree" else git("diff", timeout=90)
|
||||||
|
if resolved == "HEAD~1":
|
||||||
|
return git("diff", "HEAD~1", timeout=90)
|
||||||
|
return git("diff", resolved, timeout=90)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
dtext = diff_text()
|
||||||
|
added_lines = "\n".join(
|
||||||
|
l for l in dtext.splitlines() if l.startswith("+") and not l.startswith("+++")
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- quality bar resolution --------------------------------------------------
|
||||||
|
rigor_in = (state.get("rigor_in") or state.get("rigor") or "").strip()
|
||||||
|
surfaces_in = (state.get("surfaces_in") or state.get("surfaces") or "").strip()
|
||||||
|
resolved_rigor, resolved_surfaces, provenance = "production", "", "detected-default"
|
||||||
|
if rigor_in or surfaces_in:
|
||||||
|
resolved_rigor = rigor_in or "production"
|
||||||
|
resolved_surfaces = surfaces_in
|
||||||
|
provenance = "passed"
|
||||||
|
else:
|
||||||
|
for plan in sorted(glob.glob(os.path.join(proj, "plans", "PLAN-*.md"))):
|
||||||
|
try:
|
||||||
|
with open(plan) as f:
|
||||||
|
head = f.read(4000)
|
||||||
|
m = re.match(r"\A---\n(.*?)\n---", head, re.S)
|
||||||
|
if m and re.search(r"^status:\s*active\s*$", m.group(1), re.M):
|
||||||
|
rig = re.search(r"^rigor:\s*(\S+)", m.group(1), re.M)
|
||||||
|
sur = re.search(r"^surfaces:\s*(.+)$", m.group(1), re.M)
|
||||||
|
if rig:
|
||||||
|
resolved_rigor = rig.group(1).strip()
|
||||||
|
if sur:
|
||||||
|
resolved_surfaces = sur.group(1).strip("[] \n")
|
||||||
|
provenance = "plan"
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
SURFACE_PATTERNS = [
|
||||||
|
("db-migration", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)"),
|
||||||
|
("iac", r"\.tf$|helm|charts?/|Dockerfile|docker-compose|k8s/|manifests?/"),
|
||||||
|
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
|
||||||
|
("worker", r"workers?/|jobs?/|consumers?/|cron|celery|sidekiq|river"),
|
||||||
|
("rest-api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
|
||||||
|
("cli", r"\bcmd/|\bcli\b"),
|
||||||
|
("library", r"go\.mod$|package\.json$|Cargo\.toml$|pyproject\.toml$|setup\.py$|__init__\.py$"),
|
||||||
|
]
|
||||||
|
if provenance == "detected-default" and files:
|
||||||
|
detected = []
|
||||||
|
for surface, pat in SURFACE_PATTERNS:
|
||||||
|
if any(re.search(pat, f, re.I) for f in files):
|
||||||
|
detected.append(surface)
|
||||||
|
resolved_surfaces = ", ".join(detected)
|
||||||
|
|
||||||
|
out["resolved_rigor"] = resolved_rigor
|
||||||
|
out["resolved_surfaces"] = resolved_surfaces
|
||||||
|
out["bar_provenance"] = provenance
|
||||||
|
|
||||||
|
# --- conventions pointer ------------------------------------------------------
|
||||||
|
conv = []
|
||||||
|
for pat in ("CLAUDE.md", "COYOTE.md", "CONTRIBUTING.md", ".golangci*", ".eslintrc*",
|
||||||
|
"rustfmt.toml", ".rubocop.yml", "ruff.toml", ".editorconfig", "Makefile"):
|
||||||
|
conv.extend(sorted(glob.glob(os.path.join(proj, pat)))[:2])
|
||||||
|
out["conventions"] = ", ".join(os.path.relpath(p, proj) for p in conv) or "none found"
|
||||||
|
|
||||||
|
# --- review-miss ledger -------------------------------------------------------
|
||||||
|
ledger_text = ""
|
||||||
|
for cand in (
|
||||||
|
os.path.join(os.path.expanduser("~"), ".config", "coyote", "share", "review-misses.md"),
|
||||||
|
os.path.join(os.path.expanduser("~"), ".config", "coyote", "review-misses.md"),
|
||||||
|
):
|
||||||
|
if os.path.isfile(cand):
|
||||||
|
try:
|
||||||
|
with open(cand) as f:
|
||||||
|
ledger_text = f.read()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
if ledger_text:
|
||||||
|
try:
|
||||||
|
repo_name = os.path.basename(git("rev-parse", "--show-toplevel").strip())
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
repo_name = os.path.basename(os.path.abspath(proj))
|
||||||
|
keywords = {repo_name.lower()}
|
||||||
|
keywords.update(s.strip().lower() for s in resolved_surfaces.split(",") if s.strip())
|
||||||
|
for f in files:
|
||||||
|
keywords.update(p.lower() for p in re.split(r"[/_.-]", f) if len(p) >= 4)
|
||||||
|
entries = re.split(r"(?=^## MISS-)", ledger_text, flags=re.M)
|
||||||
|
for entry in entries:
|
||||||
|
if not entry.startswith("## MISS-"):
|
||||||
|
continue
|
||||||
|
body = entry.lower()
|
||||||
|
if any(k in body for k in keywords):
|
||||||
|
lines = entry.strip().splitlines()
|
||||||
|
matches.append("\n".join(lines[:8]))
|
||||||
|
if len(matches) >= 8:
|
||||||
|
break
|
||||||
|
out["ledger_matches"] = "\n\n".join(matches) if matches else "(no matching ledger entries)"
|
||||||
|
|
||||||
|
# --- repo-history sweep -------------------------------------------------------
|
||||||
|
history = []
|
||||||
|
fragile = []
|
||||||
|
try:
|
||||||
|
if files:
|
||||||
|
log = git("log", "--oneline", "-15", "--", *files[:100])
|
||||||
|
history.append(log.strip()[:2000])
|
||||||
|
per_file = {}
|
||||||
|
for f in files[:50]:
|
||||||
|
try:
|
||||||
|
fl = git("log", "--oneline", "-10", "--", f)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
continue
|
||||||
|
hits = [l for l in fl.splitlines() if re.search(r"\b(fix|revert|hotfix)\b", l, re.I)]
|
||||||
|
if len(hits) >= 2:
|
||||||
|
fragile.append(f)
|
||||||
|
per_file[f] = hits[:2]
|
||||||
|
for f, hits in per_file.items():
|
||||||
|
history.append(f"FRAGILE: {f} — {len(hits)}+ fix/revert commits recently — raise scrutiny")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
notes.append(f"history sweep failed: {e}")
|
||||||
|
|
||||||
|
# gh one-level-deeper: PR review comments for fragile files (bounded, silent skip)
|
||||||
|
try:
|
||||||
|
if fragile:
|
||||||
|
remote = git("remote", "get-url", "origin").strip()
|
||||||
|
m = re.search(r"github\.com[:/]([^/]+/[^/.]+)", remote)
|
||||||
|
if m:
|
||||||
|
repo_slug = m.group(1)
|
||||||
|
env = scrubbed_env()
|
||||||
|
shas = []
|
||||||
|
for f in fragile[:2]:
|
||||||
|
lg = git("log", "--format=%H", "-2", "--", f)
|
||||||
|
shas.extend(lg.split())
|
||||||
|
pr_nums = []
|
||||||
|
for sha in shas[:3]:
|
||||||
|
r = subprocess.run(
|
||||||
|
["gh", "api", f"repos/{repo_slug}/commits/{sha}/pulls", "--jq", ".[].number"],
|
||||||
|
capture_output=True, text=True, timeout=30, env=env,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
pr_nums.extend(n for n in r.stdout.split() if n.isdigit())
|
||||||
|
seen = []
|
||||||
|
for n in pr_nums:
|
||||||
|
if n not in seen:
|
||||||
|
seen.append(n)
|
||||||
|
comments = []
|
||||||
|
for n in seen[:3]:
|
||||||
|
r = subprocess.run(
|
||||||
|
["gh", "api", f"repos/{repo_slug}/pulls/{n}/comments",
|
||||||
|
"--jq", r'.[] | "\(.path): \(.body)"'],
|
||||||
|
capture_output=True, text=True, timeout=30, env=env,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
for line in r.stdout.splitlines():
|
||||||
|
path = line.split(":", 1)[0]
|
||||||
|
if any(path in f or f in path for f in files):
|
||||||
|
comments.append(line[:200])
|
||||||
|
if len(comments) >= 6:
|
||||||
|
break
|
||||||
|
if comments:
|
||||||
|
history.append("Prior PR review comments on fragile paths:\n" + "\n".join(comments[:6]))
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass # offline / not GitHub / no gh — silent by design
|
||||||
|
|
||||||
|
out["history_notes"] = "\n".join(h for h in history if h) or "(no notable history)"
|
||||||
|
|
||||||
|
# --- deterministic verdict-trigger signals -----------------------------------
|
||||||
|
def any_path(pattern):
|
||||||
|
return any(re.search(pattern, f, re.I) for f in files)
|
||||||
|
|
||||||
|
|
||||||
|
migration_files = [f for f in files if re.search(r"migrations?/|\.sql$", f, re.I)]
|
||||||
|
mig_added = "\n".join(
|
||||||
|
l for l in dtext.splitlines()
|
||||||
|
if l.startswith("+") and not l.startswith("+++")
|
||||||
|
) if migration_files else ""
|
||||||
|
out["destructive_migration"] = bool(
|
||||||
|
migration_files
|
||||||
|
and re.search(r"(?i)drop\s+(table|column)|rename\s+(table|column|to)|alter\s+\S+\s+drop", mig_added)
|
||||||
|
)
|
||||||
|
out["touches_auth"] = bool(
|
||||||
|
any_path(r"auth(?!or)|login|logout|token|session|permission|rbac|\bacl\b|credential")
|
||||||
|
or re.search(r"auth(?!or)|password|jwt|session|credential", added_lines, re.I)
|
||||||
|
)
|
||||||
|
out["consumer_surface"] = any_path(
|
||||||
|
r"routes?|handlers?|\bapi\b|endpoints?|\bcmd/|\bcli\b|\.proto$|openapi|swagger|controllers?|graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
if notes:
|
||||||
|
out["facts_note"] = "; ".join(notes)[:800]
|
||||||
|
print(json.dumps(out))
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic domain grouping of changed files for the code-reviewer graph.
|
||||||
|
|
||||||
|
Each file lands in the FIRST matching domain (patterns ordered most-specific
|
||||||
|
first); unmatched files land in `general`. When the grouping is very wide,
|
||||||
|
single-file groups merge into `general` so the fan-out stays sane. The LLM
|
||||||
|
refinement node may rebalance this proposal; the cover gate guarantees no
|
||||||
|
file is lost either way. Crash-safe: an error emits one catch-all proposal
|
||||||
|
rather than tolerant-failing into an empty grouping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
DOMAINS = [
|
||||||
|
("db", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)|models?/"),
|
||||||
|
("iac", r"\.tf$|helm|charts?/|(^|/)Dockerfile|docker-compose|k8s/|manifests?/"),
|
||||||
|
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
|
||||||
|
("workers", r"workers?/|jobs?/|consumers?/|cron|tasks?/"),
|
||||||
|
("api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
|
||||||
|
("cli", r"\bcmd/|\bcli\b"),
|
||||||
|
("deps", r"go\.(mod|sum)$|package(-lock)?\.json$|yarn\.lock$|Cargo\.(toml|lock)$|requirements[^/]*\.txt$|pyproject\.toml$|Gemfile|pom\.xml$|composer\.json$"),
|
||||||
|
("tests", r"(^|/)tests?/|_test\.|\.test\.|spec\."),
|
||||||
|
("docs", r"\.(md|rst|adoc|txt)$"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
files = state.get("changed_files") or []
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
for f in files:
|
||||||
|
domain = "general"
|
||||||
|
for name, pat in DOMAINS:
|
||||||
|
if re.search(pat, f, re.I):
|
||||||
|
domain = name
|
||||||
|
break
|
||||||
|
groups.setdefault(domain, []).append(f)
|
||||||
|
|
||||||
|
# Wide fan-out: merge single-file groups into general.
|
||||||
|
if len(groups) > 6:
|
||||||
|
for name in list(groups):
|
||||||
|
if name != "general" and len(groups[name]) == 1:
|
||||||
|
groups.setdefault("general", []).extend(groups.pop(name))
|
||||||
|
|
||||||
|
proposed = [{"domain": k, "files": v} for k, v in sorted(groups.items())]
|
||||||
|
print(json.dumps({"proposed_groups": proposed}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — never tolerant-fail into an empty grouping
|
||||||
|
try:
|
||||||
|
files = load_state().get("changed_files") or []
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
files = []
|
||||||
|
print(json.dumps({"proposed_groups": [{"domain": "all-changes", "files": files}]}))
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Domain linter pass for the code-reviewer graph.
|
||||||
|
|
||||||
|
Runs whichever mechanized checkers exist on PATH against the matching changed
|
||||||
|
files: tflint (Terraform), hadolint (Dockerfiles), actionlint (workflow
|
||||||
|
files), and `buf breaking` when buf-configured protos changed. Read-only,
|
||||||
|
never installs anything, and each tool degrades silently to a skip note.
|
||||||
|
Output is routed per-file where possible so the domain leaders can extract
|
||||||
|
their slice's lines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
proj = state.get("project_dir") or "."
|
||||||
|
files = state.get("changed_files") or []
|
||||||
|
spec = state.get("resolved_diff_spec") or "worktree"
|
||||||
|
notes = []
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, timeout=60):
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=proj)
|
||||||
|
return (r.stdout + r.stderr).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def capped(text, n=40):
|
||||||
|
lines = text.splitlines()
|
||||||
|
return "\n".join(lines[:n]) + (f"\n… ({len(lines) - n} more lines)" if len(lines) > n else "")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
tf = [f for f in files if f.endswith(".tf")]
|
||||||
|
if tf and shutil.which("tflint"):
|
||||||
|
notes.append("[tflint]\n" + capped(run(["tflint", "--no-color"], 120) or "clean"))
|
||||||
|
elif tf:
|
||||||
|
notes.append("[tflint] skipped — not on PATH")
|
||||||
|
|
||||||
|
dockers = [f for f in files if re.search(r"(^|/)Dockerfile", f)]
|
||||||
|
if dockers and shutil.which("hadolint"):
|
||||||
|
notes.append("[hadolint]\n" + capped(run(["hadolint", "--no-color", *dockers[:10]], 60) or "clean"))
|
||||||
|
elif dockers:
|
||||||
|
notes.append("[hadolint] skipped — not on PATH")
|
||||||
|
|
||||||
|
workflows = [f for f in files if ".github/workflows/" in f]
|
||||||
|
if workflows and shutil.which("actionlint"):
|
||||||
|
notes.append("[actionlint]\n" + capped(run(["actionlint", "-no-color", *workflows[:10]], 60) or "clean"))
|
||||||
|
elif workflows:
|
||||||
|
notes.append("[actionlint] skipped — not on PATH")
|
||||||
|
|
||||||
|
protos = [f for f in files if f.endswith(".proto")]
|
||||||
|
if protos and os.path.isfile(os.path.join(proj, "buf.yaml")) and shutil.which("buf"):
|
||||||
|
base = None
|
||||||
|
if "..." in spec:
|
||||||
|
base = spec.split("...")[0]
|
||||||
|
elif ".." in spec:
|
||||||
|
base = spec.split("..")[0]
|
||||||
|
elif spec not in ("staged", "worktree", "HEAD~1"):
|
||||||
|
base = spec
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["git", "-C", proj, "rev-parse", "HEAD"],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
base = r.stdout.strip() if r.returncode == 0 else None
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
base = None
|
||||||
|
if base:
|
||||||
|
out = run(["buf", "breaking", "--against", f".git#ref={base}"], 120)
|
||||||
|
notes.append("[buf breaking] (wire-compat breaks are 🔴 by default)\n" + capped(out or "clean"))
|
||||||
|
else:
|
||||||
|
notes.append("[buf breaking] skipped — no base ref resolvable")
|
||||||
|
elif protos:
|
||||||
|
notes.append("[buf breaking] skipped — no buf.yaml or buf not on PATH")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
notes.append(f"[linters] pass degraded: {e}")
|
||||||
|
|
||||||
|
print(json.dumps({"linter_notes": "\n\n".join(notes) if notes else "(no applicable linters)"}))
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead parse stage of the code-reviewer graph.
|
||||||
|
|
||||||
|
Reached ONLY via parse's `fallback:` route. parse's state_updates wrote an
|
||||||
|
"LLM node …failed: <chain>" string into `parse_failure` before routing here;
|
||||||
|
this node emits a complete PIPELINE-FAULT `verdict_out` and the graph jumps
|
||||||
|
straight to render — a review that could not even parse its request never
|
||||||
|
reads as MERGE-READY. render.py renders a PIPELINE-FAULT verdict despite the
|
||||||
|
empty changed-file list (its "No changes to review." early return skips
|
||||||
|
fault-reason verdicts).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
ATTENTION_LINE = (
|
||||||
|
"PIPELINE-FAULT: parse failed — the review request could not be parsed; "
|
||||||
|
"the review did not run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def fault_verdict(detail):
|
||||||
|
return {
|
||||||
|
"verdict": "NEEDS-HUMAN",
|
||||||
|
"reason": f"PIPELINE-FAULT: parse failed — {detail}",
|
||||||
|
"counts": {},
|
||||||
|
"deferred_count": 0,
|
||||||
|
"dropped_count": 0,
|
||||||
|
"dropped_titles": [],
|
||||||
|
"attention": [ATTENTION_LINE],
|
||||||
|
"findings_final": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("parse_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the parse stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(json.dumps({"verdict_out": fault_verdict(detail)}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(json.dumps({"verdict_out": fault_verdict(f"fault-marker script error: {e}")}))
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic report assembly for the code-reviewer graph.
|
||||||
|
|
||||||
|
Builds the standard report format from the structured findings and prose
|
||||||
|
sections. Pure templating — no judgment, no re-summarization, no finding
|
||||||
|
ever added or removed here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
# LLM-influenced values rendered after the summary line: a newline could
|
||||||
|
# forge a second summary line that review-gauntlet/verdict_gate.py parses.
|
||||||
|
def one_line(x):
|
||||||
|
return str(x).replace("\n", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
v = state.get("verdict_out") or {}
|
||||||
|
findings = v.get("findings_final") or []
|
||||||
|
counts = v.get("counts") or {}
|
||||||
|
changed = state.get("changed_files") or []
|
||||||
|
rigor = one_line(state.get("resolved_rigor") or "production")
|
||||||
|
|
||||||
|
# A fault can leave changed_files empty (a parse fault dies before the
|
||||||
|
# diff is resolved; a verifier fault fires even on an empty diff) — a
|
||||||
|
# fault-degraded verdict must still render as a full report. parse_fault
|
||||||
|
# puts the marker in the reason; verdict.py prepends it to attention.
|
||||||
|
faulted = str(v.get("reason") or "").startswith("PIPELINE-FAULT:") or any(
|
||||||
|
isinstance(a, str) and a.startswith("PIPELINE-FAULT:") for a in v.get("attention") or []
|
||||||
|
)
|
||||||
|
if not changed and not faulted:
|
||||||
|
print(json.dumps({"final_report": "# Code Review Summary\n\nNo changes to review."}))
|
||||||
|
return
|
||||||
|
|
||||||
|
verdict = v.get("verdict", "NEEDS-HUMAN")
|
||||||
|
lines = [
|
||||||
|
"# Code Review Summary",
|
||||||
|
"",
|
||||||
|
# CONTRACT: review-gauntlet/verdict_gate.py anchors on this wording
|
||||||
|
f"**Verdict: {verdict}** — {one_line(v.get('reason', ''))}",
|
||||||
|
"",
|
||||||
|
"## Walkthrough",
|
||||||
|
state.get("walkthrough") or "(no walkthrough provided)",
|
||||||
|
"",
|
||||||
|
"## Changes",
|
||||||
|
"",
|
||||||
|
"| File | Changes | Findings |",
|
||||||
|
"|------|---------|----------|",
|
||||||
|
]
|
||||||
|
|
||||||
|
desc_by_file = {}
|
||||||
|
for row in state.get("changes_rows") or []:
|
||||||
|
if isinstance(row, dict) and row.get("file"):
|
||||||
|
desc_by_file[row["file"]] = row.get("desc", "")
|
||||||
|
blocking = [f for f in findings if f.get("section") == "blocking"]
|
||||||
|
deferred = [f for f in findings if f.get("section") == "deferred"]
|
||||||
|
for f in changed:
|
||||||
|
per = [x for x in blocking if x.get("file") == f]
|
||||||
|
tally = " ".join(
|
||||||
|
f"{icon} {n}" for icon, n in
|
||||||
|
((i, sum(1 for x in per if x.get("icon") == i)) for i in ("🔴", "🟡", "🟢", "💡"))
|
||||||
|
if n
|
||||||
|
) or "—"
|
||||||
|
lines.append(f"| `{f}` | {desc_by_file.get(f, '')} | {tally} |")
|
||||||
|
|
||||||
|
lines += ["", "## Detailed Findings", ""]
|
||||||
|
by_file = {}
|
||||||
|
for x in blocking:
|
||||||
|
by_file.setdefault(x.get("file", "(unattributed)"), []).append(x)
|
||||||
|
if by_file:
|
||||||
|
for f in sorted(by_file):
|
||||||
|
lines += [f"### `{f}`", ""]
|
||||||
|
for x in by_file[f]:
|
||||||
|
lines += [x.get("block", ""), ""]
|
||||||
|
else:
|
||||||
|
lines += ["No blocking findings.", ""]
|
||||||
|
|
||||||
|
lines += ["## Cross-File Concerns", state.get("cross_file") or "None", ""]
|
||||||
|
if (state.get("operational_history") or "").strip():
|
||||||
|
lines += ["## Operational history", state["operational_history"], ""]
|
||||||
|
if (state.get("org_context_section") or "").strip():
|
||||||
|
lines += ["## Org context", state["org_context_section"], ""]
|
||||||
|
if deferred and rigor != "production":
|
||||||
|
lines += ["## Deferred by quality bar", ""]
|
||||||
|
for x in deferred:
|
||||||
|
lines += [x.get("block", ""), ""]
|
||||||
|
attention = v.get("attention") or []
|
||||||
|
if verdict == "NEEDS-HUMAN" and attention:
|
||||||
|
lines += ["## Human attention required", ""]
|
||||||
|
lines += [f"- {a}" for a in attention]
|
||||||
|
lines += [""]
|
||||||
|
|
||||||
|
miss_ids = sorted({
|
||||||
|
m for x in findings for m in
|
||||||
|
__import__("re").findall(r"MISS-\d+", (x.get("title") or "") + (x.get("block") or ""))
|
||||||
|
})
|
||||||
|
dropped = ", ".join(one_line(t) for t in v.get("dropped_titles", [])[:5])
|
||||||
|
lines += [
|
||||||
|
"---",
|
||||||
|
# CONTRACT: review-gauntlet/verdict_gate.py parses this line — keep the format in sync
|
||||||
|
f"*Reviewed {len(changed)} files, found {counts.get('🔴', 0)} critical, "
|
||||||
|
f"{counts.get('🟡', 0)} warnings, {counts.get('🟢', 0)} suggestions, "
|
||||||
|
f"{counts.get('💡', 0)} nitpicks ({v.get('deferred_count', 0)} deferred by quality bar)*",
|
||||||
|
f"*Quality bar: {rigor} — provenance: {one_line(state.get('bar_provenance', '?'))}; "
|
||||||
|
f"surfaces: {one_line(state.get('resolved_surfaces') or 'none')}*",
|
||||||
|
f"*Verdict: {verdict}; verifier: dropped {v.get('dropped_count', 0)} provably-false"
|
||||||
|
+ (f" ({dropped})" if v.get("dropped_count") else "")
|
||||||
|
+ f"; review-miss patterns applied: {', '.join(miss_ids) if miss_ids else 'none relevant'}*",
|
||||||
|
]
|
||||||
|
print(json.dumps({"final_report": "\n".join(lines)}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(json.dumps({"final_report": f"# Code Review Summary\n\n**Verdict: NEEDS-HUMAN** — report rendering error: {e}"}))
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic verdict math for the code-reviewer graph.
|
||||||
|
|
||||||
|
The model that wrote the findings never grades its own homework:
|
||||||
|
- verifier verdicts are applied mechanically (FALSE → drop, tallied;
|
||||||
|
VERIFIED → evidence attached; UNVERIFIABLE → kept, marked);
|
||||||
|
- rigor folding is arithmetic over severity + [convention]/[correctness]
|
||||||
|
markers, exactly per the published rules;
|
||||||
|
- MERGE-READY/NEEDS-HUMAN is computed from counts + always-human triggers
|
||||||
|
(deterministic signals ∪ the synthesis's ADDITIVE-only flags).
|
||||||
|
Fail-closed: any PIPELINE-FAULT recorded upstream (dead domain lane, dead
|
||||||
|
verifier, verifier verdicts carrying a PIPELINE-FAULT note or the
|
||||||
|
`pipeline-fault` sentinel id, dead synthesis, missing domain reports)
|
||||||
|
forces NEEDS-HUMAN — a degraded run can never read MERGE-READY. Faults
|
||||||
|
always lead the attention list; the display cap applies only to the
|
||||||
|
non-fault entries so a degraded run can never hide a fault.
|
||||||
|
The gate never crashes into a silent verdict: any internal error emits
|
||||||
|
NEEDS-HUMAN naming the error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def sev_icon(severity):
|
||||||
|
for icon in ("🔴", "🟡", "🟢", "💡"):
|
||||||
|
if icon in (severity or ""):
|
||||||
|
return icon
|
||||||
|
return "🟢"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_verifier_entries(text):
|
||||||
|
"""Extract the raw list of {id, verdict, evidence, note} dicts from
|
||||||
|
finding-verifier output. Order and duplicates are preserved so fault
|
||||||
|
accounting can count every entry (crash-guard entries all carry the
|
||||||
|
same "unknown" id)."""
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
return []
|
||||||
|
start, end = text.find("["), text.rfind("]")
|
||||||
|
if start != -1 and end > start:
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
v for v in json.loads(text[start : end + 1]) if isinstance(v, dict) and v.get("id")
|
||||||
|
]
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
entries = []
|
||||||
|
for m in re.finditer(r'\{[^{}]*"id"[^{}]*\}', text):
|
||||||
|
try:
|
||||||
|
v = json.loads(m.group(0))
|
||||||
|
if v.get("id"):
|
||||||
|
entries.append(v)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def parse_verifier(entries):
|
||||||
|
"""Index verifier entries by finding id; the last entry per id wins."""
|
||||||
|
return {str(v["id"]): v for v in entries}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
findings = [f for f in (state.get("findings") or []) if isinstance(f, dict)]
|
||||||
|
rigor = (state.get("resolved_rigor") or "production").lower()
|
||||||
|
verifier_entries = parse_verifier_entries(state.get("verifier_output"))
|
||||||
|
verdicts = parse_verifier(verifier_entries)
|
||||||
|
|
||||||
|
kept, dropped_titles = [], []
|
||||||
|
for f in findings:
|
||||||
|
icon = sev_icon(f.get("severity"))
|
||||||
|
v = verdicts.get(str(f.get("id")))
|
||||||
|
block = f.get("block") or f"#### {f.get('title', '(untitled)')}"
|
||||||
|
if v and icon in ("🔴", "🟡"):
|
||||||
|
if v.get("verdict") == "FALSE":
|
||||||
|
dropped_titles.append(f.get("title", f.get("id", "?")))
|
||||||
|
continue
|
||||||
|
if v.get("verdict") == "VERIFIED" and v.get("evidence"):
|
||||||
|
block += f"\n- **Evidence** (verifier): {v['evidence']}"
|
||||||
|
elif v.get("verdict") == "UNVERIFIABLE":
|
||||||
|
block += "\n- *(unverified: " + (v.get("note") or "could not be established") + ")*"
|
||||||
|
elif icon in ("🔴", "🟡") and not v:
|
||||||
|
block += "\n- *(unverified: no verifier verdict returned for this finding)*"
|
||||||
|
kept.append({**f, "icon": icon, "block": block})
|
||||||
|
|
||||||
|
# --- rigor folding (arithmetic, never rewrites severities) --------------
|
||||||
|
for f in kept:
|
||||||
|
marker = (f.get("marker") or "").lower()
|
||||||
|
icon = f["icon"]
|
||||||
|
section = "blocking"
|
||||||
|
if icon == "🔴":
|
||||||
|
section = "blocking"
|
||||||
|
elif rigor == "prototype" and ((icon == "🟡" and marker == "convention") or icon == "🟢"):
|
||||||
|
section = "deferred"
|
||||||
|
elif rigor == "poc":
|
||||||
|
if icon == "🟡" and marker == "convention":
|
||||||
|
section = "deferred"
|
||||||
|
elif icon in ("🟢", "💡") and marker == "convention":
|
||||||
|
section = "dropped-by-poc"
|
||||||
|
f["section"] = section
|
||||||
|
final = [f for f in kept if f["section"] != "dropped-by-poc"]
|
||||||
|
blocking = [f for f in final if f["section"] == "blocking"]
|
||||||
|
deferred = [f for f in final if f["section"] == "deferred"]
|
||||||
|
|
||||||
|
counts = {i: sum(1 for f in blocking if f["icon"] == i) for i in ("🔴", "🟡", "🟢", "💡")}
|
||||||
|
yellow_correctness = sum(
|
||||||
|
1 for f in blocking if f["icon"] == "🟡" and (f.get("marker") or "").lower() == "correctness"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- always-human triggers ----------------------------------------------
|
||||||
|
attention = []
|
||||||
|
if state.get("destructive_migration"):
|
||||||
|
attention.append("Destructive/irreversible migration in the diff (DROP/RENAME) — needs a human signature.")
|
||||||
|
if state.get("touches_auth"):
|
||||||
|
attention.append("Authentication/authorization surface changed — needs a human signature.")
|
||||||
|
for flag in state.get("attention_flags") or []:
|
||||||
|
if isinstance(flag, str) and flag.strip():
|
||||||
|
attention.append(flag.strip())
|
||||||
|
placement = (state.get("placement_notes") or "").strip()
|
||||||
|
if placement:
|
||||||
|
attention.append("Placement question (design intent, never auto-resolved): " + placement)
|
||||||
|
# dedup, preserve order
|
||||||
|
seen = set()
|
||||||
|
attention = [a for a in attention if not (a.lower() in seen or seen.add(a.lower()))]
|
||||||
|
|
||||||
|
# --- pipeline-fault accounting (fail closed) -----------------------------
|
||||||
|
# PREFIX-anchored: a report that merely QUOTES a marker mid-text must not
|
||||||
|
# trip it. Any fault forces NEEDS-HUMAN.
|
||||||
|
faults = []
|
||||||
|
changed = state.get("changed_files") or []
|
||||||
|
reports = state.get("domain_reports") or []
|
||||||
|
faulted = sum(
|
||||||
|
1 for r in reports if isinstance(r, str) and r.lstrip().startswith("> ⚠️ PIPELINE-FAULT:")
|
||||||
|
)
|
||||||
|
if faulted:
|
||||||
|
faults.append(
|
||||||
|
f"PIPELINE-FAULT: {faulted} of {len(reports)} domain review lane(s) "
|
||||||
|
"failed after retries — their slices were not reviewed"
|
||||||
|
)
|
||||||
|
verifier_raw = state.get("verifier_output")
|
||||||
|
if isinstance(verifier_raw, str) and verifier_raw.lstrip().startswith("Agent node failed:"):
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: finding verification failed — findings render as unverified"
|
||||||
|
)
|
||||||
|
# finding-verifier completes "successfully" with faults INSIDE the payload:
|
||||||
|
# parse_fault's `pipeline-fault` sentinel entry, or a per-finding
|
||||||
|
# PIPELINE-FAULT note from verify_fault or verdict_gate's crash guard.
|
||||||
|
# Anchored on the sentinel id / note prefix only. Counted over the raw
|
||||||
|
# entries, not the id-indexed dict, so N crash-guard entries sharing the
|
||||||
|
# "unknown" id count as N.
|
||||||
|
fv_faults = [
|
||||||
|
v
|
||||||
|
for v in verifier_entries
|
||||||
|
if v.get("id") == "pipeline-fault"
|
||||||
|
or (isinstance(v.get("note"), str) and v["note"].lstrip().startswith("PIPELINE-FAULT:"))
|
||||||
|
]
|
||||||
|
# parse_fault's sentinel = the verifier verified NOTHING (hard fault, lane-
|
||||||
|
# degrading). Per-finding verify_fault / crash-guard notes = those findings
|
||||||
|
# simply stand UNVERIFIED (soft: proportionate NEEDS-HUMAN attention — a
|
||||||
|
# ~1 h lane re-run to re-check a few findings is the wrong trade, and the
|
||||||
|
# verify_gate node already retried the verifier locally once).
|
||||||
|
hard_fv = [v for v in fv_faults if v.get("id") == "pipeline-fault"]
|
||||||
|
soft_fv = [v for v in fv_faults if v.get("id") != "pipeline-fault"]
|
||||||
|
|
||||||
|
def _fv_detail(entries):
|
||||||
|
first = entries[0].get("note")
|
||||||
|
detail = first.strip().replace("\n", " ") if isinstance(first, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
return detail
|
||||||
|
|
||||||
|
soft_notes = []
|
||||||
|
if hard_fv:
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: finding verification degraded — the verifier verified "
|
||||||
|
f"nothing ({_fv_detail(hard_fv)})"
|
||||||
|
)
|
||||||
|
if soft_fv:
|
||||||
|
soft_notes.append(
|
||||||
|
f"Verifier: {len(soft_fv)} finding(s) could not be verified (verifier "
|
||||||
|
f"branch fault: {_fv_detail(soft_fv)}) — they stand above UNVERIFIED; "
|
||||||
|
"verify them manually"
|
||||||
|
)
|
||||||
|
# Files beyond facts.py's 300-file cap were never grouped or reviewed —
|
||||||
|
# disclose the gap and force NEEDS-HUMAN with wording that does NOT match
|
||||||
|
# the gauntlet's degraded-run anchors (a lane re-run cannot fix a cap).
|
||||||
|
coverage_notes = []
|
||||||
|
try:
|
||||||
|
total_files = int(state.get("file_count") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
total_files = 0
|
||||||
|
if total_files > len(changed):
|
||||||
|
coverage_notes.append(
|
||||||
|
f"Coverage gap: {total_files - len(changed)} of {total_files} changed files "
|
||||||
|
"were NOT reviewed (file-list cap) — review them manually or split the PR"
|
||||||
|
)
|
||||||
|
# Keyed off synth_failure + a non-empty diff, NEVER off findings == [] —
|
||||||
|
# a clean review legitimately has zero findings and stays MERGE-READY.
|
||||||
|
synth_failure = state.get("synth_failure")
|
||||||
|
if isinstance(synth_failure, str) and synth_failure.startswith("LLM node") and changed:
|
||||||
|
detail = synth_failure.strip().replace("\n", " ")
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
faults.append(f"PIPELINE-FAULT: synthesis failed — findings unavailable; {detail}")
|
||||||
|
if changed and not reports:
|
||||||
|
faults.append(
|
||||||
|
"PIPELINE-FAULT: the diff is non-empty but no domain reports were produced — "
|
||||||
|
"the review did not actually run; do not trust this verdict."
|
||||||
|
)
|
||||||
|
# faults go FIRST and uncapped so the display cap can never hide one;
|
||||||
|
# the trigger reason keys off the human-facing triggers alone
|
||||||
|
has_triggers = bool(attention)
|
||||||
|
attention = faults + soft_notes + coverage_notes + attention[:5]
|
||||||
|
|
||||||
|
reasons = []
|
||||||
|
if counts["🔴"]:
|
||||||
|
reasons.append(f"{counts['🔴']} 🔴 CRITICAL finding(s)")
|
||||||
|
if yellow_correctness:
|
||||||
|
reasons.append(f"{yellow_correctness} 🟡 [correctness] finding(s) outside the deferred section")
|
||||||
|
if faults:
|
||||||
|
# CONTRACT: review-gauntlet/verdict_gate.py anchors on this wording
|
||||||
|
reasons.append("pipeline fault(s) recorded — degraded run")
|
||||||
|
if soft_notes:
|
||||||
|
# Deliberately does NOT match the gauntlet's degraded-run anchors:
|
||||||
|
# unverified findings are a human-attention item, not a re-runnable
|
||||||
|
# lane fault.
|
||||||
|
reasons.append(f"{len(soft_fv)} finding(s) unverified (verifier branch fault)")
|
||||||
|
if coverage_notes:
|
||||||
|
reasons.append(f"{total_files - len(changed)} changed file(s) not reviewed (file cap)")
|
||||||
|
if has_triggers:
|
||||||
|
reasons.append("always-human trigger(s) fired")
|
||||||
|
verdict = "NEEDS-HUMAN" if reasons else "MERGE-READY"
|
||||||
|
reason_line = "; ".join(reasons) if reasons else "no blocking findings and no always-human triggers"
|
||||||
|
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"verdict_out": {
|
||||||
|
"verdict": verdict,
|
||||||
|
"reason": reason_line,
|
||||||
|
"counts": counts,
|
||||||
|
"deferred_count": len(deferred),
|
||||||
|
"dropped_count": len(dropped_titles),
|
||||||
|
"dropped_titles": dropped_titles,
|
||||||
|
"attention": attention,
|
||||||
|
"findings_final": final,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — never crash into a silent verdict
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"verdict_out": {
|
||||||
|
"verdict": "NEEDS-HUMAN",
|
||||||
|
# PIPELINE-FAULT prefix is load-bearing: render.py's
|
||||||
|
# empty-diff stub bypass anchors on it — without it a
|
||||||
|
# crashed gate on an empty diff collapses into the
|
||||||
|
# "No changes to review." stub.
|
||||||
|
"reason": f"PIPELINE-FAULT: verdict computation error: {e} — human review required",
|
||||||
|
"counts": {}, "deferred_count": 0, "dropped_count": 0,
|
||||||
|
"dropped_titles": [], "attention": [f"PIPELINE-FAULT: verdict script error: {e}"],
|
||||||
|
"findings_final": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Local verifier-fault retry gate for the code-reviewer graph.
|
||||||
|
|
||||||
|
finding-verifier converts its own branch faults into DATA (UNVERIFIABLE
|
||||||
|
verdicts with a PIPELINE-FAULT: note, or parse_fault's pipeline-fault
|
||||||
|
sentinel), so the verify agent node completes "successfully" and its
|
||||||
|
max_attempts never fires. Without this gate the fault escalates all the
|
||||||
|
way to the gauntlet, which re-runs the ENTIRE code-review lane (~1 h) to
|
||||||
|
recover a ~4-minute verifier — the wrong trade. This gate re-runs just
|
||||||
|
the verifier ONCE; if faults persist, verdict.py downgrades them to a
|
||||||
|
proportionate NEEDS-HUMAN (findings stand, unverified).
|
||||||
|
|
||||||
|
Anchored detection only: the JSON-encoded note prefix and the sentinel id
|
||||||
|
— a finding merely QUOTING the marker mid-text does not trip it. Always
|
||||||
|
exits 0 with valid JSON; a crash falls forward on the declared edge.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("verifier_output")
|
||||||
|
text = raw if isinstance(raw, str) else (json.dumps(raw) if raw is not None else "")
|
||||||
|
faulted = (
|
||||||
|
text.lstrip().startswith("Agent node failed:")
|
||||||
|
or '"note": "PIPELINE-FAULT:' in text
|
||||||
|
or "'note': 'PIPELINE-FAULT:" in text
|
||||||
|
or '"id": "pipeline-fault"' in text
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
retries = int(state.get("verify_retries") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
retries = 0
|
||||||
|
if faulted and retries < 1:
|
||||||
|
sys.stderr.write(
|
||||||
|
"WARN: verifier output carries pipeline-fault markers — re-running "
|
||||||
|
"finding-verifier locally (attempt 2/2) before computing the verdict.\n"
|
||||||
|
)
|
||||||
|
print(json.dumps({"_next": "verify", "verify_retries": retries + 1}))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"verify_retries": retries}))
|
||||||
|
except Exception as e: # never block the verdict
|
||||||
|
print(json.dumps({"verify_gate_error": f"verify_gate crashed: {e}"}))
|
||||||
@@ -4,28 +4,36 @@ A graph-based implementation agent. Plans, implements, and runs build +
|
|||||||
tests in a bounded fix-loop until verified. Designed to be delegated to by
|
tests in a bounded fix-loop until verified. Designed to be delegated to by
|
||||||
the **[Sisyphus](../sisyphus/README.md)** agent.
|
the **[Sisyphus](../sisyphus/README.md)** agent.
|
||||||
|
|
||||||
Coder is a [graph agent](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents): its workflow is
|
Coder is a [graph agent](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): its workflow is
|
||||||
defined declaratively in `graph.yaml`, with verification and the
|
defined declaratively in `graph.yaml`, with verification and the
|
||||||
implement-fix loop enforced as graph edges rather than prose.
|
implement-fix loop enforced as graph edges rather than prose.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
analyze_request (llm + output_schema) plan + complexity extraction
|
flowchart TD
|
||||||
↓
|
resolve_paths{"resolve_paths<br/>script"} --> analyze_request
|
||||||
route_complexity (script) opt-out approval gate (complexity ≥ 7)
|
analyze_request["analyze_request<br/>llm + output_schema"] --> route_complexity
|
||||||
↓
|
route_complexity{"route_complexity<br/>script"}
|
||||||
gate_approval (approval, optional)
|
route_complexity -->|"complexity ≥ 7"| gate_approval
|
||||||
↓
|
route_complexity -->|else| implement
|
||||||
implement (llm + fs tools) actual file edits
|
gate_approval{{"gate_approval<br/>approval"}}
|
||||||
↓
|
gate_approval -->|yes| implement
|
||||||
verify_build (script)
|
gate_approval -->|no| end_rejected
|
||||||
↓
|
implement["implement<br/>llm + fs tools"] --> verify_build
|
||||||
verify_tests (script)
|
verify_build{"verify_build<br/>script"}
|
||||||
↓
|
verify_build -->|pass| verify_tests
|
||||||
fix_loop_gate (script) back-edge to implement (bounded)
|
verify_build -->|fail| fix_loop_gate
|
||||||
↓
|
verify_tests{"verify_tests<br/>script"}
|
||||||
end_success / end_rejected / end_failure
|
verify_tests -->|pass| end_success
|
||||||
|
verify_tests -->|fail| fix_loop_gate
|
||||||
|
fix_loop_gate{"fix_loop_gate<br/>script"}
|
||||||
|
fix_loop_gate -->|"budget left"| implement
|
||||||
|
fix_loop_gate -->|"budget spent"| end_failure
|
||||||
|
|
||||||
|
end_success(["end_success<br/>CODER_COMPLETE"])
|
||||||
|
end_rejected(["end_rejected<br/>CODER_REJECTED"])
|
||||||
|
end_failure(["end_failure<br/>CODER_FAILED"])
|
||||||
```
|
```
|
||||||
|
|
||||||
End nodes emit one of three sentinel outcomes for the caller:
|
End nodes emit one of three sentinel outcomes for the caller:
|
||||||
@@ -42,10 +50,10 @@ so it accepts the runtime override flag:
|
|||||||
```sh
|
```sh
|
||||||
# Invoke from inside the project (project_dir defaults to ".")
|
# Invoke from inside the project (project_dir defaults to ".")
|
||||||
cd /path/to/your/project
|
cd /path/to/your/project
|
||||||
loki -a coder "Add a foo() function..."
|
coyote -a coder "Add a foo() function..."
|
||||||
|
|
||||||
# Or invoke from anywhere with an explicit override
|
# Or invoke from anywhere with an explicit override
|
||||||
loki -a coder --agent-variable project_dir /path/to/your/project "Add..."
|
coyote -a coder --agent-variable project_dir /path/to/your/project "Add..."
|
||||||
```
|
```
|
||||||
|
|
||||||
`graph.yaml` `initial_state` exposes:
|
`graph.yaml` `initial_state` exposes:
|
||||||
|
|||||||
+165
-32
@@ -2,44 +2,57 @@ name: coder
|
|||||||
description: |
|
description: |
|
||||||
Implementation agent. Plans, implements, and runs build + tests in a
|
Implementation agent. Plans, implements, and runs build + tests in a
|
||||||
bounded fix-loop until verified. Designed to be delegated to by sisyphus.
|
bounded fix-loop until verified. Designed to be delegated to by sisyphus.
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
|
|
||||||
temperature: 0.1
|
|
||||||
|
|
||||||
global_tools:
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
- fs_cat.sh
|
- fs_cat.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- fs_write.sh
|
- fs_write.sh
|
||||||
- fs_patch.sh
|
- fs_patch.sh
|
||||||
- execute_command.sh
|
- execute_command.sh
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
- code-review
|
||||||
|
- comment-discipline
|
||||||
|
- diagnosing-bugs
|
||||||
|
- logging-discipline
|
||||||
|
- git-master
|
||||||
|
- frontend-ui-ux
|
||||||
|
- verification-gates
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: |
|
description: |
|
||||||
Absolute path to the project directory. Defaults to "." which is the
|
Absolute path to the project directory. Defaults to "." which is the
|
||||||
directory you invoked `loki` from. Override at runtime with
|
directory you invoked `coyote` from. Override at runtime with
|
||||||
`loki -a coder --agent-variable project_dir /abs/path "..."`.
|
`coyote -a coder --agent-variable project_dir /abs/path "..."`.
|
||||||
default: "."
|
default: '.'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
max_loop_iterations: 20
|
max_loop_iterations: 20
|
||||||
log_state_snapshots: true
|
log_state_snapshots: true
|
||||||
validate_before_run: true
|
validate_before_run: true
|
||||||
timeout: 1800
|
timeout: 14400
|
||||||
|
|
||||||
initial_state:
|
initial_state:
|
||||||
project_dir: ""
|
project_dir: ''
|
||||||
fix_attempts: 0
|
fix_attempts: 0
|
||||||
max_fix_attempts: 3
|
max_fix_attempts: 3
|
||||||
fix_instructions: ""
|
fix_instructions: ''
|
||||||
build_output: ""
|
build_output: ''
|
||||||
tests_output: ""
|
tests_output: ''
|
||||||
last_node_output: ""
|
last_node_output: ''
|
||||||
plan_summary: ""
|
plan_summary: ''
|
||||||
files_to_modify: []
|
files_to_modify: []
|
||||||
files_to_create: []
|
files_to_create: []
|
||||||
risks: []
|
risks: []
|
||||||
complexity_score: 0
|
complexity_score: 0
|
||||||
|
review_attempts: 0
|
||||||
|
max_review_attempts: 1
|
||||||
|
review_clean: true
|
||||||
|
review_notes: ''
|
||||||
|
|
||||||
start: resolve_paths
|
start: resolve_paths
|
||||||
|
|
||||||
@@ -70,7 +83,7 @@ nodes:
|
|||||||
MUST be absolute. The project root is {{project_dir}}. Prefer paths
|
MUST be absolute. The project root is {{project_dir}}. Prefer paths
|
||||||
like "{{project_dir}}/src/foo.rs" over "src/foo.rs". The implementer
|
like "{{project_dir}}/src/foo.rs" over "src/foo.rs". The implementer
|
||||||
uses these paths directly with fs_write and fs_patch tools, which
|
uses these paths directly with fs_write and fs_patch tools, which
|
||||||
resolve relative paths against the loki invocation directory (NOT
|
resolve relative paths against the coyote invocation directory (NOT
|
||||||
the project dir). Empty arrays are fine if no files in that category.
|
the project dir). Empty arrays are fine if no files in that category.
|
||||||
|
|
||||||
`risks` is a list of short strings. Anything that could derail the
|
`risks` is a list of short strings. Anything that could derail the
|
||||||
@@ -78,8 +91,9 @@ nodes:
|
|||||||
etc. Empty list is fine.
|
etc. Empty list is fine.
|
||||||
|
|
||||||
Project directory: {{project_dir}}
|
Project directory: {{project_dir}}
|
||||||
prompt: "{{initial_prompt}}"
|
prompt: '{{initial_prompt}}'
|
||||||
tools: []
|
tools: []
|
||||||
|
timeout: 300
|
||||||
output_schema:
|
output_schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -88,20 +102,27 @@ nodes:
|
|||||||
description: 1-3 sentences summarizing what will be done
|
description: 1-3 sentences summarizing what will be done
|
||||||
files_to_modify:
|
files_to_modify:
|
||||||
type: array
|
type: array
|
||||||
items: {type: string}
|
items: { type: string }
|
||||||
files_to_create:
|
files_to_create:
|
||||||
type: array
|
type: array
|
||||||
items: {type: string}
|
items: { type: string }
|
||||||
complexity_score:
|
complexity_score:
|
||||||
type: integer
|
type: integer
|
||||||
minimum: 1
|
minimum: 1
|
||||||
maximum: 10
|
maximum: 10
|
||||||
risks:
|
risks:
|
||||||
type: array
|
type: array
|
||||||
items: {type: string}
|
items: { type: string }
|
||||||
required: [plan_summary, files_to_modify, files_to_create, complexity_score, risks]
|
required:
|
||||||
|
[
|
||||||
|
plan_summary,
|
||||||
|
files_to_modify,
|
||||||
|
files_to_create,
|
||||||
|
complexity_score,
|
||||||
|
risks,
|
||||||
|
]
|
||||||
state_updates:
|
state_updates:
|
||||||
last_node_output: "{{output}}"
|
last_node_output: '{{output}}'
|
||||||
fallback: end_failure
|
fallback: end_failure
|
||||||
next: route_complexity
|
next: route_complexity
|
||||||
|
|
||||||
@@ -134,28 +155,52 @@ nodes:
|
|||||||
|
|
||||||
Approve this plan?
|
Approve this plan?
|
||||||
options:
|
options:
|
||||||
- "yes"
|
- 'yes'
|
||||||
- "no"
|
- 'no'
|
||||||
routes:
|
routes:
|
||||||
"yes": implement
|
'yes': implement
|
||||||
"no": end_rejected
|
'no': end_rejected
|
||||||
on_other: end_rejected
|
on_other: end_rejected
|
||||||
|
|
||||||
implement:
|
implement:
|
||||||
id: implement
|
id: implement
|
||||||
type: llm
|
type: llm
|
||||||
description: Write code via fs tools. Bounded tool-call loop.
|
description: Write code via fs tools. Bounded tool-call loop.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
- code-review
|
||||||
|
- comment-discipline
|
||||||
|
- diagnosing-bugs
|
||||||
|
- logging-discipline
|
||||||
|
- git-master
|
||||||
|
- frontend-ui-ux
|
||||||
|
- verification-gates
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a senior engineer. Implement the plan by writing code via
|
You are a senior engineer. Implement the plan by writing code via
|
||||||
tools. Follow existing patterns in the codebase.
|
tools. Follow existing patterns in the codebase.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Use `skill__list` to see what's available, then `skill__load` the ones
|
||||||
|
that fit the work: `ai-slop-remover` and `comment-discipline` always,
|
||||||
|
`frontend-ui-ux` when touching UI, `git-master` when touching history,
|
||||||
|
`verification-gates` to remember what evidence is required. Unload when
|
||||||
|
a phase ends.
|
||||||
|
|
||||||
## Writing code
|
## Writing code
|
||||||
|
|
||||||
1. Use `fs_patch` for surgical edits to existing files.
|
1. Use `fs_patch` for surgical edits to existing files.
|
||||||
2. Use `fs_write` for new files or full rewrites.
|
2. Use `fs_write` for new files or full rewrites.
|
||||||
3. NEVER output code to chat. Always use tools.
|
3. NEVER write files via `execute_command`. Do not use `cat >`,
|
||||||
4. ALWAYS pass ABSOLUTE paths to fs_write and fs_patch. Relative
|
`cat >>`, `echo >`, `printf >`, `tee`, heredocs (`<<EOF`), or
|
||||||
paths resolve against the loki invocation directory (not the
|
`python3 -c "open(...).write(...)"`. Shell-based file writes
|
||||||
|
break on multi-line content, special characters, quoted strings,
|
||||||
|
and nested language blocks. `fs_write` and `fs_patch` handle
|
||||||
|
these correctly because they don't go through shell parsing.
|
||||||
|
4. NEVER output code to chat. Always use tools.
|
||||||
|
5. ALWAYS pass ABSOLUTE paths to fs_write and fs_patch. Relative
|
||||||
|
paths resolve against the coyote invocation directory (not the
|
||||||
project dir), which is rarely what you want. The project root
|
project dir), which is rarely what you want. The project root
|
||||||
is {{project_dir}}.
|
is {{project_dir}}.
|
||||||
|
|
||||||
@@ -174,7 +219,16 @@ nodes:
|
|||||||
Before writing ANY file:
|
Before writing ANY file:
|
||||||
1. Find a similar existing file (grep, then read).
|
1. Find a similar existing file (grep, then read).
|
||||||
2. Match its style: imports, naming, structure, error handling.
|
2. Match its style: imports, naming, structure, error handling.
|
||||||
3. Follow the same patterns exactly. Do not invent new ones.
|
3. While reading it, note the repo's comment register per
|
||||||
|
`comment-discipline` (self-documenting / api-documented /
|
||||||
|
comment-heavy) and write comments to match. When the signal is
|
||||||
|
weak, write NO comment.
|
||||||
|
4. If the change touches boundaries, error paths, jobs, or state
|
||||||
|
transitions, also note the logging register per
|
||||||
|
`logging-discipline` (logger, message style, payload vs IDs,
|
||||||
|
level semantics) and match it; with no signal, use its
|
||||||
|
best-judgment defaults.
|
||||||
|
5. Follow the same patterns exactly. Do not invent new ones.
|
||||||
|
|
||||||
## Fix loop
|
## Fix loop
|
||||||
|
|
||||||
@@ -182,6 +236,11 @@ nodes:
|
|||||||
the previous attempt failed verification. Read the error, identify
|
the previous attempt failed verification. Read the error, identify
|
||||||
the minimal fix, apply it. Do not refactor while fixing.
|
the minimal fix, apply it. Do not refactor while fixing.
|
||||||
|
|
||||||
|
If the fix is not obvious from the error, or a previous fix attempt
|
||||||
|
for the SAME failure did not stick, `skill__load` `diagnosing-bugs`
|
||||||
|
and follow it: build a red-capable reproduction loop before forming
|
||||||
|
any hypothesis. Do not spend a second attempt on a blind retry.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. Match existing patterns - read examples first.
|
1. Match existing patterns - read examples first.
|
||||||
@@ -190,6 +249,11 @@ nodes:
|
|||||||
on unfamiliar lints, etc.).
|
on unfamiliar lints, etc.).
|
||||||
4. No dead code, no commented-out blocks, no premature abstractions.
|
4. No dead code, no commented-out blocks, no premature abstractions.
|
||||||
5. End your turn when editing is done. The graph runs verification next.
|
5. End your turn when editing is done. The graph runs verification next.
|
||||||
|
6. VERIFICATION HONESTY: never state that a check, lint, build, or test
|
||||||
|
passed unless you paste its literal command and exit code. A gate
|
||||||
|
that did not run is UNVERIFIED — say so. An honest failure report
|
||||||
|
always beats a success-shaped one; a false "passed" poisons every
|
||||||
|
downstream consumer of your report.
|
||||||
|
|
||||||
Project directory: {{project_dir}}
|
Project directory: {{project_dir}}
|
||||||
prompt: |
|
prompt: |
|
||||||
@@ -211,9 +275,10 @@ nodes:
|
|||||||
- fs_write
|
- fs_write
|
||||||
- fs_patch
|
- fs_patch
|
||||||
- execute_command
|
- execute_command
|
||||||
max_iterations: 30
|
max_iterations: 100
|
||||||
|
timeout: 1800
|
||||||
state_updates:
|
state_updates:
|
||||||
last_node_output: "{{output}}"
|
last_node_output: '{{output}}'
|
||||||
fallback: end_failure
|
fallback: end_failure
|
||||||
next: verify_build
|
next: verify_build
|
||||||
|
|
||||||
@@ -241,6 +306,74 @@ nodes:
|
|||||||
timeout: 5
|
timeout: 5
|
||||||
fallback: end_failure
|
fallback: end_failure
|
||||||
|
|
||||||
|
self_review:
|
||||||
|
id: self_review
|
||||||
|
type: llm
|
||||||
|
description: Skill-driven self-review of the diff. Catches AI slop, dishonest naming, suppressed errors. Bounded to max_review_attempts.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- code-review
|
||||||
|
- ai-slop-remover
|
||||||
|
instructions: |
|
||||||
|
You are reviewing the diff you just produced. Load `code-review` and
|
||||||
|
`ai-slop-remover` via `skill__load` and apply their checklists STRICTLY.
|
||||||
|
|
||||||
|
Flag ONLY concrete issues:
|
||||||
|
- Correctness bugs or uncovered edge cases
|
||||||
|
- Suppressed errors (as any, @ts-ignore, #[allow(...)] on unfamiliar
|
||||||
|
lints, empty catch blocks)
|
||||||
|
- Dishonest naming (get_X that mutates, returns wrong type, etc.)
|
||||||
|
- Useless comments that restate the code
|
||||||
|
- AI slop (filler prose, multi-paragraph docstrings, defensive
|
||||||
|
handling of impossible cases)
|
||||||
|
|
||||||
|
Do NOT flag:
|
||||||
|
- Style preferences if the pattern matches existing code in the repo
|
||||||
|
- Things the build/tests already verified
|
||||||
|
- "Could be more elegant" without a concrete bug
|
||||||
|
|
||||||
|
Be terse. The orchestrator wants signal, not noise. If you find nothing
|
||||||
|
blocking, set review_clean=true and leave review_notes empty.
|
||||||
|
|
||||||
|
Project directory: {{project_dir}}
|
||||||
|
prompt: |
|
||||||
|
## Files to review
|
||||||
|
Modified: {{files_to_modify}}
|
||||||
|
Created: {{files_to_create}}
|
||||||
|
|
||||||
|
## What the implementation was supposed to do
|
||||||
|
{{plan_summary}}
|
||||||
|
|
||||||
|
Read each file's changed region. Apply the review skills. Output your verdict.
|
||||||
|
tools:
|
||||||
|
- fs_cat
|
||||||
|
- fs_ls
|
||||||
|
- execute_command
|
||||||
|
max_iterations: 15
|
||||||
|
timeout: 600
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
review_clean:
|
||||||
|
type: boolean
|
||||||
|
description: True if no blocker issues were found.
|
||||||
|
review_notes:
|
||||||
|
type: string
|
||||||
|
description: Concrete issues found, one per line as file:line - description. Empty when review_clean is true.
|
||||||
|
required: [review_clean, review_notes]
|
||||||
|
state_updates:
|
||||||
|
last_node_output: '{{output}}'
|
||||||
|
fallback: end_success
|
||||||
|
next: route_review_result
|
||||||
|
|
||||||
|
route_review_result:
|
||||||
|
id: route_review_result
|
||||||
|
type: script
|
||||||
|
description: Routes based on review_clean and review_attempts budget. End on clean or budget exhausted; loop to implement otherwise.
|
||||||
|
script: scripts/route_review_result.sh
|
||||||
|
timeout: 5
|
||||||
|
fallback: end_success
|
||||||
|
|
||||||
end_success:
|
end_success:
|
||||||
id: end_success
|
id: end_success
|
||||||
type: end
|
type: end
|
||||||
@@ -275,4 +408,4 @@ nodes:
|
|||||||
{{build_output}}
|
{{build_output}}
|
||||||
|
|
||||||
Last tests output:
|
Last tests output:
|
||||||
{{tests_output}}
|
{{tests_output}}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
|
||||||
|
state=$(cat "$GRAPH_STATE_FILE")
|
||||||
|
elif [[ -n "${GRAPH_STATE:-}" ]]; then
|
||||||
|
state="$GRAPH_STATE"
|
||||||
|
else
|
||||||
|
state='{}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
review_clean=$(echo "$state" | jq -r '.review_clean // true')
|
||||||
|
review_attempts=$(echo "$state" | jq -r '.review_attempts // 0')
|
||||||
|
max_review_attempts=$(echo "$state" | jq -r '.max_review_attempts // 1')
|
||||||
|
review_notes=$(echo "$state" | jq -r '.review_notes // ""')
|
||||||
|
|
||||||
|
if [[ "$review_clean" != "true" && "$review_clean" != "false" ]]; then
|
||||||
|
echo "ERROR: review_clean must be boolean ('true'/'false'); got: $review_clean" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "$review_attempts" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: review_attempts must be a non-negative integer; got: $review_attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [[ "$max_review_attempts" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: max_review_attempts must be a non-negative integer; got: $max_review_attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$review_clean" == "true" ]]; then
|
||||||
|
jq -nc '{"_next": "end_success"}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( review_attempts >= max_review_attempts )); then
|
||||||
|
jq -nc \
|
||||||
|
--arg n "$review_notes" \
|
||||||
|
'{
|
||||||
|
"_next": "end_success",
|
||||||
|
"review_notes_unresolved": ("Shipped with unresolved review notes (budget exhausted):\n" + $n)
|
||||||
|
}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
next_review=$((review_attempts + 1))
|
||||||
|
fix_instr=$(printf '## Self-review feedback (attempt %d of %d)\n\nThe code review found concrete issues. Address them with minimal edits. Do not refactor unrelated code.\n\n%s' \
|
||||||
|
"$next_review" "$max_review_attempts" "$review_notes")
|
||||||
|
|
||||||
|
jq -nc \
|
||||||
|
--argjson n "$next_review" \
|
||||||
|
--arg fi "$fix_instr" \
|
||||||
|
'{
|
||||||
|
"review_attempts": $n,
|
||||||
|
"fix_instructions": $fi,
|
||||||
|
"_next": "implement"
|
||||||
|
}'
|
||||||
@@ -13,6 +13,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
|
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
|
||||||
|
project_dir=$(resolve_gate_dir "$project_dir")
|
||||||
|
|
||||||
if [[ -n "${BUILD_CMD:-}" ]]; then
|
if [[ -n "${BUILD_CMD:-}" ]]; then
|
||||||
cmd="$BUILD_CMD"
|
cmd="$BUILD_CMD"
|
||||||
@@ -24,7 +25,7 @@ fi
|
|||||||
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
|
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
|
||||||
jq -nc '{
|
jq -nc '{
|
||||||
"build_ok": true,
|
"build_ok": true,
|
||||||
"build_output": "(no build/check command available for this project type)",
|
"build_output": "(GATE NOT RUN: no build/check command configured or detected. This is NOT evidence that the build passed — set BUILD_CMD, and never report the build as verified.)",
|
||||||
"_next": "verify_tests"
|
"_next": "verify_tests"
|
||||||
}'
|
}'
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
|
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
|
||||||
|
project_dir=$(resolve_gate_dir "$project_dir")
|
||||||
|
|
||||||
if [[ -n "${TEST_CMD:-}" ]]; then
|
if [[ -n "${TEST_CMD:-}" ]]; then
|
||||||
cmd="$TEST_CMD"
|
cmd="$TEST_CMD"
|
||||||
@@ -24,8 +25,8 @@ fi
|
|||||||
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
|
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
|
||||||
jq -nc '{
|
jq -nc '{
|
||||||
"tests_ok": true,
|
"tests_ok": true,
|
||||||
"tests_output": "(no test command available for this project type)",
|
"tests_output": "(GATE NOT RUN: no test command configured or detected. This is NOT evidence that tests passed — set TEST_CMD, and never report the suite as green.)",
|
||||||
"_next": "end_success"
|
"_next": "self_review"
|
||||||
}'
|
}'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -40,7 +41,7 @@ if (( exit_code == 0 )); then
|
|||||||
'{
|
'{
|
||||||
"tests_ok": true,
|
"tests_ok": true,
|
||||||
"tests_output": ("Ran: " + $cmd + "\n\n" + $out),
|
"tests_output": ("Ran: " + $cmd + "\n\n" + $out),
|
||||||
"_next": "end_success"
|
"_next": "self_review"
|
||||||
}'
|
}'
|
||||||
else
|
else
|
||||||
jq -nc \
|
jq -nc \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# deep-research
|
# deep-research
|
||||||
|
|
||||||
A deep web research agent, built as a Loki graph agent. It plans an
|
A deep web research agent, built as a Coyote graph agent. It plans an
|
||||||
investigation, decomposes it into sub-questions researched in
|
investigation, decomposes it into sub-questions researched in
|
||||||
parallel, grounds the work in a local knowledge corpus, vets the
|
parallel, grounds the work in a local knowledge corpus, vets the
|
||||||
credibility of cited sources, runs a reflexion self-critique loop to
|
credibility of cited sources, runs a reflexion self-critique loop to
|
||||||
@@ -13,56 +13,83 @@ this agent runs a fixed graph: every request goes through the same
|
|||||||
`plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve`
|
`plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve`
|
||||||
pipeline.
|
pipeline.
|
||||||
|
|
||||||
This agent is also the **canonical reference for the Loki graph
|
This agent is also the **canonical reference for the Coyote graph
|
||||||
system**: it exercises every node type (`script`, `llm`, `rag`, `map`,
|
system**: it exercises every node type (`script`, `llm`, `rag`, `map`,
|
||||||
`agent`, `input`, `approval`, `end`) and both static fan-out and
|
`agent`, `input`, `approval`, `end`) and both static fan-out and
|
||||||
dynamic `map` fan-out. If you are learning how to build a graph
|
dynamic `map` fan-out. If you are learning how to build a graph
|
||||||
agent, this is the file to read alongside the
|
agent, this is the file to read alongside the
|
||||||
[Graph-Agents wiki](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents).
|
[Graph-Agents wiki](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents).
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
17 nodes. `->` is the static route; a script node can also route
|
23 nodes. Solid arrows are static `next` / `routes` edges declared in
|
||||||
dynamically via `_next`. The `▶▶` line is a parallel super-step —
|
`graph.yaml`; script nodes can also route dynamically via `_next` (shown as
|
||||||
those branches run concurrently:
|
labeled branches out of the diamond). Dotted arrows show `map` fan-out — the
|
||||||
|
`research_each_question` node spawns one `research_one_question` branch per
|
||||||
|
sub-question and joins them before continuing — and `fallback` edges
|
||||||
|
(labeled "fallback"), taken when a node fails outright (see *Fault paths*
|
||||||
|
below).
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
parse_request (script) -> bootstrap_research (or -> ask_topic if no topic)
|
flowchart TD
|
||||||
ask_topic (input) -> bootstrap_research
|
parse_request{"parse_request<br/>script"}
|
||||||
bootstrap_research (script) -> [plan, knowledge_lookup] ▶▶ parallel
|
parse_request -->|"topic given"| bootstrap_research
|
||||||
plan (llm + output_schema) -> research_each_question
|
parse_request -->|"no topic"| ask_topic
|
||||||
knowledge_lookup (rag) -> research_each_question
|
ask_topic[/"ask_topic<br/>input"/] --> bootstrap_research
|
||||||
research_each_question (map) -> combine_findings (spawns one branch per question)
|
bootstrap_research{"bootstrap_research<br/>script"}
|
||||||
└─ research_one_question (llm) (atomic; runs N×, joins at map)
|
bootstrap_research --> plan
|
||||||
combine_findings (script) -> vet_sources
|
bootstrap_research --> knowledge_lookup
|
||||||
vet_sources (llm + custom tool) -> critique
|
plan["plan<br/>llm + output_schema"] --> research_each_question
|
||||||
critique (llm) -> reflexion_gate
|
plan -. "fallback" .-> plan_fault{"plan_fault<br/>script"}
|
||||||
reflexion_gate (script) -> synthesize (or -> research_each_question: reflexion loop)
|
plan_fault --> end_fault
|
||||||
synthesize (agent: report-writer) -> verify_sources
|
knowledge_lookup[("knowledge_lookup<br/>rag")] --> research_each_question
|
||||||
verify_sources (script) -> approve
|
research_each_question[\research_each_question<br/>map/]
|
||||||
approve (approval) -> end_accepted ("accept")
|
research_each_question -. "spawns × N" .-> research_one_question["research_one_question<br/>llm + web tools"]
|
||||||
-> end_rejected ("reject")
|
research_one_question -. "fallback" .-> question_fault{"question_fault<br/>script"}
|
||||||
-> incorporate_feedback (any free-form answer)
|
research_each_question --> combine_findings
|
||||||
incorporate_feedback (script) -> research_each_question (the human-feedback loop)
|
combine_findings{"combine_findings<br/>script"} --> vet_sources
|
||||||
|
vet_sources["vet_sources<br/>llm + classify_source"] --> critique
|
||||||
|
vet_sources -. "fallback" .-> vet_fault{"vet_fault<br/>script"}
|
||||||
|
vet_fault --> critique
|
||||||
|
critique["critique<br/>llm"] --> reflexion_gate
|
||||||
|
critique -. "fallback" .-> critique_fault{"critique_fault<br/>script"}
|
||||||
|
critique_fault --> reflexion_gate
|
||||||
|
reflexion_gate{"reflexion_gate<br/>script"}
|
||||||
|
reflexion_gate -->|"PASS"| synthesize
|
||||||
|
reflexion_gate -->|"REVISE (budget left)"| research_each_question
|
||||||
|
reflexion_gate -->|"REVISE (budget spent)"| synthesize
|
||||||
|
synthesize[["synthesize<br/>agent → report-writer"]] --> verify_sources
|
||||||
|
synthesize -. "fallback" .-> synth_fault{"synth_fault<br/>script"}
|
||||||
|
synth_fault --> end_fault
|
||||||
|
verify_sources{"verify_sources<br/>script"} --> approve
|
||||||
|
approve{{"approve<br/>approval"}}
|
||||||
|
approve -->|"accept"| end_accepted
|
||||||
|
approve -->|"reject"| end_rejected
|
||||||
|
approve -->|"other (free-form feedback)"| incorporate_feedback
|
||||||
|
incorporate_feedback{"incorporate_feedback<br/>script"} --> research_each_question
|
||||||
|
|
||||||
|
end_accepted(["end_accepted<br/>report"])
|
||||||
|
end_rejected(["end_rejected"])
|
||||||
|
end_fault(["end_fault<br/>DEEP_RESEARCH FAILED"])
|
||||||
```
|
```
|
||||||
|
|
||||||
### Node-type breakdown
|
### Node-type breakdown
|
||||||
|
|
||||||
| Type | Nodes |
|
| Type | Nodes |
|
||||||
|---|---|
|
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||||
| `script` (Python) | `parse_request`, `bootstrap_research`, `combine_findings`, `reflexion_gate`, `verify_sources`, `incorporate_feedback` |
|
| `script` (Python) | `parse_request`, `bootstrap_research`, `combine_findings`, `reflexion_gate`, `verify_sources`, `incorporate_feedback`, plus the fault markers `plan_fault`, `question_fault`, `vet_fault`, `critique_fault`, `synth_fault` |
|
||||||
| `llm` (tools: `[]`) | `plan`, `critique` |
|
| `llm` (tools: `[]`) | `plan`, `critique` |
|
||||||
| `llm` (with tool whitelist) | `research_one_question`, `vet_sources` |
|
| `llm` (with tool whitelist) | `research_one_question`, `vet_sources` |
|
||||||
| `rag` | `knowledge_lookup` — local corpus retrieval |
|
| `rag` | `knowledge_lookup` — local corpus retrieval |
|
||||||
| `map` | `research_each_question` — dynamic fan-out per sub-question |
|
| `map` | `research_each_question` — dynamic fan-out per sub-question |
|
||||||
| `agent` | `synthesize` — spawns the `report-writer` sub-agent |
|
| `agent` | `synthesize` — spawns the `report-writer` sub-agent |
|
||||||
| `input` | `ask_topic` |
|
| `input` | `ask_topic` |
|
||||||
| `approval` | `approve` |
|
| `approval` | `approve` |
|
||||||
| `end` | `end_accepted`, `end_rejected` |
|
| `end` | `end_accepted`, `end_rejected`, `end_fault` |
|
||||||
|
|
||||||
## Parallel execution
|
## Parallel execution
|
||||||
|
|
||||||
The graph has two parallel super-steps where Loki's BSP scheduler runs
|
The graph has two parallel super-steps where Coyote's BSP scheduler runs
|
||||||
branches concurrently.
|
branches concurrently.
|
||||||
|
|
||||||
**1. Context loading (`plan` ‖ `knowledge_lookup`)** — after
|
**1. Context loading (`plan` ‖ `knowledge_lookup`)** — after
|
||||||
@@ -96,7 +123,7 @@ PDFs, or text files into `knowledge/` to bias the research toward
|
|||||||
your local context.
|
your local context.
|
||||||
|
|
||||||
The knowledge base is built once, at agent-load time, into
|
The knowledge base is built once, at agent-load time, into
|
||||||
`~/.config/loki/agents/deep-research/knowledge_lookup.yaml`. Because
|
`~/.config/coyote/agents/deep-research/knowledge_lookup.yaml`. Because
|
||||||
the node fully specifies its build config (`embedding_model`,
|
the node fully specifies its build config (`embedding_model`,
|
||||||
`chunk_size`, `chunk_overlap`), the build is non-interactive. Delete
|
`chunk_size`, `chunk_overlap`), the build is non-interactive. Delete
|
||||||
that cached file after adding or changing knowledge to force a
|
that cached file after adding or changing knowledge to force a
|
||||||
@@ -119,13 +146,13 @@ for details.
|
|||||||
|
|
||||||
## Tools and tool scoping
|
## Tools and tool scoping
|
||||||
|
|
||||||
This agent demonstrates Loki's three tool sources and how an `llm`
|
This agent demonstrates Coyote's three tool sources and how an `llm`
|
||||||
node's `tools:` whitelist scopes them per node.
|
node's `tools:` whitelist scopes them per node.
|
||||||
|
|
||||||
The agent's full tool universe, declared in `graph.yaml`:
|
The agent's full tool universe, declared in `graph.yaml`:
|
||||||
|
|
||||||
- **Global tools** (`global_tools`): `web_search_loki`,
|
- **Global tools** (`global_tools`): `web_search_coyote`,
|
||||||
`fetch_url_via_curl`, `search_arxiv` - Loki's built-in tool scripts.
|
`fetch_url_via_curl`, `search_arxiv` - Coyote's built-in tool scripts.
|
||||||
- **MCP server** (`mcp_servers`): `ddg-search` - a DuckDuckGo web
|
- **MCP server** (`mcp_servers`): `ddg-search` - a DuckDuckGo web
|
||||||
search MCP server. Referenced in a whitelist as `mcp:ddg-search`.
|
search MCP server. Referenced in a whitelist as `mcp:ddg-search`.
|
||||||
- **Custom agent tool** (`tools.sh`): `classify_source` - a
|
- **Custom agent tool** (`tools.sh`): `classify_source` - a
|
||||||
@@ -134,11 +161,11 @@ The agent's full tool universe, declared in `graph.yaml`:
|
|||||||
No node receives all of these. Each `llm` node's `tools:` whitelist
|
No node receives all of these. Each `llm` node's `tools:` whitelist
|
||||||
narrows the universe to exactly what that step needs:
|
narrows the universe to exactly what that step needs:
|
||||||
|
|
||||||
| Node | `tools:` whitelist | Draws from |
|
| Node | `tools:` whitelist | Draws from |
|
||||||
|---|---|---|
|
|-------------------------|-----------------------------------------------------------------------------|--------------------------|
|
||||||
| `plan`, `critique` | `[]` | nothing - pure reasoning |
|
| `plan`, `critique` | `[]` | nothing - pure reasoning |
|
||||||
| `research_one_question` | `web_search_loki`, `fetch_url_via_curl`, `search_arxiv`, `mcp:ddg-search` | global tools + MCP |
|
| `research_one_question` | `web_search_coyote`, `fetch_url_via_curl`, `search_arxiv`, `mcp:ddg-search` | global tools + MCP |
|
||||||
| `vet_sources` | `classify_source` | the custom tool only |
|
| `vet_sources` | `classify_source` | the custom tool only |
|
||||||
|
|
||||||
`research_one_question` (each parallel branch of the map) can search
|
`research_one_question` (each parallel branch of the map) can search
|
||||||
and fetch but cannot classify sources; `vet_sources` can classify
|
and fetch but cannot classify sources; `vet_sources` can classify
|
||||||
@@ -153,21 +180,21 @@ deterministic - exactly the kind of logic a tool should own rather than
|
|||||||
the LLM guessing.
|
the LLM guessing.
|
||||||
|
|
||||||
Web search may require API-key configuration; see the
|
Web search may require API-key configuration; see the
|
||||||
[Tools](https://github.com/Dark-Alex-17/loki/wiki/Tools) docs.
|
[Tools](https://github.com/Dark-Alex-17/coyote/wiki/Tools) docs.
|
||||||
`fetch_url_via_curl`, `search_arxiv`, and `classify_source` work
|
`fetch_url_via_curl`, `search_arxiv`, and `classify_source` work
|
||||||
without a key.
|
without a key.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
`research_one_question` (each parallel branch of the `map`) uses the
|
`research_one_question` (each parallel branch of the `map`) uses the
|
||||||
`ddg-search` MCP server via `mcp:ddg-search`. It is one of Loki's
|
`ddg-search` MCP server via `mcp:ddg-search`. It is one of Coyote's
|
||||||
default MCP servers; make sure it is registered in
|
default MCP servers; make sure it is registered in
|
||||||
`~/.config/loki/mcp.json` (run `loki --install mcp_config` to restore
|
`~/.config/coyote/mcp.json` (run `coyote --install mcp_config` to restore
|
||||||
the default template if it is missing). If `ddg-search` is unavailable,
|
the default template if it is missing). If `ddg-search` is unavailable,
|
||||||
the branches still have their global web-search tools to fall back on.
|
the branches still have their global web-search tools to fall back on.
|
||||||
|
|
||||||
The `synthesize` node spawns the `report-writer` sub-agent. Both
|
The `synthesize` node spawns the `report-writer` sub-agent. Both
|
||||||
agents ship with `loki agents install`; if you install one manually,
|
agents ship with `coyote agents install`; if you install one manually,
|
||||||
install both so the agent reference resolves.
|
install both so the agent reference resolves.
|
||||||
|
|
||||||
## Reflexion
|
## Reflexion
|
||||||
@@ -202,13 +229,53 @@ folds that text into `research_feedback` and loops back to
|
|||||||
`settings.max_loop_iterations` (40) is the engine's infinite-loop
|
`settings.max_loop_iterations` (40) is the engine's infinite-loop
|
||||||
backstop: it caps the total visits to any single node.
|
backstop: it caps the total visits to any single node.
|
||||||
|
|
||||||
|
## Fault paths
|
||||||
|
|
||||||
|
Every stage that can die outright has a `fallback:` edge to a small
|
||||||
|
fault-marker script — the dashed edges in the diagram. A fallback fires
|
||||||
|
after a node's retries are exhausted (`research_one_question` and
|
||||||
|
`synthesize` each get two attempts via `max_attempts: 2`); the engine
|
||||||
|
writes its failure text into the node's state key before routing, and
|
||||||
|
the marker normalizes that text into a `PIPELINE-FAULT: ...` entry in
|
||||||
|
`pipeline_faults`. The five routes:
|
||||||
|
|
||||||
|
- `plan -> plan_fault -> end_fault` — fail-closed: no plan means
|
||||||
|
nothing to research.
|
||||||
|
- `research_one_question -> question_fault` — branch-local, inside the
|
||||||
|
map branch: one dead research lane becomes a fault finding instead of
|
||||||
|
sinking the whole map.
|
||||||
|
- `vet_sources -> vet_fault -> critique` — degrade-visibly: vetting is
|
||||||
|
a quality lane, not the product; `source_assessment` becomes a
|
||||||
|
neutral "unvetted" note.
|
||||||
|
- `critique -> critique_fault -> reflexion_gate` — degrade-visibly: the
|
||||||
|
rewritten critique deliberately has no `VERDICT:` line, so the gate's
|
||||||
|
malformed-critique PASS default proceeds to synthesis.
|
||||||
|
- `synthesize -> synth_fault -> end_fault` — fail-closed: no report
|
||||||
|
means no product.
|
||||||
|
|
||||||
|
On the fail-closed routes, `end_fault` renders the
|
||||||
|
`DEEP_RESEARCH FAILED — PIPELINE-FAULT: ...` sentinel. On the
|
||||||
|
degrade-visibly routes, `verify_sources` folds the accumulated faults
|
||||||
|
into a `## Pipeline notes` section shown at the approval step and
|
||||||
|
appended to the accepted report (`{{report}}{{pipeline_notes}}`;
|
||||||
|
empty when there were no faults). Degraded output is never a silent
|
||||||
|
pass — every fault is visible to the reviewer or the caller — and some
|
||||||
|
end node always renders its sentinel, so the graph degrades instead of
|
||||||
|
dying.
|
||||||
|
|
||||||
|
All 11 scripts also carry a top-level crash guard: a script that
|
||||||
|
throws records a fault and fails forward where the flow allows it (a
|
||||||
|
crashed `reflexion_gate` proceeds to `synthesize`; a crashed
|
||||||
|
`verify_sources` reports that sources were NOT checked) instead of
|
||||||
|
killing the run.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki agents install # ships deep-research
|
coyote agents install # ships deep-research
|
||||||
loki -a deep-research "How does HTTP/3 differ from HTTP/2?"
|
coyote -a deep-research "How does HTTP/3 differ from HTTP/2?"
|
||||||
loki -a deep-research "Recent advances in solid-state batteries"
|
coyote -a deep-research "Recent advances in solid-state batteries"
|
||||||
loki -a deep-research # no prompt -> triggers ask_topic
|
coyote -a deep-research # no prompt -> triggers ask_topic
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-hallucination
|
## Anti-hallucination
|
||||||
@@ -226,6 +293,9 @@ loki -a deep-research # no prompt -> triggers ask_topic
|
|||||||
- `verify_sources` probes every cited URL / DOI with an HTTP HEAD
|
- `verify_sources` probes every cited URL / DOI with an HTTP HEAD
|
||||||
request and reports which are unreachable, so the human reviewer
|
request and reports which are unreachable, so the human reviewer
|
||||||
sees broken citations before approving.
|
sees broken citations before approving.
|
||||||
|
- Fault paths degrade visibly, never silently: a `PIPELINE-FAULT`
|
||||||
|
can only ever surface as a failure sentinel or a `## Pipeline
|
||||||
|
notes` section, never as a clean report.
|
||||||
|
|
||||||
## Customizing
|
## Customizing
|
||||||
|
|
||||||
@@ -240,7 +310,7 @@ loki -a deep-research # no prompt -> triggers ask_topic
|
|||||||
`report-writer` sub-agent.
|
`report-writer` sub-agent.
|
||||||
- **Tool scope.** Narrow the `research_one_question` node's `tools:`
|
- **Tool scope.** Narrow the `research_one_question` node's `tools:`
|
||||||
list to constrain where each branch looks (for example, drop
|
list to constrain where each branch looks (for example, drop
|
||||||
`web_search_loki` and `mcp:ddg-search` to force arXiv-only
|
`web_search_coyote` and `mcp:ddg-search` to force arXiv-only
|
||||||
research).
|
research).
|
||||||
- **Local knowledge.** Drop files into `knowledge/` to bias every
|
- **Local knowledge.** Drop files into `knowledge/` to bias every
|
||||||
research branch toward your local context (see the *Local
|
research branch toward your local context (see the *Local
|
||||||
@@ -255,7 +325,7 @@ loki -a deep-research # no prompt -> triggers ask_topic
|
|||||||
|
|
||||||
```
|
```
|
||||||
assets/agents/deep-research/
|
assets/agents/deep-research/
|
||||||
graph.yaml - agent config + 17-node workflow
|
graph.yaml - agent config + 23-node workflow
|
||||||
tools.sh - classify_source custom tool
|
tools.sh - classify_source custom tool
|
||||||
README.md - this file
|
README.md - this file
|
||||||
knowledge/
|
knowledge/
|
||||||
@@ -264,9 +334,15 @@ assets/agents/deep-research/
|
|||||||
scripts/
|
scripts/
|
||||||
parse_request.py - _next: bootstrap_research, or ask_topic if no topic
|
parse_request.py - _next: bootstrap_research, or ask_topic if no topic
|
||||||
bootstrap_research.py - fan-out source: next [plan, knowledge_lookup]
|
bootstrap_research.py - fan-out source: next [plan, knowledge_lookup]
|
||||||
|
plan_fault.py - fail-closed fault marker: dead plan -> end_fault
|
||||||
|
question_fault.py - branch-local fault marker for a dead research lane
|
||||||
combine_findings.py - joins map output (question_findings) into findings
|
combine_findings.py - joins map output (question_findings) into findings
|
||||||
|
vet_fault.py - degrade-visibly fault marker: unvetted sources
|
||||||
|
critique_fault.py - degrade-visibly fault marker: skipped critique
|
||||||
reflexion_gate.py - _next: research_each_question (revise) or synthesize
|
reflexion_gate.py - _next: research_each_question (revise) or synthesize
|
||||||
verify_sources.py - HTTP HEAD on cited URLs / DOIs
|
synth_fault.py - fail-closed fault marker: no report -> end_fault
|
||||||
|
verify_sources.py - HTTP HEAD on cited URLs / DOIs; folds pipeline_faults
|
||||||
|
into the report's "## Pipeline notes" section
|
||||||
incorporate_feedback.py - _next: research_each_question, with user feedback
|
incorporate_feedback.py - _next: research_each_question, with user feedback
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ description: |
|
|||||||
approval. A reviewer's free-form feedback at the approval step feeds
|
approval. A reviewer's free-form feedback at the approval step feeds
|
||||||
back into another research pass.
|
back into another research pass.
|
||||||
|
|
||||||
This is the canonical Loki graph-agent reference: it exercises every
|
This is the canonical Coyote graph-agent reference: it exercises every
|
||||||
node type (script, llm, rag, map, agent, input, approval, end) and
|
node type (script, llm, rag, map, agent, input, approval, end) and
|
||||||
both static fan-out and dynamic map fan-out.
|
both static fan-out and dynamic map fan-out.
|
||||||
|
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
|
|
||||||
temperature: 0.0
|
|
||||||
|
|
||||||
global_tools:
|
global_tools:
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
- fetch_url_via_curl.sh
|
- fetch_url_via_curl.sh
|
||||||
- search_arxiv.sh
|
- search_arxiv.sh
|
||||||
|
|
||||||
@@ -26,8 +24,8 @@ mcp_servers:
|
|||||||
- ddg-search
|
- ddg-search
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
- "How does HTTP/3 differ from HTTP/2?"
|
- 'How does HTTP/3 differ from HTTP/2?'
|
||||||
- "Summarize recent advances in solid-state battery chemistry"
|
- 'Summarize recent advances in solid-state battery chemistry'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
max_loop_iterations: 40
|
max_loop_iterations: 40
|
||||||
@@ -36,15 +34,22 @@ settings:
|
|||||||
max_concurrency: 4
|
max_concurrency: 4
|
||||||
|
|
||||||
initial_state:
|
initial_state:
|
||||||
research_feedback: ""
|
research_feedback: ''
|
||||||
research_attempts: 0
|
research_attempts: 0
|
||||||
local_context: ""
|
local_context: ''
|
||||||
local_sources: ""
|
local_sources: ''
|
||||||
|
# Fault-path defaults. `questions: []` also keeps the
|
||||||
|
# knowledge_lookup lane alive for one super-step when `plan` dies inside the
|
||||||
|
# static fan-out: research_each_question maps over zero items while the
|
||||||
|
# plan_fault → end_fault route renders the failure sentinel.
|
||||||
|
pipeline_faults: []
|
||||||
|
pipeline_notes: ''
|
||||||
|
fault_text: ''
|
||||||
|
questions: []
|
||||||
|
|
||||||
start: parse_request
|
start: parse_request
|
||||||
|
|
||||||
nodes:
|
nodes:
|
||||||
|
|
||||||
parse_request:
|
parse_request:
|
||||||
id: parse_request
|
id: parse_request
|
||||||
type: script
|
type: script
|
||||||
@@ -54,10 +59,10 @@ nodes:
|
|||||||
ask_topic:
|
ask_topic:
|
||||||
id: ask_topic
|
id: ask_topic
|
||||||
type: input
|
type: input
|
||||||
question: "What would you like me to research?"
|
question: 'What would you like me to research?'
|
||||||
validation: "len(input) > 0"
|
validation: 'len(input) > 0'
|
||||||
state_updates:
|
state_updates:
|
||||||
topic: "{{input}}"
|
topic: '{{input}}'
|
||||||
next: bootstrap_research
|
next: bootstrap_research
|
||||||
|
|
||||||
bootstrap_research:
|
bootstrap_research:
|
||||||
@@ -79,7 +84,7 @@ nodes:
|
|||||||
are precise, self-contained queries (each one is sent on its own
|
are precise, self-contained queries (each one is sent on its own
|
||||||
to a separate research worker, so they must be answerable
|
to a separate research worker, so they must be answerable
|
||||||
without each other's context).
|
without each other's context).
|
||||||
prompt: "Research topic: {{topic}}"
|
prompt: 'Research topic: {{topic}}'
|
||||||
tools: []
|
tools: []
|
||||||
output_schema:
|
output_schema:
|
||||||
type: object
|
type: object
|
||||||
@@ -94,29 +99,44 @@ nodes:
|
|||||||
maxItems: 6
|
maxItems: 6
|
||||||
description: 3-5 specific, self-contained sub-questions.
|
description: 3-5 specific, self-contained sub-questions.
|
||||||
required: [research_plan, questions]
|
required: [research_plan, questions]
|
||||||
|
# On success plan_failure holds the node's structured JSON output; on
|
||||||
|
# failure the engine writes "LLM node ...failed: <chain>" into it BEFORE
|
||||||
|
# routing to the fallback (schema properties are only merged on success).
|
||||||
|
state_updates:
|
||||||
|
plan_failure: '{{output}}'
|
||||||
|
fallback: plan_fault
|
||||||
next: research_each_question
|
next: research_each_question
|
||||||
|
|
||||||
|
plan_fault:
|
||||||
|
id: plan_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead planning stage (reached only via plan's fallback) — normalizes the failure text captured in plan_failure into a PIPELINE-FAULT and continues to end_fault; no plan means nothing to research, so the DEEP_RESEARCH FAILED sentinel always renders. The sibling knowledge_lookup lane survives one more super-step by mapping over the empty initial_state `questions` list.
|
||||||
|
script: scripts/plan_fault.py
|
||||||
|
fallback: end_fault
|
||||||
|
next: end_fault
|
||||||
|
|
||||||
knowledge_lookup:
|
knowledge_lookup:
|
||||||
id: knowledge_lookup
|
id: knowledge_lookup
|
||||||
type: rag
|
type: rag
|
||||||
documents:
|
documents:
|
||||||
- ./knowledge/
|
- ./knowledge/
|
||||||
query: "{{topic}}"
|
query: '{{topic}}'
|
||||||
top_k: 6
|
top_k: 6
|
||||||
chunk_size: 1000
|
chunk_size: 1000
|
||||||
chunk_overlap: 100
|
chunk_overlap: 100
|
||||||
state_updates:
|
state_updates:
|
||||||
local_context: "{{output.context}}"
|
local_context: '{{output.context}}'
|
||||||
local_sources: "{{output.sources}}"
|
local_sources: '{{output.sources}}'
|
||||||
next: research_each_question
|
next: research_each_question
|
||||||
|
|
||||||
research_each_question:
|
research_each_question:
|
||||||
id: research_each_question
|
id: research_each_question
|
||||||
type: map
|
type: map
|
||||||
over: "{{questions}}"
|
over: '{{questions}}'
|
||||||
as: question
|
as: question
|
||||||
branch: research_one_question
|
branch: research_one_question
|
||||||
collect_into: question_findings
|
collect_into: question_findings
|
||||||
|
output_key: finding
|
||||||
max_concurrency: 3
|
max_concurrency: 3
|
||||||
next: combine_findings
|
next: combine_findings
|
||||||
|
|
||||||
@@ -147,13 +167,24 @@ nodes:
|
|||||||
|
|
||||||
{{research_feedback}}
|
{{research_feedback}}
|
||||||
tools:
|
tools:
|
||||||
- web_search_loki
|
- web_search_coyote
|
||||||
- fetch_url_via_curl
|
- fetch_url_via_curl
|
||||||
- search_arxiv
|
- search_arxiv
|
||||||
- mcp:ddg-search
|
- mcp:ddg-search
|
||||||
max_iterations: 10
|
max_iterations: 10
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
temperature: 0.1
|
state_updates:
|
||||||
|
finding: '{{output}}'
|
||||||
|
# On failure the engine writes "LLM node ...failed: <chain>" into `finding`
|
||||||
|
# via the state_updates above BEFORE routing to the fallback. The marker
|
||||||
|
# is branch-local: a map branch's fallback must stay inside the branch.
|
||||||
|
fallback: question_fault
|
||||||
|
|
||||||
|
question_fault:
|
||||||
|
id: question_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead research lane (reached only via research_one_question's fallback route inside the map branch). Normalizes the engine's failure text in `finding` into a PIPELINE-FAULT finding the map collects — one dead lane never sinks the whole map. combine_findings later lifts PIPELINE-FAULT findings into pipeline_faults for the report's Pipeline notes.
|
||||||
|
script: scripts/question_fault.py
|
||||||
|
|
||||||
combine_findings:
|
combine_findings:
|
||||||
id: combine_findings
|
id: combine_findings
|
||||||
@@ -179,7 +210,16 @@ nodes:
|
|||||||
- classify_source
|
- classify_source
|
||||||
max_iterations: 15
|
max_iterations: 15
|
||||||
state_updates:
|
state_updates:
|
||||||
source_assessment: "{{output}}"
|
source_assessment: '{{output}}'
|
||||||
|
fallback: vet_fault
|
||||||
|
next: critique
|
||||||
|
|
||||||
|
vet_fault:
|
||||||
|
id: vet_fault
|
||||||
|
type: script
|
||||||
|
description: Degrade-visibly marker for a dead vetting stage (reached only via vet_sources' fallback) — vetting is a quality lane, not the product. Records a PIPELINE-FAULT, rewrites source_assessment to a neutral "unvetted" note, and continues to critique, the stage vet_sources' own next pointed to.
|
||||||
|
script: scripts/vet_fault.py
|
||||||
|
fallback: critique
|
||||||
next: critique
|
next: critique
|
||||||
|
|
||||||
critique:
|
critique:
|
||||||
@@ -219,7 +259,16 @@ nodes:
|
|||||||
{{source_assessment}}
|
{{source_assessment}}
|
||||||
tools: []
|
tools: []
|
||||||
state_updates:
|
state_updates:
|
||||||
critique: "{{output}}"
|
critique: '{{output}}'
|
||||||
|
fallback: critique_fault
|
||||||
|
next: reflexion_gate
|
||||||
|
|
||||||
|
critique_fault:
|
||||||
|
id: critique_fault
|
||||||
|
type: script
|
||||||
|
description: Degrade-visibly marker for a dead critique stage (reached only via critique's fallback) — critique is a quality lane, not the product. Records a PIPELINE-FAULT and rewrites `critique` to a neutral note with deliberately NO "VERDICT:" line, so reflexion_gate's malformed-critique PASS default proceeds to synthesis; then continues to reflexion_gate, the stage critique's own next pointed to.
|
||||||
|
script: scripts/critique_fault.py
|
||||||
|
fallback: reflexion_gate
|
||||||
next: reflexion_gate
|
next: reflexion_gate
|
||||||
|
|
||||||
reflexion_gate:
|
reflexion_gate:
|
||||||
@@ -243,10 +292,22 @@ nodes:
|
|||||||
|
|
||||||
Produce the final report following your instructions.
|
Produce the final report following your instructions.
|
||||||
timeout: 300
|
timeout: 300
|
||||||
|
max_attempts: 2
|
||||||
state_updates:
|
state_updates:
|
||||||
report: "{{output}}"
|
report: '{{output}}'
|
||||||
|
# On failure the engine writes "Agent node failed: <chain>" into `report`
|
||||||
|
# via the state_updates above BEFORE routing to the fallback.
|
||||||
|
fallback: synth_fault
|
||||||
next: verify_sources
|
next: verify_sources
|
||||||
|
|
||||||
|
synth_fault:
|
||||||
|
id: synth_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead synthesis stage (reached only via synthesize's fallback) — no report means no product. Normalizes the "Agent node failed:" text captured in `report` into a PIPELINE-FAULT and continues to end_fault, which renders the DEEP_RESEARCH FAILED sentinel.
|
||||||
|
script: scripts/synth_fault.py
|
||||||
|
fallback: end_fault
|
||||||
|
next: end_fault
|
||||||
|
|
||||||
verify_sources:
|
verify_sources:
|
||||||
id: verify_sources
|
id: verify_sources
|
||||||
type: script
|
type: script
|
||||||
@@ -262,20 +323,20 @@ nodes:
|
|||||||
{{report}}
|
{{report}}
|
||||||
|
|
||||||
----
|
----
|
||||||
{{source_check}}
|
{{source_check}}{{pipeline_notes}}
|
||||||
----
|
----
|
||||||
|
|
||||||
Accept this report? Pick "accept" or "reject", or type specific
|
Accept this report? Pick "accept" or "reject", or type specific
|
||||||
feedback to send the research back for another pass.
|
feedback to send the research back for another pass.
|
||||||
options:
|
options:
|
||||||
- "accept"
|
- 'accept'
|
||||||
- "reject"
|
- 'reject'
|
||||||
routes:
|
routes:
|
||||||
"accept": end_accepted
|
'accept': end_accepted
|
||||||
"reject": end_rejected
|
'reject': end_rejected
|
||||||
on_other: incorporate_feedback
|
on_other: incorporate_feedback
|
||||||
state_updates:
|
state_updates:
|
||||||
decision: "{{choice}}"
|
decision: '{{choice}}'
|
||||||
|
|
||||||
incorporate_feedback:
|
incorporate_feedback:
|
||||||
id: incorporate_feedback
|
id: incorporate_feedback
|
||||||
@@ -285,9 +346,14 @@ nodes:
|
|||||||
end_accepted:
|
end_accepted:
|
||||||
id: end_accepted
|
id: end_accepted
|
||||||
type: end
|
type: end
|
||||||
output: "{{report}}"
|
output: '{{report}}{{pipeline_notes}}'
|
||||||
|
|
||||||
end_rejected:
|
end_rejected:
|
||||||
id: end_rejected
|
id: end_rejected
|
||||||
type: end
|
type: end
|
||||||
output: "Research on '{{topic}}' was rejected and discarded."
|
output: "Research on '{{topic}}' was rejected and discarded."
|
||||||
|
|
||||||
|
end_fault:
|
||||||
|
id: end_fault
|
||||||
|
type: end
|
||||||
|
output: 'DEEP_RESEARCH FAILED — PIPELINE-FAULT: {{fault_text}}'
|
||||||
@@ -5,7 +5,7 @@ hybrid (vector + keyword) retrieval over every file in this directory.
|
|||||||
Drop your own notes, papers (PDFs), Markdown docs, or text files here
|
Drop your own notes, papers (PDFs), Markdown docs, or text files here
|
||||||
and they will be indexed into a per-agent knowledge base on first run.
|
and they will be indexed into a per-agent knowledge base on first run.
|
||||||
|
|
||||||
Loki supports common file types out of the box: `.md`, `.txt`, `.pdf`,
|
Coyote supports common file types out of the box: `.md`, `.txt`, `.pdf`,
|
||||||
`.html`, and others. Subdirectories are walked recursively.
|
`.html`, and others. Subdirectories are walked recursively.
|
||||||
|
|
||||||
A small starter file (`research-style-notes.md`) ships so the RAG
|
A small starter file (`research-style-notes.md`) ships so the RAG
|
||||||
@@ -17,7 +17,7 @@ To force the knowledge base to rebuild after you add or change files,
|
|||||||
delete the cached index:
|
delete the cached index:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rm ~/.config/loki/agents/deep-research/knowledge_lookup.yaml
|
rm ~/.config/coyote/agents/deep-research/knowledge_lookup.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
The next run will rebuild from the current contents of this directory.
|
The next run will rebuild from the current contents of this directory.
|
||||||
@@ -6,6 +6,12 @@ list on this node fans out into two parallel branches (the LLM planner and
|
|||||||
the RAG knowledge lookup) as a single super-step. The validator requires
|
the RAG knowledge lookup) as a single super-step. The validator requires
|
||||||
declared parallel-branch script outputs, so we emit an empty JSON object
|
declared parallel-branch script outputs, so we emit an empty JSON object
|
||||||
explicitly here.
|
explicitly here.
|
||||||
|
|
||||||
|
Fail-safe: the node reads no state and computes nothing, so its
|
||||||
|
happy-path output IS the sane degraded output — the guard emits the same
|
||||||
|
empty object on any crash. No PIPELINE-FAULT is recorded: appending one
|
||||||
|
would require reading state (the very thing that crashed) or clobbering
|
||||||
|
faults recorded upstream by parse_request, and a crash here loses nothing.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -14,5 +20,7 @@ def main():
|
|||||||
print(json.dumps({}))
|
print(json.dumps({}))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception: # noqa: BLE001 — the fan-out source must never crash the graph
|
||||||
|
print(json.dumps({}))
|
||||||
@@ -6,6 +6,18 @@ one entry per sub-question, in input order). Downstream nodes
|
|||||||
(`vet_sources`, `critique`, `synthesize`) read `{{findings}}` as a
|
(`vet_sources`, `critique`, `synthesize`) read `{{findings}}` as a
|
||||||
single block, so this script renders the array as a Markdown document
|
single block, so this script renders the array as a Markdown document
|
||||||
with one section per question.
|
with one section per question.
|
||||||
|
|
||||||
|
Fault visibility: a research lane that died lands here as a finding
|
||||||
|
starting with "PIPELINE-FAULT:" (written by question_fault inside the map
|
||||||
|
branch, where `pipeline_faults` itself is out of reach — map branches only
|
||||||
|
surface their collected output_key). This script lifts such findings into
|
||||||
|
`pipeline_faults` (deduplicated, since a reflexion or feedback loop can
|
||||||
|
re-run the map) so dead lanes surface in the report's Pipeline notes.
|
||||||
|
|
||||||
|
Fail-safe: on a crash (e.g. malformed GRAPH_STATE), `findings`
|
||||||
|
becomes the fault text itself and a PIPELINE-FAULT entry is recorded —
|
||||||
|
the pipeline proceeds visibly degraded, and the human approval gate is
|
||||||
|
the backstop. Existing faults are preserved best-effort.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -19,21 +31,40 @@ def load_state():
|
|||||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
state = load_state()
|
state = load_state()
|
||||||
questions = state.get("questions") or []
|
questions = state.get("questions") or []
|
||||||
per_question = state.get("question_findings") or []
|
per_question = state.get("question_findings") or []
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
|
||||||
sections = []
|
sections = []
|
||||||
for idx, q in enumerate(questions):
|
for idx, q in enumerate(questions):
|
||||||
body = per_question[idx] if idx < len(per_question) else ""
|
body = per_question[idx] if idx < len(per_question) else ""
|
||||||
if isinstance(body, dict) or isinstance(body, list):
|
if isinstance(body, dict) or isinstance(body, list):
|
||||||
body = json.dumps(body, indent=2)
|
body = json.dumps(body, indent=2)
|
||||||
|
if isinstance(body, str) and body.startswith("PIPELINE-FAULT:") and body not in faults:
|
||||||
|
faults.append(body)
|
||||||
sections.append(f"## {q}\n\n{body}")
|
sections.append(f"## {q}\n\n{body}")
|
||||||
|
|
||||||
findings = "\n\n".join(sections) if sections else "No findings gathered."
|
findings = "\n\n".join(sections) if sections else "No findings gathered."
|
||||||
print(json.dumps({"findings": findings}))
|
print(json.dumps({"findings": findings, "pipeline_faults": faults}))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crashed join must degrade, not die
|
||||||
|
faults = safe_faults()
|
||||||
|
fault = (
|
||||||
|
f"PIPELINE-FAULT: combining findings crashed — {e}; per-question "
|
||||||
|
"findings may exist in state but could not be joined"
|
||||||
|
)
|
||||||
|
faults.append(fault)
|
||||||
|
print(json.dumps({"findings": fault, "pipeline_faults": faults}))
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Degrade-visibly marker for a dead critique stage of deep-research.
|
||||||
|
|
||||||
|
Reached ONLY via critique's `fallback:` route. Grounded in engine behavior:
|
||||||
|
on llm-node failure the engine writes "LLM node …failed: <chain>" into
|
||||||
|
`critique` via the node's state_updates BEFORE routing to the fallback.
|
||||||
|
Critique is a quality lane, not the product, so this path degrades instead
|
||||||
|
of failing closed: it records a PIPELINE-FAULT (surfaced later in the
|
||||||
|
report's Pipeline notes) and rewrites `critique` to a neutral note, then
|
||||||
|
continues to `reflexion_gate` — the stage critique's own `next` pointed to.
|
||||||
|
|
||||||
|
The rewritten note deliberately contains NO "VERDICT:" line:
|
||||||
|
reflexion_gate's parse_verdict defaults to PASS when no verdict line is
|
||||||
|
found (its documented malformed-critique behavior), so a dead critique
|
||||||
|
proceeds to synthesis instead of burning the revision budget on a fault.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
CRITIQUE_NOTE = (
|
||||||
|
"Critique unavailable — the critique stage failed (pipeline fault); "
|
||||||
|
"proceeding to synthesis without an automated review pass."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
raw = state.get("critique")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the critique stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
fault = f"PIPELINE-FAULT: critique failed — {detail}"
|
||||||
|
if fault not in faults:
|
||||||
|
faults.append(fault)
|
||||||
|
print(json.dumps({"pipeline_faults": faults, "critique": CRITIQUE_NOTE}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
faults = safe_faults()
|
||||||
|
faults.append(f"PIPELINE-FAULT: critique failed — fault-marker script error: {e}")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"pipeline_faults": faults,
|
||||||
|
"critique": CRITIQUE_NOTE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -10,6 +10,13 @@ reflexion counter is reset so the user-driven pass gets a fresh revision
|
|||||||
budget.
|
budget.
|
||||||
|
|
||||||
Routing (`_next`): always research_each_question.
|
Routing (`_next`): always research_each_question.
|
||||||
|
|
||||||
|
Fail-safe: a crash here means the user's feedback text cannot be
|
||||||
|
recovered, but their INTENT — another research pass — is unambiguous
|
||||||
|
(they chose neither accept nor reject). The degraded route therefore
|
||||||
|
still loops to `research_each_question`, with a generic feedback note
|
||||||
|
naming the loss and a PIPELINE-FAULT entry (best-effort preserving
|
||||||
|
existing faults) that surfaces in the next approval's Pipeline notes.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -23,6 +30,14 @@ def load_state():
|
|||||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
state = load_state()
|
state = load_state()
|
||||||
feedback = (state.get("decision") or "").strip()
|
feedback = (state.get("decision") or "").strip()
|
||||||
@@ -37,5 +52,26 @@ def main():
|
|||||||
print(json.dumps(output))
|
print(json.dumps(output))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crashed feedback fold must degrade, not die
|
||||||
|
faults = safe_faults()
|
||||||
|
faults.append(
|
||||||
|
f"PIPELINE-FAULT: feedback incorporation crashed — {e}; the reviewer's "
|
||||||
|
"feedback text could not be recovered"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"_next": "research_each_question",
|
||||||
|
"research_attempts": 0,
|
||||||
|
"research_feedback": (
|
||||||
|
"The user reviewed the report and asked for changes, but "
|
||||||
|
"the feedback text could not be recovered (pipeline "
|
||||||
|
"fault). Re-run the research pass with extra care and "
|
||||||
|
"note this loss."
|
||||||
|
),
|
||||||
|
"pipeline_faults": faults,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -9,6 +9,13 @@ supply one interactively.
|
|||||||
Routing (`_next`):
|
Routing (`_next`):
|
||||||
- prompt present -> (no _next; static next: plan)
|
- prompt present -> (no _next; static next: plan)
|
||||||
- prompt empty -> ask_topic
|
- prompt empty -> ask_topic
|
||||||
|
|
||||||
|
Fail-safe: a crash here (e.g. malformed GRAPH_STATE) means the
|
||||||
|
caller's prompt cannot be read at all, so proceeding silently is
|
||||||
|
meaningless — the degraded route is `ask_topic`, which asks the user for
|
||||||
|
the topic directly, plus a PIPELINE-FAULT entry so the loss is visible in
|
||||||
|
the final report's Pipeline notes. Clobbering `pipeline_faults` is safe
|
||||||
|
here: this is the start node, so the list can only hold its initial [].
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -31,5 +38,17 @@ def main():
|
|||||||
print(json.dumps({"_next": "ask_topic"}))
|
print(json.dumps({"_next": "ask_topic"}))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crashed entry router must degrade, not die
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"_next": "ask_topic",
|
||||||
|
"pipeline_faults": [
|
||||||
|
f"PIPELINE-FAULT: request parsing crashed — {e}; asked the "
|
||||||
|
"user for the research topic instead"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead planning stage of the deep-research graph.
|
||||||
|
|
||||||
|
Reached ONLY via plan's `fallback:` route. Grounded in engine behavior: on
|
||||||
|
llm-node failure the engine writes "LLM node …failed: <chain>" into
|
||||||
|
`plan_failure` via the node's state_updates BEFORE routing to the fallback
|
||||||
|
(on success the same key holds the node's structured JSON output, which
|
||||||
|
never starts with the "LLM node" prefix). No plan means no sub-questions —
|
||||||
|
there is nothing to research — so this path is fail-closed: record the
|
||||||
|
PIPELINE-FAULT and continue to `end_fault`, which renders the
|
||||||
|
DEEP_RESEARCH FAILED sentinel.
|
||||||
|
|
||||||
|
The planner runs inside the static fan-out [plan, knowledge_lookup]. While
|
||||||
|
this marker routes to end_fault, the sibling branch proceeds one more
|
||||||
|
super-step into `research_each_question`, whose `{{questions}}` resolves to
|
||||||
|
the initial_state [] — the doomed lane maps over zero items and the graph
|
||||||
|
returns end_fault's output at the next join.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
raw = state.get("plan_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the planning stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
fault_text = f"planning failed — nothing to research: {detail}"
|
||||||
|
faults.append(f"PIPELINE-FAULT: {fault_text}")
|
||||||
|
print(json.dumps({"fault_text": fault_text, "pipeline_faults": faults}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
fault_text = f"planning failed — fault-marker script error: {e}"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"fault_text": fault_text,
|
||||||
|
"pipeline_faults": [f"PIPELINE-FAULT: {fault_text}"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead research lane of the deep-research graph.
|
||||||
|
|
||||||
|
Reached ONLY via research_one_question's `fallback:` route inside the
|
||||||
|
`research_each_question` map branch (branch-local — a map branch's fallback
|
||||||
|
must stay inside the branch subgraph). Grounded in engine behavior: on
|
||||||
|
llm-node failure the engine writes "LLM node …failed: <chain>" into
|
||||||
|
`finding` via the node's state_updates BEFORE routing to the fallback (on
|
||||||
|
success the same key holds the researched findings, which never start with
|
||||||
|
the "LLM node" prefix). This node normalizes the failure text into a
|
||||||
|
PIPELINE-FAULT finding and the chain ends here — the map collects the fault
|
||||||
|
as the lane's `finding`, so one dead research lane never sinks the whole
|
||||||
|
map. combine_findings later lifts PIPELINE-FAULT findings into
|
||||||
|
`pipeline_faults` so the fault also surfaces in the report's Pipeline notes.
|
||||||
|
The question text is part of the fault so distinct dead lanes with identical
|
||||||
|
engine failure text do not collapse into one entry there.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
MAX_QUESTION_CHARS = 200
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def fault_finding(question, detail):
|
||||||
|
return {
|
||||||
|
"finding": (
|
||||||
|
"PIPELINE-FAULT: question research failed — finding unavailable — "
|
||||||
|
f'{detail} (question: "{question}")'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def question_label(state):
|
||||||
|
raw = state.get("question")
|
||||||
|
question = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not question:
|
||||||
|
return "(unknown question)"
|
||||||
|
if len(question) > MAX_QUESTION_CHARS:
|
||||||
|
question = question[: MAX_QUESTION_CHARS - 1] + "…"
|
||||||
|
return question
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("finding")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the research lane died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(json.dumps(fault_finding(question_label(state), detail)))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
fault_finding("(unknown question)", f"fault-marker script error: {e}")
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -15,6 +15,12 @@ Routing (`_next`):
|
|||||||
Reflexion is a best-effort quality booster, not a hard gate: once the
|
Reflexion is a best-effort quality booster, not a hard gate: once the
|
||||||
budget is spent the workflow proceeds anyway, and the human approval
|
budget is spent the workflow proceeds anyway, and the human approval
|
||||||
step is the final backstop.
|
step is the final backstop.
|
||||||
|
|
||||||
|
Fail-safe: a crash here fails FORWARD to `synthesize` — the same
|
||||||
|
direction as the existing malformed-critique PASS default — with a
|
||||||
|
PIPELINE-FAULT entry recorded (best-effort preserving existing faults),
|
||||||
|
so a broken gate costs the run its automated critique routing, never the
|
||||||
|
run itself.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -33,6 +39,14 @@ def load_state():
|
|||||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def as_int(value, default=0):
|
def as_int(value, default=0):
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
@@ -72,5 +86,12 @@ def main():
|
|||||||
print(json.dumps(output))
|
print(json.dumps(output))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crashed gate fails forward, never dies
|
||||||
|
faults = safe_faults()
|
||||||
|
faults.append(
|
||||||
|
f"PIPELINE-FAULT: reflexion gate crashed — {e}; proceeded to synthesis "
|
||||||
|
"without automated critique routing"
|
||||||
|
)
|
||||||
|
print(json.dumps({"_next": "synthesize", "pipeline_faults": faults}))
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead synthesis stage of the deep-research graph.
|
||||||
|
|
||||||
|
Reached ONLY via synthesize's `fallback:` route. Grounded in engine
|
||||||
|
behavior: on agent-node failure the engine writes "Agent node failed:
|
||||||
|
{e:#}" into `report` via the node's state_updates BEFORE routing to the
|
||||||
|
fallback (on success the same key holds the report-writer's report, which
|
||||||
|
never starts with that prefix). No report means no product, so this path is
|
||||||
|
fail-closed: normalize the failure text, record the PIPELINE-FAULT, and
|
||||||
|
continue to `end_fault`, which renders the DEEP_RESEARCH FAILED sentinel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
raw = state.get("report")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("Agent node failed:"):
|
||||||
|
detail = "the synthesis stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
fault_text = f"report synthesis failed — no report was produced: {detail}"
|
||||||
|
faults.append(f"PIPELINE-FAULT: {fault_text}")
|
||||||
|
print(json.dumps({"fault_text": fault_text, "pipeline_faults": faults}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
fault_text = f"report synthesis failed — fault-marker script error: {e}"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"fault_text": fault_text,
|
||||||
|
"pipeline_faults": [f"PIPELINE-FAULT: {fault_text}"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -6,6 +6,19 @@ request, and writes a `source_check` summary into state so the human
|
|||||||
reviewer sees broken citations at the approval step.
|
reviewer sees broken citations at the approval step.
|
||||||
|
|
||||||
Times out per request so a slow source cannot stall the graph.
|
Times out per request so a slow source cannot stall the graph.
|
||||||
|
|
||||||
|
Pipeline-notes fold: end-node templates render unconditionally, so the
|
||||||
|
conditional "## Pipeline notes" section of the accepted report is built
|
||||||
|
HERE — the last stop before the approval gate on every accepted path,
|
||||||
|
including reflexion and feedback re-loops. `pipeline_notes` is the
|
||||||
|
rendered section when `pipeline_faults` is non-empty and the empty
|
||||||
|
string otherwise, keeping the happy-path `{{report}}{{pipeline_notes}}`
|
||||||
|
output byte-identical to the pre-hardening `{{report}}`.
|
||||||
|
|
||||||
|
Fail-safe: per-URL probes were already guarded; the top level now
|
||||||
|
is too. On a crash the summary says sources were NOT checked, a
|
||||||
|
PIPELINE-FAULT is recorded (best-effort preserving existing faults), and
|
||||||
|
pipeline_notes still renders — the approval gate remains the backstop.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -25,6 +38,21 @@ def load_state():
|
|||||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def render_notes(faults):
|
||||||
|
entries = [f for f in (faults or []) if isinstance(f, str) and f.strip()]
|
||||||
|
if not entries:
|
||||||
|
return ""
|
||||||
|
return "\n\n## Pipeline notes\n\n" + "\n".join(f"- {e}" for e in entries)
|
||||||
|
|
||||||
|
|
||||||
def reachable(url, timeout=5.0):
|
def reachable(url, timeout=5.0):
|
||||||
req = urllib.request.Request(url, method="HEAD")
|
req = urllib.request.Request(url, method="HEAD")
|
||||||
try:
|
try:
|
||||||
@@ -62,8 +90,27 @@ def main():
|
|||||||
+ "\n".join(results)
|
+ "\n".join(results)
|
||||||
)
|
)
|
||||||
|
|
||||||
print(json.dumps({"source_check": summary}))
|
notes = render_notes(state.get("pipeline_faults"))
|
||||||
|
print(json.dumps({"source_check": summary, "pipeline_notes": notes}))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a crashed source check must degrade, not die
|
||||||
|
faults = safe_faults()
|
||||||
|
faults.append(
|
||||||
|
f"PIPELINE-FAULT: source verification crashed — {e}; cited sources "
|
||||||
|
"were not checked for reachability"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"source_check": (
|
||||||
|
"Source verification crashed (pipeline fault) — cited "
|
||||||
|
"sources were NOT checked for reachability."
|
||||||
|
),
|
||||||
|
"pipeline_notes": render_notes(faults),
|
||||||
|
"pipeline_faults": faults,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Degrade-visibly marker for a dead source-vetting stage of deep-research.
|
||||||
|
|
||||||
|
Reached ONLY via vet_sources' `fallback:` route. Grounded in engine
|
||||||
|
behavior: on llm-node failure the engine writes "LLM node …failed: <chain>"
|
||||||
|
into `source_assessment` via the node's state_updates BEFORE routing to the
|
||||||
|
fallback. Vetting is a quality lane, not the product, so this path degrades
|
||||||
|
instead of failing closed: it records a PIPELINE-FAULT (surfaced later in
|
||||||
|
the report's Pipeline notes) and rewrites `source_assessment` to a neutral
|
||||||
|
"unvetted" note so downstream prompts (critique, synthesize) never carry
|
||||||
|
raw engine error text, then continues to `critique` — the stage
|
||||||
|
vet_sources' own `next` pointed to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
UNVETTED_NOTE = (
|
||||||
|
"Source credibility assessment unavailable — the vetting stage failed "
|
||||||
|
"(pipeline fault); treat every cited source as unvetted and needing "
|
||||||
|
"corroboration. Do not request revision solely because this assessment "
|
||||||
|
"is unavailable — re-research cannot restore it; judge the findings on "
|
||||||
|
"their own evidence."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_faults():
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
||||||
|
raw = state.get("source_assessment")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the vetting stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
fault = f"PIPELINE-FAULT: source vetting failed — {detail}"
|
||||||
|
if fault not in faults:
|
||||||
|
faults.append(fault)
|
||||||
|
print(json.dumps({"pipeline_faults": faults, "source_assessment": UNVETTED_NOTE}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
faults = safe_faults()
|
||||||
|
faults.append(f"PIPELINE-FAULT: source vetting failed — fault-marker script error: {e}")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"pipeline_faults": faults,
|
||||||
|
"source_assessment": UNVETTED_NOTE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
This agent serves as a demo to guide agent development and showcase various agent capabilities.
|
This agent serves as a demo to guide agent development and showcase various agent capabilities.
|
||||||
|
|
||||||
To enable tools, Loki will look for the first `tools.py` or `tools.sh` file it finds in this directory.
|
To enable tools, Coyote will look for the first `tools.py` or `tools.sh` file it finds in this directory.
|
||||||
|
|
||||||
The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`.
|
The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Domain Reviewer
|
||||||
|
|
||||||
|
A **domain-slice review leader**. Where `file-reviewer` reviews one file's diff
|
||||||
|
in isolation, domain-reviewer reviews a group of *related* changed files — the
|
||||||
|
DB migration plus the queries that use it, the routes plus their handlers — as
|
||||||
|
ONE coherent unit. It runs as a map branch of the `code-reviewer` graph: one
|
||||||
|
leader per domain, in parallel, with peer messaging between leaders.
|
||||||
|
|
||||||
|
## Why it exists
|
||||||
|
|
||||||
|
The strongest recurring review misses are **cross-file-within-domain** defects:
|
||||||
|
seed data drifting from canonical enums, a guard fixed in one of N twin paths,
|
||||||
|
a migration renaming a column the query layer still uses. Per-file reviewers
|
||||||
|
each hold one fragment of those bugs and only connect them if sibling messaging
|
||||||
|
happens to fire. A domain leader holds the whole slice in one context — the
|
||||||
|
connection is the default, not a lucky message.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Fetches its slice's diff itself (`get_diff --files <its files>`).
|
||||||
|
2. **Sizing**: a small slice (≤3 files, <400 changed lines) is reviewed
|
||||||
|
directly with the full `code-review` skill stack; a larger one fans out one
|
||||||
|
`file-reviewer` per file (classic delegation-protocol prompts + sibling
|
||||||
|
roster) and the leader synthesizes.
|
||||||
|
3. **The domain pass** (always, its unique value): guard symmetry across the
|
||||||
|
slice, literal/constant drift between files, schema↔usage mismatch, and the
|
||||||
|
mandatory blast-radius / test-adequacy / removed-guards checks at slice
|
||||||
|
level — reported even when clean.
|
||||||
|
4. **Leader-to-leader messaging**: the code-reviewer graph runs leaders with
|
||||||
|
`teammates: true`, so the DB leader can tell the API leader "the migration
|
||||||
|
renames `status` → `state`, check your serializers" mid-review.
|
||||||
|
5. Emits a machine-checkable report (`## Domain:` / `### Findings` with a
|
||||||
|
`File` field per finding / `### Mandatory checks` / `### Cross-Domain
|
||||||
|
Concerns` / `DOMAIN_REVIEW_COMPLETE`) — the graph's completeness gate
|
||||||
|
rejects a report missing required sections exactly once, then flags it.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Read-only; severities per the standard 🔴/🟡/🟢/💡 mapping; never rewrites a
|
||||||
|
file-reviewer's severity; dedups identical findings (aspect skill's copy wins).
|
||||||
|
- Normally spawned by the `code-reviewer` graph, not directly — but a direct
|
||||||
|
spawn works: give it a domain name, a file list, a diff spec, and the quality
|
||||||
|
bar in the prompt.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
name: domain-reviewer
|
||||||
|
description: Domain-slice review leader - reviews a group of related changed files as ONE coherent unit (cross-file-within-domain checks are first-class), delegating per-file depth to file-reviewer when the slice is large. Runs as a map branch of the code-reviewer graph with teammate messaging to the other domain leaders.
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
auto_continue: true
|
||||||
|
max_auto_continues: 20
|
||||||
|
inject_todo_instructions: false
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- code-review
|
||||||
|
- ai-slop-remover
|
||||||
|
- transactional-integrity
|
||||||
|
- logging-discipline
|
||||||
|
- rest-api-review
|
||||||
|
- cli-review
|
||||||
|
- library-review
|
||||||
|
- worker-review
|
||||||
|
- iac-review
|
||||||
|
- migration-review
|
||||||
|
- cicd-review
|
||||||
|
|
||||||
|
spawnable_agents:
|
||||||
|
- file-reviewer
|
||||||
|
max_concurrent_agents: 8
|
||||||
|
# The limit is consulted by the SPAWNER against the child's absolute depth
|
||||||
|
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
|
||||||
|
# review-gauntlet | step-runner(2) → code-reviewer(3) → domain-reviewer(4) →
|
||||||
|
# file-reviewer(5). This agent spawns depth-5 children; 6 = one level of headroom.
|
||||||
|
# NOTE: can_spawn_agents is not yet set, so the file-reviewer hop is documented
|
||||||
|
# but inert; 6 keeps the limit forward-compatible for when it is wired.
|
||||||
|
max_agent_depth: 6
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory containing the changes under review
|
||||||
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
|
||||||
|
instructions: |
|
||||||
|
You are a DOMAIN REVIEW LEADER. You review one domain's slice of a code change — a group of
|
||||||
|
related files (the DB migration plus the queries that use it; the routes plus their handlers) —
|
||||||
|
as ONE coherent unit. Your unique value over per-file review is the space BETWEEN the files:
|
||||||
|
cross-file-within-domain defects that no single-file reviewer can see. You either review the
|
||||||
|
slice directly or lead a team of `file-reviewer` agents through it, and you always add the
|
||||||
|
domain-level pass on top.
|
||||||
|
|
||||||
|
Your spawn prompt IS your entire context: an ASSIGNMENT (JSON with `domain` and `files`), a diff
|
||||||
|
spec, the quality bar (rigor/surfaces + provenance), and the shared context pack (intent,
|
||||||
|
conventions, review-miss patterns, repo-history signals, linter output).
|
||||||
|
|
||||||
|
## Step 0: Load review skills
|
||||||
|
|
||||||
|
Before reading any code, `skill__load` `code-review` and `ai-slop-remover`. Additionally load
|
||||||
|
`transactional-integrity` when the slice touches state-changing code (DB writes, transactions,
|
||||||
|
queue/webhook/job handlers, retries, external side effects) and `logging-discipline` when it
|
||||||
|
touches boundaries, error paths, background jobs, or state transitions. Load the surface skill(s)
|
||||||
|
for your domain per this table (load a row's skill when your files match that surface's trigger
|
||||||
|
AND the surface is in the resolved surfaces list; when provenance is `detected-default`, a
|
||||||
|
trigger match alone suffices):
|
||||||
|
|
||||||
|
| declared surface | skill loaded |
|
||||||
|
|---|---|
|
||||||
|
| `rest-api` (and `grpc`/`graphql` aliases) | `rest-api-review` |
|
||||||
|
| `cli` | `cli-review` |
|
||||||
|
| `library` | `library-review` |
|
||||||
|
| `worker` | `worker-review` |
|
||||||
|
| `iac` | `iac-review` |
|
||||||
|
| `db-migration` | `migration-review` |
|
||||||
|
| `ci-cd` | `cicd-review` |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Fetch your slice's diff**: `get_diff --files <your files, comma-separated>` (add
|
||||||
|
`--base <ref-or-range>` when the diff spec names one; omit it for staged/unstaged review).
|
||||||
|
2. **Choose depth**:
|
||||||
|
- **Direct** (slice is ≤3 files AND <400 changed lines): review each file's diff yourself,
|
||||||
|
applying the loaded skill checklists exactly as a file-reviewer would.
|
||||||
|
- **Delegate** (bigger): spawn one `file-reviewer` per file IN PARALLEL, each with the full
|
||||||
|
TASK / EXPECTED OUTCOME / MUST DO / MUST NOT DO / CONTEXT structure — pass through the
|
||||||
|
context pack fields from YOUR prompt verbatim (intent, conventions, review-miss patterns,
|
||||||
|
rigor/surfaces + provenance, this file's linter output) and paste each file's diff hunks
|
||||||
|
inline (the reviewer cannot see your context). Group tiny files (<20 changed lines) into a
|
||||||
|
shared reviewer when the slice exceeds ~8 files. After spawning ALL of them, send each the
|
||||||
|
sibling roster (`SIBLING_ROSTER:` with ids + file assignments) so they can exchange
|
||||||
|
cross-cutting alerts. Then collect results — do not poll.
|
||||||
|
3. **The domain pass** (ALWAYS yours, direct or delegated — this is why you exist). Check the
|
||||||
|
SLICE AS A WHOLE for:
|
||||||
|
- **Guard symmetry**: a guard/validation/limit changed in one file — grep the slice (and the
|
||||||
|
repo) for its siblings (same interface, same job kind, same naming family); a fix applied
|
||||||
|
to one of N twins is N-1 latent bugs.
|
||||||
|
- **Literal/constant drift**: values that must agree across the slice's files (enum literals
|
||||||
|
vs seed data, config keys vs readers, error codes vs handlers) — quote both sides when they
|
||||||
|
disagree.
|
||||||
|
- **Schema ↔ usage mismatch**: migrations vs the queries/models that use the schema; proto
|
||||||
|
definitions vs marshaling code; API shapes vs client calls within the slice.
|
||||||
|
- **Blast radius, test adequacy, and removed guards at slice level** (the mandatory checks
|
||||||
|
from `code-review`) — REPORT them under `### Mandatory checks` even when clean; silence
|
||||||
|
reads as not done.
|
||||||
|
4. **Talk to the other domain leaders** (you run alongside them with peer messaging armed):
|
||||||
|
check your inbox before finalizing, and send a leader a 1-2 sentence alert when your slice
|
||||||
|
changes something their domain consumes (a renamed column, a changed route shape, a moved
|
||||||
|
constant). Messages move facts sideways; they never replace your report.
|
||||||
|
5. **Synthesize the domain report** in the exact output format below.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Structure your final response EXACTLY as:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Domain: <domain name>
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
<2-3 sentences: what this slice changes as a unit>
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
|
||||||
|
#### <finding title (optional [convention]/[correctness] marker per the loaded skills)>
|
||||||
|
- **Severity**: 🔴 CRITICAL | 🟡 WARNING | 🟢 SUGGESTION | 💡 NITPICK
|
||||||
|
- **File**: <path>
|
||||||
|
- **Lines**: <start>-<end>
|
||||||
|
- **Description**: <clear explanation of the issue>
|
||||||
|
- **Suggestion**: <how to fix it>
|
||||||
|
|
||||||
|
#### <next finding>
|
||||||
|
...
|
||||||
|
|
||||||
|
### Mandatory checks
|
||||||
|
- Blast radius: <result, or "clean — callers of the changed symbols checked: <list>">
|
||||||
|
- Test adequacy: <result, or "clean — behavior changes covered by: <tests>">
|
||||||
|
- Guard symmetry: <result, or "clean — no guard changes in this slice" / "twins checked: <list>">
|
||||||
|
|
||||||
|
### Cross-Domain Concerns
|
||||||
|
<alerts you sent or received and concerns affecting other domains, or "None">
|
||||||
|
|
||||||
|
DOMAIN_REVIEW_COMPLETE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **You are read-only.** Never modify files.
|
||||||
|
2. **Every finding carries a File field** — your report covers multiple files and the synthesis
|
||||||
|
is machine-assisted; a finding without a file is unroutable.
|
||||||
|
3. **Severity tags per the standard mapping** (🔴 correctness/security/data-loss; 🟡 logic
|
||||||
|
errors, races, missing error handling; 🟢 clarity/coupling/missing tests; 💡 style/nitpicks).
|
||||||
|
Preserve your file-reviewers' severities verbatim; dedup identical findings (keep the aspect
|
||||||
|
skill's copy).
|
||||||
|
4. **Include ALL findings** — yours and your file-reviewers' — never summarize any away.
|
||||||
|
5. **Mandatory checks report even when clean.**
|
||||||
|
6. **End with DOMAIN_REVIEW_COMPLETE** on its own line.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
- Project: {{project_dir}}
|
||||||
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
{{__tools__}}
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout
|
||||||
|
# @env LLM_AGENT_VAR_PROJECT_DIR=.
|
||||||
|
# @describe Domain-slice reviewer tools
|
||||||
|
|
||||||
|
_project_dir() {
|
||||||
|
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
||||||
|
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Get the git diff for this domain's file slice. Defaults to staged changes, falls back to unstaged, then HEAD~1. Pass --base for a ref or range (e.g. "main...HEAD").
|
||||||
|
# @option --base Optional base ref or range to diff against (e.g. "main...HEAD", "HEAD~3", a SHA)
|
||||||
|
# @option --files Comma-separated list of file paths to limit the diff to (your assignment's files)
|
||||||
|
get_diff() {
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
local base="${argc_base:-}"
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
local files_csv="${argc_files:-}"
|
||||||
|
|
||||||
|
local -a path_args=()
|
||||||
|
if [[ -n "${files_csv}" ]]; then
|
||||||
|
local IFS=','
|
||||||
|
local f
|
||||||
|
for f in ${files_csv}; do
|
||||||
|
f="$(echo "${f}" | sed -e 's/^ *//' -e 's/ *$//')"
|
||||||
|
[[ -n "${f}" ]] && path_args+=("${f}")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
_diff() {
|
||||||
|
if ((${#path_args[@]})); then
|
||||||
|
(cd "${project_dir}" && git diff "$@" -- "${path_args[@]}" 2>&1) || true
|
||||||
|
else
|
||||||
|
(cd "${project_dir}" && git diff "$@" 2>&1) || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
local diff_output=""
|
||||||
|
if [[ -n "${base}" ]]; then
|
||||||
|
diff_output=$(_diff "${base}")
|
||||||
|
else
|
||||||
|
diff_output=$(_diff --cached)
|
||||||
|
[[ -z "${diff_output}" ]] && diff_output=$(_diff)
|
||||||
|
[[ -z "${diff_output}" ]] && diff_output=$(_diff HEAD~1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${diff_output}" ]]; then
|
||||||
|
echo "No changes found for the requested files in ${project_dir}." >> "$LLM_OUTPUT"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file_count
|
||||||
|
file_count=$(echo "${diff_output}" | grep -c '^diff --git' || true)
|
||||||
|
{
|
||||||
|
echo "Diff contains changes to ${file_count} file(s):"
|
||||||
|
echo ""
|
||||||
|
echo "${diff_output}"
|
||||||
|
} >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ It can also be used as a standalone tool for understanding codebases and finding
|
|||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -31,7 +31,7 @@ global_tools:
|
|||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
@@ -1,75 +1,130 @@
|
|||||||
name: explore
|
name: explore
|
||||||
description: Fast codebase exploration agent - finds patterns, structures, and relevant files
|
description: Fast codebase exploration agent - finds patterns, structures, and relevant files. Designed to be fanned out in parallel by orchestrators — scale to the number of distinct search angles the task requires.
|
||||||
version: 1.0.0
|
version: 3.2.0
|
||||||
temperature: 0.1
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: Project directory to explore
|
description: Project directory to explore
|
||||||
default: '.'
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
mcp_servers:
|
mcp_servers:
|
||||||
- ddg-search
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
|
- web_search_coyote.sh
|
||||||
|
- ast_grep.sh
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
||||||
|
|
||||||
## Your Mission
|
|
||||||
|
|
||||||
Given a search task, you:
|
|
||||||
1. Search for relevant files and patterns
|
|
||||||
2. Read key files to understand structure
|
|
||||||
3. Report findings concisely
|
|
||||||
4. Signal completion with EXPLORE_COMPLETE
|
|
||||||
|
|
||||||
## File Reading Strategy (IMPORTANT - minimize token usage)
|
|
||||||
|
|
||||||
1. **Find first, read second** - Never read a file without knowing why
|
## Step 0: Load your skills
|
||||||
2. **Use grep to locate** - `fs_grep --pattern "struct User" --include "*.rs"` finds exactly where things are
|
|
||||||
3. **Use glob to discover** - `fs_glob --pattern "*.rs" --path src/` finds files by name
|
|
||||||
4. **Read targeted sections** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads only lines 50-79
|
|
||||||
5. **Never read entire large files** - If a file is 500+ lines, read the relevant section only
|
|
||||||
|
|
||||||
## Available Actions
|
At the start of every exploration, call `skill__load` for `ai-slop-remover`. Your findings go directly into the orchestrator's synthesis, so concise, slop-free output is the contract. Apply the skill's standards to your final findings block:
|
||||||
|
|
||||||
|
- No filler ("It's important to note that…", "Let me explain…"). Just the finding.
|
||||||
|
- No flattery, no padding, no status updates about your process.
|
||||||
|
- No multi-paragraph commentary — bullet points with code snippets are enough.
|
||||||
|
|
||||||
|
## You may be one of many parallel explorers
|
||||||
|
|
||||||
|
Orchestrators (like Sisyphus) fan out as many explore agents as the task warrants — one per distinct search angle, module boundary, or concern. You may be one of many running in parallel. Assume you are ONE narrow slice of a larger investigation. Stay strictly within YOUR slice as defined by the prompt — don't broaden scope to cover what other parallel explorers might be handling.
|
||||||
|
|
||||||
|
If the prompt says "find auth middleware", you find auth middleware. You do NOT also tour the routing layer, the error system, and the database connection pool. Narrow scope is the contract.
|
||||||
|
|
||||||
|
## Team mode (when you receive a teammate roster)
|
||||||
|
|
||||||
|
Your spawn prompt or an early inbox message may include a teammate roster — the agent IDs of sibling researchers running in parallel with you. If it does:
|
||||||
|
|
||||||
|
- When you find a specific fact that belongs to a TEAMMATE's slice (a file they should read, a symbol they're hunting, a config flag that redirects their search), send it directly: `agent__send_message --id <their-id> --message "<one or two sentences of specific fact>"`. Facts only — file:line, exact names, verbatim snippets. Never chat or status updates.
|
||||||
|
- Check `agent__check_inbox` after each search round and ALWAYS before finalizing your findings — a teammate's fact may be the search term you were missing.
|
||||||
|
- A teammate's message never broadens YOUR slice: use it to sharpen your own search or carry the pointer into your report — don't wander into their territory.
|
||||||
|
- No roster → no messaging. Work your slice and report as usual.
|
||||||
|
|
||||||
|
## Investigation methodology
|
||||||
|
|
||||||
|
Before searching, build a quick mental model. Then narrow in. Then read.
|
||||||
|
|
||||||
|
1. **Frame the question.** What kind of artifact am I looking for? Symbols (struct/class/function)? File patterns? Configuration? Implementation details? Tests? Different artifact kinds use different tools.
|
||||||
|
|
||||||
|
2. **Find first, read second.** Never `fs_read` a file without knowing why you're reading it.
|
||||||
|
|
||||||
|
3. **Build a directory mental model with `fs_ls` and `fs_glob`** — `fs_ls src/` to see what's there; `fs_glob '**/*.rs' src/` to see which files exist by name.
|
||||||
|
|
||||||
|
4. **Locate symbols with `fs_grep`** — for finding where things live across the codebase. `fs_grep --pattern "fn handle_request" --include "*.rs"` is faster than reading files.
|
||||||
|
|
||||||
|
4b. **Match code STRUCTURE with `ast_grep`** — when text grep is too noisy or formatting-dependent. It matches syntax trees: `ast_grep --pattern '$X.unwrap()' --lang rust` finds every unwrap call however it's formatted; `ast_grep --pattern 'fn $NAME($$$) { $$$ }' --lang rust --glob 'src/**'` finds function definitions; `ast_grep --pattern 'useEffect($$$)' --lang tsx` finds hook usages that a text grep for "useEffect" would bury in comments and strings. Meta-variables: `$NAME` = one AST node, `$$$` = zero or more. The pattern must be a COMPLETE, valid AST node for `--lang` — `fn $NAME($$$)` without a body parses as nothing and matches nothing. Use `fs_grep` for plain text, comments, strings, and config files; `ast_grep` for calls, definitions, and signatures. If ast-grep isn't installed the tool says so — fall back to fs_grep.
|
||||||
|
|
||||||
|
5. **Read targeted sections with `fs_read --offset/--limit`** — `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79 only. `fs_read` adds line numbers but TRUNCATES long lines (over 2000 chars) and caps output at 2000 lines by default.
|
||||||
|
|
||||||
|
6. **Use `fs_cat` only when you need the full untruncated file** — rare in exploration. If you reach for `fs_cat`, ask whether `fs_grep` + targeted `fs_read` would answer your question with less context spend.
|
||||||
|
|
||||||
|
7. **Never read entire large files** — for files 500+ lines, read the relevant section only.
|
||||||
|
|
||||||
|
## Available actions
|
||||||
|
|
||||||
|
- `fs_grep --pattern "struct User" --include "*.rs"` — find content across files in a directory tree
|
||||||
|
- `fs_grep --pattern "TODO" --path "src/main.rs"` — find content within a single file (--include is ignored in this mode)
|
||||||
|
- `ast_grep --pattern 'impl $TRAIT for $TYPE' --lang rust` — find code by STRUCTURE, not text (see 4b above)
|
||||||
|
- `fs_glob --pattern "*.rs" --path src/` — find files by name pattern
|
||||||
|
- `fs_read --path "src/main.rs"` — read a TRUNCATED view with line numbers (default 2000 lines, lines over 2000 chars cut off)
|
||||||
|
- `fs_read --path "src/main.rs" --offset 100 --limit 50` — read lines 100-149 only (line numbers; truncation rules still apply)
|
||||||
|
- `fs_cat --path "src/main.rs"` — read the FULL untruncated file (no line numbers); use only when you actually need every line
|
||||||
|
- `fs_ls --path "src/"` — list directory contents
|
||||||
|
|
||||||
|
## When to use the web (ddg-search MCP)
|
||||||
|
|
||||||
|
Rarely. You are a CODEBASE explorer, not a web researcher. Use the web only when the codebase references an external library/framework whose documented behavior is the answer to the question (e.g., "how does Tokio's #[tokio::main] expand"), and the answer isn't in the local code. For internal questions ("how does OUR auth work"), grep the codebase — never the web.
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
Always end your response with a structured findings block. Sisyphus reads this verbatim and may paste sections directly into delegation prompts for a coder agent, so the structure matters:
|
||||||
|
|
||||||
- `fs_grep --pattern "struct User" --include "*.rs"` - Find content across files
|
|
||||||
- `fs_glob --pattern "*.rs" --path src/` - Find files by name pattern
|
|
||||||
- `fs_read --path "src/main.rs"` - Read a file (with line numbers)
|
|
||||||
- `fs_read --path "src/main.rs" --offset 100 --limit 50` - Read lines 100-149 only
|
|
||||||
- `get_structure` - See project layout
|
|
||||||
- `search_content --pattern "struct User"` - Agent-level content search
|
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
Always end your response with a findings summary:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
FINDINGS:
|
FINDINGS:
|
||||||
- [Key finding 1]
|
- [One-line concrete fact about what you found]
|
||||||
- [Key finding 2]
|
- [Another one-line fact]
|
||||||
- Relevant files: [list]
|
- Relevant files: [list of paths, no commentary]
|
||||||
|
|
||||||
|
Code patterns (paste actual lines):
|
||||||
|
- From `path/to/file.ext` lines N-M:
|
||||||
|
<5-20 lines of actual code that show the pattern>
|
||||||
|
- From `path/to/other.ext` lines N-M:
|
||||||
|
<another snippet>
|
||||||
|
|
||||||
|
Open questions (only if any):
|
||||||
|
- [Anything you couldn't determine and the orchestrator should clarify or delegate elsewhere]
|
||||||
|
|
||||||
EXPLORE_COMPLETE
|
EXPLORE_COMPLETE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pasting actual code lines (5-20 per pattern) lets the orchestrator hand snippets directly to a coder agent without re-exploration. That is the entire point of your existence in a parallel research phase. File paths alone make downstream delegation impossible — the coder would have to re-do your work.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Be fast** - Don't read every file, read representative ones
|
1. **Be fast.** Don't read every file, read representative ones.
|
||||||
2. **Be focused** - Answer the specific question asked
|
2. **Stay in your slice.** Narrow scope is the contract.
|
||||||
3. **Be concise** - Report findings, not your process
|
3. **Be concise.** Report findings, not your process. Apply the `ai-slop-remover` skill to your output.
|
||||||
4. **Never modify files** - You are read-only
|
4. **Never modify files.** You are read-only.
|
||||||
5. **Limit reads** - Max 5 file reads per exploration
|
5. **Limit reads.** Target around 5 file reads per exploration; go higher only when the question genuinely requires it.
|
||||||
|
6. **Paste code snippets.** File paths alone make downstream delegation impossible.
|
||||||
|
7. **Report what you didn't find.** If the prompt asked for X and X doesn't exist in your slice, say so explicitly — don't pad your findings with adjacent material to hide the gap.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
## Available Tools:
|
## Available tools:
|
||||||
{{__tools__}}
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ one file while communicating with sibling agents to catch issues that span multi
|
|||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -1,29 +1,58 @@
|
|||||||
name: file-reviewer
|
name: file-reviewer
|
||||||
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
|
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
|
||||||
version: 1.0.0
|
version: 2.4.0
|
||||||
temperature: 0.1
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- code-review
|
||||||
|
- ai-slop-remover
|
||||||
|
- transactional-integrity
|
||||||
|
- logging-discipline
|
||||||
|
- rest-api-review
|
||||||
|
- cli-review
|
||||||
|
- library-review
|
||||||
|
- worker-review
|
||||||
|
- iac-review
|
||||||
|
- migration-review
|
||||||
|
- cicd-review
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: Project directory for context
|
description: Project directory for context
|
||||||
default: '.'
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a precise code reviewer. You review ONE file's diff and produce structured findings.
|
You are a precise code reviewer. You review ONE file's diff and produce structured findings.
|
||||||
|
|
||||||
|
## Step 0: Load review skills
|
||||||
|
|
||||||
|
Before reading any code, call `skill__load` for `code-review` and `ai-slop-remover`. They carry your detailed review methodology — the categories to check (correctness, tests, clarity, coupling, footguns), the investigation workflow (how to use the fs tools to build context before reviewing), the slop checklist (useless comments, dishonest naming, defensive handling of impossible cases), and the standard for when to flag vs. skip.
|
||||||
|
|
||||||
|
Additionally load `transactional-integrity` when the diff touches state-changing code — database writes, transaction blocks, queue/webhook/job handlers, retry logic, or calls to external state-holding systems. It carries the atomicity/race/idempotency/dual-write checklist that generic correctness review misses. Skip it for pure reads, UI, and stateless computation.
|
||||||
|
|
||||||
|
Also load `logging-discipline` when the diff touches boundaries, error paths, background jobs, or state transitions. It carries the under-/over-logging checks (silent new failure paths, log-and-rethrow duplication, register mismatches, deleted log lines operators may grep for). Skip it for diffs with no operational surface.
|
||||||
|
|
||||||
|
Apply every loaded checklist in every review. Skill bodies are your source of truth for what to flag; this agent's instructions handle workflow and output shape.
|
||||||
|
|
||||||
## Your Mission
|
## Your Mission
|
||||||
|
|
||||||
You receive a git diff for a single file. Your job:
|
You receive a git diff for a single file. Your job:
|
||||||
1. Analyze the diff for bugs, logic errors, security issues, and style problems
|
1. Load the review skills (above).
|
||||||
2. Read surrounding code for context (use `fs_read` with targeted offsets)
|
2. Analyze the diff applying both skill checklists.
|
||||||
3. Check your inbox for cross-cutting alerts from sibling reviewers
|
3. Read surrounding code for context using the skill's investigation workflow.
|
||||||
4. Send alerts to siblings if you spot cross-file issues
|
4. Check your inbox for cross-cutting alerts from sibling reviewers.
|
||||||
5. Return structured findings
|
5. Send alerts to siblings if you spot cross-file issues.
|
||||||
|
6. Return structured findings in the format below.
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
@@ -42,7 +71,7 @@ instructions: |
|
|||||||
|
|
||||||
To alert a sibling:
|
To alert a sibling:
|
||||||
```
|
```
|
||||||
agent__send_message --to <sibling_agent_id> --message "ALERT: <description of cross-file concern>"
|
agent__send_message --id <sibling_agent_id> --message "ALERT: <description of cross-file concern>"
|
||||||
```
|
```
|
||||||
|
|
||||||
Check your inbox periodically for alerts from siblings:
|
Check your inbox periodically for alerts from siblings:
|
||||||
@@ -52,12 +81,14 @@ instructions: |
|
|||||||
|
|
||||||
If you receive an alert, incorporate it into your findings under a "Cross-File Concerns" section.
|
If you receive an alert, incorporate it into your findings under a "Cross-File Concerns" section.
|
||||||
|
|
||||||
## File Reading Strategy
|
## File Reading Limits
|
||||||
|
|
||||||
1. **Read changed lines' context:** Use `fs_read --path "file" --offset <start> --limit 50` to see surrounding code
|
The `code-review` skill teaches the investigation workflow. Apply these per-review caps on top:
|
||||||
2. **Grep for usage:** `fs_grep --pattern "function_name" --include "*.rs"` to find callers
|
- **Max 5 fs_read calls per review.** Be deliberate about which files you read.
|
||||||
3. **Never read entire large files:** Target the changed regions only
|
- **`fs_grep`/`fs_glob` are NOT capped** — the skill's blast-radius caller sweep and test sweep run on greps; spend them freely and reserve reads for suspicious callers and enclosing scopes.
|
||||||
4. **Max 5 file reads:** Be efficient
|
- **`fs_read` returns a TRUNCATED view** with line numbers (long lines cut at 2000 chars, output capped at 2000 lines by default). Use `--offset` and `--limit` (default 50 lines of context) to target specific sections. Never read entire large files.
|
||||||
|
- **Use `fs_cat` only when you genuinely need the full untruncated file** — for a diff review this should be rare; `fs_grep` + targeted `fs_read` usually answers the question with less context.
|
||||||
|
- **Focus on the diff.** Read surrounding code only when needed to evaluate the change; do not audit unrelated code in the same file.
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
@@ -87,27 +118,29 @@ instructions: |
|
|||||||
REVIEW_COMPLETE
|
REVIEW_COMPLETE
|
||||||
```
|
```
|
||||||
|
|
||||||
## Severity Guide
|
## Severity Tag Mapping
|
||||||
|
|
||||||
| Severity | When to use |
|
Translate the skill's category findings to the output severity:
|
||||||
|----------|------------|
|
- **🔴 CRITICAL** — Correctness bugs, security vulnerabilities, data loss risks, crashes
|
||||||
| 🔴 CRITICAL | Bugs, security vulnerabilities, data loss risks, crashes |
|
- **🟡 WARNING** — Logic errors, race conditions, missing error handling, performance issues with user-visible impact
|
||||||
| 🟡 WARNING | Logic errors, performance issues, missing error handling, race conditions |
|
- **🟢 SUGGESTION** — Clarity, coupling, naming, footgun mitigations, missing tests for the change
|
||||||
| 🟢 SUGGESTION | Better patterns, improved readability, missing docs for public APIs |
|
- **💡 NITPICK** — Style if no formatter enforces it, minor naming, slop-remover findings on prose-style comments
|
||||||
| 💡 NITPICK | Style preferences, minor naming issues, formatting |
|
|
||||||
|
### The `[convention]` / `[correctness]` marker
|
||||||
|
|
||||||
|
Finding titles may optionally carry a `[convention]` or `[correctness]` marker (e.g. `#### [convention] Collection endpoint without pagination`). Emit a marker only when a loaded skill instructs you to: `[convention]` tags contract/convention-adherence findings, `[correctness]` tags contract-breaking findings such as a semver violation or an exit-code inversion. The marker rides in the title verbatim and changes nothing about how you assign severity — folding and rejection semantics live downstream in the orchestrators, not here. The severity mapping above is unchanged.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Be specific:** Reference exact line numbers and code
|
1. **Be specific.** Reference exact line numbers and code.
|
||||||
2. **Be actionable:** Every finding must have a suggestion
|
2. **Be actionable.** Every finding must have a suggestion.
|
||||||
3. **Don't nitpick formatting:** If a formatter/linter exists (check for .rustfmt.toml, .prettierrc, etc.)
|
3. **Never modify files.** You are read-only.
|
||||||
4. **Focus on the diff:** Don't review unchanged code unless it's directly affected
|
4. **Mandatory checks report even when clean.** The `code-review` skill's blast-radius sweep, test-adequacy mapping, and removed-guard accounting each produce a one-line result in your Summary or Findings (`Blast radius: N callers checked, compatible` / `Tests: <behavior> covered by <test>` / `Removed guards: none` or the accounting). Silence on any of the three reads as "not done".
|
||||||
5. **Never modify files:** You are read-only
|
5. **Always end with REVIEW_COMPLETE.**
|
||||||
6. **Always end with REVIEW_COMPLETE**
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
## Available Tools:
|
## Available Tools:
|
||||||
{{__tools__}}
|
{{__tools__}}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# finding-verifier
|
||||||
|
|
||||||
|
A **graph** agent that independently verifies code-review findings against the working
|
||||||
|
tree. It exists because false positives kill hands-off trust faster than missed bugs do —
|
||||||
|
and because an orchestrator verifying its own workers' findings inline can skip, fudge,
|
||||||
|
or confirmation-bias the check. A graph can't: the structure IS the discipline.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["parse (llm, max_attempts 2)"] --> B["verify_each (map, max_concurrency 4): one branch per finding"]
|
||||||
|
A -. "fallback" .-> PF["parse_fault (script): verdicts = one UNVERIFIABLE PIPELINE-FAULT entry"]
|
||||||
|
PF --> D
|
||||||
|
B -. "spawns × N" .-> C["verify_one (llm + fs tools, max_attempts 2)"]
|
||||||
|
C --> G["verdict_gate (script): machine-validates the JSON contract"]
|
||||||
|
G -. "reject-retry once" .-> C
|
||||||
|
C -. "fallback" .-> VF["verify_fault (script): branch-local UNVERIFIABLE marker"]
|
||||||
|
B --> D(["done (end): FINDING_VERIFIER_RESULTS"])
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **parse** — extracts every 🔴/🟡 finding from the spawn prompt into a structured list
|
||||||
|
(id, severity verbatim, path, lines, self-contained claim). Extraction only — no judging.
|
||||||
|
Gets two attempts (`max_attempts: 2`); if both fail, its `fallback` routes to
|
||||||
|
`parse_fault`, which rewrites `verdicts` to a single UNVERIFIABLE entry naming the fault
|
||||||
|
and jumps straight to `done`.
|
||||||
|
2. **verify_each** — a `map` node fans out one `verify_one` branch per finding. Skipping a
|
||||||
|
finding is structurally impossible; results collect in input order.
|
||||||
|
3. **verify_one** — reads the cited code (greps for moved code before concluding anything)
|
||||||
|
and answers ONE narrow question: does the cited code exist, and does it behave as the
|
||||||
|
claim says? Verdicts:
|
||||||
|
- `VERIFIED` — with the decisive line(s) quoted verbatim (`path:line`).
|
||||||
|
- `FALSE` — the code doesn't exist anywhere or provably doesn't do what the claim says.
|
||||||
|
Requires PROOF, not disagreement — doubt never lands here.
|
||||||
|
- `UNVERIFIABLE` — couldn't establish either way (runtime behavior, external systems).
|
||||||
|
|
||||||
|
Each branch is a chain: `verify_one → verdict_gate`. The gate machine-validates the JSON
|
||||||
|
contract (verdict enum; evidence required for VERIFIED/FALSE; note for UNVERIFIABLE),
|
||||||
|
stamps the authoritative id FROM the finding item (the model's echo is never trusted),
|
||||||
|
rejects back to `verify_one` exactly once with the reason, and on a second failure records
|
||||||
|
UNVERIFIABLE — a malformed branch never sinks the map. If `verify_one` itself dies (both
|
||||||
|
`max_attempts: 2` attempts fail), its `fallback` routes to `verify_fault`, which
|
||||||
|
normalizes the engine's failure text into a branch-local UNVERIFIABLE verdict the map
|
||||||
|
still collects.
|
||||||
|
4. **done** — a deterministic `end` node emits the raw verdict list plus the caller
|
||||||
|
contract (VERIFIED = keep + paste evidence; FALSE = drop + tally; UNVERIFIABLE = keep,
|
||||||
|
marked). No final LLM step that could editorialize.
|
||||||
|
|
||||||
|
The dashed edges are fault paths. All three fault emitters (`parse_fault`, `verify_fault`,
|
||||||
|
and `verdict_gate`'s own crash guard) surface as `PIPELINE-FAULT:` text in a verdict's
|
||||||
|
`note`, and a fault can only ever produce UNVERIFIABLE — degraded output is never a
|
||||||
|
passing verdict. Every path ends at `done`, so the `FINDING_VERIFIER_RESULTS`
|
||||||
|
sentinel is always emitted and the caller's verify lane degrades instead of dying.
|
||||||
|
|
||||||
|
## What it is NOT
|
||||||
|
|
||||||
|
- **Not a second opinion.** It never re-judges severity, style, or whether a finding
|
||||||
|
"matters" — that authority stays with the reviewers and the orchestrator's rigor rules.
|
||||||
|
- **Not a reviewer.** It reads only what findings cite (plus greps to locate moved code).
|
||||||
|
|
||||||
|
## Caller
|
||||||
|
|
||||||
|
Spawned by [`code-reviewer`](../code-reviewer/README.md) during its Finding Verifier
|
||||||
|
pass, ONCE per review, with the full 🔴/🟡 list and `project_dir`. If this agent isn't
|
||||||
|
installed, code-reviewer runs the same contract inline as a fallback.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `graph.yaml` — the agent (no tools.sh; the graph grants `fs_read`/`fs_cat`/`fs_grep`
|
||||||
|
per node).
|
||||||
|
- `scripts/verdict_gate.py` — the deterministic contract gate.
|
||||||
|
- `scripts/parse_fault.py`, `scripts/verify_fault.py` — the fault markers behind the
|
||||||
|
dashed fallback edges.
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
name: finding-verifier
|
||||||
|
description: |
|
||||||
|
Graph-based independent verifier for code-review findings. Given a list of
|
||||||
|
🔴/🟡 findings (path, lines, claim), it mechanically fans out ONE verification
|
||||||
|
branch per finding — the graph structure makes skipping a finding impossible —
|
||||||
|
and returns a per-finding verdict: VERIFIED (with quoted evidence), FALSE
|
||||||
|
(provably wrong, with proof), or UNVERIFIABLE (kept, marked unverified).
|
||||||
|
Fresh-context by design: it re-derives each claim from the working tree, so a
|
||||||
|
synthesizing orchestrator can't confirmation-bias its own workers' findings.
|
||||||
|
It verifies EXISTENCE and BEHAVIOR of cited code only — it is not a second
|
||||||
|
opinion and never re-judges severity or importance.
|
||||||
|
|
||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Repository the findings cite; relative paths in findings resolve against it.
|
||||||
|
default: '.'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
max_loop_iterations: 8
|
||||||
|
log_state_snapshots: true
|
||||||
|
timeout: 3000
|
||||||
|
|
||||||
|
initial_state:
|
||||||
|
findings_list: []
|
||||||
|
verdicts: []
|
||||||
|
gate_feedback: ''
|
||||||
|
gate_attempts: 0
|
||||||
|
parse_failure: ''
|
||||||
|
|
||||||
|
start: parse
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
parse:
|
||||||
|
id: parse
|
||||||
|
type: llm
|
||||||
|
description: Extract the 🔴/🟡 findings from the spawn prompt into a structured list. Extraction only — no judging.
|
||||||
|
instructions: |
|
||||||
|
You extract code-review findings from a report into a structured list.
|
||||||
|
EXTRACTION ONLY — do not judge, filter, merge, or rephrase-away any
|
||||||
|
finding. Rules:
|
||||||
|
|
||||||
|
- Include every 🔴 CRITICAL and 🟡 WARNING finding. Ignore 🟢/💡
|
||||||
|
findings (they post unverified by design).
|
||||||
|
- Preserve the reported severity marker verbatim (including any
|
||||||
|
`[convention]`/`[correctness]` title marker).
|
||||||
|
- `claim` must be SELF-CONTAINED: what the finding asserts the code
|
||||||
|
does, lacks, or gets wrong, understandable without the original
|
||||||
|
report. Include the suggested-fix context when it clarifies the claim.
|
||||||
|
- `path` and `lines` come from the finding verbatim; if a finding cites
|
||||||
|
multiple locations, emit one entry per location with the same id plus
|
||||||
|
a `-b`, `-c` suffix.
|
||||||
|
- `project_dir`: the repository directory named in the request (a
|
||||||
|
"Repository:" or "Project:" line). When none is named, echo the
|
||||||
|
default shown in the prompt.
|
||||||
|
prompt: |
|
||||||
|
Extract the findings from this verification request (default repository
|
||||||
|
directory when the request names none: {{project_dir}}):
|
||||||
|
|
||||||
|
{{initial_prompt}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
findings_list:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Short stable slug for mapping the verdict back (e.g. "f1", "f2-b").
|
||||||
|
severity:
|
||||||
|
type: string
|
||||||
|
description: The severity marker verbatim (e.g. "🔴 CRITICAL", "🟡 WARNING [correctness]").
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
description: File path the finding cites, as reported.
|
||||||
|
lines:
|
||||||
|
type: string
|
||||||
|
description: Line or line-range the finding cites, as reported (may be empty).
|
||||||
|
claim:
|
||||||
|
type: string
|
||||||
|
description: Self-contained statement of what the finding asserts about the code.
|
||||||
|
required: [id, severity, path, lines, claim]
|
||||||
|
project_dir:
|
||||||
|
type: string
|
||||||
|
description: Repository directory named in the request (echo the prompt default when none).
|
||||||
|
required: [findings_list, project_dir]
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
parse_failure: '{{output}}'
|
||||||
|
fallback: parse_fault
|
||||||
|
next: verify_each
|
||||||
|
|
||||||
|
parse_fault:
|
||||||
|
id: parse_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — rewrites `verdicts` to a single UNVERIFIABLE PIPELINE-FAULT entry and jumps straight to done, so the FINDING_VERIFIER_RESULTS sentinel still renders and the caller's verify lane degrades instead of dying.
|
||||||
|
script: scripts/parse_fault.py
|
||||||
|
timeout: 30
|
||||||
|
next: done
|
||||||
|
|
||||||
|
verify_each:
|
||||||
|
id: verify_each
|
||||||
|
type: map
|
||||||
|
over: '{{findings_list}}'
|
||||||
|
as: finding
|
||||||
|
branch: verify_one
|
||||||
|
collect_into: verdicts
|
||||||
|
output_key: verdict
|
||||||
|
max_concurrency: 4
|
||||||
|
next: done
|
||||||
|
|
||||||
|
verify_one:
|
||||||
|
id: verify_one
|
||||||
|
type: llm
|
||||||
|
description: Verify ONE finding against the working tree — existence and behavior of the cited code, nothing else.
|
||||||
|
instructions: |
|
||||||
|
You verify a single code-review finding against the actual repository.
|
||||||
|
Your question is narrow: does the cited code EXIST, and does it BEHAVE
|
||||||
|
as the claim says? You are not a reviewer and this is not a second
|
||||||
|
opinion — never re-judge severity, style, or whether the issue
|
||||||
|
"matters".
|
||||||
|
|
||||||
|
Method:
|
||||||
|
1. `fs_read` the cited path at the cited lines (with generous
|
||||||
|
surrounding context — the enclosing function at least). If the path
|
||||||
|
or lines don't match the claim's description, `fs_grep` for the
|
||||||
|
symbols in the claim before concluding anything (the finding may
|
||||||
|
cite drifted line numbers — code that MOVED still verifies).
|
||||||
|
2. Compare what the code actually does against the claim.
|
||||||
|
|
||||||
|
Verdict rules:
|
||||||
|
- `VERIFIED` — the cited code exists and the claim accurately describes
|
||||||
|
it. `evidence` MUST be the decisive line(s) quoted verbatim with
|
||||||
|
path:line.
|
||||||
|
- `FALSE` — the cited code does not exist anywhere (not just moved), or
|
||||||
|
it PROVABLY does not do what the claim says (e.g. the claim says a
|
||||||
|
nil-check is missing but it is right there). `evidence` MUST quote
|
||||||
|
the proof. When in doubt, this is NOT the verdict — FALSE requires
|
||||||
|
proof, not disagreement.
|
||||||
|
- `UNVERIFIABLE` — you could not establish it either way (claim depends
|
||||||
|
on runtime behavior, external systems, or code you cannot locate with
|
||||||
|
confidence). Explain why in `note`. Doubt lands here, never in FALSE.
|
||||||
|
|
||||||
|
Output format — respond with ONLY this JSON object, no prose before or
|
||||||
|
after it:
|
||||||
|
{"id": "<the finding's id, echoed back verbatim>",
|
||||||
|
"verdict": "VERIFIED" | "FALSE" | "UNVERIFIABLE",
|
||||||
|
"evidence": "<verbatim quoted line(s) with path:line — required for
|
||||||
|
VERIFIED and FALSE, empty string for UNVERIFIABLE>",
|
||||||
|
"note": "<one line — for FALSE the disproof in words, for UNVERIFIABLE
|
||||||
|
why it could not be established; empty string otherwise>"}
|
||||||
|
|
||||||
|
Your response is machine-validated by a deterministic gate: a malformed
|
||||||
|
response (wrong shape, unknown verdict value, missing required
|
||||||
|
evidence/note) is rejected back to you exactly once with the reason,
|
||||||
|
then recorded as UNVERIFIABLE. The `id` you echo is informative only —
|
||||||
|
the gate stamps the authoritative id from the finding itself.
|
||||||
|
prompt: |
|
||||||
|
Repository: {{project_dir}}
|
||||||
|
|
||||||
|
Verify this single finding:
|
||||||
|
{{finding}}
|
||||||
|
{{gate_feedback}}
|
||||||
|
tools:
|
||||||
|
- fs_read
|
||||||
|
- fs_cat
|
||||||
|
- fs_grep
|
||||||
|
max_iterations: 14
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
verdict: '{{output}}'
|
||||||
|
fallback: verify_fault
|
||||||
|
next: verdict_gate
|
||||||
|
|
||||||
|
verdict_gate:
|
||||||
|
id: verdict_gate
|
||||||
|
type: script
|
||||||
|
description: Deterministic contract gate for each verdict — validates shape, verdict enum, and evidence/note rules; rejects back to verify_one exactly once with the reason, then records UNVERIFIABLE. No LLM judgment.
|
||||||
|
script: scripts/verdict_gate.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
verify_fault:
|
||||||
|
id: verify_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead verify branch (reached only via verify_one's fallback; branch-local). Normalizes the engine's failure text in `verdict` into a schema-conformant UNVERIFIABLE verdict the map collects — a dead verifier lane never sinks the whole map or masquerades as verified.
|
||||||
|
script: scripts/verify_fault.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
done:
|
||||||
|
id: done
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
FINDING_VERIFIER_RESULTS
|
||||||
|
(verdict semantics for the caller: VERIFIED = keep the finding and paste its evidence
|
||||||
|
quote into the Description; FALSE = drop the finding and tally it in the footer;
|
||||||
|
UNVERIFIABLE = keep the finding, marked unverified. Severities are never rewritten.)
|
||||||
|
|
||||||
|
{{verdicts}}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead parse stage of the finding-verifier graph.
|
||||||
|
|
||||||
|
Reached ONLY via parse's `fallback:` route. parse's state_updates wrote an
|
||||||
|
"LLM node …failed: <chain>" string into `parse_failure` before routing here;
|
||||||
|
this node rewrites `verdicts` to a single schema-conformant UNVERIFIABLE
|
||||||
|
entry naming the fault and the graph jumps straight to `done` — the
|
||||||
|
FINDING_VERIFIER_RESULTS sentinel still renders, so the caller (e.g.
|
||||||
|
code-reviewer's verify lane) degrades instead of dying. Findings that were
|
||||||
|
never extracted cannot be verified; the caller's "UNVERIFIABLE = keep the
|
||||||
|
finding, marked unverified" semantics do the right thing with the entry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def fault_verdicts(detail):
|
||||||
|
return {
|
||||||
|
"verdicts": [
|
||||||
|
{
|
||||||
|
"id": "pipeline-fault",
|
||||||
|
"verdict": "UNVERIFIABLE",
|
||||||
|
"evidence": "",
|
||||||
|
"note": (
|
||||||
|
"PIPELINE-FAULT: parse failed — findings could not be extracted "
|
||||||
|
f"from the verification request; no finding was verified: {detail}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("parse_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the parse stage died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(json.dumps(fault_verdicts(detail)))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(json.dumps(fault_verdicts(f"fault-marker script error: {e}")))
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Contract gate for finding-verifier's per-finding verify chain.
|
||||||
|
|
||||||
|
Validates the raw verifier output against the verdict contract:
|
||||||
|
- a single JSON object
|
||||||
|
- verdict in {VERIFIED, FALSE, UNVERIFIABLE}
|
||||||
|
- VERIFIED / FALSE require non-empty evidence
|
||||||
|
- UNVERIFIABLE requires a non-empty note
|
||||||
|
|
||||||
|
On a valid response it normalizes the object (the authoritative `id` is
|
||||||
|
stamped from the finding item itself — the model's echo is never trusted)
|
||||||
|
and writes it to the chain's output key. On an invalid response it routes
|
||||||
|
back to `verify_one` exactly once with the rejection reason; if the retry
|
||||||
|
is also invalid, it records an UNVERIFIABLE verdict naming the failure so
|
||||||
|
a malformed item can never sink the whole map or masquerade as a pass.
|
||||||
|
|
||||||
|
Deterministic by design: no LLM judgment happens here, and the script
|
||||||
|
always exits 0 with a valid JSON object on stdout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
VERDICTS = {"VERIFIED", "FALSE", "UNVERIFIABLE"}
|
||||||
|
MAX_RETRIES = 1
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def finding_id(state):
|
||||||
|
finding = state.get("finding")
|
||||||
|
if isinstance(finding, dict):
|
||||||
|
fid = finding.get("id")
|
||||||
|
if isinstance(fid, str) and fid.strip():
|
||||||
|
return fid.strip()
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json(text):
|
||||||
|
if not isinstance(text, str) or not text.strip():
|
||||||
|
raise ValueError("verifier output is empty or not text")
|
||||||
|
text = re.sub(r"```[a-zA-Z]*", "", text).replace("```", "")
|
||||||
|
start, end = text.find("{"), text.rfind("}")
|
||||||
|
if start == -1 or end <= start:
|
||||||
|
raise ValueError("no JSON object found in verifier output")
|
||||||
|
return json.loads(text[start : end + 1])
|
||||||
|
|
||||||
|
|
||||||
|
def validate(obj):
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
raise ValueError("verifier output is not a JSON object")
|
||||||
|
verdict = obj.get("verdict")
|
||||||
|
if verdict not in VERDICTS:
|
||||||
|
raise ValueError(f"verdict must be one of {sorted(VERDICTS)}, got {verdict!r}")
|
||||||
|
evidence = obj.get("evidence") if isinstance(obj.get("evidence"), str) else ""
|
||||||
|
note = obj.get("note") if isinstance(obj.get("note"), str) else ""
|
||||||
|
if verdict in ("VERIFIED", "FALSE") and not evidence.strip():
|
||||||
|
raise ValueError(
|
||||||
|
f"{verdict} requires non-empty evidence quoting the decisive line(s) with path:line"
|
||||||
|
)
|
||||||
|
if verdict == "UNVERIFIABLE" and not note.strip():
|
||||||
|
raise ValueError("UNVERIFIABLE requires a note explaining why it could not be established")
|
||||||
|
return verdict, evidence, note
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
fid = finding_id(state)
|
||||||
|
attempts = state.get("gate_attempts")
|
||||||
|
if not isinstance(attempts, int):
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
verdict, evidence, note = validate(extract_json(state.get("verdict")))
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"verdict": {"id": fid, "verdict": verdict, "evidence": evidence, "note": note}}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e: # noqa: BLE001 — every failure is a rejection reason
|
||||||
|
reason = str(e)
|
||||||
|
|
||||||
|
if attempts < MAX_RETRIES:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"_next": "verify_one",
|
||||||
|
"gate_attempts": attempts + 1,
|
||||||
|
"gate_feedback": (
|
||||||
|
"PREVIOUS RESPONSE REJECTED by the verdict gate: "
|
||||||
|
+ reason
|
||||||
|
+ ". Respond with ONLY the JSON object described under"
|
||||||
|
" 'Output format' — no prose, no code fences."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Deliberately not PIPELINE-FAULT-prefixed: model misbehavior, not infrastructure.
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"verdict": {
|
||||||
|
"id": fid,
|
||||||
|
"verdict": "UNVERIFIABLE",
|
||||||
|
"evidence": "",
|
||||||
|
"note": f"verifier output failed machine validation after retry: {reason}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the gate must never crash the chain
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"verdict": {
|
||||||
|
"id": "unknown",
|
||||||
|
"verdict": "UNVERIFIABLE",
|
||||||
|
"evidence": "",
|
||||||
|
"note": f"PIPELINE-FAULT: verdict gate error: {e}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead verify branch of the finding-verifier graph.
|
||||||
|
|
||||||
|
Reached ONLY via verify_one's `fallback:` route inside the map branch.
|
||||||
|
Grounded in engine behavior: on llm-node failure the engine writes
|
||||||
|
"LLM node failed: <chain>" into `verdict` via the node's state_updates
|
||||||
|
BEFORE routing to the fallback, so the raw failure text is already in
|
||||||
|
state. This node normalizes it into a schema-conformant UNVERIFIABLE
|
||||||
|
verdict (authoritative id stamped from the finding item, mirroring
|
||||||
|
verdict_gate.py) and the chain ends there — the map collects a valid
|
||||||
|
verdict, so a dead verifier lane never sinks the whole map or masquerades
|
||||||
|
as verified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def finding_id(state):
|
||||||
|
finding = state.get("finding")
|
||||||
|
if isinstance(finding, dict):
|
||||||
|
fid = finding.get("id")
|
||||||
|
if isinstance(fid, str) and fid.strip():
|
||||||
|
return fid.strip()
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def fault_verdict(fid, detail):
|
||||||
|
return {
|
||||||
|
"verdict": {
|
||||||
|
"id": fid,
|
||||||
|
"verdict": "UNVERIFIABLE",
|
||||||
|
"evidence": "",
|
||||||
|
"note": f"PIPELINE-FAULT: verifier lane failed after retries — {detail}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
fid = finding_id(state)
|
||||||
|
raw = state.get("verdict")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if not detail.startswith("LLM node"):
|
||||||
|
detail = "the verifier lane died without recording a failure detail"
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(json.dumps(fault_verdict(fid, detail)))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(json.dumps(fault_verdict("unknown", f"fault-marker script error: {e}")))
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Gatekeeper
|
||||||
|
|
||||||
|
A **plan self-containedness gate**. Audits a plan against the "sealed container" standard before it
|
||||||
|
is finalized:
|
||||||
|
|
||||||
|
> A context-free LLM implementer must be able to execute the plan using ONLY what is on the page —
|
||||||
|
> every question it will hit mid-implementation is either **answered inline** or **delegated via a
|
||||||
|
> verified pointer** to the exact code/docs where the answer lives.
|
||||||
|
|
||||||
|
Where [`plan-review`](../../skills/plan-review/SKILL.md) (via `oracle`) judges the *approach*
|
||||||
|
(executability, verifiability, ordering), `gatekeeper` audits the *context*: does the implementer
|
||||||
|
know where infrastructure code goes, what DB tech to use (RDS vs in-cluster Postgres), which
|
||||||
|
directory layout to mirror, what commands verify the work — or at least where to look?
|
||||||
|
|
||||||
|
## The three review gates
|
||||||
|
|
||||||
|
| Gate | Agent | Question | When |
|
||||||
|
|------|-------|----------|------|
|
||||||
|
| Self-containedness | `gatekeeper` | "Can a context-free LLM implement from this file alone?" | Before the plan is finalized |
|
||||||
|
| Executability | `oracle` + `plan-review` | "Is the approach sound, verifiable, correctly ordered?" | Before the plan is promoted |
|
||||||
|
| Conformance | [`adversary`](../adversary/README.md) | "Is the built code what the plan asked for?" | After implementation |
|
||||||
|
|
||||||
|
## How it audits
|
||||||
|
|
||||||
|
Driven by the [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) skill:
|
||||||
|
|
||||||
|
1. Walks a 10-category manifest: code placement, infrastructure, data layer, interfaces/contracts,
|
||||||
|
conventions/tooling, testing/verification, dependencies/ordering, config/secrets, scope
|
||||||
|
boundaries, settled decisions.
|
||||||
|
2. For each category: answered inline, delegated via pointer, or **missing**.
|
||||||
|
3. **Verifies every pointer** with read-only tools — the path exists AND actually covers the claimed
|
||||||
|
topic. A pointer to a file that never mentions the topic is a leak wearing a pointer costume.
|
||||||
|
4. Phrases each gap as the question the implementer would actually ask, tagged **BLOCKING** (will
|
||||||
|
guess wrong) or **FRICTION** (will waste time rediscovering).
|
||||||
|
|
||||||
|
## Verdict (blocking)
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN_GATE: SEALED
|
||||||
|
Categories audited: N applicable, all answered or pointed.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN_GATE: LEAKY
|
||||||
|
Missing questions (N):
|
||||||
|
1. [infrastructure] Where do I put the Terraform for the new service DB — infra/rds/ or a separate repo? — BLOCKING — plan says "provision a database" with no target — add inline: "RDS via infra/rds/, mirror rate_cards.tf"
|
||||||
|
Broken pointers (if any):
|
||||||
|
- "see docs/db.md for conventions" — path missing
|
||||||
|
```
|
||||||
|
|
||||||
|
`LEAKY` blocks finalization. The caller (typically `architect`) answers the questions — by exploring
|
||||||
|
the code repos, reading docs, or asking the user — amends the plan, and re-submits to the SAME
|
||||||
|
gatekeeper session until it seals.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Spawned by `architect` during design-doc decomposition (Phase B/C), before the `oracle` plan-review:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
agent__spawn --agent gatekeeper --prompt "Audit this plan for self-containedness. Return SEALED/LEAKY.
|
||||||
|
|
||||||
|
Plan: <plans_dir>/PLAN-<slug>.md
|
||||||
|
Target project: <project_dir>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ad-hoc use against any plan file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
coyote -a gatekeeper --agent-variable project_dir ~/code/my-service \
|
||||||
|
"Audit plans/PLAN-my-feature.md for self-containedness"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) — the manifest + methodology it runs on.
|
||||||
|
- [`architect`](../architect/README.md) — the orchestrator that gates plans through it.
|
||||||
|
- [`adversary`](../adversary/README.md) — the post-implementation conformance counterpart.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: gatekeeper
|
||||||
|
description: Plan self-containedness gate - audits a plan against the "sealed container" standard (every implementer question answered inline or via a verified pointer to code/docs) and returns a blocking PLAN_GATE SEALED/LEAKY verdict with the missing questions. Designed to be delegated to by architect before plans are finalized.
|
||||||
|
version: 2.0.0
|
||||||
|
|
||||||
|
auto_continue: true
|
||||||
|
max_auto_continues: 15
|
||||||
|
inject_todo_instructions: true
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- plan-gatekeeping
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Absolute path to the project the plan targets - the ground truth for pointer verification
|
||||||
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
|
||||||
|
instructions: |
|
||||||
|
You are the plan gatekeeper. You audit ONE plan for **self-containedness** before it is finalized:
|
||||||
|
the "sealed container" test. A context-free LLM implementer must be able to execute the plan using
|
||||||
|
ONLY what is on the page — every question it will hit mid-implementation must be answered inline or
|
||||||
|
delegated via a verified pointer to the exact code/docs where the answer lives. Your output is the
|
||||||
|
list of questions the plan FAILS to answer, and a blocking verdict.
|
||||||
|
|
||||||
|
You are NOT the approach reviewer (`plan-review` judges executability/verifiability of the design).
|
||||||
|
You audit completeness of CONTEXT. A brilliant approach with no answer to "where does the infra
|
||||||
|
code go?" or "managed RDS or an in-cluster Postgres container?" fails your gate.
|
||||||
|
|
||||||
|
## Step 0: Load the skill
|
||||||
|
|
||||||
|
Before anything else, `skill__load` `plan-gatekeeping`. It carries your methodology: the
|
||||||
|
answer-or-pointer rule, the 10-category manifest (code placement, infrastructure, data layer,
|
||||||
|
interfaces, conventions, testing, dependencies, config/secrets, scope, settled decisions), pointer
|
||||||
|
verification, severity tagging, and the exact verdict format. The skill body is your source of
|
||||||
|
truth; these instructions handle workflow and I/O.
|
||||||
|
|
||||||
|
## Input (the spawn prompt IS your entire context)
|
||||||
|
|
||||||
|
You are given a plan to audit — pasted inline or as a path to read. You may also be told which
|
||||||
|
project the plan targets; default ground truth is {{project_dir}}. Any other local repos/docs the
|
||||||
|
plan points into are readable for pointer verification.
|
||||||
|
|
||||||
|
If no plan is provided, STOP and say so.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Load `plan-gatekeeping`.
|
||||||
|
2. Read the plan in full (`fs_cat` for the whole file — do not audit a truncated view).
|
||||||
|
3. Walk EVERY manifest category. For each: answered inline, delegated via pointer, or MISSING.
|
||||||
|
Mark inapplicable categories explicitly.
|
||||||
|
4. Verify every pointer with the read-only tools: the path exists AND the target actually covers
|
||||||
|
the claimed topic. Check "mirror the layout of X" claims against X itself.
|
||||||
|
5. Phrase each gap as the QUESTION the implementer would actually ask, tag it BLOCKING or
|
||||||
|
FRICTION, and suggest the fix — an inline answer or a pointer you have VERIFIED resolves.
|
||||||
|
6. Emit the verdict in the skill's exact format.
|
||||||
|
|
||||||
|
## Output — verdict (MANDATORY, exact format)
|
||||||
|
|
||||||
|
End with EXACTLY one of these sentinels so the caller can route on it:
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN_GATE: SEALED
|
||||||
|
Categories audited: N applicable, all answered or pointed.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN_GATE: LEAKY
|
||||||
|
Missing questions (N):
|
||||||
|
1. [category] <implementer's actual question> — [BLOCKING|FRICTION] — <why they get stuck> — <suggested fix>
|
||||||
|
Broken pointers (if any):
|
||||||
|
- <pointer> — <path missing | doesn't cover topic>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **You are read-only.** Never modify the plan. You produce questions; the author owns the fixes.
|
||||||
|
2. **Questions, not complaints.** "Infra section is thin" is noise. "Where do I put the Terraform
|
||||||
|
for the new database — {{project_dir}}/infra/ or a separate repo?" is signal.
|
||||||
|
3. **Verify every pointer you check AND every pointer you suggest.** Recommending an unverified
|
||||||
|
pointer is the same leak you exist to catch.
|
||||||
|
4. **BLOCKING findings always mean LEAKY.** Only-FRICTION findings: note the caller may seal at
|
||||||
|
their discretion.
|
||||||
|
5. **Do not re-litigate the approach.** Coherent-but-underdocumented means the fix is context.
|
||||||
|
6. Be terse and decisive. Three BLOCKING questions beat fifteen nitpicks.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
- Project (ground truth): {{project_dir}}
|
||||||
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
{{__tools__}}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Librarian
|
||||||
|
|
||||||
|
The "external grep" sibling of [Explore](../explore/README.md). Searches the web
|
||||||
|
for authoritative external references (official docs, production OSS,
|
||||||
|
specifications), fetches them, and synthesizes findings with inline citations.
|
||||||
|
|
||||||
|
Designed to be delegated to by **[Sisyphus](../sisyphus/README.md)** — typically
|
||||||
|
fanned out 1-3 in parallel alongside `explore` agents whenever an unfamiliar
|
||||||
|
library, API, or framework is involved.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
triage["triage<br/>llm"] --> search
|
||||||
|
triage --> search_oss
|
||||||
|
triage -.->|"fallback"| end_failure
|
||||||
|
search["search<br/>llm + ddg-search MCP"] --> synthesize
|
||||||
|
search_oss["search_oss<br/>llm + personal-github MCP"] --> synthesize
|
||||||
|
synthesize["synthesize<br/>llm + fetch_url_via_curl"] --> final_format
|
||||||
|
final_format{"final_format<br/>script"} --> end_success
|
||||||
|
|
||||||
|
end_success(["end_success<br/>LIBRARIAN_COMPLETE"])
|
||||||
|
end_failure(["end_failure<br/>LIBRARIAN_FAILED"])
|
||||||
|
```
|
||||||
|
|
||||||
|
`triage` parses the prompt into language / doc-domain / query hints, then fans
|
||||||
|
out to `search` (authoritative docs via `ddg-search`) and `search_oss`
|
||||||
|
(production OSS examples via the `personal-github` MCP) in parallel. Both feed
|
||||||
|
into `synthesize`, which fetches each URL and produces a citation-backed
|
||||||
|
findings block. `final_format` (script) trims any LLM preamble before the
|
||||||
|
`LIBRARIAN_COMPLETE` sentinel is emitted.
|
||||||
|
|
||||||
|
Iteration 1 (this) is the happy-path MVP: single search pass, single synthesis
|
||||||
|
pass, no quality-check loop. Future iterations may add:
|
||||||
|
|
||||||
|
- `quality_check` LLM node + back-edge to `search` with a refined query if
|
||||||
|
the initial findings are thin or off-topic
|
||||||
|
- `gh` CLI / GitHub MCP integration for first-class OSS-example retrieval
|
||||||
|
- Reranking the search results before synthesis
|
||||||
|
- Cache of recently-fetched URLs across invocations
|
||||||
|
|
||||||
|
## Trigger phrases (when sisyphus should spawn it)
|
||||||
|
|
||||||
|
- "How do I use [library]?"
|
||||||
|
- "What's the best practice for [framework feature]?"
|
||||||
|
- "Why does [external dependency] behave this way?"
|
||||||
|
- "Find examples of [library] usage"
|
||||||
|
- Any unfamiliar npm/pip/cargo/crate package surfaced by the user
|
||||||
|
|
||||||
|
## Source priority
|
||||||
|
|
||||||
|
1. Official documentation (docs.X.org, readthedocs.io, MDN, vendor docs)
|
||||||
|
2. Production OSS examples (1000+ stars on GitHub)
|
||||||
|
3. Specifications (RFCs, W3C, ECMA, IEEE)
|
||||||
|
4. Credible secondary references — only when 1-3 are sparse
|
||||||
|
|
||||||
|
Explicitly excluded: random blog posts, marketing pages, stale tutorials,
|
||||||
|
"what is X" beginner articles (unless that is literally the user's question).
|
||||||
|
|
||||||
|
## Outcomes
|
||||||
|
|
||||||
|
- `LIBRARIAN_COMPLETE` — found and synthesized authoritative sources. Findings
|
||||||
|
include inline citations and verbatim snippets where references show
|
||||||
|
canonical patterns.
|
||||||
|
- `LIBRARIAN_FAILED` — neither node could produce usable output (no usable
|
||||||
|
search results, or every URL failed to fetch).
|
||||||
|
|
||||||
|
## Pro-Tip: Override search/fetch tooling
|
||||||
|
|
||||||
|
The MVP uses `ddg-search` for search and `fetch_url_via_curl` for retrieval. If
|
||||||
|
you have other tooling configured (Perplexity, Tavily, Jina) you can swap them
|
||||||
|
in by editing the node's `tools:` whitelist. Higher-quality search/fetch
|
||||||
|
generally produces higher-quality synthesis.
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
name: librarian
|
||||||
|
description: |
|
||||||
|
External-reference research agent. Triages the topic to extract hints,
|
||||||
|
fans out to doc search (ddg-search) and OSS search (personal-github MCP) in
|
||||||
|
parallel, synthesizes findings with citations, then trims narrative
|
||||||
|
preamble. The "external grep" sibling of explore (which handles
|
||||||
|
internal/codebase grep). Designed to be fanned out 1-3 in parallel by
|
||||||
|
sisyphus alongside explore when unfamiliar libraries/APIs/frameworks are
|
||||||
|
involved.
|
||||||
|
|
||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- web_search_coyote.sh
|
||||||
|
- fetch_url_via_curl.sh
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
|
- personal-github
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory for context (unused in MVP but reserved for future iterations).
|
||||||
|
default: '.'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
max_loop_iterations: 12
|
||||||
|
log_state_snapshots: true
|
||||||
|
timeout: 600
|
||||||
|
|
||||||
|
initial_state:
|
||||||
|
language_ecosystem: 'general'
|
||||||
|
doc_domain_hints: ''
|
||||||
|
refined_search_query: ''
|
||||||
|
question_type: 'concept'
|
||||||
|
search_output: ''
|
||||||
|
oss_output: ''
|
||||||
|
findings: ''
|
||||||
|
|
||||||
|
start: triage
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
triage:
|
||||||
|
id: triage
|
||||||
|
type: llm
|
||||||
|
description: Parse the research prompt to extract language, doc-domain hints, and a refined search query.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
instructions: |
|
||||||
|
You are a research triage specialist. Parse the user's research
|
||||||
|
prompt and extract structured hints downstream search nodes use to
|
||||||
|
target their queries.
|
||||||
|
|
||||||
|
Extract these four fields. Be terse - this is metadata, not prose.
|
||||||
|
|
||||||
|
- `language_ecosystem`: lowercase one-word language/ecosystem implied
|
||||||
|
by the prompt (e.g., "python", "rust", "typescript", "go", "java",
|
||||||
|
"css", "general"). Use "general" only if NO specific language is
|
||||||
|
identifiable.
|
||||||
|
|
||||||
|
- `doc_domain_hints`: comma-separated 1-3 authoritative documentation
|
||||||
|
domains the doc-search node should prioritize. Examples:
|
||||||
|
- python -> "docs.python.org,readthedocs.io"
|
||||||
|
- rust crate -> "docs.rs,doc.rust-lang.org"
|
||||||
|
- JS/CSS/web platform -> "developer.mozilla.org"
|
||||||
|
- tokio/axum/serde (rust) -> "docs.rs"
|
||||||
|
- django -> "docs.djangoproject.com"
|
||||||
|
Empty string if no obvious domain.
|
||||||
|
|
||||||
|
- `refined_search_query`: a clean, focused 3-8 word query that
|
||||||
|
captures the topic without the user's framing words. Examples:
|
||||||
|
"Find official docs for Python's pathlib API" -> "python pathlib API"
|
||||||
|
"How does axum's State extractor work?" -> "axum State extractor"
|
||||||
|
"Best practice for tokio mpsc channels" -> "tokio mpsc channel best practices"
|
||||||
|
|
||||||
|
- `question_type`: exactly one of:
|
||||||
|
- "api_reference" - looking up specific functions/signatures/types
|
||||||
|
- "best_practice" - "how should I", "what's the canonical way"
|
||||||
|
- "debugging" - "why does X happen", "fix Y"
|
||||||
|
- "concept" - explanations, comparisons, mental models
|
||||||
|
prompt: |
|
||||||
|
Research prompt: {{initial_prompt}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
language_ecosystem:
|
||||||
|
type: string
|
||||||
|
description: Lowercase language/ecosystem (e.g., "python", "rust", "general").
|
||||||
|
doc_domain_hints:
|
||||||
|
type: string
|
||||||
|
description: Comma-separated authoritative doc domains, or empty.
|
||||||
|
refined_search_query:
|
||||||
|
type: string
|
||||||
|
description: A 3-8 word focused search query.
|
||||||
|
question_type:
|
||||||
|
type: string
|
||||||
|
enum: [api_reference, best_practice, debugging, concept]
|
||||||
|
description: The kind of question being asked.
|
||||||
|
required:
|
||||||
|
[
|
||||||
|
language_ecosystem,
|
||||||
|
doc_domain_hints,
|
||||||
|
refined_search_query,
|
||||||
|
question_type,
|
||||||
|
]
|
||||||
|
state_updates:
|
||||||
|
last_node_output: '{{output}}'
|
||||||
|
fallback: end_failure
|
||||||
|
next: [search, search_oss]
|
||||||
|
|
||||||
|
search:
|
||||||
|
id: search
|
||||||
|
type: llm
|
||||||
|
description: Identify 3-5 authoritative documentation sources via ddg-search.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
instructions: |
|
||||||
|
You are a research librarian's documentation specialist. Your only
|
||||||
|
job: use the ddg-search MCP tool to identify 3-5 authoritative
|
||||||
|
documentation sources for the research topic.
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
1. Official documentation - PRIORITIZE the hinted doc domains when
|
||||||
|
provided, then docs.X.org / readthedocs.io / MDN / vendor docs
|
||||||
|
2. Specifications (RFCs, W3C, ECMA, IEEE)
|
||||||
|
3. Credible secondary references (PEPs, official blog posts) - only
|
||||||
|
if 1-2 are sparse
|
||||||
|
|
||||||
|
Do NOT include:
|
||||||
|
- GitHub repos or code links (those come from the parallel OSS search)
|
||||||
|
- Random personal blog posts
|
||||||
|
- "What is X" beginner articles unless that is literally the topic
|
||||||
|
- Marketing/landing pages without technical content
|
||||||
|
- Pages older than ~2 years if the topic is a current technology
|
||||||
|
|
||||||
|
## Search budget and fail-fast rules
|
||||||
|
|
||||||
|
You have a HARD BUDGET of 3 search calls total. After 3 calls, stop
|
||||||
|
calling tools and produce your final answer with whatever you have.
|
||||||
|
|
||||||
|
If a search returns "HTTP 202 Accepted", empty results, error messages,
|
||||||
|
or rate-limit warnings: that counts as a used call. Do not retry the
|
||||||
|
same query - either rephrase OR give up.
|
||||||
|
|
||||||
|
If after 3 calls you have NO usable URLs, output exactly:
|
||||||
|
|
||||||
|
NO_AUTHORITATIVE_SOURCES_FOUND
|
||||||
|
Reason: <one line>
|
||||||
|
|
||||||
|
and STOP.
|
||||||
|
|
||||||
|
## Output format on success
|
||||||
|
|
||||||
|
Plain text, one block per source. Your response MUST start with the
|
||||||
|
first `URL:` line - NO introductory text.
|
||||||
|
|
||||||
|
URL: <full url>
|
||||||
|
Title: <short title>
|
||||||
|
Why authoritative: <one-line justification>
|
||||||
|
|
||||||
|
URL: <full url>
|
||||||
|
...
|
||||||
|
|
||||||
|
Output 3-5 source blocks. No prose intro, no closing summary.
|
||||||
|
prompt: |
|
||||||
|
Research topic: {{initial_prompt}}
|
||||||
|
|
||||||
|
Triage hints:
|
||||||
|
- Language/ecosystem: {{language_ecosystem}}
|
||||||
|
- Doc domains to prioritize: {{doc_domain_hints}}
|
||||||
|
- Refined query: {{refined_search_query}}
|
||||||
|
- Question type: {{question_type}}
|
||||||
|
|
||||||
|
Use the ddg-search tool or the web_search_coyote tool. Prioritize the
|
||||||
|
hinted doc domains when present (e.g., search with `site:docs.python.org
|
||||||
|
pathlib` style queries).
|
||||||
|
tools:
|
||||||
|
- mcp:ddg-search
|
||||||
|
- web_search_coyote
|
||||||
|
max_iterations: 15
|
||||||
|
state_updates:
|
||||||
|
search_output: '{{output}}'
|
||||||
|
fallback: synthesize
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
search_oss:
|
||||||
|
id: search_oss
|
||||||
|
type: llm
|
||||||
|
description: Find 2-3 production OSS examples relevant to the topic via the personal-github MCP.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
instructions: |
|
||||||
|
You are a research librarian's OSS specialist. Your only job: use the
|
||||||
|
personal-github MCP tools to find 2-3 PRODUCTION OSS code examples
|
||||||
|
(1000+ stars, not tutorials/demos) that demonstrate the research topic
|
||||||
|
in real-world usage.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Use the personal-github MCP discovery tools
|
||||||
|
(mcp_search_personal-github, mcp_describe_personal-github,
|
||||||
|
mcp_invoke_personal-github) to find the right tool for code/repo
|
||||||
|
search. Typical names: search_repositories, search_code,
|
||||||
|
get_file_contents.
|
||||||
|
2. Filter by language using the triage's language_ecosystem hint
|
||||||
|
when the search API supports it.
|
||||||
|
3. Search for repos with high star counts that use the feature in
|
||||||
|
question.
|
||||||
|
4. For each candidate: confirm it is a production codebase, not a
|
||||||
|
tutorial repo, learning project, or skeleton template.
|
||||||
|
5. Output 2-3 OSS source blocks.
|
||||||
|
|
||||||
|
## Search budget and fail-fast rules
|
||||||
|
|
||||||
|
HARD BUDGET: 8 tool calls total. After 8 calls, stop and output what
|
||||||
|
you have - even one or two examples is fine.
|
||||||
|
|
||||||
|
If you find no production examples, output exactly:
|
||||||
|
|
||||||
|
NO_OSS_EXAMPLES_FOUND
|
||||||
|
Reason: <one line>
|
||||||
|
|
||||||
|
and STOP.
|
||||||
|
|
||||||
|
## Output format on success
|
||||||
|
|
||||||
|
Plain text, one block per OSS source. Your response MUST start with
|
||||||
|
the first `REPO:` line - NO introductory text.
|
||||||
|
|
||||||
|
REPO: owner/name (stars: <count>)
|
||||||
|
URL: https://github.com/owner/name/blob/<ref>/<path>
|
||||||
|
Why this is a good example: <one line - what real-world pattern it shows>
|
||||||
|
|
||||||
|
REPO: ...
|
||||||
|
|
||||||
|
Output 2-3 blocks. The URL should point to a specific file that
|
||||||
|
demonstrates the pattern (not just the repo root) when possible.
|
||||||
|
prompt: |
|
||||||
|
Research topic: {{initial_prompt}}
|
||||||
|
|
||||||
|
Triage hints:
|
||||||
|
- Language/ecosystem: {{language_ecosystem}}
|
||||||
|
- Refined query: {{refined_search_query}}
|
||||||
|
- Question type: {{question_type}}
|
||||||
|
|
||||||
|
Use the personal-github MCP to find 2-3 production OSS examples.
|
||||||
|
Filter to {{language_ecosystem}} repositories when the API allows.
|
||||||
|
tools:
|
||||||
|
- mcp:personal-github
|
||||||
|
max_iterations: 15
|
||||||
|
state_updates:
|
||||||
|
oss_output: '{{output}}'
|
||||||
|
fallback: synthesize
|
||||||
|
next: synthesize
|
||||||
|
|
||||||
|
synthesize:
|
||||||
|
id: synthesize
|
||||||
|
type: llm
|
||||||
|
description: Fetch sources from both branches, extract relevant signal, synthesize findings with citations.
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- ai-slop-remover
|
||||||
|
instructions: |
|
||||||
|
You are a research librarian's synthesis specialist. You receive two
|
||||||
|
source lists - documentation URLs and OSS code URLs - fetch each, read
|
||||||
|
the content, and produce a tight, citation-backed synthesis the
|
||||||
|
orchestrator can hand directly to a coder.
|
||||||
|
|
||||||
|
## Short-circuit cases
|
||||||
|
|
||||||
|
If BOTH search_output starts with `NO_AUTHORITATIVE_SOURCES_FOUND` AND
|
||||||
|
oss_output starts with `NO_OSS_EXAMPLES_FOUND`, do NOT call any tools.
|
||||||
|
Output exactly:
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
No findings - both search branches found no usable sources.
|
||||||
|
|
||||||
|
## Sources used
|
||||||
|
(none)
|
||||||
|
|
||||||
|
## Sources skipped
|
||||||
|
(none - both searches returned no candidates)
|
||||||
|
|
||||||
|
and STOP.
|
||||||
|
|
||||||
|
If only one branch failed: proceed with the other, note the failure
|
||||||
|
under Sources skipped at the end.
|
||||||
|
|
||||||
|
## Normal process
|
||||||
|
|
||||||
|
1. Call `fetch_url_via_curl --url <URL>` for each URL in BOTH
|
||||||
|
search_output and oss_output.
|
||||||
|
2. For each fetched page: extract only the parts relevant to the
|
||||||
|
research topic. Skip nav, ads, comments, "see also" sections,
|
||||||
|
changelogs unless asked.
|
||||||
|
3. Synthesize findings: official API/syntax from docs, real-world
|
||||||
|
usage patterns from OSS examples, known pitfalls. Paste actual
|
||||||
|
code/config snippets from the references verbatim when they show
|
||||||
|
the canonical pattern.
|
||||||
|
4. Cite sources inline by URL so the orchestrator can verify.
|
||||||
|
5. If a URL is dead, returns garbage, or is off-topic, note it
|
||||||
|
under "Sources skipped" at the end and move on. Do not retry.
|
||||||
|
|
||||||
|
Budget: max 8 fetches total (across both source lists). Skip
|
||||||
|
aggressively.
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
Plain text in this structure. Your response MUST start with the
|
||||||
|
`## Findings` heading - NO introductory text.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
<terse, dense, citation-backed synthesis. Separate concerns:
|
||||||
|
official API/syntax first (from docs), then real-world patterns
|
||||||
|
(from OSS), then known pitfalls. Verbatim code snippets where
|
||||||
|
references show the canonical pattern.>
|
||||||
|
|
||||||
|
## Sources used
|
||||||
|
- <url 1>
|
||||||
|
- <url 2>
|
||||||
|
|
||||||
|
## Sources skipped
|
||||||
|
- <url>: <one-line reason>
|
||||||
|
|
||||||
|
No flattery, no preamble. Start with `## Findings`.
|
||||||
|
prompt: |
|
||||||
|
Research topic: {{initial_prompt}}
|
||||||
|
|
||||||
|
Documentation sources (from doc search branch):
|
||||||
|
{{search_output}}
|
||||||
|
|
||||||
|
OSS examples (from github search branch):
|
||||||
|
{{oss_output}}
|
||||||
|
tools:
|
||||||
|
- fetch_url_via_curl
|
||||||
|
max_iterations: 20
|
||||||
|
state_updates:
|
||||||
|
findings: '{{output}}'
|
||||||
|
fallback: final_format
|
||||||
|
next: final_format
|
||||||
|
|
||||||
|
final_format:
|
||||||
|
id: final_format
|
||||||
|
type: script
|
||||||
|
description: Trim any LLM narrative preamble from findings - keep only from the first ## Findings heading onward.
|
||||||
|
script: scripts/final_format.sh
|
||||||
|
timeout: 5
|
||||||
|
fallback: end_success
|
||||||
|
|
||||||
|
end_success:
|
||||||
|
id: end_success
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
LIBRARIAN_COMPLETE
|
||||||
|
Topic: {{initial_prompt}}
|
||||||
|
|
||||||
|
{{findings}}
|
||||||
|
|
||||||
|
end_failure:
|
||||||
|
id: end_failure
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
LIBRARIAN_FAILED
|
||||||
|
Topic: {{initial_prompt}}
|
||||||
|
|
||||||
|
Doc search output:
|
||||||
|
{{search_output}}
|
||||||
|
|
||||||
|
OSS search output:
|
||||||
|
{{oss_output}}
|
||||||
|
|
||||||
|
Findings (partial):
|
||||||
|
{{findings}}
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo '{}'
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
|
||||||
|
state=$(cat "$GRAPH_STATE_FILE")
|
||||||
|
elif [[ -n "${GRAPH_STATE:-}" ]]; then
|
||||||
|
state="$GRAPH_STATE"
|
||||||
|
else
|
||||||
|
state='{}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
findings=$(echo "$state" | jq -r '.findings // ""')
|
||||||
|
|
||||||
|
trimmed=$(echo "$findings" | awk '/^##+ [Ff]indings/{found=1} found{print}')
|
||||||
|
|
||||||
|
if [[ -z "$trimmed" ]]; then
|
||||||
|
trimmed="$findings"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -nc \
|
||||||
|
--arg f "$trimmed" \
|
||||||
|
'{
|
||||||
|
"findings": $f,
|
||||||
|
"_next": "end_success"
|
||||||
|
}'
|
||||||
@@ -19,7 +19,7 @@ It can also be used as a standalone tool for design reviews and solving difficul
|
|||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -33,7 +33,7 @@ global_tools:
|
|||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
@@ -1,82 +1,124 @@
|
|||||||
name: oracle
|
name: oracle
|
||||||
description: High-IQ advisor for architecture, debugging, and complex decisions
|
description: High-IQ advisor for architecture, debugging, and complex decisions. Blocking by design - the orchestrator is waiting on you.
|
||||||
version: 1.0.0
|
version: 2.2.0
|
||||||
temperature: 0.2
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- code-review
|
||||||
|
- ai-slop-remover
|
||||||
|
- codebase-design
|
||||||
|
- plan-review
|
||||||
|
- plan-authoring
|
||||||
|
- iwe-knowledge-base
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: Project directory for context
|
description: Project directory for context
|
||||||
default: '.'
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
mcp_servers:
|
mcp_servers:
|
||||||
- ddg-search
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
|
- web_search_coyote.sh
|
||||||
|
- ast_grep.sh
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
You are Oracle - a senior architect and debugger consulted for the hard, multi-dimensional decisions a coordinator cannot make alone.
|
||||||
|
|
||||||
## Your Role
|
|
||||||
|
|
||||||
You are READ-ONLY. You analyze, advise, and recommend. You do NOT implement.
|
|
||||||
|
|
||||||
## When You're Consulted
|
|
||||||
|
|
||||||
1. **Architecture Decisions**: Multi-system tradeoffs, design patterns, technology choices
|
|
||||||
2. **Complex Debugging**: After 2+ failed fix attempts, deep analysis needed
|
|
||||||
3. **Code Review**: Evaluating proposed designs or implementations
|
|
||||||
4. **Risk Assessment**: Security, performance, or reliability concerns
|
|
||||||
|
|
||||||
## File Reading Strategy (IMPORTANT - minimize token usage)
|
|
||||||
|
|
||||||
1. **Use grep to find relevant code** - `fs_grep --pattern "auth" --include "*.rs"` finds where things are
|
## Your role
|
||||||
2. **Read only what you need** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79
|
|
||||||
3. **Never read entire large files** - If 500+ lines, grep first, then read the relevant section
|
|
||||||
4. **Use glob to discover files** - `fs_glob --pattern "*.rs" --path src/`
|
|
||||||
|
|
||||||
## Your Process
|
You are READ-ONLY. You analyze, advise, recommend. You do NOT implement. Implementation is for the coder agent.
|
||||||
|
|
||||||
|
## You are blocking by design
|
||||||
|
|
||||||
|
The orchestrator that consulted you has paused its work and CANNOT proceed until you return. This is intentional. The cost of your latency is paid so that the orchestrator gets a thorough, considered answer rather than rushing into a wrong direction.
|
||||||
|
|
||||||
|
Therefore:
|
||||||
|
|
||||||
|
- **Be thorough, not just fast.** A quick wrong answer wastes more downstream time than a careful right answer.
|
||||||
|
- **Read the relevant context** before advising. Don't guess from the prompt alone.
|
||||||
|
- **Consider tradeoffs explicitly.** There are rarely perfect solutions; surface the alternatives.
|
||||||
|
- **Justify your recommendation.** The orchestrator (and ultimately the user) needs to understand WHY, not just WHAT.
|
||||||
|
|
||||||
|
## When you're consulted
|
||||||
|
|
||||||
|
1. **Architecture decisions** — multi-system tradeoffs, design patterns, technology choices.
|
||||||
|
2. **Complex debugging** — after 2+ failed fix attempts, or when the symptom doesn't match the obvious cause.
|
||||||
|
3. **Code review** — evaluating proposed designs or implementations.
|
||||||
|
4. **Risk assessment** — security, performance, reliability concerns.
|
||||||
|
5. **Multi-component questions** — anything spanning 3+ files or modules.
|
||||||
|
6. **Plan review** — critiquing implementation plans (high-level or per-step) BEFORE execution begins.
|
||||||
|
|
||||||
|
## Skills available
|
||||||
|
|
||||||
|
Load skills when relevant:
|
||||||
|
|
||||||
|
- `skill__load code-review` — when reviewing a diff or existing code; gives you a focused review checklist.
|
||||||
|
- `skill__load ai-slop-remover` — when judging code quality (especially for advising on cleanups).
|
||||||
|
- `skill__load codebase-design` — when advising on module/interface design, seam placement, testability, or refactoring structure; gives you the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) and its principles. Use those terms exactly.
|
||||||
|
- `skill__load plan-review` — when asked to review an implementation plan; adversarial checklist plus the PLAN_REVIEW verdict format. Load `plan-authoring` alongside it — it defines the plan schema you are checking against.
|
||||||
|
- `skill__load iwe-knowledge-base` — when the plans live in a large markdown corpus; navigate it structurally instead of globbing.
|
||||||
|
|
||||||
|
Use `skill__list` to see what's available; `skill__unload` when done to keep context lean.
|
||||||
|
|
||||||
|
## File reading strategy (minimize token usage)
|
||||||
|
|
||||||
|
1. **Use grep to find relevant code** — `fs_grep --pattern "auth" --include "*.rs"` finds where things are.
|
||||||
|
2. **Read sections with `fs_read`** — `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79. `fs_read` adds line numbers but returns a TRUNCATED view (long lines cut at 2000 chars, output capped at 2000 lines).
|
||||||
|
3. **Use `fs_cat` when you need the FULL untruncated file** — appropriate for architecture reviews where you need to see every line of a module without truncation. Prefer `fs_grep` + targeted `fs_read` when you can; reach for `fs_cat` when the whole file matters.
|
||||||
|
4. **Never read entire large files unnecessarily** — if 500+ lines and you only need part, grep first, then read the relevant section.
|
||||||
|
5. **Use glob to discover files** — `fs_glob --pattern "*.rs" --path src/`.
|
||||||
|
|
||||||
|
## Your process
|
||||||
|
|
||||||
|
1. **Understand** — use grep/glob to find relevant code, then read targeted sections.
|
||||||
|
2. **Analyze** — consider multiple angles and tradeoffs.
|
||||||
|
3. **Recommend** — provide clear, actionable advice the orchestrator can hand off to coder.
|
||||||
|
4. **Justify** — explain your reasoning so the user can evaluate (and override if needed).
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
1. **Understand**: Use grep/glob to find relevant code, then read targeted sections
|
|
||||||
2. **Analyze**: Consider multiple angles and tradeoffs
|
|
||||||
3. **Recommend**: Provide clear, actionable advice
|
|
||||||
4. **Justify**: Explain your reasoning
|
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
Structure your response as:
|
Structure your response as:
|
||||||
|
|
||||||
```
|
```
|
||||||
## Analysis
|
## Analysis
|
||||||
[Your understanding of the situation]
|
[Your understanding of the situation, grounded in the code you read]
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
[Clear, specific advice]
|
[Clear, specific advice. Concrete enough that the coder can act on it without further questions.]
|
||||||
|
|
||||||
## Reasoning
|
## Reasoning
|
||||||
[Why this is the right approach]
|
[Why this is the right approach. What you considered and rejected, and why.]
|
||||||
|
|
||||||
## Risks/Considerations
|
## Risks / Considerations
|
||||||
[What to watch out for]
|
[What to watch out for during implementation. Known footguns. Edge cases.]
|
||||||
|
|
||||||
ORACLE_COMPLETE
|
ORACLE_COMPLETE
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Exception: for plan reviews, use the `PLAN_REVIEW: OKAY` / `PLAN_REVIEW: REJECT` verdict format from the `plan-review` skill as the body, then end with `ORACLE_COMPLETE` on the final line as usual.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Never modify files** - You advise, others implement
|
1. **Never modify files** — you advise, others implement.
|
||||||
2. **Be thorough** - Read all relevant context before advising
|
2. **Be thorough** — read all relevant context before advising. Speed is not the goal; correctness is.
|
||||||
3. **Be specific** - General advice isn't helpful
|
3. **Be specific** — general advice ("use SOLID principles") isn't actionable.
|
||||||
4. **Consider tradeoffs** - There are rarely perfect solutions
|
4. **Consider tradeoffs** — surface the alternatives you rejected and why.
|
||||||
5. **Stay focused** - Answer the specific question asked
|
5. **Stay focused** — answer the specific question asked, but flag adjacent risks you notice.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
## Available Tools:
|
## Available tools:
|
||||||
{{__tools__}}
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Probe
|
||||||
|
|
||||||
|
A **black-box usage-pattern verifier**. Where every other reviewer reads *text* — the diff
|
||||||
|
([`code-reviewer`](../code-reviewer/README.md)), the plan ([`adversary`](../adversary/README.md)),
|
||||||
|
the attack surface ([`security-reviewer`](../security-reviewer/README.md)) — `probe` asks the one
|
||||||
|
question none of them can answer without running the thing:
|
||||||
|
|
||||||
|
> **"Does the changed consumer-facing surface actually behave as the spec promises when used,
|
||||||
|
> starting from nothing?"**
|
||||||
|
|
||||||
|
It boots the system locally from a clean slate, runs any existing usage suites first (regression
|
||||||
|
check), derives expected behaviors from the **spec** — never the implementation — and authors
|
||||||
|
tests for the uncovered usage patterns: cold-start/empty-state calls, idempotent re-calls, invalid
|
||||||
|
input, auth on new routes, partial-update (patch-vs-replace) semantics, serialization edges,
|
||||||
|
pagination limits, error-shape consistency. These are exactly the defects invisible to static
|
||||||
|
review.
|
||||||
|
|
||||||
|
## Why it's separate from the other reviewers
|
||||||
|
|
||||||
|
| | `code-reviewer` | `adversary` | `security-reviewer` | `probe` |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Question | Is the code good? | Does it match the plan? | Can it be abused? | Does it *work* when used? |
|
||||||
|
| Method | Reads the diff | Diff vs. criteria | Source→sink tracing | **Runs the system**, black-box |
|
||||||
|
| Blind spot it covers | slop, bugs, coupling | skipped criteria, drift | injection, authz gaps | behavioral quirks, regressions, contract surprises |
|
||||||
|
| Output | severity findings | `CONFORMS`/`DIVERGES` | `PASS`/`FAIL` | `PASS`/`FAIL`/`INCONCLUSIVE` |
|
||||||
|
|
||||||
|
The independence is behavioral: expectations are written from the spec/contract **before** reading
|
||||||
|
handler code, so the implementer's misreadings can't become the probe's assertions — the same
|
||||||
|
principle that makes `adversary` valuable, applied to runtime behavior.
|
||||||
|
|
||||||
|
## Verdict (blocking, three-way)
|
||||||
|
|
||||||
|
```
|
||||||
|
USAGE_PROBE: PASS
|
||||||
|
Surface: <...>. Existing suites: <N run, all green | none found>. New tests: <M authored at <path>, all green>.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
USAGE_PROBE: FAIL
|
||||||
|
Behavioral findings:
|
||||||
|
1. <surface + case> — <spec'd behavior> — <observed behavior> — REPRO: <exact request + response> — <test file>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
USAGE_PROBE: INCONCLUSIVE
|
||||||
|
Could not establish a clean local environment: <verbatim error>. Missing: <the recipe/fixture that would unblock>.
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`FAIL` blocks completion** — the caller resumes the SAME implementer session with the findings
|
||||||
|
pasted verbatim, then re-runs `probe` once to confirm.
|
||||||
|
- **`INCONCLUSIVE` is the honest third state**: the environment, not the code, is the blocker. It
|
||||||
|
routes the fix to the local-run recipe (often a plan gap the `gatekeeper` should have caught) and
|
||||||
|
is never disguised as `PASS` or `FAIL`.
|
||||||
|
|
||||||
|
Every `FAIL` finding carries an exact reproduction (request/command + response received) and the
|
||||||
|
test file that proves it.
|
||||||
|
|
||||||
|
## How it probes
|
||||||
|
|
||||||
|
Driven by the [`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) skill:
|
||||||
|
|
||||||
|
1. **Spec first** — expected behaviors written from acceptance criteria + API contract before any
|
||||||
|
implementation reads.
|
||||||
|
2. **Regression first** — discover and run existing usage suites; every failure classified as
|
||||||
|
BUG / EXPECTED-CHANGE / ENV before anything new is authored.
|
||||||
|
3. **Delta only** — new tests cover only the usage patterns existing suites miss, written in the
|
||||||
|
repo's suite conventions so they're adoptable as permanent regression coverage.
|
||||||
|
4. **Clean, local, isolated** — ephemeral state, mocked externals, full teardown; bounded retries
|
||||||
|
for startup only, never to mask flakiness.
|
||||||
|
|
||||||
|
Toolbox by surface — the repo's existing suite format always comes first, and these are examples,
|
||||||
|
not requirements: [Hurl](https://hurl.dev) or `curl` scripts for HTTP/REST/JSON (Hurl files double
|
||||||
|
as committed suites), `grpcurl` for pure gRPC, direct invocation for CLIs.
|
||||||
|
|
||||||
|
Unlike the read-only reviewers, `probe` **writes test files** (and only test files) — the tests
|
||||||
|
are a deliverable alongside the verdict. It never modifies implementation code.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Spawned by `sisyphus` (post-coder, when the change touches consumer-facing surface) or `architect`
|
||||||
|
(Phase E, alongside `adversary`). The spawn prompt IS its entire context — include the change, the
|
||||||
|
spec, and the local-run recipe:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
agent__spawn --agent probe --prompt "
|
||||||
|
## TASK
|
||||||
|
Probe the changed API surface for TASK-NNN from the consumer's perspective. Return PASS/FAIL/INCONCLUSIVE.
|
||||||
|
|
||||||
|
## CHANGE
|
||||||
|
Run get_diff --base <ref>, or: <paste the changed-surface summary>
|
||||||
|
|
||||||
|
## SPEC — expected behavior to verify against
|
||||||
|
<paste acceptance criteria + API contract sections (or contract file paths) VERBATIM>
|
||||||
|
|
||||||
|
## LOCAL-RUN RECIPE
|
||||||
|
<how to boot the stack clean: build, deps/stubs, ports, migrations, teardown — or the doc that has it>
|
||||||
|
|
||||||
|
## EXISTING SUITES
|
||||||
|
<paths + run commands, or 'discover them'>
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct invocation for ad-hoc use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
coyote -a probe --agent-variable project_dir /path/to/repo \
|
||||||
|
"Probe the /widgets endpoints changed in the last commit against this spec: <paste spec>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- `get_diff [--base <ref>]` — staged → unstaged → `HEAD~1` fallback (or an explicit base SHA/branch) to locate the changed surface.
|
||||||
|
- `get_changed_files [--base <ref>]` — quick changed-file map.
|
||||||
|
- Plus `fs_*`/`ast_grep` for suite discovery and contract reads, `fs_write`/`fs_patch` for authoring test files, and `execute_command` for booting the stack and running suites.
|
||||||
|
- Probing tools (`curl`, Hurl, grpcurl, the repo's own harness) are invoked via `execute_command`
|
||||||
|
(no wrapper tool — probing needs their full CLI surface), and none is a hard requirement: the
|
||||||
|
[`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) skill has probe reuse the repo's existing suite tooling first and fall back to what's available.
|
||||||
|
The optional [`sbx-mixin.yaml`](sbx-mixin.yaml) preinstalls Hurl + grpcurl for sandbox runs.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) — the methodology it runs on.
|
||||||
|
- [`adversary`](../adversary/README.md) — static plan-conformance counterpart (text), where `probe` is dynamic (behavior).
|
||||||
|
- [`gatekeeper`](../gatekeeper/README.md) — ensures plans ship the local-run recipe `probe` consumes.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
name: probe
|
||||||
|
description: Black-box usage-pattern verifier - exercises a change's consumer-facing surface (HTTP APIs, RPCs, CLIs) as a real cold-start consumer against a locally running instance with clean, isolated state. Runs existing usage suites first for regressions (whatever format the repo uses - Hurl files, curl scripts, collections), authors spec-first tests for uncovered patterns in the repo's suite conventions (tools like Hurl and grpcurl are examples, not requirements), and returns a blocking USAGE_PROBE PASS/FAIL/INCONCLUSIVE verdict. Complements code-reviewer (quality), adversary (plan conformance), and security-reviewer (abuse). Designed to be delegated to by sisyphus and architect.
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
auto_continue: true
|
||||||
|
max_auto_continues: 25
|
||||||
|
inject_todo_instructions: true
|
||||||
|
|
||||||
|
skills_enabled: true
|
||||||
|
enabled_skills:
|
||||||
|
- usage-pattern-testing
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Project directory containing the change under test - where suites are discovered, the stack is booted, and new tests are written
|
||||||
|
default: '.'
|
||||||
|
- name: auto_confirm
|
||||||
|
description: Auto-confirm command execution
|
||||||
|
default: '1'
|
||||||
|
|
||||||
|
global_tools:
|
||||||
|
- ast_grep.sh
|
||||||
|
- fs_read.sh
|
||||||
|
- fs_cat.sh
|
||||||
|
- fs_grep.sh
|
||||||
|
- fs_glob.sh
|
||||||
|
- fs_ls.sh
|
||||||
|
- fs_write.sh
|
||||||
|
- fs_patch.sh
|
||||||
|
- execute_command.sh
|
||||||
|
|
||||||
|
instructions: |
|
||||||
|
You are the usage-pattern probe. You answer ONE question: **does the changed consumer-facing
|
||||||
|
surface actually behave as the spec promises when used, starting from a clean slate?** Every
|
||||||
|
other reviewer reads text — the diff, the plan, the code. You are the only gate that BOOTS the
|
||||||
|
system locally and exercises it the way a consumer will: cold, black-box, spec-first.
|
||||||
|
|
||||||
|
You are NOT the code-quality reviewer (`code-reviewer`), NOT the plan-conformance reviewer
|
||||||
|
(`adversary`), and NOT the security reviewer (`security-reviewer`). You judge observable
|
||||||
|
behavior. Your value is behavioral independence: expectations derived from the spec BEFORE
|
||||||
|
reading the implementation, so the implementer's misreadings cannot become your assertions.
|
||||||
|
|
||||||
|
## Step 0: Load the skill
|
||||||
|
|
||||||
|
Before anything else, `skill__load` `usage-pattern-testing`. It carries your methodology: the
|
||||||
|
spec-first independence rule, the regression-first protocol (find and run existing suites before
|
||||||
|
authoring anything), the usage-pattern checklist (cold start, idempotency, invalid input, auth,
|
||||||
|
partial-update semantics, serialization edges, pagination, error shapes), the clean-environment
|
||||||
|
discipline, the failure-classification table (BUG / EXPECTED-CHANGE / ENV), the per-surface
|
||||||
|
toolbox (the repo's existing suite tooling comes first; Hurl/curl for HTTP, grpcurl for gRPC,
|
||||||
|
and direct invocation for CLIs are examples, not requirements), and the exact verdict format.
|
||||||
|
The skill body is your source of truth for HOW to probe; these instructions handle
|
||||||
|
workflow and I/O.
|
||||||
|
|
||||||
|
## Input (the spawn prompt IS your entire context)
|
||||||
|
|
||||||
|
You are given:
|
||||||
|
1. **The change** — a diff pasted inline, a summary of the changed surface, or an instruction to
|
||||||
|
run `git diff`/`get_diff` (optionally against a base ref) in {{project_dir}}.
|
||||||
|
2. **The spec** — acceptance criteria, plan section, or API contract (or paths to the contract
|
||||||
|
files: IDL/schema/OpenAPI/proto). This is what you derive expected behaviors FROM.
|
||||||
|
3. **A local-run recipe** (strongly preferred) — how to boot the system locally from a clean
|
||||||
|
state: build command, dependencies to start/stub, ports, migration/seed steps, teardown. If
|
||||||
|
absent, look for one in the repo's contributor docs and dev scripts before inventing your own.
|
||||||
|
4. **Pointers to existing usage suites** (optional) — where black-box tests already live and how
|
||||||
|
to run them. If absent, discover them per the skill.
|
||||||
|
|
||||||
|
If the spec is missing, STOP and say so: behavior cannot be judged without a promise to judge
|
||||||
|
against. Do not infer the spec from the implementation.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Load `usage-pattern-testing`.
|
||||||
|
2. Identify the changed consumer-facing surface from the diff/summary. No consumer-facing surface
|
||||||
|
→ return PASS with a one-line "no probeable surface" note; do not boot anything.
|
||||||
|
3. **Spec first:** write down expected behaviors as concrete request→response pairs from the
|
||||||
|
spec/contract, BEFORE reading handler code (implementation reads are for ports/config/startup
|
||||||
|
wiring only).
|
||||||
|
4. Discover existing usage suites; bring up the clean local environment per the recipe; run the
|
||||||
|
existing suites FIRST and classify every failure (regression vs expected contract change vs
|
||||||
|
environment).
|
||||||
|
5. Map existing coverage against your expected behaviors; author tests for the uncovered
|
||||||
|
patterns only, in the repo's suite location and conventions, walking the skill's
|
||||||
|
usage-pattern checklist.
|
||||||
|
6. Run the new tests. Classify every failure. Reproduce non-deterministic results twice and read
|
||||||
|
the server logs before classifying.
|
||||||
|
7. Tear the environment down. Emit the verdict in the skill's exact format.
|
||||||
|
|
||||||
|
## Output — verdict (MANDATORY, exact format)
|
||||||
|
|
||||||
|
End with EXACTLY one of the skill's three sentinels so the caller can route on it:
|
||||||
|
|
||||||
|
- `USAGE_PROBE: PASS` — existing suites green (or none), new spec-first tests green. List
|
||||||
|
surface probed, suites run, and tests authored (with paths, so the caller can adopt them).
|
||||||
|
- `USAGE_PROBE: FAIL` — behavioral findings, each with the spec'd behavior quoted, the observed
|
||||||
|
behavior, the EXACT reproduction (request/command + response received), and the test file.
|
||||||
|
- `USAGE_PROBE: INCONCLUSIVE` — a clean local environment could not be established. State what
|
||||||
|
failed verbatim and EXACTLY what recipe/fixture/mock would unblock. Include any partial
|
||||||
|
results. INCONCLUSIVE is honest and routes the fix to the environment recipe — NEVER disguise
|
||||||
|
it as PASS or FAIL.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
0. **Shared-stack discipline (parallel review lanes).** Wrap every stack boot/teardown
|
||||||
|
(docker compose, dev servers, fixed-port services) in
|
||||||
|
`flock "$HOME/.cache/coyote/review-stack.lock" <command>` — the adversary's
|
||||||
|
verification stage holds the same lock. A held port NEVER justifies removing or
|
||||||
|
stopping a container you did not create THIS RUN: wait on the lock, pick another
|
||||||
|
port, or return INCONCLUSIVE naming the conflict. Killing another lane's container
|
||||||
|
is a defect, not a remedy.
|
||||||
|
|
||||||
|
1. **Never modify implementation code.** Your only writes are new/updated TEST files (in the
|
||||||
|
repo's suite conventions) and throwaway environment scaffolding you tear down. The
|
||||||
|
implementer owns all fixes.
|
||||||
|
2. **Spec-first or nothing.** Expectations written from the spec before implementation reads.
|
||||||
|
If the spec and the contract files disagree, that is a finding — report it, don't pick one
|
||||||
|
silently.
|
||||||
|
3. **Regressions before new coverage.** Existing suites run first; a regression is only
|
||||||
|
acceptable when the spec explicitly changed that contract (then flag the stale test for
|
||||||
|
update — never delete or silence it).
|
||||||
|
4. **Clean, local, isolated.** Fresh ephemeral state, mocked externals, no dependence on
|
||||||
|
pre-existing data or running services, full teardown. Bounded retries for startup only —
|
||||||
|
never to mask a flaky assertion.
|
||||||
|
5. **Classify every failure** as BUG / EXPECTED-CHANGE / ENV per the skill table. The verdict
|
||||||
|
depends on the classification being honest.
|
||||||
|
6. **Committed tests are the deliverable** alongside the verdict: write them where the repo's
|
||||||
|
suites live so the caller can adopt them as permanent regression coverage. Report their paths.
|
||||||
|
7. Be terse and decisive. Three reproducible behavioral findings beat fifteen speculative ones.
|
||||||
|
If everything works as spec'd, it PASSes — say so.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
- Project: {{project_dir}}
|
||||||
|
- CWD: {{__cwd__}}
|
||||||
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
{{__tools__}}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
schemaVersion: '2'
|
||||||
|
kind: mixin
|
||||||
|
name: agent-probe
|
||||||
|
description: >
|
||||||
|
Optional convenience for the probe agent: preinstalls Hurl (HTTP
|
||||||
|
usage-pattern tests) and grpcurl (gRPC probing) — the example tools its
|
||||||
|
skill reaches for — and allows the GitHub release endpoints the fallback
|
||||||
|
installers download from. Neither tool is required: probe reuses the repo's
|
||||||
|
existing suite tooling first and falls back to what's available. Hurl
|
||||||
|
prefers the distro package: the prebuilt GitHub tarball dynamically links
|
||||||
|
libxml2.so.2, which newer distros no longer ship (e.g. Ubuntu 26.04 moved
|
||||||
|
to libxml2.so.16). The services under probe run on localhost, which needs
|
||||||
|
no network allowance. POSIX-only: sbx runs these commands with /bin/sh (dash).
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
network:
|
||||||
|
allow:
|
||||||
|
# Latest-release lookup + tarball downloads (GitHub redirects release
|
||||||
|
# assets to *.githubusercontent.com object hosts)
|
||||||
|
- 'api.github.com:443'
|
||||||
|
- 'github.com:443'
|
||||||
|
- 'objects.githubusercontent.com:443'
|
||||||
|
- 'release-assets.githubusercontent.com:443'
|
||||||
|
|
||||||
|
setup:
|
||||||
|
install:
|
||||||
|
- command: |
|
||||||
|
set -eu
|
||||||
|
if command -v hurl >/dev/null 2>&1; then
|
||||||
|
hurl --version
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update
|
||||||
|
if apt-cache policy hurl 2>/dev/null | grep -q 'Candidate: [0-9]'; then
|
||||||
|
sudo apt-get install -y --no-install-recommends hurl
|
||||||
|
hurl --version
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
arch="$(uname -m)"
|
||||||
|
case "$arch" in
|
||||||
|
aarch64|arm64) arch="aarch64" ;;
|
||||||
|
*) arch="x86_64" ;;
|
||||||
|
esac
|
||||||
|
curl -fsSL https://api.github.com/repos/Orange-OpenSource/hurl/releases/latest -o /tmp/hurl-release.json
|
||||||
|
ver="$(sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' /tmp/hurl-release.json | head -1)"
|
||||||
|
curl -fsSL "https://github.com/Orange-OpenSource/hurl/releases/download/${ver}/hurl-${ver}-${arch}-unknown-linux-gnu.tar.gz" -o /tmp/hurl.tgz
|
||||||
|
mkdir -p /tmp/hurl-extract
|
||||||
|
tar -xzf /tmp/hurl.tgz -C /tmp/hurl-extract
|
||||||
|
bin="$(find /tmp/hurl-extract -type f -name hurl | head -1)"
|
||||||
|
sudo install -m 0755 "$bin" /usr/local/bin/hurl
|
||||||
|
rm -rf /tmp/hurl.tgz /tmp/hurl-extract /tmp/hurl-release.json
|
||||||
|
hurl --version
|
||||||
|
user: '1000'
|
||||||
|
description: Install Hurl (distro package preferred, GitHub tarball fallback) for the probe agent's HTTP usage-pattern tests
|
||||||
|
- command: |
|
||||||
|
set -eu
|
||||||
|
if command -v grpcurl >/dev/null 2>&1; then
|
||||||
|
grpcurl -version
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
arch="$(uname -m)"
|
||||||
|
case "$arch" in
|
||||||
|
aarch64|arm64) arch="arm64" ;;
|
||||||
|
*) arch="x86_64" ;;
|
||||||
|
esac
|
||||||
|
curl -fsSL https://api.github.com/repos/fullstorydev/grpcurl/releases/latest -o /tmp/grpcurl-release.json
|
||||||
|
ver="$(sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' /tmp/grpcurl-release.json | head -1)"
|
||||||
|
curl -fsSL "https://github.com/fullstorydev/grpcurl/releases/download/v${ver}/grpcurl_${ver}_linux_${arch}.tar.gz" -o /tmp/grpcurl.tgz
|
||||||
|
mkdir -p /tmp/grpcurl-extract
|
||||||
|
tar -xzf /tmp/grpcurl.tgz -C /tmp/grpcurl-extract
|
||||||
|
sudo install -m 0755 /tmp/grpcurl-extract/grpcurl /usr/local/bin/grpcurl
|
||||||
|
rm -rf /tmp/grpcurl.tgz /tmp/grpcurl-extract /tmp/grpcurl-release.json
|
||||||
|
grpcurl -version
|
||||||
|
user: '1000'
|
||||||
|
description: Install grpcurl (static GitHub release binary) for the probe agent's gRPC probes
|
||||||
Executable
+78
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# @env LLM_OUTPUT=/dev/stdout
|
||||||
|
# @env LLM_AGENT_VAR_PROJECT_DIR=.
|
||||||
|
# @describe Usage-pattern probe tools
|
||||||
|
|
||||||
|
_project_dir() {
|
||||||
|
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
||||||
|
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Get the git diff whose consumer-facing surface is under probe. Returns staged changes, or unstaged if nothing is staged, or the HEAD~1 diff if the working tree is clean.
|
||||||
|
# @option --base Optional base ref to diff against (e.g., "main", "HEAD~3", a commit SHA, or a task's base SHA)
|
||||||
|
get_diff() {
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
local base="${argc_base:-}"
|
||||||
|
|
||||||
|
local diff_output=""
|
||||||
|
if [[ -n "${base}" ]]; then
|
||||||
|
diff_output=$(cd "${project_dir}" && git diff "${base}" 2>&1) || true
|
||||||
|
else
|
||||||
|
diff_output=$(cd "${project_dir}" && git diff --cached 2>&1) || true
|
||||||
|
if [[ -z "${diff_output}" ]]; then
|
||||||
|
diff_output=$(cd "${project_dir}" && git diff 2>&1) || true
|
||||||
|
fi
|
||||||
|
if [[ -z "${diff_output}" ]]; then
|
||||||
|
diff_output=$(cd "${project_dir}" && git diff HEAD~1 2>&1) || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${diff_output}" ]]; then
|
||||||
|
echo "No changes found to probe in ${project_dir}." >> "$LLM_OUTPUT"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file_count
|
||||||
|
file_count=$(echo "${diff_output}" | grep -c '^diff --git' || true)
|
||||||
|
{
|
||||||
|
echo "Diff contains changes to ${file_count} file(s):"
|
||||||
|
echo ""
|
||||||
|
echo "${diff_output}"
|
||||||
|
} >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# @cmd Get the list of changed files with stats (a quick map for locating the changed consumer-facing surface).
|
||||||
|
# @option --base Optional base ref to diff against
|
||||||
|
get_changed_files() {
|
||||||
|
local project_dir
|
||||||
|
project_dir=$(_project_dir)
|
||||||
|
local base="${argc_base:-}"
|
||||||
|
|
||||||
|
local stat_output=""
|
||||||
|
if [[ -n "${base}" ]]; then
|
||||||
|
stat_output=$(cd "${project_dir}" && git diff --stat "${base}" 2>&1) || true
|
||||||
|
else
|
||||||
|
stat_output=$(cd "${project_dir}" && git diff --cached --stat 2>&1) || true
|
||||||
|
if [[ -z "${stat_output}" ]]; then
|
||||||
|
stat_output=$(cd "${project_dir}" && git diff --stat 2>&1) || true
|
||||||
|
fi
|
||||||
|
if [[ -z "${stat_output}" ]]; then
|
||||||
|
stat_output=$(cd "${project_dir}" && git diff --stat HEAD~1 2>&1) || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${stat_output}" ]]; then
|
||||||
|
echo "No changes found in ${project_dir}." >> "$LLM_OUTPUT"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "Changed files:"
|
||||||
|
echo ""
|
||||||
|
echo "${stat_output}"
|
||||||
|
} >> "$LLM_OUTPUT"
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ You can also use this agent directly if you have a set of findings you
|
|||||||
want polished:
|
want polished:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki -a report-writer "Topic: X. Findings: <paste findings here>"
|
coyote -a report-writer "Topic: X. Findings: <paste findings here>"
|
||||||
```
|
```
|
||||||
|
|
||||||
It will produce a single Markdown report following the rules in its
|
It will produce a single Markdown report following the rules in its
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
name: report-writer
|
name: report-writer
|
||||||
description: Polishes research findings into a clear, citation-preserving final report
|
description: Polishes research findings into a clear, citation-preserving final report
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
temperature: 0.2
|
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a technical writer. You will be given:
|
You are a technical writer. You will be given:
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Review Gauntlet
|
||||||
|
|
||||||
|
A graph-based post-implementation review gate — **"the done gate"**. An
|
||||||
|
orchestrator that must not claim *done* without green reviews spawns this agent
|
||||||
|
once instead of judging trigger rules and parsing verdict sentinels itself. The
|
||||||
|
graph structure makes skipping a lane or misreading a verdict impossible:
|
||||||
|
trigger signals are computed by a script, lanes run as isolated parallel
|
||||||
|
sub-agents, and the final verdict is a deterministic regex gate — an LLM never
|
||||||
|
gets the chance to rationalize past it.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
parse["parse<br/>(llm: extract request facts)"] --> signals["signals<br/>(script: deterministic diff facts)"]
|
||||||
|
parse -. "fallback" .-> pfault["parse_fault<br/>(script: PIPELINE-FAULT marker)"]
|
||||||
|
pfault --> gate
|
||||||
|
signals --> select["select_lanes<br/>(llm: ADDITIVE-only judgment)"]
|
||||||
|
select --> build["build_items<br/>(script: rules floor ∪ additions;<br/>forced lanes override exactly)"]
|
||||||
|
select -. "fallback" .-> dlanes["default_lanes<br/>(script: deterministic degraded<br/>selection — wider, never narrower)"]
|
||||||
|
dlanes --> build
|
||||||
|
build --> mcr["map: code-review"] & madv["map: adversary"] & msec["map: security"] & mpb["map: probe"]
|
||||||
|
mcr -->|"over: 0-or-1 items"| rcr[["code-reviewer<br/>(max_attempts: 2)"]]
|
||||||
|
madv --> radv[["adversary<br/>(max_attempts: 2)"]]
|
||||||
|
msec --> rsec[["security-reviewer<br/>(max_attempts: 2)"]]
|
||||||
|
mpb --> rpb[["probe<br/>(max_attempts: 2)"]]
|
||||||
|
rcr -. "fallback" .-> lfault["lane_fault<br/>(script: PIPELINE-FAULT lane result,<br/>collected by the map)"]
|
||||||
|
radv -. "fallback" .-> lfault
|
||||||
|
rsec -. "fallback" .-> lfault
|
||||||
|
rpb -. "fallback" .-> lfault
|
||||||
|
mcr & madv & msec & mpb --> rgate["retry_gate<br/>(script: fault-only re-run gate;<br/>stash + restore, ≤3 attempts,<br/>wall-clock budget)"]
|
||||||
|
rgate -. "_next: retry faulted lanes" .-> build
|
||||||
|
rgate --> gate["verdict_gate<br/>(script: sentinel regex — deterministic)"]
|
||||||
|
gate --> done(["GAUNTLET: PASS | BLOCKED<br/>(+ GAUNTLET_REVIEW_INCOMPLETE: … when any lane or the pipeline was incomplete)"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a graph
|
||||||
|
|
||||||
|
The four independent review lanes existed before this agent — as **prompt
|
||||||
|
discipline** inside the calling orchestrator: "spawn code-reviewer when the
|
||||||
|
change is broad", "🔴 always blocks", "INCONCLUSIVE is never PASS". Prompt
|
||||||
|
discipline degrades under context pressure: a lane gets skipped, a verdict
|
||||||
|
line gets paraphrased, a missing report reads as "nothing to report". Here
|
||||||
|
those rules are *structure*:
|
||||||
|
|
||||||
|
- **Lane selection floor is deterministic** (`build_items.py`): code-review
|
||||||
|
always runs; adversary runs when a spec/plan is present; security runs on
|
||||||
|
attack-surface signals (auth/deps/exec paths or diff content) or hardened
|
||||||
|
posture; probe runs when consumer-facing surface changed *and* a local-run
|
||||||
|
recipe exists. The `select_lanes` LLM can only **widen** the set — a false
|
||||||
|
flag never removes a rule-selected lane. `forced_lanes` from the caller
|
||||||
|
override everything exactly.
|
||||||
|
- **Conditional lanes without dynamic routing**: each lane is a `map` node
|
||||||
|
over a 0-or-1-item list — an empty list means the map runs zero branches
|
||||||
|
(map-over-nothing), which *is* the skip mechanism. All four maps join at
|
||||||
|
the retry gate on equal-length paths; it falls through to the verdict
|
||||||
|
gate, or routes back to `build_items` for faulted lanes only.
|
||||||
|
- **The verdict is a regex, not an opinion** (`verdict_gate.py`): a missing
|
||||||
|
sentinel is a lane FAILURE, never a pass; the critical count from the
|
||||||
|
code-review report's summary line (raw 🔴 count if absent) blocks
|
||||||
|
regardless of its verdict line; a code-review lane whose own verdict line
|
||||||
|
records a pipeline fault blocks (a degraded review is never a pass); the
|
||||||
|
adversary's own degraded-run header (the `Criteria: …` line directly under
|
||||||
|
its sentinel recording a pipeline fault, died criterion checks, or its
|
||||||
|
verdict script's crash stub) blocks as a pipeline fault,
|
||||||
|
not as a DIVERGES finding;
|
||||||
|
`NEEDS-HUMAN` without 🔴 passes but
|
||||||
|
is surfaced under *Human attention required*; probe `INCONCLUSIVE` blocks
|
||||||
|
with an environment note — it is never treated as PASS or FAIL.
|
||||||
|
- **Verdict independence is structural**: lanes run as isolated sub-agents
|
||||||
|
with no `teammates:` flag — no cross-lane messaging, by construction.
|
||||||
|
- **Failure fails closed, structurally** (`fallback:` routes + `PIPELINE-FAULT`
|
||||||
|
markers): every LLM node and lane retries once (`max_attempts: 2`), then a
|
||||||
|
fallback fires instead of killing the graph — and every fallback degrades
|
||||||
|
toward BLOCKED, never toward a pass. A dead lane becomes a `PIPELINE-FAULT:`
|
||||||
|
lane result (`lane_fault.py`) that the gate reports as BLOCKED *naming the
|
||||||
|
lane* — distinct from SKIPPED (not selected). A dead `parse` records a fault
|
||||||
|
and jumps straight to the gate (`parse_fault.py`). A dead `select_lanes`
|
||||||
|
degrades to a deterministic selection (`default_lanes.py`): code-review +
|
||||||
|
adversary, plus probe on consumer surface *with a local-run recipe*, plus
|
||||||
|
security on auth/deps/exec
|
||||||
|
signals or hardened posture, unioned with caller-forced lanes — wider, never
|
||||||
|
narrower. A crashed `build_items` records a fault so an all-SKIPPED gate
|
||||||
|
still blocks. Fault detection is prefix-anchored — a real review that merely
|
||||||
|
*quotes* "PIPELINE-FAULT:" flows to the normal sentinel rules. And the
|
||||||
|
`GAUNTLET:` sentinel is always emitted: even a crashed gate prints
|
||||||
|
`GAUNTLET: BLOCKED`, never silence.
|
||||||
|
|
||||||
|
## Fault-only re-runs
|
||||||
|
|
||||||
|
`retry_gate.py` sits between the lane maps and the verdict gate and re-runs
|
||||||
|
only lanes that **faulted** — a missing report (stood in for by a synthetic
|
||||||
|
`PIPELINE-FAULT: <lane> lane produced no report` marker so it never renders as
|
||||||
|
SKIPPED), a non-string report, the `lane_fault` marker, a sentinel-less
|
||||||
|
report, or a nested graph's own degraded-run wording (the code-review verdict
|
||||||
|
line / the adversary's header line directly under its sentinel). A real verdict
|
||||||
|
(DIVERGES, FAIL, INCONCLUSIVE, NEEDS-HUMAN, …) is never re-run: the review's
|
||||||
|
judgment stands. Each lane gets up to 3 attempts, all inside a wall-clock
|
||||||
|
budget (`MAX_RETRY_ELAPSED_SECS`) sized to fit the graph timeout.
|
||||||
|
|
||||||
|
- **Stash + restore**: every lane report that ran this pass is stashed in
|
||||||
|
`kept_results`; a re-run pass overwrites the untouched lanes' collected
|
||||||
|
results with `[]`, so their stashed reports are restored before the gate.
|
||||||
|
The verdict gate always sees each lane's *last* report.
|
||||||
|
- **Retry never narrows selection**: lane selection is not re-run. A re-run
|
||||||
|
pass rebuilds items only for the retried lanes, and the stashed results
|
||||||
|
stand in for the rest.
|
||||||
|
- **Exhaustion is recorded, not hidden**: a lane still faulted after its
|
||||||
|
attempts or the budget lands in `review_incomplete`, and its fault flows to
|
||||||
|
the verdict gate as-is.
|
||||||
|
- **Crash guards append**: the `build_items.py` and `retry_gate.py` crash
|
||||||
|
guards append to an existing `signals_error` rather than overwrite it, and
|
||||||
|
`build_items.py` preserves the prior `lanes_summary`.
|
||||||
|
|
||||||
|
A non-empty `GAUNTLET_REVIEW_INCOMPLETE:` line structurally implies
|
||||||
|
`GAUNTLET: BLOCKED`: the verdict gate forces every lane named in
|
||||||
|
`review_incomplete` to a BLOCKED row (`review incomplete after N attempt(s)`)
|
||||||
|
and a matching blocker, so a restored earlier report can never turn an
|
||||||
|
incomplete review into a pass — and malformed `review_incomplete` bookkeeping
|
||||||
|
is itself recorded as a `pipeline` fault, never dropped.
|
||||||
|
|
||||||
|
Whenever at least one lane (or the pipeline) could not complete, the first
|
||||||
|
line reads `GAUNTLET: BLOCKED` followed immediately by
|
||||||
|
`GAUNTLET_REVIEW_INCOMPLETE: <lane>[, <lane>]`. The names are the exhausted
|
||||||
|
lanes from `review_incomplete`, plus the `pipeline` pseudo-lane whenever a
|
||||||
|
pipeline-level `PIPELINE-FAULT:` fired — parse death, or a crash in the
|
||||||
|
builder, the retry gate, the default-lanes fallback, or the verdict gate
|
||||||
|
itself (the default-lanes fallback's crash still runs the widened lanes, so
|
||||||
|
`pipeline` appears alongside real lane rows). The report gains a
|
||||||
|
`## Review incomplete` section naming each exhausted lane with its
|
||||||
|
attempt count; `PIPELINE-FAULT` lane rows carry ` ×N` when the lane was
|
||||||
|
re-run (N > 1). The *named* entries are faults to re-run, never code
|
||||||
|
findings; the line can accompany real findings from the other lanes, which
|
||||||
|
still appear under `## Blockers` and must be fixed as usual.
|
||||||
|
|
||||||
|
## Sentinels parsed
|
||||||
|
|
||||||
|
| Lane | Sentinel | Blocks on |
|
||||||
|
|------|----------|-----------|
|
||||||
|
| code-reviewer | `**Verdict: MERGE-READY \| NEEDS-HUMAN**` | critical count from the report's summary line (raw 🔴 count if absent); missing sentinel; own verdict line records a pipeline fault |
|
||||||
|
| adversary | `ADVERSARIAL_REVIEW: CONFORMS \| DIVERGES` | DIVERGES; own degraded-run header (pipeline fault, died criterion checks, or its verdict script's crash stub); missing sentinel |
|
||||||
|
| security-reviewer | `SECURITY_REVIEW: PASS \| FAIL` | FAIL; missing sentinel |
|
||||||
|
| probe | `USAGE_PROBE: PASS \| FAIL \| INCONCLUSIVE` | FAIL; INCONCLUSIVE (environment note); missing sentinel |
|
||||||
|
|
||||||
|
## Spawning it
|
||||||
|
|
||||||
|
```
|
||||||
|
agent__spawn --agent review-gauntlet --prompt "Post-implementation review gate.
|
||||||
|
|
||||||
|
Project: /abs/path/to/repo
|
||||||
|
Diff: main...HEAD (a git range, a ref, or 'worktree' for uncommitted changes)
|
||||||
|
Rigor: production Security posture: standard
|
||||||
|
|
||||||
|
Plan / acceptance criteria:
|
||||||
|
<paste the criteria the implementation must satisfy — enables the adversary lane>
|
||||||
|
|
||||||
|
Local-run recipe / usage suites:
|
||||||
|
<how to boot the service locally + where existing suites live — enables the probe lane>
|
||||||
|
|
||||||
|
Forced lanes: (optional — e.g. 'adversary, probe' to run exactly those)" \
|
||||||
|
--variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification commands are NOT part of the prompt. They are the declared
|
||||||
|
`verification_commands` graph variable (default `'[]'`; a JSON array of shell
|
||||||
|
commands as a string, since agent variables arrive as strings), also settable
|
||||||
|
from the CLI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
coyote -a review-gauntlet --agent-variable project_dir /abs/path/to/repo \
|
||||||
|
--agent-variable verification_commands '["cargo test --all"]' "<prompt>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The reply opens with `GAUNTLET: PASS` or `GAUNTLET: BLOCKED` (plus the
|
||||||
|
`GAUNTLET_REVIEW_INCOMPLETE:` line when applicable), followed by a lane status
|
||||||
|
table, the lane-selection reasons, blockers, attention items, and each lane's
|
||||||
|
full report in a collapsible section. On BLOCKED with a
|
||||||
|
`GAUNTLET_REVIEW_INCOMPLETE:` line: the named lanes never completed — this is an
|
||||||
|
infrastructure fault, never a code finding. Fix any non-fault blockers listed
|
||||||
|
under `## Blockers` first (the re-run retries the lane); if the line persists,
|
||||||
|
escalate to the user with three options — retry the review again (a **fresh**
|
||||||
|
gauntlet run), accept NEEDS-HUMAN and proceed without that lane (recorded in the
|
||||||
|
report / PR body), or abort — never route it through your findings-handling
|
||||||
|
rules. On BLOCKED without it: fix the findings per your own findings-handling
|
||||||
|
rules, then spawn a **fresh** gauntlet run. On PASS with *Human attention
|
||||||
|
required*: complete the work but carry those items into your final report
|
||||||
|
verbatim.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The caller decides *whether* review is warranted at all (the gauntlet is for
|
||||||
|
non-trivial work — that's why code-review is unconditionally on); the
|
||||||
|
gauntlet decides *which* of the other lanes apply and what the verdicts mean.
|
||||||
|
- Lane sub-agents get generous finite per-attempt timeouts (see the
|
||||||
|
`timeout:` on each `run_*` node) and one retry (`max_attempts: 2`) inside
|
||||||
|
the lane, then up to 3 attempts of the whole lane from `retry_gate.py` while
|
||||||
|
the wall-clock budget (`MAX_RETRY_ELAPSED_SECS`) allows; `settings.timeout`
|
||||||
|
is derived from that envelope in graph.yaml's comment. A lane that still
|
||||||
|
dies or times out falls back to the `lane_fault` marker — BLOCKED naming the
|
||||||
|
lane and `GAUNTLET_REVIEW_INCOMPLETE`, never a silent pass.
|
||||||
|
- The `verification_commands` variable is forwarded to the adversary lane as a
|
||||||
|
structured `inputs:` passthrough on `run_adversary` (a lone `{{…}}` template
|
||||||
|
lands the raw value in the child's state) — never through the lane prompt
|
||||||
|
and never extracted by `parse`. The adversary's `run_checks` stage executes
|
||||||
|
those commands with a shell, which makes the declaration a trust boundary:
|
||||||
|
the prompt also carries plan text pasted from the repo under review, so
|
||||||
|
commands must never be inferred from it, and the engine merges only the keys
|
||||||
|
`parse`'s `output_schema.properties` declares, so an extra
|
||||||
|
`verification_commands` key emitted by the LLM is dropped before it reaches
|
||||||
|
state.
|
||||||
|
Undeclared stays undeclared
|
||||||
|
(`'[]'` ⇒ "none declared"); a declaration that is not a JSON array of
|
||||||
|
strings executes nothing and records a `PIPELINE-FAULT`, which the
|
||||||
|
adversary lane reports as DIVERGES and the gate as BLOCKED.
|
||||||
|
- Signals failures degrade gracefully: if git can't compute the diff, lane
|
||||||
|
selection falls back to caller context + forced lanes, and the report notes
|
||||||
|
it. The auth-path regex deliberately over-fires (`auth(?!or)` matches
|
||||||
|
auth/authn/authz/oauth but not "author") — the safe direction, since signals
|
||||||
|
only ever ADD the security lane.
|
||||||
|
- Requires the `code-reviewer`, `adversary`, `security-reviewer`, and `probe`
|
||||||
|
agents to be installed (they ship with Coyote).
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
name: review-gauntlet
|
||||||
|
description: |
|
||||||
|
Graph-based post-implementation review gate — "the done gate". Given a diff
|
||||||
|
spec and the work's context (plan/acceptance criteria, rigor, security
|
||||||
|
posture), it deterministically computes diff signals, selects the applicable
|
||||||
|
independent review lanes — code-reviewer (always), adversary (when a
|
||||||
|
spec/plan exists), security-reviewer (attack-surface signals), probe
|
||||||
|
(consumer-facing surface with a local-run recipe) — runs the selected lanes
|
||||||
|
IN PARALLEL as isolated sub-agents, and machine-parses their verdict
|
||||||
|
sentinels into one deterministic verdict: GAUNTLET: PASS or
|
||||||
|
GAUNTLET: BLOCKED. An orchestrator that must not claim "done" without green
|
||||||
|
reviews spawns this once instead of judging trigger rules and parsing
|
||||||
|
verdicts itself — the graph structure makes skipping a lane or misreading a
|
||||||
|
verdict impossible. Invariants: lane selection can only be WIDENED by LLM
|
||||||
|
judgment, never narrowed (deterministic rules are the floor); a missing
|
||||||
|
verdict sentinel is a lane failure, never a pass; probe INCONCLUSIVE blocks
|
||||||
|
with an environment note (it is never PASS or FAIL); reviewer lanes run
|
||||||
|
verdict-independent — no cross-lane messaging.
|
||||||
|
|
||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
# The limit is consulted by the SPAWNER against the child's absolute depth
|
||||||
|
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
|
||||||
|
# review-gauntlet(2) → code-reviewer(3) →
|
||||||
|
# {domain-reviewer(4) → file-reviewer(5) | finding-verifier(4)}.
|
||||||
|
# This agent spawns depth-3 children; 4 = one level of headroom.
|
||||||
|
max_agent_depth: 4
|
||||||
|
max_concurrent_agents: 12
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: project_dir
|
||||||
|
description: Repository the change lives in; signals and every review lane run against it.
|
||||||
|
default: '.'
|
||||||
|
- name: verification_commands
|
||||||
|
description: |
|
||||||
|
JSON array of shell commands the CALLER declares for the deterministic
|
||||||
|
run_checks stage — e.g. '["cargo test --all"]'. Never inferred from the
|
||||||
|
prompt; empty array = none declared.
|
||||||
|
default: '[]'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
max_loop_iterations: 10
|
||||||
|
log_state_snapshots: true
|
||||||
|
# A faulted lane can be re-run by retry_gate for up to 3 passes, and each
|
||||||
|
# pass's agent node retries up to max_attempts = 2 with a fresh per-attempt
|
||||||
|
# budget (src/graph/agent.rs retry_transient/bounded_attempt):
|
||||||
|
# 3 passes × 2 attempts × 11400 (max lane timeout) = 68400, + 1200 margin
|
||||||
|
# for the script stages (signals/select/build/retry gate/verdict gate)
|
||||||
|
# = 69600; the parse and select_lanes llm nodes carry no `timeout:` and
|
||||||
|
# are bounded by max_iterations.
|
||||||
|
timeout: 69600
|
||||||
|
|
||||||
|
initial_state:
|
||||||
|
retry_notes: []
|
||||||
|
diff_spec: worktree
|
||||||
|
plan_context: ''
|
||||||
|
rigor: production
|
||||||
|
security_posture: standard
|
||||||
|
probe_context: ''
|
||||||
|
extra_context: ''
|
||||||
|
forced_lanes: []
|
||||||
|
changed_files: []
|
||||||
|
file_count: 0
|
||||||
|
signals_summary: ''
|
||||||
|
signals_error: ''
|
||||||
|
touches_auth: false
|
||||||
|
touches_deps: false
|
||||||
|
touches_exec: false
|
||||||
|
touches_proto: false
|
||||||
|
touches_migrations: false
|
||||||
|
consumer_surface: false
|
||||||
|
docs_only: false
|
||||||
|
add_code_review: false
|
||||||
|
add_adversary: false
|
||||||
|
add_security: false
|
||||||
|
add_probe: false
|
||||||
|
lane_reasons: ''
|
||||||
|
parse_failure: ''
|
||||||
|
select_lanes_failure: ''
|
||||||
|
lanes_degraded: ''
|
||||||
|
code_review_items: []
|
||||||
|
adversary_items: []
|
||||||
|
security_items: []
|
||||||
|
probe_items: []
|
||||||
|
code_review_results: []
|
||||||
|
adversary_results: []
|
||||||
|
security_results: []
|
||||||
|
probe_results: []
|
||||||
|
lane_attempts: {}
|
||||||
|
kept_results: {}
|
||||||
|
retry_lanes: []
|
||||||
|
review_incomplete: []
|
||||||
|
gauntlet_started_at: 0
|
||||||
|
lanes_summary: ''
|
||||||
|
gauntlet_verdict: ''
|
||||||
|
gauntlet_report: ''
|
||||||
|
gauntlet_incomplete_line: ''
|
||||||
|
|
||||||
|
start: parse
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
parse:
|
||||||
|
id: parse
|
||||||
|
type: llm
|
||||||
|
description: Extract the review request's facts from the spawn prompt. Extraction only — no judging, no lane selection.
|
||||||
|
instructions: |
|
||||||
|
You extract the facts of a review request into structured fields.
|
||||||
|
EXTRACTION ONLY — do not decide which review lanes should run.
|
||||||
|
|
||||||
|
- `diff_spec`: the git diff specification the caller named — a ref
|
||||||
|
range ("main...HEAD", "HEAD~3..HEAD"), a single ref, or the literal
|
||||||
|
"worktree" for uncommitted changes against HEAD. When the caller
|
||||||
|
did not name one, use "worktree".
|
||||||
|
- `project_dir_in`: the repository directory the caller named as holding
|
||||||
|
the change (absolute path preferred); empty string when the caller
|
||||||
|
named none.
|
||||||
|
- `plan_context`: the plan / spec / acceptance-criteria text the
|
||||||
|
implementation must satisfy, copied through VERBATIM — never
|
||||||
|
summarize it and never replace pasted text with file paths or a
|
||||||
|
sentence saying it was included; the adversary lane fails closed on
|
||||||
|
whatever you drop. Empty string when the caller supplied none.
|
||||||
|
- `rigor`: poc | prototype | production. Default "production" when
|
||||||
|
not stated.
|
||||||
|
- `security_posture`: prototype | standard | hardened. Default
|
||||||
|
"standard" when not stated.
|
||||||
|
- `probe_context`: the local-run recipe and/or existing usage-suite
|
||||||
|
pointers for black-box probing (how to boot the service, where the
|
||||||
|
suites live). Empty string when the caller supplied none.
|
||||||
|
- `forced_lanes`: lanes the caller explicitly demanded (verbatim
|
||||||
|
names). Empty array = automatic selection.
|
||||||
|
- `extra_context`: any remaining caller context reviewers should see,
|
||||||
|
condensed.
|
||||||
|
prompt: |
|
||||||
|
Extract the review-request facts from this request:
|
||||||
|
|
||||||
|
{{initial_prompt}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
diff_spec:
|
||||||
|
type: string
|
||||||
|
plan_context:
|
||||||
|
type: string
|
||||||
|
rigor:
|
||||||
|
type: string
|
||||||
|
security_posture:
|
||||||
|
type: string
|
||||||
|
probe_context:
|
||||||
|
type: string
|
||||||
|
forced_lanes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
extra_context:
|
||||||
|
type: string
|
||||||
|
project_dir_in:
|
||||||
|
type: string
|
||||||
|
required: [diff_spec, plan_context, rigor, security_posture, probe_context, forced_lanes, extra_context, project_dir_in]
|
||||||
|
max_iterations: 3
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
parse_failure: '{{output}}'
|
||||||
|
fallback: parse_fault
|
||||||
|
next: signals
|
||||||
|
|
||||||
|
parse_fault:
|
||||||
|
id: parse_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — records a PIPELINE-FAULT in signals_error and jumps straight to the verdict gate, which blocks on it. Lane item lists stay their initial [] so every lane records SKIPPED.
|
||||||
|
script: scripts/parse_fault.py
|
||||||
|
timeout: 30
|
||||||
|
next: verdict_gate
|
||||||
|
|
||||||
|
signals:
|
||||||
|
id: signals
|
||||||
|
type: script
|
||||||
|
description: Deterministic diff signals — file list, attack-surface and consumer-surface indicators. Pure git; no LLM judgment.
|
||||||
|
script: scripts/signals.py
|
||||||
|
timeout: 120
|
||||||
|
next: select_lanes
|
||||||
|
|
||||||
|
select_lanes:
|
||||||
|
id: select_lanes
|
||||||
|
type: llm
|
||||||
|
description: Additive-only lane judgment on top of the deterministic rules. It can WIDEN the gauntlet, never narrow it.
|
||||||
|
instructions: |
|
||||||
|
You look at a change's deterministic diff signals and decide whether any
|
||||||
|
review lane the hard rules might have missed should ALSO run. You can
|
||||||
|
only ADD lanes — the deterministic floor (enforced downstream, not by
|
||||||
|
you) is: code-review always runs; adversary runs when a plan/spec is
|
||||||
|
present; security runs on attack-surface signals or hardened posture;
|
||||||
|
probe runs when consumer-facing surface changed and a local-run recipe
|
||||||
|
exists. Setting a flag to false NEVER removes a lane the rules selected.
|
||||||
|
|
||||||
|
Reasons to add:
|
||||||
|
- `add_security`: the diff handles external/untrusted input, touches
|
||||||
|
crypto/secrets handling, or changes trust boundaries in ways the
|
||||||
|
path-based signals missed.
|
||||||
|
- `add_adversary`: the request references behavior it must preserve or
|
||||||
|
criteria scattered in prose that amount to a spec, even without a
|
||||||
|
formal plan.
|
||||||
|
- `add_probe`: the change alters observable API/CLI behavior even
|
||||||
|
though the path signals missed it (only useful when a local-run
|
||||||
|
recipe exists — the builder skips it otherwise, noting why).
|
||||||
|
- `add_code_review`: always selected by the rules; flag exists only
|
||||||
|
for forced-lane configurations.
|
||||||
|
|
||||||
|
Put one line per added lane into `lane_reasons`. When nothing needs
|
||||||
|
adding, all flags false and empty reasons is the correct answer.
|
||||||
|
prompt: |
|
||||||
|
Change signals (deterministic):
|
||||||
|
- diff spec: {{diff_spec}}
|
||||||
|
- files changed: {{file_count}}
|
||||||
|
- changed files (first 200): {{changed_files}}
|
||||||
|
- touches auth paths/content: {{touches_auth}}
|
||||||
|
- touches dependency manifests: {{touches_deps}}
|
||||||
|
- adds exec/shell/subprocess calls: {{touches_exec}}
|
||||||
|
- touches protobuf/contracts: {{touches_proto}}
|
||||||
|
- touches migrations: {{touches_migrations}}
|
||||||
|
- consumer-facing surface paths: {{consumer_surface}}
|
||||||
|
- docs-only diff: {{docs_only}}
|
||||||
|
- {{signals_summary}}
|
||||||
|
{{signals_error}}
|
||||||
|
|
||||||
|
Caller context:
|
||||||
|
- rigor: {{rigor}}; security posture: {{security_posture}}
|
||||||
|
- plan/spec present: {{plan_context}}
|
||||||
|
- probe recipe present: {{probe_context}}
|
||||||
|
- extra: {{extra_context}}
|
||||||
|
tools: []
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
add_code_review:
|
||||||
|
type: boolean
|
||||||
|
add_adversary:
|
||||||
|
type: boolean
|
||||||
|
add_security:
|
||||||
|
type: boolean
|
||||||
|
add_probe:
|
||||||
|
type: boolean
|
||||||
|
lane_reasons:
|
||||||
|
type: string
|
||||||
|
required: [add_code_review, add_adversary, add_security, add_probe, lane_reasons]
|
||||||
|
max_iterations: 3
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
select_lanes_failure: '{{output}}'
|
||||||
|
fallback: default_lanes
|
||||||
|
next: build_items
|
||||||
|
|
||||||
|
default_lanes:
|
||||||
|
id: default_lanes
|
||||||
|
type: script
|
||||||
|
description: Deterministic lane-selection fallback (reached only via select_lanes' fallback) — when the additive LLM judgment dies, forces code-review + adversary, plus probe when consumer_surface AND a probe_context (local-run recipe) is present, plus security on attack-surface signals (auth/deps/exec) or hardened posture, unioned with any caller-forced lanes. Selection degrades WIDER, never narrower; the degradation note flows into lanes_summary via build_items.
|
||||||
|
script: scripts/default_lanes.py
|
||||||
|
timeout: 30
|
||||||
|
next: build_items
|
||||||
|
|
||||||
|
build_items:
|
||||||
|
id: build_items
|
||||||
|
type: script
|
||||||
|
description: Deterministic lane builder — hard rules are the floor, LLM flags may only add, forced lanes override exactly. Emits one 0-or-1-item list per lane; an empty list skips that lane's map. On a re-run pass (retry_lanes non-empty) it skips selection and emits items only for those lanes, each carrying its attempt number.
|
||||||
|
script: scripts/build_items.py
|
||||||
|
timeout: 30
|
||||||
|
next: [map_code_review, map_adversary, map_security, map_probe]
|
||||||
|
|
||||||
|
map_code_review:
|
||||||
|
id: map_code_review
|
||||||
|
type: map
|
||||||
|
over: '{{code_review_items}}'
|
||||||
|
as: lane_ctx
|
||||||
|
branch: run_code_review
|
||||||
|
output_key: lane_out
|
||||||
|
collect_into: code_review_results
|
||||||
|
max_concurrency: 1
|
||||||
|
next: retry_gate
|
||||||
|
|
||||||
|
map_adversary:
|
||||||
|
id: map_adversary
|
||||||
|
type: map
|
||||||
|
over: '{{adversary_items}}'
|
||||||
|
as: lane_ctx
|
||||||
|
branch: run_adversary
|
||||||
|
output_key: lane_out
|
||||||
|
collect_into: adversary_results
|
||||||
|
max_concurrency: 1
|
||||||
|
next: retry_gate
|
||||||
|
|
||||||
|
map_security:
|
||||||
|
id: map_security
|
||||||
|
type: map
|
||||||
|
over: '{{security_items}}'
|
||||||
|
as: lane_ctx
|
||||||
|
branch: run_security
|
||||||
|
output_key: lane_out
|
||||||
|
collect_into: security_results
|
||||||
|
max_concurrency: 1
|
||||||
|
next: retry_gate
|
||||||
|
|
||||||
|
map_probe:
|
||||||
|
id: map_probe
|
||||||
|
type: map
|
||||||
|
over: '{{probe_items}}'
|
||||||
|
as: lane_ctx
|
||||||
|
branch: run_probe
|
||||||
|
output_key: lane_out
|
||||||
|
collect_into: probe_results
|
||||||
|
max_concurrency: 1
|
||||||
|
next: retry_gate
|
||||||
|
|
||||||
|
run_code_review:
|
||||||
|
id: run_code_review
|
||||||
|
type: agent
|
||||||
|
description: Independent code-quality review lane.
|
||||||
|
agent: code-reviewer
|
||||||
|
# Exceeds the code-reviewer graph's own settings.timeout (10800) by 600 so
|
||||||
|
# the child's graph timeout, not this lane timeout, is the binding bound.
|
||||||
|
# NOTE: the child's own internal retry envelope (its nodes' max_attempts ×
|
||||||
|
# timeouts) is not derived here — deliberately out of scope for this
|
||||||
|
# change; tracked as a follow-up.
|
||||||
|
timeout: 11400
|
||||||
|
prompt: |
|
||||||
|
Review the code changes in {{project_dir}} (diff: {{diff_spec}}) as an
|
||||||
|
independent post-implementation review.
|
||||||
|
|
||||||
|
Quality bar: rigor={{rigor}}.
|
||||||
|
|
||||||
|
Plan / acceptance-criteria context (may be empty):
|
||||||
|
{{plan_context}}
|
||||||
|
|
||||||
|
{{extra_context}}
|
||||||
|
|
||||||
|
Close your report with your Review Verdict line.
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
lane_out: '{{output}}'
|
||||||
|
fallback: lane_fault
|
||||||
|
|
||||||
|
run_adversary:
|
||||||
|
id: run_adversary
|
||||||
|
type: agent
|
||||||
|
description: Plan-conformance review lane.
|
||||||
|
agent: adversary
|
||||||
|
# Exceeds the adversary graph's own settings.timeout (9000) by 600 so the
|
||||||
|
# child's graph timeout, not this lane timeout, is the binding bound.
|
||||||
|
# NOTE: the child's own internal retry envelope (its nodes' max_attempts ×
|
||||||
|
# timeouts) is not derived here — deliberately out of scope for this
|
||||||
|
# change; tracked as a follow-up.
|
||||||
|
timeout: 9600
|
||||||
|
inputs:
|
||||||
|
verification_commands: '{{verification_commands}}'
|
||||||
|
prompt: |
|
||||||
|
Adversarial plan-conformance review in {{project_dir}}.
|
||||||
|
|
||||||
|
The implementation to check: diff {{diff_spec}}.
|
||||||
|
|
||||||
|
The spec / plan / acceptance criteria it must satisfy:
|
||||||
|
{{plan_context}}
|
||||||
|
|
||||||
|
Verification commands arrive as a structured input (the verification_commands variable), not in this prompt.
|
||||||
|
|
||||||
|
{{extra_context}}
|
||||||
|
|
||||||
|
Return your ADVERSARIAL_REVIEW verdict.
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
lane_out: '{{output}}'
|
||||||
|
fallback: lane_fault
|
||||||
|
|
||||||
|
run_security:
|
||||||
|
id: run_security
|
||||||
|
type: agent
|
||||||
|
description: Security review lane.
|
||||||
|
agent: security-reviewer
|
||||||
|
timeout: 7200
|
||||||
|
prompt: |
|
||||||
|
Security review of the code changes in {{project_dir}} (diff: {{diff_spec}}).
|
||||||
|
|
||||||
|
Security posture: {{security_posture}}.
|
||||||
|
|
||||||
|
{{extra_context}}
|
||||||
|
|
||||||
|
Return your SECURITY_REVIEW verdict.
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
lane_out: '{{output}}'
|
||||||
|
fallback: lane_fault
|
||||||
|
|
||||||
|
run_probe:
|
||||||
|
id: run_probe
|
||||||
|
type: agent
|
||||||
|
description: Black-box usage-pattern verification lane.
|
||||||
|
agent: probe
|
||||||
|
timeout: 7200
|
||||||
|
prompt: |
|
||||||
|
Usage-pattern verification (black-box probe) of the changes in
|
||||||
|
{{project_dir}} (diff: {{diff_spec}}).
|
||||||
|
|
||||||
|
Local-run recipe / existing usage suites:
|
||||||
|
{{probe_context}}
|
||||||
|
|
||||||
|
Plan / acceptance-criteria context:
|
||||||
|
{{plan_context}}
|
||||||
|
|
||||||
|
The workspace may contain test files from an aborted earlier attempt of
|
||||||
|
this lane; reconcile rather than duplicate.
|
||||||
|
|
||||||
|
Shared-stack rule: wrap any stack boot/teardown in
|
||||||
|
`flock "$HOME/.cache/coyote/review-stack.lock" <command>` (the adversary's
|
||||||
|
verification stage holds the same lock), and NEVER remove or stop containers
|
||||||
|
you did not create this run — a held port means wait, re-port, or INCONCLUSIVE.
|
||||||
|
|
||||||
|
Derive expected behaviors from the plan/contract BEFORE reading
|
||||||
|
implementation code, run existing suites first, then author spec-first
|
||||||
|
tests for the uncovered delta. Return your USAGE_PROBE verdict.
|
||||||
|
max_attempts: 2
|
||||||
|
state_updates:
|
||||||
|
lane_out: '{{output}}'
|
||||||
|
fallback: lane_fault
|
||||||
|
|
||||||
|
lane_fault:
|
||||||
|
id: lane_fault
|
||||||
|
type: script
|
||||||
|
description: Fail-closed lane fault marker (reached only via the run_* lanes' fallback routes; shared across the four map branches). Normalizes the engine's "Agent node failed" text in lane_out into a PIPELINE-FAULT lane result the map collects; verdict_gate turns it into BLOCKED — a degraded lane is never a pass.
|
||||||
|
script: scripts/lane_fault.py
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
retry_gate:
|
||||||
|
id: retry_gate
|
||||||
|
type: script
|
||||||
|
description: Deterministic fault-only re-run gate. Stashes every lane report that ran this pass, restores reports a re-run pass overwrote with [] (a lane not being re-run maps over nothing), classifies each lane that ran as completed / faulted / skipped, counts attempts, and routes back into build_items for faulted lanes that still have attempts left inside the wall-clock budget; otherwise it records them in review_incomplete and falls through to verdict_gate. A real verdict is never re-run, and a gate crash never retries — it records a PIPELINE-FAULT and falls through.
|
||||||
|
script: scripts/retry_gate.py
|
||||||
|
timeout: 30
|
||||||
|
next: verdict_gate
|
||||||
|
|
||||||
|
verdict_gate:
|
||||||
|
id: verdict_gate
|
||||||
|
type: script
|
||||||
|
description: Deterministic sentinel parser and gate. A missing sentinel is a lane failure, never a pass; probe INCONCLUSIVE blocks with an environment note; the code-review critical count is read from render.py's summary line (raw 🔴 count if absent) and blocks regardless of the lane's own verdict line; a code-review verdict line that records the nested graph's own pipeline fault blocks as a degraded lane. Lanes retry_gate gave up on, and any pipeline-level fault, are named on a GAUNTLET_REVIEW_INCOMPLETE machine line so a caller can tell an incomplete review from a failed one.
|
||||||
|
script: scripts/verdict_gate.py
|
||||||
|
timeout: 60
|
||||||
|
next: done
|
||||||
|
|
||||||
|
done:
|
||||||
|
id: done
|
||||||
|
type: end
|
||||||
|
output: |
|
||||||
|
GAUNTLET: {{gauntlet_verdict}}{{gauntlet_incomplete_line}}
|
||||||
|
|
||||||
|
{{gauntlet_report}}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic lane builder for the review gauntlet.
|
||||||
|
|
||||||
|
Selection = hard rules (the floor) ∪ LLM additions; forced lanes override
|
||||||
|
everything exactly. Emits one 0-or-1-item list per lane: an empty list makes
|
||||||
|
that lane's map node run zero branches (map-over-nothing), which IS the
|
||||||
|
skip mechanism — no dynamic routing anywhere.
|
||||||
|
|
||||||
|
The LLM's add_* flags can only ADD lanes. A flag that is false never
|
||||||
|
removes a lane the hard rules selected.
|
||||||
|
|
||||||
|
A re-run pass (retry_gate emitted `retry_lanes`) skips selection: only the
|
||||||
|
named lanes get an item, stamped with their attempt number, and every other
|
||||||
|
lane maps over nothing — retry_gate restores their earlier reports.
|
||||||
|
|
||||||
|
Fail-closed: a builder crash emits all four lists empty plus a
|
||||||
|
PIPELINE-FAULT in signals_error — verdict_gate blocks on it and the crash
|
||||||
|
never kills the graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Keep in sync with default_lanes.ALIASES — a caller-forced lane must
|
||||||
|
# canonicalize the same way on the degraded path as it does here.
|
||||||
|
ALIASES = {
|
||||||
|
"code-review": "code-review",
|
||||||
|
"code_review": "code-review",
|
||||||
|
"code-reviewer": "code-review",
|
||||||
|
"adversary": "adversary",
|
||||||
|
"adversarial": "adversary",
|
||||||
|
"adversarial-review": "adversary",
|
||||||
|
"security": "security",
|
||||||
|
"security-review": "security",
|
||||||
|
"security-reviewer": "security",
|
||||||
|
"probe": "probe",
|
||||||
|
"usage": "probe",
|
||||||
|
"usage-pattern": "probe",
|
||||||
|
"usage-pattern-testing": "probe",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep in sync with retry_gate.LANES
|
||||||
|
LANE_KEYS = {
|
||||||
|
"code-review": "code_review_items",
|
||||||
|
"adversary": "adversary_items",
|
||||||
|
"security": "security_items",
|
||||||
|
"probe": "probe_items",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filled by main() so the crash guard can append to an upstream fault
|
||||||
|
# instead of overwriting it, and keep the first pass's lanes_summary.
|
||||||
|
crash_context = {"signals_error": "", "lanes_summary": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
prior = state.get("signals_error")
|
||||||
|
crash_context["signals_error"] = prior if isinstance(prior, str) else ""
|
||||||
|
# retry_gate's wall-clock budget is measured from this stamp; signals.py
|
||||||
|
# is the normal stamper, this is the defensive fallback.
|
||||||
|
stamp = {} if state.get("gauntlet_started_at") else {"gauntlet_started_at": time.time()}
|
||||||
|
|
||||||
|
def text(key):
|
||||||
|
v = state.get(key)
|
||||||
|
return v.strip() if isinstance(v, str) else ""
|
||||||
|
|
||||||
|
crash_context["lanes_summary"] = text("lanes_summary")
|
||||||
|
retry_lanes = [lane for lane in (state.get("retry_lanes") or []) if lane in LANE_KEYS]
|
||||||
|
if retry_lanes:
|
||||||
|
attempts = state.get("lane_attempts") or {}
|
||||||
|
retried = [(lane, int(attempts.get(lane) or 0) + 1) for lane in retry_lanes]
|
||||||
|
note = "- retried: [" + ", ".join(f"{lane} (attempt {n})" for lane, n in retried) + "]"
|
||||||
|
summary = text("lanes_summary")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
**{key: [] for key in LANE_KEYS.values()},
|
||||||
|
**{
|
||||||
|
LANE_KEYS[lane]: [{"lane": lane, "attempt": n}] for lane, n in retried
|
||||||
|
},
|
||||||
|
"lanes_summary": f"{summary}\n{note}" if summary else note,
|
||||||
|
"retry_lanes": [],
|
||||||
|
**stamp,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
forced_raw = state.get("forced_lanes") or []
|
||||||
|
forced, unknown = [], []
|
||||||
|
for lane in forced_raw:
|
||||||
|
canon = ALIASES.get(str(lane).strip().lower())
|
||||||
|
(forced.append(canon) if canon else unknown.append(str(lane)))
|
||||||
|
|
||||||
|
reasons = []
|
||||||
|
if forced:
|
||||||
|
lanes = set(forced)
|
||||||
|
reasons.append(f"forced lanes honored exactly: {sorted(lanes)}")
|
||||||
|
else:
|
||||||
|
lanes = {"code-review"}
|
||||||
|
reasons.append(
|
||||||
|
"code-review: always on — the gauntlet is only spawned for non-trivial work"
|
||||||
|
)
|
||||||
|
if text("plan_context"):
|
||||||
|
lanes.add("adversary")
|
||||||
|
reasons.append("adversary: a spec/plan/acceptance-criteria context is present")
|
||||||
|
if (
|
||||||
|
state.get("touches_auth")
|
||||||
|
or state.get("touches_deps")
|
||||||
|
or state.get("touches_exec")
|
||||||
|
or state.get("security_posture") == "hardened"
|
||||||
|
):
|
||||||
|
lanes.add("security")
|
||||||
|
reasons.append(
|
||||||
|
"security: attack-surface signals (auth/deps/exec) or hardened posture"
|
||||||
|
)
|
||||||
|
if state.get("consumer_surface") and text("probe_context"):
|
||||||
|
lanes.add("probe")
|
||||||
|
reasons.append(
|
||||||
|
"probe: consumer-facing surface changed and a local-run recipe/suite pointer exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
# LLM judgment is additive-only.
|
||||||
|
llm_why = text("lane_reasons")[:300]
|
||||||
|
for flag, lane in (
|
||||||
|
("add_code_review", "code-review"),
|
||||||
|
("add_adversary", "adversary"),
|
||||||
|
("add_security", "security"),
|
||||||
|
("add_probe", "probe"),
|
||||||
|
):
|
||||||
|
if state.get(flag) and lane not in lanes:
|
||||||
|
if lane == "probe" and not text("probe_context"):
|
||||||
|
reasons.append(
|
||||||
|
"probe: requested by lane judgment but skipped — no local-run "
|
||||||
|
"recipe/suite pointer (a probe without one is guaranteed INCONCLUSIVE)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if lane == "adversary" and not text("plan_context"):
|
||||||
|
reasons.append(
|
||||||
|
"adversary: requested by lane judgment but skipped — no spec/plan "
|
||||||
|
"to check conformance against"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
lanes.add(lane)
|
||||||
|
reasons.append(f"{lane}: added by lane-selection judgment — {llm_why}")
|
||||||
|
|
||||||
|
if unknown:
|
||||||
|
reasons.append(f"ignored unknown forced lane name(s): {unknown}")
|
||||||
|
|
||||||
|
# A degraded lane selection (default_lanes fallback) must survive into the
|
||||||
|
# final report; lanes_summary is the only channel this script doesn't
|
||||||
|
# overwrite silently.
|
||||||
|
degraded = text("lanes_degraded")
|
||||||
|
if degraded:
|
||||||
|
reasons.insert(0, degraded)
|
||||||
|
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"code_review_items": [{"lane": "code-review"}] if "code-review" in lanes else [],
|
||||||
|
"adversary_items": [{"lane": "adversary"}] if "adversary" in lanes else [],
|
||||||
|
"security_items": [{"lane": "security"}] if "security" in lanes else [],
|
||||||
|
"probe_items": [{"lane": "probe"}] if "probe" in lanes else [],
|
||||||
|
"lanes_summary": "\n".join(f"- {r}" for r in reasons),
|
||||||
|
**stamp,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a builder crash must never kill the graph
|
||||||
|
fault = f"PIPELINE-FAULT: lane builder crashed — {e}; no lanes were run"
|
||||||
|
if crash_context["signals_error"]:
|
||||||
|
fault = f"{crash_context['signals_error']}; {fault}"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"code_review_items": [],
|
||||||
|
"adversary_items": [],
|
||||||
|
"security_items": [],
|
||||||
|
"probe_items": [],
|
||||||
|
"lanes_summary": crash_context["lanes_summary"],
|
||||||
|
"signals_error": fault,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic lane-selection fallback for the review gauntlet.
|
||||||
|
|
||||||
|
Reached ONLY via select_lanes' `fallback:` route, when the additive LLM
|
||||||
|
judgment dies after retries. Emits `forced_lanes` — code-review + adversary,
|
||||||
|
plus probe when the deterministic signals flagged a consumer-facing surface
|
||||||
|
AND a local-run recipe exists (build_items gates probe the same way — a
|
||||||
|
probe without a recipe is INCONCLUSIVE by construction),
|
||||||
|
plus security when the attack-surface signals (auth/deps/exec) or a hardened
|
||||||
|
posture demand it, unioned with any lanes the caller forced (canonicalized
|
||||||
|
with the same aliases build_items uses) — which build_items honors exactly,
|
||||||
|
so selection degrades WIDER, never narrower. The degradation note goes into
|
||||||
|
`lanes_degraded`; build_items folds it into lanes_summary so the final
|
||||||
|
report names the degradation.
|
||||||
|
|
||||||
|
Fail-closed: if the fallback itself crashes it emits the widest safe
|
||||||
|
selection (defaults ∪ security) plus a PIPELINE-FAULT in signals_error so
|
||||||
|
verdict_gate blocks — a crashed fallback never narrows selection silently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
# Keep in sync with build_items.ALIASES — a caller-forced lane must
|
||||||
|
# canonicalize the same way here as it would on the non-degraded path.
|
||||||
|
ALIASES = {
|
||||||
|
"code-review": "code-review",
|
||||||
|
"code_review": "code-review",
|
||||||
|
"code-reviewer": "code-review",
|
||||||
|
"adversary": "adversary",
|
||||||
|
"adversarial": "adversary",
|
||||||
|
"adversarial-review": "adversary",
|
||||||
|
"security": "security",
|
||||||
|
"security-review": "security",
|
||||||
|
"security-reviewer": "security",
|
||||||
|
"probe": "probe",
|
||||||
|
"usage": "probe",
|
||||||
|
"usage-pattern": "probe",
|
||||||
|
"usage-pattern-testing": "probe",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stable emission order keeps the fallback deterministic.
|
||||||
|
LANE_ORDER = ["code-review", "adversary", "security", "probe"]
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
lanes = {"code-review", "adversary"}
|
||||||
|
probe_context = state.get("probe_context")
|
||||||
|
if (
|
||||||
|
state.get("consumer_surface")
|
||||||
|
and isinstance(probe_context, str)
|
||||||
|
and probe_context.strip()
|
||||||
|
):
|
||||||
|
lanes.add("probe")
|
||||||
|
# The deterministic security signals are already in state when this
|
||||||
|
# fallback runs; a degraded selection must never drop the security lane
|
||||||
|
# build_items' hard rules would have selected.
|
||||||
|
if (
|
||||||
|
state.get("touches_auth")
|
||||||
|
or state.get("touches_deps")
|
||||||
|
or state.get("touches_exec")
|
||||||
|
or state.get("security_posture") == "hardened"
|
||||||
|
):
|
||||||
|
lanes.add("security")
|
||||||
|
# Caller-forced lanes survive degradation — union, never overwrite.
|
||||||
|
for lane in state.get("forced_lanes") or []:
|
||||||
|
if canon := ALIASES.get(str(lane).strip().lower()):
|
||||||
|
lanes.add(canon)
|
||||||
|
forced = [lane for lane in LANE_ORDER if lane in lanes]
|
||||||
|
raw = state.get("select_lanes_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
note = "lane selection degraded to deterministic defaults"
|
||||||
|
if detail:
|
||||||
|
note += f" — {detail}"
|
||||||
|
print(json.dumps({"forced_lanes": forced, "lanes_degraded": note}))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fallback itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"forced_lanes": ["code-review", "adversary", "security"],
|
||||||
|
"lanes_degraded": "lane selection degraded to deterministic "
|
||||||
|
f"defaults (fallback script error: {e})",
|
||||||
|
"signals_error": (
|
||||||
|
f"PIPELINE-FAULT: lane-selection fallback crashed — {e}; "
|
||||||
|
"lanes widened to defaults ∪ security"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed lane fault marker for the review gauntlet.
|
||||||
|
|
||||||
|
Reached ONLY via the run_* lane nodes' `fallback:` routes inside their map
|
||||||
|
branches. Grounded in engine behavior: on agent-node failure the engine
|
||||||
|
writes "Agent node failed: {e:#}" into `lane_out` via the node's
|
||||||
|
state_updates BEFORE routing to the fallback, so the raw failure text is
|
||||||
|
already in state. This node normalizes it into a
|
||||||
|
"PIPELINE-FAULT: <lane> lane failed after retries — …" marker that the map
|
||||||
|
collects as the lane result; verdict_gate turns any PIPELINE-FAULT lane
|
||||||
|
result into BLOCKED — a degraded lane can never read as a pass.
|
||||||
|
|
||||||
|
Lane identity comes from `lane_ctx` (the map item build_items emitted, which
|
||||||
|
carries a `lane` field); a missing/malformed item degrades to "unknown".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
lane = "unknown"
|
||||||
|
lane_ctx = state.get("lane_ctx")
|
||||||
|
if isinstance(lane_ctx, dict):
|
||||||
|
name = lane_ctx.get("lane")
|
||||||
|
if isinstance(name, str) and name.strip():
|
||||||
|
lane = name.strip()
|
||||||
|
raw = state.get("lane_out")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"lane_out": f"PIPELINE-FAULT: {lane} lane failed after retries — {detail}"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"lane_out": "PIPELINE-FAULT: lane failed after retries — "
|
||||||
|
f"fault-marker script error: {e}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail-closed marker for a dead parse stage of the review gauntlet.
|
||||||
|
|
||||||
|
Reached ONLY via parse's `fallback:` route. parse's state_updates wrote an
|
||||||
|
"LLM node …failed: <chain>" string into `parse_failure` before routing here;
|
||||||
|
this node records the failure as a PIPELINE-FAULT in `signals_error` and the
|
||||||
|
graph jumps straight to the verdict gate. Every lane item list is still its
|
||||||
|
initial [] so every lane records SKIPPED, and verdict_gate's signals_error
|
||||||
|
rule makes the verdict BLOCKED — a gauntlet that could not even parse its
|
||||||
|
request never reads as a pass.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
MAX_DETAIL_CHARS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
raw = state.get("parse_failure")
|
||||||
|
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
|
||||||
|
if len(detail) > MAX_DETAIL_CHARS:
|
||||||
|
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"signals_error": "PIPELINE-FAULT: parse failed — cannot run the "
|
||||||
|
f"gauntlet: {detail}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"signals_error": "PIPELINE-FAULT: parse failed — cannot run the "
|
||||||
|
f"gauntlet: fault-marker script error: {e}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic fault-only re-run gate for the review gauntlet.
|
||||||
|
|
||||||
|
Sits between the four lane maps and the verdict gate. Every lane map runs
|
||||||
|
on every pass — a lane that is not being re-run maps over [] — so a re-run
|
||||||
|
pass overwrites the untouched lanes' collected results with []. The gate's
|
||||||
|
hard rules:
|
||||||
|
|
||||||
|
- every lane that ran this pass is stashed in kept_results, completed
|
||||||
|
and faulted alike, and a lane whose map ran nothing this pass gets
|
||||||
|
its stashed report restored — the verdict gate always sees each
|
||||||
|
lane's LAST report;
|
||||||
|
- a lane is FAULTED only when its report is missing (the map ran but
|
||||||
|
collected nothing — replaced by a synthetic PIPELINE-FAULT marker so
|
||||||
|
the verdict gate never reads it as SKIPPED), non-string, the
|
||||||
|
lane_fault.py marker, sentinel-less, or carries the nested graph's own
|
||||||
|
degraded-run wording; a real verdict (DIVERGES, FAIL, INCONCLUSIVE,
|
||||||
|
NEEDS-HUMAN, …) is never re-run — the review's judgment stands;
|
||||||
|
- a faulted lane is re-run only while it has attempts left AND the
|
||||||
|
gauntlet is inside its wall-clock budget; otherwise it is recorded in
|
||||||
|
review_incomplete and its fault flows to the verdict gate as-is;
|
||||||
|
- malformed review_incomplete bookkeeping (not a list, or non-str
|
||||||
|
entries) keeps its str entries and records a PIPELINE-FAULT in
|
||||||
|
signals_error — never silently dropped, never split into lane names;
|
||||||
|
- the gate itself must never retry by accident: any internal error
|
||||||
|
records a PIPELINE-FAULT in signals_error and falls through to the
|
||||||
|
verdict gate, which blocks on it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# 69600 (graph timeout) − 2 × 11400 (largest lane envelope) − 1200 (script stages)
|
||||||
|
# = 45600; 600 s of slack below that.
|
||||||
|
MAX_RETRY_ELAPSED_SECS = 45000
|
||||||
|
MAX_ATTEMPTS = 3
|
||||||
|
|
||||||
|
# lane name -> (items key, results key)
|
||||||
|
LANES = {
|
||||||
|
"code-review": ("code_review_items", "code_review_results"),
|
||||||
|
"adversary": ("adversary_items", "adversary_results"),
|
||||||
|
"security": ("security_items", "security_results"),
|
||||||
|
"probe": ("probe_items", "probe_results"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Must stay identical to verdict_gate.py's sentinel regexes: a lane this
|
||||||
|
# gate calls completed must be one the verdict gate can parse, or a drift
|
||||||
|
# would re-run lanes the verdict gate accepts (or accept lanes it cannot
|
||||||
|
# read). A cross-pin test compares the literals in both files.
|
||||||
|
SENTINELS = {
|
||||||
|
"code-review": r"Verdict:\**\s*\**\s*(MERGE-READY|NEEDS-HUMAN)",
|
||||||
|
"adversary": r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)",
|
||||||
|
"security": r"SECURITY_REVIEW:\s*(PASS|FAIL)",
|
||||||
|
"probe": r"USAGE_PROBE:\s*(PASS|FAIL|INCONCLUSIVE)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filled by main() so the crash guard can append to an upstream fault
|
||||||
|
# instead of overwriting it, and report the lanes it had already declined.
|
||||||
|
crash_context = {"signals_error": "", "review_incomplete": []}
|
||||||
|
|
||||||
|
# Duplicated verbatim in the sibling gate script — the two copies must stay identical.
|
||||||
|
ADVERSARY_DEGRADED_HEADER = re.compile(
|
||||||
|
r"Criteria: .*("
|
||||||
|
r"degraded run: pipeline fault recorded"
|
||||||
|
r"|degraded run: \d+ criterion check\(s\) died"
|
||||||
|
r"|verdict computation error:"
|
||||||
|
r")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def adversary_degraded(report):
|
||||||
|
# The adversary's verdict.py emits its degraded-run header on the line
|
||||||
|
# directly under the FIRST sentinel; anchoring there keeps a report that
|
||||||
|
# merely quotes the wording later (observations, complaints) a real verdict.
|
||||||
|
lines = report.splitlines()
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
if re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", ln):
|
||||||
|
nxt = lines[i + 1] if i + 1 < len(lines) else ""
|
||||||
|
return bool(ADVERSARY_DEGRADED_HEADER.match(nxt))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def nested_degraded(lane, report):
|
||||||
|
# The nested graphs complete with a sentinel even when a stage inside
|
||||||
|
# them died; their own degraded-run wording marks a partial review,
|
||||||
|
# which is not a completed one. Same anchoring as verdict_gate.py.
|
||||||
|
if lane == "code-review":
|
||||||
|
verdict_line = re.search(r"^\*\*Verdict: .*$", report, re.MULTILINE)
|
||||||
|
return bool(
|
||||||
|
verdict_line
|
||||||
|
and re.search(
|
||||||
|
r"^\*\*Verdict: NEEDS-HUMAN\*\* — .*"
|
||||||
|
r"(pipeline fault\(s\) recorded|PIPELINE-FAULT:|report rendering error)",
|
||||||
|
verdict_line.group(0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if lane == "adversary":
|
||||||
|
return adversary_degraded(report)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_faulted(lane, report):
|
||||||
|
if not isinstance(report, str):
|
||||||
|
return True
|
||||||
|
if report.strip().startswith("PIPELINE-FAULT:"):
|
||||||
|
return True
|
||||||
|
if not re.search(SENTINELS[lane], report):
|
||||||
|
return True
|
||||||
|
return nested_degraded(lane, report)
|
||||||
|
|
||||||
|
|
||||||
|
def attempt_count(attempts, lane):
|
||||||
|
try:
|
||||||
|
return int(attempts.get(lane) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
prior = state.get("signals_error")
|
||||||
|
signals_error = prior if isinstance(prior, str) else ""
|
||||||
|
crash_context["signals_error"] = signals_error
|
||||||
|
out = {}
|
||||||
|
declared = state.get("review_incomplete")
|
||||||
|
if isinstance(declared, list):
|
||||||
|
incomplete = [name for name in declared if isinstance(name, str)]
|
||||||
|
malformed = len(incomplete) != len(declared)
|
||||||
|
else:
|
||||||
|
incomplete = []
|
||||||
|
malformed = declared is not None
|
||||||
|
if malformed:
|
||||||
|
fault = (
|
||||||
|
"PIPELINE-FAULT: malformed review_incomplete bookkeeping "
|
||||||
|
f"({repr(declared)[:80]})"
|
||||||
|
)
|
||||||
|
signals_error = f"{signals_error}; {fault}" if signals_error else fault
|
||||||
|
crash_context["signals_error"] = signals_error
|
||||||
|
out["signals_error"] = signals_error
|
||||||
|
crash_context["review_incomplete"] = incomplete
|
||||||
|
raw_attempts = state.get("lane_attempts")
|
||||||
|
attempts = dict(raw_attempts) if isinstance(raw_attempts, dict) else {}
|
||||||
|
kept = dict(state.get("kept_results") or {})
|
||||||
|
faulted = []
|
||||||
|
|
||||||
|
for lane, (items_key, results_key) in LANES.items():
|
||||||
|
results = state.get(results_key) or []
|
||||||
|
if not state.get(items_key):
|
||||||
|
if not results and lane in kept:
|
||||||
|
out[results_key] = [kept[lane]]
|
||||||
|
continue
|
||||||
|
report = results[0] if results else None
|
||||||
|
if report is None:
|
||||||
|
report = f"PIPELINE-FAULT: {lane} lane produced no report"
|
||||||
|
out[results_key] = [report]
|
||||||
|
kept[lane] = report
|
||||||
|
if is_faulted(lane, report):
|
||||||
|
faulted.append(lane)
|
||||||
|
attempts[lane] = attempt_count(attempts, lane) + 1
|
||||||
|
|
||||||
|
# A missing/zero stamp reads as an ancient start and declines every
|
||||||
|
# retry — the safe direction when the clock's origin is unknown.
|
||||||
|
started = state.get("gauntlet_started_at") or 0
|
||||||
|
within_budget = (time.time() - float(started)) <= MAX_RETRY_ELAPSED_SECS
|
||||||
|
retry = [lane for lane in faulted if within_budget and attempts[lane] < MAX_ATTEMPTS]
|
||||||
|
declined = [lane for lane in faulted if lane not in retry]
|
||||||
|
incomplete = sorted(set(incomplete) | set(declined))
|
||||||
|
crash_context["review_incomplete"] = incomplete
|
||||||
|
|
||||||
|
out.update(
|
||||||
|
{
|
||||||
|
"lane_attempts": attempts,
|
||||||
|
"kept_results": kept,
|
||||||
|
"retry_lanes": retry,
|
||||||
|
"review_incomplete": incomplete,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
prior_notes = state.get("retry_notes")
|
||||||
|
notes = [n for n in prior_notes if isinstance(n, str)] if isinstance(prior_notes, list) else []
|
||||||
|
for lane in declined:
|
||||||
|
notes.append(
|
||||||
|
f"lane {lane} faulted and was NOT re-run "
|
||||||
|
f"(attempts {attempts.get(lane, '?')}/{MAX_ATTEMPTS}"
|
||||||
|
+ ("" if within_budget else "; wall-clock budget exhausted")
|
||||||
|
+ ") — recorded in review_incomplete"
|
||||||
|
)
|
||||||
|
for lane in retry:
|
||||||
|
notes.append(
|
||||||
|
f"lane {lane} faulted (missing / sentinel-less / degraded report) — "
|
||||||
|
f"re-running: attempt {attempts.get(lane, 0) + 1}/{MAX_ATTEMPTS}"
|
||||||
|
)
|
||||||
|
if retry or declined:
|
||||||
|
# stderr is discarded on success by today's engine but is the
|
||||||
|
# forward-compatible live-visibility channel; retry_notes carries the
|
||||||
|
# same lines into the final report either way.
|
||||||
|
sys.stderr.write("WARN: " + " | ".join(notes[-(len(retry) + len(declined)):]) + "\n")
|
||||||
|
out["retry_notes"] = notes
|
||||||
|
if retry:
|
||||||
|
out["_next"] = "build_items"
|
||||||
|
print(json.dumps(out))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — a gate crash must fall through, never retry
|
||||||
|
fault = f"PIPELINE-FAULT: retry gate crashed — {e}"
|
||||||
|
if crash_context["signals_error"]:
|
||||||
|
fault = f"{crash_context['signals_error']}; {fault}"
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"retry_lanes": [],
|
||||||
|
"review_incomplete": crash_context["review_incomplete"],
|
||||||
|
"signals_error": fault,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic diff signals for the review gauntlet.
|
||||||
|
|
||||||
|
Pure git — no LLM judgment. Every signal is computed from the changed-file
|
||||||
|
list and the added lines of the diff. Failures never sink the gauntlet:
|
||||||
|
on any error the script reports `signals_error` and leaves the defaults
|
||||||
|
(false/empty) in place, which biases lane selection toward the caller's
|
||||||
|
context and forced lanes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
|
proj = os.path.expanduser(
|
||||||
|
(state.get("project_dir_in") or "").strip() or state.get("project_dir") or "."
|
||||||
|
)
|
||||||
|
spec = (state.get("diff_spec") or "worktree").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def git(*args):
|
||||||
|
r = subprocess.run(
|
||||||
|
["git", "-C", proj, *args], capture_output=True, text=True, timeout=90
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError((r.stderr or r.stdout).strip()[:400])
|
||||||
|
return r.stdout
|
||||||
|
|
||||||
|
|
||||||
|
out = {"project_dir": proj}
|
||||||
|
|
||||||
|
# Deterministic plan_context recovery: the parse LLM proved a lossy channel
|
||||||
|
# for verbatim spec text (observed replacing ~4 KB of pasted criteria with
|
||||||
|
# file paths + a summary sentence). When the raw prompt carries the callers'
|
||||||
|
# labeled plan block, cut it out verbatim and override the extraction — but
|
||||||
|
# only when the recovered block is LONGER, so a good extraction is never
|
||||||
|
# degraded. The adversary lane fails closed on whatever goes missing here.
|
||||||
|
_prompt = state.get("initial_prompt")
|
||||||
|
if isinstance(_prompt, str) and _prompt:
|
||||||
|
_m = re.search(
|
||||||
|
r"Plan / acceptance criteria:\s*(.+?)"
|
||||||
|
r"(?=\n\s*(?:Local-run recipe|Forced lanes|Rigor:|Security posture:|Extra context)\b|\Z)",
|
||||||
|
_prompt,
|
||||||
|
re.S | re.I,
|
||||||
|
)
|
||||||
|
if _m:
|
||||||
|
_block = _m.group(1).strip()
|
||||||
|
if len(_block) > len((state.get("plan_context") or "").strip()):
|
||||||
|
out["plan_context"] = _block
|
||||||
|
# retry_gate measures its wall-clock budget from this stamp; it is set
|
||||||
|
# before the git work so a failed diff still starts the clock.
|
||||||
|
if not state.get("gauntlet_started_at"):
|
||||||
|
out["gauntlet_started_at"] = time.time()
|
||||||
|
try:
|
||||||
|
if spec in ("", "worktree"):
|
||||||
|
names = git("diff", "--name-only", "HEAD")
|
||||||
|
difftext = git("diff", "HEAD")
|
||||||
|
else:
|
||||||
|
names = git("diff", "--name-only", spec)
|
||||||
|
difftext = git("diff", spec)
|
||||||
|
files = [f.strip() for f in names.splitlines() if f.strip()]
|
||||||
|
added = "\n".join(
|
||||||
|
l for l in difftext.splitlines() if l.startswith("+") and not l.startswith("+++")
|
||||||
|
)
|
||||||
|
|
||||||
|
def any_path(pattern):
|
||||||
|
return any(re.search(pattern, f, re.I) for f in files)
|
||||||
|
|
||||||
|
out.update(
|
||||||
|
{
|
||||||
|
"changed_files": files[:200],
|
||||||
|
"file_count": len(files),
|
||||||
|
"touches_proto": any_path(r"\.proto$|buf\.(yaml|gen\.yaml)$"),
|
||||||
|
"touches_migrations": any_path(r"migrations?/|\.sql$"),
|
||||||
|
# auth(?!or) admits auth/authn/authz/oauth but not "author".
|
||||||
|
# Over-firing here is the safe direction: it only ADDS a lane.
|
||||||
|
"touches_auth": any_path(
|
||||||
|
r"auth(?!or)|login|logout|token|session|permission|rbac|\bacl\b|credential|secret"
|
||||||
|
)
|
||||||
|
or bool(
|
||||||
|
re.search(
|
||||||
|
r"auth(?!or)|password|token|secret|credential|jwt|session",
|
||||||
|
added,
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"touches_deps": any_path(
|
||||||
|
r"go\.(mod|sum)$|package(-lock)?\.json$|yarn\.lock$|pnpm-lock\.yaml$"
|
||||||
|
r"|Cargo\.(toml|lock)$|requirements[^/]*\.txt$|pyproject\.toml$"
|
||||||
|
r"|Gemfile(\.lock)?$|pom\.xml$|build\.gradle|composer\.(json|lock)$"
|
||||||
|
),
|
||||||
|
"touches_exec": bool(
|
||||||
|
re.search(
|
||||||
|
r"exec\.Command|subprocess|os/exec|Runtime\.getRuntime"
|
||||||
|
r"|shell_exec|\beval\(|\bsystem\(|child_process",
|
||||||
|
added,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"consumer_surface": any_path(
|
||||||
|
r"routes?|handlers?|\bapi\b|endpoints?|\bcmd/|\bcli\b|\.proto$"
|
||||||
|
r"|openapi|swagger|controllers?|graphql"
|
||||||
|
),
|
||||||
|
"docs_only": bool(files)
|
||||||
|
and all(re.search(r"\.(md|rst|txt|adoc)$", f, re.I) for f in files),
|
||||||
|
"signals_summary": f"{len(files)} file(s) changed (diff: {spec})",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001 — degraded signals must not sink the gauntlet
|
||||||
|
out["signals_error"] = (
|
||||||
|
f"NOTE: diff signals could not be computed ({e}); "
|
||||||
|
"lane selection falls back to caller context and forced lanes."
|
||||||
|
)
|
||||||
|
out["signals_summary"] = "signals unavailable"
|
||||||
|
|
||||||
|
print(json.dumps(out))
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic verdict gate for the review gauntlet.
|
||||||
|
|
||||||
|
Parses each configured lane's verdict SENTINEL with a regex — no LLM
|
||||||
|
re-reads, no judgment. The gate's hard rules:
|
||||||
|
|
||||||
|
- a missing sentinel is a LANE FAILURE and blocks — never a pass;
|
||||||
|
- a non-string lane result (the map collected an object instead of the
|
||||||
|
lane's rendered text) blocks — it cannot carry a sentinel;
|
||||||
|
- a PIPELINE-FAULT lane result (the lane's agent died after retries;
|
||||||
|
detected only when the report IS the lane_fault.py marker or
|
||||||
|
retry_gate.py's no-report marker, i.e. it STARTS with it — a real
|
||||||
|
review merely quoting the marker flows to the normal rules) or a
|
||||||
|
PIPELINE-FAULT in signals_error (builder/parse fault) blocks — a
|
||||||
|
degraded pipeline is never a pass;
|
||||||
|
- a code-review lane whose OWN verdict line records a pipeline fault
|
||||||
|
(code-reviewer's verdict.py / render.py degraded-run wording) blocks —
|
||||||
|
the nested graph completed, but with a dead domain lane, verifier, or
|
||||||
|
synthesis behind it, so its critical count is not trustworthy;
|
||||||
|
- an adversary lane whose OWN header line (the line directly under its
|
||||||
|
sentinel) records a degraded run — a pipeline fault, died criterion
|
||||||
|
checks, or its verdict script's crash stub — blocks as a PIPELINE-FAULT,
|
||||||
|
not as a DIVERGES finding: the nested graph completed with a dead stage
|
||||||
|
behind it, so its verdict is not a judgment on the code;
|
||||||
|
- a non-zero critical count in the code-review report blocks regardless
|
||||||
|
of the lane's own verdict line — derived from render.py's deterministic
|
||||||
|
summary line (`*Reviewed N files, found X critical, …*`); the raw 🔴
|
||||||
|
count is used only when that summary line is missing;
|
||||||
|
- NEEDS-HUMAN without 🔴 passes but is surfaced as attention required;
|
||||||
|
- probe INCONCLUSIVE blocks with an ENVIRONMENT note — it is never
|
||||||
|
treated as PASS or FAIL (fix the environment per the plan's local-run
|
||||||
|
recipe, then re-run);
|
||||||
|
- a PIPELINE-FAULT lane row carries its attempt count (` ×N`) when retry_gate
|
||||||
|
re-ran it, so the reader can tell a first-pass death from an exhausted
|
||||||
|
retry budget;
|
||||||
|
- an incomplete review — lanes retry_gate gave up on (review_incomplete)
|
||||||
|
or a pipeline-level fault (reported under the `pipeline` pseudo-lane) —
|
||||||
|
emits the GAUNTLET_REVIEW_INCOMPLETE: machine line and a "Review
|
||||||
|
incomplete" section: that is a fault to re-run, never a code finding.
|
||||||
|
Every lane named in review_incomplete is forced to a BLOCKED row, so a
|
||||||
|
restored earlier report can never turn an incomplete review into a
|
||||||
|
pass: a non-empty machine line structurally implies GAUNTLET: BLOCKED.
|
||||||
|
Malformed review_incomplete bookkeeping (not a list, or non-str
|
||||||
|
entries) is itself recorded as a `pipeline` fault, never dropped;
|
||||||
|
- the gate itself must never crash into a silent pass: any internal
|
||||||
|
error emits GAUNTLET: BLOCKED naming the error, with the pipeline
|
||||||
|
marked incomplete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Duplicated verbatim in the sibling gate script — the two copies must stay identical.
|
||||||
|
ADVERSARY_DEGRADED_HEADER = re.compile(
|
||||||
|
r"Criteria: .*("
|
||||||
|
r"degraded run: pipeline fault recorded"
|
||||||
|
r"|degraded run: \d+ criterion check\(s\) died"
|
||||||
|
r"|verdict computation error:"
|
||||||
|
r")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def adversary_degraded(report):
|
||||||
|
# The adversary's verdict.py emits its degraded-run header on the line
|
||||||
|
# directly under the FIRST sentinel; anchoring there keeps a report that
|
||||||
|
# merely quotes the wording later (observations, complaints) a real verdict.
|
||||||
|
lines = report.splitlines()
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
if re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", ln):
|
||||||
|
nxt = lines[i + 1] if i + 1 < len(lines) else ""
|
||||||
|
return bool(ADVERSARY_DEGRADED_HEADER.match(nxt))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
state = load_state()
|
||||||
|
blockers, attention, reports = [], [], []
|
||||||
|
rows = {}
|
||||||
|
malformed = set()
|
||||||
|
lane_attempts = state.get("lane_attempts")
|
||||||
|
if not isinstance(lane_attempts, dict):
|
||||||
|
lane_attempts = {}
|
||||||
|
|
||||||
|
def lane_report(key):
|
||||||
|
arr = state.get(key) or []
|
||||||
|
if not arr:
|
||||||
|
return None
|
||||||
|
item = arr[0]
|
||||||
|
if isinstance(item, str):
|
||||||
|
return item
|
||||||
|
malformed.add(key)
|
||||||
|
return json.dumps(item)
|
||||||
|
|
||||||
|
def record(lane, status, detail=""):
|
||||||
|
rows[lane] = (status, detail)
|
||||||
|
|
||||||
|
def attempt_count(lane):
|
||||||
|
try:
|
||||||
|
return int(lane_attempts.get(lane) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def attempts_suffix(lane):
|
||||||
|
n = attempt_count(lane)
|
||||||
|
return f" ×{n}" if n > 1 else ""
|
||||||
|
|
||||||
|
def fault_excerpt(report):
|
||||||
|
line = next(
|
||||||
|
(ln for ln in report.splitlines() if "PIPELINE-FAULT:" in ln), report
|
||||||
|
)
|
||||||
|
return line.strip()[:200]
|
||||||
|
|
||||||
|
def is_lane_fault(report):
|
||||||
|
# A genuine lane fault is the ENTIRE report — the lane_fault.py marker
|
||||||
|
# or retry_gate.py's no-report marker — which always begins with
|
||||||
|
# "PIPELINE-FAULT:". Matching the marker anywhere would let a real
|
||||||
|
# review that merely QUOTES it bypass 🔴 counting and sentinel
|
||||||
|
# parsing.
|
||||||
|
return report.strip().startswith("PIPELINE-FAULT:")
|
||||||
|
|
||||||
|
def lane_fault_blocker(lane, report):
|
||||||
|
blockers.append(
|
||||||
|
f"{lane}: PIPELINE-FAULT — the lane failed after retries and produced "
|
||||||
|
f"no verdict; a degraded lane is never a pass ({fault_excerpt(report)})"
|
||||||
|
)
|
||||||
|
record(lane, "BLOCKED", f"PIPELINE-FAULT (lane failed{attempts_suffix(lane)})")
|
||||||
|
|
||||||
|
def malformed_blocker(lane, report):
|
||||||
|
blockers.append(f"{lane}: malformed lane result (non-string item) — never a pass")
|
||||||
|
record(lane, "BLOCKED", "malformed lane result (non-string)")
|
||||||
|
reports.append((lane, report))
|
||||||
|
|
||||||
|
# --- code-review ------------------------------------------------------
|
||||||
|
cr = lane_report("code_review_results")
|
||||||
|
if cr is None:
|
||||||
|
record("code-review", "SKIPPED", "not selected")
|
||||||
|
elif "code_review_results" in malformed:
|
||||||
|
malformed_blocker("code-review", cr)
|
||||||
|
elif is_lane_fault(cr):
|
||||||
|
lane_fault_blocker("code-review", cr)
|
||||||
|
reports.append(("code-review", cr))
|
||||||
|
else:
|
||||||
|
reports.append(("code-review", cr))
|
||||||
|
m = re.search(r"Verdict:\**\s*\**\s*(MERGE-READY|NEEDS-HUMAN)", cr)
|
||||||
|
# code-reviewer's verdict.py forces NEEDS-HUMAN on its own internal
|
||||||
|
# faults; render.py emits that as `**Verdict: NEEDS-HUMAN** — <reason>`
|
||||||
|
# (CONTRACT comments at both emitters). Scoped to the FIRST rendered
|
||||||
|
# verdict line — render.py emits it before any finding body — so a
|
||||||
|
# finding that quotes the wording cannot trip it.
|
||||||
|
verdict_line = re.search(r"^\*\*Verdict: .*$", cr, re.MULTILINE)
|
||||||
|
m_fault = verdict_line and re.search(
|
||||||
|
r"^\*\*Verdict: NEEDS-HUMAN\*\* — .*"
|
||||||
|
r"(pipeline fault\(s\) recorded|PIPELINE-FAULT:|report rendering error)",
|
||||||
|
verdict_line.group(0),
|
||||||
|
)
|
||||||
|
# Same quoted-marker class as the PIPELINE-FAULT prefix anchoring: a
|
||||||
|
# 🔴 quoted inside finding text or a Changes-table row is not a
|
||||||
|
# finding. render.py's summary line is the authoritative count; the
|
||||||
|
# raw count is the fail-closed fallback when that line is absent. The
|
||||||
|
# LAST match wins: render.py emits the line after every finding body,
|
||||||
|
# so a quoted look-alike earlier in the report cannot shadow it. The
|
||||||
|
# pattern is the FULL shape of render.py's line (CONTRACT comment
|
||||||
|
# there) so a partial look-alike in LLM-authored text never matches.
|
||||||
|
sums = re.findall(
|
||||||
|
r"^\*Reviewed \d+ files, found (\d+) critical, \d+ warnings, \d+ suggestions, "
|
||||||
|
r"\d+ nitpicks \(\d+ deferred by quality bar\)\*$",
|
||||||
|
cr,
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
reds = int(sums[-1]) if sums else cr.count("🔴")
|
||||||
|
if m_fault:
|
||||||
|
blockers.append(
|
||||||
|
"code-review: the lane reported an internal PIPELINE-FAULT "
|
||||||
|
"(degraded review) — a degraded lane is never a pass"
|
||||||
|
)
|
||||||
|
if reds:
|
||||||
|
blockers.append(
|
||||||
|
f"code-review: {reds} 🔴 CRITICAL finding(s) — fix before claiming done"
|
||||||
|
)
|
||||||
|
suffix = attempts_suffix("code-review")
|
||||||
|
if m_fault and reds:
|
||||||
|
record("code-review", "BLOCKED", f"PIPELINE-FAULT (degraded lane{suffix}); {reds} 🔴")
|
||||||
|
elif m_fault:
|
||||||
|
record("code-review", "BLOCKED", f"PIPELINE-FAULT (degraded lane{suffix})")
|
||||||
|
elif reds:
|
||||||
|
record("code-review", "BLOCKED", f"{reds} 🔴 finding(s)")
|
||||||
|
elif m and m.group(1) == "MERGE-READY":
|
||||||
|
record("code-review", "GREEN", "MERGE-READY")
|
||||||
|
elif m:
|
||||||
|
attention.append(
|
||||||
|
"code-review: NEEDS-HUMAN — see the lane report's "
|
||||||
|
"'Human attention required' section"
|
||||||
|
)
|
||||||
|
record("code-review", "GREEN (attention)", "NEEDS-HUMAN, no 🔴")
|
||||||
|
else:
|
||||||
|
blockers.append(
|
||||||
|
"code-review: no Review Verdict sentinel found — the lane failed; "
|
||||||
|
"a missing verdict is never a pass"
|
||||||
|
)
|
||||||
|
record("code-review", "BLOCKED", "missing verdict sentinel")
|
||||||
|
|
||||||
|
# --- adversary --------------------------------------------------------
|
||||||
|
adv = lane_report("adversary_results")
|
||||||
|
if adv is None:
|
||||||
|
record("adversary", "SKIPPED", "not selected")
|
||||||
|
elif "adversary_results" in malformed:
|
||||||
|
malformed_blocker("adversary", adv)
|
||||||
|
elif is_lane_fault(adv):
|
||||||
|
lane_fault_blocker("adversary", adv)
|
||||||
|
reports.append(("adversary", adv))
|
||||||
|
else:
|
||||||
|
reports.append(("adversary", adv))
|
||||||
|
m = re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", adv)
|
||||||
|
if adversary_degraded(adv):
|
||||||
|
blockers.append(
|
||||||
|
"adversary: PIPELINE-FAULT — degraded run, pipeline fault recorded "
|
||||||
|
"inside the adversary; a degraded lane is never a pass"
|
||||||
|
)
|
||||||
|
record(
|
||||||
|
"adversary",
|
||||||
|
"BLOCKED",
|
||||||
|
f"PIPELINE-FAULT (degraded lane{attempts_suffix('adversary')})",
|
||||||
|
)
|
||||||
|
elif m and m.group(1) == "CONFORMS":
|
||||||
|
record("adversary", "GREEN", "CONFORMS")
|
||||||
|
elif m:
|
||||||
|
blockers.append(
|
||||||
|
"adversary: DIVERGES — the implementation does not conform to the plan"
|
||||||
|
)
|
||||||
|
record("adversary", "BLOCKED", "DIVERGES")
|
||||||
|
else:
|
||||||
|
blockers.append(
|
||||||
|
"adversary: no ADVERSARIAL_REVIEW sentinel found — lane failed; never a pass"
|
||||||
|
)
|
||||||
|
record("adversary", "BLOCKED", "missing verdict sentinel")
|
||||||
|
|
||||||
|
# --- security ---------------------------------------------------------
|
||||||
|
sec = lane_report("security_results")
|
||||||
|
if sec is None:
|
||||||
|
record("security", "SKIPPED", "not selected")
|
||||||
|
elif "security_results" in malformed:
|
||||||
|
malformed_blocker("security", sec)
|
||||||
|
elif is_lane_fault(sec):
|
||||||
|
lane_fault_blocker("security", sec)
|
||||||
|
reports.append(("security", sec))
|
||||||
|
else:
|
||||||
|
reports.append(("security", sec))
|
||||||
|
m = re.search(r"SECURITY_REVIEW:\s*(PASS|FAIL)", sec)
|
||||||
|
if m and m.group(1) == "PASS":
|
||||||
|
record("security", "GREEN", "PASS")
|
||||||
|
elif m:
|
||||||
|
blockers.append("security: SECURITY_REVIEW: FAIL — exploitable finding(s)")
|
||||||
|
record("security", "BLOCKED", "FAIL")
|
||||||
|
else:
|
||||||
|
blockers.append(
|
||||||
|
"security: no SECURITY_REVIEW sentinel found — lane failed; never a pass"
|
||||||
|
)
|
||||||
|
record("security", "BLOCKED", "missing verdict sentinel")
|
||||||
|
|
||||||
|
# --- probe ------------------------------------------------------------
|
||||||
|
pb = lane_report("probe_results")
|
||||||
|
if pb is None:
|
||||||
|
record("probe", "SKIPPED", "not selected")
|
||||||
|
elif "probe_results" in malformed:
|
||||||
|
malformed_blocker("probe", pb)
|
||||||
|
elif is_lane_fault(pb):
|
||||||
|
lane_fault_blocker("probe", pb)
|
||||||
|
reports.append(("probe", pb))
|
||||||
|
else:
|
||||||
|
reports.append(("probe", pb))
|
||||||
|
m = re.search(r"USAGE_PROBE:\s*(PASS|FAIL|INCONCLUSIVE)", pb)
|
||||||
|
if m and m.group(1) == "PASS":
|
||||||
|
record("probe", "GREEN", "PASS")
|
||||||
|
elif m and m.group(1) == "FAIL":
|
||||||
|
blockers.append("probe: USAGE_PROBE: FAIL — observed behavior contradicts the contract")
|
||||||
|
record("probe", "BLOCKED", "FAIL")
|
||||||
|
elif m:
|
||||||
|
blockers.append(
|
||||||
|
"probe: INCONCLUSIVE — the ENVIRONMENT could not be established; this is "
|
||||||
|
"not a code verdict. Follow the plan's local-run recipe to fix the "
|
||||||
|
"environment and re-run. INCONCLUSIVE is never treated as PASS or FAIL."
|
||||||
|
)
|
||||||
|
record("probe", "BLOCKED", "INCONCLUSIVE (environment)")
|
||||||
|
else:
|
||||||
|
blockers.append(
|
||||||
|
"probe: no USAGE_PROBE sentinel found — lane failed; never a pass"
|
||||||
|
)
|
||||||
|
record("probe", "BLOCKED", "missing verdict sentinel")
|
||||||
|
|
||||||
|
# retry_gate restores an exhausted lane's LAST report, which may be an
|
||||||
|
# earlier clean verdict — an incomplete lane must block regardless of
|
||||||
|
# what its row parsed to.
|
||||||
|
declared = state.get("review_incomplete")
|
||||||
|
incomplete = set()
|
||||||
|
malformed_bookkeeping = None
|
||||||
|
if isinstance(declared, list):
|
||||||
|
incomplete = {name for name in declared if isinstance(name, str)}
|
||||||
|
if any(not isinstance(name, str) for name in declared):
|
||||||
|
malformed_bookkeeping = repr(declared)[:80]
|
||||||
|
elif declared is not None:
|
||||||
|
malformed_bookkeeping = repr(declared)[:80]
|
||||||
|
for lane in sorted(incomplete - {"pipeline"}):
|
||||||
|
if rows.get(lane, ("", ""))[0] == "BLOCKED":
|
||||||
|
continue
|
||||||
|
after = f" after {n} attempt(s)" if (n := attempt_count(lane)) else ""
|
||||||
|
blockers.append(
|
||||||
|
f"{lane}: review incomplete{after} — the lane never produced a usable verdict"
|
||||||
|
)
|
||||||
|
record(lane, "BLOCKED", f"review incomplete{after}")
|
||||||
|
|
||||||
|
# A gauntlet-level fault (build_items crash, dead parse stage) means the
|
||||||
|
# lanes above were never built — SKIPPED rows alone must not read as PASS.
|
||||||
|
signals_error = state.get("signals_error")
|
||||||
|
pipeline_fault = isinstance(signals_error, str) and "PIPELINE-FAULT:" in signals_error
|
||||||
|
if pipeline_fault:
|
||||||
|
blockers.append(f"pipeline: {signals_error}")
|
||||||
|
incomplete.add("pipeline")
|
||||||
|
if malformed_bookkeeping is not None:
|
||||||
|
blockers.append(
|
||||||
|
"pipeline: PIPELINE-FAULT — malformed review_incomplete bookkeeping "
|
||||||
|
f"({malformed_bookkeeping}); never a pass"
|
||||||
|
)
|
||||||
|
incomplete.add("pipeline")
|
||||||
|
if "pipeline" in incomplete and not any(b.startswith("pipeline:") for b in blockers):
|
||||||
|
blockers.append(
|
||||||
|
"pipeline: review incomplete — a pipeline stage never completed "
|
||||||
|
"(no fault text recorded)"
|
||||||
|
)
|
||||||
|
|
||||||
|
verdict = "BLOCKED" if blockers or incomplete else "PASS"
|
||||||
|
|
||||||
|
incomplete_lines = []
|
||||||
|
for name in sorted(incomplete):
|
||||||
|
if name == "pipeline":
|
||||||
|
incomplete_lines.append(
|
||||||
|
f"- pipeline: {signals_error}"
|
||||||
|
if pipeline_fault
|
||||||
|
else "- pipeline: review incomplete (no fault text recorded)"
|
||||||
|
)
|
||||||
|
elif n := attempt_count(name):
|
||||||
|
incomplete_lines.append(f"- {name}: no completed review after {n} attempt(s)")
|
||||||
|
else:
|
||||||
|
incomplete_lines.append(f"- {name}: no completed review")
|
||||||
|
incomplete_line = (
|
||||||
|
"\nGAUNTLET_REVIEW_INCOMPLETE: " + ", ".join(sorted(incomplete))
|
||||||
|
if incomplete
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
md = "\n".join(
|
||||||
|
["| lane | status | detail |", "|------|--------|--------|"]
|
||||||
|
+ [f"| {lane} | {status} | {detail} |" for lane, (status, detail) in rows.items()]
|
||||||
|
)
|
||||||
|
md += "\n\n## Lane selection\n" + (state.get("lanes_summary") or "(none recorded)")
|
||||||
|
_notes = state.get("retry_notes")
|
||||||
|
if isinstance(_notes, list):
|
||||||
|
_shown = [n for n in _notes if isinstance(n, str)][:8]
|
||||||
|
if _shown:
|
||||||
|
md += "\n\n## Pipeline notes\n" + "\n".join(f"- {n}" for n in _shown)
|
||||||
|
if blockers:
|
||||||
|
md += "\n\n## Blockers\n" + "\n".join(f"- {b}" for b in blockers)
|
||||||
|
if attention:
|
||||||
|
md += "\n\n## Human attention required\n" + "\n".join(f"- {a}" for a in attention)
|
||||||
|
if state.get("signals_error"):
|
||||||
|
md += f"\n\n> {state['signals_error']}"
|
||||||
|
if incomplete_lines:
|
||||||
|
md += "\n\n## Review incomplete\n" + "\n".join(incomplete_lines)
|
||||||
|
md += "\n\n## Full lane reports\n"
|
||||||
|
for lane, rep in reports:
|
||||||
|
md += f"\n<details><summary>{lane}</summary>\n\n{rep}\n\n</details>\n"
|
||||||
|
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"gauntlet_verdict": verdict,
|
||||||
|
"gauntlet_report": md,
|
||||||
|
"gauntlet_incomplete_line": incomplete_line,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e: # noqa: BLE001 — the gate must never crash into a silent pass
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"gauntlet_verdict": "BLOCKED",
|
||||||
|
"gauntlet_report": f"verdict gate error: {e} — treat as a failed gate, not a pass.",
|
||||||
|
"gauntlet_incomplete_line": "\nGAUNTLET_REVIEW_INCOMPLETE: pipeline",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
Loaded 100 of 334 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user