okstra 0.127.0 → 0.129.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/architecture/storage-model.md +23 -3
- package/docs/architecture.md +28 -1
- package/docs/cli.md +9 -0
- package/docs/project-structure-overview.md +17 -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 +5 -5
- 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 +45 -83
- package/runtime/prompts/lead/okstra-lead-contract.md +12 -8
- package/runtime/prompts/lead/report-writer.md +3 -2
- package/runtime/prompts/lead/team-contract.md +19 -9
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +10 -6
- 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 +14 -15
- package/runtime/python/okstra_ctl/codex_dispatch.py +51 -80
- 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 +60 -1
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/log_report.py +44 -5
- package/runtime/python/okstra_ctl/path_hints.py +28 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +78 -11
- package/runtime/python/okstra_ctl/run.py +66 -2
- package/runtime/python/okstra_ctl/wizard.py +15 -4
- package/runtime/python/okstra_ctl/work_categories.py +37 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +316 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +109 -10
- 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 -227
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +228 -45
- package/src/cli-registry.mjs +7 -0
- package/src/commands/execute/convergence.mjs +34 -0
- package/src/commands/inspect/log-report.mjs +5 -3
|
@@ -24,30 +24,23 @@ PROFILE_SECTIONS = (
|
|
|
24
24
|
"Primary focus areas",
|
|
25
25
|
"Expected output emphasis",
|
|
26
26
|
"Non-goals",
|
|
27
|
-
"Clarification request policy",
|
|
28
27
|
)
|
|
29
|
-
|
|
28
|
+
WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
30
29
|
"requirements-discovery": (
|
|
31
|
-
"
|
|
32
|
-
"Decision-tree walk",
|
|
30
|
+
"Worker discovery procedure",
|
|
33
31
|
),
|
|
34
32
|
"error-analysis": (
|
|
35
|
-
"
|
|
33
|
+
"Worker diagnosis procedure",
|
|
36
34
|
),
|
|
37
35
|
"implementation-planning": (
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"No-placeholder rule",
|
|
41
|
-
"Self-review pass before finalising the report",
|
|
36
|
+
"Worker planning procedure",
|
|
37
|
+
"Design principles applied when scoring options",
|
|
42
38
|
),
|
|
43
39
|
"final-verification": (
|
|
44
|
-
"
|
|
45
|
-
"Self-review pass before finalising the report",
|
|
40
|
+
"Worker verification procedure",
|
|
46
41
|
),
|
|
47
42
|
"improvement-discovery": (
|
|
48
|
-
"
|
|
49
|
-
"Worker diversity rule",
|
|
50
|
-
"Decision-tree walk",
|
|
43
|
+
"Worker candidate procedure",
|
|
51
44
|
),
|
|
52
45
|
}
|
|
53
46
|
CLARIFICATION_SECTIONS = (
|
|
@@ -81,6 +74,7 @@ def build_analysis_packet(
|
|
|
81
74
|
parts.extend(
|
|
82
75
|
_intro_block(
|
|
83
76
|
task_key,
|
|
77
|
+
task_type,
|
|
84
78
|
instruction_set_relative_path,
|
|
85
79
|
bool(clarification_response_path),
|
|
86
80
|
)
|
|
@@ -113,6 +107,7 @@ def _packet_frontmatter(brief_text: str, task_key: str) -> str:
|
|
|
113
107
|
|
|
114
108
|
def _intro_block(
|
|
115
109
|
task_key: str,
|
|
110
|
+
task_type: str,
|
|
116
111
|
instruction_set: str,
|
|
117
112
|
has_clarification: bool,
|
|
118
113
|
) -> list[str]:
|
|
@@ -132,6 +127,10 @@ def _intro_block(
|
|
|
132
127
|
f"- Analysis material: `{instruction_set}/analysis-material.md`",
|
|
133
128
|
f"- Reference expectations: `{instruction_set}/reference-expectations.md`",
|
|
134
129
|
]
|
|
130
|
+
if task_type == "final-verification":
|
|
131
|
+
lines.append(
|
|
132
|
+
f"- Verification target: `{instruction_set}/verification-target.md`"
|
|
133
|
+
)
|
|
135
134
|
if has_clarification:
|
|
136
135
|
lines.append(
|
|
137
136
|
f"- Clarification response: `{instruction_set}/clarification-response.md`"
|
|
@@ -151,7 +150,7 @@ def _brief_block(brief_text: str) -> list[str]:
|
|
|
151
150
|
def _profile_block(task_type: str, profile_text: str) -> list[str]:
|
|
152
151
|
section_names = (
|
|
153
152
|
PROFILE_SECTIONS
|
|
154
|
-
+
|
|
153
|
+
+ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE.get(task_type, ())
|
|
155
154
|
)
|
|
156
155
|
profile_sections = _extract_sections(profile_text, section_names)
|
|
157
156
|
profile_bullets = _extract_bullet_sections(profile_text, section_names)
|
|
@@ -27,6 +27,18 @@ from .path_hints import hydrate_active_run_context
|
|
|
27
27
|
from .wrapper_status import status_path_for_prompt
|
|
28
28
|
from .paths import task_dir
|
|
29
29
|
from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
|
|
30
|
+
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
31
|
+
from .worker_prompt_body import (
|
|
32
|
+
analysis_input_lines as _analysis_input_lines,
|
|
33
|
+
analysis_prompt_body as _analysis_prompt_body,
|
|
34
|
+
existing_input_lines as _existing_input_lines,
|
|
35
|
+
instruction_path as _instruction_path,
|
|
36
|
+
mcp_pointer_line as _mcp_pointer_line,
|
|
37
|
+
)
|
|
38
|
+
from .worker_prompt_policy import (
|
|
39
|
+
PromptPlan,
|
|
40
|
+
resolve_prompt_plan_for_manifest,
|
|
41
|
+
)
|
|
30
42
|
|
|
31
43
|
|
|
32
44
|
SUPPORTED_CLI_WORKERS = {
|
|
@@ -38,10 +50,6 @@ BACKEND_MIXED = "mixed"
|
|
|
38
50
|
MAX_WORKER_ATTEMPTS = 2
|
|
39
51
|
REPORT_WRITER_WORKER_ID = "report-writer"
|
|
40
52
|
REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
|
|
41
|
-
ANALYSIS_WORKER_LABELS = {
|
|
42
|
-
"codex": "Codex worker",
|
|
43
|
-
"antigravity": "Antigravity worker",
|
|
44
|
-
}
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
class DispatchError(Exception):
|
|
@@ -182,6 +190,7 @@ def build_dispatch_plan(
|
|
|
182
190
|
)
|
|
183
191
|
for worker_id in selected_workers
|
|
184
192
|
)
|
|
193
|
+
_validate_initial_prompts(manifest, workers)
|
|
185
194
|
return DispatchPlan(
|
|
186
195
|
project_root=project_root,
|
|
187
196
|
workspace_root=workspace_root,
|
|
@@ -193,6 +202,20 @@ def build_dispatch_plan(
|
|
|
193
202
|
)
|
|
194
203
|
|
|
195
204
|
|
|
205
|
+
def _validate_initial_prompts(
|
|
206
|
+
manifest: Mapping[str, Any],
|
|
207
|
+
workers: Sequence[WorkerDispatch],
|
|
208
|
+
) -> None:
|
|
209
|
+
records = [
|
|
210
|
+
PromptRecord(worker.worker_id, "initial", worker.prompt_path)
|
|
211
|
+
for worker in workers
|
|
212
|
+
]
|
|
213
|
+
errors = validate_initial_prompt_records(manifest=manifest, records=records)
|
|
214
|
+
if errors:
|
|
215
|
+
task_type = _require_string(manifest, "taskType")
|
|
216
|
+
raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
|
|
217
|
+
|
|
218
|
+
|
|
196
219
|
def dispatch_plan(plan: DispatchPlan) -> int:
|
|
197
220
|
_set_dispatch_mode(plan.team_state_path, _dispatch_mode(plan.workers))
|
|
198
221
|
for worker in plan.workers:
|
|
@@ -441,6 +464,7 @@ def _render_missing_analysis_worker_prompt(
|
|
|
441
464
|
prompt_rel = _worker_prompt_path(manifest, worker_id)
|
|
442
465
|
model = _require_string(worker_state, "modelExecutionValue")
|
|
443
466
|
role = _analysis_pane_role(manifest, active_context, worker_id)
|
|
467
|
+
plan = _initial_prompt_plan(manifest, worker_id)
|
|
444
468
|
lines = _base_prompt_headers(
|
|
445
469
|
project_root=project_root,
|
|
446
470
|
manifest=manifest,
|
|
@@ -450,7 +474,16 @@ def _render_missing_analysis_worker_prompt(
|
|
|
450
474
|
result_rel=result_rel,
|
|
451
475
|
)
|
|
452
476
|
lines.extend(_worktree_headers(manifest, active_context, role))
|
|
453
|
-
lines.extend(
|
|
477
|
+
lines.extend(
|
|
478
|
+
_analysis_prompt_body(
|
|
479
|
+
manifest,
|
|
480
|
+
active_context,
|
|
481
|
+
worker_id,
|
|
482
|
+
model,
|
|
483
|
+
role,
|
|
484
|
+
plan,
|
|
485
|
+
)
|
|
486
|
+
)
|
|
454
487
|
executor_tail = _implementation_executor_tail(
|
|
455
488
|
manifest,
|
|
456
489
|
active_context,
|
|
@@ -595,43 +628,6 @@ def _build_report_writer_dispatch(
|
|
|
595
628
|
)
|
|
596
629
|
|
|
597
630
|
|
|
598
|
-
def _analysis_prompt_body(
|
|
599
|
-
manifest: Mapping[str, Any],
|
|
600
|
-
active_context: Mapping[str, Any],
|
|
601
|
-
worker_id: str,
|
|
602
|
-
model: str,
|
|
603
|
-
role: str,
|
|
604
|
-
) -> list[str]:
|
|
605
|
-
label = ANALYSIS_WORKER_LABELS[worker_id]
|
|
606
|
-
return [
|
|
607
|
-
f"**Model:** {label}, {model}",
|
|
608
|
-
f"**Pane role:** {role}",
|
|
609
|
-
"",
|
|
610
|
-
f"# {label} Dispatch",
|
|
611
|
-
"",
|
|
612
|
-
"## Role",
|
|
613
|
-
(
|
|
614
|
-
f"You are the {label} for okstra cross-verification. "
|
|
615
|
-
"Produce an independent worker result."
|
|
616
|
-
),
|
|
617
|
-
"",
|
|
618
|
-
"## Task",
|
|
619
|
-
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
620
|
-
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
621
|
-
"",
|
|
622
|
-
"## Inputs",
|
|
623
|
-
*_analysis_input_lines(manifest, active_context),
|
|
624
|
-
"",
|
|
625
|
-
_mcp_pointer_line(),
|
|
626
|
-
"",
|
|
627
|
-
"## Output Contract",
|
|
628
|
-
"- Read the Worker Preamble Path end-to-end before analysis.",
|
|
629
|
-
"- Write the worker result to Result Path and no other canonical result path.",
|
|
630
|
-
"- Write the audit sidecar and any tool-failure entries as the preamble requires.",
|
|
631
|
-
"- Cite evidence with file paths and line numbers whenever you make a claim.",
|
|
632
|
-
]
|
|
633
|
-
|
|
634
|
-
|
|
635
631
|
def _report_writer_prompt_body(
|
|
636
632
|
manifest: Mapping[str, Any],
|
|
637
633
|
active_context: Mapping[str, Any],
|
|
@@ -669,13 +665,6 @@ def _report_writer_prompt_body(
|
|
|
669
665
|
]
|
|
670
666
|
|
|
671
667
|
|
|
672
|
-
def _mcp_pointer_line() -> str:
|
|
673
|
-
return (
|
|
674
|
-
'**MCP servers:** follow the task brief\'s "## Available MCP Servers" '
|
|
675
|
-
"section (already in your Required reading)."
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
|
|
679
668
|
def _base_prompt_headers(
|
|
680
669
|
*,
|
|
681
670
|
project_root: Path,
|
|
@@ -691,6 +680,7 @@ def _base_prompt_headers(
|
|
|
691
680
|
prompt_rel=prompt_rel,
|
|
692
681
|
result_rel=result_rel,
|
|
693
682
|
worker_id=worker_id,
|
|
683
|
+
dispatch_kind="initial",
|
|
694
684
|
manifest=manifest,
|
|
695
685
|
active_context=active_context,
|
|
696
686
|
)
|
|
@@ -698,19 +688,18 @@ def _base_prompt_headers(
|
|
|
698
688
|
raise DispatchError(str(exc)) from exc
|
|
699
689
|
|
|
700
690
|
|
|
701
|
-
def
|
|
691
|
+
def _initial_prompt_plan(
|
|
702
692
|
manifest: Mapping[str, Any],
|
|
703
|
-
|
|
704
|
-
) ->
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return _existing_input_lines(inputs)
|
|
693
|
+
worker_id: str,
|
|
694
|
+
) -> PromptPlan:
|
|
695
|
+
try:
|
|
696
|
+
return resolve_prompt_plan_for_manifest(
|
|
697
|
+
manifest=manifest,
|
|
698
|
+
worker_id=worker_id,
|
|
699
|
+
dispatch_kind="initial",
|
|
700
|
+
)
|
|
701
|
+
except ValueError as exc:
|
|
702
|
+
raise DispatchError(str(exc)) from exc
|
|
714
703
|
|
|
715
704
|
|
|
716
705
|
def _report_writer_input_lines(
|
|
@@ -733,11 +722,6 @@ def _report_writer_input_lines(
|
|
|
733
722
|
return lines or ["- No report-writer inputs were recorded in active-run-context."]
|
|
734
723
|
|
|
735
724
|
|
|
736
|
-
def _existing_input_lines(inputs: Sequence[tuple[str, str]]) -> list[str]:
|
|
737
|
-
lines = [f"- {label}: `{path}`" for label, path in inputs if path]
|
|
738
|
-
return lines or ["- No input paths were recorded in active-run-context."]
|
|
739
|
-
|
|
740
|
-
|
|
741
725
|
def _analysis_worker_result_lines(team_state: Mapping[str, Any]) -> list[str]:
|
|
742
726
|
lines = []
|
|
743
727
|
workers = team_state.get("workers")
|
|
@@ -755,19 +739,6 @@ def _analysis_worker_result_lines(team_state: Mapping[str, Any]) -> list[str]:
|
|
|
755
739
|
return lines
|
|
756
740
|
|
|
757
741
|
|
|
758
|
-
def _instruction_path(
|
|
759
|
-
manifest: Mapping[str, Any],
|
|
760
|
-
active_context: Mapping[str, Any],
|
|
761
|
-
key: str,
|
|
762
|
-
) -> str:
|
|
763
|
-
instruction_set = active_context.get("instructionSet")
|
|
764
|
-
if isinstance(instruction_set, Mapping):
|
|
765
|
-
value = _string_value(instruction_set.get(key))
|
|
766
|
-
if value:
|
|
767
|
-
return value
|
|
768
|
-
return _string_value(manifest.get(key))
|
|
769
|
-
|
|
770
|
-
|
|
771
742
|
def _analysis_pane_role(
|
|
772
743
|
manifest: Mapping[str, Any],
|
|
773
744
|
active_context: Mapping[str, Any],
|
|
@@ -800,7 +771,7 @@ def _worktree_headers(
|
|
|
800
771
|
) -> list[str]:
|
|
801
772
|
task_type = _require_string(manifest, "taskType")
|
|
802
773
|
worktree = _worktree_path(manifest, active_context)
|
|
803
|
-
if task_type
|
|
774
|
+
if task_type != "implementation" or not worktree:
|
|
804
775
|
return []
|
|
805
776
|
headers = [f"**Worktree:** {worktree}"]
|
|
806
777
|
if role == "executor":
|
|
@@ -125,6 +125,57 @@ def _installed_or_dev(installed: Path, dev_relative: str) -> Path:
|
|
|
125
125
|
return Path(__file__).resolve().parents[2] / dev_relative
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def _runtime_template(filename: str) -> Path:
|
|
129
|
+
return _installed_or_dev(
|
|
130
|
+
Path.home() / ".okstra" / "templates" / filename,
|
|
131
|
+
f"templates/{filename}",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _header_path(prompt_path: Path | None, header: str) -> Path | None:
|
|
136
|
+
if prompt_path is None or not prompt_path.is_file():
|
|
137
|
+
return None
|
|
138
|
+
prefix = f"**{header}:**"
|
|
139
|
+
for line in prompt_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
140
|
+
stripped = line.strip()
|
|
141
|
+
if stripped.startswith(prefix):
|
|
142
|
+
value = stripped[len(prefix):].strip().strip("`")
|
|
143
|
+
return Path(value) if value else None
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _prompt_contract_paths(
|
|
148
|
+
prompt_path: Path | None,
|
|
149
|
+
default_preamble: str,
|
|
150
|
+
) -> tuple[Path, Path]:
|
|
151
|
+
selected_preamble = _header_path(prompt_path, "Worker Preamble Path")
|
|
152
|
+
selected_error = _header_path(prompt_path, "Worker Error Contract Path")
|
|
153
|
+
preamble = (
|
|
154
|
+
selected_preamble
|
|
155
|
+
if selected_preamble and selected_preamble.is_file()
|
|
156
|
+
else _runtime_template(
|
|
157
|
+
selected_preamble.name if selected_preamble else default_preamble
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
error_contract = (
|
|
161
|
+
selected_error
|
|
162
|
+
if selected_error and selected_error.is_file()
|
|
163
|
+
else _runtime_template("worker-error-contract.md")
|
|
164
|
+
)
|
|
165
|
+
return preamble, error_contract
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _initial_prompt(run_dir: Path | None, *, report_writer: bool) -> Path | None:
|
|
169
|
+
if run_dir is None:
|
|
170
|
+
return None
|
|
171
|
+
candidates = sorted((run_dir / "prompts").glob("*.md"))
|
|
172
|
+
for path in candidates:
|
|
173
|
+
is_report_writer = "report-writer" in path.name
|
|
174
|
+
if is_report_writer == report_writer and "-reverify-r" not in path.name:
|
|
175
|
+
return path
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
128
179
|
def _instruction_set_metric(task_root: Path, project_root: Path) -> dict:
|
|
129
180
|
instruction_set = task_root / "instruction-set"
|
|
130
181
|
files = _all_files(instruction_set)
|
|
@@ -233,21 +284,31 @@ def _lead_phase1_metric(
|
|
|
233
284
|
}
|
|
234
285
|
|
|
235
286
|
|
|
236
|
-
def _analysis_worker_metric(
|
|
287
|
+
def _analysis_worker_metric(
|
|
288
|
+
task_root: Path,
|
|
289
|
+
project_root: Path,
|
|
290
|
+
run_dir: Path | None,
|
|
291
|
+
task_type: str,
|
|
292
|
+
) -> dict:
|
|
237
293
|
instruction_set = task_root / "instruction-set"
|
|
238
294
|
full_contract_files = [instruction_set / name for name in INPUT_FILES]
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"
|
|
295
|
+
default_preamble = (
|
|
296
|
+
"implementation-worker-preamble.md"
|
|
297
|
+
if task_type == "implementation"
|
|
298
|
+
else "worker-prompt-preamble.md"
|
|
242
299
|
)
|
|
243
|
-
|
|
300
|
+
preamble, error_contract = _prompt_contract_paths(
|
|
301
|
+
_initial_prompt(run_dir, report_writer=False),
|
|
302
|
+
default_preamble,
|
|
303
|
+
)
|
|
304
|
+
full_contract_files.extend((preamble, error_contract))
|
|
244
305
|
full_file_count, full_byte_count = _count_files(full_contract_files)
|
|
245
306
|
|
|
246
307
|
packet = instruction_set / "analysis-packet.md"
|
|
247
308
|
legacy_packet = instruction_set / "task-packet.md"
|
|
248
309
|
primary_packet = packet if packet.is_file() else legacy_packet
|
|
249
310
|
has_packet = primary_packet.is_file()
|
|
250
|
-
packet_files = [primary_packet, preamble] if has_packet else []
|
|
311
|
+
packet_files = [primary_packet, preamble, error_contract] if has_packet else []
|
|
251
312
|
packet_file_count, packet_byte_count = _count_files(packet_files)
|
|
252
313
|
mode = "analysis-packet-primary" if packet.is_file() else "full-input-contract"
|
|
253
314
|
byte_count = packet_byte_count if packet.is_file() else full_byte_count
|
|
@@ -266,6 +327,8 @@ def _analysis_worker_metric(task_root: Path, project_root: Path) -> dict:
|
|
|
266
327
|
"legacyFullContractFileCount": full_file_count,
|
|
267
328
|
"estimatedPacketModeBytesPerWorker": packet_byte_count,
|
|
268
329
|
"estimatedReductionPercent": reduction_percent,
|
|
330
|
+
"promptPreamblePath": str(preamble),
|
|
331
|
+
"workerErrorContractPath": str(error_contract),
|
|
269
332
|
"files": [project_rel(path, project_root) for path in current_files if path.is_file()],
|
|
270
333
|
"legacyFullContractFiles": [
|
|
271
334
|
project_rel(path, project_root)
|
|
@@ -313,6 +376,11 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
|
|
|
313
376
|
instruction_set / "final-report-template.md",
|
|
314
377
|
instruction_set / "final-report-schema.json",
|
|
315
378
|
])
|
|
379
|
+
preamble, error_contract = _prompt_contract_paths(
|
|
380
|
+
_initial_prompt(run_dir, report_writer=True),
|
|
381
|
+
"report-writer-prompt-preamble.md",
|
|
382
|
+
)
|
|
383
|
+
files.extend((preamble, error_contract))
|
|
316
384
|
if run_dir is not None:
|
|
317
385
|
files.extend(_current_seq_worker_results(run_dir))
|
|
318
386
|
convergence = _latest_matching_file(
|
|
@@ -326,6 +394,8 @@ def _report_writer_metric(run_dir: Path | None, task_root: Path, project_root: P
|
|
|
326
394
|
"fileCount": file_count,
|
|
327
395
|
"bytes": byte_count,
|
|
328
396
|
"estimatedTokens": _estimate_tokens(files),
|
|
397
|
+
"promptPreamblePath": str(preamble),
|
|
398
|
+
"workerErrorContractPath": str(error_contract),
|
|
329
399
|
"files": [project_rel(path, project_root) for path in files if path.is_file()],
|
|
330
400
|
}
|
|
331
401
|
|
|
@@ -357,7 +427,12 @@ def analyze_task_bundle(task_root: Path, project_root: Path) -> dict:
|
|
|
357
427
|
},
|
|
358
428
|
"instructionSet": _instruction_set_metric(task_root, project_root),
|
|
359
429
|
"leadPhase1": _lead_phase1_metric(task_root, run_dir, manifest, project_root),
|
|
360
|
-
"analysisWorker": _analysis_worker_metric(
|
|
430
|
+
"analysisWorker": _analysis_worker_metric(
|
|
431
|
+
task_root,
|
|
432
|
+
project_root,
|
|
433
|
+
run_dir,
|
|
434
|
+
str(manifest.get("taskType") or ""),
|
|
435
|
+
),
|
|
361
436
|
"reportWriter": _report_writer_metric(run_dir, task_root, project_root),
|
|
362
437
|
"skillAssets": _skill_assets_metric(),
|
|
363
438
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""CLI orchestration for deterministic Phase 5.5 convergence state."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .convergence_engine import (
|
|
11
|
+
ConvergenceContractError,
|
|
12
|
+
apply_round_results,
|
|
13
|
+
finalize_working_state,
|
|
14
|
+
plan_next_round,
|
|
15
|
+
seed_working_state,
|
|
16
|
+
validate_final_state,
|
|
17
|
+
validate_working_state,
|
|
18
|
+
)
|
|
19
|
+
from .convergence_migration import (
|
|
20
|
+
SeedDecision,
|
|
21
|
+
archive_state_bytes,
|
|
22
|
+
decide_seed_action,
|
|
23
|
+
migration_record,
|
|
24
|
+
)
|
|
25
|
+
from .convergence_store import (
|
|
26
|
+
load_json_object,
|
|
27
|
+
write_final_state_atomic,
|
|
28
|
+
write_json_atomic,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parser() -> argparse.ArgumentParser:
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="okstra convergence",
|
|
35
|
+
description="Advance and validate deterministic re-verification state.",
|
|
36
|
+
)
|
|
37
|
+
subparsers = parser.add_subparsers(dest="operation", required=True)
|
|
38
|
+
|
|
39
|
+
seed = subparsers.add_parser("seed", help="create, resume, or recover working state")
|
|
40
|
+
seed.add_argument("--groups", type=Path, required=True)
|
|
41
|
+
seed.add_argument("--work-state", type=Path, required=True)
|
|
42
|
+
seed.add_argument("--final-state", type=Path, required=True)
|
|
43
|
+
seed.add_argument("--migration-dir", type=Path, required=True)
|
|
44
|
+
seed.add_argument("--restart-from-round0", action="store_true")
|
|
45
|
+
|
|
46
|
+
plan = subparsers.add_parser("plan-round", help="write the next dispatch plan")
|
|
47
|
+
plan.add_argument("--work-state", type=Path, required=True)
|
|
48
|
+
plan.add_argument("--plan", type=Path, required=True)
|
|
49
|
+
|
|
50
|
+
apply = subparsers.add_parser("apply-round", help="apply structured round results")
|
|
51
|
+
apply.add_argument("--work-state", type=Path, required=True)
|
|
52
|
+
apply.add_argument("--plan", type=Path, required=True)
|
|
53
|
+
apply.add_argument("--results", type=Path, required=True)
|
|
54
|
+
|
|
55
|
+
finalize = subparsers.add_parser("finalize", help="write the public final state")
|
|
56
|
+
finalize.add_argument("--work-state", type=Path, required=True)
|
|
57
|
+
finalize.add_argument("--output", type=Path, required=True)
|
|
58
|
+
|
|
59
|
+
validate = subparsers.add_parser("validate", help="validate a persisted state")
|
|
60
|
+
validate.add_argument("--state", type=Path, required=True)
|
|
61
|
+
validate.add_argument("--kind", choices=("working", "final"), required=True)
|
|
62
|
+
return parser
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _emit(operation: str, action: str, path: Path) -> None:
|
|
66
|
+
print(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"ok": True,
|
|
70
|
+
"operation": operation,
|
|
71
|
+
"action": action,
|
|
72
|
+
"path": str(path),
|
|
73
|
+
},
|
|
74
|
+
ensure_ascii=False,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _archive_existing_states(
|
|
80
|
+
*,
|
|
81
|
+
work_state_path: Path,
|
|
82
|
+
final_state_path: Path,
|
|
83
|
+
migration_dir: Path,
|
|
84
|
+
) -> dict[str, str] | None:
|
|
85
|
+
records: list[dict[str, str]] = []
|
|
86
|
+
for path in (work_state_path, final_state_path):
|
|
87
|
+
if not path.exists():
|
|
88
|
+
continue
|
|
89
|
+
archive_path, digest = archive_state_bytes(
|
|
90
|
+
source_path=path,
|
|
91
|
+
migration_dir=migration_dir,
|
|
92
|
+
)
|
|
93
|
+
records.append(migration_record(path, archive_path, digest))
|
|
94
|
+
if not records:
|
|
95
|
+
return None
|
|
96
|
+
final_record = next(
|
|
97
|
+
(record for record in records if record["sourcePath"] == str(final_state_path)),
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
return final_record or records[0]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _resume_after_legacy_final(
|
|
104
|
+
decision: SeedDecision,
|
|
105
|
+
*,
|
|
106
|
+
work_state_path: Path,
|
|
107
|
+
final_state_path: Path,
|
|
108
|
+
migration_dir: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
if decision.archive_path is None:
|
|
111
|
+
return
|
|
112
|
+
archive_path, digest = archive_state_bytes(
|
|
113
|
+
source_path=final_state_path,
|
|
114
|
+
migration_dir=migration_dir,
|
|
115
|
+
)
|
|
116
|
+
state = load_json_object(work_state_path)
|
|
117
|
+
state["migration"] = migration_record(final_state_path, archive_path, digest)
|
|
118
|
+
write_json_atomic(work_state_path, state)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
122
|
+
grouped_input = load_json_object(args.groups)
|
|
123
|
+
decision = decide_seed_action(
|
|
124
|
+
grouped_input=grouped_input,
|
|
125
|
+
work_state_path=args.work_state,
|
|
126
|
+
final_state_path=args.final_state,
|
|
127
|
+
restart_from_round0=args.restart_from_round0,
|
|
128
|
+
)
|
|
129
|
+
if decision.action == "reuse-final":
|
|
130
|
+
return decision.action, args.final_state
|
|
131
|
+
if decision.action == "resume-work":
|
|
132
|
+
_resume_after_legacy_final(
|
|
133
|
+
decision,
|
|
134
|
+
work_state_path=args.work_state,
|
|
135
|
+
final_state_path=args.final_state,
|
|
136
|
+
migration_dir=args.migration_dir,
|
|
137
|
+
)
|
|
138
|
+
return decision.action, args.work_state
|
|
139
|
+
|
|
140
|
+
state = seed_working_state(grouped_input)
|
|
141
|
+
if decision.action == "restart-round0":
|
|
142
|
+
migration = _archive_existing_states(
|
|
143
|
+
work_state_path=args.work_state,
|
|
144
|
+
final_state_path=args.final_state,
|
|
145
|
+
migration_dir=args.migration_dir,
|
|
146
|
+
)
|
|
147
|
+
if migration is None:
|
|
148
|
+
raise ConvergenceContractError("restart-round0 has no state to archive")
|
|
149
|
+
state["migration"] = migration
|
|
150
|
+
write_json_atomic(args.work_state, state)
|
|
151
|
+
return decision.action, args.work_state
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _plan_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
155
|
+
state = load_json_object(args.work_state)
|
|
156
|
+
errors = validate_working_state(state)
|
|
157
|
+
if errors:
|
|
158
|
+
raise ConvergenceContractError("invalid working state: " + "; ".join(errors))
|
|
159
|
+
plan = plan_next_round(state)
|
|
160
|
+
write_json_atomic(args.plan, plan)
|
|
161
|
+
return str(plan["action"]), args.plan
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _apply_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
165
|
+
state = load_json_object(args.work_state)
|
|
166
|
+
plan = load_json_object(args.plan)
|
|
167
|
+
results = load_json_object(args.results)
|
|
168
|
+
updated = apply_round_results(state, plan, results)
|
|
169
|
+
errors = validate_working_state(updated)
|
|
170
|
+
if errors:
|
|
171
|
+
raise ConvergenceContractError("invalid updated working state: " + "; ".join(errors))
|
|
172
|
+
write_json_atomic(args.work_state, updated)
|
|
173
|
+
return "applied", args.work_state
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
177
|
+
state = load_json_object(args.work_state)
|
|
178
|
+
final = finalize_working_state(state)
|
|
179
|
+
migration = state.get("migration")
|
|
180
|
+
write_final_state_atomic(args.output, final, migration=migration)
|
|
181
|
+
return "finalized", args.output
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _validate(args: argparse.Namespace) -> tuple[str, Path]:
|
|
185
|
+
state = load_json_object(args.state)
|
|
186
|
+
errors = (
|
|
187
|
+
validate_working_state(state)
|
|
188
|
+
if args.kind == "working"
|
|
189
|
+
else validate_final_state(state)
|
|
190
|
+
)
|
|
191
|
+
if errors:
|
|
192
|
+
raise ConvergenceContractError("; ".join(errors))
|
|
193
|
+
return "valid", args.state
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _execute(args: argparse.Namespace) -> tuple[str, Path]:
|
|
197
|
+
operations: dict[str, Any] = {
|
|
198
|
+
"seed": _seed,
|
|
199
|
+
"plan-round": _plan_round,
|
|
200
|
+
"apply-round": _apply_round,
|
|
201
|
+
"finalize": _finalize,
|
|
202
|
+
"validate": _validate,
|
|
203
|
+
}
|
|
204
|
+
return operations[args.operation](args)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv: list[str] | None = None) -> int:
|
|
208
|
+
args = _parser().parse_args(argv)
|
|
209
|
+
try:
|
|
210
|
+
action, path = _execute(args)
|
|
211
|
+
except (ConvergenceContractError, json.JSONDecodeError, ValueError) as exc:
|
|
212
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
213
|
+
return 2
|
|
214
|
+
except OSError as exc:
|
|
215
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
_emit(args.operation, action, path)
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
raise SystemExit(main())
|
|
223
|
+
|