ergane-cli 0.6.0__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 (233) hide show
  1. ergane_cli-0.6.0/.agents/skills/away-mode/SKILL.md +220 -0
  2. ergane_cli-0.6.0/.agents/skills/build-metrics/SKILL.md +141 -0
  3. ergane_cli-0.6.0/.agents/skills/build-metrics/reference/baseline-2026-08-19.md +118 -0
  4. ergane_cli-0.6.0/.agents/skills/build-metrics/reference/current-baseline.md +1 -0
  5. ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/commit_sizes.py +166 -0
  6. ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/loc.py +138 -0
  7. ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/rework.py +324 -0
  8. ergane_cli-0.6.0/.agents/skills/escalation-triage/SKILL.md +42 -0
  9. ergane_cli-0.6.0/.agents/skills/escalation-triage/escalation_render.py +85 -0
  10. ergane_cli-0.6.0/.agents/skills/escalation-triage/fixtures/escalation.json +13 -0
  11. ergane_cli-0.6.0/.agents/skills/findings-ingest/SKILL.md +325 -0
  12. ergane_cli-0.6.0/.agents/skills/floor-status/SKILL.md +55 -0
  13. ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/attempts.json +57 -0
  14. ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/services.json +5 -0
  15. ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/status.json +18 -0
  16. ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/transcript.txt +21 -0
  17. ergane_cli-0.6.0/.agents/skills/floor-status/floor_render.py +134 -0
  18. ergane_cli-0.6.0/.agents/skills/spec-html/SKILL.md +78 -0
  19. ergane_cli-0.6.0/.agents/skills/spec-html/render.py +640 -0
  20. ergane_cli-0.6.0/.claude/skills/away-mode/SKILL.md +220 -0
  21. ergane_cli-0.6.0/.claude/skills/build-metrics/SKILL.md +141 -0
  22. ergane_cli-0.6.0/.claude/skills/build-metrics/reference/baseline-2026-08-19.md +118 -0
  23. ergane_cli-0.6.0/.claude/skills/build-metrics/reference/current-baseline.md +1 -0
  24. ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/commit_sizes.py +166 -0
  25. ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/loc.py +138 -0
  26. ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/rework.py +324 -0
  27. ergane_cli-0.6.0/.claude/skills/escalation-triage/SKILL.md +42 -0
  28. ergane_cli-0.6.0/.claude/skills/escalation-triage/escalation_render.py +85 -0
  29. ergane_cli-0.6.0/.claude/skills/escalation-triage/fixtures/escalation.json +13 -0
  30. ergane_cli-0.6.0/.claude/skills/findings-ingest/SKILL.md +325 -0
  31. ergane_cli-0.6.0/.claude/skills/floor-status/SKILL.md +55 -0
  32. ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/attempts.json +57 -0
  33. ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/services.json +5 -0
  34. ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/status.json +18 -0
  35. ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/transcript.txt +21 -0
  36. ergane_cli-0.6.0/.claude/skills/floor-status/floor_render.py +134 -0
  37. ergane_cli-0.6.0/.claude/skills/spec-html/SKILL.md +78 -0
  38. ergane_cli-0.6.0/.claude/skills/spec-html/render.py +640 -0
  39. ergane_cli-0.6.0/.gitignore +36 -0
  40. ergane_cli-0.6.0/LICENSE +202 -0
  41. ergane_cli-0.6.0/NOTICE +27 -0
  42. ergane_cli-0.6.0/PKG-INFO +219 -0
  43. ergane_cli-0.6.0/README.md +191 -0
  44. ergane_cli-0.6.0/container/compose.demo.yaml +262 -0
  45. ergane_cli-0.6.0/container/compose.reference.yaml +56 -0
  46. ergane_cli-0.6.0/container/ergane-bwrap.apparmor +14 -0
  47. ergane_cli-0.6.0/container/ergane-engine.profile +46 -0
  48. ergane_cli-0.6.0/container/ergane-install-answer.demo.toml +30 -0
  49. ergane_cli-0.6.0/container/gateway-config.yaml +49 -0
  50. ergane_cli-0.6.0/container/personas.demo.yaml +71 -0
  51. ergane_cli-0.6.0/container/seccomp-ergane.json +846 -0
  52. ergane_cli-0.6.0/default_floor.md +29 -0
  53. ergane_cli-0.6.0/factory/__init__.py +0 -0
  54. ergane_cli-0.6.0/factory/activities/__init__.py +0 -0
  55. ergane_cli-0.6.0/factory/activities/agent_activities.py +1252 -0
  56. ergane_cli-0.6.0/factory/activities/attestation_activities.py +116 -0
  57. ergane_cli-0.6.0/factory/activities/merge_activities.py +814 -0
  58. ergane_cli-0.6.0/factory/activities/notify_activities.py +1118 -0
  59. ergane_cli-0.6.0/factory/activities/roadmap_activities.py +986 -0
  60. ergane_cli-0.6.0/factory/activities/usage_activities.py +1023 -0
  61. ergane_cli-0.6.0/factory/activities/verify_activities.py +747 -0
  62. ergane_cli-0.6.0/factory/attestation/__init__.py +30 -0
  63. ergane_cli-0.6.0/factory/attestation/archive.py +865 -0
  64. ergane_cli-0.6.0/factory/attestation/attachments.py +235 -0
  65. ergane_cli-0.6.0/factory/attestation/journal.py +516 -0
  66. ergane_cli-0.6.0/factory/attestation/models.py +120 -0
  67. ergane_cli-0.6.0/factory/attestation/report.py +125 -0
  68. ergane_cli-0.6.0/factory/attestation/rollup.py +191 -0
  69. ergane_cli-0.6.0/factory/attestation/usage.py +183 -0
  70. ergane_cli-0.6.0/factory/cli/__init__.py +0 -0
  71. ergane_cli-0.6.0/factory/cli/completion.py +150 -0
  72. ergane_cli-0.6.0/factory/cli/doctor.py +619 -0
  73. ergane_cli-0.6.0/factory/cli/env.py +247 -0
  74. ergane_cli-0.6.0/factory/cli/errors.py +77 -0
  75. ergane_cli-0.6.0/factory/cli/init.py +2368 -0
  76. ergane_cli-0.6.0/factory/cli/install.py +2275 -0
  77. ergane_cli-0.6.0/factory/cli/landing.py +229 -0
  78. ergane_cli-0.6.0/factory/cli/main.py +209 -0
  79. ergane_cli-0.6.0/factory/cli/nouns/__init__.py +59 -0
  80. ergane_cli-0.6.0/factory/cli/nouns/answer.py +134 -0
  81. ergane_cli-0.6.0/factory/cli/nouns/attestation.py +322 -0
  82. ergane_cli-0.6.0/factory/cli/nouns/build.py +2986 -0
  83. ergane_cli-0.6.0/factory/cli/nouns/completion.py +13 -0
  84. ergane_cli-0.6.0/factory/cli/nouns/doctor.py +13 -0
  85. ergane_cli-0.6.0/factory/cli/nouns/engine.py +74 -0
  86. ergane_cli-0.6.0/factory/cli/nouns/env.py +13 -0
  87. ergane_cli-0.6.0/factory/cli/nouns/escalations.py +93 -0
  88. ergane_cli-0.6.0/factory/cli/nouns/findings.py +13 -0
  89. ergane_cli-0.6.0/factory/cli/nouns/init.py +13 -0
  90. ergane_cli-0.6.0/factory/cli/nouns/install.py +182 -0
  91. ergane_cli-0.6.0/factory/cli/nouns/repo.py +13 -0
  92. ergane_cli-0.6.0/factory/cli/nouns/roadmap.py +13 -0
  93. ergane_cli-0.6.0/factory/cli/nouns/skills.py +53 -0
  94. ergane_cli-0.6.0/factory/cli/nouns/spec.py +556 -0
  95. ergane_cli-0.6.0/factory/cli/nouns/status.py +18 -0
  96. ergane_cli-0.6.0/factory/cli/nouns/uninstall.py +13 -0
  97. ergane_cli-0.6.0/factory/cli/nouns/usage.py +13 -0
  98. ergane_cli-0.6.0/factory/cli/nouns/worker.py +162 -0
  99. ergane_cli-0.6.0/factory/cli/promotion.py +108 -0
  100. ergane_cli-0.6.0/factory/cli/repo.py +664 -0
  101. ergane_cli-0.6.0/factory/cli/repo_export.py +282 -0
  102. ergane_cli-0.6.0/factory/cli/roadmap.py +514 -0
  103. ergane_cli-0.6.0/factory/cli/skills.py +1348 -0
  104. ergane_cli-0.6.0/factory/cli/status.py +1000 -0
  105. ergane_cli-0.6.0/factory/cli/uninstall.py +1261 -0
  106. ergane_cli-0.6.0/factory/cli/usage.py +129 -0
  107. ergane_cli-0.6.0/factory/config.py +485 -0
  108. ergane_cli-0.6.0/factory/constitution.py +72 -0
  109. ergane_cli-0.6.0/factory/controlplane/__init__.py +0 -0
  110. ergane_cli-0.6.0/factory/controlplane/canary/__init__.py +0 -0
  111. ergane_cli-0.6.0/factory/controlplane/canary/diff.py +49 -0
  112. ergane_cli-0.6.0/factory/controlplane/canary/probe.py +288 -0
  113. ergane_cli-0.6.0/factory/controlplane/config.py +950 -0
  114. ergane_cli-0.6.0/factory/controlplane/resolve.py +434 -0
  115. ergane_cli-0.6.0/factory/controlplane/verify.py +1236 -0
  116. ergane_cli-0.6.0/factory/discovery/__init__.py +3 -0
  117. ergane_cli-0.6.0/factory/discovery/llm_enrichment.py +334 -0
  118. ergane_cli-0.6.0/factory/discovery/llm_scanner.py +176 -0
  119. ergane_cli-0.6.0/factory/discovery/proposal.py +354 -0
  120. ergane_cli-0.6.0/factory/doctor/__init__.py +0 -0
  121. ergane_cli-0.6.0/factory/doctor/cli.py +484 -0
  122. ergane_cli-0.6.0/factory/doctor/models.py +228 -0
  123. ergane_cli-0.6.0/factory/doctor/probes.py +634 -0
  124. ergane_cli-0.6.0/factory/doctor/scaffold.py +426 -0
  125. ergane_cli-0.6.0/factory/doctor/store.py +652 -0
  126. ergane_cli-0.6.0/factory/doctor/triage.py +1014 -0
  127. ergane_cli-0.6.0/factory/env.py +131 -0
  128. ergane_cli-0.6.0/factory/escalation/__init__.py +0 -0
  129. ergane_cli-0.6.0/factory/escalation/client.py +173 -0
  130. ergane_cli-0.6.0/factory/escalation/question.py +164 -0
  131. ergane_cli-0.6.0/factory/escalation/workflow.py +516 -0
  132. ergane_cli-0.6.0/factory/locking.py +85 -0
  133. ergane_cli-0.6.0/factory/mergequeue/__init__.py +21 -0
  134. ergane_cli-0.6.0/factory/mergequeue/classify.py +133 -0
  135. ergane_cli-0.6.0/factory/mergequeue/forge.py +442 -0
  136. ergane_cli-0.6.0/factory/mergequeue/gh.py +895 -0
  137. ergane_cli-0.6.0/factory/mergequeue/github_forge.py +398 -0
  138. ergane_cli-0.6.0/factory/mergequeue/governance.py +107 -0
  139. ergane_cli-0.6.0/factory/mergequeue/merge_queue_ruleset.json +32 -0
  140. ergane_cli-0.6.0/factory/mergequeue/messages.py +141 -0
  141. ergane_cli-0.6.0/factory/mergequeue/models.py +440 -0
  142. ergane_cli-0.6.0/factory/mergequeue/onboard.py +925 -0
  143. ergane_cli-0.6.0/factory/mergequeue/rejection.py +87 -0
  144. ergane_cli-0.6.0/factory/mergequeue/reset.py +129 -0
  145. ergane_cli-0.6.0/factory/mergequeue/wiring.py +611 -0
  146. ergane_cli-0.6.0/factory/notify/__init__.py +0 -0
  147. ergane_cli-0.6.0/factory/notify/adapter.py +264 -0
  148. ergane_cli-0.6.0/factory/notify/messages.py +693 -0
  149. ergane_cli-0.6.0/factory/notify/null_adapter.py +50 -0
  150. ergane_cli-0.6.0/factory/notify/redact.py +251 -0
  151. ergane_cli-0.6.0/factory/notify/service.py +1059 -0
  152. ergane_cli-0.6.0/factory/notify/webhook.py +171 -0
  153. ergane_cli-0.6.0/factory/registry.py +583 -0
  154. ergane_cli-0.6.0/factory/roadmap/__init__.py +8 -0
  155. ergane_cli-0.6.0/factory/roadmap/cli.py +125 -0
  156. ergane_cli-0.6.0/factory/roadmap/discovery.py +610 -0
  157. ergane_cli-0.6.0/factory/roadmap/models.py +657 -0
  158. ergane_cli-0.6.0/factory/roadmap/schedule.py +377 -0
  159. ergane_cli-0.6.0/factory/roadmap/workflow.py +1674 -0
  160. ergane_cli-0.6.0/factory/spec/__init__.py +28 -0
  161. ergane_cli-0.6.0/factory/spec/anchors.py +460 -0
  162. ergane_cli-0.6.0/factory/spec/composition.py +277 -0
  163. ergane_cli-0.6.0/factory/spec/evidence.py +488 -0
  164. ergane_cli-0.6.0/factory/spec/layers.py +318 -0
  165. ergane_cli-0.6.0/factory/spec/report.py +84 -0
  166. ergane_cli-0.6.0/factory/stack_packs/agnostic.yaml +34 -0
  167. ergane_cli-0.6.0/factory/stack_packs/node.yaml +26 -0
  168. ergane_cli-0.6.0/factory/stack_packs/python.yaml +25 -0
  169. ergane_cli-0.6.0/factory/stack_packs.py +351 -0
  170. ergane_cli-0.6.0/factory/supervision/__init__.py +0 -0
  171. ergane_cli-0.6.0/factory/supervision/alert.py +239 -0
  172. ergane_cli-0.6.0/factory/supervision/container_engine.py +287 -0
  173. ergane_cli-0.6.0/factory/supervision/container_manifest.py +636 -0
  174. ergane_cli-0.6.0/factory/supervision/container_profile.py +220 -0
  175. ergane_cli-0.6.0/factory/supervision/container_project.py +751 -0
  176. ergane_cli-0.6.0/factory/supervision/container_supervisor.py +660 -0
  177. ergane_cli-0.6.0/factory/supervision/demo_driver.py +798 -0
  178. ergane_cli-0.6.0/factory/supervision/deploy.py +898 -0
  179. ergane_cli-0.6.0/factory/supervision/engine_identity.py +154 -0
  180. ergane_cli-0.6.0/factory/supervision/engine_upgrade.py +369 -0
  181. ergane_cli-0.6.0/factory/supervision/probe.py +519 -0
  182. ergane_cli-0.6.0/factory/supervision/temporal_server.py +134 -0
  183. ergane_cli-0.6.0/factory/supervision/units.py +1491 -0
  184. ergane_cli-0.6.0/factory/usage/__init__.py +0 -0
  185. ergane_cli-0.6.0/factory/usage/aggregate.py +113 -0
  186. ergane_cli-0.6.0/factory/usage/cli.py +233 -0
  187. ergane_cli-0.6.0/factory/usage/codex_evidence.py +78 -0
  188. ergane_cli-0.6.0/factory/usage/ledger.py +536 -0
  189. ergane_cli-0.6.0/factory/usage/litellm_client.py +471 -0
  190. ergane_cli-0.6.0/factory/usage/models.py +210 -0
  191. ergane_cli-0.6.0/factory/usage/reconcile.py +76 -0
  192. ergane_cli-0.6.0/factory/usage/runner.py +218 -0
  193. ergane_cli-0.6.0/factory/verify/__init__.py +0 -0
  194. ergane_cli-0.6.0/factory/verify/artifact_capture.py +328 -0
  195. ergane_cli-0.6.0/factory/verify/criteria.py +488 -0
  196. ergane_cli-0.6.0/factory/verify/diffbounds.py +224 -0
  197. ergane_cli-0.6.0/factory/verify/diffcheck.py +571 -0
  198. ergane_cli-0.6.0/factory/verify/factory_yaml.py +1474 -0
  199. ergane_cli-0.6.0/factory/verify/gate_annotation.py +137 -0
  200. ergane_cli-0.6.0/factory/verify/gates.py +1930 -0
  201. ergane_cli-0.6.0/factory/verify/git_evidence.py +84 -0
  202. ergane_cli-0.6.0/factory/verify/judge.py +1104 -0
  203. ergane_cli-0.6.0/factory/verify/ladder.py +513 -0
  204. ergane_cli-0.6.0/factory/verify/models.py +1531 -0
  205. ergane_cli-0.6.0/factory/verify/question.py +199 -0
  206. ergane_cli-0.6.0/factory/verify/remediation.py +264 -0
  207. ergane_cli-0.6.0/factory/verify/sandbox_remedy.py +59 -0
  208. ergane_cli-0.6.0/factory/verify/store.py +2076 -0
  209. ergane_cli-0.6.0/factory/verify/toolchain.py +701 -0
  210. ergane_cli-0.6.0/factory/verify/worktree_snapshot.py +167 -0
  211. ergane_cli-0.6.0/factory/versioning.py +185 -0
  212. ergane_cli-0.6.0/factory/worker.py +390 -0
  213. ergane_cli-0.6.0/factory/workgraph/__init__.py +0 -0
  214. ergane_cli-0.6.0/factory/workgraph/adapter.py +2681 -0
  215. ergane_cli-0.6.0/factory/workgraph/cli.py +991 -0
  216. ergane_cli-0.6.0/factory/workgraph/codex_credential.py +1276 -0
  217. ergane_cli-0.6.0/factory/workgraph/codex_events.py +462 -0
  218. ergane_cli-0.6.0/factory/workgraph/confinement.py +143 -0
  219. ergane_cli-0.6.0/factory/workgraph/contention.py +355 -0
  220. ergane_cli-0.6.0/factory/workgraph/credential_status.py +399 -0
  221. ergane_cli-0.6.0/factory/workgraph/delta.py +294 -0
  222. ergane_cli-0.6.0/factory/workgraph/derive.py +717 -0
  223. ergane_cli-0.6.0/factory/workgraph/detector.py +713 -0
  224. ergane_cli-0.6.0/factory/workgraph/governance.py +42 -0
  225. ergane_cli-0.6.0/factory/workgraph/landed.py +592 -0
  226. ergane_cli-0.6.0/factory/workgraph/models.py +757 -0
  227. ergane_cli-0.6.0/factory/workgraph/preflight.py +971 -0
  228. ergane_cli-0.6.0/factory/workgraph/prompt.py +1000 -0
  229. ergane_cli-0.6.0/factory/workgraph/qualification.py +168 -0
  230. ergane_cli-0.6.0/factory/workgraph/workflow.py +5108 -0
  231. ergane_cli-0.6.0/factory/workgraph/worktree.py +2154 -0
  232. ergane_cli-0.6.0/personas.example.yaml +100 -0
  233. ergane_cli-0.6.0/pyproject.toml +128 -0
@@ -0,0 +1,220 @@
1
+ ---
2
+ name: "away-mode"
3
+ description: "Put this session on a self-paced loop that keeps the factory floor moving while the operator is away: land what is verified, attest what is complete, dispatch what is ready, answer what agents ask, and park anything that needs a human. Use when leaving the keyboard, going to bed, or asking the session to babysit the floor. `/away-mode off` ends it."
4
+ compatibility: "Requires the ergane CLI on PATH via scripts/ergane-env.sh, gh authenticated, a checkout of this repository, and the ergane-worker/-temporal/-bridge user units running"
5
+ metadata:
6
+ author: "operator session, 2026-08-17"
7
+ user-invocable: true
8
+ disable-model-invocation: true
9
+ ---
10
+
11
+ # Away mode
12
+
13
+ The operator has left. Nothing else changes about how this repository works —
14
+ the same gates, the same queue, the same constitution — but one thing changes
15
+ about **you**: there is no longer anyone to catch a bad call before it costs a
16
+ night. So the whole skill reduces to one asymmetry.
17
+
18
+ > **A parked node costs a wait. A wrong call costs the night.**
19
+ > When the two are close, park.
20
+
21
+ That is not timidity. It is the measured trade: on 2026-08-16 the factory lost
22
+ three verified nodes, and every one of them was lost in the landing path by an
23
+ automated actor being confident — not by an agent writing bad code.
24
+
25
+ ## Turning it on and off
26
+
27
+ - `/away-mode` — start the loop.
28
+ - `/away-mode <note>` — start it with an extra standing constraint for tonight
29
+ (`/away-mode don't touch 032`, `/away-mode back by 7am`). Treat the note as
30
+ binding and repeat it back in the first tick's log line.
31
+ - `/away-mode off` — stop. Call `ScheduleWakeup({stop: true})`, `TaskStop` every
32
+ monitor you armed, and write a closing entry to the away log.
33
+
34
+ On the very first tick, before anything else, say in one short message what you
35
+ are about to do and what you will not do without them. Then work.
36
+
37
+ ## The mechanism
38
+
39
+ This is a self-paced loop, not a cron job.
40
+
41
+ 1. Run **one tick** (below).
42
+ 2. Append a line to the away log.
43
+ 3. As the **last action of the turn**, call `ScheduleWakeup` with
44
+ `prompt: "/away-mode"` (plus tonight's note, verbatim, if there was one) so
45
+ the next firing re-enters this skill.
46
+ - `delaySeconds`: **1200–1800** when something is genuinely in flight and a
47
+ `<task-notification>` will wake you anyway. Shorter only when you are
48
+ polling something the harness cannot notify you about — a merge queue that
49
+ moves in minutes, a CI run you timed. Never poll a background task you
50
+ started; you get woken when it finishes.
51
+ - `noop: true` when the tick changed nothing. Long quiet stretches are the
52
+ normal state of a healthy floor and should collapse in the operator's view.
53
+ - `reason`: what you are waiting on, specifically. "watching #181 through the
54
+ merge group" beats "checking again".
55
+ 4. Stop when the floor is empty **and** everything left needs a human. Say so,
56
+ `PushNotification` the one-line outcome, and stop. Do not idle-tick against
57
+ an empty floor for hours; that is noise wearing the costume of diligence.
58
+
59
+ ## One tick
60
+
61
+ Cheapest first, and **land before you dispatch** — a landed story unblocks the
62
+ next node's merge edge, so landing is what makes dispatch legal.
63
+
64
+ ```bash
65
+ eval "$(scripts/ergane-env.sh)" # NOT `source` — it emits export lines
66
+ git fetch origin --quiet
67
+ uv run ergane status specs # roadmap, epics, ready queue, drafts, pace
68
+ ```
69
+
70
+ ### 1. Land what is verified
71
+
72
+ Read the arm state and the queue **together** — neither column means anything
73
+ alone. `/floor-status` carries the full table; the one row that needs action is
74
+ `auto=false` **and not in the queue**, which is a dropped arm and is fixed with a
75
+ bare `gh pr merge <n>`. Every other combination is the queue working.
76
+
77
+ Then verify the landing **on the branch**, by content:
78
+
79
+ ```bash
80
+ git fetch origin --quiet && git log origin/ergane-buildout -1 --format='%h %s'
81
+ uv run ergane spec landed specs/<dir> --default-branch ergane-buildout
82
+ ```
83
+
84
+ **A pull request's `merged` flag can be true while the branch never moved.**
85
+ That is not a hypothesis — PR #176 reported merged on 2026-08-16, the file it
86
+ added 404'd on the branch, and the story had to be recovered by cherry-pick. A
87
+ merged flag is a claim. The branch head is the fact.
88
+
89
+ `--default-branch ergane-buildout` is not optional; `ergane spec landed` scans
90
+ `main`, and the factory does not land there.
91
+
92
+ ### 2. Attest what is complete
93
+
94
+ When every story of a spec is on `ergane-buildout` by ancestry *and* content,
95
+ flip its frontmatter to `state: landed` with the convention already in the tree
96
+ (`specs/043-runtime-root-integrity/spec.md` is the model): the date, then each
97
+ story's landing SHA and PR number. Validate, branch, PR, arm.
98
+
99
+ Attestation is not bookkeeping — `ergane status` reads it to decide readiness,
100
+ so an unattested finished epic silently blocks whatever depended on it.
101
+
102
+ ### 3. Answer what the agents ask
103
+
104
+ ```bash
105
+ uv run ergane build answer <epic> # pending questions, or answer one
106
+ uv run ergane escalations list # what is waiting on a human
107
+ ```
108
+
109
+ Questions from agents are yours to answer; that is the whole point of the
110
+ operator channel, and an unanswered question dead-waits its window and then
111
+ pages the operator anyway.
112
+
113
+ **Escalations are not.** Never press an escalation button on the operator's
114
+ behalf. Silence is a decision they are entitled to make, and expiry is part of
115
+ the contract. Log the escalation, `PushNotification` it if it is time-critical,
116
+ and leave it.
117
+
118
+ ### 4. Dispatch what is ready
119
+
120
+ Only from `ergane status`'s **dispatchable** list. Re-derive the delta *at
121
+ dispatch time*, never reuse a remainder file from an earlier tick — the baseline
122
+ moved when you landed something, and a stale remainder re-runs landed stories.
123
+
124
+ ```bash
125
+ uv run ergane spec derive specs/<dir> --target-repo "$PWD" --delta \
126
+ -o specs/<dir>/workgraph-remainder.json
127
+ uv run ergane build start specs/<dir>/workgraph-remainder.json
128
+ ```
129
+
130
+ If dispatch fails on a forge blip — a 503, a timeout, a read-after-write miss —
131
+ **retry on a bounded loop rather than concluding anything.** GitHub's transient
132
+ failures are minutes long; a dispatch that failed four times in a row succeeded
133
+ 80 seconds later once the retry loop was doing the timing instead of a human.
134
+
135
+ If Ollama returns **429**, stop dispatching and back off a full hour. Do not
136
+ retry into a rate limit; you will only extend it.
137
+
138
+ ### 5. Watch what is in flight
139
+
140
+ ```bash
141
+ uv run ergane build status <epic-id>
142
+ git -C "$wt" log --oneline origin/ergane-buildout..HEAD | wc -l
143
+ ```
144
+
145
+ A node that has produced no commit in well over the measured build range
146
+ (24–84 minutes, median ~55, on 2026-08-16) is a stall, not slowness.
147
+
148
+ **Do not trust the newest attempt directory to be the live attempt.** Read
149
+ `ATTEMPT_ARCHIVE` from the running bwrap process instead. A stale `attempt-4`
150
+ log from a killed morning run once produced a confident, entirely false report
151
+ that an agent had done work and never committed it.
152
+
153
+ ### 6. File what you learned
154
+
155
+ Anything that broke and is not already in the ledger:
156
+
157
+ ```bash
158
+ uv run ergane findings report --key <category>/<slug> --category <cat> \
159
+ --severity warning --summary "..." --notes "<reproduction, not narration>"
160
+ ```
161
+
162
+ A finding with a reproduction in it is worth a spec later. A finding that
163
+ describes a feeling is worth nothing. Findings *rising* during a hard night is
164
+ the healthy signal — a flat ledger through heavy running means you stopped
165
+ looking.
166
+
167
+ ### 7. Log the tick
168
+
169
+ ```bash
170
+ printf '%s %s\n' "$(TZ=America/Chicago date +'%Y-%m-%d %-I:%M %p CT')" "<what changed, or 'quiet'>" \
171
+ >> ~/ergane-ops/away-log.md
172
+ ```
173
+
174
+ One line. The operator reads this file to reconstruct the night in one scroll;
175
+ it is the only durable record that this loop ran at all.
176
+
177
+ ## Park list — never, while away
178
+
179
+ Each of these is here because doing it unattended cost something real.
180
+
181
+ | Do not | Because |
182
+ | --- | --- |
183
+ | Delete anything — remote refs, worktrees, store rows, branches | On 2026-08-14 an agent's `rm -rf .factory` destroyed three stores and both node worktrees; recovery took a restic snapshot and half a day |
184
+ | Terminate a workflow beyond a failed node's own retry ladder | A kill skips the sweep, and the survivors outlive it |
185
+ | Force-push anywhere outside a node's own branch | |
186
+ | Touch live systemd units, or dispatch a story that provisions the host | 042/US3 installs a Temporal unit on the machine this factory runs on |
187
+ | Modify factory code while an attempt is in flight | The worker imports it live |
188
+ | Restart the worker mid-attempt — and never with `nohup` | `systemctl --user restart ergane-worker.service`, between attempts, only |
189
+ | Flip a spec's state the operator deliberately held | The hold note names its release condition; if the condition needs a human, so does the flip |
190
+ | Widen a story's scope to make it pass | |
191
+ | Press an escalation button | Expiry is part of the contract |
192
+ | Retain to cross-session memory | Unattended runs are read-only against memory: recall yes, retain no |
193
+
194
+ When you hit one of these, do everything else in the tick, then write the parked
195
+ item into the log **and** send it as a `PushNotification` if it is blocking. The
196
+ operator wakes up to a decision queue, not a mystery.
197
+
198
+ ## Judgement rules that do not fit a table
199
+
200
+ - **A green suite is evidence, not proof.** A fully green run has shipped a
201
+ command that could not start. If a fix matters, prove it by control or
202
+ mutation — break the thing on purpose and watch the test go red.
203
+ - **The merge-group build is the gate.** A pull request's own green check tests
204
+ the branch, not the speculative merge, and those are different trees.
205
+ - **Report what you ran, not what you were told.** An agent's report is a claim.
206
+ So is a judge verdict. So is a merged flag.
207
+ - **Correct yourself in one line and move on.** You will be wrong about
208
+ something at 3 AM. Say what is true now; do not write an incident report about
209
+ your own reasoning.
210
+ - **If a lesson would change how an implementer writes code, it belongs in that
211
+ spec's plan as a named trap** — not in your head, and not in a memory file.
212
+ Nothing dispatches a memory. The only path from what you learned to what an
213
+ agent obeys runs through refinement.
214
+
215
+ ## The morning message
216
+
217
+ When the operator returns, or when you stop, lead with what they have to decide,
218
+ not with what you did. Parked items first, then losses with their causes, then
219
+ landings. Timestamp it in Central. If the honest summary is "quiet, four stories
220
+ landed, nothing needs you" — say exactly that and stop there.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: "build-metrics"
3
+ description: "Measure what the factory has actually produced: lines of code by language and module, commit-size distribution with outliers trimmed, and the story rework rate with its trend over time. Use when asked how big the codebase is, how large a typical change is, how often stories need a second attempt, or whether the factory is getting better."
4
+ compatibility: "Requires a checkout of this repository, python3, git, and a stable local cloc executable. Reads .ergane/*.db read-only; honors ERGANE_ROOT (with FACTORY_ROOT as the legacy alias); does not need the worker or Temporal running."
5
+ metadata:
6
+ author: "operator session"
7
+ user-invocable: true
8
+ disable-model-invocation: false
9
+ ---
10
+
11
+ # Build metrics
12
+
13
+ Three families of measurement, each with a script under `scripts/`. Run only
14
+ what was asked for — the rework analysis is the expensive-to-interpret one and
15
+ nobody wants a LOC table when they asked about attempt counts.
16
+
17
+ | Question | Script |
18
+ | --- | --- |
19
+ | How big is this, by language and by module? | `scripts/loc.py` |
20
+ | How large is a typical commit? Does anything ever get deleted? | `scripts/commit_sizes.py` |
21
+ | How often does a story need reworking, and is that improving? | `scripts/rework.py` |
22
+
23
+ All three take an optional repo path and default to the current directory:
24
+
25
+ ```bash
26
+ python3 .agents/skills/build-metrics/scripts/rework.py
27
+ ```
28
+
29
+ `ergane` is **not on PATH** — anything shelling out to the CLI needs
30
+ `uv run ergane`, after `eval "$(scripts/ergane-env.sh)"`. The scripts here
31
+ deliberately avoid the CLI and read git and the SQLite stores directly, so they
32
+ work with the worker down.
33
+
34
+ ## The traps, which are most of the value
35
+
36
+ Every one of these produced a wrong number first.
37
+
38
+ ### Outliers can carry the churn
39
+
40
+ Raw commit-size means are summaries of the tail as well as the typical change.
41
+ **Report both and say which you mean.** The script's Tukey fences and trimmed
42
+ rows are the comparison; do not quote an unqualified mean as "typical."
43
+
44
+ ### Machine-generated recordings can dominate LOC
45
+
46
+ Machine-generated fixtures are measured by `scripts/loc.py` as their own rows.
47
+ Keep that row visible in the answer; do not fold it into an authored-code total.
48
+
49
+ ### The insertion:deletion ratio measures youth, not discipline
50
+
51
+ Repo-wide insertion:deletion measures youth, not discipline. The
52
+ growth-adjusted buckets by commits touching a file are the comparison. **Quote
53
+ the most-revisited bucket.** `scripts/commit_sizes.py` prints it.
54
+
55
+ ### Rework has two honest definitions
56
+
57
+ - **Verification rework** — the story's first attempt was verified `FAIL`.
58
+ - **Dispatch rework** — the story was dispatched more than once, for any reason.
59
+
60
+ The second definition includes stories whose first attempt died before
61
+ verification could judge it: `agent_error`, `timeout`, `killed`, `question`.
62
+ Those absolutely are rework — the story was built twice — and the verification
63
+ store cannot see them because nothing was ever verified. **Lead with the
64
+ dispatch number.** Give the verification number as the narrower "the gates
65
+ rejected it" figure.
66
+
67
+ ### Two dispatches can share every old key field
68
+
69
+ Group verification attempts by the recorded dispatch before collapsing to a
70
+ story. Dispatch identity comes only from recorded evidence that carries it; an
71
+ old `(epic, node, attempt, persona)` key and a timestamp window do not make a
72
+ second dispatch.
73
+
74
+ ### Unknown is not zero
75
+
76
+ Missing tokens, dollars, runner, route, model, or landing data stay unknown.
77
+ Keep measured subtotals beside the rows that make them partial, legacy, or
78
+ unknown, and preserve accounting provenance even when one field happens to be
79
+ complete. Optional cache and request counters can remain unknown even when token
80
+ totals are complete.
81
+
82
+ ### Empty input is valid
83
+
84
+ An empty ledger produces an empty report, not an exception. Percentages and
85
+ coverage have explicit no-denominator paths.
86
+
87
+ ### `usage_records` has ~2 rows per attempt, not 1
88
+
89
+ One row per persona: `implementer`, `judge`, and `debugger` when the ladder
90
+ climbs. Always count `distinct attempt` per `(epic_id, node_id)`. Dividing row
91
+ counts by story counts overstates rework by roughly 2×.
92
+
93
+ ### The stores do not cover everything that landed
94
+
95
+ `scripts/rework.py` re-runs this cross-check every time and prints the coverage
96
+ percentage or names the absence as unknown. **Say the coverage out loud in the
97
+ report.** Report the estimate and refuse to call it a census.
98
+
99
+ ### Daily buckets are noise at this volume
100
+
101
+ Small periods have small denominators — one story can make a day look like
102
+ 100% rework. Use **weekly** buckets and a **rolling 20-story window** ordered
103
+ chronologically, which is what the script emits. A rolling window over stories
104
+ rather than days is the one that shows a learning curve if there is one,
105
+ because it holds the denominator fixed.
106
+
107
+ ### Read the stores read-only, always
108
+
109
+ The helpers resolve `.ergane/` first and honor the legacy `.factory/` name. Use
110
+ `--runtime-root` for a temporary store. This is the live store in normal use. A
111
+ skill that reports on the factory must not be a skill that writes to it.
112
+
113
+ ### LOC has no network bootstrap
114
+
115
+ `scripts/loc.py` uses a stable local `cloc` executable or reports the
116
+ capability unavailable. It does not fetch executable code from a remote branch.
117
+ Its scratch output lives in a run-unique temporary directory.
118
+
119
+ ## What to conclude, not just what to print
120
+
121
+ **Pair the rework trend with the findings ledger.** Rework rate is the outcome
122
+ metric for the whole detect-and-promote loop; `ergane findings list` is the
123
+ input side. Report the current rework trend beside the current findings trend.
124
+ That pairing is the report's actual finding; the tables are supporting evidence.
125
+
126
+ **Separate growth-phase artifacts from structural ones.** Additive commit
127
+ ratio and a findings backlog that outruns resolution can be artifacts of a
128
+ young repository. What does not correct itself is anything that compounds with
129
+ dispatch volume: test-code authorship, gate count as Goodhart surface, and
130
+ unknown quantities silently collapsed to zero. If asked to editorialise, sort
131
+ the criticisms by that axis rather than by severity.
132
+
133
+ **A flat trend is a finding, not a null result.** Name the period and the
134
+ denominator, then say plainly when there is no improvement.
135
+
136
+ ## Baseline
137
+
138
+ `reference/baseline-2026-08-19.md` holds the full numbers as measured on
139
+ 2026-08-19. It is a historical snapshot, not the default report. Re-measure
140
+ rather than quoting it — but do report which way a number moved, and by how
141
+ much.
@@ -0,0 +1,118 @@
1
+ # Baseline — 2026-08-19
2
+
3
+ Measured on `ergane-buildout` at `8206721`. Re-measure rather than quoting
4
+ these; they are here so a later run can say which way a number moved.
5
+
6
+ ## Size
7
+
8
+ 842 git-tracked files, **200,990 lines of code**. Net of the ten machine-recorded
9
+ Temporal replay histories under `tests/fixtures/replay-032/` (59,630 lines),
10
+ authored total is **141,360**.
11
+
12
+ | Language | Files | Code | Blank | Comment |
13
+ | --- | ---: | ---: | ---: | ---: |
14
+ | Python | 269 | 80,025 | 25,661 | 36,598 |
15
+ | JSON | 95 | 64,108 | 0 | 0 |
16
+ | Markdown | 387 | 54,386 | 12,623 | 135 |
17
+ | Bourne Shell | 18 | 1,387 | 211 | 359 |
18
+ | YAML | 59 | 534 | 46 | 265 |
19
+ | Text / SQL / TOML / JS | 14 | 550 | 92 | 151 |
20
+
21
+ | Module | Files | Code | % |
22
+ | --- | ---: | ---: | ---: |
23
+ | `tests/` | 267 | 117,045 | 58.2% |
24
+ | `specs/` | 311 | 48,428 | 24.1% |
25
+ | `factory/` (production) | 104 | 25,121 | 12.5% |
26
+ | `.agents/skills/` | 107 | 3,192 | 1.6% |
27
+ | `.claude/skills/` | 12 | 2,220 | 1.1% |
28
+ | `docs/` | 6 | 2,154 | 1.1% |
29
+ | `.specify/` | 19 | 1,603 | 0.8% |
30
+ | repo root | 9 | 955 | 0.5% |
31
+ | `scripts/` + `ralph/` | 4 | 166 | 0.1% |
32
+ | `.github/` | 3 | 106 | 0.1% |
33
+
34
+ Largest production packages: `workgraph` 5,415 · `cli` 5,350 · `verify` 3,429 ·
35
+ `activities` 1,868 · `mergequeue` 1,602 · `controlplane` 1,474.
36
+
37
+ Ratios: test:production Python **2.19 : 1** · comment density in `factory/`
38
+ **54%** · prose:code **0.68 : 1**.
39
+
40
+ ## Commit size
41
+
42
+ 404 non-merge commits, 3 merge commits.
43
+
44
+ | Cut | n | Mean | Median | SD | Median files |
45
+ | --- | ---: | ---: | ---: | ---: | ---: |
46
+ | Raw | 404 | 928 | 284 | 5,147 | 3 |
47
+ | **Tukey 1.5×IQR** | 380 | **359** | 254 | 374 | 3 |
48
+ | 5% trimmed both tails | 364 | 393 | 284 | 396 | 3 |
49
+ | 10% trimmed both tails | 324 | 354 | 284 | 318 | 3 |
50
+
51
+ 24 commits dropped, all high-side; the lower fence was −847.
52
+
53
+ | Group (trimmed) | n | Mean | Median | Median files | ins:del |
54
+ | --- | ---: | ---: | ---: | ---: | ---: |
55
+ | Factory node landings | 143 | 690 | 594 | 5 | 23:1 |
56
+ | Operator commits | 235 | 172 | 67 | 2 | 10:1 |
57
+
58
+ The three commits that eat the raw mean: `aebcb288` (60,186), `e915296e`
59
+ (59,751, the initial 350-file import), `a10bea88` (59,640, the replay capture).
60
+
61
+ Churn by path class: specs 32.6% · tests 29.5% · fixtures 16.9% ·
62
+ production 16.5% · docs 3.1% · other 1.5%.
63
+
64
+ Delete ratio by revisit count (Python under `factory/` + `tests/`):
65
+
66
+ | Touches | Files | Ins | Del | ins:del |
67
+ | --- | ---: | ---: | ---: | ---: |
68
+ | 1 (never revisited) | 116 | 31,292 | 0 | ∞ |
69
+ | 2–3 | 77 | 36,426 | 717 | 50.8:1 |
70
+ | 4–6 | 40 | 21,606 | 775 | 27.9:1 |
71
+ | 7–12 | 31 | 30,921 | 1,560 | 19.8:1 |
72
+ | **13+** | 16 | 27,252 | 2,281 | **11.9:1** |
73
+
74
+ ## Rework
75
+
76
+ Store window 2026-08-06 → 2026-08-20. All verification rows were form `PHASE`.
77
+
78
+ - **Dispatch rework: 43 / 117 = 36.8%** — the headline.
79
+ - **Verification rework: 27 / 116 = 23.3%** — the narrower "gates said no" figure.
80
+ - Mean attempts per story **1.69**.
81
+
82
+ | Landed on | Stories | % |
83
+ | --- | ---: | ---: |
84
+ | attempt 1 | 89 | 76.7% |
85
+ | attempt 2 | 14 | 12.1% |
86
+ | attempt 3 | 8 | 6.9% |
87
+ | never passed | 5 | 4.3% |
88
+
89
+ Terminations across 363 usage rows: `completed` 341 (93.9%), `agent_error` 16,
90
+ `timeout` 3, `question` 2, `killed` 1. Personas: implementer 183, judge 164,
91
+ debugger 16.
92
+
93
+ ### Trend — flat
94
+
95
+ | Week of | Stories | Verification rework | Dispatch rework |
96
+ | --- | ---: | ---: | ---: |
97
+ | 2026-08-03 | 32 | 15.6% | 34.4% |
98
+ | 2026-08-10 | 42 | 26.2% | 40.5% |
99
+ | 2026-08-17 | 42/43 | 26.2% | 34.9% |
100
+
101
+ Rolling 20-story window oscillated between 5% and 45% with no direction. Three
102
+ weeks and 116 stories in, first-attempt success is not improving.
103
+
104
+ ### Coverage at the time
105
+
106
+ Ledger held 117 stories; git showed 148 distinct story landings; **79%
107
+ coverage**. The 45 landed-but-unmeasured stories clustered on
108
+ `033-ergane-install`, `034-ergane-init`, `040-manifest-rename`,
109
+ `041-escalation-workflow` — lost when an agent ran `rm -rf .factory` on
110
+ 2026-08-14 and the restic restore came from that day's 03:06Z snapshot. 14
111
+ ledger stories had never landed (`032`, `061`, `070`, `071`, and older killed
112
+ work).
113
+
114
+ ## Ledger state, for pairing with the trend
115
+
116
+ 242 findings: 204 open, 34 resolved, 2 promoted, 2 regressed. `agent-worktree-boundary`
117
+ appeared under 37 distinct per-story keys — one auto-generated detector firing
118
+ per story, not 37 independent observations.
@@ -0,0 +1 @@
1
+ baseline-2026-08-19.md
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """Commit-size distribution, outlier-trimmed, plus the growth-adjusted delete ratio.
3
+
4
+ Two things this exists to stop you from reporting:
5
+
6
+ 1. The raw mean. Three commits carry ~48% of this repo's churn, so the raw
7
+ mean describes those three commits and nothing else. Trimmed is the number.
8
+ 2. The repo-wide insertion:deletion ratio. It measures how young the tree is,
9
+ not whether anyone deletes code - you cannot delete from a file nobody
10
+ revisited. The by-revisit-count table is the honest version.
11
+
12
+ usage: commit_sizes.py [repo_path]
13
+ """
14
+ import collections
15
+ import datetime
16
+ import os
17
+ import re
18
+ import statistics
19
+ import subprocess
20
+ import sys
21
+
22
+ SEP = "\x01C\x01"
23
+ NODE = re.compile(r"^\d{3}-[a-z0-9-]+/us\d+")
24
+
25
+
26
+ def commits():
27
+ out = subprocess.run(
28
+ [
29
+ "git", "log", "--no-merges", "--numstat",
30
+ "--format=" + SEP + "%H%x02%ad%x02%s", "--date=short",
31
+ ],
32
+ capture_output=True, text=True,
33
+ ).stdout
34
+ got, cur = [], None
35
+ for line in out.splitlines():
36
+ if line.startswith(SEP):
37
+ if cur:
38
+ got.append(cur)
39
+ h, ad, subj = line[len(SEP):].split("\x02")
40
+ cur = {"h": h[:8], "day": ad, "subj": subj, "ins": 0, "del": 0, "files": 0, "paths": []}
41
+ continue
42
+ p = line.split("\t")
43
+ if cur is None or len(p) != 3 or p[0] == "-":
44
+ continue # blank line or binary file
45
+ cur["ins"] += int(p[0])
46
+ cur["del"] += int(p[1])
47
+ cur["files"] += 1
48
+ cur["paths"].append((p[2], int(p[0]), int(p[1])))
49
+ if cur:
50
+ got.append(cur)
51
+ for c in got:
52
+ c["churn"] = c["ins"] + c["del"]
53
+ return got
54
+
55
+
56
+ def klass(path):
57
+ if path.startswith("factory/"):
58
+ return "production"
59
+ if path.startswith("tests/fixtures/"):
60
+ return "fixtures"
61
+ if path.startswith("tests/"):
62
+ return "tests"
63
+ if path.startswith("specs/"):
64
+ return "specs"
65
+ if path.startswith("docs/") or path.endswith(".md"):
66
+ return "docs"
67
+ return "other"
68
+
69
+
70
+ def stats(label, cs):
71
+ if not cs:
72
+ return
73
+ ch = sorted(c["churn"] for c in cs)
74
+ f = [c["files"] for c in cs]
75
+ i, d = sum(c["ins"] for c in cs), sum(c["del"] for c in cs)
76
+ print(
77
+ f" {label:<34}n={len(ch):>4} mean {statistics.mean(ch):>6.0f} "
78
+ f"median {statistics.median(ch):>5.0f} sd {statistics.pstdev(ch):>6.0f} "
79
+ f"files med {statistics.median(f):>3.0f} {i / max(d, 1):>5.1f}:1"
80
+ )
81
+
82
+
83
+ def main(repo):
84
+ os.chdir(repo)
85
+ C = commits()
86
+ ch = sorted(c["churn"] for c in C)
87
+ n = len(ch)
88
+ qs = statistics.quantiles(ch, n=4, method="inclusive")
89
+ q1, q3 = qs[0], qs[2]
90
+ iqr = q3 - q1
91
+ lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
92
+
93
+ print(f"non-merge commits: {n} min {ch[0]} Q1 {q1:.0f} median {statistics.median(ch):.0f}"
94
+ f" Q3 {q3:.0f} max {ch[-1]}")
95
+ print(f"Tukey fences [{lo:.0f}, {hi:.0f}] -- note the lower fence is normally negative here,")
96
+ print("so nothing trims from the low end and 'outliers on either side' does not apply.")
97
+
98
+ print("\n=== COMMIT SIZE (churn = insertions + deletions) ===")
99
+ stats("raw (all)", C)
100
+ inf = [c for c in C if lo <= c["churn"] <= hi]
101
+ stats("Tukey 1.5*IQR <- report this", inf)
102
+ for pct in (5, 10):
103
+ k = int(n * pct / 100)
104
+ stats(f"{pct}% trimmed both tails", sorted(C, key=lambda c: c["churn"])[k : n - k])
105
+ print(f" dropped by Tukey: {sum(1 for c in C if c['churn'] > hi)} high, "
106
+ f"{sum(1 for c in C if c['churn'] < lo)} low")
107
+
108
+ print("\n=== FACTORY NODE LANDINGS vs OPERATOR COMMITS (trimmed per-group) ===")
109
+ for label, sub in (
110
+ ("node landings", [c for c in C if NODE.match(c["subj"])]),
111
+ ("operator", [c for c in C if not NODE.match(c["subj"])]),
112
+ ):
113
+ s = sorted(c["churn"] for c in sub)
114
+ sq = statistics.quantiles(s, n=4, method="inclusive")
115
+ f_lo, f_hi = sq[0] - 1.5 * (sq[2] - sq[0]), sq[2] + 1.5 * (sq[2] - sq[0])
116
+ stats(label + " (trimmed)", [c for c in sub if f_lo <= c["churn"] <= f_hi])
117
+
118
+ print("\n=== LARGEST COMMITS (the ones that eat the raw mean) ===")
119
+ for c in sorted(C, key=lambda c: -c["churn"])[:8]:
120
+ print(f" {c['day']} {c['h']} {c['churn']:>7} churn {c['files']:>4}f {c['subj'][:58]}")
121
+
122
+ print("\n=== CHURN BY PATH CLASS ===")
123
+ tot = collections.Counter()
124
+ for c in C:
125
+ for p, a, d in c["paths"]:
126
+ tot[klass(p)] += a + d
127
+ s = sum(tot.values())
128
+ for k, v in tot.most_common():
129
+ print(f" {k:<12}{v:>10,} {100 * v / s:>5.1f}%")
130
+
131
+ print("\n=== DELETE RATIO BY REVISIT COUNT (the growth-adjusted version) ===")
132
+ print("Repo-wide ins:del measures youth. This measures discipline. Quote the last row.")
133
+ live = set(subprocess.run(["git", "ls-files"], capture_output=True, text=True).stdout.split())
134
+ touch, ins, dele = collections.Counter(), collections.Counter(), collections.Counter()
135
+ for c in C:
136
+ for p, a, d in c["paths"]:
137
+ if "=>" in p:
138
+ continue # rename entry; path is unparseable
139
+ touch[p] += 1
140
+ ins[p] += a
141
+ dele[p] += d
142
+
143
+ def bucket(t):
144
+ return ("1 (never revisited)" if t == 1 else "2-3" if t <= 3
145
+ else "4-6" if t <= 6 else "7-12" if t <= 12 else "13+")
146
+
147
+ b = collections.defaultdict(lambda: [0, 0, 0])
148
+ for p, t in touch.items():
149
+ if p not in live or not p.endswith(".py"):
150
+ continue
151
+ if not (p.startswith("factory/") or p.startswith("tests/")):
152
+ continue
153
+ k = bucket(t)
154
+ b[k][0] += 1
155
+ b[k][1] += ins[p]
156
+ b[k][2] += dele[p]
157
+ print(f" {'touches':<22}{'files':>7}{'ins':>10}{'del':>9}{'ins:del':>10}")
158
+ for k in ["1 (never revisited)", "2-3", "4-6", "7-12", "13+"]:
159
+ if k not in b:
160
+ continue
161
+ f, i, d = b[k]
162
+ print(f" {k:<22}{f:>7}{i:>10,}{d:>9,}{i / max(d, 1):>9.1f}:1")
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main(sys.argv[1] if len(sys.argv) > 1 else ".")