jeanclode 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. jeanclode-0.1.0.dist-info/METADATA +21 -0
  2. jeanclode-0.1.0.dist-info/RECORD +155 -0
  3. jeanclode-0.1.0.dist-info/WHEEL +5 -0
  4. jeanclode-0.1.0.dist-info/entry_points.txt +2 -0
  5. jeanclode-0.1.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +8 -0
  7. src/activities/__init__.py +5 -0
  8. src/activities/ci_watch/__init__.py +19 -0
  9. src/activities/ci_watch/github.py +157 -0
  10. src/activities/ci_watch/gitlab.py +120 -0
  11. src/activities/ci_watch/poll.py +137 -0
  12. src/activities/ci_watch/schemas.py +43 -0
  13. src/activities/decorator.py +111 -0
  14. src/activities/git/__init__.py +29 -0
  15. src/activities/git/ops.py +195 -0
  16. src/activities/git/pr.py +178 -0
  17. src/activities/git/schemas.py +22 -0
  18. src/activities/git/targets.py +79 -0
  19. src/activities/issue/__init__.py +11 -0
  20. src/activities/issue/comment.py +73 -0
  21. src/activities/issue/fetch.py +188 -0
  22. src/activities/issue/schemas.py +17 -0
  23. src/activities/issue/utils.py +12 -0
  24. src/activities/notify.py +130 -0
  25. src/activities/respond/__init__.py +21 -0
  26. src/activities/respond/relabel.py +58 -0
  27. src/activities/respond/schemas.py +42 -0
  28. src/activities/respond/threads.py +149 -0
  29. src/activities/review/__init__.py +16 -0
  30. src/activities/review/dedup.py +18 -0
  31. src/activities/review/guardrail.py +44 -0
  32. src/activities/review/posting.py +407 -0
  33. src/activities/review/recovery.py +128 -0
  34. src/activities/review/routing.py +50 -0
  35. src/activities/review/schemas.py +111 -0
  36. src/activities/review/styling.py +25 -0
  37. src/activities/sentry/__init__.py +47 -0
  38. src/activities/sentry/fetch.py +208 -0
  39. src/activities/sentry/git.py +9 -0
  40. src/activities/sentry/pr.py +3 -0
  41. src/activities/sentry/route.py +139 -0
  42. src/activities/sentry/schemas.py +152 -0
  43. src/activities/sentry/secrets.py +57 -0
  44. src/activities/summary/__init__.py +17 -0
  45. src/activities/summary/format_summary.py +13 -0
  46. src/activities/summary/schemas.py +54 -0
  47. src/activities/summary/update_description.py +78 -0
  48. src/adaptors/__init__.py +5 -0
  49. src/adaptors/base.py +85 -0
  50. src/adaptors/diffn.py +68 -0
  51. src/adaptors/discussions.py +103 -0
  52. src/adaptors/display.py +50 -0
  53. src/adaptors/github/__init__.py +1 -0
  54. src/adaptors/github/adaptor.py +83 -0
  55. src/adaptors/github/auth.py +36 -0
  56. src/adaptors/github/client.py +92 -0
  57. src/adaptors/github/context.py +477 -0
  58. src/adaptors/github/display.py +338 -0
  59. src/adaptors/github/resolver.py +24 -0
  60. src/adaptors/github/sha.py +100 -0
  61. src/adaptors/gitlab/__init__.py +1 -0
  62. src/adaptors/gitlab/adaptor.py +79 -0
  63. src/adaptors/gitlab/auth.py +49 -0
  64. src/adaptors/gitlab/client.py +77 -0
  65. src/adaptors/gitlab/context.py +388 -0
  66. src/adaptors/gitlab/display.py +9 -0
  67. src/adaptors/gitlab/resolver.py +24 -0
  68. src/adaptors/gitlab/sha.py +73 -0
  69. src/adaptors/registry.py +31 -0
  70. src/adaptors/sentry/__init__.py +5 -0
  71. src/adaptors/sentry/adaptor.py +71 -0
  72. src/adaptors/sentry/auth.py +31 -0
  73. src/adaptors/sentry/client.py +78 -0
  74. src/adaptors/sentry/display.py +41 -0
  75. src/adaptors/sentry/resolver.py +71 -0
  76. src/agents/__init__.py +6 -0
  77. src/agents/_memory_backend.py +151 -0
  78. src/agents/base.py +430 -0
  79. src/agents/hooks.py +481 -0
  80. src/agents/issue/__init__.py +21 -0
  81. src/agents/issue/base.py +21 -0
  82. src/agents/issue/fixer.py +22 -0
  83. src/agents/issue/schemas.py +84 -0
  84. src/agents/issue/triage.py +20 -0
  85. src/agents/memory_tool.py +248 -0
  86. src/agents/respond/__init__.py +10 -0
  87. src/agents/respond/base.py +21 -0
  88. src/agents/respond/planner.py +31 -0
  89. src/agents/respond/schemas.py +47 -0
  90. src/agents/review/__init__.py +21 -0
  91. src/agents/review/analyzer.py +24 -0
  92. src/agents/review/base.py +12 -0
  93. src/agents/review/deduplicator.py +22 -0
  94. src/agents/review/fact_checker.py +26 -0
  95. src/agents/review/issue_explorer.py +22 -0
  96. src/agents/review/schemas.py +22 -0
  97. src/agents/review/styler.py +21 -0
  98. src/agents/review/synthesizer.py +23 -0
  99. src/agents/schemas.py +46 -0
  100. src/agents/sentry/__init__.py +17 -0
  101. src/agents/sentry/base.py +21 -0
  102. src/agents/sentry/fixer.py +43 -0
  103. src/agents/sentry/synthesis.py +24 -0
  104. src/agents/sentry/triage.py +29 -0
  105. src/agents/summary/__init__.py +9 -0
  106. src/agents/summary/base.py +21 -0
  107. src/agents/summary/parser.py +20 -0
  108. src/agents/summary/schemas.py +15 -0
  109. src/agents/summary/summarizer.py +21 -0
  110. src/agents/utils.py +84 -0
  111. src/cli.py +182 -0
  112. src/config.py +142 -0
  113. src/config_file.py +179 -0
  114. src/log.py +50 -0
  115. src/main.py +38 -0
  116. src/output.py +429 -0
  117. src/plugin_paths.py +25 -0
  118. src/runner/__init__.py +5 -0
  119. src/runner/display_subscriber.py +128 -0
  120. src/runner/preflight.py +238 -0
  121. src/runner/run.py +310 -0
  122. src/runtime/__init__.py +25 -0
  123. src/runtime/bots.py +29 -0
  124. src/runtime/bus.py +39 -0
  125. src/runtime/context.py +53 -0
  126. src/runtime/events.py +65 -0
  127. src/runtime/mcp_connectors.py +74 -0
  128. src/runtime/notify.py +94 -0
  129. src/skills/__init__.py +30 -0
  130. src/skills/discovery.py +99 -0
  131. src/skills/prompt.py +123 -0
  132. src/skills/schemas.py +23 -0
  133. src/skills/thirdparty.py +154 -0
  134. src/workflows/__init__.py +29 -0
  135. src/workflows/_smoke/__init__.py +0 -0
  136. src/workflows/_smoke/echo.py +55 -0
  137. src/workflows/base.py +97 -0
  138. src/workflows/code_review/__init__.py +1 -0
  139. src/workflows/code_review/runner.py +315 -0
  140. src/workflows/code_review/utils.py +205 -0
  141. src/workflows/issue_resolve/__init__.py +1 -0
  142. src/workflows/issue_resolve/runner.py +362 -0
  143. src/workflows/issue_resolve/utils.py +141 -0
  144. src/workflows/jeanclode_respond/__init__.py +5 -0
  145. src/workflows/jeanclode_respond/runner.py +212 -0
  146. src/workflows/jeanclode_respond/utils.py +150 -0
  147. src/workflows/pr_summary/__init__.py +5 -0
  148. src/workflows/pr_summary/runner.py +126 -0
  149. src/workflows/pr_summary/utils.py +138 -0
  150. src/workflows/schemas.py +13 -0
  151. src/workflows/sentry_fix/__init__.py +5 -0
  152. src/workflows/sentry_fix/runner.py +425 -0
  153. src/workflows/sentry_fix/utils.py +235 -0
  154. src/workflows/utils.py +9 -0
  155. src/workspace.py +241 -0
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: jeanclode
3
+ Version: 0.1.0
4
+ Summary: Multi-plugin CLI — auto-detects what to do from URLs and invokes Claude Code skills
5
+ Author: Jeanclode Contributors
6
+ License-Expression: AGPL-3.0
7
+ Project-URL: Homepage, https://jeanclode.com
8
+ Project-URL: Repository, https://github.com/jeanclode-hq/jeanclode
9
+ Project-URL: Issues, https://github.com/jeanclode-hq/jeanclode/issues
10
+ Project-URL: Documentation, https://jeanclode.com/docs
11
+ Keywords: sentry,ai,llm,bug-fix,automation,cli
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.14
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: claude-agent-sdk>=0.2.144
19
+ Requires-Dist: jinja2>=3.1.0
20
+ Requires-Dist: pydantic>=2.9.0
21
+ Requires-Dist: rich>=13.0.0
@@ -0,0 +1,155 @@
1
+ src/__init__.py,sha256=PFlAsL7ia5bBJmoF1h4eAEHtBAgf8oIZp41kzWh97V0,255
2
+ src/cli.py,sha256=cdD-bQPGLh6RbFkD4oZQWDfC4BwNmL0SIOG7yndT6sw,5814
3
+ src/config.py,sha256=z7Lq90cBnucmrIFyPRe0aoj-N_R-wwRo89gtaGalY50,4501
4
+ src/config_file.py,sha256=-iMnxv-IhIraJPdlP7NNzVWZbDpsHmyUWq6widRqmBA,6097
5
+ src/log.py,sha256=vEHIyDOeEwPaLKqnpcwVVrrAwcGWd87nCZbuW3a1L3Y,1509
6
+ src/main.py,sha256=pLXNB6auUS6EOObKqufHbEvOK_u3_oRpceo6UFAh480,786
7
+ src/output.py,sha256=qfnMqxmQpLiZljl4vFJNVbgx-Cv6HA1fRmO5NkaNhQo,15460
8
+ src/plugin_paths.py,sha256=_x7WzQViVW8LAV188iWMPlo06LVBmGIFJARMhv73FQQ,956
9
+ src/workspace.py,sha256=rQDmTIOS6rPGA9gqGWaN51NdkN53zbl9jZLkAiNasIg,8570
10
+ src/activities/__init__.py,sha256=xBAT__P1-0W0KhU5p6p7qyTDg5RVsibNFr8owJqmbfs,149
11
+ src/activities/decorator.py,sha256=kbHq8amxzD6LfPDlSIn_mfBxQXMKkHyhzVT5yLL9M9I,3537
12
+ src/activities/notify.py,sha256=ZvR9WdC98XzfJdH0jKnI9tKrIR5EwQ3xFD6NaVJQOp0,4527
13
+ src/activities/ci_watch/__init__.py,sha256=rfcKs_WjYWkZxF88MXyh2ugZIK1A5ulFhRKCHcSSnZU,782
14
+ src/activities/ci_watch/github.py,sha256=tsgzGH1g1u53P8KgDXDuB95Okz-2fahcZXgpQ-Ui-n0,6052
15
+ src/activities/ci_watch/gitlab.py,sha256=bjJcml8VW1oWyAB4OWrLZaJBkfnmqXTd408GiQ9z1v0,4426
16
+ src/activities/ci_watch/poll.py,sha256=VLOciLj9rIi5ekKSQsWIT1POo2DfUFcFhVFex_0E1Sg,5451
17
+ src/activities/ci_watch/schemas.py,sha256=cYQn1sthJrwTEPPldjCd98xJLXePJgN0A1SXDNXxCX4,1364
18
+ src/activities/git/__init__.py,sha256=avqxCDzRrESEoKh-2pkPcWET3M1ZrZdWYtAZEeVBXT0,682
19
+ src/activities/git/ops.py,sha256=eVHwK3WNz3uWA9WHLXkYMnMAYhBgh79TRjvtEY0VWUs,7782
20
+ src/activities/git/pr.py,sha256=U9xHXTLo2Pgbi8D23gdJu1svrfRGAkX4HhbDqGfbBdw,7088
21
+ src/activities/git/schemas.py,sha256=uiJMCLsye5r3nV-QhfEeiEKfTZ9PMSu5thnoLnoRJak,490
22
+ src/activities/git/targets.py,sha256=oxRa0Rox876lilG48EglC83b0qYz3kYq8N3g0-EfMN4,3072
23
+ src/activities/issue/__init__.py,sha256=okY9P9M-qaPhZc5PxONZsNZz0293EvSz9AL_ykOO134,332
24
+ src/activities/issue/comment.py,sha256=ANj9s_K3Ts2GCRu7C-ect1kHNxo6LAcAMy_rfogt0i0,1975
25
+ src/activities/issue/fetch.py,sha256=HKiuwgOHNb-qeB5dDts4EU8_YhJYFJj9TAGntNA_Frw,5823
26
+ src/activities/issue/schemas.py,sha256=uP0mPljRCYdnP6SNoMQN4I29aiXVnoayfeqIP0dMsMc,453
27
+ src/activities/issue/utils.py,sha256=cyPlR051MHSkEbBvN3R0DjIvMX0p1VzMsv5qMrcz2-s,298
28
+ src/activities/respond/__init__.py,sha256=HoVZZ-aqx9HwNGO6h97yfyYSquNdsWgcFrJYchRiDQo,797
29
+ src/activities/respond/relabel.py,sha256=YfpokKjPEWts3jAEkcvkAmaKI3D5piDdwK2kh-jJyUE,1924
30
+ src/activities/respond/schemas.py,sha256=oXD7z4SknRSCZCpzaGPXLI6Xw3rmSGxB1UMhk9rON9k,1180
31
+ src/activities/respond/threads.py,sha256=bxIbiZWfwQGzYJRMOnOvwWftFa7rf6rbpdrn8n2QSSI,4747
32
+ src/activities/review/__init__.py,sha256=vwJDLP02aPIZZxJPX7-G7lsW6Z8WBjb1K_uzd_FRqdk,582
33
+ src/activities/review/dedup.py,sha256=3lKnXnyx0h2mPtRQsfQPu_Npyi1feP4bCVoch1cuewQ,551
34
+ src/activities/review/guardrail.py,sha256=RHWl4jRXJST8ubP2VeuVkX5L2MtqKo9pAzvScEJkFzY,1174
35
+ src/activities/review/posting.py,sha256=dh0T6vRsEFcYcjvEgb0lkWFx7zkiyQo8x0tG_abkQLY,13757
36
+ src/activities/review/recovery.py,sha256=jIcw5GaMQQxyQ4eU9RyvlGbc2Buupw_fx9oi7tkoVVk,3576
37
+ src/activities/review/routing.py,sha256=27dujJtRWIUO9_x6WpzWdsll6IkDumpfJo4Pgpyg2X4,1811
38
+ src/activities/review/schemas.py,sha256=gFhwdS0_l4xwjo52acL6trhTDGHInEeIl3ZU7QY63Zw,2576
39
+ src/activities/review/styling.py,sha256=_GEEQ1LS5S2RPT8n9nL9c9b5EyN7wwxL925o1F62qO0,748
40
+ src/activities/sentry/__init__.py,sha256=3M1SiXOrwByYBusU6TwnhDG2WTbI7a4bK5_ooa589Lg,1106
41
+ src/activities/sentry/fetch.py,sha256=wJxgjJoZWeDN7zgww1_rnSw1F_rKwNsbcWTDqobwcBo,7536
42
+ src/activities/sentry/git.py,sha256=336E4p9flk5pOwM20t7nXFXkzdeQYLxr3rCP8TdoxOM,239
43
+ src/activities/sentry/pr.py,sha256=eBf3_W7yKq-whsPMBK-nGaKRLQngrBN4Yt5E_eUz060,156
44
+ src/activities/sentry/route.py,sha256=pkdfWM0lXT40U87cWhcMzHWycftnkcq3BaGVACklh08,4970
45
+ src/activities/sentry/schemas.py,sha256=fpKHS54kh9C9ZK3oLldUXMkurorsjH9jCZS2TnT6ZlQ,5583
46
+ src/activities/sentry/secrets.py,sha256=nxN0DPpdH8Cl0GPL3tA9JQVrAj1P9YDlLhdc7-_J_SI,1618
47
+ src/activities/summary/__init__.py,sha256=2T5f1b_kwJtztF-GPfi8xCbUZHKRDAnrRnhlKJ1fnvQ,404
48
+ src/activities/summary/format_summary.py,sha256=J6HciDZb_RYB5RmO6zwZGopx71F6wFiz-Th3Og_bpSo,508
49
+ src/activities/summary/schemas.py,sha256=HNvSOKNmMMjn1rpOETr9s4EON-k8pgTzqHsY470QcWE,1315
50
+ src/activities/summary/update_description.py,sha256=vEPCrc0dmWnaLu0G2ALWlwDuwEriNMpc7K_DItiRtvI,2073
51
+ src/adaptors/__init__.py,sha256=bvfvViz9OP8imPpNCmTOpfuzroEEbGSwai3OPdan-VQ,177
52
+ src/adaptors/base.py,sha256=7rdrP6mnvFo0v3S7gqzJ_i__5tPWJJddIqt6TNnFC7A,2953
53
+ src/adaptors/diffn.py,sha256=B9qmgo8_NJaHAuYyv7piWYl2zu8l6DjK8mw68HyF3C0,2187
54
+ src/adaptors/discussions.py,sha256=JgZ9nJlhCyLJSBXcEnEBIRT-LQfhgWDkQcyOGbdq8Ng,2793
55
+ src/adaptors/display.py,sha256=bFjhb8MTgbAPqouamGJYUhiYEMqGtaM1nAtHh8yCDxA,1577
56
+ src/adaptors/registry.py,sha256=iOBCi69LnvnCjZ_iGrljhq4SjsFrZjUgVC1cVBO0NWs,913
57
+ src/adaptors/github/__init__.py,sha256=PYYFoAGUBkFg7CTUlR8XopmRF6-gE5iBQWzh9u9j2cg,82
58
+ src/adaptors/github/adaptor.py,sha256=5lIbKOPJK7xO_EUW6Muuk6agMWRnH4NIf6wzCsVgBgY,2909
59
+ src/adaptors/github/auth.py,sha256=_Zp6zdwwxVA2HmE5C3wbXBCFksd4756Tm-HCy11MrCU,1021
60
+ src/adaptors/github/client.py,sha256=bwzsLpc2CRFOKgmC-5DhCTQRvw-0pZ64kf9Gxb6uYsQ,3152
61
+ src/adaptors/github/context.py,sha256=qRdfbuBbvpYVsJbTDtVb43MrFKzxnVKeUeJOcGB7VaQ,15338
62
+ src/adaptors/github/display.py,sha256=Epeio8B35naZ03RFpV0YS--EsuYx68uz9Z-VuS3BdWo,12339
63
+ src/adaptors/github/resolver.py,sha256=IfRE2CbfEkhSOezvXUzUpqQFMlDxuRoJKxNu4IrN2Fc,746
64
+ src/adaptors/github/sha.py,sha256=EHcKyqDryzF-lZayyS1avSXNuYVhuKmt8ST_Q-fgXMI,2895
65
+ src/adaptors/gitlab/__init__.py,sha256=GEgeab4S9gJoxmQUXtzOqF8VPDnzN_4UbrTPqJ1ymCw,76
66
+ src/adaptors/gitlab/adaptor.py,sha256=D4QwoVB1WLLsn1_4OwB4DObt_hw5h406b-nmIJsL_uQ,2647
67
+ src/adaptors/gitlab/auth.py,sha256=fh87zFxLc9AxjWla7j65uEtH9nPOGMqIhipMXsdHDcs,1663
68
+ src/adaptors/gitlab/client.py,sha256=Vz6-6JSXkNzkY3fi_WClYgsWjDEnMz_KJkZsovkYeZ0,2688
69
+ src/adaptors/gitlab/context.py,sha256=2LOWx03RDq_v0J1TTl-w0qYCk996g6P_s3lajjY0kXY,13932
70
+ src/adaptors/gitlab/display.py,sha256=Glux_tMz0I8Ip0_UD7o11_Q6_ZCl2hJGvVqps7wUV9E,283
71
+ src/adaptors/gitlab/resolver.py,sha256=GNBxeGV4JO5y3vUIgFmWZSgC81pnacU-g4kmyyjOjOw,739
72
+ src/adaptors/gitlab/sha.py,sha256=5lLoGgP9ndomBijsfcy-EdOsdInvr8b6Q5DVgT6TfQ8,2382
73
+ src/adaptors/sentry/__init__.py,sha256=uMDl2pKZKJ1cimS9XtpI6Oo-x8fIjliJORyWxwkqEBQ,156
74
+ src/adaptors/sentry/adaptor.py,sha256=isR1TIvpG2tIO7sbW28LISMwIZzeuXZ4sxDw_WSICz4,2368
75
+ src/adaptors/sentry/auth.py,sha256=RaQgebf3MlNe_rGrg-Nn09LwGszavIW7wkxEhuCEV6U,886
76
+ src/adaptors/sentry/client.py,sha256=P_mAXI9idM2Fc4UYmlE0MO7fc-FVcAGH7nvDmt_L_nI,3013
77
+ src/adaptors/sentry/display.py,sha256=5lhS0enjfphQF9wQn1h12Sigek3KKGDNDIyhEODL4Dg,1197
78
+ src/adaptors/sentry/resolver.py,sha256=dgUAnu5_1YdVnMBfmMwvOX3Bq02_E4aeSQiotLke6-U,2052
79
+ src/agents/__init__.py,sha256=PSNtDMD4YiFQ3xvqKnaN8WjAvwxSYZ3e9HlDWxFfgBI,199
80
+ src/agents/_memory_backend.py,sha256=J9j6S-sm3l8dgvvDsJE2rNy8k4P9lwTTTTR6veGq8Tw,5347
81
+ src/agents/base.py,sha256=q_06jjpP4D19kfp7SNw8qX4cG6h4B1wc2X58AOJXGD0,18469
82
+ src/agents/hooks.py,sha256=taTQ6dR3QFkLGLNE_yzsmXAfLy6Zqo9cw7HKrkVyfEY,20279
83
+ src/agents/memory_tool.py,sha256=uIhe3qMtIOI_wH1QuXCD8eUGZhA_tYoKRBcEHG8mUNA,8426
84
+ src/agents/schemas.py,sha256=am5dfV9EWbUi2V6l1Ch43-RZMkM7Vb2gYjy_3DqUOAI,1551
85
+ src/agents/utils.py,sha256=H9Pw4CODfjoNJqUIcck7YoAkLCbbekn8G4dM0hI_E5A,2845
86
+ src/agents/issue/__init__.py,sha256=kJR2wz5Jax24i2VHYiMV8VtF_3XNfteB01bK8Yue5LM,520
87
+ src/agents/issue/base.py,sha256=eoswbNCP_TmfpxLcUqzgKBt6IKe-a-uzpITmCrdEQGw,564
88
+ src/agents/issue/fixer.py,sha256=CZsrpSlk9REOzEb3LP8Xgbw74U-TDOknJT8xNUhI8NQ,770
89
+ src/agents/issue/schemas.py,sha256=x8nni61WelAm7Q2MqPeJNnVxXEOEuXUMOCbbvsBTAec,3000
90
+ src/agents/issue/triage.py,sha256=Br_RKiVMPX763hSrwt4BSwNf1N1Udk6L9hNsJHX0Bv8,637
91
+ src/agents/respond/__init__.py,sha256=US0fal3ORGeUb0Yqn_tCPskXkF07xpplt615nr1VWeI,245
92
+ src/agents/respond/base.py,sha256=z0nTQnoapT-cAgSxgoCJICxS_R1-3jlYMtn6TziXAPU,566
93
+ src/agents/respond/planner.py,sha256=Ld4X0HUa3N0IRM_anvDihmYxNS-QVB9ScltgQ0xNezw,1241
94
+ src/agents/respond/schemas.py,sha256=oFdIijsckD3U0mfTt_xOwNh3Rgl37DA3kpA-y9aYzak,1299
95
+ src/agents/review/__init__.py,sha256=tvxbaTRoEDCDc4lL4kWyW_a8l1lgpUfxyRaWd-w0AFM,742
96
+ src/agents/review/analyzer.py,sha256=phqgQ4Phlz6SNi9sjVZ9BEmp6hDNCobWx2KEibC84x0,625
97
+ src/agents/review/base.py,sha256=SOMJjwGfxfIzPgqB4Pv_HOXIM-isEPC1rn4I7FeBCwo,271
98
+ src/agents/review/deduplicator.py,sha256=nxWGzyf2ZTp6rFVQrfG1FYmh0io_RNLg-KuherqUXTc,601
99
+ src/agents/review/fact_checker.py,sha256=1rdIg2GLVlSQ3f0sQLljAzSIYeXCIO0Sp6yBuI4Qd50,761
100
+ src/agents/review/issue_explorer.py,sha256=tTU4IMLnrUMyETE_ucHyYWK4Ev75gmoPFNU5nQ_KWQU,592
101
+ src/agents/review/schemas.py,sha256=TMf_AwJGHDtSnXhVPI2aBeYk3t1FpGJAmRtimh5t1JM,520
102
+ src/agents/review/styler.py,sha256=bwauDpB6Skc92OSuOjotFiYYbuntWB5ixAPEt88o-Sg,554
103
+ src/agents/review/synthesizer.py,sha256=-avkHAm7q6sPFSfoWbmEuMvXEuI7MGWlLtS1XqsadOg,542
104
+ src/agents/sentry/__init__.py,sha256=dKF2NpZdbQZkKdzaumj9aRg3brrHBj57IcOjA_PV7JM,517
105
+ src/agents/sentry/base.py,sha256=mJmLhd6dJNV1ybv3IYKOO4vi2zbHNcsiSWyNBQXnl6M,565
106
+ src/agents/sentry/fixer.py,sha256=Wxk6rVfK9k2918Umh7gHHMAaLopwpVXSzkYBT3Jp0e0,1516
107
+ src/agents/sentry/synthesis.py,sha256=eiBp3_eQVrtQwngBzdav-D6-YdacTmM_sRyiNA5m9ko,715
108
+ src/agents/sentry/triage.py,sha256=XT552igNDRgyRd-R2HWzILVhfkx9w2iP6fejmGlH2cg,849
109
+ src/agents/summary/__init__.py,sha256=vDOTHBZMyinRKw6Qnx70esJq7H1NeKETyLHMrw6GO-M,237
110
+ src/agents/summary/base.py,sha256=tZ2OB5FFb25H0q398IkTXXiYmNvCwNjtVPRhFmJyp7Y,569
111
+ src/agents/summary/parser.py,sha256=rZT0JqODkVFlBLIffpB0nE3unvkdLJuZgpU4dE_zSf4,506
112
+ src/agents/summary/schemas.py,sha256=ewwXKsLWABHILq-Z1_CmUZ0VTRlCjQs-gW05kO7TWCc,295
113
+ src/agents/summary/summarizer.py,sha256=uW2T0uEnTgbDbtsgWmHE_EhgIYWkMqCePJYTBtC9ah8,553
114
+ src/runner/__init__.py,sha256=Zpxe88lQdZDUmzlPoha2ZHwNZgcCimbIyfhZRhXO424,148
115
+ src/runner/display_subscriber.py,sha256=V7dkCSi-vFqPYg2-TBs6gUFlUpT_VM75m5yEtYjwVcw,4797
116
+ src/runner/preflight.py,sha256=pXtNM8jdtzlW9G-xpzH2O9IcawBUdJEAadbc3GiAaZ4,9324
117
+ src/runner/run.py,sha256=yi6jy7uNfSwxWh4s9pwmwPHLrNylBuZHnCjCd5hh9Sk,11499
118
+ src/runtime/__init__.py,sha256=JVVw8jIl-oLzWmQjmn_SG9dhMwgdxED_mX8GJIwJ_6M,458
119
+ src/runtime/bots.py,sha256=SQVSzfhVBXE5UaKTPKh_mojdpXVx765c9kcHrYL2_yQ,1072
120
+ src/runtime/bus.py,sha256=NHqZL41Y3xXp9yx-6kmlBapcUWqUmK5M5muF7LmhH_o,1055
121
+ src/runtime/context.py,sha256=CHS184RwsEAykI4AhqZnWfZLWFgDcYcsXqd4-p76Lus,2100
122
+ src/runtime/events.py,sha256=P1ZPSgKBGTMHsGOCme7oJ5Qd7yDnv8BiyWMqPJZ4_jY,1592
123
+ src/runtime/mcp_connectors.py,sha256=Hnz9E7Nc-tUiYFdF-ZS025bIcJVKsE__rsaUCO0Bx98,2771
124
+ src/runtime/notify.py,sha256=Rkx8xPJ0TCoozw5IuHPx6FLtcqkj_P0jo71aFys6_VI,3303
125
+ src/skills/__init__.py,sha256=_EFHUZCqv0uq48uzbFm5RXrXz21KQEFGnluTw1hGAIM,1132
126
+ src/skills/discovery.py,sha256=ZOKpSueHgUQQ-dXudBEkiKeDsBblPo6v3OE7BtlWV8g,3229
127
+ src/skills/prompt.py,sha256=Zf_un5J0KBf8JAgCH9O0AOrKdcemLCFgYhX9SHjK91k,5085
128
+ src/skills/schemas.py,sha256=c0F72LFtBFnsnIpIn-i7b5vLyA3-vNkdIo_8gCaj1xY,601
129
+ src/skills/thirdparty.py,sha256=bVMuQe7wHEMPNum_83evo7nrYii84vqFppcUn1EV1Aw,6008
130
+ src/workflows/__init__.py,sha256=s8gOYswcafrBMZSjmE2oKSrmtavsusKANlg4z-xLKv8,1137
131
+ src/workflows/base.py,sha256=-c9KisIF2AL3p_DmQlxRHtAXGsCUu-fV4G9Ptemi-tY,3001
132
+ src/workflows/schemas.py,sha256=yrhBoAxmFpC1lwWDzbezLBtV0FMyoTIX6KRwxPhyV60,338
133
+ src/workflows/utils.py,sha256=JirKsmCN45I0X9yTH7K4-jG22n5ytQ-2ZLGb9WNTqsg,298
134
+ src/workflows/_smoke/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
+ src/workflows/_smoke/echo.py,sha256=6sCxWkjjtj1-ww1gk3vYXQM6Q-BcEk1G-Xg3IjqYW3c,1970
136
+ src/workflows/code_review/__init__.py,sha256=3qGSFphzlWwkIXYOZksFsI57i950e-5s9ceSodbTf38,56
137
+ src/workflows/code_review/runner.py,sha256=5RVgboUssr5RskCe5N8Z0oFV2xOiwxnLpq9Hhwg138w,10781
138
+ src/workflows/code_review/utils.py,sha256=d0NfHg2QICELsc3ZrfUftvGbSMJGxfEPNmdQcqlWvJw,6705
139
+ src/workflows/issue_resolve/__init__.py,sha256=NovxCN5QdNxe83QxgacWUY_LGoatx6t4JR-LkFBkAfk,71
140
+ src/workflows/issue_resolve/runner.py,sha256=AqbouFbvSlMHfDe2bTejuVxusjdoVR9nuhXYyjlpaQo,14923
141
+ src/workflows/issue_resolve/utils.py,sha256=RmdipNtaaELg7aW-79mMIwNm4C0Hj7qhl5peU13C44c,5100
142
+ src/workflows/jeanclode_respond/__init__.py,sha256=vV-nLRNhA6GN5IIt51iv-yOdEchIGQuQ-zOzD-tIQLA,191
143
+ src/workflows/jeanclode_respond/runner.py,sha256=KZfcfU_XM0Z7OFB7_aQfzy2UE-zOqftuE7dpBm0dpn4,8499
144
+ src/workflows/jeanclode_respond/utils.py,sha256=TQISiACEleSJC4kYB-tkqk1HD6W6C2cnRECE8ILbqP0,4924
145
+ src/workflows/pr_summary/__init__.py,sha256=Lom7VuwQoeDDbnvUaQWMXtrgHIzDWggC61Rcl0N5jRY,126
146
+ src/workflows/pr_summary/runner.py,sha256=yaVyTcT_9FYfi4soH18pJQVYHzl6aGPuLsH_MbGjTmQ,4264
147
+ src/workflows/pr_summary/utils.py,sha256=OgF8fJ82ns02tSbV2ZBDMiw062agluwrOMtzxWQ3DHU,4951
148
+ src/workflows/sentry_fix/__init__.py,sha256=ulPoPKbU2I1IUb1WnLKx6-AKgIu-kD-jIfGVrmlMe8U,159
149
+ src/workflows/sentry_fix/runner.py,sha256=rBkanjsW8rCowIBFU1LkOm_pTKty4yK0gaKWzgG7ZoA,17560
150
+ src/workflows/sentry_fix/utils.py,sha256=uae-F-YDnVD8YonRaoNyqfCiv9Ljyy-3eiqIRyTqDKI,9044
151
+ jeanclode-0.1.0.dist-info/METADATA,sha256=dzPuiyusouUi-Te9OgEFlBB5DdaMdyKm2ecen0bt6oo,872
152
+ jeanclode-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
153
+ jeanclode-0.1.0.dist-info/entry_points.txt,sha256=Mz-uC7wnfMW-4ShEY1ML4Onmn-sLnoDzhYvryr30qtg,49
154
+ jeanclode-0.1.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
155
+ jeanclode-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jeanclode = src.main:cli_entry
@@ -0,0 +1 @@
1
+ src
src/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Jeanclode CLI — triage Sentry errors and invoke Claude Code to fix bugs."""
2
+
3
+ import importlib.metadata
4
+
5
+ try:
6
+ __version__: str = importlib.metadata.version("jeanclode")
7
+ except importlib.metadata.PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,5 @@
1
+ """Activities — small deterministic units of work composed by workflows."""
2
+
3
+ from src.activities.decorator import activity
4
+
5
+ __all__ = ["activity"]
@@ -0,0 +1,19 @@
1
+ """ci-watch — verify a pushed fix against the target repo's own CI.
2
+
3
+ Enforced via ``src.agents.hooks.require_ci_pass_hook`` — a PreToolUse hook on
4
+ the schema-bound fixer's ``StructuredOutput`` call, not an agent-callable
5
+ tool (a tool call is something a model can skip; this hook fires on every
6
+ attempt to finalize the turn and can't be opted out of). It gates
7
+ ``StructuredOutput`` rather than ``Stop`` because a ``Stop`` block lands
8
+ after the structured output is already submitted and the CLI drops it. See
9
+ ``docs/adr/008-ci-gated-fix-verification.md`` for the design rationale.
10
+ """
11
+
12
+ from src.activities.ci_watch.poll import check_ci
13
+ from src.activities.ci_watch.schemas import CheckResult, CiWatchResult
14
+
15
+ __all__ = [
16
+ "CheckResult",
17
+ "CiWatchResult",
18
+ "check_ci",
19
+ ]
@@ -0,0 +1,157 @@
1
+ """GitHub CI discovery — Checks API + legacy Status API, via `gh`.
2
+
3
+ Both are queried and merged: modern checks (Actions, most integrations) live
4
+ in the Checks API, older/third-party CI in the legacy Status API. `gh`
5
+ infers `{owner}/{repo}` from the git remote via its own placeholder
6
+ substitution, so no repo slug is threaded through these functions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import subprocess
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from src.activities.ci_watch.schemas import CheckBucket, CheckResult, truncate_log
18
+
19
+ _BLOCKED_CONCLUSIONS = {"action_required", "stale", "skipped", "neutral"}
20
+ _FAILURE_CONCLUSIONS = {"failure", "cancelled", "timed_out"}
21
+
22
+ # GitHub's check-runs/status list endpoints default to 30 per page; 100 is
23
+ # the max. Same failure mode verified live on GitLab's job list (a 25-job
24
+ # pipeline silently lost its one real failure past the default page of 20).
25
+ _MAX_PER_PAGE = 100
26
+
27
+ # An Actions check-run's details_url looks like
28
+ # https://github.com/{owner}/{repo}/actions/runs/{run_id}/job/{job_id} —
29
+ # verified against a live check-run. Non-Actions apps (CodeQL, third-party
30
+ # CI) point elsewhere and simply won't match.
31
+ _ACTIONS_RUN_JOB_RE = re.compile(r"/actions/runs/(?P<run_id>\d+)/job/(?P<job_id>\d+)")
32
+
33
+
34
+ def _run_gh(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
35
+ return subprocess.run(["gh", *args], cwd=cwd, capture_output=True, text=True, check=False)
36
+
37
+
38
+ def _api_json(args: list[str], *, cwd: Path) -> Any | None:
39
+ """Run `gh api ...` and return parsed JSON, or None on any failure."""
40
+ proc = _run_gh(args, cwd=cwd)
41
+ if proc.returncode != 0 or not proc.stdout.strip():
42
+ return None
43
+ try:
44
+ return json.loads(proc.stdout)
45
+ except json.JSONDecodeError:
46
+ return None
47
+
48
+
49
+ def _bucket_for_conclusion(conclusion: str) -> CheckBucket:
50
+ if conclusion == "success":
51
+ return "success"
52
+ if conclusion in _FAILURE_CONCLUSIONS:
53
+ return "failure"
54
+ if conclusion in _BLOCKED_CONCLUSIONS:
55
+ return "blocked"
56
+ return "running"
57
+
58
+
59
+ def list_check_runs(ref: str, *, cwd: Path) -> list[dict[str, Any]]:
60
+ """Return every check-run on `ref` (a SHA or branch name)."""
61
+ data = _api_json(
62
+ ["api", f"repos/{{owner}}/{{repo}}/commits/{ref}/check-runs?per_page={_MAX_PER_PAGE}"],
63
+ cwd=cwd,
64
+ )
65
+ return data.get("check_runs", []) if isinstance(data, dict) else []
66
+
67
+
68
+ def combined_status(ref: str, *, cwd: Path) -> list[dict[str, Any]]:
69
+ """Return every legacy Status API entry on `ref`."""
70
+ data = _api_json(
71
+ ["api", f"repos/{{owner}}/{{repo}}/commits/{ref}/status?per_page={_MAX_PER_PAGE}"], cwd=cwd
72
+ )
73
+ return data.get("statuses", []) if isinstance(data, dict) else []
74
+
75
+
76
+ def required_check_names(branch: str, *, cwd: Path) -> set[str] | None:
77
+ """Best-effort: names of checks that actually block merging on `branch`.
78
+
79
+ Requires admin-level read access to branch protection, which we may not
80
+ have. Returns None on any failure (403/404/malformed) so the caller can
81
+ fall back to treating every discovered check as required.
82
+ """
83
+ data = _api_json(
84
+ ["api", f"repos/{{owner}}/{{repo}}/branches/{branch}/protection/required_status_checks"],
85
+ cwd=cwd,
86
+ )
87
+ if not isinstance(data, dict):
88
+ return None
89
+ contexts = data.get("contexts") or []
90
+ return set(contexts) if contexts else None
91
+
92
+
93
+ def failed_run_log(details_url: str, *, cwd: Path) -> str:
94
+ """Best-effort failing-step log for a GitHub Actions check-run.
95
+
96
+ Parses the run/job id straight out of the check-run's own `details_url`
97
+ rather than matching by name: a check-run's `name` is per-job (e.g.
98
+ "test-backend"), but `gh run list` only exposes per-*workflow* names
99
+ (e.g. "CI") — they never match. `gh run view --log-failed` then handles
100
+ the zip/redirect dance the raw Actions logs API otherwise needs.
101
+ Non-Actions checks (CodeQL, third-party apps) have a different
102
+ `details_url` shape and simply return "".
103
+ """
104
+ match = _ACTIONS_RUN_JOB_RE.search(details_url)
105
+ if not match:
106
+ return ""
107
+ proc = _run_gh(
108
+ ["run", "view", match["run_id"], "--job", match["job_id"], "--log-failed"],
109
+ cwd=cwd,
110
+ )
111
+ if proc.returncode != 0:
112
+ return ""
113
+ return truncate_log(proc.stdout)
114
+
115
+
116
+ def discover(ref: str, *, cwd: Path, fetch_logs: bool = True) -> list[CheckResult]:
117
+ """Discover every check on `ref` (the head commit's SHA) across both
118
+ GitHub signal APIs.
119
+
120
+ Required-check weighting is applied by the caller (`poll.py`), which
121
+ knows the actual PR branch — required-status-checks are keyed by branch,
122
+ not SHA, so that step can't happen in here.
123
+ """
124
+ results: list[CheckResult] = []
125
+ for run in list_check_runs(ref, cwd=cwd):
126
+ status = run.get("status")
127
+ conclusion = run.get("conclusion") or ""
128
+ bucket: CheckBucket = (
129
+ "running" if status != "completed" else _bucket_for_conclusion(conclusion)
130
+ )
131
+ name = run.get("name", "")
132
+ details_url = run.get("details_url", "")
133
+ log_excerpt = ""
134
+ if fetch_logs and bucket == "failure":
135
+ log_excerpt = failed_run_log(details_url, cwd=cwd)
136
+ results.append(
137
+ CheckResult(
138
+ name=name,
139
+ bucket=bucket,
140
+ raw_conclusion=conclusion or status or "",
141
+ log_excerpt=log_excerpt,
142
+ details_url=details_url,
143
+ )
144
+ )
145
+ status_buckets: dict[str, CheckBucket] = {"success": "success", "pending": "running"}
146
+ for status_entry in combined_status(ref, cwd=cwd):
147
+ state = status_entry.get("state", "")
148
+ status_bucket = status_buckets.get(state, "failure")
149
+ results.append(
150
+ CheckResult(
151
+ name=status_entry.get("context", ""),
152
+ bucket=status_bucket,
153
+ raw_conclusion=state,
154
+ details_url=status_entry.get("target_url", ""),
155
+ )
156
+ )
157
+ return results
@@ -0,0 +1,120 @@
1
+ """GitLab CI discovery — MR pipelines + jobs, via `glab`.
2
+
3
+ Unlike GitHub's many independent checks, a GitLab MR has one pipeline whose
4
+ own `status` is authoritative (a job can be `allow_failure` without failing
5
+ the pipeline, or `manual` without blocking it) — gating reads the pipeline
6
+ status, not a per-job fan-out. Jobs are only fetched for failing-job traces.
7
+
8
+ `glab` substitutes `:id` from the git remote and already reads `GITLAB_HOST`
9
+ for self-hosted instances, so no host/project-id resolution is needed here.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import subprocess
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from src.activities.ci_watch.schemas import CheckBucket, CheckResult, truncate_log
20
+
21
+ # Traces for the first N non-allow_failure failing jobs are fed back to the
22
+ # fixer. Each is tail-capped (truncate_log), so 5 stays well within the
23
+ # agent's context while covering a pipeline where a shared cause broke
24
+ # several jobs at once.
25
+ _MAX_FAILED_JOB_LOGS = 5
26
+ # GitLab's list endpoints default to 20 per page — verified live against a
27
+ # 25-job pipeline where the unpaginated call silently dropped the one
28
+ # non-allow_failure failing job. 100 is GitLab's max per_page.
29
+ _MAX_PER_PAGE = 100
30
+
31
+ # Anything not in these two sets (created, pending, running, ...) falls
32
+ # through to the "running" bucket — the safe default mid-pipeline.
33
+ _BLOCKED_STATUSES = {"skipped", "manual"}
34
+ _FAILURE_STATUSES = {"failed", "canceled"}
35
+
36
+
37
+ def _run_glab(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
38
+ return subprocess.run(["glab", *args], cwd=cwd, capture_output=True, text=True, check=False)
39
+
40
+
41
+ def _api_json(args: list[str], *, cwd: Path) -> Any | None:
42
+ """Run `glab api ...` and return parsed JSON, or None on any failure."""
43
+ proc = _run_glab(args, cwd=cwd)
44
+ if proc.returncode != 0 or not proc.stdout.strip():
45
+ return None
46
+ try:
47
+ return json.loads(proc.stdout)
48
+ except json.JSONDecodeError:
49
+ return None
50
+
51
+
52
+ def _bucket_for_status(status: str) -> CheckBucket:
53
+ if status == "success":
54
+ return "success"
55
+ if status in _FAILURE_STATUSES:
56
+ return "failure"
57
+ if status in _BLOCKED_STATUSES:
58
+ return "blocked"
59
+ return "running"
60
+
61
+
62
+ def mr_pipelines(mr_iid: str, *, cwd: Path) -> list[dict[str, Any]]:
63
+ data = _api_json(
64
+ ["api", f"projects/:id/merge_requests/{mr_iid}/pipelines?per_page={_MAX_PER_PAGE}"], cwd=cwd
65
+ )
66
+ return data if isinstance(data, list) else []
67
+
68
+
69
+ def pipeline_jobs(pipeline_id: int, *, cwd: Path) -> list[dict[str, Any]]:
70
+ data = _api_json(
71
+ ["api", f"projects/:id/pipelines/{pipeline_id}/jobs?per_page={_MAX_PER_PAGE}"], cwd=cwd
72
+ )
73
+ return data if isinstance(data, list) else []
74
+
75
+
76
+ def job_trace(job_id: int, *, cwd: Path) -> str:
77
+ """Plain-text job trace — no redirect/zip dance needed, unlike GitHub."""
78
+ proc = _run_glab(["api", f"projects/:id/jobs/{job_id}/trace"], cwd=cwd)
79
+ if proc.returncode != 0:
80
+ return ""
81
+ return truncate_log(proc.stdout)
82
+
83
+
84
+ def discover_head(
85
+ mr_iid: str, head_sha: str, *, cwd: Path, fetch_logs: bool = True
86
+ ) -> list[CheckResult]:
87
+ """Discover the pipeline for the MR's head commit specifically.
88
+
89
+ Matches by SHA against the MR's own pipelines (not just "latest") so a
90
+ second push during a retry can't read a stale result from a superseded
91
+ commit.
92
+ """
93
+ pipelines = mr_pipelines(mr_iid, cwd=cwd)
94
+ pipeline = next((p for p in pipelines if p.get("sha") == head_sha), None)
95
+ if pipeline is None:
96
+ return []
97
+ return _discover_from_pipeline(pipeline, cwd=cwd, fetch_logs=fetch_logs)
98
+
99
+
100
+ def _discover_from_pipeline(
101
+ pipeline: dict[str, Any], *, cwd: Path, fetch_logs: bool
102
+ ) -> list[CheckResult]:
103
+ status = pipeline.get("status", "")
104
+ bucket = _bucket_for_status(status)
105
+ result = CheckResult(
106
+ name="pipeline",
107
+ bucket=bucket,
108
+ raw_conclusion=status,
109
+ details_url=pipeline.get("web_url", ""),
110
+ )
111
+ if fetch_logs and bucket == "failure":
112
+ jobs = pipeline_jobs(pipeline["id"], cwd=cwd)
113
+ failed_jobs = [
114
+ j for j in jobs if j.get("status") == "failed" and not j.get("allow_failure")
115
+ ][:_MAX_FAILED_JOB_LOGS]
116
+ excerpts = [
117
+ f"--- job: {j.get('name', '?')} ---\n{job_trace(j['id'], cwd=cwd)}" for j in failed_jobs
118
+ ]
119
+ result.log_excerpt = "\n\n".join(excerpts)
120
+ return [result]
@@ -0,0 +1,137 @@
1
+ """ci-watch gating state machine — dispatches to github.py / gitlab.py.
2
+
3
+ One deterministic function, `check_ci`, used both from the fixer agent's
4
+ Stop hook (`src.agents.hooks.require_ci_pass_hook`) and as the runner's own
5
+ authoritative post-session gate. Calling it twice with the same inputs must
6
+ always agree — that's the whole point of sharing one implementation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Literal
15
+
16
+ from src.activities.ci_watch import github, gitlab
17
+ from src.activities.ci_watch.schemas import CheckResult, CiWatchResult
18
+ from src.activities.git.pr import pr_id
19
+ from src.activities.git.schemas import PRRef
20
+
21
+ # A few short polls a few seconds apart before concluding "no CI configured"
22
+ # — webhook delivery and runner scheduling both have real lag, so an empty
23
+ # result immediately after push is not conclusive.
24
+ _SETTLE_POLLS = 3
25
+ _SETTLE_INTERVAL_SECONDS = 5.0
26
+
27
+ # Bounded wait for still-running checks within a single ci-watch call. A
28
+ # call that times out with checks still running is treated as a failure for
29
+ # gating purposes, same as a real one — the caller just checks again later
30
+ # rather than one call blocking indefinitely.
31
+ _WAIT_TIMEOUT_SECONDS = 480.0
32
+ _WAIT_POLL_INTERVAL_SECONDS = 15.0
33
+
34
+
35
+ def _head_sha(cwd: Path) -> str:
36
+ return subprocess.run(
37
+ ["git", "rev-parse", "HEAD"], cwd=cwd, capture_output=True, text=True, check=True
38
+ ).stdout.strip()
39
+
40
+
41
+ def _discover_head(pr: PRRef, head_sha: str, *, cwd: Path, fetch_logs: bool) -> list[CheckResult]:
42
+ if pr.platform == "github":
43
+ return github.discover(head_sha, cwd=cwd, fetch_logs=fetch_logs)
44
+ return gitlab.discover_head(pr_id(pr), head_sha, cwd=cwd, fetch_logs=fetch_logs)
45
+
46
+
47
+ def _apply_required_weighting(pr: PRRef, checks: list[CheckResult], *, cwd: Path) -> None:
48
+ """GitHub only: mark non-required checks so gating can ignore their
49
+ failures. Best-effort — leaves every check `required=True` (the safe
50
+ default already set by the schema) if branch protection isn't readable.
51
+ """
52
+ if pr.platform != "github":
53
+ return
54
+ required = github.required_check_names(pr.branch, cwd=cwd)
55
+ if required is None:
56
+ return
57
+ for check in checks:
58
+ check.required = check.name in required
59
+
60
+
61
+ def _summarize(checks: list[CheckResult], outcome: str, reason: str) -> str:
62
+ lines = [f"ci-watch: {outcome} — {reason}", ""]
63
+ for c in checks:
64
+ marker = "required" if c.required else "informational"
65
+ lines.append(f"- [{c.bucket}/{marker}] {c.name} ({c.raw_conclusion or 'n/a'})")
66
+ if c.log_excerpt:
67
+ lines.append(f" log excerpt:\n{c.log_excerpt}")
68
+ return "\n".join(lines)
69
+
70
+
71
+ def check_ci(pr: PRRef, *, cwd: Path) -> CiWatchResult:
72
+ """Resolve the head commit and discover every check on it, waiting for
73
+ completion.
74
+
75
+ This function only answers "is CI currently green" — it doesn't decide
76
+ whether a failure is this change's fault. Within the fixer's own Stop
77
+ hook that's what drives the fix-and-recheck loop (see
78
+ src.agents.hooks); after that hook's retry budget is spent, or from the
79
+ runner's own post-session call, a still-red result is informational
80
+ only — the PR gets labeled regardless.
81
+
82
+ Same function backs the fixer agent's Stop hook and the runner's own
83
+ post-session check (see module docstring).
84
+ """
85
+ head_sha = _head_sha(cwd)
86
+
87
+ checks = _discover_head(pr, head_sha, cwd=cwd, fetch_logs=False)
88
+ settled = 0
89
+ while not checks and settled < _SETTLE_POLLS:
90
+ time.sleep(_SETTLE_INTERVAL_SECONDS)
91
+ checks = _discover_head(pr, head_sha, cwd=cwd, fetch_logs=False)
92
+ settled += 1
93
+
94
+ if not checks:
95
+ return CiWatchResult(
96
+ outcome="finish",
97
+ reason="no CI configured on this commit",
98
+ summary_text="No CI checks found on this commit — nothing to verify against.",
99
+ )
100
+
101
+ deadline = time.monotonic() + _WAIT_TIMEOUT_SECONDS
102
+ while any(c.bucket == "running" for c in checks):
103
+ if time.monotonic() >= deadline:
104
+ break
105
+ time.sleep(_WAIT_POLL_INTERVAL_SECONDS)
106
+ checks = _discover_head(pr, head_sha, cwd=cwd, fetch_logs=False)
107
+
108
+ still_running = [c for c in checks if c.bucket == "running"]
109
+ any_failed = any(c.bucket == "failure" for c in checks)
110
+
111
+ # Only pay for failing-step logs and required-check weighting when
112
+ # there's actually something to explain — every green (or still-running)
113
+ # result skips both extra round trips.
114
+ if any_failed:
115
+ checks = _discover_head(pr, head_sha, cwd=cwd, fetch_logs=True)
116
+ _apply_required_weighting(pr, checks, cwd=cwd)
117
+
118
+ failing_required = [c for c in checks if c.bucket == "failure" and c.required]
119
+
120
+ outcome: Literal["finish", "failure"]
121
+ if still_running:
122
+ outcome = "failure"
123
+ reason = f"ci-watch's own wait timed out with {len(still_running)} check(s) still running"
124
+ elif failing_required:
125
+ outcome = "failure"
126
+ names = ", ".join(c.name for c in failing_required)
127
+ reason = f"{len(failing_required)} required check(s) failed: {names}"
128
+ else:
129
+ outcome = "finish"
130
+ reason = "all required checks passed (or nothing to verify against)"
131
+
132
+ return CiWatchResult(
133
+ outcome=outcome,
134
+ reason=reason,
135
+ checks=checks,
136
+ summary_text=_summarize(checks, outcome, reason),
137
+ )