autoforge-cli 0.1.0a0__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.
- autoforge/__init__.py +5 -0
- autoforge/agent_worktree.py +653 -0
- autoforge/certificate.py +495 -0
- autoforge/child_env.py +871 -0
- autoforge/claims.py +726 -0
- autoforge/claude_stream.py +501 -0
- autoforge/cli.py +2884 -0
- autoforge/config.py +2178 -0
- autoforge/config_fingerprint.py +374 -0
- autoforge/doctor.py +1551 -0
- autoforge/doctor_sandbox.py +691 -0
- autoforge/effect_ops.py +1272 -0
- autoforge/effects.py +3210 -0
- autoforge/engine.py +16637 -0
- autoforge/epic_commands.py +1486 -0
- autoforge/epic_graph.py +852 -0
- autoforge/errors.py +536 -0
- autoforge/evidence.py +1722 -0
- autoforge/executor.py +1601 -0
- autoforge/executor_duplex.py +985 -0
- autoforge/executor_supervised.py +144 -0
- autoforge/git_transport.py +970 -0
- autoforge/github.py +3402 -0
- autoforge/interaction.py +1027 -0
- autoforge/local_workspace.py +1253 -0
- autoforge/locking.py +1050 -0
- autoforge/loop_detect.py +660 -0
- autoforge/loop_guard.py +404 -0
- autoforge/materialize.py +722 -0
- autoforge/ownership.py +240 -0
- autoforge/pi_rpc.py +713 -0
- autoforge/premerge.py +181 -0
- autoforge/profiles.py +106 -0
- autoforge/progress.py +392 -0
- autoforge/prompts/__init__.py +173 -0
- autoforge/prompts/acceptance_actions.md +39 -0
- autoforge/prompts/analyze_execute.md +128 -0
- autoforge/prompts/common.md +132 -0
- autoforge/prompts/correction.md +12 -0
- autoforge/prompts/fix.md +252 -0
- autoforge/prompts/local_analyze_execute.md +69 -0
- autoforge/prompts/local_common.md +149 -0
- autoforge/prompts/local_fix.md +96 -0
- autoforge/prompts/local_review.md +103 -0
- autoforge/prompts/operator_acceptance.md +61 -0
- autoforge/prompts/replan_reexecute.md +991 -0
- autoforge/prompts/review.md +312 -0
- autoforge/prompts/update_epic.md +120 -0
- autoforge/prompts/update_epic_rerequest.md +85 -0
- autoforge/prompts/validation_evidence.md +26 -0
- autoforge/prompts/validation_failure.md +22 -0
- autoforge/prompts/worktree_state.md +45 -0
- autoforge/providers.py +2893 -0
- autoforge/read_context.py +389 -0
- autoforge/redaction.py +408 -0
- autoforge/replan.py +208 -0
- autoforge/replan_txn.py +1930 -0
- autoforge/result_parser.py +2234 -0
- autoforge/roadmap.py +151 -0
- autoforge/run_contract.py +408 -0
- autoforge/run_namespace.py +839 -0
- autoforge/runlog.py +764 -0
- autoforge/safefs.py +1717 -0
- autoforge/sandbox.py +1313 -0
- autoforge/sandbox_bwrap.py +815 -0
- autoforge/sandbox_pin.py +170 -0
- autoforge/sandbox_policy.py +1107 -0
- autoforge/scheduler.py +1143 -0
- autoforge/scheduler_state.py +2316 -0
- autoforge/scheduler_stop.py +421 -0
- autoforge/sdk_policy.py +988 -0
- autoforge/state.py +2417 -0
- autoforge/transitions.py +388 -0
- autoforge/validation.py +324 -0
- autoforge/worker.py +568 -0
- autoforge_cli-0.1.0a0.dist-info/METADATA +525 -0
- autoforge_cli-0.1.0a0.dist-info/RECORD +81 -0
- autoforge_cli-0.1.0a0.dist-info/WHEEL +5 -0
- autoforge_cli-0.1.0a0.dist-info/entry_points.txt +2 -0
- autoforge_cli-0.1.0a0.dist-info/licenses/LICENSE +21 -0
- autoforge_cli-0.1.0a0.dist-info/top_level.txt +1 -0
autoforge/errors.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Error taxonomy for the AutoForge controller.
|
|
2
|
+
|
|
3
|
+
Callers must be able to distinguish transient execution failures from
|
|
4
|
+
malformed LLM output, controller invariant violations, and real GitHub
|
|
5
|
+
state conflicts — so every failure raises one of these, never a bare
|
|
6
|
+
``Exception("something failed")``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AutoForgeError(Exception):
|
|
11
|
+
"""Base class for all controller errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigurationError(AutoForgeError):
|
|
15
|
+
"""Invalid CLI args, config file, profile, or URL."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigurationDriftError(ConfigurationError):
|
|
19
|
+
"""A REMOTE run's configuration differs from the one it was recorded under (#219).
|
|
20
|
+
|
|
21
|
+
Raised by ``resume``, ``step`` and ``unblock`` before any phase executes,
|
|
22
|
+
with nothing written; the message names each changed key, the command
|
|
23
|
+
that restores the recorded configuration, and the opt-in that accepts
|
|
24
|
+
the change.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SandboxCapabilityError(ConfigurationError):
|
|
29
|
+
"""This host cannot enforce the outer sandbox, and the mode refuses (ADR 0005 D12.2).
|
|
30
|
+
|
|
31
|
+
Raised by the engine's pre-launch gate under ``sandbox.mode: required``,
|
|
32
|
+
and under ``preferred`` in a REMOTE run, before anything is spawned,
|
|
33
|
+
before the attempt counter is saved and before a LOCAL launch is
|
|
34
|
+
charged: state is unchanged and nothing needs recovery. A configuration
|
|
35
|
+
or capability error, never a phase failure. ``capability`` names what
|
|
36
|
+
the host lacks (``autoforge.sandbox_bwrap.Capability``'s value).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, capability: str = "") -> None:
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.capability = capability
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StateError(AutoForgeError):
|
|
45
|
+
"""State file missing, unreadable, corrupted, or schema-invalid.
|
|
46
|
+
|
|
47
|
+
Raised on read of a corrupted state; the controller must never silently
|
|
48
|
+
re-initialize a fresh state over existing data.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NamespaceError(StateError):
|
|
53
|
+
"""A per-issue scheduler namespace cannot be used as asked (ADR 0008 D5, #178).
|
|
54
|
+
|
|
55
|
+
Raised by :mod:`autoforge.run_namespace` for a ``binding.json`` that is
|
|
56
|
+
not a valid ``autoforge.run-binding/1`` document (an unknown field or
|
|
57
|
+
schema version, a missing field, an out-of-bound string, an id that is
|
|
58
|
+
not a positive integer), a binding written a second time, a namespace of
|
|
59
|
+
another repository and an ``issues/`` directory beyond its listing
|
|
60
|
+
bound. Nothing is created, moved or rewritten when it is raised.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NamespaceMismatch(NamespaceError):
|
|
65
|
+
"""A namespace's binding names another run than the caller expects (ADR 0008 D5.1, D5.2).
|
|
66
|
+
|
|
67
|
+
``field`` says which identity differs: ``repository_id`` (the scheduler's
|
|
68
|
+
global uncertainty, D14.3), ``issue_number``, ``issue_id`` or ``epic``
|
|
69
|
+
(the namespace's ``uncertain``, D14.2). The caller decides what the
|
|
70
|
+
difference means; the binding is never rewritten to agree.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, message: str, field: str) -> None:
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
self.field = field
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SchedulerStateError(StateError):
|
|
79
|
+
"""A scheduler file cannot be used as asked (ADR 0008 D6, D7, #181).
|
|
80
|
+
|
|
81
|
+
Raised by :mod:`autoforge.scheduler_state` for a ``claim.json`` or a
|
|
82
|
+
``scheduler.json`` that is not a valid document of its schema (an unknown
|
|
83
|
+
field or version, a missing field, an out-of-bound string or list, a
|
|
84
|
+
broken generation order), for a claim operation its closing rule refuses,
|
|
85
|
+
and for an operation called without the issue's run lock (L2). Nothing is
|
|
86
|
+
written when it is raised. A corrupt ``scheduler.json`` is the
|
|
87
|
+
scheduler's global uncertainty, a corrupt ``claim.json`` its issue's
|
|
88
|
+
``uncertain``.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ClaimConflict(SchedulerStateError):
|
|
93
|
+
"""``claim.json`` on disk is not what its writer expected (ADR 0008 D7.2, #181).
|
|
94
|
+
|
|
95
|
+
The ``(claim_id, generation)`` the caller holds is not the open claim on
|
|
96
|
+
disk, or the slot it means to fill is taken: a stale claimant, which can
|
|
97
|
+
never overwrite or finalize a newer generation. Classifies as
|
|
98
|
+
``uncertain``; the file is left as it is, never overwritten to agree.
|
|
99
|
+
Distinct from :class:`ClaimConflictError`, which is about GitHub claims.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OpenSchedulerClaimsError(SchedulerStateError):
|
|
104
|
+
"""``epic run`` found claims of its EPIC still open (ADR 0008 D14.4, #183).
|
|
105
|
+
|
|
106
|
+
An earlier invocation ended without closing them (a crash, a kill, a
|
|
107
|
+
second Ctrl-C, or an issue it held uncertain), so ``epic resume``, which
|
|
108
|
+
reconciles them first, is the command to run. Raised holding L0, before
|
|
109
|
+
``scheduler.json`` is written: nothing of the invocation was written.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SchedulerStateDirError(ConfigurationError):
|
|
114
|
+
"""A standalone command was pointed at a state directory inside the scheduler root.
|
|
115
|
+
|
|
116
|
+
``run``, ``step``, ``resume`` and ``local run`` refuse a ``--state-dir``
|
|
117
|
+
or ``config.state_dir`` that resolves inside
|
|
118
|
+
``<git common dir>/autoforge/scheduler/`` (ADR 0008 D17.3, #178), before
|
|
119
|
+
the lock is taken or any state is read or written; the message names the
|
|
120
|
+
scheduler commands instead. ``status`` reads such a directory and
|
|
121
|
+
``unblock`` acts on it.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StateTransitionError(AutoForgeError):
|
|
126
|
+
"""Illegal phase transition or step on a terminal phase."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class LockError(AutoForgeError):
|
|
130
|
+
"""Another controller instance holds the repository lock."""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class IssueBusy(LockError):
|
|
134
|
+
"""An issue's run lock (L2) is held by another process (ADR 0008 D11.1, #180).
|
|
135
|
+
|
|
136
|
+
L2 is only ever taken non-blocking, so a held one is this refusal at
|
|
137
|
+
once, never a wait.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class LockProofError(LockError):
|
|
142
|
+
"""An inherited lock descriptor was not proven to hold its lock (ADR 0008 D9.2, #180).
|
|
143
|
+
|
|
144
|
+
The descriptor names another object than the expected one, or nothing
|
|
145
|
+
holds the expected object's lock; nothing was acquired or released.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class NetworkFilesystemError(LockError):
|
|
150
|
+
"""A scheduler lock lies on a filesystem whose ``flock`` is not a local kernel lock.
|
|
151
|
+
|
|
152
|
+
NFS, CIFS/SMB, 9p, AFS, cluster filesystems and every FUSE filesystem
|
|
153
|
+
(ADR 0008 D11.4, #180): the live-ownership proof depends on local locks,
|
|
154
|
+
so scheduler mode refuses them. Standalone commands never check.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SchedulerOwnedIssueError(ConfigurationError):
|
|
159
|
+
"""A standalone command was asked to act on an issue a scheduler namespace owns.
|
|
160
|
+
|
|
161
|
+
ADR 0008 D17.2's forward guard (#180): the namespace holds a state, a
|
|
162
|
+
live worker holds its run lock, or the namespace cannot be read
|
|
163
|
+
(fail-closed), so only the scheduler continues it until ``autoforge epic
|
|
164
|
+
archive`` releases it. Nothing was read past the guard and nothing was
|
|
165
|
+
written.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class GitLockTimeout(AutoForgeError):
|
|
170
|
+
"""The shared-Git lock (L1) was not obtained within its bounded wait (ADR 0008 D11.3).
|
|
171
|
+
|
|
172
|
+
Transient. Each operation L1 guards turns it into its own failure (a
|
|
173
|
+
fetch's :class:`GitTransportError`, an import's failure, a worktree
|
|
174
|
+
creation's :class:`VerificationError`) and is handled exactly as that
|
|
175
|
+
operation failing; it is never a :class:`LockError` exit and never
|
|
176
|
+
retried in place.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class LockOrderError(AutoForgeError):
|
|
181
|
+
"""A lock was taken out of the order L0 -> L2 -> L1, or work was done under L1.
|
|
182
|
+
|
|
183
|
+
ADR 0008 D11.2's debug assertion (#180): the holder of the shared-Git
|
|
184
|
+
lock acquires no lock, invokes no agent, calls no GitHub and saves no
|
|
185
|
+
state. Always a controller bug; never retried.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class WorkerStartupError(AutoForgeError):
|
|
190
|
+
"""A scheduler worker refused to start (ADR 0008 D9.2, #182).
|
|
191
|
+
|
|
192
|
+
Its argv does not parse, an inherited descriptor was not proven to hold
|
|
193
|
+
its lock, or the namespace's binding, its open claim, ``scheduler.json``
|
|
194
|
+
or the configuration digest does not match what the worker was started
|
|
195
|
+
for. The worker exits 2 before it reads or writes any state.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ExecutionError(AutoForgeError):
|
|
200
|
+
"""Agent/CLI subprocess failed (non-zero exit, spawn failure, ...)."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ExecutionTimeoutError(ExecutionError):
|
|
204
|
+
"""Subprocess exceeded its timeout and was terminated."""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class InteractionDataError(ExecutionError):
|
|
208
|
+
"""A provider's interaction data is refused, never clipped or rewritten (#149).
|
|
209
|
+
|
|
210
|
+
Raised by :mod:`autoforge.sdk_policy` for a value that decides what a
|
|
211
|
+
tool may do or what the agent is told (a tool input, a question's
|
|
212
|
+
option, an answer, a modified input) when it is past its bound, carries
|
|
213
|
+
a control character or an escape sequence, or would be changed by
|
|
214
|
+
redaction: approving a clipped or redacted rendering would approve
|
|
215
|
+
different work. The message names the field and the rule, never the
|
|
216
|
+
value.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class InteractionRequiredError(ExecutionError):
|
|
221
|
+
"""The agent asked for a decision it did not get (ADR 0007 D7.6, D10.5; #134).
|
|
222
|
+
|
|
223
|
+
Raised by the engine for an invocation whose
|
|
224
|
+
``AgentExecutionResult.interaction`` ended ``failed``: its policy is
|
|
225
|
+
``fail``, or a live wait could not be answered and no pause is
|
|
226
|
+
available. Raised too for a ``paused`` one while no pending-interaction
|
|
227
|
+
record exists to pause into (#126): nothing is pending. Never corrected
|
|
228
|
+
and never relaunched as transient, since a relaunch would ask again; the
|
|
229
|
+
phase is unchanged, and what was saved before the launch (the attempt
|
|
230
|
+
counter, a LOCAL launch charge) stands.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ChildStdinClosedError(ExecutionError):
|
|
235
|
+
"""A duplex child closed its stdin while a record was being written (EPIPE)."""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class SandboxPolicyError(AutoForgeError):
|
|
239
|
+
"""A sandbox launch cannot be described as asked (ADR 0005 D4.2, D4.3, D7.2).
|
|
240
|
+
|
|
241
|
+
Raised by the policy compiler (``autoforge.sandbox``) before anything is
|
|
242
|
+
spawned: a relative or missing source, a link in a controller-owned
|
|
243
|
+
path, a socket or device, a forbidden source, a denied root left
|
|
244
|
+
visible, an overlap, a keyring login. ``path`` and ``rule`` name what was
|
|
245
|
+
refused and why; the message never quotes a file's contents.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(self, path: str, rule: str, detail: str) -> None:
|
|
249
|
+
super().__init__(f"sandbox policy refuses {path}: {rule}: {detail}")
|
|
250
|
+
self.path = path
|
|
251
|
+
self.rule = rule
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SandboxLaunchRefusedError(ExecutionError):
|
|
255
|
+
"""The outer sandbox could not be established for a launch (ADR 0005 D10.8, D14.2).
|
|
256
|
+
|
|
257
|
+
The platform is not Linux, the backend is missing or lacks a capability,
|
|
258
|
+
or the backend failed to set the sandbox up (a bind source gone, an
|
|
259
|
+
executable the sandbox cannot run, a namespace refused). It is a launch
|
|
260
|
+
refusal on the path of a failure to spawn: the command never ran, and
|
|
261
|
+
nothing it would have written is attributed to it. ``facts`` is the
|
|
262
|
+
launch's ``autoforge.sandbox.SandboxFacts`` (``enforced`` False, the
|
|
263
|
+
bounded, redacted reason) when the backend got as far as preparing it.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, message: str, facts: object = None) -> None:
|
|
267
|
+
super().__init__(message)
|
|
268
|
+
self.facts = facts
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ControlResultError(AutoForgeError):
|
|
272
|
+
"""CONTROL_RESULT block missing, ambiguous, or not valid JSON."""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class ControlResultValidationError(ControlResultError):
|
|
276
|
+
"""CONTROL_RESULT parsed but failed schema/phase validation."""
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class GitHubError(AutoForgeError):
|
|
280
|
+
"""`gh` CLI invocation or GitHub state problem.
|
|
281
|
+
|
|
282
|
+
Raised as-is for *conclusive* failures (authentication, permissions,
|
|
283
|
+
a missing PR, malformed data); see :class:`GitHubUnavailableError` for
|
|
284
|
+
the transient kind. Callers that must not guess distinguish the two.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class GitHubUnavailableError(GitHubError):
|
|
289
|
+
"""GitHub could not be reached or answered with a transient error.
|
|
290
|
+
|
|
291
|
+
Timeouts, connection resets, 5xx / rate limiting: the same read may well
|
|
292
|
+
succeed later, so the controller treats the data as *inconclusive*
|
|
293
|
+
(bounded re-checks) rather than as a conclusive verification failure.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class GitHubNotFoundError(GitHubError):
|
|
298
|
+
"""The referenced issue / PR does not resolve on GitHub (conclusive).
|
|
299
|
+
|
|
300
|
+
``gh`` answered, and the answer is that the object is not there (or is
|
|
301
|
+
not visible to the authenticated token, which GitHub reports the same
|
|
302
|
+
way). Callers that verify an *untrusted selection* treat this as a bad
|
|
303
|
+
selection; every other conclusive :class:`GitHubError` (authentication,
|
|
304
|
+
permissions, malformed data) is not about the selection at all.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class GitHubChecksAccessError(GitHubError):
|
|
309
|
+
"""GitHub refused this credential a pull request's check results (conclusive, #291).
|
|
310
|
+
|
|
311
|
+
``gh pr view --json ...,statusCheckRollup`` fails as a whole when GitHub
|
|
312
|
+
answers the rollup with "Resource not accessible by personal access
|
|
313
|
+
token" (or "... by integration"): a fine-grained personal access token
|
|
314
|
+
on a private repository whose PR has a check run, which cannot be given
|
|
315
|
+
Checks: read. The PR exists and the token may read it; only its checks
|
|
316
|
+
are refused. So this is never a not-found, never transient (a retry
|
|
317
|
+
reads the same refusal) and never "no checks": a caller that needs the
|
|
318
|
+
checks fails closed on it, and one that does not reads the PR without
|
|
319
|
+
them. ``credential`` is GitHub's name for what it refused:
|
|
320
|
+
``personal access token`` or ``integration``.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(self, message: str, credential: str = "") -> None:
|
|
324
|
+
super().__init__(message)
|
|
325
|
+
self.credential = credential
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class GitHubPlanLimitedError(GitHubError):
|
|
329
|
+
"""GitHub's plan for the repository does not offer what was read (conclusive, #289).
|
|
330
|
+
|
|
331
|
+
On GitHub Free a private repository has no rulesets and no classic
|
|
332
|
+
branch protection, and GitHub answers those endpoints with ``HTTP 403
|
|
333
|
+
"Upgrade to GitHub Pro or make this repository public to enable this
|
|
334
|
+
feature."`` to every caller, the owner's admin login included. That is
|
|
335
|
+
about the plan, not about the credential: no token permission changes
|
|
336
|
+
it, so it is never reported as access denied.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class GitHubWrongTargetError(GitHubError):
|
|
341
|
+
"""GitHub answered a read of one object with another object (conclusive, #243).
|
|
342
|
+
|
|
343
|
+
``gh issue view`` of an issue number that is a pull request answers
|
|
344
|
+
with the pull request, and one of a transferred issue answers with the
|
|
345
|
+
issue it became. ``answered_url`` is the URL GitHub answered with, so a
|
|
346
|
+
caller can tell the two apart (``is_pull_request``) without parsing the
|
|
347
|
+
message.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
def __init__(self, message: str, answered_url: str = "") -> None:
|
|
351
|
+
super().__init__(message)
|
|
352
|
+
self.answered_url = answered_url
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def is_pull_request(self) -> bool:
|
|
356
|
+
"""Whether GitHub answered with a pull request URL (not, say, an issue of a repository
|
|
357
|
+
named ``pull``)."""
|
|
358
|
+
from .validation import parse_pr_url # validation imports this module
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
parse_pr_url(self.answered_url)
|
|
362
|
+
except ConfigurationError:
|
|
363
|
+
return False
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class SnapshotRefused(AutoForgeError):
|
|
368
|
+
"""An EPIC's dependency graph cannot be taken as a complete snapshot (ADR 0008 D3, #179).
|
|
369
|
+
|
|
370
|
+
Raised by :func:`autoforge.epic_graph.build_epic_snapshot` instead of
|
|
371
|
+
returning a partial snapshot: a read that failed, a listing whose length
|
|
372
|
+
differs from its summary count, a bound exceeded, an EPIC that is not
|
|
373
|
+
open or not in the verified repository, an inconsistent answer, or a
|
|
374
|
+
cycle among open children. ``reason`` is one of :data:`SNAPSHOT_REFUSALS`;
|
|
375
|
+
``cycle`` is the canonical cycle (issue numbers, starting at the lowest)
|
|
376
|
+
when ``reason`` is ``cycle``; ``transient`` is whether the refusal came
|
|
377
|
+
from a GitHub read that may well succeed later. Nothing is admitted
|
|
378
|
+
from a refused snapshot, and the next pass takes a new one. The message
|
|
379
|
+
names issue numbers, states and reasons, never a title or a body.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def __init__(
|
|
383
|
+
self,
|
|
384
|
+
message: str,
|
|
385
|
+
reason: str,
|
|
386
|
+
*,
|
|
387
|
+
cycle: tuple[int, ...] = (),
|
|
388
|
+
transient: bool = False,
|
|
389
|
+
) -> None:
|
|
390
|
+
if reason not in SNAPSHOT_REFUSALS:
|
|
391
|
+
raise ValueError(f"unknown snapshot refusal reason {reason!r}")
|
|
392
|
+
super().__init__(message)
|
|
393
|
+
self.reason = reason
|
|
394
|
+
self.cycle = tuple(cycle)
|
|
395
|
+
self.transient = transient
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# The closed set of :class:`SnapshotRefused` reasons.
|
|
399
|
+
SNAPSHOT_REFUSALS = frozenset(
|
|
400
|
+
{
|
|
401
|
+
"read-failed",
|
|
402
|
+
"repository-mismatch",
|
|
403
|
+
"epic-not-open",
|
|
404
|
+
"children-bound",
|
|
405
|
+
"children-count",
|
|
406
|
+
"blockers-bound",
|
|
407
|
+
"blockers-count",
|
|
408
|
+
"inconsistent",
|
|
409
|
+
"cycle",
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class ClaimConflictError(AutoForgeError):
|
|
415
|
+
"""A durable-claim read is inconclusive or ambiguous (``autoforge.claims``).
|
|
416
|
+
|
|
417
|
+
Raised when the objects carrying a marker kind cannot answer "which one
|
|
418
|
+
is this phase's write": a marker that could not be read (defective
|
|
419
|
+
object), several objects claiming the same key, or none where exactly
|
|
420
|
+
one is required. A phase entry turns it into ``BLOCKED`` without
|
|
421
|
+
launching an agent; a read-back turns it into :class:`VerificationError`.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class VerificationError(AutoForgeError):
|
|
426
|
+
"""Post-execution verification failed (HEAD mismatch, PR state, guards).
|
|
427
|
+
|
|
428
|
+
Also used for Phase-1 safety gates (e.g. merge not explicitly allowed).
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class CheckoutDriftError(VerificationError):
|
|
433
|
+
"""The operator's checkout (HEAD or branch) moved while an agent ran.
|
|
434
|
+
|
|
435
|
+
Raised by the engine around a REMOTE agent invocation; the step enters
|
|
436
|
+
BLOCKED with the message as its reason. The invocation's own outcome, if
|
|
437
|
+
it failed, is the ``__cause__`` and is quoted in the message.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class SandboxTamperError(VerificationError):
|
|
442
|
+
"""An issue's private object directory holds what no git object store does.
|
|
443
|
+
|
|
444
|
+
A directory that is not a real one reached from the common dir through
|
|
445
|
+
real directories (a link, another file type, one that cannot be
|
|
446
|
+
checked), an ``info/alternates`` or ``info/http-alternates`` file, or an
|
|
447
|
+
entry that is neither a directory, a regular file nor an overlay
|
|
448
|
+
whiteout (ADR 0005 D6.5): git would read another repository's objects
|
|
449
|
+
with the operator's authority. Raised before anything is read from it;
|
|
450
|
+
the step enters BLOCKED with the message, which names the entry.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class CandidateNotMaterializableError(VerificationError):
|
|
455
|
+
"""A candidate's objects are not in the operator's shared object store.
|
|
456
|
+
|
|
457
|
+
The commit, or a blob its tree names, is missing from the store the
|
|
458
|
+
controller's own git reads (ADR 0006 D6.1): a candidate still only in a
|
|
459
|
+
private object directory, before the strict import, is one. Raised by
|
|
460
|
+
:mod:`autoforge.materialize` before anything is written for a missing
|
|
461
|
+
commit; a refusal of that candidate, not a reason to materialize it again.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class MaterializationError(VerificationError):
|
|
466
|
+
"""A candidate's tree was not written out, or not proven to be what was written.
|
|
467
|
+
|
|
468
|
+
Raised by :mod:`autoforge.materialize` (ADR 0006 D6.2, D6.3) for a
|
|
469
|
+
listing it refuses (an entry name that is ``.``, ``..`` or ``.git``, a
|
|
470
|
+
name holding ``/``, an unknown mode), a write that failed (a full disk,
|
|
471
|
+
a name the filesystem merged with another) or a directory whose bytes
|
|
472
|
+
and modes do not hash back to the commit's tree. Inconclusive: no
|
|
473
|
+
evidence, and the message names the commit and the failing entry.
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class ValidationDefinitionError(AutoForgeError):
|
|
478
|
+
"""A validation definition is beyond the bounds a record can hold (ADR 0006 D10.3).
|
|
479
|
+
|
|
480
|
+
More than 16 definitions in a set, more than 32 elements in an argv, a
|
|
481
|
+
blank element, an element over 1,024 characters or an argv over 4,096
|
|
482
|
+
in all. The configuration loader turns it into a
|
|
483
|
+
:class:`ConfigurationError`; a LOCAL run contract persisted before the
|
|
484
|
+
bounds existed that holds one enters ``BLOCKED`` before any validator is
|
|
485
|
+
launched. The message names the definition and the bound, never the argv.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class ValidationInconclusiveError(VerificationError):
|
|
490
|
+
"""Candidate validation observed no result (ADR 0006 D10.5).
|
|
491
|
+
|
|
492
|
+
A launch or setup failure the executor or the sandbox backend reported,
|
|
493
|
+
or #165's preflight refusing the launch, after the bounded in-process
|
|
494
|
+
re-check; or a materialization failure (an ``OSError`` or a git process
|
|
495
|
+
the executor could not launch among them), or failing to find the
|
|
496
|
+
shared store or read the candidate's commit there, after one fresh
|
|
497
|
+
attempt. No ``ValidationRecord`` is written. The caller enters
|
|
498
|
+
``BLOCKED`` with the message, which names the command or the candidate
|
|
499
|
+
and the failing entry.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class ValidationInterruptedError(AutoForgeError):
|
|
504
|
+
"""A definition was interrupted more often than recovery may rerun it (ADR 0006 D10.7).
|
|
505
|
+
|
|
506
|
+
Raised when the resume path's ``INTERRUPTED`` record is the third, or
|
|
507
|
+
later, for one candidate, definition and policy (records at or below the
|
|
508
|
+
operator's override watermark not counted). The record is still written:
|
|
509
|
+
``records`` is the list with it appended, which the caller persists in
|
|
510
|
+
the write that enters ``BLOCKED`` with the message, naming the candidate
|
|
511
|
+
and the definition.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
def __init__(self, message: str, records: list | None = None) -> None:
|
|
515
|
+
super().__init__(message)
|
|
516
|
+
self.records = list(records or [])
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class GitTransportError(AutoForgeError):
|
|
520
|
+
"""A controller git operation could not establish what it needs (fails closed).
|
|
521
|
+
|
|
522
|
+
Raised by :mod:`autoforge.git_transport` when a fetch or push fails, a
|
|
523
|
+
commit's bytes do not hash to its id, a range walk exceeds its bound or
|
|
524
|
+
never reaches its base, or GitHub's ancestry answer is unavailable. It is
|
|
525
|
+
never read as "allowed" (ADR 0004 D7.5).
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class EffectConflictError(AutoForgeError):
|
|
530
|
+
"""A controller-owned effect cannot be completed without guessing (ADR 0004 D4.2).
|
|
531
|
+
|
|
532
|
+
The effect's identity resolves to an object whose payload or state differs
|
|
533
|
+
from the record, to two objects, or the precondition no longer holds and
|
|
534
|
+
the attempt bound leaves no issue. The message names the object and the
|
|
535
|
+
rule, never the payload; the phase enters ``BLOCKED`` with it.
|
|
536
|
+
"""
|