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
@@ -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