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.
- ergane_cli-0.1.0.dist-info/METADATA +148 -0
- ergane_cli-0.1.0.dist-info/RECORD +121 -0
- ergane_cli-0.1.0.dist-info/WHEEL +4 -0
- ergane_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ergane_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- ergane_cli-0.1.0.dist-info/licenses/NOTICE +27 -0
- factory/__init__.py +0 -0
- factory/activities/__init__.py +0 -0
- factory/activities/agent_activities.py +745 -0
- factory/activities/merge_activities.py +654 -0
- factory/activities/notify_activities.py +1078 -0
- factory/activities/roadmap_activities.py +577 -0
- factory/activities/usage_activities.py +593 -0
- factory/activities/verify_activities.py +617 -0
- factory/cli/__init__.py +0 -0
- factory/cli/completion.py +150 -0
- factory/cli/doctor.py +484 -0
- factory/cli/env.py +247 -0
- factory/cli/errors.py +77 -0
- factory/cli/init.py +1057 -0
- factory/cli/install.py +679 -0
- factory/cli/main.py +212 -0
- factory/cli/nouns/__init__.py +59 -0
- factory/cli/nouns/answer.py +134 -0
- factory/cli/nouns/build.py +1222 -0
- factory/cli/nouns/completion.py +13 -0
- factory/cli/nouns/doctor.py +13 -0
- factory/cli/nouns/env.py +13 -0
- factory/cli/nouns/escalations.py +93 -0
- factory/cli/nouns/findings.py +13 -0
- factory/cli/nouns/init.py +13 -0
- factory/cli/nouns/install.py +99 -0
- factory/cli/nouns/repo.py +13 -0
- factory/cli/nouns/roadmap.py +13 -0
- factory/cli/nouns/spec.py +511 -0
- factory/cli/nouns/status.py +18 -0
- factory/cli/nouns/usage.py +13 -0
- factory/cli/nouns/worker.py +69 -0
- factory/cli/repo.py +569 -0
- factory/cli/repo_export.py +282 -0
- factory/cli/roadmap.py +434 -0
- factory/cli/status.py +844 -0
- factory/cli/usage.py +129 -0
- factory/config.py +262 -0
- factory/controlplane/__init__.py +0 -0
- factory/controlplane/config.py +863 -0
- factory/controlplane/resolve.py +434 -0
- factory/controlplane/verify.py +827 -0
- factory/discovery/__init__.py +3 -0
- factory/discovery/llm_scanner.py +176 -0
- factory/doctor/__init__.py +0 -0
- factory/doctor/cli.py +392 -0
- factory/doctor/models.py +165 -0
- factory/doctor/probes.py +522 -0
- factory/doctor/scaffold.py +184 -0
- factory/doctor/store.py +449 -0
- factory/env.py +131 -0
- factory/escalation/__init__.py +0 -0
- factory/escalation/client.py +168 -0
- factory/escalation/question.py +161 -0
- factory/escalation/workflow.py +470 -0
- factory/locking.py +85 -0
- factory/mergequeue/__init__.py +21 -0
- factory/mergequeue/classify.py +133 -0
- factory/mergequeue/forge.py +401 -0
- factory/mergequeue/gh.py +484 -0
- factory/mergequeue/github_forge.py +349 -0
- factory/mergequeue/merge_queue_ruleset.json +32 -0
- factory/mergequeue/messages.py +141 -0
- factory/mergequeue/models.py +324 -0
- factory/mergequeue/onboard.py +537 -0
- factory/mergequeue/wiring.py +466 -0
- factory/notify/__init__.py +0 -0
- factory/notify/adapter.py +263 -0
- factory/notify/messages.py +474 -0
- factory/notify/service.py +809 -0
- factory/notify/webhook.py +151 -0
- factory/personas.yaml +161 -0
- factory/registry.py +583 -0
- factory/roadmap/__init__.py +8 -0
- factory/roadmap/cli.py +125 -0
- factory/roadmap/discovery.py +286 -0
- factory/roadmap/models.py +583 -0
- factory/roadmap/schedule.py +377 -0
- factory/roadmap/workflow.py +1364 -0
- factory/supervision/__init__.py +0 -0
- factory/supervision/alert.py +239 -0
- factory/supervision/probe.py +467 -0
- factory/supervision/temporal_server.py +121 -0
- factory/supervision/units.py +649 -0
- factory/usage/__init__.py +0 -0
- factory/usage/aggregate.py +113 -0
- factory/usage/cli.py +223 -0
- factory/usage/ledger.py +295 -0
- factory/usage/litellm_client.py +486 -0
- factory/usage/models.py +149 -0
- factory/verify/__init__.py +0 -0
- factory/verify/criteria.py +488 -0
- factory/verify/diffbounds.py +159 -0
- factory/verify/diffcheck.py +506 -0
- factory/verify/factory_yaml.py +842 -0
- factory/verify/gates.py +1341 -0
- factory/verify/judge.py +775 -0
- factory/verify/ladder.py +145 -0
- factory/verify/models.py +745 -0
- factory/verify/question.py +167 -0
- factory/verify/store.py +1366 -0
- factory/verify/toolchain.py +370 -0
- factory/worker.py +296 -0
- factory/workgraph/__init__.py +0 -0
- factory/workgraph/adapter.py +1333 -0
- factory/workgraph/cli.py +801 -0
- factory/workgraph/delta.py +278 -0
- factory/workgraph/derive.py +607 -0
- factory/workgraph/detector.py +509 -0
- factory/workgraph/landed.py +496 -0
- factory/workgraph/models.py +494 -0
- factory/workgraph/preflight.py +651 -0
- factory/workgraph/prompt.py +796 -0
- factory/workgraph/workflow.py +2733 -0
- factory/workgraph/worktree.py +1322 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""The pure onboarding judgment: what a forge reported + gates → findings.
|
|
2
|
+
|
|
3
|
+
`onboard_target_repo` (the activity) reads a repository through a forge: what it
|
|
4
|
+
calls the repository, what the branch a landing goes into does to a proposal,
|
|
5
|
+
and the clone's committed `factory.yaml`. This module turns those readings into a
|
|
6
|
+
`TargetRepoProfile` whose `findings` say, one check at a time, whether the repo
|
|
7
|
+
is ready for the factory to dispatch against it. It is pure — no forge, no git,
|
|
8
|
+
no filesystem, no clock — so it is table-tested with no fakes at all
|
|
9
|
+
(`tests/test_onboard.py`).
|
|
10
|
+
|
|
11
|
+
049's US2 made these questions forge-neutral: they were three settings only
|
|
12
|
+
GitHub has, so a target elsewhere failed onboarding not for being unready but
|
|
13
|
+
because they did not apply (D-046). Nothing here spells one forge's
|
|
14
|
+
configuration; `tests/test_forge_readiness.py` reads this source to keep it so.
|
|
15
|
+
|
|
16
|
+
- **`gated_landing`** (Q2) — the branch must refuse a landing until *named*
|
|
17
|
+
checks pass: the gates the manifest declares are the gates the forge runs.
|
|
18
|
+
- **`autonomous_landing`** (Q3) — and then complete the merge itself, separate
|
|
19
|
+
because separately actionable: a branch that gates and waits for a click is
|
|
20
|
+
one this factory cannot land through (D-024).
|
|
21
|
+
- **`factory_yaml`** — the repo must commit a valid, non-empty-gated
|
|
22
|
+
`factory.yaml`. A missing or malformed manifest is a failing finding carrying
|
|
23
|
+
the 002 loader's error, never a pass by default: a verifier that shrugged at
|
|
24
|
+
a broken manifest would find no gates, therefore see nothing fail.
|
|
25
|
+
- **`landing_title`** (Q5) — the commit the forge writes when it lands must
|
|
26
|
+
carry the proposal's title, because `ergane spec landed` reads the story out
|
|
27
|
+
of that subject line. The forge's spelling of the setting travels as evidence
|
|
28
|
+
and its remedy as the fix; neither is decided here.
|
|
29
|
+
- **`gate_check:<gate>`** — every declared gate must have a required check
|
|
30
|
+
named *exactly* after it. The naming convention is the contract between
|
|
31
|
+
`factory.yaml` and the repo's CI; a declared gate with no matching check
|
|
32
|
+
would land a PR that never runs that gate.
|
|
33
|
+
- **`unknown_check:<name>`** — every required check must map back to a declared
|
|
34
|
+
gate. Deterministic gates only is FR-003 made structural: a required check
|
|
35
|
+
that is not a declared gate is a check the factory does not control, and this
|
|
36
|
+
is precisely what keeps the LLM judge out of CI.
|
|
37
|
+
|
|
38
|
+
Each check fails closed: `passed` is the conjunction, and a repo that fails any
|
|
39
|
+
check is rejected for dispatch with instructions for the operator (spec US3 AS2).
|
|
40
|
+
What a forge alone knows arrives on `RepositoryDescription.findings` and is
|
|
41
|
+
appended here without this module knowing what it means (FR-007) — which is how
|
|
42
|
+
a forge-specific rule fails a repository without the judgment learning that
|
|
43
|
+
forge's vocabulary, and is where the rule that a GitHub repo must be public now
|
|
44
|
+
lives.
|
|
45
|
+
|
|
46
|
+
**034 US4 extends this judgment; it does not fork it.** `ergane init --check`
|
|
47
|
+
gathers four more facts and passes them as `init_facts`; the 003 dispatch path
|
|
48
|
+
passes nothing and sees exactly the checks it always saw, so a check added here
|
|
49
|
+
can never start refusing a dispatch it used to allow. Both doors share this
|
|
50
|
+
function, so the gate↔check parity an operator reads at their terminal is the
|
|
51
|
+
same `Finding`, detail and all, the interpreter reads at dispatch (FR-010). The
|
|
52
|
+
added checks are `runtime_root_ignored` (whose detail names the exact
|
|
53
|
+
`.gitignore` line, because a runtime root reaching git history is how a node
|
|
54
|
+
commits its own transcripts onto a landing branch), `runtime_root_migration`
|
|
55
|
+
(only for a repo still on legacy `.factory/`, trap 12), `registry_entry`,
|
|
56
|
+
`landing_branch`, and one `control_plane` summary over 033's probes — a summary
|
|
57
|
+
because the control plane is a property of the host rather than of this repo,
|
|
58
|
+
and `ergane install --verify` renders its checks individually.
|
|
59
|
+
|
|
60
|
+
**034 US6 adds one more**: `roadmap_schedule`, failing when the repo has no
|
|
61
|
+
Temporal schedule, when the one it has is paused, and when that schedule's
|
|
62
|
+
arguments disagree with the committed manifest. Here rather than in a second
|
|
63
|
+
judgment for the reason the rest of this module exists — "ready" is decided in
|
|
64
|
+
exactly one place, and a joined repo with no scheduler is not ready.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
from __future__ import annotations
|
|
68
|
+
|
|
69
|
+
from dataclasses import dataclass, field
|
|
70
|
+
from typing import Mapping, Sequence
|
|
71
|
+
|
|
72
|
+
from factory.mergequeue.forge import LandingPolicy, RepositoryDescription
|
|
73
|
+
from factory.mergequeue.models import Finding, TargetRepoProfile
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class InitFacts:
|
|
78
|
+
"""What `ergane init --check` gathered, for this module to judge (034 FR-010).
|
|
79
|
+
|
|
80
|
+
Facts only — every field is something the CLI read from git, the registry or
|
|
81
|
+
033's probes — so the judgment over them stays pure and table-testable. The
|
|
82
|
+
`None` fields are "could not be determined", and each renders a *failing*
|
|
83
|
+
finding: `control_plane=()` with no error fails, because unknown readiness is
|
|
84
|
+
not readiness.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
#: Absolute path of the repository judged; quoted in every remedy.
|
|
88
|
+
repo_root: str
|
|
89
|
+
#: The root this repo has — `.ergane`, or `.factory` when unmigrated.
|
|
90
|
+
runtime_root: str = ".ergane"
|
|
91
|
+
runtime_root_is_legacy: bool = False
|
|
92
|
+
runtime_root_ignored: bool = True
|
|
93
|
+
#: The `.gitignore` a remedy would edit, so the fix is copy-pasteable.
|
|
94
|
+
gitignore: str = ""
|
|
95
|
+
registry_path: str = ""
|
|
96
|
+
#: The slug whose entry points here, or None when no entry does.
|
|
97
|
+
registry_slug: str | None = None
|
|
98
|
+
registry_error: str | None = None
|
|
99
|
+
#: The manifest's landing branch, or None when the manifest did not load.
|
|
100
|
+
landing_branch: str | None = None
|
|
101
|
+
landing_branch_exists: bool = False
|
|
102
|
+
#: 033's probe findings, carried through rather than re-derived.
|
|
103
|
+
control_plane: tuple[Finding, ...] = field(default_factory=tuple)
|
|
104
|
+
control_plane_error: str | None = None
|
|
105
|
+
#: 034 US6. The schedule this repo's slug names, and what is wrong with it.
|
|
106
|
+
#: The defaults fail closed on purpose: a caller that gathered nothing has
|
|
107
|
+
#: not established that a scheduler exists, and a repo with no scheduler is
|
|
108
|
+
#: one whose `ready` specs dispatch nothing.
|
|
109
|
+
schedule_id: str = ""
|
|
110
|
+
schedule_present: bool = False
|
|
111
|
+
schedule_paused: bool = False
|
|
112
|
+
#: One line per declared field the live schedule states differently.
|
|
113
|
+
schedule_drift: tuple[str, ...] = field(default_factory=tuple)
|
|
114
|
+
#: Why the schedule could not be read at all — unreachable engine, or no
|
|
115
|
+
#: registry entry naming the slug that would identify it.
|
|
116
|
+
schedule_error: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def evaluate_repo(
|
|
120
|
+
*,
|
|
121
|
+
repo: str,
|
|
122
|
+
reading: "RepositoryDescription",
|
|
123
|
+
policy: "LandingPolicy",
|
|
124
|
+
declared_gates: Sequence[str],
|
|
125
|
+
factory_yaml_error: str | None = None,
|
|
126
|
+
init_facts: "InitFacts | None" = None,
|
|
127
|
+
) -> TargetRepoProfile:
|
|
128
|
+
"""Judge a repository's two readings against what the factory needs.
|
|
129
|
+
|
|
130
|
+
Each argument is something the caller already read; this holds no I/O.
|
|
131
|
+
`reading` answers Q1 and carries what only this forge knows (FR-007),
|
|
132
|
+
appended without understanding it and read *first*, so a forge-authored
|
|
133
|
+
refusal lands where an operator has always found the repo's own health.
|
|
134
|
+
`policy` answers Q2, Q3 and Q5 for the branch a landing goes into and
|
|
135
|
+
carries the checks it requires, so Q4 can be asked without knowing where a
|
|
136
|
+
forge keeps them. `factory_yaml_error` is the 002 loader's message — a
|
|
137
|
+
broken manifest fails, never passes by default.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
findings: list[Finding] = list(reading.findings)
|
|
141
|
+
|
|
142
|
+
# Q2: the branch must refuse a landing until named checks pass, and Q3: it
|
|
143
|
+
# must then finish the job itself. One forge may answer both from a single
|
|
144
|
+
# setting; that is its coincidence, not a property of forges, so they are
|
|
145
|
+
# two findings and an operator is told which one is missing.
|
|
146
|
+
_gated_landing_finding(findings, policy)
|
|
147
|
+
_autonomous_landing_finding(findings, policy)
|
|
148
|
+
|
|
149
|
+
# The manifest must be present, valid, and declare gates. A broken manifest
|
|
150
|
+
# is a failing finding carrying the loader's error — never a shrug that
|
|
151
|
+
# would read as "no gates, so nothing to fail".
|
|
152
|
+
_manifest_finding(findings, factory_yaml_error)
|
|
153
|
+
|
|
154
|
+
# Q5: the landing commit must carry the proposal's title so the landing
|
|
155
|
+
# grammar survives the merge. A forge that would not say fails closed,
|
|
156
|
+
# matching the factory_yaml precedent.
|
|
157
|
+
_landing_title_finding(findings, policy)
|
|
158
|
+
|
|
159
|
+
# Q4: the gate ↔ check mapping, by name (position is irrelevant).
|
|
160
|
+
declared = set(declared_gates)
|
|
161
|
+
required = set(policy.required_checks)
|
|
162
|
+
for gate in declared:
|
|
163
|
+
_gate_check_finding(findings, gate, gate in required)
|
|
164
|
+
for check in sorted(required - declared):
|
|
165
|
+
_unknown_check_finding(findings, check)
|
|
166
|
+
|
|
167
|
+
# Init's own checks, when the caller gathered them (034 FR-010). Appended
|
|
168
|
+
# rather than interleaved so the 003 report an operator already knows how to
|
|
169
|
+
# read does not change shape underneath them.
|
|
170
|
+
findings.extend(evaluate_init_facts(init_facts))
|
|
171
|
+
|
|
172
|
+
return TargetRepoProfile.from_readiness(
|
|
173
|
+
repo=repo,
|
|
174
|
+
reading=reading,
|
|
175
|
+
policy=policy,
|
|
176
|
+
declared_gates=tuple(declared_gates),
|
|
177
|
+
findings=tuple(findings),
|
|
178
|
+
passed=all(f.passed for f in findings),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --- the checks ----------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _gated_landing_finding(findings: list[Finding], policy: "LandingPolicy") -> None:
|
|
186
|
+
if policy.gates_on_named_checks:
|
|
187
|
+
findings.append(Finding(
|
|
188
|
+
"gated_landing", True,
|
|
189
|
+
f"{policy.branch} refuses a landing until its named checks pass",
|
|
190
|
+
))
|
|
191
|
+
else:
|
|
192
|
+
findings.append(Finding(
|
|
193
|
+
"gated_landing", False,
|
|
194
|
+
f"{policy.branch!r} does not refuse a landing until named checks pass, "
|
|
195
|
+
"so a change could land with no gate having run; configure the branch "
|
|
196
|
+
"to require the gates this repository declares",
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _autonomous_landing_finding(
|
|
201
|
+
findings: list[Finding], policy: "LandingPolicy"
|
|
202
|
+
) -> None:
|
|
203
|
+
if policy.lands_without_a_human:
|
|
204
|
+
findings.append(Finding(
|
|
205
|
+
"autonomous_landing", True,
|
|
206
|
+
f"{policy.branch} completes a landing without a human",
|
|
207
|
+
))
|
|
208
|
+
else:
|
|
209
|
+
findings.append(Finding(
|
|
210
|
+
"autonomous_landing", False,
|
|
211
|
+
f"{policy.branch!r} will not complete a landing without a human; this "
|
|
212
|
+
"factory has no human in the loop by construction (D-024), so configure "
|
|
213
|
+
"the branch to land a proposal itself once its named checks pass",
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _manifest_finding(
|
|
218
|
+
findings: list[Finding], factory_yaml_error: str | None
|
|
219
|
+
) -> None:
|
|
220
|
+
if factory_yaml_error is None:
|
|
221
|
+
findings.append(Finding("factory_yaml", True, "manifest is valid"))
|
|
222
|
+
else:
|
|
223
|
+
findings.append(
|
|
224
|
+
Finding(
|
|
225
|
+
"factory_yaml",
|
|
226
|
+
False,
|
|
227
|
+
f"manifest failed to load: {factory_yaml_error} — fix the "
|
|
228
|
+
"manifest so the repo declares its gates",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _landing_title_finding(findings: list[Finding], policy: "LandingPolicy") -> None:
|
|
234
|
+
if policy.landing_title_from_proposal:
|
|
235
|
+
findings.append(Finding(
|
|
236
|
+
"landing_title", True, "a landing commit takes the proposal's title",
|
|
237
|
+
))
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
if policy.landing_title_source is None:
|
|
241
|
+
observed = "the forge would not report its title source, so it is unreadable"
|
|
242
|
+
else:
|
|
243
|
+
observed = f"the forge reports its title source as {policy.landing_title_source!r}"
|
|
244
|
+
|
|
245
|
+
findings.append(Finding(
|
|
246
|
+
"landing_title", False,
|
|
247
|
+
f"a landing commit will not take the proposal's title — {observed}; "
|
|
248
|
+
"`ergane spec landed` reads a story out of the landed subject line, so a "
|
|
249
|
+
"landing titled from anything else is work this factory cannot see it did"
|
|
250
|
+
+ (f" — {policy.landing_title_remedy}" if policy.landing_title_remedy else ""),
|
|
251
|
+
))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _gate_check_finding(findings: list[Finding], gate: str, matched: bool) -> None:
|
|
255
|
+
if matched:
|
|
256
|
+
findings.append(
|
|
257
|
+
Finding(f"gate_check:{gate}", True, f"required check '{gate}' exists")
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
findings.append(
|
|
261
|
+
Finding(
|
|
262
|
+
f"gate_check:{gate}",
|
|
263
|
+
False,
|
|
264
|
+
f"gate '{gate}' is declared in factory.yaml but the landing "
|
|
265
|
+
f"branch requires no check named '{gate}' — add it to the "
|
|
266
|
+
"branch's required checks so the forge runs the gate the "
|
|
267
|
+
"factory declares",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _unknown_check_finding(findings: list[Finding], check: str) -> None:
|
|
273
|
+
findings.append(
|
|
274
|
+
Finding(
|
|
275
|
+
f"unknown_check:{check}",
|
|
276
|
+
False,
|
|
277
|
+
f"required check '{check}' is not a declared gate in factory.yaml — "
|
|
278
|
+
"deterministic gates only (FR-003): a required check must map to a "
|
|
279
|
+
"declared gate, so the LLM judge can never be a CI check",
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# --- 034 US4: the checks init's own facts answer -------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def evaluate_init_facts(init_facts: "InitFacts | None") -> tuple[Finding, ...]:
|
|
288
|
+
"""Judge the facts `ergane init --check` gathered — or nothing, when it did not.
|
|
289
|
+
|
|
290
|
+
Split out of `evaluate_repo` for one reason: `onboard_target_repo` short
|
|
291
|
+
circuits into `_profile_from_gh_failure` when `gh` cannot read the repo, and
|
|
292
|
+
a repo with no GitHub remote is exactly the repo whose *local* findings
|
|
293
|
+
matter most. Both paths call this, so a `gh` refusal never masks the
|
|
294
|
+
gitignore check ("no failure masking another").
|
|
295
|
+
|
|
296
|
+
Every check renders unconditionally: no early return, no exception skipping
|
|
297
|
+
the rest. An unknown answer is a failing finding, never an absent one.
|
|
298
|
+
"""
|
|
299
|
+
if init_facts is None:
|
|
300
|
+
return ()
|
|
301
|
+
|
|
302
|
+
findings: list[Finding] = []
|
|
303
|
+
_runtime_root_findings(findings, init_facts)
|
|
304
|
+
_registry_finding(findings, init_facts)
|
|
305
|
+
_landing_branch_finding(findings, init_facts)
|
|
306
|
+
_control_plane_finding(findings, init_facts)
|
|
307
|
+
_roadmap_schedule_finding(findings, init_facts)
|
|
308
|
+
return tuple(findings)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _runtime_root_findings(findings: list[Finding], facts: "InitFacts") -> None:
|
|
312
|
+
"""The runtime root must be ignored by git — the root this repo *has*.
|
|
313
|
+
|
|
314
|
+
Trap 12: a repo that never ran `ergane repo migrate-runtime-root` keeps its
|
|
315
|
+
state in `.factory/`. Demanding `.ergane/` there reports a failure the
|
|
316
|
+
operator cannot act on while missing the directory actually holding their
|
|
317
|
+
evidence, so the resolved root is judged and the legacy name earns a second
|
|
318
|
+
finding naming its remedy.
|
|
319
|
+
"""
|
|
320
|
+
root = facts.runtime_root
|
|
321
|
+
if facts.runtime_root_ignored:
|
|
322
|
+
findings.append(
|
|
323
|
+
Finding("runtime_root_ignored", True, f"{root}/ is ignored by git")
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
findings.append(
|
|
327
|
+
Finding(
|
|
328
|
+
"runtime_root_ignored",
|
|
329
|
+
False,
|
|
330
|
+
f"{root}/ is not ignored by git; add the line `{root}/` to "
|
|
331
|
+
f"{facts.gitignore} — the runtime root holds worktrees, session "
|
|
332
|
+
"transcripts and verification evidence, and one that reaches git "
|
|
333
|
+
"history lands megabytes of agent output on a landing branch",
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if facts.runtime_root_is_legacy:
|
|
338
|
+
findings.append(
|
|
339
|
+
Finding(
|
|
340
|
+
"runtime_root_migration",
|
|
341
|
+
False,
|
|
342
|
+
f"this repository's runtime root is still the legacy {root}/; run "
|
|
343
|
+
"`ergane repo migrate-runtime-root` to move it to .ergane/",
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _registry_finding(findings: list[Finding], facts: "InitFacts") -> None:
|
|
349
|
+
"""The engine must know where this repo is; the registry is how it knows."""
|
|
350
|
+
if facts.registry_error is not None:
|
|
351
|
+
findings.append(
|
|
352
|
+
Finding(
|
|
353
|
+
"registry_entry",
|
|
354
|
+
False,
|
|
355
|
+
f"the engine registry {facts.registry_path} could not be read: "
|
|
356
|
+
f"{facts.registry_error} — it is a cache: re-derive it with "
|
|
357
|
+
f"`ergane repo rebuild {facts.repo_root}`",
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
if facts.registry_slug is not None:
|
|
363
|
+
findings.append(
|
|
364
|
+
Finding(
|
|
365
|
+
"registry_entry",
|
|
366
|
+
True,
|
|
367
|
+
f"registered as '{facts.registry_slug}' in {facts.registry_path}",
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
findings.append(
|
|
373
|
+
Finding(
|
|
374
|
+
"registry_entry",
|
|
375
|
+
False,
|
|
376
|
+
f"no entry in the engine registry {facts.registry_path} points at "
|
|
377
|
+
f"{facts.repo_root}; run `ergane init` here to register it — the "
|
|
378
|
+
"committed manifest is what makes a repo managed, but the engine "
|
|
379
|
+
"cannot enumerate repos it has no pointer to",
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _landing_branch_finding(findings: list[Finding], facts: "InitFacts") -> None:
|
|
385
|
+
"""The branch the manifest declares must be a branch the repo has."""
|
|
386
|
+
if facts.landing_branch is None:
|
|
387
|
+
findings.append(
|
|
388
|
+
Finding(
|
|
389
|
+
"landing_branch",
|
|
390
|
+
False,
|
|
391
|
+
"the landing branch could not be judged: this repository's "
|
|
392
|
+
"manifest did not load (see the factory_yaml finding)",
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
branch = facts.landing_branch
|
|
398
|
+
if facts.landing_branch_exists:
|
|
399
|
+
findings.append(
|
|
400
|
+
Finding("landing_branch", True, f"landing branch '{branch}' exists")
|
|
401
|
+
)
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
findings.append(
|
|
405
|
+
Finding(
|
|
406
|
+
"landing_branch",
|
|
407
|
+
False,
|
|
408
|
+
f"the manifest declares `landing_branch: {branch}` but "
|
|
409
|
+
f"{facts.repo_root} has no such branch; create it with "
|
|
410
|
+
f"`git -C {facts.repo_root} branch {branch}`, or declare the branch "
|
|
411
|
+
"this repository actually lands on",
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _control_plane_finding(findings: list[Finding], facts: "InitFacts") -> None:
|
|
417
|
+
"""One summary finding over 033's probes, carrying their detail through.
|
|
418
|
+
|
|
419
|
+
Fails closed three ways: the probes could not run, some probe failed, or no
|
|
420
|
+
probe ran at all. The last is the one worth naming — "reachable" because
|
|
421
|
+
nothing was probed would be worse than no check.
|
|
422
|
+
"""
|
|
423
|
+
if facts.control_plane_error is not None:
|
|
424
|
+
findings.append(
|
|
425
|
+
Finding(
|
|
426
|
+
"control_plane",
|
|
427
|
+
False,
|
|
428
|
+
f"the control plane could not be probed: {facts.control_plane_error}"
|
|
429
|
+
" — `ergane install --verify` reports each subsystem separately",
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
if not facts.control_plane:
|
|
435
|
+
findings.append(
|
|
436
|
+
Finding(
|
|
437
|
+
"control_plane",
|
|
438
|
+
False,
|
|
439
|
+
"no control-plane probe ran, so control-plane readiness is "
|
|
440
|
+
"unknown; run `ergane install --verify` to see why",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
failed: list[Finding] = []
|
|
446
|
+
for probe in facts.control_plane:
|
|
447
|
+
if not probe.passed:
|
|
448
|
+
failed.append(probe)
|
|
449
|
+
|
|
450
|
+
if not failed:
|
|
451
|
+
names: list[str] = []
|
|
452
|
+
for probe in facts.control_plane:
|
|
453
|
+
names.append(probe.check)
|
|
454
|
+
findings.append(
|
|
455
|
+
Finding(
|
|
456
|
+
"control_plane",
|
|
457
|
+
True,
|
|
458
|
+
f"control plane reachable: {len(names)} probes passed "
|
|
459
|
+
f"({', '.join(names)})",
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
reasons: list[str] = []
|
|
465
|
+
for probe in failed:
|
|
466
|
+
reasons.append(f"{probe.check}: {probe.detail}")
|
|
467
|
+
findings.append(
|
|
468
|
+
Finding(
|
|
469
|
+
"control_plane",
|
|
470
|
+
False,
|
|
471
|
+
f"{len(failed)} of {len(facts.control_plane)} control-plane probes "
|
|
472
|
+
f"failed — {'; '.join(reasons)}",
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _roadmap_schedule_finding(findings: list[Finding], facts: "InitFacts") -> None:
|
|
478
|
+
"""The repo must have a scheduler, running, pointed here (034 FR-016).
|
|
479
|
+
|
|
480
|
+
One finding whose detail names *every* problem found rather than the first:
|
|
481
|
+
a schedule both paused and drifted has two things wrong with it, and naming
|
|
482
|
+
only one would be the masking this judgment forbids. Each failure ends in
|
|
483
|
+
the command that fixes it — the operator reading it is at a terminal.
|
|
484
|
+
"""
|
|
485
|
+
if facts.schedule_error is not None:
|
|
486
|
+
findings.append(
|
|
487
|
+
Finding(
|
|
488
|
+
"roadmap_schedule",
|
|
489
|
+
False,
|
|
490
|
+
"this repository's roadmap schedule could not be judged: "
|
|
491
|
+
f"{facts.schedule_error}",
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
if not facts.schedule_present:
|
|
497
|
+
findings.append(
|
|
498
|
+
Finding(
|
|
499
|
+
"roadmap_schedule",
|
|
500
|
+
False,
|
|
501
|
+
f"no roadmap schedule '{facts.schedule_id}' exists on the control "
|
|
502
|
+
f"plane, so no tick will ever dispatch {facts.repo_root}'s specs — "
|
|
503
|
+
f"run `ergane init {facts.repo_root}` to create it; flipping a spec "
|
|
504
|
+
"to `ready` without one does nothing, with no error",
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
problems: list[str] = []
|
|
510
|
+
if facts.schedule_paused:
|
|
511
|
+
problems.append(
|
|
512
|
+
"it is paused, so no tick will start a run — resume it with "
|
|
513
|
+
"`ergane roadmap resume <specs root>` when you mean dispatch to run"
|
|
514
|
+
)
|
|
515
|
+
for line in facts.schedule_drift:
|
|
516
|
+
problems.append(line)
|
|
517
|
+
|
|
518
|
+
if not problems:
|
|
519
|
+
findings.append(
|
|
520
|
+
Finding(
|
|
521
|
+
"roadmap_schedule",
|
|
522
|
+
True,
|
|
523
|
+
f"roadmap schedule '{facts.schedule_id}' is running and matches "
|
|
524
|
+
"the manifest",
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
remedy = f" — re-run `ergane init {facts.repo_root}`" if facts.schedule_drift else ""
|
|
530
|
+
findings.append(
|
|
531
|
+
Finding(
|
|
532
|
+
"roadmap_schedule",
|
|
533
|
+
False,
|
|
534
|
+
f"roadmap schedule '{facts.schedule_id}' disagrees with this "
|
|
535
|
+
f"repository: {'; '.join(problems)}{remedy}",
|
|
536
|
+
)
|
|
537
|
+
)
|