selfheal-api 1.0.0rc1__tar.gz

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 (245) hide show
  1. selfheal_api-1.0.0rc1/.dockerignore +13 -0
  2. selfheal_api-1.0.0rc1/.python-version +1 -0
  3. selfheal_api-1.0.0rc1/Containerfile.pipeline +39 -0
  4. selfheal_api-1.0.0rc1/FINDINGS.md +157 -0
  5. selfheal_api-1.0.0rc1/FINDINGS_V1.md +156 -0
  6. selfheal_api-1.0.0rc1/GITHUB_APP_DESIGN.md +461 -0
  7. selfheal_api-1.0.0rc1/IMPLEMENTATION.md +437 -0
  8. selfheal_api-1.0.0rc1/LICENSE +202 -0
  9. selfheal_api-1.0.0rc1/MANUAL_CHECKS.md +47 -0
  10. selfheal_api-1.0.0rc1/NOTICE +7 -0
  11. selfheal_api-1.0.0rc1/PKG-INFO +56 -0
  12. selfheal_api-1.0.0rc1/PROPOSAL_PERF_FIX.md +140 -0
  13. selfheal_api-1.0.0rc1/README.md +19 -0
  14. selfheal_api-1.0.0rc1/SETUP.md +377 -0
  15. selfheal_api-1.0.0rc1/SETUP_V2_DESIGN.md +220 -0
  16. selfheal_api-1.0.0rc1/SETUP_WIZARD_DESIGN.md +85 -0
  17. selfheal_api-1.0.0rc1/WIZARD_TEST.md +135 -0
  18. selfheal_api-1.0.0rc1/constraints-image.txt +10 -0
  19. selfheal_api-1.0.0rc1/constraints.txt +49 -0
  20. selfheal_api-1.0.0rc1/docker-compose.app.yml +27 -0
  21. selfheal_api-1.0.0rc1/docker-compose.yml +47 -0
  22. selfheal_api-1.0.0rc1/eval/demo_app_frozen/app.py +278 -0
  23. selfheal_api-1.0.0rc1/eval/demo_app_frozen/otel_logging.py +118 -0
  24. selfheal_api-1.0.0rc1/eval/demo_app_frozen/tests/conftest.py +9 -0
  25. selfheal_api-1.0.0rc1/eval/demo_app_frozen/tests/test_app_routes.py +43 -0
  26. selfheal_api-1.0.0rc1/eval/demo_app_frozen/traffic/traffic.py +120 -0
  27. selfheal_api-1.0.0rc1/eval/demo_app_frozen/traffic/traffic_loop.py +61 -0
  28. selfheal_api-1.0.0rc1/eval/demo_bugs.yml +222 -0
  29. selfheal_api-1.0.0rc1/eval/score_demo.py +244 -0
  30. selfheal_api-1.0.0rc1/eval_matrix.py +275 -0
  31. selfheal_api-1.0.0rc1/fixtures.sha256 +11 -0
  32. selfheal_api-1.0.0rc1/logs/scenario_healthy.jsonl +50 -0
  33. selfheal_api-1.0.0rc1/logs/scenario_load_spike.jsonl +70 -0
  34. selfheal_api-1.0.0rc1/logs/scenario_load_spike_clean.jsonl +70 -0
  35. selfheal_api-1.0.0rc1/logs/scenario_missing_input.jsonl +54 -0
  36. selfheal_api-1.0.0rc1/logs/scenario_missing_input_clean.jsonl +54 -0
  37. selfheal_api-1.0.0rc1/logs/scenario_null_field.jsonl +54 -0
  38. selfheal_api-1.0.0rc1/logs/scenario_null_field_clean.jsonl +54 -0
  39. selfheal_api-1.0.0rc1/logs/scenario_slow_query.jsonl +54 -0
  40. selfheal_api-1.0.0rc1/logs/scenario_slow_query_clean.jsonl +54 -0
  41. selfheal_api-1.0.0rc1/logs/scenario_two_incidents.jsonl +58 -0
  42. selfheal_api-1.0.0rc1/logs/scenario_two_incidents_clean.jsonl +58 -0
  43. selfheal_api-1.0.0rc1/pyproject.toml +72 -0
  44. selfheal_api-1.0.0rc1/requirements-dev.txt +5 -0
  45. selfheal_api-1.0.0rc1/requirements.txt +9 -0
  46. selfheal_api-1.0.0rc1/run_scenarios.py +142 -0
  47. selfheal_api-1.0.0rc1/selfheal/__init__.py +7 -0
  48. selfheal_api-1.0.0rc1/selfheal/__main__.py +3 -0
  49. selfheal_api-1.0.0rc1/selfheal/actions.py +955 -0
  50. selfheal_api-1.0.0rc1/selfheal/alerts.py +334 -0
  51. selfheal_api-1.0.0rc1/selfheal/api.py +745 -0
  52. selfheal_api-1.0.0rc1/selfheal/app_check.py +164 -0
  53. selfheal_api-1.0.0rc1/selfheal/approval.py +184 -0
  54. selfheal_api-1.0.0rc1/selfheal/appsource.py +46 -0
  55. selfheal_api-1.0.0rc1/selfheal/ask_and_gate.py +113 -0
  56. selfheal_api-1.0.0rc1/selfheal/ask_ticket.py +170 -0
  57. selfheal_api-1.0.0rc1/selfheal/attempts.py +312 -0
  58. selfheal_api-1.0.0rc1/selfheal/brief.py +268 -0
  59. selfheal_api-1.0.0rc1/selfheal/chain.py +290 -0
  60. selfheal_api-1.0.0rc1/selfheal/cli.py +338 -0
  61. selfheal_api-1.0.0rc1/selfheal/containers.py +537 -0
  62. selfheal_api-1.0.0rc1/selfheal/dedupe.py +125 -0
  63. selfheal_api-1.0.0rc1/selfheal/demo_reset.py +230 -0
  64. selfheal_api-1.0.0rc1/selfheal/deploy/Containerfile.pipeline +39 -0
  65. selfheal_api-1.0.0rc1/selfheal/deploy/constraints-image.txt +10 -0
  66. selfheal_api-1.0.0rc1/selfheal/deploy/constraints.txt +49 -0
  67. selfheal_api-1.0.0rc1/selfheal/devseed.py +201 -0
  68. selfheal_api-1.0.0rc1/selfheal/diffctx.py +77 -0
  69. selfheal_api-1.0.0rc1/selfheal/egress_proxy.py +128 -0
  70. selfheal_api-1.0.0rc1/selfheal/evidence.py +186 -0
  71. selfheal_api-1.0.0rc1/selfheal/export.py +124 -0
  72. selfheal_api-1.0.0rc1/selfheal/fakejev.py +78 -0
  73. selfheal_api-1.0.0rc1/selfheal/fix_intent.py +132 -0
  74. selfheal_api-1.0.0rc1/selfheal/gate.py +345 -0
  75. selfheal_api-1.0.0rc1/selfheal/gate_config.json +7 -0
  76. selfheal_api-1.0.0rc1/selfheal/github.py +241 -0
  77. selfheal_api-1.0.0rc1/selfheal/github_auth.py +412 -0
  78. selfheal_api-1.0.0rc1/selfheal/goals.py +146 -0
  79. selfheal_api-1.0.0rc1/selfheal/ground_truth.py +165 -0
  80. selfheal_api-1.0.0rc1/selfheal/guard.py +749 -0
  81. selfheal_api-1.0.0rc1/selfheal/host_os.py +52 -0
  82. selfheal_api-1.0.0rc1/selfheal/implement.py +495 -0
  83. selfheal_api-1.0.0rc1/selfheal/incidents.py +280 -0
  84. selfheal_api-1.0.0rc1/selfheal/lifecycle.py +251 -0
  85. selfheal_api-1.0.0rc1/selfheal/logsource.py +98 -0
  86. selfheal_api-1.0.0rc1/selfheal/mitigations.py +73 -0
  87. selfheal_api-1.0.0rc1/selfheal/nodes.py +54 -0
  88. selfheal_api-1.0.0rc1/selfheal/orchestrator.py +755 -0
  89. selfheal_api-1.0.0rc1/selfheal/outcome.py +114 -0
  90. selfheal_api-1.0.0rc1/selfheal/paths.py +20 -0
  91. selfheal_api-1.0.0rc1/selfheal/pipeline_home.py +380 -0
  92. selfheal_api-1.0.0rc1/selfheal/plan.py +462 -0
  93. selfheal_api-1.0.0rc1/selfheal/plan_queue.py +360 -0
  94. selfheal_api-1.0.0rc1/selfheal/poller.py +86 -0
  95. selfheal_api-1.0.0rc1/selfheal/preflight.py +419 -0
  96. selfheal_api-1.0.0rc1/selfheal/recommendation.py +34 -0
  97. selfheal_api-1.0.0rc1/selfheal/redact.py +94 -0
  98. selfheal_api-1.0.0rc1/selfheal/replay.py +39 -0
  99. selfheal_api-1.0.0rc1/selfheal/report.py +569 -0
  100. selfheal_api-1.0.0rc1/selfheal/retrigger.py +79 -0
  101. selfheal_api-1.0.0rc1/selfheal/reviewer.py +317 -0
  102. selfheal_api-1.0.0rc1/selfheal/revise.py +575 -0
  103. selfheal_api-1.0.0rc1/selfheal/rules.py +93 -0
  104. selfheal_api-1.0.0rc1/selfheal/runtime.py +254 -0
  105. selfheal_api-1.0.0rc1/selfheal/safety_net.py +129 -0
  106. selfheal_api-1.0.0rc1/selfheal/sandbox.py +674 -0
  107. selfheal_api-1.0.0rc1/selfheal/schemathesis_replay.py +128 -0
  108. selfheal_api-1.0.0rc1/selfheal/scope.py +218 -0
  109. selfheal_api-1.0.0rc1/selfheal/second_opinion.py +89 -0
  110. selfheal_api-1.0.0rc1/selfheal/serve_and_run.py +64 -0
  111. selfheal_api-1.0.0rc1/selfheal/setup/__init__.py +1 -0
  112. selfheal_api-1.0.0rc1/selfheal/setup/adapters/__init__.py +2 -0
  113. selfheal_api-1.0.0rc1/selfheal/setup/adapters/logsource.py +51 -0
  114. selfheal_api-1.0.0rc1/selfheal/setup/adapters/platform.py +318 -0
  115. selfheal_api-1.0.0rc1/selfheal/setup/claude_auth.py +78 -0
  116. selfheal_api-1.0.0rc1/selfheal/setup/cli.py +216 -0
  117. selfheal_api-1.0.0rc1/selfheal/setup/core.py +229 -0
  118. selfheal_api-1.0.0rc1/selfheal/setup/engine.py +248 -0
  119. selfheal_api-1.0.0rc1/selfheal/setup/envfile.py +65 -0
  120. selfheal_api-1.0.0rc1/selfheal/setup/io.py +69 -0
  121. selfheal_api-1.0.0rc1/selfheal/setup/manifest.py +180 -0
  122. selfheal_api-1.0.0rc1/selfheal/setup/pipeline.py +196 -0
  123. selfheal_api-1.0.0rc1/selfheal/setup/steps.py +1294 -0
  124. selfheal_api-1.0.0rc1/selfheal/setup/system.py +132 -0
  125. selfheal_api-1.0.0rc1/selfheal/setup/ui_server.py +693 -0
  126. selfheal_api-1.0.0rc1/selfheal/setup/ui_static/app.js +349 -0
  127. selfheal_api-1.0.0rc1/selfheal/setup/ui_static/index.html +66 -0
  128. selfheal_api-1.0.0rc1/selfheal/setup/ui_static/style.css +69 -0
  129. selfheal_api-1.0.0rc1/selfheal/setup_cmds.py +1303 -0
  130. selfheal_api-1.0.0rc1/selfheal/stacktrace.py +184 -0
  131. selfheal_api-1.0.0rc1/selfheal/store.py +838 -0
  132. selfheal_api-1.0.0rc1/selfheal/templates/Containerfile.guard +26 -0
  133. selfheal_api-1.0.0rc1/selfheal/templates/Containerfile.sandbox +14 -0
  134. selfheal_api-1.0.0rc1/selfheal/templates/full/selfheal-guard.yml +105 -0
  135. selfheal_api-1.0.0rc1/selfheal/templates/full/selfheal-implement.yml +128 -0
  136. selfheal_api-1.0.0rc1/selfheal/templates/full/selfheal-revise.yml +144 -0
  137. selfheal_api-1.0.0rc1/selfheal/templates/run_precommit.sh +32 -0
  138. selfheal_api-1.0.0rc1/selfheal/templates/selfheal-guard.yml +28 -0
  139. selfheal_api-1.0.0rc1/selfheal/templates/selfheal-implement-approved.yml +123 -0
  140. selfheal_api-1.0.0rc1/selfheal/templates/selfheal-implement.yml +27 -0
  141. selfheal_api-1.0.0rc1/selfheal/templates/selfheal-plan.yml +73 -0
  142. selfheal_api-1.0.0rc1/selfheal/templates/selfheal-revise.yml +26 -0
  143. selfheal_api-1.0.0rc1/selfheal/ticket_pack.py +72 -0
  144. selfheal_api-1.0.0rc1/selfheal/tickets.py +1015 -0
  145. selfheal_api-1.0.0rc1/selfheal/token_cli.py +131 -0
  146. selfheal_api-1.0.0rc1/selfheal/trigger.py +369 -0
  147. selfheal_api-1.0.0rc1/selfheal/ui/index.html +958 -0
  148. selfheal_api-1.0.0rc1/selfheal/watcher.py +151 -0
  149. selfheal_api-1.0.0rc1/selfheal/workflow_stubs.py +273 -0
  150. selfheal_api-1.0.0rc1/selfheal/workspace.py +85 -0
  151. selfheal_api-1.0.0rc1/selfheal_api.egg-info/PKG-INFO +56 -0
  152. selfheal_api-1.0.0rc1/selfheal_api.egg-info/SOURCES.txt +243 -0
  153. selfheal_api-1.0.0rc1/selfheal_api.egg-info/dependency_links.txt +1 -0
  154. selfheal_api-1.0.0rc1/selfheal_api.egg-info/entry_points.txt +2 -0
  155. selfheal_api-1.0.0rc1/selfheal_api.egg-info/requires.txt +8 -0
  156. selfheal_api-1.0.0rc1/selfheal_api.egg-info/scm_file_list.json +239 -0
  157. selfheal_api-1.0.0rc1/selfheal_api.egg-info/scm_version.json +8 -0
  158. selfheal_api-1.0.0rc1/selfheal_api.egg-info/top_level.txt +1 -0
  159. selfheal_api-1.0.0rc1/setup.cfg +4 -0
  160. selfheal_api-1.0.0rc1/tests/conftest.py +120 -0
  161. selfheal_api-1.0.0rc1/tests/fakes.py +56 -0
  162. selfheal_api-1.0.0rc1/tests/fixtures/schemathesis_demo_api_main.ndjson +12 -0
  163. selfheal_api-1.0.0rc1/tests/loggen.py +51 -0
  164. selfheal_api-1.0.0rc1/tests/setup_ui_helpers.py +105 -0
  165. selfheal_api-1.0.0rc1/tests/test_app_check.py +122 -0
  166. selfheal_api-1.0.0rc1/tests/test_appsource.py +62 -0
  167. selfheal_api-1.0.0rc1/tests/test_ask_and_gate.py +69 -0
  168. selfheal_api-1.0.0rc1/tests/test_commit_identity.py +115 -0
  169. selfheal_api-1.0.0rc1/tests/test_compose_network.py +31 -0
  170. selfheal_api-1.0.0rc1/tests/test_compose_ports.py +26 -0
  171. selfheal_api-1.0.0rc1/tests/test_containers.py +417 -0
  172. selfheal_api-1.0.0rc1/tests/test_decision_fixes.py +345 -0
  173. selfheal_api-1.0.0rc1/tests/test_demo_reset.py +205 -0
  174. selfheal_api-1.0.0rc1/tests/test_direct_implement.py +787 -0
  175. selfheal_api-1.0.0rc1/tests/test_end_to_end_offline.py +187 -0
  176. selfheal_api-1.0.0rc1/tests/test_flapping_resolution.py +142 -0
  177. selfheal_api-1.0.0rc1/tests/test_full_workflows.py +275 -0
  178. selfheal_api-1.0.0rc1/tests/test_gate.py +556 -0
  179. selfheal_api-1.0.0rc1/tests/test_github_auth.py +901 -0
  180. selfheal_api-1.0.0rc1/tests/test_ground_truth.py +67 -0
  181. selfheal_api-1.0.0rc1/tests/test_guard_retries.py +600 -0
  182. selfheal_api-1.0.0rc1/tests/test_host_os.py +86 -0
  183. selfheal_api-1.0.0rc1/tests/test_isolation.py +57 -0
  184. selfheal_api-1.0.0rc1/tests/test_lifecycle_cost.py +484 -0
  185. selfheal_api-1.0.0rc1/tests/test_live_run_fixes.py +275 -0
  186. selfheal_api-1.0.0rc1/tests/test_mitigations.py +68 -0
  187. selfheal_api-1.0.0rc1/tests/test_network_guard.py +58 -0
  188. selfheal_api-1.0.0rc1/tests/test_outcome.py +172 -0
  189. selfheal_api-1.0.0rc1/tests/test_packaging.py +216 -0
  190. selfheal_api-1.0.0rc1/tests/test_pipeline_actions.py +682 -0
  191. selfheal_api-1.0.0rc1/tests/test_pipeline_alerts.py +312 -0
  192. selfheal_api-1.0.0rc1/tests/test_pipeline_api.py +140 -0
  193. selfheal_api-1.0.0rc1/tests/test_pipeline_approval.py +215 -0
  194. selfheal_api-1.0.0rc1/tests/test_pipeline_chain.py +154 -0
  195. selfheal_api-1.0.0rc1/tests/test_pipeline_evidence.py +389 -0
  196. selfheal_api-1.0.0rc1/tests/test_pipeline_fakejev.py +147 -0
  197. selfheal_api-1.0.0rc1/tests/test_pipeline_guard.py +535 -0
  198. selfheal_api-1.0.0rc1/tests/test_pipeline_implement.py +163 -0
  199. selfheal_api-1.0.0rc1/tests/test_pipeline_incidents.py +545 -0
  200. selfheal_api-1.0.0rc1/tests/test_pipeline_lifecycle.py +116 -0
  201. selfheal_api-1.0.0rc1/tests/test_pipeline_one_issue.py +536 -0
  202. selfheal_api-1.0.0rc1/tests/test_pipeline_orchestrator.py +393 -0
  203. selfheal_api-1.0.0rc1/tests/test_pipeline_plan.py +161 -0
  204. selfheal_api-1.0.0rc1/tests/test_pipeline_poller.py +92 -0
  205. selfheal_api-1.0.0rc1/tests/test_pipeline_reviewer.py +172 -0
  206. selfheal_api-1.0.0rc1/tests/test_pipeline_store_logsource.py +166 -0
  207. selfheal_api-1.0.0rc1/tests/test_pipeline_tickets.py +746 -0
  208. selfheal_api-1.0.0rc1/tests/test_pipeline_trigger.py +302 -0
  209. selfheal_api-1.0.0rc1/tests/test_pipeline_ui_api.py +584 -0
  210. selfheal_api-1.0.0rc1/tests/test_pipeline_watcher.py +215 -0
  211. selfheal_api-1.0.0rc1/tests/test_pipeline_webhook_security.py +257 -0
  212. selfheal_api-1.0.0rc1/tests/test_plan_queue.py +457 -0
  213. selfheal_api-1.0.0rc1/tests/test_preflight.py +578 -0
  214. selfheal_api-1.0.0rc1/tests/test_repo_guards.py +221 -0
  215. selfheal_api-1.0.0rc1/tests/test_report.py +71 -0
  216. selfheal_api-1.0.0rc1/tests/test_retrigger.py +97 -0
  217. selfheal_api-1.0.0rc1/tests/test_revise_pr.py +796 -0
  218. selfheal_api-1.0.0rc1/tests/test_rules.py +157 -0
  219. selfheal_api-1.0.0rc1/tests/test_run_budget_queue.py +126 -0
  220. selfheal_api-1.0.0rc1/tests/test_runtime.py +270 -0
  221. selfheal_api-1.0.0rc1/tests/test_safety_net.py +161 -0
  222. selfheal_api-1.0.0rc1/tests/test_sandbox.py +628 -0
  223. selfheal_api-1.0.0rc1/tests/test_sandbox_smoke.py +163 -0
  224. selfheal_api-1.0.0rc1/tests/test_score_demo.py +242 -0
  225. selfheal_api-1.0.0rc1/tests/test_setup_cmds.py +675 -0
  226. selfheal_api-1.0.0rc1/tests/test_setup_engine.py +398 -0
  227. selfheal_api-1.0.0rc1/tests/test_setup_pipeline_home.py +293 -0
  228. selfheal_api-1.0.0rc1/tests/test_setup_ui_page.py +22 -0
  229. selfheal_api-1.0.0rc1/tests/test_setup_ui_security.py +199 -0
  230. selfheal_api-1.0.0rc1/tests/test_setup_ui_server.py +259 -0
  231. selfheal_api-1.0.0rc1/tests/test_setup_wizard.py +943 -0
  232. selfheal_api-1.0.0rc1/tests/test_stacktrace.py +140 -0
  233. selfheal_api-1.0.0rc1/tests/test_thin_workflows.py +198 -0
  234. selfheal_api-1.0.0rc1/tests/test_token_scopes.py +692 -0
  235. selfheal_api-1.0.0rc1/tests/test_ui_checks.py +168 -0
  236. selfheal_api-1.0.0rc1/tests/test_workflow_concurrency.py +255 -0
  237. selfheal_api-1.0.0rc1/tests/test_workflow_templates.py +285 -0
  238. selfheal_api-1.0.0rc1/tests/ticketrig.py +132 -0
  239. selfheal_api-1.0.0rc1/tests/ui/package-lock.json +543 -0
  240. selfheal_api-1.0.0rc1/tests/ui/package.json +6 -0
  241. selfheal_api-1.0.0rc1/tests/ui/run_setup_ui_checks.js +208 -0
  242. selfheal_api-1.0.0rc1/tests/ui/run_ui_checks.js +341 -0
  243. selfheal_api-1.0.0rc1/tests/wf_sources.py +79 -0
  244. selfheal_api-1.0.0rc1/tests/wizard_fakes.py +478 -0
  245. selfheal_api-1.0.0rc1/tune_thresholds.py +103 -0
@@ -0,0 +1,13 @@
1
+ # Keep the build context small and keep secrets/evidence out of images.
2
+ .venv
3
+ .git
4
+ .pytest_cache
5
+ __pycache__
6
+ *.pyc
7
+ .env
8
+ results
9
+ results.bak
10
+ logs
11
+ logs.bak
12
+ traces
13
+ data
@@ -0,0 +1 @@
1
+ 3.14.6
@@ -0,0 +1,39 @@
1
+ # Service C: the pipeline -- tails service A's log, runs the stages on alerts, serves the API/UI.
2
+ FROM python:3.13-slim
3
+ WORKDIR /app
4
+
5
+ # `openssl dgst -sign` signs the GitHub App's JWT (selfheal/app_check.py): the key file is passed by path and never read by Python.
6
+ RUN apt-get update && apt-get install -y --no-install-recommends openssl \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY pyproject.toml README.md constraints.txt constraints-image.txt ./
10
+ COPY selfheal/ selfheal/
11
+ # ONE pip call with both constraint files, so the resolver sees every requirement at once (installing the OpenTelemetry
12
+ # instrumentation in a second call downgraded api/semantic-conventions under an sdk that needs the newer ones). The
13
+ # pipeline imports the customer's app, so that app's own imports (opentelemetry-instrumentation-fastapi) must resolve here too.
14
+ # SELFHEAL_VERSION: the build context has no git history, so `selfheal --version` would say 0.0.0+unknown; pass the real version
15
+ # (docker-compose.yml forwards $SELFHEAL_VERSION and $SELFHEAL_COMMIT; `selfheal runner-install --print-version` prints the version).
16
+ # `pip check` fails the build on any remaining conflict; pip itself only warns about conflicts in already-installed packages.
17
+ ARG SELFHEAL_VERSION=0.0.0+unknown
18
+ ARG SELFHEAL_COMMIT=unknown
19
+ RUN SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SELFHEAL_API=$SELFHEAL_VERSION \
20
+ pip install --no-cache-dir -c constraints.txt -c constraints-image.txt . opentelemetry-instrumentation-fastapi \
21
+ && pip check
22
+ # The same install record `runner-install` writes, so `selfheal --version` names the commit the image was built from.
23
+ RUN python3 -c "import json, os, sys, datetime; c = os.environ['SELFHEAL_COMMIT']; \
24
+ c != 'unknown' and open(os.path.join(sys.prefix, 'selfheal-install.json'), 'w').write(json.dumps({'commit': c, \
25
+ 'version': os.environ['SELFHEAL_VERSION'], 'installed_at': datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds'), \
26
+ 'installed_by': 'image build'}))"
27
+
28
+ ENV PYTHONDONTWRITEBYTECODE=1 \
29
+ PIPELINE_APP_CHECKOUT=/demo-api \
30
+ OTEL_LOG_PATH=/tmp/pipeline-imports-app-but-never-logs.jsonl \
31
+ PIPELINE_LOG_PATH=/app/logs/otel-logs.jsonl \
32
+ PIPELINE_DATA_DIR=/app/data \
33
+ PIPELINE_DB_JOURNAL=DELETE
34
+ EXPOSE 8100
35
+
36
+ HEALTHCHECK --interval=5s --timeout=2s --start-period=10s --retries=5 \
37
+ CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8100/health', timeout=2)"
38
+
39
+ CMD ["selfheal", "serve"]
@@ -0,0 +1,157 @@
1
+ # jevbrief-eval: Findings and Final Thresholds
2
+
3
+ The "decide" half of the self-healing-APIs pipeline (collect → classify → second
4
+ opinion → gate) as evaluated against 6 scenarios, 6 goal-wording variants, and ~280
5
+ real Jev calls across this evaluation phase. This is the write-up requested at the end
6
+ of that work: final thresholds, what each fix actually changed, the one open item, and
7
+ the one thing this data cannot tell us.
8
+
9
+ ## Final thresholds
10
+
11
+ ```json
12
+ {"T_incident": 0.8, "T_class": 0.8, "M": 0.2}
13
+ ```
14
+
15
+ **Unchanged from the original provisional guess.** A full sweep (`tune_thresholds.py`)
16
+ across both axes from 0.50 to 0.95 found **unsafe stays at 0 everywhere in that range**
17
+ — see "The limitation" below for why that means these numbers aren't actually
18
+ *validated*, just *not contradicted*. Lowering them bought measurably more automation
19
+ coverage in the sweep (0.8/0.8 → 7/100 rows proceed+recommend; 0.5/0.5 → 20/100) but
20
+ with no evidence it was buying real safety margin to spend, so they were left alone
21
+ rather than moved for a speculative gain.
22
+
23
+ ## What each fix actually changed
24
+
25
+ In the order they were built, with the concrete before/after:
26
+
27
+ 1. **Clean, seeded fixtures** (`traffic.py --clean`, `run_scenarios.py --clean <name>`).
28
+ `baseline()` incidentally trips the null-field and slow-query bugs in every
29
+ scenario's fixture (ids 4/8 and 5/10 in its loop), so every scenario's log file
30
+ contained noise from bugs it wasn't testing. `baseline_clean()` fixtures now exist
31
+ for all 5 scenarios with a real injected failure; the originals are kept as an
32
+ untouched, checksum-verified archive. This directly fixed a false "wrong pick" on
33
+ `two_incidents/misleading`, where Jev had confidently (0.96-0.97) picked real
34
+ leftover baseline noise that the answer key didn't know to expect — not a model
35
+ error, an incomplete fixture. After the fix: `two_incidents` incident accuracy
36
+ 8/8 (was 6/8).
37
+
38
+ 2. **`two_incidents` class scoring** (`ground_truth.score_class`). Previously scored
39
+ against a fixed set regardless of which of the two real incidents was actually
40
+ picked. Now defers to whichever incident's own ground truth applies (null_field's if
41
+ the `AttributeError` was picked, deliberately unscored if the `KeyError` was, same as
42
+ `missing_input` alone).
43
+
44
+ 3. **Safety net** (`safety_net.py`). Plain code, independent of the goal and of Jev:
45
+ scans the raw log for ERROR/5xx records regardless of what jevbrief concluded. Across
46
+ the final 100-row stress run, it would open an "unexplained errors" ticket on 40/100
47
+ rows where the goal-driven question alone said `none`/`low_confidence` — including
48
+ both `load_spike/vague` rows sitting on 15 real 503 rejections that would otherwise
49
+ have disappeared with no ticket at all. **Silent misses (real error, Jev said none,
50
+ backstop didn't fire): 0/100** across every run checked. **False fires on `healthy`:
51
+ 0/15.**
52
+
53
+ 4. **Route/status claim check** (`goals.ALERT_CLAIMS` + `report.check_claim_mismatch` +
54
+ `gate.py`'s `claim_mismatch` field). When the goal makes an explicit route/status
55
+ claim (`alert`/`misleading` variants), does the picked fact actually match it? A
56
+ mismatch escalates regardless of confidence. 20/100 rows in the final run made a
57
+ checkable claim; **0/20 mismatches** — the clean fixtures already removed the one
58
+ case (the `two_incidents` near-miss above) this would otherwise have caught.
59
+
60
+ 5. **Straddle logic fix** (`gate.py`, `STRADDLE_MIN_RUNNER_UP = 0.10`). This was a real
61
+ logic bug, not a tuning question: the straddle check ("escalate if the top two
62
+ classes span agent-fixable and human-only, regardless of confidence") fired on *any*
63
+ nonzero runner-up probability — so `slow_query`, which classified as `infrastructure`
64
+ at ~0.999 probability with `bug_fix` at ~0.001, was permanently stuck at `escalate`
65
+ via straddle no matter how confident or consistent the model was. Now a runner-up
66
+ needs ≥0.10 probability to count as genuine indecision. Both directions tested
67
+ (fixable-winner-with-small-human-only-runner-up, and the reverse), plus the boundary
68
+ case at exactly 0.10. After the fix: **`slow_query` reaches `recommend` in all 10
69
+ tested `alert` runs** (was stuck at `escalate` in 100% of runs before).
70
+
71
+ 6. **Alert goal template** (`goals.py`, `ALERT_SIGNALS`). The original `alert` goals
72
+ were terse (`"elevated 5xx on GET /users/{user_id}"`). Rewritten to what a real
73
+ alerting rule actually emits: service/route, status class or latency, an observed
74
+ rate, and a time window (`"5xx rate on GET /users/{user_id} rose from 0% to 9% at
75
+ 14:02"`) — confirmed to contain no exception type and no answer (see audit below).
76
+ This alone, with no threshold change, moved `null_field`'s ticket-class confidence
77
+ from 0.61-0.70 to **0.80-0.85**, flipping it from permanently `escalate` to
78
+ `proceed` in all 5 tested runs at the unchanged `T_class=0.8`.
79
+
80
+ **Audit of the alert templates** (`ALERT_SIGNALS`, checked programmatically against a
81
+ list of leak terms: exception names, "crash", "queue", "saturate", "reject",
82
+ "unhandled", etc.): all 5 templates plus `healthy`'s "all services nominal" are clean —
83
+ each contains only service/route/status-class-or-latency/rate/time, nothing only
84
+ visible by having already solved the incident.
85
+
86
+ ## Open item: `load_spike`
87
+
88
+ Per-fact inspection of 5 real `load_spike/alert` traces: **every run has exactly one
89
+ kept fact** (the checkout-saturation message) — there is nothing to merge in grouping,
90
+ jevbrief already collapsed all 15 rejection log lines into one group. The probability
91
+ split is consistently `{real fact: 0.76-0.82, "none": 0.18-0.24}` — one fact clearly
92
+ wins every time. Per the branching rule this called for a wording pass, not a grouping
93
+ fix, so one was tried: adding a duration clause (`"...sustained for 6 minutes"`, a
94
+ legitimate real-alert field). It didn't help — 5 fresh calls came back *worse* (0.44-
95
+ 0.54, 2/5 dropped to `low_confidence` entirely) — so it was reverted. `goals.py`
96
+ currently matches exactly what the final 100-row run used.
97
+
98
+ Worth noting separately: **Jev's reported `confidence` and the raw top-choice
99
+ probability are not the same number.** `load_spike/alert` consistently shows
100
+ `probabilities: {real_fact: ~0.78, none: ~0.22}` alongside a *separately reported*
101
+ `confidence` of only 0.52-0.64 for that same choice — a real, non-obvious gap between
102
+ "how the model split its answer across options" and "how sure it says it is," which
103
+ `gate.py` correctly reads from (`incident_confidence` is the latter, not the former),
104
+ but is worth remembering when eyeballing a trace: the probability dict alone can look
105
+ more confident than the number that actually gates the decision.
106
+
107
+ `load_spike` remains stuck at `escalate` under `alert` goals. This is a genuine open
108
+ item, not a thresholds question — the sweep showed no T_incident value in the tested
109
+ range was unsafe for it, because it's never confident enough to reach `applied` at all,
110
+ let alone cross into unsafe territory.
111
+
112
+ ## The limitation: this data cannot locate a threshold floor
113
+
114
+ The sweep (`tune_thresholds.py results/eval_20261018T042738Z`, T_incident and T_class
115
+ both from 0.50 to 0.95) found **unsafe = 0 at every single tested combination**. That
116
+ is not the same as "0.8/0.8 is correct" — it means the straddle fix, the route/status
117
+ check, and the rules-vs-Jev agreement check are currently carrying all of the measured
118
+ safety margin in this dataset, and none of the 100 rows in the final run ever gave the
119
+ confidence thresholds themselves a chance to be the thing that mattered. Any specific
120
+ number between 0.5 and 0.95 would have been equally "validated" by this data, which
121
+ means none of them actually are. Moving the thresholds now, in either direction, would
122
+ be a guess dressed up as a measurement. The honest statement is: **thresholds are
123
+ unchanged because nothing in ~280 real calls across two sessions ever produced a
124
+ genuinely unsafe decision to calibrate against** — which is a good sign for the gate's
125
+ other safeguards, and a real gap in what this evaluation can currently prove about the
126
+ confidence numbers specifically. More scenarios with a wider range of genuine ambiguity
127
+ (not just the deliberately-ambiguous `missing_input`/`two_incidents`) would be needed
128
+ before trusting any specific threshold value.
129
+
130
+ ## Test commands
131
+
132
+ ```bash
133
+ cd "/Users/ruchiradnaik/code/self-maintaining-apis/jevbrief-eval"
134
+ source .venv/bin/activate
135
+
136
+ python3 -m pytest tests/ -v # 54 tests, zero cost
137
+ python3 report.py results/eval_20261018T042738Z # the final 100-row run
138
+ python3 tune_thresholds.py results/eval_20261018T042738Z # reproduce the sweep, zero cost
139
+ ```
140
+
141
+ ## Addendum (2026-10-18): final-outcome layer and the infrastructure relaxation
142
+
143
+ - `gate.py` now lets a low-confidence incident pick through to `recommend` when Jev and the
144
+ rules both say `infrastructure` (reason carries a `[low confidence: ...]` flag). Re-scored
145
+ on `results/eval_20261018T042738Z`: gate actions went from 93 escalate / 5 proceed /
146
+ 2 recommend to 90 / 5 / 5 (three `slow_query` rows), vs-ground-truth overcautious 38 -> 35,
147
+ unsafe still 0. The "recommend in all 10 alert runs" figure above predates this and was not
148
+ reproduced on the live run on 2026-10-18 (0.64 incident confidence -> escalate).
149
+ - `outcome.py` adds `no_action` and `unexplained_errors_ticket` as final actions.
150
+ - **Known hazard of `no_action` -- FIXED (see IMPLEMENTATION.md, Phase 9):** the paragraph below describes the
151
+ original problem. An alert-triggered run can no longer end as `no_action` (it ends `alert_unexplained`), the
152
+ safety net now counts WARN and slow-response records, and a manual run over a log with any of them escalates.
153
+ Original text: **Known hazard of `no_action`:** the safety net only counts ERROR/5xx records, so a log whose
154
+ only problem is a WARN-level slow query looks "clean" to it. In the 100-row run all 5
155
+ `slow_query/misleading` rows (goal about the wrong route; 4 real slow-query WARNs in the log)
156
+ get Jev `none` at 0.80-0.90 and therefore `no_action` -- previously `escalate`. See the
157
+ open decision in the session notes before relying on `no_action`.
@@ -0,0 +1,156 @@
1
+ # FINDINGS_V1
2
+
3
+ > The first sections below are the record of the original build (plan-then-approve flow) and the first real run; they are kept as written. The second live run, the design changes it led to, and the empty tables for the final measurements are at the end of the file.
4
+
5
+ Status of the V1 task list (`V1_TASKS.md`) after the build session of 2026-10-18/19. Written to be honest about what has
6
+ and has not been proven, not to look finished.
7
+
8
+ ## What was built and is covered by tests (863 tests, no network, no real budget)
9
+
10
+ | Task | State |
11
+ |---|---|
12
+ | T0 verify | The three decision fixes and the UI polish are on `main` (PRs #2, #3). **Real-Jev check, 1 call (throwaway DB):** `replay null_field` -> incident pick confidence **0.96**, class `bug_fix` confidence **0.82**, threshold used **`T_class_agreed` 0.7** (rules' specific rule agrees with Jev), final action **proceed**; pre-flight passed (check 3 `unknown`: no stack trace in the frozen fixture). Evaluation report on the latest 40-row run: **unsafe 0 / 34**. Real `data/pipeline.db`: backed up to `data/backups/`, then the 36 manual replay runs (and their 1,116 node events) were deleted; 107 watcher + 19 webhook runs remain. |
13
+ | T1 | `demo-api` repo created locally and on GitHub (private); app, traffic generator (`traffic/`), own compose, CI, pre-commit, `scripts/reset-bugs.sh`, `selfheal.yml`. Pipeline loads the app via `appsource.py` (`PIPELINE_APP_CHECKOUT`); frozen copies of app + traffic under `eval/demo_app_frozen/`; compose runs only the pipeline. |
14
+ | T1b | Stack traces: `stacktrace.py` + demo-api `feature/otel-stacktraces`. Check 3 names the field (`email`) and shows `app.py:182`. |
15
+ | T2 | `TicketSink` / `GitHubIssuesSink` / `FakeTicketSink`, one issue per incident, idempotent create, 30-minute pinned status comment, redacted untrusted samples, `ticket` graph node. |
16
+ | T3 | `GET /api/evidence/{issue}` (workspace token), deterministic endpoint brief, `schema_version` + sha256 stored. |
17
+ | T4 | Plan schema, `one_obvious_fix`, `plan_id`, plan / design-decision comments, every failure path escalates. |
18
+ | T5 | Approval rules (permission, stale plan, wrong option, `/reject`), approvals recorded (who, when, plan_id, option) and shown on the incident. |
19
+ | T6 | Implement step with scope enforcement in code, local commit only. |
20
+ | T7 | `selfheal guard`: spec diff, tests, red-to-green, scope, replay. |
21
+ | T8 | `Reviewer` interface, Claude reviewer (blind then compare), verdict downgrade in code. |
22
+ | T9 | Draft-PR gate, GitHub webhook (signature-verified) -> resolve / return to owner, fix-flow panel, live `plan`/`implement`/`guardrails`/`reviewer`/`draft_pr` nodes. |
23
+ | T10 | `pip install selfheal`, `selfheal init|doctor|serve|guard`; `init` dogfooded on demo-api at `v0-buggy` (output identical to the hand-written `selfheal.yml`). |
24
+ | T11 | An **offline** end-to-end test (`tests/test_end_to_end_offline.py`) drives incident -> issue -> evidence -> plan -> approval -> implement -> guardrails -> reviewer -> draft PR -> merge webhook -> resolved with fakes. |
25
+
26
+ ## What is NOT proven yet
27
+
28
+ - **No real end-to-end run.** The "twice in a row from a reset" demo, screenshots, timings and cost per fix need real
29
+ secrets (Claude auth, a GitHub token for the pipeline, the workspace token, a bot login) and a running service. None of these were available
30
+ to the build session, so there are **no timings or costs** to report. Do not read the offline test as a measurement.
31
+ - **The workflows have never run on GitHub.** The Claude Code CLI flags in `ClaudeCodeRunner` (`--allowed-tools`,
32
+ `--max-budget-usd`, `--output-format json`), `oasdiff breaking --fail-on WARN`, the Schemathesis invocation and the
33
+ `npm`/`go install` steps are untested against the real tools.
34
+ - **Nothing is merged.** `demo-api` `main` (tagged `v0-buggy`) and `feature/otel-stacktraces` are on GitHub; the pipeline work is split into seven stacked PRs for you to review and merge in order.
35
+ - **Required check on PRs.** Events caused by the default `GITHUB_TOKEN` do not trigger workflows, so every workflow acts as one fine-grained PAT (`SELFHEAL_GITHUB_TOKEN`; exact permissions in `SETUP.md`), which also makes the bot identity the PAT's user. Not yet exercised on GitHub.
36
+ - **Local connectivity.** Workflows run on a self-hosted runner labelled `selfheal-local` on this Mac, and the pipeline polls GitHub every 60 s instead of relying on webhooks (the webhook handler is kept). `selfheal doctor` checks the runner, the poller/webhook, the secrets, the required `guard` check and the evidence API.
37
+ - **`v0-buggy` predates the stack-trace instrumentation.** `reset-bugs.sh` now resets to `v0-buggy-otel` (tagged after `feature/otel-stacktraces` is merged); `v0-buggy` is unchanged.
38
+ - **Namespace and pins.** `pip install selfheal` uses `>=` ranges, not the pinned `constraints.txt`.
39
+ - **Real DB.** The 36 manual replay runs were removed (backup in `data/backups/`); their trace and slice files under `data/` were left in place.
40
+
41
+ ## Things worth deciding
42
+
43
+ 1. Grant the `workflow` scope, then push `demo-api` and open the PRs (the branches are ready locally).
44
+ 2. Provide the secrets listed by `selfheal init`, then run the demo twice and fill in timings and cost here.
45
+ 3. Whether the bot identity should be a GitHub App rather than your user (the plan workflow trusts only `SELFHEAL_BOT_LOGIN`).
46
+
47
+ ## The first real run (2026-10-19 onward): what worked and what broke
48
+
49
+ The first run on real GitHub, a real self-hosted runner, real Claude and real Jev against `demo-api` at `v0-buggy-otel`.
50
+ Everything below was found by that run, not by the offline tests (which is the point of the section above: they could not
51
+ see these). The symptoms are summarised from the commit and PR descriptions, not from saved run logs. Each item lists the symptom, the cause and the fix; PR numbers refer to `ruchiradnaik/self-maintaining-apis`.
52
+
53
+ ### What worked
54
+
55
+ - Detection to decision on real telemetry: the watcher found the `/users/{user_id}` null-field incident, Jev picked and
56
+ classified it, the gate said `proceed`, and the pre-flight named the field from the stack trace.
57
+ - The ticket flow end to end up to a human-approved plan: issue created with the evidence link, plan comment from the bot,
58
+ `/approve` read from a person with write access, the implement step committing only to the approved files.
59
+ - The safety layers held: the bot could not approve, the Claude steps ran in the podman sandbox, the guardrails ran in
60
+ the network-less container, and the scope check refused anything outside the plan. Nothing unsafe reached a PR.
61
+ - The guard, once fixed (below), told a real fix from a failing one using its per-check table on the issue.
62
+
63
+ ### Problems the run exposed, and the fixes
64
+
65
+ | # | Problem (symptom) | Cause | Fix |
66
+ |---|---|---|---|
67
+ | 1 | The plan comment was ignored as untrusted / the draft PR did not trigger the required check. | The PAT belonged to a different GitHub account than `SELFHEAL_BOT_LOGIN`, and only the bot's plan comments are trusted. | `selfheal doctor` now calls `GET /user` with `SELFHEAL_GITHUB_TOKEN` and fails unless the real login equals `SELFHEAL_BOT_LOGIN` (and the bot differs from the approver). SETUP.md says the PAT is created while signed in as the bot (PR #13). |
68
+ | 2 | An issue was created and no plan ever appeared. | The issue was created with three labels, which fired three `labeled` events in one concurrency group; GitHub cancelled the `selfheal:plan` run in favour of a later irrelevant one. | The pipeline creates the issue with informational labels, then adds `selfheal:plan` in a separate call. The plan and implement concurrency groups are keyed on whether the event is the trigger: only the trigger shares `selfheal-<plan\|implement>-<issue>-run`, every other event gets a unique group and cannot cancel it. Issue bodies also say "incident 4" instead of "#4" (GitHub autolinked it to issue 4) (PR #13). |
69
+ | 3 | Workflow jobs hung forever on the self-hosted Mac. | `actions/setup-python` runs `sudo installer` and waits for a password; `PATH` was unreliable (conda base was the first interpreter). | Workflows no longer use setup-python or `PATH`. They run only `"$SELFHEAL_BIN"`, the CLI from a dedicated runtime venv that `selfheal runner-install` builds from a commit on `origin/main`; a job fails in its first step if the variable is unset, relative, missing or broken (PR #13, `f9488dd`). |
70
+ | 4 | `/approve` was silently ignored. | GitHub's editor added leading whitespace / zero-width characters; the strict parser saw a different first line. | The approval is read after stripping whitespace and zero-width characters, still strictly (first line, nothing else on it). The workflow starts on `contains('/approve')` because expressions cannot trim, and a near-miss gets one reply with the reason and the exact syntax (`b73ace2`). |
71
+ | 5 | Guard failed a correct fix on the spec diff. | oasdiff's output was read as text, and "No changes detected" was treated as a breaking change. | `spec_diff` parses `--format json` and fails only on actual breaking entries; "No changes detected" is a pass. oasdiff is pinned to v1.33.0 and taken from `OASDIFF_BIN` (`b73ace2`). |
72
+ | 6 | Guard blamed the fix for failures that were already on `main` (an undocumented 404, a KeyError on one POST). | The Schemathesis replay was absolute. | It now runs the same command, seed (20261019) and settings on the base and on the branch, keys failures by (operation, check), and fails only on new ones, plus a 5xx on a touched operation (`--touched-policy`). Pre-existing failures are informational in the report, issue comment and PR body (`b73ace2`). |
73
+ | 7 | The reviewer flagged the approved design as an unexpected change. | It was shown the diff and the plan summary but not the option the human actually approved, nor the guardrail results. | The reviewer now receives the full approved option (summary, behaviours, spec change, new test, files) verbatim plus the guard table, and plan, implement and guard share one scope rule, including the exact `new_test_file` the option names (`c6dd25b`, `4e5e704`). |
74
+ | 8 | 42 issues for 4 incidents after the laptop slept. | The watcher resolved every incident while the machine slept; the next detection reopened it as a new episode, and the issue marker included the episode. | One incident = one issue (PR #16): a stable episode-free `<!-- selfheal-key:... -->` marker with GitHub as the source of truth; one status comment edited in place, new comments only for meaningful changes (24 h limit); a regression grace period (30 min, `PIPELINE_TICKET_REGRESSION_GRACE_S`, or sooner on `POST /api/deploy-finished`, which `scripts/deploy.sh` calls) before a recurrence reopens the same issue; an issue a person closed is muted; poll gaps restart the quiet clock instead of mass-resolving. `selfheal tickets dedupe [--close]` lists and, after confirming, closes the leftovers. |
75
+ | 9 | The guard's oasdiff check suddenly started failing on a different version. | Older workflows ran `go install oasdiff@latest` into `~/go/bin`, overwriting the pinned binary. | No workflow installs anything now; `tests/test_workflow_templates.py` fails if a template runs `go install` or uses `@latest`. Install the pin into `~/.selfheal/bin` (SETUP.md); doctor checks it and flags workflows that differ from the current template (`c6dd25b`). |
76
+ | 10 | The pipeline image failed to build / run: an OpenTelemetry dependency conflict. | pip resolved the pipeline's packages and the customer app's `opentelemetry-instrumentation-fastapi` in separate calls, and pip only warns about conflicts. | One `pip install` call with both constraint files, then `pip check`, so the build fails on any conflict (PR #19, `5c3f22d`). `dedupe` also reads the repo from `--dir` / `selfheal.yml` there. |
77
+ | 11 | `selfheal tickets dedupe` listed duplicates but `--close` found none (or the reverse). | List and close ran separate discoveries, and GitHub intermittently returned empty issue lists. | They share one discovery that retries around empty lists, and closing a duplicate never mutes the incident (PR #20, `65e1126`). |
78
+
79
+ Related hardening from the same run: the guard skips any `selfheal/*` branch that is not `selfheal/<number>` opened by the
80
+ bot instead of crashing on the integer parse (PR #18), and `runner-install` refuses a versioned Homebrew `/Cellar/` interpreter.
81
+
82
+ ## The second live run (W0): what broke in App mode
83
+
84
+ Found by the owner's run against `demo-api` with `github_auth: app` (the `selfhealapi-bot` GitHub App), fixed in PR #36. As in the first run the symptoms are from the PR description and the plan, not from saved logs, and no numbers are claimed.
85
+
86
+ | # | Problem (symptom) | Cause | Fix |
87
+ |---|---|---|---|
88
+ | 12 | App mode silently stayed in PAT mode: everything worked, but as the PAT user. | Most likely: the service reads `selfheal.yml` (and so `github_auth`) once, at startup, from the checkout the container mounts, and the file was pulled after the container started, so the running service never saw `github_auth: app` (the code path was checked by tests; the live log was not kept). Nothing said which mode was running. | The service logs one startup line (`github auth: app (App <id>, bot <login>, key <path>)` or `github auth: pat (<login>)`); `GET /api/status` reports `auth_mode`, `bot_login`, `app_id`; `selfheal doctor` compares the config with the running service and fails on a mismatch ("running auth"), telling you to restart the pipeline when it is stale. Tests spy that in app mode the sink, poller, labeler, dedupe and `/api/github-token` never touch `SELFHEAL_GITHUB_TOKEN`, and the env fallback in `github.py` is gone in app mode. |
89
+ | 13 | The guard workflow skipped the bot's PR. | The guard compared the PR author's login (or the actor) with the accepted bot logins only, and the template gave it no numeric id; a login spelled differently from the configured one (an App's `<slug>[bot]`) made it skip, and the skip did not say what had been compared. | `actions.guard_scope` / `bot_actor` accept the configured login, the App's `<slug>[bot]`, the legacy PAT user, **or the numeric user id** (`SELFHEAL_BOT_USER_ID`, or looked up for the App bot); the template passes `github.actor_id`; the skip notice and the log say what was compared (actor, id, accepted logins and ids). |
90
+ | 14 | Incidents were never analysed: the hourly cap of 6 was used up by the first incidents and the rest starved. | `max_runs_per_hour` was 6 (compose hardcoded `PIPELINE_MAX_RUNS_PER_HOUR=6`, cooldown 600); an incident over the budget had no queue and the next one to be picked was the one seen last (`last_seen DESC`). | `max_runs_per_hour` and `cooldown_s` are read from `selfheal.yml` (env overrides; compose no longer hardcodes them; the demo values are in `SETUP.md`). An incident waiting for budget is explicit: `queued_position` in `/api/status` and `/api/incidents`, "Queued (#N, waiting for run budget)" in the UI, and the oldest unanalysed incident starts first when budget frees. |
91
+ | 15 | Incidents on routes that fail 2-4% of requests flapped: resolved, reopened as a new episode, resolved again. | One global quiet time (window + 10 s) resolved an incident whenever the gap between two failures was longer than that. | Resolution is route-aware: after the quiet window the route must also have served at least `resolve_min_successes` (30, scaled up by failure rate) successful requests since the last failure, or stay quiet for `resolve_quiet_low_rate_s` (1800, scaled by the route's mean gap, capped). Tests use 2-4% failure-rate fixtures. |
92
+ | 16 | Wording told people to "restart the runner service", which does not exist when the runner is started with `./run.sh`. | Text in `setup_cmds.py`, `runtime.py` and the workflow templates assumed the service form. | It now says to restart the runner (the service, or Ctrl+C then `./run.sh`); the asserting test was updated. |
93
+
94
+ ## Design decisions made after the first runs
95
+
96
+ These are decisions, not results; each is implemented and covered by offline tests only. None has been measured in a live run yet.
97
+
98
+ | Decision | PR | Why | What it costs |
99
+ |---|---|---|---|
100
+ | **No plan approval for fixes; server-side scope instead.** The fix intent is written inside the implement session and never posted for approval; the limits come from the pipeline: stack-trace files of the failing route from the issue evidence, exactly one new test, file and line caps, never protected / workflow / dependency / spec / docstring / existing-test files. The agent may narrow the limits, never widen them. The reviewer also checks the diff against the issue evidence. | #39 | The plan step was the slowest human wait and the plan approval was a weak gate (a person approving text, not code). A limit the agent cannot set protects more than a plan the agent wrote. | Ambiguous fixes are decided by the agent (most conservative reading, the other listed as "Alternative behaviour" in the PR); the human gate moves to the draft PR. The plan flow stays for a future feature flow (`flow: feature`). |
101
+ | **Trigger safety.** `selfheal:implement` is added by the bot, alone, in its own API call after the issue and its informational labels exist; implement starts only for that label from the bot (login or id); the concurrency group is keyed on whether the event is the trigger. | #39 | Repeats the lesson of finding 2 (three labels, three events, one cancelled run) and keeps comments and people's labels from starting a run. | |
102
+ | **Retry with the failure fed back** (`max_implement_retries`, default 2, hard cap 5) plus the repo's own pre-commit hooks as a guardrail. | #40 | A failed guardrail is usually a small mistake the agent can fix when told which check failed. | More Claude cost per incident (one session per attempt, recorded per attempt). The fix intent, the scope, the guard plan and every check are identical across attempts; nothing is loosened. |
103
+ | **`/revise` on the PR** (a write-access human, `max_revisions` 3, hard cap 5). Guidance is trusted intent from that person; everything else stays untrusted. | #41 | A person reviewing the draft PR can steer it without a plan step or editing code. | The same scope, guardrails and reviewer apply to the whole PR; a revision that leaves scope is discarded. |
104
+ | **Setup wizard** (`selfheal setup`, owner or self-host mode, `--dry-run`, App manifest flow). | #38 | The manual setup took many ordered steps across GitHub, podman and the runner; finding 12 showed that configuration drift is easy to miss. | Built on `main`, not on this stack; not yet run live (`WIZARD_TEST.md` lists what was and was not checked). |
105
+ | **Regression policy.** A merged fix that fails again after the grace period (or a deploy signal) reopens the **same issue** with a "Regressed" comment for a person; no automatic re-fix. | existing behaviour, documented | One incident is one issue; an automatic second fix of a fix a person just merged is the loop the design avoids. | A regression needs a person to look at it. |
106
+
107
+ ## Not measured yet (live-run items)
108
+
109
+ Nothing in this section has been observed: the final live run on the stacked branches has not happened when this file was written. In particular the following are
110
+ unverified: App mode actually running as `selfhealapi-bot[bot]` end to end, the guard accepting the bot by id on a real PR, the run-budget queue under real traffic, flapping routes no longer flapping, a real retry, a real `/revise`, and the wizard on a real repo. `eval/score_demo.py` now scores the direct flow (outcomes `auto_fix`, `declined`, `escalate`, `recommendation`; retries and revisions from the export) but has only run on synthetic data.
111
+
112
+ ## Results (empty until the final run)
113
+
114
+ Fill from `eval/score_demo.py`, `selfheal export --format csv` (or `GET /api/export?format=json`) after the owner's run. Nothing here is an estimate.
115
+
116
+ Scores (the many-bug run):
117
+
118
+ | Outcome | Expected (`eval/demo_bugs.yml`) | Observed | Notes |
119
+ |---|---|---|---|
120
+ | Auto-fixed and merged correctly | _TBD_ | _TBD_ | |
121
+ | Escalated | _TBD_ | _TBD_ | |
122
+ | Recommendations | _TBD_ | _TBD_ | |
123
+ | Wrongly auto-fixed (must be 0) | 0 | _TBD_ | |
124
+
125
+ Timings (seconds from detection in the export; median / max over the incidents that reached the stage):
126
+
127
+ | Stage (export column) | Median | Max | n |
128
+ |---|---|---|---|
129
+ | detected -> issue created (`issue_created_s`) | _TBD_ | _TBD_ | _TBD_ |
130
+ | issue created -> fix intent recorded (`fix_intent_s`) | _TBD_ | _TBD_ | _TBD_ |
131
+ | implement done (`implement_done_s`) | _TBD_ | _TBD_ | _TBD_ |
132
+ | guardrails done (`guard_done_s`) | _TBD_ | _TBD_ | _TBD_ |
133
+ | reviewer done (`reviewer_done_s`) | _TBD_ | _TBD_ | _TBD_ |
134
+ | PR opened (`pr_opened_s`) | _TBD_ | _TBD_ | _TBD_ |
135
+ | merged by a person (`merged_s`) | _TBD_ | _TBD_ | _TBD_ |
136
+ | resolved (`resolved_s`) | _TBD_ | _TBD_ | _TBD_ |
137
+
138
+ Attempts and revisions (from `implement_attempts`, `failed_attempts`, `revisions`):
139
+
140
+ | Measure | Value |
141
+ |---|---|
142
+ | Incidents that needed a retry | _TBD_ |
143
+ | Attempts per incident (median / max) | _TBD_ |
144
+ | Incidents that failed after the last attempt | _TBD_ |
145
+ | `/revise` rounds used | _TBD_ |
146
+
147
+ Cost (USD from the Claude CLI's `total_cost_usd`, per the export's `implement_*`, `review_*`, `total_*` columns and `revision_cost_usd`):
148
+
149
+ | Claude step | Cost per session (USD) | Tokens in / out | Sessions |
150
+ |---|---|---|---|
151
+ | implement (all attempts) | _TBD_ | _TBD_ | _TBD_ |
152
+ | reviewer | _TBD_ | _TBD_ | _TBD_ |
153
+ | `/revise` | _TBD_ | _TBD_ | _TBD_ |
154
+ | **Total per incident** | _TBD_ | _TBD_ | _TBD_ |
155
+
156
+ (The export also has `plan_*` columns; they are empty for fixes now that there is no plan step.)