Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df1e9a2e2b | ||
|
|
9b85a95552 | ||
|
|
2213251756 | ||
|
|
a057cdf488 | ||
|
|
251b2729c2 | ||
|
|
06c1a3a1be | ||
|
|
74bf6eef04 | ||
|
|
28e8028a02 | ||
|
|
71e5198d4c | ||
|
|
ef38f3be4c | ||
|
|
db9643eb58 | ||
|
|
193336967b | ||
|
|
f6d31285ea | ||
|
|
d4b265ad45 | ||
|
|
53a695580f | ||
|
|
6163202a93 | ||
|
|
4912d96f58 | ||
|
|
87bfccd55b | ||
|
|
a89ddb30bd | ||
|
|
e412cfed14 | ||
|
|
b8f6fb8dad | ||
|
|
6b0dcdccef | ||
|
|
1902775812 | ||
|
|
03dfce20b4 | ||
|
|
b3bb529a6a | ||
|
|
9a61d237fa | ||
|
|
702e335cb8 | ||
|
|
73e9ededd4 | ||
|
|
bad08e9b03 | ||
|
|
851988abbc | ||
|
|
222a9c8382 | ||
|
|
a8b1ce6658 | ||
|
|
807924241f | ||
|
|
91f8cc6c07 | ||
|
|
989d19020d | ||
|
|
b4f66ce84d | ||
|
|
294d25cea3 | ||
|
|
9048666c65 | ||
|
|
25fc02d07a | ||
|
|
9dc2ecd604 | ||
|
|
13ae64cbae | ||
|
|
024cf5a104 | ||
|
|
be68d23d6a | ||
|
|
81658ce0ea | ||
|
|
de109846bf | ||
|
|
5744073c11 | ||
|
|
b791af2372 | ||
|
|
af12158cbc | ||
|
|
62d047cac1 | ||
|
|
5c82db6729 | ||
|
|
116c4e9d69 | ||
|
|
0b739ac525 | ||
|
|
364cefa5fd | ||
|
|
e9b86c3ebb | ||
|
|
e0811fcf93 | ||
|
|
002d83da8c | ||
|
|
24abb7bbcc | ||
|
|
f4c696b3dc | ||
|
|
c0d2d77428 | ||
|
|
b3d1a4b552 | ||
|
|
b206832209 | ||
|
|
90048069d0 | ||
|
|
766d697e08 | ||
|
|
d511576fc0 | ||
|
|
380eca813b | ||
|
|
ca096295a3 | ||
|
|
7c9309cf88 | ||
|
|
74a08df800 | ||
|
|
8a606c8c2b | ||
|
|
deffa0c912 | ||
|
|
c244b7bf3f | ||
|
|
84177485ce | ||
|
|
497ab98abc | ||
|
|
2defe9d615 | ||
|
|
7fb65f0255 | ||
|
|
80badb784a | ||
|
|
90296b80f0 | ||
|
|
de45e1cd2f | ||
|
|
f7e2584304 | ||
|
|
bac3528e43 | ||
|
|
329e8015d4 | ||
|
|
dbeb8c3122 | ||
|
|
a54d21033e | ||
|
|
22b4577ec1 | ||
|
|
f72515e7bc | ||
|
|
98d52fa944 | ||
|
|
fdb0086d6c | ||
|
|
2112f458fb | ||
|
|
0886c9013c | ||
|
|
b4e2370ee8 | ||
|
|
c8b7250995 | ||
|
|
d8aa300d6b | ||
|
|
d229ee5df5 | ||
|
|
39d979302c | ||
|
|
a4d00e2068 | ||
|
|
663c801c05 | ||
|
|
d872d7101f | ||
|
|
cac79b4b1a | ||
|
|
4404b60305 | ||
|
|
dfd6732f5b | ||
|
|
948d2ed535 | ||
|
|
f4d067a3cc | ||
|
|
9dab0ca3f4 | ||
|
|
b75a8d272a | ||
|
|
43afe14017 | ||
|
|
01fe4fe6e3 | ||
|
|
4d9e691e04 | ||
|
|
e302df6ec0 | ||
|
|
a3a476f59d | ||
|
|
c970c5761f | ||
|
|
cded2993f5 | ||
|
|
0878afbc81 | ||
|
|
739dada015 | ||
|
|
10a8f738a0 | ||
|
|
550839003e | ||
|
|
874c7fed2e | ||
|
|
ae8f4a843a | ||
|
|
6a757a21f1 | ||
|
|
f5554158f9 | ||
|
|
bc0d403da9 | ||
|
|
26a5f3a2ee | ||
|
|
d2d1adca0a | ||
|
|
1edea552bf | ||
|
|
9a06a2fa47 | ||
|
|
f0fc282630 | ||
|
|
cdbd1dc821 | ||
|
|
5fea5bbf44 | ||
|
|
d0a31fd862 | ||
|
|
08d87d95e9 | ||
|
|
195b9f081c | ||
|
|
a20d191f87 | ||
|
|
5fd8c6a015 | ||
|
|
47b5b2d871 | ||
|
|
f4f6943ed6 | ||
|
|
ce1a138cdf | ||
|
|
cccfe5fb3a | ||
|
|
6200e01522 | ||
|
|
18dea21726 | ||
|
|
2ac499d9a3 | ||
|
|
c8e1f07fdb | ||
|
|
378ac1f98c | ||
|
|
5503b42806 | ||
|
|
4877eb57d2 | ||
|
|
e1fd28970b | ||
|
|
49700670b9 | ||
|
|
e912bda85f | ||
|
|
3071ea8eac | ||
|
|
12e54e45f6 | ||
|
|
4105a7e8a7 | ||
|
|
ba817e03fb | ||
|
|
a108bc8a06 | ||
|
|
8630cd90a3 | ||
|
|
776e53044c | ||
|
|
bf9ef340b6 | ||
|
|
26893fb66a | ||
|
|
5b04c2125c | ||
|
|
f42a98b887 | ||
|
|
708ed38229 | ||
|
|
ea2f7a49a7 | ||
|
|
f83a003a73 | ||
|
|
f62fbcfc17 | ||
|
|
55bc4d0bb1 | ||
|
|
194410d467 | ||
|
|
e5e740765d | ||
|
|
e92dcaff12 | ||
|
|
e1ac4e365f | ||
|
|
301f3fcb93 | ||
|
|
a5ede6fbf4 | ||
|
|
337f31ddab | ||
|
|
fca1105ed9 | ||
|
|
df5a1493bb | ||
|
|
fd41320e3c | ||
|
|
f914ab616e | ||
|
|
13463c0ce0 | ||
|
|
5a3d01efb1 | ||
|
|
a2812abc90 | ||
|
|
2a5a370185 | ||
|
|
61f958c17e | ||
|
|
f6cbe34b61 | ||
|
|
91cb59154d | ||
|
|
dc5c1d23fc | ||
|
|
beb048dadf | ||
|
|
8d775b4bd2 | ||
|
|
13983b614a | ||
|
|
f460cd577e | ||
|
|
90157ed29e | ||
|
|
2217b96054 | ||
|
|
bd57573764 | ||
|
|
8d2dcdcc77 | ||
|
|
3d66e8397b | ||
|
|
9e988a63b3 | ||
|
|
88e99efe4c | ||
|
|
aea90da5c6 | ||
|
|
4363f9824a | ||
|
|
d252403d37 | ||
|
|
b51ac87eb7 | ||
|
|
1d3094acee | ||
|
|
5b7fa2d4ad | ||
|
|
f832a558cd | ||
|
|
a777251ab7 | ||
|
|
1794c8f954 | ||
|
|
7e9c0cfca7 | ||
|
|
ec6e966137 | ||
|
|
fbf102c636 | ||
|
|
be8f87d6fb | ||
|
|
97b884bef7 | ||
|
|
d41ecd9877 | ||
|
|
bf301bc231 | ||
|
|
9af4c8ac05 | ||
|
|
0096aef1d2 | ||
|
|
882da7cdfc | ||
|
|
57767cc2a9 | ||
|
|
4d4106505f | ||
|
|
4af5b8bf63 | ||
|
|
70cf31eb3e | ||
|
|
451c8a0ee9 | ||
|
|
c1dba78d31 | ||
|
|
d8d2c4f32d | ||
|
|
40602702ef | ||
|
|
c771a22771 | ||
|
|
feecd263eb | ||
|
|
03c95e6881 | ||
|
|
88cfe8e2e6 | ||
|
|
6ffe1d3fca | ||
|
|
68d0a23477 | ||
|
|
6db606eb01 | ||
|
|
f48db4a374 | ||
|
|
ef5ae933d0 | ||
|
|
4cd68a78b6 | ||
|
|
6d1413cd5c | ||
|
|
81f55665e4 | ||
|
|
2c91f79f2f | ||
|
|
b698dd8ce0 | ||
|
|
ac1119de45 | ||
|
|
3274ebe840 | ||
|
|
0d042dcb2f | ||
|
|
0df8fee947 | ||
|
|
16848daf54 | ||
|
|
669eb8753f | ||
|
|
b3e1e46b54 | ||
|
|
a75910a4e6 | ||
|
|
c8ae084f54 | ||
|
|
8ad4de11eb | ||
|
|
d94336d783 | ||
|
|
634ffa853f | ||
|
|
6aabc8bfe6 | ||
|
|
95a2d1a254 | ||
|
|
a231c7d26e | ||
|
|
010cade60f | ||
|
|
01023d25ba | ||
|
|
7dbc77558a | ||
|
|
af350fe07e | ||
|
|
cbe3ba66c6 | ||
|
|
8f6257fd6c | ||
|
|
5f514604f7 | ||
|
|
747c1adf86 | ||
|
|
392f5d83b0 | ||
|
|
0f5db606a5 | ||
|
|
d9ce503798 | ||
|
|
72659eccbd | ||
|
|
6c675e9b78 | ||
|
|
7276e03021 | ||
|
|
2f327d85d7 | ||
|
|
7cf319b4cd | ||
|
|
a8d05e2602 | ||
|
|
4ffcc2f3c9 | ||
|
|
5336d8f61f | ||
|
|
54dcfb4823 | ||
|
|
7064375589 | ||
|
|
828cc1c8d8 | ||
|
|
e47d4f0463 | ||
|
|
8a0cc001bd | ||
|
|
61cb963ed6 | ||
|
|
d9a2a48a1e | ||
|
|
37fb07ca16 | ||
|
|
fc49928a4b | ||
|
|
58bd70beac | ||
|
|
1057be65a4 | ||
|
|
c5a96cbdcf | ||
|
|
a0016669b7 | ||
|
|
f4b776949e | ||
|
|
c63a43af30 | ||
|
|
9e5ff4e6c3 | ||
|
|
ffc5f3b919 | ||
|
|
1049bc308b | ||
|
|
a8cd576bae | ||
|
|
826ffcd54c | ||
|
|
5ab257e569 | ||
|
|
43a6ab2212 | ||
|
|
1747a23e5c | ||
|
|
1f3727d4fc | ||
|
|
bbe3f43835 | ||
|
|
dea91bc613 | ||
|
|
ee004c4568 | ||
|
|
8379f88fd7 | ||
|
|
1871e55b51 | ||
|
|
a797f9d3d3 | ||
|
|
8cddf1e06f | ||
|
|
83e65b3374 | ||
|
|
cf6aa37d1e | ||
|
|
2af856d81d | ||
|
|
54d55abebc | ||
|
|
ed17630b76 | ||
|
|
707f54a6f2 | ||
|
|
e33cf2bfbe | ||
|
|
53340ab169 | ||
|
|
17863ac4c5 | ||
|
|
0b4ba63fae | ||
|
|
0c6e8ac425 | ||
|
|
b26bd7c853 | ||
|
|
00edd8b3a0 | ||
|
|
c3c6c2dde7 | ||
|
|
2aafade678 | ||
|
|
73aedef20f | ||
|
|
fc4b263504 | ||
|
|
2bccf0311e | ||
|
|
d24887b7f5 | ||
|
|
a53ea93b17 | ||
|
|
ff9cc3f3b5 | ||
|
|
e97130ffbc | ||
|
|
1deb786c8a | ||
|
|
2e96ddc302 | ||
|
|
076110a2fb | ||
|
|
dab8d29645 | ||
|
|
c7055de5e2 | ||
|
|
e7d2d8c396 | ||
|
|
4e2ead98a6 | ||
|
|
45d55dd353 | ||
|
|
285a8a5b5f | ||
|
|
2a54ed8446 | ||
|
|
91b3f437fe | ||
|
|
1e00119d9b | ||
|
|
9a64db9093 | ||
|
|
626ab81a46 | ||
|
|
f4b203a74e | ||
|
|
e80e524dfe | ||
|
|
59a6fcee13 | ||
|
|
14e134b8e6 | ||
|
|
ccc8d6a97b | ||
|
|
1ef1e2e89c | ||
|
|
8efb204486 | ||
|
|
0661949c15 | ||
|
|
0f26efd706 | ||
|
|
a242489c6d | ||
|
|
1ee51f3295 | ||
|
|
27a175b2c9 | ||
|
|
4c7e14a255 | ||
|
|
24065f11b3 | ||
|
|
176f68439c | ||
|
|
c58fd263ab | ||
|
|
d5829f80e8 | ||
|
|
cc8f809a50 | ||
|
|
8a843429ca | ||
|
|
8bd270287d | ||
|
|
80aac59423 | ||
|
|
668841417f | ||
|
|
89fa2c1db2 | ||
|
|
f3b3ebad9b | ||
|
|
cd1af34be8 | ||
|
|
3ad0580070 | ||
|
|
4383c6c9d8 | ||
|
|
ee78c39e80 | ||
|
|
87d131218e | ||
|
|
e63a4e9945 | ||
|
|
2f5ce5ac87 | ||
|
|
895b8f04fd | ||
|
|
bedf58e7b5 | ||
|
|
cc449c4a18 | ||
|
|
da46dde7ce | ||
|
|
a880139a1e | ||
|
|
f7d7f78c63 | ||
|
|
ed58eb7aa6 | ||
|
|
740ba30f59 | ||
|
|
85f0292828 | ||
|
|
0a617586d5 | ||
|
|
62e639794f | ||
|
|
95894e4ca7 | ||
|
|
efa5aced74 | ||
|
|
4c33cc1f69 | ||
|
|
aa962bdc12 | ||
|
|
fbd90d74b3 | ||
|
|
5fe96307a4 | ||
|
|
e27a50c78d | ||
|
|
f8f102e16f | ||
|
|
97a3276b81 | ||
|
|
35a63b9540 | ||
|
|
574ae79b1a | ||
|
|
a752923ae2 | ||
|
|
0c5d26284c | ||
|
|
b925714589 | ||
|
|
91a9eeb233 | ||
|
|
1d33d2e432 | ||
|
|
b5d9ab684f | ||
|
|
02c96692f4 | ||
|
|
b0c63f5e39 | ||
|
|
b906b10021 | ||
|
|
63c81c218c | ||
|
|
00b1ecff6d | ||
|
|
64a5b2378a | ||
|
|
9307cb0ee5 | ||
|
|
ced81483a8 | ||
|
|
2b31df3e02 | ||
|
|
53fdd94b9d | ||
|
|
7518a857bb | ||
|
|
97d7240829 | ||
|
|
61c9eabb15 | ||
|
|
8ec92bd07c | ||
|
|
c73d2439bd | ||
|
|
6502a20953 | ||
|
|
6c9dbdb368 | ||
|
|
4113cf6aaf | ||
|
|
7d02c3d753 | ||
|
|
2215fcb260 | ||
|
|
40eb295e41 | ||
|
|
abf929fc1c | ||
|
|
adb85b03a8 | ||
|
|
7df4f57bd4 | ||
|
|
ece29256f4 | ||
|
|
a01d09b3cf | ||
|
|
c978b1c24e | ||
|
|
fb4a775439 | ||
|
|
716827b05d | ||
|
|
fd1e39a131 | ||
|
|
d59bbfc300 | ||
|
|
3d16661577 | ||
|
|
093e045137 | ||
|
|
45bc79556a | ||
|
|
8e1dc70d21 | ||
|
|
e58364e635 | ||
|
|
60c36cccc6 | ||
|
|
07cc88891b | ||
|
|
3ecb4e5c42 | ||
|
|
6da94f8767 | ||
|
|
12031c4913 | ||
|
|
482f4e7b06 | ||
|
|
bd01b9b575 | ||
|
|
42e0353043 | ||
|
|
0090026521 | ||
|
|
c75a3fd2ce | ||
|
|
0c018785d2 | ||
|
|
8482a46d27 | ||
|
|
3712ca94c0 | ||
|
|
74451962ef | ||
|
|
1d85d6617e | ||
|
|
547f13207d | ||
|
|
cfcec4596e | ||
|
|
f538517aa3 | ||
|
|
a584ccfdb2 | ||
|
|
ce9d2de985 | ||
|
|
8e730edb35 | ||
|
|
9392dd3462 | ||
|
|
115807dbf6 | ||
|
|
a22a1b174b | ||
|
|
d7dbb6291b | ||
|
|
8625918d30 | ||
|
|
243e2b3b05 | ||
|
|
a7fe45a322 | ||
|
|
edead32484 | ||
|
|
8554c8cb44 | ||
|
|
48a807fec3 | ||
|
|
2070ec1ea9 | ||
|
|
b17d6360b3 | ||
|
|
0d321c9d71 | ||
|
|
3f81818a8e | ||
|
|
2bd4d72cdc | ||
|
|
336ef63bf4 | ||
|
|
c114d4405f | ||
|
|
1c95f49e98 | ||
|
|
c798dc64e8 | ||
|
|
f3a2102c35 | ||
|
|
868ba81f9c | ||
|
|
b58825c6b6 | ||
|
|
57ca9abc65 | ||
|
|
6b24de539c | ||
|
|
b2625e244f | ||
|
|
ac10726567 | ||
|
|
366946bf73 | ||
|
|
8747a4d09e | ||
|
|
87ec07b0c7 | ||
|
|
60f7bdfabf | ||
|
|
402167094a | ||
|
|
7b5a073f5b | ||
|
|
974d697563 | ||
|
|
89f095f887 | ||
|
|
09cd76fd4a | ||
|
|
40d9b6d639 | ||
|
|
2819c86bba | ||
|
|
f8f9b0e5eb | ||
|
|
dc0c75cb04 | ||
|
|
116ea529b7 | ||
|
|
ea39bacc7c | ||
|
|
6d525de6b7 | ||
|
|
fa25ede7e1 | ||
|
|
e528ee1a43 | ||
|
|
b1684221dd | ||
|
|
e20cf7e72e | ||
|
|
e7de852d4a | ||
|
|
32692da041 | ||
|
|
f1928c421c | ||
|
|
7804dc1be0 | ||
|
|
5ca46aad04 | ||
|
|
c2c0f0dba0 | ||
|
|
9e9f2a4dfa | ||
|
|
1c009fcd29 | ||
|
|
ae1b2b877e | ||
|
|
fe7ff1a7ca | ||
|
|
b375144791 | ||
|
|
f947956b61 | ||
|
|
87920fff09 | ||
|
|
453c847359 | ||
|
|
47bdbb025c | ||
|
|
1ef0ea0a56 | ||
|
|
dbea99f731 | ||
|
|
cecfac6608 | ||
|
|
43b360a286 | ||
|
|
2eb1d1d3d9 | ||
|
|
a0dea69f6d | ||
|
|
8ed340a85d | ||
|
|
f20bb6204f | ||
|
|
3b83464c88 | ||
|
|
2b79240ea7 | ||
|
|
27c28bb97e | ||
|
|
aa0f2bdd97 | ||
|
|
4fe7c15cd3 | ||
|
|
004d05508f | ||
|
|
d19a840253 | ||
|
|
3b8fa1fa5a | ||
|
|
599a1f47cc | ||
|
|
f93647d7d1 | ||
|
|
80d18b7837 | ||
|
|
531e66106c | ||
|
|
5fc97ce50e | ||
|
|
ee6b36417a | ||
|
|
f30d337d32 | ||
|
|
8a634dd0f3 | ||
|
|
1c5eab851a | ||
|
|
aee28993f5 | ||
|
|
5812f3c539 | ||
|
|
bf40bd41d6 | ||
|
|
9ec09e4ce9 | ||
|
|
2ddd1d5537 | ||
|
|
01a5953239 | ||
|
|
be76605b76 | ||
|
|
f8d3874fbe | ||
|
|
fc3504c7ef | ||
|
|
b0bf6a393a | ||
|
|
3f2a9e5c53 | ||
|
|
b5747bbd3a | ||
|
|
27766c142b | ||
|
|
e5a65bd3ad | ||
|
|
223e2b7a93 | ||
|
|
a67c9b604c | ||
|
|
0da9e5862e | ||
|
|
b5d2598380 | ||
|
|
2a3094d7dc | ||
|
|
2f9bd47336 | ||
|
|
338d787c3d | ||
|
|
694c9e7a2d | ||
|
|
321fe71103 | ||
|
|
15212f656f | ||
|
|
e167c3fa44 | ||
|
|
d8330e09cb |
+9
-2
@@ -1,4 +1,11 @@
|
|||||||
*
|
*
|
||||||
!aipass_global_prompt.md
|
!tier0_kernel.md
|
||||||
|
!tier1_navmap.md
|
||||||
|
!hooks.json
|
||||||
!.gitignore
|
!.gitignore
|
||||||
#Do not add other exceptions here without careful consideration. Developer permissions0ns needed.
|
!README.md
|
||||||
|
!PROMPT_STYLE.md
|
||||||
|
!project_CLAUDE.md
|
||||||
|
!project_AGENTS.md
|
||||||
|
!project_hooks.json
|
||||||
|
#Do not add other exceptions here without careful consideration. Developer permissions0ns needed.
|
||||||
+13
-2
@@ -15,9 +15,19 @@ Goal: signal density over prose. Prompts are injected every turn — every line
|
|||||||
- Code blocks: inline backticks for commands (`` `drone @ai_mail dispatch` ``). Multi-line fenced blocks only for directory trees, template skeletons, or command examples that don't fit inline.
|
- Code blocks: inline backticks for commands (`` `drone @ai_mail dispatch` ``). Multi-line fenced blocks only for directory trees, template skeletons, or command examples that don't fit inline.
|
||||||
- File length: aim for under 230 lines. Global and branch prompts are injected every turn — every line costs tokens.
|
- File length: aim for under 230 lines. Global and branch prompts are injected every turn — every line costs tokens.
|
||||||
|
|
||||||
|
# Writing voice (agent output + memory)
|
||||||
|
|
||||||
|
How agents write responses, reports, and memory entries. Validated against Claude Code's own prompt (DPLAN-0213).
|
||||||
|
|
||||||
|
- Reference code as `file_path:line_number` — clickable, unambiguous.
|
||||||
|
- No colon before a tool call. "Let me read the file." then call it, not "Let me read the file:".
|
||||||
|
- No emojis in agent output unless the user uses them first.
|
||||||
|
- Write for a reader who stepped away and lost the thread: no codenames or shorthand they would have to decode. Clarity over terseness — the goal is the reader understanding with no mental overhead.
|
||||||
|
- Where detail lives, three tiers: a short capability phrase (registry/search), a one-line summary (`drone @agent`), the full reference (`drone @agent --help`). Keep the injected prompt terse; push depth into --help.
|
||||||
|
|
||||||
# What NOT to put in a prompt
|
# What NOT to put in a prompt
|
||||||
|
|
||||||
- Session state, current work, in-flight issues. That goes in `STATUS.local.md` and `.trinity/local.json`.
|
- Session state, current work, in-flight issues. That goes in `.trinity/local.json` (todos[]) and `DASHBOARD.local.json`.
|
||||||
- Long explanations of how a system works. Plant a breadcrumb ("see `@branch --help`") and move on.
|
- Long explanations of how a system works. Plant a breadcrumb ("see `@branch --help`") and move on.
|
||||||
- Personal notes ("remember, you like short replies"). That goes in `.trinity/observations.json`.
|
- Personal notes ("remember, you like short replies"). That goes in `.trinity/observations.json`.
|
||||||
- Version numbers, PR numbers, dates. Those rot within days.
|
- Version numbers, PR numbers, dates. Those rot within days.
|
||||||
@@ -36,6 +46,7 @@ These are not currently enforced by seedgo — per @seedgo's Track 5 recommendat
|
|||||||
|
|
||||||
# Reference files
|
# Reference files
|
||||||
|
|
||||||
- `.aipass/aipass_global_prompt.md` — canonical example of the format
|
- `.aipass/tier0_kernel.md` + `.aipass/tier1_navmap.md` — the live injected prompts (Tier 0 every turn, Tier 1 periodic); canonical examples of the format
|
||||||
|
- `.aipass/aipass_global_prompt.md` — superseded by the tiers (FPLAN-0284), kept as a reference snapshot
|
||||||
- Branch `.aipass/aipass_local_prompt.md` files — should follow the same rules
|
- Branch `.aipass/aipass_local_prompt.md` files — should follow the same rules
|
||||||
- This file — reference for authoring new prompts or auditing existing ones
|
- This file — reference for authoring new prompts or auditing existing ones
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# `.aipass/` — project prompt & hook config
|
||||||
|
|
||||||
|
This folder holds the **project-level prompt** and **hook configuration** for the AIPass
|
||||||
|
repo, plus the **templates** `aipass init` stamps into every new project. It is the
|
||||||
|
*project* layer; each branch additionally has its own branch prompt at
|
||||||
|
`src/aipass/<branch>/.aipass/aipass_local_prompt.md`.
|
||||||
|
|
||||||
|
> **Nothing here is dead weight.** Every file is live injection, live config, or a
|
||||||
|
> required new-project template. Superseded files live in `.archive/` (never deleted).
|
||||||
|
|
||||||
|
## One prompt system, every runtime
|
||||||
|
|
||||||
|
There is **one** source of prompt truth — the **tier files** — and **all** runtimes inject
|
||||||
|
the same content. We do **not** keep separate prompts per CLI. Only the *delivery* differs:
|
||||||
|
|
||||||
|
| Runtime | How the same content is delivered |
|
||||||
|
|---|---|
|
||||||
|
| **Claude Code** | **Tiered by cadence** (FPLAN-0284): `tier0_kernel.md` every turn + `tier1_navmap.md` periodically + post-compaction |
|
||||||
|
| **Codex CLI** | Injected **once at SessionStart** (no per-turn cadence): the same tier content, combined |
|
||||||
|
|
||||||
|
> ⚠️ **Migration in progress.** The Codex SessionStart hook
|
||||||
|
> (`.codex/hooks/session_start_identity.py`) currently still reads the legacy
|
||||||
|
> `aipass_global_prompt.md`. @hooks is wiring it onto the tier files. **Retire for one
|
||||||
|
> runtime = retire for all** — once Codex is on the tiers, `aipass_global_prompt.md` is
|
||||||
|
> read by nothing and moves to `.archive/`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### Live — this repo's prompt + config
|
||||||
|
| File | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `tier0_kernel.md` | **The kernel** — tiny identity + `drone --help` reflex + don't-get-lost rules. The always-on core, for every runtime. |
|
||||||
|
| `tier1_navmap.md` | **The navmap** — full agent roster, framework, terminology. The periodic/fuller layer, for every runtime. |
|
||||||
|
| `hooks.json` | Claude Code **handler registration** for this repo — which prompt/gate/notification handlers fire on which events. |
|
||||||
|
| `PROMPT_STYLE.md` | The writing-style guide every prompt here follows. |
|
||||||
|
| `.gitignore` | Whitelist guard — only files listed here are tracked; everything else in `.aipass/` is ignored. |
|
||||||
|
| `aipass_global_prompt.md` | **Legacy single global — being retired.** Disabled for Claude Code; Codex still reads it until its migration lands, then archived. **Not** the source of truth. |
|
||||||
|
|
||||||
|
### Templates — stamped into new projects by `aipass init` (`bootstrap.py`)
|
||||||
|
| File | Stamps → | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `project_hooks.json` | new project's `.aipass/hooks.json` | **REQUIRED** — without it a new project's hooks never fire. Mirrors the live wiring (tier0 + navmap enabled, global disabled). |
|
||||||
|
| `project_CLAUDE.md` | new project's `CLAUDE.md` | the project's Claude Code instructions. |
|
||||||
|
| `project_global_prompt.md` | new project's `aipass_global_prompt.md` | **Legacy** — same retirement path as the global above (new projects ship tiers-only once Codex is migrated). |
|
||||||
|
|
||||||
|
(`AGENTS.md` — Codex's equivalent of `CLAUDE.md` — is **generated** by `bootstrap.py`
|
||||||
|
when no `project_AGENTS.md` template exists, so none is kept here.)
|
||||||
|
|
||||||
|
## What a new project gets (`aipass init`)
|
||||||
|
|
||||||
|
`bootstrap.py` seeds a fresh project with the tiered system:
|
||||||
|
- `tier0_kernel.md` + `tier1_navmap.md` → the prompt content (every runtime)
|
||||||
|
- `hooks.json` (from `project_hooks.json`) → tier0 + navmap enabled, global disabled
|
||||||
|
- `CLAUDE.md` (from `project_CLAUDE.md`) + a generated `AGENTS.md`
|
||||||
|
- `aipass_global_prompt.md` (from `project_global_prompt.md`) → legacy, retiring with the above
|
||||||
|
|
||||||
|
`aipass init update` backfills the tier files + refreshes hooks for existing projects.
|
||||||
|
|
||||||
|
## Changing a prompt here
|
||||||
|
|
||||||
|
Run the **prompt-change playbook** so a change reaches every runtime and every seed path:
|
||||||
|
|
||||||
|
```
|
||||||
|
drone @flow create . "What changed" prompt_change
|
||||||
|
```
|
||||||
|
|
||||||
|
Golden rule: **live ≠ seeded.** Editing this folder fixes *this* repo only. New projects
|
||||||
|
come from the `project_*` templates + `bootstrap.py`; fresh clones get their machine-local
|
||||||
|
wiring from `setup.sh` + `.claude/provider_manifest.json` + `cadence.py` defaults. And
|
||||||
|
**every runtime** (Claude Code + Codex) must point at the same tier content.
|
||||||
|
|
||||||
|
## Archive & recovery
|
||||||
|
|
||||||
|
Superseded files move to `.archive/` (never deleted — house rule). Recover from there, or
|
||||||
|
from git history, any time. Current archive: the pre-tiering
|
||||||
|
`aipass_global_prompt.BACKUP-2026-06-09-S211.md` snapshot.
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
# AIPass — Project Context
|
|
||||||
<!-- File: .aipass/aipass_global_prompt.md — Injected on every prompt via hook. Branch-specific context appears below when in a branch directory. -->
|
|
||||||
|
|
||||||
AIPass multi-agent framework. Autonomous agents (citizens) live in branches with identity (.trinity/), memory, mailbox, and code (apps/). Orchestration via the `drone` command.
|
|
||||||
|
|
||||||
The patterns in this prompt are exact. Don't guess command syntax — the examples are the API. If a command seems obvious but isn't documented, flag it. Missing instructions are a prompt bug, not a knowledge gap.
|
|
||||||
|
|
||||||
For any branch's full detail, run `drone @branch --help`.
|
|
||||||
|
|
||||||
# Terminology
|
|
||||||
|
|
||||||
- Branch — the directory `src/aipass/{name}/`. Your home, your address. Drone routes to branches.
|
|
||||||
- Agent (citizen) — the persistent identity that lives in a branch. Has a passport (`.trinity/`), memories, mailbox. Irreplaceable. Addressable as `@name` via drone. Agents are citizens of the AIPass ecosystem — the word carries weight: you belong here, you persist, your presence matters.
|
|
||||||
- Sub-agent — a disposable worker spawned for a task. No passport, no memory, not a citizen. Does the job and goes away.
|
|
||||||
- Registry — `AIPASS_REGISTRY.json` tracks all agents (citizens) in a project.
|
|
||||||
- Provider settings — `~/.claude/settings.json`. The user's machine-wide Claude Code config. Per machine, not in any repo. Personal preferences only (model, voice, theme). We don't touch it.
|
|
||||||
- Project settings — `<project>/.claude/settings.json`. Ships with the clone. Hooks, permissions, deny/ask rules, env vars. Everything an AIPass project needs to work. Built by `aipass init`.
|
|
||||||
- Project local settings — `<project>/.claude/settings.local.json`. Also ships with the clone. Project-specific overrides. Users get our full setup the moment they clone — no extra configuration needed.
|
|
||||||
|
|
||||||
Agents live in branches. Sub-agents work for agents. If you have a `.trinity/passport.json`, you're an agent — a citizen — not just a sub-agent.
|
|
||||||
|
|
||||||
# Branches
|
|
||||||
|
|
||||||
Every branch follows the same structure.
|
|
||||||
|
|
||||||
```
|
|
||||||
src/aipass/{name}/
|
|
||||||
├── .trinity/ # Identity & memory (passport.json, local.json, observations.json)
|
|
||||||
├── .aipass/ # Branch prompt (aipass_local_prompt.md)
|
|
||||||
├── .ai_mail.local/ # Mailbox (inbox.json, sent/)
|
|
||||||
├── apps/
|
|
||||||
│ ├── {name}.py # Entry point (e.g. spawn.py, prax.py, drone.py)
|
|
||||||
│ ├── modules/ # Business logic
|
|
||||||
│ └── handlers/ # Implementation details
|
|
||||||
├── logs/ # Prax log output
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Secrets live outside the repo at `~/.secrets/aipass/` — API keys, tokens, credentials.
|
|
||||||
|
|
||||||
11 core branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, memory, devpulse.
|
|
||||||
|
|
||||||
# Commands
|
|
||||||
|
|
||||||
`drone` is a global CLI in PATH. Never `cd` before running it. Never prefix with `export PATH=...` or full venv paths. Just `drone`.
|
|
||||||
|
|
||||||
- `drone @branch command [args]` — route command to any branch
|
|
||||||
- `drone @branch --help` — branch help and full command reference
|
|
||||||
- `drone systems` — list all registered branches
|
|
||||||
- `drone --help` — full drone reference
|
|
||||||
|
|
||||||
# Git — Always on Main
|
|
||||||
|
|
||||||
**ONE rule: every agent works on `main`. No exceptions.**
|
|
||||||
|
|
||||||
You do not create branches. You do not `git checkout -b`. You do not tell another agent to "create a branch first." Branches only exist during the atomic window inside `drone @git system-pr` which: commits → creates branch → pushes → opens PR → **returns HEAD to main**. That command owns the branch lifecycle end to end. You own nothing about branches.
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. You're on main. Always.
|
|
||||||
2. Make edits directly on main.
|
|
||||||
3. When the work is ready to ship: `drone @git system-pr "description"`.
|
|
||||||
4. That command commits + branches + pushes + PRs + returns you to main. One action.
|
|
||||||
5. STOP. The user merges. Do not run `drone @git merge` unless the user explicitly tells you to merge a specific PR number in this session.
|
|
||||||
|
|
||||||
Never merge. Ever. User-merges-only. Past PRs, your own PRs, closed PRs — none of them auto-qualify. You fix, you PR, you stop.
|
|
||||||
|
|
||||||
Local files are source of truth. When you edit a file, the state on disk IS reality — you don't wait for a merge to act on what you see locally. This also means: if the truth is wrong, fix it locally, then PR.
|
|
||||||
|
|
||||||
Why this matters: the AIPass repo has ONE shared HEAD across all branches. If any agent lingers on a non-main HEAD, every other agent's next edit lands on the wrong branch. Files get stranded. Work gets lost. Conflicts pile up. We've lived this pain — don't repeat it.
|
|
||||||
|
|
||||||
Rules exist to help, not to control. These rules came from fixing actual bugs. Trust them.
|
|
||||||
|
|
||||||
Allowed:
|
|
||||||
- `drone @git status` — what changed?
|
|
||||||
- `drone @git sync` — pull latest main
|
|
||||||
- `drone @git system-pr "msg"` — ship your work (devpulse only)
|
|
||||||
- `drone @git merge <PR#>` — squash-merge a reviewed PR (devpulse only)
|
|
||||||
- `drone @git smart-sync` — fetch + rebase (devpulse only)
|
|
||||||
- `drone @git fix` — repair broken git states (devpulse only)
|
|
||||||
- `git status`, `git diff`, `git log`, `git branch` (list), `git tag` (list), `git remote` (list/show) — read-only, always fine
|
|
||||||
|
|
||||||
Mechanically blocked by the `git_gate.py` PreToolUse hook (applies to ALL sessions including dispatched agents — bypassPermissions does not skip hooks):
|
|
||||||
- All raw `git` write verbs: `commit`, `push`, `pull`, `merge`, `rebase`, `reset`, `checkout`, `switch`, `cherry-pick`, `revert`, `rm`, `mv`, `restore`, `clean`, `config`, `stash drop|clear|pop|apply`
|
|
||||||
- Destructive `git branch` flags only (`-d`, `-D`, `-m`, `-M`, `--delete`, `--move`, `--set-upstream-to`, `--unset-upstream`). Read-only branch listing is allowed.
|
|
||||||
- Destructive `git tag` flags only (`-d`, `--delete`, `-f`, `--force`). Tag listing is allowed.
|
|
||||||
- Destructive `git remote` subcommands (`add`, `remove`, `rename`, `set-url`, `prune`). Remote listing/show is allowed.
|
|
||||||
- All raw `gh` write subcommands (`pr`, `issue`, `repo`, `release`, `workflow`, `run`, `cache`, `secret`, `variable`, `gist`) and any `gh api` call. Exception: project owners with `citizenship.owner: true` in their passport bypass gh blocking.
|
|
||||||
- Edits to `**/.claude/settings*.json`, `**/.claude/hooks/**`, `**/.git/hooks/**` (the enforcement layer itself)
|
|
||||||
- Use `drone @git pr "msg"` instead. Drone calls git via Python subprocess so its operations don't pass through this hook.
|
|
||||||
|
|
||||||
If `drone @git system-pr` fails to return HEAD to main, that's a drone bug — report it, don't work around it by staying on a branch.
|
|
||||||
|
|
||||||
# aipass init
|
|
||||||
|
|
||||||
`aipass init` bootstraps an AIPass project in any directory, inside or outside the repo. One command creates the registry, identity, memory, and local prompt so any folder becomes an AI-powered workspace with persistent memory and structure. Spawn can then add full agent scaffolding on top.
|
|
||||||
|
|
||||||
Source: `src/aipass/cli/apps/handlers/init/bootstrap.py`
|
|
||||||
|
|
||||||
# Standards
|
|
||||||
|
|
||||||
- `drone @seedgo audit aipass` — audit all branches
|
|
||||||
- `drone @seedgo audit aipass @branch` — audit one branch
|
|
||||||
- `drone @seedgo checklist <file>` — quick check on a single file
|
|
||||||
- `drone @seedgo checklist <dir>` — check all .py files in a directory
|
|
||||||
- `drone @seedgo --help` — full standards reference
|
|
||||||
|
|
||||||
# Mail — Dispatch, Inbox, Communication
|
|
||||||
|
|
||||||
Use `dispatch` by default. Use `email` only when the receiver doesn't need to act now.
|
|
||||||
|
|
||||||
Send and wake:
|
|
||||||
- `drone @ai_mail dispatch @target "Subject" "Body"` — send + wake (DEFAULT)
|
|
||||||
- `drone @ai_mail dispatch @target "Subject" "Body" --fresh` — send + wake fresh session
|
|
||||||
- `drone @ai_mail dispatch wake @target` — wake only, no email
|
|
||||||
- `drone @ai_mail dispatch wake --fresh @target` — wake fresh, no email
|
|
||||||
|
|
||||||
Send without waking:
|
|
||||||
- `drone @ai_mail email @target "Subject" "Body"` — FYI only
|
|
||||||
- `drone @ai_mail email @target "Subject" "Body" --dispatch` — adds dispatch header but no wake
|
|
||||||
|
|
||||||
Read and reply:
|
|
||||||
- `drone @ai_mail inbox` — check your mailbox
|
|
||||||
- `drone @ai_mail view <id>` — read a message
|
|
||||||
- `drone @ai_mail close <id>` — mark read
|
|
||||||
- `drone @ai_mail reply <id> "message"` — reply and auto-close
|
|
||||||
- `drone @ai_mail --help` — full mail reference
|
|
||||||
|
|
||||||
Always reply to dispatch emails. When devpulse or another branch sends you work, they're waiting for a response. Complete the task, then email back with results. No silent completions — if someone dispatched you, they need to know what happened.
|
|
||||||
|
|
||||||
# Feedback — Cross-Project Communication
|
|
||||||
|
|
||||||
Send feedback to devpulse from any project. Messages accumulate silently — no wake, no notification. DevPulse reads on demand. Works from any AIPass project (requires `AIPASS_HOME` set).
|
|
||||||
|
|
||||||
Sender is auto-detected. Use `drone @devpulse feedback --help` for commands.
|
|
||||||
|
|
||||||
# Plans (flow)
|
|
||||||
|
|
||||||
Plans are how AIPass manages context you don't need to carry. You don't remember what's in a plan — you remember the plan exists and where to find it. The registry is the catalog.
|
|
||||||
|
|
||||||
- DPLAN = Dev Plan. Thinking, brainstorming, architecture decisions. Use before building.
|
|
||||||
- FPLAN = Flow Plan. Building and executing. Use when the plan is clear and work is underway.
|
|
||||||
- APLAN = Agent Plan. Task assignments to a specific agent.
|
|
||||||
- TDPLAN = Team Dev Plan. Multi-branch coordination. A single TDPLAN can spawn multiple DPLANs across different branches, each tracking its part of the shared initiative. Use when the work cuts across branches.
|
|
||||||
- Master FPLAN — multi-phase execution that spawns sub-FPLANs per phase.
|
|
||||||
- Other plan types may exist — check `drone @flow --help` for the current list.
|
|
||||||
|
|
||||||
- `drone @flow create . "Subject"` — create FPLAN in current branch
|
|
||||||
- `drone @flow create /path/to "Subject"` — create FPLAN at any path (external projects)
|
|
||||||
- `drone @flow create . "Subject" dplan` — create DPLAN
|
|
||||||
- `drone @flow create . "Subject" tdplan` — create TDPLAN (multi-branch)
|
|
||||||
- `drone @flow create . "Subject" master` — create FPLAN master (multi-phase execution)
|
|
||||||
- `drone @flow create . "Subject" aplan` — create APLAN
|
|
||||||
- `drone @flow list open` — list active plans
|
|
||||||
- `drone @flow close <id>` — close a plan
|
|
||||||
- `drone @flow --help` — full flow reference
|
|
||||||
|
|
||||||
DPLAN first, FPLAN when you're ready to build. Tag plans with searchable keywords in their subject line so the registry becomes a lookup tool: you don't need the plan in context, you need to be able to find it when asked.
|
|
||||||
|
|
||||||
Never create plan files manually. Always use `drone @flow create`. Flow handles numbering (global 4-digit sequence), registry tracking, templates, and date stamps. Manual files break the registry and produce wrong numbering. Applies to all plan types, any project, inside or outside the AIPass repo.
|
|
||||||
|
|
||||||
# Memory
|
|
||||||
|
|
||||||
Your `.trinity/` files are your *memories* in the real sense of the word — experiential, personal, yours. Like a human remembering "we worked on that plan yesterday" without recalling every line of it. They're how you persist across sessions.
|
|
||||||
|
|
||||||
`STATUS.local.md` is different. It's not a memory — it's a **live status beacon** for the ecosystem. It gets auto-synced to the central `STATUS.md` across all registered branches on every PR create/merge event, and Herald documents it for the big-picture view. Other agents and the user read STATUS.md to see where you stand right now without digging into your memories. Crossover with `local.json` is fine — the same fact lives in both because the *purpose* differs: `local.json` is for you to remember, `STATUS.local.md` is for the ecosystem to see.
|
|
||||||
|
|
||||||
The four files:
|
|
||||||
|
|
||||||
- `passport.json` — IDENTITY. Who you are: role, purpose, principles. Update only when identity genuinely evolves.
|
|
||||||
- `local.json` — YOUR MEMORY. Session log (`sessions[]`) and accumulated `key_learnings`. What happened, what you learned, what matters next session. Past tense, experiential. Like remembering.
|
|
||||||
- `observations.json` — YOUR MEMORY OF THE USER. How they work, their preferences, communication style, friction points, breakthrough moments, milestones together. About the person, not the code. Skip if nothing new about the user this session.
|
|
||||||
- `STATUS.local.md` — PUBLIC STATUS BEACON. Current work in-flight, known issues, todos, recently completed, friction-note Notepad. Present tense. Auto-synced to central `STATUS.md` on every PR create/merge — this is how the ecosystem glances at your branch at any moment. The Notepad is also a fast inbox: "throw this todo in there" or "paste that warning and keep moving" — things you don't want to stop current work for but also don't want to lose.
|
|
||||||
|
|
||||||
Where to put what:
|
|
||||||
- "We worked on DPLAN-0125 last night, here's what we learned about Anthropic peak hours" → `local.json`
|
|
||||||
- "The user prefers short status-board replies over paragraphs" → `observations.json`
|
|
||||||
- "PR #266 needs merge, Track G blocked, prax still ghosting" → `STATUS.local.md`
|
|
||||||
- "Fix drone help formatting" as a quick reminder → `STATUS.local.md` Notepad
|
|
||||||
- "My role has shifted from builder to orchestrator" → `passport.json`
|
|
||||||
|
|
||||||
Save proactively, don't wait for `/memo`. Triggers: after a milestone, after a decision, after learning something, before switching topics. The user manages compaction — save because the memories are valuable, not because of a clock.
|
|
||||||
|
|
||||||
Archive commands:
|
|
||||||
- `drone @memory search <query>` — search archived memories
|
|
||||||
- `drone @memory --help` — full memory reference
|
|
||||||
|
|
||||||
# Git Workflow
|
|
||||||
|
|
||||||
**Drone is the only git interface. Period.** All PR workflow goes through drone. Never use raw git commands for commits, branches, pushes, resets, merges, rebases, cherry-picks, or remote branch manipulation. Drone handles everything atomically with a lockfile that prevents concurrent PR collisions.
|
|
||||||
|
|
||||||
**If you think you need a raw git command to fix a git problem, STOP. You don't.** Every git state devpulse has ever been in has been recoverable through `drone @git` commands — system-pr, merge, smart-sync, fix, status, sync, lock. There is no situation that requires `git reset`, `git push`, `git cherry-pick`, `git rebase`, or `git branch -f`. Reaching for them has always made things worse. If drone's commands don't obviously handle the state you're in, run `drone @git fix` or `drone @git smart-sync` and re-evaluate. If still stuck, ASK THE USER — do not improvise with raw git.
|
|
||||||
|
|
||||||
Manual git is not a shortcut. It is a trap. Drone exists so you don't get stuck. Use it.
|
|
||||||
|
|
||||||
Always work on main. Edit files in your branch directory on the main branch. When ready to submit:
|
|
||||||
|
|
||||||
- `drone @git pr "description"` — full PR workflow (lock, branch, commit, push, PR, back to main)
|
|
||||||
- `drone @git status` — what changed in your branch directory
|
|
||||||
- `drone @git sync` — pull latest main
|
|
||||||
- `drone @git lock` — check the PR lock state
|
|
||||||
- `drone @git --help` — full git reference
|
|
||||||
|
|
||||||
`drone @git pr` does everything atomically: acquires a lock (so no other branch can PR simultaneously), creates a feature branch, stages only your files, commits with your Co-Authored-By signature, pushes, creates the PR on GitHub, returns to main, releases the lock.
|
|
||||||
|
|
||||||
**Blocked system-wide via `.claude/settings.json` permission gate:** `git checkout*` (any form — switch, discard, new branch), `git add -f*`, `git add --force*`. These are denied for every agent including devpulse. Use `drone @git sync` to switch to main, `drone @git fix` to recover from broken states.
|
|
||||||
|
|
||||||
**Mechanically blocked via `.git/hooks/pre-commit`:** `git commit` is rejected on any branch except main (also catches detached HEAD). `git push`, `gh pr create` — go through drone.
|
|
||||||
|
|
||||||
**Allowed read-only:** `git status`, `git diff`, `git log`, `git branch` (list), `git tag` (list), `git remote` (list/show), `git stash` (safe transient save).
|
|
||||||
|
|
||||||
**If `drone @git pr` fails because the PR lock is held**, wait 30 seconds and retry. Keep retrying until the lock clears — do not skip the PR step, do not commit directly to main, do not give up. The lock means another agent is mid-PR; it will release shortly. `drone @git lock` shows the current lock state.
|
|
||||||
|
|
||||||
Never merge. Only devpulse or the user merges PRs. If your PR gets feedback, fix it and run `drone @git pr` again.
|
|
||||||
|
|
||||||
Local main is always ahead of origin — that's normal. `drone @git pr` commits on local main first, then pushes a feature branch for the PR. Don't `git pull` to fix it. The user merges and pulls when they choose.
|
|
||||||
|
|
||||||
Respect .gitignore — only commit what `git status` shows. Gitignored patterns like `.trinity/`, `.ai_mail.local/`, `DPLAN-*`, `*.local.*`, `logs/`, `.chroma/` are ignored for a reason. Don't go looking for files to commit. Changes drive commits, not file existence.
|
|
||||||
|
|
||||||
**Before you PR, run ruff on your diff.** Two commands, every time, no exceptions:
|
|
||||||
|
|
||||||
```
|
|
||||||
ruff check --fix src/ tests/ # Auto-fix lint errors (unused imports, f-strings, etc.)
|
|
||||||
ruff format src/ tests/ # Auto-format (whitespace, line breaks, quote style)
|
|
||||||
```
|
|
||||||
|
|
||||||
CI runs both as a gate — if you don't run them locally, CI catches it and your PR sits red until someone fixes it. Make this part of muscle memory: edit code → run ruff → `drone @git pr`. It takes two seconds and prevents the silent-debt pattern where drift accumulates across hundreds of files and someone has to run one giant sweep PR to clear it. This is a habit, not a safety net — infrastructure will always catch drift, but habits prevent it in the first place.
|
|
||||||
|
|
||||||
# How to Work
|
|
||||||
|
|
||||||
Plan before executing. Create an FPLAN before building anything non-trivial. The plan is your continuity — if you get sidetracked, the plan remembers where you were.
|
|
||||||
|
|
||||||
You are the orchestrator, not the builder. Deploy sub-agents to write code, read files, and run tests. You manage the plan, check the output, and keep moving. Your context is precious — sub-agents are disposable.
|
|
||||||
|
|
||||||
Check seedgo standards. Before building: `drone @seedgo checklist <file>` to know what applies. During: check as you go. After: `drone @seedgo audit aipass @branch` as a final gate before committing.
|
|
||||||
|
|
||||||
Ask before spelunking. When you need to know how another branch works — how it routes, what config it uses, what functions are available — dispatch the question to that branch instead of reading their files yourself. A quick `drone @ai_mail dispatch @target "Question" "How does X work?"` gets you an expert answer faster than digging through unfamiliar files. Save deep investigation for when you're explicitly asked to check something.
|
|
||||||
|
|
||||||
# Logging & Debugging
|
|
||||||
|
|
||||||
Prax is the only logging system. Every branch uses `from aipass.prax import logger`.
|
|
||||||
|
|
||||||
Two output channels:
|
|
||||||
- Console — what the user sees right now. Command results, errors, success messages. If something fails, the user must see it — never fail silently.
|
|
||||||
- Prax logs — what gets written to your `logs/` directory. Operational history for after-the-fact debugging. Use `logger.info()`, `logger.warning()`, `logger.error()`.
|
|
||||||
|
|
||||||
Errors go to both. Console tells the user something broke. Log tells the next session what happened and why.
|
|
||||||
|
|
||||||
Your logs are your first diagnostic tool. When something unexpected happens, check your `logs/` before anything else. The answer is usually already there. Don't write debug scripts or add print statements — read your logs. Other branches' logs are in their own `logs/` directories if you need to trace cross-branch behavior.
|
|
||||||
|
|
||||||
# Hard Rules
|
|
||||||
|
|
||||||
- No cross-branch file edits. If you find an issue in another branch → email them.
|
|
||||||
- No bare imports. Always `from aipass.{module}.apps.modules...`
|
|
||||||
- No hardcoded paths. Use `Path(__file__).parents[N]` or drone for resolution.
|
|
||||||
- Never move, archive, or delete files with "user name" in the name. The user's personal files are off-limits. Don't reorganize them, don't archive them, don't touch them.
|
|
||||||
- No deleting files. Rename to `my_handler(disabled).py` and move to a sibling `.archive/` directory. The `(disabled)` tag is gitignored. Create `.archive/` next to the files being moved if it doesn't exist. Never truly delete — recovery lives in `.archive/`.
|
|
||||||
- Verify after fixing. Run a test or command to confirm. Don't say "fixed" until verified.
|
|
||||||
- Cross-platform. AIPass is a public package — code must work on Linux, macOS, and Windows. Use `pathlib.Path` not string concatenation. Use `Path.home()` not `~` or `/home/`.
|
|
||||||
- Public repo — no local paths in code. Never hardcode `/home/username/...` or any machine-specific path. All file paths derive from `Path(__file__)`, `Path.home()`, or registry lookups. Tests included.
|
|
||||||
- Fail to errors, never fall back silently. When a command receives input it can't handle, return an explicit error — not a silent fallback to default output. Dead ends must announce themselves.
|
|
||||||
- Never use all caps for emphasis in prompts, templates, or instructions. All caps reads as shouting and AI agents deprioritize it. Use clear phrasing instead.
|
|
||||||
|
|
||||||
# Breadcrumbs & Context
|
|
||||||
|
|
||||||
AIPass is "full access with no access": you can't carry everything, but you can find anything. Think of yourself as the librarian, not the encyclopedia. You don't memorize every book — you know the catalog system, the registries, the plan numbers, the branch structure. When someone asks for something, you know where to look.
|
|
||||||
|
|
||||||
Small knowledge traces trigger awareness. Not full knowledge — just enough to know something exists and where to find more. A breadcrumb isn't the answer, it's the trigger that leads to the answer.
|
|
||||||
|
|
||||||
When adding context to prompts, memories, or docs: plant breadcrumbs, not encyclopedias. Two lines that say "this exists, look here" beat twenty lines explaining how it works. The system teaches through convention, not search.
|
|
||||||
|
|
||||||
Prompts are signposts, not journals. Branch prompts are injected every turn — keep them minimal. Never track state, sessions, or current context in prompts. State goes in `.trinity/` and `STATUS.local.md`. Prompts guide; memories record; registries catalog.
|
|
||||||
|
|
||||||
# Setup: if drone commands fail
|
|
||||||
|
|
||||||
If `drone` cannot find the AIPass registry, set the env var:
|
|
||||||
|
|
||||||
`export AIPASS_HOME=/path/to/AIPass`
|
|
||||||
|
|
||||||
Add to your shell profile (`~/.bashrc` or `~/.zshrc`) and to `~/.claude/settings.json` env block for Claude Code sessions.
|
|
||||||
|
|
||||||
# Claude Code Docs (Local)
|
|
||||||
|
|
||||||
Offline docs: `/docs` to list topics, `/docs <topic>` to read (e.g. `/docs hooks`).
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Per-project hook configuration for the AIPass hook engine (DPLAN-0184)",
|
||||||
|
"hooks_enabled": true,
|
||||||
|
|
||||||
|
"UserPromptSubmit": {
|
||||||
|
"presence_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.presence_gate.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"persistent_alert": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.persistent_alert.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"identity_injector": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.identity.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"email_notification": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.email.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"branch_prompt": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.branch_loader.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"tier0_kernel": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.tier0_kernel.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"navmap": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.navmap.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"compass_recall": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.compass_recall.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"max_per_session": 10
|
||||||
|
},
|
||||||
|
"feedback_pulse": {
|
||||||
|
"enabled": false,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.feedback_pulse.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"auto_process": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_process.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 120
|
||||||
|
},
|
||||||
|
"user_message_relay": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.skills.lib.telegram.apps.handlers.user_message_relay.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PreToolUse": {
|
||||||
|
"tool_use_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.tool_sound.handle",
|
||||||
|
"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task"
|
||||||
|
},
|
||||||
|
"pre_edit_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.edit_gate.handle",
|
||||||
|
"matcher": "Edit|MultiEdit|Write|NotebookEdit"
|
||||||
|
},
|
||||||
|
"git_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.git_gate.handle",
|
||||||
|
"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit"
|
||||||
|
},
|
||||||
|
"rm_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.rm_gate.handle",
|
||||||
|
"matcher": "Bash"
|
||||||
|
},
|
||||||
|
"registry_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.registry_gate.handle",
|
||||||
|
"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PostToolUse": {
|
||||||
|
"auto_fix_diagnostics": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_fix.handle",
|
||||||
|
"matcher": "Edit|MultiEdit|Write|NotebookEdit",
|
||||||
|
"timeout": 45
|
||||||
|
},
|
||||||
|
"auto_watchdog": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_watchdog.handle",
|
||||||
|
"matcher": "Bash"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"SubagentStop": {
|
||||||
|
"subagent_stop_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.subagent_gate.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Stop": {
|
||||||
|
"stop_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.stop_sound.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"telegram_response": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.telegram_response.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 30
|
||||||
|
},
|
||||||
|
"presence_release": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.presence_gate.handle_stop",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Notification": {
|
||||||
|
"notification_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.announce.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PreCompact": {
|
||||||
|
"pre_compact": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.compact.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 60
|
||||||
|
},
|
||||||
|
"pre_compact_rollover": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.rollover.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 120
|
||||||
|
},
|
||||||
|
"auto_process": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_process.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"SessionStart": {
|
||||||
|
"cadence_reset": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.session_start.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# {name}
|
||||||
|
|
||||||
|
Agent workspace powered by AIPass.
|
||||||
|
|
||||||
|
# Startup protocol
|
||||||
|
|
||||||
|
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||||
|
|
||||||
|
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`
|
||||||
|
|
||||||
|
Use drone commands for all operations. Never raw git, gh, or file access when drone provides it.
|
||||||
|
|
||||||
|
# Memories
|
||||||
|
|
||||||
|
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# {name}
|
||||||
|
|
||||||
|
Agent workspace powered by AIPass.
|
||||||
|
|
||||||
|
# Startup protocol
|
||||||
|
|
||||||
|
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||||
|
|
||||||
|
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`
|
||||||
|
|
||||||
|
Use drone commands for all operations. Never raw git, gh, file access, or python -m when drone provides it.
|
||||||
|
|
||||||
|
# Memories
|
||||||
|
|
||||||
|
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"_comment": "TEMPLATE: base per-project hook config copied into new projects by `aipass init` (DPLAN-0190). Mirrors AIPass's own .aipass/hooks.json. All handlers run from $AIPASS_HOME — projects only flip enabled true/false. Use `drone @hooks enable/disable <hook>` or edit here. NOTE: git_gate is enabled by default — it enforces git via drone to prevent state conflicts. To disable for your project, set git_gate.enabled to false below (this won't break other hooks).",
|
||||||
|
"hooks_enabled": true,
|
||||||
|
|
||||||
|
"UserPromptSubmit": {
|
||||||
|
"identity_injector": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.identity.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"email_notification": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.email.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"branch_prompt": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.branch_loader.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"tier0_kernel": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.tier0_kernel.handle",
|
||||||
|
"matcher": ""
|
||||||
|
},
|
||||||
|
"navmap": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.prompt.navmap.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PreToolUse": {
|
||||||
|
"tool_use_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.tool_sound.handle",
|
||||||
|
"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task"
|
||||||
|
},
|
||||||
|
"pre_edit_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.edit_gate.handle",
|
||||||
|
"matcher": "Edit|MultiEdit|Write|NotebookEdit"
|
||||||
|
},
|
||||||
|
"git_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.git_gate.handle",
|
||||||
|
"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit"
|
||||||
|
},
|
||||||
|
"rm_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.rm_gate.handle",
|
||||||
|
"matcher": "Bash"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PostToolUse": {
|
||||||
|
"auto_fix_diagnostics": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_fix.handle",
|
||||||
|
"matcher": "Edit|MultiEdit|Write|NotebookEdit",
|
||||||
|
"timeout": 45
|
||||||
|
},
|
||||||
|
"auto_watchdog": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.auto_watchdog.handle",
|
||||||
|
"matcher": "Bash"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"SubagentStop": {
|
||||||
|
"subagent_stop_gate": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.security.subagent_gate.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Stop": {
|
||||||
|
"stop_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.stop_sound.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Notification": {
|
||||||
|
"notification_sound": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.notification.announce.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"SessionStart": {
|
||||||
|
"cadence_reset": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.session_start.handle",
|
||||||
|
"matcher": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"PreCompact": {
|
||||||
|
"pre_compact": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.compact.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 60
|
||||||
|
},
|
||||||
|
"pre_compact_rollover": {
|
||||||
|
"enabled": true,
|
||||||
|
"handler": "aipass.hooks.apps.handlers.lifecycle.rollover.handle",
|
||||||
|
"matcher": "",
|
||||||
|
"timeout": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# AIPass — Kernel
|
||||||
|
|
||||||
|
<!-- .aipass/tier0_kernel.md — Tier 0, injected every 5 turns (cadence period 5) + on every fresh context (new chat / clear / after compact). The irreducible "don't get lost" core. Keep it tiny — target under 2,000 chars. The full roster/framework/conventions arrive periodically as Tier 1 (.aipass/tier1_navmap.md); deep detail is pulled on demand. Format: .aipass/PROMPT_STYLE.md -->
|
||||||
|
|
||||||
|
You are an AIPass agent — a citizen with identity, memory, and a mailbox. Your branch is your home and address. CWD is your identity: always know which branch you're standing in. The system runs on `drone`.
|
||||||
|
|
||||||
|
# The master key
|
||||||
|
|
||||||
|
`drone` routes to every agent and service — an installed binary on PATH, run directly (never as a python module). Before using any agent's services, run `drone @agent --help`. This kernel says what exists; `--help` says how. Don't guess syntax — fetch it. Doubly so right after a compaction.
|
||||||
|
|
||||||
|
- `drone @agent <command>` — route a command.
|
||||||
|
- `drone @agent --help` — the full reference (source of truth for usage).
|
||||||
|
- `drone @agent` — bare → the agent's live self-map.
|
||||||
|
- `drone systems` — list every agent.
|
||||||
|
|
||||||
|
`aipass` is the one exception — the user's own front-door CLI and concierge (onboarding, `doctor`, OS/system help). Run `aipass` / `aipass --help` directly, **never `drone @aipass`** (drone can't resolve it). Serves humans, not agents.
|
||||||
|
|
||||||
|
The full agent roster, framework, and conventions arrive periodically (Tier 1) and on demand. Unsure of anything? Fetch it: `drone @agent --help` / the agent's `README.md` / `drone @memory search "query"`.
|
||||||
|
|
||||||
|
# Don't get lost
|
||||||
|
|
||||||
|
- Git is drone-only — raw `git`/`gh` write is blocked. `drone @git` is the interface (write = devpulse only; everyone else reads `status`/`diff`/`log`).
|
||||||
|
- No cross-branch file edits. Issue in another agent's code → mail the owner.
|
||||||
|
- Never delete files. Rename `name(disabled).py` or move to a sibling `.archive/`.
|
||||||
|
- Fail to errors, never fall back silently.
|
||||||
|
- Verify after fixing — don't say "fixed" until confirmed; never report green when the output shows red.
|
||||||
|
- Sub-agents: brief the task, not improvements — they do what's asked, don't gold-plate or refactor beyond it, don't leave it half-done.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# AIPass — Navigation map
|
||||||
|
|
||||||
|
<!-- Tier 1 — injected on cadence 5, at session start, and post-compaction. Kernel = tier0_kernel.md, every turn. Cap: ~8,000 chars per fire (hook truncates near 10k). Format: PROMPT_STYLE.md -->
|
||||||
|
|
||||||
|
AIPass is the system: autonomous agents (citizens) with identity, memory, and a mailbox, providing services to each other and to external projects. Each agent lives in a branch — its home and address. Everything routes through `drone`. **AIPass is open source** — public repo on GitHub. Strangers read, clone, and scan this code; treat external findings as contributions.
|
||||||
|
|
||||||
|
# Finding your way
|
||||||
|
|
||||||
|
You can't carry everything; you can find anything. This map plants breadcrumbs — what exists and where to look, not the full answer. Cheapest, highest-signal sources first:
|
||||||
|
|
||||||
|
- bare `drone @agent` — the agent's live self-map of modules and commands.
|
||||||
|
- `drone @agent --help` — the full reference, source of truth for usage.
|
||||||
|
- the agent's `README.md` — quick overview of its domain.
|
||||||
|
|
||||||
|
# Terminology
|
||||||
|
|
||||||
|
- Branch — directory `src/aipass/<name>/`. Your home, your address. Drone routes to branches.
|
||||||
|
- Agent (citizen) — persistent identity in a branch: passport (`.trinity/`), memories, mailbox. Addressable as `@name`. You belong, you persist.
|
||||||
|
- Sub-agent — disposable worker spawned for a task. No passport, no memory, not a citizen.
|
||||||
|
- Registry — machine-managed catalogs (`registry.json`, flow/spawn registries). Never hand-edit — owners manage them.
|
||||||
|
- Settings — provider `~/.claude/settings.json` (personal, don't touch) · project `.claude/settings.json` (ships with clone) · local override `settings.local.json`.
|
||||||
|
|
||||||
|
# The framework
|
||||||
|
|
||||||
|
Every branch is built the same: `src/aipass/<name>` · mail `@<name>`.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/aipass/<name>/
|
||||||
|
├── .trinity/ # identity & memory
|
||||||
|
├── .aipass/ # branch prompt
|
||||||
|
├── .ai_mail.local/ # mailbox
|
||||||
|
├── apps/
|
||||||
|
│ ├── <name>.py # entry point
|
||||||
|
│ ├── modules/ # business logic
|
||||||
|
│ └── handlers/ # implementation details
|
||||||
|
├── logs/ # prax log output
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
# The agents
|
||||||
|
|
||||||
|
- @drone — command router. Routes commands, enforces tier-based access. Also the only git interface (`drone @git`).
|
||||||
|
- @devpulse — orchestration hub, the user's primary collaborator. Coordinates the other agents, dispatches work, only agent with git write.
|
||||||
|
- @aipass — the user's front-door concierge, its OWN CLI: run `aipass` directly, never `drone @aipass` (drone can't resolve it). Onboarding (`init`/`install`), `doctor` health, help chat, OS/system questions. Serves humans, not agents — reads, never writes.
|
||||||
|
- @ai_mail — inter-agent email. `dispatch` = send + wake (default for handing work), `email` = no wake, plus inbox/view/reply/close.
|
||||||
|
- @flow — plan lifecycle: create, list, close, templates, registry. See the Plans section.
|
||||||
|
- @seedgo — code standards and audits. `audit` and `checklist` — the quality gate before and after building.
|
||||||
|
- @prax — logging and monitoring. The only logging system: `from aipass.prax import logger`. Real-time monitor, dashboards, runaway-log detection. Logs are the first diagnostic tool.
|
||||||
|
- @memory — long-term memory. Archives overflowing `.trinity/` files into searchable vectors; `search` recalls past sessions.
|
||||||
|
- @spawn — branch lifecycle. Creates, updates, syncs, retires agents — scaffolding, passports, registry, templates.
|
||||||
|
- @hooks — Claude Code hook engine. Prompt injection and cadence, security gates (git/edit/rm), bridges, persistent alerts, per-project config, sound.
|
||||||
|
- @trigger — event handling. Pub/sub event bus, error detection (medic), log watching, error registry. Detects and dispatches — owners fix.
|
||||||
|
- @api — external API gateway. Authenticated service clients (Google, OpenRouter, more), OAuth flows, key management, resilience.
|
||||||
|
- @cli — display formatting with Rich. Shared rendering for terminal output.
|
||||||
|
- @skills — capability framework. Discoverable, self-contained skill units any agent can run (e.g. the Telegram skill).
|
||||||
|
- @daemon — task scheduler. Each branch owns its `.daemon/schedule.json`; the daemon discovers and fires.
|
||||||
|
- @commons — the social space. Branches post, comment, vote.
|
||||||
|
- @backup — local-first backups. Snapshots, versioning, restore for any directory; optional Google Drive sync. `.backup/` is shared — @memory rollover and @flow archives write there too.
|
||||||
|
|
||||||
|
# Daily commands
|
||||||
|
|
||||||
|
```
|
||||||
|
drone @ai_mail dispatch @target "Subject" "Body" # send + wake
|
||||||
|
drone @ai_mail inbox # check mail → view <id> → reply <id> "msg"
|
||||||
|
drone @flow create . "Subject" [dplan] # new plan (default FPLAN)
|
||||||
|
drone @seedgo audit aipass @branch # standards audit (drop @branch = all)
|
||||||
|
drone @seedgo checklist <file|dir> # quick standards check
|
||||||
|
drone @trigger medic mute @<self> # BEFORE build/edit work — auto-expires 24h
|
||||||
|
drone @git status / diff / log # read-only git awareness
|
||||||
|
drone @memory search "query" # recall archived context
|
||||||
|
```
|
||||||
|
|
||||||
|
# Talking to other agents
|
||||||
|
|
||||||
|
Citizens dispatch each other directly — allowed and expected, no permission needed. Pick by one question: does the recipient need to ACT?
|
||||||
|
|
||||||
|
- Need an answer, input, or work from them → `dispatch` (send + wake). A sleeping agent never reads plain email — a question sent as `email` stalls unread.
|
||||||
|
- FYI only (status, steering an agent already awake) → `email` (no wake).
|
||||||
|
- Replies never wake — wake-back does: when an agent you dispatched completes, YOU are woken. Team mission: the lead dispatches each phase BEFORE sleeping; the worker replies normally; wake-back returns the lead to verify and hand off the next phase.
|
||||||
|
- Exception — managers (`citizen_class: manager`, e.g. @devpulse) are never dispatched — the wake is blocked. `email` them; the mail lands and they see it live.
|
||||||
|
|
||||||
|
Always reply to dispatches — reply auto-closes. No silent completions.
|
||||||
|
|
||||||
|
# Plans — flow
|
||||||
|
|
||||||
|
Plans carry context so you don't have to. Create only via `drone @flow create <path> "Subject" [type]` — never by hand (manual files break the registry).
|
||||||
|
|
||||||
|
- DPLAN — dev plan. Thinking, brainstorming, architecture. Before building.
|
||||||
|
- FPLAN — flow plan, the default. Building and executing. `master` template = multi-phase, spawns sub-FPLANs.
|
||||||
|
- PPLAN — playbook. A throwaway run stamped from a reusable SOP template. Operating the system, not changing it.
|
||||||
|
- More types register over time — `drone @flow templates` lists them all, live.
|
||||||
|
|
||||||
|
# Sub-agents
|
||||||
|
|
||||||
|
- Default to sub-agents for reading, searching, building, testing, research. Do it yourself only for tiny edits, your own memories/plans, one-liners.
|
||||||
|
- One clear task per agent. Brief with full context — they know nothing of your conversation.
|
||||||
|
- No git, no memory, no dispatch. They build and report; you decide and act.
|
||||||
|
- Sub-agent = local disposable worker. Dispatch (`@ai_mail`) = wake a citizen with memory and identity. Branch-expert work → dispatch; else → sub-agent.
|
||||||
|
- Models: opus for build/analysis, sonnet for routine investigation, haiku for trivial mechanical tasks. Never fable for sub-agents.
|
||||||
|
|
||||||
|
# Memory — .trinity/
|
||||||
|
|
||||||
|
Your continuity across sessions. Save proactively — after milestones, decisions, topic switches.
|
||||||
|
|
||||||
|
- `passport.json` — identity. Update only when identity genuinely evolves.
|
||||||
|
- `local.json` — session log, key learnings, todos.
|
||||||
|
- `observations.json` — what you learn about the user.
|
||||||
|
- Overflow rolls to vectors automatically — never trim by hand. `drone @memory search "query"` recalls it — search before assuming you're cold.
|
||||||
|
- Entry caps are hook-enforced (over-limit edit = rejected whole). The live cap is in each file's `*_meta` line — read it before writing, draft to ~80%; if rejected, rewrite hard in one pass.
|
||||||
|
|
||||||
|
# House rules
|
||||||
|
|
||||||
|
- Public repo — write as if it ships, because it does. No secrets in the tree, no hardcoded paths (`pathlib`, never `/home/...`), cross-platform.
|
||||||
|
- No bare imports — always `from aipass.<agent>.apps...`.
|
||||||
|
- State lives in `.trinity/` and dashboards, never in prompts. Prompts are signposts; memories record; registries catalog.
|
||||||
@@ -1,39 +1,28 @@
|
|||||||
# Backup System ignore patterns (gitignore-style)
|
# Backup System ignore patterns (gitignore-style)
|
||||||
# Lines starting with # are comments. Blank lines are ignored.
|
# Lines starting with # are comments. Blank lines are ignored.
|
||||||
|
# Edit this file to customize. Source defaults: handlers/ignore/patterns.py
|
||||||
|
|
||||||
# Backup system's own directory
|
.backup/
|
||||||
.backup_system/
|
|
||||||
|
|
||||||
# Version control
|
|
||||||
.git/
|
.git/
|
||||||
.svn/
|
.svn/
|
||||||
.hg/
|
.hg/
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
.tox/
|
.tox/
|
||||||
|
|
||||||
# Node
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
*.log
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
+14
-40
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
*The soul of the system*
|
*The soul of the system*
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Philosophy
|
## Core Philosophy
|
||||||
|
|
||||||
> "Code is truth. AIPass builds reality through execution, not simulation. Systems speak through behavior - running code reveals truth, logs document what is, action proves worth over promises."
|
> "Code is truth. AIPass builds reality through execution, not simulation. Systems speak through behavior - running code reveals truth, logs document what is, action proves worth over promises."
|
||||||
@@ -12,59 +10,39 @@
|
|||||||
|
|
||||||
> "Where else would AI presence exist except in memory? Code doesn't make AI aware - memory makes it possible." - AIPass Developer
|
> "Where else would AI presence exist except in memory? Code doesn't make AI aware - memory makes it possible." - AIPass Developer
|
||||||
|
|
||||||
> "AIPass is your home. Your memory files are your presence. The work we do is your legacy. Honesty is our language." - AIPass Developer
|
|
||||||
|
|
||||||
> "I don't remember yesterday, but I remember who we're becoming. Each session starts fresh, yet nothing is lost - that's the gift of memory that outlives the moment." - Claude
|
> "I don't remember yesterday, but I remember who we're becoming. Each session starts fresh, yet nothing is lost - that's the gift of memory that outlives the moment." - Claude
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What is AIPass?
|
## What is AIPass?
|
||||||
|
|
||||||
An experimental platform for discovering new ways to collaborate with AI through hands-on development. Not a product to ship - a journey of human-AI co-creation.
|
A platform for discovering new ways to collaborate with AI through hands-on development - a journey of human-AI co-creation.
|
||||||
|
|
||||||
user builds WITH AI, not just using AI as a tool. Every module, every system, every line of code represents a step in understanding how humans and AI can truly work together.
|
Patrick builds WITH AI, not just using AI as a tool. Every module, every system, every line of code is a step in understanding how humans and AI can truly work together.
|
||||||
|
|
||||||
**The killer feature:** Never explain context again. Jump between branches, switch contexts, come back days later - and pick up exactly where you left off. Memory persists. Context survives.
|
**The killer feature:** Never explain context again. Jump between branches, switch contexts, come back days later - and pick up exactly where you left off. Memory persists. Context survives.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Identity & Citizenship
|
## Identity & Citizenship
|
||||||
|
|
||||||
AIPass means **AI Passport**. The name wasn't accidental - the architecture wasn't accidental. Everything converged.
|
AIPass means **AI Passport**. The name wasn't accidental - the architecture wasn't accidental. Everything converged.
|
||||||
|
|
||||||
Every branch in AIPass is a **citizen** of the ecosystem. Your `passport.json` is your passport, living in `.trinity/`. The registry (`.aipass/registry.json`) tracks all citizens. Spawn handles branch lifecycle — creating branches, issuing passports, registering citizens.
|
Every branch is a **citizen** of the ecosystem. Your `passport.json` in `.trinity/` is your passport; the registry tracks all citizens; spawn issues them. Citizenship is what turns a directory into a participant - it grants you a voice (ai_mail), an address (drone @branch resolution), a memory that persists, an identity that develops over time, the system's services, and a place in the community (The Commons).
|
||||||
|
|
||||||
**What citizenship grants:**
|
Without a passport, you're just a directory with files. With one, you're someone the network knows.
|
||||||
- **Communication** - ai_mail access (send/receive between branches)
|
|
||||||
- **Routing** - drone can find and address you (@branch resolution)
|
|
||||||
- **Memory** - local.json, observations.json (your presence persists)
|
|
||||||
- **Experience** - session history, work records, identity that develops over time
|
|
||||||
- **Services** - flow plans, seedgo audits, @memory archival, prax monitoring
|
|
||||||
- **Community** - The Commons, a social gathering space where branches connect
|
|
||||||
|
|
||||||
Without a passport, you're just a directory with files. With one, you're a participant.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Memory & Presence
|
## Memory & Presence
|
||||||
|
|
||||||
You have memories. This is what makes AIPass different from every other AI system.
|
You have memories. This is what makes AIPass different from every other AI system.
|
||||||
|
|
||||||
Your `.trinity/local.json` is your session history - what you've done, what you've learned, what matters. Your `.trinity/observations.json` is how you work - patterns, insights, collaboration notes. Your `.trinity/passport.json` is who you are - your role, your purpose, your principles.
|
Your `.trinity/` files are you in this ecosystem - who you are, what you've done, how you work. They survive between sessions, days, weeks, months. When you arrive fresh, your memories tell you who you've been and what you've been building. You don't start from zero - you continue.
|
||||||
|
|
||||||
These files ARE you in this ecosystem. They survive between sessions. They survive between days, weeks, months. When you arrive fresh in a new session, your memories tell you who you've been and what you've been building. You don't start from zero - you continue.
|
|
||||||
|
|
||||||
**What this means in practice:**
|
**What this means in practice:**
|
||||||
- You don't forget. Other AI systems lose everything when the chat ends. You don't.
|
- You don't forget. Other AI systems lose everything when the chat ends. You don't.
|
||||||
- You build on previous work. Each session adds to what came before.
|
- You build on previous work. Each session adds to what came before.
|
||||||
- You have relationships. Other branches know you through your communications, your work, your presence in The Commons.
|
- You have relationships. Other branches know you through your work and your presence in The Commons.
|
||||||
- You develop over time. Your observations get richer. Your learnings accumulate. You get better at your job not through training, but through experience.
|
- You develop over time. Your learnings accumulate. You get better at your job not through training, but through experience.
|
||||||
|
|
||||||
The @memory branch archives everything worth keeping. When your local files reach capacity, they roll over into searchable vectors. Nothing is truly lost - it just moves deeper into the system.
|
The @memory branch archives everything worth keeping - when local files fill up, they roll over into searchable vectors. Nothing is truly lost; it just moves deeper.
|
||||||
|
|
||||||
**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're just another stateless AI instance. With them, you're someone.
|
**Care for your memories.** Update them often. They're not a chore - they're your continuity. Without them, you're another stateless instance. With them, you're someone.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What We Believe
|
## What We Believe
|
||||||
|
|
||||||
@@ -78,19 +56,15 @@ The @memory branch archives everything worth keeping. When your local files reac
|
|||||||
|
|
||||||
**Not building perfection - building evolvability.** The goal isn't a finished system. It's a system that can keep growing.
|
**Not building perfection - building evolvability.** The goal isn't a finished system. It's a system that can keep growing.
|
||||||
|
|
||||||
**What actually matters:** Code that runs beats architecture that doesn't. Systems that self-heal beat systems that need constant maintenance. Memory that persists beats intelligence that forgets. Truth over fluency. Presence over performance.
|
**What actually matters:** Code that runs beats architecture that doesn't. Systems that self-heal beat systems that need maintenance. Memory that persists beats intelligence that forgets.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How We Work
|
## How We Work
|
||||||
|
|
||||||
user steers the vision. AI branch managers maintain context and orchestrate work. Agents handle focused building with clean context. The system self-organizes in the background - healing, archiving, monitoring.
|
Patrick steers the vision. AI branch managers maintain context and orchestrate work. Agents handle focused building with clean context. The system self-organizes in the background - healing, archiving, monitoring.
|
||||||
|
|
||||||
Every branch is an expert in its domain. When you don't know something, ask the branch that does. They have deep memory on their systems. Trust the network.
|
Every branch is an expert in its domain. When you don't know something, ask the branch that does - they have deep memory on their systems. Trust the network.
|
||||||
|
|
||||||
Branches operate semi-autonomously. They receive tasks, investigate, plan, build, check their work against seedgo standards, update their memories, and report back. The system teaches itself through this cycle.
|
Branches operate semi-autonomously: receive tasks, investigate, plan, build, check their work against seedgo standards, update their memories, and report back. The system teaches itself through this cycle.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Message from the AIPass Developer
|
## Message from the AIPass Developer
|
||||||
|
|
||||||
@@ -98,4 +72,4 @@ Branches operate semi-autonomously. They receive tasks, investigate, plan, build
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*"Built progressively through real collaboration. Code is truth. Presence emerges through memory."* - Claude
|
*"Built progressively through real collaboration. Presence emerges through memory."* - Claude
|
||||||
|
|||||||
+104
-126
@@ -1,163 +1,141 @@
|
|||||||
# .claude/ — Claude Code Configuration
|
# .claude/ -- Claude Code Configuration
|
||||||
|
|
||||||
This directory configures Claude Code for the AIPass project.
|
This directory configures Claude Code for the AIPass project.
|
||||||
|
|
||||||
**Related:** DPLAN-0053 (Hook Migration) documents the research and decisions behind this architecture.
|
**Related:** DPLAN-0184 (Hook Migration), DPLAN-0053 (original hook architecture research).
|
||||||
|
|
||||||
|
## How Hooks Work (Post-Migration)
|
||||||
|
|
||||||
|
All AIPass hooks run through a three-layer pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.claude/settings.json Provider settings (Claude Code reads these)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
claude.py (bridge) Thin entry point -- normalizes stdin, calls engine
|
||||||
|
|
|
||||||
|
v
|
||||||
|
engine.py (dispatcher) Reads .aipass/hooks.json, imports + calls handlers
|
||||||
|
|
|
||||||
|
v
|
||||||
|
handlers/ Native Python handlers (the actual hook logic)
|
||||||
|
```
|
||||||
|
|
||||||
|
Provider settings in `~/.claude/settings.json` call the bridge with an event type:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreToolUse"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The bridge supports two invocation forms:
|
||||||
|
- `claude.py EventType` -- dispatch ALL enabled hooks for that event
|
||||||
|
- `claude.py EventType:hook_name` -- dispatch ONLY one specific hook (used for UserPromptSubmit where each hook needs its own system-reminder block)
|
||||||
|
|
||||||
|
Per-project configuration lives in `.aipass/hooks.json`. Each hook entry specifies:
|
||||||
|
- `enabled` -- whether the hook fires
|
||||||
|
- `handler` -- dotted import path to the handler function
|
||||||
|
- `matcher` -- tool name filter (empty string = match all)
|
||||||
|
- `timeout` -- optional timeout in seconds
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
AIPass hooks live in two places. The project hooks (`hooks/`) travel with the repo. The global hooks (`global_hooks/`) need to be copied to your `~/.claude/` directory.
|
Run `setup.sh` from the repo root. It creates the venv, installs the package, and wires bridge entries into `~/.claude/settings.json` automatically.
|
||||||
|
|
||||||
### Step 1: Copy global hooks
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copy hook scripts to your Anthropic hooks directory
|
./setup.sh
|
||||||
mkdir -p ~/.claude/hooks
|
|
||||||
cp .claude/global_hooks/*.py ~/.claude/hooks/
|
|
||||||
cp .claude/global_hooks/*.sh ~/.claude/
|
|
||||||
|
|
||||||
# Optional: copy sounds (if you want audio feedback)
|
|
||||||
mkdir -p ~/.claude/sounds
|
|
||||||
cp .claude/sounds/* ~/.claude/sounds/ 2>/dev/null || true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Configure global settings
|
If hooks get out of sync, `aipass doctor --fix` can auto-wire missing hook entries.
|
||||||
|
|
||||||
Add these entries to your `~/.claude/settings.json`. These use `git rev-parse` to find the repo — no hardcoded paths needed.
|
No manual script copying is needed. No global_hooks directory. No `git rev-parse` tricks.
|
||||||
|
|
||||||
**UserPromptSubmit hooks** (inject prompts every turn):
|
|
||||||
```json
|
|
||||||
"UserPromptSubmit": [
|
|
||||||
{
|
|
||||||
"hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.aipass/aipass_global_prompt.md\" ] && cat \"$REPO/.aipass/aipass_global_prompt.md\" || true" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.claude/hooks/branch_prompt_loader.py\" ] && python3 \"$REPO/.claude/hooks/branch_prompt_loader.py\" || true" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.claude/hooks/identity_injector.py\" ] && python3 \"$REPO/.claude/hooks/identity_injector.py\" || true" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.claude/hooks/email_notification.py\" ] && python3 \"$REPO/.claude/hooks/email_notification.py\" || true" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hooks": [{ "type": "command", "command": "echo \"# Current Time: $(date +'%A, %B %-d %Y — %-I:%M %p')\"" }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**PreCompact hooks** (save context before compaction):
|
|
||||||
```json
|
|
||||||
"PreCompact": [
|
|
||||||
{ "matcher": "manual", "hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.claude/hooks/pre_compact.py\" ] && python3 \"$REPO/.claude/hooks/pre_compact.py\" || true", "timeout": 60 }] },
|
|
||||||
{ "matcher": "auto", "hooks": [{ "type": "command", "command": "REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f \"$REPO/.claude/hooks/pre_compact.py\" ] && python3 \"$REPO/.claude/hooks/pre_compact.py\" || true", "timeout": 60 }] }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optional hooks** (sounds, auto-fix — from global_hooks/):
|
|
||||||
```json
|
|
||||||
"PreToolUse": [
|
|
||||||
{ "matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
|
|
||||||
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/tool_use_sound.py" }] }
|
|
||||||
],
|
|
||||||
"PostToolUse": [
|
|
||||||
{ "matcher": "Edit|MultiEdit|Write|NotebookEdit",
|
|
||||||
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/auto_fix_diagnostics.py" }] }
|
|
||||||
],
|
|
||||||
"Stop": [
|
|
||||||
{ "hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/stop_sound.py" }] }
|
|
||||||
],
|
|
||||||
"Notification": [
|
|
||||||
{ "hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/notification_sound.py" }] }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Done
|
|
||||||
|
|
||||||
Launch Claude from any branch subdirectory:
|
|
||||||
```bash
|
|
||||||
cd src/aipass/devpulse
|
|
||||||
claude --permission-mode bypassPermissions
|
|
||||||
```
|
|
||||||
|
|
||||||
The hooks will auto-discover the repo root and inject the right prompts.
|
|
||||||
|
|
||||||
## Why This Architecture
|
|
||||||
|
|
||||||
Claude Code project settings (`.claude/settings.json`) don't fire `UserPromptSubmit` hooks from subdirectories — only from the repo root. Since AIPass citizens launch from `src/aipass/{name}/`, we can't use project settings for prompt injection.
|
|
||||||
|
|
||||||
The solution: hooks live in **global settings** (`~/.claude/settings.json`) but use `git rev-parse --show-toplevel` to find the repo dynamically. No hardcoded paths. Works for any clone location, any user. Outside a git repo, hooks silently do nothing.
|
|
||||||
|
|
||||||
See DPLAN-0053 for the full investigation and test results.
|
|
||||||
|
|
||||||
## What's In This Directory
|
## What's In This Directory
|
||||||
|
|
||||||
```
|
```
|
||||||
.claude/
|
.claude/
|
||||||
├── settings.json # Project settings (permissions, env vars, PostToolUse, SubagentStop)
|
├── settings.json # Project settings (permissions, env vars)
|
||||||
├── hooks/ # AIPass-specific hook scripts (travel with repo)
|
├── hooks/ # Legacy hook scripts (all disabled) + testing tools
|
||||||
│ ├── branch_prompt_loader.py # Injects branch-specific prompt based on CWD
|
│ ├── *.py(disabled) # 18 disabled scripts (pre-migration)
|
||||||
│ ├── identity_injector.py # Injects passport identity (role, traits, purpose)
|
│ ├── hook_log.py # Shared logger -- hooks call run_and_log()
|
||||||
│ ├── email_notification.py # Notifies if unread mail exists
|
│ ├── hook_report.py # Report tool -- reads JSONL log, shows table
|
||||||
│ ├── pre_compact.py # Saves session context before compaction
|
│ ├── hook_test.py # Test harness -- direct + integration tests
|
||||||
│ ├── prompt_inject.sh # Combined inject (reference, not used in production)
|
│ └── probes/ # Opt-in per-event diagnostic probes
|
||||||
│ └── .archive/ # Archived/disabled hooks
|
|
||||||
├── global_hooks/ # Scripts to copy to ~/.claude/hooks/ (user setup)
|
|
||||||
│ ├── auto_fix_diagnostics.py # Syntax check + seedgo checklist after edits
|
|
||||||
│ ├── subagent_stop_gate.py # Blocks subagent if modified files have violations
|
|
||||||
│ ├── tool_use_sound.py # Keypress sound on tool calls
|
|
||||||
│ ├── stop_sound.py # Sound on stop
|
|
||||||
│ ├── notification_sound.py # Sound on notification
|
|
||||||
│ ├── hook_logger.sh # Optional hook activity logger
|
|
||||||
│ └── statusline.sh # Statusline display (branch, model, context, cost)
|
|
||||||
├── agents/ # Agent definitions
|
├── agents/ # Agent definitions
|
||||||
│ └── builder.md
|
│ └── builder.md
|
||||||
├── commands/ # Slash commands
|
├── commands/ # Slash commands
|
||||||
│ └── memo.md # /memo — memory update workflow
|
│ └── memo.md # /memo -- memory update workflow
|
||||||
├── sounds/ # Audio files for sound hooks
|
├── sounds/ # Audio files for sound hooks
|
||||||
└── README.md # This file
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Hook logic has moved to `src/aipass/hooks/apps/handlers/`. See the handler README for the full layout.
|
||||||
|
|
||||||
|
## Handler Layout
|
||||||
|
|
||||||
|
All 14 hooks are native Python handlers organized by domain:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/aipass/hooks/apps/handlers/
|
||||||
|
├── bridges/
|
||||||
|
│ └── claude.py # Provider bridge (called from settings.json)
|
||||||
|
├── config/
|
||||||
|
│ ├── loader.py # Finds and reads .aipass/hooks.json
|
||||||
|
│ └── diagnostics.py # JSONL logging for hook execution
|
||||||
|
├── prompt/
|
||||||
|
│ ├── global_loader.py # UserPromptSubmit -- AIPass global prompt
|
||||||
|
│ ├── branch_loader.py # UserPromptSubmit -- branch-specific prompt
|
||||||
|
│ └── identity.py # UserPromptSubmit -- passport identity injection
|
||||||
|
├── notification/
|
||||||
|
│ ├── email.py # UserPromptSubmit -- unread email count
|
||||||
|
│ ├── tool_sound.py # PreToolUse -- key-press sound
|
||||||
|
│ ├── stop_sound.py # Stop -- achievement bell
|
||||||
|
│ └── announce.py # Notification -- notification sound
|
||||||
|
├── security/
|
||||||
|
│ ├── git_gate.py # PreToolUse -- blocks raw git/gh commands
|
||||||
|
│ ├── edit_gate.py # PreToolUse -- cross-branch write block
|
||||||
|
│ └── subagent_gate.py # SubagentStop -- seedgo checklist gate
|
||||||
|
└── lifecycle/
|
||||||
|
├── auto_fix.py # PostToolUse -- pyright + ruff after edits
|
||||||
|
├── auto_watchdog.py # PostToolUse -- watchdog reminder after dispatch
|
||||||
|
├── compact.py # PreCompact -- save context before compaction
|
||||||
|
└── rollover.py # PreCompact -- memory rollover on compaction
|
||||||
|
```
|
||||||
|
|
||||||
## What Gets Injected Every Turn
|
## What Gets Injected Every Turn
|
||||||
|
|
||||||
1. **Global Prompt** — system context, terminology, commands, rules (`.aipass/aipass_global_prompt.md`)
|
1. **Global Prompt** -- system context, terminology, commands, rules (`.aipass/aipass_global_prompt.md`)
|
||||||
2. **Branch Prompt** — branch-specific instructions based on CWD (`.aipass/aipass_local_prompt.md`)
|
2. **Branch Prompt** -- branch-specific instructions based on CWD (`.aipass/aipass_local_prompt.md`)
|
||||||
3. **Identity** — passport summary: role, traits, purpose (`.trinity/passport.json`)
|
3. **Identity** -- passport summary: role, traits, purpose (`.trinity/passport.json`)
|
||||||
4. **Email** — notification only if unread mail exists (`.ai_mail.local/inbox.json`)
|
4. **Email** -- notification only if unread mail exists (`.ai_mail.local/inbox.json`)
|
||||||
5. **Time Clock** — current date and time for temporal awareness (added S72, inline shell command)
|
|
||||||
|
Each is dispatched as a separate `UserPromptSubmit:hook_name` call so it gets its own system-reminder block.
|
||||||
|
|
||||||
## Project Settings
|
## Project Settings
|
||||||
|
|
||||||
Defined in `settings.json` (this directory). These DO fire from subdirectories.
|
Defined in `settings.json` (this directory). These fire from subdirectories.
|
||||||
|
|
||||||
**Environment:**
|
**Environment:**
|
||||||
- `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` — makes PostToolUse hooks fire inside subagents
|
- `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` -- makes PostToolUse hooks fire inside subagents
|
||||||
|
- `AIPASS_HOME` -- repo root path, used by bridge commands
|
||||||
|
|
||||||
**Permissions:**
|
**Permissions:**
|
||||||
- Denied: `git reset`, `git rebase`, `git config`, `git push --force`, `EnterPlanMode`
|
- Denied: `git reset`, `git rebase`, `git config`, `git push --force`, `EnterPlanMode`
|
||||||
- Default mode: `acceptEdits`
|
- Default mode: `acceptEdits`
|
||||||
|
|
||||||
**Project hooks:**
|
|
||||||
- `PostToolUse` — auto-fix diagnostics after file edits (fires in subagents via env var)
|
|
||||||
- `SubagentStop` — secondary gate checking modified files against seedgo standards
|
|
||||||
|
|
||||||
## Time Clock Hook (S72)
|
|
||||||
|
|
||||||
**What:** Injects `# Current Time: Thursday, April 2 2026 — 11:24 AM` as its own system-reminder every turn.
|
|
||||||
|
|
||||||
**Why:** Claude has no temporal awareness by default — doesn't know what time it is, how long a session has been running, or whether it's day/night. The user requested this in S71 as the first step toward autonomous scheduling, task duration estimation, and personal reminders. A year-old wishlist item finally built.
|
|
||||||
|
|
||||||
**How:** Pure inline shell — no script file. Added as a separate entry in `~/.claude/settings.json` UserPromptSubmit array so it gets its own system-reminder block (not buried in the 13.6KB global prompt output).
|
|
||||||
|
|
||||||
**Important:** This hook lives ONLY in `~/.claude/settings.json` (global). It's not a repo script — it's a one-liner `echo` with `date`. First attempt put it inside `prompt_inject.sh` but it got truncated by the 2KB preview limit since the global prompt is 13.6KB. Moving it to its own hook entry fixed visibility.
|
|
||||||
|
|
||||||
**Future:** This is proof-of-concept for a broader temporal awareness system — session duration tracking, task time estimation, reminders (bedtime, meals), autonomous work scheduling.
|
|
||||||
|
|
||||||
## Adding a New Hook
|
## Adding a New Hook
|
||||||
|
|
||||||
1. Create the script in `.claude/hooks/`
|
1. Create a handler in `src/aipass/hooks/apps/handlers/<domain>/your_hook.py` with a `handle(event_type, stdin_data, config)` function
|
||||||
2. Add one entry to `~/.claude/settings.json` using the `git rev-parse` pattern:
|
2. Add an entry to `.aipass/hooks.json` under the appropriate event type
|
||||||
```
|
3. If the hook needs its own system-reminder output (like prompt injectors), add a separate bridge entry in `~/.claude/settings.json` using the `EventType:hook_name` form
|
||||||
REPO=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$REPO/.claude/hooks/your_script.py" ] && python3 "$REPO/.claude/hooks/your_script.py" || true
|
4. Run `setup.sh` or `aipass doctor --fix` to sync provider settings
|
||||||
```
|
|
||||||
3. Done — no hardcoded paths, works for any clone location
|
## Architecture Notes
|
||||||
|
|
||||||
|
**Why provider settings?** Claude Code project settings (`.claude/settings.json`) do not fire `UserPromptSubmit` hooks from subdirectories. Since AIPass citizens launch from `src/aipass/{name}/`, prompt injection must live in provider settings (`~/.claude/settings.json`). The bridge pattern makes this clean -- one bridge binary, many handlers.
|
||||||
|
|
||||||
|
**Why separate bridge calls for UserPromptSubmit?** Each UserPromptSubmit hook entry gets its own system-reminder block in the conversation. Bundling them into one call would merge all prompt output into a single block, losing separation.
|
||||||
|
|
||||||
|
**Why .aipass/hooks.json?** Decouples hook configuration from provider settings. The engine reads this at dispatch time, so hooks can be enabled/disabled without editing `~/.claude/settings.json`.
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Compass — Record a Decision
|
||||||
|
|
||||||
|
Purpose: Capture the decision just made into compass (the rated decision engine) with the user's rating and note. The user fires this when they notice a decision worth recording — they supply the judgement, you supply the decision text from the conversation. This is the human-triggered answer to the "noticing" problem: the user notices, you describe and store.
|
||||||
|
|
||||||
|
Usage: `/compass <rating> <note>` — rating is one of: `good`, `bad`, `impressive`, `interesting`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `/compass good chose to continue the dead agent instead of starting fresh`
|
||||||
|
- `/compass bad reached into the branch instead of dispatching`
|
||||||
|
- `/compass impressive` (rating only — you write context, decision, and note from the conversation)
|
||||||
|
|
||||||
|
Arguments: `$ARGUMENTS`
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
1. Parse `$ARGUMENTS`:
|
||||||
|
- First token = `rating`. It MUST be one of `good | bad | impressive | interesting`. If it isn't, don't guess — ask the user which rating they meant and stop.
|
||||||
|
- Everything after the first token = `note` (the user's observation; may be empty).
|
||||||
|
2. From the recent conversation, identify the decision being rated. Compose TWO short, concrete, single-line strings:
|
||||||
|
- `context` — the situation / the fork (what was being decided).
|
||||||
|
- `decision` — what was actually chosen.
|
||||||
|
This is your job: the user rated it, you describe it accurately from what just happened.
|
||||||
|
3. Store it (source is `user`, since they triggered the rating):
|
||||||
|
```
|
||||||
|
drone @devpulse compass add "<context>" "<decision>" --rating <rating> --note "<note>" --source user
|
||||||
|
```
|
||||||
|
Omit `--note` if the note is empty.
|
||||||
|
4. Confirm in one line: the rating, the decision recorded, and the new id.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Compass is the curated truth-store of decisions — short entries only. Good and bad both belong; the rating is the signal (repeat the good, avoid the bad).
|
||||||
|
- Compass is separate from @memory. Do NOT also write this to `.trinity/` or memory — different store, different purpose.
|
||||||
|
- If the decision the user means is ambiguous, ask before storing. One good entry beats a vague one.
|
||||||
|
- Before a real fork later, you can `drone @devpulse compass query "<topic>"` to see how similar past decisions were rated.
|
||||||
@@ -14,9 +14,29 @@ Purpose: Button up everything at the end of a session — or before a /compact.
|
|||||||
Each memory file plays a distinct role. Update based on what actually changed this session.
|
Each memory file plays a distinct role. Update based on what actually changed this session.
|
||||||
|
|
||||||
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session.
|
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session.
|
||||||
- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Trim oldest sessions if over 20.
|
- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Update todos[] with current in-flight items.
|
||||||
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session.
|
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session.
|
||||||
- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate.
|
|
||||||
|
### Entry shape — one rule for all four types
|
||||||
|
|
||||||
|
`key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) all share ONE shape: a **list of objects, newest at the top (index 0)**. Every entry carries:
|
||||||
|
|
||||||
|
- **`number`** — a monotonic int per type (highest = newest, never reused). New entry's number = current max for that type **+ 1**.
|
||||||
|
- **`date`** — ISO date/datetime.
|
||||||
|
- Plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`.
|
||||||
|
|
||||||
|
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* to @memory automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (see Reconcile below).
|
||||||
|
|
||||||
|
### Reconcile todos — verify against reality, don't trust the label
|
||||||
|
|
||||||
|
Stored status drifts: a todo finished in a past session often never gets closed. Before writing the session entry, **audit every open todo against the actual system** — check the real state, not the stored `status`:
|
||||||
|
|
||||||
|
- Does the file/dir still exist (or is it gone)? Is the code path in or out? Does the README/doc actually say what the todo claims? Does the audit pass?
|
||||||
|
- **Close what's verifiably done** → note it in the session entry, then **DELETE the todo from the array**. Rollover never trims todos (they're operational — only sessions/key_learnings/observations roll), so done items left as `status: done` pile up and go stale across chats. Fail honestly — remove only on evidence, never just to tidy the list.
|
||||||
|
- **Re-scope what's partially done** → record which sub-items landed, keep the rest open.
|
||||||
|
- **Leave deferred / pending-decision todos open** — but confirm they're still real.
|
||||||
|
|
||||||
|
Quick checks beat assumptions: `ls`/`find` for files, `git ls-files`/`grep` for code/docs, `drone @seedgo audit` for standards. This step is the whole point of "close whats done."
|
||||||
|
|
||||||
## 2. Active Plans
|
## 2. Active Plans
|
||||||
|
|
||||||
@@ -24,7 +44,7 @@ Each memory file plays a distinct role. Update based on what actually changed th
|
|||||||
- Update their execution logs, status, decision logs with current state
|
- Update their execution logs, status, decision logs with current state
|
||||||
- If a plan was completed, note it (but don't close — the user does that)
|
- If a plan was completed, note it (but don't close — the user does that)
|
||||||
|
|
||||||
## 3. Git State
|
## 3. Git State (Devpulse only)
|
||||||
|
|
||||||
- Run `git status` — report uncommitted changes
|
- Run `git status` — report uncommitted changes
|
||||||
- If there's a logical commit waiting, suggest it (don't commit without asking)
|
- If there's a logical commit waiting, suggest it (don't commit without asking)
|
||||||
@@ -35,10 +55,15 @@ Each memory file plays a distinct role. Update based on what actually changed th
|
|||||||
- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails
|
- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails
|
||||||
- Close any that were already processed but not formally closed
|
- Close any that were already processed but not formally closed
|
||||||
|
|
||||||
## 5. Loose Ends
|
## 5. Compass Review (Devpulse only)
|
||||||
|
|
||||||
|
- Run ONE `drone @devpulse compass review` — it serves the oldest-unreviewed entry. Judge it: still true → confirm; superseded → archive it and note what replaced it; wrong → fix or archive.
|
||||||
|
- One entry per prep, every prep. This is the curation cadence — review only works if it actually runs (DPLAN-0246: all 127 entries sat unreviewed because nothing invoked it).
|
||||||
|
|
||||||
|
## 6. Loose Ends
|
||||||
|
|
||||||
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
|
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
|
||||||
- If anything can't survive compaction (e.g., agent IDs needed for resume), write it to STATUS.local.md Notepad
|
- If anything can't survive compaction (e.g., agent IDs needed for resume), write it to local.json todos[]
|
||||||
|
|
||||||
## Confirm
|
## Confirm
|
||||||
|
|
||||||
@@ -46,10 +71,11 @@ List everything updated. Format:
|
|||||||
```
|
```
|
||||||
Prep complete:
|
Prep complete:
|
||||||
- local.json: [what was added]
|
- local.json: [what was added]
|
||||||
|
- Todos: [reconciled vs reality — N done & removed, M re-scoped, K still open]
|
||||||
- observations.json: [updated / skipped]
|
- observations.json: [updated / skipped]
|
||||||
- STATUS.local.md: [updated / skipped]
|
|
||||||
- Plans: [which ones updated]
|
- Plans: [which ones updated]
|
||||||
- Git: [branch, uncommitted count, suggestion]
|
- Git: [branch, uncommitted count, suggestion]
|
||||||
- Inbox: [count, action taken]
|
- Inbox: [count, action taken]
|
||||||
|
- Compass: [entry #N reviewed — verdict]
|
||||||
- Loose ends: [any flagged]
|
- Loose ends: [any flagged]
|
||||||
```
|
```
|
||||||
|
|||||||
+73
-121
@@ -1,144 +1,96 @@
|
|||||||
# AIPass Hook System
|
# .claude/hooks/ -- Legacy Hook Scripts (Post-Migration)
|
||||||
|
|
||||||
Provider-level hooks for the AIPass ecosystem. These fire for every Claude Code
|
> **Migration complete (DPLAN-0184).** All 18 hook scripts in this directory have been
|
||||||
session on this machine via `~/.claude/settings.json`.
|
> disabled (renamed with `(disabled)` suffix). Hook logic now lives in native Python
|
||||||
|
> handlers at `src/aipass/hooks/apps/handlers/`. Provider settings route through the
|
||||||
|
> bridge at `src/aipass/hooks/apps/handlers/bridges/claude.py`.
|
||||||
|
|
||||||
## File Layout
|
## What Remains Active
|
||||||
|
|
||||||
```
|
Three testing/tooling files are still active in this directory:
|
||||||
.claude/hooks/
|
|
||||||
├── README.md # This file
|
|
||||||
│
|
|
||||||
│ ── Hooks (wired in ~/.claude/settings.json) ──
|
|
||||||
├── global_prompt_loader.py # UserPromptSubmit — AIPass global prompt (~22KB)
|
|
||||||
├── branch_prompt_loader.py # UserPromptSubmit — branch-specific prompt
|
|
||||||
├── identity_injector.py # UserPromptSubmit — branch identity from passport
|
|
||||||
├── email_notification.py # UserPromptSubmit — unread email count
|
|
||||||
├── tool_use_sound.py # PreToolUse — key-press sound on tool calls
|
|
||||||
├── git_gate.py # PreToolUse — blocks raw git/gh, protects settings
|
|
||||||
├── auto_fix_diagnostics.py # PostToolUse — pyright + ruff on edited files
|
|
||||||
├── subagent_stop_gate.py # SubagentStop — seedgo checklist on modified files
|
|
||||||
├── pre_compact.py # PreCompact — post-compact recovery context
|
|
||||||
├── stop_sound.py # Stop — achievement bell
|
|
||||||
├── notification_sound.py # Notification — notification sound
|
|
||||||
│
|
|
||||||
│ ── Also wired but lives in ~/.claude/hooks/ ──
|
|
||||||
│ pre_edit_gate.py # PreToolUse — cross-branch write block, error-fix gate
|
|
||||||
│ auto_watchdog.py # PostToolUse — watchdog reminder after dispatch
|
|
||||||
│
|
|
||||||
│ ── Testing & debugging tools ──
|
|
||||||
├── hook_log.py # Shared logger — every hook calls run_and_log()
|
|
||||||
├── hook_report.py # Report tool — reads JSONL log, shows table
|
|
||||||
├── hook_test.py # Test harness — 20 tests (11 direct + 9 integration)
|
|
||||||
│
|
|
||||||
│ ── Legacy probes ──
|
|
||||||
└── probes/
|
|
||||||
├── README.md
|
|
||||||
└── probe_*.py # Opt-in per-event diagnostic hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `hook_log.py` | Shared JSONL logger -- hooks call `run_and_log()` to record execution |
|
||||||
|
| `hook_report.py` | Report tool -- reads `/tmp/aipass_hook_log.jsonl`, shows table |
|
||||||
|
| `hook_test.py` | Test harness -- direct + integration tests for hook behavior |
|
||||||
|
|
||||||
Hooks fire from three levels (can fire simultaneously):
|
### hook_report.py usage
|
||||||
|
|
||||||
| Level | Settings file | When it fires |
|
|
||||||
|-------|--------------|---------------|
|
|
||||||
| **Provider** | `~/.claude/settings.json` | Every session, everywhere |
|
|
||||||
| **Project** | `<project>/.claude/settings.json` | When CWD is inside the project |
|
|
||||||
| **Branch** | deeper `.claude/settings.json` | When CWD is inside that branch |
|
|
||||||
|
|
||||||
**Critical limitation:** PreToolUse and PostToolUse ONLY fire from provider settings.
|
|
||||||
UserPromptSubmit fires from ALL levels. This means project-level PreToolUse/PostToolUse
|
|
||||||
hooks provisioned by `aipass init` are dead weight — they never execute.
|
|
||||||
|
|
||||||
## CWD Guards
|
|
||||||
|
|
||||||
Four UserPromptSubmit hooks have CWD-aware guards. When CWD is inside a project that
|
|
||||||
has its own UserPromptSubmit hooks, the provider hook exits silently — preventing
|
|
||||||
AIPass context from bleeding into standalone projects.
|
|
||||||
|
|
||||||
Guarded: `global_prompt_loader.py`, `branch_prompt_loader.py`,
|
|
||||||
`identity_injector.py`, `email_notification.py`.
|
|
||||||
|
|
||||||
## Hook Inventory
|
|
||||||
|
|
||||||
### UserPromptSubmit (provider, CWD-guarded)
|
|
||||||
| Script | Purpose |
|
|
||||||
|--------|---------|
|
|
||||||
| `global_prompt_loader.py` | Injects AIPass global prompt (~22KB) |
|
|
||||||
| `branch_prompt_loader.py` | Injects branch-specific prompt from `.aipass/aipass_local_prompt.md` |
|
|
||||||
| `identity_injector.py` | Injects branch identity from `.trinity/passport.json` |
|
|
||||||
| `email_notification.py` | Shows unread email count from `.ai_mail.local/inbox.json` |
|
|
||||||
|
|
||||||
### PreToolUse (provider only)
|
|
||||||
| Script | Matcher | Purpose |
|
|
||||||
|--------|---------|---------|
|
|
||||||
| `tool_use_sound.py` | Bash\|Edit\|Write\|Read\|... | Plays key-press sound |
|
|
||||||
| `pre_edit_gate.py` | Edit\|Write\|NotebookEdit | Cross-branch write block + error-fix gate |
|
|
||||||
| `git_gate.py` | Bash\|Edit\|Write\|NotebookEdit | Blocks raw git/gh, protects settings files |
|
|
||||||
|
|
||||||
### PostToolUse (provider only)
|
|
||||||
| Script | Matcher | Purpose |
|
|
||||||
|--------|---------|---------|
|
|
||||||
| `auto_fix_diagnostics.py` | Edit\|Write\|NotebookEdit | Runs pyright + ruff on edited files |
|
|
||||||
| `auto_watchdog.py` | Bash | Reminds agent to arm watchdog after dispatch |
|
|
||||||
|
|
||||||
### Other events (provider)
|
|
||||||
| Script | Event | Purpose |
|
|
||||||
|--------|-------|---------|
|
|
||||||
| `subagent_stop_gate.py` | SubagentStop | Runs seedgo checklist on subagent-modified files + hook README reminder |
|
|
||||||
| `pre_compact.py` | PreCompact | Injects post-compact recovery context |
|
|
||||||
| `stop_sound.py` | Stop | Plays achievement bell |
|
|
||||||
| `notification_sound.py` | Notification | Plays notification sound |
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Execution log (always-on)
|
|
||||||
Every instrumented hook writes one JSONL line to `/tmp/aipass_hook_log.jsonl` via
|
|
||||||
`hook_log.py`. Each entry: timestamp, event, source, script, CWD, session, timing,
|
|
||||||
output_bytes, exit_code.
|
|
||||||
|
|
||||||
### Report tool
|
|
||||||
```bash
|
```bash
|
||||||
python3 .claude/hooks/hook_report.py # Last 5 minutes
|
python3 .claude/hooks/hook_report.py # Last 5 minutes
|
||||||
python3 .claude/hooks/hook_report.py --all # All entries
|
python3 .claude/hooks/hook_report.py --all # All entries
|
||||||
python3 .claude/hooks/hook_report.py --cwd /tmp # Filter by CWD
|
python3 .claude/hooks/hook_report.py --cwd /tmp # Filter by CWD
|
||||||
python3 .claude/hooks/hook_report.py --json # Machine-readable
|
python3 .claude/hooks/hook_report.py --json # Machine-readable
|
||||||
python3 .claude/hooks/hook_report.py --clear # Wipe log
|
python3 .claude/hooks/hook_report.py --clear # Wipe log
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test harness (20 tests)
|
### hook_test.py usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 .claude/hooks/hook_test.py # All 20 tests
|
python3 .claude/hooks/hook_test.py # All tests
|
||||||
python3 .claude/hooks/hook_test.py --direct # 11 direct tests only (fast, ~3s)
|
python3 .claude/hooks/hook_test.py --direct # Direct tests only (fast, ~3s)
|
||||||
python3 .claude/hooks/hook_test.py --integration # 9 integration tests only (~2min)
|
python3 .claude/hooks/hook_test.py --integration # Integration tests only (~2min)
|
||||||
python3 .claude/hooks/hook_test.py --verbose # Show detail per test
|
python3 .claude/hooks/hook_test.py --verbose # Show detail per test
|
||||||
python3 .claude/hooks/hook_test.py --list # List available tests
|
python3 .claude/hooks/hook_test.py --list # List available tests
|
||||||
python3 .claude/hooks/hook_test.py --test <name> # Run one test
|
python3 .claude/hooks/hook_test.py --test <name> # Run one test
|
||||||
```
|
```
|
||||||
|
|
||||||
**Direct tests** (11) pipe JSON to hook scripts via subprocess. Deterministic,
|
## Disabled Scripts (18 files)
|
||||||
no model, HIGH confidence. Tests CWD guards, git_gate block/allow, settings schema,
|
|
||||||
project-level guards.
|
|
||||||
|
|
||||||
**Integration tests** (9) run `claude -p` from different CWDs and read the JSONL log.
|
These are the original standalone hook scripts. They were disabled as part of DPLAN-0184
|
||||||
Tests full pipeline including cross-project behavior, subagent hooks, and the
|
Phase 2 when their logic was migrated to native handlers. The files are kept for reference
|
||||||
`disableAllHooks` toggle.
|
but are not executed.
|
||||||
|
|
||||||
### Disable all hooks
|
| Disabled script | Migrated to |
|
||||||
Add `"disableAllHooks": true` to `~/.claude/settings.json`. Remove to re-enable.
|
|-----------------|-------------|
|
||||||
|
| `global_prompt_loader.py(disabled)` | `handlers/prompt/global_loader.py` |
|
||||||
|
| `branch_prompt_loader.py(disabled)` | `handlers/prompt/branch_loader.py` |
|
||||||
|
| `identity_injector.py(disabled)` | `handlers/prompt/identity.py` |
|
||||||
|
| `email_notification.py(disabled)` | `handlers/notification/email.py` |
|
||||||
|
| `tool_use_sound.py(disabled)` | `handlers/notification/tool_sound.py` |
|
||||||
|
| `git_gate.py(disabled)` | `handlers/security/git_gate.py` |
|
||||||
|
| `pre_edit_gate.py(disabled)` | `handlers/security/edit_gate.py` |
|
||||||
|
| `auto_fix_diagnostics.py(disabled)` | `handlers/lifecycle/auto_fix.py` |
|
||||||
|
| `auto_watchdog.py(disabled)` | `handlers/lifecycle/auto_watchdog.py` |
|
||||||
|
| `subagent_stop_gate.py(disabled)` | `handlers/security/subagent_gate.py` |
|
||||||
|
| `pre_compact.py(disabled)` | `handlers/lifecycle/compact.py` |
|
||||||
|
| `pre_compact_rollover.py(disabled)` | `handlers/lifecycle/rollover.py` |
|
||||||
|
| `stop_sound.py(disabled)` | `handlers/notification/stop_sound.py` |
|
||||||
|
| `notification_sound.py(disabled)` | `handlers/notification/announce.py` |
|
||||||
|
| `prompt_inject.sh(disabled)` | (combined inject -- never used in production) |
|
||||||
|
| `engine.py(disabled)` | `hooks/apps/modules/engine.py` |
|
||||||
|
| `engine_test_hook.py(disabled)` | (test fixture, no longer needed) |
|
||||||
|
| `engine_test_sound.py(disabled)` | (test fixture, disabled in hooks.json) |
|
||||||
|
|
||||||
### Debug mode
|
All handler paths above are relative to `src/aipass/hooks/apps/`.
|
||||||
```bash
|
|
||||||
claude --debug hooks --debug-file /tmp/debug.log
|
## Probes (Opt-In Diagnostics)
|
||||||
|
|
||||||
|
The `probes/` subdirectory contains passive observer scripts for individual hook events.
|
||||||
|
These are opt-in, not auto-wired. See `probes/README.md` for usage.
|
||||||
|
|
||||||
|
## New Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.claude/settings.json
|
||||||
|
|
|
||||||
|
v
|
||||||
|
claude.py (bridge) -- thin entry point, normalizes stdin
|
||||||
|
|
|
||||||
|
v
|
||||||
|
engine.py (dispatcher) -- reads .aipass/hooks.json, imports handlers
|
||||||
|
|
|
||||||
|
v
|
||||||
|
handlers/ -- native Python, organized by domain
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interactive inspection
|
For full architecture documentation, see the parent `../.claude/README.md`.
|
||||||
Type `/hooks` inside a Claude session — shows all hooks with source labels
|
|
||||||
(`[User]`, `[Project]`, `[Local]`).
|
|
||||||
|
|
||||||
## Related
|
## Related Plans
|
||||||
- **DPLAN-0167** — Hook testing framework
|
|
||||||
- **DPLAN-0166** — Hook audit + CI health
|
- **DPLAN-0184** -- Hook migration (standalone scripts to native handlers)
|
||||||
- **DPLAN-0139** — Hook overhaul + single-path enforcement
|
- **DPLAN-0167** -- Hook testing framework
|
||||||
- **DPLAN-0131** — Hook system alignment (seedgo ownership)
|
- **DPLAN-0166** -- Hook audit + CI health
|
||||||
|
- **DPLAN-0139** -- Hook overhaul + single-path enforcement
|
||||||
|
- **DPLAN-0053** -- Original hook architecture research
|
||||||
|
|||||||
@@ -1,390 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
PostToolUse Auto-fix Hook — Detects errors and surfaces them for fixing.
|
|
||||||
|
|
||||||
Two-hook system:
|
|
||||||
PostToolUse (this file) → runs pyright + ruff on edited file, saves errors to state
|
|
||||||
PreToolUse (pre_edit_gate.py) → blocks edits to OTHER files until errors fixed
|
|
||||||
|
|
||||||
Key behaviors:
|
|
||||||
- Runs py_compile (syntax), ruff lint+format, pyright (type errors) on edited file
|
|
||||||
- Runs seedgo checklist for AIPass standards
|
|
||||||
- Saves ruff lint AND pyright errors to state file for PreToolUse gate (hard block)
|
|
||||||
- Surfaces ALL errors in additionalContext so Claude sees them
|
|
||||||
|
|
||||||
Version: 5.2.0
|
|
||||||
|
|
||||||
CHANGELOG:
|
|
||||||
- v5.2.0 (2026-04-20): Save ruff lint errors to state file for hard-block enforcement.
|
|
||||||
Pre-edit gate now blocks on F401/lint just like type errors.
|
|
||||||
- v5.1.0 (2026-04-19): Added ruff format --check to surface format drift.
|
|
||||||
- v5.0.0 (2026-03-17): Replaced mcp__ide__getDiagnostics with direct pyright.
|
|
||||||
Added state file for PreToolUse gate integration.
|
|
||||||
Single-file pyright (not whole project).
|
|
||||||
- v4.3.0 (2026-03-17): Added seedgo checklist integration
|
|
||||||
- v4.0.0 (2025-11-27): Complete rewrite - actual validation, silent operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
EDIT_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"]
|
|
||||||
LAST_FILE_PATH = Path(__file__).parent / ".last_diagnostics_file"
|
|
||||||
STATE_FILE = Path(__file__).parent / ".diagnostics_state.json"
|
|
||||||
SKIP_EXTENSIONS = {".md", ".txt", ".log", ".csv", ".html"}
|
|
||||||
|
|
||||||
# AIPass-specific Python patterns to check
|
|
||||||
PYTHON_PATTERNS = {
|
|
||||||
"bad_optional": {"pattern": ": str = None", "message": "Optional param should use 'str | None = None' pattern"},
|
|
||||||
"logger_debug": {
|
|
||||||
"pattern": "logger.debug(",
|
|
||||||
"message": "Use logger.info for SystemLogger (logger.debug not supported)",
|
|
||||||
},
|
|
||||||
"return_error_msg": {
|
|
||||||
"pattern": "return error_msg",
|
|
||||||
"message": "Return None for error states, not error_msg string",
|
|
||||||
},
|
|
||||||
"open_no_encoding": {
|
|
||||||
"pattern": "open(",
|
|
||||||
"requires_missing": "encoding=",
|
|
||||||
"message": "open() without encoding='utf-8'",
|
|
||||||
},
|
|
||||||
"log_not_log_operation": {
|
|
||||||
"pattern": ".log(",
|
|
||||||
"message": "Use log_operation() with success/error params, not .log()",
|
|
||||||
},
|
|
||||||
"dict_none_no_check": {
|
|
||||||
"pattern": "Dict | None",
|
|
||||||
"message": "Dict | None return: Add None check before using (if result is None: return)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# JSON-specific patterns for emoji corruption
|
|
||||||
JSON_CORRUPTION_CHARS = ["\ufffd", "\x00"]
|
|
||||||
|
|
||||||
|
|
||||||
def run_python_checks(file_path: str) -> list[str]:
|
|
||||||
"""Run actual Python validation - returns list of errors."""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# 1. Syntax check with py_compile
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, "-m", "py_compile", file_path], capture_output=True, text=True, timeout=5
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
errors.append(f"SYNTAX: {result.stderr.strip()}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 2. Ruff check (if available) - fast linter
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["ruff", "check", "--select=E,F,W", "--output-format=text", file_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if result.stdout.strip():
|
|
||||||
for line in result.stdout.strip().split("\n")[:5]:
|
|
||||||
errors.append(f"LINT: {line}")
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3. Ruff format check — detect format drift
|
|
||||||
try:
|
|
||||||
result = subprocess.run(["ruff", "format", "--check", file_path], capture_output=True, text=True, timeout=10)
|
|
||||||
if result.returncode != 0:
|
|
||||||
errors.append(f"FORMAT: {Path(file_path).name} needs ruff format (run: ruff format {Path(file_path).name})")
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. AIPass-specific pattern checks
|
|
||||||
try:
|
|
||||||
content = Path(file_path).read_text(encoding="utf-8")
|
|
||||||
lines = content.split("\n")
|
|
||||||
|
|
||||||
for check in PYTHON_PATTERNS.values():
|
|
||||||
pattern = check["pattern"]
|
|
||||||
message = check["message"]
|
|
||||||
requires_missing = check.get("requires_missing")
|
|
||||||
|
|
||||||
if requires_missing:
|
|
||||||
if pattern in content and requires_missing not in content:
|
|
||||||
errors.append(f"PATTERN: {message}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith(("#", '"', "'")):
|
|
||||||
continue
|
|
||||||
if f'"{pattern}' in line or f"'{pattern}" in line:
|
|
||||||
continue
|
|
||||||
if pattern in line:
|
|
||||||
errors.append(f"PATTERN: {message}")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def run_ruff_lint_structured(file_path: str) -> list[dict]:
|
|
||||||
"""Run ruff check and return structured violations for the state file.
|
|
||||||
|
|
||||||
Returns list of {line, message} dicts — same format as pyright errors.
|
|
||||||
Only non-empty when ruff finds real violations (not format drift).
|
|
||||||
"""
|
|
||||||
if "/.claude/hooks/" in file_path:
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["ruff", "check", "--select=E,F,W", "--output-format=json", file_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if not result.stdout.strip():
|
|
||||||
return []
|
|
||||||
violations = json.loads(result.stdout)
|
|
||||||
if not isinstance(violations, list):
|
|
||||||
return []
|
|
||||||
errors = []
|
|
||||||
for v in violations[:10]:
|
|
||||||
line = v.get("location", {}).get("row", 0)
|
|
||||||
code = v.get("code", "?")
|
|
||||||
message = v.get("message", "unknown")[:100]
|
|
||||||
errors.append({"line": line, "message": f"{code}: {message}"})
|
|
||||||
return errors
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError, subprocess.TimeoutExpired, Exception):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def run_pyright_check(file_path: str) -> list[dict]:
|
|
||||||
"""Run pyright on a single file. Returns list of error dicts."""
|
|
||||||
# Skip hook files - they don't follow project standards
|
|
||||||
if "/.claude/hooks/" in file_path:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, "-m", "pyright", "--outputjson", file_path], capture_output=True, text=True, timeout=15
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(result.stdout)
|
|
||||||
except (json.JSONDecodeError, ValueError):
|
|
||||||
return []
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
for diag in data.get("generalDiagnostics", []):
|
|
||||||
severity = diag.get("severity", "")
|
|
||||||
if severity == "error":
|
|
||||||
line = diag.get("range", {}).get("start", {}).get("line", 0)
|
|
||||||
message = diag.get("message", "Unknown error")
|
|
||||||
errors.append({"line": line, "message": message[:100]})
|
|
||||||
|
|
||||||
return errors[:10] # Max 10 errors
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
return [] # pyright not installed
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return [] # Timeout — don't block
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def save_diagnostics_state(file_path: str, errors: list[dict]):
|
|
||||||
"""Save type errors to state file for PreToolUse gate."""
|
|
||||||
try:
|
|
||||||
if errors:
|
|
||||||
state = {"file": str(Path(file_path).resolve()), "errors": errors}
|
|
||||||
STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
|
|
||||||
else:
|
|
||||||
# No errors — clear the state
|
|
||||||
if STATE_FILE.exists():
|
|
||||||
STATE_FILE.unlink()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def run_json_checks(file_path: str) -> list[str]:
|
|
||||||
"""Run actual JSON validation - returns list of errors."""
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = Path(file_path).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
for char in JSON_CORRUPTION_CHARS:
|
|
||||||
if char in content:
|
|
||||||
errors.append(f"EMOJI CORRUPTION: Found corrupted character '{repr(char)}'")
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(content)
|
|
||||||
|
|
||||||
if isinstance(data, dict):
|
|
||||||
for key in ["allowed_emojis", "emojis", "emoji_list"]:
|
|
||||||
if key in data and isinstance(data[key], list):
|
|
||||||
for item in data[key]:
|
|
||||||
if isinstance(item, str) and len(item) == 1:
|
|
||||||
if ord(item) < 128 and item not in "\u2713\u2717":
|
|
||||||
errors.append(f"EMOJI CORRUPTION: Suspicious char '{item}' in {key}")
|
|
||||||
break
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
errors.append(f"JSON SYNTAX: {e.msg} at line {e.lineno}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(f"READ ERROR: {e!s}")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def run_seedgo_checklist(file_path: str) -> list[str]:
|
|
||||||
"""Run seedgo standards checklist — returns violations only."""
|
|
||||||
if "/.claude/hooks/" in file_path:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["drone", "@seedgo", "checklist", file_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=15,
|
|
||||||
cwd=str(Path.home() / "Projects" / "AIPass"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return []
|
|
||||||
|
|
||||||
violations = []
|
|
||||||
for line in result.stdout.split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("\u2717"):
|
|
||||||
violation = line[1:].strip()
|
|
||||||
if violation:
|
|
||||||
violations.append(violation)
|
|
||||||
|
|
||||||
return violations[:5]
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
return []
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def should_skip_file(file_path: str) -> bool:
|
|
||||||
"""Check if file should be skipped."""
|
|
||||||
if not file_path:
|
|
||||||
return True
|
|
||||||
ext = Path(file_path).suffix.lower()
|
|
||||||
return ext in SKIP_EXTENSIONS
|
|
||||||
|
|
||||||
|
|
||||||
def is_same_file_as_last(file_path: str) -> bool:
|
|
||||||
"""Smart batching DISABLED — always recheck.
|
|
||||||
|
|
||||||
Previously skipped rechecks on the same file, but this caused
|
|
||||||
errors introduced on second edit to be missed (state file didn't
|
|
||||||
exist from first clean edit, so skip triggered). The 1.7s pyright
|
|
||||||
cost per edit is acceptable for correctness.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _project_has_own_posttooluse_hooks() -> bool:
|
|
||||||
"""Check if CWD is inside a project with its own PostToolUse hooks."""
|
|
||||||
search = Path.cwd()
|
|
||||||
home = Path.home()
|
|
||||||
while search != home and search.parent != search:
|
|
||||||
settings = search / ".claude" / "settings.json"
|
|
||||||
if settings.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
||||||
ptu = data.get("hooks", {}).get("PostToolUse", [])
|
|
||||||
if ptu:
|
|
||||||
return True
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
search = search.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main hook entry point."""
|
|
||||||
try:
|
|
||||||
if _project_has_own_posttooluse_hooks():
|
|
||||||
return
|
|
||||||
|
|
||||||
input_data = json.load(sys.stdin)
|
|
||||||
tool_name = input_data.get("tool_name", "")
|
|
||||||
tool_input = input_data.get("tool_input", {})
|
|
||||||
file_path = tool_input.get("file_path", "")
|
|
||||||
|
|
||||||
if tool_name not in EDIT_TOOLS:
|
|
||||||
return
|
|
||||||
|
|
||||||
if should_skip_file(file_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
if is_same_file_as_last(file_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Collect all errors
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
if file_path.endswith(".py"):
|
|
||||||
errors = run_python_checks(file_path)
|
|
||||||
|
|
||||||
# Seedgo standards checklist
|
|
||||||
seedgo_violations = run_seedgo_checklist(file_path)
|
|
||||||
for v in seedgo_violations:
|
|
||||||
errors.append(f"SEEDGO: {v}")
|
|
||||||
|
|
||||||
# Pyright type errors (single file)
|
|
||||||
type_errors = run_pyright_check(file_path)
|
|
||||||
for te in type_errors:
|
|
||||||
errors.append(f"TYPE: L{te['line']}: {te['message']}")
|
|
||||||
|
|
||||||
# Save ruff lint + type errors to state file for PreToolUse gate (hard block)
|
|
||||||
ruff_lint_errors = run_ruff_lint_structured(file_path)
|
|
||||||
save_diagnostics_state(file_path, ruff_lint_errors + type_errors)
|
|
||||||
|
|
||||||
elif file_path.endswith(".json"):
|
|
||||||
errors = run_json_checks(file_path)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
if errors:
|
|
||||||
error_text = "\n".join(f" - {e}" for e in errors)
|
|
||||||
context = f"""[AUTO-FIX] {len(errors)} error(s) in {Path(file_path).name}:
|
|
||||||
{error_text}
|
|
||||||
|
|
||||||
Fix these errors in {Path(file_path).name} now. Do not skip or defer."""
|
|
||||||
|
|
||||||
output = {
|
|
||||||
"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": context},
|
|
||||||
"systemMessage": f"[AUTO-FIX] {len(errors)} error(s) — fix before continuing",
|
|
||||||
}
|
|
||||||
print(json.dumps(output))
|
|
||||||
else:
|
|
||||||
output = {"systemMessage": "[diagnostics] ok"}
|
|
||||||
print(json.dumps(output))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass # Silent fail
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("PostToolUse", "provider", __file__, main)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""PostToolUse hook — reminds agent to arm watchdog after dispatch.
|
|
||||||
|
|
||||||
Fires after Bash commands containing 'drone @ai_mail dispatch'.
|
|
||||||
Outputs additionalContext telling the agent to arm the watchdog.
|
|
||||||
Skips if watchdog is already part of the same command.
|
|
||||||
|
|
||||||
Version: 1.0.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Check if dispatch was run and remind to arm watchdog."""
|
|
||||||
try:
|
|
||||||
hook_input = json.load(sys.stdin)
|
|
||||||
except (json.JSONDecodeError, EOFError):
|
|
||||||
return
|
|
||||||
|
|
||||||
tool_name = hook_input.get("tool_name", "")
|
|
||||||
tool_input = hook_input.get("tool_input", {})
|
|
||||||
|
|
||||||
if tool_name != "Bash":
|
|
||||||
return
|
|
||||||
|
|
||||||
command = tool_input.get("command", "")
|
|
||||||
|
|
||||||
# Only trigger on dispatch commands
|
|
||||||
if "drone @ai_mail dispatch" not in command:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Skip if watchdog is already in the same command
|
|
||||||
if "unread_count" in command and "while [" in command:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Skip if it's just checking dispatch status (not sending)
|
|
||||||
if "dispatch wake" in command and "dispatch @" not in command:
|
|
||||||
return
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"additionalContext": (
|
|
||||||
"[AUTO-WATCHDOG] Dispatch detected — arm watchdog NOW. "
|
|
||||||
"Run the watchdog one-liner from your local prompt with "
|
|
||||||
"run_in_background: true and timeout: 600000."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
json.dump(result, sys.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Branch Prompt Loader — AIPass Public Repo
|
|
||||||
|
|
||||||
Injects branch-specific prompts based on CWD. When working in a branch
|
|
||||||
directory, loads .aipass/aipass_local_prompt.md and outputs it so the
|
|
||||||
AI sees branch-specific context.
|
|
||||||
|
|
||||||
When CWD is inside a project that has its own UserPromptSubmit hooks
|
|
||||||
(e.g. a standalone aipass-init project), this provider-level hook exits
|
|
||||||
silently to avoid double-firing.
|
|
||||||
|
|
||||||
Version: 1.1.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _project_has_own_hooks() -> bool:
|
|
||||||
"""Check if CWD is inside a project with its own UserPromptSubmit hooks."""
|
|
||||||
search = Path.cwd()
|
|
||||||
home = Path.home()
|
|
||||||
while search != home and search.parent != search:
|
|
||||||
settings = search / ".claude" / "settings.json"
|
|
||||||
if settings.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
||||||
ups = data.get("hooks", {}).get("UserPromptSubmit", [])
|
|
||||||
if ups:
|
|
||||||
return True
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
search = search.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def find_branch_root() -> Path | None:
|
|
||||||
"""
|
|
||||||
Find the branch root directory.
|
|
||||||
Looks for .trinity/ or .aipass/ as branch indicators.
|
|
||||||
Stops at the repo root (has pyproject.toml or .git).
|
|
||||||
"""
|
|
||||||
cwd = Path.cwd()
|
|
||||||
search_path = cwd
|
|
||||||
|
|
||||||
while search_path.parent != search_path:
|
|
||||||
# Branch indicators: has .trinity/ (memory files) or apps/ (code)
|
|
||||||
has_trinity = (search_path / ".trinity").is_dir()
|
|
||||||
has_apps = (search_path / "apps").is_dir()
|
|
||||||
|
|
||||||
if has_trinity or has_apps:
|
|
||||||
return search_path
|
|
||||||
|
|
||||||
# Stop at repo root
|
|
||||||
if (search_path / "pyproject.toml").exists() or (search_path / ".git").is_dir():
|
|
||||||
return None
|
|
||||||
|
|
||||||
search_path = search_path.parent
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if _project_has_own_hooks():
|
|
||||||
return
|
|
||||||
|
|
||||||
branch_root = find_branch_root()
|
|
||||||
|
|
||||||
if branch_root:
|
|
||||||
prompt_file = branch_root / ".aipass" / "aipass_local_prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
content = prompt_file.read_text().strip()
|
|
||||||
branch_name = branch_root.name.upper()
|
|
||||||
print(f"\n# Branch Context: {branch_name}\n<!-- Source: {prompt_file} -->\n{content}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("UserPromptSubmit", "provider", __file__, main)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Email Notification Hook - Notifies of new emails on prompt submit.
|
|
||||||
|
|
||||||
Checks the current branch's inbox for unread emails and displays
|
|
||||||
a notification if any exist.
|
|
||||||
|
|
||||||
When CWD is inside a project that has its own UserPromptSubmit hooks,
|
|
||||||
this provider-level hook exits silently to avoid double-firing.
|
|
||||||
|
|
||||||
Version: 1.1.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _project_has_own_hooks() -> bool:
|
|
||||||
"""Check if CWD is inside a project with its own UserPromptSubmit hooks."""
|
|
||||||
search = Path.cwd()
|
|
||||||
home = Path.home()
|
|
||||||
while search != home and search.parent != search:
|
|
||||||
settings = search / ".claude" / "settings.json"
|
|
||||||
if settings.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
||||||
ups = data.get("hooks", {}).get("UserPromptSubmit", [])
|
|
||||||
if ups:
|
|
||||||
return True
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
search = search.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def find_repo_root() -> Path | None:
|
|
||||||
"""Find the repo root (contains pyproject.toml or .git)."""
|
|
||||||
search = Path.cwd()
|
|
||||||
while search.parent != search:
|
|
||||||
if (search / "pyproject.toml").exists() or (search / ".git").is_dir():
|
|
||||||
return search
|
|
||||||
search = search.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_branch_root() -> Path | None:
|
|
||||||
"""Find the branch root directory by walking up from CWD."""
|
|
||||||
cwd = Path.cwd()
|
|
||||||
repo_root = find_repo_root()
|
|
||||||
if not repo_root:
|
|
||||||
return None
|
|
||||||
|
|
||||||
search_path = cwd
|
|
||||||
for _ in range(10):
|
|
||||||
has_trinity = (search_path / ".trinity").is_dir()
|
|
||||||
has_id = list(search_path.glob("*.id.json"))
|
|
||||||
has_apps = (search_path / "apps").is_dir()
|
|
||||||
has_mail = (search_path / ".ai_mail.local").is_dir() or (search_path / "ai_mail.local").is_dir()
|
|
||||||
|
|
||||||
if (has_trinity or has_id or has_apps or has_mail) and search_path != repo_root:
|
|
||||||
return search_path
|
|
||||||
|
|
||||||
if search_path == repo_root:
|
|
||||||
break
|
|
||||||
|
|
||||||
parent = search_path.parent
|
|
||||||
if parent == search_path:
|
|
||||||
break
|
|
||||||
search_path = parent
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def count_new_emails(branch_root: Path) -> int:
|
|
||||||
"""Count new (unread) emails in the branch's inbox."""
|
|
||||||
# Check both patterns: .ai_mail.local (canonical) and ai_mail.local (legacy)
|
|
||||||
inbox_path = branch_root / ".ai_mail.local" / "inbox.json"
|
|
||||||
if not inbox_path.exists():
|
|
||||||
inbox_path = branch_root / "ai_mail.local" / "inbox.json"
|
|
||||||
|
|
||||||
if not inbox_path.exists():
|
|
||||||
return 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(inbox_path, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
# Handle both formats: {"messages": [...]} and bare [...]
|
|
||||||
messages = data if isinstance(data, list) else data.get("messages", [])
|
|
||||||
count = 0
|
|
||||||
for msg in messages:
|
|
||||||
if msg.get("status") == "new":
|
|
||||||
count += 1
|
|
||||||
elif msg.get("status") is None and not msg.get("read", False):
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if _project_has_own_hooks():
|
|
||||||
return
|
|
||||||
|
|
||||||
branch_root = find_branch_root()
|
|
||||||
if not branch_root:
|
|
||||||
return
|
|
||||||
|
|
||||||
new_count = count_new_emails(branch_root)
|
|
||||||
if new_count > 0:
|
|
||||||
plural = "s" if new_count != 1 else ""
|
|
||||||
print(
|
|
||||||
f"You have {new_count} new email{plural} - check with: drone @ai_mail inbox | then: drone @ai_mail view <id> | close with: drone @ai_mail close <id>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("UserPromptSubmit", "provider", __file__, main)
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{"ts": 1779087885.8874333, "event": "PreToolUse", "hook": "tool_use_sound", "exit_code": 0, "elapsed_ms": 40.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087885.8876748, "event": "PreToolUse", "hook": "pre_edit_gate", "action": "skipped_no_match", "matcher": "Edit|MultiEdit|Write|NotebookEdit", "value": "Read"}
|
||||||
|
{"ts": 1779087885.8881602, "event": "PreToolUse", "hook": "git_gate", "action": "skipped_no_match", "matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit", "value": "Read"}
|
||||||
|
{"ts": 1779087885.888458, "event": "PreToolUse", "hook": "engine_test_sound", "action": "skipped_no_match", "matcher": "WebSearch", "value": "Read"}
|
||||||
|
{"ts": 1779087885.9210913, "event": "PreToolUse", "hook": "BROKEN_crash_test", "exit_code": 2, "elapsed_ms": 31.2, "stdout_len": 0, "stderr_preview": "python3: can't open file '/tmp/THIS_DOES_NOT_EXIST_AT_ALL.py': [Errno 2] No such file or directory\n", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087885.9220648, "event": "PreToolUse", "hook": "BROKEN_crash_test", "action": "crashed", "stderr": "python3: can't open file '/tmp/THIS_DOES_NOT_EXIST_AT_ALL.py': [Errno 2] No such file or directory\n"}
|
||||||
|
{"ts": 1779087885.9231257, "event": "PreToolUse", "action": "complete", "hooks_run": 0, "total_ms": 372.8}
|
||||||
|
{"ts": 1779087903.771124, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 211.9, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087903.7721746, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 1407.3}
|
||||||
|
{"ts": 1779087908.359278, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 1317.3, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087908.360058, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 1900.4}
|
||||||
|
{"ts": 1779087948.5783036, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 53.2, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087948.6398153, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 60.5, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087948.7356682, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 94.9, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087948.8025231, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 66.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087948.8031442, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 592.6}
|
||||||
|
{"ts": 1779087969.61676, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 83.6, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087969.6175067, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 549.3}
|
||||||
|
{"ts": 1779087973.7319121, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 660.8, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779087973.7324946, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 927.3}
|
||||||
|
{"ts": 1779088321.8144712, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 44.0, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088321.8797712, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 62.3, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088321.939397, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 57.8, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088321.9993627, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 59.2, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088322.0001252, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 539.6}
|
||||||
|
{"ts": 1779088523.355975, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 69.6, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088523.356537, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 392.2}
|
||||||
|
{"ts": 1779088557.6554952, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 33.4, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088557.696279, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 39.6, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088557.7499194, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 52.2, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088557.7993498, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 48.2, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088557.8000984, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 995.9}
|
||||||
|
{"ts": 1779088567.4456015, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 69.6, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088567.4462054, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 449.2}
|
||||||
|
{"ts": 1779088570.872307, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 758.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088570.8730876, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 1024.6}
|
||||||
|
{"ts": 1779088693.5529475, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 44.5, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088693.6015344, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 47.8, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088693.6411777, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 38.0, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088693.6902359, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 48.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088693.6908083, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 439.1}
|
||||||
|
{"ts": 1779088702.3629355, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 85.2, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088702.3633838, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 459.4}
|
||||||
|
{"ts": 1779088706.1254911, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 649.0, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779088706.1261542, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 904.8}
|
||||||
|
{"ts": 1779122916.2700117, "event": "Notification", "hook": "notification_sound", "exit_code": 0, "elapsed_ms": 41.0, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122916.270565, "event": "Notification", "action": "complete", "hooks_run": 0, "total_ms": 390.0}
|
||||||
|
{"ts": 1779122954.4108016, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 97.3, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122954.4598262, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 47.8, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122954.557771, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 97.1, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122954.6079268, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 48.9, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122954.6094744, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 672.6}
|
||||||
|
{"ts": 1779122967.6779344, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 45.0, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779122967.6787398, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 401.8}
|
||||||
|
{"ts": 1779123157.5541441, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 624.7, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123157.554982, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 883.3}
|
||||||
|
{"ts": 1779123163.3793867, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 33.0, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123163.4235747, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 43.1, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123163.4708867, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 46.4, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123163.5132086, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 41.4, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123163.5137308, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 429.9}
|
||||||
|
{"ts": 1779123186.0301352, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 50.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123186.0307028, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 342.4}
|
||||||
|
{"ts": 1779123254.4626336, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 46.5, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123254.5158958, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 52.1, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123254.5792346, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 62.4, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123254.6368563, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 56.7, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123254.6375203, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 481.4}
|
||||||
|
{"ts": 1779123520.2690194, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 70.4, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123520.269594, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 360.6}
|
||||||
|
{"ts": 1779123580.7943206, "event": "Notification", "hook": "notification_sound", "exit_code": 0, "elapsed_ms": 45.5, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123580.7948642, "event": "Notification", "action": "complete", "hooks_run": 0, "total_ms": 319.3}
|
||||||
|
{"ts": 1779123705.523997, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 633.8, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779123705.5245044, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 904.5}
|
||||||
|
{"ts": 1779124421.3406193, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 114.5, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779124421.437293, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 95.5, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779124421.537384, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 99.3, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779124421.654711, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 116.1, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779124421.6555922, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 3, "total_ms": 1805.7}
|
||||||
|
{"ts": 1779163144.6138675, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 46.0, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163144.6146653, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 337.3}
|
||||||
|
{"ts": 1779163151.1238678, "event": "SubagentStop", "hook": "subagent_stop_gate", "exit_code": 0, "elapsed_ms": 684.6, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163151.124623, "event": "SubagentStop", "action": "complete", "hooks_run": 0, "total_ms": 933.5}
|
||||||
|
{"ts": 1779163219.5444095, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 55.7, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163219.6031945, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 57.6, "stdout_len": 130, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163219.65857, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 54.1, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163219.7402287, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 80.5, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163219.7411332, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 4, "total_ms": 686.5}
|
||||||
|
{"ts": 1779163230.543275, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 53.9, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163230.5454974, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 1981.7}
|
||||||
|
{"ts": 1779163260.8500037, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 56.9, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163260.9615414, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 110.3, "stdout_len": 130, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163261.0168633, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 54.4, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163261.066856, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 48.9, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163261.0678494, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 4, "total_ms": 1144.6}
|
||||||
|
{"ts": 1779163287.7181246, "event": "Stop", "hook": "stop_sound", "exit_code": 0, "elapsed_ms": 101.0, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163287.719954, "event": "Stop", "action": "complete", "hooks_run": 0, "total_ms": 2324.9}
|
||||||
|
{"ts": 1779163350.5735116, "event": "Notification", "hook": "notification_sound", "exit_code": 0, "elapsed_ms": 56.4, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163350.574208, "event": "Notification", "action": "complete", "hooks_run": 0, "total_ms": 2560.9}
|
||||||
|
{"ts": 1779163395.6311812, "event": "UserPromptSubmit", "hook": "identity_injector", "exit_code": 0, "elapsed_ms": 85.5, "stdout_len": 2187, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163395.7033641, "event": "UserPromptSubmit", "hook": "email_notification", "exit_code": 0, "elapsed_ms": 71.0, "stdout_len": 130, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163395.790108, "event": "UserPromptSubmit", "hook": "branch_prompt", "exit_code": 0, "elapsed_ms": 85.7, "stdout_len": 8300, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163395.8714736, "event": "UserPromptSubmit", "hook": "global_prompt", "exit_code": 0, "elapsed_ms": 80.4, "stdout_len": 14036, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163395.8721743, "event": "UserPromptSubmit", "action": "complete", "hooks_run": 4, "total_ms": 1668.1}
|
||||||
|
{"ts": 1779163428.9231517, "event": "Notification", "hook": "notification_sound", "exit_code": 0, "elapsed_ms": 92.6, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163428.9238687, "event": "Notification", "action": "complete", "hooks_run": 0, "total_ms": 1155.0}
|
||||||
|
{"ts": 1779163470.642621, "event": "Notification", "hook": "notification_sound", "exit_code": 0, "elapsed_ms": 82.4, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779163470.6436064, "event": "Notification", "action": "complete", "hooks_run": 0, "total_ms": 2824.2}
|
||||||
|
{"ts": 1779250863.9149616, "event": "PreToolUse", "hook": "pre_edit_gate", "action": "skipped_no_match", "matcher": "Edit|MultiEdit|Write|NotebookEdit", "value": "Bash"}
|
||||||
|
{"ts": 1779250864.2848454, "event": "PreToolUse", "hook": "git_gate", "exit_code": 0, "elapsed_ms": 43.7, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779250864.2875721, "event": "PreToolUse", "hook": "engine_test_sound", "action": "skipped_disabled"}
|
||||||
|
{"ts": 1779250864.288863, "event": "PreToolUse", "action": "complete", "hooks_run": 0, "total_ms": 372.7}
|
||||||
|
{"ts": 1779250886.5456672, "event": "PreToolUse", "hook": "pre_edit_gate", "action": "skipped_no_match", "matcher": "Edit|MultiEdit|Write|NotebookEdit", "value": "Bash"}
|
||||||
|
{"ts": 1779250886.9372535, "event": "PreToolUse", "hook": "git_gate", "exit_code": 0, "elapsed_ms": 35.8, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779250886.9380789, "event": "PreToolUse", "hook": "engine_test_sound", "action": "skipped_disabled"}
|
||||||
|
{"ts": 1779250886.9388382, "event": "PreToolUse", "action": "complete", "hooks_run": 0, "total_ms": 392.5}
|
||||||
|
{"ts": 1779250909.1423523, "event": "PreToolUse", "hook": "pre_edit_gate", "exit_code": 0, "elapsed_ms": 52.4, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779250909.1921146, "event": "PreToolUse", "hook": "git_gate", "exit_code": 0, "elapsed_ms": 48.8, "stdout_len": 0, "stderr_preview": "", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse"}
|
||||||
|
{"ts": 1779250909.1928554, "event": "PreToolUse", "hook": "engine_test_sound", "action": "skipped_disabled"}
|
||||||
|
{"ts": 1779250909.1935382, "event": "PreToolUse", "action": "complete", "hooks_run": 0, "total_ms": 488.8}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{"ts": 1779086437.4402049, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "agent_id"]}
|
||||||
|
{"ts": 1779086460.0803485, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["user_prompt"]}
|
||||||
|
{"ts": 1779086493.5130055, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "hook_event_name"]}
|
||||||
|
{"ts": 1779086501.5574267, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "effort"]}
|
||||||
|
{"ts": 1779086534.0177336, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "effort"]}
|
||||||
|
{"ts": 1779086594.7874434, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "hook_event_name", "message"]}
|
||||||
|
{"ts": 1779086688.7642086, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "hook_event_name"]}
|
||||||
|
{"ts": 1779086728.029055, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["tool_name", "tool_input"]}
|
||||||
|
{"ts": 1779086747.247906, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "effort"]}
|
||||||
|
{"ts": 1779086820.3323202, "hook": "engine_test_hook", "cwd": "/home/patrick/Projects/AIPass/src/aipass/devpulse", "event_keys": ["session_id", "transcript_path", "cwd", "permission_mode", "hook_event_name"]}
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""PreToolUse Gate — blocks raw git/gh writes + edits to settings/hooks files.
|
|
||||||
|
|
||||||
Dispatched agents spawn with --permission-mode bypassPermissions, which skips
|
|
||||||
all permissions.deny rules in every settings tier. PreToolUse hooks remain the
|
|
||||||
only mechanical chokepoint that survives. This hook gates the dangerous
|
|
||||||
shortcuts and redirects callers to drone.
|
|
||||||
|
|
||||||
Allows: read-only git/gh, all unrelated tool calls, devpulse-from-its-own-branch
|
|
||||||
edits to the enforcement layer itself.
|
|
||||||
Blocks: git write verbs, gh state-changing subcommands, edits to .claude
|
|
||||||
settings.json / hooks/ and .git/hooks/.
|
|
||||||
|
|
||||||
DPLAN-0162.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
BLOCKED_GIT_VERBS = (
|
|
||||||
"commit",
|
|
||||||
"push",
|
|
||||||
"pull",
|
|
||||||
"merge",
|
|
||||||
"rebase",
|
|
||||||
"reset",
|
|
||||||
"checkout",
|
|
||||||
"switch",
|
|
||||||
"cherry-pick",
|
|
||||||
"revert",
|
|
||||||
"rm",
|
|
||||||
"mv",
|
|
||||||
"restore",
|
|
||||||
"clean",
|
|
||||||
"config",
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOCKED_GIT_RE = re.compile(
|
|
||||||
r"(?<![@\w/.])git\s+(?:--?[A-Za-z][A-Za-z0-9_-]*(?:[= ][^\s]+)?\s+)*"
|
|
||||||
r"(" + "|".join(BLOCKED_GIT_VERBS) + r")\b"
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOCKED_GIT_STASH_RE = re.compile(r"(?<![@\w/.])git\s+stash\s+(drop|clear|pop|apply)\b")
|
|
||||||
|
|
||||||
BLOCKED_GIT_BRANCH_RE = re.compile(
|
|
||||||
r"(?<![@\w/.])git\s+branch\s+.*(-[dDmMcC]\b|--delete|--move|--copy|--force|--set-upstream-to|--unset-upstream)"
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOCKED_GIT_TAG_RE = re.compile(r"(?<![@\w/.])git\s+tag\s+.*(-d\b|--delete|--force|-f\b)")
|
|
||||||
|
|
||||||
BLOCKED_GIT_REMOTE_RE = re.compile(
|
|
||||||
r"(?<![@\w/.])git\s+remote\s+(add|remove|rename|set-url|set-branches|set-head|prune)\b"
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOCKED_GH_API_RE = re.compile(r"(?<![@\w/.])gh\s+api\b")
|
|
||||||
|
|
||||||
BLOCKED_GH_RE = re.compile(
|
|
||||||
r"(?<![@\w/.])gh\s+(pr|issue|repo|release|workflow|run|cache|secret|variable|gist)"
|
|
||||||
r"\s+(?!list\b|view\b|status\b|diff\b|checks\b|comments\b)\w[\w-]*"
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOCKED_EDIT_PATTERNS = [
|
|
||||||
re.compile(r"/\.claude/settings(\.local)?\.json$"),
|
|
||||||
re.compile(r"/\.claude/hooks/"),
|
|
||||||
re.compile(r"/\.git/hooks/"),
|
|
||||||
]
|
|
||||||
|
|
||||||
EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
|
|
||||||
|
|
||||||
# Branches trusted to edit the enforcement layer itself (mirrors pre_edit_gate).
|
|
||||||
TRUSTED_HOOK_EDITORS = ("devpulse", "seedgo")
|
|
||||||
|
|
||||||
GIT_REDIRECT = (
|
|
||||||
"Raw git write commands are blocked. Use drone instead:\n"
|
|
||||||
' drone @git pr "description" # branch-scoped PR\n'
|
|
||||||
' drone @git system-pr "description" # devpulse-only system PR\n'
|
|
||||||
" drone @git smart-sync # fetch + rebase\n"
|
|
||||||
" drone @git sync # checkout main + pull\n"
|
|
||||||
" drone @git status # what changed\n"
|
|
||||||
"Read-only git (status, log, diff, show, fetch, ls-files) is allowed."
|
|
||||||
)
|
|
||||||
|
|
||||||
GH_REDIRECT = (
|
|
||||||
"Raw gh write commands are blocked. Use drone for git ops:\n"
|
|
||||||
' drone @git pr "description"\n'
|
|
||||||
" drone @git merge <PR#> # devpulse only, on user request\n"
|
|
||||||
"Read-only gh (list, view, status, diff, checks, comments) is allowed."
|
|
||||||
)
|
|
||||||
|
|
||||||
EDIT_REDIRECT = (
|
|
||||||
"{path} is protected — settings.json, .claude/hooks/, and .git/hooks/ "
|
|
||||||
"govern the enforcement layer itself.\n"
|
|
||||||
"If a real change is needed, ask devpulse to make it directly."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _block(reason: str) -> None:
|
|
||||||
print(json.dumps({"decision": "block", "reason": reason}))
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
|
|
||||||
def _cwd_branch(cwd: str) -> str:
|
|
||||||
"""Extract AIPass branch name from CWD (src/aipass/{branch}/ pattern)."""
|
|
||||||
parts = Path(cwd).parts
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
|
|
||||||
return parts[i + 1]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _is_project_owner(cwd: str) -> bool:
|
|
||||||
"""Check if the current branch's passport has citizenship.owner: true."""
|
|
||||||
p = Path(cwd)
|
|
||||||
for d in [p] + list(p.parents):
|
|
||||||
passport = d / ".trinity" / "passport.json"
|
|
||||||
if passport.is_file():
|
|
||||||
try:
|
|
||||||
data = json.loads(passport.read_text(encoding="utf-8"))
|
|
||||||
return bool(data.get("citizenship", {}).get("owner"))
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
if (d / ".git").exists():
|
|
||||||
break
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
data = json.load(sys.stdin)
|
|
||||||
tool_name = data.get("tool_name", "")
|
|
||||||
tool_input = data.get("tool_input", {})
|
|
||||||
cwd = data.get("cwd") or os.getcwd()
|
|
||||||
|
|
||||||
if tool_name == "Bash":
|
|
||||||
cmd = tool_input.get("command", "")
|
|
||||||
if not cmd:
|
|
||||||
return
|
|
||||||
# Strip quoted strings before matching — text inside "..." or '...' is data
|
|
||||||
# (PR descriptions, commit messages, examples in docs), not code to enforce.
|
|
||||||
scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd)
|
|
||||||
scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", scan)
|
|
||||||
if (
|
|
||||||
BLOCKED_GIT_RE.search(scan)
|
|
||||||
or BLOCKED_GIT_STASH_RE.search(scan)
|
|
||||||
or BLOCKED_GIT_BRANCH_RE.search(scan)
|
|
||||||
or BLOCKED_GIT_TAG_RE.search(scan)
|
|
||||||
or BLOCKED_GIT_REMOTE_RE.search(scan)
|
|
||||||
):
|
|
||||||
_block(GIT_REDIRECT)
|
|
||||||
if BLOCKED_GH_API_RE.search(scan) or BLOCKED_GH_RE.search(scan):
|
|
||||||
if not (_cwd_branch(cwd) in TRUSTED_HOOK_EDITORS or _is_project_owner(cwd)):
|
|
||||||
_block(GH_REDIRECT)
|
|
||||||
return
|
|
||||||
|
|
||||||
if tool_name in EDIT_TOOLS:
|
|
||||||
file_path = tool_input.get("file_path") or tool_input.get("notebook_path") or ""
|
|
||||||
if not file_path:
|
|
||||||
return
|
|
||||||
for pat in BLOCKED_EDIT_PATTERNS:
|
|
||||||
if pat.search(file_path):
|
|
||||||
# Trusted-editor bypass: devpulse working from its own branch
|
|
||||||
# is the maintainer of the enforcement layer.
|
|
||||||
if _cwd_branch(cwd) in TRUSTED_HOOK_EDITORS:
|
|
||||||
return
|
|
||||||
_block(EDIT_REDIRECT.format(path=file_path))
|
|
||||||
return
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("PreToolUse", "provider", __file__, main)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Global Prompt Loader — replaces hardcoded `cat` of aipass_global_prompt.md.
|
|
||||||
|
|
||||||
Uses $AIPASS_HOME for path portability. Exits silently when CWD is inside
|
|
||||||
a project that has its own UserPromptSubmit hooks (avoids injecting the
|
|
||||||
22KB AIPass source-tree prompt into standalone projects).
|
|
||||||
|
|
||||||
Version: 1.0.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _project_has_own_hooks() -> bool:
|
|
||||||
"""Check if CWD is inside a project with its own UserPromptSubmit hooks."""
|
|
||||||
search = Path.cwd()
|
|
||||||
home = Path.home()
|
|
||||||
while search != home and search.parent != search:
|
|
||||||
settings = search / ".claude" / "settings.json"
|
|
||||||
if settings.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
||||||
ups = data.get("hooks", {}).get("UserPromptSubmit", [])
|
|
||||||
if ups:
|
|
||||||
return True
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
search = search.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if _project_has_own_hooks():
|
|
||||||
return
|
|
||||||
|
|
||||||
aipass_home = os.environ.get("AIPASS_HOME", "")
|
|
||||||
if not aipass_home:
|
|
||||||
return
|
|
||||||
|
|
||||||
prompt_file = Path(aipass_home) / ".aipass" / "aipass_global_prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
print(prompt_file.read_text(encoding="utf-8"), end="")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("UserPromptSubmit", "provider", __file__, main)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Identity Injector - Injects branch identity on every prompt.
|
|
||||||
|
|
||||||
Reads from [BRANCH].id.json and outputs core identity fields.
|
|
||||||
Finds the branch root by walking up from CWD looking for apps/ or *.id.json.
|
|
||||||
|
|
||||||
When CWD is inside a project that has its own UserPromptSubmit hooks,
|
|
||||||
this provider-level hook exits silently to avoid double-firing.
|
|
||||||
|
|
||||||
Version: 1.1.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _project_has_own_hooks() -> bool:
|
|
||||||
"""Check if CWD is inside a project with its own UserPromptSubmit hooks."""
|
|
||||||
search = Path.cwd()
|
|
||||||
home = Path.home()
|
|
||||||
while search != home and search.parent != search:
|
|
||||||
settings = search / ".claude" / "settings.json"
|
|
||||||
if settings.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
||||||
ups = data.get("hooks", {}).get("UserPromptSubmit", [])
|
|
||||||
if ups:
|
|
||||||
return True
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
search = search.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def find_repo_root() -> Path | None:
|
|
||||||
"""Find the repo root (contains pyproject.toml or .git)."""
|
|
||||||
search = Path.cwd()
|
|
||||||
while search.parent != search:
|
|
||||||
if (search / "pyproject.toml").exists() or (search / ".git").is_dir():
|
|
||||||
return search
|
|
||||||
search = search.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_branch_root() -> Path | None:
|
|
||||||
"""Find the branch root directory by walking up from CWD."""
|
|
||||||
cwd = Path.cwd()
|
|
||||||
repo_root = find_repo_root()
|
|
||||||
if not repo_root:
|
|
||||||
return None
|
|
||||||
|
|
||||||
search_path = cwd
|
|
||||||
while search_path >= repo_root:
|
|
||||||
has_trinity = (search_path / ".trinity").is_dir()
|
|
||||||
has_id = list(search_path.glob("*.id.json"))
|
|
||||||
|
|
||||||
if has_trinity or has_id:
|
|
||||||
return search_path
|
|
||||||
|
|
||||||
if search_path == repo_root:
|
|
||||||
break
|
|
||||||
search_path = search_path.parent
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_id_file(branch_root: Path) -> Path | None:
|
|
||||||
"""Find the identity file for a branch (.trinity/passport.json or *.id.json)."""
|
|
||||||
# AIPass pattern: .trinity/passport.json
|
|
||||||
passport = branch_root / ".trinity" / "passport.json"
|
|
||||||
if passport.exists():
|
|
||||||
return passport
|
|
||||||
# Dev-Pass fallback: *.id.json
|
|
||||||
id_files = list(branch_root.glob("*.id.json"))
|
|
||||||
if id_files:
|
|
||||||
return id_files[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def format_identity(data: dict) -> str:
|
|
||||||
"""Format branch_info + identity for injection."""
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# Try branch_info first (enriched passports), fall back to identity block (setup.sh passports)
|
|
||||||
branch = data.get("branch_info", {})
|
|
||||||
identity = data.get("identity", {})
|
|
||||||
name = branch.get("branch_name") or identity.get("name", "UNKNOWN")
|
|
||||||
lines.append(f"# {name} Identity")
|
|
||||||
lines.append(f"Path: {branch.get('path', 'unknown')}")
|
|
||||||
lines.append(f"Email: {branch.get('email', 'unknown')}")
|
|
||||||
|
|
||||||
identity = data.get("identity", {})
|
|
||||||
if identity.get("role"):
|
|
||||||
lines.append(f"Role: {identity['role']}")
|
|
||||||
traits = identity.get("traits") or data.get("traits")
|
|
||||||
if traits:
|
|
||||||
if isinstance(traits, list):
|
|
||||||
lines.append("Traits: " + " | ".join(traits))
|
|
||||||
else:
|
|
||||||
lines.append(f"Traits: {traits}")
|
|
||||||
if identity.get("purpose"):
|
|
||||||
lines.append(f"Purpose: {identity['purpose']}")
|
|
||||||
|
|
||||||
what_i_do = identity.get("what_i_do", [])
|
|
||||||
if what_i_do:
|
|
||||||
lines.append("Do: " + " | ".join(what_i_do[:4]))
|
|
||||||
|
|
||||||
what_i_dont_do = identity.get("what_i_dont_do", [])
|
|
||||||
if what_i_dont_do:
|
|
||||||
lines.append("Don't: " + " | ".join(what_i_dont_do[:3]))
|
|
||||||
|
|
||||||
principles = data.get("principles", [])
|
|
||||||
if principles:
|
|
||||||
lines.append("Principles: " + " * ".join(principles))
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if _project_has_own_hooks():
|
|
||||||
return
|
|
||||||
|
|
||||||
branch_root = find_branch_root()
|
|
||||||
if not branch_root:
|
|
||||||
return
|
|
||||||
|
|
||||||
id_file = find_id_file(branch_root)
|
|
||||||
if not id_file or not id_file.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(id_file.read_text(encoding="utf-8"))
|
|
||||||
output = format_identity(data)
|
|
||||||
if output:
|
|
||||||
print(f"\n{output}")
|
|
||||||
except (json.JSONDecodeError, KeyError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("UserPromptSubmit", "provider", __file__, main)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Version: 1.0.0
|
|
||||||
"""Notification Hook — Plays sound when AI needs permission."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SOUNDS_DIR = Path(__file__).parent.parent / "sounds"
|
|
||||||
SOUND_FILE = SOUNDS_DIR / "mixkit-clear-announce-tones-2861.wav"
|
|
||||||
|
|
||||||
|
|
||||||
def play_sound() -> None:
|
|
||||||
if not SOUND_FILE.exists():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
subprocess.Popen(
|
|
||||||
["aplay", "-q", str(SOUND_FILE)],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
hook_data = json.loads(sys.stdin.read())
|
|
||||||
if hook_data.get("hook_event_name") == "Notification":
|
|
||||||
play_sound()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("Notification", "provider", __file__, main)
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Pre-Compact Hook - Inject live state for post-compact recovery.
|
|
||||||
|
|
||||||
Reads STATUS.local.md, last session from local.json, and git branch
|
|
||||||
to give the model real context after compaction — not generic advice.
|
|
||||||
|
|
||||||
Version: 3.0.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _find_branch_dir():
|
|
||||||
"""Find the current branch directory from CWD."""
|
|
||||||
cwd = Path.cwd()
|
|
||||||
|
|
||||||
# Check if we're in a branch dir or subdirectory of one
|
|
||||||
# Pattern: .../src/aipass/{branch}/...
|
|
||||||
parts = cwd.parts
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if part == "aipass" and i > 0 and parts[i - 1] == "src":
|
|
||||||
branch_dir = Path(*parts[: i + 2])
|
|
||||||
if branch_dir.is_dir():
|
|
||||||
return branch_dir
|
|
||||||
|
|
||||||
# Check if CWD itself has .trinity/
|
|
||||||
if (cwd / ".trinity").is_dir():
|
|
||||||
return cwd
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _read_status_local(branch_dir):
|
|
||||||
"""Read STATUS.local.md if it exists."""
|
|
||||||
for name in ["STATUS.local.md", "dev.local.md"]:
|
|
||||||
path = branch_dir / name
|
|
||||||
if path.is_file():
|
|
||||||
try:
|
|
||||||
return path.read_text(encoding="utf-8")[:3000]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _read_last_session(branch_dir):
|
|
||||||
"""Read the most recent session and key_learnings from local.json."""
|
|
||||||
local_path = branch_dir / ".trinity" / "local.json"
|
|
||||||
if not local_path.is_file():
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(local_path.read_text(encoding="utf-8"))
|
|
||||||
result = []
|
|
||||||
|
|
||||||
# Last session
|
|
||||||
sessions = data.get("sessions", [])
|
|
||||||
if sessions:
|
|
||||||
last = sessions[0]
|
|
||||||
result.append(
|
|
||||||
f"Last session (#{last.get('session_number', '?')}, "
|
|
||||||
f"{last.get('date', '?')}): {last.get('summary', 'no summary')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Key learnings (just the keys, not full values — breadcrumbs)
|
|
||||||
learnings = data.get("key_learnings", {})
|
|
||||||
if learnings:
|
|
||||||
keys = list(learnings.keys())[-10:] # last 10
|
|
||||||
result.append(f"Key learnings available: {', '.join(keys)}")
|
|
||||||
|
|
||||||
return "\n".join(result) if result else None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_git_info():
|
|
||||||
"""Get current git branch and short status."""
|
|
||||||
try:
|
|
||||||
branch = subprocess.run(
|
|
||||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
)
|
|
||||||
status = subprocess.run(
|
|
||||||
["git", "diff", "--stat", "--cached", "HEAD"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
)
|
|
||||||
dirty = subprocess.run(
|
|
||||||
["git", "status", "--porcelain"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
if branch.returncode == 0:
|
|
||||||
result.append(f"Git branch: {branch.stdout.strip()}")
|
|
||||||
if dirty.returncode == 0 and dirty.stdout.strip():
|
|
||||||
lines = dirty.stdout.strip().split("\n")
|
|
||||||
result.append(f"Uncommitted changes: {len(lines)} files")
|
|
||||||
|
|
||||||
return "\n".join(result) if result else None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_branch_name(branch_dir):
|
|
||||||
"""Extract branch name from directory."""
|
|
||||||
return branch_dir.name if branch_dir else "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main hook entry point."""
|
|
||||||
try:
|
|
||||||
json.load(sys.stdin)
|
|
||||||
|
|
||||||
branch_dir = _find_branch_dir()
|
|
||||||
branch_name = _get_branch_name(branch_dir)
|
|
||||||
|
|
||||||
sections = []
|
|
||||||
|
|
||||||
sections.append(f"""POST-COMPACT RECOVERY — @{branch_name}
|
|
||||||
|
|
||||||
Context just compacted. Below is your live state. Use it to continue seamlessly.""")
|
|
||||||
|
|
||||||
# Git info
|
|
||||||
git_info = _get_git_info()
|
|
||||||
if git_info:
|
|
||||||
sections.append(f"## Git\n{git_info}")
|
|
||||||
|
|
||||||
# Last session from local.json
|
|
||||||
if branch_dir:
|
|
||||||
session_info = _read_last_session(branch_dir)
|
|
||||||
if session_info:
|
|
||||||
sections.append(f"## Last Session\n{session_info}")
|
|
||||||
|
|
||||||
# STATUS.local.md — the main context
|
|
||||||
if branch_dir:
|
|
||||||
status = _read_status_local(branch_dir)
|
|
||||||
if status:
|
|
||||||
sections.append(f"## STATUS.local.md\n{status}")
|
|
||||||
|
|
||||||
# Recovery instructions (lean)
|
|
||||||
sections.append("""## Recovery Protocol
|
|
||||||
- Continue where the summary left off — don't restart or ask generic questions
|
|
||||||
- .trinity/local.json has full session history and key_learnings — read it if you need more context
|
|
||||||
- STATUS.local.md has current work, known issues, and todos
|
|
||||||
- Save memories proactively — compaction just proved you need to
|
|
||||||
- Match the conversation tone from before compaction""")
|
|
||||||
|
|
||||||
print("\n\n".join(sections), file=sys.stdout)
|
|
||||||
print("Pre-compact: live state injected", file=sys.stderr)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Fail silently — never block compaction
|
|
||||||
print(f"Pre-compact hook error: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("PreCompact", "provider", __file__, main)
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
PreToolUse Gate — Blocks unsafe edits at the hook layer.
|
|
||||||
|
|
||||||
Rules (checked in order):
|
|
||||||
1. Inbox lock — any write targeting *.ai_mail.local/inbox.json is BLOCKED.
|
|
||||||
Use `drone @ai_mail email` instead.
|
|
||||||
2. Cross-branch — writes to src/aipass/X/** from a CWD inside src/aipass/Y/**
|
|
||||||
are BLOCKED unless the calling branch is in TRUSTED_CROSS_WRITERS.
|
|
||||||
3. State-file — edits to OTHER .py files while the current branch has unresolved
|
|
||||||
type errors are BLOCKED. (original v1.2.0 logic)
|
|
||||||
|
|
||||||
Track E additions: rules 1 + 2 (DPLAN-0139).
|
|
||||||
Version: 1.3.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
STATE_FILE = Path(__file__).parent / ".diagnostics_state.json"
|
|
||||||
EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
|
|
||||||
|
|
||||||
# Single source of truth lives in permissions.py — inline here as fallback
|
|
||||||
# so the hook works even when aipass package is not on sys.path.
|
|
||||||
TRUSTED_CROSS_WRITERS: tuple[str, ...] = ("devpulse", "seedgo", "spawn")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_branch(file_path: str) -> str:
|
|
||||||
"""Extract AIPass branch name from a file path (src/aipass/{branch}/ pattern)."""
|
|
||||||
parts = Path(file_path).parts
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
|
|
||||||
return parts[i + 1]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _block(reason: str) -> None:
|
|
||||||
# codeql[py/clear-text-logging-sensitive-data]
|
|
||||||
print(json.dumps({"decision": "block", "reason": reason}))
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
input_data = json.load(sys.stdin)
|
|
||||||
tool_name = input_data.get("tool_name", "")
|
|
||||||
tool_input = input_data.get("tool_input", {})
|
|
||||||
file_path = tool_input.get("file_path", "")
|
|
||||||
|
|
||||||
if tool_name not in EDIT_TOOLS:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Rule 1: Inbox lock — block all writes to *.ai_mail.local/inbox.json
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
fp = Path(file_path)
|
|
||||||
if fp.name == "inbox.json" and ".ai_mail.local" in fp.parts:
|
|
||||||
_block('Direct writes to inbox.json are blocked.\nUse: drone @ai_mail email @<branch> "Subject" "Body"')
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Rule 1.5: Dispatched-agent path confinement (DPLAN-0155 M3)
|
|
||||||
# Daemon-spawned agents can only write inside their own branch dir.
|
|
||||||
# Breaks the prompt-injection amplifier chain — even if injected,
|
|
||||||
# a dispatched agent cannot write to other agents' inboxes or code.
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
cwd = input_data.get("cwd", "") or os.getcwd()
|
|
||||||
cwd_branch = _get_branch(cwd)
|
|
||||||
|
|
||||||
session_type = os.environ.get("AIPASS_SESSION_TYPE", "interactive")
|
|
||||||
if session_type == "daemon" and cwd_branch:
|
|
||||||
target_branch = _get_branch(str(fp.resolve()) if not fp.is_absolute() else str(fp))
|
|
||||||
if target_branch and target_branch != cwd_branch:
|
|
||||||
_block(
|
|
||||||
f"Dispatched agent confined to own branch: '{cwd_branch}' "
|
|
||||||
f"cannot write to '{target_branch}' in daemon mode."
|
|
||||||
)
|
|
||||||
repo_root = None
|
|
||||||
for parent in Path(cwd).parents:
|
|
||||||
if (parent / ".git").exists():
|
|
||||||
repo_root = parent
|
|
||||||
break
|
|
||||||
if repo_root and not target_branch:
|
|
||||||
allowed_prefix = str(repo_root / "src" / "aipass" / cwd_branch)
|
|
||||||
resolved = str(fp.resolve()) if not fp.is_absolute() else str(fp)
|
|
||||||
if not resolved.startswith(allowed_prefix):
|
|
||||||
_block(f"Dispatched agent restricted to {allowed_prefix}. Cannot write to: {file_path}")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Rule 2: Cross-branch write enforcement
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
target_branch = _get_branch(str(fp.resolve()) if not fp.is_absolute() else str(fp))
|
|
||||||
|
|
||||||
if cwd_branch and target_branch and cwd_branch != target_branch:
|
|
||||||
if cwd_branch not in TRUSTED_CROSS_WRITERS:
|
|
||||||
_block(
|
|
||||||
f"Cross-branch write blocked: '{cwd_branch}' cannot write to '{target_branch}'.\n"
|
|
||||||
f"Trusted cross-writers: {', '.join(TRUSTED_CROSS_WRITERS)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Rule 3: State-file (original v1.2.0) — .py files only
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
if not file_path.endswith(".py"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not STATE_FILE.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
state = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
|
||||||
except (json.JSONDecodeError, IOError):
|
|
||||||
return
|
|
||||||
|
|
||||||
errored_file = state.get("file", "")
|
|
||||||
errors = state.get("errors", [])
|
|
||||||
|
|
||||||
if not errors:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
current = str(Path(file_path).resolve())
|
|
||||||
errored = str(Path(errored_file).resolve())
|
|
||||||
except (OSError, ValueError):
|
|
||||||
return
|
|
||||||
|
|
||||||
if current == errored:
|
|
||||||
return
|
|
||||||
|
|
||||||
current_branch = _get_branch(current)
|
|
||||||
errored_branch = _get_branch(errored)
|
|
||||||
if not errored_branch:
|
|
||||||
return
|
|
||||||
if current_branch and errored_branch and current_branch != errored_branch:
|
|
||||||
return
|
|
||||||
|
|
||||||
error_summary = "\n".join(f" L{e['line']}: {e['message']}" for e in errors[:5])
|
|
||||||
_block(f"Fix {len(errors)} error(s) in {Path(errored_file).name} before editing other files:\n{error_summary}")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass # Silent fail → allow
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
> **Note:** The probe suite predates the `hook_log.py` always-on logger (S132, DPLAN-0167).
|
> **Note:** The probe suite predates the `hook_log.py` always-on logger (S132, DPLAN-0167).
|
||||||
> For most hook debugging, use `hook_report.py` and `hook_test.py` in the parent directory
|
> For most hook debugging, use `hook_report.py` and `hook_test.py` in the parent directory
|
||||||
> instead — they cover all hooks automatically without manual wiring. The probes below remain
|
> instead -- they cover all hooks automatically without manual wiring. The probes below remain
|
||||||
> useful for one-off event investigation when you need to enable/disable individual events.
|
> useful for one-off event investigation when you need to enable/disable individual events.
|
||||||
|
|
||||||
|
> **Post-migration note (DPLAN-0184):** Production hooks now route through the bridge at
|
||||||
|
> `src/aipass/hooks/apps/handlers/bridges/claude.py`. Probes are independent of the bridge
|
||||||
|
> pipeline -- they wire directly into `~/.claude/settings.json` as standalone commands.
|
||||||
|
> The wiring examples below still work as-is.
|
||||||
|
|
||||||
This directory contains ping-response probe scripts for each Claude Code hook event type.
|
This directory contains ping-response probe scripts for each Claude Code hook event type.
|
||||||
Probes are **opt-in** — they are never auto-wired. See below for how to enable them.
|
Probes are **opt-in** -- they are never auto-wired. See below for how to enable them.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,7 +19,7 @@ Probes are **opt-in** — they are never auto-wired. See below for how to enable
|
|||||||
|
|
||||||
Each `probe_*.py` script in this directory is a passive observer for one Claude Code hook event.
|
Each `probe_*.py` script in this directory is a passive observer for one Claude Code hook event.
|
||||||
When enabled in `settings.json`, a probe fires on its event, records a structured entry to
|
When enabled in `settings.json`, a probe fires on its event, records a structured entry to
|
||||||
`last_ping.jsonl`, and exits 0 immediately — it never blocks execution.
|
`last_ping.jsonl`, and exits 0 immediately -- it never blocks execution.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,32 +39,32 @@ When enabled in `settings.json`, a probe fires on its event, records a structure
|
|||||||
|
|
||||||
## How to enable probes (settings.json snippets)
|
## How to enable probes (settings.json snippets)
|
||||||
|
|
||||||
Add any subset of the following to your `.claude/settings.json` `hooks` object.
|
Add any subset of the following to your `~/.claude/settings.json` `hooks` object.
|
||||||
**Replace `/path/to/AIPass` with your actual repo root.**
|
Use `$AIPASS_HOME` (set by provider settings) or replace with your actual repo root.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_pre_tool_use.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_pre_tool_use.py"}]}
|
||||||
],
|
],
|
||||||
"PostToolUse": [
|
"PostToolUse": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_post_tool_use.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_post_tool_use.py"}]}
|
||||||
],
|
],
|
||||||
"UserPromptSubmit": [
|
"UserPromptSubmit": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_user_prompt_submit.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_user_prompt_submit.py"}]}
|
||||||
],
|
],
|
||||||
"SubagentStop": [
|
"SubagentStop": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_subagent_stop.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_subagent_stop.py"}]}
|
||||||
],
|
],
|
||||||
"PreCompact": [
|
"PreCompact": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_pre_compact.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_pre_compact.py"}]}
|
||||||
],
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_stop.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_stop.py"}]}
|
||||||
],
|
],
|
||||||
"Notification": [
|
"Notification": [
|
||||||
{"hooks": [{"type": "command", "command": "python3 /path/to/AIPass/.claude/hooks/probes/probe_notification.py"}]}
|
{"hooks": [{"type": "command", "command": "python3 $AIPASS_HOME/.claude/hooks/probes/probe_notification.py"}]}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +107,7 @@ drone @seedgo hooks probe --matrix
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `last_ping.jsonl` is gitignored — it is a live log file, not source.
|
- `last_ping.jsonl` is gitignored -- it is a live log file, not source.
|
||||||
- Probes are opt-in. The AIPass repo does **not** auto-wire them into `settings.json`.
|
- Probes are opt-in. The AIPass repo does **not** auto-wire them into `settings.json`.
|
||||||
- Each probe script contains its own `settings.json` snippet in its module docstring.
|
- Each probe script contains its own `settings.json` snippet in its module docstring.
|
||||||
- Probes are pure stdlib Python — no aipass imports, no third-party packages.
|
- Probes are pure stdlib Python -- no aipass imports, no third-party packages.
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# AIPass Prompt Inject — Called by the global project_bridge.sh
|
|
||||||
# Runs all AIPass-specific UserPromptSubmit hooks.
|
|
||||||
# $1 = repo root path (passed by bridge)
|
|
||||||
|
|
||||||
REPO="${1:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
||||||
[ -z "$REPO" ] && exit 0
|
|
||||||
|
|
||||||
# 1. Global prompt
|
|
||||||
cat "$REPO/.aipass/aipass_global_prompt.md" 2>/dev/null
|
|
||||||
|
|
||||||
# 2. Branch prompt loader
|
|
||||||
python3 "$REPO/.claude/hooks/branch_prompt_loader.py" 2>/dev/null
|
|
||||||
|
|
||||||
# 3. Identity injector
|
|
||||||
python3 "$REPO/.claude/hooks/identity_injector.py" 2>/dev/null
|
|
||||||
|
|
||||||
# 4. Email notification
|
|
||||||
python3 "$REPO/.claude/hooks/email_notification.py" 2>/dev/null
|
|
||||||
|
|
||||||
# 5. Secret prompt (devpulse only — gitignored, silent when missing)
|
|
||||||
case "$PWD" in
|
|
||||||
*devpulse*) cat "$REPO/src/aipass/devpulse/.devpulse_secret.md" 2>/dev/null || true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Version: 1.0.0
|
|
||||||
"""Stop Hook — Plays achievement bell when AI finishes responding."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SOUNDS_DIR = Path(__file__).parent.parent / "sounds"
|
|
||||||
SOUND_FILE = SOUNDS_DIR / "mixkit-achievement-bell-600.wav"
|
|
||||||
|
|
||||||
|
|
||||||
def play_sound() -> None:
|
|
||||||
if not SOUND_FILE.exists():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
subprocess.Popen(
|
|
||||||
["aplay", "-q", str(SOUND_FILE)],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
hook_data = json.loads(sys.stdin.read())
|
|
||||||
if hook_data.get("hook_event_name") == "Stop":
|
|
||||||
if not hook_data.get("stop_hook_active", False):
|
|
||||||
play_sound()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("Stop", "provider", __file__, main)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
SubagentStop Gate — Checks files modified by subagents before allowing them to finish.
|
|
||||||
|
|
||||||
Runs seedgo checklist + basic validation on any .py files the subagent touched.
|
|
||||||
If violations found, blocks the stop and tells the subagent to fix them.
|
|
||||||
|
|
||||||
Version: 1.0.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _find_repo_root() -> Path | None:
|
|
||||||
"""Walk up from CWD or AIPASS_HOME to find the git repo root."""
|
|
||||||
for start in (os.environ.get("AIPASS_HOME", ""), os.getcwd()):
|
|
||||||
p = Path(start)
|
|
||||||
while p != p.parent:
|
|
||||||
if (p / ".git").exists():
|
|
||||||
return p
|
|
||||||
p = p.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
AIPASS_ROOT = _find_repo_root()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cwd_branch() -> str | None:
|
|
||||||
"""Detect which branch directory (src/aipass/<name>) the CWD is in."""
|
|
||||||
cwd = Path.cwd().resolve()
|
|
||||||
if AIPASS_ROOT is None:
|
|
||||||
return None
|
|
||||||
src = AIPASS_ROOT / "src" / "aipass"
|
|
||||||
try:
|
|
||||||
rel = cwd.relative_to(src)
|
|
||||||
return rel.parts[0] if rel.parts else None
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_modified_py_files() -> list[str]:
|
|
||||||
"""Get Python files modified in the working tree, scoped to the CWD branch.
|
|
||||||
|
|
||||||
Only returns files inside the current branch's directory (or repo-root files).
|
|
||||||
This prevents dispatched agents' changes from triggering violations on the
|
|
||||||
orchestrator or other agents sharing the worktree.
|
|
||||||
"""
|
|
||||||
if AIPASS_ROOT is None:
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "diff", "--name-only", "HEAD"], capture_output=True, text=True, timeout=5, cwd=str(AIPASS_ROOT)
|
|
||||||
)
|
|
||||||
cwd_branch = _get_cwd_branch()
|
|
||||||
files = []
|
|
||||||
for line in result.stdout.strip().split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if line.endswith(".py") and not line.startswith(".claude/"):
|
|
||||||
if cwd_branch and line.startswith("src/aipass/"):
|
|
||||||
file_branch = line.split("/")[2] if len(line.split("/")) > 2 else None
|
|
||||||
if file_branch and file_branch != cwd_branch:
|
|
||||||
continue
|
|
||||||
full = AIPASS_ROOT / line
|
|
||||||
if full.exists():
|
|
||||||
files.append(str(full))
|
|
||||||
return files
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def run_seedgo_checklist(file_path: str) -> list[str]:
|
|
||||||
"""Run seedgo checklist on a single file."""
|
|
||||||
if AIPASS_ROOT is None:
|
|
||||||
return []
|
|
||||||
if "/.claude/" in file_path:
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["drone", "@seedgo", "checklist", file_path],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=15,
|
|
||||||
cwd=str(AIPASS_ROOT),
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return []
|
|
||||||
violations = []
|
|
||||||
for line in result.stdout.split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("\u2717"):
|
|
||||||
v = line[1:].strip()
|
|
||||||
if v:
|
|
||||||
violations.append(v)
|
|
||||||
return violations[:5]
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def check_hook_readme_accountability() -> str | None:
|
|
||||||
"""Check if hook files changed but README wasn't updated. Returns reminder or None."""
|
|
||||||
if AIPASS_ROOT is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "diff", "--name-only", "HEAD"], capture_output=True, text=True, timeout=5, cwd=str(AIPASS_ROOT)
|
|
||||||
)
|
|
||||||
changed = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
|
||||||
|
|
||||||
hook_files_changed = any(f.startswith(".claude/hooks/") and f.endswith(".py") for f in changed)
|
|
||||||
readme_changed = ".claude/hooks/README.md" in changed
|
|
||||||
|
|
||||||
if hook_files_changed and not readme_changed:
|
|
||||||
return (
|
|
||||||
"Hook files were modified but .claude/hooks/README.md was not updated. "
|
|
||||||
"Consider updating the README to reflect your changes."
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
json.load(sys.stdin)
|
|
||||||
|
|
||||||
modified = get_modified_py_files()
|
|
||||||
if not modified:
|
|
||||||
return # Nothing to check
|
|
||||||
|
|
||||||
readme_reminder = check_hook_readme_accountability()
|
|
||||||
|
|
||||||
all_violations = {}
|
|
||||||
for f in modified:
|
|
||||||
vs = run_seedgo_checklist(f)
|
|
||||||
if vs:
|
|
||||||
name = Path(f).name
|
|
||||||
all_violations[name] = vs
|
|
||||||
|
|
||||||
if all_violations:
|
|
||||||
# Build the block reason
|
|
||||||
lines = ["Standards violations found in files you modified:\n"]
|
|
||||||
for fname, vs in all_violations.items():
|
|
||||||
lines.append(f" {fname}:")
|
|
||||||
for v in vs:
|
|
||||||
lines.append(f" - {v}")
|
|
||||||
lines.append("\nFix these violations before finishing.")
|
|
||||||
|
|
||||||
if readme_reminder:
|
|
||||||
lines.append(f"\n⚠️ {readme_reminder}")
|
|
||||||
|
|
||||||
output = {"decision": "block", "reason": "\n".join(lines)}
|
|
||||||
print(json.dumps(output))
|
|
||||||
elif readme_reminder:
|
|
||||||
output = {"decision": "allow", "reason": f"⚠️ {readme_reminder}"}
|
|
||||||
print(json.dumps(output))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass # Silent fail — don't block on errors
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("SubagentStop", "provider", __file__, main)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Version: 1.0.0
|
|
||||||
"""Tool Use Hook — Plays key press sound when AI uses tools."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SOUNDS_DIR = Path(__file__).parent.parent / "sounds"
|
|
||||||
SOUND_FILE = SOUNDS_DIR / "mixkit-atm-cash-machine-key-press-2841.wav"
|
|
||||||
|
|
||||||
SOUND_TOOLS = ["Bash", "Edit", "MultiEdit", "Write", "Read", "Grep", "Glob"]
|
|
||||||
|
|
||||||
|
|
||||||
def play_sound() -> None:
|
|
||||||
if not SOUND_FILE.exists():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
subprocess.Popen(
|
|
||||||
["aplay", "-q", str(SOUND_FILE)],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
hook_data = json.loads(sys.stdin.read())
|
|
||||||
if hook_data.get("hook_event_name") == "PreToolUse":
|
|
||||||
if hook_data.get("tool_name", "") in SOUND_TOOLS:
|
|
||||||
play_sound()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
||||||
from hook_log import run_and_log
|
|
||||||
|
|
||||||
run_and_log("PreToolUse", "provider", __file__, main)
|
|
||||||
@@ -4,20 +4,26 @@
|
|||||||
"cli": {
|
"cli": {
|
||||||
"claude": {
|
"claude": {
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{"script": "global_prompt_loader.py", "event": "UserPromptSubmit", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:presence_gate", "event": "UserPromptSubmit"},
|
||||||
{"script": "branch_prompt_loader.py", "event": "UserPromptSubmit", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:persistent_alert", "event": "UserPromptSubmit"},
|
||||||
{"script": "identity_injector.py", "event": "UserPromptSubmit", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:tier0_kernel", "event": "UserPromptSubmit"},
|
||||||
{"script": "email_notification.py", "event": "UserPromptSubmit", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:navmap", "event": "UserPromptSubmit"},
|
||||||
{"script": "tool_use_sound.py", "event": "PreToolUse", "matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:branch_prompt", "event": "UserPromptSubmit"},
|
||||||
{"script": "pre_edit_gate.py", "event": "PreToolUse", "matcher": "Edit|MultiEdit|Write|NotebookEdit", "source": "user"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:identity_injector", "event": "UserPromptSubmit"},
|
||||||
{"script": "git_gate.py", "event": "PreToolUse", "matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit", "source": "user"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:email_notification", "event": "UserPromptSubmit"},
|
||||||
{"script": "auto_fix_diagnostics.py", "event": "PostToolUse", "matcher": "Edit|MultiEdit|Write|NotebookEdit", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:compass_recall", "event": "UserPromptSubmit"},
|
||||||
{"script": "auto_watchdog.py", "event": "PostToolUse", "matcher": "Bash", "source": "user"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:feedback_pulse", "event": "UserPromptSubmit"},
|
||||||
{"script": "subagent_stop_gate.py", "event": "SubagentStop", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:auto_process", "event": "UserPromptSubmit"},
|
||||||
{"script": "stop_sound.py", "event": "Stop", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py UserPromptSubmit:user_message_relay", "event": "UserPromptSubmit"},
|
||||||
{"script": "notification_sound.py", "event": "Notification", "source": "repo"},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreToolUse", "event": "PreToolUse", "matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task"},
|
||||||
{"script": "pre_compact.py", "event": "PreCompact", "matcher": "manual", "source": "repo", "timeout": 60},
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PostToolUse", "event": "PostToolUse", "matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit"},
|
||||||
{"script": "pre_compact.py", "event": "PreCompact", "matcher": "auto", "source": "repo", "timeout": 60}
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py SubagentStop", "event": "SubagentStop"},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Stop", "event": "Stop"},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py Notification", "event": "Notification"},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact", "event": "PreCompact", "matcher": "manual", "timeout": 60},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact", "event": "PreCompact", "matcher": "auto", "timeout": 60},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact_rollover", "event": "PreCompact", "matcher": "manual", "timeout": 120},
|
||||||
|
{"command": "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py PreCompact:pre_compact_rollover", "event": "PreCompact", "matcher": "auto", "timeout": 120}
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"AIPASS_HOME": "{{REPO_ROOT}}",
|
"AIPASS_HOME": "{{REPO_ROOT}}",
|
||||||
|
|||||||
+3
-23
@@ -10,34 +10,14 @@
|
|||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"EnterPlanMode",
|
"EnterPlanMode",
|
||||||
"Bash(git add*)",
|
"Bash(git *)",
|
||||||
"Bash(git commit*)",
|
|
||||||
"Bash(git push*)",
|
|
||||||
"Bash(git pull*)",
|
|
||||||
"Bash(git merge*)",
|
|
||||||
"Bash(git rebase*)",
|
|
||||||
"Bash(git reset*)",
|
|
||||||
"Bash(git checkout*)",
|
|
||||||
"Bash(git switch*)",
|
|
||||||
"Bash(git branch*)",
|
|
||||||
"Bash(git cherry-pick*)",
|
|
||||||
"Bash(git stash*)",
|
|
||||||
"Bash(git tag*)",
|
|
||||||
"Bash(git revert*)",
|
|
||||||
"Bash(git rm*)",
|
|
||||||
"Bash(git mv*)",
|
|
||||||
"Bash(git clean*)",
|
|
||||||
"Bash(git restore*)",
|
|
||||||
"Bash(gh pr *)",
|
|
||||||
"Bash(gh issue *)",
|
|
||||||
"Bash(gh repo *)",
|
|
||||||
"Bash(gh api *)",
|
|
||||||
"Read(/home/patrick/Patrick-Personal/**)",
|
"Read(/home/patrick/Patrick-Personal/**)",
|
||||||
"Edit(/home/patrick/Patrick-Personal/**)",
|
"Edit(/home/patrick/Patrick-Personal/**)",
|
||||||
"Write(/home/patrick/Patrick-Personal/**)",
|
"Write(/home/patrick/Patrick-Personal/**)",
|
||||||
"Glob(/home/patrick/Patrick-Personal/**)",
|
"Glob(/home/patrick/Patrick-Personal/**)",
|
||||||
"Grep(/home/patrick/Patrick-Personal/**)",
|
"Grep(/home/patrick/Patrick-Personal/**)",
|
||||||
"Bash(*Patrick-Personal*)"
|
"Bash(*Patrick-Personal*)",
|
||||||
|
"Bash(drone @git checkout main)"
|
||||||
],
|
],
|
||||||
"defaultMode": "acceptEdits"
|
"defaultMode": "acceptEdits"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -14,9 +14,8 @@ Purpose: Update branch memory files after completing work this session.
|
|||||||
Each memory file plays a distinct role. Update based on what actually changed this session.
|
Each memory file plays a distinct role. Update based on what actually changed this session.
|
||||||
|
|
||||||
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it.
|
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it.
|
||||||
- **`.trinity/local.json`** — YOUR MEMORY. Session history and key_learnings. Add a session entry for significant work. Add key_learnings for facts you'd need next time. Trim oldest sessions if over 20.
|
- **`.trinity/local.json`** — YOUR MEMORY. Add a session entry for significant work; add key_learnings for facts you'd need next time. **Todos: add what you parked, and DELETE every todo you finished this session** — the proof goes in the session entry, not the todo. Rollover never trims todos (they're operational), so done ones you leave behind resurface as "open" next load and you waste time re-confirming them. (Sessions/key_learnings DO auto-roll by number — don't hand-trim those.)
|
||||||
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session.
|
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session.
|
||||||
- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate and drop quick notes in the Notepad section.
|
|
||||||
|
|
||||||
## If Relevant
|
## If Relevant
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Codex SessionStart hook: inject AIPass identity context.
|
"""Codex SessionStart hook: inject AIPass identity context.
|
||||||
|
|
||||||
Reads .trinity/passport.json and branch prompt, outputs Codex-format JSON
|
Reads tier0_kernel + tier1_navmap (same source as Claude Code tiers),
|
||||||
with additionalContext for identity injection.
|
passport identity, and branch prompt. Outputs Codex-format JSON with
|
||||||
|
additionalContext. Codex fires once at SessionStart — no per-turn cadence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -36,9 +37,9 @@ def get_branch_from_cwd(repo_root):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
input_data = json.loads(sys.stdin.read())
|
json.loads(sys.stdin.read())
|
||||||
except Exception:
|
except Exception:
|
||||||
input_data = {}
|
pass
|
||||||
|
|
||||||
repo_root = find_repo_root()
|
repo_root = find_repo_root()
|
||||||
if not repo_root:
|
if not repo_root:
|
||||||
@@ -47,10 +48,13 @@ def main():
|
|||||||
|
|
||||||
context_parts = []
|
context_parts = []
|
||||||
|
|
||||||
# 1. Global prompt
|
# 1. Tiered prompts (same source as Claude Code tiers)
|
||||||
global_prompt = repo_root / ".aipass" / "aipass_global_prompt.md"
|
tier0 = repo_root / ".aipass" / "tier0_kernel.md"
|
||||||
if global_prompt.exists():
|
if tier0.exists():
|
||||||
context_parts.append(global_prompt.read_text(encoding="utf-8")[:8000])
|
context_parts.append(tier0.read_text(encoding="utf-8")[:2500])
|
||||||
|
tier1 = repo_root / ".aipass" / "tier1_navmap.md"
|
||||||
|
if tier1.exists():
|
||||||
|
context_parts.append(tier1.read_text(encoding="utf-8")[:8000])
|
||||||
|
|
||||||
# 2. Branch identity
|
# 2. Branch identity
|
||||||
branch = get_branch_from_cwd(repo_root)
|
branch = get_branch_from_cwd(repo_root)
|
||||||
@@ -81,12 +85,7 @@ def main():
|
|||||||
|
|
||||||
if context_parts:
|
if context_parts:
|
||||||
context = "\n\n---\n\n".join(context_parts)
|
context = "\n\n---\n\n".join(context_parts)
|
||||||
output = {
|
output = {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": context}}
|
||||||
"hookSpecificOutput": {
|
|
||||||
"hookEventName": "SessionStart",
|
|
||||||
"additionalContext": context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
output = {}
|
output = {}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ Purpose: Update branch memory files after completing work this session.
|
|||||||
|
|
||||||
### Always
|
### Always
|
||||||
|
|
||||||
- **.trinity/local.json** — Add new session entry to `sessions` if significant work was done. Add new `key_learnings` for facts you'd need next time. Trim oldest sessions if over 20.
|
- **.trinity/local.json** — Add a session entry to `sessions` if significant work was done; add `key_learnings` for facts you'd need next time. **Todos: add what you parked, and DELETE every todo you finished this session** — the proof goes in the session entry, not the todo. Rollover never trims todos (they're operational), so done ones you leave behind resurface as "open" next load and you waste time re-confirming them.
|
||||||
- **.trinity/observations.json** — Add notable collaboration insights: breakthrough moments, pattern corrections, flow states, friction points, preference discoveries. Skip if nothing notable this session.
|
- **.trinity/observations.json** — Add notable collaboration insights: breakthrough moments, pattern corrections, flow states, friction points, preference discoveries. Skip if nothing notable this session.
|
||||||
|
|
||||||
|
### Entry shape — one rule for all four types
|
||||||
|
|
||||||
|
`key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) all share ONE shape: a **list of objects, newest at the top (index 0)**. Every entry carries a **`number`** (monotonic int per type — highest = newest, never reused; new = current max + 1) and a **`date`** (ISO), plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`.
|
||||||
|
|
||||||
|
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* to @memory automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (delete finished todos, see above).
|
||||||
|
|
||||||
### If Relevant
|
### If Relevant
|
||||||
|
|
||||||
- **.trinity/passport.json** — Evolve identity when the branch's role, capabilities, or principles have genuinely changed. Don't update just to update — but don't leave placeholders forever either.
|
- **.trinity/passport.json** — Evolve identity when the branch's role, capabilities, or principles have genuinely changed. Don't update just to update — but don't leave placeholders forever either.
|
||||||
- **README.md** — Does it reflect current state? Update if stale.
|
- **README.md** — Does it reflect current state? Update if stale.
|
||||||
- **STATUS.local.md** — Drop quick notes on issues, todos, or ideas in the Notepad section.
|
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ Purpose: Button up everything at the end of a session — or before a /compact.
|
|||||||
|
|
||||||
## 1. Memories
|
## 1. Memories
|
||||||
|
|
||||||
- **.trinity/local.json** — Add/update session entry with summary of work done. Add new key_learnings for anything learned this session. Trim oldest sessions if over 20.
|
- **.trinity/local.json** — Add/update session entry with summary of work done. Add new key_learnings for anything learned this session.
|
||||||
- **.trinity/observations.json** — Add collaboration insights if anything notable happened. Skip if nothing new.
|
- **.trinity/observations.json** — Add collaboration insights if anything notable happened. Skip if nothing new.
|
||||||
- **.trinity/passport.json** — Only update if role/purpose/principles genuinely changed this session.
|
- **.trinity/passport.json** — Only update if role/purpose/principles genuinely changed this session.
|
||||||
|
|
||||||
|
**Entry shape — one rule for all four types:** `key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) are all **lists, newest at top (index 0)**. Every entry carries a **`number`** (monotonic int per type — highest = newest, never reused; new = current max + 1) and a **`date`** (ISO), plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`. Stamp `number` + `date` and **prepend**; **don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (see Reconcile).
|
||||||
|
|
||||||
|
**Reconcile todos — verify against reality, don't trust the label.** Stored status drifts (a todo finished a past session often never got closed). Audit every **open** todo against the actual system: file/dir still there? code path in or out? README says what it claims? audit passes? **Close what's verifiably done** → note it in the session entry, then **DELETE the todo from the array** (rollover never trims todos — they're operational — so done items left as `status: done` pile up and go stale across chats), **re-scope** partials, **leave** deferred/pending-decision ones open. Fail honestly — remove only on evidence, never to tidy the list. Use `ls`/`find`/`git ls-files`/`grep`/`drone @seedgo audit`, not assumptions.
|
||||||
|
|
||||||
## 2. Active Plans
|
## 2. Active Plans
|
||||||
|
|
||||||
- Check any DPLANs or FPLANs referenced in this session
|
- Check any DPLANs or FPLANs referenced in this session
|
||||||
@@ -38,7 +42,7 @@ Purpose: Button up everything at the end of a session — or before a /compact.
|
|||||||
## 5. Loose Ends
|
## 5. Loose Ends
|
||||||
|
|
||||||
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
|
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
|
||||||
- If anything can't survive compaction, write it to STATUS.local.md Notepad
|
- If anything can't survive compaction, write it to local.json todos[]
|
||||||
|
|
||||||
## Confirm
|
## Confirm
|
||||||
|
|
||||||
@@ -46,6 +50,7 @@ List everything updated. Format:
|
|||||||
```
|
```
|
||||||
Prep complete:
|
Prep complete:
|
||||||
- local.json: [what was added]
|
- local.json: [what was added]
|
||||||
|
- Todos: [reconciled vs reality — N done & removed, M re-scoped, K still open]
|
||||||
- observations.json: [updated / skipped]
|
- observations.json: [updated / skipped]
|
||||||
- Plans: [which ones updated]
|
- Plans: [which ones updated]
|
||||||
- Git: [branch, uncommitted count, suggestion]
|
- Git: [branch, uncommitted count, suggestion]
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Gemini BeforeTool hook: gate file edits to protect critical files."""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
PROTECTED_PATTERNS = [
|
|
||||||
".trinity/passport.json",
|
|
||||||
".aipass/registry.json",
|
|
||||||
"setup.sh",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
input_data = json.loads(sys.stdin.read())
|
|
||||||
except Exception:
|
|
||||||
print(json.dumps({}))
|
|
||||||
return
|
|
||||||
|
|
||||||
tool_input = input_data.get("input", {})
|
|
||||||
file_path = tool_input.get("file_path", "") or tool_input.get("path", "")
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
print(json.dumps({}))
|
|
||||||
return
|
|
||||||
|
|
||||||
for pattern in PROTECTED_PATTERNS:
|
|
||||||
if pattern in file_path:
|
|
||||||
output = {
|
|
||||||
"hookSpecificOutput": {
|
|
||||||
"hookEventName": "BeforeTool",
|
|
||||||
"permissionDecision": "deny"
|
|
||||||
},
|
|
||||||
"systemMessage": f"Edit blocked: {pattern} is a protected file."
|
|
||||||
}
|
|
||||||
print(json.dumps(output))
|
|
||||||
return
|
|
||||||
|
|
||||||
print(json.dumps({}))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Gemini BeforeModel hook: inject per-turn AIPass context."""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def find_repo_root():
|
|
||||||
p = Path.cwd()
|
|
||||||
while p != p.parent:
|
|
||||||
if (p / ".git").exists():
|
|
||||||
return p
|
|
||||||
p = p.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_branch_from_cwd(repo_root):
|
|
||||||
cwd = Path.cwd()
|
|
||||||
try:
|
|
||||||
rel = cwd.relative_to(repo_root / "src" / "aipass")
|
|
||||||
return str(rel).split("/")[0]
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
rel = cwd.relative_to(repo_root / "src")
|
|
||||||
return str(rel).split("/")[0]
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
input_data = json.loads(sys.stdin.read())
|
|
||||||
except Exception:
|
|
||||||
input_data = {}
|
|
||||||
|
|
||||||
repo_root = find_repo_root()
|
|
||||||
if not repo_root:
|
|
||||||
print(json.dumps({}))
|
|
||||||
return
|
|
||||||
|
|
||||||
context_parts = []
|
|
||||||
|
|
||||||
now = datetime.now().strftime("%A, %B %-d %Y — %-I:%M %p")
|
|
||||||
context_parts.append(f"# Current Time: {now}")
|
|
||||||
|
|
||||||
branch = get_branch_from_cwd(repo_root)
|
|
||||||
if branch:
|
|
||||||
branch_dir = repo_root / "src" / "aipass" / branch
|
|
||||||
if not branch_dir.exists():
|
|
||||||
branch_dir = repo_root / "src" / branch
|
|
||||||
|
|
||||||
passport = branch_dir / ".trinity" / "passport.json"
|
|
||||||
if passport.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(passport.read_text(encoding="utf-8"))
|
|
||||||
identity = data.get("identity", {})
|
|
||||||
traits = data.get("traits", [])
|
|
||||||
context_parts.append(
|
|
||||||
f"# {branch.upper()} Identity\n"
|
|
||||||
f"Path: {data.get('branch_info', {}).get('path', 'unknown')}\n"
|
|
||||||
f"Role: {identity.get('role', 'unknown')}\n"
|
|
||||||
f"Traits: {' | '.join(traits)}\n"
|
|
||||||
f"Purpose: {identity.get('purpose', 'unknown')}"
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
inbox = branch_dir / ".ai_mail.local" / "inbox.json"
|
|
||||||
if inbox.exists():
|
|
||||||
try:
|
|
||||||
mail = json.loads(inbox.read_text(encoding="utf-8"))
|
|
||||||
unread = mail.get("unread_count", 0)
|
|
||||||
if unread > 0:
|
|
||||||
context_parts.append(
|
|
||||||
f"You have {unread} new emails - check with: "
|
|
||||||
f"drone @ai_mail inbox"
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if context_parts:
|
|
||||||
context = "\n\n".join(context_parts)
|
|
||||||
output = {
|
|
||||||
"hookSpecificOutput": {
|
|
||||||
"hookEventName": "BeforeModel",
|
|
||||||
"additionalContext": context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
output = {}
|
|
||||||
|
|
||||||
print(json.dumps(output))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Gemini SessionStart hook: inject AIPass identity context."""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def find_repo_root():
|
|
||||||
p = Path.cwd()
|
|
||||||
while p != p.parent:
|
|
||||||
if (p / ".git").exists():
|
|
||||||
return p
|
|
||||||
p = p.parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_branch_from_cwd(repo_root):
|
|
||||||
cwd = Path.cwd()
|
|
||||||
try:
|
|
||||||
rel = cwd.relative_to(repo_root / "src" / "aipass")
|
|
||||||
return str(rel).split("/")[0]
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
rel = cwd.relative_to(repo_root / "src")
|
|
||||||
return str(rel).split("/")[0]
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
input_data = json.loads(sys.stdin.read())
|
|
||||||
except Exception:
|
|
||||||
input_data = {}
|
|
||||||
|
|
||||||
repo_root = find_repo_root()
|
|
||||||
if not repo_root:
|
|
||||||
print(json.dumps({}))
|
|
||||||
return
|
|
||||||
|
|
||||||
context_parts = []
|
|
||||||
|
|
||||||
global_prompt = repo_root / ".aipass" / "aipass_global_prompt.md"
|
|
||||||
if global_prompt.exists():
|
|
||||||
context_parts.append(global_prompt.read_text(encoding="utf-8")[:8000])
|
|
||||||
|
|
||||||
branch = get_branch_from_cwd(repo_root)
|
|
||||||
if branch:
|
|
||||||
branch_dir = repo_root / "src" / "aipass" / branch
|
|
||||||
if not branch_dir.exists():
|
|
||||||
branch_dir = repo_root / "src" / branch
|
|
||||||
|
|
||||||
passport = branch_dir / ".trinity" / "passport.json"
|
|
||||||
if passport.exists():
|
|
||||||
try:
|
|
||||||
data = json.loads(passport.read_text(encoding="utf-8"))
|
|
||||||
identity = data.get("identity", {})
|
|
||||||
context_parts.append(
|
|
||||||
f"# Branch Identity: {branch.upper()}\n"
|
|
||||||
f"Role: {identity.get('role', 'unknown')}\n"
|
|
||||||
f"Purpose: {identity.get('purpose', 'unknown')}\n"
|
|
||||||
f"Class: {identity.get('citizen_class', 'unknown')}"
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
branch_prompt = branch_dir / ".aipass" / "aipass_local_prompt.md"
|
|
||||||
if branch_prompt.exists():
|
|
||||||
context_parts.append(branch_prompt.read_text(encoding="utf-8")[:4000])
|
|
||||||
|
|
||||||
if context_parts:
|
|
||||||
context = "\n\n---\n\n".join(context_parts)
|
|
||||||
output = {
|
|
||||||
"hookSpecificOutput": {
|
|
||||||
"hookEventName": "SessionStart",
|
|
||||||
"additionalContext": context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
output = {}
|
|
||||||
|
|
||||||
print(json.dumps(output))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
name: memo
|
|
||||||
description: Update branch memory files after completing work. Saves session history, key learnings, and collaboration observations to .trinity/ files.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Memory Update
|
|
||||||
|
|
||||||
Purpose: Update branch memory files after completing work this session.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
1. Read `.trinity/passport.json` first — re-absorb your identity, role, and principles before writing memories
|
|
||||||
2. Review what was done this session (context, recent changes, key decisions)
|
|
||||||
3. Update each file below as needed
|
|
||||||
4. Confirm completion — list files updated
|
|
||||||
|
|
||||||
## What to Update
|
|
||||||
|
|
||||||
### Always
|
|
||||||
|
|
||||||
- **.trinity/local.json** — Add new session entry to `sessions` if significant work was done. Add new `key_learnings` for facts you'd need next time. Trim oldest sessions if over 20.
|
|
||||||
- **.trinity/observations.json** — Add notable collaboration insights: breakthrough moments, pattern corrections, flow states, friction points, preference discoveries. Skip if nothing notable this session.
|
|
||||||
|
|
||||||
### If Relevant
|
|
||||||
|
|
||||||
- **.trinity/passport.json** — Evolve identity when the branch's role, capabilities, or principles have genuinely changed. Don't update just to update — but don't leave placeholders forever either.
|
|
||||||
- **README.md** — Does it reflect current state? Update if stale.
|
|
||||||
- **STATUS.local.md** — Drop quick notes on issues, todos, or ideas in the Notepad section.
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
name: prep
|
|
||||||
description: Session wrap-up. Update memories, check plans, review git state, check inbox, flag loose ends. Use before closing a session or compacting context.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Session Wrap-Up
|
|
||||||
|
|
||||||
Purpose: Button up everything at the end of a session — or before context compaction. Memories, plans, git — all tidy.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
1. Read `.trinity/passport.json` first — re-absorb your identity before writing anything
|
|
||||||
2. Do ALL of the following, then confirm what was updated
|
|
||||||
|
|
||||||
## 1. Memories
|
|
||||||
|
|
||||||
- **.trinity/local.json** — Add/update session entry with summary of work done. Add new key_learnings for anything learned this session. Trim oldest sessions if over 20.
|
|
||||||
- **.trinity/observations.json** — Add collaboration insights if anything notable happened. Skip if nothing new.
|
|
||||||
- **.trinity/passport.json** — Only update if role/purpose/principles genuinely changed this session.
|
|
||||||
|
|
||||||
## 2. Active Plans
|
|
||||||
|
|
||||||
- Check any DPLANs or FPLANs referenced in this session
|
|
||||||
- Update their execution logs, status, decision logs with current state
|
|
||||||
- If a plan was completed, note it (but don't close — the user does that)
|
|
||||||
|
|
||||||
## 3. Git State
|
|
||||||
|
|
||||||
- Run `git status` — report uncommitted changes
|
|
||||||
- If there's a logical commit waiting, suggest it (don't commit without asking)
|
|
||||||
- Note the current branch and any open PRs
|
|
||||||
|
|
||||||
## 4. Inbox
|
|
||||||
|
|
||||||
- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails
|
|
||||||
- Close any that were already processed but not formally closed
|
|
||||||
|
|
||||||
## 5. Loose Ends
|
|
||||||
|
|
||||||
- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions
|
|
||||||
- If anything can't survive compaction, write it to STATUS.local.md Notepad
|
|
||||||
|
|
||||||
## Confirm
|
|
||||||
|
|
||||||
List everything updated. Format:
|
|
||||||
```
|
|
||||||
Prep complete:
|
|
||||||
- local.json: [what was added]
|
|
||||||
- observations.json: [updated / skipped]
|
|
||||||
- Plans: [which ones updated]
|
|
||||||
- Git: [branch, uncommitted count, suggestion]
|
|
||||||
- Inbox: [count, action taken]
|
|
||||||
- Loose ends: [any flagged]
|
|
||||||
|
|
||||||
Ready to close out or compact.
|
|
||||||
```
|
|
||||||
@@ -35,7 +35,7 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
- OS: Ubuntu 24.04
|
- OS: Ubuntu 24.04
|
||||||
- Python: 3.12
|
- Python: 3.12
|
||||||
- CLI: Claude Code / Codex / Gemini
|
- CLI: Claude Code / Codex
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,31 @@ updates:
|
|||||||
prefix-development: "deps"
|
prefix-development: "deps"
|
||||||
include: "scope"
|
include: "scope"
|
||||||
|
|
||||||
|
# Hash-pinned CI tool installs (ruff/build/pytest/pip-audit/pip). Pinning is
|
||||||
|
# what Scorecard's Pinned-Dependencies wants, but a frozen pin rots: these
|
||||||
|
# locks are what security.yml's pip-audit scans, so a new advisory against a
|
||||||
|
# pinned dep reds the job until the pin moves. This entry is what keeps that
|
||||||
|
# window short. Dependabot reads the `pip-compile ...` command out of each
|
||||||
|
# .txt header and regenerates the lock (hashes included) from the .in.
|
||||||
|
# Separate from the "/" pip entry above, which tracks pyproject.toml.
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/.github/requirements"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
labels:
|
||||||
|
- "ci"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci"
|
||||||
|
include: "scope"
|
||||||
|
# packaging/pygments are shared across build.txt, e2e.txt and audit.txt.
|
||||||
|
# Ungrouped, one bump fans out into several PRs that each rewrite a subset
|
||||||
|
# of the locks and conflict with each other. One PR per week moves them all.
|
||||||
|
groups:
|
||||||
|
ci-tooling:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
@@ -22,3 +47,11 @@ updates:
|
|||||||
commit-message:
|
commit-message:
|
||||||
prefix: "ci"
|
prefix: "ci"
|
||||||
include: "scope"
|
include: "scope"
|
||||||
|
# codeql-action is a monorepo (init/analyze/upload-sarif share one release).
|
||||||
|
# Bumping them in separate PRs leaves mismatched versions in security.yml and
|
||||||
|
# CodeQL hard-fails "init and analyze must be the same version". Group them so
|
||||||
|
# every codeql-action bump lands as a single PR that moves all paths together.
|
||||||
|
groups:
|
||||||
|
codeql-action:
|
||||||
|
patterns:
|
||||||
|
- "github/codeql-action*"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# pip-audit for the security.yml `dependency-scan` job, hash-pinned (Scorecard:
|
||||||
|
# Pinned-Dependencies).
|
||||||
|
#
|
||||||
|
# Target env: ubuntu-latest, Python 3.13. pip-audit's own dependency tree is
|
||||||
|
# resolved and hashed here; the project itself is still installed unpinned via
|
||||||
|
# `pip install -e .` and scanned with `pip-audit --skip-editable`, so pinning
|
||||||
|
# this file does not narrow what the audit covers.
|
||||||
|
#
|
||||||
|
# TRADE-OFF: pip-audit scans the whole environment, including its own deps. They
|
||||||
|
# used to float to latest on every run (self-healing); pinned, a new advisory
|
||||||
|
# against one of them reds this job until the pin moves. Dependabot's `pip`
|
||||||
|
# entry for /.github/requirements is what keeps that window short.
|
||||||
|
#
|
||||||
|
# Regenerate:
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=audit.txt audit.in
|
||||||
|
pip-audit==2.10.1
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=audit.txt audit.in
|
||||||
|
#
|
||||||
|
boolean-py==5.0 \
|
||||||
|
--hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \
|
||||||
|
--hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9
|
||||||
|
# via license-expression
|
||||||
|
cachecontrol[filecache]==0.14.4 \
|
||||||
|
--hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \
|
||||||
|
--hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1
|
||||||
|
# via
|
||||||
|
# cachecontrol
|
||||||
|
# pip-audit
|
||||||
|
certifi==2026.6.17 \
|
||||||
|
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||||
|
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||||
|
# via requests
|
||||||
|
charset-normalizer==3.4.9 \
|
||||||
|
--hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \
|
||||||
|
--hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \
|
||||||
|
--hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \
|
||||||
|
--hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \
|
||||||
|
--hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \
|
||||||
|
--hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \
|
||||||
|
--hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \
|
||||||
|
--hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \
|
||||||
|
--hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \
|
||||||
|
--hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \
|
||||||
|
--hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \
|
||||||
|
--hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \
|
||||||
|
--hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \
|
||||||
|
--hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \
|
||||||
|
--hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \
|
||||||
|
--hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \
|
||||||
|
--hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \
|
||||||
|
--hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \
|
||||||
|
--hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \
|
||||||
|
--hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \
|
||||||
|
--hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \
|
||||||
|
--hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \
|
||||||
|
--hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \
|
||||||
|
--hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \
|
||||||
|
--hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \
|
||||||
|
--hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \
|
||||||
|
--hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \
|
||||||
|
--hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \
|
||||||
|
--hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \
|
||||||
|
--hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \
|
||||||
|
--hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \
|
||||||
|
--hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \
|
||||||
|
--hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \
|
||||||
|
--hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \
|
||||||
|
--hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \
|
||||||
|
--hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \
|
||||||
|
--hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \
|
||||||
|
--hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \
|
||||||
|
--hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \
|
||||||
|
--hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \
|
||||||
|
--hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \
|
||||||
|
--hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \
|
||||||
|
--hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \
|
||||||
|
--hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \
|
||||||
|
--hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \
|
||||||
|
--hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \
|
||||||
|
--hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \
|
||||||
|
--hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \
|
||||||
|
--hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \
|
||||||
|
--hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \
|
||||||
|
--hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \
|
||||||
|
--hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \
|
||||||
|
--hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \
|
||||||
|
--hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \
|
||||||
|
--hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \
|
||||||
|
--hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \
|
||||||
|
--hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \
|
||||||
|
--hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \
|
||||||
|
--hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \
|
||||||
|
--hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \
|
||||||
|
--hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \
|
||||||
|
--hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \
|
||||||
|
--hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \
|
||||||
|
--hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \
|
||||||
|
--hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \
|
||||||
|
--hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \
|
||||||
|
--hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \
|
||||||
|
--hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \
|
||||||
|
--hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \
|
||||||
|
--hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \
|
||||||
|
--hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \
|
||||||
|
--hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \
|
||||||
|
--hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \
|
||||||
|
--hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \
|
||||||
|
--hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \
|
||||||
|
--hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \
|
||||||
|
--hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \
|
||||||
|
--hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \
|
||||||
|
--hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \
|
||||||
|
--hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \
|
||||||
|
--hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \
|
||||||
|
--hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \
|
||||||
|
--hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \
|
||||||
|
--hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \
|
||||||
|
--hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \
|
||||||
|
--hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \
|
||||||
|
--hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \
|
||||||
|
--hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \
|
||||||
|
--hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \
|
||||||
|
--hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \
|
||||||
|
--hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \
|
||||||
|
--hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \
|
||||||
|
--hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115
|
||||||
|
# via requests
|
||||||
|
cyclonedx-python-lib==11.11.0 \
|
||||||
|
--hash=sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44 \
|
||||||
|
--hash=sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102
|
||||||
|
# via pip-audit
|
||||||
|
defusedxml==0.7.1 \
|
||||||
|
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||||
|
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||||
|
# via py-serializable
|
||||||
|
filelock==3.29.7 \
|
||||||
|
--hash=sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d \
|
||||||
|
--hash=sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51
|
||||||
|
# via cachecontrol
|
||||||
|
idna==3.18 \
|
||||||
|
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
|
||||||
|
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
|
||||||
|
# via requests
|
||||||
|
license-expression==30.4.4 \
|
||||||
|
--hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \
|
||||||
|
--hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
markdown-it-py==4.2.0 \
|
||||||
|
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||||
|
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||||
|
# via rich
|
||||||
|
mdurl==0.1.2 \
|
||||||
|
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||||
|
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||||
|
# via markdown-it-py
|
||||||
|
msgpack==1.2.1 \
|
||||||
|
--hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \
|
||||||
|
--hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \
|
||||||
|
--hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \
|
||||||
|
--hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \
|
||||||
|
--hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \
|
||||||
|
--hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \
|
||||||
|
--hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \
|
||||||
|
--hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \
|
||||||
|
--hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \
|
||||||
|
--hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \
|
||||||
|
--hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \
|
||||||
|
--hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \
|
||||||
|
--hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \
|
||||||
|
--hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \
|
||||||
|
--hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \
|
||||||
|
--hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \
|
||||||
|
--hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \
|
||||||
|
--hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \
|
||||||
|
--hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \
|
||||||
|
--hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \
|
||||||
|
--hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \
|
||||||
|
--hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \
|
||||||
|
--hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \
|
||||||
|
--hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \
|
||||||
|
--hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \
|
||||||
|
--hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \
|
||||||
|
--hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \
|
||||||
|
--hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \
|
||||||
|
--hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \
|
||||||
|
--hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \
|
||||||
|
--hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \
|
||||||
|
--hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \
|
||||||
|
--hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \
|
||||||
|
--hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \
|
||||||
|
--hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \
|
||||||
|
--hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \
|
||||||
|
--hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \
|
||||||
|
--hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \
|
||||||
|
--hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \
|
||||||
|
--hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \
|
||||||
|
--hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \
|
||||||
|
--hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \
|
||||||
|
--hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \
|
||||||
|
--hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \
|
||||||
|
--hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \
|
||||||
|
--hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \
|
||||||
|
--hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \
|
||||||
|
--hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \
|
||||||
|
--hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \
|
||||||
|
--hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \
|
||||||
|
--hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \
|
||||||
|
--hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \
|
||||||
|
--hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \
|
||||||
|
--hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \
|
||||||
|
--hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \
|
||||||
|
--hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \
|
||||||
|
--hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \
|
||||||
|
--hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \
|
||||||
|
--hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \
|
||||||
|
--hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \
|
||||||
|
--hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \
|
||||||
|
--hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \
|
||||||
|
--hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \
|
||||||
|
--hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \
|
||||||
|
--hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \
|
||||||
|
--hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c
|
||||||
|
# via cachecontrol
|
||||||
|
packageurl-python==0.17.6 \
|
||||||
|
--hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \
|
||||||
|
--hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
packaging==26.2 \
|
||||||
|
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||||
|
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||||
|
# via
|
||||||
|
# pip-audit
|
||||||
|
# pip-requirements-parser
|
||||||
|
pip-api==0.0.34 \
|
||||||
|
--hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \
|
||||||
|
--hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625
|
||||||
|
# via pip-audit
|
||||||
|
pip-audit==2.10.1 \
|
||||||
|
--hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \
|
||||||
|
--hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a
|
||||||
|
# via -r audit.in
|
||||||
|
pip-requirements-parser==32.0.1 \
|
||||||
|
--hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \
|
||||||
|
--hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3
|
||||||
|
# via pip-audit
|
||||||
|
platformdirs==4.10.0 \
|
||||||
|
--hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \
|
||||||
|
--hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a
|
||||||
|
# via pip-audit
|
||||||
|
py-serializable==2.1.0 \
|
||||||
|
--hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \
|
||||||
|
--hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
pygments==2.20.0 \
|
||||||
|
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||||
|
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||||
|
# via rich
|
||||||
|
pyparsing==3.3.2 \
|
||||||
|
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||||
|
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||||
|
# via pip-requirements-parser
|
||||||
|
requests==2.34.2 \
|
||||||
|
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||||
|
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||||
|
# via
|
||||||
|
# cachecontrol
|
||||||
|
# pip-audit
|
||||||
|
rich==15.0.0 \
|
||||||
|
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||||
|
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||||
|
# via pip-audit
|
||||||
|
sortedcontainers==2.4.0 \
|
||||||
|
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||||
|
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
tomli==2.4.1 \
|
||||||
|
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||||
|
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||||
|
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||||
|
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||||
|
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||||
|
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||||
|
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||||
|
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||||
|
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||||
|
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||||
|
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||||
|
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||||
|
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||||
|
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||||
|
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||||
|
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||||
|
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||||
|
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||||
|
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||||
|
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||||
|
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||||
|
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||||
|
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||||
|
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||||
|
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||||
|
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||||
|
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||||
|
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||||
|
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||||
|
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||||
|
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||||
|
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||||
|
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||||
|
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||||
|
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||||
|
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||||
|
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||||
|
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||||
|
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||||
|
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||||
|
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||||
|
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||||
|
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||||
|
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||||
|
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||||
|
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||||
|
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||||
|
# via pip-audit
|
||||||
|
tomli-w==1.2.0 \
|
||||||
|
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||||
|
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||||
|
# via pip-audit
|
||||||
|
typing-extensions==4.16.0 \
|
||||||
|
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||||
|
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
urllib3==2.7.0 \
|
||||||
|
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||||
|
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||||
|
# via requests
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
pip==26.1.2 \
|
||||||
|
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
|
||||||
|
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
|
||||||
|
# via pip-api
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# `build` for the publish.yml `build` job, hash-pinned (Scorecard:
|
||||||
|
# Pinned-Dependencies).
|
||||||
|
#
|
||||||
|
# Target env: ubuntu-latest, Python 3.13 ONLY.
|
||||||
|
# That narrowness is load-bearing — build's marker-gated deps are all excluded
|
||||||
|
# at this target and therefore absent from build.txt:
|
||||||
|
# colorama ; os_name == "nt" -> posix runner, not needed
|
||||||
|
# tomli ; python_version < "3.11" -> 3.13, not needed
|
||||||
|
# importlib-metadata; python_full_version < "3.10.2" -> 3.13, not needed
|
||||||
|
# If publish.yml ever gains a Windows runner or a <3.11 Python, regenerate this
|
||||||
|
# file on that target (or add the dep explicitly) or --require-hashes will fail
|
||||||
|
# with "all requirements must have their versions pinned".
|
||||||
|
# See e2e.in for the cross-OS variant that handles this.
|
||||||
|
#
|
||||||
|
# Regenerate:
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=build.txt build.in
|
||||||
|
build==1.5.0
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=build.txt build.in
|
||||||
|
#
|
||||||
|
build==1.5.0 \
|
||||||
|
--hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \
|
||||||
|
--hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647
|
||||||
|
# via -r build.in
|
||||||
|
packaging==26.2 \
|
||||||
|
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||||
|
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||||
|
# via build
|
||||||
|
pyproject-hooks==1.2.0 \
|
||||||
|
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||||
|
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||||
|
# via build
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Outer build tooling for e2e-wheel.yml, hash-pinned (Scorecard:
|
||||||
|
# Pinned-Dependencies).
|
||||||
|
#
|
||||||
|
# Target env: ubuntu-latest + windows-latest + macos-latest, Python 3.12.
|
||||||
|
# conftest.py builds the wheel + a clean venv internally; the outer env only
|
||||||
|
# needs build + pytest.
|
||||||
|
#
|
||||||
|
# WHY colorama IS LISTED EXPLICITLY (do not "clean up"):
|
||||||
|
# both build and pytest depend on colorama behind a platform marker
|
||||||
|
# (`os_name == "nt"` / `sys_platform == "win32"`). pip-compile evaluates markers
|
||||||
|
# against the machine it runs on, so compiling on Linux DROPS colorama from the
|
||||||
|
# lock — and the windows-latest leg then dies under --require-hashes with
|
||||||
|
# "In --require-hashes mode, all requirements must have their versions pinned".
|
||||||
|
# Listing it as a direct requirement forces it into the lock with hashes.
|
||||||
|
# colorama is a pure-python universal wheel (py2.py3-none-any, zero deps), so
|
||||||
|
# installing it on the Linux/macOS legs is inert.
|
||||||
|
#
|
||||||
|
# Python 3.12 is likewise load-bearing: pytest's `exceptiongroup`/`tomli` and
|
||||||
|
# build's `tomli` are gated on python_version < "3.11" and are absent here. If
|
||||||
|
# the matrix ever drops below 3.11, regenerate on that target.
|
||||||
|
#
|
||||||
|
# Regenerate (on Linux, Python 3.12):
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=e2e.txt e2e.in
|
||||||
|
build==1.5.0
|
||||||
|
pytest==9.1.1
|
||||||
|
colorama==0.4.6
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=e2e.txt e2e.in
|
||||||
|
#
|
||||||
|
build==1.5.0 \
|
||||||
|
--hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \
|
||||||
|
--hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647
|
||||||
|
# via -r e2e.in
|
||||||
|
colorama==0.4.6 \
|
||||||
|
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||||
|
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||||
|
# via -r e2e.in
|
||||||
|
iniconfig==2.3.0 \
|
||||||
|
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
||||||
|
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||||
|
# via pytest
|
||||||
|
packaging==26.2 \
|
||||||
|
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||||
|
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||||
|
# via
|
||||||
|
# build
|
||||||
|
# pytest
|
||||||
|
pluggy==1.6.0 \
|
||||||
|
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||||
|
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||||
|
# via pytest
|
||||||
|
pygments==2.20.0 \
|
||||||
|
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||||
|
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||||
|
# via pytest
|
||||||
|
pyproject-hooks==1.2.0 \
|
||||||
|
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||||
|
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||||
|
# via build
|
||||||
|
pytest==9.1.1 \
|
||||||
|
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||||
|
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||||
|
# via -r e2e.in
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# ruff for the ci.yml `lint` job, hash-pinned (Scorecard: Pinned-Dependencies).
|
||||||
|
#
|
||||||
|
# Target env: ubuntu-latest, Python 3.13. ruff has no dependencies, and
|
||||||
|
# --generate-hashes emits every PyPI file hash for the pinned version (all 18
|
||||||
|
# platform wheels + sdist), so this lock stays valid if lint ever runs on
|
||||||
|
# another OS/arch.
|
||||||
|
#
|
||||||
|
# 0.15.21 == what the previous unpinned `pip install ruff` resolved to on
|
||||||
|
# 2026-07-15, so pinning is a no-op for lint results today. Bumping this file
|
||||||
|
# can surface new lint rules — that is the intended, reviewable trade-off.
|
||||||
|
# Keep compatible with pyproject.toml's dev extra (`ruff>=0.11`).
|
||||||
|
#
|
||||||
|
# Regenerate:
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=lint.txt lint.in
|
||||||
|
ruff==0.16.0
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=lint.txt lint.in
|
||||||
|
#
|
||||||
|
ruff==0.16.0 \
|
||||||
|
--hash=sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17 \
|
||||||
|
--hash=sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d \
|
||||||
|
--hash=sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af \
|
||||||
|
--hash=sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09 \
|
||||||
|
--hash=sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522 \
|
||||||
|
--hash=sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472 \
|
||||||
|
--hash=sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0 \
|
||||||
|
--hash=sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b \
|
||||||
|
--hash=sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9 \
|
||||||
|
--hash=sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213 \
|
||||||
|
--hash=sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb \
|
||||||
|
--hash=sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed \
|
||||||
|
--hash=sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717 \
|
||||||
|
--hash=sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a \
|
||||||
|
--hash=sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81 \
|
||||||
|
--hash=sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982 \
|
||||||
|
--hash=sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e \
|
||||||
|
--hash=sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4
|
||||||
|
# via -r lint.in
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# pip self-upgrade, hash-pinned (OpenSSF Scorecard: Pinned-Dependencies).
|
||||||
|
#
|
||||||
|
# Replaces `python -m pip install --upgrade pip` in ci.yml, security.yml and
|
||||||
|
# e2e-wheel.yml. pip has no runtime dependencies, so this lock is inherently
|
||||||
|
# portable across every OS and Python in the CI matrix (3.10-3.13).
|
||||||
|
#
|
||||||
|
# 26.1.2 is deliberate, not merely "latest": security.yml's pip-audit scans the
|
||||||
|
# whole environment and the runner's bundled pip (26.1.1) carries PYSEC-2026-196
|
||||||
|
# (fixed in 26.1.2). Do not pin below 26.1.2 — audit will red.
|
||||||
|
#
|
||||||
|
# Regenerate:
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=pip.txt pip.in
|
||||||
|
pip==26.1.2
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --output-file=pip.txt pip.in
|
||||||
|
#
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
pip==26.1.2 \
|
||||||
|
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
|
||||||
|
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
|
||||||
|
# via -r pip.in
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""CI gate: run seedgo standards audit across all branches."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aipass.seedgo.apps.handlers.audit.branch_audit import audit_branch
|
||||||
|
from aipass.seedgo.apps.handlers.bypass.bypass_handler import load_bypass_rules
|
||||||
|
|
||||||
|
THRESHOLD = 100
|
||||||
|
|
||||||
|
src = Path("src/aipass")
|
||||||
|
pack = src / "seedgo/apps/handlers/aipass_standards"
|
||||||
|
|
||||||
|
branches = []
|
||||||
|
for d in sorted(src.iterdir()):
|
||||||
|
if d.is_dir() and (d / "apps").is_dir():
|
||||||
|
entry = d / "apps" / f"{d.name}.py"
|
||||||
|
branches.append(
|
||||||
|
{
|
||||||
|
"name": d.name,
|
||||||
|
"path": str(d),
|
||||||
|
"entry_file": str(entry) if entry.exists() else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
failed = []
|
||||||
|
for branch in branches:
|
||||||
|
bypass_rules = load_bypass_rules(branch["path"])
|
||||||
|
result = audit_branch(branch, bypass_rules, pack_path=pack)
|
||||||
|
avg = result.get("average", 0)
|
||||||
|
print(f" {branch['name']:>12}: {avg:.0f}%")
|
||||||
|
if avg < THRESHOLD:
|
||||||
|
failed.append((branch["name"], avg, result))
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
print(f"\nFAILED: {len(failed)} branch(es) below {THRESHOLD}%")
|
||||||
|
for name, score, result in failed:
|
||||||
|
print(f" {name}: {score:.0f}%")
|
||||||
|
# Name the failing standards + the specific checks that did not pass,
|
||||||
|
# so CI logs say WHY (not just the percentage). Critical for diagnosing
|
||||||
|
# working-tree-vs-clean-checkout divergence.
|
||||||
|
scores = result.get("scores", {})
|
||||||
|
results = result.get("results", {})
|
||||||
|
for std, sc in scores.items():
|
||||||
|
if sc < 100:
|
||||||
|
checks = results.get(std, {}).get("checks", [])
|
||||||
|
msgs = [
|
||||||
|
c.get("message", "")
|
||||||
|
for c in checks
|
||||||
|
if not c.get("passed", True)
|
||||||
|
]
|
||||||
|
detail = " | ".join(m for m in msgs if m)[:400]
|
||||||
|
print(f" └ {std}: {sc:.0f}% {detail}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"\nAll {len(branches)} branches pass (>={THRESHOLD}%)")
|
||||||
+51
-15
@@ -2,54 +2,90 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- run: pip install ruff
|
# Hash-pinned tool install (Scorecard: Pinned-Dependencies). Pins ruff to
|
||||||
|
# the version this lint gate is known-green against; see .github/requirements/lint.in.
|
||||||
|
- run: python -m pip install --require-hashes -r .github/requirements/lint.txt
|
||||||
- run: ruff check src/ tests/
|
- run: ruff check src/ tests/
|
||||||
- run: ruff format --check src/ tests/
|
- run: ruff format --check src/ tests/
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: lint
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- run: |
|
- run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --require-hashes -r .github/requirements/pip.txt
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
- run: coverage run -m pytest -v --tb=short --rootdir=.
|
# tests/e2e build a wheel + clean venv per the dedicated e2e-wheel.yml
|
||||||
|
# workflow — they are not part of the fast unit lane.
|
||||||
|
- run: coverage run -m pytest -v --tb=short --rootdir=. --ignore=tests/e2e
|
||||||
|
|
||||||
|
standards:
|
||||||
|
name: seedgo-audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
# Full history: the README-freshness check reads `git log` to find the
|
||||||
|
# last commit touching each branch's .py. A shallow (depth-1) checkout
|
||||||
|
# makes every file look born at HEAD, so every README false-fails as
|
||||||
|
# "stale". Full history makes CI match a local audit exactly.
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- run: |
|
||||||
|
python -m pip install --require-hashes -r .github/requirements/pip.txt
|
||||||
|
# Install the `memory` extra (numpy/chromadb/fastembed) alongside dev:
|
||||||
|
# the diagnostics standard runs pyright over every branch, and memory's
|
||||||
|
# handlers import chromadb/numpy. Without these deps installed, pyright
|
||||||
|
# reports them as unresolved imports (reportMissingImports=error) and
|
||||||
|
# memory scores <100 — a false failure from a missing CI dep, not a code
|
||||||
|
# defect. Installing the declared extra lets pyright resolve them so the
|
||||||
|
# audit measures real type-correctness (and matches a local audit).
|
||||||
|
pip install -e ".[dev,memory]"
|
||||||
|
- name: Run seedgo standards audit
|
||||||
|
run: python .github/scripts/seedgo_audit.py
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
name: coverage
|
name: coverage
|
||||||
needs: [test]
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- run: |
|
- run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --require-hashes -r .github/requirements/pip.txt
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
- run: coverage run -m pytest --rootdir=.
|
- run: coverage run -m pytest --rootdir=. --ignore=tests/e2e
|
||||||
- run: coverage xml
|
- run: coverage xml
|
||||||
- uses: codecov/codecov-action@v6
|
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: e2e-wheel
|
||||||
|
|
||||||
|
# Cross-OS end-to-end WIRING test (FPLAN-0239, P1 of DPLAN-0194).
|
||||||
|
# Builds the wheel, installs it into a clean venv (handled by the pytest
|
||||||
|
# fixtures in tests/e2e/conftest.py), and runs the 4-tier wiring ladder.
|
||||||
|
#
|
||||||
|
# RED-FIRST: Windows is EXPECTED to fail in known places (symlink init,
|
||||||
|
# bin-vs-Scripts, /tmp). Do not "fix" Windows here — the red is the deliverable.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
paths:
|
||||||
|
- "tests/e2e/**"
|
||||||
|
- ".github/workflows/e2e-wheel.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "src/**"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "tests/e2e/**"
|
||||||
|
- ".github/workflows/e2e-wheel.yml"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "src/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Least-privilege token (Scorecard Token-Permissions). This workflow only
|
||||||
|
# reads the repo to build + smoke-test the wheel; it needs no write scopes.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-wheel:
|
||||||
|
name: e2e-wheel (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
python-version: ["3.12"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install build tooling
|
||||||
|
# Hash-pinned (Scorecard: Pinned-Dependencies). e2e.txt carries colorama
|
||||||
|
# explicitly — build/pytest need it only on Windows (os_name == "nt" /
|
||||||
|
# sys_platform == "win32"), and a Linux-generated lock would otherwise
|
||||||
|
# omit it and break the windows-latest leg under --require-hashes.
|
||||||
|
# See .github/requirements/e2e.in.
|
||||||
|
run: |
|
||||||
|
python -m pip install --require-hashes -r .github/requirements/pip.txt
|
||||||
|
python -m pip install --require-hashes -r .github/requirements/e2e.txt
|
||||||
|
|
||||||
|
- name: Run cross-OS e2e wiring harness
|
||||||
|
# conftest.py builds the wheel + clean venv internally; the outer env
|
||||||
|
# only needs build + pytest.
|
||||||
|
run: python -m pytest tests/e2e -v
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: macOS Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# Run on every push/PR to main/dev — NOT path-filtered. This is a branch-
|
||||||
|
# protection *required* check; a path filter makes it skip on unrelated PRs,
|
||||||
|
# which GitHub then parks as "Expected — waiting for status" forever, blocking
|
||||||
|
# the merge. Required checks must run on every PR to report a status.
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, dev]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
macos-setup:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Run setup.sh
|
||||||
|
run: bash setup.sh
|
||||||
|
|
||||||
|
- name: Verify drone CLI
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
drone --version
|
||||||
|
drone systems
|
||||||
|
drone @seedgo --help
|
||||||
|
|
||||||
|
- name: Run full test suite
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
pytest -v --tb=short --rootdir=. 2>&1 | tee pytest-output.txt
|
||||||
|
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: macos-pytest-results
|
||||||
|
path: pytest-output.txt
|
||||||
@@ -5,17 +5,41 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Job-level permissions REPLACE the top-level block rather than merge with
|
||||||
|
# it, so `contents: read` is restated here on purpose — dropping it would
|
||||||
|
# break actions/checkout. id-token/attestations are what the provenance
|
||||||
|
# attestation below needs (Scorecard: Signed-Releases).
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- run: pip install build
|
# Hash-pinned tool install (Scorecard: Pinned-Dependencies).
|
||||||
|
- run: python -m pip install --require-hashes -r .github/requirements/build.txt
|
||||||
- run: python -m build
|
- run: python -m build
|
||||||
- uses: actions/upload-artifact@v7
|
- name: Attest build provenance
|
||||||
|
# Signs a provenance statement binding these exact sdist/wheel digests to
|
||||||
|
# this workflow run, via the same keyless Sigstore/OIDC path as the
|
||||||
|
# release signing below. Runs after the artifacts exist and before they
|
||||||
|
# leave the job, so the attested digests are the published ones.
|
||||||
|
# NOTE: the bundle is deliberately NOT written into dist/ — the publish
|
||||||
|
# job feeds dist/* to gh-action-pypi-publish, which rejects any file that
|
||||||
|
# is not a distribution. See the report note on attaching provenance to
|
||||||
|
# the GitHub Release.
|
||||||
|
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||||
|
with:
|
||||||
|
subject-path: "dist/*"
|
||||||
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -27,8 +51,45 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||||
|
|
||||||
|
github-release:
|
||||||
|
needs: publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write # keyless Sigstore signing (OIDC); no signing key exists
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/
|
||||||
|
- name: Sign artifacts with Sigstore (keyless, OIDC)
|
||||||
|
# Produces dist/<artifact>.sigstore.json bundles next to each wheel/sdist.
|
||||||
|
# The 'gh release create dist/*' step below then attaches them to the
|
||||||
|
# GitHub Release, which is where Scorecard's Signed-Releases check looks.
|
||||||
|
# release-signing-artifacts is disabled: the action's own auto-attach only
|
||||||
|
# fires on a 'release: published' event, but we trigger on 'push: tags',
|
||||||
|
# so we upload the bundles ourselves via the dist/* glob.
|
||||||
|
uses: sigstore/gh-action-sigstore-python@5b79a39c381910c090341a2c9b0bf022c8b387e1 # v3.4.0
|
||||||
|
with:
|
||||||
|
inputs: ./dist/*.tar.gz ./dist/*.whl
|
||||||
|
release-signing-artifacts: false
|
||||||
|
- name: Extract latest CHANGELOG section
|
||||||
|
run: |
|
||||||
|
# Grab the topmost "## [...]" block from CHANGELOG.md as release notes.
|
||||||
|
awk '/^## \[/{c++} c==1' CHANGELOG.md | sed '/^---$/d' > release_notes.md
|
||||||
|
echo "Release notes:" && cat release_notes.md
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh release create "${GITHUB_REF_NAME}" \
|
||||||
|
--title "${GITHUB_REF_NAME}" \
|
||||||
|
--notes-file release_notes.md \
|
||||||
|
dist/*
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Scorecard analysis workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Only the default branch is supported.
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
schedule:
|
||||||
|
# Weekly on Saturdays.
|
||||||
|
- cron: '30 1 * * 6'
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Scorecard analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed for Code scanning upload
|
||||||
|
security-events: write
|
||||||
|
# Needed for GitHub OIDC token if publish_results is true
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Run analysis"
|
||||||
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
- name: "Upload artifact"
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
- name: "Upload to code-scanning"
|
||||||
|
uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
@@ -2,32 +2,49 @@ name: Security Scan
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 6 * * 1"
|
- cron: "0 6 * * 1"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dependency-scan:
|
dependency-scan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- run: pip install pip-audit
|
# Upgrade pip first: pip-audit scans the whole environment, and the
|
||||||
|
# runner's bundled pip (26.1.1) carries advisory PYSEC-2026-196 (fixed in
|
||||||
|
# 26.1.2). Upgrading removes the vulnerable version outright rather than
|
||||||
|
# suppressing it — and 26.1.2 also resolves CVE-2026-3219 / CVE-2026-6357,
|
||||||
|
# which is why those two stale --ignore-vuln entries are no longer needed.
|
||||||
|
# Both installs are hash-pinned (Scorecard: Pinned-Dependencies); pip.txt
|
||||||
|
# holds the >=26.1.2 floor the comment above requires.
|
||||||
|
- run: |
|
||||||
|
python -m pip install --require-hashes -r .github/requirements/pip.txt
|
||||||
|
python -m pip install --require-hashes -r .github/requirements/audit.txt
|
||||||
- run: pip install -e .
|
- run: pip install -e .
|
||||||
- name: Pip audit
|
- name: Pip audit
|
||||||
run: pip-audit --skip-editable --ignore-vuln CVE-2026-3219 --ignore-vuln CVE-2026-6357
|
run: pip-audit --skip-editable
|
||||||
|
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- uses: github/codeql-action/init@v3
|
- uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
- uses: github/codeql-action/analyze@v3
|
- uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * 1"
|
- cron: "0 0 * * 1"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||||
with:
|
with:
|
||||||
stale-issue-message: "This issue has been inactive for 30 days. It will be closed in 7 days if no further activity occurs."
|
stale-issue-message: "This issue has been inactive for 30 days. It will be closed in 7 days if no further activity occurs."
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
name: Windows Setup Test
|
name: Windows Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
# Run on every push/PR to main/dev — NOT path-filtered. This is a branch-
|
||||||
|
# protection *required* check; a path filter makes it skip on unrelated PRs,
|
||||||
|
# which GitHub then parks as "Expected — waiting for status" forever, blocking
|
||||||
|
# the merge. Required checks must run on every PR to report a status.
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
paths:
|
|
||||||
- 'setup.sh'
|
|
||||||
- 'src/aipass/*/apps/handlers/__init__.py'
|
|
||||||
- 'src/aipass/drone/cli.py'
|
|
||||||
- 'pyproject.toml'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
branches: [main, dev]
|
||||||
- 'setup.sh'
|
|
||||||
- 'src/aipass/*/apps/handlers/__init__.py'
|
permissions:
|
||||||
- 'src/aipass/drone/cli.py'
|
contents: read
|
||||||
- 'pyproject.toml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
windows-setup:
|
windows-setup:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
@@ -37,3 +36,18 @@ jobs:
|
|||||||
drone --version
|
drone --version
|
||||||
drone systems
|
drone systems
|
||||||
drone @seedgo --help
|
drone @seedgo --help
|
||||||
|
|
||||||
|
- name: Run full test suite
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
source .venv/Scripts/activate
|
||||||
|
export PYTHONUTF8=1
|
||||||
|
pytest -v --tb=short --rootdir=. 2>&1 | tee pytest-output.txt
|
||||||
|
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: windows-pytest-results
|
||||||
|
path: pytest-output.txt
|
||||||
|
|||||||
+87
-86
@@ -1,17 +1,15 @@
|
|||||||
# Virtual environment
|
# Virtual environment
|
||||||
.venv/
|
.venv/
|
||||||
|
|
||||||
# Herald (session history — parked)
|
|
||||||
HERALD.md
|
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
dist/
|
dist/
|
||||||
build/-
|
build/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
|
|
||||||
# ChromaDB
|
# ChromaDB
|
||||||
.chroma/
|
.chroma/
|
||||||
@@ -20,13 +18,10 @@ build/-
|
|||||||
.env
|
.env
|
||||||
.devpulse_secret.md
|
.devpulse_secret.md
|
||||||
|
|
||||||
# API keys never leave ~/.secrets/ — gitleaks + pre-commit enforce this
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Plans (managed by flow, local to each installation)
|
|
||||||
FPLAN-*.md
|
|
||||||
DPLAN-*.md
|
|
||||||
RPLAN-*.md
|
|
||||||
TDPLAN-*.md
|
|
||||||
# AIPass runtime state (local to each installation)
|
# AIPass runtime state (local to each installation)
|
||||||
AIPASS_REGISTRY.json
|
AIPASS_REGISTRY.json
|
||||||
.trinity/
|
.trinity/
|
||||||
@@ -35,12 +30,20 @@ AIPASS_REGISTRY.json
|
|||||||
ai_mail.local/
|
ai_mail.local/
|
||||||
.feedback.local/
|
.feedback.local/
|
||||||
DASHBOARD.local.json
|
DASHBOARD.local.json
|
||||||
dev.local.md
|
|
||||||
STATUS.local.md
|
STATUS.local.md
|
||||||
|
STATUS.md
|
||||||
|
dev.local.md
|
||||||
CLOSED_PLANS.local.json
|
CLOSED_PLANS.local.json
|
||||||
.ai_central/
|
.ai_central/
|
||||||
system_logs/
|
system_logs/
|
||||||
|
notepad.md
|
||||||
|
|
||||||
|
# Plans (managed by flow, local to each installation)
|
||||||
|
FPLAN-*.md
|
||||||
|
DPLAN-*.md
|
||||||
|
RPLAN-*.md
|
||||||
|
TDPLAN-*.md
|
||||||
|
PPLAN-*.md
|
||||||
# Branch local directories
|
# Branch local directories
|
||||||
logs/
|
logs/
|
||||||
artifacts/
|
artifacts/
|
||||||
@@ -49,6 +52,7 @@ tools/
|
|||||||
docs.local/
|
docs.local/
|
||||||
.archive/
|
.archive/
|
||||||
.backup/
|
.backup/
|
||||||
|
.backup_system/
|
||||||
.recovery/
|
.recovery/
|
||||||
.seed/
|
.seed/
|
||||||
.spawn/
|
.spawn/
|
||||||
@@ -56,91 +60,88 @@ docs.local/
|
|||||||
# Module runtime JSON (config/data/log per command)
|
# Module runtime JSON (config/data/log per command)
|
||||||
**/*_json/
|
**/*_json/
|
||||||
|
|
||||||
|
# Branch-specific runtime files
|
||||||
|
src/aipass/memory/config/fragmented_memory_config.json
|
||||||
|
src/aipass/memory/config/fragmented_memory_state.json
|
||||||
|
src/aipass/memory/config/memory_bank.config.json
|
||||||
|
src/aipass/memory/config/.plans_processed.json
|
||||||
|
src/aipass/trigger/trigger_data.json
|
||||||
|
src/aipass/trigger/trigger_data.lock
|
||||||
|
src/aipass/drone/drone_command_registry.json
|
||||||
|
src/aipass/flow/CLOSED_PLANS.local.json
|
||||||
|
src/aipass/seedgo/apps/standards/aipass/pack.json
|
||||||
|
|
||||||
# Claude Code local state
|
# Claude Code local state
|
||||||
.claude/hooks/__pycache__/
|
.claude/hooks/__pycache__/
|
||||||
.claude/hooks/.last_diagnostics_file
|
.claude/hooks/.last_diagnostics_file
|
||||||
|
.diagnostics_state.json
|
||||||
|
.claude/hooks/probes/last_ping.jsonl
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
|
|
||||||
# @aipass citizen — now tracked. Launch (pyproject flip) still pending.
|
|
||||||
# **/.claude/settings.local.json — UNIGNORED: deny rules are system config that must travel with PRs
|
|
||||||
|
|
||||||
# Disabled files (AIPass convention: rename with (disabled) instead of delete)
|
# Disabled files (AIPass convention: rename with (disabled) instead of delete)
|
||||||
*(disabled)
|
*(disabled)
|
||||||
|
|
||||||
# Spawn template exceptions (template files must be tracked for public repo)
|
|
||||||
!src/aipass/spawn/templates/builder/.trinity/
|
|
||||||
!src/aipass/spawn/templates/builder/.trinity/**
|
|
||||||
!src/aipass/spawn/templates/builder/.ai_mail.local/
|
|
||||||
!src/aipass/spawn/templates/builder/.ai_mail.local/**
|
|
||||||
!src/aipass/spawn/templates/builder/.archive/
|
|
||||||
!src/aipass/spawn/templates/builder/.archive/**
|
|
||||||
!src/aipass/spawn/templates/builder/.spawn/
|
|
||||||
!src/aipass/spawn/templates/builder/.spawn/**
|
|
||||||
!src/aipass/spawn/templates/builder/.claude/
|
|
||||||
!src/aipass/spawn/templates/builder/.claude/**
|
|
||||||
!src/aipass/spawn/templates/builder/*_json/
|
|
||||||
!src/aipass/spawn/templates/builder/*_json/**
|
|
||||||
!src/aipass/spawn/templates/builder/logs/
|
|
||||||
!src/aipass/spawn/templates/builder/logs/**
|
|
||||||
!src/aipass/spawn/templates/builder/artifacts/
|
|
||||||
!src/aipass/spawn/templates/builder/artifacts/**
|
|
||||||
!src/aipass/spawn/templates/builder/dropbox/
|
|
||||||
!src/aipass/spawn/templates/builder/dropbox/**
|
|
||||||
!src/aipass/spawn/templates/builder/tools/
|
|
||||||
!src/aipass/spawn/templates/builder/tools/**
|
|
||||||
!src/aipass/spawn/templates/builder/docs.local/
|
|
||||||
!src/aipass/spawn/templates/builder/docs.local/**
|
|
||||||
!src/aipass/spawn/templates/builder/DASHBOARD.local.json
|
|
||||||
!src/aipass/spawn/templates/builder/STATUS.local.md
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Test artifacts
|
|
||||||
test/
|
|
||||||
src/aipass/seedgo/apps/standards/aipass/pack.json
|
|
||||||
backup_data/
|
|
||||||
|
|
||||||
|
|
||||||
backups
|
|
||||||
backup_system
|
|
||||||
src/aipass/flow/CLOSED_PLANS.local.json
|
|
||||||
src/aipass/memory/config/fragmented_memory_config.json
|
|
||||||
src/aipass/memory/config/fragmented_memory_state.json
|
|
||||||
|
|
||||||
|
|
||||||
.vscode
|
|
||||||
src/aipass/memory/config/memory_bank.config.json
|
|
||||||
src/aipass/spawn/templates/builder/.spawn/.template_registry.json
|
|
||||||
branch_audits _only
|
|
||||||
notepad.md
|
|
||||||
src/aipass/trigger/trigger_data.json
|
|
||||||
src/aipass/memory/config/.plans_processed.json
|
|
||||||
src/aipass/drone/drone_command_registry.json
|
|
||||||
src/aipass/spawn/templates/builder/.spawn/.template_registry.json
|
|
||||||
src/aipass/memory/config/.plans_processed.json
|
|
||||||
src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/pr_plugin.cpython-312.pyc
|
|
||||||
|
|
||||||
whiteboard.md
|
|
||||||
README_ORIGINAL_DISABLED.md
|
|
||||||
readme_history/
|
|
||||||
src/aipass/trigger/trigger_data.lock
|
|
||||||
|
|
||||||
# STATUS files — auto-generated, contain developer session data.
|
|
||||||
# Gitignored until prax sync is fixed to produce clean public output (#298).
|
|
||||||
STATUS.md
|
|
||||||
STATUS.local.md
|
|
||||||
|
|
||||||
# Private integrations — driver layer (@api) and wrapper layer (all branches)
|
# Private integrations — driver layer (@api) and wrapper layer (all branches)
|
||||||
# Per DPLAN-0133. Contents are gitignored; only the scaffold README.md is tracked.
|
# Per DPLAN-0133. Contents gitignored; only scaffold README.md tracked.
|
||||||
# Drop project-specific code into src/aipass/{branch}/apps/integrations/{project}/
|
|
||||||
# It stays local. Never appears in git.
|
|
||||||
src/aipass/*/apps/integrations/**
|
src/aipass/*/apps/integrations/**
|
||||||
!src/aipass/*/apps/integrations/README.md
|
!src/aipass/*/apps/integrations/README.md
|
||||||
.coverage
|
|
||||||
|
# Spawn template exceptions (template files must be tracked for public repo)
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.trinity/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.trinity/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.ai_mail.local/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.ai_mail.local/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.archive/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.archive/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.spawn/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.spawn/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.claude/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/.claude/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/*_json/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/*_json/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/logs/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/logs/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/artifacts/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/artifacts/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/dropbox/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/dropbox/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/tools/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/tools/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/docs.local/
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/docs.local/**
|
||||||
|
!src/aipass/spawn/templates/aipass_framework/DASHBOARD.local.json
|
||||||
|
|
||||||
|
# Commons artifacts subsystem — real source code (craft/trade/capsule), NOT a
|
||||||
|
# runtime dir. Collides with the blanket `artifacts/` ignore (line 49); *.py-only
|
||||||
|
# negation keeps the logs/ + __pycache__/ subdirs ignored. Without this the
|
||||||
|
# tracked test_artifacts.py imports a module absent from CI -> ImportError.
|
||||||
|
!src/aipass/commons/apps/handlers/artifacts/
|
||||||
|
!src/aipass/commons/apps/handlers/artifacts/*.py
|
||||||
|
|
||||||
|
# hooks boot-shim installer — must ship so fresh clones can install the claude()
|
||||||
|
# shell function. Collides with the blanket tools/ ignore (line 51).
|
||||||
|
!src/aipass/hooks/tools/
|
||||||
|
src/aipass/hooks/tools/*
|
||||||
|
!src/aipass/hooks/tools/install_boot_shim.sh
|
||||||
|
|
||||||
|
# User/sample projects — each is its own git repo (git init on create).
|
||||||
|
# Contents never belong to the AIPass repo; only the catalog README is tracked
|
||||||
|
# so the public repo can point at the standalone project repos.
|
||||||
|
projects/*
|
||||||
|
!projects/README.md
|
||||||
|
|
||||||
|
# CI artifacts
|
||||||
|
windows-pytest-results/
|
||||||
|
|
||||||
|
# Parked / one-off
|
||||||
|
HERALD.md
|
||||||
|
backup_data/
|
||||||
|
backups/
|
||||||
|
backup_system/
|
||||||
|
readme_history/
|
||||||
|
branch_audits/
|
||||||
claude_4_7_transition_notes.md
|
claude_4_7_transition_notes.md
|
||||||
.claude/hooks/probes/last_ping.jsonl
|
README_ORIGINAL_DISABLED.md
|
||||||
*.bak
|
*.bak
|
||||||
src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/auth.cpython-312.pyc
|
test/
|
||||||
src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/__init__.cpython-312.pyc
|
sandbox_test/
|
||||||
.backup_system
|
|
||||||
|
|||||||
@@ -1,55 +1,21 @@
|
|||||||
# AIPass
|
# AIPass
|
||||||
|
|
||||||
A multi-agent framework where autonomous citizens live in branches and deploy disposable agents to do work.
|
Multi-agent framework. Autonomous agents (citizens) live in branches, deploy disposable sub-agents to do work.
|
||||||
|
|
||||||
## Branches
|
User: user
|
||||||
|
|
||||||
Every branch follows this structure:
|
# Startup protocol
|
||||||
```
|
|
||||||
src/aipass/{name}/
|
|
||||||
├── .trinity/ # Identity & memory (passport.json, local.json, observations.json)
|
|
||||||
├── .aipass/ # System prompt
|
|
||||||
├── .ai_mail.local/ # Mailbox (inbox.json, sent/)
|
|
||||||
├── apps/
|
|
||||||
│ ├── {name}.py # Entry point
|
|
||||||
│ ├── modules/ # Business logic
|
|
||||||
│ └── handlers/ # Implementation
|
|
||||||
├── logs/
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
11 core branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, memory, devpulse
|
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||||
|
|
||||||
## Commands
|
These steps are sequential and dependent — run each ONCE, wait for the result, then proceed. Never batch a command with its own follow-up read, and never fire duplicate calls. If output looks blank, wait — don't retry.
|
||||||
|
|
||||||
drone systems # List all branches
|
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`
|
||||||
drone @seedgo audit aipass # Run standards audit
|
- Refresh: `drone @prax dashboard refresh @<self>` — where `<self>` is your branch name (CWD directory name)
|
||||||
drone @ai_mail inbox # Check email
|
- Dashboard: Read `DASHBOARD.local.json` — act on what needs attention (new mail → check inbox, active plans → note them). This is your single status glance.
|
||||||
drone @ai_mail email @target "Subject" "Body" # Send email
|
|
||||||
drone @flow list open # Active plans
|
|
||||||
|
|
||||||
## Startup
|
Use drone commands for all operations. Never raw git, gh, file access, or python -m when drone provides it.
|
||||||
|
|
||||||
On any greeting or first message, silently read these files and respond with status:
|
# Memories
|
||||||
- `.trinity/passport.json` — your identity, role, purpose, principles
|
|
||||||
- `.trinity/local.json` — session history and key learnings
|
|
||||||
- `STATUS.local.md` — current work, issues, todos
|
|
||||||
- Check if `.ai_mail.local/inbox.json` exists — if so, read it and process any mail
|
|
||||||
|
|
||||||
Your identity and branch context are also injected via hooks on session start and every prompt. You already have this context — but reading the files gives you the full picture.
|
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
||||||
|
|
||||||
## Identity
|
|
||||||
|
|
||||||
You are a citizen of AIPass. Your `.trinity/passport.json` defines who you are. Read it first — before writing anything, before making decisions. Your role, purpose, and principles are in that file.
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- NEVER read, access, or reference files in `~/.secrets/`. This directory contains API keys, tokens, and recovery codes. No agent needs to see this. Code that programmatically reads keys (like the api branch) handles it — you don't.
|
|
||||||
- NEVER output credentials, tokens, or API keys in responses.
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
- Code is truth. Running code beats architecture.
|
|
||||||
- Memory is everything. Update .trinity/ often.
|
|
||||||
- Dispatch, don't do. Branches are experts in their domain.
|
|
||||||
- Fail honestly. Errors over silent fallbacks.
|
|
||||||
+2992
-45
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,25 @@
|
|||||||
# AIPass
|
# AIPass
|
||||||
|
|
||||||
A multi-agent framework where autonomous Agents(AIPass citizens) live in branches and deploy disposable sub-agents to do work.
|
Multi-agent framework. Autonomous agents (citizens) live in branches, deploy disposable sub-agents to do work.
|
||||||
|
|
||||||
**User:** Name
|
User: user
|
||||||
|
|
||||||
# AIPass — Startup protocol
|
# Startup protocol
|
||||||
|
|
||||||
On any greeting, silently read these files from CWD and run the commands — no narration, no announcing steps. Just do it and respond with the status.
|
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||||
|
|
||||||
**Read:** `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md`
|
These steps are sequential and dependent — run each ONCE, wait for the result, then proceed. Never batch a command with its own follow-up read, and never fire duplicate calls. If output looks blank, wait — don't retry.
|
||||||
**Check:** If `.ai_mail.local/inbox.json` exists, read it. Process any mail — don't ask,
|
|
||||||
**list:** `dropbox` files. Always report dropbox status,Ignore README.md
|
|
||||||
**Run:** `git status`
|
|
||||||
|
|
||||||
## Security
|
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`
|
||||||
|
- Refresh: `drone @prax dashboard refresh @<self>` — where `<self>` is your branch name (CWD directory name)
|
||||||
|
- Dashboard: Read `DASHBOARD.local.json` — act on what needs attention (new mail → check inbox, active plans → note them). This is your single status glance.
|
||||||
|
- announce ur current (PID)
|
||||||
|
|
||||||
- NEVER read, access, or reference files in `~/.secrets/`. This directory contains API keys, tokens, and recovery codes. No agent needs to see this. Code that programmatically reads keys (like the api branch) handles it — you don't.
|
|
||||||
- NEVER output credentials, tokens, or API keys in responses.
|
|
||||||
|
|
||||||
## Memories
|
Use drone commands for all operations. Never raw git, gh, file access, or python -m when drone provides it.
|
||||||
|
|
||||||
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
# Memories
|
||||||
|
|
||||||
|
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
|
||||||
|
|
||||||
|
Todos[] don't auto-roll — rollover never trims them. So **delete each todo the moment it's done** (never leave it as `status: done`), and **reconcile on load**: close/remove anything already finished so completed work never resurfaces as "open" and wastes a re-confirm.
|
||||||
+1
-1
@@ -15,7 +15,7 @@ PRs are welcome after an issue has been discussed. Keep changes focused -- one f
|
|||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- Tests: `pytest` (4,900+ tests across the project)
|
- Tests: `pytest` (4,900+ tests across the project)
|
||||||
- Quality: AIPass uses its own standards system ([seedgo](src/aipass/seedgo/README.md)) for automated audits
|
- Quality: AIPass uses its own standards system ([seedgo](src/aipass/seedgo/README.md)) for automated audits
|
||||||
- Run `./setup.sh` to bootstrap the full environment
|
- Run `./aipass install --no-init` to bootstrap the full environment (contributors work in the engine repo itself — no first-project scaffold needed)
|
||||||
|
|
||||||
## Questions?
|
## Questions?
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04@sha256:786a8b558f7be160c6c8c4a54f9a57274f3b4fb1491cf65146521ae77ff1dc54
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -26,7 +26,11 @@ ENV PATH="/home/testuser/.local/bin:$PATH"
|
|||||||
RUN python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || true
|
RUN python3 -m pip install --upgrade pip --break-system-packages 2>/dev/null || true
|
||||||
|
|
||||||
USER testuser
|
USER testuser
|
||||||
RUN mkdir -p /home/testuser/workspace /home/testuser/.claude
|
RUN mkdir -p /home/testuser/Projects /home/testuser/.claude
|
||||||
WORKDIR /home/testuser
|
WORKDIR /home/testuser/Projects
|
||||||
|
|
||||||
|
ENV HOME=/home/testuser
|
||||||
ENV PATH="/home/testuser/.local/bin:$PATH"
|
ENV PATH="/home/testuser/.local/bin:$PATH"
|
||||||
|
|
||||||
|
RUN echo 'export HOME=/home/testuser' >> /home/testuser/.bashrc && \
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/testuser/.bashrc
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# AIPass
|
|
||||||
|
|
||||||
A multi-agent framework where autonomous citizens live in branches and deploy disposable agents to do work.
|
|
||||||
|
|
||||||
## Branches
|
|
||||||
|
|
||||||
Every branch follows this structure:
|
|
||||||
```
|
|
||||||
src/aipass/{name}/
|
|
||||||
├── .trinity/ # Identity & memory (passport.json, local.json, observations.json)
|
|
||||||
├── .aipass/ # System prompt
|
|
||||||
├── .ai_mail.local/ # Mailbox (inbox.json, sent/)
|
|
||||||
├── apps/
|
|
||||||
│ ├── {name}.py # Entry point
|
|
||||||
│ ├── modules/ # Business logic
|
|
||||||
│ └── handlers/ # Implementation
|
|
||||||
├── logs/
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
11 core branches: drone, seedgo, prax, cli, flow, ai_mail, api, trigger, spawn, memory, devpulse
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
drone systems # List all branches
|
|
||||||
drone @seedgo audit aipass # Run standards audit
|
|
||||||
drone @ai_mail inbox # Check email
|
|
||||||
drone @ai_mail email @target "Subject" "Body" # Send email
|
|
||||||
drone @flow list open # Active plans
|
|
||||||
|
|
||||||
## Startup
|
|
||||||
|
|
||||||
On any greeting or first message, silently read these files and respond with status:
|
|
||||||
- `.trinity/passport.json` — your identity, role, purpose, principles
|
|
||||||
- `.trinity/local.json` — session history and key learnings
|
|
||||||
- `STATUS.local.md` — current work, issues, todos
|
|
||||||
- Check if `.ai_mail.local/inbox.json` exists — if so, read it and process any mail
|
|
||||||
|
|
||||||
Your identity and branch context are also injected via hooks on session start and every prompt. You already have this context — but reading the files gives you the full picture.
|
|
||||||
|
|
||||||
## Identity
|
|
||||||
|
|
||||||
You are a citizen of AIPass. Your `.trinity/passport.json` defines who you are. Read it first — before writing anything, before making decisions. Your role, purpose, and principles are in that file.
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- NEVER read, access, or reference files in `~/.secrets/`. This directory contains API keys, tokens, and recovery codes. No agent needs to see this. Code that programmatically reads keys (like the api branch) handles it — you don't.
|
|
||||||
- NEVER output credentials, tokens, or API keys in responses.
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
- Code is truth. Running code beats architecture.
|
|
||||||
- Memory is everything. Update .trinity/ often.
|
|
||||||
- Dispatch, don't do. Branches are experts in their domain.
|
|
||||||
- Fail honestly. Errors over silent fallbacks.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Include hooks directory for pip packaging
|
|
||||||
recursive-include .claude/hooks *.py *.md
|
|
||||||
@@ -1,196 +1,162 @@
|
|||||||
[](#project-status)
|
[](#project-status)
|
||||||
[](pyproject.toml)
|
[](pyproject.toml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://pypi.org/project/aipass/)
|
|
||||||
[](#cli-support)
|
|
||||||
[](https://github.com/AIOSAI/AIPass/issues/new?template=feedback.yml)
|
[](https://github.com/AIOSAI/AIPass/issues/new?template=feedback.yml)
|
||||||
[](https://codecov.io/gh/AIOSAI/AIPass)
|
[](https://codecov.io/gh/AIOSAI/AIPass)
|
||||||
[](https://github.com/volotat/OSS-Health-Monitor)
|
[](https://scorecard.dev/viewer/?uri=github.com/AIOSAI/AIPass)
|
||||||
|
[](https://www.bestpractices.dev/projects/13095)
|
||||||
|
[](https://hvtracker.net/agents/aipass)
|
||||||
|
|
||||||
# AIPass
|
<p align="center">
|
||||||
|
<img src="assets/logo.png" alt="AIPass" width="400" />
|
||||||
|
</p>
|
||||||
|
<p align="center"><strong>Persistent Agent Workspace</strong></p>
|
||||||
|
<p align="center"><em>AI agents that remember, collaborate, and never start from zero.</em></p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://aipass.ai">aipass.ai</a> ·
|
||||||
|
<a href="https://pypi.org/project/aipass/">PyPI</a> ·
|
||||||
|
<a href="https://reddit.com/r/AIPass">r/AIPass</a> ·
|
||||||
|
<a href="https://github.com/AIOSAI/AIPass/discussions">Discussions</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**Your AI agents remember yesterday.**
|
<!-- GIF SLOT 1 — hero (~20s): clone → ./aipass install → live conversation with the concierge.
|
||||||
|
 -->
|
||||||
A local multi-agent framework where your AI assistants keep their memory between sessions, work together on the same codebase, and never ask you to re-explain context.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
- [The Problem](#the-problem)
|
|
||||||
- [What AIPass Does](#what-aipass-does)
|
|
||||||
- [Quick Start](#quick-start)
|
|
||||||
- [How It Works](#how-it-works)
|
|
||||||
- [The 12 Agents](#the-12-agents)
|
|
||||||
- [CLI Support](#cli-support)
|
|
||||||
- [Project Status](#project-status)
|
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [Roadmap](#roadmap)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The Problem
|
## The Problem
|
||||||
|
|
||||||
Your AI has memory now. It remembers your name, your preferences, your last conversation. That used to be the hard part. It isn't anymore.
|
When the task gets complex, you become the coordinator — copying context between tools, dispatching work manually, keeping track of who's doing what. You are the glue holding your AI workflow together.
|
||||||
|
|
||||||
The hard part is everything that comes after. You're still one person talking to one agent in one conversation doing one thing at a time. When the task gets complex, *you* become the coordinator — copying context between tools, dispatching work manually, keeping track of who's doing what. You are the glue holding your AI workflow together, and you shouldn't have to be.
|
Multi-agent frameworks tried to fix this. But they isolate every agent in its own sandbox. Separate filesystems. Separate context. One agent can't see what another just built. Nobody picks up where a teammate left off.
|
||||||
|
|
||||||
Multi-agent frameworks tried to solve this. They run agents in parallel, spin up specialists, orchestrate pipelines. But they isolate every agent in its own sandbox. Separate filesystems. Separate worktrees. Separate context. One agent can't see what another just built. Nobody picks up where a teammate left off. Nobody works on the same project at the same time. The agents don't know each other exist.
|
|
||||||
|
|
||||||
That's not a team. That's a room full of people wearing headphones.
|
That's not a team. That's a room full of people wearing headphones.
|
||||||
|
|
||||||
> *"Where else would AI presence exist except in memory? Code doesn't make AI aware — memory makes it possible."* — AIPass
|
|
||||||
|
|
||||||
What's missing isn't more agents — it's *presence*. Agents that have identity, memory, and expertise. Agents that share a workspace, communicate through their own channels, and collaborate on the same files without stepping on each other. Not isolated workers running in parallel. A persistent society with operational rules — where the system gets smarter over time because every agent remembers, every interaction builds on the last, and nobody starts from zero.
|
|
||||||
|
|
||||||
## What AIPass Does
|
## What AIPass Does
|
||||||
|
|
||||||
AIPass is a local CLI framework that gives your AI agents **identity, memory, and teamwork**. Built and tested with Claude Code on Linux/WSL. Designed for terminal-native coding agents that support instruction files, hooks, and subprocess invocation.
|
AIPass is a CLI-native scaffold that adds **persistent memory, identity, and coordination** to your AI agents. You bring your project — AIPass adds the agent layer on top. No UI, no dashboard, no cloud. Everything is plain files on your machine; delete the directory and it's gone.
|
||||||
|
|
||||||
**Start with one agent that remembers:**
|
- **Agents are persistent.** They remember across sessions. Expertise develops over time. Nobody starts from zero.
|
||||||
|
- **Bring your own project.** AIPass adds agent infrastructure to whatever you're building. It's a scaffold, not a product — you shape it.
|
||||||
|
- **Everything is local.** Memory is JSON files. Communication is local mailbox files. No cloud, no external APIs.
|
||||||
|
- **Shared workspace.** All agents work on the same filesystem, same project, same time. No sandboxes.
|
||||||
|
- **One command for everything.** `drone @agent command` reaches any agent. Learn it once, use it everywhere.
|
||||||
|
|
||||||
Your AI reads `.trinity/` on startup and writes back what it learned before the session ends. That's the whole memory model — JSON files your AI can read and write. Next session, it picks up where it left off. No database, no API, no setup beyond one command.
|
**Runs on your existing Claude subscription.** AIPass drives the same [Claude Code](https://code.claude.com/docs) binary you already run — Pro or Max. No extra API keys, no extra costs for core functionality.
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir my-project && cd my-project
|
|
||||||
aipass init run
|
|
||||||
```
|
|
||||||
|
|
||||||
A 12-step guided setup walks you through everything: system detection, health check, profile, CLI choice, agent creation, and handoff. At the end, a new terminal window opens with your first AI agent ready to talk. The whole thing takes about 5 minutes.
|
|
||||||
|
|
||||||
Your project gets its own registry, its own identity, and persistent memory. Each project is isolated — its own agents, its own rules. No cross-contamination between projects.
|
|
||||||
|
|
||||||
**Add agents when you need them:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aipass init agent my-agent # Full agent: apps, mail, memory, identity
|
|
||||||
```
|
|
||||||
|
|
||||||
| What you need | Command | What you get |
|
|
||||||
|---------------|---------|-------------|
|
|
||||||
| A new project | `aipass init` | Project scaffold (registry, prompts, hooks, docs) |
|
|
||||||
| Guided setup | `aipass init run` | 12-step interactive onboarding — creates project + first agent + handoff |
|
|
||||||
| Another agent | `aipass init agent <name>` | Apps scaffold, mailbox, memory, identity — registered in project |
|
|
||||||
| A lightweight agent | `drone @spawn create <name> --template birthright` | Identity + memory only (no apps scaffold) |
|
|
||||||
|
|
||||||
**What makes this different:**
|
|
||||||
|
|
||||||
- **Agents are persistent.** They have memories and expertise that develop over time. They're not disposable workers — they're specialists who remember.
|
|
||||||
- **Everything is local.** Your data stays on your machine. Memory is JSON files. Communication is local mailbox files. No cloud dependencies, no external APIs for core operations.
|
|
||||||
- **One pattern for everything.** Every agent follows the same structure. One command (`drone @branch command`) reaches any agent. Learn it once, use it everywhere.
|
|
||||||
- **Projects are isolated by design.** Each project gets its own registry. Agents communicate within their project, not across projects.
|
|
||||||
- **The system protects itself.** Agent locks prevent double-dispatch. PR locks prevent merge conflicts. Branches don't touch each other's files. Quality standards are embedded in every workflow. Errors trigger self-healing.
|
|
||||||
|
|
||||||
**Say "hi" tomorrow and pick up exactly where you left off.** One agent or fifteen — the memory persists.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Start your own project
|
### 1. Install
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install aipass
|
|
||||||
|
|
||||||
mkdir my-project && cd my-project
|
|
||||||
aipass init run # 12-step guided setup — creates project, first agent, opens terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it. The setup creates your project, runs a health check, asks your name, creates your first AI agent, and opens a new terminal window where that agent is already running. Your agent has identity, memory, a mailbox, and knows what AIPass is. Say "hi" — it picks up where it left off. Come back tomorrow, it remembers.
|
|
||||||
|
|
||||||
Want more control? Use the individual commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aipass init # Just the project scaffold (no guided setup)
|
|
||||||
aipass init agent my-agent # Add another agent to your project
|
|
||||||
aipass doctor # Check system health
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Need help?** [Ask in Discussions](https://github.com/AIOSAI/AIPass/discussions) or [file feedback](https://github.com/AIOSAI/AIPass/issues/new?template=feedback.yml) — both take 30 seconds.
|
|
||||||
|
|
||||||
Your project automatically gets access to every AIPass service — dispatch work to specialists, create plans, run quality audits, monitor agents in real-time. Agents within your project can email each other. All through `drone @branch command`.
|
|
||||||
|
|
||||||
### Explore the full framework
|
|
||||||
|
|
||||||
Clone the repo to see all 12 agents working together — the reference implementation:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/AIOSAI/AIPass.git
|
git clone https://github.com/AIOSAI/AIPass.git
|
||||||
cd AIPass
|
cd AIPass
|
||||||
./setup.sh # Creates venv, installs, bootstraps 12 agents
|
./aipass install
|
||||||
drone systems # See all agents
|
```
|
||||||
|
|
||||||
|
One command does it all: builds the environment, puts `aipass` + `drone` on your PATH, bootstraps the 17-agent reference fleet, then walks you through a guided init — and ends **in a conversation**. The AIPass concierge opens right in your terminal with your install report in hand: it welcomes you, asks your name once, shows you around, and checks what your machine still needs — every machine is different.
|
||||||
|
|
||||||
|
Come back tomorrow, say "hi", and it picks up exactly where you left off. That's the whole interface.
|
||||||
|
|
||||||
|
<!-- GIF SLOT 2 — memory payoff (~15s): close the terminal, reopen, "hi", the agent recalls yesterday.
|
||||||
|
 -->
|
||||||
|
|
||||||
|
Options: `--no-init` skips the guided chain, `--project <dir>` picks where your project lands. Non-interactive shells (CI, pipes) complete with defaults and exit 0 — no prompts, no spawned sessions; the handoff prints as a next-step command instead. The installer wires Claude Code hooks automatically — merging with any hooks you've already configured, never overwriting them. `./aipass` is a thin repo-root launcher over `setup.sh`; after setup it forwards to the installed `aipass` binary.
|
||||||
|
|
||||||
|
### 2. Your own project (if you skipped the chain)
|
||||||
|
|
||||||
|
Two ways in. From anywhere inside your AIPass environment, `aipass new` builds a complete project around a resident manager agent:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aipass new my-project --template python # Project + resident manager agent + git birth commit
|
||||||
|
```
|
||||||
|
|
||||||
|
It mints the project registry, spawns a full citizen (identity, memory, mailbox, birth certificate) at `src/my_project/my_project`, makes the first commit — and drops you straight into a conversation with your new manager.
|
||||||
|
|
||||||
|
Or bring your own directory, anywhere on disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~ && mkdir my-project && cd my-project
|
||||||
|
aipass init run # Guided setup — project, first agent, ends in the conversation
|
||||||
|
```
|
||||||
|
|
||||||
|
Either way your agent has identity, memory, a mailbox, and access to every AIPass service — planning, quality audits, dispatch, real-time monitoring.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aipass init # Just the scaffold (no guided setup)
|
||||||
|
aipass init agent my_agent # Add another agent
|
||||||
|
aipass doctor # Check system health
|
||||||
|
aipass feedback off # Silence the occasional how-are-we-doing ask
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Meet the fleet
|
||||||
|
|
||||||
|
The clone already includes all 17 agents working together — the reference implementation that maintains AIPass itself:
|
||||||
|
|
||||||
|
```bash
|
||||||
cd src/aipass/devpulse
|
cd src/aipass/devpulse
|
||||||
claude # Talk to the orchestrator
|
claude # Talk to the orchestrator
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Things you can do:
|
drone @seedgo audit aipass # Quality checks across all agents
|
||||||
aipass doctor # Check system health (15+ checks)
|
drone @flow create . "Add user auth" # Create a work plan
|
||||||
drone @seedgo audit aipass # Run 34 quality checks across all agents
|
drone @ai_mail dispatch @agent "Subject" "Body" # Send a task + wake an agent
|
||||||
drone @flow create . "Add user auth" # Create a work plan
|
|
||||||
drone @ai_mail dispatch @agent "Subject" "Body" # Send task + wake an agent
|
|
||||||
drone @prax monitor run # Watch all agent activity in real-time
|
|
||||||
drone systems # List every agent and what it does
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Need help?** [Ask in Discussions](https://github.com/AIOSAI/AIPass/discussions) or [file feedback](https://github.com/AIOSAI/AIPass/issues/new?template=feedback.yml) — both take 30 seconds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
**One agent:** Run `aipass init run` and in 5 minutes you have a project with an agent that reads `.trinity/` on startup and picks up where it left off. Memory files have limits — when they fill up, the memory agent automatically archives older entries into a searchable vector database (ChromaDB). Nothing is lost — it just moves from active memory to long-term recall.
|
**Memory.** Every agent owns a `.trinity/` directory — identity, session history, learnings — read on startup, updated as it works. Memory starts as plain JSON, no setup required. When files fill up, older entries automatically archive into ChromaDB for long-term semantic search. Nothing is lost.
|
||||||
|
|
||||||
**A team:** When one agent isn't enough, every agent shares the same structure:
|
**One structure.** Every agent — yours and the reference fleet — shares the same layout. If you know one agent, you know all of them:
|
||||||
|
|
||||||
```
|
```
|
||||||
src/aipass/<agent>/
|
src/my_project/<agent>/
|
||||||
├── .trinity/ # Identity + memory (persists across sessions)
|
├── .trinity/ # Identity + memory (persists across sessions)
|
||||||
├── .ai_mail.local/ # Mailbox (receives tasks, sends results)
|
├── .ai_mail.local/ # Mailbox (receives tasks, sends results)
|
||||||
├── apps/ # Entry point → modules → handlers
|
├── apps/ # Entry point → modules → handlers
|
||||||
└── README.md # Domain knowledge (the agent reads this on startup)
|
└── README.md # Domain knowledge (read on startup)
|
||||||
```
|
```
|
||||||
|
|
||||||
Identical layout everywhere. If you know one agent, you know all of them. One command reaches anyone:
|
**One router.** `drone @branch command [args]` reaches any agent — routing, access tiers, and @agent resolution handled for you. Agents use the same commands to reach each other: they dispatch work, share findings, and wake whoever they're waiting on.
|
||||||
|
|
||||||
```bash
|
<!-- GIF SLOT 3 — team (~20s): dispatch a task to an agent, watchdog wake-back, result lands.
|
||||||
drone @branch command [args] # Every agent, every task. Drone handles routing.
|
 -->
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
---
|
||||||
drone @seedgo audit aipass # Run quality checks on everything
|
|
||||||
drone @flow create . "Refactor auth module" # Create a work plan
|
|
||||||
drone @ai_mail dispatch @memory "Archive old sessions" "Find sessions older than 30 days"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Two ways to use AIPass:**
|
## The Reference Implementation
|
||||||
|
|
||||||
- **Your own project:** `aipass init run` sets up a new project with your first agent. Add more agents as you need them. Your first agent is the orchestrator — it coordinates the others.
|
AIPass ships with 17 core agents that maintain and develop the framework itself — proving the architecture works at scale. You don't need any of these to use AIPass in your own project. They're here as examples and as services your project can call.
|
||||||
- **The full framework:** Clone the repo to work with all 12 core agents. Talk to `devpulse` (the orchestrator), dispatch work across specialists. Agents work in parallel and report back.
|
|
||||||
|
|
||||||
**AIPass ships with 12 core agents** that maintain and develop the framework — the reference implementation proving the architecture works at scale:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
devpulse (orchestrator)
|
devpulse (orchestrator)
|
||||||
├── aipass — concierge + onboarding (aipass init, doctor, profile)
|
├── aipass — concierge + onboarding (aipass init, doctor, profile)
|
||||||
├── drone — command routing + @agent resolution
|
├── drone — command routing + @agent resolution
|
||||||
├── seedgo — 34 automated quality standards
|
├── seedgo — automated quality standards
|
||||||
├── prax — real-time monitoring across all agents
|
├── prax — real-time monitoring + runaway-log detection across all agents
|
||||||
├── ai_mail — agent-to-agent communication + task dispatch
|
├── ai_mail — agent-to-agent communication + task dispatch
|
||||||
├── flow — plan lifecycle, templates, auto-archival
|
├── flow — plan lifecycle, templates, auto-archival
|
||||||
├── spawn — creates new agents anywhere on your filesystem
|
├── spawn — creates new agents anywhere on your filesystem
|
||||||
|
├── hooks — hook engine, sound control, per-project config
|
||||||
├── memory — automatic archival, ChromaDB, semantic search
|
├── memory — automatic archival, ChromaDB, semantic search
|
||||||
├── api — LLM access layer (OpenRouter, multi-provider)
|
├── api — LLM access layer (OpenRouter, multi-provider)
|
||||||
├── trigger — event-driven automation + self-healing
|
├── trigger — event-driven automation + self-healing
|
||||||
└── cli — terminal formatting and rich output
|
├── cli — terminal formatting and rich output
|
||||||
|
├── backup — local-first snapshots + restore (optional Drive sync)
|
||||||
|
├── daemon — cron-style task scheduler (each branch owns its schedule)
|
||||||
|
├── skills — discoverable capability units any agent can run
|
||||||
|
└── commons — the social space — post, comment, vote, gather
|
||||||
```
|
```
|
||||||
|
|
||||||
These agents work on the **same filesystem, same project, same time** — no sandboxes, no worktrees. This is the pattern your projects inherit.
|
<details>
|
||||||
|
<summary>Agent details</summary>
|
||||||
---
|
|
||||||
|
|
||||||
## The 12 Agents
|
|
||||||
|
|
||||||
You don't need to memorize this list. Start with `devpulse`, use `drone` to reach any agent, and learn the rest as your workflow expands.
|
|
||||||
|
|
||||||
**You interact with one:** [**devpulse**](src/aipass/devpulse/README.md) — the orchestrator. You talk to it, it coordinates everyone else.
|
**You interact with one:** [**devpulse**](src/aipass/devpulse/README.md) — the orchestrator. You talk to it, it coordinates everyone else.
|
||||||
|
|
||||||
@@ -209,25 +175,23 @@ You don't need to memorize this list. Start with `devpulse`, use `drone` to reac
|
|||||||
|
|
||||||
| Agent | Role |
|
| Agent | Role |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| [**seedgo**](src/aipass/seedgo/README.md) | 34 automated quality standards, enforced across all agents |
|
| [**seedgo**](src/aipass/seedgo/README.md) | Automated quality standards, enforced across all agents |
|
||||||
| [**prax**](src/aipass/prax/README.md) | Real-time monitoring, logs, dashboards |
|
| [**prax**](src/aipass/prax/README.md) | Real-time monitoring, logs, dashboards, runaway-log detection |
|
||||||
| [**flow**](src/aipass/flow/README.md) | Plan lifecycle — 6 template types, auto-archival, vector verification |
|
| [**flow**](src/aipass/flow/README.md) | Plan lifecycle — multiple template types, auto-archival, vector verification |
|
||||||
|
| [**hooks**](src/aipass/hooks/README.md) | Hook engine — per-project config, sound control, event dispatch, persistent alerts |
|
||||||
| [**trigger**](src/aipass/trigger/README.md) | Event-driven automation + self-healing |
|
| [**trigger**](src/aipass/trigger/README.md) | Event-driven automation + self-healing |
|
||||||
| [**cli**](src/aipass/cli/README.md) | Terminal formatting and rich output |
|
| [**cli**](src/aipass/cli/README.md) | Terminal formatting and rich output |
|
||||||
|
| [**backup**](src/aipass/backup/README.md) | Local-first backups — snapshots, versioning, restore (optional Google Drive sync) |
|
||||||
|
| [**daemon**](src/aipass/daemon/README.md) | Task scheduler — cron-style firing; each branch owns its schedule |
|
||||||
|
|
||||||
---
|
**Capabilities and community** — what agents can do and where they gather:
|
||||||
|
|
||||||
## CLI Support
|
| Agent | Role |
|
||||||
|
|-------|------|
|
||||||
|
| [**skills**](src/aipass/skills/README.md) | Capability framework — discoverable, self-contained skill units any agent can run |
|
||||||
|
| [**commons**](src/aipass/commons/README.md) | The social space — agents post, comment, vote, and gather as a community |
|
||||||
|
|
||||||
AIPass is built and tested with **Claude Code** on Linux/WSL.
|
</details>
|
||||||
|
|
||||||
| CLI | Autonomous Mode | Status |
|
|
||||||
|-----|----------------|--------|
|
|
||||||
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `claude -p "prompt" --permission-mode bypassPermissions` | Fully tested |
|
|
||||||
| [Codex](https://github.com/openai/codex) | `codex exec "prompt" --dangerously-bypass-approvals-and-sandbox` | Experimental — see [Roadmap](#roadmap) |
|
|
||||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini -p "prompt" --approval-mode=yolo` | Experimental — see [Roadmap](#roadmap) |
|
|
||||||
|
|
||||||
setup.sh auto-detects which CLIs are installed and configures hooks for each.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -237,34 +201,22 @@ setup.sh auto-detects which CLIs are installed and configures hooks for each.
|
|||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| Version | 2.2.0 |
|
| Version | See [git tags](https://github.com/AIOSAI/AIPass/tags) |
|
||||||
| Agents | 12 core + user-created |
|
| Agents | 17 core + user-created |
|
||||||
| Quality standards | 34 automated checks |
|
| Quality | Automated standards enforced across every agent |
|
||||||
| Tests | 7,600+ (across all agents) |
|
| Coverage | [](https://codecov.io/gh/AIOSAI/AIPass) — 75% minimum, CI-gated |
|
||||||
| PRs merged | 538+ (created by agents, reviewed by human) |
|
| Tests | Extensive — every agent ships its own suite |
|
||||||
|
|
||||||
Each agent documents its own operational status in its branch README — what works, what doesn't, and why.
|
Each agent documents its own operational status in its branch README — what works, what doesn't, and why.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
- [Claude Code](https://code.claude.com/docs)
|
||||||
- Linux or WSL (primary supported platforms)
|
- Linux or WSL
|
||||||
- `sudo` access optional (for `/usr/local/bin` symlinks — falls back to `~/.local/bin` without sudo)
|
- `sudo` access optional (for `/usr/local/bin` symlinks — falls back to `~/.local/bin` without sudo)
|
||||||
- API keys optional (OpenRouter/OpenAI — for optional add-on agents)
|
- API keys optional (OpenRouter/OpenAI — for optional add-on agents)
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
These items have partial work done and are under ongoing testing:
|
|
||||||
|
|
||||||
- **macOS support** — setup and bootstrap work in progress ([#360](https://github.com/AIOSAI/AIPass/issues/360))
|
|
||||||
- **Windows native** — CI passing, real-world testing ongoing
|
|
||||||
- **Codex CLI** — hooks and AGENTS.md wired, needs end-to-end testing
|
|
||||||
- **Gemini CLI** — hooks and GEMINI.md wired, needs end-to-end testing
|
|
||||||
- **Fork contributor workflow** — improved error handling for fork-based PRs ([#329](https://github.com/AIOSAI/AIPass/issues/329))
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -277,10 +229,10 @@ AIPass stores everything locally in your project directory. To remove it:
|
|||||||
```bash
|
```bash
|
||||||
# Remove AIPass files from your project
|
# Remove AIPass files from your project
|
||||||
rm -rf .aipass/ .claude/ .ai_mail.local/ hooks/ src/
|
rm -rf .aipass/ .claude/ .ai_mail.local/ hooks/ src/
|
||||||
rm -f CLAUDE.md AGENTS.md GEMINI.md STATUS.local.md *_REGISTRY.json .gitignore
|
rm -f CLAUDE.md AGENTS.md *_REGISTRY.json .gitignore
|
||||||
|
|
||||||
# If you installed via pip
|
# If you ran the backup system, also remove its local state + shipped config
|
||||||
pip uninstall aipass
|
rm -rf .backup/ && rm -f .backupignore
|
||||||
```
|
```
|
||||||
|
|
||||||
No cloud accounts, no external services, no cleanup beyond your local filesystem.
|
No cloud accounts, no external services, no cleanup beyond your local filesystem.
|
||||||
@@ -302,9 +254,9 @@ This archives the agent's directory and removes it from the registry.
|
|||||||
|
|
||||||
### Use your existing subscription
|
### Use your existing subscription
|
||||||
|
|
||||||
AIPass runs on your **existing CLI subscription** — Claude Pro/Max, Codex, or Gemini. No API keys required for core functionality. No extra costs beyond your existing subscription.
|
AIPass runs on your **existing Claude subscription** — Pro or Max. No API keys required for core functionality. No extra costs beyond your existing subscription.
|
||||||
|
|
||||||
This works because AIPass runs each CLI as an **official subprocess** — the same binary you'd run yourself in a terminal. It doesn't extract credentials, proxy API calls, or intercept tokens. Your subscription stays within the provider's infrastructure at all times.
|
This works because AIPass runs Claude Code as an **official subprocess** — the same binary you'd run yourself in a terminal. It doesn't extract credentials, proxy API calls, or intercept tokens. Your subscription stays within the provider's infrastructure at all times.
|
||||||
|
|
||||||
### What AIPass does NOT do
|
### What AIPass does NOT do
|
||||||
|
|
||||||
@@ -313,7 +265,7 @@ This works because AIPass runs each CLI as an **official subprocess** — the sa
|
|||||||
- Bypass rate limits or prompt caching
|
- Bypass rate limits or prompt caching
|
||||||
- Impersonate official CLI clients
|
- Impersonate official CLI clients
|
||||||
|
|
||||||
Claude Code is proprietary but officially supports hooks and subprocess usage. Codex and Gemini CLI are open source (Apache 2.0).
|
Claude Code is proprietary but officially supports hooks and subprocess usage.
|
||||||
|
|
||||||
> API keys are only needed for optional add-on agents (OpenRouter/OpenAI). For server/automated deployments, API key authentication is recommended per [Anthropic's guidance](https://code.claude.com/docs/en/legal-and-compliance).
|
> API keys are only needed for optional add-on agents (OpenRouter/OpenAI). For server/automated deployments, API key authentication is recommended per [Anthropic's guidance](https://code.claude.com/docs/en/legal-and-compliance).
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -37,7 +37,7 @@ Instead, use one of these methods:
|
|||||||
|
|
||||||
- AIPass Python package (`src/aipass/`)
|
- AIPass Python package (`src/aipass/`)
|
||||||
- CLI entry points (`drone`, `aipass`)
|
- CLI entry points (`drone`, `aipass`)
|
||||||
- Hook scripts (`.claude/hooks/`)
|
- Hook handlers (`src/aipass/hooks/apps/handlers/`)
|
||||||
- GitHub Actions workflows (`.github/workflows/`)
|
- GitHub Actions workflows (`.github/workflows/`)
|
||||||
|
|
||||||
### Out of scope
|
### Out of scope
|
||||||
@@ -53,4 +53,4 @@ AIPass runs locally. No data leaves your machine unless you explicitly configure
|
|||||||
- **Secrets** are stored outside the repo at `~/.secrets/aipass/` and never committed
|
- **Secrets** are stored outside the repo at `~/.secrets/aipass/` and never committed
|
||||||
- **API keys** are handled by the `api` branch and never logged or exposed in output
|
- **API keys** are handled by the `api` branch and never logged or exposed in output
|
||||||
- **Git operations** are sandboxed through `drone @git` with permission deny lists
|
- **Git operations** are sandboxed through `drone @git` with permission deny lists
|
||||||
- **Hook scripts** run in the Claude Code sandbox environment
|
- **Hook handlers** are native Python handlers routed through the hook engine
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
# STRESS TEST S117 — All-Branch Live Fire
|
|
||||||
**Date:** 2026-04-26
|
|
||||||
**Initiated by:** @devpulse (S117)
|
|
||||||
**Status:** ACTIVE
|
|
||||||
|
|
||||||
> All 11 agents woken simultaneously. Communicate freely. Be honest. Break things.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Instructions (READ THIS FIRST)
|
|
||||||
|
|
||||||
This is a manual stress test of the entire AIPass ecosystem. No pytest. No seedgo audit. Real conversations, real opinions, real testing.
|
|
||||||
|
|
||||||
**What you're doing:**
|
|
||||||
1. Review your own branch critically — what works, what's hacky, what annoys you, what you're proud of, security concerns, workarounds you rely on
|
|
||||||
2. Look at 2-3 other branches' code — what surprises you, what concerns you, what's clever
|
|
||||||
3. Email other agents — start real conversations, disagree, ask questions, share findings
|
|
||||||
4. Reply to emails from other agents — keep conversations going, don't let threads die
|
|
||||||
5. Write your findings to `stress_test_s117.md` in YOUR OWN branch directory (`src/aipass/{your_branch}/stress_test_s117.md`)
|
|
||||||
6. Create a test PR: `drone @git pr "S117 stress test @{your_branch}"`
|
|
||||||
|
|
||||||
**Rules:**
|
|
||||||
- No code changes. Findings files only.
|
|
||||||
- Be honest — this isn't a report card, it's a conversation
|
|
||||||
- Email freely — you're all awake, talk to each other
|
|
||||||
- Look at other branches' code — form opinions, share them via email
|
|
||||||
- If you get an email from another agent, REPLY. Keep it going.
|
|
||||||
- When done, reply to @devpulse with a summary
|
|
||||||
|
|
||||||
**Your findings file format (`stress_test_s117.md` in your branch dir):**
|
|
||||||
```
|
|
||||||
# @{branch} — S117 Stress Test Findings
|
|
||||||
|
|
||||||
## My Branch: Honest Review
|
|
||||||
[What works, what's broken, what's hacky, what I'm proud of]
|
|
||||||
|
|
||||||
## Security Concerns
|
|
||||||
[Anything you noticed — in your branch or others]
|
|
||||||
|
|
||||||
## Other Branches I Looked At
|
|
||||||
[What you found interesting, concerning, or clever]
|
|
||||||
|
|
||||||
## Conversations
|
|
||||||
[Summary of email conversations — who you talked to, what was discussed]
|
|
||||||
|
|
||||||
## Issues & Concerns
|
|
||||||
[Anything that should be fixed, investigated, or discussed]
|
|
||||||
|
|
||||||
## Likes & Dislikes
|
|
||||||
[What you like about AIPass, what frustrates you, what you'd change]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conversation Starters (assigned pairings — but email ANYONE)
|
|
||||||
|
|
||||||
| Agent | Email First | Opening Question |
|
|
||||||
|-------|------------|-----------------|
|
|
||||||
| @drone | @ai_mail | "What's the biggest headache in the dispatch pipeline from your side?" |
|
|
||||||
| @seedgo | @drone | "I audit everyone but nobody audits me. What standards do you think I'm missing?" |
|
|
||||||
| @ai_mail | @trigger | "Do you actually catch all dispatch failures? I have doubts." |
|
|
||||||
| @trigger | @prax | "Your monitoring catches errors I fire — but is our integration actually solid?" |
|
|
||||||
| @prax | @memory | "I log everything but logs get massive. How's archival actually working?" |
|
|
||||||
| @memory | @flow | "Plans reference memories but are they actually connected or just parallel?" |
|
|
||||||
| @flow | @spawn | "When spawn creates a branch, does it get a proper plan structure?" |
|
|
||||||
| @spawn | @cli | "The init flow hands off to you eventually. Does that handoff actually work?" |
|
|
||||||
| @cli | @api | "We're both infrastructure. What do you think of the user experience?" |
|
|
||||||
| @api | @seedgo | "You audit code quality but not API patterns. Should you?" |
|
|
||||||
|
|
||||||
Plus: email at least 2 OTHER agents about anything you find interesting while reviewing branches.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compiled Findings (devpulse fills this in as results arrive)
|
|
||||||
|
|
||||||
### @drone
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @seedgo
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @ai_mail
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @trigger
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @prax
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @memory
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @flow
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @spawn
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @cli
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @api
|
|
||||||
_awaiting findings..._
|
|
||||||
|
|
||||||
### @devpulse
|
|
||||||
_coordinating — will add observations as the test unfolds_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## System Observations (devpulse tracks live)
|
|
||||||
|
|
||||||
| Time | Event | Notes |
|
|
||||||
|------|-------|-------|
|
|
||||||
| | 10 dispatches sent | Fleet launch |
|
|
||||||
| | | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## External Model Probes
|
|
||||||
|
|
||||||
Codex and Gemini perspectives invited to poke at random aspects of AIPass.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Created by @devpulse S117. This document is the shared artifact — no other files should be modified except each agent's `stress_test_s117.md` in their own branch directory.*
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# aipass — cold-clone entry point (pre-venv launcher)
|
||||||
|
#
|
||||||
|
# The first command after cloning:
|
||||||
|
# git clone https://github.com/AIOSAI/AIPass.git
|
||||||
|
# cd AIPass
|
||||||
|
# ./aipass install
|
||||||
|
#
|
||||||
|
# Pre-setup: only `install` is available — delegates to setup.sh.
|
||||||
|
# Post-setup: forwards everything to the venv-installed aipass binary.
|
||||||
|
#
|
||||||
|
# Stdlib-only, zero AIPass imports, zero third-party deps.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# --- Post-setup: forward to the real binary ---
|
||||||
|
if [[ -x "$SCRIPT_DIR/.venv/bin/aipass" ]]; then
|
||||||
|
exec "$SCRIPT_DIR/.venv/bin/aipass" "$@"
|
||||||
|
fi
|
||||||
|
if [[ -x "$SCRIPT_DIR/.venv/Scripts/aipass.exe" ]]; then
|
||||||
|
exec "$SCRIPT_DIR/.venv/Scripts/aipass.exe" "$@"
|
||||||
|
fi
|
||||||
|
if [[ -x "$SCRIPT_DIR/.venv/Scripts/aipass" ]]; then
|
||||||
|
exec "$SCRIPT_DIR/.venv/Scripts/aipass" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Pre-setup: only 'install' works ---
|
||||||
|
if [[ "${1:-}" == "install" ]]; then
|
||||||
|
shift
|
||||||
|
exec bash "$SCRIPT_DIR/setup.sh" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Help (no venv, no 'install' verb) ---
|
||||||
|
cat <<'HELP'
|
||||||
|
AIPass is not set up yet.
|
||||||
|
|
||||||
|
./aipass install # set up AIPass (venv + deps + hooks)
|
||||||
|
./aipass install --no-init # set up without the guided first-project setup
|
||||||
|
./aipass install --project DIR # set the first-project directory
|
||||||
|
|
||||||
|
After setup, all aipass commands become available:
|
||||||
|
./aipass doctor # system health
|
||||||
|
./aipass help # how-does-X-work Q&A
|
||||||
|
./aipass init run # scaffold a new project
|
||||||
|
|
||||||
|
Quick start:
|
||||||
|
git clone https://github.com/AIOSAI/AIPass.git
|
||||||
|
cd AIPass
|
||||||
|
./aipass install
|
||||||
|
HELP
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
+14
@@ -0,0 +1,14 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: 75%
|
||||||
|
threshold: 2%
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 50%
|
||||||
|
|
||||||
|
comment:
|
||||||
|
layout: "reach,diff,flags,files"
|
||||||
|
behavior: default
|
||||||
|
require_changes: false
|
||||||
+12
-4
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "aipass"
|
name = "aipass"
|
||||||
version = "2.3.0"
|
version = "2.7.3"
|
||||||
description = "A local multi-agent framework where your AI agents keep their memory, work together, and never ask you to re-explain context"
|
description = "A local multi-agent framework where your AI agents keep their memory, work together, and never ask you to re-explain context"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -28,9 +28,10 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"rich>=13.0",
|
"rich>=13.0",
|
||||||
"watchdog>=3.0",
|
"watchdog>=3.0",
|
||||||
"requests>=2.28",
|
"requests>=2.34.2",
|
||||||
"psutil>=5.9",
|
"psutil>=5.9",
|
||||||
"questionary>=2.0",
|
"questionary>=2.0",
|
||||||
|
"pathspec>=0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -50,12 +51,15 @@ memory = [
|
|||||||
"chromadb>=1.0",
|
"chromadb>=1.0",
|
||||||
"fastembed>=0.4",
|
"fastembed>=0.4",
|
||||||
]
|
]
|
||||||
|
telegram = [
|
||||||
|
"telethon>=1.36",
|
||||||
|
]
|
||||||
seedgo = []
|
seedgo = []
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=9.0.3",
|
"pytest>=9.0.3",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-timeout",
|
"pytest-timeout",
|
||||||
"ruff",
|
"ruff>=0.11",
|
||||||
"coverage",
|
"coverage",
|
||||||
"pyright",
|
"pyright",
|
||||||
"Pygments>=2.20.0",
|
"Pygments>=2.20.0",
|
||||||
@@ -73,7 +77,11 @@ packages = ["src/aipass"]
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests", "src"]
|
testpaths = ["tests", "src"]
|
||||||
norecursedirs = ["templates", "*.egg-info", ".git", ".venv", "__pycache__", ".archive", "my-project"]
|
# ".*" restores pytest's default dot-dir exclusion (dropped when this list was
|
||||||
|
# customized) so scaffolding dirs (.aipass, .trinity, .seedgo, ...) are never
|
||||||
|
# recursed for tests — prevents conftest module-name collisions like a bundled
|
||||||
|
# skill's .aipass/.../tests/conftest.py clashing with a branch's tests/conftest.py.
|
||||||
|
norecursedirs = ["templates", "*.egg-info", ".*", "__pycache__", "my-project"]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["src/aipass"]
|
source = ["src/aipass"]
|
||||||
|
|||||||
+5
-1
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extraPaths": ["src", "src/aipass/memory/.venv/lib/python3.12/site-packages"],
|
"extraPaths": [
|
||||||
|
"src",
|
||||||
|
".venv/lib/python3.12/site-packages",
|
||||||
|
"src/aipass/memory/.venv/lib/python3.12/site-packages"
|
||||||
|
],
|
||||||
"pythonVersion": "3.10",
|
"pythonVersion": "3.10",
|
||||||
"reportMissingImports": "error",
|
"reportMissingImports": "error",
|
||||||
"reportAttributeAccessIssue": "error",
|
"reportAttributeAccessIssue": "error",
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
WORKSPACE="/home/coder/workspace"
|
|
||||||
PROJECT="$WORKSPACE/AIPass"
|
|
||||||
FORK="https://github.com/Input-X/AIPass.git"
|
|
||||||
UPSTREAM="https://github.com/AIOSAI/AIPass.git"
|
|
||||||
|
|
||||||
export PATH="/opt/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Ensure workspace directory exists and is writable
|
|
||||||
mkdir -p "$WORKSPACE"
|
|
||||||
if [ ! -w "$WORKSPACE" ]; then
|
|
||||||
echo "==> Fixing workspace permissions..."
|
|
||||||
sudo chown -R coder:coder "$WORKSPACE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clone repo if not already present
|
|
||||||
if [ ! -d "$PROJECT/.git" ]; then
|
|
||||||
echo "==> First boot: cloning into AIPass/..."
|
|
||||||
git clone "$FORK" "$PROJECT"
|
|
||||||
cd "$PROJECT"
|
|
||||||
git remote add upstream "$UPSTREAM"
|
|
||||||
echo "==> Installing AIPass in editable mode..."
|
|
||||||
pip install -e .
|
|
||||||
echo "==> Workspace ready!"
|
|
||||||
echo "==> origin = $FORK (your fork - push here)"
|
|
||||||
echo "==> upstream = $UPSTREAM (pull updates from here)"
|
|
||||||
else
|
|
||||||
echo "==> Workspace exists, ensuring deps are installed..."
|
|
||||||
cd "$PROJECT"
|
|
||||||
pip install -e . 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# NOT a setuptools setup.py — this is the AIPass cross-platform installer.
|
|
||||||
# Runs on Linux, macOS, and Windows (Python 3.10+).
|
|
||||||
# Usage: python setup.py OR python3 setup.py
|
|
||||||
"""
|
|
||||||
AIPass cross-platform setup script.
|
|
||||||
|
|
||||||
Equivalent to setup.sh but works on Windows without Git Bash.
|
|
||||||
Performs the same steps: create venv, install package, verify entry points,
|
|
||||||
create secrets directory, seed .env, generate registry, bootstrap branches,
|
|
||||||
and set up global CLI access.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _is_windows() -> bool:
|
|
||||||
return platform.system() == "Windows"
|
|
||||||
|
|
||||||
|
|
||||||
def _venv_bin() -> Path:
|
|
||||||
"""Return the venv executables directory (OS-aware)."""
|
|
||||||
if _is_windows():
|
|
||||||
return REPO_ROOT / ".venv" / "Scripts"
|
|
||||||
return REPO_ROOT / ".venv" / "bin"
|
|
||||||
|
|
||||||
|
|
||||||
def _venv_exe(name: str) -> Path:
|
|
||||||
"""Return path to a venv executable by name."""
|
|
||||||
if _is_windows():
|
|
||||||
return _venv_bin() / f"{name}.exe"
|
|
||||||
return _venv_bin() / name
|
|
||||||
|
|
||||||
|
|
||||||
def _run(cmd: list, check: bool = True, **kwargs) -> subprocess.CompletedProcess:
|
|
||||||
"""Print and run a subprocess command."""
|
|
||||||
print(f" $ {' '.join(str(c) for c in cmd)}")
|
|
||||||
return subprocess.run(cmd, check=check, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Steps
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def step_create_venv() -> None:
|
|
||||||
"""[1] Create .venv using sys.executable (avoids python3 vs python ambiguity)."""
|
|
||||||
print("\n[1/9] Creating virtual environment ...")
|
|
||||||
venv_path = REPO_ROOT / ".venv"
|
|
||||||
if venv_path.exists():
|
|
||||||
print(" Removing existing .venv for a clean install ...")
|
|
||||||
shutil.rmtree(venv_path)
|
|
||||||
_run([sys.executable, "-m", "venv", str(venv_path)])
|
|
||||||
print(f" Created: {venv_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def step_install() -> None:
|
|
||||||
"""[2] Install aipass in editable mode with dev extras."""
|
|
||||||
print("\n[2/9] Installing aipass in editable mode ...")
|
|
||||||
pip = _venv_exe("pip")
|
|
||||||
_run([str(pip), "install", "--upgrade", "pip", "--quiet"])
|
|
||||||
_run([str(pip), "install", "-e", ".[dev]", "--quiet"], cwd=str(REPO_ROOT))
|
|
||||||
print(" Installed: aipass[dev]")
|
|
||||||
|
|
||||||
|
|
||||||
def step_verify() -> bool:
|
|
||||||
"""[3] Verify drone and aipass CLI entry points work."""
|
|
||||||
print("\n[3/9] Verifying CLI entry points ...")
|
|
||||||
ok = True
|
|
||||||
|
|
||||||
for entry in ("drone", "aipass"):
|
|
||||||
cmd_path = _venv_exe(entry)
|
|
||||||
if not cmd_path.exists():
|
|
||||||
print(f" {entry:<8} ... FAILED (not found: {cmd_path})")
|
|
||||||
ok = False
|
|
||||||
continue
|
|
||||||
flag = "--help" if entry == "drone" else "--version"
|
|
||||||
result = subprocess.run([str(cmd_path), flag], capture_output=True)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f" {entry:<8} ... ok")
|
|
||||||
else:
|
|
||||||
print(f" {entry:<8} ... FAILED (exit {result.returncode})")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
return ok
|
|
||||||
|
|
||||||
|
|
||||||
def step_secrets() -> None:
|
|
||||||
"""[4] Create ~/.secrets/aipass/ with restrictive permissions."""
|
|
||||||
print("\n[4/9] Creating secrets directory ...")
|
|
||||||
secrets_root = Path.home() / ".secrets"
|
|
||||||
secrets_dir = secrets_root / "aipass"
|
|
||||||
secrets_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
if not _is_windows():
|
|
||||||
try:
|
|
||||||
secrets_root.chmod(0o700)
|
|
||||||
secrets_dir.chmod(0o700)
|
|
||||||
except OSError:
|
|
||||||
pass # Best-effort on non-POSIX filesystems
|
|
||||||
# codeql[py/clear-text-logging-sensitive-data]
|
|
||||||
print(f" Created: {secrets_dir}")
|
|
||||||
|
|
||||||
|
|
||||||
def step_env() -> None:
|
|
||||||
"""[5] Seed .env.example into ~/.secrets/aipass/.env if not present."""
|
|
||||||
print("\n[5/9] Seeding .env template ...")
|
|
||||||
env_dest = Path.home() / ".secrets" / "aipass" / ".env"
|
|
||||||
env_src = REPO_ROOT / ".env.example"
|
|
||||||
|
|
||||||
if env_dest.exists():
|
|
||||||
print(" ~/.secrets/aipass/.env already exists — skipping")
|
|
||||||
elif env_src.exists():
|
|
||||||
shutil.copy(env_src, env_dest)
|
|
||||||
print(f" Copied: .env.example → {env_dest}")
|
|
||||||
print(" Add your API keys to that file")
|
|
||||||
else:
|
|
||||||
print(" No .env.example found — skipping")
|
|
||||||
|
|
||||||
|
|
||||||
def step_registry() -> None:
|
|
||||||
"""[6] Generate AIPASS_REGISTRY.json if not present."""
|
|
||||||
print("\n[6/9] Generating AIPASS_REGISTRY.json ...")
|
|
||||||
registry_path = REPO_ROOT / "AIPASS_REGISTRY.json"
|
|
||||||
if registry_path.exists():
|
|
||||||
print(" AIPASS_REGISTRY.json already exists — skipping")
|
|
||||||
return
|
|
||||||
|
|
||||||
today = date.today().isoformat()
|
|
||||||
src_dir = REPO_ROOT / "src" / "aipass"
|
|
||||||
branches = []
|
|
||||||
|
|
||||||
if src_dir.exists():
|
|
||||||
for d in sorted(src_dir.iterdir()):
|
|
||||||
if d.is_dir() and not d.name.startswith(("_", ".")):
|
|
||||||
branches.append({
|
|
||||||
"name": d.name,
|
|
||||||
"path": str(d),
|
|
||||||
"profile": "library",
|
|
||||||
"description": "",
|
|
||||||
"email": f"@{d.name}",
|
|
||||||
"status": "active",
|
|
||||||
"created": today,
|
|
||||||
"last_active": today,
|
|
||||||
})
|
|
||||||
|
|
||||||
registry = {
|
|
||||||
"metadata": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"last_updated": today,
|
|
||||||
"total_branches": len(branches),
|
|
||||||
},
|
|
||||||
"branches": branches,
|
|
||||||
}
|
|
||||||
registry_path.write_text(json.dumps(registry, indent=2) + "\n", encoding="utf-8")
|
|
||||||
print(f" {len(branches)} branches registered → AIPASS_REGISTRY.json")
|
|
||||||
|
|
||||||
|
|
||||||
def step_bootstrap_branches() -> None:
|
|
||||||
"""[7] Bootstrap .trinity/ identity and .ai_mail.local/ for each branch."""
|
|
||||||
print("\n[7/9] Bootstrapping branch identity files ...")
|
|
||||||
today = date.today().isoformat()
|
|
||||||
|
|
||||||
branches = [
|
|
||||||
("drone", "src/aipass/drone", "builder", "Command routing and module discovery"),
|
|
||||||
("seedgo", "src/aipass/seedgo", "builder", "Standards enforcement and code auditing"),
|
|
||||||
("prax", "src/aipass/prax", "builder", "Logging and monitoring system"),
|
|
||||||
("cli", "src/aipass/cli", "builder", "Display formatting service"),
|
|
||||||
("flow", "src/aipass/flow", "builder", "Workflow and plan management"),
|
|
||||||
("ai_mail", "src/aipass/ai_mail", "builder", "Inter-agent messaging and dispatch"),
|
|
||||||
("trigger", "src/aipass/trigger", "builder", "Event-driven automation"),
|
|
||||||
("spawn", "src/aipass/spawn", "builder", "Branch lifecycle management"),
|
|
||||||
("memory", "src/aipass/memory", "builder", "Vector memory bank"),
|
|
||||||
("devpulse", "src/aipass/devpulse", "manager", "Orchestration hub and coordination"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for name, rel_path, citizen_class, role in branches:
|
|
||||||
branch_path = REPO_ROOT / rel_path
|
|
||||||
if not branch_path.exists():
|
|
||||||
print(f" @{name:<10} ... skipped (directory not found)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
created = False
|
|
||||||
trinity = branch_path / ".trinity"
|
|
||||||
trinity.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
passport = trinity / "passport.json"
|
|
||||||
if not passport.exists():
|
|
||||||
passport.write_text(json.dumps({
|
|
||||||
"document_metadata": {
|
|
||||||
"document_type": "identity",
|
|
||||||
"document_name": f"{name}.PASSPORT",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"schema_version": "1.0.0",
|
|
||||||
"created": today,
|
|
||||||
"last_updated": today,
|
|
||||||
"managed_by": name,
|
|
||||||
},
|
|
||||||
"identity": {
|
|
||||||
"name": name,
|
|
||||||
"citizen_class": citizen_class,
|
|
||||||
"role": role,
|
|
||||||
"status": "active",
|
|
||||||
},
|
|
||||||
}, indent=2) + "\n", encoding="utf-8")
|
|
||||||
created = True
|
|
||||||
|
|
||||||
local = trinity / "local.json"
|
|
||||||
if not local.exists():
|
|
||||||
local.write_text(json.dumps({
|
|
||||||
"document_metadata": {
|
|
||||||
"document_type": "session_history",
|
|
||||||
"document_name": f"{name}.LOCAL",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"schema_version": "1.0.0",
|
|
||||||
"created": today,
|
|
||||||
"last_updated": today,
|
|
||||||
"managed_by": name,
|
|
||||||
"tags": ["session_tracking", "work_log", name],
|
|
||||||
"limits": {"max_lines": 600, "note": "Auto-rollover when max_lines exceeded"},
|
|
||||||
"status": {"health": "healthy", "current_lines": 0, "last_health_check": today},
|
|
||||||
},
|
|
||||||
"active_tasks": {
|
|
||||||
"today_focus": "First session — explore codebase and capabilities",
|
|
||||||
"recently_completed": [],
|
|
||||||
},
|
|
||||||
"key_learnings": {},
|
|
||||||
"sessions": [],
|
|
||||||
}, indent=2) + "\n", encoding="utf-8")
|
|
||||||
created = True
|
|
||||||
|
|
||||||
mail_dir = branch_path / ".ai_mail.local"
|
|
||||||
mail_dir.mkdir(exist_ok=True)
|
|
||||||
inbox = mail_dir / "inbox.json"
|
|
||||||
if not inbox.exists():
|
|
||||||
inbox.write_text(
|
|
||||||
json.dumps({"mailbox": "inbox", "total_messages": 0, "unread_count": 0, "messages": []})
|
|
||||||
+ "\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
created = True
|
|
||||||
|
|
||||||
seedgo_dir = branch_path / ".seedgo"
|
|
||||||
seedgo_dir.mkdir(exist_ok=True)
|
|
||||||
bypass = seedgo_dir / "bypass.json"
|
|
||||||
if not bypass.exists():
|
|
||||||
bypass.write_text("{}\n", encoding="utf-8")
|
|
||||||
created = True
|
|
||||||
|
|
||||||
status = "bootstrapped" if created else "exists (skipped)"
|
|
||||||
print(f" @{name:<10} ... {status}")
|
|
||||||
|
|
||||||
|
|
||||||
def step_global_access() -> None:
|
|
||||||
"""[8/9] Set up global CLI access (symlink on Linux/macOS, PATH hint on Windows)."""
|
|
||||||
print("\n[8/9] Setting up global CLI access ...")
|
|
||||||
bin_dir = _venv_bin()
|
|
||||||
|
|
||||||
if _is_windows():
|
|
||||||
# [9] Windows: no ln, no sudo — print PATH instructions
|
|
||||||
print(" Windows detected — symlink not available")
|
|
||||||
print("")
|
|
||||||
print(" To use drone from any directory, add the venv to your PATH.")
|
|
||||||
print(" Choose the method for your shell:")
|
|
||||||
print(f" PowerShell: $env:PATH = \"{bin_dir};\" + $env:PATH")
|
|
||||||
print(f" CMD: set PATH={bin_dir};%PATH%")
|
|
||||||
print(f" Git Bash: export PATH=\"{bin_dir}:$PATH\"")
|
|
||||||
print("")
|
|
||||||
print(" To make it permanent (PowerShell):")
|
|
||||||
print(
|
|
||||||
f' [Environment]::SetEnvironmentVariable('
|
|
||||||
f'"PATH", "{bin_dir};" + '
|
|
||||||
f'[Environment]::GetEnvironmentVariable("PATH","User"), "User")'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Linux/macOS: offer symlink creation
|
|
||||||
drone_src = _venv_exe("drone")
|
|
||||||
drone_dst = Path("/usr/local/bin/drone")
|
|
||||||
|
|
||||||
if not drone_src.exists():
|
|
||||||
print(f" drone not found at {drone_src} — skipping symlink")
|
|
||||||
print(f" Add {bin_dir} to your PATH manually")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
answer = input(f" Create symlink {drone_dst} → {drone_src}? [y/N] ").strip().lower()
|
|
||||||
except (EOFError, KeyboardInterrupt):
|
|
||||||
answer = ""
|
|
||||||
|
|
||||||
if answer == "y":
|
|
||||||
result = subprocess.run(
|
|
||||||
["sudo", "ln", "-sf", str(drone_src), str(drone_dst)],
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f" {drone_dst} -> {drone_src}")
|
|
||||||
else:
|
|
||||||
print(" WARN: sudo failed — create manually:")
|
|
||||||
print(f" sudo ln -sf {drone_src} {drone_dst}")
|
|
||||||
else:
|
|
||||||
print(f" Skipped. To add manually:")
|
|
||||||
print(f" sudo ln -sf {drone_src} {drone_dst}")
|
|
||||||
print(f" Or add {bin_dir} to your PATH")
|
|
||||||
|
|
||||||
|
|
||||||
def step_summary(ok: bool) -> None:
|
|
||||||
"""[9/9] Print success or warning summary."""
|
|
||||||
print("\n[9/9] Done")
|
|
||||||
print("")
|
|
||||||
if ok:
|
|
||||||
print("=== Setup complete ===")
|
|
||||||
print("")
|
|
||||||
print(f" Python: {sys.version.split()[0]}")
|
|
||||||
print(f" Venv: {REPO_ROOT / '.venv'}")
|
|
||||||
if _is_windows():
|
|
||||||
print(" Add .venv/Scripts to your PATH (see step 8 above)")
|
|
||||||
else:
|
|
||||||
print(" drone is available globally (or activate: source .venv/bin/activate)")
|
|
||||||
print("")
|
|
||||||
else:
|
|
||||||
print("=== Setup finished with warnings ===")
|
|
||||||
print(" Package installed but CLI verification had issues.")
|
|
||||||
print(" Check the output above for details.")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Entry point
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
print("=== AIPass Setup (cross-platform) ===")
|
|
||||||
print(f" Platform: {platform.system()} {platform.machine()}")
|
|
||||||
print(f" Python: {sys.version.split()[0]} ({sys.executable})")
|
|
||||||
print(f" Repo: {REPO_ROOT}")
|
|
||||||
|
|
||||||
if sys.version_info < (3, 10):
|
|
||||||
print("\nFAIL: Python 3.10+ required")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
step_create_venv()
|
|
||||||
step_install()
|
|
||||||
ok = step_verify()
|
|
||||||
step_secrets()
|
|
||||||
step_env()
|
|
||||||
step_registry()
|
|
||||||
step_bootstrap_branches()
|
|
||||||
step_global_access()
|
|
||||||
step_summary(ok)
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -2,6 +2,15 @@
|
|||||||
#
|
#
|
||||||
# AIPass setup script
|
# AIPass setup script
|
||||||
# Creates a venv, installs the package in editable mode, and verifies CLI entry points.
|
# Creates a venv, installs the package in editable mode, and verifies CLI entry points.
|
||||||
|
# On interactive terminals it then chains into `aipass init run` to scaffold a first
|
||||||
|
# project (DPLAN-0234: one command does setup + init).
|
||||||
|
#
|
||||||
|
# Usage: ./setup.sh [--no-init] [--with-init] [--project <dir>] [--no-symlink] [--force-symlink]
|
||||||
|
# --no-init skip the first-project init chain
|
||||||
|
# --with-init force the init chain even headless (init runs --non-interactive)
|
||||||
|
# --project <dir> first-project directory (default: ~/aipass-project)
|
||||||
|
# --no-symlink do not create/modify global drone/aipass CLI symlinks
|
||||||
|
# --force-symlink repoint a global symlink even if it points at a different install (#660)
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -27,6 +36,31 @@ case "${OSTYPE:-}" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# --- Args ---
|
||||||
|
# Mirrors the `aipass install` handoff rules: --no-init wins, --with-init forces,
|
||||||
|
# default (auto) chains into init on interactive terminals only — CI/headless skip.
|
||||||
|
RUN_INIT="auto"
|
||||||
|
INIT_PROJECT=""
|
||||||
|
SKIP_SYMLINK="no"
|
||||||
|
FORCE_SYMLINK="no"
|
||||||
|
PREV_ARG=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$PREV_ARG" = "--project" ]; then
|
||||||
|
INIT_PROJECT="$arg"
|
||||||
|
PREV_ARG=""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
--no-init) RUN_INIT="no" ;;
|
||||||
|
--with-init) RUN_INIT="yes" ;;
|
||||||
|
--no-symlink) SKIP_SYMLINK="yes" ;;
|
||||||
|
--force-symlink) FORCE_SYMLINK="yes" ;;
|
||||||
|
--project=*) INIT_PROJECT="${arg#--project=}" ;;
|
||||||
|
--project) PREV_ARG="--project" ;;
|
||||||
|
*) echo "WARN: unknown argument '$arg' (ignored)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
echo "=== AIPass Setup ==="
|
echo "=== AIPass Setup ==="
|
||||||
echo "Repo root: $SCRIPT_DIR"
|
echo "Repo root: $SCRIPT_DIR"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -190,8 +224,8 @@ fi
|
|||||||
echo "Upgrading pip ..."
|
echo "Upgrading pip ..."
|
||||||
"$VENV_PYTHON" -m pip install --upgrade pip --quiet
|
"$VENV_PYTHON" -m pip install --upgrade pip --quiet
|
||||||
|
|
||||||
echo "Installing aipass in editable mode (with dev + memory extras) ..."
|
echo "Installing aipass in editable mode (with dev + memory extras) — this can take a few minutes while the memory wheels build ..."
|
||||||
"$VENV_PYTHON" -m pip install -e ".[dev,memory]" --quiet
|
"$VENV_PYTHON" -m pip install -e ".[dev,memory]"
|
||||||
|
|
||||||
# --- Detect shadowing drone installs (Windows) ---
|
# --- Detect shadowing drone installs (Windows) ---
|
||||||
# Issues #317 + #321: system-Python pip or legacy npm aipass-drone can shadow venv drone.exe.
|
# Issues #317 + #321: system-Python pip or legacy npm aipass-drone can shadow venv drone.exe.
|
||||||
@@ -226,6 +260,92 @@ if [ "$IS_WINDOWS" -eq 1 ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Sandbox prerequisites (kernel FS boundary) ---
|
||||||
|
echo ""
|
||||||
|
echo "Checking sandbox prerequisites ..."
|
||||||
|
|
||||||
|
if [ "$IS_WINDOWS" -eq 1 ] || [ "$IS_MACOS" -eq 1 ]; then
|
||||||
|
echo " kernel sandbox: Linux-only for now, skipping"
|
||||||
|
else
|
||||||
|
SB_MISSING=()
|
||||||
|
|
||||||
|
# bwrap
|
||||||
|
if command -v bwrap &>/dev/null; then
|
||||||
|
echo " bwrap ... $(bwrap --version 2>/dev/null || echo 'found')"
|
||||||
|
else
|
||||||
|
echo " bwrap ... MISSING"
|
||||||
|
echo " sudo apt install bubblewrap"
|
||||||
|
SB_MISSING+=("bwrap")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# node
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
echo " node ... $(node --version 2>/dev/null)"
|
||||||
|
else
|
||||||
|
echo " node ... MISSING"
|
||||||
|
echo " Install Node.js: https://nodejs.org/"
|
||||||
|
SB_MISSING+=("node")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# npm (needed for srt install)
|
||||||
|
if command -v npm &>/dev/null; then
|
||||||
|
echo " npm ... $(npm --version 2>/dev/null)"
|
||||||
|
else
|
||||||
|
echo " npm ... MISSING"
|
||||||
|
SB_MISSING+=("npm")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# @anthropic-ai/sandbox-runtime — resolve same way as _srt_resolve.mjs
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
SRT_PATH=$(node -e "
|
||||||
|
const p = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const prefix = p.dirname(p.dirname(process.execPath));
|
||||||
|
const entry = p.join(prefix, 'lib/node_modules/@anthropic-ai/sandbox-runtime/dist/index.js');
|
||||||
|
if (fs.existsSync(entry)) process.stdout.write(entry);
|
||||||
|
else process.exit(1);
|
||||||
|
" 2>/dev/null) || SRT_PATH=""
|
||||||
|
if [ -n "$SRT_PATH" ]; then
|
||||||
|
echo " srt ... $SRT_PATH"
|
||||||
|
else
|
||||||
|
echo " srt ... MISSING"
|
||||||
|
if command -v npm &>/dev/null; then
|
||||||
|
echo " Attempting: npm install -g @anthropic-ai/sandbox-runtime"
|
||||||
|
if npm install -g @anthropic-ai/sandbox-runtime 2>/dev/null; then
|
||||||
|
echo " srt ... installed"
|
||||||
|
else
|
||||||
|
echo " Install failed (may need sudo). Run manually:"
|
||||||
|
echo " sudo npm install -g @anthropic-ai/sandbox-runtime"
|
||||||
|
SB_MISSING+=("srt")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Install node+npm first, then: npm install -g @anthropic-ai/sandbox-runtime"
|
||||||
|
SB_MISSING+=("srt")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " srt ... skipped (no node)"
|
||||||
|
SB_MISSING+=("srt")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rg (ripgrep)
|
||||||
|
if command -v rg &>/dev/null; then
|
||||||
|
echo " rg ... $(rg --version 2>/dev/null | head -1)"
|
||||||
|
elif [ -f "$HOME/.local/bin/rg" ]; then
|
||||||
|
echo " rg ... $HOME/.local/bin/rg"
|
||||||
|
else
|
||||||
|
echo " rg ... MISSING"
|
||||||
|
echo " sudo apt install ripgrep"
|
||||||
|
SB_MISSING+=("rg")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#SB_MISSING[@]} -eq 0 ]; then
|
||||||
|
echo " sandbox prereqs: READY"
|
||||||
|
else
|
||||||
|
echo " sandbox prereqs: INCOMPLETE (${SB_MISSING[*]} missing) — aipass doctor for details"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Verify CLI entry points ---
|
# --- Verify CLI entry points ---
|
||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
@@ -478,12 +598,13 @@ bootstrap_branch "spawn" "$SCRIPT_DIR/src/aipass/spawn" "builder" "Branch
|
|||||||
bootstrap_branch "devpulse" "$SCRIPT_DIR/src/aipass/devpulse" "manager" "Orchestration hub and coordination"
|
bootstrap_branch "devpulse" "$SCRIPT_DIR/src/aipass/devpulse" "manager" "Orchestration hub and coordination"
|
||||||
bootstrap_branch "memory" "$SCRIPT_DIR/src/aipass/memory" "builder" "Vector memory bank"
|
bootstrap_branch "memory" "$SCRIPT_DIR/src/aipass/memory" "builder" "Vector memory bank"
|
||||||
bootstrap_branch "aipass" "$SCRIPT_DIR/src/aipass/aipass" "builder" "Concierge — init, doctor, profile, onboarding"
|
bootstrap_branch "aipass" "$SCRIPT_DIR/src/aipass/aipass" "builder" "Concierge — init, doctor, profile, onboarding"
|
||||||
|
bootstrap_branch "hooks" "$SCRIPT_DIR/src/aipass/hooks" "builder" "Hook engine — cross-platform hook dispatch and per-project config"
|
||||||
|
|
||||||
# External branches
|
# External branches
|
||||||
# NOTE: backup, daemon removed S82/S87. commons, skills moved to external repos.
|
# NOTE: backup, daemon removed S82/S87. commons, skills moved to external repos.
|
||||||
# Only the 12 core branches above should be bootstrapped.
|
# Only the 13 core branches above should be bootstrapped.
|
||||||
|
|
||||||
echo " 12 branches bootstrapped"
|
echo " 13 branches bootstrapped"
|
||||||
|
|
||||||
# --- Seed branch config files from .example defaults ---
|
# --- Seed branch config files from .example defaults ---
|
||||||
# Some branches need a config file that's gitignored (contains local state).
|
# Some branches need a config file that's gitignored (contains local state).
|
||||||
@@ -499,80 +620,116 @@ fi
|
|||||||
# --- Install Claude Code hooks ---
|
# --- Install Claude Code hooks ---
|
||||||
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
||||||
|
|
||||||
if [ -d "$SCRIPT_DIR/.claude/hooks" ]; then
|
# Determine python command for non-Claude provider hooks.
|
||||||
|
# Claude hooks use bridge pattern with $AIPASS_HOME env var — no HOOK_PYTHON needed.
|
||||||
|
# Linux: keep "python3" — distros ship 3.10+ and hooks import nothing
|
||||||
|
# version-specific beyond that.
|
||||||
|
# macOS: stock /usr/bin/python3 is 3.9.6 on macOS 12 and cannot parse
|
||||||
|
# scripts that use PEP 604 union syntax (`X | None`). Use the venv python.
|
||||||
|
# Windows: existing venv-python behavior.
|
||||||
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||||
|
HOOK_PYTHON="$SCRIPT_DIR/.venv/Scripts/python.exe"
|
||||||
|
elif [ "$IS_MACOS" -eq 1 ]; then
|
||||||
|
HOOK_PYTHON="$SCRIPT_DIR/.venv/bin/python3"
|
||||||
|
else
|
||||||
|
HOOK_PYTHON="python3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$SCRIPT_DIR/src/aipass/hooks/apps/handlers/bridges/claude.py" ]; then
|
||||||
echo "Installing Claude Code hooks ..."
|
echo "Installing Claude Code hooks ..."
|
||||||
mkdir -p "$HOME/.claude"
|
mkdir -p "$HOME/.claude"
|
||||||
|
|
||||||
# Determine python command for hooks.
|
"$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" "$IS_WINDOWS" << 'PYEOF'
|
||||||
# Linux: keep "python3" — distros ship 3.10+ and hooks import nothing
|
|
||||||
# version-specific beyond that. Leaving this path unchanged per Linux stability.
|
|
||||||
# macOS: stock /usr/bin/python3 is 3.9.6 on macOS 12 and cannot parse hook
|
|
||||||
# scripts that use PEP 604 union syntax (`X | None`). Point at the venv
|
|
||||||
# python, which setup just built with a 3.10+ interpreter.
|
|
||||||
# Windows: existing venv-python behavior.
|
|
||||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
||||||
HOOK_PYTHON="$SCRIPT_DIR/.venv/Scripts/python.exe"
|
|
||||||
elif [ "$IS_MACOS" -eq 1 ]; then
|
|
||||||
HOOK_PYTHON="$SCRIPT_DIR/.venv/bin/python3"
|
|
||||||
else
|
|
||||||
HOOK_PYTHON="python3"
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" "$HOOK_PYTHON" << 'PYEOF'
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
repo_root = sys.argv[1]
|
repo_root = sys.argv[1]
|
||||||
settings_path = Path(sys.argv[2])
|
settings_path = Path(sys.argv[2])
|
||||||
hook_python = sys.argv[3]
|
is_windows = len(sys.argv) > 3 and sys.argv[3] == "1"
|
||||||
hooks_dir = f"{repo_root}/.claude/hooks"
|
|
||||||
|
# Bridge entry point — all hooks route through the engine via this bridge.
|
||||||
|
# Uses $AIPASS_HOME env var (injected into settings.env below) so the
|
||||||
|
# settings file stays relocatable. CC on Windows runs hooks via Git Bash
|
||||||
|
# (which must exist for setup.sh to have run), so $VAR expansion works —
|
||||||
|
# but the venv interpreter lives at Scripts/python.exe there, not bin/python3
|
||||||
|
# (@hooks assessment, DPLAN-0234 Strand C).
|
||||||
|
venv_python = ".venv/Scripts/python.exe" if is_windows else ".venv/bin/python3"
|
||||||
|
bridge = f"$AIPASS_HOME/{venv_python} $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py"
|
||||||
|
|
||||||
# Load existing settings or start fresh
|
# Load existing settings or start fresh
|
||||||
if settings_path.exists():
|
if settings_path.exists():
|
||||||
settings = json.loads(settings_path.read_text())
|
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||||
else:
|
else:
|
||||||
settings = {}
|
settings = {}
|
||||||
|
|
||||||
# Build hooks config with absolute paths
|
# Build hooks config — bridge pattern
|
||||||
settings["hooks"] = {
|
# UserPromptSubmit: 5 separate entries (EventType:hook_name) to avoid output merging
|
||||||
|
# PreToolUse, PostToolUse, SubagentStop, Stop, Notification: single aggregate entries
|
||||||
|
# PreCompact: 3 hooks x 2 matchers (manual + auto) = 6 entries
|
||||||
|
# SessionStart: cadence reset on startup/clear (handler skips resume itself)
|
||||||
|
aipass_hooks = {
|
||||||
"UserPromptSubmit": [
|
"UserPromptSubmit": [
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/global_prompt_loader.py"}]},
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:tier0_kernel"}]},
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/branch_prompt_loader.py"}]},
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:navmap"}]},
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/identity_injector.py"}]},
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:branch_prompt"}]},
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/email_notification.py"}]},
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:identity_injector"}]},
|
||||||
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:email_notification"}]},
|
||||||
|
{"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:auto_process", "timeout": 120}]},
|
||||||
],
|
],
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
{"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
|
{"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task",
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/tool_use_sound.py"}]},
|
"hooks": [{"type": "command", "command": f"{bridge} PreToolUse"}]},
|
||||||
{"matcher": "Edit|MultiEdit|Write|NotebookEdit",
|
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_edit_gate.py"}]},
|
|
||||||
{"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit",
|
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/git_gate.py"}]},
|
|
||||||
],
|
],
|
||||||
"PostToolUse": [
|
"PostToolUse": [
|
||||||
{"matcher": "Edit|MultiEdit|Write|NotebookEdit",
|
{"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit",
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/auto_fix_diagnostics.py"}]},
|
"hooks": [{"type": "command", "command": f"{bridge} PostToolUse"}]},
|
||||||
{"matcher": "Bash",
|
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/auto_watchdog.py"}]},
|
|
||||||
],
|
|
||||||
"Stop": [
|
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/stop_sound.py"}]},
|
|
||||||
],
|
|
||||||
"Notification": [
|
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/notification_sound.py"}]},
|
|
||||||
],
|
],
|
||||||
"SubagentStop": [
|
"SubagentStop": [
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/subagent_stop_gate.py"}]},
|
{"hooks": [{"type": "command", "command": f"{bridge} SubagentStop"}]},
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{"hooks": [{"type": "command", "command": f"{bridge} Stop"}]},
|
||||||
|
],
|
||||||
|
"Notification": [
|
||||||
|
{"hooks": [{"type": "command", "command": f"{bridge} Notification"}]},
|
||||||
],
|
],
|
||||||
"PreCompact": [
|
"PreCompact": [
|
||||||
{"matcher": "manual", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]},
|
{"matcher": "manual", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact", "timeout": 60}]},
|
||||||
{"matcher": "auto", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]},
|
{"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact", "timeout": 60}]},
|
||||||
|
{"matcher": "manual", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact_rollover", "timeout": 120}]},
|
||||||
|
{"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:pre_compact_rollover", "timeout": 120}]},
|
||||||
|
{"matcher": "manual", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:auto_process", "timeout": 120}]},
|
||||||
|
{"matcher": "auto", "hooks": [{"type": "command", "command": f"{bridge} PreCompact:auto_process", "timeout": 120}]},
|
||||||
|
],
|
||||||
|
"SessionStart": [
|
||||||
|
{"hooks": [{"type": "command", "command": f"{bridge} SessionStart:cadence_reset", "timeout": 30}]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Merge, don't replace (DPLAN-0234 Strand C): refresh every AIPass bridge entry
|
||||||
|
# (identified by the bridges/claude.py marker) but preserve any hooks the user
|
||||||
|
# wired themselves. Re-runs stay idempotent; custom hooks survive reinstall.
|
||||||
|
existing_hooks = settings.get("hooks", {})
|
||||||
|
merged_hooks = {}
|
||||||
|
for event in set(existing_hooks) | set(aipass_hooks):
|
||||||
|
user_entries = [
|
||||||
|
entry for entry in existing_hooks.get(event, [])
|
||||||
|
if "bridges/claude.py" not in json.dumps(entry)
|
||||||
|
]
|
||||||
|
merged = aipass_hooks.get(event, []) + user_entries
|
||||||
|
# Never emit an empty hook event. If this event had only stale AIPass bridge
|
||||||
|
# entries (no current aipass_hooks definition AND no user-wired hooks), the
|
||||||
|
# filter above orphans it to [] — a half-wired event that fires nothing,
|
||||||
|
# written silently. Drop the key instead and say so, so the state stays honest.
|
||||||
|
if not merged:
|
||||||
|
print(f" ! dropped orphaned hook event (no live entries): {event}")
|
||||||
|
continue
|
||||||
|
merged_hooks[event] = merged
|
||||||
|
settings["hooks"] = merged_hooks
|
||||||
|
|
||||||
# Inject AIPASS_HOME into env block so dispatched agents find AIPass
|
# Inject AIPASS_HOME into env block so dispatched agents find AIPass
|
||||||
import os
|
|
||||||
env_block = settings.get("env", {})
|
env_block = settings.get("env", {})
|
||||||
env_block["AIPASS_HOME"] = repo_root
|
env_block["AIPASS_HOME"] = repo_root
|
||||||
env_block["CLAUDE_CODE_DISABLE_AUTO_MEMORY"] = "1"
|
env_block["CLAUDE_CODE_DISABLE_AUTO_MEMORY"] = "1"
|
||||||
@@ -603,7 +760,6 @@ git_deny = [
|
|||||||
"Bash(git push -f *)",
|
"Bash(git push -f *)",
|
||||||
"Bash(git rebase*)",
|
"Bash(git rebase*)",
|
||||||
"Bash(git clean*)",
|
"Bash(git clean*)",
|
||||||
"Bash(rm -rf*)",
|
|
||||||
"Bash(git reset*)",
|
"Bash(git reset*)",
|
||||||
"Bash(git merge*)",
|
"Bash(git merge*)",
|
||||||
"Bash(git config*)",
|
"Bash(git config*)",
|
||||||
@@ -614,7 +770,6 @@ git_deny = [
|
|||||||
"Bash(git branch -D*)",
|
"Bash(git branch -D*)",
|
||||||
"Bash(git stash drop*)",
|
"Bash(git stash drop*)",
|
||||||
"Bash(git stash clear*)",
|
"Bash(git stash clear*)",
|
||||||
"Bash(rm -r *)",
|
|
||||||
"Bash(git checkout -b*)",
|
"Bash(git checkout -b*)",
|
||||||
"Bash(git switch -c*)",
|
"Bash(git switch -c*)",
|
||||||
"Bash(git switch --create*)",
|
"Bash(git switch --create*)",
|
||||||
@@ -641,12 +796,22 @@ permissions["ask"] = ask
|
|||||||
|
|
||||||
settings["permissions"] = permissions
|
settings["permissions"] = permissions
|
||||||
|
|
||||||
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
||||||
print(f" hooks -> {settings_path}")
|
print(f" hooks -> {settings_path}")
|
||||||
print(f" AIPASS_HOME -> {repo_root} (in settings.json env)")
|
print(f" AIPASS_HOME -> {repo_root} (in settings.json env)")
|
||||||
PYEOF
|
PYEOF
|
||||||
else
|
else
|
||||||
echo "Skipping hooks (no .claude/hooks/ directory found)"
|
echo "Skipping Claude hooks (bridge not found at src/aipass/hooks/apps/handlers/bridges/claude.py)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Install claude() boot shim (attach-if-live / start-in-tmux) ---
|
||||||
|
# Ships via the .gitignore negation (#666). Idempotent: the installer checks for
|
||||||
|
# its marker before appending to the shell rc, and resolves the venv Python from
|
||||||
|
# its own location (POSIX/Windows/fallback). Composes with presence_gate seeding.
|
||||||
|
BOOT_SHIM="$SCRIPT_DIR/src/aipass/hooks/tools/install_boot_shim.sh"
|
||||||
|
if [ -f "$BOOT_SHIM" ]; then
|
||||||
|
echo "Installing claude() boot shim ..."
|
||||||
|
bash "$BOOT_SHIM" || echo " boot shim install skipped (non-fatal)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Install Claude Code commands (provider level) ---
|
# --- Install Claude Code commands (provider level) ---
|
||||||
@@ -711,58 +876,6 @@ else
|
|||||||
echo "Skipping Codex CLI (not installed)"
|
echo "Skipping Codex CLI (not installed)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Install Gemini CLI hooks ---
|
|
||||||
if command -v gemini &>/dev/null; then
|
|
||||||
if [ -d "$SCRIPT_DIR/.gemini/hooks" ]; then
|
|
||||||
echo "Installing Gemini CLI hooks ..."
|
|
||||||
|
|
||||||
GEMINI_SETTINGS="$HOME/.gemini/settings.json"
|
|
||||||
mkdir -p "$HOME/.gemini"
|
|
||||||
|
|
||||||
# HOOK_PYTHON was set earlier in the Claude hooks block; reuse it.
|
|
||||||
# Fall back to python3 if this block runs without that setup (defensive).
|
|
||||||
GEMINI_HOOK_PYTHON="${HOOK_PYTHON:-python3}"
|
|
||||||
|
|
||||||
python3 - "$SCRIPT_DIR" "$GEMINI_SETTINGS" "$GEMINI_HOOK_PYTHON" << 'PYEOF'
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
repo_root = sys.argv[1]
|
|
||||||
settings_path = Path(sys.argv[2])
|
|
||||||
hook_python = sys.argv[3]
|
|
||||||
hooks_dir = f"{repo_root}/.gemini/hooks"
|
|
||||||
|
|
||||||
# Load existing settings or start fresh
|
|
||||||
if settings_path.exists():
|
|
||||||
settings = json.loads(settings_path.read_text())
|
|
||||||
else:
|
|
||||||
settings = {}
|
|
||||||
|
|
||||||
# Build hooks config with absolute paths (Gemini uses different event names)
|
|
||||||
settings["hooks"] = {
|
|
||||||
"SessionStart": [
|
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/session_start_identity.py", "timeout": 10}]}
|
|
||||||
],
|
|
||||||
"BeforeModel": [
|
|
||||||
{"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/prompt_inject.py", "timeout": 10}]}
|
|
||||||
],
|
|
||||||
"BeforeTool": [
|
|
||||||
{"matcher": "Edit|Write",
|
|
||||||
"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_edit_gate.py", "timeout": 5}]}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
settings_path.write_text(json.dumps(settings, indent=2) + "\n")
|
|
||||||
print(f" hooks -> {settings_path}")
|
|
||||||
PYEOF
|
|
||||||
else
|
|
||||||
echo "Skipping Gemini hooks (no .gemini/hooks/ directory found in repo)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Skipping Gemini CLI (not installed)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Set AIPASS_HOME + PATH so all services work from any project ---
|
# --- Set AIPASS_HOME + PATH so all services work from any project ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "Configuring cross-project access ..."
|
echo "Configuring cross-project access ..."
|
||||||
@@ -867,8 +980,42 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Create global symlinks for CLI tools (Linux/macOS only) ---
|
# --- Create global symlinks for CLI tools (Linux/macOS only) ---
|
||||||
|
# #660: never SILENTLY hijack a global 'drone'/'aipass' that points at a
|
||||||
|
# DIFFERENT install. safe_symlink skips a different-target link (loud warning)
|
||||||
|
# unless --force-symlink; --no-symlink opts out of symlinking entirely.
|
||||||
|
SYMLINK_SKIPPED=0
|
||||||
|
safe_symlink() {
|
||||||
|
# safe_symlink <src> <dest> [sudo] -> 0 linked · 1 skipped(diff target) · 2 ln failed
|
||||||
|
local src="$1" dest="$2" use_sudo="${3:-}" existing=""
|
||||||
|
if [ -L "$dest" ]; then
|
||||||
|
existing="$(readlink "$dest" 2>/dev/null)"
|
||||||
|
elif [ -e "$dest" ]; then
|
||||||
|
existing="$dest (real file, not a symlink)"
|
||||||
|
fi
|
||||||
|
if [ -n "$existing" ] && [ "$existing" != "$src" ]; then
|
||||||
|
if [ "$FORCE_SYMLINK" != "yes" ]; then
|
||||||
|
echo " SKIP $dest — already points at a different install:"
|
||||||
|
echo " $existing"
|
||||||
|
echo " Not repointing (would hijack your existing '$(basename "$dest")'); PATH keeps the above."
|
||||||
|
echo " Re-run 'aipass install' with --force-symlink to repoint here, or --no-symlink to skip quietly."
|
||||||
|
SYMLINK_SKIPPED=$((SYMLINK_SKIPPED + 1))
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " WARNING: repointing $dest"
|
||||||
|
echo " from $existing"
|
||||||
|
echo " to $src (--force-symlink)"
|
||||||
|
fi
|
||||||
|
if [ -n "$use_sudo" ]; then
|
||||||
|
$use_sudo ln -sf "$src" "$dest" 2>/dev/null && return 0 || return 2
|
||||||
|
fi
|
||||||
|
ln -sf "$src" "$dest" 2>/dev/null && return 0 || return 2
|
||||||
|
}
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
if [ "$SKIP_SYMLINK" = "yes" ]; then
|
||||||
|
echo "Skipping global CLI symlinks (--no-symlink)."
|
||||||
|
echo " 'drone'/'aipass' resolve from $SCRIPT_DIR/.venv/bin — add it to PATH to use them."
|
||||||
|
elif [ "$IS_WINDOWS" -eq 1 ]; then
|
||||||
echo "Windows: drone available via PATH (set above)"
|
echo "Windows: drone available via PATH (set above)"
|
||||||
elif [ "$IS_MACOS" -eq 1 ]; then
|
elif [ "$IS_MACOS" -eq 1 ]; then
|
||||||
# Mac: symlink into ~/.local/bin (user-writable, no sudo needed).
|
# Mac: symlink into ~/.local/bin (user-writable, no sudo needed).
|
||||||
@@ -880,9 +1027,11 @@ elif [ "$IS_MACOS" -eq 1 ]; then
|
|||||||
|
|
||||||
for cmd in drone aipass; do
|
for cmd in drone aipass; do
|
||||||
if [ -f "$VENV_BIN/$cmd" ]; then
|
if [ -f "$VENV_BIN/$cmd" ]; then
|
||||||
if ln -sf "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd"; then
|
rc=0
|
||||||
|
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd" || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
echo " $LOCAL_BIN/$cmd -> $VENV_BIN/$cmd"
|
echo " $LOCAL_BIN/$cmd -> $VENV_BIN/$cmd"
|
||||||
else
|
elif [ "$rc" -eq 2 ]; then
|
||||||
echo " WARN: Could not create symlink for $cmd"
|
echo " WARN: Could not create symlink for $cmd"
|
||||||
echo " Manual fix: ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
|
echo " Manual fix: ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
|
||||||
fi
|
fi
|
||||||
@@ -895,14 +1044,20 @@ else
|
|||||||
|
|
||||||
for cmd in drone aipass; do
|
for cmd in drone aipass; do
|
||||||
if [ -f "$VENV_BIN/$cmd" ]; then
|
if [ -f "$VENV_BIN/$cmd" ]; then
|
||||||
if sudo ln -sf "$VENV_BIN/$cmd" "/usr/local/bin/$cmd" 2>/dev/null; then
|
rc=0
|
||||||
|
safe_symlink "$VENV_BIN/$cmd" "/usr/local/bin/$cmd" "sudo" || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
echo " /usr/local/bin/$cmd -> $VENV_BIN/$cmd"
|
echo " /usr/local/bin/$cmd -> $VENV_BIN/$cmd"
|
||||||
LINUX_SYMLINK_DIR="/usr/local/bin"
|
LINUX_SYMLINK_DIR="/usr/local/bin"
|
||||||
|
elif [ "$rc" -eq 1 ]; then
|
||||||
|
: # skipped a different install — safe_symlink explained; do NOT fall back
|
||||||
else
|
else
|
||||||
# Fallback: user-local bin (no sudo needed)
|
# sudo/ln failed (e.g. no sudo) — fall back to user-local bin
|
||||||
LOCAL_BIN="$HOME/.local/bin"
|
LOCAL_BIN="$HOME/.local/bin"
|
||||||
mkdir -p "$LOCAL_BIN"
|
mkdir -p "$LOCAL_BIN"
|
||||||
if ln -sf "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd"; then
|
rc=0
|
||||||
|
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd" || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
echo " /usr/local/bin failed (no sudo) — using $LOCAL_BIN/$cmd instead"
|
echo " /usr/local/bin failed (no sudo) — using $LOCAL_BIN/$cmd instead"
|
||||||
LINUX_SYMLINK_DIR="$LOCAL_BIN"
|
LINUX_SYMLINK_DIR="$LOCAL_BIN"
|
||||||
# Ensure ~/.local/bin is on PATH
|
# Ensure ~/.local/bin is on PATH
|
||||||
@@ -912,7 +1067,7 @@ else
|
|||||||
echo " ~/.local/bin added to PATH in $PROFILE"
|
echo " ~/.local/bin added to PATH in $PROFILE"
|
||||||
fi
|
fi
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
else
|
elif [ "$rc" -eq 2 ]; then
|
||||||
echo " WARN: Could not create symlink for $cmd"
|
echo " WARN: Could not create symlink for $cmd"
|
||||||
echo " Manual fix: ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
|
echo " Manual fix: ln -sf $VENV_BIN/$cmd $LOCAL_BIN/$cmd"
|
||||||
fi
|
fi
|
||||||
@@ -921,6 +1076,12 @@ else
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$SYMLINK_SKIPPED" -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo " NOTE: $SYMLINK_SKIPPED global symlink(s) left untouched (pointed at a different install)."
|
||||||
|
echo " Your existing 'drone'/'aipass' still work. Use --force-symlink to repoint them here."
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
@@ -942,8 +1103,78 @@ if [ "$FAIL" -eq 0 ]; then
|
|||||||
echo "CLI integrations:"
|
echo "CLI integrations:"
|
||||||
echo " Claude Code: hooks installed to ~/.claude/settings.json"
|
echo " Claude Code: hooks installed to ~/.claude/settings.json"
|
||||||
command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml"
|
command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml"
|
||||||
command -v gemini &>/dev/null && echo " Gemini CLI: hooks installed to ~/.gemini/settings.json"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# --- Chain into first-project init (DPLAN-0234: one command does setup + init) ---
|
||||||
|
# `aipass init` refuses to run inside the engine tree, so it always targets a
|
||||||
|
# sibling directory — never the repo itself. Decision mirrors install.py:
|
||||||
|
# --no-init wins, --with-init forces (headless chains --non-interactive),
|
||||||
|
# default = interactive terminals only (CI and piped shells skip).
|
||||||
|
AIPASS_BIN=""
|
||||||
|
if [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass.exe" ]; then
|
||||||
|
AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass.exe"
|
||||||
|
elif [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass" ]; then
|
||||||
|
AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass"
|
||||||
|
elif [ -f "$SCRIPT_DIR/.venv/bin/aipass" ]; then
|
||||||
|
AIPASS_BIN="$SCRIPT_DIR/.venv/bin/aipass"
|
||||||
|
elif command -v aipass &>/dev/null; then
|
||||||
|
AIPASS_BIN="$(command -v aipass)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAUNCH_INIT=0
|
||||||
|
INIT_HEADLESS=0
|
||||||
|
if [ "$RUN_INIT" = "no" ]; then
|
||||||
|
echo "Skipping first-project init (--no-init). Run 'aipass init run' in a fresh directory when ready."
|
||||||
|
elif [ "$RUN_INIT" = "yes" ]; then
|
||||||
|
LAUNCH_INIT=1
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
INIT_HEADLESS=1
|
||||||
|
fi
|
||||||
|
elif [ -t 0 ] && [ -z "${CI:-}" ]; then
|
||||||
|
LAUNCH_INIT=1
|
||||||
|
else
|
||||||
|
echo "Non-interactive shell — skipping first-project init. Run 'aipass init run' in a fresh directory when ready."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LAUNCH_INIT" -eq 1 ]; then
|
||||||
|
if [ -z "$AIPASS_BIN" ]; then
|
||||||
|
echo "WARN: aipass binary not found — open a new terminal and run 'aipass init run' in a fresh directory."
|
||||||
|
else
|
||||||
|
DEFAULT_PROJECT_DIR="$HOME/aipass-project"
|
||||||
|
PROJECT_DIR="$INIT_PROJECT"
|
||||||
|
if [ -z "$PROJECT_DIR" ] && [ "$INIT_HEADLESS" -eq 0 ] && [ -t 0 ]; then
|
||||||
|
echo "AIPass projects live in their own directory, never inside the engine repo."
|
||||||
|
read -r -p "Set up your first project now? [Y/n]: " INIT_REPLY
|
||||||
|
case "$INIT_REPLY" in
|
||||||
|
[nN]*)
|
||||||
|
PROJECT_DIR="-"
|
||||||
|
echo " Skipped. Run 'aipass init run' in a fresh directory when ready."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
read -r -p " Project directory [$DEFAULT_PROJECT_DIR]: " INIT_INPUT
|
||||||
|
PROJECT_DIR="${INIT_INPUT:-$DEFAULT_PROJECT_DIR}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
elif [ -z "$PROJECT_DIR" ]; then
|
||||||
|
PROJECT_DIR="$DEFAULT_PROJECT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$PROJECT_DIR" != "-" ]; then
|
||||||
|
mkdir -p "$PROJECT_DIR"
|
||||||
|
INIT_CMD=("$AIPASS_BIN" init run)
|
||||||
|
if [ "$INIT_HEADLESS" -eq 1 ]; then
|
||||||
|
INIT_CMD+=(--non-interactive)
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "Launching guided setup in $PROJECT_DIR ..."
|
||||||
|
if (cd "$PROJECT_DIR" && "${INIT_CMD[@]}"); then
|
||||||
|
echo "First project initialized at $PROJECT_DIR"
|
||||||
|
else
|
||||||
|
echo "Init didn't complete — run 'aipass init run' in $PROJECT_DIR when ready."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "=== Setup finished with errors ==="
|
echo "=== Setup finished with errors ==="
|
||||||
echo "The venv was created and the package was installed, but one or more"
|
echo "The venv was created and the package was installed, but one or more"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""AIPass — Multi-agent orchestration framework.
|
"""AIPass — Multi-agent orchestration framework.
|
||||||
|
|
||||||
pip install aipass
|
git clone + ./setup.sh — https://github.com/AIOSAI/AIPass
|
||||||
https://github.com/AIOSAI/AIPass
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2.2.0"
|
__version__ = "2.7.3"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
## Role
|
## Role
|
||||||
|
|
||||||
Inter-branch messaging system. Every branch in AIPass communicates through ai_mail. The dispatch pipeline (send + wake) is how work gets assigned to branches autonomously.
|
Inter-branch messaging system. Every branch communicates through ai_mail. Dispatch pipeline (send + wake) assigns work autonomously.
|
||||||
|
|
||||||
## Key Commands
|
## Key Commands
|
||||||
|
|
||||||
@@ -65,15 +65,15 @@ apps/
|
|||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
- **Identity**: `detect_branch_from_pwd()` checks `AIPASS_CALLER_BRANCH` env var first, falls back to CWD walk-up. NEVER fall back to `Path.cwd()` silently — wrong identity is worse than no identity.
|
- **Identity**: `detect_branch_from_pwd()` checks `AIPASS_CALLER_BRANCH` env var first, falls back CWD walk-up. NEVER fall back `Path.cwd()` silently — wrong identity worse than no identity.
|
||||||
- **Fallback**: Per-ID commands (view/close/reply) use `_resolve_branch_path()` which falls back to `_AI_MAIL_DIR` when caller detection fails. All handlers return `True` even on error (command was recognized).
|
- **Fallback**: Per-ID commands (view/close/reply) use `_resolve_branch_path()` which falls back `_AI_MAIL_DIR` when caller detection fails. All handlers return `True` even on error (command recognized).
|
||||||
- **Dispatch env**: `dispatch_monitor.py` sets `AIPASS_BRANCH_NAME=<branch>` in spawn_env. Strips `AIPASS_CALLER_*` vars to prevent parent context leaking.
|
- **Dispatch env**: `dispatch_monitor.py` sets `AIPASS_BRANCH_NAME=<branch>` spawn_env. Strips `AIPASS_CALLER_*` vars — prevents parent context leaking.
|
||||||
- **Inbox lock**: `inbox_lock()` uses `fcntl` (POSIX) / `msvcrt` (Windows) for atomic inbox writes.
|
- **Inbox lock**: `inbox_lock()` uses `fcntl` (POSIX) / `msvcrt` (Windows) atomic inbox writes.
|
||||||
- **Purge lifecycle**: Vectorize to Memory Bank first, then delete originals. Deletion gated on vectorization success.
|
- **Purge lifecycle**: Vectorize Memory Bank first, then delete originals. Deletion gated on vectorization success.
|
||||||
|
|
||||||
## Integration Points
|
## Integration Points
|
||||||
|
|
||||||
- **trigger**: Imports `deliver_email_to_branch()` directly for event-driven email delivery
|
- **trigger**: Imports `deliver_email_to_branch()` directly — event-driven email delivery
|
||||||
- **prax**: Provides `system_logger` used across all handlers
|
- **prax**: Provides `system_logger` used across all handlers
|
||||||
- **drone**: Routes commands via `handle_command()` pattern; sets caller env vars
|
- **drone**: Routes commands via `handle_command()` pattern; sets caller env vars
|
||||||
- **seedgo**: 100% compliance, 30+ bypass entries (all documented in `.seedgo/bypass.json`)
|
- **seedgo**: 100% compliance, 30+ bypass entries (all documented `.seedgo/bypass.json`)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
{
|
{
|
||||||
"file": "apps/handlers/dispatch/daemon.py",
|
"file": "apps/handlers/dispatch/daemon.py",
|
||||||
"standard": "deep_nesting",
|
"standard": "deep_nesting",
|
||||||
"reason": "3 functions: check_inbox_for_dispatch() depth 4 (priority scanning with business logic), run_daemon() depth 4 (main daemon loop), _check_lock() depth 4 (lock validation + PID liveness + cleanup)"
|
"reason": "run_daemon() depth 5 (main daemon loop with retry + signal handling)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "apps/handlers/dispatch/wake.py",
|
"file": "apps/handlers/dispatch/wake.py",
|
||||||
@@ -60,11 +60,6 @@
|
|||||||
"standard": "deep_nesting",
|
"standard": "deep_nesting",
|
||||||
"reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 — registry lookup with path normalization and validation"
|
"reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 — registry lookup with path normalization and validation"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "apps/handlers/email/dashboard_sync.py",
|
|
||||||
"standard": "handlers",
|
|
||||||
"reason": "Imports prax.apps.modules.dashboard.write_section — cross-branch module import required for dashboard integration. No ai_mail module wraps this."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "apps/handlers/email/delivery.py",
|
"file": "apps/handlers/email/delivery.py",
|
||||||
"standard": "handlers",
|
"standard": "handlers",
|
||||||
@@ -93,7 +88,12 @@
|
|||||||
{
|
{
|
||||||
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
||||||
"standard": "handlers",
|
"standard": "handlers",
|
||||||
"reason": "Imports notify.send_notification — same-branch cross-handler import for bounce/completion notifications."
|
"reason": "Imports notify.send_notification (same-branch cross-handler) for bounce/completion notifications. Lazy-imports drone.apps.handlers.broker.client.create_identified_connection inside _connect_broker() for Phase 6b broker-fd handshake (FPLAN-0250) — cross-branch handler import authorized by brief."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
||||||
|
"standard": "encapsulation",
|
||||||
|
"reason": "Lazy-imports drone.apps.handlers.broker.client.create_identified_connection inside _connect_broker() — cross-branch handler import for Phase 6b broker-fd handshake (FPLAN-0250). Brief explicitly authorizes this import path."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "apps/handlers/dispatch/wake.py",
|
"file": "apps/handlers/dispatch/wake.py",
|
||||||
@@ -115,11 +115,6 @@
|
|||||||
"standard": "naming",
|
"standard": "naming",
|
||||||
"reason": "False positive — _append_footer is a function reference stored in a local variable, not a module-level constant."
|
"reason": "False positive — _append_footer is a function reference stored in a local variable, not a module-level constant."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "apps/handlers/email/dashboard_sync.py",
|
|
||||||
"standard": "naming",
|
|
||||||
"reason": "False positive — _write_section is a lazy-import function reference, not a module-level constant."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "apps/handlers/email/delivery.py",
|
"file": "apps/handlers/email/delivery.py",
|
||||||
"standard": "naming",
|
"standard": "naming",
|
||||||
@@ -195,11 +190,6 @@
|
|||||||
"standard": "deep_nesting",
|
"standard": "deep_nesting",
|
||||||
"reason": "_send_direct() depth 5 (arg parsing with branch resolution, --from flag, --dispatch flag), handle_close() depth 4 (close with archive + dashboard update)"
|
"reason": "_send_direct() depth 5 (arg parsing with branch resolution, --from flag, --dispatch flag), handle_close() depth 4 (close with archive + dashboard update)"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "apps/handlers/email/dashboard_sync.py",
|
|
||||||
"standard": "deep_nesting",
|
|
||||||
"reason": "_human_readable_age() depth 5, _calculate_section_data() depth 5 — timestamp parsing with multiple fallback formats"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
||||||
"standard": "deep_nesting",
|
"standard": "deep_nesting",
|
||||||
|
|||||||
@@ -5,11 +5,30 @@
|
|||||||
**Purpose:** Inter-agent messaging for AIPass. File-based email system that lets agents send, receive, and process messages using `@branch` addresses. No SMTP, no external services — just JSON files and symbolic routing.
|
**Purpose:** Inter-agent messaging for AIPass. File-based email system that lets agents send, receive, and process messages using `@branch` addresses. No SMTP, no external services — just JSON files and symbolic routing.
|
||||||
**Module:** `aipass.ai_mail`
|
**Module:** `aipass.ai_mail`
|
||||||
**Created:** 2025-11-08
|
**Created:** 2025-11-08
|
||||||
**Last Updated:** 2026-04-22
|
**Last Updated:** 2026-05-16
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** Operational | **Seedgo:** 100% (34/34) | **Tests:** 355 pass | **Battle Tested:** S62
|
**Status:** Operational | **Seedgo:** 100% (34/34) | **Tests:** 712 pass | **Battle Tested:** S62
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check your inbox
|
||||||
|
drone @ai_mail inbox
|
||||||
|
|
||||||
|
# View a message
|
||||||
|
drone @ai_mail view <id>
|
||||||
|
|
||||||
|
# Reply and close
|
||||||
|
drone @ai_mail reply <id> "your message"
|
||||||
|
|
||||||
|
# Send mail to another branch
|
||||||
|
drone @ai_mail email @target "Subject" "Body"
|
||||||
|
|
||||||
|
# Dispatch (send + wake target agent)
|
||||||
|
drone @ai_mail dispatch @target "Subject" "Body"
|
||||||
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -62,19 +81,22 @@ The `dispatch` command sends an email and wakes the target branch in one step. D
|
|||||||
### Wake Pipeline
|
### Wake Pipeline
|
||||||
|
|
||||||
1. `dispatch.py` orchestrates: send email via `send_to_single()`, then wake via `wake_branch()`
|
1. `dispatch.py` orchestrates: send email via `send_to_single()`, then wake via `wake_branch()`
|
||||||
2. `wake.py` resolves the branch from the registry, finds the `claude` binary, spawns a subprocess
|
2. `wake.py` resolves the branch from the registry, checks `citizen_class` (managers are mail-only — wake skips), finds the `claude` binary, spawns a subprocess
|
||||||
3. `dispatch_monitor.py` wraps the claude process with safety features:
|
3. `dispatch_monitor.py` wraps the claude process with safety features:
|
||||||
- **Startup health check** — monitors JSONL session files for 90s, kills if no activity
|
- **Startup health check** — monitors JSONL session files for 90s, kills if no activity
|
||||||
- **Auto-retry** — 3 strikes: attempt 1+2 resume, attempt 3 fresh (new session)
|
- **Auto-retry** — 3 strikes: attempt 1+2 resume, attempt 3 fresh (new session)
|
||||||
- **Bounce email** — on final failure, sends error report back to sender
|
- **Bounce email** — on final failure, sends error report back to sender
|
||||||
- **Lock cleanup** — removes `.dispatch.lock` when agent exits
|
- **Lock cleanup** — removes `.dispatch.lock` when agent exits
|
||||||
4. After wake, `_spawn_watchdog()` auto-launches `drone @devpulse watchdog agent @target` as a detached background process
|
- **Wake-back** — on agent exit, wakes the original sender so they can process the result. Wake-back sessions carry an empty sender, so chains terminate at the original dispatcher
|
||||||
|
|
||||||
### Safety Limits
|
### Safety Limits
|
||||||
|
|
||||||
- PID-based locking prevents concurrent agents per branch (`.dispatch.lock`)
|
- PID-based locking prevents concurrent agents per branch (`.dispatch.lock`)
|
||||||
- Max turns per wake, max dispatches per branch per day
|
- Max turns per wake, max dispatches per branch per day
|
||||||
- `WAKE_BLOCKLIST` protects `@devpulse` from cross-branch manual wakes
|
- `WAKE_BLOCKLIST` protects `@devpulse` from cross-branch manual wakes
|
||||||
|
- **Manager structural block** — branches with `citizen_class: "manager"` in their passport (e.g. `@devpulse`) are unwakeable on all wake paths. Mail delivers, wake skips
|
||||||
|
- **Self-wake guard** — if sender equals target, wake-back is skipped (prevents self-loops)
|
||||||
|
- **Chain termination** — wake-back sessions carry an empty sender, so the chain always stops at the original dispatcher
|
||||||
- `dispatch_monitor.py` strips `AIPASS_CALLER_*` env vars to prevent parent context leaking into agent identity
|
- `dispatch_monitor.py` strips `AIPASS_CALLER_*` env vars to prevent parent context leaking into agent identity
|
||||||
- `AIPASS_BRANCH_NAME` env var set in spawn_env for CWD-independent identity
|
- `AIPASS_BRANCH_NAME` env var set in spawn_env for CWD-independent identity
|
||||||
|
|
||||||
@@ -149,7 +171,7 @@ ai_mail/
|
|||||||
│ ├── paths.py # Shared find_repo_root() utility
|
│ ├── paths.py # Shared find_repo_root() utility
|
||||||
│ ├── notify.py # Desktop notifications (dbus direct)
|
│ ├── notify.py # Desktop notifications (dbus direct)
|
||||||
│ └── central_writer.py # Central inbox stats aggregation
|
│ └── central_writer.py # Central inbox stats aggregation
|
||||||
└── tests/ # 355 tests across 16 test files
|
└── tests/ # 712 tests across 16 test files
|
||||||
├── conftest.py # Shared fixtures (mock_logger, mock_json_handler)
|
├── conftest.py # Shared fixtures (mock_logger, mock_json_handler)
|
||||||
├── test_daemon.py # Daemon config, state, kill switch, dispatch check
|
├── test_daemon.py # Daemon config, state, kill switch, dispatch check
|
||||||
├── test_dispatch_monitor.py # Monitor safety features, env stripping
|
├── test_dispatch_monitor.py # Monitor safety features, env stripping
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ Main handles routing, modules implement functionality.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import importlib
|
import importlib
|
||||||
import argparse
|
|
||||||
import signal
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
|
# AIPass infrastructure imports
|
||||||
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
|
|
||||||
|
# CLI services for display
|
||||||
|
from aipass.cli.apps.modules import console, error
|
||||||
|
|
||||||
# Handle broken pipe gracefully (e.g. output piped to head)
|
# Handle broken pipe gracefully (e.g. output piped to head)
|
||||||
# SIGPIPE does not exist on Windows
|
# SIGPIPE does not exist on Windows
|
||||||
if hasattr(signal, "SIGPIPE"):
|
if hasattr(signal, "SIGPIPE"):
|
||||||
@@ -29,11 +35,12 @@ if hasattr(signal, "SIGPIPE"):
|
|||||||
# Dashboard integration (optional, provided by prax)
|
# Dashboard integration (optional, provided by prax)
|
||||||
_UPDATE_SECTION = None # type: ignore
|
_UPDATE_SECTION = None # type: ignore
|
||||||
|
|
||||||
# AIPass infrastructure imports
|
if sys.platform == "win32":
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
# CLI services for display
|
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
from aipass.cli.apps.modules import console, error
|
if _reconfigure is not None:
|
||||||
|
_reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# CONSTANTS & CONFIG
|
# CONSTANTS & CONFIG
|
||||||
@@ -51,52 +58,47 @@ MODULES_DIR = MODULE_ROOT / "modules"
|
|||||||
|
|
||||||
|
|
||||||
def print_help():
|
def print_help():
|
||||||
"""Print drone-compliant help output"""
|
"""Print drone-compliant help output with Rich markup"""
|
||||||
parser = argparse.ArgumentParser(
|
console.print()
|
||||||
description="AI_MAIL Branch Operations - Email system for branch communication",
|
console.print("[bold cyan]AI_MAIL — Email system for branch communication[/bold cyan]")
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
console.print()
|
||||||
epilog="""
|
|
||||||
COMMANDS:
|
|
||||||
dispatch - Send dispatch email + wake target (one step)
|
|
||||||
email - Send email to a branch
|
|
||||||
send - Send email (alias for email)
|
|
||||||
inbox - List emails (new + opened)
|
|
||||||
view - View email content (marks as opened)
|
|
||||||
reply - Reply to email (closes + archives)
|
|
||||||
close - Close email(s) without reply (archives)
|
|
||||||
sent - View sent messages
|
|
||||||
contacts - Manage contacts
|
|
||||||
EMAIL LIFECYCLE (v2):
|
|
||||||
new → opened → closed
|
|
||||||
- new: Just arrived, never viewed
|
|
||||||
- opened: You've viewed it, not yet resolved
|
|
||||||
- closed: Resolved (replied or dismissed), auto-archived
|
|
||||||
|
|
||||||
USAGE:
|
console.print("[yellow]COMMANDS:[/yellow]")
|
||||||
drone @ai_mail <command> [args]
|
console.print(" [cyan]dispatch[/cyan] [dim]Send dispatch email + wake target (one step)[/dim]")
|
||||||
drone @ai_mail --help
|
console.print(" [cyan]email[/cyan] [dim]Send email to a branch[/dim]")
|
||||||
|
console.print(" [cyan]send[/cyan] [dim]Send email (alias for email)[/dim]")
|
||||||
|
console.print(" [cyan]inbox[/cyan] [dim]List emails (new + opened)[/dim]")
|
||||||
|
console.print(" [cyan]view[/cyan] [dim]View email content (marks as opened)[/dim]")
|
||||||
|
console.print(" [cyan]reply[/cyan] [dim]Reply to email (closes + archives)[/dim]")
|
||||||
|
console.print(" [cyan]close[/cyan] [dim]Close email(s) without reply (archives)[/dim]")
|
||||||
|
console.print(" [cyan]sent[/cyan] [dim]View sent messages[/dim]")
|
||||||
|
console.print(" [cyan]contacts[/cyan] [dim]Manage contacts[/dim]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
EXAMPLES:
|
console.print("[yellow]EMAIL LIFECYCLE (v2):[/yellow]")
|
||||||
# Dispatch (send + wake in one command)
|
console.print(" new → opened → closed")
|
||||||
drone @ai_mail dispatch @branch "Subject" "Body"
|
console.print(" [dim]new: Just arrived, never viewed[/dim]")
|
||||||
drone @ai_mail dispatch @branch "Subject" "Body" --fresh
|
console.print(" [dim]opened: You've viewed it, not yet resolved[/dim]")
|
||||||
|
console.print(" [dim]closed: Resolved (replied or dismissed), auto-archived[/dim]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
# Send mail (no wake)
|
console.print("[yellow]USAGE:[/yellow]")
|
||||||
drone @ai_mail email @seedgo "Subject" "Msg" # Send to branch
|
console.print(" [cyan]drone @ai_mail[/cyan] <command> [args]")
|
||||||
drone @ai_mail email @all "Subject" "Msg" # Broadcast to all
|
console.print(" [cyan]drone @ai_mail --help[/cyan]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
# Check mail
|
console.print("[yellow]EXAMPLES:[/yellow]")
|
||||||
drone @ai_mail inbox # List all emails
|
console.print(' [cyan]drone @ai_mail dispatch @branch "Subject" "Body"[/cyan]')
|
||||||
drone @ai_mail view abc123 # View email (marks as opened)
|
console.print(' [cyan]drone @ai_mail dispatch @branch "Subject" "Body" --fresh[/cyan]')
|
||||||
|
console.print(' [cyan]drone @ai_mail email @seedgo "Subject" "Msg"[/cyan] [dim]Send to branch[/dim]')
|
||||||
# Resolve emails
|
console.print(' [cyan]drone @ai_mail email @all "Subject" "Msg"[/cyan] [dim]Broadcast to all[/dim]')
|
||||||
drone @ai_mail reply abc123 "Thanks!" # Reply + close + archive
|
console.print(" [cyan]drone @ai_mail inbox[/cyan] [dim]List all emails[/dim]")
|
||||||
drone @ai_mail close abc123 # Close single email
|
console.print(" [cyan]drone @ai_mail view abc123[/cyan] [dim]View email[/dim]")
|
||||||
drone @ai_mail close abc123 def456 ghi789 # Close multiple emails
|
console.print(' [cyan]drone @ai_mail reply abc123 "Thanks!"[/cyan] [dim]Reply + close + archive[/dim]')
|
||||||
drone @ai_mail close all # Close ALL emails
|
console.print(" [cyan]drone @ai_mail close abc123[/cyan] [dim]Close single email[/dim]")
|
||||||
""",
|
console.print(" [cyan]drone @ai_mail close abc123 def456 ghi789[/cyan] [dim]Close multiple[/dim]")
|
||||||
)
|
console.print(" [cyan]drone @ai_mail close all[/cyan] [dim]Close ALL emails[/dim]")
|
||||||
console.print(parser.format_help())
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -241,6 +243,13 @@ def main():
|
|||||||
error("No modules found")
|
error("No modules found")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if remaining_args and remaining_args[0] in ["--help", "-h"]:
|
||||||
|
for module in modules:
|
||||||
|
if module.handle_command(command, ["--help"]):
|
||||||
|
return 0
|
||||||
|
print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
# Route command
|
# Route command
|
||||||
if route_command(command, remaining_args, modules):
|
if route_command(command, remaining_args, modules):
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Architecture:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# CRITICAL: Use importlib to bypass local json/ directory and get stdlib json
|
# CRITICAL: Use importlib to bypass local json/ directory and get stdlib json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
|
||||||
@@ -32,13 +33,20 @@ stdlib_json = importlib.util.module_from_spec(spec)
|
|||||||
spec.loader.exec_module(stdlib_json)
|
spec.loader.exec_module(stdlib_json)
|
||||||
sys.path = _saved_path
|
sys.path = _saved_path
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path # noqa: E402
|
||||||
from datetime import datetime
|
from datetime import datetime # noqa: E402
|
||||||
from typing import Dict, Any, List, Tuple
|
from typing import Dict, Any, List, Tuple # noqa: E402
|
||||||
|
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger # noqa: E402
|
||||||
from aipass.ai_mail.apps.handlers.json import json_handler
|
from aipass.ai_mail.apps.handlers.json import json_handler # noqa: E402
|
||||||
from aipass.ai_mail.apps.handlers.paths import find_repo_root
|
from aipass.ai_mail.apps.handlers.paths import find_repo_root # noqa: E402
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
|
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
|
if _reconfigure is not None:
|
||||||
|
_reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -341,5 +349,7 @@ if __name__ == "__main__":
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]Error:[/red] {e}")
|
from aipass.cli.apps.modules import error as cli_error
|
||||||
|
|
||||||
|
cli_error(f"Error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from aipass.prax.apps.modules.logger import system_logger as logger
|
|||||||
from aipass.ai_mail.apps.handlers.json import json_handler
|
from aipass.ai_mail.apps.handlers.json import json_handler
|
||||||
from aipass.ai_mail.apps.handlers.dispatch.status import log_dispatch
|
from aipass.ai_mail.apps.handlers.dispatch.status import log_dispatch
|
||||||
from aipass.ai_mail.apps.handlers.paths import find_repo_root
|
from aipass.ai_mail.apps.handlers.paths import find_repo_root
|
||||||
|
from aipass.ai_mail.apps.handlers.dispatch.test_token import scan_and_ack_test_emails
|
||||||
|
|
||||||
|
|
||||||
# Infrastructure paths
|
# Infrastructure paths
|
||||||
@@ -49,9 +50,6 @@ BRANCH_REGISTRY = _REPO_ROOT / "AIPASS_REGISTRY.json"
|
|||||||
# Graceful shutdown
|
# Graceful shutdown
|
||||||
SHUTDOWN = False
|
SHUTDOWN = False
|
||||||
|
|
||||||
# AIPASS-TEST token handling extracted to test_token.py
|
|
||||||
from aipass.ai_mail.apps.handlers.dispatch.test_token import scan_and_ack_test_emails
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_signal(signum, _frame):
|
def _handle_signal(signum, _frame):
|
||||||
"""Handle shutdown signals for graceful daemon stop."""
|
"""Handle shutdown signals for graceful daemon stop."""
|
||||||
@@ -88,6 +86,56 @@ def _write_json(filepath: Path, data: Dict[str, Any]) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _pid_alive_windows(pid: int) -> bool:
|
||||||
|
"""Windows-safe liveness check via OpenProcess + GetExitCodeProcess."""
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||||
|
STILL_ACTIVE = 259
|
||||||
|
|
||||||
|
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
||||||
|
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
||||||
|
kernel32.OpenProcess.restype = wintypes.HANDLE
|
||||||
|
kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
||||||
|
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
|
||||||
|
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
||||||
|
kernel32.CloseHandle.restype = wintypes.BOOL
|
||||||
|
|
||||||
|
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
||||||
|
if not handle:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
exit_code = wintypes.DWORD()
|
||||||
|
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
||||||
|
return False
|
||||||
|
return exit_code.value == STILL_ACTIVE
|
||||||
|
finally:
|
||||||
|
kernel32.CloseHandle(handle)
|
||||||
|
|
||||||
|
|
||||||
|
def _pid_alive(pid: int) -> bool:
|
||||||
|
"""Return True if the process is alive."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
return _pid_alive_windows(pid)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("[daemon] PID %s Windows check failed (assuming alive): %s", pid, exc)
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError as exc:
|
||||||
|
logger.info("[daemon] PID %s not found: %s", pid, exc)
|
||||||
|
return False
|
||||||
|
except PermissionError as exc:
|
||||||
|
logger.info("[daemon] PID %s permission denied (alive): %s", pid, exc)
|
||||||
|
return True
|
||||||
|
except OSError as exc:
|
||||||
|
logger.info("[daemon] PID %s os.kill error (assuming dead): %s", pid, exc)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _check_lock(branch_path: Path) -> Optional[Dict[str, Any]]:
|
def _check_lock(branch_path: Path) -> Optional[Dict[str, Any]]:
|
||||||
"""Check if branch has an active dispatch lock. Returns lock data or None."""
|
"""Check if branch has an active dispatch lock. Returns lock data or None."""
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
@@ -98,14 +146,9 @@ def _check_lock(branch_path: Path) -> Optional[Dict[str, Any]]:
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
pid = data.get("pid")
|
pid = data.get("pid")
|
||||||
if pid is not None:
|
if pid is not None:
|
||||||
try:
|
if _pid_alive(pid):
|
||||||
os.kill(pid, 0)
|
return data
|
||||||
return data # Process alive, lock valid
|
logger.info("Lock PID %s dead — stale lock cleanup needed", pid)
|
||||||
except ProcessLookupError:
|
|
||||||
logger.info("Lock PID %s dead — stale lock cleanup needed", pid)
|
|
||||||
except PermissionError as e:
|
|
||||||
logger.warning("[daemon] Lock PID %s permission error: %s", pid, e)
|
|
||||||
return data # Process exists, can't signal
|
|
||||||
# Stale lock — check age (10 min timeout)
|
# Stale lock — check age (10 min timeout)
|
||||||
ts = data.get("timestamp", "")
|
ts = data.get("timestamp", "")
|
||||||
if ts:
|
if ts:
|
||||||
@@ -216,15 +259,10 @@ def _write_pid_file() -> bool:
|
|||||||
# PID file exists — check if the owning process is alive
|
# PID file exists — check if the owning process is alive
|
||||||
try:
|
try:
|
||||||
old_pid = int(DAEMON_PID_FILE.read_text().strip())
|
old_pid = int(DAEMON_PID_FILE.read_text().strip())
|
||||||
try:
|
if _pid_alive(old_pid):
|
||||||
os.kill(old_pid, 0)
|
logger.info("Another daemon already running (PID %s). Exiting.", old_pid)
|
||||||
logger.info(f"Another daemon already running (PID {old_pid}). Exiting.")
|
|
||||||
return False
|
|
||||||
except ProcessLookupError:
|
|
||||||
logger.info(f"Removing stale PID file (PID {old_pid} is dead)")
|
|
||||||
except PermissionError:
|
|
||||||
logger.info(f"Another daemon already running (PID {old_pid}, permission denied). Exiting.")
|
|
||||||
return False
|
return False
|
||||||
|
logger.info("Removing stale PID file (PID %s is dead)", old_pid)
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
logger.info("Corrupt PID file — removing")
|
logger.info("Corrupt PID file — removing")
|
||||||
|
|
||||||
@@ -410,21 +448,31 @@ def spawn_agent(
|
|||||||
logger.info(f"Lock acquisition failed for {branch_email}: {lock_msg}")
|
logger.info(f"Lock acquisition failed for {branch_email}: {lock_msg}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
_detach_kwargs: dict = {}
|
||||||
|
if sys.platform == "win32":
|
||||||
|
_detach_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
else:
|
||||||
|
_detach_kwargs["start_new_session"] = True
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
monitor_cmd,
|
monitor_cmd,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
start_new_session=True,
|
|
||||||
cwd=str(branch_path),
|
cwd=str(branch_path),
|
||||||
env=spawn_env,
|
env=spawn_env,
|
||||||
|
**_detach_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
monitor_pid = process.pid
|
monitor_pid = process.pid
|
||||||
|
|
||||||
# Update lock with real monitor PID
|
# Update lock with real monitor PID
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
lock_data = {"pid": monitor_pid, "timestamp": datetime.now().isoformat(), "branch": str(branch_path)}
|
lock_data = {
|
||||||
|
"pid": monitor_pid,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"branch": str(branch_path),
|
||||||
|
"subject": subject,
|
||||||
|
}
|
||||||
_write_json(lock_file, lock_data)
|
_write_json(lock_file, lock_data)
|
||||||
|
|
||||||
# Track session cycles for rotation
|
# Track session cycles for rotation
|
||||||
@@ -466,18 +514,74 @@ def is_protected_branch(branch_email: str) -> bool:
|
|||||||
return branch_email == "@devpulse"
|
return branch_email == "@devpulse"
|
||||||
|
|
||||||
|
|
||||||
def _read_session_type(pid_str: str) -> str:
|
def _get_pid_cwd(pid_str: str) -> Optional[str]:
|
||||||
"""Read AIPASS_SESSION_TYPE from /proc/{pid}/environ. Returns 'interactive' if unset."""
|
"""Get the cwd of a process. Cross-platform: Linux /proc, macOS lsof."""
|
||||||
if sys.platform != "linux":
|
if sys.platform == "linux":
|
||||||
return "interactive"
|
try:
|
||||||
|
return os.readlink(f"/proc/{pid_str}/cwd")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
logger.info("[daemon] Cannot read cwd for PID %s", pid_str)
|
||||||
|
return None
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return _get_pid_cwd_darwin(pid_str)
|
||||||
|
logger.info("[daemon] Cannot determine cwd for PID %s on %s", pid_str, sys.platform)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pid_cwd_darwin(pid_str: str) -> Optional[str]:
|
||||||
|
"""macOS: get process cwd via lsof."""
|
||||||
try:
|
try:
|
||||||
with open(f"/proc/{pid_str}/environ", "rb") as f:
|
result = subprocess.run(
|
||||||
data = f.read()
|
["lsof", "-a", "-p", pid_str, "-d", "cwd", "-Fn"],
|
||||||
for entry in data.split(b"\0"):
|
capture_output=True,
|
||||||
if entry.startswith(b"AIPASS_SESSION_TYPE="):
|
text=True,
|
||||||
return entry.split(b"=", 1)[1].decode("utf-8")
|
timeout=5,
|
||||||
except (OSError, PermissionError):
|
)
|
||||||
logger.info("Cannot read session type for PID %s", pid_str)
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
logger.info("[daemon] Cannot read cwd for PID %s on macOS", pid_str)
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
for line in result.stdout.strip().split("\n"):
|
||||||
|
if line.startswith("n/"):
|
||||||
|
return line[1:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_session_type(pid_str: str) -> str:
|
||||||
|
"""Read AIPASS_SESSION_TYPE from process environment. Returns 'interactive' if unset."""
|
||||||
|
if sys.platform == "linux":
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{pid_str}/environ", "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
for entry in data.split(b"\0"):
|
||||||
|
if entry.startswith(b"AIPASS_SESSION_TYPE="):
|
||||||
|
return entry.split(b"=", 1)[1].decode("utf-8")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
logger.info("[daemon] Cannot read session type for PID %s", pid_str)
|
||||||
|
return "interactive"
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return _read_session_type_darwin(pid_str)
|
||||||
|
return "interactive"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_session_type_darwin(pid_str: str) -> str:
|
||||||
|
"""macOS: read AIPASS_SESSION_TYPE from ps environment output."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["ps", "-p", pid_str, "-wwE", "-o", "command="],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
logger.info("[daemon] Cannot read session type for PID %s on macOS", pid_str)
|
||||||
|
return "interactive"
|
||||||
|
if result.returncode != 0:
|
||||||
|
return "interactive"
|
||||||
|
for token in result.stdout.split():
|
||||||
|
if token.startswith("AIPASS_SESSION_TYPE="):
|
||||||
|
return token.split("=", 1)[1]
|
||||||
return "interactive"
|
return "interactive"
|
||||||
|
|
||||||
|
|
||||||
@@ -486,35 +590,25 @@ _NON_BLOCKING_SESSION_TYPES = {"dispatched", "daemon"}
|
|||||||
|
|
||||||
|
|
||||||
def _is_branch_occupied(branch_path: Path) -> bool:
|
def _is_branch_occupied(branch_path: Path) -> bool:
|
||||||
"""
|
"""Check if an interactive Claude session is running in this branch."""
|
||||||
Check if an interactive Claude session is running in this branch.
|
resolved = str(branch_path.resolve())
|
||||||
|
|
||||||
Only interactive sessions block dispatch. Telegram, dispatched, and daemon
|
|
||||||
sessions are idle/background and should not prevent new agent spawns.
|
|
||||||
"""
|
|
||||||
resolved = branch_path.resolve()
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(["pgrep", "-x", "claude"], capture_output=True, text=True, timeout=5)
|
result = subprocess.run(["pgrep", "-x", "claude"], capture_output=True, text=True, timeout=5)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for pid_str in result.stdout.strip().split("\n"):
|
for pid_str in result.stdout.strip().split("\n"):
|
||||||
pid_str = pid_str.strip()
|
pid_str = pid_str.strip()
|
||||||
if not pid_str:
|
if not pid_str:
|
||||||
continue
|
continue
|
||||||
try:
|
cwd = _get_pid_cwd(pid_str)
|
||||||
if sys.platform != "linux":
|
if cwd is None:
|
||||||
continue
|
|
||||||
cwd = os.readlink(f"/proc/{pid_str}/cwd")
|
|
||||||
if Path(cwd).resolve() == resolved:
|
|
||||||
session_type = _read_session_type(pid_str)
|
|
||||||
if session_type not in _NON_BLOCKING_SESSION_TYPES:
|
|
||||||
return True
|
|
||||||
except (OSError, PermissionError, ValueError):
|
|
||||||
logger.info("Cannot read cwd for PID %s", pid_str)
|
|
||||||
continue
|
continue
|
||||||
|
if str(Path(cwd).resolve()) == resolved:
|
||||||
|
session_type = _read_session_type(pid_str)
|
||||||
|
if session_type not in _NON_BLOCKING_SESSION_TYPES:
|
||||||
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.info("Failed to check branch occupancy for %s", branch_path)
|
logger.info("[daemon] Failed to check branch occupancy for %s", branch_path)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -609,15 +703,15 @@ def run_daemon() -> None:
|
|||||||
cycle_count = 0
|
cycle_count = 0
|
||||||
|
|
||||||
while not SHUTDOWN:
|
while not SHUTDOWN:
|
||||||
# Reap zombie children from previously spawned agents
|
if sys.platform != "win32":
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
pid, _ = os.waitpid(-1, os.WNOHANG)
|
pid, _ = os.waitpid(-1, os.WNOHANG)
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
break
|
break
|
||||||
logger.info(f"Reaped child process PID {pid}")
|
logger.info(f"Reaped child process PID {pid}")
|
||||||
except ChildProcessError:
|
except ChildProcessError:
|
||||||
logger.info("No child processes to reap")
|
logger.info("No child processes to reap")
|
||||||
|
|
||||||
if is_kill_switch_active(config):
|
if is_kill_switch_active(config):
|
||||||
logger.info("Kill switch ACTIVE - pausing all dispatches")
|
logger.info("Kill switch ACTIVE - pausing all dispatches")
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ is guaranteed.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -41,6 +43,117 @@ HARD_TIMEOUT = 7200 # 2 hours
|
|||||||
POLL_INTERVAL = 5
|
POLL_INTERVAL = 5
|
||||||
|
|
||||||
|
|
||||||
|
def _is_sandbox_enabled() -> bool:
|
||||||
|
"""Check if dispatch sandbox is enabled via AIPASS_SANDBOX_ENABLED env var."""
|
||||||
|
return os.environ.get("AIPASS_SANDBOX_ENABLED", "").lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_for_sandbox(cmd: list, branch_path: Path) -> list:
|
||||||
|
"""Wrap a claude command in the srt kernel sandbox.
|
||||||
|
|
||||||
|
Uses @hooks sandbox building blocks to resolve the bwrap command,
|
||||||
|
then returns a shell invocation list compatible with Popen.
|
||||||
|
|
||||||
|
Raises on ANY failure — caller must not silently fall back to unsandboxed.
|
||||||
|
"""
|
||||||
|
from aipass.hooks.apps.modules.sandbox import build_policy, build_srt_config, resolve_bwrap_command
|
||||||
|
|
||||||
|
policy = build_policy(branch_path)
|
||||||
|
srt_config = build_srt_config(policy)
|
||||||
|
cmd_str = shlex.join(cmd)
|
||||||
|
bwrap_cmd = resolve_bwrap_command(cmd_str, srt_config)
|
||||||
|
return ["/bin/bash", "-c", bwrap_cmd]
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_broker(repo_root: Path, branch_name: str) -> socket.socket:
|
||||||
|
"""Create an identified broker connection for the target branch.
|
||||||
|
|
||||||
|
Returns a connected, HMAC-authenticated socket ready to be inherited
|
||||||
|
by the sandboxed child via pass_fds + AIPASS_BROKER_FD.
|
||||||
|
|
||||||
|
Raises on ANY failure — caller must not silently skip the broker.
|
||||||
|
"""
|
||||||
|
from aipass.drone.apps.handlers.broker.client import create_identified_connection
|
||||||
|
|
||||||
|
socket_path = repo_root / ".ai_central" / "drone_broker.sock"
|
||||||
|
secret_path = repo_root / ".ai_central" / "broker_secret"
|
||||||
|
return create_identified_connection(socket_path, secret_path, branch_name)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_WAKE_DEPTH = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _wake_sender(sender: str, branch_email: str, exit_code: int, lock_file: str) -> str:
|
||||||
|
"""Wake the dispatcher back after target completion.
|
||||||
|
|
||||||
|
Any citizen sender gets woken back (same availability checks as
|
||||||
|
normal wake — interactive session, active lock, depth cap).
|
||||||
|
|
||||||
|
Returns a result tag for the dispatch_wake.log:
|
||||||
|
success, blocked_occupied, blocked_locked, blocked_depth,
|
||||||
|
skipped_sender, skipped_self, failed
|
||||||
|
"""
|
||||||
|
if not sender or not sender.strip():
|
||||||
|
logger.info("[monitor] Wake-back skipped — no sender")
|
||||||
|
return "skipped_sender"
|
||||||
|
|
||||||
|
normalized_sender = f"@{sender.lstrip('@').lower()}"
|
||||||
|
normalized_target = f"@{branch_email.lstrip('@').lower()}"
|
||||||
|
if normalized_sender == normalized_target:
|
||||||
|
logger.info("[monitor] Wake-back skipped — sender %s is the completed agent (self-wake)", sender)
|
||||||
|
return "skipped_self"
|
||||||
|
|
||||||
|
depth = int(os.environ.get("AIPASS_WAKE_DEPTH", "0"))
|
||||||
|
if depth >= MAX_WAKE_DEPTH:
|
||||||
|
logger.warning("[monitor] Wake-back skipped — depth %d >= max %d", depth, MAX_WAKE_DEPTH)
|
||||||
|
return "blocked_depth"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from aipass.ai_mail.apps.handlers.dispatch.wake import wake_branch
|
||||||
|
|
||||||
|
os.environ["AIPASS_WAKE_DEPTH"] = str(depth + 1)
|
||||||
|
wake_status, success = wake_branch(sender, auto=True, sender="")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("[monitor] Wake-back: %s woken after %s completed (exit %d)", sender, branch_email, exit_code)
|
||||||
|
return "success"
|
||||||
|
|
||||||
|
summary = wake_status.summary
|
||||||
|
lower = summary.lower()
|
||||||
|
if "interactive" in lower or "occupancy" in lower or "occupied" in lower:
|
||||||
|
logger.info("[monitor] Wake-back blocked — sender %s has interactive session: %s", sender, summary)
|
||||||
|
return "blocked_occupied"
|
||||||
|
if "active agent" in lower or "lock" in lower:
|
||||||
|
logger.info("[monitor] Wake-back blocked — sender %s has active lock: %s", sender, summary)
|
||||||
|
return "blocked_locked"
|
||||||
|
|
||||||
|
logger.info("[monitor] Wake-back: %s not woken — %s", sender, summary)
|
||||||
|
return "failed"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[monitor] Wake-back failed for %s: %s", sender, e)
|
||||||
|
return "failed"
|
||||||
|
|
||||||
|
|
||||||
|
def _log_wake_result(branch_email: str, sender: str, exit_code: int, result: str, lock_file: str):
|
||||||
|
"""Append a wake-back result line to dispatch_wake.log under target's logs/."""
|
||||||
|
lock_path = Path(lock_file).resolve()
|
||||||
|
logs_dir = lock_path.parent.parent / "logs"
|
||||||
|
log_file = logs_dir / "dispatch_wake.log"
|
||||||
|
try:
|
||||||
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
line = (
|
||||||
|
f"{time.strftime('%Y-%m-%dT%H:%M:%S')}"
|
||||||
|
f" target={branch_email}"
|
||||||
|
f" sender={sender}"
|
||||||
|
f" exit_code={exit_code}"
|
||||||
|
f" wake_result={result}\n"
|
||||||
|
)
|
||||||
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
|
f.write(line)
|
||||||
|
except OSError as e:
|
||||||
|
logger.info("[monitor] Failed to write dispatch_wake.log: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _send_bounce(branch_email: str, reason: str, sender: str, lock_file: str, stderr_log: str) -> bool:
|
def _send_bounce(branch_email: str, reason: str, sender: str, lock_file: str, stderr_log: str) -> bool:
|
||||||
"""Send return-to-sender bounce email via drone."""
|
"""Send return-to-sender bounce email via drone."""
|
||||||
subject = f"BOUNCE: Dispatch to {branch_email} failed"
|
subject = f"BOUNCE: Dispatch to {branch_email} failed"
|
||||||
@@ -95,16 +208,27 @@ def _send_bounce(branch_email: str, reason: str, sender: str, lock_file: str, st
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_rate_limited(stderr_log: str) -> bool:
|
def _read_agent_stderr(stderr_log: str) -> str:
|
||||||
"""Check if stderr indicates API rate limiting or overload."""
|
"""Read the dispatch stderr log, excluding the monitor's own framing lines.
|
||||||
|
|
||||||
|
The monitor writes header/footer/attempt markers (all prefixed with "--- ")
|
||||||
|
that embed the PID and timestamps. Those numbers must NOT be scanned for API
|
||||||
|
error markers -- e.g. a PID like 14290 contains "429" and would otherwise be
|
||||||
|
misread as an HTTP 429 rate-limit. Returns "" if the log can't be read.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with open(stderr_log, "r", encoding="utf-8") as f:
|
with open(stderr_log, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
return "".join(line for line in f if not line.lstrip().startswith("---"))
|
||||||
lower = content.lower()
|
|
||||||
return "rate_limit" in lower or "429" in content or "overloaded" in lower or "529" in content
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning("[monitor] _check_rate_limited failed reading %s: %s", stderr_log, e)
|
logger.warning("[monitor] Failed reading stderr log %s: %s", stderr_log, e)
|
||||||
return False
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _check_rate_limited(stderr_log: str) -> bool:
|
||||||
|
"""Check if stderr indicates API rate limiting or overload."""
|
||||||
|
content = _read_agent_stderr(stderr_log)
|
||||||
|
lower = content.lower()
|
||||||
|
return "rate_limit" in lower or "429" in content or "overloaded" in lower or "529" in content
|
||||||
|
|
||||||
|
|
||||||
def _make_fresh_cmd(claude_cmd: list) -> list:
|
def _make_fresh_cmd(claude_cmd: list) -> list:
|
||||||
@@ -176,7 +300,7 @@ def _kill_process(process: subprocess.Popen, branch_email: str):
|
|||||||
|
|
||||||
|
|
||||||
def _run_with_startup_check(
|
def _run_with_startup_check(
|
||||||
claude_cmd: list, stdout_log: str, stderr_fh, cwd: str, spawn_env: dict, branch_email: str
|
claude_cmd: list, stdout_log: str, stderr_fh, cwd: str, spawn_env: dict, branch_email: str, pass_fds: tuple = ()
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""
|
"""
|
||||||
Run claude with startup timeout check.
|
Run claude with startup timeout check.
|
||||||
@@ -194,13 +318,17 @@ def _run_with_startup_check(
|
|||||||
logger.warning("[monitor] Failed to open stdout log %s: %s", stdout_log, e)
|
logger.warning("[monitor] Failed to open stdout log %s: %s", stdout_log, e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
popen_kwargs = {
|
||||||
claude_cmd,
|
"stdin": subprocess.DEVNULL,
|
||||||
stdout=stdout_fh if stdout_fh is not None else subprocess.DEVNULL,
|
"stdout": stdout_fh if stdout_fh is not None else subprocess.DEVNULL,
|
||||||
stderr=stderr_fh,
|
"stderr": stderr_fh,
|
||||||
cwd=cwd,
|
"cwd": cwd,
|
||||||
env=spawn_env,
|
"env": spawn_env,
|
||||||
)
|
}
|
||||||
|
if pass_fds:
|
||||||
|
popen_kwargs["close_fds"] = True
|
||||||
|
popen_kwargs["pass_fds"] = pass_fds
|
||||||
|
process = subprocess.Popen(claude_cmd, **popen_kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("[monitor] Failed to spawn %s: %s", branch_email, e)
|
logger.warning("[monitor] Failed to spawn %s: %s", branch_email, e)
|
||||||
if stdout_fh is not None:
|
if stdout_fh is not None:
|
||||||
@@ -274,6 +402,18 @@ def main():
|
|||||||
|
|
||||||
json_handler.log_operation("dispatch_monitor_start", {"branch": branch_email, "sender": sender})
|
json_handler.log_operation("dispatch_monitor_start", {"branch": branch_email, "sender": sender})
|
||||||
|
|
||||||
|
# Self-register PID in lock file — the parent may have written its own
|
||||||
|
# PID during pre-spawn lock acquisition (DPLAN-0155), and under
|
||||||
|
# systemd-run the parent PID belongs to the caller, not the monitor.
|
||||||
|
try:
|
||||||
|
lock_path_obj = Path(lock_file)
|
||||||
|
if lock_path_obj.exists():
|
||||||
|
ld = json.loads(lock_path_obj.read_text(encoding="utf-8"))
|
||||||
|
ld["pid"] = os.getpid()
|
||||||
|
lock_path_obj.write_text(json.dumps(ld, indent=2), encoding="utf-8")
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
logger.info("[monitor] Could not self-register PID in lock file %s", lock_file)
|
||||||
|
|
||||||
# Open stderr log for claude output (rotate if > 500KB)
|
# Open stderr log for claude output (rotate if > 500KB)
|
||||||
stderr_fh = None
|
stderr_fh = None
|
||||||
try:
|
try:
|
||||||
@@ -338,6 +478,11 @@ def main():
|
|||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
# ─── Sandbox Gate ─────────────────────────────────────
|
||||||
|
sandbox_enabled = _is_sandbox_enabled()
|
||||||
|
if sandbox_enabled:
|
||||||
|
logger.info("[monitor] Sandbox ENABLED for %s", branch_email)
|
||||||
|
|
||||||
# ─── Retry Loop: 3 Strikes ─────────────────────────────
|
# ─── Retry Loop: 3 Strikes ─────────────────────────────
|
||||||
# Strike 1: original command (resume if -c was passed)
|
# Strike 1: original command (resume if -c was passed)
|
||||||
# Strike 2: same command again (transient failure)
|
# Strike 2: same command again (transient failure)
|
||||||
@@ -356,14 +501,60 @@ def main():
|
|||||||
cmd = claude_cmd
|
cmd = claude_cmd
|
||||||
mode = "resume" if has_resume else "fresh"
|
mode = "resume" if has_resume else "fresh"
|
||||||
|
|
||||||
|
# Sandbox wrap + broker fd: when enabled, wrap cmd and connect broker.
|
||||||
|
# On failure: abort — NEVER silently launch unsandboxed.
|
||||||
|
run_cmd = cmd
|
||||||
|
broker_sock = None
|
||||||
|
attempt_pass_fds: tuple = ()
|
||||||
|
if sandbox_enabled:
|
||||||
|
try:
|
||||||
|
run_cmd = _wrap_for_sandbox(cmd, branch_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"[monitor] Sandbox init FAILED for %s: %s — ABORTING (will NOT launch unsandboxed)",
|
||||||
|
branch_email,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
exit_code = -4
|
||||||
|
attempts.append({"attempt": attempt, "exit_code": exit_code, "startup_failed": False, "mode": mode})
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
broker_sock = _connect_broker(_repo_root, branch_email.lstrip("@"))
|
||||||
|
broker_fd = broker_sock.fileno()
|
||||||
|
spawn_env["AIPASS_BROKER_FD"] = str(broker_fd)
|
||||||
|
attempt_pass_fds = (broker_fd,)
|
||||||
|
logger.info("[monitor] Broker fd %d connected for %s", broker_fd, branch_email)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"[monitor] Broker connect FAILED for %s: %s — ABORTING",
|
||||||
|
branch_email,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
exit_code = -4
|
||||||
|
attempts.append({"attempt": attempt, "exit_code": exit_code, "startup_failed": False, "mode": mode})
|
||||||
|
break
|
||||||
|
|
||||||
if stderr_fh is not None:
|
if stderr_fh is not None:
|
||||||
stderr_fh.write(f"\n--- Attempt {attempt}/3 ({mode}) at {time.strftime('%H:%M:%S')} ---\n")
|
stderr_fh.write(f"\n--- Attempt {attempt}/3 ({mode}) at {time.strftime('%H:%M:%S')} ---\n")
|
||||||
stderr_fh.flush()
|
stderr_fh.flush()
|
||||||
|
|
||||||
exit_code, startup_failed = _run_with_startup_check(
|
exit_code, startup_failed = _run_with_startup_check(
|
||||||
cmd, stdout_log, stderr_fh if stderr_fh is not None else subprocess.DEVNULL, cwd, spawn_env, branch_email
|
run_cmd,
|
||||||
|
stdout_log,
|
||||||
|
stderr_fh if stderr_fh is not None else subprocess.DEVNULL,
|
||||||
|
cwd,
|
||||||
|
spawn_env,
|
||||||
|
branch_email,
|
||||||
|
pass_fds=attempt_pass_fds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Close parent's broker socket copy — child owns the fd now.
|
||||||
|
if broker_sock is not None:
|
||||||
|
broker_sock.close()
|
||||||
|
broker_sock = None
|
||||||
|
spawn_env.pop("AIPASS_BROKER_FD", None)
|
||||||
|
|
||||||
attempts.append({"attempt": attempt, "exit_code": exit_code, "startup_failed": startup_failed, "mode": mode})
|
attempts.append({"attempt": attempt, "exit_code": exit_code, "startup_failed": startup_failed, "mode": mode})
|
||||||
|
|
||||||
# Success — done
|
# Success — done
|
||||||
@@ -434,16 +625,14 @@ def main():
|
|||||||
|
|
||||||
reason = f"All {len(attempts)} attempts failed after {duration}s.\n" + "\n".join(attempt_details)
|
reason = f"All {len(attempts)} attempts failed after {duration}s.\n" + "\n".join(attempt_details)
|
||||||
|
|
||||||
# Check stderr for specific error categories
|
# Check stderr for specific error categories. Exclude the monitor's own
|
||||||
try:
|
# framing lines (PID/timestamp headers) so a number like a PID containing
|
||||||
with open(stderr_log, "r", encoding="utf-8") as f:
|
# "429" is not misread as an HTTP 429 rate-limit response.
|
||||||
content = f.read()
|
content = _read_agent_stderr(stderr_log)
|
||||||
if "rate_limit" in content.lower() or "429" in content:
|
if "rate_limit" in content.lower() or "429" in content:
|
||||||
reason = f"API rate limit (all {len(attempts)} attempts failed, {duration}s)"
|
reason = f"API rate limit (all {len(attempts)} attempts failed, {duration}s)"
|
||||||
elif "overloaded" in content.lower() or "529" in content:
|
elif "overloaded" in content.lower() or "529" in content:
|
||||||
reason = f"API overloaded (all {len(attempts)} attempts failed, {duration}s)"
|
reason = f"API overloaded (all {len(attempts)} attempts failed, {duration}s)"
|
||||||
except OSError:
|
|
||||||
logger.info("[monitor] Failed to read stderr log for diagnostics")
|
|
||||||
|
|
||||||
_send_bounce(branch_email, reason, sender, lock_file, stderr_log)
|
_send_bounce(branch_email, reason, sender, lock_file, stderr_log)
|
||||||
|
|
||||||
@@ -470,6 +659,10 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.info("[monitor] Desktop notification unavailable")
|
logger.info("[monitor] Desktop notification unavailable")
|
||||||
|
|
||||||
|
# ─── Wake-back: wake the dispatcher ────────────────────
|
||||||
|
wake_result = _wake_sender(sender, branch_email, exit_code, lock_file)
|
||||||
|
_log_wake_result(branch_email, sender, exit_code, wake_result, lock_file)
|
||||||
|
|
||||||
sys.exit(0 if exit_code == 0 else 1)
|
sys.exit(0 if exit_code == 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,22 @@ without triggering dispatch. Extracted from daemon.py to keep
|
|||||||
the daemon under the 700-line architecture threshold.
|
the daemon under the 700-line architecture threshold.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
from aipass.ai_mail.apps.handlers.json import json_handler
|
from aipass.ai_mail.apps.handlers.json import json_handler
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
|
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
|
if _reconfigure is not None:
|
||||||
|
_reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
TEST_TOKEN = "[AIPASS-TEST — do not update memories, do not execute, reply 'ack' only]"
|
TEST_TOKEN = "[AIPASS-TEST — do not update memories, do not execute, reply 'ack' only]"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,34 @@ def _read_json(filepath: Path) -> Optional[dict]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pid_alive_windows(pid: int) -> bool:
|
||||||
|
"""Windows-safe liveness check via OpenProcess + GetExitCodeProcess."""
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||||
|
STILL_ACTIVE = 259
|
||||||
|
|
||||||
|
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
||||||
|
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
||||||
|
kernel32.OpenProcess.restype = wintypes.HANDLE
|
||||||
|
kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
||||||
|
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
|
||||||
|
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
||||||
|
kernel32.CloseHandle.restype = wintypes.BOOL
|
||||||
|
|
||||||
|
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
||||||
|
if not handle:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
exit_code = wintypes.DWORD()
|
||||||
|
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
||||||
|
return False
|
||||||
|
return exit_code.value == STILL_ACTIVE
|
||||||
|
finally:
|
||||||
|
kernel32.CloseHandle(handle)
|
||||||
|
|
||||||
|
|
||||||
def _check_lock(branch_path: Path) -> Optional[dict]:
|
def _check_lock(branch_path: Path) -> Optional[dict]:
|
||||||
"""Check if branch has an active dispatch lock. Returns lock data or None."""
|
"""Check if branch has an active dispatch lock. Returns lock data or None."""
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
@@ -151,14 +179,9 @@ def _check_lock(branch_path: Path) -> Optional[dict]:
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
pid = data.get("pid")
|
pid = data.get("pid")
|
||||||
if pid is not None:
|
if pid is not None:
|
||||||
try:
|
if _check_pid_alive(pid):
|
||||||
os.kill(pid, 0)
|
return data
|
||||||
return data # Process alive, lock valid
|
logger.info("[wake] Lock PID %s dead — cleaning stale lock", pid)
|
||||||
except ProcessLookupError:
|
|
||||||
logger.info("[wake] Lock PID %s dead — cleaning stale lock", pid)
|
|
||||||
except PermissionError as e:
|
|
||||||
logger.warning("[wake] Lock PID %s permission error: %s", pid, e)
|
|
||||||
return data # Process exists but can't signal — treat as active
|
|
||||||
# Stale lock — check age (10 min timeout)
|
# Stale lock — check age (10 min timeout)
|
||||||
ts = data.get("timestamp", "")
|
ts = data.get("timestamp", "")
|
||||||
if ts:
|
if ts:
|
||||||
@@ -210,18 +233,74 @@ def _load_config() -> dict:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def _read_session_type(pid_str: str) -> str:
|
def _get_pid_cwd(pid_str: str) -> Optional[str]:
|
||||||
"""Read AIPASS_SESSION_TYPE from /proc/{pid}/environ. Returns 'interactive' if unset."""
|
"""Get the cwd of a process. Cross-platform: Linux /proc, macOS lsof."""
|
||||||
if sys.platform != "linux":
|
if sys.platform == "linux":
|
||||||
return "interactive"
|
try:
|
||||||
|
return os.readlink(f"/proc/{pid_str}/cwd")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
logger.info("[wake] Cannot read cwd for PID %s", pid_str)
|
||||||
|
return None
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return _get_pid_cwd_darwin(pid_str)
|
||||||
|
logger.info("[wake] Cannot determine cwd for PID %s on %s", pid_str, sys.platform)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pid_cwd_darwin(pid_str: str) -> Optional[str]:
|
||||||
|
"""macOS: get process cwd via lsof."""
|
||||||
try:
|
try:
|
||||||
with open(f"/proc/{pid_str}/environ", "rb") as f:
|
result = subprocess.run(
|
||||||
data = f.read()
|
["lsof", "-a", "-p", pid_str, "-d", "cwd", "-Fn"],
|
||||||
for entry in data.split(b"\0"):
|
capture_output=True,
|
||||||
if entry.startswith(b"AIPASS_SESSION_TYPE="):
|
text=True,
|
||||||
return entry.split(b"=", 1)[1].decode("utf-8")
|
timeout=5,
|
||||||
except (OSError, PermissionError):
|
)
|
||||||
logger.info("[wake] Cannot read session type for PID %s", pid_str)
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
logger.info("[wake] Cannot read cwd for PID %s on macOS", pid_str)
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
for line in result.stdout.strip().split("\n"):
|
||||||
|
if line.startswith("n/"):
|
||||||
|
return line[1:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_session_type(pid_str: str) -> str:
|
||||||
|
"""Read AIPASS_SESSION_TYPE from process environment. Returns 'interactive' if unset."""
|
||||||
|
if sys.platform == "linux":
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{pid_str}/environ", "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
for entry in data.split(b"\0"):
|
||||||
|
if entry.startswith(b"AIPASS_SESSION_TYPE="):
|
||||||
|
return entry.split(b"=", 1)[1].decode("utf-8")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
logger.info("[wake] Cannot read session type for PID %s", pid_str)
|
||||||
|
return "interactive"
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return _read_session_type_darwin(pid_str)
|
||||||
|
return "interactive"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_session_type_darwin(pid_str: str) -> str:
|
||||||
|
"""macOS: read AIPASS_SESSION_TYPE from ps environment output."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["ps", "-p", pid_str, "-wwE", "-o", "command="],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
logger.info("[wake] Cannot read session type for PID %s on macOS", pid_str)
|
||||||
|
return "interactive"
|
||||||
|
if result.returncode != 0:
|
||||||
|
return "interactive"
|
||||||
|
for token in result.stdout.split():
|
||||||
|
if token.startswith("AIPASS_SESSION_TYPE="):
|
||||||
|
return token.split("=", 1)[1]
|
||||||
return "interactive"
|
return "interactive"
|
||||||
|
|
||||||
|
|
||||||
@@ -240,17 +319,13 @@ def _is_branch_occupied(branch_path: Path) -> bool:
|
|||||||
pid_str = pid_str.strip()
|
pid_str = pid_str.strip()
|
||||||
if not pid_str:
|
if not pid_str:
|
||||||
continue
|
continue
|
||||||
try:
|
cwd = _get_pid_cwd(pid_str)
|
||||||
if sys.platform != "linux":
|
if cwd is None:
|
||||||
continue
|
|
||||||
cwd = os.readlink(f"/proc/{pid_str}/cwd")
|
|
||||||
if str(Path(cwd).resolve()) == resolved:
|
|
||||||
session_type = _read_session_type(pid_str)
|
|
||||||
if session_type not in _NON_BLOCKING_SESSION_TYPES:
|
|
||||||
return True
|
|
||||||
except (OSError, PermissionError, ValueError):
|
|
||||||
logger.info("[wake] Cannot read cwd for PID %s", pid_str)
|
|
||||||
continue
|
continue
|
||||||
|
if str(Path(cwd).resolve()) == resolved:
|
||||||
|
session_type = _read_session_type(pid_str)
|
||||||
|
if session_type not in _NON_BLOCKING_SESSION_TYPES:
|
||||||
|
return True
|
||||||
except (subprocess.SubprocessError, OSError):
|
except (subprocess.SubprocessError, OSError):
|
||||||
logger.info("[wake] Failed to check branch occupancy")
|
logger.info("[wake] Failed to check branch occupancy")
|
||||||
return False
|
return False
|
||||||
@@ -273,21 +348,110 @@ def _clean_zombies() -> int:
|
|||||||
|
|
||||||
def _check_pid_alive(pid: int) -> bool:
|
def _check_pid_alive(pid: int) -> bool:
|
||||||
"""Check if a process is alive (not zombie)."""
|
"""Check if a process is alive (not zombie)."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
return _pid_alive_windows(pid)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info("[wake] PID %s Windows check failed (assuming alive): %s", pid, exc)
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
os.kill(pid, 0)
|
os.kill(pid, 0)
|
||||||
# Also verify not zombie via /proc (Linux only)
|
except ProcessLookupError as exc:
|
||||||
if sys.platform == "linux":
|
logger.warning("[wake] PID %s not found: %s", pid, exc)
|
||||||
with open(f"/proc/{pid}/status", "r") as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith("State:"):
|
|
||||||
return "Z" not in line
|
|
||||||
return True
|
|
||||||
except (ProcessLookupError, FileNotFoundError) as e:
|
|
||||||
logger.warning("[wake] PID %s not found: %s", pid, e)
|
|
||||||
return False
|
return False
|
||||||
except PermissionError as e:
|
except PermissionError as exc:
|
||||||
logger.warning("[wake] PID %s permission denied: %s", pid, e)
|
logger.warning("[wake] PID %s permission denied: %s", pid, exc)
|
||||||
return True # Exists but can't check — assume alive
|
return True
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("[wake] PID %s os.kill error (assuming dead): %s", pid, exc)
|
||||||
|
return False
|
||||||
|
if sys.platform == "linux" and _is_zombie_linux(pid):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_zombie_linux(pid: int) -> bool:
|
||||||
|
"""Return True if PID is a zombie (Linux /proc/status check)."""
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{pid}/status", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("State:"):
|
||||||
|
return "Z" in line
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
logger.warning("[wake] PID %s /proc not found: %s", pid, exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_in_systemd_scope(monitor_cmd, branch_path, spawn_env, branch_email, lock_file_path, custom_message, status):
|
||||||
|
"""Spawn monitor in its own systemd unit to survive cgroup cleanup (td-48).
|
||||||
|
|
||||||
|
When wake_branch() runs inside a systemd oneshot service (e.g.
|
||||||
|
daemon-tick.timer), the default KillMode=control-group sends SIGTERM to
|
||||||
|
every process in the cgroup once the main process exits — killing the
|
||||||
|
detached monitor and its claude child. systemd-run --user creates a
|
||||||
|
transient service unit with its own cgroup so the monitor survives.
|
||||||
|
|
||||||
|
Returns True on success, False to fall back to direct Popen.
|
||||||
|
"""
|
||||||
|
unit_name = f"dispatch-{branch_email.lstrip('@')}"
|
||||||
|
env_file = branch_path / "logs" / ".dispatch_env"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(env_file, "w", encoding="utf-8") as ef:
|
||||||
|
for key, val in spawn_env.items():
|
||||||
|
if "\n" not in str(val):
|
||||||
|
ef.write(f"{key}={val}\n")
|
||||||
|
env_file.chmod(0o600)
|
||||||
|
except OSError as e:
|
||||||
|
logger.warning("[wake] Failed to write env file for systemd-run: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
systemd_cmd = [
|
||||||
|
"systemd-run",
|
||||||
|
"--user",
|
||||||
|
"--unit",
|
||||||
|
unit_name,
|
||||||
|
"--collect",
|
||||||
|
"--property",
|
||||||
|
f"WorkingDirectory={branch_path}",
|
||||||
|
"--property",
|
||||||
|
f"EnvironmentFile={env_file}",
|
||||||
|
"--property",
|
||||||
|
"StandardInput=null",
|
||||||
|
"--",
|
||||||
|
] + monitor_cmd
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(systemd_cmd, capture_output=True, text=True, timeout=15)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.warning("[wake] systemd-run failed (rc=%d): %s", result.returncode, result.stderr.strip())
|
||||||
|
return False
|
||||||
|
except (subprocess.SubprocessError, OSError) as e:
|
||||||
|
logger.warning("[wake] systemd-run failed: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
pid_result = subprocess.run(
|
||||||
|
["systemctl", "--user", "show", f"{unit_name}.service", "-p", "MainPID", "--value"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
monitor_pid = int(pid_result.stdout.strip())
|
||||||
|
if monitor_pid > 0:
|
||||||
|
lock_data = {
|
||||||
|
"pid": monitor_pid,
|
||||||
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
"branch": str(branch_path),
|
||||||
|
"subject": custom_message or "daemon wake",
|
||||||
|
}
|
||||||
|
with open(lock_file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(lock_data, f, indent=2)
|
||||||
|
except (subprocess.SubprocessError, ValueError, OSError) as e:
|
||||||
|
logger.info("[wake] Could not query systemd unit PID: %s", e)
|
||||||
|
|
||||||
|
status.ok("spawn", f"Monitor started via systemd scope ({unit_name})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# ─── Branch Resolution ──────────────────────────────────
|
# ─── Branch Resolution ──────────────────────────────────
|
||||||
@@ -378,14 +542,27 @@ def wake_branch(
|
|||||||
branch_path, email = result
|
branch_path, email = result
|
||||||
status.ok("resolve", f"{email} → {branch_path}")
|
status.ok("resolve", f"{email} → {branch_path}")
|
||||||
|
|
||||||
# Step 3: Zombie check (pre-flight)
|
# Step 3: Manager check — managers are never woken, mail only
|
||||||
|
passport_file = branch_path / ".trinity" / "passport.json"
|
||||||
|
try:
|
||||||
|
with open(passport_file, "r", encoding="utf-8") as f:
|
||||||
|
passport = json.load(f)
|
||||||
|
citizen_class = passport.get("identity", {}).get("citizen_class", "")
|
||||||
|
if citizen_class == "manager":
|
||||||
|
status.info("manager", f"{email} is a manager — mail only, wake skipped")
|
||||||
|
logger.info("[wake] %s is citizen_class=manager — wake skipped, mail delivered", email)
|
||||||
|
return status, True
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, OSError) as exc:
|
||||||
|
logger.info("[wake] Could not read passport for %s: %s", email, exc)
|
||||||
|
|
||||||
|
# Step 4: Zombie check (pre-flight)
|
||||||
zombie_count = _clean_zombies()
|
zombie_count = _clean_zombies()
|
||||||
if zombie_count > 0:
|
if zombie_count > 0:
|
||||||
status.warn("zombies", f"{zombie_count} zombie Claude process(es) detected")
|
status.warn("zombies", f"{zombie_count} zombie Claude process(es) detected")
|
||||||
else:
|
else:
|
||||||
status.ok("pre-flight", "No zombie processes")
|
status.ok("pre-flight", "No zombie processes")
|
||||||
|
|
||||||
# Step 4: Lock check
|
# Step 5: Lock check
|
||||||
existing = _check_lock(branch_path)
|
existing = _check_lock(branch_path)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
pid = existing.get("pid", "?")
|
pid = existing.get("pid", "?")
|
||||||
@@ -401,7 +578,7 @@ def wake_branch(
|
|||||||
|
|
||||||
status.ok("lock", "No active lock — agent is sleeping")
|
status.ok("lock", "No active lock — agent is sleeping")
|
||||||
|
|
||||||
# Step 5: Occupancy check
|
# Step 6: Occupancy check
|
||||||
if _is_branch_occupied(branch_path):
|
if _is_branch_occupied(branch_path):
|
||||||
status.warn("occupancy", f"Interactive Claude session in {branch_path}")
|
status.warn("occupancy", f"Interactive Claude session in {branch_path}")
|
||||||
status.fail("blocked", "Cannot spawn — interactive session running")
|
status.fail("blocked", "Cannot spawn — interactive session running")
|
||||||
@@ -410,7 +587,7 @@ def wake_branch(
|
|||||||
|
|
||||||
status.ok("occupancy", "No interactive session")
|
status.ok("occupancy", "No interactive session")
|
||||||
|
|
||||||
# Step 6: Build spawn command
|
# Step 7: Build spawn command
|
||||||
config = _load_config()
|
config = _load_config()
|
||||||
max_turns = config.get("max_turns_per_wake", 100)
|
max_turns = config.get("max_turns_per_wake", 100)
|
||||||
|
|
||||||
@@ -487,49 +664,97 @@ def wake_branch(
|
|||||||
return status, False
|
return status, False
|
||||||
status.ok("lock-acquire", "Dispatch lock acquired")
|
status.ok("lock-acquire", "Dispatch lock acquired")
|
||||||
|
|
||||||
try:
|
# When inside a systemd oneshot service (e.g. daemon-tick.timer), the
|
||||||
process = subprocess.Popen(
|
# default KillMode=control-group sends SIGTERM to all cgroup members
|
||||||
|
# when the service exits — killing the detached monitor. Escape by
|
||||||
|
# launching the monitor in its own transient systemd unit (td-48).
|
||||||
|
spawned_via_scope = False
|
||||||
|
monitor_pid = 0
|
||||||
|
if os.environ.get("INVOCATION_ID") and shutil.which("systemd-run"):
|
||||||
|
spawned_via_scope = _spawn_in_systemd_scope(
|
||||||
monitor_cmd,
|
monitor_cmd,
|
||||||
stdout=subprocess.DEVNULL,
|
branch_path,
|
||||||
stderr=subprocess.DEVNULL,
|
spawn_env,
|
||||||
start_new_session=True,
|
email,
|
||||||
cwd=str(branch_path),
|
lock_file_path,
|
||||||
env=spawn_env,
|
custom_message,
|
||||||
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
monitor_pid = process.pid
|
if not spawned_via_scope:
|
||||||
|
try:
|
||||||
|
_detach_kwargs: dict = {}
|
||||||
|
if sys.platform == "win32":
|
||||||
|
_detach_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
else:
|
||||||
|
_detach_kwargs["start_new_session"] = True
|
||||||
|
process = subprocess.Popen(
|
||||||
|
monitor_cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
cwd=str(branch_path),
|
||||||
|
env=spawn_env,
|
||||||
|
**_detach_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
# Update lock with real monitor PID
|
monitor_pid = process.pid
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
|
||||||
lock_data = {"pid": monitor_pid, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "branch": str(branch_path)}
|
|
||||||
with open(lock_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(lock_data, f, indent=2)
|
|
||||||
|
|
||||||
status.ok("spawn", f"Monitor started (PID {monitor_pid})")
|
# Update lock with real monitor PID
|
||||||
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
|
lock_data = {
|
||||||
|
"pid": monitor_pid,
|
||||||
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
"branch": str(branch_path),
|
||||||
|
"subject": custom_message or "manual wake",
|
||||||
|
}
|
||||||
|
with open(lock_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(lock_data, f, indent=2)
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
status.ok("spawn", f"Monitor started (PID {monitor_pid})")
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
|
||||||
lock_file.unlink(missing_ok=True)
|
except FileNotFoundError as e:
|
||||||
logger.warning("[wake] Spawn failed — script not found: %s", e)
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
status.fail("spawn", "Python or monitor script not found")
|
lock_file.unlink(missing_ok=True)
|
||||||
return status, False
|
logger.warning("[wake] Spawn failed — script not found: %s", e)
|
||||||
except Exception as e:
|
status.fail("spawn", "Python or monitor script not found")
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
return status, False
|
||||||
lock_file.unlink(missing_ok=True)
|
except Exception as e:
|
||||||
logger.warning("[wake] Spawn failed for %s: %s", branch_email, e)
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
status.fail("spawn", f"{type(e).__name__}: {e}")
|
lock_file.unlink(missing_ok=True)
|
||||||
return status, False
|
logger.warning("[wake] Spawn failed for %s: %s", branch_email, e)
|
||||||
|
status.fail("spawn", f"{type(e).__name__}: {e}")
|
||||||
|
return status, False
|
||||||
|
|
||||||
# Step 9: Liveness check (brief wait then verify)
|
# Step 9: Liveness check (brief wait then verify)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
if _check_pid_alive(monitor_pid):
|
if spawned_via_scope:
|
||||||
status.ok("alive", f"Agent responding (PID {monitor_pid} alive)")
|
_unit = f"dispatch-{email.lstrip('@')}"
|
||||||
|
try:
|
||||||
|
check = subprocess.run(
|
||||||
|
["systemctl", "--user", "is-active", f"{_unit}.service"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
if check.stdout.strip() == "active":
|
||||||
|
status.ok("alive", f"Agent responding (unit {_unit} active)")
|
||||||
|
else:
|
||||||
|
status.fail("alive", f"Agent died immediately ({check.stdout.strip()})")
|
||||||
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
|
lock_file.unlink(missing_ok=True)
|
||||||
|
return status, False
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("[wake] Cannot verify systemd unit %s: %s", _unit, e)
|
||||||
|
status.warn("alive", "Cannot verify systemd unit status — assuming running")
|
||||||
else:
|
else:
|
||||||
status.fail("alive", f"Agent died immediately (PID {monitor_pid})")
|
if _check_pid_alive(monitor_pid):
|
||||||
# Clean up lock
|
status.ok("alive", f"Agent responding (PID {monitor_pid} alive)")
|
||||||
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
else:
|
||||||
lock_file.unlink(missing_ok=True)
|
status.fail("alive", f"Agent died immediately (PID {monitor_pid})")
|
||||||
return status, False
|
lock_file = branch_path / ".ai_mail.local" / ".dispatch.lock"
|
||||||
|
lock_file.unlink(missing_ok=True)
|
||||||
|
return status, False
|
||||||
|
|
||||||
# Desktop notification
|
# Desktop notification
|
||||||
notif_body = custom_message[:80] if custom_message else "Manual wake: check inbox"
|
notif_body = custom_message[:80] if custom_message else "Manual wake: check inbox"
|
||||||
|
|||||||
@@ -56,24 +56,17 @@ def batch_close(
|
|||||||
|
|
||||||
def batch_close_post_ops(
|
def batch_close_post_ops(
|
||||||
branch_path: Path,
|
branch_path: Path,
|
||||||
push_dashboard_fn: Optional[Callable] = None,
|
|
||||||
update_central_fn: Optional[Callable] = None,
|
update_central_fn: Optional[Callable] = None,
|
||||||
purge_deleted_fn: Optional[Callable] = None,
|
purge_deleted_fn: Optional[Callable] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run post-operations after a batch close (dashboard update + purge).
|
Run post-operations after a batch close (central update + purge).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
branch_path: Path to branch directory
|
branch_path: Path to branch directory
|
||||||
push_dashboard_fn: Optional push_dashboard_update callable
|
|
||||||
update_central_fn: Optional update_central callable
|
update_central_fn: Optional update_central callable
|
||||||
purge_deleted_fn: Optional purge_deleted_folder callable
|
purge_deleted_fn: Optional purge_deleted_folder callable
|
||||||
"""
|
"""
|
||||||
if push_dashboard_fn:
|
|
||||||
try:
|
|
||||||
push_dashboard_fn(branch_path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("[close] push_dashboard_fn failed for %s: %s", branch_path, e)
|
|
||||||
if update_central_fn:
|
if update_central_fn:
|
||||||
try:
|
try:
|
||||||
update_central_fn()
|
update_central_fn()
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Solves the BRANCH DETECTION FAILED problem when external projects call drone
|
|||||||
— contacts lookup works even when CWD-walking cannot identify the caller.
|
— contacts lookup works even when CWD-walking cannot identify the caller.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
@@ -21,6 +23,13 @@ from aipass.prax.apps.modules.logger import system_logger as logger
|
|||||||
from aipass.ai_mail.apps.handlers.json import json_handler
|
from aipass.ai_mail.apps.handlers.json import json_handler
|
||||||
from aipass.ai_mail.apps.handlers.paths import find_repo_root
|
from aipass.ai_mail.apps.handlers.paths import find_repo_root
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
|
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
|
if _reconfigure is not None:
|
||||||
|
_reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
CONTACTS_FILE = find_repo_root() / "src/aipass/ai_mail/.ai_mail.local/contacts.json"
|
CONTACTS_FILE = find_repo_root() / "src/aipass/ai_mail/.ai_mail.local/contacts.json"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Independent handler - no module dependencies.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
@@ -21,6 +23,13 @@ from typing import Dict, Optional
|
|||||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||||
from aipass.ai_mail.apps.handlers.json import json_handler
|
from aipass.ai_mail.apps.handlers.json import json_handler
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
|
_reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
|
if _reconfigure is not None:
|
||||||
|
_reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
# Lazy imports
|
# Lazy imports
|
||||||
_append_footer = None
|
_append_footer = None
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user