okstra 0.128.0 → 0.129.1
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/docs/architecture/storage-model.md +15 -2
- package/docs/architecture.md +21 -5
- package/docs/cli.md +13 -2
- package/docs/project-structure-overview.md +22 -7
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -3
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +6 -6
- package/runtime/prompts/lead/adapters/claude-code.md +2 -1
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/convergence.md +42 -84
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -8
- package/runtime/prompts/lead/report-writer.md +22 -28
- package/runtime/prompts/lead/team-contract.md +28 -11
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -0
- package/runtime/prompts/profiles/improvement-discovery.md +11 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -13
- package/runtime/python/okstra_ctl/codex_dispatch.py +70 -319
- package/runtime/python/okstra_ctl/context_cost.py +82 -7
- package/runtime/python/okstra_ctl/convergence.py +223 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
- package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
- package/runtime/python/okstra_ctl/convergence_store.py +85 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -14
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/path_hints.py +23 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +56 -11
- package/runtime/python/okstra_ctl/report_finalize.py +394 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +6 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +102 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +79 -9
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
- package/runtime/templates/implementation-worker-preamble.md +51 -0
- package/runtime/templates/operating-standard.md +4 -4
- package/runtime/templates/report-writer-prompt-preamble.md +37 -0
- package/runtime/templates/worker-error-contract.md +46 -0
- package/runtime/templates/worker-prompt-preamble.md +28 -234
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +201 -72
- package/src/cli-registry.mjs +18 -0
- package/src/commands/execute/convergence.mjs +34 -0
- package/src/commands/report/finalize.mjs +65 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Phase 7 report post-processing — the single reference point.
|
|
2
|
+
|
|
3
|
+
Phase 7 turns a Phase 6 final-report data.json into shippable artifacts through
|
|
4
|
+
four ordered steps: usage substitution, html view rendering, follow-up task
|
|
5
|
+
spawning, and run validation. The order is load-bearing — rendering before
|
|
6
|
+
substitution ships `--` token cells, and validating before rendering trips the
|
|
7
|
+
report-views contract.
|
|
8
|
+
|
|
9
|
+
Every lead adapter drives Phase 7 through this module: the Codex adapter calls
|
|
10
|
+
it in-process (``codex_dispatch``), and a Claude-led run reaches the same code
|
|
11
|
+
through the ``okstra report-finalize`` CLI. Neither reimplements the sequence.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
22
|
+
|
|
23
|
+
from .final_report_paths import final_report_data_path, final_report_markdown_path
|
|
24
|
+
from .paths import task_dir
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
STEP_TOKEN_USAGE = "token-usage"
|
|
28
|
+
STEP_RENDER_VIEWS = "render-views"
|
|
29
|
+
STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
30
|
+
STEP_VALIDATE_RUN = "validate-run"
|
|
31
|
+
|
|
32
|
+
STEP_ORDER = (
|
|
33
|
+
STEP_TOKEN_USAGE,
|
|
34
|
+
STEP_RENDER_VIEWS,
|
|
35
|
+
STEP_SPAWN_FOLLOWUPS,
|
|
36
|
+
STEP_VALIDATE_RUN,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FinalizeError(Exception):
|
|
41
|
+
"""Raised when the Phase 7 sequence cannot be assembled."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def string_value(value: Any) -> str:
|
|
45
|
+
if not isinstance(value, str):
|
|
46
|
+
return ""
|
|
47
|
+
return value.strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def require_string(payload: Mapping[str, Any], key: str) -> str:
|
|
51
|
+
value = payload.get(key)
|
|
52
|
+
if not isinstance(value, str) or not value.strip():
|
|
53
|
+
raise FinalizeError(f"missing required string field: {key}")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_project_path(project_root: Path, value: str) -> Path:
|
|
58
|
+
path = Path(value)
|
|
59
|
+
return path if path.is_absolute() else project_root / path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_optional_path(project_root: Path, value: Any) -> Path | None:
|
|
63
|
+
if not isinstance(value, str) or not value.strip():
|
|
64
|
+
return None
|
|
65
|
+
return resolve_project_path(project_root, value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def project_relative_path(project_root: Path, path: Path) -> str:
|
|
69
|
+
try:
|
|
70
|
+
return path.relative_to(project_root).as_posix()
|
|
71
|
+
except ValueError:
|
|
72
|
+
return str(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def task_group(manifest: Mapping[str, Any]) -> str:
|
|
76
|
+
value = string_value(manifest.get("taskGroup"))
|
|
77
|
+
if value:
|
|
78
|
+
return value
|
|
79
|
+
task_key = require_string(manifest, "taskKey")
|
|
80
|
+
colon_parts = task_key.split(":")
|
|
81
|
+
if len(colon_parts) >= 3 and colon_parts[-2].strip():
|
|
82
|
+
return colon_parts[-2].strip()
|
|
83
|
+
slash_parts = task_key.split("/")
|
|
84
|
+
if len(slash_parts) >= 2 and slash_parts[0].strip():
|
|
85
|
+
return slash_parts[0].strip()
|
|
86
|
+
raise FinalizeError(f"cannot infer task group from taskKey: {task_key}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def task_id(manifest: Mapping[str, Any]) -> str:
|
|
90
|
+
value = string_value(manifest.get("taskId"))
|
|
91
|
+
if value:
|
|
92
|
+
return value
|
|
93
|
+
task_key = require_string(manifest, "taskKey")
|
|
94
|
+
colon_parts = task_key.split(":")
|
|
95
|
+
if len(colon_parts) >= 3 and colon_parts[-1].strip():
|
|
96
|
+
return colon_parts[-1].strip()
|
|
97
|
+
slash_parts = task_key.split("/")
|
|
98
|
+
if len(slash_parts) >= 2 and slash_parts[-1].strip():
|
|
99
|
+
return slash_parts[-1].strip()
|
|
100
|
+
raise FinalizeError(f"cannot infer task id from taskKey: {task_key}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_seq(manifest: Mapping[str, Any]) -> str:
|
|
104
|
+
seqs = manifest.get("runSequencesByCategory")
|
|
105
|
+
if not isinstance(seqs, Mapping):
|
|
106
|
+
raise FinalizeError("run manifest has no runSequencesByCategory object")
|
|
107
|
+
return require_string(seqs, "manifests")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def task_manifest_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
|
|
111
|
+
value = string_value(manifest.get("taskManifestPath"))
|
|
112
|
+
if value:
|
|
113
|
+
return resolve_project_path(project_root, value)
|
|
114
|
+
task_root = string_value(manifest.get("taskRootPath"))
|
|
115
|
+
if task_root:
|
|
116
|
+
return resolve_project_path(project_root, task_root) / "task-manifest.json"
|
|
117
|
+
return (
|
|
118
|
+
task_dir(project_root, task_group(manifest), task_id(manifest))
|
|
119
|
+
/ "task-manifest.json"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_workspace_script(workspace_root: Path, script_name: str) -> Path:
|
|
124
|
+
return _resolve_workspace_file(workspace_root, "scripts", script_name)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_workspace_validator(workspace_root: Path, validator_name: str) -> Path:
|
|
128
|
+
return _resolve_workspace_file(workspace_root, "validators", validator_name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _resolve_workspace_file(
|
|
132
|
+
workspace_root: Path,
|
|
133
|
+
subdir: str,
|
|
134
|
+
name: str,
|
|
135
|
+
) -> Path:
|
|
136
|
+
candidates = [workspace_root / subdir / name, workspace_root / name]
|
|
137
|
+
for candidate in candidates:
|
|
138
|
+
if candidate.is_file():
|
|
139
|
+
return candidate.resolve()
|
|
140
|
+
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
141
|
+
raise FinalizeError(f"{name} not found (searched: {searched})")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def tail(text: str, *, limit: int = 4000) -> str:
|
|
145
|
+
if len(text) <= limit:
|
|
146
|
+
return text
|
|
147
|
+
return text[-limit:]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class FinalizeContext:
|
|
152
|
+
"""Everything the Phase 7 sequence needs, resolved from a run manifest."""
|
|
153
|
+
|
|
154
|
+
project_root: Path
|
|
155
|
+
workspace_root: Path
|
|
156
|
+
manifest_path: Path
|
|
157
|
+
team_state_path: Path
|
|
158
|
+
data_path: Path
|
|
159
|
+
task_manifest_path: Path
|
|
160
|
+
task_key: str
|
|
161
|
+
task_type: str
|
|
162
|
+
task_group: str
|
|
163
|
+
seq: str
|
|
164
|
+
final_status_path: Path | None
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def markdown_path(self) -> Path:
|
|
168
|
+
return final_report_markdown_path(self.data_path)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_manifest(
|
|
172
|
+
cls,
|
|
173
|
+
*,
|
|
174
|
+
project_root: Path,
|
|
175
|
+
workspace_root: Path,
|
|
176
|
+
manifest_path: Path,
|
|
177
|
+
team_state_path: Path,
|
|
178
|
+
data_path: Path,
|
|
179
|
+
manifest: Mapping[str, Any],
|
|
180
|
+
) -> "FinalizeContext":
|
|
181
|
+
return cls(
|
|
182
|
+
project_root=project_root,
|
|
183
|
+
workspace_root=workspace_root,
|
|
184
|
+
manifest_path=manifest_path,
|
|
185
|
+
team_state_path=team_state_path,
|
|
186
|
+
data_path=data_path,
|
|
187
|
+
task_manifest_path=task_manifest_path(project_root, manifest),
|
|
188
|
+
task_key=require_string(manifest, "taskKey"),
|
|
189
|
+
task_type=require_string(manifest, "taskType"),
|
|
190
|
+
task_group=task_group(manifest),
|
|
191
|
+
seq=run_seq(manifest),
|
|
192
|
+
final_status_path=resolve_optional_path(
|
|
193
|
+
project_root, manifest.get("finalStatusPath")
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
199
|
+
"""Assemble the ordered Phase 7 argv list. Order is contractual."""
|
|
200
|
+
markdown_path = ctx.markdown_path
|
|
201
|
+
return [
|
|
202
|
+
(
|
|
203
|
+
STEP_TOKEN_USAGE,
|
|
204
|
+
[
|
|
205
|
+
sys.executable,
|
|
206
|
+
str(resolve_workspace_script(ctx.workspace_root, "okstra-token-usage.py")),
|
|
207
|
+
str(ctx.team_state_path),
|
|
208
|
+
"--project-root",
|
|
209
|
+
str(ctx.project_root),
|
|
210
|
+
"--write",
|
|
211
|
+
"--substitute-data",
|
|
212
|
+
str(ctx.data_path),
|
|
213
|
+
],
|
|
214
|
+
),
|
|
215
|
+
(
|
|
216
|
+
STEP_RENDER_VIEWS,
|
|
217
|
+
[
|
|
218
|
+
sys.executable,
|
|
219
|
+
str(
|
|
220
|
+
resolve_workspace_script(
|
|
221
|
+
ctx.workspace_root, "okstra-render-report-views.py"
|
|
222
|
+
)
|
|
223
|
+
),
|
|
224
|
+
str(markdown_path),
|
|
225
|
+
"--task-key",
|
|
226
|
+
ctx.task_key,
|
|
227
|
+
"--task-type",
|
|
228
|
+
ctx.task_type,
|
|
229
|
+
"--seq",
|
|
230
|
+
ctx.seq,
|
|
231
|
+
"--source-report",
|
|
232
|
+
project_relative_path(ctx.project_root, markdown_path),
|
|
233
|
+
],
|
|
234
|
+
),
|
|
235
|
+
(
|
|
236
|
+
STEP_SPAWN_FOLLOWUPS,
|
|
237
|
+
[
|
|
238
|
+
sys.executable,
|
|
239
|
+
str(
|
|
240
|
+
resolve_workspace_script(
|
|
241
|
+
ctx.workspace_root, "okstra-spawn-followups.py"
|
|
242
|
+
)
|
|
243
|
+
),
|
|
244
|
+
str(ctx.data_path),
|
|
245
|
+
"--project-root",
|
|
246
|
+
str(ctx.project_root),
|
|
247
|
+
"--task-group",
|
|
248
|
+
ctx.task_group,
|
|
249
|
+
"--parent-task-key",
|
|
250
|
+
ctx.task_key,
|
|
251
|
+
],
|
|
252
|
+
),
|
|
253
|
+
(STEP_VALIDATE_RUN, _validate_run_command(ctx, markdown_path)),
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _validate_run_command(ctx: FinalizeContext, markdown_path: Path) -> list[str]:
|
|
258
|
+
command = [
|
|
259
|
+
sys.executable,
|
|
260
|
+
str(resolve_workspace_validator(ctx.workspace_root, "validate-run.py")),
|
|
261
|
+
"--team-state",
|
|
262
|
+
str(ctx.team_state_path),
|
|
263
|
+
"--report",
|
|
264
|
+
str(markdown_path),
|
|
265
|
+
"--run-manifest",
|
|
266
|
+
str(ctx.manifest_path),
|
|
267
|
+
"--task-manifest",
|
|
268
|
+
str(ctx.task_manifest_path),
|
|
269
|
+
]
|
|
270
|
+
if ctx.final_status_path is not None:
|
|
271
|
+
command.extend(["--final-status", str(ctx.final_status_path)])
|
|
272
|
+
return command
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def step_payload(
|
|
276
|
+
name: str,
|
|
277
|
+
command: Sequence[str],
|
|
278
|
+
result: subprocess.CompletedProcess[str],
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
return {
|
|
281
|
+
"name": name,
|
|
282
|
+
"command": list(command),
|
|
283
|
+
"exitCode": result.returncode,
|
|
284
|
+
"stdoutTail": tail(result.stdout),
|
|
285
|
+
"stderrTail": tail(result.stderr),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def run_finalize(
|
|
290
|
+
ctx: FinalizeContext,
|
|
291
|
+
*,
|
|
292
|
+
before_step: Callable[[str], None] | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
"""Run the Phase 7 steps in order, stopping at the first non-zero exit.
|
|
295
|
+
|
|
296
|
+
``before_step`` fires immediately before each step is spawned, letting an
|
|
297
|
+
adapter settle state the step will read (the Codex adapter marks the report
|
|
298
|
+
writer `completed` before `validate-run` inspects team-state).
|
|
299
|
+
"""
|
|
300
|
+
steps: list[dict[str, Any]] = []
|
|
301
|
+
try:
|
|
302
|
+
commands = build_commands(ctx)
|
|
303
|
+
except FinalizeError as exc:
|
|
304
|
+
return {"ok": False, "reason": str(exc), "steps": steps}
|
|
305
|
+
|
|
306
|
+
for name, command in commands:
|
|
307
|
+
if before_step is not None:
|
|
308
|
+
before_step(name)
|
|
309
|
+
result = subprocess.run(
|
|
310
|
+
command,
|
|
311
|
+
cwd=ctx.project_root,
|
|
312
|
+
text=True,
|
|
313
|
+
capture_output=True,
|
|
314
|
+
)
|
|
315
|
+
steps.append(step_payload(name, command, result))
|
|
316
|
+
if result.returncode != 0:
|
|
317
|
+
return {
|
|
318
|
+
"ok": False,
|
|
319
|
+
"reason": f"{name} failed with exit code {result.returncode}",
|
|
320
|
+
"steps": steps,
|
|
321
|
+
}
|
|
322
|
+
return {"ok": True, "reason": "", "steps": steps}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _load_manifest(path: Path) -> Mapping[str, Any]:
|
|
326
|
+
try:
|
|
327
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
328
|
+
except FileNotFoundError as exc:
|
|
329
|
+
raise FinalizeError(f"run manifest not found: {path}") from exc
|
|
330
|
+
except json.JSONDecodeError as exc:
|
|
331
|
+
raise FinalizeError(f"run manifest is not valid JSON: {path} ({exc})") from exc
|
|
332
|
+
if not isinstance(payload, Mapping):
|
|
333
|
+
raise FinalizeError(f"run manifest is not a JSON object: {path}")
|
|
334
|
+
return payload
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _context_from_args(args: argparse.Namespace) -> FinalizeContext:
|
|
338
|
+
project_root = Path(args.project_root).resolve()
|
|
339
|
+
manifest_path = resolve_project_path(project_root, args.run_manifest)
|
|
340
|
+
manifest = _load_manifest(manifest_path)
|
|
341
|
+
if args.team_state:
|
|
342
|
+
team_state_path = resolve_project_path(project_root, args.team_state)
|
|
343
|
+
else:
|
|
344
|
+
team_state_path = resolve_project_path(
|
|
345
|
+
project_root, require_string(manifest, "teamStatePath")
|
|
346
|
+
)
|
|
347
|
+
report_path = resolve_project_path(project_root, args.report)
|
|
348
|
+
return FinalizeContext.from_manifest(
|
|
349
|
+
project_root=project_root,
|
|
350
|
+
workspace_root=Path(args.workspace_root).resolve(),
|
|
351
|
+
manifest_path=manifest_path,
|
|
352
|
+
team_state_path=team_state_path,
|
|
353
|
+
data_path=final_report_data_path(report_path),
|
|
354
|
+
manifest=manifest,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _parser() -> argparse.ArgumentParser:
|
|
359
|
+
parser = argparse.ArgumentParser(
|
|
360
|
+
description="Run the Phase 7 report post-processing sequence.",
|
|
361
|
+
)
|
|
362
|
+
parser.add_argument("--project-root", required=True)
|
|
363
|
+
parser.add_argument("--run-manifest", required=True)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"--report",
|
|
366
|
+
required=True,
|
|
367
|
+
help="final-report markdown path (its .data.json sibling is derived)",
|
|
368
|
+
)
|
|
369
|
+
parser.add_argument("--workspace-root", required=True)
|
|
370
|
+
parser.add_argument(
|
|
371
|
+
"--team-state",
|
|
372
|
+
default="",
|
|
373
|
+
help="defaults to the run manifest's teamStatePath",
|
|
374
|
+
)
|
|
375
|
+
return parser
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
379
|
+
args = _parser().parse_args(argv)
|
|
380
|
+
try:
|
|
381
|
+
ctx = _context_from_args(args)
|
|
382
|
+
except FinalizeError as exc:
|
|
383
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
384
|
+
return 2
|
|
385
|
+
result = run_finalize(ctx)
|
|
386
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
387
|
+
if not result["ok"]:
|
|
388
|
+
print(f"error: {result['reason']}", file=sys.stderr)
|
|
389
|
+
return 1
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
raise SystemExit(main())
|
|
@@ -110,9 +110,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
110
110
|
description="Report whether pending workers are still alive (read-only).",
|
|
111
111
|
)
|
|
112
112
|
parser.add_argument("--audit", action="append", default=[],
|
|
113
|
-
help="claude-worker audit sidecar path (repeatable)")
|
|
113
|
+
help="in-process claude-worker audit sidecar path (repeatable)")
|
|
114
114
|
parser.add_argument("--prompt", action="append", default=[],
|
|
115
|
-
help="CLI-wrapper prompt-history path
|
|
115
|
+
help="CLI-wrapper (codex/antigravity) prompt-history path "
|
|
116
|
+
"(repeatable). Only the wrappers write the artifacts "
|
|
117
|
+
"this probe looks for, so passing an in-process "
|
|
118
|
+
"claude-worker path here always reports "
|
|
119
|
+
"did-not-launch — probe those with --audit")
|
|
116
120
|
parser.add_argument("--max-idle", type=float, default=HEARTBEAT_MAX_GAP_SECONDS,
|
|
117
121
|
help="heartbeat staleness budget in seconds")
|
|
118
122
|
parser.add_argument("--launch-grace", type=float, default=DEFAULT_LAUNCH_GRACE_SECONDS,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Shared initial analysis-worker prompt body rendering."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from .worker_prompt_policy import PromptPlan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ANALYSIS_WORKER_LABELS = {
|
|
10
|
+
"claude": "Claude worker",
|
|
11
|
+
"codex": "Codex worker",
|
|
12
|
+
"antigravity": "Antigravity worker",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def analysis_prompt_body(
|
|
17
|
+
manifest: Mapping[str, Any],
|
|
18
|
+
active_context: Mapping[str, Any],
|
|
19
|
+
worker_id: str,
|
|
20
|
+
model: str,
|
|
21
|
+
role: str,
|
|
22
|
+
plan: PromptPlan,
|
|
23
|
+
) -> list[str]:
|
|
24
|
+
"""Render the provider-neutral body for an initial analysis audience."""
|
|
25
|
+
label = ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
|
|
26
|
+
return [
|
|
27
|
+
f"**Model:** {label}, {model}",
|
|
28
|
+
f"**Pane role:** {role}",
|
|
29
|
+
"",
|
|
30
|
+
f"# {label} Dispatch",
|
|
31
|
+
"",
|
|
32
|
+
"## Role",
|
|
33
|
+
(
|
|
34
|
+
f"You are the {label} for okstra cross-verification. "
|
|
35
|
+
"Produce an independent worker result."
|
|
36
|
+
),
|
|
37
|
+
"",
|
|
38
|
+
"## Task",
|
|
39
|
+
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
40
|
+
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
41
|
+
"",
|
|
42
|
+
"## Inputs",
|
|
43
|
+
*analysis_input_lines(manifest, active_context, plan),
|
|
44
|
+
"",
|
|
45
|
+
mcp_pointer_line(),
|
|
46
|
+
"",
|
|
47
|
+
"## Output Contract",
|
|
48
|
+
"- Read the Worker Preamble Path end-to-end before analysis.",
|
|
49
|
+
"- Write the worker result to Result Path and no other canonical result path.",
|
|
50
|
+
"- Write the audit sidecar and any tool-failure entries as the preamble requires.",
|
|
51
|
+
"- Cite evidence with file paths and line numbers whenever you make a claim.",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def analysis_input_lines(
|
|
56
|
+
manifest: Mapping[str, Any],
|
|
57
|
+
active_context: Mapping[str, Any],
|
|
58
|
+
plan: PromptPlan,
|
|
59
|
+
) -> list[str]:
|
|
60
|
+
if plan.packet_only:
|
|
61
|
+
packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
|
|
62
|
+
return existing_input_lines((("Primary analysis packet", packet_path),))
|
|
63
|
+
inputs = [
|
|
64
|
+
("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
|
|
65
|
+
("Task brief", instruction_path(manifest, active_context, "taskBriefPath")),
|
|
66
|
+
("Analysis profile", instruction_path(manifest, active_context, "analysisProfilePath")),
|
|
67
|
+
("Analysis material", instruction_path(manifest, active_context, "analysisMaterialPath")),
|
|
68
|
+
("Reference expectations", instruction_path(manifest, active_context, "referenceExpectationsPath")),
|
|
69
|
+
("Clarification response", instruction_path(manifest, active_context, "clarificationResponsePath")),
|
|
70
|
+
]
|
|
71
|
+
return existing_input_lines(inputs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def existing_input_lines(inputs: Sequence[tuple[str, str]]) -> list[str]:
|
|
75
|
+
lines = [f"- {label}: `{path}`" for label, path in inputs if path]
|
|
76
|
+
return lines or ["- No input paths were recorded in active-run-context."]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def instruction_path(
|
|
80
|
+
manifest: Mapping[str, Any],
|
|
81
|
+
active_context: Mapping[str, Any],
|
|
82
|
+
key: str,
|
|
83
|
+
) -> str:
|
|
84
|
+
instruction_set = active_context.get("instructionSet")
|
|
85
|
+
if isinstance(instruction_set, Mapping):
|
|
86
|
+
value = _string_value(instruction_set.get(key))
|
|
87
|
+
if value:
|
|
88
|
+
return value
|
|
89
|
+
return _string_value(manifest.get(key))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mcp_pointer_line() -> str:
|
|
93
|
+
return (
|
|
94
|
+
'**MCP servers:** follow the task brief\'s "## Available MCP Servers" '
|
|
95
|
+
"section (already in your Required reading)."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _require_string(payload: Mapping[str, Any], key: str) -> str:
|
|
100
|
+
value = _string_value(payload.get(key))
|
|
101
|
+
if not value:
|
|
102
|
+
raise ValueError(f"missing required string field: {key}")
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _string_value(value: Any) -> str:
|
|
107
|
+
return value.strip() if isinstance(value, str) else ""
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Mapping
|
|
7
|
+
from typing import Any, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
from .worker_prompt_policy import PromptPlan, resolve_prompt_plan_for_manifest
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
MAX_FINAL_VERIFICATION_DIRECTIVE_LINES = 40
|
|
10
13
|
MAX_FINAL_VERIFICATION_BODY_LINES = 96
|
|
11
14
|
|
|
12
15
|
_DIRECTIVE_HEADING = "## Run-specific directive"
|
|
16
|
+
_WORKER_ERROR_CONTRACT_HEADER = "**Worker Error Contract Path:**"
|
|
13
17
|
_PRIMARY_PACKET_RE = re.compile(
|
|
14
18
|
r"(?im)^-\s+Primary analysis packet:\s+`[^`\n]*analysis-packet\.md`\s*$"
|
|
15
19
|
)
|
|
@@ -63,6 +67,13 @@ _WORKER_LABEL_RE = re.compile(
|
|
|
63
67
|
)
|
|
64
68
|
|
|
65
69
|
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class PromptRecord:
|
|
72
|
+
worker_id: str
|
|
73
|
+
dispatch_kind: str
|
|
74
|
+
path: Path
|
|
75
|
+
|
|
76
|
+
|
|
66
77
|
def validate_final_verification_initial_prompt(text: str) -> list[str]:
|
|
67
78
|
"""Return deterministic compact-prompt contract violations."""
|
|
68
79
|
errors: list[str] = []
|
|
@@ -155,23 +166,105 @@ def validate_final_verification_prompt_paths(
|
|
|
155
166
|
prompt_paths: Mapping[str, Path],
|
|
156
167
|
) -> list[str]:
|
|
157
168
|
"""Validate persisted initial prompts and their normalized equality."""
|
|
158
|
-
|
|
169
|
+
records = [
|
|
170
|
+
PromptRecord(worker_id, "initial", path)
|
|
171
|
+
for worker_id, path in sorted(prompt_paths.items())
|
|
172
|
+
]
|
|
173
|
+
return validate_initial_prompt_records(
|
|
174
|
+
manifest={"taskType": "final-verification"},
|
|
175
|
+
records=records,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_initial_prompt_records(
|
|
180
|
+
*,
|
|
181
|
+
manifest: Mapping[str, Any],
|
|
182
|
+
records: Sequence[PromptRecord],
|
|
183
|
+
) -> list[str]:
|
|
184
|
+
"""Validate prompt audiences and compare their normalized equality groups."""
|
|
159
185
|
errors: list[str] = []
|
|
160
|
-
|
|
186
|
+
equality_groups: dict[str, dict[str, str]] = {}
|
|
187
|
+
for record in records:
|
|
188
|
+
plan = _resolve_record_plan(manifest, record, errors)
|
|
189
|
+
if plan is None or plan.audience in {"lead-only", "reverify"}:
|
|
190
|
+
continue
|
|
161
191
|
try:
|
|
162
|
-
text = path.read_text(encoding="utf-8")
|
|
192
|
+
text = record.path.read_text(encoding="utf-8")
|
|
163
193
|
except OSError as exc:
|
|
164
|
-
errors.append(
|
|
194
|
+
errors.append(
|
|
195
|
+
f"{record.worker_id}: cannot read prompt {record.path}: {exc}"
|
|
196
|
+
)
|
|
165
197
|
continue
|
|
166
|
-
prompts[worker_id] = text
|
|
167
198
|
errors.extend(
|
|
168
|
-
f"{worker_id}: {error}"
|
|
169
|
-
for error in
|
|
199
|
+
f"{record.worker_id}: {error}"
|
|
200
|
+
for error in _validate_prompt_for_plan(text, plan, manifest)
|
|
170
201
|
)
|
|
171
|
-
|
|
202
|
+
if plan.equality_group:
|
|
203
|
+
group = equality_groups.setdefault(plan.equality_group, {})
|
|
204
|
+
group[record.worker_id] = text
|
|
205
|
+
for prompts in equality_groups.values():
|
|
206
|
+
errors.extend(validate_analysis_prompt_set(prompts))
|
|
172
207
|
return errors
|
|
173
208
|
|
|
174
209
|
|
|
210
|
+
def _resolve_record_plan(
|
|
211
|
+
manifest: Mapping[str, Any],
|
|
212
|
+
record: PromptRecord,
|
|
213
|
+
errors: list[str],
|
|
214
|
+
) -> PromptPlan | None:
|
|
215
|
+
try:
|
|
216
|
+
return resolve_prompt_plan_for_manifest(
|
|
217
|
+
manifest=manifest,
|
|
218
|
+
worker_id=record.worker_id,
|
|
219
|
+
dispatch_kind=record.dispatch_kind,
|
|
220
|
+
)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
errors.append(f"{record.worker_id}: {exc}")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _validate_prompt_for_plan(
|
|
227
|
+
text: str,
|
|
228
|
+
plan: PromptPlan,
|
|
229
|
+
manifest: Mapping[str, Any],
|
|
230
|
+
) -> list[str]:
|
|
231
|
+
errors: list[str] = []
|
|
232
|
+
_require_non_empty_header(text, _WORKER_ERROR_CONTRACT_HEADER, errors)
|
|
233
|
+
if manifest.get("taskType") == "final-verification" and plan.audience == "analysis":
|
|
234
|
+
return [*errors, *validate_final_verification_initial_prompt(text)]
|
|
235
|
+
if not plan.allow_coding_preflight:
|
|
236
|
+
_reject_literal(
|
|
237
|
+
text,
|
|
238
|
+
"**Coding preflight pack:**",
|
|
239
|
+
"Coding preflight pack is forbidden for this prompt audience",
|
|
240
|
+
errors,
|
|
241
|
+
)
|
|
242
|
+
for prefix in plan.required_headers:
|
|
243
|
+
_require_non_empty_header(text, prefix, errors)
|
|
244
|
+
if plan.audience == "analysis":
|
|
245
|
+
packet_count = len(_PRIMARY_PACKET_RE.findall(text))
|
|
246
|
+
if packet_count != 1:
|
|
247
|
+
errors.append(
|
|
248
|
+
"exactly one Primary analysis packet path is required "
|
|
249
|
+
f"(found {packet_count})"
|
|
250
|
+
)
|
|
251
|
+
return errors
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _require_non_empty_header(
|
|
255
|
+
text: str,
|
|
256
|
+
prefix: str,
|
|
257
|
+
errors: list[str],
|
|
258
|
+
) -> None:
|
|
259
|
+
matches = [
|
|
260
|
+
line.strip()
|
|
261
|
+
for line in text.splitlines()
|
|
262
|
+
if line.strip().startswith(prefix)
|
|
263
|
+
]
|
|
264
|
+
if len(matches) != 1 or not matches[0][len(prefix):].strip():
|
|
265
|
+
errors.append(f"exactly one non-empty {prefix} header is required")
|
|
266
|
+
|
|
267
|
+
|
|
175
268
|
def _reject_literal(
|
|
176
269
|
text: str,
|
|
177
270
|
literal: str,
|