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.
Files changed (83) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/docs/architecture/storage-model.md +1 -1
  4. package/docs/architecture.md +6 -4
  5. package/docs/cli.md +13 -9
  6. package/docs/project-structure-overview.md +5 -1
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/prompts/launch.template.md +1 -1
  10. package/runtime/prompts/lead/convergence.md +3 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
  12. package/runtime/prompts/lead/phase-routing.md +1 -1
  13. package/runtime/prompts/lead/team-contract.md +1 -1
  14. package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +3 -2
  15. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +3 -3
  16. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +4 -2
  17. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +3 -3
  18. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +27 -0
  19. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +24 -0
  20. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +24 -0
  21. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +25 -0
  22. package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +20 -1
  23. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +3 -0
  24. package/runtime/python/okstra_ctl/analysis_packet.py +14 -0
  25. package/runtime/python/okstra_ctl/business_flow/engine.py +1 -0
  26. package/runtime/python/okstra_ctl/convergence.py +155 -19
  27. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +3 -2
  28. package/runtime/python/okstra_ctl/convergence_migration.py +10 -1
  29. package/runtime/python/okstra_ctl/convergence_provenance.py +69 -24
  30. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +4 -1
  31. package/runtime/python/okstra_ctl/correction_cli.py +50 -11
  32. package/runtime/python/okstra_ctl/correction_round.py +80 -11
  33. package/runtime/python/okstra_ctl/domain/mcp.py +43 -0
  34. package/runtime/python/okstra_ctl/domain/provider.py +2 -0
  35. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +89 -8
  36. package/runtime/python/okstra_ctl/file_size_cap.py +142 -0
  37. package/runtime/python/okstra_ctl/fix_cycles.py +14 -1
  38. package/runtime/python/okstra_ctl/git_reconcile.py +62 -8
  39. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +3 -6
  40. package/runtime/python/okstra_ctl/manager_cli.py +11 -1
  41. package/runtime/python/okstra_ctl/manager_sync.py +38 -2
  42. package/runtime/python/okstra_ctl/mcp_cli.py +179 -0
  43. package/runtime/python/okstra_ctl/mcp_servers.py +421 -0
  44. package/runtime/python/okstra_ctl/next_phase.py +22 -9
  45. package/runtime/python/okstra_ctl/phases/final_verification/target.py +4 -2
  46. package/runtime/python/okstra_ctl/phases/implementation/entry.py +6 -2
  47. package/runtime/python/okstra_ctl/phases/implementation/host-rules.md +1 -0
  48. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
  49. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -0
  50. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-self-check.md +1 -0
  51. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +3 -2
  52. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +1 -1
  53. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +19 -0
  54. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +14 -0
  55. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
  56. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +37 -0
  57. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +3 -3
  58. package/runtime/python/okstra_ctl/process_group.py +4 -0
  59. package/runtime/python/okstra_ctl/render.py +7 -48
  60. package/runtime/python/okstra_ctl/report_assembly.py +50 -13
  61. package/runtime/python/okstra_ctl/report_html/common.py +3 -1
  62. package/runtime/python/okstra_ctl/report_language.py +4 -5
  63. package/runtime/python/okstra_ctl/report_projections.py +45 -4
  64. package/runtime/python/okstra_ctl/report_synthesis_packet.py +22 -1
  65. package/runtime/python/okstra_ctl/run.py +36 -5
  66. package/runtime/python/okstra_ctl/stage_done.py +37 -3
  67. package/runtime/python/okstra_ctl/stage_reconcile.py +49 -1
  68. package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -3
  69. package/runtime/schemas/final-report-v3.0.schema.json +36 -0
  70. package/runtime/skills/_fragments/wizard-lead-takeover.md +1 -1
  71. package/runtime/skills/_fragments/wizard-relay-protocol.md +2 -2
  72. package/runtime/skills/okstra-discussion/SKILL.md +3 -3
  73. package/runtime/skills/okstra-manager/SKILL.md +1 -0
  74. package/runtime/skills/okstra-run/SKILL.md +4 -3
  75. package/runtime/skills/okstra-setup/SKILL.md +2 -0
  76. package/runtime/skills/okstra-setup/references/project-config.md +34 -0
  77. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  78. package/runtime/templates/reports/html/base.template.html +6 -1
  79. package/runtime/templates/reports/html/i18n/en.json +1 -0
  80. package/runtime/templates/reports/html/i18n/ko.json +1 -0
  81. package/runtime/templates/reports/task-brief.template.md +3 -3
  82. package/runtime/validators/validate-run.py +18 -2
  83. 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
- if GROUPS_BASENAME_RE.match(groups_path.name) is None:
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
- _validate_v2_state_artifact_path(
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
- discarded = discarded_worker_errors(grouped_input, execution_manifest)
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
- dispatches = _dispatch_rows_from_manifest(plan, manifest, args.run_manifest)
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