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.
Files changed (81) hide show
  1. autoforge/__init__.py +5 -0
  2. autoforge/agent_worktree.py +653 -0
  3. autoforge/certificate.py +495 -0
  4. autoforge/child_env.py +871 -0
  5. autoforge/claims.py +726 -0
  6. autoforge/claude_stream.py +501 -0
  7. autoforge/cli.py +2884 -0
  8. autoforge/config.py +2178 -0
  9. autoforge/config_fingerprint.py +374 -0
  10. autoforge/doctor.py +1551 -0
  11. autoforge/doctor_sandbox.py +691 -0
  12. autoforge/effect_ops.py +1272 -0
  13. autoforge/effects.py +3210 -0
  14. autoforge/engine.py +16637 -0
  15. autoforge/epic_commands.py +1486 -0
  16. autoforge/epic_graph.py +852 -0
  17. autoforge/errors.py +536 -0
  18. autoforge/evidence.py +1722 -0
  19. autoforge/executor.py +1601 -0
  20. autoforge/executor_duplex.py +985 -0
  21. autoforge/executor_supervised.py +144 -0
  22. autoforge/git_transport.py +970 -0
  23. autoforge/github.py +3402 -0
  24. autoforge/interaction.py +1027 -0
  25. autoforge/local_workspace.py +1253 -0
  26. autoforge/locking.py +1050 -0
  27. autoforge/loop_detect.py +660 -0
  28. autoforge/loop_guard.py +404 -0
  29. autoforge/materialize.py +722 -0
  30. autoforge/ownership.py +240 -0
  31. autoforge/pi_rpc.py +713 -0
  32. autoforge/premerge.py +181 -0
  33. autoforge/profiles.py +106 -0
  34. autoforge/progress.py +392 -0
  35. autoforge/prompts/__init__.py +173 -0
  36. autoforge/prompts/acceptance_actions.md +39 -0
  37. autoforge/prompts/analyze_execute.md +128 -0
  38. autoforge/prompts/common.md +132 -0
  39. autoforge/prompts/correction.md +12 -0
  40. autoforge/prompts/fix.md +252 -0
  41. autoforge/prompts/local_analyze_execute.md +69 -0
  42. autoforge/prompts/local_common.md +149 -0
  43. autoforge/prompts/local_fix.md +96 -0
  44. autoforge/prompts/local_review.md +103 -0
  45. autoforge/prompts/operator_acceptance.md +61 -0
  46. autoforge/prompts/replan_reexecute.md +991 -0
  47. autoforge/prompts/review.md +312 -0
  48. autoforge/prompts/update_epic.md +120 -0
  49. autoforge/prompts/update_epic_rerequest.md +85 -0
  50. autoforge/prompts/validation_evidence.md +26 -0
  51. autoforge/prompts/validation_failure.md +22 -0
  52. autoforge/prompts/worktree_state.md +45 -0
  53. autoforge/providers.py +2893 -0
  54. autoforge/read_context.py +389 -0
  55. autoforge/redaction.py +408 -0
  56. autoforge/replan.py +208 -0
  57. autoforge/replan_txn.py +1930 -0
  58. autoforge/result_parser.py +2234 -0
  59. autoforge/roadmap.py +151 -0
  60. autoforge/run_contract.py +408 -0
  61. autoforge/run_namespace.py +839 -0
  62. autoforge/runlog.py +764 -0
  63. autoforge/safefs.py +1717 -0
  64. autoforge/sandbox.py +1313 -0
  65. autoforge/sandbox_bwrap.py +815 -0
  66. autoforge/sandbox_pin.py +170 -0
  67. autoforge/sandbox_policy.py +1107 -0
  68. autoforge/scheduler.py +1143 -0
  69. autoforge/scheduler_state.py +2316 -0
  70. autoforge/scheduler_stop.py +421 -0
  71. autoforge/sdk_policy.py +988 -0
  72. autoforge/state.py +2417 -0
  73. autoforge/transitions.py +388 -0
  74. autoforge/validation.py +324 -0
  75. autoforge/worker.py +568 -0
  76. autoforge_cli-0.1.0a0.dist-info/METADATA +525 -0
  77. autoforge_cli-0.1.0a0.dist-info/RECORD +81 -0
  78. autoforge_cli-0.1.0a0.dist-info/WHEEL +5 -0
  79. autoforge_cli-0.1.0a0.dist-info/entry_points.txt +2 -0
  80. autoforge_cli-0.1.0a0.dist-info/licenses/LICENSE +21 -0
  81. 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
+ """