@matt82198/aesop 0.3.2 → 0.4.0
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.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +5 -2
- package/docs/INSTALL.md +104 -14
- package/docs/MICROKERNEL.md +452 -0
- package/docs/README.md +4 -0
- package/docs/THE-AESOP-HYPOTHESIS.md +140 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +1 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""proc_util -- bounded shell execution shared by the concrete AgentDriver adapters.
|
|
3
|
+
|
|
4
|
+
run_shell_bounded() backs run_command for ClaudeCodeDriver and CodexDriver
|
|
5
|
+
(OpenAICompatibleDriver inherits it). It exists because plain
|
|
6
|
+
``subprocess.run(command, shell=True, timeout=N)`` does NOT bound wall-clock on
|
|
7
|
+
Windows: on timeout CPython kills only the shell (cmd.exe), then its cleanup
|
|
8
|
+
``communicate()`` re-blocks until the orphaned grandchild -- the actual
|
|
9
|
+
test/git process -- closes the inherited pipe handles. A deadlocked grandchild
|
|
10
|
+
therefore stalled the caller indefinitely: the exact wave-hang class the
|
|
11
|
+
timeout was meant to prevent (RS-A F1; live-proven: timeout_s=0.5 vs a 6s
|
|
12
|
+
sleep returned exit 124 only after 6.1s).
|
|
13
|
+
|
|
14
|
+
Guarantees:
|
|
15
|
+
* The timeout truly bounds wall-clock (plus a small fixed drain allowance).
|
|
16
|
+
* The whole process TREE is killed on timeout: the child is spawned in its
|
|
17
|
+
own group/session, then ``taskkill /T /F`` on Windows (kills grandchildren
|
|
18
|
+
by parent-child walk) or ``os.killpg(SIGKILL)`` on POSIX.
|
|
19
|
+
* Exit code 124 on timeout (shell convention); 127 when spawn fails.
|
|
20
|
+
* Partial stdout/stderr captured before the kill is preserved (RS-A F7), so
|
|
21
|
+
a timed-out 119s suite still yields its printed diagnostics instead of a
|
|
22
|
+
blind rerun.
|
|
23
|
+
|
|
24
|
+
stdlib-only, ASCII-only, Windows + Linux safe.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import signal
|
|
29
|
+
import subprocess
|
|
30
|
+
|
|
31
|
+
from agent_driver import CommandResult
|
|
32
|
+
|
|
33
|
+
# Post-kill drain bound. With the tree dead the pipes close and communicate()
|
|
34
|
+
# returns immediately; this cap only protects against a survivor that
|
|
35
|
+
# inherited the handles (e.g. a detached daemon) re-introducing the hang.
|
|
36
|
+
_DRAIN_TIMEOUT_S = 5.0
|
|
37
|
+
|
|
38
|
+
_TIMEOUT_NOTE = "Command timed out after {t}s; process tree killed (exit 124)"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def kill_process_tree(proc):
|
|
42
|
+
"""Best-effort, bounded kill of proc and every descendant."""
|
|
43
|
+
if os.name == "nt":
|
|
44
|
+
try:
|
|
45
|
+
subprocess.run(
|
|
46
|
+
["taskkill", "/T", "/F", "/PID", str(proc.pid)],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
timeout=_DRAIN_TIMEOUT_S,
|
|
49
|
+
)
|
|
50
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
51
|
+
pass
|
|
52
|
+
else:
|
|
53
|
+
try:
|
|
54
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
55
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
56
|
+
pass
|
|
57
|
+
# Belt and braces: direct child, idempotent if already dead.
|
|
58
|
+
try:
|
|
59
|
+
proc.kill()
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _as_text(value):
|
|
65
|
+
"""Normalize partial-output payloads (None/bytes/str) to str."""
|
|
66
|
+
if value is None:
|
|
67
|
+
return ""
|
|
68
|
+
if isinstance(value, bytes):
|
|
69
|
+
return value.decode("utf-8", errors="replace")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_shell_bounded(command, cwd=None, timeout_s=120.0):
|
|
74
|
+
"""Run `command` through the platform shell, hard-bounded by timeout_s.
|
|
75
|
+
|
|
76
|
+
Returns CommandResult. On timeout: exit 124, partial output preserved,
|
|
77
|
+
stderr carries a "Command timed out" note. On spawn failure: exit 127.
|
|
78
|
+
"""
|
|
79
|
+
popen_kwargs = {}
|
|
80
|
+
if os.name == "nt":
|
|
81
|
+
# Own process group so the child tree is a coherent kill target.
|
|
82
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
83
|
+
else:
|
|
84
|
+
# Own session -> killpg reaches every descendant.
|
|
85
|
+
popen_kwargs["start_new_session"] = True
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
proc = subprocess.Popen(
|
|
89
|
+
command,
|
|
90
|
+
cwd=cwd,
|
|
91
|
+
shell=True,
|
|
92
|
+
stdout=subprocess.PIPE,
|
|
93
|
+
stderr=subprocess.PIPE,
|
|
94
|
+
text=True,
|
|
95
|
+
encoding="utf-8",
|
|
96
|
+
errors="replace",
|
|
97
|
+
**popen_kwargs,
|
|
98
|
+
)
|
|
99
|
+
except OSError as exc:
|
|
100
|
+
return CommandResult(exit_code=127, stdout="", stderr=str(exc))
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
stdout, stderr = proc.communicate(timeout=timeout_s)
|
|
104
|
+
return CommandResult(
|
|
105
|
+
exit_code=proc.returncode,
|
|
106
|
+
stdout=stdout or "",
|
|
107
|
+
stderr=stderr or "",
|
|
108
|
+
)
|
|
109
|
+
except subprocess.TimeoutExpired as exc:
|
|
110
|
+
# POSIX populates partial output on the exception; Windows does not
|
|
111
|
+
# (its reader threads still hold it). Keep the exception's copy as a
|
|
112
|
+
# fallback, but prefer the post-kill drain below (complete on both).
|
|
113
|
+
partial_out = _as_text(exc.stdout)
|
|
114
|
+
partial_err = _as_text(exc.stderr)
|
|
115
|
+
|
|
116
|
+
# Kill the WHOLE tree first -- never re-block on live grandchildren.
|
|
117
|
+
kill_process_tree(proc)
|
|
118
|
+
|
|
119
|
+
# Drain what the pipe readers captured. Tree dead -> pipes closed ->
|
|
120
|
+
# this returns promptly; still bounded so a handle-holding survivor
|
|
121
|
+
# cannot re-introduce the unbounded hang.
|
|
122
|
+
try:
|
|
123
|
+
drained_out, drained_err = proc.communicate(timeout=_DRAIN_TIMEOUT_S)
|
|
124
|
+
except (subprocess.TimeoutExpired, ValueError, OSError):
|
|
125
|
+
drained_out, drained_err = None, None
|
|
126
|
+
|
|
127
|
+
out = _as_text(drained_out) or partial_out
|
|
128
|
+
err = _as_text(drained_err) or partial_err
|
|
129
|
+
note = _TIMEOUT_NOTE.format(t=timeout_s)
|
|
130
|
+
return CommandResult(
|
|
131
|
+
exit_code=124,
|
|
132
|
+
stdout=out,
|
|
133
|
+
stderr=(err + "\n" + note) if err else note,
|
|
134
|
+
)
|