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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""A supervised child process: the scheduler's handle on one worker (ADR 0008 D9, D13; #182).
|
|
2
|
+
|
|
3
|
+
Executor layer, with no workflow semantics: it starts one process, signals
|
|
4
|
+
it and observes its exit, and knows nothing of what the process does.
|
|
5
|
+
|
|
6
|
+
* :func:`spawn_supervised` starts ``argv`` in a session of its own
|
|
7
|
+
(``start_new_session=True``), with every descriptor closed but the three
|
|
8
|
+
standard streams and the ones named in ``pass_fds`` (``close_fds=True``),
|
|
9
|
+
stdin on ``/dev/null`` and stdout and stderr both on ``stdout_fd`` (the
|
|
10
|
+
worker log; there is no pipe between the supervisor and the child). The
|
|
11
|
+
new session keeps the terminal's signals away from the child: only the
|
|
12
|
+
supervisor's own :meth:`SupervisedChild.signal` reaches it. Signal
|
|
13
|
+
dispositions are inherited as ``subprocess`` leaves them (an ignored
|
|
14
|
+
SIGINT stays ignored): a child that must be interruptible installs its
|
|
15
|
+
handlers itself, as the worker does.
|
|
16
|
+
* :class:`SupervisedChild` signals only a child that this process has not
|
|
17
|
+
reaped yet, so its pid cannot have been reused, and never sends
|
|
18
|
+
``SIGKILL``: a worker that is stopped returns through its own teardown.
|
|
19
|
+
* :func:`process_start_time` is the kernel's start time of a pid, for the
|
|
20
|
+
claim's diagnostics; never authority.
|
|
21
|
+
|
|
22
|
+
Unlike the agent launches of :mod:`autoforge.executor` this is not a
|
|
23
|
+
contained invocation: the supervisor is no subreaper, kills no process
|
|
24
|
+
group and runs no sandbox (ADR 0002 is unchanged). A worker runs its own
|
|
25
|
+
contained agent invocations, one at a time.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
import signal as signals
|
|
32
|
+
import subprocess
|
|
33
|
+
import time
|
|
34
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
35
|
+
|
|
36
|
+
from .errors import ExecutionError
|
|
37
|
+
|
|
38
|
+
Popen = Callable[..., subprocess.Popen[bytes]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SupervisedChild:
|
|
42
|
+
"""One started child, until it is reaped; what :func:`spawn_supervised` returns."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, process: subprocess.Popen[bytes]) -> None:
|
|
45
|
+
self._process = process
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def pid(self) -> int:
|
|
49
|
+
return self._process.pid
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def returncode(self) -> int | None:
|
|
53
|
+
"""The exit status once reaped: ``0``-``255``, or minus the signal that ended it."""
|
|
54
|
+
return self._process.returncode
|
|
55
|
+
|
|
56
|
+
def poll(self) -> int | None:
|
|
57
|
+
"""Reap the child if it has exited; its exit status, or ``None`` while it lives."""
|
|
58
|
+
return self._process.poll()
|
|
59
|
+
|
|
60
|
+
def wait(self, deadline: float) -> int | None:
|
|
61
|
+
"""Wait until the child exits or ``time.monotonic()`` reaches ``deadline``.
|
|
62
|
+
|
|
63
|
+
The exit status, or ``None`` when the deadline came first; a
|
|
64
|
+
deadline already past is one :meth:`poll`.
|
|
65
|
+
"""
|
|
66
|
+
remaining = deadline - time.monotonic()
|
|
67
|
+
if remaining <= 0:
|
|
68
|
+
return self.poll()
|
|
69
|
+
try:
|
|
70
|
+
return self._process.wait(timeout=remaining)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def signal(self, sig: int) -> bool:
|
|
75
|
+
"""Send ``sig`` to the child; ``False`` (nothing sent) once it was reaped.
|
|
76
|
+
|
|
77
|
+
``SIGKILL`` is refused (ValueError): a supervised child is never
|
|
78
|
+
killed, it is interrupted and returns through its own teardown.
|
|
79
|
+
"""
|
|
80
|
+
if sig == signals.SIGKILL:
|
|
81
|
+
raise ValueError("a supervised child is never sent SIGKILL")
|
|
82
|
+
if self._process.poll() is not None:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
self._process.send_signal(sig)
|
|
86
|
+
except ProcessLookupError: # pragma: no cover - unreaped, so it exists
|
|
87
|
+
return False
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def spawn_supervised(
|
|
92
|
+
argv: Sequence[str],
|
|
93
|
+
*,
|
|
94
|
+
pass_fds: Sequence[int],
|
|
95
|
+
stdout_fd: int,
|
|
96
|
+
cwd: str | os.PathLike[str],
|
|
97
|
+
env: Mapping[str, str],
|
|
98
|
+
popen: Popen = subprocess.Popen,
|
|
99
|
+
) -> SupervisedChild:
|
|
100
|
+
"""Start ``argv`` as a supervised child (see the module docstring).
|
|
101
|
+
|
|
102
|
+
``argv[0]`` is an absolute path: nothing is looked up on ``PATH``. A
|
|
103
|
+
start that fails (the ``Popen`` call raised) is an
|
|
104
|
+
:class:`~autoforge.errors.ExecutionError`, and no child exists; once
|
|
105
|
+
this returns, one does. ``popen`` is the injection point of tests.
|
|
106
|
+
"""
|
|
107
|
+
command = [str(arg) for arg in argv]
|
|
108
|
+
if not command or not os.path.isabs(command[0]):
|
|
109
|
+
raise ExecutionError(f"a supervised child is started by absolute path, got {command[:1]!r}")
|
|
110
|
+
try:
|
|
111
|
+
process = popen( # noqa: S603 - argv list, no shell
|
|
112
|
+
command,
|
|
113
|
+
stdin=subprocess.DEVNULL,
|
|
114
|
+
stdout=stdout_fd,
|
|
115
|
+
stderr=stdout_fd,
|
|
116
|
+
cwd=os.fspath(cwd),
|
|
117
|
+
env=dict(env),
|
|
118
|
+
pass_fds=tuple(pass_fds),
|
|
119
|
+
close_fds=True,
|
|
120
|
+
start_new_session=True,
|
|
121
|
+
)
|
|
122
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
123
|
+
raise ExecutionError(f"failed to start {command[0]}: {exc}") from exc
|
|
124
|
+
return SupervisedChild(process)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def process_start_time(pid: int) -> int | None:
|
|
128
|
+
"""``pid``'s start time in clock ticks since boot (``/proc/<pid>/stat`` field 22).
|
|
129
|
+
|
|
130
|
+
``None`` where it cannot be read (no ``/proc``, the process is gone).
|
|
131
|
+
The pair ``(pid, start time)`` names one process across a pid reuse.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
with open(f"/proc/{int(pid)}/stat", "rb") as fh:
|
|
135
|
+
stat = fh.read()
|
|
136
|
+
except OSError:
|
|
137
|
+
return None
|
|
138
|
+
# ``pid (comm) state ppid ...``: comm may hold spaces and parentheses, so
|
|
139
|
+
# the fields are counted from the last ``)``.
|
|
140
|
+
fields = stat[stat.rfind(b")") + 2 :].split()
|
|
141
|
+
try:
|
|
142
|
+
return int(fields[19])
|
|
143
|
+
except (IndexError, ValueError):
|
|
144
|
+
return None
|