@giamme/forge 0.0.0-stage → 0.8.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env python3
2
+ """Readiness scheduler. Only the coordinator merges; tasks keep pinned baselines.
3
+
4
+ Two opt-in behaviours live here, both off by default so a plain `run` is unchanged:
5
+
6
+ * ``--retry-failed N`` re-queues a task whose reviewer said FAIL/UNKNOWN up to N more
7
+ times *inside the same run*, so one failing task never holds the others back until the
8
+ run exits. Dependents already wait for MERGED, so nothing downstream starts early.
9
+ * ``--infra-retries M`` waits out an infrastructure failure (quota, auth, rate limit,
10
+ network, empty output) up to M times instead of stopping. Without it the scheduler
11
+ stops dispatching, lets running tasks drain and exits 8: nothing was the task's fault
12
+ and no attempt was spent, so the next `run` simply resumes.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import fcntl
18
+ import os
19
+ from pathlib import Path
20
+ import posixpath
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+
26
+
27
+ def dataset_event(name: str, plan: Path, task: str, values: dict) -> None:
28
+ """Record a status only the scheduler sets (INTERRUPTED, BLOCKED, ERROR) in the run
29
+ dataset. No task process is alive to record it. Fail-open, like every capture."""
30
+ try:
31
+ scripts = str(Path(__file__).resolve().parent)
32
+ if scripts not in sys.path:
33
+ sys.path.insert(0, scripts)
34
+ from forge_dataset.capture import point
35
+ point(name, str(plan), task, values)
36
+ except Exception:
37
+ pass
38
+
39
+
40
+ def overlap(a: str, b: str) -> bool:
41
+ def paths(value: str) -> list[str]:
42
+ return [posixpath.normpath(p) for p in value.split(',') if p and p != '-']
43
+
44
+ return any(
45
+ x == '.' or y == '.' or x == y or x.startswith(y + '/') or y.startswith(x + '/')
46
+ for x in paths(a) for y in paths(b)
47
+ )
48
+
49
+
50
+ INFRA_EXIT = 8
51
+ # A parsed retry-after beyond this is not a pause worth waiting for inside one run.
52
+ INFRA_WAIT_CAP = 6 * 3600
53
+
54
+
55
+ def infra_info(root: Path) -> dict:
56
+ """key=value lines the task wrote when it stopped on an infrastructure failure."""
57
+ info = {}
58
+ try:
59
+ for line in (root / 'infra.txt').read_text(errors='replace').splitlines():
60
+ key, _, value = line.partition('=')
61
+ if key:
62
+ info[key.strip()] = value.strip()
63
+ except OSError:
64
+ pass
65
+ return info
66
+
67
+
68
+ def infra_wait(info: dict, cycle: int) -> float:
69
+ """Seconds to wait before re-dispatching after an infrastructure failure."""
70
+ try:
71
+ base = float(os.environ.get('FORGE_INFRA_BACKOFF', '60'))
72
+ except ValueError:
73
+ base = 60.0
74
+ try:
75
+ hinted = float(info.get('retry_after', ''))
76
+ except ValueError:
77
+ hinted = None
78
+ if hinted is not None and hinted >= 0:
79
+ return min(hinted, INFRA_WAIT_CAP)
80
+ return min(base * (2 ** cycle), max(base, 1800.0))
81
+
82
+
83
+ def schedule(script: str, plan: str, capacity: int, retry_failed: int = 0,
84
+ infra_retries: int = 0) -> int:
85
+ if capacity <= 0:
86
+ raise ValueError('capacity must be positive')
87
+ if retry_failed < 0 or infra_retries < 0:
88
+ raise ValueError('retry counts must not be negative')
89
+ plan = Path(plan)
90
+ rows = [
91
+ line.split('\t') for line in (plan / 'tasks.tsv').read_text().splitlines()
92
+ if line and not line.startswith('#')
93
+ ]
94
+ if any(len(row) != 7 for row in rows):
95
+ raise ValueError('tasks.tsv requires seven columns')
96
+ tasks = {row[0]: row for row in rows}
97
+ if len(tasks) != len(rows):
98
+ raise ValueError('duplicate task IDs')
99
+ for name in tasks:
100
+ if not name or name in ('.', '..') or '/' in name:
101
+ raise ValueError('invalid task ID: ' + name)
102
+ (plan / 'tasks' / name).mkdir(parents=True, exist_ok=True)
103
+ active = {}
104
+ pending = list(tasks)
105
+ interrupted = False
106
+ retries = {} # automatic reviewer-driven retries used per task, this run
107
+ retried = set() # tasks whose next dispatch is a retry (FORGE_FRACTAL_RETRY=1)
108
+ offsets = {} # how much of each task.out was already echoed under --output full
109
+ infra_seen = [] # tasks stopped by an infrastructure failure, awaiting resume
110
+ infra_cycles = 0 # how many times this run already waited out an infrastructure pause
111
+ paused = False # an infrastructure failure was seen: dispatch nothing new
112
+ resume_at = 0.0
113
+
114
+ def status(name: str) -> str:
115
+ root = plan / 'tasks' / name
116
+ if (root / 'merged').exists():
117
+ return 'MERGED'
118
+ file = root / 'status'
119
+ return file.read_text().strip() if file.exists() else 'PENDING'
120
+
121
+ def mark(name: str, state: str) -> None:
122
+ (plan / 'tasks' / name / 'status').write_text(state + '\n')
123
+ dataset_event('sched_terminal', plan, name, dict(status=state))
124
+
125
+ def merge(name: str) -> None:
126
+ result = subprocess.run(['/bin/bash', script, '_merge', str(plan), name])
127
+ if result.returncode and status(name) == 'PASS':
128
+ mark(name, 'ERROR')
129
+
130
+ def retryable(name: str) -> bool:
131
+ return (
132
+ retry_failed > 0
133
+ and retries.get(name, 0) < retry_failed
134
+ and status(name) in ('FAIL', 'UNKNOWN')
135
+ # A task that produced no new work (empty diff, byte-identical retry) would
136
+ # only repeat itself; its marker says so and it is left for a human.
137
+ and not (plan / 'tasks' / name / 'noretry').exists()
138
+ # The retry policy found no stronger model after repeated failures on one: a
139
+ # split, or an explicit `retry --dwarf`, is the human's call.
140
+ and not (plan / 'tasks' / name / 'escalation.stop').exists()
141
+ )
142
+
143
+ def requeue(name: str) -> bool:
144
+ """Prepare a retry exactly as `retry` does, then put the task at the queue head."""
145
+ result = subprocess.run(['/bin/bash', script, '_prepare_retry', str(plan), name])
146
+ if result.returncode:
147
+ return False
148
+ retries[name] = retries.get(name, 0) + 1
149
+ retried.add(name)
150
+ if name not in pending:
151
+ pending.insert(0, name)
152
+ print('forge: retrying ' + name + ' (automatic retry ' + str(retries[name]) + '/'
153
+ + str(retry_failed) + ')', file=sys.stderr, flush=True)
154
+ return True
155
+
156
+ def echo_output(name: str) -> None:
157
+ # task.out is opened in append mode, so a retried task's file keeps its earlier
158
+ # attempts; print only what is new.
159
+ try:
160
+ data = (plan / 'tasks' / name / 'task.out').read_bytes()
161
+ except OSError:
162
+ return
163
+ start = offsets.get(name, 0)
164
+ offsets[name] = len(data)
165
+ print(data[start:].decode('utf-8', 'replace'), end='', flush=True)
166
+
167
+ def stop(signum: int, frame: object) -> None:
168
+ nonlocal interrupted
169
+ interrupted = True
170
+
171
+ signal.signal(signal.SIGTERM, stop)
172
+ signal.signal(signal.SIGINT, stop)
173
+ try:
174
+ # Failures require an explicit retry unless this run asked for automatic ones.
175
+ # Saved PASS results use the same fingerprint checks without paying for
176
+ # implementation or QA again; an INFRA stop is not a failure of the work, so it
177
+ # is simply dispatched again.
178
+ for name in pending[:]:
179
+ state = status(name)
180
+ if state == 'PASS':
181
+ merge(name)
182
+ if state == 'RUNNING':
183
+ mark(name, 'INTERRUPTED')
184
+ if state in ('FAIL', 'UNKNOWN') and retryable(name):
185
+ requeue(name)
186
+ if status(name) not in ('PENDING', 'BLOCKED', 'INFRA'):
187
+ pending.remove(name)
188
+ while pending or active:
189
+ if interrupted:
190
+ break
191
+ for name, (process, log) in list(active.items()):
192
+ if process.poll() is not None:
193
+ log.close()
194
+ del active[name]
195
+ if os.environ.get('FORGE_OUTPUT') == 'full':
196
+ echo_output(name)
197
+ state = status(name)
198
+ if state == 'PASS':
199
+ merge(name)
200
+ elif state == 'RUNNING':
201
+ mark(name, 'ERROR')
202
+ elif state == 'INFRA':
203
+ paused = True
204
+ if name not in infra_seen:
205
+ infra_seen.append(name)
206
+ elif state in ('FAIL', 'UNKNOWN') and retryable(name):
207
+ requeue(name)
208
+ if paused and not active and infra_seen and infra_cycles < infra_retries:
209
+ # Everything running has drained. Wait out the outage once for all the
210
+ # tasks it stopped, then dispatch them again.
211
+ wait = max(infra_wait(infra_info(plan / 'tasks' / n), infra_cycles) for n in infra_seen)
212
+ infra_cycles += 1
213
+ print('forge: infrastructure failure on ' + ', '.join(infra_seen)
214
+ + '; waiting ' + ('%g' % wait) + 's before resuming (no attempt spent)',
215
+ file=sys.stderr, flush=True)
216
+ resume_at = time.time() + wait
217
+ while time.time() < resume_at and not interrupted:
218
+ time.sleep(min(0.05, max(resume_at - time.time(), 0)))
219
+ if interrupted:
220
+ break
221
+ for name in reversed(infra_seen):
222
+ if name not in pending:
223
+ pending.insert(0, name)
224
+ infra_seen.clear()
225
+ paused = False
226
+ for name in pending[:]:
227
+ if paused or len(active) >= capacity or interrupted:
228
+ break
229
+ deps = [d for d in tasks[name][1].split(',') if d and d != '-']
230
+ if any(status(dep) != 'MERGED' for dep in deps):
231
+ continue
232
+ if any(overlap(tasks[name][3], tasks[other][3]) for other in active):
233
+ continue
234
+ root = plan / 'tasks' / name
235
+ if not (root / 'base_ref').exists():
236
+ worktree = Path((plan / 'wt_root').read_text().strip()) / '_integration'
237
+ base = subprocess.check_output(['git', '-C', str(worktree), 'rev-parse', 'HEAD'])
238
+ (root / 'base_ref').write_bytes(base)
239
+ log = open(root / 'task.out', 'ab')
240
+ # A retried task runs with the same environment `retry` gives it, so the
241
+ # no-progress guard (byte-identical diff) applies under Fractal too.
242
+ env = (dict(os.environ, FORGE_FRACTAL_RETRY='1', FORGE_DATASET_TRIGGER='auto')
243
+ if name in retried else None)
244
+ process = subprocess.Popen(
245
+ ['/bin/bash', script, '_task', str(plan), name],
246
+ stdout=log, stderr=subprocess.STDOUT, start_new_session=True, env=env,
247
+ )
248
+ active[name] = process, log
249
+ pending.remove(name)
250
+ print('forge: dispatched ' + name + ' -> ' + str(root), file=sys.stderr, flush=True)
251
+ if not active:
252
+ if paused:
253
+ # Infrastructure stop: leave everything not yet run PENDING. Marking it
254
+ # BLOCKED would turn "the provider was down" into a recorded failure.
255
+ break
256
+ for name in pending:
257
+ mark(name, 'BLOCKED')
258
+ break
259
+ time.sleep(0.05)
260
+ finally:
261
+ for process, log in active.values():
262
+ try:
263
+ os.killpg(process.pid, signal.SIGTERM)
264
+ except ProcessLookupError:
265
+ pass
266
+ for name, (process, log) in active.items():
267
+ try:
268
+ process.wait(timeout=5)
269
+ except subprocess.TimeoutExpired:
270
+ pass
271
+ # Remove descendants even if the shell exited before its backend.
272
+ try:
273
+ os.killpg(process.pid, signal.SIGKILL)
274
+ except ProcessLookupError:
275
+ pass
276
+ process.wait()
277
+ log.close()
278
+ mark(name, 'INTERRUPTED')
279
+ if interrupted:
280
+ return 130
281
+ if paused or any(status(name) == 'INFRA' for name in tasks):
282
+ return INFRA_EXIT
283
+ return 0 if all(status(name) == 'MERGED' for name in tasks) else 5
284
+
285
+
286
+ def main() -> None:
287
+ try:
288
+ if sys.argv[1] == 'lock':
289
+ _, script, command, plan, *args = sys.argv[1:]
290
+ # The shell inherits the lock for its entire operation, including
291
+ # preflight, setup, scheduling, verification and cleanup.
292
+ lock = open(Path(plan) / 'schedule.lock', 'a')
293
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
294
+ os.set_inheritable(lock.fileno(), True)
295
+ env = dict(os.environ, FORGE_PLAN_LOCK=plan)
296
+ os.execve('/bin/bash', ['/bin/bash', script, command, plan, *args], env)
297
+ parser = argparse.ArgumentParser(prog='forge-schedule.py')
298
+ parser.add_argument('script')
299
+ parser.add_argument('plan')
300
+ parser.add_argument('capacity', type=int)
301
+ parser.add_argument('--retry-failed', type=int, default=0, metavar='N')
302
+ parser.add_argument('--infra-retries', type=int, default=0, metavar='M')
303
+ args = parser.parse_args(sys.argv[1:])
304
+ sys.exit(schedule(args.script, args.plan, args.capacity, args.retry_failed,
305
+ args.infra_retries))
306
+ except (OSError, ValueError, KeyError) as error:
307
+ print('forge scheduler: ' + str(error), file=sys.stderr)
308
+ sys.exit(3)
309
+
310
+
311
+ if __name__ == '__main__':
312
+ main()
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env python3
2
+ """A shadow review: a second, independent review of a sampled task attempt, never consumed.
3
+
4
+ Nothing in forge has ever reviewed one diff twice, so how often two reviewers agree
5
+ -- the noise in every review label -- is unknown. `plan --shadow-qa <spec>[,<spec>...]`
6
+ turns this on for a sampled share of reviewed attempts (`--shadow-rate`, default 0.1).
7
+
8
+ forge-shadow.py run --plan P --task ID --worktree W --base B --reviewed R --attempt N --qa SPEC [--yolo]
9
+ Started in the background by the task, just before the primary review. Exits at once
10
+ unless this attempt is sampled. Otherwise it builds its own review snapshot and run
11
+ dir under <plan>/shadow/<id>/<attempt>-<k>/, reviews a byte-identical copy of the
12
+ primary's qa.input, and returns. Never exits nonzero.
13
+ forge-shadow.py preflight --plan P
14
+ Which listed shadow specs resolve here; writes <plan>/shadow_qa.ok. Never fails a run.
15
+
16
+ Guarantees, because the task waits for this process before it writes any status:
17
+ - nothing reads the shadow's result in the run path; it changes no status, memory,
18
+ ledger, gate or retry, and its files never share a name or a dir with the primary's
19
+ - it holds no inherited file descriptor (close_fds), so no lock outlives the task
20
+ - it dies with the task: when its parent goes away (an interrupt, a killed scheduler,
21
+ Ctrl-C on a manual retry, which never reaches a background job) it stops the review
22
+ - it is bounded: once the task signals that the primary review finished (SIGUSR1), the
23
+ shadow gets as long again as the primary took, at least SHADOW_GRACE_S, then stops
24
+ <shadow dir>/shadow.status (and a `shadow_review` dataset event) says how it ended: done,
25
+ timeout, error, invalid (the reviewer changed its snapshot), overtime, stopped, orphaned.
26
+ Only a `done` review counts as a second opinion.
27
+ Forge never picks a model: the reviewer is one the user listed, preferring a different
28
+ model family from the primary reviewer's, so two reviewers do not share one blind spot.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import hashlib
34
+ import json
35
+ import os
36
+ import shutil
37
+ import signal
38
+ import subprocess
39
+ import sys
40
+ import tempfile
41
+ import time
42
+ from pathlib import Path
43
+
44
+ SCRIPTS = Path(__file__).resolve().parent
45
+ DISPATCH = SCRIPTS / 'forge-dispatch.sh'
46
+ ARTIFACT = SCRIPTS / 'forge-artifact.sh'
47
+ SHADOW_GRACE_S = float(os.environ.get('FORGE_SHADOW_GRACE_S') or 300)
48
+ STOP_WAIT_S = 10
49
+ PREPARE_TIMEOUT_S = 120
50
+
51
+
52
+ def rate_of(plan: Path) -> float:
53
+ try:
54
+ rate = float((plan / 'shadow_rate').read_text().strip())
55
+ except (OSError, ValueError):
56
+ rate = 0.1
57
+ return min(max(rate, 0.0), 1.0)
58
+
59
+
60
+ def sampled(plan: Path, task: str, attempt: str, rate: float) -> bool:
61
+ """Deterministic per attempt: a QA-only resume reaches the same answer."""
62
+ run = ''
63
+ for name in ('dataset.uid', 'run_id'):
64
+ try:
65
+ run = (plan / name).read_text().strip()
66
+ break
67
+ except OSError:
68
+ continue
69
+ digest = hashlib.sha256(('%s:%s:%s' % (run, task, attempt)).encode()).digest()
70
+ return int.from_bytes(digest[:8], 'big') < rate * 2 ** 64
71
+
72
+
73
+ def family(model: str) -> str:
74
+ m = (model or '').lower().rsplit('/', 1)[-1]
75
+ if m.startswith(('claude', 'opus', 'sonnet', 'haiku', 'fable')):
76
+ return 'claude'
77
+ if m.startswith(('gpt', 'sol', 'astra', 'luna', 'terra', 'o1', 'o3', 'o4')) or 'gpt-' in m:
78
+ return 'gpt'
79
+ if m.startswith('gemini'):
80
+ return 'gemini'
81
+ if m.startswith('grok'):
82
+ return 'grok'
83
+ return m
84
+
85
+
86
+ def resolved_model(spec: str) -> str | None:
87
+ """The model a qa dispatch of `spec` would use, by dispatch's own dry run."""
88
+ with tempfile.TemporaryDirectory(prefix='forge-shadow-resolve-') as tmp:
89
+ prompt = Path(tmp) / 'input'
90
+ prompt.write_text('Offline resolution')
91
+ try:
92
+ out = subprocess.run(['/bin/bash', str(DISPATCH), 'qa', spec, '--run-dir', tmp, '--prompt-file',
93
+ str(prompt), '--dry-run'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
94
+ timeout=60, text=True)
95
+ except (OSError, subprocess.SubprocessError):
96
+ return None
97
+ fields = dict(line.split('=', 1) for line in out.stdout.splitlines() if '=' in line)
98
+ return fields.get('model') or None
99
+
100
+
101
+ def candidates(plan: Path) -> list:
102
+ path = plan / 'shadow_qa.ok' if (plan / 'shadow_qa.ok').exists() else plan / 'shadow_qa'
103
+ try:
104
+ text = path.read_text()
105
+ except OSError:
106
+ return []
107
+ return [s.strip() for s in text.replace('\n', ',').split(',') if s.strip()]
108
+
109
+
110
+ def pick(listed: list, primary: str) -> tuple[str | None, str | None]:
111
+ """(spec, why): the first listed spec of another family than the primary's, else the first."""
112
+ if not listed:
113
+ return None, None
114
+ mine = family(resolved_model(primary) or primary)
115
+ for spec in listed:
116
+ if family(resolved_model(spec) or spec) != mine:
117
+ return spec, 'other_family'
118
+ return listed[0], 'same_family'
119
+
120
+
121
+ def fresh_dir(base: Path, attempt: str) -> Path:
122
+ """A dir no earlier try used: a snapshot refuses an existing destination."""
123
+ base.mkdir(parents=True, exist_ok=True)
124
+ k = 1
125
+ while True:
126
+ candidate = base / ('%s-%d' % (attempt, k))
127
+ try:
128
+ candidate.mkdir()
129
+ return candidate
130
+ except FileExistsError:
131
+ k += 1
132
+
133
+
134
+ def bash(function: str, *args) -> subprocess.CompletedProcess:
135
+ return subprocess.run(['/bin/bash', '-c', 'source "$1"; shift; "$@"', '_', str(ARTIFACT), function, *args],
136
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=PREPARE_TIMEOUT_S)
137
+
138
+
139
+ def write(path: Path, text: str) -> None:
140
+ try:
141
+ path.write_text(text)
142
+ except OSError:
143
+ pass
144
+
145
+
146
+ def dataset_event(out: Path, task: str, values: dict) -> None:
147
+ """How the shadow ended, in the run dataset. Its dispatch was captured when it exited,
148
+ before the snapshot was checked again, so an `invalid` verdict can only be told here."""
149
+ try:
150
+ if str(SCRIPTS) not in sys.path:
151
+ sys.path.insert(0, str(SCRIPTS))
152
+ from forge_dataset.capture import point
153
+ point('shadow_review', str(out), task, values)
154
+ except Exception:
155
+ pass
156
+
157
+
158
+ class Stopped(Exception):
159
+ pass
160
+
161
+
162
+ STATE = dict(child=None, primary_done=None, stop=False, started=time.time())
163
+
164
+
165
+ def _primary_done(_signum, _frame):
166
+ if STATE['primary_done'] is None:
167
+ STATE['primary_done'] = time.time()
168
+
169
+
170
+ def _stop(_signum, _frame):
171
+ # Before the review starts there is nothing to stop but this process; afterwards the
172
+ # watch loop stops the review itself, so the status file is always written.
173
+ if STATE['child'] is None:
174
+ raise Stopped()
175
+ STATE['stop'] = True
176
+
177
+
178
+ def run(args) -> int:
179
+ # First, before anything slow: the task may signal that its review finished at any time,
180
+ # and SIGUSR1's default action would kill a launcher that is still preparing.
181
+ signal.signal(signal.SIGUSR1, _primary_done)
182
+ signal.signal(signal.SIGTERM, _stop)
183
+ plan = Path(args.plan)
184
+ parent = os.getppid()
185
+ status, out, spec, why = 'skipped', None, None, None
186
+ try:
187
+ if os.environ.get('FORGE_FRACTAL_RUN') or not sampled(plan, args.task, args.attempt, rate_of(plan)):
188
+ return 0
189
+ spec, why = pick(candidates(plan), args.qa)
190
+ if not spec:
191
+ return 0
192
+ out = fresh_dir(plan / 'shadow' / args.task, args.attempt)
193
+ meta = dict(task=args.task, attempt=args.attempt, reviewed=args.reviewed, primary=args.qa, spec=spec,
194
+ pick=why, started=time.time())
195
+ write(out / 'shadow.json', json.dumps(meta, indent=1))
196
+ review = out / 'review-snapshot' # review-*: the importer never walks into a snapshot
197
+ if bash('forge_review_snapshot', args.worktree, args.base, args.reviewed, str(review)).returncode:
198
+ status = 'error'
199
+ return 0
200
+ before = bash('forge_fingerprint', str(review)).stdout.strip()
201
+ shutil.copyfile(Path(args.tdir) / 'qa.input', out / 'qa.input')
202
+ env = dict(os.environ, FORGE_SHADOW='1', FORGE_SHADOW_ATTEMPT=args.attempt,
203
+ FORGE_SHADOW_REVIEWED=args.reviewed, FORGE_SHADOW_FROM=args.qa)
204
+ argv = ['/bin/bash', str(DISPATCH), 'qa', spec, '--repo', str(review), '--run-dir', str(out),
205
+ '--review-base', (out / 'review.base').read_text().strip(),
206
+ '--ripwire-query-file', str(Path(args.tdir) / 'prompt.md'), '--prompt-file', str(out / 'qa.input'),
207
+ '--output', 'summary']
208
+ if args.yolo:
209
+ argv.append('--yolo')
210
+ with open(out / 'shadow.out', 'wb') as log:
211
+ STATE['child'] = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT,
212
+ env=env, close_fds=True)
213
+ status = watch(STATE['child'], parent)
214
+ after = bash('forge_fingerprint', str(review)).stdout.strip()
215
+ if status == 'done' and (not before or after != before):
216
+ status = 'invalid' # the reviewer changed its snapshot: its verdict counts for nothing
217
+ except Stopped:
218
+ status = 'stopped'
219
+ except Exception as error: # a shadow never fails the task
220
+ status = 'error'
221
+ if out is not None:
222
+ write(out / 'shadow.error', '%s: %s\n' % (type(error).__name__, error))
223
+ finally:
224
+ if out is not None:
225
+ write(out / 'shadow.status', status + '\n')
226
+ dataset_event(out, args.task, dict(status=status, attempt=args.attempt, reviewed=args.reviewed,
227
+ primary=args.qa, spec=spec, pick=why))
228
+ return 0
229
+
230
+
231
+ def grace_ends() -> float | None:
232
+ """When the shadow must stop: as long again as the primary took, at least SHADOW_GRACE_S."""
233
+ done = STATE['primary_done']
234
+ if done is None:
235
+ return None
236
+ return done + max(SHADOW_GRACE_S, done - STATE['started'])
237
+
238
+
239
+ def watch(child: subprocess.Popen, parent: int) -> str:
240
+ """Wait for the review; stop it when the task dies or stops it, or its grace runs out."""
241
+ while True:
242
+ try:
243
+ child.wait(timeout=1)
244
+ return 'done' if child.returncode == 0 else ('timeout' if child.returncode == 7 else 'error')
245
+ except subprocess.TimeoutExpired:
246
+ pass
247
+ ends = grace_ends()
248
+ if os.getppid() != parent:
249
+ reason = 'orphaned' # the task is gone: an interrupt, or a killed scheduler
250
+ elif STATE['stop']:
251
+ reason = 'stopped'
252
+ elif ends is not None and time.time() >= ends:
253
+ reason = 'overtime'
254
+ else:
255
+ continue
256
+ child.terminate() # dispatch's TERM trap reaps the harness's process group
257
+ try:
258
+ child.wait(timeout=STOP_WAIT_S)
259
+ except subprocess.TimeoutExpired:
260
+ child.kill()
261
+ child.wait()
262
+ return reason
263
+
264
+
265
+ def preflight(args) -> int:
266
+ plan = Path(args.plan)
267
+ listed = [s.strip() for s in (plan / 'shadow_qa').read_text().replace('\n', ',').split(',') if s.strip()] \
268
+ if (plan / 'shadow_qa').exists() else []
269
+ ok = []
270
+ for spec in listed:
271
+ flags = ['--yolo'] if (plan / 'yolo_qa').exists() else []
272
+ try:
273
+ result = subprocess.run(['/bin/bash', str(DISPATCH), 'doctor', '--spec', spec, '--role', 'qa', *flags],
274
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120)
275
+ good = result.returncode == 0
276
+ except (OSError, subprocess.SubprocessError):
277
+ good = False
278
+ if good:
279
+ ok.append(spec)
280
+ else:
281
+ print('forge: shadow review spec %s is not usable here; it is skipped' % spec, file=sys.stderr)
282
+ write(plan / 'shadow_qa.ok', ','.join(ok) + '\n')
283
+ if listed and not ok:
284
+ print('forge: no shadow review spec is usable; shadow reviews are off for this run', file=sys.stderr)
285
+ return 0
286
+
287
+
288
+ def main(argv=None) -> int:
289
+ ap = argparse.ArgumentParser(prog='forge-shadow.py')
290
+ sub = ap.add_subparsers(dest='command', required=True)
291
+ p = sub.add_parser('run')
292
+ for name in ('plan', 'task', 'worktree', 'base', 'reviewed', 'attempt', 'qa', 'tdir'):
293
+ p.add_argument('--' + name, required=True)
294
+ p.add_argument('--yolo', action='store_true')
295
+ p = sub.add_parser('preflight')
296
+ p.add_argument('--plan', required=True)
297
+ args = ap.parse_args(argv)
298
+ try:
299
+ return run(args) if args.command == 'run' else preflight(args)
300
+ except Exception:
301
+ return 0
302
+
303
+
304
+ if __name__ == '__main__':
305
+ sys.exit(main())