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
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
- before deciding whether to fan out, read `Related Task Graph`. Use `depends-on`, `blocks` / `blocked-by`, parent/child, follow-up, and split edges as seed ordering constraints. Do not flatten a directed graph into unrelated tasks.
|
|
15
15
|
- `intent-inference` augmentations whose paired `intent-check:` row carries `[CONFIRMED …]` are treated as **confirmed**; trust the confirmation text in `## Reporter Confirmations` over the original inference if they differ. Unconfirmed `intent-inference` rows under `reporter-confirmations: skipped` follow the precondition's `skipped` branch above.
|
|
16
16
|
- `conversion-block:` rows are explicit "translation failed" signals — never attempt to resolve them by inference here; the precondition above already handled them.
|
|
17
|
+
- Worker discovery procedure:
|
|
18
|
+
- classify the request and cite the evidence that determines both its work category and safest next phase
|
|
19
|
+
- identify independently startable decomposition candidates without publishing or rendering fan-out artifacts; preserve every directed dependency from `Related Task Graph`
|
|
20
|
+
- resolve codebase-answerable ambiguity by inspection and record file:line evidence; return only human-owned decisions as clarification candidates with the evidence already checked
|
|
21
|
+
- state the reporter's rejection criteria, missing routing inputs, and the evidence boundary behind each recommendation
|
|
17
22
|
- Primary focus areas:
|
|
18
23
|
- classify the work as bugfix, feature, improvement, refactor, or ops
|
|
19
24
|
- determine whether `error-analysis` or `implementation-planning` is the next safe step. Direct `implementation` handoff is never a valid routing target — implementation requires an approved `implementation-planning` report
|
|
@@ -49,7 +54,9 @@
|
|
|
49
54
|
- evidence-backed routing decision
|
|
50
55
|
- uncertainty boundaries and missing inputs
|
|
51
56
|
- next recommended phase and safe resume guidance
|
|
52
|
-
- canonical-term resolution for every `terminology:*` brief item
|
|
57
|
+
- canonical-term resolution for every `terminology:*` brief item as `<term> = <definition>`, plus whether `<PROJECT_ROOT>/.okstra/glossary.md` should be updated
|
|
58
|
+
- Report assembly instructions:
|
|
59
|
+
- write canonical-term resolutions in a new `Domain Alignment` subsection of the final report; actual glossary writes happen via `okstra-brief-gen` Step 4.5 on a subsequent run
|
|
53
60
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
54
61
|
- if any blocking input is missing at the time of writing the final report, populate `## 1. Clarification Items` in `final-report-template.md` (a single unified table; `Blocks=next-phase` for items the next run cannot start without)
|
|
55
62
|
- prefer concrete questions whose answers map directly to a routing decision (`bugfix` vs `feature`, `error-analysis` vs `implementation-planning`, etc.). State each option in plain language with one sentence describing what choosing it would mean for the next phase.
|
|
@@ -24,29 +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
40
|
"Worker verification procedure",
|
|
45
41
|
),
|
|
46
42
|
"improvement-discovery": (
|
|
47
|
-
"
|
|
48
|
-
"Worker diversity rule",
|
|
49
|
-
"Decision-tree walk",
|
|
43
|
+
"Worker candidate procedure",
|
|
50
44
|
),
|
|
51
45
|
}
|
|
52
46
|
CLARIFICATION_SECTIONS = (
|
|
@@ -156,7 +150,7 @@ def _brief_block(brief_text: str) -> list[str]:
|
|
|
156
150
|
def _profile_block(task_type: str, profile_text: str) -> list[str]:
|
|
157
151
|
section_names = (
|
|
158
152
|
PROFILE_SECTIONS
|
|
159
|
-
+
|
|
153
|
+
+ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE.get(task_type, ())
|
|
160
154
|
)
|
|
161
155
|
profile_sections = _extract_sections(profile_text, section_names)
|
|
162
156
|
profile_bullets = _extract_bullet_sections(profile_text, section_names)
|
|
@@ -24,10 +24,32 @@ from .final_report_paths import (
|
|
|
24
24
|
)
|
|
25
25
|
from .lead_events import LeadEvent, append_lead_event
|
|
26
26
|
from .path_hints import hydrate_active_run_context
|
|
27
|
+
from .report_finalize import (
|
|
28
|
+
STEP_VALIDATE_RUN,
|
|
29
|
+
FinalizeContext,
|
|
30
|
+
FinalizeError,
|
|
31
|
+
project_relative_path as _project_relative_path,
|
|
32
|
+
require_string as _require_string,
|
|
33
|
+
resolve_optional_path as _resolve_optional_path,
|
|
34
|
+
resolve_project_path as _resolve_project_path,
|
|
35
|
+
run_finalize,
|
|
36
|
+
run_seq as _run_seq,
|
|
37
|
+
string_value as _string_value,
|
|
38
|
+
)
|
|
27
39
|
from .wrapper_status import status_path_for_prompt
|
|
28
|
-
from .paths import task_dir
|
|
29
40
|
from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
|
|
30
|
-
from .worker_prompt_contract import
|
|
41
|
+
from .worker_prompt_contract import PromptRecord, validate_initial_prompt_records
|
|
42
|
+
from .worker_prompt_body import (
|
|
43
|
+
analysis_input_lines as _analysis_input_lines,
|
|
44
|
+
analysis_prompt_body as _analysis_prompt_body,
|
|
45
|
+
existing_input_lines as _existing_input_lines,
|
|
46
|
+
instruction_path as _instruction_path,
|
|
47
|
+
mcp_pointer_line as _mcp_pointer_line,
|
|
48
|
+
)
|
|
49
|
+
from .worker_prompt_policy import (
|
|
50
|
+
PromptPlan,
|
|
51
|
+
resolve_prompt_plan_for_manifest,
|
|
52
|
+
)
|
|
31
53
|
|
|
32
54
|
|
|
33
55
|
SUPPORTED_CLI_WORKERS = {
|
|
@@ -39,10 +61,6 @@ BACKEND_MIXED = "mixed"
|
|
|
39
61
|
MAX_WORKER_ATTEMPTS = 2
|
|
40
62
|
REPORT_WRITER_WORKER_ID = "report-writer"
|
|
41
63
|
REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
|
|
42
|
-
ANALYSIS_WORKER_LABELS = {
|
|
43
|
-
"codex": "Codex worker",
|
|
44
|
-
"antigravity": "Antigravity worker",
|
|
45
|
-
}
|
|
46
64
|
|
|
47
65
|
|
|
48
66
|
class DispatchError(Exception):
|
|
@@ -183,7 +201,7 @@ def build_dispatch_plan(
|
|
|
183
201
|
)
|
|
184
202
|
for worker_id in selected_workers
|
|
185
203
|
)
|
|
186
|
-
|
|
204
|
+
_validate_initial_prompts(manifest, workers)
|
|
187
205
|
return DispatchPlan(
|
|
188
206
|
project_root=project_root,
|
|
189
207
|
workspace_root=workspace_root,
|
|
@@ -195,21 +213,18 @@ def build_dispatch_plan(
|
|
|
195
213
|
)
|
|
196
214
|
|
|
197
215
|
|
|
198
|
-
def
|
|
216
|
+
def _validate_initial_prompts(
|
|
199
217
|
manifest: Mapping[str, Any],
|
|
200
218
|
workers: Sequence[WorkerDispatch],
|
|
201
219
|
) -> None:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
prompt_paths = {
|
|
205
|
-
worker.worker_id: worker.prompt_path
|
|
220
|
+
records = [
|
|
221
|
+
PromptRecord(worker.worker_id, "initial", worker.prompt_path)
|
|
206
222
|
for worker in workers
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
errors = validate_final_verification_prompt_paths(prompt_paths)
|
|
223
|
+
]
|
|
224
|
+
errors = validate_initial_prompt_records(manifest=manifest, records=records)
|
|
211
225
|
if errors:
|
|
212
|
-
|
|
226
|
+
task_type = _require_string(manifest, "taskType")
|
|
227
|
+
raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
|
|
213
228
|
|
|
214
229
|
|
|
215
230
|
def dispatch_plan(plan: DispatchPlan) -> int:
|
|
@@ -344,8 +359,6 @@ def _worker_dispatch_record(
|
|
|
344
359
|
}
|
|
345
360
|
|
|
346
361
|
|
|
347
|
-
|
|
348
|
-
|
|
349
362
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
350
363
|
parser = _parser()
|
|
351
364
|
args = parser.parse_args(argv)
|
|
@@ -367,7 +380,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
367
380
|
code = dispatch_plan(plan)
|
|
368
381
|
_print_json({**plan.to_payload(dry_run=False), "exitCode": code})
|
|
369
382
|
return code
|
|
370
|
-
except DispatchError as exc:
|
|
383
|
+
except (DispatchError, FinalizeError) as exc:
|
|
371
384
|
print(f"error: {exc}", file=sys.stderr)
|
|
372
385
|
return 2
|
|
373
386
|
|
|
@@ -460,6 +473,7 @@ def _render_missing_analysis_worker_prompt(
|
|
|
460
473
|
prompt_rel = _worker_prompt_path(manifest, worker_id)
|
|
461
474
|
model = _require_string(worker_state, "modelExecutionValue")
|
|
462
475
|
role = _analysis_pane_role(manifest, active_context, worker_id)
|
|
476
|
+
plan = _initial_prompt_plan(manifest, worker_id)
|
|
463
477
|
lines = _base_prompt_headers(
|
|
464
478
|
project_root=project_root,
|
|
465
479
|
manifest=manifest,
|
|
@@ -469,7 +483,16 @@ def _render_missing_analysis_worker_prompt(
|
|
|
469
483
|
result_rel=result_rel,
|
|
470
484
|
)
|
|
471
485
|
lines.extend(_worktree_headers(manifest, active_context, role))
|
|
472
|
-
lines.extend(
|
|
486
|
+
lines.extend(
|
|
487
|
+
_analysis_prompt_body(
|
|
488
|
+
manifest,
|
|
489
|
+
active_context,
|
|
490
|
+
worker_id,
|
|
491
|
+
model,
|
|
492
|
+
role,
|
|
493
|
+
plan,
|
|
494
|
+
)
|
|
495
|
+
)
|
|
473
496
|
executor_tail = _implementation_executor_tail(
|
|
474
497
|
manifest,
|
|
475
498
|
active_context,
|
|
@@ -614,43 +637,6 @@ def _build_report_writer_dispatch(
|
|
|
614
637
|
)
|
|
615
638
|
|
|
616
639
|
|
|
617
|
-
def _analysis_prompt_body(
|
|
618
|
-
manifest: Mapping[str, Any],
|
|
619
|
-
active_context: Mapping[str, Any],
|
|
620
|
-
worker_id: str,
|
|
621
|
-
model: str,
|
|
622
|
-
role: str,
|
|
623
|
-
) -> list[str]:
|
|
624
|
-
label = ANALYSIS_WORKER_LABELS[worker_id]
|
|
625
|
-
return [
|
|
626
|
-
f"**Model:** {label}, {model}",
|
|
627
|
-
f"**Pane role:** {role}",
|
|
628
|
-
"",
|
|
629
|
-
f"# {label} Dispatch",
|
|
630
|
-
"",
|
|
631
|
-
"## Role",
|
|
632
|
-
(
|
|
633
|
-
f"You are the {label} for okstra cross-verification. "
|
|
634
|
-
"Produce an independent worker result."
|
|
635
|
-
),
|
|
636
|
-
"",
|
|
637
|
-
"## Task",
|
|
638
|
-
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
639
|
-
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
640
|
-
"",
|
|
641
|
-
"## Inputs",
|
|
642
|
-
*_analysis_input_lines(manifest, active_context),
|
|
643
|
-
"",
|
|
644
|
-
_mcp_pointer_line(),
|
|
645
|
-
"",
|
|
646
|
-
"## Output Contract",
|
|
647
|
-
"- Read the Worker Preamble Path end-to-end before analysis.",
|
|
648
|
-
"- Write the worker result to Result Path and no other canonical result path.",
|
|
649
|
-
"- Write the audit sidecar and any tool-failure entries as the preamble requires.",
|
|
650
|
-
"- Cite evidence with file paths and line numbers whenever you make a claim.",
|
|
651
|
-
]
|
|
652
|
-
|
|
653
|
-
|
|
654
640
|
def _report_writer_prompt_body(
|
|
655
641
|
manifest: Mapping[str, Any],
|
|
656
642
|
active_context: Mapping[str, Any],
|
|
@@ -688,13 +674,6 @@ def _report_writer_prompt_body(
|
|
|
688
674
|
]
|
|
689
675
|
|
|
690
676
|
|
|
691
|
-
def _mcp_pointer_line() -> str:
|
|
692
|
-
return (
|
|
693
|
-
'**MCP servers:** follow the task brief\'s "## Available MCP Servers" '
|
|
694
|
-
"section (already in your Required reading)."
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
|
|
698
677
|
def _base_prompt_headers(
|
|
699
678
|
*,
|
|
700
679
|
project_root: Path,
|
|
@@ -710,6 +689,7 @@ def _base_prompt_headers(
|
|
|
710
689
|
prompt_rel=prompt_rel,
|
|
711
690
|
result_rel=result_rel,
|
|
712
691
|
worker_id=worker_id,
|
|
692
|
+
dispatch_kind="initial",
|
|
713
693
|
manifest=manifest,
|
|
714
694
|
active_context=active_context,
|
|
715
695
|
)
|
|
@@ -717,26 +697,18 @@ def _base_prompt_headers(
|
|
|
717
697
|
raise DispatchError(str(exc)) from exc
|
|
718
698
|
|
|
719
699
|
|
|
720
|
-
def
|
|
700
|
+
def _initial_prompt_plan(
|
|
721
701
|
manifest: Mapping[str, Any],
|
|
722
|
-
|
|
723
|
-
) ->
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
manifest,
|
|
727
|
-
|
|
728
|
-
"
|
|
702
|
+
worker_id: str,
|
|
703
|
+
) -> PromptPlan:
|
|
704
|
+
try:
|
|
705
|
+
return resolve_prompt_plan_for_manifest(
|
|
706
|
+
manifest=manifest,
|
|
707
|
+
worker_id=worker_id,
|
|
708
|
+
dispatch_kind="initial",
|
|
729
709
|
)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
("Primary analysis packet", _instruction_path(manifest, active_context, "analysisPacketPath")),
|
|
733
|
-
("Task brief", _instruction_path(manifest, active_context, "taskBriefPath")),
|
|
734
|
-
("Analysis profile", _instruction_path(manifest, active_context, "analysisProfilePath")),
|
|
735
|
-
("Analysis material", _instruction_path(manifest, active_context, "analysisMaterialPath")),
|
|
736
|
-
("Reference expectations", _instruction_path(manifest, active_context, "referenceExpectationsPath")),
|
|
737
|
-
("Clarification response", _instruction_path(manifest, active_context, "clarificationResponsePath")),
|
|
738
|
-
]
|
|
739
|
-
return _existing_input_lines(inputs)
|
|
710
|
+
except ValueError as exc:
|
|
711
|
+
raise DispatchError(str(exc)) from exc
|
|
740
712
|
|
|
741
713
|
|
|
742
714
|
def _report_writer_input_lines(
|
|
@@ -759,11 +731,6 @@ def _report_writer_input_lines(
|
|
|
759
731
|
return lines or ["- No report-writer inputs were recorded in active-run-context."]
|
|
760
732
|
|
|
761
733
|
|
|
762
|
-
def _existing_input_lines(inputs: Sequence[tuple[str, str]]) -> list[str]:
|
|
763
|
-
lines = [f"- {label}: `{path}`" for label, path in inputs if path]
|
|
764
|
-
return lines or ["- No input paths were recorded in active-run-context."]
|
|
765
|
-
|
|
766
|
-
|
|
767
734
|
def _analysis_worker_result_lines(team_state: Mapping[str, Any]) -> list[str]:
|
|
768
735
|
lines = []
|
|
769
736
|
workers = team_state.get("workers")
|
|
@@ -781,19 +748,6 @@ def _analysis_worker_result_lines(team_state: Mapping[str, Any]) -> list[str]:
|
|
|
781
748
|
return lines
|
|
782
749
|
|
|
783
750
|
|
|
784
|
-
def _instruction_path(
|
|
785
|
-
manifest: Mapping[str, Any],
|
|
786
|
-
active_context: Mapping[str, Any],
|
|
787
|
-
key: str,
|
|
788
|
-
) -> str:
|
|
789
|
-
instruction_set = active_context.get("instructionSet")
|
|
790
|
-
if isinstance(instruction_set, Mapping):
|
|
791
|
-
value = _string_value(instruction_set.get(key))
|
|
792
|
-
if value:
|
|
793
|
-
return value
|
|
794
|
-
return _string_value(manifest.get(key))
|
|
795
|
-
|
|
796
|
-
|
|
797
751
|
def _analysis_pane_role(
|
|
798
752
|
manifest: Mapping[str, Any],
|
|
799
753
|
active_context: Mapping[str, Any],
|
|
@@ -875,13 +829,6 @@ def _implementation_executor_tail(
|
|
|
875
829
|
return "\n\n".join(parts)
|
|
876
830
|
|
|
877
831
|
|
|
878
|
-
def _project_relative_path(project_root: Path, path: Path) -> str:
|
|
879
|
-
try:
|
|
880
|
-
return path.relative_to(project_root).as_posix()
|
|
881
|
-
except ValueError:
|
|
882
|
-
return str(path)
|
|
883
|
-
|
|
884
|
-
|
|
885
832
|
def _resolve_report_language(
|
|
886
833
|
project_root: Path,
|
|
887
834
|
manifest: Mapping[str, Any],
|
|
@@ -930,12 +877,6 @@ def _infer_report_language(
|
|
|
930
877
|
return "ko" if hangul_count >= 10 else "en"
|
|
931
878
|
|
|
932
879
|
|
|
933
|
-
def _string_value(value: Any) -> str:
|
|
934
|
-
if not isinstance(value, str):
|
|
935
|
-
return ""
|
|
936
|
-
return value.strip()
|
|
937
|
-
|
|
938
|
-
|
|
939
880
|
def _validate_codex_manifest(manifest: Mapping[str, Any], path: Path) -> None:
|
|
940
881
|
if manifest.get("leadRuntime") != "codex":
|
|
941
882
|
raise DispatchError(f"run manifest is not a Codex lead run: {path}")
|
|
@@ -1121,190 +1062,25 @@ def _post_process_report_writer_result(
|
|
|
1121
1062
|
) -> dict[str, Any]:
|
|
1122
1063
|
if worker.worker_id != REPORT_WRITER_WORKER_ID:
|
|
1123
1064
|
return {"ok": True, "reason": "", "steps": []}
|
|
1124
|
-
steps = []
|
|
1125
1065
|
try:
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
command,
|
|
1134
|
-
cwd=plan.project_root,
|
|
1135
|
-
text=True,
|
|
1136
|
-
capture_output=True,
|
|
1066
|
+
ctx = FinalizeContext.from_manifest(
|
|
1067
|
+
project_root=plan.project_root,
|
|
1068
|
+
workspace_root=plan.workspace_root,
|
|
1069
|
+
manifest_path=plan.manifest_path,
|
|
1070
|
+
team_state_path=plan.team_state_path,
|
|
1071
|
+
data_path=worker.result_path,
|
|
1072
|
+
manifest=plan.manifest,
|
|
1137
1073
|
)
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if result.returncode != 0:
|
|
1141
|
-
return {
|
|
1142
|
-
"ok": False,
|
|
1143
|
-
"reason": f"{name} failed with exit code {result.returncode}",
|
|
1144
|
-
"steps": steps,
|
|
1145
|
-
}
|
|
1146
|
-
return {"ok": True, "reason": "", "steps": steps}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
def _report_post_process_commands(
|
|
1150
|
-
plan: DispatchPlan,
|
|
1151
|
-
worker: WorkerDispatch,
|
|
1152
|
-
) -> list[tuple[str, list[str]]]:
|
|
1153
|
-
markdown_path = _final_report_markdown_path(worker.result_path)
|
|
1154
|
-
commands = [
|
|
1155
|
-
(
|
|
1156
|
-
"token-usage",
|
|
1157
|
-
[
|
|
1158
|
-
sys.executable,
|
|
1159
|
-
str(_resolve_workspace_script(plan.workspace_root, "okstra-token-usage.py")),
|
|
1160
|
-
str(plan.team_state_path),
|
|
1161
|
-
"--project-root",
|
|
1162
|
-
str(plan.project_root),
|
|
1163
|
-
"--write",
|
|
1164
|
-
"--substitute-data",
|
|
1165
|
-
str(worker.result_path),
|
|
1166
|
-
],
|
|
1167
|
-
),
|
|
1168
|
-
(
|
|
1169
|
-
"render-views",
|
|
1170
|
-
[
|
|
1171
|
-
sys.executable,
|
|
1172
|
-
str(_resolve_workspace_script(plan.workspace_root, "okstra-render-report-views.py")),
|
|
1173
|
-
str(markdown_path),
|
|
1174
|
-
"--task-key",
|
|
1175
|
-
_require_string(plan.manifest, "taskKey"),
|
|
1176
|
-
"--task-type",
|
|
1177
|
-
_require_string(plan.manifest, "taskType"),
|
|
1178
|
-
"--seq",
|
|
1179
|
-
_run_seq(plan.manifest),
|
|
1180
|
-
"--source-report",
|
|
1181
|
-
_project_relative_path(plan.project_root, markdown_path),
|
|
1182
|
-
],
|
|
1183
|
-
),
|
|
1184
|
-
(
|
|
1185
|
-
"spawn-followups",
|
|
1186
|
-
[
|
|
1187
|
-
sys.executable,
|
|
1188
|
-
str(_resolve_workspace_script(plan.workspace_root, "okstra-spawn-followups.py")),
|
|
1189
|
-
str(worker.result_path),
|
|
1190
|
-
"--project-root",
|
|
1191
|
-
str(plan.project_root),
|
|
1192
|
-
"--task-group",
|
|
1193
|
-
_task_group(plan.manifest),
|
|
1194
|
-
"--parent-task-key",
|
|
1195
|
-
_require_string(plan.manifest, "taskKey"),
|
|
1196
|
-
],
|
|
1197
|
-
),
|
|
1198
|
-
]
|
|
1199
|
-
commands.append(("validate-run", _validate_run_command(plan, markdown_path)))
|
|
1200
|
-
return commands
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
def _validate_run_command(plan: DispatchPlan, markdown_path: Path) -> list[str]:
|
|
1204
|
-
task_manifest_path = _task_manifest_path(plan.project_root, plan.manifest)
|
|
1205
|
-
command = [
|
|
1206
|
-
sys.executable,
|
|
1207
|
-
str(_resolve_workspace_validator(plan.workspace_root, "validate-run.py")),
|
|
1208
|
-
"--team-state",
|
|
1209
|
-
str(plan.team_state_path),
|
|
1210
|
-
"--report",
|
|
1211
|
-
str(markdown_path),
|
|
1212
|
-
"--run-manifest",
|
|
1213
|
-
str(plan.manifest_path),
|
|
1214
|
-
"--task-manifest",
|
|
1215
|
-
str(task_manifest_path),
|
|
1216
|
-
]
|
|
1217
|
-
final_status_path = _resolve_optional_path(
|
|
1218
|
-
plan.project_root, plan.manifest.get("finalStatusPath")
|
|
1219
|
-
)
|
|
1220
|
-
if final_status_path is not None:
|
|
1221
|
-
command.extend(["--final-status", str(final_status_path)])
|
|
1222
|
-
return command
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
def _task_manifest_path(project_root: Path, manifest: Mapping[str, Any]) -> Path:
|
|
1226
|
-
value = _string_value(manifest.get("taskManifestPath"))
|
|
1227
|
-
if value:
|
|
1228
|
-
return _resolve_project_path(project_root, value)
|
|
1229
|
-
task_root = _string_value(manifest.get("taskRootPath"))
|
|
1230
|
-
if task_root:
|
|
1231
|
-
return _resolve_project_path(project_root, task_root) / "task-manifest.json"
|
|
1232
|
-
return (
|
|
1233
|
-
task_dir(project_root, _task_group(manifest), _task_id(manifest))
|
|
1234
|
-
/ "task-manifest.json"
|
|
1235
|
-
)
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
def _post_process_step_payload(
|
|
1239
|
-
name: str,
|
|
1240
|
-
command: Sequence[str],
|
|
1241
|
-
result: subprocess.CompletedProcess[str],
|
|
1242
|
-
) -> dict[str, Any]:
|
|
1243
|
-
return {
|
|
1244
|
-
"name": name,
|
|
1245
|
-
"command": list(command),
|
|
1246
|
-
"exitCode": result.returncode,
|
|
1247
|
-
"stdoutTail": _tail(result.stdout),
|
|
1248
|
-
"stderrTail": _tail(result.stderr),
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
def _resolve_workspace_script(workspace_root: Path, script_name: str) -> Path:
|
|
1253
|
-
candidates = [
|
|
1254
|
-
workspace_root / "scripts" / script_name,
|
|
1255
|
-
workspace_root / script_name,
|
|
1256
|
-
]
|
|
1257
|
-
for candidate in candidates:
|
|
1258
|
-
if candidate.is_file():
|
|
1259
|
-
return candidate.resolve()
|
|
1260
|
-
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
1261
|
-
raise DispatchError(f"{script_name} not found (searched: {searched})")
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
def _resolve_workspace_validator(workspace_root: Path, validator_name: str) -> Path:
|
|
1265
|
-
candidates = [
|
|
1266
|
-
workspace_root / "validators" / validator_name,
|
|
1267
|
-
workspace_root / validator_name,
|
|
1268
|
-
]
|
|
1269
|
-
for candidate in candidates:
|
|
1270
|
-
if candidate.is_file():
|
|
1271
|
-
return candidate.resolve()
|
|
1272
|
-
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
1273
|
-
raise DispatchError(f"{validator_name} not found (searched: {searched})")
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
def _task_group(manifest: Mapping[str, Any]) -> str:
|
|
1277
|
-
value = _string_value(manifest.get("taskGroup"))
|
|
1278
|
-
if value:
|
|
1279
|
-
return value
|
|
1280
|
-
task_key = _require_string(manifest, "taskKey")
|
|
1281
|
-
colon_parts = task_key.split(":")
|
|
1282
|
-
if len(colon_parts) >= 3 and colon_parts[-2].strip():
|
|
1283
|
-
return colon_parts[-2].strip()
|
|
1284
|
-
slash_parts = task_key.split("/")
|
|
1285
|
-
if len(slash_parts) >= 2 and slash_parts[0].strip():
|
|
1286
|
-
return slash_parts[0].strip()
|
|
1287
|
-
raise DispatchError(f"cannot infer task group from taskKey: {task_key}")
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
def _task_id(manifest: Mapping[str, Any]) -> str:
|
|
1291
|
-
value = _string_value(manifest.get("taskId"))
|
|
1292
|
-
if value:
|
|
1293
|
-
return value
|
|
1294
|
-
task_key = _require_string(manifest, "taskKey")
|
|
1295
|
-
colon_parts = task_key.split(":")
|
|
1296
|
-
if len(colon_parts) >= 3 and colon_parts[-1].strip():
|
|
1297
|
-
return colon_parts[-1].strip()
|
|
1298
|
-
slash_parts = task_key.split("/")
|
|
1299
|
-
if len(slash_parts) >= 2 and slash_parts[-1].strip():
|
|
1300
|
-
return slash_parts[-1].strip()
|
|
1301
|
-
raise DispatchError(f"cannot infer task id from taskKey: {task_key}")
|
|
1074
|
+
except FinalizeError as exc:
|
|
1075
|
+
return {"ok": False, "reason": str(exc), "steps": []}
|
|
1302
1076
|
|
|
1077
|
+
def settle_before(step_name: str) -> None:
|
|
1078
|
+
# validate-run reads team-state; the writer is done by the time its
|
|
1079
|
+
# artifacts pass the earlier steps.
|
|
1080
|
+
if step_name == STEP_VALIDATE_RUN:
|
|
1081
|
+
_set_worker_status(plan.team_state_path, worker.worker_id, "completed", "")
|
|
1303
1082
|
|
|
1304
|
-
|
|
1305
|
-
if len(text) <= limit:
|
|
1306
|
-
return text
|
|
1307
|
-
return text[-limit:]
|
|
1083
|
+
return run_finalize(ctx, before_step=settle_before)
|
|
1308
1084
|
|
|
1309
1085
|
|
|
1310
1086
|
def _append_event(
|
|
@@ -1466,37 +1242,12 @@ def _resolve_required_path(
|
|
|
1466
1242
|
return _resolve_project_path(project_root, _require_string(manifest, key))
|
|
1467
1243
|
|
|
1468
1244
|
|
|
1469
|
-
def _resolve_optional_path(project_root: Path, value: Any) -> Path | None:
|
|
1470
|
-
if not isinstance(value, str) or not value.strip():
|
|
1471
|
-
return None
|
|
1472
|
-
return _resolve_project_path(project_root, value)
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
def _resolve_project_path(project_root: Path, value: str) -> Path:
|
|
1476
|
-
path = Path(value)
|
|
1477
|
-
return path if path.is_absolute() else project_root / path
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
def _require_string(payload: Mapping[str, Any], key: str) -> str:
|
|
1481
|
-
value = payload.get(key)
|
|
1482
|
-
if not isinstance(value, str) or not value.strip():
|
|
1483
|
-
raise DispatchError(f"missing required string field: {key}")
|
|
1484
|
-
return value
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
1245
|
def _string_list(value: Any) -> list[str]:
|
|
1488
1246
|
if not isinstance(value, list):
|
|
1489
1247
|
return []
|
|
1490
1248
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
1491
1249
|
|
|
1492
1250
|
|
|
1493
|
-
def _run_seq(manifest: Mapping[str, Any]) -> str:
|
|
1494
|
-
seqs = manifest.get("runSequencesByCategory")
|
|
1495
|
-
if not isinstance(seqs, Mapping):
|
|
1496
|
-
raise DispatchError("run manifest has no runSequencesByCategory object")
|
|
1497
|
-
return _require_string(seqs, "manifests")
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
1251
|
def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
|
|
1501
1252
|
if exit_code != 0:
|
|
1502
1253
|
return f"wrapper exited with code {exit_code}"
|