@giamme/forge 0.0.0-stage → 0.9.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 +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +365 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +671 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +240 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +84 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Live capture: called from forge-runtime.py's begin()/finish() and from point events.
|
|
2
|
+
|
|
3
|
+
Every function here is an entry point a running pipeline calls, so every one of them
|
|
4
|
+
is fail-open: errors go to errors.log, nothing is printed (begin()'s stdout is the
|
|
5
|
+
attempt path the shell captures), and nothing is raised.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import PROVENANCE, enabled, location_refused, log_error, provenance, put_blob, put_file, record
|
|
15
|
+
from . import harvest as h
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _environment() -> dict:
|
|
19
|
+
return dict(parent=os.environ.get('FORGE_DATASET_PARENT') or None,
|
|
20
|
+
inv=os.environ.get('FORGE_DATASET_INVOCATION') or None,
|
|
21
|
+
trigger=os.environ.get('FORGE_DATASET_TRIGGER') or None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def on_begin(attempt: str, root: str, role: str) -> None:
|
|
25
|
+
"""Write the execution's provenance into its immutable attempt dir and a start receipt.
|
|
26
|
+
|
|
27
|
+
The provenance travels with the attempt dir, so a plan split or combined later (which
|
|
28
|
+
copies attempt dirs unchanged) still names the run the execution really belonged to.
|
|
29
|
+
"""
|
|
30
|
+
if not enabled():
|
|
31
|
+
return
|
|
32
|
+
try:
|
|
33
|
+
attempt_dir, root_dir = Path(attempt), Path(root)
|
|
34
|
+
ctx = h.locate(root_dir, role)
|
|
35
|
+
if location_refused(ctx['repo']):
|
|
36
|
+
return
|
|
37
|
+
uid = h.run_uid(ctx['run_dir'], create=True)
|
|
38
|
+
env = _environment()
|
|
39
|
+
if role == 'task' and not env['trigger']:
|
|
40
|
+
env['trigger'] = 'initial'
|
|
41
|
+
# The code is fingerprinted here, when the execution starts: the prompt is built
|
|
42
|
+
# from it now, and finish() runs in another process that may see a later edit.
|
|
43
|
+
prov = dict(
|
|
44
|
+
schema=1, run=uid, run_id=ctx['run_id'], mode=ctx['mode'], scope=ctx['scope'],
|
|
45
|
+
plan=str(ctx['plan']) if ctx['plan'] else None, task=ctx['task'], repo=ctx['repo'],
|
|
46
|
+
link=ctx['link'], role=role, root=str(root_dir), wall_started_ns=time.time_ns(),
|
|
47
|
+
provenance=provenance(), **env,
|
|
48
|
+
)
|
|
49
|
+
if os.environ.get('FORGE_SHADOW') == '1':
|
|
50
|
+
# Carried in the attempt dir, so an import long after still knows what this was.
|
|
51
|
+
prov['shadow'] = dict(attempt=h.as_int(os.environ.get('FORGE_SHADOW_ATTEMPT')),
|
|
52
|
+
reviewed=os.environ.get('FORGE_SHADOW_REVIEWED') or None,
|
|
53
|
+
primary=os.environ.get('FORGE_SHADOW_FROM') or None)
|
|
54
|
+
if role in h.PIPELINE_ROLES:
|
|
55
|
+
prov['files'] = h.snapshot(root_dir)
|
|
56
|
+
if role == 'task':
|
|
57
|
+
prov['attempt_start'] = h.as_int(h.first_line(root_dir / 'attempt')) or 0
|
|
58
|
+
prov['resume'] = h.first_line(root_dir / 'resume') or None
|
|
59
|
+
h.write_json(attempt_dir / 'dataset.json', prov)
|
|
60
|
+
record([dict(rid=attempt_dir.name + ':start', kind='start', ts=time.time(), run=uid,
|
|
61
|
+
run_id=ctx['run_id'], mode=ctx['mode'], task=ctx['task'], parent=env['parent'],
|
|
62
|
+
inv=env['inv'], data=dict(role=role, root=str(root_dir), scope=ctx['scope'],
|
|
63
|
+
trigger=env['trigger'], link=ctx['link']))],
|
|
64
|
+
repo=ctx['repo'], where='begin')
|
|
65
|
+
except Exception as error:
|
|
66
|
+
log_error('begin', error)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def provenance_for(attempt_dir: Path, root_dir: Path, role: str) -> dict:
|
|
70
|
+
"""The provenance begin() wrote, or one reconstructed for an attempt dir without it."""
|
|
71
|
+
prov = h.read_json(attempt_dir / 'dataset.json')
|
|
72
|
+
if prov is None:
|
|
73
|
+
ctx = h.locate(root_dir, role)
|
|
74
|
+
prov = dict(run=h.run_uid(ctx['run_dir'], create=False), run_id=ctx['run_id'], mode=ctx['mode'],
|
|
75
|
+
scope=ctx['scope'], plan=str(ctx['plan']) if ctx['plan'] else None, task=ctx['task'],
|
|
76
|
+
repo=ctx['repo'], link=ctx['link'], files=None)
|
|
77
|
+
return prov
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def events_for(attempt_dir: Path, root_dir: Path, role: str, harness: str, rc, prov: dict, src: str) -> list:
|
|
81
|
+
if harness != 'pipeline':
|
|
82
|
+
if prov.get('shadow'):
|
|
83
|
+
prov['attempt_now'] = prov['shadow'].get('attempt') # its dir has no attempt counter
|
|
84
|
+
elif prov.get('scope') == 'task':
|
|
85
|
+
prov['attempt_now'] = h.as_int(h.first_line(root_dir / 'attempt'))
|
|
86
|
+
return [h.dispatch_event(attempt_dir, root_dir, role, harness, rc, prov, src)]
|
|
87
|
+
if role == 'task':
|
|
88
|
+
return [h.task_event(attempt_dir, root_dir, rc, prov, src)]
|
|
89
|
+
if role == 'solo':
|
|
90
|
+
return [h.solo_event(attempt_dir, root_dir, rc, prov, src)]
|
|
91
|
+
if role == 'verification':
|
|
92
|
+
return h.verification_events(attempt_dir, root_dir, rc, prov, src)
|
|
93
|
+
if role == 'parallel':
|
|
94
|
+
return [h.run_end_event(attempt_dir, root_dir, rc, prov, src)]
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def stamp(events: list, started: dict | None, check_change: bool = True) -> None:
|
|
99
|
+
"""Attribute an execution's events to the code it started with.
|
|
100
|
+
|
|
101
|
+
A live edit between begin and finish would otherwise credit the outcome to code the
|
|
102
|
+
execution never ran; the change itself is kept, so a mixed execution can be dropped.
|
|
103
|
+
An import has no finish-time code to compare with, so it only keeps what begin saw.
|
|
104
|
+
"""
|
|
105
|
+
if not started:
|
|
106
|
+
return
|
|
107
|
+
changed = check_change and started.get('code_sha') != provenance().get('code_sha')
|
|
108
|
+
for event in events:
|
|
109
|
+
for key in PROVENANCE:
|
|
110
|
+
event.setdefault(key, started.get(key))
|
|
111
|
+
if changed:
|
|
112
|
+
event.setdefault('data', {})['code_changed'] = True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def on_finish(attempt: str, root: str, role: str, harness: str, rc) -> None:
|
|
116
|
+
if not enabled():
|
|
117
|
+
return
|
|
118
|
+
try:
|
|
119
|
+
attempt_dir, root_dir = Path(attempt), Path(root)
|
|
120
|
+
prov = provenance_for(attempt_dir, root_dir, role)
|
|
121
|
+
if location_refused(prov.get('repo')):
|
|
122
|
+
return
|
|
123
|
+
prov['wall_ended'] = time.time()
|
|
124
|
+
events = events_for(attempt_dir, root_dir, role, harness, rc, prov, 'live')
|
|
125
|
+
stamp(events, prov.get('provenance'))
|
|
126
|
+
record(events, repo=prov.get('repo'), where='finish')
|
|
127
|
+
except Exception as error:
|
|
128
|
+
log_error('finish', error)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def point(name: str, directory: str | None, task: str | None = None, values: dict | None = None,
|
|
132
|
+
files: dict | None = None, kind: str = 'event', texts: dict | None = None,
|
|
133
|
+
repo: str | None = None) -> None:
|
|
134
|
+
"""A fact that would be gone by the time any EXIT trap could read it.
|
|
135
|
+
|
|
136
|
+
Merge conflicts are aborted, combine rolls back, integrate removes its candidate
|
|
137
|
+
worktree; this records each one at the moment it is still on disk.
|
|
138
|
+
"""
|
|
139
|
+
if not enabled():
|
|
140
|
+
return
|
|
141
|
+
try:
|
|
142
|
+
env = _environment()
|
|
143
|
+
ctx = h.locate(Path(directory)) if directory else dict(run_dir=None, run_id=None, mode=None, repo=None)
|
|
144
|
+
if location_refused(repo or ctx.get('repo')):
|
|
145
|
+
return
|
|
146
|
+
uid = h.run_uid(ctx['run_dir'], create=False) if ctx.get('run_dir') else None
|
|
147
|
+
data = dict(values or {}, name=name)
|
|
148
|
+
refs = {}
|
|
149
|
+
for label, path in (files or {}).items():
|
|
150
|
+
ref = put_file(path, 'diff' if path.endswith('.diff') else 'text')
|
|
151
|
+
if ref:
|
|
152
|
+
refs[label] = ref
|
|
153
|
+
for label, value in (texts or {}).items():
|
|
154
|
+
if value:
|
|
155
|
+
refs[label] = put_blob(value, 'log')
|
|
156
|
+
task = task or ctx.get('task')
|
|
157
|
+
data['blobs'] = refs
|
|
158
|
+
rid = 'event:%s:%s' % (name, h.sha256(json.dumps([uid, task, time.time_ns(), data], sort_keys=True,
|
|
159
|
+
default=str).encode())[:24])
|
|
160
|
+
record([dict(rid=rid, kind=kind, ts=time.time(), run=uid, run_id=ctx.get('run_id'),
|
|
161
|
+
mode=ctx.get('mode'), task=task, parent=env['parent'], inv=env['inv'], data=data)],
|
|
162
|
+
repo=repo or ctx.get('repo'), where='event:' + name)
|
|
163
|
+
except Exception as error:
|
|
164
|
+
log_error('event:' + name, error)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def mint_run_uid(run_dir) -> None:
|
|
168
|
+
"""Create the run dir's dataset uid now, if capture is on. Write-only, fail-open.
|
|
169
|
+
|
|
170
|
+
For decisions recorded before the run's own first capture would mint it (a plan's
|
|
171
|
+
predictions run inside `plan`): without it each would carry a legacy id that never
|
|
172
|
+
joins the attempts the plan goes on to run.
|
|
173
|
+
"""
|
|
174
|
+
if not enabled():
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
h.run_uid(Path(run_dir), create=True)
|
|
178
|
+
except Exception as error:
|
|
179
|
+
log_error('mint_run_uid', error)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def jev(*, site, key, canonical: bytes, ok, reason, latency, model, usage, questions, answers,
|
|
183
|
+
run_dir, acting, shadow, extra=None) -> None:
|
|
184
|
+
"""One Jev request: the full state and questions it judged, and what came back.
|
|
185
|
+
|
|
186
|
+
The request blob's original sha equals jev.jsonl's `key`, so a run dir that still
|
|
187
|
+
exists joins to it, and a judgment can later be replayed against any other model.
|
|
188
|
+
`extra` carries the provider, the occurrence id (a body asked twice is two
|
|
189
|
+
decisions), chunk hashes, and for plan-level sites the task they were about.
|
|
190
|
+
"""
|
|
191
|
+
if not enabled():
|
|
192
|
+
return
|
|
193
|
+
try:
|
|
194
|
+
env = _environment()
|
|
195
|
+
ctx = h.locate(Path(run_dir)) if run_dir else dict(run_dir=None, run_id=None, mode=None, task=None, repo=None)
|
|
196
|
+
if location_refused(ctx.get('repo')):
|
|
197
|
+
return
|
|
198
|
+
uid = h.run_uid(ctx['run_dir'], create=False) if ctx.get('run_dir') else None
|
|
199
|
+
at = time.time()
|
|
200
|
+
rid = 'jev:' + h.sha256(('%s\0%r\0%d\0%d' % (key, at, os.getpid(), time.perf_counter_ns())).encode())[:24]
|
|
201
|
+
data = dict(site=site, key=key, ok=ok, reason=reason, latency_s=latency, model=model, usage=usage,
|
|
202
|
+
questions=questions, answers=answers, acting=acting, shadow=shadow,
|
|
203
|
+
run_dir=str(run_dir) if run_dir else None, blobs=dict(request=put_blob(canonical, 'request')))
|
|
204
|
+
data.update(extra or {})
|
|
205
|
+
record([dict(rid=rid, kind='jev', ts=at, run=uid, run_id=ctx.get('run_id'), mode=ctx.get('mode'),
|
|
206
|
+
task=ctx.get('task') or (extra or {}).get('task'), parent=env['parent'], inv=env['inv'],
|
|
207
|
+
data=data)],
|
|
208
|
+
repo=ctx.get('repo'), where='jev:' + site)
|
|
209
|
+
except Exception as error:
|
|
210
|
+
log_error('jev:' + str(site), error)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def fractal_step(task_dir, node: dict, step_dir, *, phase: str, iteration, rc, cost, resolved) -> None:
|
|
214
|
+
"""One Fractal step, keyed by its own step uuid.
|
|
215
|
+
|
|
216
|
+
node.json is mutable (a retry resets `iteration` to 0), so a step is recorded by
|
|
217
|
+
the uuid of the dir it ran in rather than by (node, iteration), which can repeat.
|
|
218
|
+
Called from worker threads: no signals, a connection per call.
|
|
219
|
+
"""
|
|
220
|
+
if not enabled():
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
task_dir, step_dir = Path(task_dir), Path(step_dir)
|
|
224
|
+
ctx = h.fractal_context(task_dir, create=True)
|
|
225
|
+
if location_refused(ctx['repo']):
|
|
226
|
+
return
|
|
227
|
+
record([h.fractal_step_event(task_dir, node, step_dir, ctx, phase=phase, iteration=iteration, rc=rc,
|
|
228
|
+
cost=cost, resolved=resolved)], repo=ctx['repo'], where='fractal_step')
|
|
229
|
+
except Exception as error:
|
|
230
|
+
log_error('fractal_step', error)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def fractal_outcome(task_dir) -> None:
|
|
234
|
+
"""Every node's final state and the task outcome, once per distinct state."""
|
|
235
|
+
if not enabled():
|
|
236
|
+
return
|
|
237
|
+
try:
|
|
238
|
+
task_dir = Path(task_dir)
|
|
239
|
+
ctx = h.fractal_context(task_dir, create=True)
|
|
240
|
+
if location_refused(ctx['repo']):
|
|
241
|
+
return
|
|
242
|
+
record(h.fractal_task_events(task_dir, ctx), repo=ctx['repo'], where='fractal_outcome')
|
|
243
|
+
except Exception as error:
|
|
244
|
+
log_error('fractal_outcome', error)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def plan_snapshot(plan: str) -> None:
|
|
248
|
+
"""Every routing, decomposition and scheduling decision made before the first dispatch."""
|
|
249
|
+
if not enabled():
|
|
250
|
+
return
|
|
251
|
+
try:
|
|
252
|
+
plan_dir = Path(plan)
|
|
253
|
+
if not (plan_dir / 'tasks.tsv').exists():
|
|
254
|
+
return
|
|
255
|
+
ctx = h.locate(plan_dir)
|
|
256
|
+
if location_refused(ctx['repo']):
|
|
257
|
+
return
|
|
258
|
+
prov = dict(run=h.run_uid(plan_dir, create=True), run_id=ctx['run_id'],
|
|
259
|
+
inv=_environment()['inv'])
|
|
260
|
+
record([h.plan_event(plan_dir, prov)], repo=ctx['repo'], where='plan')
|
|
261
|
+
except Exception as error:
|
|
262
|
+
log_error('plan', error)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def invocation(command: str, directory: str | None, rc, started: float | None, argv: list,
|
|
266
|
+
pid: str | None = None) -> None:
|
|
267
|
+
"""One forge-parallel.sh subcommand: what was asked, how it ended, and what it left.
|
|
268
|
+
|
|
269
|
+
Artifacts a subcommand writes into a dir named after its own pid (final-review-*-PID,
|
|
270
|
+
integrate-*-PID) are read here, after they exist and before anything deletes them.
|
|
271
|
+
"""
|
|
272
|
+
if not enabled():
|
|
273
|
+
return
|
|
274
|
+
try:
|
|
275
|
+
env = _environment()
|
|
276
|
+
plan = Path(directory) if directory and Path(directory).is_dir() else None
|
|
277
|
+
ctx = h.locate(plan) if plan else dict(run_dir=None, run_id=None, mode=None, repo=None)
|
|
278
|
+
if location_refused(ctx.get('repo')):
|
|
279
|
+
return
|
|
280
|
+
# `plan` is what makes a plan dir a run, so it may mint the id; every other command
|
|
281
|
+
# finds the one plan (or a dispatch's begin) already wrote.
|
|
282
|
+
uid = h.run_uid(plan, create=command == 'plan') if plan and (plan / 'tasks.tsv').exists() else None
|
|
283
|
+
data = dict(command=command, argv=argv, exit=h.as_int(rc), plan=str(plan) if plan else None,
|
|
284
|
+
duration_s=round(time.time() - float(started), 3) if started else None)
|
|
285
|
+
refs = {}
|
|
286
|
+
if plan and pid:
|
|
287
|
+
produced = []
|
|
288
|
+
for path in sorted(plan.glob('*-%s' % pid)):
|
|
289
|
+
if not path.is_dir():
|
|
290
|
+
continue
|
|
291
|
+
entry = dict(dir=path.name)
|
|
292
|
+
for name in ('verdict', 'verification.status', 'verification.exit', 'verification.command'):
|
|
293
|
+
value = h.first_line(path / name)
|
|
294
|
+
if value is not None:
|
|
295
|
+
entry[name] = value
|
|
296
|
+
inner = path / 'final-review'
|
|
297
|
+
if inner.is_dir():
|
|
298
|
+
entry['final_review_verdict'] = h.first_line(inner / 'verdict')
|
|
299
|
+
entry['final_review_meta'] = h.kv(inner / 'review.meta') or None
|
|
300
|
+
if (path / 'review.meta').exists():
|
|
301
|
+
entry['review_meta'] = h.kv(path / 'review.meta')
|
|
302
|
+
ref = put_file(path / 'qa.last', 'message')
|
|
303
|
+
if ref:
|
|
304
|
+
refs['final_review_message'] = ref
|
|
305
|
+
ref = put_file(path / 'combined.diff', 'diff')
|
|
306
|
+
if ref:
|
|
307
|
+
refs['final_review_diff'] = ref
|
|
308
|
+
produced.append(entry)
|
|
309
|
+
data['produced'] = produced
|
|
310
|
+
data['final_review_verdict'] = h.first_line(plan / 'final-review.verdict')
|
|
311
|
+
if command == 'accept' and plan and len(argv) > 0:
|
|
312
|
+
task = next((a for a in argv if not a.startswith('-') and (plan / 'tasks' / a).is_dir()), None)
|
|
313
|
+
if task:
|
|
314
|
+
data['task'] = task
|
|
315
|
+
data['accepted_by'] = h.text(plan / 'tasks' / task / 'accepted.by')
|
|
316
|
+
data['status_after'] = h.first_line(plan / 'tasks' / task / 'status')
|
|
317
|
+
ref = put_file(plan / 'tasks' / task / 'known_issues.md')
|
|
318
|
+
if ref:
|
|
319
|
+
refs['known_issues'] = ref
|
|
320
|
+
if command == 'retry' and plan and len(argv) > 0:
|
|
321
|
+
data['task'] = next((a for a in argv if (plan / 'tasks' / a).is_dir()), None)
|
|
322
|
+
data['blobs'] = refs
|
|
323
|
+
record([dict(rid='invocation:%s' % (env['inv'] or h.sha256(repr((command, argv, started)).encode())[:24]),
|
|
324
|
+
kind='invocation', ts=time.time(), run=uid, run_id=ctx.get('run_id'), mode=ctx.get('mode'),
|
|
325
|
+
task=data.get('task'), inv=env['inv'], data=data)],
|
|
326
|
+
repo=ctx.get('repo'), where='invocation:' + command)
|
|
327
|
+
if command == 'plan' and plan:
|
|
328
|
+
plan_snapshot(str(plan))
|
|
329
|
+
except Exception as error:
|
|
330
|
+
log_error('invocation:' + command, error)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Completeness checks for `forge dataset status`.
|
|
2
|
+
|
|
3
|
+
Each check is specific to a mode, because each mode legitimately lacks different
|
|
4
|
+
things: a solo run has no separate verification, and a final review's QA dispatch has
|
|
5
|
+
no task attempt. A check reports what it expected and did not find. It never repairs
|
|
6
|
+
anything, since the run dir that could fill the gap is usually gone.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import dataset_dir
|
|
16
|
+
|
|
17
|
+
RUNNING_GRACE_S = 6 * 3600 # a start this recent may simply still be running
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _events(db: sqlite3.Connection, where: str = '', params: tuple = ()) -> list[dict]:
|
|
21
|
+
cursor = db.execute('SELECT rid, kind, ts, src, run, mode, task, parent, data FROM events %s' % where, params)
|
|
22
|
+
names = [d[0] for d in cursor.description]
|
|
23
|
+
out = []
|
|
24
|
+
for row in cursor:
|
|
25
|
+
event = dict(zip(names, row))
|
|
26
|
+
event['data'] = json.loads(event['data'])
|
|
27
|
+
out.append(event)
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _refs(value) -> list[str]:
|
|
32
|
+
found = []
|
|
33
|
+
if isinstance(value, dict):
|
|
34
|
+
if set(value) >= {'sha', 'n', 'kept'} and isinstance(value.get('sha'), str):
|
|
35
|
+
found.append(value['sha'])
|
|
36
|
+
for item in value.values():
|
|
37
|
+
found.extend(_refs(item))
|
|
38
|
+
elif isinstance(value, list):
|
|
39
|
+
for item in value:
|
|
40
|
+
found.extend(_refs(item))
|
|
41
|
+
return found
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run(db: sqlite3.Connection, verbose: bool = False) -> dict:
|
|
45
|
+
now = time.time()
|
|
46
|
+
events = _events(db)
|
|
47
|
+
by_rid = {e['rid'] for e in events}
|
|
48
|
+
results = {}
|
|
49
|
+
|
|
50
|
+
unfinished = [e for e in events if e['kind'] == 'start' and e['rid'][:-len(':start')] not in by_rid]
|
|
51
|
+
results['incomplete'] = dict(
|
|
52
|
+
what='executions that started but never recorded an end (killed, or still running)',
|
|
53
|
+
items=sorted(e['rid'][:-len(':start')] for e in unfinished if now - e['ts'] > RUNNING_GRACE_S),
|
|
54
|
+
running=sum(1 for e in unfinished if now - e['ts'] <= RUNNING_GRACE_S))
|
|
55
|
+
|
|
56
|
+
attempts = {e['rid'] for e in events if e['kind'] == 'attempt'}
|
|
57
|
+
orphan_qa = [e['rid'] for e in events if e['kind'] == 'dispatch' and e['data'].get('role') == 'qa'
|
|
58
|
+
and e['data'].get('scope') == 'task' and e['src'] == 'live'
|
|
59
|
+
and e.get('parent') and e['parent'] not in attempts and now - e['ts'] > RUNNING_GRACE_S]
|
|
60
|
+
results['qa_without_attempt'] = dict(what='task QA dispatches whose task attempt was never recorded',
|
|
61
|
+
items=sorted(orphan_qa))
|
|
62
|
+
|
|
63
|
+
ended = {e['run'] for e in events if e['kind'] == 'run_end'}
|
|
64
|
+
merges = {(e['run'], e['task']) for e in events if e['kind'] == 'event' and e['data'].get('name') == 'merge'}
|
|
65
|
+
unmerged = sorted('%s/%s' % (e['run'], e['task']) for e in events
|
|
66
|
+
if e['kind'] == 'attempt' and e['mode'] == 'parallel' and e['src'] == 'live'
|
|
67
|
+
and e['data'].get('status') == 'PASS' and e['run'] in ended
|
|
68
|
+
and (e['run'], e['task']) not in merges)
|
|
69
|
+
results['pass_without_merge'] = dict(what='parallel PASS attempts with no merge outcome in a finished run',
|
|
70
|
+
items=unmerged)
|
|
71
|
+
|
|
72
|
+
last_seen = {}
|
|
73
|
+
for e in events:
|
|
74
|
+
if e['mode'] == 'parallel' and e['kind'] == 'attempt' and e['src'] == 'live':
|
|
75
|
+
last_seen[e['run']] = max(last_seen.get(e['run'], 0), e['ts'])
|
|
76
|
+
results['run_without_end'] = dict(what='parallel runs with task attempts but no run end',
|
|
77
|
+
items=sorted(r for r, ts in last_seen.items()
|
|
78
|
+
if r not in ended and now - ts > RUNNING_GRACE_S))
|
|
79
|
+
|
|
80
|
+
blobs = dataset_dir() / 'blobs'
|
|
81
|
+
missing = sorted({sha for e in events for sha in _refs(e['data'])
|
|
82
|
+
if not (blobs / sha[:2] / (sha + '.gz')).exists()})
|
|
83
|
+
results['missing_blobs'] = dict(what='blob references with no stored blob', items=missing)
|
|
84
|
+
|
|
85
|
+
if not verbose:
|
|
86
|
+
for check in results.values():
|
|
87
|
+
check['count'] = len(check['items'])
|
|
88
|
+
check['items'] = check['items'][:5]
|
|
89
|
+
else:
|
|
90
|
+
for check in results.values():
|
|
91
|
+
check['count'] = len(check['items'])
|
|
92
|
+
return results
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def report(results: dict) -> list[str]:
|
|
96
|
+
lines = []
|
|
97
|
+
problems = {name: c for name, c in results.items() if c['count']}
|
|
98
|
+
running = results.get('incomplete', {}).get('running')
|
|
99
|
+
if not problems:
|
|
100
|
+
lines.append('checks: complete' + (' (%d execution(s) still running)' % running if running else ''))
|
|
101
|
+
return lines
|
|
102
|
+
lines.append('checks: %d issue(s)' % sum(c['count'] for c in problems.values()))
|
|
103
|
+
for name, check in problems.items():
|
|
104
|
+
lines.append(' %s: %d — %s' % (name, check['count'], check['what']))
|
|
105
|
+
for item in check['items']:
|
|
106
|
+
lines.append(' %s' % item)
|
|
107
|
+
if check['count'] > len(check['items']):
|
|
108
|
+
lines.append(' … (--verbose lists all)')
|
|
109
|
+
return lines
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""`forge dataset ...`: inspect the run dataset, import old runs, and the shell's capture hooks.
|
|
2
|
+
|
|
3
|
+
User-facing: status, dump, import, export, outcomes. Internal (called by forge's shell scripts, fail-open,
|
|
4
|
+
silent): event, invocation, plan.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import connect, data_home, dataset_dir, enabled, read_blob
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _size(path: Path) -> int:
|
|
18
|
+
if path.is_file():
|
|
19
|
+
return path.stat().st_size
|
|
20
|
+
return sum(p.stat().st_size for p in path.rglob('*') if p.is_file()) if path.is_dir() else 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _human(n: int) -> str:
|
|
24
|
+
for unit in ('B', 'KB', 'MB', 'GB'):
|
|
25
|
+
if n < 1024 or unit == 'GB':
|
|
26
|
+
return ('%d %s' % (n, unit)) if unit == 'B' else ('%.1f %s' % (n, unit))
|
|
27
|
+
n /= 1024.0
|
|
28
|
+
return str(n)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def status(args) -> int:
|
|
32
|
+
from . import checks
|
|
33
|
+
directory = dataset_dir()
|
|
34
|
+
summary = dict(path=str(directory), enabled=enabled(), exists=(directory / 'dataset.sqlite').exists())
|
|
35
|
+
if summary['exists']:
|
|
36
|
+
db = connect()
|
|
37
|
+
try:
|
|
38
|
+
summary['events'] = dict(db.execute('SELECT kind, COUNT(*) FROM events GROUP BY kind ORDER BY kind'))
|
|
39
|
+
summary['by_source'] = dict(db.execute('SELECT src, COUNT(*) FROM events GROUP BY src'))
|
|
40
|
+
summary['repos'] = [dict(repo=r, path=p, origin=o) for r, p, o in
|
|
41
|
+
db.execute('SELECT repo, path, origin FROM repos ORDER BY path')]
|
|
42
|
+
summary['runs'] = db.execute('SELECT COUNT(DISTINCT run) FROM events WHERE run IS NOT NULL').fetchone()[0]
|
|
43
|
+
last = db.execute('SELECT MAX(ts) FROM events').fetchone()[0]
|
|
44
|
+
summary['last_event'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last)) if last else None
|
|
45
|
+
summary['checks'] = checks.run(db, verbose=args.verbose)
|
|
46
|
+
if summary['events'].get('dispatch'):
|
|
47
|
+
from . import export
|
|
48
|
+
if db.execute("SELECT 1 FROM events WHERE kind = 'dispatch' AND data LIKE '%qa_shadow%' LIMIT 1").fetchone():
|
|
49
|
+
summary['shadow'] = export.agreement(export.Runs(export.Store(db)))
|
|
50
|
+
finally:
|
|
51
|
+
db.close()
|
|
52
|
+
summary['size'] = dict(database=_size(directory / 'dataset.sqlite') + _size(directory / 'dataset.sqlite-wal'),
|
|
53
|
+
blobs=_size(directory / 'blobs'))
|
|
54
|
+
errors = directory / 'errors.log'
|
|
55
|
+
summary['errors'] = len(errors.read_text(errors='replace').splitlines()) if errors.exists() else 0
|
|
56
|
+
legacy = data_home() / 'legacy'
|
|
57
|
+
summary['legacy_archives'] = len(list(legacy.glob('*.tar.gz'))) if legacy.is_dir() else 0
|
|
58
|
+
if args.json:
|
|
59
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
60
|
+
return 0
|
|
61
|
+
print('forge dataset: %s (%s)' % (summary['path'], 'capturing' if summary['enabled'] else 'off: FORGE_DATASET=off'))
|
|
62
|
+
if not summary['exists']:
|
|
63
|
+
print(' nothing captured yet')
|
|
64
|
+
else:
|
|
65
|
+
print(' size: database %s, blobs %s' % (_human(summary['size']['database']), _human(summary['size']['blobs'])))
|
|
66
|
+
print(' events: %d in %d run(s) across %d repo(s); last %s'
|
|
67
|
+
% (sum(summary['events'].values()), summary['runs'], len(summary['repos']), summary['last_event']))
|
|
68
|
+
print(' ' + ', '.join('%s %d' % kv for kv in summary['events'].items()))
|
|
69
|
+
if summary['by_source'].get('import'):
|
|
70
|
+
print(' %d imported from old run dirs' % summary['by_source']['import'])
|
|
71
|
+
for line in checks.report(summary['checks']):
|
|
72
|
+
print(' ' + line)
|
|
73
|
+
shadow = summary.get('shadow')
|
|
74
|
+
if shadow:
|
|
75
|
+
rate = ' (%.0f%% agree)' % (100.0 * shadow['agree'] / shadow['pairs']) if shadow['pairs'] else ''
|
|
76
|
+
print(' shadow: %d second review(s), %d usable pair(s), %d agree%s'
|
|
77
|
+
% (shadow['shadow_reviews'], shadow['pairs'], shadow['agree'], rate))
|
|
78
|
+
for pair, n in shadow['by_pair'].items():
|
|
79
|
+
print(' %s: %d of %d agree' % (pair, n['agree'], n['pairs']))
|
|
80
|
+
if summary['legacy_archives']:
|
|
81
|
+
print(' legacy: %d archived run dir(s) kept in %s (`forge dataset import %s` re-reads them;'
|
|
82
|
+
' events already stored are skipped)' % (summary['legacy_archives'], legacy, legacy))
|
|
83
|
+
if summary['errors']:
|
|
84
|
+
print(' errors: %d capture failure(s) logged in %s' % (summary['errors'], errors))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def dump(args) -> int:
|
|
89
|
+
if not (dataset_dir() / 'dataset.sqlite').exists():
|
|
90
|
+
return 0
|
|
91
|
+
db = connect()
|
|
92
|
+
try:
|
|
93
|
+
where, params = [], []
|
|
94
|
+
for column in ('kind', 'run', 'repo', 'task', 'src'):
|
|
95
|
+
value = getattr(args, column)
|
|
96
|
+
if value:
|
|
97
|
+
where.append('%s = ?' % column)
|
|
98
|
+
params.append(value)
|
|
99
|
+
if args.since:
|
|
100
|
+
where.append('ts >= ?')
|
|
101
|
+
params.append(args.since)
|
|
102
|
+
query = 'SELECT * FROM events%s ORDER BY ts, rid' % (' WHERE ' + ' AND '.join(where) if where else '')
|
|
103
|
+
cursor = db.execute(query, params)
|
|
104
|
+
names = [d[0] for d in cursor.description]
|
|
105
|
+
for row in cursor:
|
|
106
|
+
event = dict(zip(names, row))
|
|
107
|
+
event['data'] = json.loads(event['data'])
|
|
108
|
+
print(json.dumps(event, sort_keys=True))
|
|
109
|
+
finally:
|
|
110
|
+
db.close()
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def blob(args) -> int:
|
|
115
|
+
sys.stdout.buffer.write(read_blob(args.sha))
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _pairs(items: list | None) -> dict:
|
|
120
|
+
out = {}
|
|
121
|
+
for item in items or []:
|
|
122
|
+
key, _, value = item.partition('=')
|
|
123
|
+
if key:
|
|
124
|
+
out[key] = value
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def event(args) -> int:
|
|
129
|
+
from .capture import point
|
|
130
|
+
point(args.name, args.dir, args.task, _pairs(args.set), _pairs(args.file), repo=args.repo)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def invocation(args) -> int:
|
|
135
|
+
from .capture import invocation as record_invocation
|
|
136
|
+
record_invocation(args.command, args.dir, args.rc, args.started, args.argv, args.pid)
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def plan(args) -> int:
|
|
141
|
+
from .capture import plan_snapshot
|
|
142
|
+
plan_snapshot(args.dir)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_import(args) -> int:
|
|
147
|
+
from . import importer
|
|
148
|
+
return importer.main(args)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_export(args) -> int:
|
|
152
|
+
from . import export
|
|
153
|
+
return export.main(args)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_outcomes(args) -> int:
|
|
157
|
+
from . import outcomes
|
|
158
|
+
return outcomes.main(args)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run_routing(args) -> int:
|
|
162
|
+
from . import routing
|
|
163
|
+
return routing.main(args)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parser() -> argparse.ArgumentParser:
|
|
167
|
+
top = argparse.ArgumentParser(prog='forge dataset', description='The run dataset: every run\'s inputs and outcomes.')
|
|
168
|
+
sub = top.add_subparsers(dest='command', required=True)
|
|
169
|
+
p = sub.add_parser('status', help='where the dataset is, what it holds, and what looks incomplete')
|
|
170
|
+
p.add_argument('--json', action='store_true')
|
|
171
|
+
p.add_argument('--verbose', '-v', action='store_true', help='list every incomplete item, not just counts')
|
|
172
|
+
p.set_defaults(func=status)
|
|
173
|
+
p = sub.add_parser('dump', help='events as JSON lines')
|
|
174
|
+
for column in ('kind', 'run', 'repo', 'task', 'src'):
|
|
175
|
+
p.add_argument('--' + column)
|
|
176
|
+
p.add_argument('--since', type=float, help='unix time')
|
|
177
|
+
p.set_defaults(func=dump)
|
|
178
|
+
p = sub.add_parser('blob', help='print a stored blob by its sha')
|
|
179
|
+
p.add_argument('sha')
|
|
180
|
+
p.set_defaults(func=blob)
|
|
181
|
+
p = sub.add_parser('import', help='capture run dirs, archives and ledgers that predate live capture')
|
|
182
|
+
p.add_argument('paths', nargs='*', help='run/plan dirs, dirs to scan, or .tar.gz archives')
|
|
183
|
+
p.add_argument('--registry', action='store_true', help='every run dir listed in the Jev runs registry')
|
|
184
|
+
p.add_argument('--fractal', action='store_true', help='the Fractal state dir')
|
|
185
|
+
p.add_argument('--ledger', action='append', default=[], metavar='REPO', help="a repo's .forge/ledger.tsv")
|
|
186
|
+
p.add_argument('--include-temp', action='store_true', help='also import runs whose repo is a temp dir (test runs)')
|
|
187
|
+
p.add_argument('--dry-run', action='store_true')
|
|
188
|
+
p.set_defaults(func=run_import)
|
|
189
|
+
p = sub.add_parser('export', help='labeled decision cases (System One JSONL) derived from outcomes')
|
|
190
|
+
p.add_argument('--out', help='output dir (default: <data>/exports/forge-decisions)')
|
|
191
|
+
p.add_argument('--slice', dest='slices', action='append', choices=('routing', 'retry', 'review', 'drift', 'citation', 'memory'),
|
|
192
|
+
help='only this slice; repeatable (default: all)')
|
|
193
|
+
p.add_argument('--limit', type=int, help='at most this many cases per slice, spread across runs')
|
|
194
|
+
p.add_argument('--seed', type=int, default=7)
|
|
195
|
+
p.set_defaults(func=run_export)
|
|
196
|
+
p = sub.add_parser('outcomes', help='what happened to integrated work afterwards (read-only git)')
|
|
197
|
+
p.add_argument('--horizon-days', type=int, default=14, help='how long after integrate to look (default 14)')
|
|
198
|
+
p.add_argument('--repo', help='only this repo')
|
|
199
|
+
p.add_argument('--include-temp', action='store_true', help='also repos under temp dirs (test runs)')
|
|
200
|
+
p.add_argument('--dry-run', action='store_true', help='report what would be recorded, write nothing')
|
|
201
|
+
p.set_defaults(func=run_outcomes)
|
|
202
|
+
p = sub.add_parser('routing', help='pass rate and runtime per tier and model, and what the --explore draws say')
|
|
203
|
+
p.add_argument('--repo', help='only this repo')
|
|
204
|
+
p.add_argument('--since', type=float, metavar='DAYS', help='only tasks first attempted in the last DAYS days')
|
|
205
|
+
p.add_argument('--json', action='store_true')
|
|
206
|
+
p.set_defaults(func=run_routing)
|
|
207
|
+
p = sub.add_parser('event', help=argparse.SUPPRESS)
|
|
208
|
+
p.add_argument('name')
|
|
209
|
+
p.add_argument('--dir')
|
|
210
|
+
p.add_argument('--task')
|
|
211
|
+
p.add_argument('--repo')
|
|
212
|
+
p.add_argument('--set', action='append')
|
|
213
|
+
p.add_argument('--file', action='append')
|
|
214
|
+
p.set_defaults(func=event)
|
|
215
|
+
p = sub.add_parser('invocation', help=argparse.SUPPRESS)
|
|
216
|
+
p.add_argument('command')
|
|
217
|
+
p.add_argument('--dir')
|
|
218
|
+
p.add_argument('--rc')
|
|
219
|
+
p.add_argument('--started')
|
|
220
|
+
p.add_argument('--pid')
|
|
221
|
+
p.add_argument('argv', nargs=argparse.REMAINDER)
|
|
222
|
+
p.set_defaults(func=invocation)
|
|
223
|
+
p = sub.add_parser('plan', help=argparse.SUPPRESS)
|
|
224
|
+
p.add_argument('dir')
|
|
225
|
+
p.set_defaults(func=plan)
|
|
226
|
+
return top
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str] | None = None) -> int:
|
|
230
|
+
args = parser().parse_args(argv)
|
|
231
|
+
if getattr(args, 'argv', None) and args.argv[:1] == ['--']:
|
|
232
|
+
args.argv = args.argv[1:]
|
|
233
|
+
return args.func(args)
|