ergane-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. ergane_cli-0.1.0.dist-info/METADATA +148 -0
  2. ergane_cli-0.1.0.dist-info/RECORD +121 -0
  3. ergane_cli-0.1.0.dist-info/WHEEL +4 -0
  4. ergane_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. ergane_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  6. ergane_cli-0.1.0.dist-info/licenses/NOTICE +27 -0
  7. factory/__init__.py +0 -0
  8. factory/activities/__init__.py +0 -0
  9. factory/activities/agent_activities.py +745 -0
  10. factory/activities/merge_activities.py +654 -0
  11. factory/activities/notify_activities.py +1078 -0
  12. factory/activities/roadmap_activities.py +577 -0
  13. factory/activities/usage_activities.py +593 -0
  14. factory/activities/verify_activities.py +617 -0
  15. factory/cli/__init__.py +0 -0
  16. factory/cli/completion.py +150 -0
  17. factory/cli/doctor.py +484 -0
  18. factory/cli/env.py +247 -0
  19. factory/cli/errors.py +77 -0
  20. factory/cli/init.py +1057 -0
  21. factory/cli/install.py +679 -0
  22. factory/cli/main.py +212 -0
  23. factory/cli/nouns/__init__.py +59 -0
  24. factory/cli/nouns/answer.py +134 -0
  25. factory/cli/nouns/build.py +1222 -0
  26. factory/cli/nouns/completion.py +13 -0
  27. factory/cli/nouns/doctor.py +13 -0
  28. factory/cli/nouns/env.py +13 -0
  29. factory/cli/nouns/escalations.py +93 -0
  30. factory/cli/nouns/findings.py +13 -0
  31. factory/cli/nouns/init.py +13 -0
  32. factory/cli/nouns/install.py +99 -0
  33. factory/cli/nouns/repo.py +13 -0
  34. factory/cli/nouns/roadmap.py +13 -0
  35. factory/cli/nouns/spec.py +511 -0
  36. factory/cli/nouns/status.py +18 -0
  37. factory/cli/nouns/usage.py +13 -0
  38. factory/cli/nouns/worker.py +69 -0
  39. factory/cli/repo.py +569 -0
  40. factory/cli/repo_export.py +282 -0
  41. factory/cli/roadmap.py +434 -0
  42. factory/cli/status.py +844 -0
  43. factory/cli/usage.py +129 -0
  44. factory/config.py +262 -0
  45. factory/controlplane/__init__.py +0 -0
  46. factory/controlplane/config.py +863 -0
  47. factory/controlplane/resolve.py +434 -0
  48. factory/controlplane/verify.py +827 -0
  49. factory/discovery/__init__.py +3 -0
  50. factory/discovery/llm_scanner.py +176 -0
  51. factory/doctor/__init__.py +0 -0
  52. factory/doctor/cli.py +392 -0
  53. factory/doctor/models.py +165 -0
  54. factory/doctor/probes.py +522 -0
  55. factory/doctor/scaffold.py +184 -0
  56. factory/doctor/store.py +449 -0
  57. factory/env.py +131 -0
  58. factory/escalation/__init__.py +0 -0
  59. factory/escalation/client.py +168 -0
  60. factory/escalation/question.py +161 -0
  61. factory/escalation/workflow.py +470 -0
  62. factory/locking.py +85 -0
  63. factory/mergequeue/__init__.py +21 -0
  64. factory/mergequeue/classify.py +133 -0
  65. factory/mergequeue/forge.py +401 -0
  66. factory/mergequeue/gh.py +484 -0
  67. factory/mergequeue/github_forge.py +349 -0
  68. factory/mergequeue/merge_queue_ruleset.json +32 -0
  69. factory/mergequeue/messages.py +141 -0
  70. factory/mergequeue/models.py +324 -0
  71. factory/mergequeue/onboard.py +537 -0
  72. factory/mergequeue/wiring.py +466 -0
  73. factory/notify/__init__.py +0 -0
  74. factory/notify/adapter.py +263 -0
  75. factory/notify/messages.py +474 -0
  76. factory/notify/service.py +809 -0
  77. factory/notify/webhook.py +151 -0
  78. factory/personas.yaml +161 -0
  79. factory/registry.py +583 -0
  80. factory/roadmap/__init__.py +8 -0
  81. factory/roadmap/cli.py +125 -0
  82. factory/roadmap/discovery.py +286 -0
  83. factory/roadmap/models.py +583 -0
  84. factory/roadmap/schedule.py +377 -0
  85. factory/roadmap/workflow.py +1364 -0
  86. factory/supervision/__init__.py +0 -0
  87. factory/supervision/alert.py +239 -0
  88. factory/supervision/probe.py +467 -0
  89. factory/supervision/temporal_server.py +121 -0
  90. factory/supervision/units.py +649 -0
  91. factory/usage/__init__.py +0 -0
  92. factory/usage/aggregate.py +113 -0
  93. factory/usage/cli.py +223 -0
  94. factory/usage/ledger.py +295 -0
  95. factory/usage/litellm_client.py +486 -0
  96. factory/usage/models.py +149 -0
  97. factory/verify/__init__.py +0 -0
  98. factory/verify/criteria.py +488 -0
  99. factory/verify/diffbounds.py +159 -0
  100. factory/verify/diffcheck.py +506 -0
  101. factory/verify/factory_yaml.py +842 -0
  102. factory/verify/gates.py +1341 -0
  103. factory/verify/judge.py +775 -0
  104. factory/verify/ladder.py +145 -0
  105. factory/verify/models.py +745 -0
  106. factory/verify/question.py +167 -0
  107. factory/verify/store.py +1366 -0
  108. factory/verify/toolchain.py +370 -0
  109. factory/worker.py +296 -0
  110. factory/workgraph/__init__.py +0 -0
  111. factory/workgraph/adapter.py +1333 -0
  112. factory/workgraph/cli.py +801 -0
  113. factory/workgraph/delta.py +278 -0
  114. factory/workgraph/derive.py +607 -0
  115. factory/workgraph/detector.py +509 -0
  116. factory/workgraph/landed.py +496 -0
  117. factory/workgraph/models.py +494 -0
  118. factory/workgraph/preflight.py +651 -0
  119. factory/workgraph/prompt.py +796 -0
  120. factory/workgraph/workflow.py +2733 -0
  121. factory/workgraph/worktree.py +1322 -0
@@ -0,0 +1,324 @@
1
+ """The merge-queue component's data model: enums, records, and the gh payload shape.
2
+
3
+ US1's whole distinction between *verified* and *merged* — FR-004 — lives in two
4
+ `StrEnum`s and one record. `QueueOutcome` is the closed vocabulary of what a
5
+ landing's queue history can say; `LandingState` is the machine the workflow
6
+ drives across a landing's life; `Landing` is that life as workflow state on a
7
+ `NodeRecord`; `PrSnapshot` is what one poll of GitHub's queue yields, and the
8
+ classifier's only input from the world.
9
+
10
+ All of it crosses a Temporal boundary through the default JSON converter, so the
11
+ discipline is the 005 models' (models.py docstring): `StrEnum`, not
12
+ `class X(str, Enum)` — only `StrEnum` survives the converter's *deserializer*,
13
+ which rebuilds a field annotated with any other str-subclass enum as a list of
14
+ one-character strings. And every record is frozen so a value that crossed an
15
+ activity boundary can never be edited in place.
16
+
17
+ `PrSnapshot.from_gh_json` is the one place the raw `gh pr view --json` payload
18
+ becomes a decision input, so it has to survive what GitHub actually sends: an
19
+ absent `autoMergeRequest` (a PR nobody enqueued is *not* queue-requested), and a
20
+ `statusCheckRollup` that mixes `CheckRun` entries (named checks with a
21
+ `conclusion`) and legacy `StatusCheckRollup` entries (named contexts with a
22
+ `state`). Failing required checks are what the classifier reads to distinguish
23
+ `CHECKS_FAILED` from `DEQUEUED_BY_HUMAN` and a pending `CLEAN` wait, so a run
24
+ whose conclusion or state is a failure must surface here by name.
25
+
26
+ `TargetRepoProfile` and `Finding` are US3's preflight surface (plan.md § US1,
27
+ T035's slice-containment note): they live in this module because they are part
28
+ of the merge-queue component's model, whether or not US1's slice exercises them.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import dataclass
34
+ from enum import StrEnum
35
+ from typing import Any, Mapping
36
+
37
+ #: 049-US3: GitHub's spelling of "the target moved under this proposal". It is
38
+ #: spelled once, here, at the boundary where a stored record is rebuilt — never
39
+ #: in the classifier, which is the whole of FR-010.
40
+ _LEGACY_CONFLICT = "DIRTY"
41
+
42
+
43
+ # US2: one failing check's evidence as the recovery prompt and escalation page
44
+ # receive it. Lives here rather than in gh.py so the model's own dataclasses can
45
+ # annotate tuples of it without creating a circular import (gh.py already imports
46
+ # PrSnapshot from this module).
47
+ @dataclass(frozen=True)
48
+ class CheckFailure:
49
+ """US2: one failing check's evidence.
50
+
51
+ `log_tail` is the last of `gh run view --log-failed` for the run id parsed
52
+ from `url`; `note` is empty when the tail was fetched, and states why it was
53
+ not fetched when `log_tail` is empty.
54
+ """
55
+
56
+ name: str
57
+ url: str
58
+ log_tail: str
59
+ note: str
60
+
61
+
62
+ # FR-004's closed vocabulary ---------------------------------------------------
63
+
64
+
65
+ class QueueOutcome(StrEnum):
66
+ """What one landing's queue history can say (plan.md § Data Model).
67
+
68
+ The interpreter's distinction between verified and merged is made with these
69
+ exact five members; a new member would change what a PASS means, so the set
70
+ is closed on purpose.
71
+ """
72
+
73
+ MERGED = "MERGED"
74
+ CHECKS_FAILED = "CHECKS_FAILED"
75
+ CONFLICT = "CONFLICT"
76
+ DEQUEUED_BY_HUMAN = "DEQUEUED_BY_HUMAN"
77
+ STALLED = "STALLED"
78
+
79
+
80
+ class LandingState(StrEnum):
81
+ """Where one landing stands: `PR_OPEN → ENQUEUED → MERGED | REJECTED | KILLED`.
82
+
83
+ `REJECTED` is the recovery-eligible rejection (checks_failed / conflict): it
84
+ may return to `ENQUEUED` after a successful recovery cycle (FR-006's bounded
85
+ cycle). `KILLED` is terminal — operator kill, dequeue-by-human, escalation
86
+ default, epic kill — and nothing leaves it.
87
+ """
88
+
89
+ PR_OPEN = "PR_OPEN"
90
+ ENQUEUED = "ENQUEUED"
91
+ MERGED = "MERGED"
92
+ REJECTED = "REJECTED"
93
+ KILLED = "KILLED"
94
+
95
+
96
+ # The records ------------------------------------------------------------------
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class ObservedOutcome:
101
+ """One entry in a landing's queue history — what the escalation quotes.
102
+
103
+ US2: `failing_checks` carries the names of the required checks the
104
+ classifier saw failing when the outcome is `CHECKS_FAILED`. It defaults to
105
+ `()` so pre-spec histories deserialize and replay without change.
106
+ """
107
+
108
+ at: str
109
+ outcome: QueueOutcome
110
+ failing_checks: tuple[str, ...] = ()
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class Landing:
115
+ """One node's landing, as workflow state on its `NodeRecord`.
116
+
117
+ `outcomes` is the queue history — every time the poll classified a terminal
118
+ (or stall) outcome, in order, so a human being paged can see the sequence
119
+ that led here. `enqueued_at` and the PR identity are set by the landing
120
+ phase; `recovery_cycles` counts how many times `REJECTED` has gone back to
121
+ `ENQUEUED` (bounded by `LandingConfig.max_recovery_cycles`, FR-006).
122
+
123
+ US2: `check_evidence` is the fetched (name, URL, log tail, note) record for
124
+ the latest `CHECKS_FAILED` outcome, used by the recovery prompt and the
125
+ escalation page. Default `()` keeps CONFLICT and pre-spec histories intact.
126
+ """
127
+
128
+ node_id: str
129
+ branch: str
130
+ pr_number: int | None = None
131
+ pr_url: str | None = None
132
+ enqueued_at: str | None = None
133
+ outcomes: tuple[ObservedOutcome, ...] = ()
134
+ recovery_cycles: int = 0
135
+ state: LandingState = LandingState.PR_OPEN
136
+ check_evidence: tuple[CheckFailure, ...] = ()
137
+ #: US3: the commit the branch was pushed with when it was last enqueued.
138
+ #: Default `None` keeps pre-spec histories replayable (FR-009).
139
+ enqueued_tip: str | None = None
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class PrSnapshot:
144
+ """What one poll of GitHub's queue saw — the classifier's only input from the world.
145
+
146
+ `state` is GitHub's PR state (`OPEN|CLOSED|MERGED`), kept as the string GitHub
147
+ sent rather than re-derived, because the classifier reads it verbatim (a
148
+ merged PR is GitHub's word, never inferred). `failing_required_checks` is the
149
+ subset of the rollup whose conclusion/state was a failure, by name; an empty
150
+ tuple is a rollup with nothing failing. `observed_at` is the workflow's clock
151
+ at the poll, so a stall is measured on the interpreter's time, not the
152
+ subprocess's.
153
+ """
154
+
155
+ state: str
156
+ is_draft: bool
157
+ auto_merge_requested: bool
158
+ merge_state_status: str
159
+ merged_at: str | None
160
+ closed_at: str | None
161
+ failing_required_checks: tuple[str, ...]
162
+ observed_at: str
163
+ #: 049-US3: the conflict fact in the *factory's* vocabulary — the target has
164
+ #: moved under this proposal and it no longer applies as it stands.
165
+ #: `merge_state_status` stays as GitHub's spelling of the same observation,
166
+ #: kept as evidence an operator can read; nothing decides from it any more
167
+ #: (FR-010). Defaulted so a pre-spec history deserializes — `__post_init__`.
168
+ in_conflict: bool = False
169
+
170
+ def __post_init__(self) -> None:
171
+ """Carry a pre-049 history's conflict across, once, at the boundary.
172
+
173
+ A record written before this spec recorded the conflict in the only
174
+ spelling there was, and histories holding one are replayed: a replay
175
+ reaching "keep polling" where the original run reached `CONFLICT` would
176
+ diverge from its own history — the defect class 032, 038 and 039 each
177
+ shipped. So the legacy spelling is read here, in the constructor, which
178
+ is also where `from_gh_json` lands every GitHub observation. The
179
+ classifier never sees it, and a forge that never heard of `DIRTY` is
180
+ unaffected.
181
+ """
182
+ if not self.in_conflict and self.merge_state_status == _LEGACY_CONFLICT:
183
+ object.__setattr__(self, "in_conflict", True)
184
+
185
+ @classmethod
186
+ def from_gh_json(cls, payload: Mapping[str, Any], *, observed_at: str) -> "PrSnapshot":
187
+ """One `gh pr view --json` payload, turned into a decision input.
188
+
189
+ `autoMergeRequest` is null for a PR nobody enqueued, and that must read as
190
+ "not requested", not as an error or as an unreadable field. The rollup
191
+ mixes named check runs and legacy status contexts; a failing one surfaces
192
+ by name, a passing or absent one does not. No value that is missing is
193
+ invented — a PR with no `mergedAt` simply has `merged_at=None`.
194
+ """
195
+ auto_merge = payload.get("autoMergeRequest")
196
+ failing = tuple(
197
+ name
198
+ for name in _failing_check_names(payload.get("statusCheckRollup") or [])
199
+ )
200
+ return cls(
201
+ state=str(payload.get("state") or ""),
202
+ is_draft=bool(payload.get("isDraft")),
203
+ auto_merge_requested=auto_merge is not None,
204
+ merge_state_status=str(payload.get("mergeStateStatus") or ""),
205
+ merged_at=_nullable_str(payload.get("mergedAt")),
206
+ closed_at=_nullable_str(payload.get("closedAt")),
207
+ failing_required_checks=failing,
208
+ observed_at=observed_at,
209
+ )
210
+
211
+
212
+ @dataclass(frozen=True)
213
+ class TargetRepoProfile:
214
+ """US3's preflight read of the target repo (plan.md § US1, T035).
215
+
216
+ `repo` is the slug exactly as `gh` reports it; `required_checks` is what the
217
+ queue will demand of a PR, `declared_gates` what the repo's own `factory.yaml`
218
+ names — the preflight compares them to decide whether a landing can ever be
219
+ enqueued. `findings` is the actionable list, and `passed` is their
220
+ conjunction: a profile that fails any finding cannot land.
221
+ """
222
+
223
+ repo: str
224
+ default_branch: str
225
+ visibility: str
226
+ queue_enabled: bool
227
+ required_checks: tuple[str, ...]
228
+ declared_gates: tuple[str, ...]
229
+ findings: tuple["Finding", ...]
230
+ passed: bool
231
+
232
+ @classmethod
233
+ def from_readiness(
234
+ cls,
235
+ *,
236
+ repo: str,
237
+ reading: Any,
238
+ policy: Any,
239
+ declared_gates: tuple[str, ...],
240
+ findings: tuple["Finding", ...],
241
+ passed: bool,
242
+ ) -> "TargetRepoProfile":
243
+ """Build the report from a forge's two readings and a decided verdict.
244
+
245
+ The three descriptive fields above keep this record's own spelling — an
246
+ operator's report and every stored payload already read that way — so
247
+ the mapping onto them belongs with the record that owns them, not in a
248
+ judgment that may never spell them (049 US2, FR-006). `reading` and
249
+ `policy` are structural: `forge.py` imports `Finding` from here.
250
+ """
251
+ return cls(
252
+ repo=repo,
253
+ default_branch=reading.default_branch,
254
+ visibility=reading.visibility,
255
+ queue_enabled=policy.gates_on_named_checks,
256
+ required_checks=tuple(policy.required_checks),
257
+ declared_gates=declared_gates,
258
+ findings=findings,
259
+ passed=passed,
260
+ )
261
+
262
+
263
+ @dataclass(frozen=True)
264
+ class Finding:
265
+ """One preflight finding (US3): which check, whether it passed, and how to fix it.
266
+
267
+ `detail` is actionable — it names what to change, not just what is wrong — so
268
+ the operator reading a preflight report can go and change it without re-deriving
269
+ the problem from a slug.
270
+ """
271
+
272
+ check: str
273
+ passed: bool
274
+ detail: str
275
+
276
+
277
+ @dataclass(frozen=True)
278
+ class LandingConfig:
279
+ """The operator's landing knobs — `EpicInput`'s new field (plan.md § US1).
280
+
281
+ Defaults are code defaults the operator overrides per epic; none of these is
282
+ a constant buried in the workflow. `merge_method` is passed verbatim to
283
+ `gh pr merge --auto --<method>` and must match a method the repo allows.
284
+ `stall_after_s` bounds how long a landing may sit queue-requested but
285
+ unanswered before the classifier calls it `STALLED`; `max_recovery_cycles` is
286
+ FR-006's "one bounded cycle".
287
+ """
288
+
289
+ merge_method: str = "squash"
290
+ poll_interval_s: int = 60
291
+ stall_after_s: int = 7200
292
+ max_recovery_cycles: int = 1
293
+
294
+
295
+ # Helpers ----------------------------------------------------------------------
296
+
297
+
298
+ def _nullable_str(value: Any) -> str | None:
299
+ if value is None:
300
+ return None
301
+ return str(value)
302
+
303
+
304
+ def _failing_check_names(rollup: list[Any]) -> list[str]:
305
+ """Names of rollup entries whose outcome is a failure, whatever their shape.
306
+
307
+ `gh pr view --json statusCheckRollup` returns a heterogeneous list: `CheckRun`
308
+ entries carry a `conclusion` (and `name`); legacy `StatusCheckRollup` entries
309
+ carry a `state` (and `context`). A run that is still in progress is not a
310
+ failure, and an entry with no failing signal is not named.
311
+ """
312
+ names: list[str] = []
313
+ for entry in rollup:
314
+ if not isinstance(entry, dict):
315
+ continue
316
+ typename = entry.get("__typename")
317
+ if typename == "CheckRun":
318
+ if entry.get("conclusion") == "FAILURE":
319
+ names.append(str(entry.get("name") or ""))
320
+ elif typename in ("StatusCheckRollup", "StatusContext"):
321
+ if entry.get("state") == "FAILURE":
322
+ names.append(str(entry.get("context") or ""))
323
+ # A run whose name came back empty carries nothing the classifier can quote.
324
+ return [name for name in names if name]