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.
- ergane_cli-0.6.0/.agents/skills/away-mode/SKILL.md +220 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/SKILL.md +141 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/reference/baseline-2026-08-19.md +118 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/reference/current-baseline.md +1 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/commit_sizes.py +166 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/loc.py +138 -0
- ergane_cli-0.6.0/.agents/skills/build-metrics/scripts/rework.py +324 -0
- ergane_cli-0.6.0/.agents/skills/escalation-triage/SKILL.md +42 -0
- ergane_cli-0.6.0/.agents/skills/escalation-triage/escalation_render.py +85 -0
- ergane_cli-0.6.0/.agents/skills/escalation-triage/fixtures/escalation.json +13 -0
- ergane_cli-0.6.0/.agents/skills/findings-ingest/SKILL.md +325 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/SKILL.md +55 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/attempts.json +57 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/services.json +5 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/status.json +18 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/fixtures/transcript.txt +21 -0
- ergane_cli-0.6.0/.agents/skills/floor-status/floor_render.py +134 -0
- ergane_cli-0.6.0/.agents/skills/spec-html/SKILL.md +78 -0
- ergane_cli-0.6.0/.agents/skills/spec-html/render.py +640 -0
- ergane_cli-0.6.0/.claude/skills/away-mode/SKILL.md +220 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/SKILL.md +141 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/reference/baseline-2026-08-19.md +118 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/reference/current-baseline.md +1 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/commit_sizes.py +166 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/loc.py +138 -0
- ergane_cli-0.6.0/.claude/skills/build-metrics/scripts/rework.py +324 -0
- ergane_cli-0.6.0/.claude/skills/escalation-triage/SKILL.md +42 -0
- ergane_cli-0.6.0/.claude/skills/escalation-triage/escalation_render.py +85 -0
- ergane_cli-0.6.0/.claude/skills/escalation-triage/fixtures/escalation.json +13 -0
- ergane_cli-0.6.0/.claude/skills/findings-ingest/SKILL.md +325 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/SKILL.md +55 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/attempts.json +57 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/services.json +5 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/status.json +18 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/fixtures/transcript.txt +21 -0
- ergane_cli-0.6.0/.claude/skills/floor-status/floor_render.py +134 -0
- ergane_cli-0.6.0/.claude/skills/spec-html/SKILL.md +78 -0
- ergane_cli-0.6.0/.claude/skills/spec-html/render.py +640 -0
- ergane_cli-0.6.0/.gitignore +36 -0
- ergane_cli-0.6.0/LICENSE +202 -0
- ergane_cli-0.6.0/NOTICE +27 -0
- ergane_cli-0.6.0/PKG-INFO +219 -0
- ergane_cli-0.6.0/README.md +191 -0
- ergane_cli-0.6.0/container/compose.demo.yaml +262 -0
- ergane_cli-0.6.0/container/compose.reference.yaml +56 -0
- ergane_cli-0.6.0/container/ergane-bwrap.apparmor +14 -0
- ergane_cli-0.6.0/container/ergane-engine.profile +46 -0
- ergane_cli-0.6.0/container/ergane-install-answer.demo.toml +30 -0
- ergane_cli-0.6.0/container/gateway-config.yaml +49 -0
- ergane_cli-0.6.0/container/personas.demo.yaml +71 -0
- ergane_cli-0.6.0/container/seccomp-ergane.json +846 -0
- ergane_cli-0.6.0/default_floor.md +29 -0
- ergane_cli-0.6.0/factory/__init__.py +0 -0
- ergane_cli-0.6.0/factory/activities/__init__.py +0 -0
- ergane_cli-0.6.0/factory/activities/agent_activities.py +1252 -0
- ergane_cli-0.6.0/factory/activities/attestation_activities.py +116 -0
- ergane_cli-0.6.0/factory/activities/merge_activities.py +814 -0
- ergane_cli-0.6.0/factory/activities/notify_activities.py +1118 -0
- ergane_cli-0.6.0/factory/activities/roadmap_activities.py +986 -0
- ergane_cli-0.6.0/factory/activities/usage_activities.py +1023 -0
- ergane_cli-0.6.0/factory/activities/verify_activities.py +747 -0
- ergane_cli-0.6.0/factory/attestation/__init__.py +30 -0
- ergane_cli-0.6.0/factory/attestation/archive.py +865 -0
- ergane_cli-0.6.0/factory/attestation/attachments.py +235 -0
- ergane_cli-0.6.0/factory/attestation/journal.py +516 -0
- ergane_cli-0.6.0/factory/attestation/models.py +120 -0
- ergane_cli-0.6.0/factory/attestation/report.py +125 -0
- ergane_cli-0.6.0/factory/attestation/rollup.py +191 -0
- ergane_cli-0.6.0/factory/attestation/usage.py +183 -0
- ergane_cli-0.6.0/factory/cli/__init__.py +0 -0
- ergane_cli-0.6.0/factory/cli/completion.py +150 -0
- ergane_cli-0.6.0/factory/cli/doctor.py +619 -0
- ergane_cli-0.6.0/factory/cli/env.py +247 -0
- ergane_cli-0.6.0/factory/cli/errors.py +77 -0
- ergane_cli-0.6.0/factory/cli/init.py +2368 -0
- ergane_cli-0.6.0/factory/cli/install.py +2275 -0
- ergane_cli-0.6.0/factory/cli/landing.py +229 -0
- ergane_cli-0.6.0/factory/cli/main.py +209 -0
- ergane_cli-0.6.0/factory/cli/nouns/__init__.py +59 -0
- ergane_cli-0.6.0/factory/cli/nouns/answer.py +134 -0
- ergane_cli-0.6.0/factory/cli/nouns/attestation.py +322 -0
- ergane_cli-0.6.0/factory/cli/nouns/build.py +2986 -0
- ergane_cli-0.6.0/factory/cli/nouns/completion.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/doctor.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/engine.py +74 -0
- ergane_cli-0.6.0/factory/cli/nouns/env.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/escalations.py +93 -0
- ergane_cli-0.6.0/factory/cli/nouns/findings.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/init.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/install.py +182 -0
- ergane_cli-0.6.0/factory/cli/nouns/repo.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/roadmap.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/skills.py +53 -0
- ergane_cli-0.6.0/factory/cli/nouns/spec.py +556 -0
- ergane_cli-0.6.0/factory/cli/nouns/status.py +18 -0
- ergane_cli-0.6.0/factory/cli/nouns/uninstall.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/usage.py +13 -0
- ergane_cli-0.6.0/factory/cli/nouns/worker.py +162 -0
- ergane_cli-0.6.0/factory/cli/promotion.py +108 -0
- ergane_cli-0.6.0/factory/cli/repo.py +664 -0
- ergane_cli-0.6.0/factory/cli/repo_export.py +282 -0
- ergane_cli-0.6.0/factory/cli/roadmap.py +514 -0
- ergane_cli-0.6.0/factory/cli/skills.py +1348 -0
- ergane_cli-0.6.0/factory/cli/status.py +1000 -0
- ergane_cli-0.6.0/factory/cli/uninstall.py +1261 -0
- ergane_cli-0.6.0/factory/cli/usage.py +129 -0
- ergane_cli-0.6.0/factory/config.py +485 -0
- ergane_cli-0.6.0/factory/constitution.py +72 -0
- ergane_cli-0.6.0/factory/controlplane/__init__.py +0 -0
- ergane_cli-0.6.0/factory/controlplane/canary/__init__.py +0 -0
- ergane_cli-0.6.0/factory/controlplane/canary/diff.py +49 -0
- ergane_cli-0.6.0/factory/controlplane/canary/probe.py +288 -0
- ergane_cli-0.6.0/factory/controlplane/config.py +950 -0
- ergane_cli-0.6.0/factory/controlplane/resolve.py +434 -0
- ergane_cli-0.6.0/factory/controlplane/verify.py +1236 -0
- ergane_cli-0.6.0/factory/discovery/__init__.py +3 -0
- ergane_cli-0.6.0/factory/discovery/llm_enrichment.py +334 -0
- ergane_cli-0.6.0/factory/discovery/llm_scanner.py +176 -0
- ergane_cli-0.6.0/factory/discovery/proposal.py +354 -0
- ergane_cli-0.6.0/factory/doctor/__init__.py +0 -0
- ergane_cli-0.6.0/factory/doctor/cli.py +484 -0
- ergane_cli-0.6.0/factory/doctor/models.py +228 -0
- ergane_cli-0.6.0/factory/doctor/probes.py +634 -0
- ergane_cli-0.6.0/factory/doctor/scaffold.py +426 -0
- ergane_cli-0.6.0/factory/doctor/store.py +652 -0
- ergane_cli-0.6.0/factory/doctor/triage.py +1014 -0
- ergane_cli-0.6.0/factory/env.py +131 -0
- ergane_cli-0.6.0/factory/escalation/__init__.py +0 -0
- ergane_cli-0.6.0/factory/escalation/client.py +173 -0
- ergane_cli-0.6.0/factory/escalation/question.py +164 -0
- ergane_cli-0.6.0/factory/escalation/workflow.py +516 -0
- ergane_cli-0.6.0/factory/locking.py +85 -0
- ergane_cli-0.6.0/factory/mergequeue/__init__.py +21 -0
- ergane_cli-0.6.0/factory/mergequeue/classify.py +133 -0
- ergane_cli-0.6.0/factory/mergequeue/forge.py +442 -0
- ergane_cli-0.6.0/factory/mergequeue/gh.py +895 -0
- ergane_cli-0.6.0/factory/mergequeue/github_forge.py +398 -0
- ergane_cli-0.6.0/factory/mergequeue/governance.py +107 -0
- ergane_cli-0.6.0/factory/mergequeue/merge_queue_ruleset.json +32 -0
- ergane_cli-0.6.0/factory/mergequeue/messages.py +141 -0
- ergane_cli-0.6.0/factory/mergequeue/models.py +440 -0
- ergane_cli-0.6.0/factory/mergequeue/onboard.py +925 -0
- ergane_cli-0.6.0/factory/mergequeue/rejection.py +87 -0
- ergane_cli-0.6.0/factory/mergequeue/reset.py +129 -0
- ergane_cli-0.6.0/factory/mergequeue/wiring.py +611 -0
- ergane_cli-0.6.0/factory/notify/__init__.py +0 -0
- ergane_cli-0.6.0/factory/notify/adapter.py +264 -0
- ergane_cli-0.6.0/factory/notify/messages.py +693 -0
- ergane_cli-0.6.0/factory/notify/null_adapter.py +50 -0
- ergane_cli-0.6.0/factory/notify/redact.py +251 -0
- ergane_cli-0.6.0/factory/notify/service.py +1059 -0
- ergane_cli-0.6.0/factory/notify/webhook.py +171 -0
- ergane_cli-0.6.0/factory/registry.py +583 -0
- ergane_cli-0.6.0/factory/roadmap/__init__.py +8 -0
- ergane_cli-0.6.0/factory/roadmap/cli.py +125 -0
- ergane_cli-0.6.0/factory/roadmap/discovery.py +610 -0
- ergane_cli-0.6.0/factory/roadmap/models.py +657 -0
- ergane_cli-0.6.0/factory/roadmap/schedule.py +377 -0
- ergane_cli-0.6.0/factory/roadmap/workflow.py +1674 -0
- ergane_cli-0.6.0/factory/spec/__init__.py +28 -0
- ergane_cli-0.6.0/factory/spec/anchors.py +460 -0
- ergane_cli-0.6.0/factory/spec/composition.py +277 -0
- ergane_cli-0.6.0/factory/spec/evidence.py +488 -0
- ergane_cli-0.6.0/factory/spec/layers.py +318 -0
- ergane_cli-0.6.0/factory/spec/report.py +84 -0
- ergane_cli-0.6.0/factory/stack_packs/agnostic.yaml +34 -0
- ergane_cli-0.6.0/factory/stack_packs/node.yaml +26 -0
- ergane_cli-0.6.0/factory/stack_packs/python.yaml +25 -0
- ergane_cli-0.6.0/factory/stack_packs.py +351 -0
- ergane_cli-0.6.0/factory/supervision/__init__.py +0 -0
- ergane_cli-0.6.0/factory/supervision/alert.py +239 -0
- ergane_cli-0.6.0/factory/supervision/container_engine.py +287 -0
- ergane_cli-0.6.0/factory/supervision/container_manifest.py +636 -0
- ergane_cli-0.6.0/factory/supervision/container_profile.py +220 -0
- ergane_cli-0.6.0/factory/supervision/container_project.py +751 -0
- ergane_cli-0.6.0/factory/supervision/container_supervisor.py +660 -0
- ergane_cli-0.6.0/factory/supervision/demo_driver.py +798 -0
- ergane_cli-0.6.0/factory/supervision/deploy.py +898 -0
- ergane_cli-0.6.0/factory/supervision/engine_identity.py +154 -0
- ergane_cli-0.6.0/factory/supervision/engine_upgrade.py +369 -0
- ergane_cli-0.6.0/factory/supervision/probe.py +519 -0
- ergane_cli-0.6.0/factory/supervision/temporal_server.py +134 -0
- ergane_cli-0.6.0/factory/supervision/units.py +1491 -0
- ergane_cli-0.6.0/factory/usage/__init__.py +0 -0
- ergane_cli-0.6.0/factory/usage/aggregate.py +113 -0
- ergane_cli-0.6.0/factory/usage/cli.py +233 -0
- ergane_cli-0.6.0/factory/usage/codex_evidence.py +78 -0
- ergane_cli-0.6.0/factory/usage/ledger.py +536 -0
- ergane_cli-0.6.0/factory/usage/litellm_client.py +471 -0
- ergane_cli-0.6.0/factory/usage/models.py +210 -0
- ergane_cli-0.6.0/factory/usage/reconcile.py +76 -0
- ergane_cli-0.6.0/factory/usage/runner.py +218 -0
- ergane_cli-0.6.0/factory/verify/__init__.py +0 -0
- ergane_cli-0.6.0/factory/verify/artifact_capture.py +328 -0
- ergane_cli-0.6.0/factory/verify/criteria.py +488 -0
- ergane_cli-0.6.0/factory/verify/diffbounds.py +224 -0
- ergane_cli-0.6.0/factory/verify/diffcheck.py +571 -0
- ergane_cli-0.6.0/factory/verify/factory_yaml.py +1474 -0
- ergane_cli-0.6.0/factory/verify/gate_annotation.py +137 -0
- ergane_cli-0.6.0/factory/verify/gates.py +1930 -0
- ergane_cli-0.6.0/factory/verify/git_evidence.py +84 -0
- ergane_cli-0.6.0/factory/verify/judge.py +1104 -0
- ergane_cli-0.6.0/factory/verify/ladder.py +513 -0
- ergane_cli-0.6.0/factory/verify/models.py +1531 -0
- ergane_cli-0.6.0/factory/verify/question.py +199 -0
- ergane_cli-0.6.0/factory/verify/remediation.py +264 -0
- ergane_cli-0.6.0/factory/verify/sandbox_remedy.py +59 -0
- ergane_cli-0.6.0/factory/verify/store.py +2076 -0
- ergane_cli-0.6.0/factory/verify/toolchain.py +701 -0
- ergane_cli-0.6.0/factory/verify/worktree_snapshot.py +167 -0
- ergane_cli-0.6.0/factory/versioning.py +185 -0
- ergane_cli-0.6.0/factory/worker.py +390 -0
- ergane_cli-0.6.0/factory/workgraph/__init__.py +0 -0
- ergane_cli-0.6.0/factory/workgraph/adapter.py +2681 -0
- ergane_cli-0.6.0/factory/workgraph/cli.py +991 -0
- ergane_cli-0.6.0/factory/workgraph/codex_credential.py +1276 -0
- ergane_cli-0.6.0/factory/workgraph/codex_events.py +462 -0
- ergane_cli-0.6.0/factory/workgraph/confinement.py +143 -0
- ergane_cli-0.6.0/factory/workgraph/contention.py +355 -0
- ergane_cli-0.6.0/factory/workgraph/credential_status.py +399 -0
- ergane_cli-0.6.0/factory/workgraph/delta.py +294 -0
- ergane_cli-0.6.0/factory/workgraph/derive.py +717 -0
- ergane_cli-0.6.0/factory/workgraph/detector.py +713 -0
- ergane_cli-0.6.0/factory/workgraph/governance.py +42 -0
- ergane_cli-0.6.0/factory/workgraph/landed.py +592 -0
- ergane_cli-0.6.0/factory/workgraph/models.py +757 -0
- ergane_cli-0.6.0/factory/workgraph/preflight.py +971 -0
- ergane_cli-0.6.0/factory/workgraph/prompt.py +1000 -0
- ergane_cli-0.6.0/factory/workgraph/qualification.py +168 -0
- ergane_cli-0.6.0/factory/workgraph/workflow.py +5108 -0
- ergane_cli-0.6.0/factory/workgraph/worktree.py +2154 -0
- ergane_cli-0.6.0/personas.example.yaml +100 -0
- 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 ".")
|