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.
- jeanclode-0.1.0.dist-info/METADATA +21 -0
- jeanclode-0.1.0.dist-info/RECORD +155 -0
- jeanclode-0.1.0.dist-info/WHEEL +5 -0
- jeanclode-0.1.0.dist-info/entry_points.txt +2 -0
- jeanclode-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +8 -0
- src/activities/__init__.py +5 -0
- src/activities/ci_watch/__init__.py +19 -0
- src/activities/ci_watch/github.py +157 -0
- src/activities/ci_watch/gitlab.py +120 -0
- src/activities/ci_watch/poll.py +137 -0
- src/activities/ci_watch/schemas.py +43 -0
- src/activities/decorator.py +111 -0
- src/activities/git/__init__.py +29 -0
- src/activities/git/ops.py +195 -0
- src/activities/git/pr.py +178 -0
- src/activities/git/schemas.py +22 -0
- src/activities/git/targets.py +79 -0
- src/activities/issue/__init__.py +11 -0
- src/activities/issue/comment.py +73 -0
- src/activities/issue/fetch.py +188 -0
- src/activities/issue/schemas.py +17 -0
- src/activities/issue/utils.py +12 -0
- src/activities/notify.py +130 -0
- src/activities/respond/__init__.py +21 -0
- src/activities/respond/relabel.py +58 -0
- src/activities/respond/schemas.py +42 -0
- src/activities/respond/threads.py +149 -0
- src/activities/review/__init__.py +16 -0
- src/activities/review/dedup.py +18 -0
- src/activities/review/guardrail.py +44 -0
- src/activities/review/posting.py +407 -0
- src/activities/review/recovery.py +128 -0
- src/activities/review/routing.py +50 -0
- src/activities/review/schemas.py +111 -0
- src/activities/review/styling.py +25 -0
- src/activities/sentry/__init__.py +47 -0
- src/activities/sentry/fetch.py +208 -0
- src/activities/sentry/git.py +9 -0
- src/activities/sentry/pr.py +3 -0
- src/activities/sentry/route.py +139 -0
- src/activities/sentry/schemas.py +152 -0
- src/activities/sentry/secrets.py +57 -0
- src/activities/summary/__init__.py +17 -0
- src/activities/summary/format_summary.py +13 -0
- src/activities/summary/schemas.py +54 -0
- src/activities/summary/update_description.py +78 -0
- src/adaptors/__init__.py +5 -0
- src/adaptors/base.py +85 -0
- src/adaptors/diffn.py +68 -0
- src/adaptors/discussions.py +103 -0
- src/adaptors/display.py +50 -0
- src/adaptors/github/__init__.py +1 -0
- src/adaptors/github/adaptor.py +83 -0
- src/adaptors/github/auth.py +36 -0
- src/adaptors/github/client.py +92 -0
- src/adaptors/github/context.py +477 -0
- src/adaptors/github/display.py +338 -0
- src/adaptors/github/resolver.py +24 -0
- src/adaptors/github/sha.py +100 -0
- src/adaptors/gitlab/__init__.py +1 -0
- src/adaptors/gitlab/adaptor.py +79 -0
- src/adaptors/gitlab/auth.py +49 -0
- src/adaptors/gitlab/client.py +77 -0
- src/adaptors/gitlab/context.py +388 -0
- src/adaptors/gitlab/display.py +9 -0
- src/adaptors/gitlab/resolver.py +24 -0
- src/adaptors/gitlab/sha.py +73 -0
- src/adaptors/registry.py +31 -0
- src/adaptors/sentry/__init__.py +5 -0
- src/adaptors/sentry/adaptor.py +71 -0
- src/adaptors/sentry/auth.py +31 -0
- src/adaptors/sentry/client.py +78 -0
- src/adaptors/sentry/display.py +41 -0
- src/adaptors/sentry/resolver.py +71 -0
- src/agents/__init__.py +6 -0
- src/agents/_memory_backend.py +151 -0
- src/agents/base.py +430 -0
- src/agents/hooks.py +481 -0
- src/agents/issue/__init__.py +21 -0
- src/agents/issue/base.py +21 -0
- src/agents/issue/fixer.py +22 -0
- src/agents/issue/schemas.py +84 -0
- src/agents/issue/triage.py +20 -0
- src/agents/memory_tool.py +248 -0
- src/agents/respond/__init__.py +10 -0
- src/agents/respond/base.py +21 -0
- src/agents/respond/planner.py +31 -0
- src/agents/respond/schemas.py +47 -0
- src/agents/review/__init__.py +21 -0
- src/agents/review/analyzer.py +24 -0
- src/agents/review/base.py +12 -0
- src/agents/review/deduplicator.py +22 -0
- src/agents/review/fact_checker.py +26 -0
- src/agents/review/issue_explorer.py +22 -0
- src/agents/review/schemas.py +22 -0
- src/agents/review/styler.py +21 -0
- src/agents/review/synthesizer.py +23 -0
- src/agents/schemas.py +46 -0
- src/agents/sentry/__init__.py +17 -0
- src/agents/sentry/base.py +21 -0
- src/agents/sentry/fixer.py +43 -0
- src/agents/sentry/synthesis.py +24 -0
- src/agents/sentry/triage.py +29 -0
- src/agents/summary/__init__.py +9 -0
- src/agents/summary/base.py +21 -0
- src/agents/summary/parser.py +20 -0
- src/agents/summary/schemas.py +15 -0
- src/agents/summary/summarizer.py +21 -0
- src/agents/utils.py +84 -0
- src/cli.py +182 -0
- src/config.py +142 -0
- src/config_file.py +179 -0
- src/log.py +50 -0
- src/main.py +38 -0
- src/output.py +429 -0
- src/plugin_paths.py +25 -0
- src/runner/__init__.py +5 -0
- src/runner/display_subscriber.py +128 -0
- src/runner/preflight.py +238 -0
- src/runner/run.py +310 -0
- src/runtime/__init__.py +25 -0
- src/runtime/bots.py +29 -0
- src/runtime/bus.py +39 -0
- src/runtime/context.py +53 -0
- src/runtime/events.py +65 -0
- src/runtime/mcp_connectors.py +74 -0
- src/runtime/notify.py +94 -0
- src/skills/__init__.py +30 -0
- src/skills/discovery.py +99 -0
- src/skills/prompt.py +123 -0
- src/skills/schemas.py +23 -0
- src/skills/thirdparty.py +154 -0
- src/workflows/__init__.py +29 -0
- src/workflows/_smoke/__init__.py +0 -0
- src/workflows/_smoke/echo.py +55 -0
- src/workflows/base.py +97 -0
- src/workflows/code_review/__init__.py +1 -0
- src/workflows/code_review/runner.py +315 -0
- src/workflows/code_review/utils.py +205 -0
- src/workflows/issue_resolve/__init__.py +1 -0
- src/workflows/issue_resolve/runner.py +362 -0
- src/workflows/issue_resolve/utils.py +141 -0
- src/workflows/jeanclode_respond/__init__.py +5 -0
- src/workflows/jeanclode_respond/runner.py +212 -0
- src/workflows/jeanclode_respond/utils.py +150 -0
- src/workflows/pr_summary/__init__.py +5 -0
- src/workflows/pr_summary/runner.py +126 -0
- src/workflows/pr_summary/utils.py +138 -0
- src/workflows/schemas.py +13 -0
- src/workflows/sentry_fix/__init__.py +5 -0
- src/workflows/sentry_fix/runner.py +425 -0
- src/workflows/sentry_fix/utils.py +235 -0
- src/workflows/utils.py +9 -0
- 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 @@
|
|
|
1
|
+
src
|
src/__init__.py
ADDED
|
@@ -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
|
+
)
|