wydcode 0.4.3
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/LICENSE +100 -0
- package/NOTICE +5 -0
- package/README.md +434 -0
- package/SECURITY.md +64 -0
- package/SOCRA_SOURCE.md +36 -0
- package/TRADEMARKS.md +12 -0
- package/bin/wydcode.js +74 -0
- package/examples/behavior-preference-card.json +12 -0
- package/examples/exact-recurrence-card.json +11 -0
- package/examples/heldout-gene-evidence.json +6 -0
- package/examples/humaneval-ornith.md +29 -0
- package/examples/large_context_json_diagnostic.py +187 -0
- package/fixtures/failing-node-repo/package.json +9 -0
- package/fixtures/failing-node-repo/src/math.js +3 -0
- package/fixtures/failing-node-repo/test/math.test.js +8 -0
- package/package.json +55 -0
- package/pyproject.toml +24 -0
- package/socra_harness/__init__.py +3 -0
- package/socra_harness/attempts.py +76 -0
- package/socra_harness/central_traces.py +627 -0
- package/socra_harness/cli.py +265 -0
- package/socra_harness/eval/__init__.py +1 -0
- package/socra_harness/eval/humaneval.py +388 -0
- package/socra_harness/events.py +39 -0
- package/socra_harness/init.py +206 -0
- package/socra_harness/job/__init__.py +2 -0
- package/socra_harness/job/events.py +132 -0
- package/socra_harness/job/lock.py +90 -0
- package/socra_harness/job/manager.py +3785 -0
- package/socra_harness/job/planner.py +253 -0
- package/socra_harness/job/state.py +106 -0
- package/socra_harness/mumpix_trace_bridge.cjs +32 -0
- package/socra_harness/product.py +533 -0
- package/socra_harness/recurrence.py +263 -0
- package/socra_harness/scan/__init__.py +1 -0
- package/socra_harness/scan/repo.py +85 -0
- package/socra_harness/serve/__init__.py +1 -0
- package/socra_harness/serve/local_proxy.py +293 -0
- package/socra_harness/start.py +331 -0
- package/socra_harness/tui/__init__.py +1 -0
- package/socra_harness/tui/app.py +1114 -0
|
@@ -0,0 +1,3785 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import difflib
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
import uuid
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from ..attempts import AttemptFailure, attempt_failure_payload, classify_creator_finish
|
|
23
|
+
from ..central_traces import export_failed_trace, export_promoted_trace, retrieve_validated_behavior
|
|
24
|
+
from ..recurrence import canonical_json as recurrence_canonical_json
|
|
25
|
+
from ..recurrence import behavior_overlaps_patch
|
|
26
|
+
from ..recurrence import injection_budget_decision
|
|
27
|
+
from ..recurrence import sha256_json as recurrence_sha256_json
|
|
28
|
+
from .events import JobEventLog, read_events
|
|
29
|
+
from .lock import JobLock
|
|
30
|
+
from .planner import canonical_task_hash, materialize_plan_from_events, planner_tasks_for_goal, ready_task_ids, validate_task_graph
|
|
31
|
+
from .state import replay, state_to_json, write_state_snapshot
|
|
32
|
+
|
|
33
|
+
CREATOR_PROMPT_LAYOUT = "stable_prefix_v2_anchored_large_file"
|
|
34
|
+
MAX_JSON_CONTRACT_DECLARED_FILES = 2
|
|
35
|
+
ANCHORED_EDIT_OUTPUT_HEADROOM_RATIO = 0.75
|
|
36
|
+
MAX_ANCHORED_EDITS = 16
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def slugify(value: str) -> str:
|
|
40
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-")
|
|
41
|
+
return slug[:48] or "job"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def job_root(project: Path) -> Path:
|
|
45
|
+
return project / ".socra" / "jobs"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def new_job_id(goal: str) -> str:
|
|
49
|
+
return f"{slugify(goal)}-{uuid.uuid4().hex[:8]}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def copytree_ignore_for_runner(_dir: str, names: list[str]) -> set[str]:
|
|
58
|
+
ignored = {
|
|
59
|
+
".git",
|
|
60
|
+
".socra",
|
|
61
|
+
"node_modules",
|
|
62
|
+
"dist",
|
|
63
|
+
"build",
|
|
64
|
+
".next",
|
|
65
|
+
".turbo",
|
|
66
|
+
".vite",
|
|
67
|
+
"coverage",
|
|
68
|
+
}
|
|
69
|
+
return {name for name in names if name in ignored}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sha256_file(path: Path) -> str:
|
|
73
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sha256_text(text: str) -> str:
|
|
77
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def elapsed_ms(start: float) -> int:
|
|
81
|
+
return int(round((time.monotonic() - start) * 1000))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def task_inventory_package_entry(job_dir: Path | None) -> dict[str, Any] | None:
|
|
85
|
+
if job_dir is None:
|
|
86
|
+
return None
|
|
87
|
+
inventory_path = job_dir / "artifacts" / "tasks" / "task-001" / "inventory.json"
|
|
88
|
+
if not inventory_path.exists():
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
inventory = json.loads(inventory_path.read_text(encoding="utf-8"))
|
|
92
|
+
except Exception: # noqa: BLE001 - conventions are advisory context.
|
|
93
|
+
return None
|
|
94
|
+
for item in inventory.get("declared_files") or []:
|
|
95
|
+
if isinstance(item, dict) and item.get("path") == "package.json":
|
|
96
|
+
return {
|
|
97
|
+
"path": "package.json",
|
|
98
|
+
"sha256": item.get("sha256"),
|
|
99
|
+
"scripts": item.get("scripts") or [],
|
|
100
|
+
"dependencies": item.get("dependencies") or [],
|
|
101
|
+
"dev_dependencies": item.get("dev_dependencies") or [],
|
|
102
|
+
"inventory_path": str(inventory_path.relative_to(job_dir)),
|
|
103
|
+
}
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def project_conventions(project: Path, job_dir: Path | None = None) -> dict[str, Any]:
|
|
108
|
+
package_path = project / "package.json"
|
|
109
|
+
package_type = None
|
|
110
|
+
package_source = "package_json_current_file"
|
|
111
|
+
inventory_package = task_inventory_package_entry(job_dir)
|
|
112
|
+
if package_path.exists() and package_path.is_file():
|
|
113
|
+
try:
|
|
114
|
+
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
115
|
+
package_type = package.get("type") if isinstance(package.get("type"), str) else None
|
|
116
|
+
if inventory_package:
|
|
117
|
+
current_hash = sha256_file(package_path)
|
|
118
|
+
if inventory_package.get("sha256") == current_hash:
|
|
119
|
+
package_source = "task_001_inventory_package_json_hash_match"
|
|
120
|
+
else:
|
|
121
|
+
package_source = "package_json_current_file_inventory_hash_mismatch"
|
|
122
|
+
except Exception: # noqa: BLE001 - conventions are advisory context.
|
|
123
|
+
package_type = None
|
|
124
|
+
javascript_module_system = "esm" if package_type == "module" else "commonjs_or_unspecified"
|
|
125
|
+
return {
|
|
126
|
+
"source": package_source,
|
|
127
|
+
"inventory_package": inventory_package,
|
|
128
|
+
"package_type": package_type,
|
|
129
|
+
"javascript_module_system": javascript_module_system,
|
|
130
|
+
"javascript_import_rule": (
|
|
131
|
+
"Use ESM import/export syntax with explicit .js relative import extensions. "
|
|
132
|
+
"Do not use require(), module.exports, or exports.*."
|
|
133
|
+
if javascript_module_system == "esm"
|
|
134
|
+
else "Follow the existing project module syntax."
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_handoff(job_dir: Path, state: dict[str, Any], plan: dict[str, Any]) -> None:
|
|
140
|
+
tasks = plan.get("tasks") if isinstance(plan.get("tasks"), list) else []
|
|
141
|
+
pending = [task for task in tasks if task.get("status") == "pending"]
|
|
142
|
+
completed = [task for task in tasks if task.get("status") == "passed"]
|
|
143
|
+
creator = state.get("creator_config") if isinstance(state.get("creator_config"), dict) else {}
|
|
144
|
+
text = "\n".join(
|
|
145
|
+
[
|
|
146
|
+
f"# Socra Job Handoff: {state.get('goal')}",
|
|
147
|
+
"",
|
|
148
|
+
f"- Job: `{state.get('job_id')}`",
|
|
149
|
+
f"- Phase: `{state.get('phase')}`",
|
|
150
|
+
f"- Project: `{state.get('project')}`",
|
|
151
|
+
f"- Completed tasks: {len(completed)}",
|
|
152
|
+
f"- Pending tasks: {len(pending)}",
|
|
153
|
+
f"- Active task: `{state.get('active_task_id')}`",
|
|
154
|
+
f"- Creator model: `{creator.get('model_alias') or creator.get('model') or 'unconfigured'}`",
|
|
155
|
+
f"- Creator endpoint: `{creator.get('endpoint') or 'unconfigured'}`",
|
|
156
|
+
"",
|
|
157
|
+
"## Next Action",
|
|
158
|
+
"",
|
|
159
|
+
"Resume can safely replay `events.jsonl` and continue from the first ready pending task.",
|
|
160
|
+
"",
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
(job_dir / "handoff.md").write_text(text, encoding="utf-8")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def starter_plan(goal: str) -> dict[str, Any]:
|
|
167
|
+
return {
|
|
168
|
+
"version": 1,
|
|
169
|
+
"goal": goal,
|
|
170
|
+
"tasks": [
|
|
171
|
+
{
|
|
172
|
+
"id": "task-001",
|
|
173
|
+
"description": "Clarify architecture and create the first implementation slice.",
|
|
174
|
+
"acceptance": ["A concrete implementation plan exists", "Declared files are known before coding"],
|
|
175
|
+
"risk_tier": 0,
|
|
176
|
+
"status": "pending",
|
|
177
|
+
"depends_on": [],
|
|
178
|
+
"declared_files": [],
|
|
179
|
+
"attempt_count": 0,
|
|
180
|
+
"produced_artifacts": [],
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_plan(job_dir: Path) -> dict[str, Any]:
|
|
187
|
+
path = job_dir / "plan.json"
|
|
188
|
+
fallback = {"version": 1, "goal": "", "tasks": []}
|
|
189
|
+
if not path.exists():
|
|
190
|
+
return fallback
|
|
191
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def materialize_job_plan(job_dir: Path) -> dict[str, Any]:
|
|
195
|
+
return materialize_plan_from_events(read_events(job_dir / "events.jsonl"), load_plan(job_dir))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def plan_prereg_metadata(job_dir: Path) -> dict[str, Any]:
|
|
199
|
+
prereg_dir = job_dir / "artifacts" / "preregistration"
|
|
200
|
+
candidates = sorted(prereg_dir.glob("*.json"))
|
|
201
|
+
if not candidates:
|
|
202
|
+
return {}
|
|
203
|
+
path = candidates[0]
|
|
204
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
205
|
+
return {
|
|
206
|
+
"path": str(path.relative_to(job_dir)),
|
|
207
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
208
|
+
"expected_task_count": payload.get("expected_task_count"),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def find_latest_incomplete_job(project: Path) -> Path | None:
|
|
213
|
+
root = job_root(project)
|
|
214
|
+
if not root.exists():
|
|
215
|
+
return None
|
|
216
|
+
candidates = []
|
|
217
|
+
for job_dir in root.iterdir():
|
|
218
|
+
if not job_dir.is_dir() or not (job_dir / "events.jsonl").exists():
|
|
219
|
+
continue
|
|
220
|
+
state = replay(read_events(job_dir / "events.jsonl"))
|
|
221
|
+
if state.phase not in {"done", "failed"}:
|
|
222
|
+
candidates.append((state.last_seq, job_dir))
|
|
223
|
+
if not candidates:
|
|
224
|
+
return None
|
|
225
|
+
return sorted(candidates, key=lambda item: item[0], reverse=True)[0][1]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def job_start_main(args: argparse.Namespace) -> int:
|
|
229
|
+
project = args.project.expanduser().resolve()
|
|
230
|
+
project.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
job_id = args.job_id or new_job_id(args.goal)
|
|
232
|
+
job_dir = job_root(project) / job_id
|
|
233
|
+
job_dir.mkdir(parents=True, exist_ok=False)
|
|
234
|
+
with JobLock(job_dir / ".lock"):
|
|
235
|
+
for folder in ["artifacts", "patches", "sandbox", "checkpoints"]:
|
|
236
|
+
(job_dir / folder).mkdir(parents=True, exist_ok=True)
|
|
237
|
+
|
|
238
|
+
plan = starter_plan(args.goal)
|
|
239
|
+
write_json(job_dir / "plan.json", plan)
|
|
240
|
+
|
|
241
|
+
log = JobEventLog(job_dir / "events.jsonl", job_id)
|
|
242
|
+
log.append("job_created", {"goal": args.goal, "project": str(project)})
|
|
243
|
+
log.append("plan_written", {"plan": "plan.json", "task_count": len(plan["tasks"])})
|
|
244
|
+
log.append("job_paused", {"reason": "start_only_no_execution"})
|
|
245
|
+
|
|
246
|
+
state = replay(read_events(job_dir / "events.jsonl"))
|
|
247
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
248
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
249
|
+
|
|
250
|
+
print(f"Created Socra job: {job_id}")
|
|
251
|
+
print(f"Project: {project}")
|
|
252
|
+
print(f"Job dir: {job_dir}")
|
|
253
|
+
print(f"Phase: {state.phase}")
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def job_resume_main(args: argparse.Namespace) -> int:
|
|
258
|
+
project = args.project.expanduser().resolve()
|
|
259
|
+
if args.job_id:
|
|
260
|
+
job_dir = job_root(project) / args.job_id
|
|
261
|
+
else:
|
|
262
|
+
found = find_latest_incomplete_job(project)
|
|
263
|
+
if not found:
|
|
264
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
265
|
+
job_dir = found
|
|
266
|
+
with JobLock(job_dir / ".lock"):
|
|
267
|
+
events = read_events(job_dir / "events.jsonl")
|
|
268
|
+
state = replay(events)
|
|
269
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
270
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
271
|
+
write_json(job_dir / "plan.json", plan)
|
|
272
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
273
|
+
|
|
274
|
+
print(f"Resumed Socra job: {state.job_id}")
|
|
275
|
+
print(f"Project: {state.project}")
|
|
276
|
+
print(f"Job dir: {job_dir}")
|
|
277
|
+
print(f"Phase: {state.phase}")
|
|
278
|
+
print(f"Events replayed: {len(events)}")
|
|
279
|
+
print(f"Active task: {state.active_task_id or 'none'}")
|
|
280
|
+
print(f"Plan tasks: {len(plan.get('tasks', []))}")
|
|
281
|
+
print(f"Ready tasks: {', '.join(ready_task_ids(plan)[:8]) or 'none'}")
|
|
282
|
+
print(f"Handoff: {job_dir / 'handoff.md'}")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def job_plan_main(args: argparse.Namespace) -> int:
|
|
287
|
+
project = args.project.expanduser().resolve()
|
|
288
|
+
if args.job_id:
|
|
289
|
+
job_dir = job_root(project) / args.job_id
|
|
290
|
+
else:
|
|
291
|
+
found = find_latest_incomplete_job(project)
|
|
292
|
+
if not found:
|
|
293
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
294
|
+
job_dir = found
|
|
295
|
+
|
|
296
|
+
with JobLock(job_dir / ".lock"):
|
|
297
|
+
events = read_events(job_dir / "events.jsonl")
|
|
298
|
+
if any(event.type == "plan_committed" for event in events) and not args.force:
|
|
299
|
+
raise SystemExit("Plan already committed for this job. Pass --force to append a revised plan.")
|
|
300
|
+
|
|
301
|
+
state = replay(events)
|
|
302
|
+
prereg = plan_prereg_metadata(job_dir)
|
|
303
|
+
tasks = planner_tasks_for_goal(state.goal, project)
|
|
304
|
+
validate_task_graph(tasks)
|
|
305
|
+
task_hash = canonical_task_hash(tasks)
|
|
306
|
+
expected_count = prereg.get("expected_task_count")
|
|
307
|
+
actual_count = len(tasks)
|
|
308
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
309
|
+
|
|
310
|
+
previous_commits = [event for event in events if event.type == "plan_committed"]
|
|
311
|
+
revision = len(previous_commits) + 1
|
|
312
|
+
if previous_commits:
|
|
313
|
+
log.append("plan_revised", {"reason": args.reason or "manual_replan", "previous_plan_event_seen": True})
|
|
314
|
+
if args.reason and "count" in args.reason.lower():
|
|
315
|
+
log.append(
|
|
316
|
+
"planner_count_anchor_audit",
|
|
317
|
+
{
|
|
318
|
+
"previous_exact_match_to_prereg": True,
|
|
319
|
+
"template_authored_after_prereg_visible": True,
|
|
320
|
+
"runtime_planner_receives_expected_count": False,
|
|
321
|
+
"remediation": "replan_to_granularity_and_record_count_delta",
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
for task in tasks:
|
|
326
|
+
log.append("task_added", task)
|
|
327
|
+
|
|
328
|
+
if expected_count is not None and int(expected_count) != actual_count:
|
|
329
|
+
log.append(
|
|
330
|
+
"plan_count_delta_recorded",
|
|
331
|
+
{
|
|
332
|
+
"expected_task_count": int(expected_count),
|
|
333
|
+
"actual_task_count": actual_count,
|
|
334
|
+
"delta": actual_count - int(expected_count),
|
|
335
|
+
"note": "Planner follows task granularity; preregistered count is a reference, not a target.",
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
log.append(
|
|
340
|
+
"plan_committed",
|
|
341
|
+
{
|
|
342
|
+
"revision": revision,
|
|
343
|
+
"task_count": actual_count,
|
|
344
|
+
"task_hash": task_hash,
|
|
345
|
+
"preregistration": prereg,
|
|
346
|
+
"planner_rule": "plan_to_granularity_not_preregistered_count",
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
events = read_events(job_dir / "events.jsonl")
|
|
351
|
+
state = replay(events)
|
|
352
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
353
|
+
write_json(job_dir / "plan.json", plan)
|
|
354
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
355
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
356
|
+
|
|
357
|
+
ready = ready_task_ids(plan)
|
|
358
|
+
print(f"Planned Socra job: {state.job_id}")
|
|
359
|
+
print(f"Project: {project}")
|
|
360
|
+
print(f"Job dir: {job_dir}")
|
|
361
|
+
print(f"Tasks: {actual_count}")
|
|
362
|
+
if expected_count is not None:
|
|
363
|
+
print(f"Preregistered reference count: {expected_count}")
|
|
364
|
+
print(f"Task hash: {task_hash}")
|
|
365
|
+
print(f"Ready tasks: {', '.join(ready[:8]) or 'none'}")
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def job_configure_creator_main(args: argparse.Namespace) -> int:
|
|
370
|
+
project = args.project.expanduser().resolve()
|
|
371
|
+
if args.job_id:
|
|
372
|
+
job_dir = job_root(project) / args.job_id
|
|
373
|
+
else:
|
|
374
|
+
found = find_latest_incomplete_job(project)
|
|
375
|
+
if not found:
|
|
376
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
377
|
+
job_dir = found
|
|
378
|
+
|
|
379
|
+
config = {
|
|
380
|
+
"role": "creator",
|
|
381
|
+
"model_alias": args.model_alias,
|
|
382
|
+
"model": args.model,
|
|
383
|
+
"expected_served_model_id": args.expected_served_model_id or Path(str(args.model)).name,
|
|
384
|
+
"endpoint": args.endpoint,
|
|
385
|
+
"backend": args.backend,
|
|
386
|
+
"temperature": args.temperature,
|
|
387
|
+
"seed": args.seed,
|
|
388
|
+
"context_budget_tokens": args.context_budget_tokens,
|
|
389
|
+
"max_output_tokens": args.max_output_tokens,
|
|
390
|
+
"hosted_fallback": bool(args.hosted_fallback),
|
|
391
|
+
"local_first": not bool(args.hosted_fallback),
|
|
392
|
+
"notes": args.notes or "",
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
with JobLock(job_dir / ".lock"):
|
|
396
|
+
events = read_events(job_dir / "events.jsonl")
|
|
397
|
+
state = replay(events)
|
|
398
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
399
|
+
log.append("creator_configured", config)
|
|
400
|
+
events = read_events(job_dir / "events.jsonl")
|
|
401
|
+
state = replay(events)
|
|
402
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
403
|
+
write_json(job_dir / "plan.json", plan)
|
|
404
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
405
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
406
|
+
|
|
407
|
+
print(f"Configured Creator for Socra job: {state.job_id}")
|
|
408
|
+
print(f"Model alias: {args.model_alias}")
|
|
409
|
+
print(f"Model: {args.model}")
|
|
410
|
+
print(f"Endpoint: {args.endpoint}")
|
|
411
|
+
print(f"Temperature: {args.temperature}")
|
|
412
|
+
print(f"Seed: {args.seed if args.seed is not None else 'provider default'}")
|
|
413
|
+
print(f"Context budget tokens: {args.context_budget_tokens}")
|
|
414
|
+
print(f"Hosted fallback: {bool(args.hosted_fallback)}")
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def git_capture(project: Path, args: list[str]) -> tuple[int, str, str]:
|
|
419
|
+
result = subprocess.run(
|
|
420
|
+
["git", *args],
|
|
421
|
+
cwd=project,
|
|
422
|
+
text=True,
|
|
423
|
+
capture_output=True,
|
|
424
|
+
timeout=20,
|
|
425
|
+
check=False,
|
|
426
|
+
)
|
|
427
|
+
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def target_repo_state(project: Path) -> dict[str, Any]:
|
|
431
|
+
state: dict[str, Any] = {"project": str(project)}
|
|
432
|
+
code, out, err = git_capture(project, ["rev-parse", "--show-toplevel"])
|
|
433
|
+
state["is_git_repo"] = code == 0
|
|
434
|
+
state["git_toplevel"] = out if code == 0 else None
|
|
435
|
+
if code != 0:
|
|
436
|
+
state["error"] = err or "not_a_git_repository"
|
|
437
|
+
return state
|
|
438
|
+
for key, git_args in {
|
|
439
|
+
"current_head": ["rev-parse", "HEAD"],
|
|
440
|
+
"current_branch": ["branch", "--show-current"],
|
|
441
|
+
"origin_dev_head": ["rev-parse", "--verify", "refs/remotes/origin/dev"],
|
|
442
|
+
"origin_head": ["rev-parse", "--verify", "refs/remotes/origin/HEAD"],
|
|
443
|
+
}.items():
|
|
444
|
+
code, out, err = git_capture(project, git_args)
|
|
445
|
+
state[key] = out if code == 0 else None
|
|
446
|
+
if code != 0 and key in {"current_head"}:
|
|
447
|
+
state[f"{key}_error"] = err
|
|
448
|
+
code, out, _ = git_capture(project, ["status", "--short"])
|
|
449
|
+
state["dirty"] = bool(out.strip()) if code == 0 else None
|
|
450
|
+
state["status_short"] = out.splitlines() if code == 0 and out else []
|
|
451
|
+
return state
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def default_dayjs_exam_pack_path() -> Path:
|
|
455
|
+
return Path(__file__).resolve().parents[2] / "docs" / "stress_tests" / "dayjs_exam_set_v1.json"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def load_dayjs_exam_pack(path: Path) -> dict[str, Any]:
|
|
459
|
+
return json.loads(path.expanduser().resolve().read_text(encoding="utf-8"))
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def find_dayjs_candidate(pack: dict[str, Any], candidate_id: str) -> dict[str, Any]:
|
|
463
|
+
wanted = candidate_id.lower()
|
|
464
|
+
for candidate in pack.get("candidates") or []:
|
|
465
|
+
keys = {
|
|
466
|
+
str(candidate.get("exam_id") or "").lower(),
|
|
467
|
+
str(candidate.get("id") or "").lower(),
|
|
468
|
+
}
|
|
469
|
+
if wanted in keys:
|
|
470
|
+
return dict(candidate)
|
|
471
|
+
raise SystemExit(f"Unknown dayjs exam candidate: {candidate_id}")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def job_exam_dayjs_main(args: argparse.Namespace) -> int:
|
|
475
|
+
project = args.project.expanduser().resolve()
|
|
476
|
+
pack_path = args.pack.expanduser().resolve() if args.pack else default_dayjs_exam_pack_path()
|
|
477
|
+
pack = load_dayjs_exam_pack(pack_path)
|
|
478
|
+
candidate = find_dayjs_candidate(pack, args.candidate)
|
|
479
|
+
exam_id = str(candidate.get("exam_id") or args.candidate).upper()
|
|
480
|
+
prediction = args.prediction or (pack.get("preregistered_predictions") or {}).get(exam_id) or "unspecified"
|
|
481
|
+
expected_checkout = str(candidate.get("upstream_fix_commit_parent") or "")
|
|
482
|
+
repo_state = target_repo_state(project)
|
|
483
|
+
|
|
484
|
+
if not repo_state.get("is_git_repo"):
|
|
485
|
+
raise SystemExit(f"Project is not a git repository: {project}")
|
|
486
|
+
if repo_state.get("current_head") != expected_checkout and not args.allow_checkout_mismatch:
|
|
487
|
+
raise SystemExit(
|
|
488
|
+
"dayjs exam checkout mismatch: "
|
|
489
|
+
f"HEAD={repo_state.get('current_head')} expected={expected_checkout}. "
|
|
490
|
+
"Checkout the pre-fix parent or pass --allow-checkout-mismatch to record a dry scaffold."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
job_id = args.job_id or f"dayjs-exam-{exam_id.lower()}-{slugify(candidate.get('id') or '')}"
|
|
494
|
+
job_dir = job_root(project) / job_id
|
|
495
|
+
job_dir.mkdir(parents=True, exist_ok=False)
|
|
496
|
+
|
|
497
|
+
source_file = str((candidate.get("declared_files") or [""])[0])
|
|
498
|
+
hidden_test_file = str(candidate.get("hidden_test_file") or "")
|
|
499
|
+
focused_gate = str((pack.get("default_commands") or {}).get("focused_gate_template") or "npx jest <hidden_test_file> --coverage=false").replace(
|
|
500
|
+
"<hidden_test_file>", hidden_test_file
|
|
501
|
+
)
|
|
502
|
+
hidden_gate_extract = str((pack.get("protocol") or {}).get("hidden_gate_extraction_template") or "git show <upstream_fix_commit> -- <hidden_test_file> > hidden-gate.patch")
|
|
503
|
+
hidden_gate_extract = hidden_gate_extract.replace("<upstream_fix_commit>", str(candidate.get("upstream_fix_commit") or "")).replace(
|
|
504
|
+
"<hidden_test_file>", hidden_test_file
|
|
505
|
+
)
|
|
506
|
+
goal = f"dayjs exam {exam_id}: {candidate.get('task_spec', {}).get('title') or candidate.get('id')}"
|
|
507
|
+
task = {
|
|
508
|
+
"id": "exam-task-001",
|
|
509
|
+
"description": (candidate.get("task_spec") or {}).get("issue_summary") or goal,
|
|
510
|
+
"acceptance": [
|
|
511
|
+
"Fix the reported Day.js behavior without seeing the hidden regression patch.",
|
|
512
|
+
f"Focused gate passes after hidden gate is applied: {focused_gate}",
|
|
513
|
+
"Promoted source diff is graded for mechanism shape against upstream, not byte identity.",
|
|
514
|
+
],
|
|
515
|
+
"risk_tier": 1,
|
|
516
|
+
"status": "pending",
|
|
517
|
+
"depends_on": [],
|
|
518
|
+
"declared_files": [source_file],
|
|
519
|
+
"attempt_count": 0,
|
|
520
|
+
"produced_artifacts": [],
|
|
521
|
+
}
|
|
522
|
+
plan = {"version": 1, "goal": goal, "tasks": [task]}
|
|
523
|
+
|
|
524
|
+
with JobLock(job_dir / ".lock"):
|
|
525
|
+
for folder in ["artifacts", "patches", "sandbox", "checkpoints"]:
|
|
526
|
+
(job_dir / folder).mkdir(parents=True, exist_ok=True)
|
|
527
|
+
exam_dir = job_dir / "artifacts" / "exam"
|
|
528
|
+
exam_dir.mkdir(parents=True, exist_ok=True)
|
|
529
|
+
visible_spec = {
|
|
530
|
+
"exam_id": exam_id,
|
|
531
|
+
"candidate_id": candidate.get("id"),
|
|
532
|
+
"difficulty": candidate.get("difficulty"),
|
|
533
|
+
"title": (candidate.get("task_spec") or {}).get("title"),
|
|
534
|
+
"issue_summary": (candidate.get("task_spec") or {}).get("issue_summary"),
|
|
535
|
+
"source_issue_urls": candidate.get("source_issue_urls") or [],
|
|
536
|
+
"creator_declared_files": [source_file],
|
|
537
|
+
"focused_gate_command": focused_gate,
|
|
538
|
+
}
|
|
539
|
+
hidden_manifest = {
|
|
540
|
+
"exam_id": exam_id,
|
|
541
|
+
"candidate_id": candidate.get("id"),
|
|
542
|
+
"upstream_fix_commit": candidate.get("upstream_fix_commit"),
|
|
543
|
+
"upstream_fix_commit_parent": expected_checkout,
|
|
544
|
+
"upstream_commit_url": candidate.get("upstream_commit_url"),
|
|
545
|
+
"hidden_test_file": hidden_test_file,
|
|
546
|
+
"hidden_gate_extraction_command": hidden_gate_extract,
|
|
547
|
+
"upstream_changed_files": candidate.get("declared_files") or [],
|
|
548
|
+
"source_changes": candidate.get("source_changes"),
|
|
549
|
+
"test_changes": candidate.get("test_changes"),
|
|
550
|
+
}
|
|
551
|
+
code, hidden_patch, hidden_patch_err = git_capture(project, ["show", str(candidate.get("upstream_fix_commit") or ""), "--", hidden_test_file])
|
|
552
|
+
if code != 0:
|
|
553
|
+
raise SystemExit(f"Failed to extract hidden gate patch: {hidden_patch_err}")
|
|
554
|
+
hidden_patch_path = exam_dir / "hidden-gate.patch"
|
|
555
|
+
hidden_patch_path.write_text(hidden_patch + ("\n" if hidden_patch and not hidden_patch.endswith("\n") else ""), encoding="utf-8")
|
|
556
|
+
hidden_manifest.update(
|
|
557
|
+
{
|
|
558
|
+
"hidden_gate_patch": "artifacts/exam/hidden-gate.patch",
|
|
559
|
+
"hidden_gate_patch_sha256": sha256_file(hidden_patch_path),
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
write_json(exam_dir / "visible-task-spec.json", visible_spec)
|
|
563
|
+
write_json(exam_dir / "hidden-gate-manifest.json", hidden_manifest)
|
|
564
|
+
write_json(exam_dir / "exam-pack-excerpt.json", {"pack": {"name": pack.get("name"), "version": pack.get("version")}, "candidate": candidate})
|
|
565
|
+
write_json(job_dir / "plan.json", plan)
|
|
566
|
+
|
|
567
|
+
log = JobEventLog(job_dir / "events.jsonl", job_id)
|
|
568
|
+
log.append("job_created", {"goal": goal, "project": str(project)})
|
|
569
|
+
log.append(
|
|
570
|
+
"exam_pack_loaded",
|
|
571
|
+
{
|
|
572
|
+
"exam": "dayjs",
|
|
573
|
+
"pack": str(pack_path),
|
|
574
|
+
"pack_sha256": sha256_file(pack_path),
|
|
575
|
+
"pack_name": pack.get("name"),
|
|
576
|
+
"pack_version": pack.get("version"),
|
|
577
|
+
"verified_on": pack.get("verified_on"),
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
log.append("exam_target_repo_state_recorded", repo_state)
|
|
581
|
+
log.append("exam_candidate_selected", {**hidden_manifest, "visible_task_spec": "artifacts/exam/visible-task-spec.json"})
|
|
582
|
+
log.append(
|
|
583
|
+
"exam_prediction_recorded",
|
|
584
|
+
{
|
|
585
|
+
"exam_id": exam_id,
|
|
586
|
+
"candidate_id": candidate.get("id"),
|
|
587
|
+
"prediction": prediction,
|
|
588
|
+
"prediction_source": "cli_override" if args.prediction else "exam_pack_preregistered_predictions",
|
|
589
|
+
"notes": args.prediction_notes or "",
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
log.append("task_added", task)
|
|
593
|
+
task_hash = canonical_task_hash([task])
|
|
594
|
+
log.append(
|
|
595
|
+
"plan_committed",
|
|
596
|
+
{
|
|
597
|
+
"revision": 1,
|
|
598
|
+
"task_count": 1,
|
|
599
|
+
"task_hash": task_hash,
|
|
600
|
+
"source": "dayjs_exam_pack",
|
|
601
|
+
"candidate": exam_id,
|
|
602
|
+
},
|
|
603
|
+
)
|
|
604
|
+
log.append("job_paused", {"reason": "dayjs_exam_scaffold_created_no_execution"})
|
|
605
|
+
|
|
606
|
+
events = read_events(job_dir / "events.jsonl")
|
|
607
|
+
state = replay(events)
|
|
608
|
+
materialized = materialize_plan_from_events(events, plan)
|
|
609
|
+
write_json(job_dir / "plan.json", materialized)
|
|
610
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
611
|
+
write_handoff(job_dir, state_to_json(state), materialized)
|
|
612
|
+
|
|
613
|
+
print(f"Created dayjs exam job: {job_id}")
|
|
614
|
+
print(f"Candidate: {exam_id} / {candidate.get('id')}")
|
|
615
|
+
print(f"Project HEAD: {repo_state.get('current_head')}")
|
|
616
|
+
print(f"Expected pre-fix checkout: {expected_checkout}")
|
|
617
|
+
print(f"Prediction: {prediction}")
|
|
618
|
+
print(f"Job dir: {job_dir}")
|
|
619
|
+
return 0
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def endpoint_health(endpoint: str) -> dict[str, Any]:
|
|
623
|
+
def parse_model_ids(text: str) -> list[str]:
|
|
624
|
+
try:
|
|
625
|
+
payload = json.loads(text)
|
|
626
|
+
except json.JSONDecodeError:
|
|
627
|
+
return []
|
|
628
|
+
ids: list[str] = []
|
|
629
|
+
for key in ("data", "models"):
|
|
630
|
+
rows = payload.get(key)
|
|
631
|
+
if not isinstance(rows, list):
|
|
632
|
+
continue
|
|
633
|
+
for row in rows:
|
|
634
|
+
if not isinstance(row, dict):
|
|
635
|
+
continue
|
|
636
|
+
for field in ("id", "name", "model"):
|
|
637
|
+
value = row.get(field)
|
|
638
|
+
if isinstance(value, str) and value and value not in ids:
|
|
639
|
+
ids.append(value)
|
|
640
|
+
return ids
|
|
641
|
+
|
|
642
|
+
url = endpoint.rstrip("/") + "/models"
|
|
643
|
+
try:
|
|
644
|
+
with urllib.request.urlopen(url, timeout=2) as response:
|
|
645
|
+
body = response.read().decode("utf-8", errors="replace")
|
|
646
|
+
return {"ok": True, "url": url, "status": response.status, "sample": body[:200], "model_ids": parse_model_ids(body)}
|
|
647
|
+
except Exception as exc: # noqa: BLE001 - health checks should record all failures.
|
|
648
|
+
urllib_error = str(exc)
|
|
649
|
+
try:
|
|
650
|
+
result = subprocess.run(
|
|
651
|
+
["curl", "-sS", "--max-time", "5", "-w", "\n__HTTP_STATUS__:%{http_code}", url],
|
|
652
|
+
text=True,
|
|
653
|
+
capture_output=True,
|
|
654
|
+
timeout=8,
|
|
655
|
+
check=False,
|
|
656
|
+
)
|
|
657
|
+
marker = "\n__HTTP_STATUS__:"
|
|
658
|
+
body, _, raw_status = result.stdout.rpartition(marker)
|
|
659
|
+
status = int(raw_status.strip() or "0")
|
|
660
|
+
if result.returncode == 0 and 200 <= status < 500:
|
|
661
|
+
return {"ok": True, "url": url, "status": status, "sample": body[:200], "model_ids": parse_model_ids(body), "transport": "curl_fallback"}
|
|
662
|
+
return {
|
|
663
|
+
"ok": False,
|
|
664
|
+
"url": url,
|
|
665
|
+
"error": result.stderr.strip() or f"curl_status_{status}",
|
|
666
|
+
"urllib_error": urllib_error,
|
|
667
|
+
"transport": "curl_fallback",
|
|
668
|
+
}
|
|
669
|
+
except Exception as exc: # noqa: BLE001 - health checks should record all failures.
|
|
670
|
+
return {"ok": False, "url": url, "error": str(exc), "urllib_error": urllib_error}
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def inventory_declared_files(project: Path, task: dict[str, Any], artifact_dir: Path) -> list[dict[str, Any]]:
|
|
674
|
+
rows: list[dict[str, Any]] = []
|
|
675
|
+
for rel in task.get("declared_files", []):
|
|
676
|
+
path = project / str(rel)
|
|
677
|
+
row: dict[str, Any] = {
|
|
678
|
+
"path": str(rel),
|
|
679
|
+
"exists": path.exists(),
|
|
680
|
+
}
|
|
681
|
+
if path.exists() and path.is_file():
|
|
682
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
683
|
+
row.update(
|
|
684
|
+
{
|
|
685
|
+
"bytes": path.stat().st_size,
|
|
686
|
+
"lines": len(text.splitlines()),
|
|
687
|
+
"sha256": sha256_file(path),
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
if str(rel) == "server.js":
|
|
691
|
+
row["api_routes"] = sorted(set(re.findall(r"app\.(?:get|post|put|delete|patch)\(['\"]([^'\"]+)", text)))
|
|
692
|
+
row["websocket_mentions"] = len(re.findall(r"WebSocket|broadcast|WebSocketServer", text))
|
|
693
|
+
elif str(rel) == "App.jsx":
|
|
694
|
+
row["state_hooks"] = len(re.findall(r"\buseState\s*\(", text))
|
|
695
|
+
row["effect_hooks"] = len(re.findall(r"\buseEffect\s*\(", text))
|
|
696
|
+
row["api_calls"] = sorted(set(re.findall(r"api(?:Get|Post|Delete|Put)?\(['\"]([^'\"]+)", text)))
|
|
697
|
+
elif str(rel) == "package.json":
|
|
698
|
+
try:
|
|
699
|
+
package = json.loads(text)
|
|
700
|
+
except json.JSONDecodeError:
|
|
701
|
+
package = {}
|
|
702
|
+
row["scripts"] = sorted((package.get("scripts") or {}).keys())
|
|
703
|
+
row["dependencies"] = sorted((package.get("dependencies") or {}).keys())
|
|
704
|
+
row["dev_dependencies"] = sorted((package.get("devDependencies") or {}).keys())
|
|
705
|
+
elif str(rel) == "mumpixdb-core.js":
|
|
706
|
+
row["class_mentions"] = sorted(set(re.findall(r"\bclass\s+([A-Za-z0-9_]+)", text)))
|
|
707
|
+
row["method_mentions"] = sorted(set(re.findall(r"\b(?:async\s+)?([A-Za-z0-9_]+)\s*\(", text)))[:80]
|
|
708
|
+
rows.append(row)
|
|
709
|
+
|
|
710
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
711
|
+
inventory_json = artifact_dir / "inventory.json"
|
|
712
|
+
inventory_md = artifact_dir / "inventory.md"
|
|
713
|
+
inventory_json.write_text(json.dumps({"task": task["id"], "declared_files": rows}, indent=2) + "\n", encoding="utf-8")
|
|
714
|
+
lines = [
|
|
715
|
+
f"# Task {task['id']} Inventory",
|
|
716
|
+
"",
|
|
717
|
+
f"Description: {task.get('description')}",
|
|
718
|
+
"",
|
|
719
|
+
"## Declared Files",
|
|
720
|
+
"",
|
|
721
|
+
]
|
|
722
|
+
for row in rows:
|
|
723
|
+
lines.append(f"### `{row['path']}`")
|
|
724
|
+
lines.append("")
|
|
725
|
+
lines.append(f"- Exists: `{row['exists']}`")
|
|
726
|
+
if row.get("exists"):
|
|
727
|
+
lines.append(f"- Lines: `{row.get('lines')}`")
|
|
728
|
+
lines.append(f"- Bytes: `{row.get('bytes')}`")
|
|
729
|
+
lines.append(f"- SHA256: `{row.get('sha256')}`")
|
|
730
|
+
if row.get("api_routes"):
|
|
731
|
+
lines.append(f"- API routes: {', '.join(f'`{route}`' for route in row['api_routes'])}")
|
|
732
|
+
if row.get("scripts"):
|
|
733
|
+
lines.append(f"- Package scripts: {', '.join(f'`{script}`' for script in row['scripts'])}")
|
|
734
|
+
if row.get("state_hooks") is not None:
|
|
735
|
+
lines.append(f"- React state hooks: `{row.get('state_hooks')}`")
|
|
736
|
+
lines.append(f"- React effect hooks: `{row.get('effect_hooks')}`")
|
|
737
|
+
lines.append("")
|
|
738
|
+
inventory_md.write_text("\n".join(lines), encoding="utf-8")
|
|
739
|
+
return [
|
|
740
|
+
{"path": str(inventory_json.relative_to(artifact_dir.parent.parent)), "sha256": sha256_file(inventory_json)},
|
|
741
|
+
{"path": str(inventory_md.relative_to(artifact_dir.parent.parent)), "sha256": sha256_file(inventory_md)},
|
|
742
|
+
]
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def normalize_declared_path(path: str) -> str:
|
|
746
|
+
value = str(path).replace("\\", "/").strip()
|
|
747
|
+
if not value or value.startswith("/") or value.startswith("../") or "/../" in value or value == "..":
|
|
748
|
+
raise ValueError(f"unsafe relative path: {path}")
|
|
749
|
+
return value
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def declared_file_set(task: dict[str, Any]) -> set[str]:
|
|
753
|
+
return {normalize_declared_path(str(path)) for path in task.get("declared_files", [])}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def latest_passed_artifacts_by_path(job_dir: Path, plan: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
757
|
+
latest: dict[str, dict[str, Any]] = {}
|
|
758
|
+
tasks = plan.get("tasks") if isinstance(plan.get("tasks"), list) else []
|
|
759
|
+
for task in tasks:
|
|
760
|
+
if task.get("status") != "passed":
|
|
761
|
+
continue
|
|
762
|
+
task_id = str(task.get("id") or "")
|
|
763
|
+
for artifact in task.get("produced_artifacts") or []:
|
|
764
|
+
if not isinstance(artifact, dict):
|
|
765
|
+
continue
|
|
766
|
+
rel = artifact.get("path")
|
|
767
|
+
sandbox_path = artifact.get("sandbox_path")
|
|
768
|
+
if not isinstance(rel, str) or not isinstance(sandbox_path, str):
|
|
769
|
+
continue
|
|
770
|
+
absolute_path = job_dir / sandbox_path
|
|
771
|
+
if not absolute_path.exists() or not absolute_path.is_file():
|
|
772
|
+
continue
|
|
773
|
+
latest[normalize_declared_path(rel)] = {
|
|
774
|
+
"path": normalize_declared_path(rel),
|
|
775
|
+
"source": "passed_sandbox_artifact",
|
|
776
|
+
"source_task_id": task_id,
|
|
777
|
+
"sandbox_path": sandbox_path,
|
|
778
|
+
"absolute_path": str(absolute_path),
|
|
779
|
+
"sha256": artifact.get("sha256") or sha256_file(absolute_path),
|
|
780
|
+
}
|
|
781
|
+
return latest
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def read_declared_file_context(project: Path, task: dict[str, Any], effective_sources: dict[str, dict[str, Any]] | None = None) -> list[dict[str, Any]]:
|
|
785
|
+
files: list[dict[str, Any]] = []
|
|
786
|
+
for rel in task.get("declared_files", []):
|
|
787
|
+
rel_path = normalize_declared_path(str(rel))
|
|
788
|
+
source = (effective_sources or {}).get(rel_path)
|
|
789
|
+
path = Path(str(source["absolute_path"])).resolve() if source else (project / rel_path).resolve()
|
|
790
|
+
project_root = project.resolve()
|
|
791
|
+
sandbox_source = bool(source and source.get("source") == "passed_sandbox_artifact")
|
|
792
|
+
if not sandbox_source and project_root != path and project_root not in path.parents:
|
|
793
|
+
raise ValueError(f"declared file escapes project: {rel_path}")
|
|
794
|
+
if path.exists() and path.is_file():
|
|
795
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
796
|
+
files.append(
|
|
797
|
+
{
|
|
798
|
+
"path": rel_path,
|
|
799
|
+
"exists": True,
|
|
800
|
+
"content": content,
|
|
801
|
+
"sha256": sha256_text(content),
|
|
802
|
+
"source": source.get("source") if source else "project_file",
|
|
803
|
+
"source_task_id": source.get("source_task_id") if source else None,
|
|
804
|
+
"sandbox_path": source.get("sandbox_path") if source else None,
|
|
805
|
+
"absolute_path": str(path),
|
|
806
|
+
}
|
|
807
|
+
)
|
|
808
|
+
else:
|
|
809
|
+
files.append(
|
|
810
|
+
{
|
|
811
|
+
"path": rel_path,
|
|
812
|
+
"exists": False,
|
|
813
|
+
"content": "",
|
|
814
|
+
"sha256": None,
|
|
815
|
+
"source": source.get("source") if source else "project_file",
|
|
816
|
+
"source_task_id": source.get("source_task_id") if source else None,
|
|
817
|
+
"sandbox_path": source.get("sandbox_path") if source else None,
|
|
818
|
+
"absolute_path": str(path),
|
|
819
|
+
}
|
|
820
|
+
)
|
|
821
|
+
return files
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def strip_json_fences(text: str) -> str:
|
|
825
|
+
value = text.strip()
|
|
826
|
+
if value.startswith("```"):
|
|
827
|
+
value = re.sub(r"^```(?:[a-zA-Z0-9_-]+)?\s*", "", value)
|
|
828
|
+
value = re.sub(r"\s*```$", "", value)
|
|
829
|
+
return value.strip()
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def strip_json_trailing_commas(text: str) -> str:
|
|
833
|
+
return re.sub(r",(\s*[\]}])", r"\1", text)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def parse_creator_json_with_repair(raw_text: str) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
837
|
+
repair_types: list[str] = []
|
|
838
|
+
text = strip_json_fences(raw_text)
|
|
839
|
+
if text != raw_text.strip():
|
|
840
|
+
repair_types.append("strip_markdown_fence")
|
|
841
|
+
try:
|
|
842
|
+
payload = json.loads(text)
|
|
843
|
+
except json.JSONDecodeError as exc:
|
|
844
|
+
# Local reasoning models often add prose around JSON. Accept a single JSON
|
|
845
|
+
# object if it can be recovered unambiguously; otherwise attribute the
|
|
846
|
+
# failure to the Creator contract, not to the reviewer.
|
|
847
|
+
start = text.find("{")
|
|
848
|
+
end = text.rfind("}")
|
|
849
|
+
if start < 0 or end <= start:
|
|
850
|
+
raise ValueError(f"creator_output_malformed: {exc}") from exc
|
|
851
|
+
candidate = text[start : end + 1]
|
|
852
|
+
if candidate != text:
|
|
853
|
+
repair_types.append("extract_json_object")
|
|
854
|
+
try:
|
|
855
|
+
payload = json.loads(candidate)
|
|
856
|
+
except json.JSONDecodeError as inner:
|
|
857
|
+
stripped_commas = strip_json_trailing_commas(candidate)
|
|
858
|
+
if stripped_commas != candidate:
|
|
859
|
+
repair_types.append("strip_trailing_commas_before_json_closer")
|
|
860
|
+
try:
|
|
861
|
+
payload = json.loads(stripped_commas)
|
|
862
|
+
except json.JSONDecodeError:
|
|
863
|
+
raise ValueError(f"creator_output_malformed: {inner}") from inner
|
|
864
|
+
if not isinstance(payload, dict):
|
|
865
|
+
raise ValueError("creator_output_malformed: root must be an object")
|
|
866
|
+
files = payload.get("files")
|
|
867
|
+
edits = payload.get("edits")
|
|
868
|
+
if not isinstance(files, list) and not isinstance(edits, list):
|
|
869
|
+
raise ValueError("creator_output_malformed: files or edits must be a list")
|
|
870
|
+
if isinstance(files, list) and isinstance(edits, list):
|
|
871
|
+
raise ValueError("creator_output_malformed: files and edits are mutually exclusive")
|
|
872
|
+
if isinstance(files, list):
|
|
873
|
+
for index, file_payload in enumerate(files):
|
|
874
|
+
if not isinstance(file_payload, dict):
|
|
875
|
+
raise ValueError(f"creator_output_malformed: files[{index}] must be an object")
|
|
876
|
+
if not isinstance(file_payload.get("path"), str) or not file_payload.get("path"):
|
|
877
|
+
raise ValueError(f"creator_output_malformed: files[{index}].path must be a non-empty string")
|
|
878
|
+
content = file_payload.get("content")
|
|
879
|
+
content_lines = file_payload.get("content_lines")
|
|
880
|
+
if isinstance(content, str):
|
|
881
|
+
pass
|
|
882
|
+
elif isinstance(content_lines, list) and all(isinstance(line, str) for line in content_lines):
|
|
883
|
+
file_payload["content"] = "\n".join(content_lines)
|
|
884
|
+
file_payload.pop("content_lines", None)
|
|
885
|
+
else:
|
|
886
|
+
raise ValueError(f"creator_output_malformed: files[{index}] needs content string or content_lines string list")
|
|
887
|
+
if isinstance(edits, list):
|
|
888
|
+
for index, edit in enumerate(edits):
|
|
889
|
+
if not isinstance(edit, dict):
|
|
890
|
+
raise ValueError(f"creator_output_malformed: edits[{index}] must be an object")
|
|
891
|
+
if not isinstance(edit.get("path"), str) or not edit.get("path"):
|
|
892
|
+
raise ValueError(f"creator_output_malformed: edits[{index}].path must be a non-empty string")
|
|
893
|
+
if not isinstance(edit.get("before"), str) or not edit.get("before"):
|
|
894
|
+
raise ValueError(f"creator_output_malformed: edits[{index}].before must be a non-empty string")
|
|
895
|
+
if not isinstance(edit.get("after"), str):
|
|
896
|
+
raise ValueError(f"creator_output_malformed: edits[{index}].after must be a string")
|
|
897
|
+
notes = payload.get("notes")
|
|
898
|
+
if notes is not None and not isinstance(notes, str):
|
|
899
|
+
raise ValueError("creator_output_malformed: notes must be a string when present")
|
|
900
|
+
repair = None
|
|
901
|
+
if repair_types:
|
|
902
|
+
repair = {"types": repair_types, "applied": True}
|
|
903
|
+
return payload, repair
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def parse_creator_json(raw_text: str) -> dict[str, Any]:
|
|
907
|
+
payload, _repair = parse_creator_json_with_repair(raw_text)
|
|
908
|
+
return payload
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def coerce_single_file_creator_output(raw_text: str, allowed: set[str]) -> tuple[dict[str, Any], str] | None:
|
|
912
|
+
if len(allowed) != 1:
|
|
913
|
+
return None
|
|
914
|
+
rel = next(iter(allowed))
|
|
915
|
+
text = raw_text.strip()
|
|
916
|
+
if not text:
|
|
917
|
+
return None
|
|
918
|
+
if text.startswith("{") or text.startswith("["):
|
|
919
|
+
return None
|
|
920
|
+
# Recovery path for local models that follow the spirit of the request
|
|
921
|
+
# (full file content) but miss the JSON envelope. This is intentionally
|
|
922
|
+
# limited to one declared file so the path is not guessed.
|
|
923
|
+
if text.startswith("```"):
|
|
924
|
+
content = strip_json_fences(text)
|
|
925
|
+
else:
|
|
926
|
+
content = strip_reasoning_like_text(text)
|
|
927
|
+
if not content.strip():
|
|
928
|
+
return None
|
|
929
|
+
return {"files": [{"path": rel, "content": content}], "notes": "Recovered from single declared file raw code output."}, "single_file_raw_code"
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def strip_reasoning_like_text(text: str) -> str:
|
|
933
|
+
value = re.sub(r"(?is)<think>.*?</think>", "", text)
|
|
934
|
+
value = re.sub(r"(?is)<reasoning>.*?</reasoning>", "", value)
|
|
935
|
+
return value.strip()
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def select_creator_output_contract(
|
|
939
|
+
task: dict[str, Any], file_context: list[dict[str, Any]], reserved_output_tokens: int
|
|
940
|
+
) -> dict[str, Any]:
|
|
941
|
+
declared = [str(path) for path in task.get("declared_files", [])]
|
|
942
|
+
if len(declared) != 1 or len(file_context) != 1 or not file_context[0].get("exists"):
|
|
943
|
+
return {"mode": "full_replacement", "reason": "task_shape_requires_file_payload"}
|
|
944
|
+
content = str(file_context[0].get("content") or "")
|
|
945
|
+
estimated_tokens = (len(content) + 3) // 4
|
|
946
|
+
effective_output_tokens = reserved_output_tokens if reserved_output_tokens > 0 else 4096
|
|
947
|
+
threshold = max(512, int(effective_output_tokens * ANCHORED_EDIT_OUTPUT_HEADROOM_RATIO))
|
|
948
|
+
if estimated_tokens < threshold:
|
|
949
|
+
return {
|
|
950
|
+
"mode": "full_replacement",
|
|
951
|
+
"reason": "replacement_estimate_fits_output_headroom",
|
|
952
|
+
"estimated_full_replacement_tokens": estimated_tokens,
|
|
953
|
+
"anchored_edit_threshold_tokens": threshold,
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
"mode": "anchored_edits",
|
|
957
|
+
"reason": "replacement_estimate_exceeds_output_headroom",
|
|
958
|
+
"estimated_full_replacement_tokens": estimated_tokens,
|
|
959
|
+
"anchored_edit_threshold_tokens": threshold,
|
|
960
|
+
"max_edits": MAX_ANCHORED_EDITS,
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def anchored_edit_response_schema(allowed: set[str], max_edits: int = MAX_ANCHORED_EDITS) -> dict[str, Any]:
|
|
965
|
+
paths = sorted(normalize_declared_path(path) for path in allowed)
|
|
966
|
+
if not paths:
|
|
967
|
+
raise ValueError("anchored edit schema requires at least one declared path")
|
|
968
|
+
return {
|
|
969
|
+
"type": "object",
|
|
970
|
+
"properties": {
|
|
971
|
+
"edits": {
|
|
972
|
+
"type": "array",
|
|
973
|
+
"minItems": 1,
|
|
974
|
+
"maxItems": max_edits,
|
|
975
|
+
"items": {
|
|
976
|
+
"type": "object",
|
|
977
|
+
"properties": {
|
|
978
|
+
"path": {"type": "string", "enum": paths},
|
|
979
|
+
"before": {"type": "string", "minLength": 1},
|
|
980
|
+
"after": {"type": "string"},
|
|
981
|
+
},
|
|
982
|
+
"required": ["path", "before", "after"],
|
|
983
|
+
"additionalProperties": False,
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
"notes": {"type": "string"},
|
|
987
|
+
},
|
|
988
|
+
"required": ["edits"],
|
|
989
|
+
"additionalProperties": False,
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def materialize_anchored_edits(
|
|
994
|
+
payload: dict[str, Any], file_context: list[dict[str, Any]], allowed: set[str]
|
|
995
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
996
|
+
if "files" in payload:
|
|
997
|
+
raise ValueError("creator_output_contract_violation: anchored mode does not accept replacement files")
|
|
998
|
+
edits = payload.get("edits")
|
|
999
|
+
if not isinstance(edits, list) or not edits:
|
|
1000
|
+
raise ValueError("creator_output_contract_violation: anchored mode requires at least one edit")
|
|
1001
|
+
if len(edits) > MAX_ANCHORED_EDITS:
|
|
1002
|
+
raise ValueError(f"creator_output_contract_violation: anchored mode allows at most {MAX_ANCHORED_EDITS} edits")
|
|
1003
|
+
originals = {
|
|
1004
|
+
normalize_declared_path(str(item.get("path") or "")): str(item.get("content") or "")
|
|
1005
|
+
for item in file_context
|
|
1006
|
+
if item.get("exists")
|
|
1007
|
+
}
|
|
1008
|
+
by_path: dict[str, list[tuple[int, int, str, str]]] = {}
|
|
1009
|
+
metadata: list[dict[str, Any]] = []
|
|
1010
|
+
for index, edit in enumerate(edits):
|
|
1011
|
+
rel = normalize_declared_path(str(edit.get("path") or ""))
|
|
1012
|
+
if rel not in allowed:
|
|
1013
|
+
raise ValueError(f"creator_output_contract_violation: anchored edit path is not declared: {rel}")
|
|
1014
|
+
if rel not in originals:
|
|
1015
|
+
raise ValueError(f"creator_output_contract_violation: anchored edit target does not exist: {rel}")
|
|
1016
|
+
before = str(edit.get("before") or "")
|
|
1017
|
+
after = edit.get("after")
|
|
1018
|
+
if not before or not isinstance(after, str):
|
|
1019
|
+
raise ValueError(f"creator_output_contract_violation: anchored edit {index} needs before and after strings")
|
|
1020
|
+
if before == after:
|
|
1021
|
+
raise ValueError(f"creator_output_contract_violation: anchored edit {index} makes no change")
|
|
1022
|
+
positions: list[int] = []
|
|
1023
|
+
cursor = 0
|
|
1024
|
+
while True:
|
|
1025
|
+
position = originals[rel].find(before, cursor)
|
|
1026
|
+
if position < 0:
|
|
1027
|
+
break
|
|
1028
|
+
positions.append(position)
|
|
1029
|
+
cursor = position + 1
|
|
1030
|
+
if len(positions) != 1:
|
|
1031
|
+
raise ValueError(
|
|
1032
|
+
f"creator_output_contract_violation: anchored edit {index} before text matched {len(positions)} times in {rel}; exactly one required"
|
|
1033
|
+
)
|
|
1034
|
+
start = positions[0]
|
|
1035
|
+
end = start + len(before)
|
|
1036
|
+
by_path.setdefault(rel, []).append((start, end, before, after))
|
|
1037
|
+
metadata.append(
|
|
1038
|
+
{
|
|
1039
|
+
"index": index,
|
|
1040
|
+
"path": rel,
|
|
1041
|
+
"start": start,
|
|
1042
|
+
"before_bytes": len(before.encode("utf-8")),
|
|
1043
|
+
"after_bytes": len(after.encode("utf-8")),
|
|
1044
|
+
"before_sha256": sha256_text(before),
|
|
1045
|
+
"after_sha256": sha256_text(after),
|
|
1046
|
+
}
|
|
1047
|
+
)
|
|
1048
|
+
files: list[dict[str, Any]] = []
|
|
1049
|
+
for rel, path_edits in by_path.items():
|
|
1050
|
+
ordered = sorted(path_edits, key=lambda item: item[0])
|
|
1051
|
+
for previous, current in zip(ordered, ordered[1:]):
|
|
1052
|
+
if current[0] < previous[1]:
|
|
1053
|
+
raise ValueError(f"creator_output_contract_violation: anchored edits overlap in {rel}")
|
|
1054
|
+
content = originals[rel]
|
|
1055
|
+
for start, end, _before, after in reversed(ordered):
|
|
1056
|
+
content = content[:start] + after + content[end:]
|
|
1057
|
+
files.append({"path": rel, "content": content})
|
|
1058
|
+
return {"files": files, "notes": payload.get("notes") or "Applied exact anchored edits."}, metadata
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def validate_creator_declared_files(payload: dict[str, Any], allowed: set[str]) -> list[str]:
|
|
1062
|
+
violations: list[str] = []
|
|
1063
|
+
seen: set[str] = set()
|
|
1064
|
+
for file_payload in payload.get("files", []):
|
|
1065
|
+
try:
|
|
1066
|
+
rel = normalize_declared_path(str(file_payload.get("path")))
|
|
1067
|
+
except ValueError:
|
|
1068
|
+
violations.append(str(file_payload.get("path")))
|
|
1069
|
+
continue
|
|
1070
|
+
if rel not in allowed:
|
|
1071
|
+
violations.append(rel)
|
|
1072
|
+
if rel in seen:
|
|
1073
|
+
violations.append(f"{rel} (duplicate)")
|
|
1074
|
+
seen.add(rel)
|
|
1075
|
+
return violations
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def backend_smoke_endpoint_paths(job_dir: Path | None) -> list[str]:
|
|
1079
|
+
if job_dir is None:
|
|
1080
|
+
return []
|
|
1081
|
+
evidence_path = job_dir / "artifacts" / "tasks" / "task-029" / "backend-smoke-evidence.json"
|
|
1082
|
+
if not evidence_path.exists():
|
|
1083
|
+
return []
|
|
1084
|
+
try:
|
|
1085
|
+
evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
|
|
1086
|
+
except Exception: # noqa: BLE001 - reviewer evidence is best-effort.
|
|
1087
|
+
return []
|
|
1088
|
+
paths: list[str] = []
|
|
1089
|
+
for row in evidence.get("endpoints") or []:
|
|
1090
|
+
if not isinstance(row, dict):
|
|
1091
|
+
continue
|
|
1092
|
+
url = row.get("url")
|
|
1093
|
+
if not isinstance(url, str):
|
|
1094
|
+
continue
|
|
1095
|
+
parsed = urllib.parse.urlparse(url)
|
|
1096
|
+
path = parsed.path
|
|
1097
|
+
if path and path not in paths:
|
|
1098
|
+
paths.append(path)
|
|
1099
|
+
return paths
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def admin_data_client_endpoint_errors(written_text_by_path: dict[str, str], job_dir: Path | None) -> list[str]:
|
|
1103
|
+
relevant_paths = {"src/adminDashboard/config.js", "src/adminDashboard/dataClient.js"}
|
|
1104
|
+
if not any(path in written_text_by_path for path in relevant_paths):
|
|
1105
|
+
return []
|
|
1106
|
+
combined = "\n".join(written_text_by_path.values())
|
|
1107
|
+
required = backend_smoke_endpoint_paths(job_dir)
|
|
1108
|
+
missing = [path for path in required if path not in combined]
|
|
1109
|
+
errors = [f"admin data client missing backend-smoke endpoint {path}" for path in missing]
|
|
1110
|
+
forbidden = [
|
|
1111
|
+
"/api/admin/health",
|
|
1112
|
+
"/api/admin/status",
|
|
1113
|
+
"/api/admin/genes",
|
|
1114
|
+
"/api/admin/routes",
|
|
1115
|
+
"/api/admin/benchmarks",
|
|
1116
|
+
"/api/wal",
|
|
1117
|
+
"/api/genes",
|
|
1118
|
+
"/api/routes",
|
|
1119
|
+
"/api/dashboard",
|
|
1120
|
+
]
|
|
1121
|
+
for path in forbidden:
|
|
1122
|
+
if path in combined:
|
|
1123
|
+
errors.append(f"admin data client uses unverified endpoint prefix {path}")
|
|
1124
|
+
return errors
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def previous_attempt_feedback(job_dir: Path, events: list[Any], task_id: str) -> list[dict[str, Any]]:
|
|
1128
|
+
feedback: list[dict[str, Any]] = []
|
|
1129
|
+
for event in events:
|
|
1130
|
+
if event.payload.get("task_id") != task_id:
|
|
1131
|
+
continue
|
|
1132
|
+
if event.type == "task_failed":
|
|
1133
|
+
item: dict[str, Any] = {
|
|
1134
|
+
"event": event.type,
|
|
1135
|
+
"seq": event.seq,
|
|
1136
|
+
"error": event.payload.get("error"),
|
|
1137
|
+
"message": event.payload.get("message"),
|
|
1138
|
+
"reason": event.payload.get("reason"),
|
|
1139
|
+
"failure_feedback": event.payload.get("failure_feedback"),
|
|
1140
|
+
"review_errors": event.payload.get("review_errors"),
|
|
1141
|
+
"attempt": event.payload.get("attempt"),
|
|
1142
|
+
}
|
|
1143
|
+
response_path = event.payload.get("response_path")
|
|
1144
|
+
if isinstance(response_path, str):
|
|
1145
|
+
path = job_dir / response_path
|
|
1146
|
+
if path.exists():
|
|
1147
|
+
try:
|
|
1148
|
+
response = json.loads(path.read_text(encoding="utf-8"))
|
|
1149
|
+
content = str(response.get("content") or "")
|
|
1150
|
+
item["response_content_chars"] = len(content)
|
|
1151
|
+
item["response_head"] = content[:1200]
|
|
1152
|
+
item["response_tail"] = content[-2000:]
|
|
1153
|
+
item["finish_reason"] = response.get("finish_reason")
|
|
1154
|
+
item["reasoning_chars"] = len(str(response.get("reasoning_content") or ""))
|
|
1155
|
+
except Exception as exc: # noqa: BLE001 - feedback is best-effort.
|
|
1156
|
+
item["response_read_error"] = str(exc)
|
|
1157
|
+
feedback.append(item)
|
|
1158
|
+
elif event.type in {"creator_output_repaired", "declared_file_violation"}:
|
|
1159
|
+
feedback.append({"event": event.type, "seq": event.seq, **event.payload})
|
|
1160
|
+
return feedback[-6:]
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def build_creator_messages(
|
|
1164
|
+
job_dir: Path,
|
|
1165
|
+
project: Path,
|
|
1166
|
+
task: dict[str, Any],
|
|
1167
|
+
file_context: list[dict[str, Any]] | None = None,
|
|
1168
|
+
retry_feedback: list[dict[str, Any]] | None = None,
|
|
1169
|
+
validated_behavior: dict[str, Any] | None = None,
|
|
1170
|
+
output_contract: dict[str, Any] | None = None,
|
|
1171
|
+
) -> list[dict[str, str]]:
|
|
1172
|
+
handoff_path = job_dir / "handoff.md"
|
|
1173
|
+
handoff = handoff_path.read_text(encoding="utf-8", errors="replace") if handoff_path.exists() else ""
|
|
1174
|
+
files = file_context if file_context is not None else read_declared_file_context(project, task)
|
|
1175
|
+
declared = [str(path) for path in task.get("declared_files", [])]
|
|
1176
|
+
conventions = project_conventions(project, job_dir)
|
|
1177
|
+
js_rule = str(conventions.get("javascript_import_rule") or "Follow the existing project module syntax.")
|
|
1178
|
+
contract = output_contract or select_creator_output_contract(task, files, 4096)
|
|
1179
|
+
if contract.get("mode") == "anchored_edits":
|
|
1180
|
+
system = "\n".join(
|
|
1181
|
+
[
|
|
1182
|
+
"You are the Socra Creator agent.",
|
|
1183
|
+
f"Project JavaScript module rule: {js_rule}",
|
|
1184
|
+
f"This task has exactly one large declared file: {declared[0]}.",
|
|
1185
|
+
"Return only valid JSON. Do not return Markdown, prose outside JSON, a unified diff, or a full replacement file.",
|
|
1186
|
+
"Output schema:",
|
|
1187
|
+
'{"edits":[{"path":"relative/path","before":"small exact unique existing text","after":"replacement text"}],"notes":"short implementation notes"}',
|
|
1188
|
+
"Each before value must match the existing file exactly once. Include only enough unchanged context to make it unique.",
|
|
1189
|
+
f"Return at most {int(contract.get('max_edits') or MAX_ANCHORED_EDITS)} non-overlapping edits.",
|
|
1190
|
+
"You may only edit the declared path. Do not use line numbers as anchors.",
|
|
1191
|
+
]
|
|
1192
|
+
)
|
|
1193
|
+
elif len(declared) == 1:
|
|
1194
|
+
system = "\n".join(
|
|
1195
|
+
[
|
|
1196
|
+
"You are the Socra Creator agent.",
|
|
1197
|
+
f"Project JavaScript module rule: {js_rule}",
|
|
1198
|
+
f"This task has exactly one declared file: {declared[0]}.",
|
|
1199
|
+
"Return only the complete replacement file content for that one file.",
|
|
1200
|
+
"Do not return JSON, Markdown explanation, diffs, or patches.",
|
|
1201
|
+
"A code fence is allowed but not required.",
|
|
1202
|
+
"You may not create or modify any other path.",
|
|
1203
|
+
]
|
|
1204
|
+
)
|
|
1205
|
+
else:
|
|
1206
|
+
system = "\n".join(
|
|
1207
|
+
[
|
|
1208
|
+
"You are the Socra Creator agent.",
|
|
1209
|
+
f"Project JavaScript module rule: {js_rule}",
|
|
1210
|
+
"Return only valid JSON. Do not use Markdown fences unless unavoidable.",
|
|
1211
|
+
"Output schema:",
|
|
1212
|
+
'{"files":[{"path":"relative/path","content_lines":["full replacement file content line 1","line 2"]}],"notes":"short implementation notes"}',
|
|
1213
|
+
"Prefer content_lines for code so newlines do not need JSON escaping. content as one string is also accepted.",
|
|
1214
|
+
"You may only write paths listed in the declared files. Any other path fails the task.",
|
|
1215
|
+
"Return full file contents, not diffs or patches.",
|
|
1216
|
+
"If a declared file does not need changes, omit it from files and explain in notes.",
|
|
1217
|
+
]
|
|
1218
|
+
)
|
|
1219
|
+
if validated_behavior:
|
|
1220
|
+
system += "\nA validated PREFER behavior is included as advisory context. Derive a fresh fix for this task; do not copy a prior patch, and do not bypass any gate."
|
|
1221
|
+
user = json.dumps(
|
|
1222
|
+
{
|
|
1223
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
1224
|
+
"project_conventions": conventions,
|
|
1225
|
+
"handoff_md": handoff,
|
|
1226
|
+
"declared_file_contents": files,
|
|
1227
|
+
"task": {
|
|
1228
|
+
"id": task.get("id"),
|
|
1229
|
+
"description": task.get("description"),
|
|
1230
|
+
"acceptance": task.get("acceptance", []),
|
|
1231
|
+
"risk_tier": task.get("risk_tier"),
|
|
1232
|
+
"declared_files": task.get("declared_files", []),
|
|
1233
|
+
},
|
|
1234
|
+
"previous_attempt_feedback": retry_feedback or [],
|
|
1235
|
+
"validated_behavior_preference": validated_behavior,
|
|
1236
|
+
"output_contract": contract,
|
|
1237
|
+
},
|
|
1238
|
+
indent=2,
|
|
1239
|
+
)
|
|
1240
|
+
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def call_creator_model(
|
|
1244
|
+
creator: dict[str, Any],
|
|
1245
|
+
messages: list[dict[str, str]],
|
|
1246
|
+
*,
|
|
1247
|
+
json_mode: bool = True,
|
|
1248
|
+
response_schema: dict[str, Any] | None = None,
|
|
1249
|
+
) -> dict[str, Any]:
|
|
1250
|
+
endpoint = str(creator.get("endpoint") or "http://127.0.0.1:11439/v1").rstrip("/")
|
|
1251
|
+
url = endpoint + "/chat/completions"
|
|
1252
|
+
max_tokens = int(creator.get("max_output_tokens", 4096))
|
|
1253
|
+
payload = {
|
|
1254
|
+
"model": creator.get("model_alias") or creator.get("model") or "Socra-14B-Coder",
|
|
1255
|
+
"messages": messages,
|
|
1256
|
+
"temperature": float(creator.get("temperature", 0.2)),
|
|
1257
|
+
"max_tokens": max_tokens,
|
|
1258
|
+
}
|
|
1259
|
+
seed = creator.get("seed")
|
|
1260
|
+
if seed is not None:
|
|
1261
|
+
payload["seed"] = int(seed)
|
|
1262
|
+
if response_schema is not None:
|
|
1263
|
+
# llama.cpp's enforced schema transport is json_object + schema. Its
|
|
1264
|
+
# json_schema variant is accepted by some builds without constraining
|
|
1265
|
+
# output, so do not use acceptance of the request as proof of grammar use.
|
|
1266
|
+
payload["response_format"] = {"type": "json_object", "schema": response_schema}
|
|
1267
|
+
# llama.cpp applies response grammars to the answer channel. Keeping a
|
|
1268
|
+
# reasoning channel active has historically allowed unconstrained output.
|
|
1269
|
+
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
|
1270
|
+
elif json_mode:
|
|
1271
|
+
payload["response_format"] = {"type": "json_object"}
|
|
1272
|
+
timeout_seconds = int(creator.get("timeout_seconds") or max(240, min(1800, max_tokens // 8)))
|
|
1273
|
+
request = urllib.request.Request(
|
|
1274
|
+
url,
|
|
1275
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
1276
|
+
headers={"Content-Type": "application/json"},
|
|
1277
|
+
method="POST",
|
|
1278
|
+
)
|
|
1279
|
+
try:
|
|
1280
|
+
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
|
1281
|
+
body = response.read().decode("utf-8", errors="replace")
|
|
1282
|
+
except urllib.error.HTTPError as exc:
|
|
1283
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
1284
|
+
raise RuntimeError(f"creator_http_{exc.code}: {body[:2000]}") from exc
|
|
1285
|
+
data = json.loads(body)
|
|
1286
|
+
choice = (data.get("choices") or [{}])[0]
|
|
1287
|
+
message = choice.get("message") or {}
|
|
1288
|
+
content = message.get("content")
|
|
1289
|
+
if not isinstance(content, str):
|
|
1290
|
+
content = ""
|
|
1291
|
+
reasoning_content = message.get("reasoning_content")
|
|
1292
|
+
if not isinstance(reasoning_content, str):
|
|
1293
|
+
reasoning_content = ""
|
|
1294
|
+
return {
|
|
1295
|
+
"url": url,
|
|
1296
|
+
"request_model": payload["model"],
|
|
1297
|
+
"response_model": data.get("model"),
|
|
1298
|
+
"content": content,
|
|
1299
|
+
"reasoning_content": reasoning_content,
|
|
1300
|
+
"finish_reason": choice.get("finish_reason"),
|
|
1301
|
+
"usage": data.get("usage") if isinstance(data.get("usage"), dict) else {},
|
|
1302
|
+
"request_max_tokens": max_tokens,
|
|
1303
|
+
"request_temperature": payload["temperature"],
|
|
1304
|
+
"request_seed": payload.get("seed"),
|
|
1305
|
+
"json_mode": json_mode,
|
|
1306
|
+
"structured_output_schema_sha256": (
|
|
1307
|
+
recurrence_sha256_json(response_schema) if response_schema is not None else None
|
|
1308
|
+
),
|
|
1309
|
+
"structured_output_mode": "json_schema" if response_schema is not None else ("json_object" if json_mode else None),
|
|
1310
|
+
"structured_output_transport": (
|
|
1311
|
+
"llama_cpp_response_format_json_object_schema" if response_schema is not None else None
|
|
1312
|
+
),
|
|
1313
|
+
"thinking_disabled_for_schema": response_schema is not None,
|
|
1314
|
+
"raw": data,
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def atomic_write_creator_sandbox(job_dir: Path, task_id: str, payload: dict[str, Any]) -> tuple[Path, list[dict[str, Any]]]:
|
|
1319
|
+
sandbox_root = job_dir / "sandbox" / "tasks"
|
|
1320
|
+
final_dir = sandbox_root / task_id
|
|
1321
|
+
sandbox_root.mkdir(parents=True, exist_ok=True)
|
|
1322
|
+
temp_parent = job_dir / "sandbox" / ".tmp"
|
|
1323
|
+
temp_parent.mkdir(parents=True, exist_ok=True)
|
|
1324
|
+
temp_dir = Path(tempfile.mkdtemp(prefix=f"{task_id}-", dir=temp_parent))
|
|
1325
|
+
written: list[dict[str, Any]] = []
|
|
1326
|
+
try:
|
|
1327
|
+
for file_payload in payload.get("files", []):
|
|
1328
|
+
rel = normalize_declared_path(str(file_payload["path"]))
|
|
1329
|
+
target = temp_dir / rel
|
|
1330
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1331
|
+
content = str(file_payload["content"])
|
|
1332
|
+
target.write_text(content, encoding="utf-8")
|
|
1333
|
+
written.append(
|
|
1334
|
+
{
|
|
1335
|
+
"path": rel,
|
|
1336
|
+
"absolute_path": str(target),
|
|
1337
|
+
"sha256": sha256_text(content),
|
|
1338
|
+
"bytes": len(content.encode("utf-8")),
|
|
1339
|
+
"lines": len(content.splitlines()),
|
|
1340
|
+
}
|
|
1341
|
+
)
|
|
1342
|
+
if final_dir.exists():
|
|
1343
|
+
shutil.rmtree(final_dir)
|
|
1344
|
+
temp_dir.rename(final_dir)
|
|
1345
|
+
for row in written:
|
|
1346
|
+
row["absolute_path"] = str(final_dir / str(row["path"]))
|
|
1347
|
+
return final_dir, written
|
|
1348
|
+
except Exception:
|
|
1349
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
1350
|
+
raise
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def preserve_existing_final_newlines(
|
|
1354
|
+
payload: dict[str, Any], file_context: list[dict[str, Any]]
|
|
1355
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
1356
|
+
originals = {
|
|
1357
|
+
normalize_declared_path(str(item.get("path") or "")): str(item.get("content") or "")
|
|
1358
|
+
for item in file_context
|
|
1359
|
+
if item.get("exists") and str(item.get("content") or "")
|
|
1360
|
+
}
|
|
1361
|
+
normalized_files: list[dict[str, Any]] = []
|
|
1362
|
+
changes: list[dict[str, Any]] = []
|
|
1363
|
+
for file_payload in payload.get("files", []):
|
|
1364
|
+
row = dict(file_payload)
|
|
1365
|
+
rel = normalize_declared_path(str(row.get("path") or ""))
|
|
1366
|
+
content = str(row.get("content") or "")
|
|
1367
|
+
original = originals.get(rel)
|
|
1368
|
+
if original is not None:
|
|
1369
|
+
original_has_newline = original.endswith("\n")
|
|
1370
|
+
generated_has_newline = content.endswith(("\n", "\r"))
|
|
1371
|
+
if original_has_newline and not generated_has_newline:
|
|
1372
|
+
content += "\n"
|
|
1373
|
+
elif not original_has_newline and generated_has_newline:
|
|
1374
|
+
content = content.rstrip("\r\n")
|
|
1375
|
+
if content != str(row.get("content") or ""):
|
|
1376
|
+
changes.append(
|
|
1377
|
+
{
|
|
1378
|
+
"path": rel,
|
|
1379
|
+
"original_has_final_newline": original_has_newline,
|
|
1380
|
+
"generated_had_final_newline": generated_has_newline,
|
|
1381
|
+
"normalized_has_final_newline": content.endswith("\n"),
|
|
1382
|
+
"policy": "preserve_existing_file_final_newline_state",
|
|
1383
|
+
}
|
|
1384
|
+
)
|
|
1385
|
+
row["content"] = content
|
|
1386
|
+
normalized_files.append(row)
|
|
1387
|
+
return {**payload, "files": normalized_files}, changes
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def js_syntax_check(project: Path, source_path: Path, rel_path: str) -> tuple[str, str]:
|
|
1391
|
+
suffix = Path(rel_path).suffix.lower()
|
|
1392
|
+
script = f"""
|
|
1393
|
+
const fs = require("fs");
|
|
1394
|
+
const source = fs.readFileSync({json.dumps(str(source_path))}, "utf8");
|
|
1395
|
+
const project = {json.dumps(str(project))};
|
|
1396
|
+
const relPath = {json.dumps(rel_path)};
|
|
1397
|
+
const suffix = {json.dumps(suffix)};
|
|
1398
|
+
function resolveFromProject(name) {{
|
|
1399
|
+
try {{ return require.resolve(name, {{ paths: [project] }}); }} catch (_) {{ return null; }}
|
|
1400
|
+
}}
|
|
1401
|
+
const babelPath = resolveFromProject("@babel/parser");
|
|
1402
|
+
const typescriptPath = resolveFromProject("typescript");
|
|
1403
|
+
try {{
|
|
1404
|
+
if (babelPath) {{
|
|
1405
|
+
const parser = require(babelPath);
|
|
1406
|
+
parser.parse(source, {{
|
|
1407
|
+
sourceType: "module",
|
|
1408
|
+
errorRecovery: false,
|
|
1409
|
+
plugins: ["jsx", "typescript", "classProperties", "objectRestSpread", "dynamicImport"]
|
|
1410
|
+
}});
|
|
1411
|
+
}} else if (typescriptPath) {{
|
|
1412
|
+
const ts = require(typescriptPath);
|
|
1413
|
+
const kind = suffix === ".tsx" ? ts.ScriptKind.TSX
|
|
1414
|
+
: suffix === ".jsx" ? ts.ScriptKind.JSX
|
|
1415
|
+
: suffix === ".ts" ? ts.ScriptKind.TS
|
|
1416
|
+
: ts.ScriptKind.JS;
|
|
1417
|
+
const parsed = ts.createSourceFile(relPath, source, ts.ScriptTarget.Latest, true, kind);
|
|
1418
|
+
if (parsed.parseDiagnostics && parsed.parseDiagnostics.length) {{
|
|
1419
|
+
const message = parsed.parseDiagnostics
|
|
1420
|
+
.map((item) => ts.flattenDiagnosticMessageText(item.messageText, "\\n"))
|
|
1421
|
+
.join("; ");
|
|
1422
|
+
throw new Error(message);
|
|
1423
|
+
}}
|
|
1424
|
+
}} else if (suffix === ".js") {{
|
|
1425
|
+
process.stderr.write("CHECKER_UNAVAILABLE:node --check fallback required");
|
|
1426
|
+
process.exit(2);
|
|
1427
|
+
}} else {{
|
|
1428
|
+
process.stderr.write("CHECKER_UNAVAILABLE:no @babel/parser or typescript resolvable from target project");
|
|
1429
|
+
process.exit(2);
|
|
1430
|
+
}}
|
|
1431
|
+
}} catch (error) {{
|
|
1432
|
+
console.error(String(error && error.message || error));
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}}
|
|
1435
|
+
"""
|
|
1436
|
+
result = subprocess.run(
|
|
1437
|
+
["node", "-e", script],
|
|
1438
|
+
cwd=project,
|
|
1439
|
+
text=True,
|
|
1440
|
+
capture_output=True,
|
|
1441
|
+
timeout=30,
|
|
1442
|
+
check=False,
|
|
1443
|
+
)
|
|
1444
|
+
if result.returncode == 0:
|
|
1445
|
+
return "passed", "ok"
|
|
1446
|
+
if result.returncode == 2 and "CHECKER_UNAVAILABLE:" in (result.stderr or ""):
|
|
1447
|
+
if suffix == ".js":
|
|
1448
|
+
fallback = subprocess.run(
|
|
1449
|
+
["node", "--check", str(source_path)],
|
|
1450
|
+
cwd=project,
|
|
1451
|
+
text=True,
|
|
1452
|
+
capture_output=True,
|
|
1453
|
+
timeout=30,
|
|
1454
|
+
check=False,
|
|
1455
|
+
)
|
|
1456
|
+
if fallback.returncode == 0:
|
|
1457
|
+
return "passed", "ok_node_check"
|
|
1458
|
+
detail = (fallback.stderr or fallback.stdout or "unknown parse error").strip()
|
|
1459
|
+
return "failed", f"{rel_path} syntax check failed: {detail}"
|
|
1460
|
+
detail = (result.stderr or result.stdout).strip().replace("CHECKER_UNAVAILABLE:", "")
|
|
1461
|
+
return "unavailable", f"{rel_path} syntax checker unavailable: {detail}"
|
|
1462
|
+
detail = (result.stderr or result.stdout or "unknown parse error").strip()
|
|
1463
|
+
return "failed", f"{rel_path} syntax check failed: {detail}"
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
def regression_surface_tokens(source: str) -> list[str]:
|
|
1467
|
+
tokens: set[str] = set()
|
|
1468
|
+
for match in re.findall(r"app\.(?:get|post|put|delete|patch)\(\s*['\"]([^'\"]+)['\"]", source):
|
|
1469
|
+
tokens.add(match)
|
|
1470
|
+
for match in re.findall(r"\b(?:function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)", source):
|
|
1471
|
+
tokens.add(match)
|
|
1472
|
+
for match in re.findall(r"\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=", source):
|
|
1473
|
+
tokens.add(match)
|
|
1474
|
+
keywords = ("admin", "artifact", "safe", "local", "root", "wal", "event", "gene", "route", "benchmark", "health", "normalize", "summary", "source")
|
|
1475
|
+
return sorted(token for token in tokens if any(key in token.lower() for key in keywords))
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def review_creator_sandbox(
|
|
1479
|
+
project: Path,
|
|
1480
|
+
task: dict[str, Any],
|
|
1481
|
+
written: list[dict[str, Any]],
|
|
1482
|
+
output_mode: str,
|
|
1483
|
+
file_context: list[dict[str, Any]] | None = None,
|
|
1484
|
+
job_dir: Path | None = None,
|
|
1485
|
+
) -> tuple[bool, list[str], str, list[dict[str, Any]], list[str]]:
|
|
1486
|
+
errors: list[str] = []
|
|
1487
|
+
infrastructure_errors: list[str] = []
|
|
1488
|
+
if not isinstance(written, list):
|
|
1489
|
+
return False, ["written artifact list missing"], "contract", [], []
|
|
1490
|
+
syntax_checked = False
|
|
1491
|
+
written_text = ""
|
|
1492
|
+
written_text_by_path: dict[str, str] = {}
|
|
1493
|
+
for row in written:
|
|
1494
|
+
if not row.get("path") or not row.get("sha256"):
|
|
1495
|
+
errors.append(f"incomplete artifact metadata for {row!r}")
|
|
1496
|
+
path = str(row.get("path") or "")
|
|
1497
|
+
if path.endswith((".jsx", ".tsx", ".js", ".ts")):
|
|
1498
|
+
sandbox_path = row.get("absolute_path")
|
|
1499
|
+
if sandbox_path:
|
|
1500
|
+
try:
|
|
1501
|
+
content = Path(str(sandbox_path)).read_text(encoding="utf-8", errors="replace").lstrip()
|
|
1502
|
+
except OSError:
|
|
1503
|
+
content = ""
|
|
1504
|
+
written_text += "\n" + content
|
|
1505
|
+
written_text_by_path[path] = content
|
|
1506
|
+
if content.startswith('{"files"') or content.startswith("{'files'"):
|
|
1507
|
+
errors.append(f"{path} looks like an unparsed Creator JSON envelope")
|
|
1508
|
+
if sandbox_path and output_mode in {"single_file_raw_code", "anchored_edit_json"}:
|
|
1509
|
+
syntax_status, detail = js_syntax_check(project, Path(str(sandbox_path)), path)
|
|
1510
|
+
syntax_checked = True
|
|
1511
|
+
if syntax_status == "failed":
|
|
1512
|
+
errors.append(detail)
|
|
1513
|
+
elif syntax_status == "unavailable":
|
|
1514
|
+
infrastructure_errors.append(detail)
|
|
1515
|
+
if not written:
|
|
1516
|
+
errors.append("creator returned no file changes")
|
|
1517
|
+
errors.extend(admin_data_client_endpoint_errors(written_text_by_path, job_dir))
|
|
1518
|
+
regression_checks: list[dict[str, Any]] = []
|
|
1519
|
+
for item in file_context or []:
|
|
1520
|
+
if item.get("source") != "passed_sandbox_artifact":
|
|
1521
|
+
continue
|
|
1522
|
+
prior_path = item.get("sandbox_path")
|
|
1523
|
+
if not isinstance(prior_path, str):
|
|
1524
|
+
continue
|
|
1525
|
+
prior_abs = Path(str(item.get("absolute_path") or ""))
|
|
1526
|
+
if not prior_abs.exists() and prior_path:
|
|
1527
|
+
# file_context uses absolute_path internally but request artifacts only
|
|
1528
|
+
# expose sandbox_path. Keep this fallback defensive for older entries.
|
|
1529
|
+
prior_abs = Path(prior_path)
|
|
1530
|
+
if not prior_abs.exists():
|
|
1531
|
+
continue
|
|
1532
|
+
prior_text = prior_abs.read_text(encoding="utf-8", errors="replace")
|
|
1533
|
+
tokens = regression_surface_tokens(prior_text)
|
|
1534
|
+
item_path = str(item.get("path") or "")
|
|
1535
|
+
if item_path not in written_text_by_path:
|
|
1536
|
+
check = {
|
|
1537
|
+
"path": item.get("path"),
|
|
1538
|
+
"source_task_id": item.get("source_task_id"),
|
|
1539
|
+
"token_count": len(tokens),
|
|
1540
|
+
"missing": [],
|
|
1541
|
+
"ok": True,
|
|
1542
|
+
"status": "omitted_declared_file_treated_as_unchanged",
|
|
1543
|
+
}
|
|
1544
|
+
regression_checks.append(check)
|
|
1545
|
+
continue
|
|
1546
|
+
missing = [token for token in tokens if token not in written_text_by_path[item_path]]
|
|
1547
|
+
check = {
|
|
1548
|
+
"path": item.get("path"),
|
|
1549
|
+
"source_task_id": item.get("source_task_id"),
|
|
1550
|
+
"token_count": len(tokens),
|
|
1551
|
+
"missing": missing[:25],
|
|
1552
|
+
"ok": not missing,
|
|
1553
|
+
"status": "rewritten_file_checked",
|
|
1554
|
+
}
|
|
1555
|
+
regression_checks.append(check)
|
|
1556
|
+
if missing:
|
|
1557
|
+
errors.append(f"{item.get('path')} regression surface missing {len(missing)} prior tokens from {item.get('source_task_id')}: {', '.join(missing[:8])}")
|
|
1558
|
+
if syntax_checked:
|
|
1559
|
+
review_level = "static_syntax"
|
|
1560
|
+
else:
|
|
1561
|
+
review_level = "contract"
|
|
1562
|
+
return not errors and not infrastructure_errors, errors, review_level, regression_checks, infrastructure_errors
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def task_review_metadata(events: list[Any]) -> dict[str, dict[str, Any]]:
|
|
1566
|
+
reviews: dict[str, dict[str, Any]] = {}
|
|
1567
|
+
for event in events:
|
|
1568
|
+
payload = event.payload
|
|
1569
|
+
task_id = str(payload.get("task_id") or "")
|
|
1570
|
+
if not task_id:
|
|
1571
|
+
continue
|
|
1572
|
+
if event.type == "review_completed":
|
|
1573
|
+
reviews.setdefault(task_id, {}).update(
|
|
1574
|
+
{
|
|
1575
|
+
"review_ok": bool(payload.get("ok")),
|
|
1576
|
+
"review_level": payload.get("review_level") or "contract",
|
|
1577
|
+
"output_mode": payload.get("output_mode"),
|
|
1578
|
+
"review_errors": payload.get("errors") or [],
|
|
1579
|
+
"review_event_seq": event.seq,
|
|
1580
|
+
"review_event_id": event.event_id,
|
|
1581
|
+
}
|
|
1582
|
+
)
|
|
1583
|
+
elif event.type == "task_passed":
|
|
1584
|
+
reviews.setdefault(task_id, {}).update(
|
|
1585
|
+
{
|
|
1586
|
+
"task_passed": True,
|
|
1587
|
+
"review_level": payload.get("review_level") or reviews.get(task_id, {}).get("review_level"),
|
|
1588
|
+
"output_mode": payload.get("output_mode") or reviews.get(task_id, {}).get("output_mode"),
|
|
1589
|
+
"acceptance_review": payload.get("acceptance_review") or reviews.get(task_id, {}).get("acceptance_review") or "unspecified",
|
|
1590
|
+
}
|
|
1591
|
+
)
|
|
1592
|
+
elif event.type == "task_review_reclassified":
|
|
1593
|
+
reviews.setdefault(task_id, {}).update(
|
|
1594
|
+
{
|
|
1595
|
+
"review_level": payload.get("review_level"),
|
|
1596
|
+
"output_mode": payload.get("output_mode"),
|
|
1597
|
+
"acceptance_review": payload.get("acceptance_review"),
|
|
1598
|
+
"reclassified": True,
|
|
1599
|
+
"reclassification_reason": payload.get("reason"),
|
|
1600
|
+
}
|
|
1601
|
+
)
|
|
1602
|
+
elif event.type == "task_reverified":
|
|
1603
|
+
criteria = payload.get("criteria") if isinstance(payload.get("criteria"), list) else []
|
|
1604
|
+
all_passed = bool(criteria) and all(str(item.get("status")) == "pass" for item in criteria if isinstance(item, dict))
|
|
1605
|
+
reviews.setdefault(task_id, {}).update(
|
|
1606
|
+
{
|
|
1607
|
+
"review_level": payload.get("review_level") or ("acceptance_reverified" if all_passed else "acceptance_partial"),
|
|
1608
|
+
"acceptance_review": payload.get("acceptance_review") or ("full" if all_passed else "partial"),
|
|
1609
|
+
"reverified": True,
|
|
1610
|
+
"reverification_all_passed": all_passed,
|
|
1611
|
+
"reverification_event": event.event_id,
|
|
1612
|
+
"reverification_notes": payload.get("notes"),
|
|
1613
|
+
}
|
|
1614
|
+
)
|
|
1615
|
+
return reviews
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def reviewer_audit_requirements(events: list[Any]) -> dict[str, dict[str, Any]]:
|
|
1619
|
+
requirements: dict[str, dict[str, Any]] = {}
|
|
1620
|
+
for event in events:
|
|
1621
|
+
if event.type != "reviewer_path_audit":
|
|
1622
|
+
continue
|
|
1623
|
+
for item in event.payload.get("affected_events") or []:
|
|
1624
|
+
if not isinstance(item, dict):
|
|
1625
|
+
continue
|
|
1626
|
+
task_id = str(item.get("task_id") or "")
|
|
1627
|
+
if not task_id:
|
|
1628
|
+
continue
|
|
1629
|
+
effect = str(item.get("effect") or "")
|
|
1630
|
+
if "false_review_failed" in effect:
|
|
1631
|
+
continue
|
|
1632
|
+
requirements[task_id] = {
|
|
1633
|
+
"audit_event_seq": event.seq,
|
|
1634
|
+
"audit_event_id": event.event_id,
|
|
1635
|
+
"affected_review_seq": item.get("seq"),
|
|
1636
|
+
"effect": effect,
|
|
1637
|
+
"reason": "reviewer audit requires explicit reverification before promotion-grade milestone claims",
|
|
1638
|
+
}
|
|
1639
|
+
return requirements
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def tasks_requiring_milestone_reverification(events: list[Any], task_ids: list[str]) -> list[dict[str, Any]]:
|
|
1643
|
+
reviews = task_review_metadata(events)
|
|
1644
|
+
audit_requirements = reviewer_audit_requirements(events)
|
|
1645
|
+
needs: list[dict[str, Any]] = []
|
|
1646
|
+
for task_id in task_ids:
|
|
1647
|
+
meta = reviews.get(task_id, {})
|
|
1648
|
+
review_level = str(meta.get("review_level") or "")
|
|
1649
|
+
acceptance_review = str(meta.get("acceptance_review") or "")
|
|
1650
|
+
audit_requirement = audit_requirements.get(task_id)
|
|
1651
|
+
if audit_requirement and not meta.get("reverification_all_passed"):
|
|
1652
|
+
needs.append(
|
|
1653
|
+
{
|
|
1654
|
+
"task_id": task_id,
|
|
1655
|
+
"review_level": review_level or "unknown",
|
|
1656
|
+
"acceptance_review": acceptance_review or "unknown",
|
|
1657
|
+
"reason": audit_requirement["reason"],
|
|
1658
|
+
"audit_event_seq": audit_requirement.get("audit_event_seq"),
|
|
1659
|
+
"affected_review_seq": audit_requirement.get("affected_review_seq"),
|
|
1660
|
+
"effect": audit_requirement.get("effect"),
|
|
1661
|
+
}
|
|
1662
|
+
)
|
|
1663
|
+
continue
|
|
1664
|
+
if meta.get("reverification_all_passed"):
|
|
1665
|
+
continue
|
|
1666
|
+
if acceptance_review == "partial_smoke_only" or review_level.endswith("_smoke") or review_level == "static_syntax_smoke":
|
|
1667
|
+
needs.append(
|
|
1668
|
+
{
|
|
1669
|
+
"task_id": task_id,
|
|
1670
|
+
"review_level": review_level or "unknown",
|
|
1671
|
+
"acceptance_review": acceptance_review or "unknown",
|
|
1672
|
+
"reason": "partial smoke task must be explicitly reverified by milestone runner before promotion claims",
|
|
1673
|
+
}
|
|
1674
|
+
)
|
|
1675
|
+
return needs
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def validate_reverification_evidence(task: dict[str, Any], evidence: dict[str, Any]) -> tuple[list[dict[str, Any]], list[str], bool]:
|
|
1679
|
+
acceptance = [str(item) for item in task.get("acceptance", [])]
|
|
1680
|
+
criteria = evidence.get("criteria")
|
|
1681
|
+
errors: list[str] = []
|
|
1682
|
+
if not isinstance(criteria, list):
|
|
1683
|
+
return [], ["evidence.criteria must be a list"], False
|
|
1684
|
+
normalized: list[dict[str, Any]] = []
|
|
1685
|
+
seen: set[str] = set()
|
|
1686
|
+
allowed_status = {"pass", "fail", "not_checked"}
|
|
1687
|
+
for index, item in enumerate(criteria):
|
|
1688
|
+
if not isinstance(item, dict):
|
|
1689
|
+
errors.append(f"criteria[{index}] must be an object")
|
|
1690
|
+
continue
|
|
1691
|
+
criterion = str(item.get("criterion") or "")
|
|
1692
|
+
status = str(item.get("status") or "")
|
|
1693
|
+
if criterion not in acceptance:
|
|
1694
|
+
errors.append(f"criteria[{index}] does not match task acceptance: {criterion}")
|
|
1695
|
+
if criterion in seen:
|
|
1696
|
+
errors.append(f"duplicate criterion: {criterion}")
|
|
1697
|
+
if status not in allowed_status:
|
|
1698
|
+
errors.append(f"invalid status for {criterion}: {status}")
|
|
1699
|
+
seen.add(criterion)
|
|
1700
|
+
normalized.append(
|
|
1701
|
+
{
|
|
1702
|
+
"criterion": criterion,
|
|
1703
|
+
"status": status,
|
|
1704
|
+
"evidence_kind": str(item.get("evidence_kind") or ""),
|
|
1705
|
+
"details": item.get("details") if item.get("details") is not None else "",
|
|
1706
|
+
"artifacts": item.get("artifacts") if isinstance(item.get("artifacts"), list) else [],
|
|
1707
|
+
}
|
|
1708
|
+
)
|
|
1709
|
+
missing = [criterion for criterion in acceptance if criterion not in seen]
|
|
1710
|
+
for criterion in missing:
|
|
1711
|
+
errors.append(f"missing criterion evidence: {criterion}")
|
|
1712
|
+
all_passed = bool(normalized) and not errors and all(item["status"] == "pass" for item in normalized)
|
|
1713
|
+
return normalized, errors, all_passed
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def overlay_passed_task_artifacts(project: Path, job_dir: Path, plan: dict[str, Any], target_dir: Path) -> list[dict[str, Any]]:
|
|
1717
|
+
shutil.copytree(project, target_dir, ignore=copytree_ignore_for_runner)
|
|
1718
|
+
tasks = plan.get("tasks") if isinstance(plan.get("tasks"), list) else []
|
|
1719
|
+
overlays: list[dict[str, Any]] = []
|
|
1720
|
+
for task in tasks:
|
|
1721
|
+
if task.get("status") != "passed":
|
|
1722
|
+
continue
|
|
1723
|
+
task_id = str(task.get("id"))
|
|
1724
|
+
sandbox_dir = job_dir / "sandbox" / "tasks" / task_id
|
|
1725
|
+
if not sandbox_dir.exists():
|
|
1726
|
+
continue
|
|
1727
|
+
for src in sandbox_dir.rglob("*"):
|
|
1728
|
+
if not src.is_file():
|
|
1729
|
+
continue
|
|
1730
|
+
rel = src.relative_to(sandbox_dir)
|
|
1731
|
+
dest = target_dir / rel
|
|
1732
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1733
|
+
shutil.copy2(src, dest)
|
|
1734
|
+
overlays.append({"task_id": task_id, "path": str(rel), "sha256": sha256_file(dest)})
|
|
1735
|
+
return overlays
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def link_existing_dependency_trees(project: Path, overlay_dir: Path) -> list[str]:
|
|
1739
|
+
"""Reuse installed dependencies without copying them or invoking an installer."""
|
|
1740
|
+
linked: list[str] = []
|
|
1741
|
+
for root, dirs, _files in os.walk(project, topdown=True):
|
|
1742
|
+
root_path = Path(root)
|
|
1743
|
+
dirs[:] = [name for name in dirs if name not in {".git", ".socra"}]
|
|
1744
|
+
if "node_modules" not in dirs:
|
|
1745
|
+
continue
|
|
1746
|
+
source = root_path / "node_modules"
|
|
1747
|
+
rel = source.relative_to(project)
|
|
1748
|
+
target = overlay_dir / rel
|
|
1749
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1750
|
+
if not target.exists() and not target.is_symlink():
|
|
1751
|
+
target.symlink_to(source, target_is_directory=True)
|
|
1752
|
+
linked.append(rel.as_posix())
|
|
1753
|
+
dirs.remove("node_modules")
|
|
1754
|
+
return linked
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
RUNNER_INFRASTRUCTURE_PATTERNS = (
|
|
1758
|
+
"command not found",
|
|
1759
|
+
"err_pnpm_ignored_builds",
|
|
1760
|
+
"err_pnpm_aborted_remove_modules_dir_no_tty",
|
|
1761
|
+
"err_pnpm_outdated_lockfile",
|
|
1762
|
+
"err_pnpm_lockfile_config_mismatch",
|
|
1763
|
+
"could not determine executable to run",
|
|
1764
|
+
"jest: not found",
|
|
1765
|
+
"vitest: not found",
|
|
1766
|
+
"mocha: not found",
|
|
1767
|
+
"tsx: not found",
|
|
1768
|
+
)
|
|
1769
|
+
RUNNER_STARTED_INFRASTRUCTURE_PATTERNS = ("listen eperm: operation not permitted",)
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
def classify_gate_process(returncode: int, stdout: str, stderr: str) -> tuple[str, bool, str | None]:
|
|
1773
|
+
if returncode == 0:
|
|
1774
|
+
return "passed", True, None
|
|
1775
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
1776
|
+
if returncode in {126, 127} or any(pattern in combined for pattern in RUNNER_INFRASTRUCTURE_PATTERNS):
|
|
1777
|
+
return "infrastructure_unavailable", False, "runner_infrastructure_unavailable"
|
|
1778
|
+
if any(pattern in combined for pattern in RUNNER_STARTED_INFRASTRUCTURE_PATTERNS):
|
|
1779
|
+
return "infrastructure_unavailable", True, "runner_infrastructure_unavailable"
|
|
1780
|
+
return "test_failed", True, "test_command_failed"
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def infer_milestone_review_level(command: str) -> tuple[str, bool]:
|
|
1784
|
+
text = command.lower()
|
|
1785
|
+
if "preview" in text or "http.server" in text or "playwright" in text:
|
|
1786
|
+
return "preview_gate", False
|
|
1787
|
+
if "npm run build" in text or "vite" in text and "build" in text or "xcodebuild" in text:
|
|
1788
|
+
return "build_gate", False
|
|
1789
|
+
return "static_gate_only", False
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def dependency_manifest(project_dir: Path) -> dict[str, Any]:
|
|
1793
|
+
payload: dict[str, Any] = {}
|
|
1794
|
+
for name in ["package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"]:
|
|
1795
|
+
path = project_dir / name
|
|
1796
|
+
if path.exists() and path.is_file():
|
|
1797
|
+
payload[name] = {"sha256": sha256_file(path), "bytes": path.stat().st_size}
|
|
1798
|
+
return payload
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def prepare_runner_dependencies(overlay_dir: Path, mode: str, timeout_seconds: int) -> tuple[dict[str, Any], int]:
|
|
1802
|
+
if mode == "none":
|
|
1803
|
+
return {"mode": mode, "skipped": True}, 0
|
|
1804
|
+
if mode == "npm-ci":
|
|
1805
|
+
command = "npm ci"
|
|
1806
|
+
elif mode == "npm-install":
|
|
1807
|
+
command = "npm install"
|
|
1808
|
+
else:
|
|
1809
|
+
return {"mode": mode, "error": f"unknown dependency preparation mode: {mode}"}, 2
|
|
1810
|
+
before = dependency_manifest(overlay_dir)
|
|
1811
|
+
result = subprocess.run(
|
|
1812
|
+
command,
|
|
1813
|
+
cwd=overlay_dir,
|
|
1814
|
+
shell=True,
|
|
1815
|
+
text=True,
|
|
1816
|
+
capture_output=True,
|
|
1817
|
+
timeout=timeout_seconds,
|
|
1818
|
+
check=False,
|
|
1819
|
+
)
|
|
1820
|
+
after = dependency_manifest(overlay_dir)
|
|
1821
|
+
return (
|
|
1822
|
+
{
|
|
1823
|
+
"mode": mode,
|
|
1824
|
+
"command": command,
|
|
1825
|
+
"returncode": result.returncode,
|
|
1826
|
+
"stdout_tail": (result.stdout or "")[-4000:],
|
|
1827
|
+
"stderr_tail": (result.stderr or "")[-4000:],
|
|
1828
|
+
"manifest_before": before,
|
|
1829
|
+
"manifest_after": after,
|
|
1830
|
+
"node_modules_exists": (overlay_dir / "node_modules").exists(),
|
|
1831
|
+
},
|
|
1832
|
+
0 if result.returncode == 0 else 2,
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def parse_npm_vulnerability_counts(text: str) -> dict[str, int]:
|
|
1837
|
+
counts = {"low": 0, "moderate": 0, "high": 0, "critical": 0}
|
|
1838
|
+
match = re.search(r"(\d+)\s+vulnerabilities?\s*\(([^)]*)\)", text, flags=re.IGNORECASE)
|
|
1839
|
+
if not match:
|
|
1840
|
+
return counts
|
|
1841
|
+
for count, level in re.findall(r"(\d+)\s+(low|moderate|high|critical)", match.group(2), flags=re.IGNORECASE):
|
|
1842
|
+
counts[level.lower()] = int(count)
|
|
1843
|
+
return counts
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def active_vulnerability_exception_counts(events: list[Any]) -> dict[str, int]:
|
|
1847
|
+
counts = {"high": 0, "critical": 0}
|
|
1848
|
+
for event in events:
|
|
1849
|
+
if event.type != "vulnerability_exception_recorded":
|
|
1850
|
+
continue
|
|
1851
|
+
payload = event.payload
|
|
1852
|
+
if payload.get("active") is False:
|
|
1853
|
+
continue
|
|
1854
|
+
severity = str(payload.get("severity") or "").lower()
|
|
1855
|
+
if severity in counts:
|
|
1856
|
+
counts[severity] += 1
|
|
1857
|
+
return counts
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def dependency_vulnerability_policy(dep_payload: dict[str, Any], promotion_grade: bool, exception_counts: dict[str, int] | None = None) -> dict[str, Any]:
|
|
1861
|
+
text = "\n".join(str(dep_payload.get(key) or "") for key in ("stdout_tail", "stderr_tail"))
|
|
1862
|
+
counts = parse_npm_vulnerability_counts(text)
|
|
1863
|
+
exceptions = exception_counts or {}
|
|
1864
|
+
unexcepted_high = max(0, int(counts.get("high", 0)) - int(exceptions.get("high", 0)))
|
|
1865
|
+
unexcepted_critical = max(0, int(counts.get("critical", 0)) - int(exceptions.get("critical", 0)))
|
|
1866
|
+
blocking = unexcepted_high + unexcepted_critical
|
|
1867
|
+
return {
|
|
1868
|
+
"policy": "verification_records_all_vulns_promotion_blocks_high_critical",
|
|
1869
|
+
"counts": counts,
|
|
1870
|
+
"active_exception_counts": {"high": int(exceptions.get("high", 0)), "critical": int(exceptions.get("critical", 0))},
|
|
1871
|
+
"unexcepted_blocking_counts": {"high": unexcepted_high, "critical": unexcepted_critical},
|
|
1872
|
+
"blocking_vulnerabilities": blocking,
|
|
1873
|
+
"promotion_grade": promotion_grade,
|
|
1874
|
+
"blocks_promotion": bool(promotion_grade and blocking > 0),
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def find_job_dir(project: Path, job_id: str | None) -> Path:
|
|
1879
|
+
if job_id:
|
|
1880
|
+
return job_root(project) / job_id
|
|
1881
|
+
found = find_latest_incomplete_job(project)
|
|
1882
|
+
if not found:
|
|
1883
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
1884
|
+
return found
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def human_promotion_review(events: list[Any], task_id: str) -> dict[str, Any] | None:
|
|
1888
|
+
for event in reversed(events):
|
|
1889
|
+
if event.type != "promotion_human_authorized":
|
|
1890
|
+
continue
|
|
1891
|
+
payload = event.payload
|
|
1892
|
+
if payload.get("task_id") != task_id:
|
|
1893
|
+
continue
|
|
1894
|
+
if payload.get("human_source_promotion_authorized") is True:
|
|
1895
|
+
return {
|
|
1896
|
+
"event_seq": event.seq,
|
|
1897
|
+
"reviewer_name": payload.get("authorizer_name"),
|
|
1898
|
+
"review": payload.get("authorization"),
|
|
1899
|
+
"sha256": payload.get("authorization_sha256"),
|
|
1900
|
+
"event_type": event.type,
|
|
1901
|
+
"bundle_sha256": payload.get("bundle_sha256"),
|
|
1902
|
+
"dry_run_diff_sha256": payload.get("dry_run_diff_sha256"),
|
|
1903
|
+
"rollback_manifest_sha256": payload.get("rollback_manifest_sha256"),
|
|
1904
|
+
"presentation_event_seq": payload.get("presentation_event_seq"),
|
|
1905
|
+
}
|
|
1906
|
+
for event in reversed(events):
|
|
1907
|
+
if event.type != "promotion_human_diff_reviewed":
|
|
1908
|
+
continue
|
|
1909
|
+
payload = event.payload
|
|
1910
|
+
if payload.get("task_id") != task_id:
|
|
1911
|
+
continue
|
|
1912
|
+
if payload.get("human_source_promotion_approval_granted") is True:
|
|
1913
|
+
return {
|
|
1914
|
+
"event_seq": event.seq,
|
|
1915
|
+
"reviewer_name": payload.get("reviewer_name"),
|
|
1916
|
+
"review": payload.get("review"),
|
|
1917
|
+
"sha256": payload.get("sha256"),
|
|
1918
|
+
"event_type": event.type,
|
|
1919
|
+
"bundle_sha256": payload.get("bundle_sha256"),
|
|
1920
|
+
"legacy_compatibility": True,
|
|
1921
|
+
}
|
|
1922
|
+
return None
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
def latest_diff_presentation(events: list[Any], task_id: str, bundle_sha256: str, dry_run_diff_sha256: str) -> dict[str, Any] | None:
|
|
1926
|
+
for event in reversed(events):
|
|
1927
|
+
if event.type != "promotion_diff_presented":
|
|
1928
|
+
continue
|
|
1929
|
+
payload = event.payload
|
|
1930
|
+
if payload.get("task_id") != task_id:
|
|
1931
|
+
continue
|
|
1932
|
+
if payload.get("bundle_sha256") != bundle_sha256:
|
|
1933
|
+
continue
|
|
1934
|
+
if payload.get("dry_run_diff_sha256") != dry_run_diff_sha256:
|
|
1935
|
+
continue
|
|
1936
|
+
if payload.get("displayed_hash_matched_approval_target") is True:
|
|
1937
|
+
return {
|
|
1938
|
+
"event_seq": event.seq,
|
|
1939
|
+
"presentation": payload,
|
|
1940
|
+
}
|
|
1941
|
+
return None
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def matching_diff_rejection(events: list[Any], task_id: str, bundle_sha256: str, dry_run_diff_sha256: str) -> dict[str, Any] | None:
|
|
1945
|
+
for event in reversed(events):
|
|
1946
|
+
if event.type != "promotion_human_rejected":
|
|
1947
|
+
continue
|
|
1948
|
+
payload = event.payload
|
|
1949
|
+
if payload.get("task_id") != task_id:
|
|
1950
|
+
continue
|
|
1951
|
+
if payload.get("bundle_sha256") != bundle_sha256:
|
|
1952
|
+
continue
|
|
1953
|
+
if payload.get("dry_run_diff_sha256") != dry_run_diff_sha256:
|
|
1954
|
+
continue
|
|
1955
|
+
return {"event_seq": event.seq, "rejection": payload}
|
|
1956
|
+
return None
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
def ensure_promotion_artifacts(project: Path, job_dir: Path, task_id: str) -> dict[str, Any]:
|
|
1960
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / task_id
|
|
1961
|
+
dry_run_diff = artifact_dir / "dry-run-diff.md"
|
|
1962
|
+
patch_bundle = artifact_dir / "promotion-patch-bundle.json"
|
|
1963
|
+
rollback_manifest = artifact_dir / "rollback-manifest.json"
|
|
1964
|
+
if dry_run_diff.exists() and patch_bundle.exists() and rollback_manifest.exists():
|
|
1965
|
+
return {"generated": False}
|
|
1966
|
+
|
|
1967
|
+
sandbox_dir = job_dir / "sandbox" / "tasks" / task_id
|
|
1968
|
+
if not sandbox_dir.exists():
|
|
1969
|
+
return {"generated": False, "error": "promotion_sandbox_artifacts_missing"}
|
|
1970
|
+
|
|
1971
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
1972
|
+
changed_files: list[dict[str, Any]] = []
|
|
1973
|
+
rollback_entries: list[dict[str, Any]] = []
|
|
1974
|
+
diff_sections: list[str] = [f"# Promotion Diff: {task_id}", ""]
|
|
1975
|
+
|
|
1976
|
+
for src in sorted(sandbox_dir.rglob("*")):
|
|
1977
|
+
if not src.is_file():
|
|
1978
|
+
continue
|
|
1979
|
+
rel = src.relative_to(sandbox_dir).as_posix()
|
|
1980
|
+
target = project / rel
|
|
1981
|
+
old_exists = target.exists()
|
|
1982
|
+
old_text = target.read_text(encoding="utf-8", errors="replace") if old_exists and target.is_file() else ""
|
|
1983
|
+
new_text = src.read_text(encoding="utf-8", errors="replace")
|
|
1984
|
+
old_sha = sha256_text(old_text) if old_exists else None
|
|
1985
|
+
new_sha = sha256_text(new_text)
|
|
1986
|
+
if old_exists and old_sha == new_sha:
|
|
1987
|
+
continue
|
|
1988
|
+
sandbox_path = src.relative_to(job_dir).as_posix()
|
|
1989
|
+
changed_files.append(
|
|
1990
|
+
{
|
|
1991
|
+
"path": rel,
|
|
1992
|
+
"source_task_id": task_id,
|
|
1993
|
+
"sandbox_path": sandbox_path,
|
|
1994
|
+
"old_exists": old_exists,
|
|
1995
|
+
"old_sha256": old_sha,
|
|
1996
|
+
"new_sha256": new_sha,
|
|
1997
|
+
}
|
|
1998
|
+
)
|
|
1999
|
+
rollback_entries.append(
|
|
2000
|
+
{
|
|
2001
|
+
"path": rel,
|
|
2002
|
+
"old_exists": old_exists,
|
|
2003
|
+
"old_sha256": old_sha,
|
|
2004
|
+
}
|
|
2005
|
+
)
|
|
2006
|
+
old_lines = old_text.splitlines()
|
|
2007
|
+
new_lines = new_text.splitlines()
|
|
2008
|
+
diff = "\n".join(
|
|
2009
|
+
difflib.unified_diff(
|
|
2010
|
+
old_lines,
|
|
2011
|
+
new_lines,
|
|
2012
|
+
fromfile=f"a/{rel}",
|
|
2013
|
+
tofile=f"b/{rel}",
|
|
2014
|
+
lineterm="",
|
|
2015
|
+
)
|
|
2016
|
+
)
|
|
2017
|
+
diff_sections.extend([f"## {rel}", "", "```diff", diff or "(no textual diff)", "```", ""])
|
|
2018
|
+
|
|
2019
|
+
if not changed_files:
|
|
2020
|
+
diff_sections.extend(["No changed files in sandbox artifacts.", ""])
|
|
2021
|
+
|
|
2022
|
+
dry_run_diff.write_text("\n".join(diff_sections), encoding="utf-8")
|
|
2023
|
+
bundle = {
|
|
2024
|
+
"schema_version": 1,
|
|
2025
|
+
"task_id": task_id,
|
|
2026
|
+
"source": "sandbox_task_artifacts",
|
|
2027
|
+
"changed_files": changed_files,
|
|
2028
|
+
}
|
|
2029
|
+
patch_bundle.write_text(json.dumps(bundle, indent=2) + "\n", encoding="utf-8")
|
|
2030
|
+
rollback = {
|
|
2031
|
+
"schema_version": 1,
|
|
2032
|
+
"task_id": task_id,
|
|
2033
|
+
"source": "project_preimage_manifest",
|
|
2034
|
+
"entries": rollback_entries,
|
|
2035
|
+
}
|
|
2036
|
+
rollback_manifest.write_text(json.dumps(rollback, indent=2) + "\n", encoding="utf-8")
|
|
2037
|
+
return {
|
|
2038
|
+
"generated": True,
|
|
2039
|
+
"changed_file_count": len(changed_files),
|
|
2040
|
+
"dry_run_diff": str(dry_run_diff.relative_to(job_dir)),
|
|
2041
|
+
"patch_bundle": str(patch_bundle.relative_to(job_dir)),
|
|
2042
|
+
"rollback_manifest": str(rollback_manifest.relative_to(job_dir)),
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
def promotion_artifact_hashes(job_dir: Path, task_id: str) -> dict[str, Any]:
|
|
2047
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / task_id
|
|
2048
|
+
dry_run_diff = artifact_dir / "dry-run-diff.md"
|
|
2049
|
+
patch_bundle = artifact_dir / "promotion-patch-bundle.json"
|
|
2050
|
+
rollback_manifest = artifact_dir / "rollback-manifest.json"
|
|
2051
|
+
missing = [str(path.relative_to(job_dir)) for path in [dry_run_diff, patch_bundle, rollback_manifest] if not path.exists()]
|
|
2052
|
+
hashes: dict[str, Any] = {
|
|
2053
|
+
"artifact_dir": artifact_dir,
|
|
2054
|
+
"dry_run_diff": dry_run_diff,
|
|
2055
|
+
"patch_bundle": patch_bundle,
|
|
2056
|
+
"rollback_manifest": rollback_manifest,
|
|
2057
|
+
"missing": missing,
|
|
2058
|
+
}
|
|
2059
|
+
if not missing:
|
|
2060
|
+
hashes.update(
|
|
2061
|
+
{
|
|
2062
|
+
"dry_run_diff_sha256": sha256_file(dry_run_diff),
|
|
2063
|
+
"bundle_sha256": sha256_file(patch_bundle),
|
|
2064
|
+
"rollback_manifest_sha256": sha256_file(rollback_manifest),
|
|
2065
|
+
}
|
|
2066
|
+
)
|
|
2067
|
+
return hashes
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
def promotion_apply_bundle(project: Path, job_dir: Path, task_id: str) -> dict[str, Any]:
|
|
2071
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / task_id
|
|
2072
|
+
bundle_path = artifact_dir / "promotion-patch-bundle.json"
|
|
2073
|
+
if not bundle_path.exists():
|
|
2074
|
+
return {
|
|
2075
|
+
"ok": False,
|
|
2076
|
+
"error": "promotion_patch_bundle_missing",
|
|
2077
|
+
"missing": str(bundle_path.relative_to(job_dir)),
|
|
2078
|
+
"real_project_modified": False,
|
|
2079
|
+
}
|
|
2080
|
+
bundle = json.loads(bundle_path.read_text(encoding="utf-8"))
|
|
2081
|
+
changed_files = bundle.get("changed_files") or []
|
|
2082
|
+
if not isinstance(changed_files, list) or not changed_files:
|
|
2083
|
+
return {
|
|
2084
|
+
"ok": False,
|
|
2085
|
+
"error": "promotion_patch_bundle_has_no_changed_files",
|
|
2086
|
+
"bundle": str(bundle_path.relative_to(job_dir)),
|
|
2087
|
+
"real_project_modified": False,
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
errors: list[dict[str, Any]] = []
|
|
2091
|
+
rollback_entries: list[dict[str, Any]] = []
|
|
2092
|
+
for item in changed_files:
|
|
2093
|
+
try:
|
|
2094
|
+
rel = normalize_declared_path(str(item.get("path") or ""))
|
|
2095
|
+
except ValueError as exc:
|
|
2096
|
+
errors.append({"path": item.get("path"), "stage": "path_validation", "error": str(exc)})
|
|
2097
|
+
continue
|
|
2098
|
+
target = project / rel
|
|
2099
|
+
old_exists = target.exists()
|
|
2100
|
+
old_data = target.read_bytes() if old_exists else None
|
|
2101
|
+
old_sha = hashlib.sha256(old_data).hexdigest() if old_data is not None else None
|
|
2102
|
+
if old_exists != bool(item.get("old_exists")) or old_sha != item.get("old_sha256"):
|
|
2103
|
+
errors.append(
|
|
2104
|
+
{
|
|
2105
|
+
"path": rel,
|
|
2106
|
+
"stage": "preimage_check",
|
|
2107
|
+
"expected_exists": item.get("old_exists"),
|
|
2108
|
+
"actual_exists": old_exists,
|
|
2109
|
+
"expected_sha256": item.get("old_sha256"),
|
|
2110
|
+
"actual_sha256": old_sha,
|
|
2111
|
+
}
|
|
2112
|
+
)
|
|
2113
|
+
rollback_entries.append(
|
|
2114
|
+
{
|
|
2115
|
+
"path": rel,
|
|
2116
|
+
"old_exists": old_exists,
|
|
2117
|
+
"old_sha256": old_sha,
|
|
2118
|
+
"old_content_b64": base64.b64encode(old_data).decode("ascii") if old_data is not None else None,
|
|
2119
|
+
}
|
|
2120
|
+
)
|
|
2121
|
+
if errors:
|
|
2122
|
+
return {
|
|
2123
|
+
"ok": False,
|
|
2124
|
+
"error": "promotion_preimage_check_failed",
|
|
2125
|
+
"errors": errors,
|
|
2126
|
+
"real_project_modified": False,
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
apply_dir = artifact_dir / "promotion-apply"
|
|
2130
|
+
apply_dir.mkdir(parents=True, exist_ok=True)
|
|
2131
|
+
rollback_snapshot = {
|
|
2132
|
+
"task_id": task_id,
|
|
2133
|
+
"bundle_sha256": sha256_file(bundle_path),
|
|
2134
|
+
"entries": rollback_entries,
|
|
2135
|
+
}
|
|
2136
|
+
rollback_snapshot_path = apply_dir / "rollback-content-snapshot.json"
|
|
2137
|
+
rollback_snapshot_text = json.dumps(rollback_snapshot, indent=2) + "\n"
|
|
2138
|
+
rollback_snapshot_path.write_text(rollback_snapshot_text, encoding="utf-8")
|
|
2139
|
+
|
|
2140
|
+
applied: list[dict[str, Any]] = []
|
|
2141
|
+
written_targets: list[Path] = []
|
|
2142
|
+
try:
|
|
2143
|
+
for item in changed_files:
|
|
2144
|
+
rel = normalize_declared_path(str(item.get("path") or ""))
|
|
2145
|
+
sandbox_rel = normalize_declared_path(str(item.get("sandbox_path") or ""))
|
|
2146
|
+
source = (job_dir / sandbox_rel).resolve()
|
|
2147
|
+
job_root_resolved = job_dir.resolve()
|
|
2148
|
+
if job_root_resolved != source and job_root_resolved not in source.parents:
|
|
2149
|
+
raise RuntimeError(f"source artifact escapes job dir for {rel}: {source}")
|
|
2150
|
+
target = project / rel
|
|
2151
|
+
if not source.exists():
|
|
2152
|
+
raise RuntimeError(f"source artifact missing for {rel}: {source}")
|
|
2153
|
+
source_data = source.read_bytes()
|
|
2154
|
+
source_sha = hashlib.sha256(source_data).hexdigest()
|
|
2155
|
+
if source_sha != item.get("new_sha256"):
|
|
2156
|
+
raise RuntimeError(f"source hash mismatch for {rel}: {source_sha} != {item.get('new_sha256')}")
|
|
2157
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2158
|
+
# Write to a sibling temp file and replace to avoid torn targets.
|
|
2159
|
+
tmp = target.with_name(f".{target.name}.socra-promote-tmp")
|
|
2160
|
+
tmp.write_bytes(source_data)
|
|
2161
|
+
tmp.replace(target)
|
|
2162
|
+
written_targets.append(target)
|
|
2163
|
+
actual_sha = sha256_file(target)
|
|
2164
|
+
if actual_sha != item.get("new_sha256"):
|
|
2165
|
+
raise RuntimeError(f"post-write hash mismatch for {rel}: {actual_sha} != {item.get('new_sha256')}")
|
|
2166
|
+
applied.append(
|
|
2167
|
+
{
|
|
2168
|
+
"path": rel,
|
|
2169
|
+
"new_sha256": actual_sha,
|
|
2170
|
+
"source_task_id": item.get("source_task_id"),
|
|
2171
|
+
"sandbox_path": item.get("sandbox_path"),
|
|
2172
|
+
}
|
|
2173
|
+
)
|
|
2174
|
+
except Exception as exc: # noqa: BLE001 - rollback best effort is recorded.
|
|
2175
|
+
rollback_errors: list[dict[str, Any]] = []
|
|
2176
|
+
by_path = {entry["path"]: entry for entry in rollback_entries}
|
|
2177
|
+
for target in reversed(written_targets):
|
|
2178
|
+
rel = str(target.relative_to(project))
|
|
2179
|
+
entry = by_path.get(rel)
|
|
2180
|
+
if not entry:
|
|
2181
|
+
continue
|
|
2182
|
+
try:
|
|
2183
|
+
if entry.get("old_exists"):
|
|
2184
|
+
target.write_bytes(base64.b64decode(entry.get("old_content_b64") or ""))
|
|
2185
|
+
elif target.exists():
|
|
2186
|
+
target.unlink()
|
|
2187
|
+
except Exception as rollback_exc: # noqa: BLE001
|
|
2188
|
+
rollback_errors.append({"path": rel, "error": str(rollback_exc)})
|
|
2189
|
+
return {
|
|
2190
|
+
"ok": False,
|
|
2191
|
+
"error": "promotion_apply_failed",
|
|
2192
|
+
"exception": str(exc),
|
|
2193
|
+
"applied_before_error": applied,
|
|
2194
|
+
"rollback_errors": rollback_errors,
|
|
2195
|
+
"rollback_content_snapshot": str(rollback_snapshot_path.relative_to(job_dir)),
|
|
2196
|
+
"rollback_content_snapshot_sha256": sha256_text(rollback_snapshot_text),
|
|
2197
|
+
"real_project_modified": bool(applied),
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
receipt = {
|
|
2201
|
+
"task_id": task_id,
|
|
2202
|
+
"bundle_sha256": sha256_file(bundle_path),
|
|
2203
|
+
"applied_count": len(applied),
|
|
2204
|
+
"applied": applied,
|
|
2205
|
+
"rollback_content_snapshot": str(rollback_snapshot_path.relative_to(job_dir)),
|
|
2206
|
+
"rollback_content_snapshot_sha256": sha256_text(rollback_snapshot_text),
|
|
2207
|
+
"real_project_modified": True,
|
|
2208
|
+
}
|
|
2209
|
+
receipt_path = apply_dir / "promotion-apply-receipt.json"
|
|
2210
|
+
receipt_text = json.dumps(receipt, indent=2) + "\n"
|
|
2211
|
+
receipt_path.write_text(receipt_text, encoding="utf-8")
|
|
2212
|
+
return {
|
|
2213
|
+
"ok": True,
|
|
2214
|
+
"receipt": receipt,
|
|
2215
|
+
"receipt_path": str(receipt_path.relative_to(job_dir)),
|
|
2216
|
+
"receipt_sha256": sha256_text(receipt_text),
|
|
2217
|
+
"rollback_content_snapshot": str(rollback_snapshot_path.relative_to(job_dir)),
|
|
2218
|
+
"rollback_content_snapshot_sha256": sha256_text(rollback_snapshot_text),
|
|
2219
|
+
"real_project_modified": True,
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
|
|
2223
|
+
def present_promotion_diff(job_dir: Path, task_id: str, reviewer_name: str, notes: str = "") -> dict[str, Any]:
|
|
2224
|
+
artifacts = promotion_artifact_hashes(job_dir, task_id)
|
|
2225
|
+
missing = artifacts["missing"]
|
|
2226
|
+
if missing:
|
|
2227
|
+
return {
|
|
2228
|
+
"ok": False,
|
|
2229
|
+
"missing": missing,
|
|
2230
|
+
"error": "promotion_dry_run_artifacts_missing",
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
dry_run_diff = artifacts["dry_run_diff"]
|
|
2234
|
+
text = dry_run_diff.read_text(encoding="utf-8", errors="replace")
|
|
2235
|
+
start = time.monotonic()
|
|
2236
|
+
# Print the exact artifact bytes as text. This is intentionally simple and
|
|
2237
|
+
# observable in logs; callers can pipe through a pager if desired.
|
|
2238
|
+
sys.stdout.write(text)
|
|
2239
|
+
if text and not text.endswith("\n"):
|
|
2240
|
+
sys.stdout.write("\n")
|
|
2241
|
+
sys.stdout.flush()
|
|
2242
|
+
elapsed = elapsed_ms(start)
|
|
2243
|
+
rendered_lines = len(text.splitlines())
|
|
2244
|
+
return {
|
|
2245
|
+
"ok": True,
|
|
2246
|
+
"task_id": task_id,
|
|
2247
|
+
"reviewer_role": "human",
|
|
2248
|
+
"reviewer_name": reviewer_name,
|
|
2249
|
+
"dry_run_diff": str(dry_run_diff.relative_to(job_dir)),
|
|
2250
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2251
|
+
"bundle": str(artifacts["patch_bundle"].relative_to(job_dir)),
|
|
2252
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2253
|
+
"rollback_manifest": str(artifacts["rollback_manifest"].relative_to(job_dir)),
|
|
2254
|
+
"rollback_manifest_sha256": artifacts["rollback_manifest_sha256"],
|
|
2255
|
+
"presentation_method": "stdout",
|
|
2256
|
+
"rendered_line_count": rendered_lines,
|
|
2257
|
+
"process_exit_status": 0,
|
|
2258
|
+
"elapsed_ms": elapsed,
|
|
2259
|
+
"displayed_hash_matched_approval_target": True,
|
|
2260
|
+
"notes": notes,
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
def job_review_promotion_main(args: argparse.Namespace) -> int:
|
|
2265
|
+
project = args.project.expanduser().resolve()
|
|
2266
|
+
job_dir = find_job_dir(project, args.job_id)
|
|
2267
|
+
with JobLock(job_dir / ".lock"):
|
|
2268
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2269
|
+
state = replay(events)
|
|
2270
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2271
|
+
task = next((item for item in plan.get("tasks", []) if item.get("id") == args.task_id), None)
|
|
2272
|
+
behavior_preference = getattr(args, "behavior_preference", None)
|
|
2273
|
+
gene_evidence = getattr(args, "gene_evidence", None)
|
|
2274
|
+
generated = ensure_promotion_artifacts(project, job_dir, args.task_id)
|
|
2275
|
+
artifacts = promotion_artifact_hashes(job_dir, args.task_id)
|
|
2276
|
+
artifact_dir = artifacts["artifact_dir"]
|
|
2277
|
+
missing = artifacts["missing"]
|
|
2278
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
2279
|
+
if generated.get("generated"):
|
|
2280
|
+
log.append("promotion_artifacts_generated", {"task_id": args.task_id, **generated})
|
|
2281
|
+
if missing:
|
|
2282
|
+
log.append(
|
|
2283
|
+
"promotion_review_failed",
|
|
2284
|
+
{
|
|
2285
|
+
"task_id": args.task_id,
|
|
2286
|
+
"reviewer_role": "human",
|
|
2287
|
+
"reviewer_name": args.reviewer_name,
|
|
2288
|
+
"error": "promotion_dry_run_artifacts_missing",
|
|
2289
|
+
"missing": missing,
|
|
2290
|
+
},
|
|
2291
|
+
)
|
|
2292
|
+
code = 2
|
|
2293
|
+
print(f"Promotion review failed: {args.task_id}")
|
|
2294
|
+
print(f"Missing artifacts: {', '.join(missing)}")
|
|
2295
|
+
elif bool(behavior_preference) != bool(gene_evidence):
|
|
2296
|
+
log.append(
|
|
2297
|
+
"promotion_behavior_preference_blocked",
|
|
2298
|
+
{
|
|
2299
|
+
"task_id": args.task_id,
|
|
2300
|
+
"error": "behavior_and_heldout_evidence_required_together",
|
|
2301
|
+
"real_project_modified": False,
|
|
2302
|
+
},
|
|
2303
|
+
)
|
|
2304
|
+
code = 2
|
|
2305
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2306
|
+
print("Reason: behavior_and_heldout_evidence_required_together")
|
|
2307
|
+
elif behavior_preference and behavior_overlaps_patch(
|
|
2308
|
+
behavior_preference,
|
|
2309
|
+
artifacts["dry_run_diff"].read_text(encoding="utf-8", errors="replace"),
|
|
2310
|
+
):
|
|
2311
|
+
log.append(
|
|
2312
|
+
"promotion_behavior_preference_blocked",
|
|
2313
|
+
{
|
|
2314
|
+
"task_id": args.task_id,
|
|
2315
|
+
"error": "behavior_card_overlaps_patch_text",
|
|
2316
|
+
"real_project_modified": False,
|
|
2317
|
+
},
|
|
2318
|
+
)
|
|
2319
|
+
code = 2
|
|
2320
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2321
|
+
print("Reason: behavior_card_overlaps_patch_text")
|
|
2322
|
+
elif behavior_preference and (
|
|
2323
|
+
not task
|
|
2324
|
+
or not isinstance(task.get("recurrence"), dict)
|
|
2325
|
+
or task["recurrence"].get("status") != "assignable"
|
|
2326
|
+
):
|
|
2327
|
+
log.append(
|
|
2328
|
+
"promotion_behavior_preference_blocked",
|
|
2329
|
+
{
|
|
2330
|
+
"task_id": args.task_id,
|
|
2331
|
+
"error": "assignable_recurrence_key_required",
|
|
2332
|
+
"real_project_modified": False,
|
|
2333
|
+
},
|
|
2334
|
+
)
|
|
2335
|
+
code = 2
|
|
2336
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2337
|
+
print("Reason: assignable_recurrence_key_required_for_behavior_gene")
|
|
2338
|
+
else:
|
|
2339
|
+
if args.present_diff and (args.approve_source_promotion or args.attest_reviewed):
|
|
2340
|
+
log.append(
|
|
2341
|
+
"promotion_authorization_blocked",
|
|
2342
|
+
{
|
|
2343
|
+
"task_id": args.task_id,
|
|
2344
|
+
"error": "promotion_presentation_and_authorization_must_be_separate_invocations",
|
|
2345
|
+
"policy": "diff presentation must complete before a later human authorization command can approve that exact artifact hash",
|
|
2346
|
+
"real_project_modified": False,
|
|
2347
|
+
},
|
|
2348
|
+
)
|
|
2349
|
+
code = 2
|
|
2350
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2351
|
+
print("Reason: promotion_presentation_and_authorization_must_be_separate_invocations")
|
|
2352
|
+
else:
|
|
2353
|
+
if args.present_diff:
|
|
2354
|
+
presentation = present_promotion_diff(job_dir, args.task_id, args.reviewer_name, args.notes)
|
|
2355
|
+
if not presentation.get("ok"):
|
|
2356
|
+
log.append("promotion_review_failed", presentation)
|
|
2357
|
+
code = 2
|
|
2358
|
+
print(f"Promotion diff presentation failed: {args.task_id}")
|
|
2359
|
+
else:
|
|
2360
|
+
log.append("promotion_diff_presented", {key: value for key, value in presentation.items() if key != "ok"})
|
|
2361
|
+
code = 0
|
|
2362
|
+
else:
|
|
2363
|
+
presentation_event = latest_diff_presentation(
|
|
2364
|
+
events,
|
|
2365
|
+
args.task_id,
|
|
2366
|
+
str(artifacts["bundle_sha256"]),
|
|
2367
|
+
str(artifacts["dry_run_diff_sha256"]),
|
|
2368
|
+
)
|
|
2369
|
+
rejection_event = matching_diff_rejection(
|
|
2370
|
+
events,
|
|
2371
|
+
args.task_id,
|
|
2372
|
+
str(artifacts["bundle_sha256"]),
|
|
2373
|
+
str(artifacts["dry_run_diff_sha256"]),
|
|
2374
|
+
)
|
|
2375
|
+
if args.approve_source_promotion and presentation_event is None:
|
|
2376
|
+
log.append(
|
|
2377
|
+
"promotion_authorization_blocked",
|
|
2378
|
+
{
|
|
2379
|
+
"task_id": args.task_id,
|
|
2380
|
+
"error": "promotion_diff_presentation_required",
|
|
2381
|
+
"required_event": "promotion_diff_presented",
|
|
2382
|
+
"required_bundle_sha256": artifacts["bundle_sha256"],
|
|
2383
|
+
"required_dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2384
|
+
"policy": "human authorization must be tied to a presented diff artifact hash",
|
|
2385
|
+
"real_project_modified": False,
|
|
2386
|
+
},
|
|
2387
|
+
)
|
|
2388
|
+
code = 2
|
|
2389
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2390
|
+
print("Reason: promotion_diff_presentation_required")
|
|
2391
|
+
elif args.approve_source_promotion and rejection_event is not None:
|
|
2392
|
+
log.append(
|
|
2393
|
+
"promotion_authorization_blocked",
|
|
2394
|
+
{
|
|
2395
|
+
"task_id": args.task_id,
|
|
2396
|
+
"error": "promotion_diff_rejected",
|
|
2397
|
+
"rejection_event_seq": rejection_event["event_seq"],
|
|
2398
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2399
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2400
|
+
"policy": "a human-rejected artifact hash cannot later be authorized",
|
|
2401
|
+
"real_project_modified": False,
|
|
2402
|
+
},
|
|
2403
|
+
)
|
|
2404
|
+
code = 2
|
|
2405
|
+
print(f"Promotion authorization blocked: {args.task_id}")
|
|
2406
|
+
print("Reason: promotion_diff_rejected")
|
|
2407
|
+
else:
|
|
2408
|
+
if behavior_preference:
|
|
2409
|
+
preference_path = artifact_dir / "behavior-preference.json"
|
|
2410
|
+
write_json(preference_path, behavior_preference)
|
|
2411
|
+
evidence_path = artifact_dir / "heldout-gene-evidence.json"
|
|
2412
|
+
write_json(evidence_path, gene_evidence)
|
|
2413
|
+
log.append(
|
|
2414
|
+
"promotion_behavior_preference_bound",
|
|
2415
|
+
{
|
|
2416
|
+
"task_id": args.task_id,
|
|
2417
|
+
"reviewer_role": "human",
|
|
2418
|
+
"reviewer_name": args.reviewer_name,
|
|
2419
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2420
|
+
"recurrence_key_sha256": task["recurrence"]["recurrence_key_sha256"],
|
|
2421
|
+
"preference": behavior_preference,
|
|
2422
|
+
"preference_path": str(preference_path.relative_to(job_dir)),
|
|
2423
|
+
"preference_sha256": recurrence_sha256_json(behavior_preference),
|
|
2424
|
+
"heldout_evidence": gene_evidence,
|
|
2425
|
+
"heldout_evidence_path": str(evidence_path.relative_to(job_dir)),
|
|
2426
|
+
"heldout_evidence_sha256": sha256_file(evidence_path),
|
|
2427
|
+
"contains_patch_or_source": False,
|
|
2428
|
+
"behavior_not_answer_human_attested": True,
|
|
2429
|
+
"policy": "reviewer_authored_prefer_behavior_bound_to_authorized_bundle",
|
|
2430
|
+
},
|
|
2431
|
+
)
|
|
2432
|
+
authorization = {
|
|
2433
|
+
"task_id": args.task_id,
|
|
2434
|
+
"authorizer_role": "human",
|
|
2435
|
+
"authorizer_name": args.reviewer_name,
|
|
2436
|
+
"human_source_promotion_authorized": bool(args.approve_source_promotion),
|
|
2437
|
+
"bundle": str(artifacts["patch_bundle"].relative_to(job_dir)),
|
|
2438
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2439
|
+
"dry_run_diff": str(artifacts["dry_run_diff"].relative_to(job_dir)),
|
|
2440
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2441
|
+
"rollback_manifest": str(artifacts["rollback_manifest"].relative_to(job_dir)),
|
|
2442
|
+
"rollback_manifest_sha256": artifacts["rollback_manifest_sha256"],
|
|
2443
|
+
"presentation_event_seq": presentation_event["event_seq"] if presentation_event else None,
|
|
2444
|
+
"notes": args.notes,
|
|
2445
|
+
"policy": "human_authorization_required_after_ai_prescreen_before_real_project_write",
|
|
2446
|
+
}
|
|
2447
|
+
authorization_path = artifact_dir / "human-promotion-authorization.json"
|
|
2448
|
+
authorization_path.write_text(json.dumps(authorization, indent=2) + "\n", encoding="utf-8")
|
|
2449
|
+
log.append(
|
|
2450
|
+
"promotion_human_authorized",
|
|
2451
|
+
{
|
|
2452
|
+
**authorization,
|
|
2453
|
+
"authorization": str(authorization_path.relative_to(job_dir)),
|
|
2454
|
+
"authorization_sha256": sha256_file(authorization_path),
|
|
2455
|
+
},
|
|
2456
|
+
)
|
|
2457
|
+
if args.attest_reviewed:
|
|
2458
|
+
attestation = {
|
|
2459
|
+
"task_id": args.task_id,
|
|
2460
|
+
"reviewer_role": "human",
|
|
2461
|
+
"reviewer_name": args.reviewer_name,
|
|
2462
|
+
"human_review_attested": True,
|
|
2463
|
+
"attestation_scope": "self_declared_review_of_presented_diff",
|
|
2464
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2465
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2466
|
+
"presentation_event_seq": presentation_event["event_seq"] if presentation_event else None,
|
|
2467
|
+
"notes": args.notes,
|
|
2468
|
+
}
|
|
2469
|
+
attestation_path = artifact_dir / "human-review-attestation.json"
|
|
2470
|
+
attestation_path.write_text(json.dumps(attestation, indent=2) + "\n", encoding="utf-8")
|
|
2471
|
+
log.append(
|
|
2472
|
+
"promotion_human_review_attested",
|
|
2473
|
+
{
|
|
2474
|
+
**attestation,
|
|
2475
|
+
"attestation": str(attestation_path.relative_to(job_dir)),
|
|
2476
|
+
"attestation_sha256": sha256_file(attestation_path),
|
|
2477
|
+
},
|
|
2478
|
+
)
|
|
2479
|
+
# Legacy compatibility event for old analysis tools. It is now
|
|
2480
|
+
# explicitly scoped as authorization, not proof of observed review.
|
|
2481
|
+
legacy = {
|
|
2482
|
+
"task_id": args.task_id,
|
|
2483
|
+
"reviewer_role": "human",
|
|
2484
|
+
"reviewer_name": args.reviewer_name,
|
|
2485
|
+
"reviewed_artifacts": {
|
|
2486
|
+
"dry_run_diff": str(artifacts["dry_run_diff"].relative_to(job_dir)),
|
|
2487
|
+
"patch_bundle": str(artifacts["patch_bundle"].relative_to(job_dir)),
|
|
2488
|
+
"rollback_manifest": str(artifacts["rollback_manifest"].relative_to(job_dir)),
|
|
2489
|
+
},
|
|
2490
|
+
"human_source_promotion_approval_granted": bool(args.approve_source_promotion),
|
|
2491
|
+
"authorization_event": "promotion_human_authorized",
|
|
2492
|
+
"review_proof_scope": "authorization_only_unless_promotion_human_review_attested_exists",
|
|
2493
|
+
"notes": args.notes,
|
|
2494
|
+
"policy": "legacy_compatibility_event_do_not_treat_as_proof_of_observed_review",
|
|
2495
|
+
}
|
|
2496
|
+
legacy_path = artifact_dir / "human-promotion-review.json"
|
|
2497
|
+
legacy_path.write_text(json.dumps(legacy, indent=2) + "\n", encoding="utf-8")
|
|
2498
|
+
log.append(
|
|
2499
|
+
"promotion_human_diff_reviewed",
|
|
2500
|
+
{
|
|
2501
|
+
**legacy,
|
|
2502
|
+
"review": str(legacy_path.relative_to(job_dir)),
|
|
2503
|
+
"sha256": sha256_file(legacy_path),
|
|
2504
|
+
},
|
|
2505
|
+
)
|
|
2506
|
+
code = 0 if args.approve_source_promotion else 2
|
|
2507
|
+
print(f"Promotion human authorization recorded: {args.task_id}")
|
|
2508
|
+
print(f"Authorized for source promotion: {bool(args.approve_source_promotion)}")
|
|
2509
|
+
|
|
2510
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2511
|
+
state = replay(events)
|
|
2512
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2513
|
+
write_json(job_dir / "plan.json", plan)
|
|
2514
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
2515
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
2516
|
+
return code
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
def job_reject_promotion_main(args: argparse.Namespace) -> int:
|
|
2520
|
+
project = args.project.expanduser().resolve()
|
|
2521
|
+
job_dir = find_job_dir(project, args.job_id)
|
|
2522
|
+
with JobLock(job_dir / ".lock"):
|
|
2523
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2524
|
+
state = replay(events)
|
|
2525
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2526
|
+
task = next((item for item in plan.get("tasks", []) if item.get("id") == args.task_id), None)
|
|
2527
|
+
artifacts = promotion_artifact_hashes(job_dir, args.task_id)
|
|
2528
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
2529
|
+
presentation = None
|
|
2530
|
+
if not artifacts.get("missing"):
|
|
2531
|
+
presentation = latest_diff_presentation(
|
|
2532
|
+
events,
|
|
2533
|
+
args.task_id,
|
|
2534
|
+
str(artifacts["bundle_sha256"]),
|
|
2535
|
+
str(artifacts["dry_run_diff_sha256"]),
|
|
2536
|
+
)
|
|
2537
|
+
if not task or task.get("status") != "passed":
|
|
2538
|
+
log.append(
|
|
2539
|
+
"promotion_rejection_blocked",
|
|
2540
|
+
{"task_id": args.task_id, "error": "promotion_task_not_passed", "real_project_modified": False},
|
|
2541
|
+
)
|
|
2542
|
+
code = 2
|
|
2543
|
+
print(f"Promotion rejection blocked: {args.task_id}")
|
|
2544
|
+
print("Reason: promotion_task_not_passed")
|
|
2545
|
+
elif artifacts.get("missing"):
|
|
2546
|
+
log.append(
|
|
2547
|
+
"promotion_rejection_blocked",
|
|
2548
|
+
{
|
|
2549
|
+
"task_id": args.task_id,
|
|
2550
|
+
"error": "promotion_artifacts_missing",
|
|
2551
|
+
"missing": artifacts.get("missing"),
|
|
2552
|
+
"real_project_modified": False,
|
|
2553
|
+
},
|
|
2554
|
+
)
|
|
2555
|
+
code = 2
|
|
2556
|
+
print(f"Promotion rejection blocked: {args.task_id}")
|
|
2557
|
+
print("Reason: promotion_artifacts_missing")
|
|
2558
|
+
elif presentation is None:
|
|
2559
|
+
log.append(
|
|
2560
|
+
"promotion_rejection_blocked",
|
|
2561
|
+
{
|
|
2562
|
+
"task_id": args.task_id,
|
|
2563
|
+
"error": "promotion_diff_presentation_required",
|
|
2564
|
+
"required_event": "promotion_diff_presented",
|
|
2565
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2566
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2567
|
+
"real_project_modified": False,
|
|
2568
|
+
},
|
|
2569
|
+
)
|
|
2570
|
+
code = 2
|
|
2571
|
+
print(f"Promotion rejection blocked: {args.task_id}")
|
|
2572
|
+
print("Reason: promotion_diff_presentation_required")
|
|
2573
|
+
elif matching_diff_rejection(
|
|
2574
|
+
events,
|
|
2575
|
+
args.task_id,
|
|
2576
|
+
str(artifacts["bundle_sha256"]),
|
|
2577
|
+
str(artifacts["dry_run_diff_sha256"]),
|
|
2578
|
+
):
|
|
2579
|
+
print(f"Promotion rejection already recorded: {args.task_id}")
|
|
2580
|
+
code = 0
|
|
2581
|
+
else:
|
|
2582
|
+
rejection = {
|
|
2583
|
+
"task_id": args.task_id,
|
|
2584
|
+
"reviewer_role": "human",
|
|
2585
|
+
"reviewer_name": args.reviewer_name,
|
|
2586
|
+
"human_source_promotion_rejected": True,
|
|
2587
|
+
"failure_class": "human_rejects_diff",
|
|
2588
|
+
"error": "human_rejected_presented_diff",
|
|
2589
|
+
"reason": args.reason,
|
|
2590
|
+
"notes": args.notes,
|
|
2591
|
+
"bundle": str(artifacts["patch_bundle"].relative_to(job_dir)),
|
|
2592
|
+
"bundle_sha256": artifacts["bundle_sha256"],
|
|
2593
|
+
"dry_run_diff": str(artifacts["dry_run_diff"].relative_to(job_dir)),
|
|
2594
|
+
"dry_run_diff_sha256": artifacts["dry_run_diff_sha256"],
|
|
2595
|
+
"rollback_manifest": str(artifacts["rollback_manifest"].relative_to(job_dir)),
|
|
2596
|
+
"rollback_manifest_sha256": artifacts["rollback_manifest_sha256"],
|
|
2597
|
+
"presentation_event_seq": presentation["event_seq"],
|
|
2598
|
+
"policy": "human rejection is terminal for the exact presented artifact hashes",
|
|
2599
|
+
"real_project_modified": False,
|
|
2600
|
+
}
|
|
2601
|
+
rejection_path = artifacts["artifact_dir"] / "human-promotion-rejection.json"
|
|
2602
|
+
rejection_path.write_text(json.dumps(rejection, indent=2) + "\n", encoding="utf-8")
|
|
2603
|
+
rejection_event = log.append(
|
|
2604
|
+
"promotion_human_rejected",
|
|
2605
|
+
{
|
|
2606
|
+
**rejection,
|
|
2607
|
+
"rejection": str(rejection_path.relative_to(job_dir)),
|
|
2608
|
+
"rejection_sha256": sha256_file(rejection_path),
|
|
2609
|
+
},
|
|
2610
|
+
)
|
|
2611
|
+
try:
|
|
2612
|
+
current_events = read_events(job_dir / "events.jsonl")
|
|
2613
|
+
exported = export_failed_trace(project, task, current_events, rejection_event)
|
|
2614
|
+
trace_event_type = "central_trace_exported" if exported.get("index_ok") else "central_trace_index_pending"
|
|
2615
|
+
log.append(
|
|
2616
|
+
trace_event_type,
|
|
2617
|
+
{
|
|
2618
|
+
"task_id": args.task_id,
|
|
2619
|
+
"source_event_seq": rejection_event.seq,
|
|
2620
|
+
**exported,
|
|
2621
|
+
},
|
|
2622
|
+
)
|
|
2623
|
+
if not exported.get("index_ok"):
|
|
2624
|
+
print("Warning: rejection trace is durable but Mumpix indexing is pending; run `wydcode traces sync --project .`")
|
|
2625
|
+
except Exception as exc: # noqa: BLE001 - local rejection remains authoritative.
|
|
2626
|
+
log.append(
|
|
2627
|
+
"central_trace_export_failed",
|
|
2628
|
+
{
|
|
2629
|
+
"task_id": args.task_id,
|
|
2630
|
+
"source_event_seq": rejection_event.seq,
|
|
2631
|
+
"error": str(exc),
|
|
2632
|
+
"ledger_status": "unknown",
|
|
2633
|
+
"next_action": "run `wydcode traces sync --project .`",
|
|
2634
|
+
},
|
|
2635
|
+
)
|
|
2636
|
+
print(f"Warning: rejection trace export failed; run `wydcode traces sync --project .`: {exc}")
|
|
2637
|
+
code = 0
|
|
2638
|
+
print(f"Promotion rejected: {args.task_id}")
|
|
2639
|
+
print("Real project modified: false")
|
|
2640
|
+
|
|
2641
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2642
|
+
state = replay(events)
|
|
2643
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2644
|
+
write_json(job_dir / "plan.json", plan)
|
|
2645
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
2646
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
2647
|
+
return code
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
def job_promote_main(args: argparse.Namespace) -> int:
|
|
2651
|
+
project = args.project.expanduser().resolve()
|
|
2652
|
+
job_dir = find_job_dir(project, args.job_id)
|
|
2653
|
+
with JobLock(job_dir / ".lock"):
|
|
2654
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2655
|
+
state = replay(events)
|
|
2656
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2657
|
+
task = next((item for item in plan.get("tasks", []) if item.get("id") == args.task_id), None)
|
|
2658
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
2659
|
+
generated = ensure_promotion_artifacts(project, job_dir, args.task_id)
|
|
2660
|
+
if generated.get("generated"):
|
|
2661
|
+
log.append("promotion_artifacts_generated", {"task_id": args.task_id, **generated})
|
|
2662
|
+
human_review = human_promotion_review(events, args.task_id)
|
|
2663
|
+
artifacts = promotion_artifact_hashes(job_dir, args.task_id)
|
|
2664
|
+
if not task or task.get("status") != "passed":
|
|
2665
|
+
log.append(
|
|
2666
|
+
"promotion_blocked",
|
|
2667
|
+
{
|
|
2668
|
+
"task_id": args.task_id,
|
|
2669
|
+
"error": "promotion_task_not_passed",
|
|
2670
|
+
"task_status": task.get("status") if task else "missing",
|
|
2671
|
+
"policy": "promotion_requires_passed_rehearsal_task_and_human_diff_review",
|
|
2672
|
+
},
|
|
2673
|
+
)
|
|
2674
|
+
code = 2
|
|
2675
|
+
print(f"Promotion blocked: {args.task_id}")
|
|
2676
|
+
print("Reason: promotion_task_not_passed")
|
|
2677
|
+
elif human_review is None:
|
|
2678
|
+
log.append(
|
|
2679
|
+
"promotion_blocked",
|
|
2680
|
+
{
|
|
2681
|
+
"task_id": args.task_id,
|
|
2682
|
+
"error": "human_source_promotion_review_required",
|
|
2683
|
+
"policy": "ai_prescreen_is_not_source_promotion_authorization",
|
|
2684
|
+
"required_event": "promotion_human_authorized",
|
|
2685
|
+
"required_field": "human_source_promotion_authorized=true",
|
|
2686
|
+
"legacy_accepted_for_existing_jobs": "promotion_human_diff_reviewed",
|
|
2687
|
+
"real_project_modified": False,
|
|
2688
|
+
},
|
|
2689
|
+
)
|
|
2690
|
+
code = 2
|
|
2691
|
+
print(f"Promotion blocked: {args.task_id}")
|
|
2692
|
+
print("Reason: human_source_promotion_review_required")
|
|
2693
|
+
elif artifacts.get("missing"):
|
|
2694
|
+
log.append(
|
|
2695
|
+
"promotion_blocked",
|
|
2696
|
+
{
|
|
2697
|
+
"task_id": args.task_id,
|
|
2698
|
+
"error": "promotion_artifacts_missing",
|
|
2699
|
+
"missing": artifacts.get("missing"),
|
|
2700
|
+
"human_review": human_review,
|
|
2701
|
+
"real_project_modified": False,
|
|
2702
|
+
},
|
|
2703
|
+
)
|
|
2704
|
+
code = 2
|
|
2705
|
+
print(f"Promotion blocked: {args.task_id}")
|
|
2706
|
+
print("Reason: promotion_artifacts_missing")
|
|
2707
|
+
elif human_review.get("bundle_sha256") and human_review.get("bundle_sha256") != artifacts.get("bundle_sha256"):
|
|
2708
|
+
log.append(
|
|
2709
|
+
"promotion_blocked",
|
|
2710
|
+
{
|
|
2711
|
+
"task_id": args.task_id,
|
|
2712
|
+
"error": "promotion_authorized_bundle_hash_mismatch",
|
|
2713
|
+
"authorized_bundle_sha256": human_review.get("bundle_sha256"),
|
|
2714
|
+
"current_bundle_sha256": artifacts.get("bundle_sha256"),
|
|
2715
|
+
"human_review": human_review,
|
|
2716
|
+
"policy": "promotion applies only the exact bundle hash that was presented and authorized",
|
|
2717
|
+
"real_project_modified": False,
|
|
2718
|
+
},
|
|
2719
|
+
)
|
|
2720
|
+
code = 2
|
|
2721
|
+
print(f"Promotion blocked: {args.task_id}")
|
|
2722
|
+
print("Reason: promotion_authorized_bundle_hash_mismatch")
|
|
2723
|
+
elif not args.execute:
|
|
2724
|
+
log.append(
|
|
2725
|
+
"promotion_preflight_passed",
|
|
2726
|
+
{
|
|
2727
|
+
"task_id": args.task_id,
|
|
2728
|
+
"human_review": human_review,
|
|
2729
|
+
"real_project_modified": False,
|
|
2730
|
+
"next_action": "rerun with --execute to apply the approved bundle",
|
|
2731
|
+
},
|
|
2732
|
+
)
|
|
2733
|
+
code = 0
|
|
2734
|
+
print(f"Promotion preflight passed: {args.task_id}")
|
|
2735
|
+
print("Real project modified: false")
|
|
2736
|
+
else:
|
|
2737
|
+
apply_result = promotion_apply_bundle(project, job_dir, args.task_id)
|
|
2738
|
+
if apply_result.get("ok"):
|
|
2739
|
+
receipt = apply_result.get("receipt") or {}
|
|
2740
|
+
promotion_event = log.append(
|
|
2741
|
+
"promotion_applied",
|
|
2742
|
+
{
|
|
2743
|
+
"task_id": args.task_id,
|
|
2744
|
+
"bundle_sha256": receipt.get("bundle_sha256"),
|
|
2745
|
+
"applied_count": receipt.get("applied_count"),
|
|
2746
|
+
"receipt_path": apply_result.get("receipt_path"),
|
|
2747
|
+
"receipt_sha256": apply_result.get("receipt_sha256"),
|
|
2748
|
+
"rollback_content_snapshot_path": apply_result.get("rollback_content_snapshot"),
|
|
2749
|
+
"rollback_content_snapshot_sha256": apply_result.get("rollback_content_snapshot_sha256"),
|
|
2750
|
+
"human_review": human_review,
|
|
2751
|
+
"real_project_modified": True,
|
|
2752
|
+
},
|
|
2753
|
+
)
|
|
2754
|
+
fired_event = next(
|
|
2755
|
+
(
|
|
2756
|
+
event
|
|
2757
|
+
for event in reversed(read_events(job_dir / "events.jsonl"))
|
|
2758
|
+
if event.type == "retrieval_fired" and event.payload.get("task_id") == args.task_id
|
|
2759
|
+
),
|
|
2760
|
+
None,
|
|
2761
|
+
)
|
|
2762
|
+
fired = fired_event.to_json() if fired_event else None
|
|
2763
|
+
if fired:
|
|
2764
|
+
log.append(
|
|
2765
|
+
"retrieval_backed_promotion",
|
|
2766
|
+
{
|
|
2767
|
+
"task_id": args.task_id,
|
|
2768
|
+
"retrieval_event_seq": fired.get("seq"),
|
|
2769
|
+
"retrieval_event_sha256": recurrence_sha256_json(fired),
|
|
2770
|
+
"gene_id": fired.get("payload", {}).get("gene_id"),
|
|
2771
|
+
"source_central_event_sha256": fired.get("payload", {}).get("source_central_event_sha256"),
|
|
2772
|
+
"recurrence_key_sha256": fired.get("payload", {}).get("recurrence_key_sha256"),
|
|
2773
|
+
"preference_sha256": fired.get("payload", {}).get("preference_sha256"),
|
|
2774
|
+
"promotion_event_seq": promotion_event.seq,
|
|
2775
|
+
"promotion_receipt_sha256": apply_result.get("receipt_sha256"),
|
|
2776
|
+
"classification": "route_fired_and_promoted_not_paired_capability_lift",
|
|
2777
|
+
},
|
|
2778
|
+
)
|
|
2779
|
+
try:
|
|
2780
|
+
export_events = read_events(job_dir / "events.jsonl")
|
|
2781
|
+
export_result = export_promoted_trace(project, job_dir, task, export_events, promotion_event)
|
|
2782
|
+
log.append(
|
|
2783
|
+
"central_trace_exported" if export_result.get("index_ok") else "central_trace_export_partial",
|
|
2784
|
+
{
|
|
2785
|
+
"task_id": args.task_id,
|
|
2786
|
+
**export_result,
|
|
2787
|
+
"source_of_truth": "cross_project_hash_chained_jsonl",
|
|
2788
|
+
"mumpix_role": "derived_rebuildable_index",
|
|
2789
|
+
"retry_required": not export_result.get("index_ok"),
|
|
2790
|
+
},
|
|
2791
|
+
)
|
|
2792
|
+
if not export_result.get("index_ok"):
|
|
2793
|
+
print("Warning: promotion is durable, but its Mumpix index update failed.")
|
|
2794
|
+
print("Run `wydcode traces sync --project .` and verify `wydcode traces status` reports zero pending events.")
|
|
2795
|
+
except Exception as exc: # noqa: BLE001 - promotion already succeeded and must remain successful.
|
|
2796
|
+
log.append(
|
|
2797
|
+
"central_trace_export_failed",
|
|
2798
|
+
{
|
|
2799
|
+
"task_id": args.task_id,
|
|
2800
|
+
"error": str(exc)[:1000],
|
|
2801
|
+
"promotion_remains_applied": True,
|
|
2802
|
+
"retry_command": "wydcode traces sync --project .",
|
|
2803
|
+
},
|
|
2804
|
+
)
|
|
2805
|
+
print(f"Warning: promotion is durable, but central trace export failed: {exc}")
|
|
2806
|
+
print("Run `wydcode traces sync --project .` before relying on the cross-project trace index.")
|
|
2807
|
+
code = 0
|
|
2808
|
+
print(f"Promotion applied: {args.task_id}")
|
|
2809
|
+
print(f"Applied files: {receipt.get('applied_count')}")
|
|
2810
|
+
print(f"Receipt: {apply_result.get('receipt_path')}")
|
|
2811
|
+
else:
|
|
2812
|
+
log.append(
|
|
2813
|
+
"promotion_failed",
|
|
2814
|
+
{
|
|
2815
|
+
"task_id": args.task_id,
|
|
2816
|
+
"error": apply_result.get("error"),
|
|
2817
|
+
"details": apply_result,
|
|
2818
|
+
"human_review": human_review,
|
|
2819
|
+
"real_project_modified": apply_result.get("real_project_modified", False),
|
|
2820
|
+
},
|
|
2821
|
+
)
|
|
2822
|
+
code = 2
|
|
2823
|
+
print(f"Promotion failed: {args.task_id}")
|
|
2824
|
+
print(f"Reason: {apply_result.get('error')}")
|
|
2825
|
+
|
|
2826
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2827
|
+
state = replay(events)
|
|
2828
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2829
|
+
write_json(job_dir / "plan.json", plan)
|
|
2830
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
2831
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
2832
|
+
return code
|
|
2833
|
+
|
|
2834
|
+
|
|
2835
|
+
def job_reverify_task_main(args: argparse.Namespace) -> int:
|
|
2836
|
+
project = args.project.expanduser().resolve()
|
|
2837
|
+
if args.job_id:
|
|
2838
|
+
job_dir = job_root(project) / args.job_id
|
|
2839
|
+
else:
|
|
2840
|
+
found = find_latest_incomplete_job(project)
|
|
2841
|
+
if not found:
|
|
2842
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
2843
|
+
job_dir = found
|
|
2844
|
+
|
|
2845
|
+
with JobLock(job_dir / ".lock"):
|
|
2846
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2847
|
+
state = replay(events)
|
|
2848
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2849
|
+
tasks = {str(task.get("id")): task for task in plan.get("tasks", [])}
|
|
2850
|
+
task = tasks.get(args.task_id)
|
|
2851
|
+
if not task:
|
|
2852
|
+
raise SystemExit(f"Task not found: {args.task_id}")
|
|
2853
|
+
if task.get("status") != "passed" and not args.allow_unpassed:
|
|
2854
|
+
raise SystemExit(f"Task is not passed: {args.task_id}. Pass --allow-unpassed to record evidence anyway.")
|
|
2855
|
+
|
|
2856
|
+
evidence_path = args.evidence.expanduser().resolve()
|
|
2857
|
+
evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
|
|
2858
|
+
criteria, errors, all_passed = validate_reverification_evidence(task, evidence)
|
|
2859
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / args.task_id
|
|
2860
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
2861
|
+
stored_evidence = artifact_dir / "reverification-evidence.json"
|
|
2862
|
+
stored_evidence.write_text(json.dumps({"task_id": args.task_id, **evidence, "criteria": criteria}, indent=2) + "\n", encoding="utf-8")
|
|
2863
|
+
|
|
2864
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
2865
|
+
if errors:
|
|
2866
|
+
log.append(
|
|
2867
|
+
"task_reverification_failed",
|
|
2868
|
+
{
|
|
2869
|
+
"task_id": args.task_id,
|
|
2870
|
+
"errors": errors,
|
|
2871
|
+
"evidence_path": str(stored_evidence.relative_to(job_dir)),
|
|
2872
|
+
"sha256": sha256_file(stored_evidence),
|
|
2873
|
+
},
|
|
2874
|
+
)
|
|
2875
|
+
code = 2
|
|
2876
|
+
print(f"Task reverification failed: {args.task_id}")
|
|
2877
|
+
for error in errors:
|
|
2878
|
+
print(f"- {error}")
|
|
2879
|
+
else:
|
|
2880
|
+
log.append(
|
|
2881
|
+
"task_reverified",
|
|
2882
|
+
{
|
|
2883
|
+
"task_id": args.task_id,
|
|
2884
|
+
"criteria": criteria,
|
|
2885
|
+
"runner_checks": evidence.get("runner_checks") if isinstance(evidence.get("runner_checks"), list) else [],
|
|
2886
|
+
"review_level": "acceptance_reverified" if all_passed else "acceptance_partial",
|
|
2887
|
+
"acceptance_review": "full" if all_passed else "partial",
|
|
2888
|
+
"all_criteria_passed": all_passed,
|
|
2889
|
+
"evidence_path": str(stored_evidence.relative_to(job_dir)),
|
|
2890
|
+
"sha256": sha256_file(stored_evidence),
|
|
2891
|
+
"notes": evidence.get("notes") or args.notes or "",
|
|
2892
|
+
},
|
|
2893
|
+
)
|
|
2894
|
+
code = 0 if all_passed else 2
|
|
2895
|
+
print(f"Task reverified: {args.task_id}")
|
|
2896
|
+
print(f"Acceptance review: {'full' if all_passed else 'partial'}")
|
|
2897
|
+
|
|
2898
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2899
|
+
state = replay(events)
|
|
2900
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2901
|
+
write_json(job_dir / "plan.json", plan)
|
|
2902
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
2903
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
2904
|
+
return code
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
def job_run_milestone_main(args: argparse.Namespace) -> int:
|
|
2908
|
+
project = args.project.expanduser().resolve()
|
|
2909
|
+
if args.job_id:
|
|
2910
|
+
job_dir = job_root(project) / args.job_id
|
|
2911
|
+
else:
|
|
2912
|
+
found = find_latest_incomplete_job(project)
|
|
2913
|
+
if not found:
|
|
2914
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
2915
|
+
job_dir = found
|
|
2916
|
+
|
|
2917
|
+
with JobLock(job_dir / ".lock"):
|
|
2918
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2919
|
+
state = replay(events)
|
|
2920
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2921
|
+
tasks = plan.get("tasks") if isinstance(plan.get("tasks"), list) else []
|
|
2922
|
+
passed_task_ids = [str(task.get("id")) for task in tasks if task.get("status") == "passed"]
|
|
2923
|
+
milestone_id = args.milestone_id or f"milestone-{len([e for e in events if e.type == 'milestone_runner_started']) + 1:03d}"
|
|
2924
|
+
command = args.command or "npm run build"
|
|
2925
|
+
review_level, inferred_promotion_grade = infer_milestone_review_level(command)
|
|
2926
|
+
promotion_grade = bool(args.promotion_grade or inferred_promotion_grade)
|
|
2927
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
2928
|
+
log.append(
|
|
2929
|
+
"milestone_runner_started",
|
|
2930
|
+
{
|
|
2931
|
+
"milestone_id": milestone_id,
|
|
2932
|
+
"passed_task_ids": passed_task_ids,
|
|
2933
|
+
"command": command,
|
|
2934
|
+
"review_level": review_level,
|
|
2935
|
+
"promotion_grade": promotion_grade,
|
|
2936
|
+
"dependency_prep": args.prepare_deps,
|
|
2937
|
+
"policy": "partial_smoke_reviewer_audit_enforced_dependency_vulnerabilities_block_only_promotion_grade",
|
|
2938
|
+
},
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
partials = tasks_requiring_milestone_reverification(events, passed_task_ids)
|
|
2942
|
+
if partials and not args.allow_partial_smoke:
|
|
2943
|
+
log.append(
|
|
2944
|
+
"milestone_runner_failed",
|
|
2945
|
+
{
|
|
2946
|
+
"milestone_id": milestone_id,
|
|
2947
|
+
"error": "partial_smoke_tasks_require_reverification",
|
|
2948
|
+
"review_level": review_level,
|
|
2949
|
+
"promotion_grade": False,
|
|
2950
|
+
"partial_tasks": partials,
|
|
2951
|
+
"next_action": "run a milestone preview/build/review that proves acceptance, then emit a reclassification event",
|
|
2952
|
+
},
|
|
2953
|
+
)
|
|
2954
|
+
events = read_events(job_dir / "events.jsonl")
|
|
2955
|
+
state = replay(events)
|
|
2956
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
2957
|
+
write_json(job_dir / "plan.json", plan)
|
|
2958
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
2959
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
2960
|
+
print(f"Milestone runner failed: {milestone_id}")
|
|
2961
|
+
print("Reason: partial_smoke_tasks_require_reverification")
|
|
2962
|
+
for item in partials:
|
|
2963
|
+
print(f"- {item['task_id']}: {item['review_level']} / {item['acceptance_review']}")
|
|
2964
|
+
return 2
|
|
2965
|
+
|
|
2966
|
+
runner_dir = job_dir / "sandbox" / "milestones" / milestone_id
|
|
2967
|
+
if runner_dir.exists():
|
|
2968
|
+
shutil.rmtree(runner_dir)
|
|
2969
|
+
overlay_dir = runner_dir / "project"
|
|
2970
|
+
overlays = overlay_passed_task_artifacts(project, job_dir, plan, overlay_dir)
|
|
2971
|
+
dependency_links = link_existing_dependency_trees(project, overlay_dir) if args.prepare_deps == "none" else []
|
|
2972
|
+
log.append(
|
|
2973
|
+
"milestone_overlay_written",
|
|
2974
|
+
{
|
|
2975
|
+
"milestone_id": milestone_id,
|
|
2976
|
+
"overlay_dir": str(overlay_dir.relative_to(job_dir)),
|
|
2977
|
+
"overlays": overlays,
|
|
2978
|
+
"reused_dependency_trees": dependency_links,
|
|
2979
|
+
},
|
|
2980
|
+
)
|
|
2981
|
+
|
|
2982
|
+
dep_payload, dep_code = prepare_runner_dependencies(overlay_dir, args.prepare_deps, args.deps_timeout_seconds)
|
|
2983
|
+
exception_counts = active_vulnerability_exception_counts(events)
|
|
2984
|
+
dep_vuln_policy = dependency_vulnerability_policy(dep_payload, promotion_grade, exception_counts) if args.prepare_deps != "none" else None
|
|
2985
|
+
if args.prepare_deps != "none":
|
|
2986
|
+
log.append(
|
|
2987
|
+
"sandbox_env_prepared",
|
|
2988
|
+
{
|
|
2989
|
+
"milestone_id": milestone_id,
|
|
2990
|
+
"overlay_dir": str(overlay_dir.relative_to(job_dir)),
|
|
2991
|
+
**dep_payload,
|
|
2992
|
+
"dependency_vulnerability_policy": dep_vuln_policy,
|
|
2993
|
+
},
|
|
2994
|
+
)
|
|
2995
|
+
if dep_code != 0:
|
|
2996
|
+
payload = {
|
|
2997
|
+
"milestone_id": milestone_id,
|
|
2998
|
+
"command": command,
|
|
2999
|
+
"returncode": dep_code,
|
|
3000
|
+
"overlay_dir": str(overlay_dir.relative_to(job_dir)),
|
|
3001
|
+
"overlays_count": len(overlays),
|
|
3002
|
+
"review_level": review_level,
|
|
3003
|
+
"promotion_grade": False,
|
|
3004
|
+
"dependency_prep": dep_payload,
|
|
3005
|
+
"error": "sandbox_dependency_preparation_failed",
|
|
3006
|
+
}
|
|
3007
|
+
log.append(
|
|
3008
|
+
"milestone_runner_unavailable",
|
|
3009
|
+
{**payload, "outcome": "infrastructure_unavailable", "execution_reached_test_runner": False},
|
|
3010
|
+
)
|
|
3011
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3012
|
+
state = replay(events)
|
|
3013
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3014
|
+
write_json(job_dir / "plan.json", plan)
|
|
3015
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3016
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3017
|
+
print(f"Milestone runner unavailable: {milestone_id}")
|
|
3018
|
+
print("Reason: sandbox_dependency_preparation_failed")
|
|
3019
|
+
print(str(dep_payload.get("stderr_tail") or dep_payload.get("stdout_tail") or "")[-2000:])
|
|
3020
|
+
return 2
|
|
3021
|
+
if dep_vuln_policy and dep_vuln_policy.get("blocks_promotion"):
|
|
3022
|
+
payload = {
|
|
3023
|
+
"milestone_id": milestone_id,
|
|
3024
|
+
"command": command,
|
|
3025
|
+
"returncode": 2,
|
|
3026
|
+
"overlay_dir": str(overlay_dir.relative_to(job_dir)),
|
|
3027
|
+
"overlays_count": len(overlays),
|
|
3028
|
+
"review_level": review_level,
|
|
3029
|
+
"promotion_grade": False,
|
|
3030
|
+
"dependency_prep": args.prepare_deps,
|
|
3031
|
+
"dependency_vulnerability_policy": dep_vuln_policy,
|
|
3032
|
+
"error": "dependency_vulnerability_policy_blocked",
|
|
3033
|
+
}
|
|
3034
|
+
log.append("milestone_runner_failed", payload)
|
|
3035
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3036
|
+
state = replay(events)
|
|
3037
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3038
|
+
write_json(job_dir / "plan.json", plan)
|
|
3039
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3040
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3041
|
+
print(f"Milestone runner failed: {milestone_id}")
|
|
3042
|
+
print("Reason: dependency_vulnerability_policy_blocked")
|
|
3043
|
+
print(f"Vulnerabilities: {dep_vuln_policy.get('counts')}")
|
|
3044
|
+
return 2
|
|
3045
|
+
|
|
3046
|
+
output_dir = runner_dir / "logs"
|
|
3047
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
3048
|
+
stdout_path = output_dir / "stdout.log"
|
|
3049
|
+
stderr_path = output_dir / "stderr.log"
|
|
3050
|
+
try:
|
|
3051
|
+
runner_env = os.environ.copy()
|
|
3052
|
+
if args.prepare_deps == "none":
|
|
3053
|
+
# A gate may inspect existing dependencies, but it must not repair or install them implicitly.
|
|
3054
|
+
runner_env["pnpm_config_verify_deps_before_run"] = "warn"
|
|
3055
|
+
result = subprocess.run(
|
|
3056
|
+
command,
|
|
3057
|
+
cwd=overlay_dir,
|
|
3058
|
+
shell=True,
|
|
3059
|
+
text=True,
|
|
3060
|
+
capture_output=True,
|
|
3061
|
+
timeout=args.timeout_seconds,
|
|
3062
|
+
check=False,
|
|
3063
|
+
env=runner_env,
|
|
3064
|
+
)
|
|
3065
|
+
stdout = result.stdout or ""
|
|
3066
|
+
stderr = result.stderr or ""
|
|
3067
|
+
returncode = result.returncode
|
|
3068
|
+
outcome, reached_test_runner, error = classify_gate_process(returncode, stdout, stderr)
|
|
3069
|
+
except subprocess.TimeoutExpired as exc:
|
|
3070
|
+
stdout = str(exc.stdout or "")
|
|
3071
|
+
stderr = str(exc.stderr or "")
|
|
3072
|
+
returncode = 124
|
|
3073
|
+
outcome, reached_test_runner, error = "infrastructure_unavailable", False, "runner_timeout"
|
|
3074
|
+
stdout_path.write_text(stdout, encoding="utf-8")
|
|
3075
|
+
stderr_path.write_text(stderr, encoding="utf-8")
|
|
3076
|
+
manifest_after_command = dependency_manifest(overlay_dir)
|
|
3077
|
+
payload = {
|
|
3078
|
+
"milestone_id": milestone_id,
|
|
3079
|
+
"command": command,
|
|
3080
|
+
"returncode": returncode,
|
|
3081
|
+
"stdout": str(stdout_path.relative_to(job_dir)),
|
|
3082
|
+
"stderr": str(stderr_path.relative_to(job_dir)),
|
|
3083
|
+
"overlay_dir": str(overlay_dir.relative_to(job_dir)),
|
|
3084
|
+
"overlays_count": len(overlays),
|
|
3085
|
+
"review_level": review_level,
|
|
3086
|
+
"promotion_grade": promotion_grade,
|
|
3087
|
+
"dependency_prep": args.prepare_deps,
|
|
3088
|
+
"dependency_auto_repair": False if args.prepare_deps == "none" else None,
|
|
3089
|
+
"reused_dependency_trees": dependency_links,
|
|
3090
|
+
"manifest_after_command": manifest_after_command,
|
|
3091
|
+
"outcome": outcome,
|
|
3092
|
+
"execution_reached_test_runner": reached_test_runner,
|
|
3093
|
+
}
|
|
3094
|
+
if dep_vuln_policy is not None:
|
|
3095
|
+
payload["dependency_vulnerability_policy"] = dep_vuln_policy
|
|
3096
|
+
if outcome == "passed":
|
|
3097
|
+
log.append("milestone_runner_passed", payload)
|
|
3098
|
+
code = 0
|
|
3099
|
+
print(f"Milestone runner passed: {milestone_id}")
|
|
3100
|
+
elif outcome == "infrastructure_unavailable":
|
|
3101
|
+
log.append("milestone_runner_unavailable", {**payload, "error": error})
|
|
3102
|
+
code = 3
|
|
3103
|
+
print(f"Milestone runner unavailable: {milestone_id}")
|
|
3104
|
+
print(f"Reason: {error}")
|
|
3105
|
+
print((stderr or stdout)[-2000:])
|
|
3106
|
+
else:
|
|
3107
|
+
log.append("milestone_runner_failed", {**payload, "error": error})
|
|
3108
|
+
code = 2
|
|
3109
|
+
print(f"Milestone runner failed: {milestone_id}")
|
|
3110
|
+
print(f"Reason: {error}")
|
|
3111
|
+
print((stderr or stdout)[-2000:])
|
|
3112
|
+
|
|
3113
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3114
|
+
state = replay(events)
|
|
3115
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3116
|
+
write_json(job_dir / "plan.json", plan)
|
|
3117
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3118
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3119
|
+
return code
|
|
3120
|
+
|
|
3121
|
+
|
|
3122
|
+
def job_run_next_main(args: argparse.Namespace) -> int:
|
|
3123
|
+
project = args.project.expanduser().resolve()
|
|
3124
|
+
if args.job_id:
|
|
3125
|
+
job_dir = job_root(project) / args.job_id
|
|
3126
|
+
else:
|
|
3127
|
+
found = find_latest_incomplete_job(project)
|
|
3128
|
+
if not found:
|
|
3129
|
+
raise SystemExit(f"No incomplete Socra jobs found under {job_root(project)}")
|
|
3130
|
+
job_dir = found
|
|
3131
|
+
|
|
3132
|
+
with JobLock(job_dir / ".lock"):
|
|
3133
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3134
|
+
state = replay(events)
|
|
3135
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3136
|
+
ready = ready_task_ids(plan)
|
|
3137
|
+
if not ready:
|
|
3138
|
+
raise SystemExit("No ready pending tasks.")
|
|
3139
|
+
task_id = args.task_id or ready[0]
|
|
3140
|
+
tasks = {str(task.get("id")): task for task in plan.get("tasks", [])}
|
|
3141
|
+
task = tasks.get(task_id)
|
|
3142
|
+
if not task:
|
|
3143
|
+
raise SystemExit(f"Task not found: {task_id}")
|
|
3144
|
+
|
|
3145
|
+
log = JobEventLog(job_dir / "events.jsonl", state.job_id or job_dir.name)
|
|
3146
|
+
creator = state.creator_config or {}
|
|
3147
|
+
health = endpoint_health(str(creator.get("endpoint") or "http://127.0.0.1:11439/v1"))
|
|
3148
|
+
expected_model = str(creator.get("expected_served_model_id") or Path(str(creator.get("model") or "")).name)
|
|
3149
|
+
if expected_model:
|
|
3150
|
+
served_ids = [str(item) for item in health.get("model_ids") or []]
|
|
3151
|
+
health["expected_served_model_id"] = expected_model
|
|
3152
|
+
health["model_identity_ok"] = expected_model in served_ids
|
|
3153
|
+
health["served_model_ids"] = served_ids
|
|
3154
|
+
log.append("creator_endpoint_health_checked", {"task_id": task_id, **health})
|
|
3155
|
+
model_backed = task_id != "task-001"
|
|
3156
|
+
if model_backed and (not health.get("ok") or not health.get("model_identity_ok")):
|
|
3157
|
+
error = "creator_endpoint_unavailable" if not health.get("ok") else "creator_model_identity_mismatch"
|
|
3158
|
+
log.append(
|
|
3159
|
+
"task_blocked",
|
|
3160
|
+
{
|
|
3161
|
+
"task_id": task_id,
|
|
3162
|
+
"error": error,
|
|
3163
|
+
"endpoint_health": health,
|
|
3164
|
+
"policy": "model_backed_tasks_require_live_endpoint_and_exact_model_identity",
|
|
3165
|
+
},
|
|
3166
|
+
)
|
|
3167
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3168
|
+
state = replay(events)
|
|
3169
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3170
|
+
write_json(job_dir / "plan.json", plan)
|
|
3171
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3172
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3173
|
+
print(f"Blocked task: {task_id}")
|
|
3174
|
+
print(f"Reason: {error}")
|
|
3175
|
+
print(f"Endpoint: {health.get('url')}")
|
|
3176
|
+
print(f"Error: {health.get('error')}")
|
|
3177
|
+
print(f"Expected model: {health.get('expected_served_model_id')}")
|
|
3178
|
+
print(f"Served models: {', '.join(health.get('served_model_ids') or []) or 'none'}")
|
|
3179
|
+
return 2
|
|
3180
|
+
if task_id != "task-001":
|
|
3181
|
+
declared_paths = [str(path) for path in task.get("declared_files", [])]
|
|
3182
|
+
if len(declared_paths) > MAX_JSON_CONTRACT_DECLARED_FILES:
|
|
3183
|
+
log.append(
|
|
3184
|
+
"task_blocked",
|
|
3185
|
+
{
|
|
3186
|
+
"task_id": task_id,
|
|
3187
|
+
"error": "task_shape_requires_replan",
|
|
3188
|
+
"declared_file_count": len(declared_paths),
|
|
3189
|
+
"max_json_contract_declared_files": MAX_JSON_CONTRACT_DECLARED_FILES,
|
|
3190
|
+
"declared_files": declared_paths,
|
|
3191
|
+
"policy": "mumpix_admin_stress_v1_observed_three_file_json_contract_unreliable; split into <=2 file module tasks or single-file raw tasks",
|
|
3192
|
+
},
|
|
3193
|
+
)
|
|
3194
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3195
|
+
state = replay(events)
|
|
3196
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3197
|
+
write_json(job_dir / "plan.json", plan)
|
|
3198
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3199
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3200
|
+
print(f"Blocked task: {task_id}")
|
|
3201
|
+
print("Reason: task_shape_requires_replan")
|
|
3202
|
+
print(f"Declared files: {len(declared_paths)} > {MAX_JSON_CONTRACT_DECLARED_FILES}")
|
|
3203
|
+
return 2
|
|
3204
|
+
task_wall_start = time.monotonic()
|
|
3205
|
+
attempt = int(task.get("attempt_count") or 0) + 1
|
|
3206
|
+
log.append(
|
|
3207
|
+
"task_started",
|
|
3208
|
+
{
|
|
3209
|
+
"task_id": task_id,
|
|
3210
|
+
"attempt": attempt,
|
|
3211
|
+
"creator_config": creator,
|
|
3212
|
+
"execution_mode": "model_creator_json_contract_sandbox_only",
|
|
3213
|
+
"declared_files": task.get("declared_files", []),
|
|
3214
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3215
|
+
},
|
|
3216
|
+
)
|
|
3217
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / task_id
|
|
3218
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
3219
|
+
effective_sources = latest_passed_artifacts_by_path(job_dir, plan)
|
|
3220
|
+
file_context = read_declared_file_context(project, task, effective_sources)
|
|
3221
|
+
retry_feedback = previous_attempt_feedback(job_dir, events, task_id)
|
|
3222
|
+
context_sources = [
|
|
3223
|
+
{
|
|
3224
|
+
"path": item.get("path"),
|
|
3225
|
+
"source": item.get("source"),
|
|
3226
|
+
"source_task_id": item.get("source_task_id"),
|
|
3227
|
+
"sandbox_path": item.get("sandbox_path"),
|
|
3228
|
+
"sha256": item.get("sha256"),
|
|
3229
|
+
"exists": item.get("exists"),
|
|
3230
|
+
}
|
|
3231
|
+
for item in file_context
|
|
3232
|
+
]
|
|
3233
|
+
log.append(
|
|
3234
|
+
"task_effective_context_resolved",
|
|
3235
|
+
{
|
|
3236
|
+
"task_id": task_id,
|
|
3237
|
+
"declared_files": task.get("declared_files", []),
|
|
3238
|
+
"context_sources": context_sources,
|
|
3239
|
+
"policy": "latest_passed_sandbox_artifact_wins",
|
|
3240
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3241
|
+
},
|
|
3242
|
+
)
|
|
3243
|
+
retrieval_mode = str(task.get("retrieval_mode") or "exact")
|
|
3244
|
+
if retrieval_mode == "disabled":
|
|
3245
|
+
retrieval = {"status": "retrieval_disabled_control", "fired": False}
|
|
3246
|
+
else:
|
|
3247
|
+
retrieval = retrieve_validated_behavior(project, task)
|
|
3248
|
+
recurrence = task.get("recurrence") if isinstance(task.get("recurrence"), dict) else {}
|
|
3249
|
+
validated_behavior = None
|
|
3250
|
+
retrieval_selection_sha256 = None
|
|
3251
|
+
retrieval_fired_payload = None
|
|
3252
|
+
context_budget_tokens = int(creator.get("context_budget_tokens") or 0)
|
|
3253
|
+
reserved_output_tokens = int(creator.get("max_output_tokens") or 0)
|
|
3254
|
+
output_contract = select_creator_output_contract(task, file_context, reserved_output_tokens)
|
|
3255
|
+
log.append("creator_output_contract_selected", {"task_id": task_id, **output_contract})
|
|
3256
|
+
base_messages = build_creator_messages(
|
|
3257
|
+
job_dir, project, task, file_context, retry_feedback, None, output_contract
|
|
3258
|
+
)
|
|
3259
|
+
base_input_tokens_estimate = (len(json.dumps(base_messages, ensure_ascii=True)) + 3) // 4
|
|
3260
|
+
injection_tokens_estimate = 0
|
|
3261
|
+
if retrieval.get("fired"):
|
|
3262
|
+
gene = retrieval.get("gene") if isinstance(retrieval.get("gene"), dict) else {}
|
|
3263
|
+
validated_behavior = gene.get("preference") if isinstance(gene.get("preference"), dict) else None
|
|
3264
|
+
if validated_behavior is None:
|
|
3265
|
+
raise RuntimeError("retrieval_fired_without_behavior_preference")
|
|
3266
|
+
injection_tokens_estimate = (len(recurrence_canonical_json(validated_behavior)) + 3) // 4
|
|
3267
|
+
budget_decision = injection_budget_decision(
|
|
3268
|
+
context_budget_tokens,
|
|
3269
|
+
reserved_output_tokens,
|
|
3270
|
+
base_input_tokens_estimate,
|
|
3271
|
+
injection_tokens_estimate,
|
|
3272
|
+
)
|
|
3273
|
+
if not budget_decision["fits"]:
|
|
3274
|
+
retrieval = {
|
|
3275
|
+
**retrieval,
|
|
3276
|
+
"status": "injection_budget_unavailable",
|
|
3277
|
+
"fired": False,
|
|
3278
|
+
"available_injection_tokens": budget_decision["available_injection_tokens"],
|
|
3279
|
+
}
|
|
3280
|
+
validated_behavior = None
|
|
3281
|
+
else:
|
|
3282
|
+
selection = {
|
|
3283
|
+
"schema_version": 1,
|
|
3284
|
+
"task_id": task_id,
|
|
3285
|
+
"mode": retrieval_mode,
|
|
3286
|
+
"gene_id": gene.get("gene_id"),
|
|
3287
|
+
"source_central_event_sha256": retrieval.get("source_central_event_sha256"),
|
|
3288
|
+
"recurrence_key_sha256": gene.get("recurrence_key_sha256"),
|
|
3289
|
+
"preference": validated_behavior,
|
|
3290
|
+
"preference_sha256": gene.get("preference_sha256"),
|
|
3291
|
+
"injection_policy": "prefer_behavior_only_no_patch_or_source",
|
|
3292
|
+
"paired_control": "rerun the same frozen task with --retrieval disabled",
|
|
3293
|
+
}
|
|
3294
|
+
selection_path = artifact_dir / "retrieval-selection.json"
|
|
3295
|
+
write_json(selection_path, selection)
|
|
3296
|
+
retrieval_selection_sha256 = sha256_file(selection_path)
|
|
3297
|
+
retrieval_fired_payload = {
|
|
3298
|
+
"task_id": task_id,
|
|
3299
|
+
"mode": retrieval_mode,
|
|
3300
|
+
"gene_id": gene.get("gene_id"),
|
|
3301
|
+
"source_central_event_sha256": retrieval.get("source_central_event_sha256"),
|
|
3302
|
+
"recurrence_key_sha256": gene.get("recurrence_key_sha256"),
|
|
3303
|
+
"preference_sha256": gene.get("preference_sha256"),
|
|
3304
|
+
"selection": str(selection_path.relative_to(job_dir)),
|
|
3305
|
+
"selection_sha256": retrieval_selection_sha256,
|
|
3306
|
+
"injected_chars": len(recurrence_canonical_json(validated_behavior)),
|
|
3307
|
+
"injected_tokens_estimate": injection_tokens_estimate,
|
|
3308
|
+
"base_input_tokens_estimate": base_input_tokens_estimate,
|
|
3309
|
+
"context_budget_tokens": context_budget_tokens,
|
|
3310
|
+
"reserved_output_tokens": reserved_output_tokens,
|
|
3311
|
+
"input_budget_only": True,
|
|
3312
|
+
"output_cap_unchanged": True,
|
|
3313
|
+
"paired_control_supported": True,
|
|
3314
|
+
}
|
|
3315
|
+
log.append(
|
|
3316
|
+
"retrieval_considered",
|
|
3317
|
+
{
|
|
3318
|
+
"task_id": task_id,
|
|
3319
|
+
"mode": retrieval_mode,
|
|
3320
|
+
"status": retrieval.get("status"),
|
|
3321
|
+
"fired": bool(retrieval.get("fired")),
|
|
3322
|
+
"match_count": retrieval.get("match_count"),
|
|
3323
|
+
"project_scope_id": retrieval.get("project_scope_id"),
|
|
3324
|
+
"recurrence_key_sha256": recurrence.get("recurrence_key_sha256"),
|
|
3325
|
+
"base_input_tokens_estimate": base_input_tokens_estimate,
|
|
3326
|
+
"injection_tokens_estimate": injection_tokens_estimate,
|
|
3327
|
+
"context_budget_tokens": context_budget_tokens,
|
|
3328
|
+
"reserved_output_tokens": reserved_output_tokens,
|
|
3329
|
+
"policy": "exact_key_equality_or_abstain_no_approximate_fallback",
|
|
3330
|
+
},
|
|
3331
|
+
)
|
|
3332
|
+
if retrieval_fired_payload:
|
|
3333
|
+
log.append("retrieval_fired", retrieval_fired_payload)
|
|
3334
|
+
messages = (
|
|
3335
|
+
build_creator_messages(
|
|
3336
|
+
job_dir, project, task, file_context, retry_feedback, validated_behavior, output_contract
|
|
3337
|
+
)
|
|
3338
|
+
if validated_behavior
|
|
3339
|
+
else base_messages
|
|
3340
|
+
)
|
|
3341
|
+
allowed = declared_file_set(task)
|
|
3342
|
+
response_schema = (
|
|
3343
|
+
anchored_edit_response_schema(allowed, int(output_contract.get("max_edits") or MAX_ANCHORED_EDITS))
|
|
3344
|
+
if output_contract.get("mode") == "anchored_edits"
|
|
3345
|
+
else None
|
|
3346
|
+
)
|
|
3347
|
+
response_schema_sha256 = recurrence_sha256_json(response_schema) if response_schema is not None else None
|
|
3348
|
+
request_path = artifact_dir / "creator-request.json"
|
|
3349
|
+
request_path.write_text(
|
|
3350
|
+
json.dumps(
|
|
3351
|
+
{
|
|
3352
|
+
"messages": messages,
|
|
3353
|
+
"structured_output": (
|
|
3354
|
+
{
|
|
3355
|
+
"mode": "json_schema",
|
|
3356
|
+
"schema": response_schema,
|
|
3357
|
+
"schema_sha256": response_schema_sha256,
|
|
3358
|
+
"transport": "llama_cpp_response_format_json_object_schema",
|
|
3359
|
+
"thinking_disabled": True,
|
|
3360
|
+
}
|
|
3361
|
+
if response_schema is not None
|
|
3362
|
+
else None
|
|
3363
|
+
),
|
|
3364
|
+
},
|
|
3365
|
+
indent=2,
|
|
3366
|
+
)
|
|
3367
|
+
+ "\n",
|
|
3368
|
+
encoding="utf-8",
|
|
3369
|
+
)
|
|
3370
|
+
log.append(
|
|
3371
|
+
"creator_request_written",
|
|
3372
|
+
{
|
|
3373
|
+
"task_id": task_id,
|
|
3374
|
+
"path": str(request_path.relative_to(job_dir)),
|
|
3375
|
+
"sha256": sha256_file(request_path),
|
|
3376
|
+
"declared_files": task.get("declared_files", []),
|
|
3377
|
+
"context_sources": context_sources,
|
|
3378
|
+
"retry_feedback_count": len(retry_feedback),
|
|
3379
|
+
"retrieval_status": retrieval.get("status"),
|
|
3380
|
+
"retrieval_fired": bool(retrieval.get("fired")),
|
|
3381
|
+
"retrieval_selection_sha256": retrieval_selection_sha256,
|
|
3382
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3383
|
+
"structured_output_mode": "json_schema" if response_schema is not None else None,
|
|
3384
|
+
"structured_output_schema_sha256": response_schema_sha256,
|
|
3385
|
+
"structured_output_transport": (
|
|
3386
|
+
"llama_cpp_response_format_json_object_schema" if response_schema is not None else None
|
|
3387
|
+
),
|
|
3388
|
+
"thinking_disabled_for_schema": response_schema is not None,
|
|
3389
|
+
},
|
|
3390
|
+
)
|
|
3391
|
+
|
|
3392
|
+
creator_call_ms = 0
|
|
3393
|
+
creator_call_start = None
|
|
3394
|
+
try:
|
|
3395
|
+
creator_call_start = time.monotonic()
|
|
3396
|
+
response = call_creator_model(
|
|
3397
|
+
creator,
|
|
3398
|
+
messages,
|
|
3399
|
+
json_mode=output_contract.get("mode") == "anchored_edits" or len(allowed) != 1,
|
|
3400
|
+
response_schema=response_schema,
|
|
3401
|
+
)
|
|
3402
|
+
creator_call_ms = elapsed_ms(creator_call_start)
|
|
3403
|
+
except Exception as exc: # noqa: BLE001 - local model failures are durable task failures.
|
|
3404
|
+
if creator_call_start is not None:
|
|
3405
|
+
creator_call_ms = elapsed_ms(creator_call_start)
|
|
3406
|
+
log.append(
|
|
3407
|
+
"task_failed",
|
|
3408
|
+
attempt_failure_payload(AttemptFailure.ENDPOINT_ERROR, **{
|
|
3409
|
+
"task_id": task_id,
|
|
3410
|
+
"error": "creator_call_failed",
|
|
3411
|
+
"message": str(exc),
|
|
3412
|
+
"attempt": attempt,
|
|
3413
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3414
|
+
"creator_call_ms": creator_call_ms,
|
|
3415
|
+
}),
|
|
3416
|
+
)
|
|
3417
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3418
|
+
state = replay(events)
|
|
3419
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3420
|
+
write_json(job_dir / "plan.json", plan)
|
|
3421
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3422
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3423
|
+
print(f"Failed task: {task_id}")
|
|
3424
|
+
print(f"Reason: creator_call_failed")
|
|
3425
|
+
print(str(exc))
|
|
3426
|
+
return 2
|
|
3427
|
+
|
|
3428
|
+
response_path = artifact_dir / "creator-response.json"
|
|
3429
|
+
response_path.write_text(json.dumps(response, indent=2) + "\n", encoding="utf-8")
|
|
3430
|
+
allowed = declared_file_set(task)
|
|
3431
|
+
parser_repair: dict[str, Any] | None = None
|
|
3432
|
+
try:
|
|
3433
|
+
creator_payload, parser_repair = parse_creator_json_with_repair(str(response.get("content") or ""))
|
|
3434
|
+
if output_contract.get("mode") == "anchored_edits":
|
|
3435
|
+
anchored_payload = artifact_dir / "creator-anchored-edits.json"
|
|
3436
|
+
anchored_payload.write_text(json.dumps(creator_payload, indent=2) + "\n", encoding="utf-8")
|
|
3437
|
+
creator_payload, anchored_metadata = materialize_anchored_edits(creator_payload, file_context, allowed)
|
|
3438
|
+
output_mode = "anchored_edit_json"
|
|
3439
|
+
log.append(
|
|
3440
|
+
"creator_anchored_edits_materialized",
|
|
3441
|
+
{
|
|
3442
|
+
"task_id": task_id,
|
|
3443
|
+
"path": str(anchored_payload.relative_to(job_dir)),
|
|
3444
|
+
"sha256": sha256_file(anchored_payload),
|
|
3445
|
+
"edit_count": len(anchored_metadata),
|
|
3446
|
+
"edits": anchored_metadata,
|
|
3447
|
+
"policy": "exact_unique_nonoverlapping_anchors_fail_closed",
|
|
3448
|
+
},
|
|
3449
|
+
)
|
|
3450
|
+
else:
|
|
3451
|
+
if "edits" in creator_payload and "files" not in creator_payload:
|
|
3452
|
+
raise ValueError("creator_output_contract_violation: full replacement mode requires files")
|
|
3453
|
+
output_mode = "json_contract"
|
|
3454
|
+
except ValueError as exc:
|
|
3455
|
+
repaired_payload = (
|
|
3456
|
+
coerce_single_file_creator_output(str(response.get("content") or ""), allowed)
|
|
3457
|
+
if output_contract.get("mode") != "anchored_edits"
|
|
3458
|
+
else None
|
|
3459
|
+
)
|
|
3460
|
+
if repaired_payload is not None:
|
|
3461
|
+
creator_payload, output_mode = repaired_payload
|
|
3462
|
+
parser_repair = {"applied": True, "types": ["single_declared_file_raw_code_output_wrapped_as_json"]}
|
|
3463
|
+
log.append(
|
|
3464
|
+
"creator_output_repaired",
|
|
3465
|
+
{
|
|
3466
|
+
"task_id": task_id,
|
|
3467
|
+
"attempt": attempt,
|
|
3468
|
+
"reason": "single_declared_file_raw_code_output_wrapped_as_json",
|
|
3469
|
+
"original_error": str(exc),
|
|
3470
|
+
"path": next(iter(allowed)),
|
|
3471
|
+
"output_mode": output_mode,
|
|
3472
|
+
"response_path": str(response_path.relative_to(job_dir)),
|
|
3473
|
+
},
|
|
3474
|
+
)
|
|
3475
|
+
else:
|
|
3476
|
+
log.append(
|
|
3477
|
+
"creator_output_received",
|
|
3478
|
+
{
|
|
3479
|
+
"task_id": task_id,
|
|
3480
|
+
"path": str(response_path.relative_to(job_dir)),
|
|
3481
|
+
"sha256": sha256_file(response_path),
|
|
3482
|
+
"request_model": response.get("request_model"),
|
|
3483
|
+
"response_model": response.get("response_model"),
|
|
3484
|
+
"content_sha256": sha256_text(str(response.get("content") or "")),
|
|
3485
|
+
"content_chars": len(str(response.get("content") or "")),
|
|
3486
|
+
"reasoning_chars": len(str(response.get("reasoning_content") or "")),
|
|
3487
|
+
"finish_reason": response.get("finish_reason"),
|
|
3488
|
+
"usage": response.get("usage") if isinstance(response.get("usage"), dict) else {},
|
|
3489
|
+
"request_max_tokens": response.get("request_max_tokens"),
|
|
3490
|
+
"request_temperature": response.get("request_temperature"),
|
|
3491
|
+
"json_mode": response.get("json_mode"),
|
|
3492
|
+
"structured_output_mode": response.get("structured_output_mode"),
|
|
3493
|
+
"structured_output_schema_sha256": response.get("structured_output_schema_sha256"),
|
|
3494
|
+
"structured_output_transport": response.get("structured_output_transport"),
|
|
3495
|
+
"thinking_disabled_for_schema": response.get("thinking_disabled_for_schema"),
|
|
3496
|
+
"creator_call_ms": creator_call_ms,
|
|
3497
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3498
|
+
"parser_repair_applied": False,
|
|
3499
|
+
"parser_repair": None,
|
|
3500
|
+
},
|
|
3501
|
+
)
|
|
3502
|
+
log.append(
|
|
3503
|
+
"task_failed",
|
|
3504
|
+
attempt_failure_payload(
|
|
3505
|
+
classify_creator_finish(str(response.get("finish_reason") or "")) or AttemptFailure.MALFORMED_OUTPUT,
|
|
3506
|
+
**{
|
|
3507
|
+
"task_id": task_id,
|
|
3508
|
+
"error": "creator_output_malformed",
|
|
3509
|
+
"message": str(exc),
|
|
3510
|
+
"attempt": attempt,
|
|
3511
|
+
"response_path": str(response_path.relative_to(job_dir)),
|
|
3512
|
+
"content_chars": len(str(response.get("content") or "")),
|
|
3513
|
+
"reasoning_chars": len(str(response.get("reasoning_content") or "")),
|
|
3514
|
+
"finish_reason": response.get("finish_reason"),
|
|
3515
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3516
|
+
"creator_call_ms": creator_call_ms,
|
|
3517
|
+
},
|
|
3518
|
+
),
|
|
3519
|
+
)
|
|
3520
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3521
|
+
state = replay(events)
|
|
3522
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3523
|
+
write_json(job_dir / "plan.json", plan)
|
|
3524
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3525
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3526
|
+
print(f"Failed task: {task_id}")
|
|
3527
|
+
print("Reason: creator_output_malformed")
|
|
3528
|
+
print(str(exc))
|
|
3529
|
+
return 2
|
|
3530
|
+
|
|
3531
|
+
log.append(
|
|
3532
|
+
"creator_output_received",
|
|
3533
|
+
{
|
|
3534
|
+
"task_id": task_id,
|
|
3535
|
+
"path": str(response_path.relative_to(job_dir)),
|
|
3536
|
+
"sha256": sha256_file(response_path),
|
|
3537
|
+
"request_model": response.get("request_model"),
|
|
3538
|
+
"response_model": response.get("response_model"),
|
|
3539
|
+
"content_sha256": sha256_text(str(response.get("content") or "")),
|
|
3540
|
+
"content_chars": len(str(response.get("content") or "")),
|
|
3541
|
+
"reasoning_chars": len(str(response.get("reasoning_content") or "")),
|
|
3542
|
+
"finish_reason": response.get("finish_reason"),
|
|
3543
|
+
"usage": response.get("usage") if isinstance(response.get("usage"), dict) else {},
|
|
3544
|
+
"request_max_tokens": response.get("request_max_tokens"),
|
|
3545
|
+
"request_temperature": response.get("request_temperature"),
|
|
3546
|
+
"json_mode": response.get("json_mode"),
|
|
3547
|
+
"structured_output_mode": response.get("structured_output_mode"),
|
|
3548
|
+
"structured_output_schema_sha256": response.get("structured_output_schema_sha256"),
|
|
3549
|
+
"structured_output_transport": response.get("structured_output_transport"),
|
|
3550
|
+
"thinking_disabled_for_schema": response.get("thinking_disabled_for_schema"),
|
|
3551
|
+
"creator_call_ms": creator_call_ms,
|
|
3552
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3553
|
+
"parser_repair_applied": bool(parser_repair),
|
|
3554
|
+
"parser_repair": parser_repair,
|
|
3555
|
+
},
|
|
3556
|
+
)
|
|
3557
|
+
|
|
3558
|
+
creator_payload, hygiene_changes = preserve_existing_final_newlines(creator_payload, file_context)
|
|
3559
|
+
if hygiene_changes:
|
|
3560
|
+
log.append(
|
|
3561
|
+
"creator_output_hygiene_normalized",
|
|
3562
|
+
{
|
|
3563
|
+
"task_id": task_id,
|
|
3564
|
+
"changes": hygiene_changes,
|
|
3565
|
+
"semantic_repair": False,
|
|
3566
|
+
"policy": "deterministic_existing_file_byte_hygiene_only",
|
|
3567
|
+
},
|
|
3568
|
+
)
|
|
3569
|
+
|
|
3570
|
+
parsed_path = artifact_dir / "creator-output-parsed.json"
|
|
3571
|
+
parsed_path.write_text(json.dumps(creator_payload, indent=2) + "\n", encoding="utf-8")
|
|
3572
|
+
violations = validate_creator_declared_files(creator_payload, allowed)
|
|
3573
|
+
if violations:
|
|
3574
|
+
log.append(
|
|
3575
|
+
"declared_file_violation",
|
|
3576
|
+
{
|
|
3577
|
+
"task_id": task_id,
|
|
3578
|
+
"attempt": attempt,
|
|
3579
|
+
"violations": violations,
|
|
3580
|
+
"allowed_declared_files": sorted(allowed),
|
|
3581
|
+
"parsed_output": str(parsed_path.relative_to(job_dir)),
|
|
3582
|
+
},
|
|
3583
|
+
)
|
|
3584
|
+
log.append(
|
|
3585
|
+
"task_failed",
|
|
3586
|
+
attempt_failure_payload(AttemptFailure.MALFORMED_OUTPUT, **{
|
|
3587
|
+
"task_id": task_id,
|
|
3588
|
+
"error": "declared_file_violation",
|
|
3589
|
+
"violations": violations,
|
|
3590
|
+
"attempt": attempt,
|
|
3591
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3592
|
+
"creator_call_ms": creator_call_ms,
|
|
3593
|
+
}),
|
|
3594
|
+
)
|
|
3595
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3596
|
+
state = replay(events)
|
|
3597
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3598
|
+
write_json(job_dir / "plan.json", plan)
|
|
3599
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3600
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3601
|
+
print(f"Failed task: {task_id}")
|
|
3602
|
+
print("Reason: declared_file_violation")
|
|
3603
|
+
print(f"Violations: {', '.join(violations)}")
|
|
3604
|
+
return 2
|
|
3605
|
+
|
|
3606
|
+
sandbox_dir, written = atomic_write_creator_sandbox(job_dir, task_id, creator_payload)
|
|
3607
|
+
log.append(
|
|
3608
|
+
"task_artifacts",
|
|
3609
|
+
{
|
|
3610
|
+
"task_id": task_id,
|
|
3611
|
+
"artifacts": [
|
|
3612
|
+
{
|
|
3613
|
+
"sandbox_path": str((sandbox_dir / row["path"]).relative_to(job_dir)),
|
|
3614
|
+
**{key: value for key, value in row.items() if key != "absolute_path"},
|
|
3615
|
+
}
|
|
3616
|
+
for row in written
|
|
3617
|
+
],
|
|
3618
|
+
"sandbox_dir": str(sandbox_dir.relative_to(job_dir)),
|
|
3619
|
+
"parsed_output": str(parsed_path.relative_to(job_dir)),
|
|
3620
|
+
"output_mode": output_mode,
|
|
3621
|
+
"parser_repair_applied": bool(parser_repair),
|
|
3622
|
+
"parser_repair": parser_repair,
|
|
3623
|
+
},
|
|
3624
|
+
)
|
|
3625
|
+
review_ok, review_errors, review_level, regression_checks, infrastructure_errors = review_creator_sandbox(
|
|
3626
|
+
project, task, written, output_mode, file_context, job_dir
|
|
3627
|
+
)
|
|
3628
|
+
log.append(
|
|
3629
|
+
"review_completed",
|
|
3630
|
+
{
|
|
3631
|
+
"task_id": task_id,
|
|
3632
|
+
"ok": review_ok,
|
|
3633
|
+
"errors": review_errors,
|
|
3634
|
+
"reviewer": "static_sandbox_artifact_contract_v1",
|
|
3635
|
+
"review_level": review_level,
|
|
3636
|
+
"output_mode": output_mode,
|
|
3637
|
+
"regression_checks": regression_checks,
|
|
3638
|
+
"infrastructure_errors": infrastructure_errors,
|
|
3639
|
+
"outcome": "infrastructure_unavailable" if infrastructure_errors else ("passed" if review_ok else "failed"),
|
|
3640
|
+
},
|
|
3641
|
+
)
|
|
3642
|
+
if infrastructure_errors:
|
|
3643
|
+
log.append(
|
|
3644
|
+
"task_blocked",
|
|
3645
|
+
{
|
|
3646
|
+
"task_id": task_id,
|
|
3647
|
+
"error": "static_checker_unavailable",
|
|
3648
|
+
"infrastructure_errors": infrastructure_errors,
|
|
3649
|
+
"attempt": attempt,
|
|
3650
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3651
|
+
"creator_call_ms": creator_call_ms,
|
|
3652
|
+
"policy": "a checker that cannot run is infrastructure-unavailable, not evidence that the patch failed",
|
|
3653
|
+
},
|
|
3654
|
+
)
|
|
3655
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3656
|
+
state = replay(events)
|
|
3657
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3658
|
+
write_json(job_dir / "plan.json", plan)
|
|
3659
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3660
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3661
|
+
print(f"Blocked task: {task_id}")
|
|
3662
|
+
print("Reason: static_checker_unavailable")
|
|
3663
|
+
print("; ".join(infrastructure_errors))
|
|
3664
|
+
return 2
|
|
3665
|
+
if not review_ok:
|
|
3666
|
+
log.append(
|
|
3667
|
+
"task_failed",
|
|
3668
|
+
attempt_failure_payload(AttemptFailure.GATE_FAILED, **{
|
|
3669
|
+
"task_id": task_id,
|
|
3670
|
+
"error": "review_failed",
|
|
3671
|
+
"review_errors": review_errors,
|
|
3672
|
+
"attempt": attempt,
|
|
3673
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3674
|
+
"creator_call_ms": creator_call_ms,
|
|
3675
|
+
}),
|
|
3676
|
+
)
|
|
3677
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3678
|
+
state = replay(events)
|
|
3679
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3680
|
+
write_json(job_dir / "plan.json", plan)
|
|
3681
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3682
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3683
|
+
print(f"Failed task: {task_id}")
|
|
3684
|
+
print("Reason: review_failed")
|
|
3685
|
+
print("; ".join(review_errors))
|
|
3686
|
+
return 2
|
|
3687
|
+
|
|
3688
|
+
checkpoint_dir = job_dir / "checkpoints" / task_id
|
|
3689
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
3690
|
+
checkpoint_manifest = {
|
|
3691
|
+
"task_id": task_id,
|
|
3692
|
+
"mode": "model_creator_json_contract_sandbox_only",
|
|
3693
|
+
"output_mode": output_mode,
|
|
3694
|
+
"review_level": review_level,
|
|
3695
|
+
"sandbox_dir": str(sandbox_dir.relative_to(job_dir)),
|
|
3696
|
+
"declared_files": sorted(allowed),
|
|
3697
|
+
"artifacts": [{key: value for key, value in row.items() if key != "absolute_path"} for row in written],
|
|
3698
|
+
"creator_response": str(response_path.relative_to(job_dir)),
|
|
3699
|
+
"creator_output": str(parsed_path.relative_to(job_dir)),
|
|
3700
|
+
"parser_repair_applied": bool(parser_repair),
|
|
3701
|
+
"parser_repair": parser_repair,
|
|
3702
|
+
}
|
|
3703
|
+
write_json(checkpoint_dir / "manifest.json", checkpoint_manifest)
|
|
3704
|
+
log.append("checkpoint_written", {"task_id": task_id, "path": str((checkpoint_dir / "manifest.json").relative_to(job_dir)), "sha256": sha256_file(checkpoint_dir / "manifest.json")})
|
|
3705
|
+
log.append(
|
|
3706
|
+
"task_passed",
|
|
3707
|
+
{
|
|
3708
|
+
"task_id": task_id,
|
|
3709
|
+
"acceptance": task.get("acceptance", []),
|
|
3710
|
+
"notes": "Creator output parsed or safely repaired, stayed within declared files, passed reviewer checks, and was written atomically to the job sandbox. No project source promotion was performed.",
|
|
3711
|
+
"output_mode": output_mode,
|
|
3712
|
+
"review_level": review_level,
|
|
3713
|
+
"sandbox_dir": str(sandbox_dir.relative_to(job_dir)),
|
|
3714
|
+
"task_wall_ms": elapsed_ms(task_wall_start),
|
|
3715
|
+
"creator_call_ms": creator_call_ms,
|
|
3716
|
+
"prompt_layout": CREATOR_PROMPT_LAYOUT,
|
|
3717
|
+
"parser_repair_applied": bool(parser_repair),
|
|
3718
|
+
"parser_repair": parser_repair,
|
|
3719
|
+
},
|
|
3720
|
+
)
|
|
3721
|
+
|
|
3722
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3723
|
+
state = replay(events)
|
|
3724
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3725
|
+
write_json(job_dir / "plan.json", plan)
|
|
3726
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3727
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3728
|
+
|
|
3729
|
+
print(f"Ran task: {task_id}")
|
|
3730
|
+
print("Execution mode: model_creator_json_contract_sandbox_only")
|
|
3731
|
+
print(f"Endpoint health: {'ok' if health.get('ok') else 'unavailable'}")
|
|
3732
|
+
print(f"Sandbox artifacts: {sandbox_dir}")
|
|
3733
|
+
print(f"Files written: {len(written)}")
|
|
3734
|
+
print(f"Ready tasks now: {', '.join(ready_task_ids(plan)[:8]) or 'none'}")
|
|
3735
|
+
return 0
|
|
3736
|
+
|
|
3737
|
+
log.append(
|
|
3738
|
+
"task_started",
|
|
3739
|
+
{
|
|
3740
|
+
"task_id": task_id,
|
|
3741
|
+
"attempt": int(task.get("attempt_count") or 0) + 1,
|
|
3742
|
+
"creator_config": creator,
|
|
3743
|
+
"execution_mode": "deterministic_terminal_inventory_no_model_call",
|
|
3744
|
+
},
|
|
3745
|
+
)
|
|
3746
|
+
artifact_dir = job_dir / "artifacts" / "tasks" / task_id
|
|
3747
|
+
artifacts = inventory_declared_files(project, task, artifact_dir)
|
|
3748
|
+
log.append("task_artifacts", {"task_id": task_id, "artifacts": artifacts})
|
|
3749
|
+
checkpoint_dir = job_dir / "checkpoints" / task_id
|
|
3750
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
3751
|
+
checkpoint_manifest = {
|
|
3752
|
+
"task_id": task_id,
|
|
3753
|
+
"declared_files": [
|
|
3754
|
+
{
|
|
3755
|
+
"path": rel,
|
|
3756
|
+
"sha256": sha256_file(project / rel) if (project / rel).exists() and (project / rel).is_file() else None,
|
|
3757
|
+
}
|
|
3758
|
+
for rel in task.get("declared_files", [])
|
|
3759
|
+
],
|
|
3760
|
+
"artifacts": artifacts,
|
|
3761
|
+
}
|
|
3762
|
+
write_json(checkpoint_dir / "manifest.json", checkpoint_manifest)
|
|
3763
|
+
log.append("checkpoint_written", {"task_id": task_id, "path": str((checkpoint_dir / "manifest.json").relative_to(job_dir)), "sha256": sha256_file(checkpoint_dir / "manifest.json")})
|
|
3764
|
+
log.append(
|
|
3765
|
+
"task_passed",
|
|
3766
|
+
{
|
|
3767
|
+
"task_id": task_id,
|
|
3768
|
+
"acceptance": task.get("acceptance", []),
|
|
3769
|
+
"notes": "Inventory completed from declared files. No project source edits were made.",
|
|
3770
|
+
},
|
|
3771
|
+
)
|
|
3772
|
+
|
|
3773
|
+
events = read_events(job_dir / "events.jsonl")
|
|
3774
|
+
state = replay(events)
|
|
3775
|
+
plan = materialize_plan_from_events(events, load_plan(job_dir))
|
|
3776
|
+
write_json(job_dir / "plan.json", plan)
|
|
3777
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
3778
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
3779
|
+
|
|
3780
|
+
print(f"Ran task: {task_id}")
|
|
3781
|
+
print(f"Execution mode: deterministic_terminal_inventory_no_model_call")
|
|
3782
|
+
print(f"Endpoint health: {'ok' if health.get('ok') else 'unavailable'}")
|
|
3783
|
+
print(f"Artifacts: {artifact_dir}")
|
|
3784
|
+
print(f"Ready tasks now: {', '.join(ready_task_ids(plan)[:8]) or 'none'}")
|
|
3785
|
+
return 0
|