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,533 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .attempts import detect_interrupted_attempt
|
|
12
|
+
from .central_traces import project_identity, sync_job_traces
|
|
13
|
+
from .init import probe_endpoint, require_git_repo
|
|
14
|
+
from .job.events import JobEventLog, read_events
|
|
15
|
+
from .job.lock import JobLock
|
|
16
|
+
from .job.manager import (
|
|
17
|
+
find_latest_incomplete_job,
|
|
18
|
+
job_promote_main,
|
|
19
|
+
job_reject_promotion_main,
|
|
20
|
+
job_review_promotion_main,
|
|
21
|
+
job_root,
|
|
22
|
+
job_run_milestone_main,
|
|
23
|
+
job_run_next_main,
|
|
24
|
+
new_job_id,
|
|
25
|
+
write_handoff,
|
|
26
|
+
write_json,
|
|
27
|
+
)
|
|
28
|
+
from .job.planner import canonical_task_hash, materialize_plan_from_events, ready_task_ids, validate_task_graph
|
|
29
|
+
from .job.state import replay, state_to_json, write_state_snapshot
|
|
30
|
+
from .recurrence import load_behavior_preference, load_gene_evidence, load_recurrence_card, normalize_repository
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def config_path(repo_root: Path) -> Path:
|
|
34
|
+
return repo_root / ".socra" / "config.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_config(repo_root: Path) -> dict[str, Any]:
|
|
38
|
+
path = config_path(repo_root)
|
|
39
|
+
if not path.exists():
|
|
40
|
+
raise SystemExit(f"Socra config not found: {path}. Run wydcode init first.")
|
|
41
|
+
try:
|
|
42
|
+
config = json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
except json.JSONDecodeError as exc:
|
|
44
|
+
raise SystemExit(f"Socra config is invalid JSON: {exc}") from exc
|
|
45
|
+
if not isinstance(config, dict):
|
|
46
|
+
raise SystemExit("Socra config must contain a JSON object.")
|
|
47
|
+
if config.get("schema_version") != 1:
|
|
48
|
+
raise SystemExit(f"Unsupported Socra config schema_version: {config.get('schema_version')}")
|
|
49
|
+
return config
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def model_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
53
|
+
model = config.get("model") if isinstance(config.get("model"), dict) else {}
|
|
54
|
+
if not model.get("endpoint"):
|
|
55
|
+
raise SystemExit("Socra config has no model.endpoint. Run wydcode init --endpoint ...")
|
|
56
|
+
return model
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def require_live_endpoint(config: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
model = model_config(config)
|
|
61
|
+
probe = probe_endpoint(str(model["endpoint"]), timeout=2.0)
|
|
62
|
+
if not probe.get("ok"):
|
|
63
|
+
errors = probe.get("errors") if isinstance(probe.get("errors"), list) else []
|
|
64
|
+
detail = f" Details: {'; '.join(str(item) for item in errors[:2])}" if errors else ""
|
|
65
|
+
raise SystemExit(f"Configured model endpoint is not reachable: {model['endpoint']}.{detail}")
|
|
66
|
+
return probe
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
SOURCE_SUFFIXES = {".js", ".jsx", ".ts", ".tsx"}
|
|
70
|
+
TEST_NAME_MARKERS = (".test.", ".spec.")
|
|
71
|
+
SOURCE_DIR_PARTS = {"src", "lib", "app", "pages", "components", "packages", "plugins", "plugin"}
|
|
72
|
+
PATH_STOPWORDS = {
|
|
73
|
+
"the",
|
|
74
|
+
"and",
|
|
75
|
+
"for",
|
|
76
|
+
"with",
|
|
77
|
+
"from",
|
|
78
|
+
"into",
|
|
79
|
+
"inside",
|
|
80
|
+
"single",
|
|
81
|
+
"value",
|
|
82
|
+
"nested",
|
|
83
|
+
"partial",
|
|
84
|
+
"updates",
|
|
85
|
+
"update",
|
|
86
|
+
"should",
|
|
87
|
+
"without",
|
|
88
|
+
"instead",
|
|
89
|
+
"removes",
|
|
90
|
+
"other",
|
|
91
|
+
"file",
|
|
92
|
+
"files",
|
|
93
|
+
"bug",
|
|
94
|
+
"fix",
|
|
95
|
+
"test",
|
|
96
|
+
"tests",
|
|
97
|
+
"plugin",
|
|
98
|
+
"src",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def split_identifier(text: str) -> list[str]:
|
|
103
|
+
parts = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", text).lower()
|
|
104
|
+
return [token for token in re.split(r"[^a-z0-9]+", parts) if token]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def goal_tokens(goal: str) -> set[str]:
|
|
108
|
+
return {token for token in split_identifier(goal) if len(token) >= 3 and token not in PATH_STOPWORDS}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def goal_terms(goal: str) -> set[str]:
|
|
112
|
+
terms = goal_tokens(goal)
|
|
113
|
+
for raw in re.findall(r"[A-Za-z][A-Za-z0-9]*", goal):
|
|
114
|
+
text = raw.lower()
|
|
115
|
+
if len(text) >= 3 and text not in PATH_STOPWORDS:
|
|
116
|
+
terms.add(text)
|
|
117
|
+
return terms
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def source_candidates(repo_root: Path) -> list[Path]:
|
|
121
|
+
candidates: list[Path] = []
|
|
122
|
+
for base in [repo_root / part for part in SOURCE_DIR_PARTS if (repo_root / part).is_dir()]:
|
|
123
|
+
for path in base.rglob("*"):
|
|
124
|
+
if not path.is_file() or path.suffix not in SOURCE_SUFFIXES:
|
|
125
|
+
continue
|
|
126
|
+
if any(marker in path.name for marker in TEST_NAME_MARKERS):
|
|
127
|
+
continue
|
|
128
|
+
rel_parts = set(path.relative_to(repo_root).parts)
|
|
129
|
+
if "node_modules" in rel_parts or ".socra" in rel_parts:
|
|
130
|
+
continue
|
|
131
|
+
candidates.append(path.relative_to(repo_root))
|
|
132
|
+
for name in ["index.js", "main.js", "index.ts", "main.ts"]:
|
|
133
|
+
path = repo_root / name
|
|
134
|
+
if path.is_file():
|
|
135
|
+
candidates.append(path.relative_to(repo_root))
|
|
136
|
+
return sorted(set(candidates), key=lambda item: item.as_posix())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def score_path_against_goal(path: Path, tokens: set[str]) -> tuple[int, list[str]]:
|
|
140
|
+
if not tokens:
|
|
141
|
+
return (0, [])
|
|
142
|
+
parts = path.parts
|
|
143
|
+
normalized_parts = {part.lower() for part in parts}
|
|
144
|
+
stem_tokens = split_identifier(path.stem)
|
|
145
|
+
path_tokens = split_identifier(path.as_posix())
|
|
146
|
+
matches = sorted(tokens.intersection(path_tokens).union(tokens.intersection(normalized_parts)))
|
|
147
|
+
score = 0
|
|
148
|
+
for token in matches:
|
|
149
|
+
score += 8
|
|
150
|
+
if token in stem_tokens:
|
|
151
|
+
score += 14
|
|
152
|
+
if token in normalized_parts:
|
|
153
|
+
score += 4
|
|
154
|
+
if "index" in stem_tokens and len(parts) > 2:
|
|
155
|
+
score += 2
|
|
156
|
+
if path.name in {"index.js", "index.ts"} and len(parts) <= 2:
|
|
157
|
+
score -= 6
|
|
158
|
+
return score, matches
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def infer_files_from_goal(repo_root: Path, goal: str) -> list[str]:
|
|
162
|
+
tokens = goal_terms(goal)
|
|
163
|
+
scored = []
|
|
164
|
+
for candidate in source_candidates(repo_root):
|
|
165
|
+
score, matches = score_path_against_goal(candidate, tokens)
|
|
166
|
+
if score > 0:
|
|
167
|
+
scored.append((score, len(matches), -len(candidate.parts), candidate.as_posix(), matches))
|
|
168
|
+
if not scored:
|
|
169
|
+
return []
|
|
170
|
+
scored.sort(reverse=True)
|
|
171
|
+
best = scored[0]
|
|
172
|
+
# Refuse ambiguous guesses instead of spending a model call on the wrong file.
|
|
173
|
+
if len(scored) > 1 and scored[1][0] >= best[0] - 3:
|
|
174
|
+
return []
|
|
175
|
+
if best[0] < 10:
|
|
176
|
+
return []
|
|
177
|
+
return [best[3]]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def infer_toy_source_file(repo_root: Path) -> list[str]:
|
|
181
|
+
preferred = [
|
|
182
|
+
"src/math.js",
|
|
183
|
+
"src/index.js",
|
|
184
|
+
"src/main.js",
|
|
185
|
+
"src/app.js",
|
|
186
|
+
"index.js",
|
|
187
|
+
"main.js",
|
|
188
|
+
]
|
|
189
|
+
for item in preferred:
|
|
190
|
+
if (repo_root / item).is_file():
|
|
191
|
+
# Keep the original toy fixture ergonomic, but avoid broad-repo first-file guesses.
|
|
192
|
+
candidates = source_candidates(repo_root)
|
|
193
|
+
if len(candidates) <= 3:
|
|
194
|
+
return [item]
|
|
195
|
+
return []
|
|
196
|
+
candidates = source_candidates(repo_root)
|
|
197
|
+
if len(candidates) == 1:
|
|
198
|
+
return [candidates[0].as_posix()]
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def infer_declared_files(repo_root: Path, requested: list[str], goal: str = "") -> list[str]:
|
|
203
|
+
if requested:
|
|
204
|
+
declared = [str(Path(item).as_posix()).lstrip("/") for item in requested]
|
|
205
|
+
else:
|
|
206
|
+
declared = infer_files_from_goal(repo_root, goal)
|
|
207
|
+
if not declared:
|
|
208
|
+
declared = infer_toy_source_file(repo_root)
|
|
209
|
+
if not declared:
|
|
210
|
+
raise SystemExit(
|
|
211
|
+
"Cannot determine a safe source file scope from the bug description. "
|
|
212
|
+
"Pass --declared-file path/to/file."
|
|
213
|
+
)
|
|
214
|
+
if len(declared) > 2:
|
|
215
|
+
raise SystemExit("The first Creator slice accepts at most two --declared-file values.")
|
|
216
|
+
missing = [path for path in declared if not (repo_root / path).is_file()]
|
|
217
|
+
if missing:
|
|
218
|
+
raise SystemExit(f"Declared file does not exist: {', '.join(missing)}")
|
|
219
|
+
return declared
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def product_fix_plan(
|
|
223
|
+
goal: str,
|
|
224
|
+
repo_root: Path,
|
|
225
|
+
requested_files: list[str],
|
|
226
|
+
recurrence: dict[str, Any] | None = None,
|
|
227
|
+
retrieval_mode: str = "exact",
|
|
228
|
+
) -> dict[str, Any]:
|
|
229
|
+
declared_files = infer_declared_files(repo_root, requested_files, goal)
|
|
230
|
+
return {
|
|
231
|
+
"version": 1,
|
|
232
|
+
"goal": goal,
|
|
233
|
+
"source": "product_fix_first_creator_slice",
|
|
234
|
+
"tasks": [
|
|
235
|
+
{
|
|
236
|
+
"id": "task-002",
|
|
237
|
+
"description": goal,
|
|
238
|
+
"acceptance": ["Creator produces a sandbox patch within the declared file set", "Repository test command passes in the staged overlay"],
|
|
239
|
+
"risk_tier": 1,
|
|
240
|
+
"status": "pending",
|
|
241
|
+
"depends_on": [],
|
|
242
|
+
"declared_files": declared_files,
|
|
243
|
+
"attempt_count": 0,
|
|
244
|
+
"produced_artifacts": [],
|
|
245
|
+
"recurrence": recurrence or {
|
|
246
|
+
"status": "recurrence_key_unassignable",
|
|
247
|
+
"reason": "structured_recurrence_card_not_supplied",
|
|
248
|
+
},
|
|
249
|
+
"retrieval_mode": retrieval_mode,
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def latest_job(repo_root: Path) -> Path | None:
|
|
256
|
+
found = find_latest_incomplete_job(repo_root)
|
|
257
|
+
if found:
|
|
258
|
+
return found
|
|
259
|
+
root = job_root(repo_root)
|
|
260
|
+
if not root.exists():
|
|
261
|
+
return None
|
|
262
|
+
candidates = []
|
|
263
|
+
for job_dir in root.iterdir():
|
|
264
|
+
if job_dir.is_dir() and (job_dir / "events.jsonl").exists():
|
|
265
|
+
state = replay(read_events(job_dir / "events.jsonl"))
|
|
266
|
+
candidates.append((state.last_seq, job_dir))
|
|
267
|
+
if not candidates:
|
|
268
|
+
return None
|
|
269
|
+
return sorted(candidates, key=lambda item: item[0], reverse=True)[0][1]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def default_reviewer_name(args: argparse.Namespace) -> str:
|
|
273
|
+
return str(getattr(args, "reviewer_name", None) or getpass.getuser() or "local-user")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def latest_passed_task_id(repo_root: Path, job_id: str | None, requested: str | None = None) -> tuple[str, str]:
|
|
277
|
+
job_dir = job_root(repo_root) / job_id if job_id else latest_job(repo_root)
|
|
278
|
+
if not job_dir:
|
|
279
|
+
raise SystemExit(f"No Socra jobs found under {job_root(repo_root)}")
|
|
280
|
+
events = read_events(job_dir / "events.jsonl")
|
|
281
|
+
plan = materialize_plan_from_events(events, json.loads((job_dir / "plan.json").read_text(encoding="utf-8")) if (job_dir / "plan.json").exists() else {"tasks": []})
|
|
282
|
+
if requested:
|
|
283
|
+
return job_dir.name, requested
|
|
284
|
+
passed = [str(task.get("id")) for task in plan.get("tasks", []) if task.get("status") == "passed"]
|
|
285
|
+
if not passed:
|
|
286
|
+
raise SystemExit("No passed task is available for review/promote.")
|
|
287
|
+
return job_dir.name, passed[-1]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def status_main(args: argparse.Namespace) -> int:
|
|
291
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
292
|
+
config = load_config(repo_root)
|
|
293
|
+
print(f"Project: {repo_root}")
|
|
294
|
+
print(f"Config: {config_path(repo_root)}")
|
|
295
|
+
print(f"Tool: {config.get('tool', 'unknown')} schema {config.get('schema_version')}")
|
|
296
|
+
project = config.get("project") if isinstance(config.get("project"), dict) else {}
|
|
297
|
+
print(f"Package manager: {project.get('package_manager') or 'not detected'}")
|
|
298
|
+
test_command = project.get("test_command")
|
|
299
|
+
print(f"Test command: {' '.join(test_command) if isinstance(test_command, list) else 'not detected'}")
|
|
300
|
+
model = config.get("model") if isinstance(config.get("model"), dict) else {}
|
|
301
|
+
print(f"Endpoint: {model.get('endpoint') or 'not configured'}")
|
|
302
|
+
print(f"Model: {model.get('model') or 'not configured'}")
|
|
303
|
+
print(f"Hosted fallback: {bool(model.get('hosted_fallback'))}")
|
|
304
|
+
|
|
305
|
+
job_dir = latest_job(repo_root)
|
|
306
|
+
if not job_dir:
|
|
307
|
+
print("Job: none")
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
events = read_events(job_dir / "events.jsonl")
|
|
311
|
+
state = replay(events)
|
|
312
|
+
plan = materialize_plan_from_events(events, json.loads((job_dir / "plan.json").read_text(encoding="utf-8")) if (job_dir / "plan.json").exists() else {"tasks": []})
|
|
313
|
+
print(f"Job: {state.job_id}")
|
|
314
|
+
print(f"Goal: {state.goal}")
|
|
315
|
+
print(f"Phase: {state.phase}")
|
|
316
|
+
print(f"Events: {len(events)}")
|
|
317
|
+
print(f"Active task: {state.active_task_id or 'none'}")
|
|
318
|
+
print(f"Completed tasks: {len(state.completed_tasks)}")
|
|
319
|
+
print(f"Blocked tasks: {len(state.blocked_tasks)}")
|
|
320
|
+
print(f"Ready tasks: {', '.join(ready_task_ids(plan)[:8]) or 'none'}")
|
|
321
|
+
interrupted = detect_interrupted_attempt(events)
|
|
322
|
+
if interrupted:
|
|
323
|
+
print(
|
|
324
|
+
"Interrupted attempt: "
|
|
325
|
+
f"{interrupted['task_id']} after {interrupted['last_event_type']} "
|
|
326
|
+
f"(seq {interrupted['last_event_seq']})"
|
|
327
|
+
)
|
|
328
|
+
if state.last_error:
|
|
329
|
+
print(f"Last error: {state.last_error}")
|
|
330
|
+
return 0
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def fix_main(args: argparse.Namespace) -> int:
|
|
334
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
335
|
+
config = load_config(repo_root)
|
|
336
|
+
endpoint_probe = {"ok": None, "status": "skipped_plan_only"} if args.plan_only else require_live_endpoint(config)
|
|
337
|
+
job_id = args.job_id or new_job_id(args.goal)
|
|
338
|
+
job_dir = job_root(repo_root) / job_id
|
|
339
|
+
if job_dir.exists():
|
|
340
|
+
raise SystemExit(f"Job already exists: {job_dir}")
|
|
341
|
+
|
|
342
|
+
recurrence = None
|
|
343
|
+
if args.recurrence_card:
|
|
344
|
+
try:
|
|
345
|
+
recurrence = load_recurrence_card(args.recurrence_card.expanduser().resolve())
|
|
346
|
+
repository = project_identity(repo_root).get("repository")
|
|
347
|
+
if not repository or normalize_repository(repository) != recurrence["recurrence_key"]["canonical_repository_identity"]:
|
|
348
|
+
raise ValueError("recurrence card repository does not match the target repository")
|
|
349
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
350
|
+
raise SystemExit(f"Invalid recurrence card: {exc}") from exc
|
|
351
|
+
plan = product_fix_plan(
|
|
352
|
+
args.goal,
|
|
353
|
+
repo_root,
|
|
354
|
+
args.declared_file or [],
|
|
355
|
+
recurrence=recurrence,
|
|
356
|
+
retrieval_mode=args.retrieval,
|
|
357
|
+
)
|
|
358
|
+
validate_task_graph(plan["tasks"])
|
|
359
|
+
|
|
360
|
+
with JobLock(job_dir / ".lock"):
|
|
361
|
+
for folder in ["artifacts", "patches", "sandbox", "checkpoints"]:
|
|
362
|
+
(job_dir / folder).mkdir(parents=True, exist_ok=True)
|
|
363
|
+
|
|
364
|
+
write_json(job_dir / "plan.json", plan)
|
|
365
|
+
log = JobEventLog(job_dir / "events.jsonl", job_id)
|
|
366
|
+
log.append("job_created", {"goal": args.goal, "project": str(repo_root), "source": "product_fix"})
|
|
367
|
+
log.append("config_loaded", {"path": str(config_path(repo_root).relative_to(repo_root)), "schema_version": config.get("schema_version")})
|
|
368
|
+
model = model_config(config)
|
|
369
|
+
budgets = config.get("budgets") if isinstance(config.get("budgets"), dict) else {}
|
|
370
|
+
log.append("creator_endpoint_health_checked", {"endpoint": model.get("endpoint"), "probe": endpoint_probe})
|
|
371
|
+
log.append(
|
|
372
|
+
"creator_configured",
|
|
373
|
+
{
|
|
374
|
+
"role": "creator",
|
|
375
|
+
"model_alias": model.get("model") or "unconfigured",
|
|
376
|
+
"model": model.get("model") or "unconfigured",
|
|
377
|
+
"endpoint": model.get("endpoint"),
|
|
378
|
+
"backend": "openai-compatible",
|
|
379
|
+
"temperature": 0.2,
|
|
380
|
+
"context_budget_tokens": budgets.get("context_budget_tokens"),
|
|
381
|
+
"max_output_tokens": budgets.get("max_output_tokens"),
|
|
382
|
+
"hosted_fallback": bool(model.get("hosted_fallback")),
|
|
383
|
+
"local_first": bool(model.get("local_first", True)),
|
|
384
|
+
"source": "socra_config",
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
log.append("plan_written", {"plan": "plan.json", "task_count": len(plan["tasks"])})
|
|
388
|
+
for task in plan["tasks"]:
|
|
389
|
+
log.append("task_added", task)
|
|
390
|
+
log.append(
|
|
391
|
+
"plan_committed",
|
|
392
|
+
{
|
|
393
|
+
"revision": 1,
|
|
394
|
+
"task_count": len(plan["tasks"]),
|
|
395
|
+
"task_hash": canonical_task_hash(plan["tasks"]),
|
|
396
|
+
"source": "product_fix_first_creator_slice",
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
if args.plan_only:
|
|
400
|
+
log.append("job_paused", {"reason": "plan_only_no_agent_execution"})
|
|
401
|
+
|
|
402
|
+
events = read_events(job_dir / "events.jsonl")
|
|
403
|
+
state = replay(events)
|
|
404
|
+
write_state_snapshot(job_dir / "job.json", state)
|
|
405
|
+
write_handoff(job_dir, state_to_json(state), plan)
|
|
406
|
+
|
|
407
|
+
print(f"Created fix job: {job_id}")
|
|
408
|
+
print(f"Project: {repo_root}")
|
|
409
|
+
print(f"Job dir: {job_dir}")
|
|
410
|
+
print(f"Phase: {state.phase}")
|
|
411
|
+
if args.plan_only:
|
|
412
|
+
print("Plan-only mode: no endpoint preflight or agent execution was run.")
|
|
413
|
+
print("Next: wydcode status")
|
|
414
|
+
return 0
|
|
415
|
+
|
|
416
|
+
run_code = job_run_next_main(argparse.Namespace(project=repo_root, job_id=job_id, task_id=None, allow_unimplemented=False))
|
|
417
|
+
if run_code != 0:
|
|
418
|
+
try:
|
|
419
|
+
trace_result = sync_job_traces(repo_root, job_dir)
|
|
420
|
+
if trace_result["index_failed"]:
|
|
421
|
+
print(f"Warning: {trace_result['index_failed']} trace event(s) are durable but not indexed; run `wydcode traces sync --project .`")
|
|
422
|
+
except Exception as exc: # noqa: BLE001 - local failure remains authoritative if central export is unavailable.
|
|
423
|
+
print(f"Warning: rejected-attempt trace export failed; run `wydcode traces sync --project .`: {exc}")
|
|
424
|
+
return run_code
|
|
425
|
+
|
|
426
|
+
project = config.get("project") if isinstance(config.get("project"), dict) else {}
|
|
427
|
+
test_command = project.get("test_command")
|
|
428
|
+
if isinstance(test_command, list) and test_command:
|
|
429
|
+
command = " ".join(shlex.quote(str(part)) for part in test_command)
|
|
430
|
+
milestone_code = job_run_milestone_main(
|
|
431
|
+
argparse.Namespace(
|
|
432
|
+
project=repo_root,
|
|
433
|
+
job_id=job_id,
|
|
434
|
+
milestone_id="product-fix-tests",
|
|
435
|
+
command=command,
|
|
436
|
+
timeout_seconds=120,
|
|
437
|
+
prepare_deps="none",
|
|
438
|
+
deps_timeout_seconds=300,
|
|
439
|
+
allow_partial_smoke=True,
|
|
440
|
+
promotion_grade=False,
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
if milestone_code != 0:
|
|
444
|
+
try:
|
|
445
|
+
trace_result = sync_job_traces(repo_root, job_dir)
|
|
446
|
+
if trace_result["index_failed"]:
|
|
447
|
+
print(f"Warning: {trace_result['index_failed']} trace event(s) are durable but not indexed; run `wydcode traces sync --project .`")
|
|
448
|
+
except Exception as exc: # noqa: BLE001
|
|
449
|
+
print(f"Warning: rejected-attempt trace export failed; run `wydcode traces sync --project .`: {exc}")
|
|
450
|
+
return milestone_code
|
|
451
|
+
else:
|
|
452
|
+
print("Endpoint preflight: ok")
|
|
453
|
+
print("No test command detected; Creator sandbox attempt completed without repo test gate.")
|
|
454
|
+
print("Next: wydcode status")
|
|
455
|
+
return 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def review_main(args: argparse.Namespace) -> int:
|
|
459
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
460
|
+
load_config(repo_root)
|
|
461
|
+
job_id, task_id = latest_passed_task_id(repo_root, args.job_id, args.task_id)
|
|
462
|
+
return job_review_promotion_main(
|
|
463
|
+
argparse.Namespace(
|
|
464
|
+
project=repo_root,
|
|
465
|
+
job_id=job_id,
|
|
466
|
+
task_id=task_id,
|
|
467
|
+
reviewer_name=default_reviewer_name(args),
|
|
468
|
+
present_diff=True,
|
|
469
|
+
approve_source_promotion=False,
|
|
470
|
+
attest_reviewed=False,
|
|
471
|
+
notes=args.notes,
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def approve_main(args: argparse.Namespace) -> int:
|
|
477
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
478
|
+
load_config(repo_root)
|
|
479
|
+
job_id, task_id = latest_passed_task_id(repo_root, args.job_id, args.task_id)
|
|
480
|
+
behavior_preference = None
|
|
481
|
+
gene_evidence = None
|
|
482
|
+
if bool(args.behavior_card) != bool(args.heldout_evidence):
|
|
483
|
+
raise SystemExit("--behavior-card and --heldout-evidence must be supplied together")
|
|
484
|
+
if args.behavior_card:
|
|
485
|
+
try:
|
|
486
|
+
behavior_preference = load_behavior_preference(args.behavior_card.expanduser().resolve())
|
|
487
|
+
gene_evidence = load_gene_evidence(args.heldout_evidence.expanduser().resolve())
|
|
488
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
489
|
+
raise SystemExit(f"Invalid behavior or held-out evidence card: {exc}") from exc
|
|
490
|
+
return job_review_promotion_main(
|
|
491
|
+
argparse.Namespace(
|
|
492
|
+
project=repo_root,
|
|
493
|
+
job_id=job_id,
|
|
494
|
+
task_id=task_id,
|
|
495
|
+
reviewer_name=default_reviewer_name(args),
|
|
496
|
+
present_diff=False,
|
|
497
|
+
approve_source_promotion=True,
|
|
498
|
+
attest_reviewed=True,
|
|
499
|
+
notes=args.notes,
|
|
500
|
+
behavior_preference=behavior_preference,
|
|
501
|
+
gene_evidence=gene_evidence,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def reject_main(args: argparse.Namespace) -> int:
|
|
507
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
508
|
+
load_config(repo_root)
|
|
509
|
+
job_id, task_id = latest_passed_task_id(repo_root, args.job_id, args.task_id)
|
|
510
|
+
return job_reject_promotion_main(
|
|
511
|
+
argparse.Namespace(
|
|
512
|
+
project=repo_root,
|
|
513
|
+
job_id=job_id,
|
|
514
|
+
task_id=task_id,
|
|
515
|
+
reviewer_name=default_reviewer_name(args),
|
|
516
|
+
reason=args.reason,
|
|
517
|
+
notes=args.notes,
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def promote_main(args: argparse.Namespace) -> int:
|
|
523
|
+
repo_root = require_git_repo(args.project.expanduser().resolve())
|
|
524
|
+
load_config(repo_root)
|
|
525
|
+
job_id, task_id = latest_passed_task_id(repo_root, args.job_id, args.task_id)
|
|
526
|
+
return job_promote_main(
|
|
527
|
+
argparse.Namespace(
|
|
528
|
+
project=repo_root,
|
|
529
|
+
job_id=job_id,
|
|
530
|
+
task_id=task_id,
|
|
531
|
+
execute=not args.preflight,
|
|
532
|
+
)
|
|
533
|
+
)
|