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
@@ -14,6 +14,7 @@ from typing import Any, Mapping
14
14
  from okstra_token_usage.blocks import accounting_workers
15
15
 
16
16
  from .analysis_packet import reference_source_extracts
17
+ from .correction_round import correction_convergence_states
17
18
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
18
19
  from .phases.implementation_option_selection.authoring import (
19
20
  requirement_instructions as selection_requirement_instructions,
@@ -251,6 +252,17 @@ class ReportSynthesisPacket:
251
252
  ]
252
253
  if self.task_type == "implementation-planning":
253
254
  return self._planning_requirement_instructions()
255
+ if self.task_type == "implementation" and any(
256
+ source.label.startswith(f"{CONVERGENCE_STATE_LABEL} (correction-r")
257
+ for source in self.sources
258
+ ):
259
+ return [
260
+ "Each `Convergence state (correction-r<N>)` source is correction round N's "
261
+ "re-convergence of the verifiers' re-verification; the plain `Convergence "
262
+ "state` is the initial round's. Describe the stage's open findings from the "
263
+ "last round's re-convergence, not the initial one; report what each round "
264
+ "fixed through `implementation.correctionRounds`.",
265
+ ]
254
266
  if self.task_type != "implementation-option-selection":
255
267
  return []
256
268
  return selection_requirement_instructions(
@@ -371,6 +383,7 @@ _SOURCE_FIELDS = (
371
383
  ("Final report schema", "orchestrator", "instructionSet", "finalReportSchemaPath", True),
372
384
  ("Convergence state", "convergence", "run", "convergenceStatePath", True),
373
385
  )
386
+ CONVERGENCE_STATE_LABEL = "Convergence state"
374
387
 
375
388
 
376
389
  _READ_CHUNK_BYTES = 16_000
@@ -514,7 +527,7 @@ def _source_markdown(
514
527
  brief = next((item.content.strip() for item in sources if item.label == "Task brief"), "")
515
528
  if len(brief.encode("utf-8")) >= _SHARED_TEXT_MIN_BYTES:
516
529
  content = content.replace(brief, 'Read the complete "Source: Task brief" above.')
517
- if source.label == "Convergence state":
530
+ if source.label.startswith(CONVERGENCE_STATE_LABEL):
518
531
  content = _convergence_view(content)
519
532
  if content != source.content.rstrip("\n"):
520
533
  view = [
@@ -815,6 +828,14 @@ def _source_specs(
815
828
  path = str(Path(instruction_set) / "final-report-schema.json")
816
829
  if path or required:
817
830
  specs.append((label, owner, path, required))
831
+ if label == CONVERGENCE_STATE_LABEL and path:
832
+ specs.extend(
833
+ (f"{CONVERGENCE_STATE_LABEL} ({scope})", owner,
834
+ _relative(project_root, round_final), True)
835
+ for scope, round_final in correction_convergence_states(
836
+ _resolve(project_root, path)
837
+ )
838
+ )
818
839
  if carried_source:
819
840
  specs.append((CARRIED_STAGES_LABEL, "orchestrator", carried_source, True))
820
841
  specs.extend(_user_response_specs(project_root, narrative_path))
@@ -23,6 +23,7 @@ from okstra_ctl.phases.implementation_planning.validation import validate_select
23
23
  from .phases.technical_verification.entry import (
24
24
  prepare_technical_verification_input,
25
25
  materialize_technical_input_packet,
26
+ technical_evidence_packet_block,
26
27
  )
27
28
 
28
29
  from argparse import (
@@ -55,6 +56,7 @@ from .phases.implementation_planning.entry import (
55
56
  )
56
57
  from . import fix_cycles
57
58
  from .analysis_packet import build_analysis_packet
59
+ from .mcp_servers import NO_MCP_SERVERS_LINE, prepare_mcp
58
60
  from .coverage_census import (
59
61
  CensusError,
60
62
  census_filename,
@@ -3051,6 +3053,16 @@ def _write_instruction_set_sources(
3051
3053
  ),
3052
3054
  coverage_census_text=coverage_census_text,
3053
3055
  has_selected_direction=(instruction_set / "selected-direction.json").is_file(),
3056
+ mcp_servers_text=(
3057
+ "" if ctx.get("AVAILABLE_MCP_SERVERS", NO_MCP_SERVERS_LINE) == NO_MCP_SERVERS_LINE
3058
+ else ctx["AVAILABLE_MCP_SERVERS"]
3059
+ ),
3060
+ technical_evidence_text=(
3061
+ technical_evidence_packet_block(
3062
+ Path(inp.clarification_response_path), Path(ctx["PROJECT_ROOT"]),
3063
+ )
3064
+ if inp.clarification_response_path else ""
3065
+ ),
3054
3066
  )
3055
3067
  if inp.task_type == "technical-verification":
3056
3068
  packet += materialize_technical_input_packet(
@@ -3358,10 +3370,11 @@ def _maybe_open_fix_cycle(inp: PrepareInputs, task_root: Path, existing: dict,
3358
3370
  f"{', '.join(fix_cycles.FIX_CYCLE_ENTRY_PHASES)})에서만 허용됩니다: "
3359
3371
  f"{inp.task_type}")
3360
3372
  workflow = existing.get("workflow") or {}
3361
- if workflow.get("lastCompletedPhase") != "release-handoff":
3373
+ if not fix_cycles.delivery_completed(workflow):
3362
3374
  raise PrepareError(
3363
- "--fix-cycle yes 는 release-handoff 까지 완료된 task 에만 허용됩니다 "
3364
- "(workflow.lastCompletedPhase 확인)")
3375
+ "--fix-cycle yes 는 release-handoff 로 전달까지 끝난 task 에만 허용됩니다 "
3376
+ "(workflow.lastCompletedPhase 가 release-handoff 이고, skip·미전달 stage 로 "
3377
+ "다음 phase 가 release-handoff 로 남아 있지 않아야 함)")
3365
3378
  brief_text = Path(inp.brief_path).read_text(encoding="utf-8")
3366
3379
  fix_cycles.append_opened(
3367
3380
  task_root, target_report=str(existing.get("latestReportRecordPath", "")),
@@ -3373,7 +3386,8 @@ def _record_fix_cycle_events(inp: PrepareInputs, ctx: dict) -> str:
3373
3386
  """fix-cycles.jsonl 의 lazy-close → opened → run 기록. cycle id 반환.
3374
3387
 
3375
3388
  - lazy-close: open cycle 에 release-handoff run 이 부착됐고 디스크 manifest 의
3376
- lastCompletedPhase 가 release-handoff 면 닫는다. manifest 는 prepare 가
3389
+ lastCompletedPhase 가 release-handoff 면 닫는다. 다음 phase 포인터가 다시
3390
+ release-handoff 를 가리키면(skip·미전달 stage) 닫지 않는다. manifest 는 prepare 가
3377
3391
  이번 run 으로 재작성하기 *전* 값이어야 하므로 finalize 직전에 호출한다.
3378
3392
  - opened: --fix-cycle yes 일 때만. 완료 task + entry phase 미충족이면
3379
3393
  PrepareError.
@@ -3386,7 +3400,7 @@ def _record_fix_cycle_events(inp: PrepareInputs, ctx: dict) -> str:
3386
3400
 
3387
3401
  open_c = fix_cycles.open_cycle(fix_cycles.read_rows(task_root))
3388
3402
 
3389
- if open_c and workflow.get("lastCompletedPhase") == "release-handoff":
3403
+ if open_c and fix_cycles.delivery_completed(workflow):
3390
3404
  rows = fix_cycles.read_rows(task_root)
3391
3405
  handoff_attached = any(
3392
3406
  r.get("event") == "run" and r.get("cycle") == open_c["cycle"]
@@ -4055,6 +4069,20 @@ def _prepare_user_authorization(inp: PrepareInputs) -> dict:
4055
4069
  return record
4056
4070
 
4057
4071
 
4072
+ def _prepare_mcp_block(
4073
+ project_root: Path, plan: AssignmentPlan, brief_path: Path,
4074
+ ) -> str:
4075
+ brief_text = brief_path.read_text(encoding="utf-8") if brief_path.is_file() else ""
4076
+ prepared = prepare_mcp(
4077
+ project_root,
4078
+ (assignment.provider_id for assignment in plan.assignments),
4079
+ brief_text,
4080
+ )
4081
+ for warning in prepared.warnings:
4082
+ print(f"prepare: {warning}", file=sys.stderr)
4083
+ return prepared.block
4084
+
4085
+
4058
4086
  def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4059
4087
  """Produce a complete okstra task bundle on disk. See module docstring."""
4060
4088
  user_authorization = _prepare_user_authorization(inp)
@@ -4326,6 +4354,9 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
4326
4354
  "DYNAMIC_EXECUTION_ROLES_JSON": json.dumps(
4327
4355
  dynamic_roles, ensure_ascii=False,
4328
4356
  ),
4357
+ "AVAILABLE_MCP_SERVERS": _prepare_mcp_block(
4358
+ project_root, assignment_plan, inp.brief_path,
4359
+ ),
4329
4360
  })
4330
4361
  refresh_run_context_snapshot(ctx)
4331
4362
  if lead_runtime == "codex":
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import argparse
13
13
  import json
14
14
  import re
15
+ import subprocess
15
16
  from datetime import datetime, timezone
16
17
  from pathlib import Path
17
18
 
@@ -23,6 +24,7 @@ from .consumers import (
23
24
  carry_head_commit,
24
25
  read_consumers,
25
26
  )
27
+ from .file_size_cap import file_size_violations
26
28
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
27
29
  from .paths import RunRef
28
30
  from .stage_reopen import resolve_planned_stage
@@ -49,6 +51,34 @@ def _read_carry(carry_file: Path, stage: int) -> dict:
49
51
  return carry
50
52
 
51
53
 
54
+ def _check_file_size(project_root: Path, carry: dict, head: str) -> str:
55
+ """Refuse to settle a stage whose diff breaks the file-size limit; return what was checked.
56
+
57
+ Verifiers report the limit from `okstra correction file-size`; this re-runs it so a stage
58
+ a verifier did not check still reaches the correction round instead of landing. A range
59
+ git cannot read is reported in the output, not refused: the limit is a code-quality rule
60
+ and must not strand a stage whose verdict already passed.
61
+ """
62
+ rng = carry.get("stageCommitRange")
63
+ base = rng.get("base") if isinstance(rng, dict) else None
64
+ if not base:
65
+ return "not checked: the carry has no stageCommitRange.base"
66
+ try:
67
+ violations = [
68
+ v for v in file_size_violations(project_root, str(base), head) if v.blocking
69
+ ]
70
+ except subprocess.CalledProcessError as exc:
71
+ return f"not checked: git: {exc.stderr.decode(errors='replace').strip()}"
72
+ if violations:
73
+ raise StateError(
74
+ "the stage diff breaks the 400-line file-size limit:\n"
75
+ + "\n".join(v.line() for v in violations)
76
+ + f"\nRun a correction round (`okstra correction prompt ... --base {head} "
77
+ f"--stage-base {base}`) so the executor splits these files, or end the run FAIL "
78
+ "with `okstra stage-fail` when no round is left.", stage="file-size")
79
+ return f"pass: {base}..{head}"
80
+
81
+
52
82
  def _supersede(target: Path, rows: list[dict], stage: int) -> Path:
53
83
  """Move the sidecar of the run that first settled a reopened stage aside, unchanged."""
54
84
  settled = [r for r in rows if r.get("stage") == stage and r.get("status") == "done"]
@@ -76,6 +106,7 @@ def done_stage(
76
106
  identity, plan_run_root = resolve_planned_stage(project_root, task_key, stage)
77
107
  carry = _read_carry(carry_file, stage)
78
108
  head = carry_head_commit(carry)
109
+ file_size_cap = _check_file_size(project_root, carry, head)
79
110
  report = report if report.is_absolute() else project_root / report
80
111
  try:
81
112
  report_rel = report.relative_to(RunRef.from_report_path(report).run_dir).as_posix()
@@ -118,6 +149,7 @@ def done_stage(
118
149
  "supersededCarryPath": str(superseded) if superseded else "",
119
150
  "reportPath": report_rel,
120
151
  "consumersPath": str(plan_run_root / "consumers.jsonl"),
152
+ "fileSizeCap": file_size_cap,
121
153
  }
122
154
 
123
155
 
@@ -131,11 +163,13 @@ canonical sidecar `runs/implementation/carry/stage-<N>.json`, then appends the
131
163
  `done` row with that carry path, the report path relative to its run directory,
132
164
  the carry's head and the completion time. A reopened stage's previous sidecar
133
165
  is moved to `carry/superseded/stage-<N>-from-<task-type>-<seq>.json` first.
134
- Refuses a carry for another stage, a failed carry, and a stage already done at
135
- another head. Re-running with the same carry writes nothing new.
166
+ Refuses a carry for another stage, a failed carry, a stage already done at
167
+ another head, and a carry whose stageCommitRange diff leaves a source or test
168
+ file over the 400-line limit (`okstra correction file-size`). Re-running with the
169
+ same carry writes nothing new.
136
170
 
137
171
  Output: JSON { ok, taskKey, stage, headCommit, carryPath, supersededCarryPath,
138
- reportPath, consumersPath }. Exit 1 on a refusal, 2 when
172
+ reportPath, consumersPath, fileSizeCap }. Exit 1 on a refusal, 2 when
139
173
  PROJECT_ROOT cannot be resolved.
140
174
  """
141
175
 
@@ -1,4 +1,4 @@
1
- """Best-effort git reconciliation shared by stage prepare flows."""
1
+ """Git reconciliation shared by stage prepare flows."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import sys
@@ -31,3 +31,51 @@ def auto_reconcile_best_effort(inp: Any, plan_run_root: Path) -> None:
31
31
  )
32
32
  except Exception as exc:
33
33
  print(f"git-reconcile skipped: {exc}", file=sys.stderr)
34
+
35
+
36
+ class StaleStageRecordsError(RuntimeError):
37
+ """A recorded stage SHA no longer matches git and only the user can say which commit counts."""
38
+
39
+
40
+ def require_reconciled_stages(
41
+ inp: Any, plan_run_root: Path, *, stage: int | None = None,
42
+ ) -> None:
43
+ """Apply auto items, then refuse while a confirm item remains.
44
+
45
+ A prepare that restores, rebases or merges stage branches calls this first:
46
+ building on a recorded commit the branch no longer carries integrates work
47
+ nobody verified. ``stage`` narrows the check to that stage's done row.
48
+ """
49
+ from .git_reconcile import _git, classify_task, guidance
50
+
51
+ auto_reconcile_best_effort(inp, plan_run_root)
52
+ # A project prepared without git (worktree status skipped-not-git) has no
53
+ # history the recorded SHAs could disagree with.
54
+ if _git(Path(inp.project_root), "rev-parse", "--git-dir").returncode != 0:
55
+ return
56
+ coordinates = dict(
57
+ project_id=inp.project_id,
58
+ task_group=inp.task_group,
59
+ task_id=inp.task_id,
60
+ work_category=inp.work_category,
61
+ )
62
+ pending = [
63
+ item
64
+ for item in classify_task(
65
+ project_root=Path(inp.project_root),
66
+ plan_run_root=plan_run_root,
67
+ **coordinates,
68
+ )
69
+ if item.classification == "confirm"
70
+ and (stage is None or item.stage == stage)
71
+ ]
72
+ if not pending:
73
+ return
74
+ lines = "\n".join(
75
+ f" {f'stage {item.stage}' if item.kind == 'done' else 'anchor'}: "
76
+ f"{item.reason} (recorded {item.recorded})"
77
+ for item in pending
78
+ )
79
+ raise StaleStageRecordsError(
80
+ f"{lines}\n{guidance(plan_run_root=plan_run_root, **coordinates)}"
81
+ )
@@ -215,14 +215,14 @@ def _stage_plan_for_confirmation(
215
215
 
216
216
 
217
217
  def _fix_cycle_confirm_required(state: WizardState) -> bool:
218
- """완료(release-handoff) task 에 entry phase 로 재진입하고, 아직 열린 fix
219
- cycle 이 없을 때만 묻는다."""
218
+ """전달까지 끝난(release-handoff) task 에 entry phase 로 재진입하고, 아직
219
+ 열린 fix cycle 이 없을 때만 묻는다."""
220
220
  if state.task_type not in fix_cycles.FIX_CYCLE_ENTRY_PHASES:
221
221
  return False
222
222
  task_root = task_dir(Path(state.project_root),
223
223
  state.task_group, state.task_id)
224
224
  workflow = (read_task_manifest(task_root) or {}).get("workflow") or {}
225
- if workflow.get("lastCompletedPhase") != "release-handoff":
225
+ if not fix_cycles.delivery_completed(workflow):
226
226
  return False
227
227
  return fix_cycles.open_cycle(fix_cycles.read_rows(task_root)) is None
228
228
 
@@ -480,6 +480,42 @@
480
480
  }
481
481
  ]
482
482
  },
483
+ "correctionRounds": {
484
+ "type": "array",
485
+ "maxItems": 2,
486
+ "description": "Each implementation correction round's re-convergence of the verifiers' re-verification, projected from `convergence-correction-r<N>-<task-type>-<seq>.json`. Row ids continue the initial round's numbering so every row keeps its own anchor.",
487
+ "items": {
488
+ "type": "object",
489
+ "required": [
490
+ "round",
491
+ "consensus",
492
+ "differences"
493
+ ],
494
+ "additionalProperties": false,
495
+ "properties": {
496
+ "round": {
497
+ "type": "integer",
498
+ "minimum": 1,
499
+ "maximum": 2
500
+ },
501
+ "roundHistory": {
502
+ "$ref": "#/properties/crossVerification/properties/roundHistory"
503
+ },
504
+ "consensus": {
505
+ "type": "array",
506
+ "items": {
507
+ "$ref": "#/$defs/ConsensusRow"
508
+ }
509
+ },
510
+ "differences": {
511
+ "type": "array",
512
+ "items": {
513
+ "$ref": "#/$defs/DifferenceRow"
514
+ }
515
+ }
516
+ }
517
+ }
518
+ },
483
519
  "criticGaps": {
484
520
  "type": "object",
485
521
  "description": "Coverage-critic gap accounting. `proposed - merged - unverified` is the count the voters judged and rejected, so a gap that vanished shows up as a mismatch instead of as nothing. Every `unverified` gap needs a matching `missingInformation` row with source `critic-unverified` (prompts/lead/convergence.md \"Gap verification\").",
@@ -1,6 +1,6 @@
1
1
  ## Step 6: Take over as Okstra lead
2
2
 
3
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
3
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
4
4
 
5
5
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
6
6
 
@@ -20,8 +20,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
20
20
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
21
21
 
22
22
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
23
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
24
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
23
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
24
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
25
25
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
26
26
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
27
27
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
47
47
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
48
48
 
49
49
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
50
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
51
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
50
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
51
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
52
52
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
53
53
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
54
54
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -290,7 +290,7 @@ You can delete the literal state-file path after this point — its job is done.
290
290
  <!-- BEGIN FRAGMENT: wizard-lead-takeover -->
291
291
  ## Step 6: Take over as Okstra lead
292
292
 
293
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
293
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
294
294
 
295
295
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
296
296
 
@@ -52,6 +52,7 @@ okstra manager view --manager-id <manager-id>
52
52
 
53
53
  `remove project`, `remove task`, `remove task-group` and `task remove-child` change nothing without `--confirm`; they print what would be removed (`Removed: no — dry run`). Run the command without `--confirm` first, show the user the listed project, path, tasks or kept children, and rerun with `--confirm` only after the user approves that exact target. These commands touch manager files only; project `.okstra/` state and briefs written by `task split` stay.
54
54
 
55
+ - `task status` shows the snapshot as of `Synced at` with its `Sync age`; a child marked `changed since sync: yes` has a project task manifest written after that sync, so run `task sync` before reporting its state.
55
56
  - `remove project` unregisters the project and keeps its child tasks. `task status` shows them with `project linked: no`, the view marks them `unlinked`, and `task run` refuses them until the project is registered again with `new project`.
56
57
  - `task update` changes only the fields given and needs no confirmation. `new task` still refuses to change an existing task's objective, common brief or progress mode.
57
58
 
@@ -47,8 +47,8 @@ On `ok: false`, re-prompt with the same `current.step` using the error message.
47
47
  The wizard tells you which relay operation to use via `next.interaction.kind`. Step 1 loads the current registered host's relay contract. Use the matching entry under its `interactions` object, including the runtime-generated navigation and completion options. If the entry is absent, stop instead of inventing a host function or falling back silently. Use the explicit `Legacy text relay compatibility` mapping only when the preflight response has no `relayContract`.
48
48
 
49
49
  - `native-single` → use the current registered host's relay contract for its native single-select function. Pass every option in its original order and submit the selected `options[].value`.
50
- - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the selected values as one CSV answer; submit an empty selection as `--answer ""`.
51
- - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; when a grouped question is multi-select, its step value is the selected option values joined as one CSV string, not a JSON array.
50
+ - `native-multi` → use the relay contract's native multi-select function. Pass every option in its original order and submit the host's selected-labels string unchanged (labels may contain `, `; the wizard decodes it against the option list); submit an empty selection as `--answer ""`.
51
+ - `native-group` → use the relay contract's native grouped-question function once. Preserve every question and option, then submit one JSON object keyed by `questions[].step`; a single-select step value is the selected option value, and a multi-select step value is the host's selected-labels string unchanged, not a JSON array.
52
52
  - `numbered-single` → render every option as a 1-based numbered Markdown list and submit the user's next message unchanged.
53
53
  - `numbered-multi` → render every option as a 1-based numbered Markdown list and submit the user's comma-separated reply unchanged.
54
54
  - `sequential-group` → render every question in order, preserve each complete option list, collect every answer, and submit one JSON object keyed by `questions[].step`.
@@ -439,6 +439,7 @@ If `render-bundle` fails with a `PrepareError` containing `Recorded stage SHAs n
439
439
  - **Enter a different ref directly** — the user names a commit/branch/tag.
440
440
  - **Abort** — stop the run without recovering.
441
441
  3. Run `okstra git-reconcile … --apply --stage <N> --use-ref <ref>` with the chosen ref, then retry the failed `render-bundle` with the same arguments.
442
+ If that output carries a `Warning` naming the implementation anchor, the chosen head no longer contains the anchor: show the warning to the user and run its printed `--reset-anchor` command only after they confirm the new base, before the retry.
442
443
 
443
444
  If the anchor (`implementation_base_commit`) is reported unresolvable, run the same command's `--reset-anchor <ref>` after user confirmation. Correcting a confirm item without the picker is forbidden — the runtime also rejects a confirm correction without `--use-ref`.
444
445
 
@@ -455,7 +456,7 @@ If `render-bundle` raises Step 5's concurrent-run conflict detection (concurrent
455
456
  <!-- BEGIN FRAGMENT: wizard-lead-takeover -->
456
457
  ## Step 6: Take over as Okstra lead
457
458
 
458
- Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it.
459
+ Read `resources.leadExecutionPromptPath` from the run manifest at `<RUN_MANIFEST_PATH>` — the same manifest Step 5 already opened for `resources.leadPromptMetadataPath` — then read that file verbatim and take over as `Okstra lead` in the current host-native session. Do not construct the path yourself: the prompt is written per run under `runs/<task-type>/prompts/lead-execution-prompt-<task-type>-<seq>.md`, not in the instruction set, and the `<seq>` is assigned at prepare time. The prompt selects exactly one runtime adapter and points to compact intake artifacts first (`active-run-context`, `analysis-profile.md`, and `analysis-packet.md`); full source files such as `analysis-material.md`, `reference-expectations.md`, and `final-report-template.md` are lazy/fallback inputs. Follow the rendered prompt order, do not preempt it. The run manifest's `reportLanguage` sets the language of okstra report artifacts; a host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not override it, and the lead never tells the report writer to use another language.
459
460
 
460
461
  Then proceed through the phases exactly as the lead prompt directs (Phase 1 context → Phase 2+ worker dispatch → final synthesis → final report).
461
462
 
@@ -174,6 +174,8 @@ matching section:
174
174
  - **F. declared architecture style** (`architecture.style`) — `hexagonal` /
175
175
  `layered` / `none`; hand-added, default `none`. Declaring one makes that
176
176
  architecture's placement rules binding in planning + verification.
177
+ - **H. MCP servers** (`mcpServers`) — `okstra mcp add` declares a server,
178
+ `okstra mcp check` shows which provider CLIs reach it.
177
179
 
178
180
  ## Step 4: Verify
179
181
 
@@ -301,3 +301,37 @@ preserves it:
301
301
  records that it read no rules and the run still passes. Whether a pack that
302
302
  does resolve was actually read and applied stays the phase's own
303
303
  `project-review-rules:` record — no machine check reads a worker's reasoning.
304
+
305
+ ## H. MCP servers (`mcpServers`)
306
+
307
+ `mcpServers` declares the MCP servers a run may use — a Figma file server for a
308
+ visual check, for example. Without a declaration the launch prompt and the
309
+ analysis packet tell every worker that MCP is unavailable, even when a provider
310
+ CLI has the server configured.
311
+
312
+ Register each server with `okstra mcp add`. `--tool` repeats per tool;
313
+ `--alias` repeats per name a provider CLI lists the server under (one provider
314
+ may list Figma as `claude.ai Figma`, another as `figma-desktop`):
315
+
316
+ ```bash
317
+ okstra mcp add --name figma --description "Figma design files" --tool get_screenshot --tool get_metadata --alias "claude.ai Figma" --alias figma-desktop
318
+ ```
319
+
320
+ Then check which providers can reach it:
321
+
322
+ ```bash
323
+ okstra mcp check
324
+ ```
325
+
326
+ Each provider adapter lists its own CLI's MCP servers without an inference
327
+ call. `reachable` means the provider listed the server enabled; `disabled`
328
+ means listed but disabled or awaiting authentication; `missing` means not
329
+ listed; `unknown` means the adapter cannot list. Show the result to the user.
330
+ For a provider that is not `reachable`, tell the user to add the server to that
331
+ provider CLI's own MCP configuration — okstra never edits a provider's
332
+ configuration. `okstra mcp list` and `okstra mcp remove --name <name>` manage
333
+ existing entries.
334
+
335
+ prepare repeats the check for the providers a run selects and prints a warning
336
+ for each gap; it never refuses the run. It also warns when the task brief cites
337
+ a Figma URL and no declared server name or alias contains `figma`.
@@ -30,7 +30,7 @@ The narrative is not free-form Markdown. A narrative file is the line `# OKSTRA
30
30
 
31
31
  Blank lines are ignored. **Every other line is rejected**, including Markdown headings (`#`, `##`, `###`), a pipe table written at column 0, code fences, bare paragraphs, JSON, YAML, and JSON Pointer. A heading or table that belongs in the reader's report is written as the text of a `> ` value, never as a narrative line of its own.
32
32
 
33
- Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
33
+ Follow the task-specific schema order. Write every `> ` prose value directly in the language named by `**Report Language:**`; do not draft in English first, because no translation step follows. A host, project or global instruction file (`CLAUDE.md`, `AGENTS.md`) does not change that language for report artifacts. Field names, `Verdict Token` and other enum or status values, finding and requirement ids, paths, symbols, commands, and model names stay exactly as the schema and sources spell them.
34
34
 
35
35
  ### Top-level fields you may author
36
36
 
@@ -70,7 +70,7 @@
70
70
  </section>
71
71
  {% endif %}
72
72
  {% set no_cross_model_check = taskType != "release-handoff" and (crossVerification.get("roundHistory") or {}).get("round2SkippedReason") == "single-analyser-only" %}
73
- {% if crossVerification.get("consensus") or crossVerification.get("differences") or no_cross_model_check %}
73
+ {% if crossVerification.get("consensus") or crossVerification.get("differences") or crossVerification.get("correctionRounds") or no_cross_model_check %}
74
74
  <section data-report-section="cross-check">
75
75
  <h2>{{ t('base.cross-check') }}</h2>
76
76
  {% if no_cross_model_check %}<p class="section-lede">{{ t('base.no-cross-model-check') }}</p>{% endif %}
@@ -82,6 +82,11 @@
82
82
  <h3>{{ t('base.workers-disagreed') }}</h3>
83
83
  <div class="summary-grid">{% for row in crossVerification.differences %}<article class="summary-card tone-important" id="id-xv-{{ row.id }}"><p class="eyebrow">{{ row.id | inline_code }}</p><h3>{{ row.disagreement | inline_code }}</h3><p>{% for pos in row.get("workersPosition", []) %}{{ pos.worker | inline_code }} · {{ pos.position | inline_code }}{% if not loop.last %} · {% endif %}{% endfor %}</p><p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.evidence | inline_code }}</p></article>{% endfor %}</div>
84
84
  {% endif %}
85
+ {% for correction in crossVerification.get("correctionRounds", []) %}
86
+ <h3>{{ t('base.cross-check-correction-round') | replace('{round}', correction.round | string) }}</h3>
87
+ {% if correction.consensus %}<div class="summary-grid">{% for row in correction.consensus %}<article class="summary-card" id="id-xv-{{ row.id }}"><p class="eyebrow">{{ row.id | inline_code }} · {{ t('base.agreed-across-workers') }}</p><h3>{{ row.statement | inline_code }}</h3><p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.evidence | inline_code }}</p></article>{% endfor %}</div>{% endif %}
88
+ {% if correction.differences %}<div class="summary-grid">{% for row in correction.differences %}<article class="summary-card tone-important" id="id-xv-{{ row.id }}"><p class="eyebrow">{{ row.id | inline_code }} · {{ t('base.workers-disagreed') }}</p><h3>{{ row.disagreement | inline_code }}</h3><p>{% for pos in row.get("workersPosition", []) %}{{ pos.worker | inline_code }} · {{ pos.position | inline_code }}{% if not loop.last %} · {% endif %}{% endfor %}</p><p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.evidence | inline_code }}</p></article>{% endfor %}</div>{% endif %}
89
+ {% endfor %}
85
90
  </section>
86
91
  {% endif %}
87
92
  {% if agentActivity %}
@@ -242,6 +242,7 @@
242
242
  "no-cross-model-check": "No cross-model check ran: fewer than two analysis workers were selected, so no other model verified these findings.",
243
243
  "agreed-across-workers": "Agreed across workers",
244
244
  "workers-disagreed": "Workers disagreed",
245
+ "cross-check-correction-round": "Re-converged after correction round {round}",
245
246
  "source": "Source",
246
247
  "confidence": "Confidence",
247
248
  "export-my-answers": "Export my answers",
@@ -242,6 +242,7 @@
242
242
  "no-cross-model-check": "교차 모델 검증을 하지 않았습니다. 분석 작업자를 두 명 미만으로 골라 다른 모델이 이 결과를 검증하지 않았습니다.",
243
243
  "agreed-across-workers": "작업자 간 합의",
244
244
  "workers-disagreed": "작업자 간 이견",
245
+ "cross-check-correction-round": "수정 라운드 {round} 뒤 재수렴",
245
246
  "source": "출처",
246
247
  "confidence": "확신도",
247
248
  "export-my-answers": "내 답변 내보내기",
@@ -154,13 +154,13 @@ taskType: "{{FM_TASK_TYPE}}"
154
154
 
155
155
  ## Available MCP Servers
156
156
 
157
- The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the Okstra lead's launch prompt under `## Available MCP Servers`. The lead is responsible for forwarding the rendered list verbatim into worker prompts (Phase 2) so each assignment can determine whether its selected runner exposes the named tools.
157
+ The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array. Prepare renders them, with the providers that list each server, into the lead's launch prompt and the analysis packet under `## Available MCP Servers`.
158
158
 
159
- To declare servers, add entries shaped `{ "name": "<server>", "description": "...", "tools": ["..."], "notes": "..." }` to that array. If the array is empty or absent, treat MCP as unavailable for this run.
159
+ To declare a server, run `okstra mcp add --name <server> --description <text> --tool <tool>... [--alias <name-as-a-provider-lists-it>...]`, then `okstra mcp check` to see which providers reach it. If nothing is declared, treat MCP as unavailable for this run.
160
160
 
161
161
  How to invoke (worker-by-worker):
162
162
 
163
- - **Native-session assignments**: invoke an MCP tool directly by its tool name (e.g. `mcp__<server>__<tool>`) only when the selected host adapter exposes it. Do not call an MCP tool name through a shell.
163
+ - **Native-session assignments**: invoke an MCP tool directly through the host's tool interface, under the name the host adapter exposes, only when it exposes the server. Do not call an MCP tool name through a shell.
164
164
  - **CLI-wrapper assignments**: use only the MCP or tool configuration explicitly supported and forwarded by that provider wrapper. If the wrapper has no matching MCP configuration, record `MCP not available in this CLI` in `Missing Information or Assumptions`; do not attempt a shell fallback.
165
165
  - All workers: cite the exact server, tool, and SELECT (or `WHERE` filters) used in the result file. Tool-call failures must be logged in the worker's `*-errors.json` (commandKind `mcp_call`) so the lead can decide whether to retry under a different worker.
166
166
 
@@ -2342,6 +2342,15 @@ def _validate_conformance_surfaces(
2342
2342
  )
2343
2343
 
2344
2344
 
2345
+ def _stage_ended_without_a_commit(report_data: dict) -> bool:
2346
+ implementation = report_data.get("implementation")
2347
+ if not isinstance(implementation, dict):
2348
+ return False
2349
+ evidence = implementation.get("stageSidecarEvidence")
2350
+ withheld = isinstance(evidence, dict) and str(evidence.get("withheld") or "").strip()
2351
+ return bool(withheld) and not implementation.get("commitList")
2352
+
2353
+
2345
2354
  def _validate_conformance(
2346
2355
  report_path: Path,
2347
2356
  failures: list[str],
@@ -2389,6 +2398,10 @@ def _validate_conformance(
2389
2398
  )
2390
2399
  if declared_manifest is None:
2391
2400
  return warnings
2401
+ # 스크립트와 manifest 항목은 stage 를 실행한 run 이 쓴다. 커밋 없이 carry 를
2402
+ # 보류한 run(첫 편집 전 정지 후 재계획)은 그것을 쓸 기회가 없었다.
2403
+ if _stage_ended_without_a_commit(report_data):
2404
+ declared_manifest = None
2392
2405
  if declared_manifest is not None:
2393
2406
  scoped_declared = _scope_manifest_entries(declared_manifest, stage_name)
2394
2407
  for error in missing_declared_scripts(
@@ -4841,8 +4854,10 @@ def _validate_convergence_group_provenance(run_dir, failures, suffix=None) -> No
4841
4854
  # provider token (`claude` vs `claude-worker`), depending on how the roster was
4842
4855
  # seeded — worker resolution below accepts both. `-reverify-r` never occurs
4843
4856
  # inside a slug, so the non-greedy slug capture stops at that literal anchor.
4857
+ # A correction round's re-convergence puts `correction-r<N>-` before
4858
+ # `reverify-r`, and its plans carry the same token before `<task-type>`.
4844
4859
  _REVERIFY_PROMPT_BASENAME_RE = re.compile(
4845
- r"^(?P<slug>[a-z][a-z0-9-]*?)-reverify-r(?P<round>\d+)"
4860
+ r"^(?P<slug>[a-z][a-z0-9-]*?)(?:-(?P<scope>correction-r\d+))?-reverify-r(?P<round>\d+)"
4846
4861
  r"-(?P<task_type>[a-z][a-z-]*?)-(?P<seq>\d{3})\.md$"
4847
4862
  )
4848
4863
 
@@ -5305,8 +5320,9 @@ def _validate_reverify_prompt_matches_plan(run_dir, failures, suffix=None) -> No
5305
5320
  if match is None:
5306
5321
  continue
5307
5322
  round_n = match.group("round")
5323
+ scope = f"{match.group('scope')}-" if match.group("scope") else ""
5308
5324
  plan_path = state_dir / (
5309
- f"convergence-round-{round_n}-plan-"
5325
+ f"convergence-round-{round_n}-plan-{scope}"
5310
5326
  f"{match.group('task_type')}-{match.group('seq')}.json"
5311
5327
  )
5312
5328
  if not plan_path.is_file():