okstra 0.130.1 → 0.130.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/docs/architecture.md +7 -1
- package/docs/cli.md +10 -2
- package/docs/project-structure-overview.md +16 -9
- package/docs/task-process/implementation-planning.md +11 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -1
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +3 -1
- package/runtime/agents/workers/report-writer-worker.md +8 -4
- package/runtime/prompts/lead/adapters/claude-code.md +2 -2
- package/runtime/prompts/lead/convergence.md +44 -21
- package/runtime/prompts/lead/okstra-lead-contract.md +9 -2
- package/runtime/prompts/lead/plan-body-verification.md +55 -12
- package/runtime/prompts/lead/report-writer.md +16 -15
- package/runtime/prompts/lead/team-contract.md +8 -6
- package/runtime/prompts/profiles/implementation-planning.md +10 -2
- package/runtime/python/okstra_ctl/codex_dispatch.py +5 -1
- package/runtime/python/okstra_ctl/convergence.py +121 -1
- package/runtime/python/okstra_ctl/convergence_engine.py +903 -13
- package/runtime/python/okstra_ctl/convergence_migration.py +9 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +13 -0
- package/runtime/python/okstra_ctl/plan_items.py +203 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +81 -0
- package/runtime/python/okstra_ctl/report_finalize.py +6 -4
- package/runtime/python/okstra_ctl/worker_artifact_paths.py +23 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +4 -4
- package/runtime/python/okstra_ctl/worker_prompt_body.py +1 -1
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +2 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +14 -0
- package/runtime/schemas/convergence-critic-results-v1.0.schema.json +57 -0
- package/runtime/schemas/convergence-groups-v1.0.schema.json +109 -0
- package/runtime/schemas/convergence-round-results-v1.0.schema.json +55 -0
- package/runtime/schemas/final-report-v1.0.schema.json +21 -2
- package/runtime/templates/report-writer-prompt-preamble.md +5 -3
- package/runtime/templates/reports/final-report.template.md +1 -1
- package/runtime/templates/worker-prompt-preamble.md +8 -7
- package/runtime/validators/validate-run.py +154 -118
- package/runtime/validators/validate_session_conformance.py +66 -23
- package/src/cli-registry.mjs +7 -0
- package/src/commands/execute/convergence.mjs +6 -1
- package/src/commands/execute/plan-items.mjs +9 -0
- package/src/commands/inspect/worker-liveness.mjs +2 -2
|
@@ -10,6 +10,7 @@ from typing import Any, Literal, Mapping
|
|
|
10
10
|
|
|
11
11
|
from .convergence_engine import (
|
|
12
12
|
ConvergenceContractError,
|
|
13
|
+
FINAL_SCHEMA_VERSION,
|
|
13
14
|
grouped_input_digest,
|
|
14
15
|
validate_final_state,
|
|
15
16
|
validate_working_state,
|
|
@@ -45,7 +46,14 @@ def decide_seed_action(
|
|
|
45
46
|
digest = grouped_input_digest(grouped_input)
|
|
46
47
|
final_status, final_value = _inspect_final(final_state_path)
|
|
47
48
|
if final_status == "terminal":
|
|
48
|
-
|
|
49
|
+
assert final_value is not None
|
|
50
|
+
version = final_value.get("schemaVersion")
|
|
51
|
+
reason = (
|
|
52
|
+
f"valid historical final {version} reused without rewrite"
|
|
53
|
+
if version != FINAL_SCHEMA_VERSION
|
|
54
|
+
else "valid terminal final state"
|
|
55
|
+
)
|
|
56
|
+
return SeedDecision("reuse-final", None, None, reason)
|
|
49
57
|
|
|
50
58
|
work_status, work_value = _inspect_work(work_state_path)
|
|
51
59
|
invalid_final = final_status == "invalid"
|
|
@@ -21,12 +21,15 @@ from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_header
|
|
|
21
21
|
from .worker_prompt_body import analysis_prompt_body
|
|
22
22
|
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
23
23
|
from .worker_prompt_policy import resolve_prompt_plan_for_manifest
|
|
24
|
+
from .worker_artifact_paths import audit_sidecar_rel
|
|
24
25
|
from .wrapper_status import read_wrapper_status, status_path_for_prompt
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
BACKEND_CLI_WRAPPER = "cli-wrapper"
|
|
28
29
|
BACKEND_TMUX_PANE = "tmux-pane"
|
|
29
30
|
BACKEND_MIXED = "mixed"
|
|
31
|
+
LIVENESS_AUDIT_HEARTBEAT = "audit-heartbeat"
|
|
32
|
+
LIVENESS_WRAPPER_STATUS = "wrapper-status"
|
|
30
33
|
MAX_WORKER_ATTEMPTS = 2
|
|
31
34
|
REPORT_WRITER_WORKER_ID = "report-writer"
|
|
32
35
|
TERMINAL_DISPATCH_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
@@ -556,6 +559,10 @@ def _dispatch_record(
|
|
|
556
559
|
"promptPath": str(job.prompt_path),
|
|
557
560
|
"resultPath": str(job.result_path),
|
|
558
561
|
"workerResultPath": str(job.worker_result_path),
|
|
562
|
+
"auditSidecarPath": audit_sidecar_rel(str(job.worker_result_path)),
|
|
563
|
+
"livenessMode": _liveness_mode(
|
|
564
|
+
BACKEND_CLI_WRAPPER if degraded_from else job.backend
|
|
565
|
+
),
|
|
559
566
|
"statusSidecarPath": str(status_path_for_prompt(job.prompt_path)),
|
|
560
567
|
"degradedFrom": degraded_from,
|
|
561
568
|
"reason": reason,
|
|
@@ -566,6 +573,12 @@ def _dispatch_record(
|
|
|
566
573
|
}
|
|
567
574
|
|
|
568
575
|
|
|
576
|
+
def _liveness_mode(backend: str) -> str:
|
|
577
|
+
if backend in (BACKEND_CLI_WRAPPER, BACKEND_TMUX_PANE):
|
|
578
|
+
return LIVENESS_WRAPPER_STATUS
|
|
579
|
+
return LIVENESS_AUDIT_HEARTBEAT
|
|
580
|
+
|
|
581
|
+
|
|
569
582
|
def _update_dispatch_status(
|
|
570
583
|
team_state_path: Path, worker_id: str, kind: str, attempt: int, status: str, reason: str
|
|
571
584
|
) -> None:
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Deterministic extraction of implementation-planning verification items."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .design_surfaces import (
|
|
9
|
+
DesignSurfaceError,
|
|
10
|
+
detect_design_surfaces,
|
|
11
|
+
expected_prep_plan_item_id,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PlanItemContractError(ValueError):
|
|
16
|
+
"""The implementation-planning body cannot produce exact plan items."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
PLAN_ITEM_SOURCES = (
|
|
20
|
+
("optionCandidates", "P-Opt", "4.5.1"),
|
|
21
|
+
("stepwiseExecution", "P-Step", "4.5.4"),
|
|
22
|
+
("dependencyMigrationRisk", "P-Dep", "4.5.5"),
|
|
23
|
+
("validationChecklist", "P-Val", "4.5.6"),
|
|
24
|
+
("rollbackStrategy", "P-Rb", "4.5.7"),
|
|
25
|
+
("requirementCoverage", "P-Req", "4.5.8"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_SUBJECT_FIELDS = {
|
|
29
|
+
"optionCandidates": ("subject", "name", "title", "action"),
|
|
30
|
+
"stepwiseExecution": ("subject", "action", "title", "name"),
|
|
31
|
+
"dependencyMigrationRisk": ("subject", "item", "title", "name", "action"),
|
|
32
|
+
"validationChecklist": ("subject", "check", "title", "name", "action"),
|
|
33
|
+
"rollbackStrategy": ("subject", "action", "title", "name"),
|
|
34
|
+
"requirementCoverage": ("subject", "requirement", "title", "name", "action"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _non_empty_string(value: object) -> str | None:
|
|
39
|
+
if isinstance(value, str) and value.strip():
|
|
40
|
+
return value
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _subject(row: Mapping[str, Any], source: str) -> str:
|
|
45
|
+
for field in _SUBJECT_FIELDS[source]:
|
|
46
|
+
value = _non_empty_string(row.get(field))
|
|
47
|
+
if value is not None:
|
|
48
|
+
return value
|
|
49
|
+
raise PlanItemContractError(f"{source} row has no non-empty subject candidate")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ticket_id(row: Mapping[str, Any]) -> str:
|
|
53
|
+
return _non_empty_string(row.get("ticketId")) or ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _row_payload(row: Mapping[str, Any], **coordinates: int) -> dict[str, Any]:
|
|
57
|
+
payload = deepcopy(dict(row))
|
|
58
|
+
for field, value in coordinates.items():
|
|
59
|
+
existing = payload.get(field)
|
|
60
|
+
if field in payload and existing != value:
|
|
61
|
+
raise PlanItemContractError(
|
|
62
|
+
f"row {field} coordinate {existing!r} conflicts with {value!r}"
|
|
63
|
+
)
|
|
64
|
+
payload[field] = value
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _add_item(items: list[dict[str, Any]], item: dict[str, Any]) -> None:
|
|
69
|
+
if any(existing["id"] == item["id"] for existing in items):
|
|
70
|
+
raise PlanItemContractError(f"duplicate plan item ID: {item['id']}")
|
|
71
|
+
items.append(item)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _rows(planning: Mapping[str, Any], source: str) -> list[Mapping[str, Any]]:
|
|
75
|
+
value = planning.get(source, [])
|
|
76
|
+
if value is None:
|
|
77
|
+
return []
|
|
78
|
+
if not isinstance(value, list):
|
|
79
|
+
raise PlanItemContractError(f"{source} must be an array")
|
|
80
|
+
rows: list[Mapping[str, Any]] = []
|
|
81
|
+
for row in value:
|
|
82
|
+
if not isinstance(row, Mapping):
|
|
83
|
+
raise PlanItemContractError(f"{source} row must be an object")
|
|
84
|
+
rows.append(row)
|
|
85
|
+
return rows
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _coordinate(value: object, field: str) -> int:
|
|
89
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
90
|
+
raise PlanItemContractError(f"{field} must be a positive integer")
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract_standard_items(planning: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
95
|
+
items: list[dict[str, Any]] = []
|
|
96
|
+
for source, prefix, section in PLAN_ITEM_SOURCES:
|
|
97
|
+
if source == "stepwiseExecution":
|
|
98
|
+
_extract_step_items(planning, items, prefix, section)
|
|
99
|
+
continue
|
|
100
|
+
for index, row in enumerate(_rows(planning, source), start=1):
|
|
101
|
+
_add_item(
|
|
102
|
+
items,
|
|
103
|
+
{
|
|
104
|
+
"id": f"{prefix}-{index}",
|
|
105
|
+
"subject": _subject(row, source),
|
|
106
|
+
"sourceSection": section,
|
|
107
|
+
"ticketId": _ticket_id(row),
|
|
108
|
+
"payload": _row_payload(row),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
return items
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_step_items(
|
|
115
|
+
planning: Mapping[str, Any],
|
|
116
|
+
items: list[dict[str, Any]],
|
|
117
|
+
prefix: str,
|
|
118
|
+
section: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
if "stages" not in planning:
|
|
121
|
+
for index, row in enumerate(_rows(planning, "stepwiseExecution"), start=1):
|
|
122
|
+
_add_item(
|
|
123
|
+
items,
|
|
124
|
+
{
|
|
125
|
+
"id": f"{prefix}-{index}",
|
|
126
|
+
"subject": _subject(row, "stepwiseExecution"),
|
|
127
|
+
"sourceSection": section,
|
|
128
|
+
"ticketId": _ticket_id(row),
|
|
129
|
+
"payload": _row_payload(row),
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
stages = planning["stages"]
|
|
134
|
+
if not isinstance(stages, list):
|
|
135
|
+
raise PlanItemContractError("stages must be an array")
|
|
136
|
+
for stage in stages:
|
|
137
|
+
if not isinstance(stage, Mapping):
|
|
138
|
+
raise PlanItemContractError("stages row must be an object")
|
|
139
|
+
stage_number = _coordinate(stage.get("stage"), "stage")
|
|
140
|
+
for row in _rows(stage, "stepwiseExecution"):
|
|
141
|
+
step_number = _coordinate(row.get("step"), "step")
|
|
142
|
+
_add_item(
|
|
143
|
+
items,
|
|
144
|
+
{
|
|
145
|
+
"id": f"{prefix}-{stage_number}.{step_number}",
|
|
146
|
+
"subject": _subject(row, "stepwiseExecution"),
|
|
147
|
+
"sourceSection": section,
|
|
148
|
+
"ticketId": _ticket_id(row),
|
|
149
|
+
"payload": _row_payload(
|
|
150
|
+
row,
|
|
151
|
+
stage=stage_number,
|
|
152
|
+
step=step_number,
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _extract_prep_items(
|
|
159
|
+
planning: Mapping[str, Any], items: list[dict[str, Any]]
|
|
160
|
+
) -> None:
|
|
161
|
+
if not planning.get("stages"):
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
triggers = detect_design_surfaces(planning)
|
|
165
|
+
except (DesignSurfaceError, TypeError, AttributeError) as exc:
|
|
166
|
+
raise PlanItemContractError(f"design-surface detection failed: {exc}") from exc
|
|
167
|
+
for trigger in triggers:
|
|
168
|
+
item_id = expected_prep_plan_item_id(trigger.stage, trigger.kind)
|
|
169
|
+
_add_item(
|
|
170
|
+
items,
|
|
171
|
+
{
|
|
172
|
+
"id": item_id,
|
|
173
|
+
"subject": f"Stage {trigger.stage} {trigger.kind} design preparation",
|
|
174
|
+
"sourceSection": "5.5.10",
|
|
175
|
+
"ticketId": "",
|
|
176
|
+
"payload": {
|
|
177
|
+
"stage": trigger.stage,
|
|
178
|
+
"kind": trigger.kind,
|
|
179
|
+
"evidence": [
|
|
180
|
+
{
|
|
181
|
+
"step": evidence.step,
|
|
182
|
+
"field": evidence.field,
|
|
183
|
+
"match": evidence.match,
|
|
184
|
+
}
|
|
185
|
+
for evidence in trigger.evidence
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def extract_plan_items(implementation_planning: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
193
|
+
"""Return deterministic, lossless P-* items in contract order."""
|
|
194
|
+
if not isinstance(implementation_planning, Mapping):
|
|
195
|
+
raise PlanItemContractError("implementationPlanning must be an object")
|
|
196
|
+
items = _extract_standard_items(implementation_planning)
|
|
197
|
+
_extract_prep_items(implementation_planning, items)
|
|
198
|
+
return items
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def expected_plan_item_ids(implementation_planning: Mapping[str, Any]) -> list[str]:
|
|
202
|
+
"""Return the exact ordered IDs produced by extract_plan_items()."""
|
|
203
|
+
return [item["id"] for item in extract_plan_items(implementation_planning)]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""CLI adapter for deterministic implementation-planning item extraction."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .convergence_store import write_json_atomic
|
|
12
|
+
from .plan_items import PlanItemContractError, extract_plan_items
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_json_object(path: Path) -> dict[str, Any]:
|
|
16
|
+
try:
|
|
17
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
18
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
19
|
+
raise PlanItemContractError(f"cannot read JSON object {path}: {exc}") from exc
|
|
20
|
+
if not isinstance(value, dict):
|
|
21
|
+
raise PlanItemContractError(f"JSON input must be an object: {path}")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _planning(data: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
26
|
+
planning = data.get("implementationPlanning")
|
|
27
|
+
if not isinstance(planning, Mapping):
|
|
28
|
+
raise PlanItemContractError("implementationPlanning must be an object")
|
|
29
|
+
return planning
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _envelope(data: Mapping[str, Any]) -> dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"schemaVersion": "1.0",
|
|
35
|
+
"taskType": "implementation-planning",
|
|
36
|
+
"items": extract_plan_items(_planning(data)),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parser() -> argparse.ArgumentParser:
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="okstra plan-items",
|
|
43
|
+
description="Extract and validate deterministic plan-body items.",
|
|
44
|
+
)
|
|
45
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
46
|
+
extract = commands.add_parser("extract")
|
|
47
|
+
extract.add_argument("--data", type=Path, required=True)
|
|
48
|
+
extract.add_argument("--output", type=Path, required=True)
|
|
49
|
+
validate = commands.add_parser("validate")
|
|
50
|
+
validate.add_argument("--data", type=Path, required=True)
|
|
51
|
+
validate.add_argument("--items", type=Path, required=True)
|
|
52
|
+
return parser
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract(args: argparse.Namespace) -> dict[str, Any]:
|
|
56
|
+
envelope = _envelope(_load_json_object(args.data))
|
|
57
|
+
write_json_atomic(args.output, envelope)
|
|
58
|
+
return {"ok": True, "operation": "extract", "path": str(args.output)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate(args: argparse.Namespace) -> dict[str, Any]:
|
|
62
|
+
expected = _envelope(_load_json_object(args.data))
|
|
63
|
+
actual = _load_json_object(args.items)
|
|
64
|
+
if actual != expected:
|
|
65
|
+
raise PlanItemContractError("plan items envelope does not match deterministic extraction")
|
|
66
|
+
return {"ok": True, "operation": "validate", "path": str(args.items)}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str] | None = None) -> int:
|
|
70
|
+
args = _parser().parse_args(argv)
|
|
71
|
+
try:
|
|
72
|
+
result = _extract(args) if args.command == "extract" else _validate(args)
|
|
73
|
+
except (PlanItemContractError, OSError, ValueError) as exc:
|
|
74
|
+
print(f"plan-items: {exc}", file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|
|
@@ -121,19 +121,21 @@ def task_manifest_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def resolve_workspace_script(workspace_root: Path, script_name: str) -> Path:
|
|
124
|
-
|
|
124
|
+
# The build lands helper scripts under `bin/`; `scripts/` is the repo layout.
|
|
125
|
+
return _resolve_workspace_file(workspace_root, ("bin", "scripts"), script_name)
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
def resolve_workspace_validator(workspace_root: Path, validator_name: str) -> Path:
|
|
128
|
-
return _resolve_workspace_file(workspace_root, "validators", validator_name)
|
|
129
|
+
return _resolve_workspace_file(workspace_root, ("validators",), validator_name)
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
def _resolve_workspace_file(
|
|
132
133
|
workspace_root: Path,
|
|
133
|
-
|
|
134
|
+
subdirs: tuple[str, ...],
|
|
134
135
|
name: str,
|
|
135
136
|
) -> Path:
|
|
136
|
-
candidates = [workspace_root / subdir / name
|
|
137
|
+
candidates = [workspace_root / subdir / name for subdir in subdirs]
|
|
138
|
+
candidates.append(workspace_root / name)
|
|
137
139
|
for candidate in candidates:
|
|
138
140
|
if candidate.is_file():
|
|
139
141
|
return candidate.resolve()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Canonical worker artifact path derivation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WorkerArtifactPathError(ValueError):
|
|
6
|
+
"""Raised when a worker artifact path is not canonical."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def audit_sidecar_rel(worker_result_rel: str) -> str:
|
|
10
|
+
"""Insert `-audit-` after the first canonical `-worker-` token."""
|
|
11
|
+
if not worker_result_rel.endswith(".md"):
|
|
12
|
+
raise WorkerArtifactPathError(
|
|
13
|
+
f"worker result path must end with .md: {worker_result_rel}"
|
|
14
|
+
)
|
|
15
|
+
directory, slash, filename = worker_result_rel.rpartition("/")
|
|
16
|
+
basename = filename if slash else worker_result_rel
|
|
17
|
+
prefix, separator, suffix = basename.partition("-worker-")
|
|
18
|
+
if not separator:
|
|
19
|
+
raise WorkerArtifactPathError(
|
|
20
|
+
f"worker result path has no canonical -worker- token: {worker_result_rel}"
|
|
21
|
+
)
|
|
22
|
+
audit_filename = f"{prefix}-worker-audit-{suffix}"
|
|
23
|
+
return f"{directory}{slash}{audit_filename}" if slash else audit_filename
|
|
@@ -12,9 +12,9 @@ verdict and spends its existing one-retry budget.
|
|
|
12
12
|
|
|
13
13
|
Two probes, matching the two ways a pending worker goes quiet:
|
|
14
14
|
|
|
15
|
-
* ``--audit`` —
|
|
16
|
-
cadence (or present with no heartbeat at all) means the
|
|
17
|
-
|
|
15
|
+
* ``--audit`` — an in-process worker audit sidecar. Stale past the heartbeat
|
|
16
|
+
cadence (or present with no heartbeat at all) means the worker hung. Uses
|
|
17
|
+
the same line shape and budget the Phase 7 validator applies.
|
|
18
18
|
* ``--prompt`` — a CLI-wrapper prompt-history path. Neither `<prompt>.log` nor
|
|
19
19
|
`<prompt>.status.json` past the launch grace means the wrapper never ran —
|
|
20
20
|
the dispatch itself failed, which no artifact otherwise distinguishes from a
|
|
@@ -110,7 +110,7 @@ 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="in-process
|
|
113
|
+
help="in-process worker audit sidecar path (repeatable)")
|
|
114
114
|
parser.add_argument("--prompt", action="append", default=[],
|
|
115
115
|
help="CLI-wrapper (codex/antigravity) prompt-history path "
|
|
116
116
|
"(repeatable). Only the wrappers write the artifacts "
|
|
@@ -47,7 +47,7 @@ def analysis_prompt_body(
|
|
|
47
47
|
"## Output Contract",
|
|
48
48
|
"- Read the Worker Preamble Path end-to-end before analysis.",
|
|
49
49
|
"- Write the worker result to Result Path and no other canonical result path.",
|
|
50
|
-
"- Write the audit sidecar
|
|
50
|
+
"- Write the audit sidecar to Audit sidecar path as the preamble requires.",
|
|
51
51
|
"- Cite evidence with file paths and line numbers whenever you make a claim.",
|
|
52
52
|
]
|
|
53
53
|
|
|
@@ -32,6 +32,7 @@ _NON_BODY_PREFIXES = (
|
|
|
32
32
|
"**Project Root:**",
|
|
33
33
|
"**Prompt History Path:**",
|
|
34
34
|
"**Result Path:**",
|
|
35
|
+
"**Audit sidecar path:**",
|
|
35
36
|
"Assigned worker prompt history path:",
|
|
36
37
|
"**Worker Preamble Path:**",
|
|
37
38
|
"**Errors log path:**",
|
|
@@ -55,6 +56,7 @@ _REQUIRED_TARGET_PREFIXES = (
|
|
|
55
56
|
_WORKER_SPECIFIC_PREFIXES = (
|
|
56
57
|
"**Prompt History Path:**",
|
|
57
58
|
"**Result Path:**",
|
|
59
|
+
"**Audit sidecar path:**",
|
|
58
60
|
"Assigned worker prompt history path:",
|
|
59
61
|
"**Errors sidecar path:**",
|
|
60
62
|
"**Worker Result Path:**",
|
|
@@ -135,7 +137,6 @@ def normalise_analysis_prompt(text: str) -> str:
|
|
|
135
137
|
"",
|
|
136
138
|
)
|
|
137
139
|
if prefix:
|
|
138
|
-
normalized.append(f"{prefix} <worker-specific>")
|
|
139
140
|
continue
|
|
140
141
|
normalized.append(_WORKER_LABEL_RE.sub("<analysis-worker>", line.rstrip()))
|
|
141
142
|
return "\n".join(normalized).strip() + "\n"
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Mapping
|
|
6
6
|
|
|
7
7
|
from .paths import okstra_home
|
|
8
|
+
from .worker_artifact_paths import WorkerArtifactPathError, audit_sidecar_rel
|
|
8
9
|
from .worker_prompt_policy import (
|
|
9
10
|
GRILLING_LOG_HEADER,
|
|
10
11
|
PromptPlan,
|
|
@@ -42,6 +43,7 @@ def worker_prompt_headers(
|
|
|
42
43
|
project_root: Path,
|
|
43
44
|
prompt_rel: str,
|
|
44
45
|
result_rel: str,
|
|
46
|
+
audit_source_rel: str | None = None,
|
|
45
47
|
worker_id: str,
|
|
46
48
|
dispatch_kind: str,
|
|
47
49
|
manifest: Mapping[str, Any],
|
|
@@ -49,6 +51,10 @@ def worker_prompt_headers(
|
|
|
49
51
|
) -> list[str]:
|
|
50
52
|
"""Render the team-contract worker prompt anchor headers."""
|
|
51
53
|
prompt_path = _resolve_project_path(project_root, prompt_rel)
|
|
54
|
+
audit_sidecar_path = _audit_sidecar_path(
|
|
55
|
+
project_root,
|
|
56
|
+
audit_source_rel or result_rel,
|
|
57
|
+
)
|
|
52
58
|
errors_log_path = _errors_log_path(project_root, manifest, active_context)
|
|
53
59
|
errors_sidecar_path = _worker_errors_sidecar_path(
|
|
54
60
|
project_root,
|
|
@@ -61,6 +67,7 @@ def worker_prompt_headers(
|
|
|
61
67
|
f"**Project Root:** {project_root}",
|
|
62
68
|
f"**Prompt History Path:** {prompt_rel}",
|
|
63
69
|
f"**Result Path:** {result_rel}",
|
|
70
|
+
f"**Audit sidecar path:** {audit_sidecar_path}",
|
|
64
71
|
f"Assigned worker prompt history path: {prompt_path}",
|
|
65
72
|
f"**Worker Preamble Path:** {_worker_preamble_path(plan, active_context)}",
|
|
66
73
|
f"**Worker Error Contract Path:** {_worker_error_contract_path(active_context)}",
|
|
@@ -235,6 +242,13 @@ def _resolve_project_path(project_root: Path, value: str) -> Path:
|
|
|
235
242
|
return path if path.is_absolute() else project_root / path
|
|
236
243
|
|
|
237
244
|
|
|
245
|
+
def _audit_sidecar_path(project_root: Path, worker_result_rel: str) -> Path:
|
|
246
|
+
try:
|
|
247
|
+
return _resolve_project_path(project_root, audit_sidecar_rel(worker_result_rel))
|
|
248
|
+
except WorkerArtifactPathError as exc:
|
|
249
|
+
raise WorkerPromptHeaderError(str(exc)) from exc
|
|
250
|
+
|
|
251
|
+
|
|
238
252
|
def _require_string(payload: Mapping[str, Any], key: str) -> str:
|
|
239
253
|
value = payload.get(key)
|
|
240
254
|
if not isinstance(value, str) or not value.strip():
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://okstra.dev/schemas/convergence-critic-results-v1.0.json",
|
|
4
|
+
"title": "OKSTRA Convergence Critic Results (v1.0)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "taskKey", "mode", "provider", "terminalStatus", "durationMs", "candidates"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": "1.0" },
|
|
10
|
+
"taskKey": { "type": "string", "pattern": "\\S" },
|
|
11
|
+
"mode": { "enum": ["coverage", "acceptance-devils-advocate"] },
|
|
12
|
+
"provider": { "type": "string", "pattern": "\\S" },
|
|
13
|
+
"terminalStatus": { "enum": ["completed", "timeout", "error", "not-run"] },
|
|
14
|
+
"durationMs": { "type": "integer", "minimum": 0 },
|
|
15
|
+
"candidates": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": { "$ref": "#/$defs/Candidate" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"$defs": {
|
|
21
|
+
"Candidate": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["candidateId", "summary", "category", "ticketIds", "originEvidence"],
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"candidateId": { "type": "string", "pattern": "\\S" },
|
|
27
|
+
"summary": { "type": "string", "pattern": "\\S" },
|
|
28
|
+
"category": { "type": "string", "pattern": "\\S" },
|
|
29
|
+
"ticketIds": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "type": "string", "pattern": "\\S" }
|
|
32
|
+
},
|
|
33
|
+
"originEvidence": { "type": "string", "pattern": "\\S" },
|
|
34
|
+
"severity": { "enum": ["critical", "major", "minor"] },
|
|
35
|
+
"evidenceArtifacts": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"minItems": 1,
|
|
38
|
+
"items": { "$ref": "#/$defs/EvidenceArtifact" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"EvidenceArtifact": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["path", "sha256", "command", "environment"],
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"properties": {
|
|
47
|
+
"path": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^\\.okstra/(?!\\.{1,2}(?:/|$))(?!.*?/\\.{1,2}(?:/|$))[^/\\\\\\r\\n]+(?:/[^/\\\\\\r\\n]+)*$"
|
|
50
|
+
},
|
|
51
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
52
|
+
"command": { "type": "string", "pattern": "\\S" },
|
|
53
|
+
"environment": { "type": "string", "pattern": "\\S" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://okstra.dev/schemas/convergence-groups-v1.0.json",
|
|
4
|
+
"title": "OKSTRA Convergence Grouped Input (v1.0)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schemaVersion", "taskKey", "config", "workers", "groups"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": "1.0" },
|
|
10
|
+
"taskKey": { "type": "string", "pattern": "\\S" },
|
|
11
|
+
"config": { "$ref": "#/$defs/Config" },
|
|
12
|
+
"workers": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"minItems": 1,
|
|
15
|
+
"items": { "$ref": "#/$defs/Worker" }
|
|
16
|
+
},
|
|
17
|
+
"groups": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "$ref": "#/$defs/Group" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"$defs": {
|
|
23
|
+
"Config": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"required": ["enabled", "adversarial", "maxRounds", "effectiveMaxRounds", "verificationMode"],
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"properties": {
|
|
28
|
+
"enabled": { "type": "boolean" },
|
|
29
|
+
"adversarial": { "type": "boolean" },
|
|
30
|
+
"maxRounds": { "type": "integer", "minimum": 1, "maximum": 3 },
|
|
31
|
+
"effectiveMaxRounds": { "type": "integer", "minimum": 1, "maximum": 3 },
|
|
32
|
+
"verificationMode": { "enum": ["lightweight", "full-reanalysis"] }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"Worker": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["workerId", "audience"],
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"properties": {
|
|
40
|
+
"workerId": { "type": "string", "pattern": "\\S" },
|
|
41
|
+
"audience": { "enum": ["analysis", "report-writer"] }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"Group": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"required": ["findingId", "summary", "category", "ticketIds", "originWorker", "originEvidence", "discoveredBy", "sourceItems"],
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"properties": {
|
|
49
|
+
"findingId": { "type": "string", "pattern": "\\S" },
|
|
50
|
+
"summary": { "type": "string", "pattern": "\\S" },
|
|
51
|
+
"category": { "type": "string", "pattern": "\\S" },
|
|
52
|
+
"ticketIds": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"minItems": 1,
|
|
55
|
+
"items": { "type": "string", "pattern": "\\S" }
|
|
56
|
+
},
|
|
57
|
+
"originWorker": { "type": "string", "pattern": "\\S" },
|
|
58
|
+
"originEvidence": { "type": "string", "pattern": "\\S" },
|
|
59
|
+
"evidenceArtifacts": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"minItems": 1,
|
|
62
|
+
"items": { "$ref": "#/$defs/EvidenceArtifact" }
|
|
63
|
+
},
|
|
64
|
+
"discoveredBy": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"minProperties": 1,
|
|
67
|
+
"additionalProperties": { "$ref": "#/$defs/Discovery" }
|
|
68
|
+
},
|
|
69
|
+
"sourceItems": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"minItems": 1,
|
|
72
|
+
"items": { "$ref": "#/$defs/SourceItem" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"EvidenceArtifact": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": ["path", "sha256", "command", "environment"],
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"path": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"pattern": "^\\.okstra/(?!\\.{1,2}(?:/|$))(?!.*?/\\.{1,2}(?:/|$))[^/\\\\\\r\\n]+(?:/[^/\\\\\\r\\n]+)*$"
|
|
84
|
+
},
|
|
85
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
86
|
+
"command": { "type": "string", "pattern": "\\S" },
|
|
87
|
+
"environment": { "type": "string", "pattern": "\\S" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"Discovery": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["itemId", "evidence"],
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"itemId": { "type": "string", "pattern": "\\S" },
|
|
96
|
+
"evidence": { "type": "string", "pattern": "\\S" }
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"SourceItem": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"required": ["worker", "itemId"],
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"properties": {
|
|
104
|
+
"worker": { "type": "string", "pattern": "\\S" },
|
|
105
|
+
"itemId": { "type": "string", "pattern": "\\S" }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|