okstra 0.220.1 → 0.222.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/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +6 -4
- package/docs/cli.md +13 -9
- package/docs/project-structure-overview.md +5 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/convergence.md +3 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/phase-routing.md +1 -1
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +27 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +24 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +24 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +25 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +20 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +3 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +14 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +1 -0
- package/runtime/python/okstra_ctl/convergence.py +155 -19
- package/runtime/python/okstra_ctl/convergence_critic_prompt.py +3 -2
- package/runtime/python/okstra_ctl/convergence_migration.py +10 -1
- package/runtime/python/okstra_ctl/convergence_provenance.py +69 -24
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +4 -1
- package/runtime/python/okstra_ctl/correction_cli.py +50 -11
- package/runtime/python/okstra_ctl/correction_round.py +80 -11
- package/runtime/python/okstra_ctl/domain/mcp.py +43 -0
- package/runtime/python/okstra_ctl/domain/provider.py +2 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +89 -8
- package/runtime/python/okstra_ctl/file_size_cap.py +142 -0
- package/runtime/python/okstra_ctl/fix_cycles.py +14 -1
- package/runtime/python/okstra_ctl/git_reconcile.py +62 -8
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +3 -6
- package/runtime/python/okstra_ctl/manager_cli.py +11 -1
- package/runtime/python/okstra_ctl/manager_sync.py +38 -2
- package/runtime/python/okstra_ctl/mcp_cli.py +179 -0
- package/runtime/python/okstra_ctl/mcp_servers.py +421 -0
- package/runtime/python/okstra_ctl/next_phase.py +22 -9
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +4 -2
- package/runtime/python/okstra_ctl/phases/implementation/entry.py +6 -2
- package/runtime/python/okstra_ctl/phases/implementation/host-rules.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-self-check.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +3 -2
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +19 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +14 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +37 -0
- package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +3 -3
- package/runtime/python/okstra_ctl/process_group.py +4 -0
- package/runtime/python/okstra_ctl/render.py +7 -48
- package/runtime/python/okstra_ctl/report_assembly.py +50 -13
- package/runtime/python/okstra_ctl/report_html/common.py +3 -1
- package/runtime/python/okstra_ctl/report_language.py +4 -5
- package/runtime/python/okstra_ctl/report_projections.py +45 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +22 -1
- package/runtime/python/okstra_ctl/run.py +36 -5
- package/runtime/python/okstra_ctl/stage_done.py +37 -3
- package/runtime/python/okstra_ctl/stage_reconcile.py +49 -1
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -3
- package/runtime/schemas/final-report-v3.0.schema.json +36 -0
- package/runtime/skills/_fragments/wizard-lead-takeover.md +1 -1
- package/runtime/skills/_fragments/wizard-relay-protocol.md +2 -2
- package/runtime/skills/okstra-discussion/SKILL.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +4 -3
- package/runtime/skills/okstra-setup/SKILL.md +2 -0
- package/runtime/skills/okstra-setup/references/project-config.md +34 -0
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/templates/reports/html/base.template.html +6 -1
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/reports/task-brief.template.md +3 -3
- package/runtime/validators/validate-run.py +18 -2
- package/runtime/validators/validate_session_conformance.py +19 -1
|
@@ -5,6 +5,7 @@ import argparse
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
import json
|
|
8
|
+
import re
|
|
8
9
|
from pathlib import Path, PurePosixPath
|
|
9
10
|
import sys
|
|
10
11
|
from typing import Any
|
|
@@ -54,9 +55,16 @@ from .convergence_migration import (
|
|
|
54
55
|
from .convergence_provenance import (
|
|
55
56
|
GROUPS_BASENAME_RE,
|
|
56
57
|
discarded_worker_errors,
|
|
58
|
+
missing_scope_results,
|
|
57
59
|
provenance_errors,
|
|
58
60
|
worker_result_suffix,
|
|
59
61
|
)
|
|
62
|
+
from .correction_round import (
|
|
63
|
+
MAX_CORRECTION_ROUNDS,
|
|
64
|
+
CorrectionRoundError,
|
|
65
|
+
convergence_scope,
|
|
66
|
+
reverify_invocation_in_scope,
|
|
67
|
+
)
|
|
60
68
|
from .convergence_store import (
|
|
61
69
|
load_acceptance_critic_batch,
|
|
62
70
|
load_convergence_critic_batch,
|
|
@@ -152,6 +160,20 @@ def canonical_run_state_artifact(
|
|
|
152
160
|
return expected
|
|
153
161
|
|
|
154
162
|
|
|
163
|
+
def scoped_convergence_state(authority: RunArtifactAuthority, scope: str | None) -> Path:
|
|
164
|
+
"""The final convergence state of one namespace: the manifest's own path, or a correction
|
|
165
|
+
round's `convergence-correction-r<N>-<task-type>-<seq>.json` beside it."""
|
|
166
|
+
initial = canonical_run_state_artifact(
|
|
167
|
+
authority, manifest_field="convergenceStatePath", prefix="convergence",
|
|
168
|
+
label="convergence state",
|
|
169
|
+
)
|
|
170
|
+
if scope is None:
|
|
171
|
+
return initial
|
|
172
|
+
return initial.parent / _canonical_run_artifact_name(
|
|
173
|
+
"convergence", authority.task_type, authority.state_sequence, scope
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
155
177
|
def canonical_run_report_artifact(authority: RunArtifactAuthority) -> Path:
|
|
156
178
|
report_sequence = _manifest_sequence(authority.payload, "reports")
|
|
157
179
|
try:
|
|
@@ -335,7 +357,7 @@ _CLI_EPILOG = r"""Usage:
|
|
|
335
357
|
--results <path>
|
|
336
358
|
okstra convergence critic-prompt --run-manifest <path>
|
|
337
359
|
okstra convergence reverify-prompt --run-manifest <path> --plan <path> \
|
|
338
|
-
--worker <worker-id>
|
|
360
|
+
--worker <worker-id> [--correction-round <N>]
|
|
339
361
|
okstra convergence apply-critic-gaps --work-state <path> --results <path>
|
|
340
362
|
okstra convergence census-audit --run-manifest <path> \
|
|
341
363
|
--result <worker>=<path>... [--gapfill <worker>=<path>...]
|
|
@@ -349,6 +371,11 @@ _CLI_EPILOG = r"""Usage:
|
|
|
349
371
|
okstra convergence example --kind <groups|round-results|critic-results|
|
|
350
372
|
coverage-batch|acceptance-batch>
|
|
351
373
|
|
|
374
|
+
Correction rounds (implementation): `prepare-groups`, `reverify-prompt` and
|
|
375
|
+
`collect-results` take --correction-round <N> to converge round N's
|
|
376
|
+
re-verification in its own namespace; `seed`, `plan-round`, `apply-round` and
|
|
377
|
+
`finalize` read it from the `-correction-r<N>-` state paths they are given.
|
|
378
|
+
|
|
352
379
|
Working lifecycle:
|
|
353
380
|
groups v1.0/v2.0 → work v1.0/v2.0 → round-plan/results v1.0 → final v1.3
|
|
354
381
|
historical final readers: v1.0, v1.1, v1.2
|
|
@@ -358,6 +385,12 @@ does not install or revive the obsolete okstra-convergence skill.
|
|
|
358
385
|
"""
|
|
359
386
|
|
|
360
387
|
|
|
388
|
+
_CORRECTION_ROUND_HELP = (
|
|
389
|
+
"converge correction round N's re-verification in its own namespace "
|
|
390
|
+
"(`correction-r<N>` in every artifact name); omit for the initial round"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
361
394
|
def _parser() -> argparse.ArgumentParser:
|
|
362
395
|
parser = argparse.ArgumentParser(
|
|
363
396
|
epilog=_CLI_EPILOG,
|
|
@@ -407,6 +440,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
407
440
|
)
|
|
408
441
|
prepare.add_argument("--run-manifest", type=Path, required=True)
|
|
409
442
|
prepare.add_argument("--input", type=Path, required=True)
|
|
443
|
+
prepare.add_argument("--correction-round", type=int, metavar="<N>",
|
|
444
|
+
help=_CORRECTION_ROUND_HELP)
|
|
410
445
|
|
|
411
446
|
plan = subparsers.add_parser("plan-round", help="write the next dispatch plan")
|
|
412
447
|
plan.add_argument("--work-state", type=Path, required=True)
|
|
@@ -426,6 +461,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
426
461
|
"attempts, and a still-`started` attempt refuses "
|
|
427
462
|
"the collect until `okstra team await` closes it")
|
|
428
463
|
collect.add_argument("--output", type=Path, required=True)
|
|
464
|
+
collect.add_argument("--correction-round", type=int, metavar="<N>",
|
|
465
|
+
help=_CORRECTION_ROUND_HELP)
|
|
429
466
|
|
|
430
467
|
apply = subparsers.add_parser("apply-round", help="apply structured round results")
|
|
431
468
|
apply.add_argument("--work-state", type=Path, required=True)
|
|
@@ -464,6 +501,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
464
501
|
reverify_prompt.add_argument("--run-manifest", type=Path, required=True)
|
|
465
502
|
reverify_prompt.add_argument("--plan", type=Path, required=True)
|
|
466
503
|
reverify_prompt.add_argument("--worker", required=True)
|
|
504
|
+
reverify_prompt.add_argument("--correction-round", type=int, metavar="<N>",
|
|
505
|
+
help=_CORRECTION_ROUND_HELP)
|
|
467
506
|
|
|
468
507
|
critic_verify_prompt = subparsers.add_parser(
|
|
469
508
|
"critic-verify-prompt",
|
|
@@ -676,7 +715,8 @@ def _seeded_groups_provenance_errors(groups_path: Path, document: dict) -> list[
|
|
|
676
715
|
file, and the same claims are re-checked at Phase 7 by `validate-run`,
|
|
677
716
|
where a bad `itemId` surfaces hours later as a blocked approval.
|
|
678
717
|
"""
|
|
679
|
-
|
|
718
|
+
match = GROUPS_BASENAME_RE.match(groups_path.name)
|
|
719
|
+
if match is None:
|
|
680
720
|
return []
|
|
681
721
|
run_dir = groups_path.parent.parent
|
|
682
722
|
worker_results_dir = run_dir / "worker-results"
|
|
@@ -692,6 +732,7 @@ def _seeded_groups_provenance_errors(groups_path: Path, document: dict) -> list[
|
|
|
692
732
|
worker_results_dir=worker_results_dir,
|
|
693
733
|
suffix=suffix,
|
|
694
734
|
groups_label=groups_path.name,
|
|
735
|
+
scope=match.group("scope"),
|
|
695
736
|
)
|
|
696
737
|
|
|
697
738
|
|
|
@@ -701,12 +742,14 @@ def _seed_execution_manifest(
|
|
|
701
742
|
manifest_path: Path | None,
|
|
702
743
|
work_state_path: Path,
|
|
703
744
|
final_state_path: Path,
|
|
704
|
-
) -> ExecutionManifest | None:
|
|
745
|
+
) -> tuple[ExecutionManifest | None, str | None]:
|
|
746
|
+
"""The run's execution manifest and the convergence namespace the state paths name."""
|
|
705
747
|
if manifest_path is None:
|
|
706
|
-
return None
|
|
748
|
+
return None, None
|
|
707
749
|
manifest, authority = read_execution_manifest_view(manifest_path)
|
|
750
|
+
scope = None
|
|
708
751
|
if grouped_input.get("schemaVersion") == "2.0":
|
|
709
|
-
_validate_v2_seed_authority(
|
|
752
|
+
scope = _validate_v2_seed_authority(
|
|
710
753
|
groups_path,
|
|
711
754
|
grouped_input,
|
|
712
755
|
manifest_path,
|
|
@@ -715,7 +758,7 @@ def _seed_execution_manifest(
|
|
|
715
758
|
final_state_path,
|
|
716
759
|
execution_manifest=manifest,
|
|
717
760
|
)
|
|
718
|
-
return manifest
|
|
761
|
+
return manifest, scope
|
|
719
762
|
|
|
720
763
|
|
|
721
764
|
def _validate_v2_seed_authority(
|
|
@@ -727,7 +770,7 @@ def _validate_v2_seed_authority(
|
|
|
727
770
|
final_state_path: Path,
|
|
728
771
|
*,
|
|
729
772
|
execution_manifest: ExecutionManifest,
|
|
730
|
-
) -> None:
|
|
773
|
+
) -> str | None:
|
|
731
774
|
validated = _validated_v2_work_authority(
|
|
732
775
|
work_state_path,
|
|
733
776
|
grouped_input,
|
|
@@ -736,11 +779,12 @@ def _validate_v2_seed_authority(
|
|
|
736
779
|
)
|
|
737
780
|
if validated is None:
|
|
738
781
|
raise ConvergenceContractError("v2 convergence groups require v2 work authority")
|
|
739
|
-
_manifest, run_dir, task_type, state_sequence = validated
|
|
782
|
+
_manifest, run_dir, task_type, state_sequence, scope = validated
|
|
740
783
|
expected_groups_name = _canonical_run_artifact_name(
|
|
741
784
|
"convergence-groups",
|
|
742
785
|
task_type,
|
|
743
786
|
state_sequence,
|
|
787
|
+
scope,
|
|
744
788
|
)
|
|
745
789
|
if groups_path.name != expected_groups_name:
|
|
746
790
|
raise ConvergenceContractError(
|
|
@@ -760,7 +804,9 @@ def _validate_v2_seed_authority(
|
|
|
760
804
|
state_sequence=state_sequence,
|
|
761
805
|
prefix="convergence",
|
|
762
806
|
label="state output",
|
|
807
|
+
scope=scope,
|
|
763
808
|
)
|
|
809
|
+
return scope
|
|
764
810
|
|
|
765
811
|
|
|
766
812
|
def _validated_v2_work_authority(
|
|
@@ -773,7 +819,7 @@ def _validated_v2_work_authority(
|
|
|
773
819
|
Mapping[str, Any],
|
|
774
820
|
] | None = None,
|
|
775
821
|
path_label: str = "working state",
|
|
776
|
-
) -> tuple[ExecutionManifest, Path, str, str] | None:
|
|
822
|
+
) -> tuple[ExecutionManifest, Path, str, str, str | None] | None:
|
|
777
823
|
if state.get("schemaVersion") != "2.0":
|
|
778
824
|
return None
|
|
779
825
|
if state.get("executionIdentityVersion") != 2:
|
|
@@ -828,7 +874,7 @@ def _validated_v2_work_authority(
|
|
|
828
874
|
manifest_path,
|
|
829
875
|
canonical_run_ref,
|
|
830
876
|
)
|
|
831
|
-
|
|
877
|
+
scope = _state_artifact_scope(
|
|
832
878
|
work_state_path,
|
|
833
879
|
state_dir=run_dir / "state",
|
|
834
880
|
task_type=task_type,
|
|
@@ -836,7 +882,7 @@ def _validated_v2_work_authority(
|
|
|
836
882
|
prefix="convergence-work",
|
|
837
883
|
label=path_label,
|
|
838
884
|
)
|
|
839
|
-
return execution_manifest, run_dir, task_type, state_sequence
|
|
885
|
+
return execution_manifest, run_dir, task_type, state_sequence, scope
|
|
840
886
|
|
|
841
887
|
|
|
842
888
|
def _project_root_for_v2_work_state(
|
|
@@ -944,6 +990,7 @@ def _canonical_run_artifact_name(
|
|
|
944
990
|
prefix: str,
|
|
945
991
|
task_type: str,
|
|
946
992
|
sequence: str,
|
|
993
|
+
scope: str | None = None,
|
|
947
994
|
) -> str:
|
|
948
995
|
try:
|
|
949
996
|
sequence_number = int(sequence)
|
|
@@ -967,7 +1014,7 @@ def _canonical_run_artifact_name(
|
|
|
967
1014
|
"run manifest sequence is not canonical"
|
|
968
1015
|
)
|
|
969
1016
|
suffix = probe.removeprefix("run-manifest").removesuffix(".json")
|
|
970
|
-
return f"{prefix}{suffix}.json"
|
|
1017
|
+
return f"{prefix}{'-' + scope if scope else ''}{suffix}.json"
|
|
971
1018
|
|
|
972
1019
|
|
|
973
1020
|
def _manifest_run_directory(
|
|
@@ -1007,11 +1054,13 @@ def _validate_v2_state_artifact_path(
|
|
|
1007
1054
|
state_sequence: str,
|
|
1008
1055
|
prefix: str,
|
|
1009
1056
|
label: str,
|
|
1057
|
+
scope: str | None = None,
|
|
1010
1058
|
) -> None:
|
|
1011
1059
|
expected_path = state_dir / _canonical_run_artifact_name(
|
|
1012
1060
|
prefix,
|
|
1013
1061
|
task_type,
|
|
1014
1062
|
state_sequence,
|
|
1063
|
+
scope,
|
|
1015
1064
|
)
|
|
1016
1065
|
if (
|
|
1017
1066
|
supplied_path.absolute() != supplied_path.resolve()
|
|
@@ -1022,6 +1071,28 @@ def _validate_v2_state_artifact_path(
|
|
|
1022
1071
|
)
|
|
1023
1072
|
|
|
1024
1073
|
|
|
1074
|
+
def _state_artifact_scope(
|
|
1075
|
+
supplied_path: Path,
|
|
1076
|
+
*,
|
|
1077
|
+
state_dir: Path,
|
|
1078
|
+
task_type: str,
|
|
1079
|
+
state_sequence: str,
|
|
1080
|
+
prefix: str,
|
|
1081
|
+
label: str,
|
|
1082
|
+
) -> str | None:
|
|
1083
|
+
"""The namespace a canonical state path names: the initial one or a correction round's."""
|
|
1084
|
+
scopes = [None, *(convergence_scope(n) for n in range(1, MAX_CORRECTION_ROUNDS + 1))]
|
|
1085
|
+
for scope in scopes:
|
|
1086
|
+
name = _canonical_run_artifact_name(prefix, task_type, state_sequence, scope)
|
|
1087
|
+
if supplied_path.name == name:
|
|
1088
|
+
_validate_v2_state_artifact_path(
|
|
1089
|
+
supplied_path, state_dir=state_dir, task_type=task_type,
|
|
1090
|
+
state_sequence=state_sequence, prefix=prefix, label=label, scope=scope,
|
|
1091
|
+
)
|
|
1092
|
+
return scope
|
|
1093
|
+
raise ConvergenceContractError(f"{label} path does not match run authority")
|
|
1094
|
+
|
|
1095
|
+
|
|
1025
1096
|
def _manifest_project_root(authority: dict[str, Any]) -> Path:
|
|
1026
1097
|
value = authority.get("projectRoot")
|
|
1027
1098
|
if not isinstance(value, str) or not value.strip():
|
|
@@ -1075,7 +1146,7 @@ def _canonical_project_artifact_path(
|
|
|
1075
1146
|
|
|
1076
1147
|
def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
1077
1148
|
grouped_input = load_owned_json_object(args.groups)
|
|
1078
|
-
execution_manifest = _seed_execution_manifest(
|
|
1149
|
+
execution_manifest, scope = _seed_execution_manifest(
|
|
1079
1150
|
args.groups,
|
|
1080
1151
|
grouped_input,
|
|
1081
1152
|
args.run_manifest,
|
|
@@ -1087,7 +1158,10 @@ def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1087
1158
|
raise ConvergenceContractError(
|
|
1088
1159
|
"grouping cites source items no worker reported: " + "; ".join(provenance)
|
|
1089
1160
|
)
|
|
1090
|
-
|
|
1161
|
+
unconverged = missing_scope_results(grouped_input, execution_manifest, scope)
|
|
1162
|
+
if unconverged:
|
|
1163
|
+
raise ConvergenceContractError("; ".join(unconverged))
|
|
1164
|
+
discarded = discarded_worker_errors(grouped_input, execution_manifest, scope)
|
|
1091
1165
|
if discarded:
|
|
1092
1166
|
raise ConvergenceContractError(
|
|
1093
1167
|
"grouping cites workers whose result was discarded: " + "; ".join(discarded)
|
|
@@ -1134,6 +1208,7 @@ def _plan_round(
|
|
|
1134
1208
|
if errors:
|
|
1135
1209
|
raise ConvergenceContractError("invalid working state: " + "; ".join(errors))
|
|
1136
1210
|
plan = plan_next_round(state)
|
|
1211
|
+
_refuse_dispatched_plan_overwrite(args.work_state, state, args.plan, plan)
|
|
1137
1212
|
write_json_atomic(args.plan, plan)
|
|
1138
1213
|
# 리드의 `phase-5.5-convergence round=<N> queue=<count>` 줄이 이 두 값을 그대로
|
|
1139
1214
|
# 옮긴다. 리드가 계획 파일을 세면 `queueFindingIds` 와 혼동해 queue=0 을 적었다.
|
|
@@ -1143,6 +1218,46 @@ def _plan_round(
|
|
|
1143
1218
|
}
|
|
1144
1219
|
|
|
1145
1220
|
|
|
1221
|
+
_PLAN_SCOPE_RE = re.compile(r"-plan-(correction-r[1-9][0-9]*)-")
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _refuse_dispatched_plan_overwrite(
|
|
1225
|
+
work_state_path: Path, state: Mapping[str, Any], plan_path: Path, plan: Mapping[str, Any],
|
|
1226
|
+
) -> None:
|
|
1227
|
+
"""A dispatched round plan is the assignment record its reverify votes are collected against.
|
|
1228
|
+
|
|
1229
|
+
Overwriting it with a different plan (dev-11378: a round-1 dispatch plan replaced by a
|
|
1230
|
+
finalize plan of another convergence) leaves those votes with no plan to match.
|
|
1231
|
+
"""
|
|
1232
|
+
if not plan_path.is_file():
|
|
1233
|
+
return
|
|
1234
|
+
existing = load_owned_json_object(plan_path)
|
|
1235
|
+
if existing == plan or existing.get("action") != "dispatch":
|
|
1236
|
+
return
|
|
1237
|
+
authority = _validated_v2_work_authority(work_state_path, state)
|
|
1238
|
+
if authority is None:
|
|
1239
|
+
return
|
|
1240
|
+
manifest = authority[0]
|
|
1241
|
+
round_number = existing.get("round")
|
|
1242
|
+
if not isinstance(round_number, int):
|
|
1243
|
+
return
|
|
1244
|
+
# 덮어쓰려는 계획의 범위는 새 계획이 아니라 그 파일 이름이 정한다.
|
|
1245
|
+
named = _PLAN_SCOPE_RE.search(plan_path.name)
|
|
1246
|
+
scope = named.group(1) if named else None
|
|
1247
|
+
dispatched = sorted(
|
|
1248
|
+
invocation.invocation_ref for invocation in manifest.invocations
|
|
1249
|
+
if invocation.dispatch_kind == f"reverify-r{round_number}"
|
|
1250
|
+
and reverify_invocation_in_scope(invocation.invocation_ref, scope, round_number)
|
|
1251
|
+
)
|
|
1252
|
+
if dispatched:
|
|
1253
|
+
raise ConvergenceContractError(
|
|
1254
|
+
f"{plan_path.name} is round {round_number}'s dispatched plan "
|
|
1255
|
+
f"({', '.join(dispatched)}); write the new plan to another path — a "
|
|
1256
|
+
"correction round's re-convergence names its plans "
|
|
1257
|
+
"`convergence-round-<k>-plan-correction-r<N>-<task-type>-<seq>.json`"
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
|
|
1146
1261
|
def _apply_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
1147
1262
|
state = load_owned_json_object(args.work_state)
|
|
1148
1263
|
_validated_v2_work_authority(args.work_state, state)
|
|
@@ -1178,7 +1293,7 @@ def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1178
1293
|
state = load_owned_json_object(args.work_state)
|
|
1179
1294
|
authority = _validated_v2_work_authority(args.work_state, state)
|
|
1180
1295
|
if authority is not None:
|
|
1181
|
-
_manifest, run_dir, task_type, state_sequence = authority
|
|
1296
|
+
_manifest, run_dir, task_type, state_sequence, scope = authority
|
|
1182
1297
|
_validate_v2_state_artifact_path(
|
|
1183
1298
|
args.output,
|
|
1184
1299
|
state_dir=run_dir / "state",
|
|
@@ -1186,6 +1301,7 @@ def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1186
1301
|
state_sequence=state_sequence,
|
|
1187
1302
|
prefix="convergence",
|
|
1188
1303
|
label="final state",
|
|
1304
|
+
scope=scope,
|
|
1189
1305
|
)
|
|
1190
1306
|
final = finalize_working_state(state)
|
|
1191
1307
|
migration = state.get("migration")
|
|
@@ -1301,6 +1417,7 @@ def _dispatch_rows_from_manifest(
|
|
|
1301
1417
|
plan: Mapping[str, Any],
|
|
1302
1418
|
manifest: Any,
|
|
1303
1419
|
run_manifest_path: Path,
|
|
1420
|
+
scope: str | None = None,
|
|
1304
1421
|
) -> list[dict[str, Any]]:
|
|
1305
1422
|
"""`dispatches[]` 를 execution manifest 의 attempt 원장에서 도출한다.
|
|
1306
1423
|
|
|
@@ -1338,6 +1455,7 @@ def _dispatch_rows_from_manifest(
|
|
|
1338
1455
|
matches = [
|
|
1339
1456
|
invocation for invocation in getattr(manifest, "invocations", ())
|
|
1340
1457
|
if invocation.dispatch_kind == kind
|
|
1458
|
+
and reverify_invocation_in_scope(invocation.invocation_ref, scope, round_number)
|
|
1341
1459
|
and (
|
|
1342
1460
|
source_roles.get(invocation.role_execution_ref) == role_ref
|
|
1343
1461
|
if isinstance(role_ref, str) and role_ref
|
|
@@ -1432,7 +1550,11 @@ def _collect_results(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1432
1550
|
"collect-results reads dispatch outcomes from a v2 execution manifest; "
|
|
1433
1551
|
"a legacy v1 roster has no attempt ledger to read"
|
|
1434
1552
|
)
|
|
1435
|
-
|
|
1553
|
+
try:
|
|
1554
|
+
scope = convergence_scope(args.correction_round)
|
|
1555
|
+
except CorrectionRoundError as exc:
|
|
1556
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
1557
|
+
dispatches = _dispatch_rows_from_manifest(plan, manifest, args.run_manifest, scope)
|
|
1436
1558
|
votes = _worker_votes(args.result, planned)
|
|
1437
1559
|
returned_without_completion = sorted(
|
|
1438
1560
|
worker for worker in votes
|
|
@@ -1700,6 +1822,18 @@ def _critic_prompt(args: argparse.Namespace) -> str:
|
|
|
1700
1822
|
)
|
|
1701
1823
|
|
|
1702
1824
|
|
|
1825
|
+
def _correction_scope(
|
|
1826
|
+
args: argparse.Namespace, authority: RunArtifactAuthority,
|
|
1827
|
+
) -> str | None:
|
|
1828
|
+
round_number = getattr(args, "correction_round", None)
|
|
1829
|
+
if round_number is not None and authority.task_type != "implementation":
|
|
1830
|
+
raise ConvergenceContractError("correction rounds exist only in implementation runs")
|
|
1831
|
+
try:
|
|
1832
|
+
return convergence_scope(round_number)
|
|
1833
|
+
except CorrectionRoundError as exc:
|
|
1834
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
1835
|
+
|
|
1836
|
+
|
|
1703
1837
|
def _prepare_groups(args: argparse.Namespace) -> tuple[str, Path]:
|
|
1704
1838
|
authority = validated_run_authority(args.run_manifest)
|
|
1705
1839
|
manifest = authority.payload
|
|
@@ -1708,7 +1842,8 @@ def _prepare_groups(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1708
1842
|
source["worker"] for group in groups for source in group["sourceItems"]
|
|
1709
1843
|
))
|
|
1710
1844
|
output = authority.run_dir / "state" / _canonical_run_artifact_name(
|
|
1711
|
-
"convergence-groups", authority.task_type, authority.state_sequence
|
|
1845
|
+
"convergence-groups", authority.task_type, authority.state_sequence,
|
|
1846
|
+
_correction_scope(args, authority),
|
|
1712
1847
|
)
|
|
1713
1848
|
config = _convergence_config(authority)
|
|
1714
1849
|
workers = [_group_worker(manifest, worker) for worker in worker_ids]
|
|
@@ -1741,8 +1876,9 @@ def _prepare_groups(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
1741
1876
|
def _reverify_prompt(args: argparse.Namespace) -> str:
|
|
1742
1877
|
"""한 워커의 reverify 지시문 본문. 입력은 매니페스트·라운드 계획·워커 id."""
|
|
1743
1878
|
authority = validated_run_authority(args.run_manifest)
|
|
1879
|
+
scope = _correction_scope(args, authority)
|
|
1744
1880
|
groups_path = authority.run_dir / "state" / _canonical_run_artifact_name(
|
|
1745
|
-
"convergence-groups", authority.task_type, authority.state_sequence
|
|
1881
|
+
"convergence-groups", authority.task_type, authority.state_sequence, scope
|
|
1746
1882
|
)
|
|
1747
1883
|
if not groups_path.is_file():
|
|
1748
1884
|
raise ConvergenceContractError(
|
|
@@ -1764,7 +1900,7 @@ def _reverify_prompt(args: argparse.Namespace) -> str:
|
|
|
1764
1900
|
round_number=round_number,
|
|
1765
1901
|
findings=reverify_findings(
|
|
1766
1902
|
groups, plan, args.worker,
|
|
1767
|
-
project_root=authority.project_root, run_dir=authority.run_dir,
|
|
1903
|
+
project_root=authority.project_root, run_dir=authority.run_dir, scope=scope,
|
|
1768
1904
|
),
|
|
1769
1905
|
)
|
|
1770
1906
|
|
|
@@ -129,10 +129,11 @@ def analyser_results(
|
|
|
129
129
|
*,
|
|
130
130
|
project_root: Path,
|
|
131
131
|
run_dir: Path,
|
|
132
|
+
scope: str | None = None,
|
|
132
133
|
) -> list[AnalyserResult]:
|
|
133
134
|
"""분석 audience 워커마다 결과 파일 경로와 finding id 를 짝지어 돌려준다.
|
|
134
135
|
|
|
135
|
-
출처 검사와 같은 실행
|
|
136
|
+
출처 검사와 같은 실행 원장·수렴 범위를 사용해야 grouping 이 인용한 결과를 재검증한다.
|
|
136
137
|
"""
|
|
137
138
|
suffix = worker_result_suffix(Path(run_dir), groups)
|
|
138
139
|
if suffix is None:
|
|
@@ -140,7 +141,7 @@ def analyser_results(
|
|
|
140
141
|
"cannot resolve the worker-result suffix from the grouping's "
|
|
141
142
|
"runManifestPath"
|
|
142
143
|
)
|
|
143
|
-
paths = worker_result_paths(Path(run_dir), groups, suffix)
|
|
144
|
+
paths = worker_result_paths(Path(run_dir), groups, suffix, scope)
|
|
144
145
|
by_worker = _findings_by_worker(groups)
|
|
145
146
|
return [
|
|
146
147
|
AnalyserResult(
|
|
@@ -45,6 +45,16 @@ def decide_seed_action(
|
|
|
45
45
|
task_key = grouped_input.get("taskKey")
|
|
46
46
|
digest = grouped_input_digest(grouped_input)
|
|
47
47
|
final_status, final_value = _inspect_final(final_state_path)
|
|
48
|
+
work_status, work_value = _inspect_work(work_state_path)
|
|
49
|
+
if restart_from_round0 and work_status != "invalid":
|
|
50
|
+
# 되살릴 무효 상태가 없는데 재시작을 요청받으면 조용히 재사용·재개하던 것이
|
|
51
|
+
# 정정 라운드 재수렴을 초기 final 재사용으로 끝냈다(dev-11378).
|
|
52
|
+
raise ConvergenceContractError(
|
|
53
|
+
"--restart-from-round0 recovers an invalid working state, and this working "
|
|
54
|
+
f"state is {work_status}; to converge a correction round's re-verification, "
|
|
55
|
+
"prepare its own namespace with `okstra convergence prepare-groups "
|
|
56
|
+
"--correction-round <N>` and seed those paths"
|
|
57
|
+
)
|
|
48
58
|
if final_status == "terminal":
|
|
49
59
|
assert final_value is not None
|
|
50
60
|
version = final_value.get("schemaVersion")
|
|
@@ -55,7 +65,6 @@ def decide_seed_action(
|
|
|
55
65
|
)
|
|
56
66
|
return SeedDecision("reuse-final", None, None, reason)
|
|
57
67
|
|
|
58
|
-
work_status, work_value = _inspect_work(work_state_path)
|
|
59
68
|
invalid_final = final_status == "invalid"
|
|
60
69
|
if work_status == "valid":
|
|
61
70
|
assert work_value is not None
|