okstra 0.67.0 → 0.69.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 (50) hide show
  1. package/bin/okstra +25 -0
  2. package/docs/kr/architecture.md +17 -1
  3. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +456 -0
  4. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +1408 -0
  5. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +1572 -0
  6. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +1 -1
  7. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +107 -0
  8. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +105 -0
  9. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +156 -0
  10. package/package.json +1 -1
  11. package/runtime/BUILD.json +2 -2
  12. package/runtime/agents/SKILL.md +8 -7
  13. package/runtime/agents/workers/claude-worker.md +1 -1
  14. package/runtime/agents/workers/codex-worker.md +3 -3
  15. package/runtime/agents/workers/gemini-worker.md +3 -3
  16. package/runtime/agents/workers/report-writer-worker.md +2 -2
  17. package/runtime/prompts/launch.template.md +2 -2
  18. package/runtime/prompts/profiles/_common-contract.md +6 -6
  19. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
  20. package/runtime/prompts/profiles/_implementation-executor.md +3 -1
  21. package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
  22. package/runtime/prompts/profiles/final-verification.md +3 -2
  23. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  24. package/runtime/prompts/profiles/release-handoff.md +12 -5
  25. package/runtime/prompts/wizard/prompts.ko.json +5 -5
  26. package/runtime/python/okstra_ctl/conformance.py +17 -0
  27. package/runtime/python/okstra_ctl/consumers.py +72 -5
  28. package/runtime/python/okstra_ctl/git_reconcile.py +322 -0
  29. package/runtime/python/okstra_ctl/handoff.py +348 -0
  30. package/runtime/python/okstra_ctl/render.py +44 -2
  31. package/runtime/python/okstra_ctl/run.py +175 -44
  32. package/runtime/python/okstra_ctl/wizard.py +89 -22
  33. package/runtime/python/okstra_ctl/worktree.py +28 -0
  34. package/runtime/python/okstra_ctl/worktree_registry.py +40 -9
  35. package/runtime/python/okstra_token_usage/collect.py +27 -0
  36. package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
  37. package/runtime/skills/okstra-convergence/SKILL.md +3 -3
  38. package/runtime/skills/okstra-report-writer/SKILL.md +8 -8
  39. package/runtime/skills/okstra-run/SKILL.md +43 -3
  40. package/runtime/skills/okstra-team-contract/SKILL.md +7 -7
  41. package/runtime/validators/validate-run.py +51 -11
  42. package/src/_python-helper.mjs +52 -0
  43. package/src/error-log.mjs +19 -0
  44. package/src/git-reconcile.mjs +31 -0
  45. package/src/handoff.mjs +30 -0
  46. package/src/inject-report-index.mjs +22 -0
  47. package/src/render-final-report.mjs +22 -0
  48. package/src/render-views.mjs +9 -48
  49. package/src/spawn-followups.mjs +23 -0
  50. package/src/token-usage.mjs +3 -34
@@ -1641,8 +1641,11 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1641
1641
  # - release-handoff (and any other phase that renders with no workers
1642
1642
  # selected) is single-lead and MUST NOT call `TeamCreate`. Emit a
1643
1643
  # short notice instead of the BLOCKING gate.
1644
+ # - concurrent implementation runs skip team creation to avoid racing the
1645
+ # shared `~/.claude/teams/` config.
1644
1646
  # - All other phases keep the full team-creation contract.
1645
1647
  task_type = ctx.get("TASK_TYPE", "")
1648
+ concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
1646
1649
  if task_type == "release-handoff" or not selected:
1647
1650
  team_creation_gate_block = (
1648
1651
  "## Single-Lead Phase (no team creation)\n"
@@ -1655,7 +1658,34 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1655
1658
  "report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
1656
1659
  "from this run — that would be a contract violation."
1657
1660
  )
1661
+ elif task_type == "implementation" and concurrent_stages:
1662
+ team_creation_gate_block = (
1663
+ "## Concurrent-run: no-team background (BLOCKING)\n"
1664
+ "\n"
1665
+ "A concurrent implementation run of the same task-key holds stage(s)\n"
1666
+ f"`{concurrent_stages}` right now. Creating a team would race the shared\n"
1667
+ "`~/.claude/teams/` config and can make another stage's `config.json`\n"
1668
+ "unreadable. This run therefore does NOT create a team.\n"
1669
+ "\n"
1670
+ "Required actions, in order:\n"
1671
+ "\n"
1672
+ "1. Do NOT call `TeamCreate`, and do NOT dispatch any worker with\n"
1673
+ " `Agent(... team_name: ...)`.\n"
1674
+ "2. Before any dispatch, record in team-state:\n"
1675
+ ' `teamCreate: { attempted: false, status: "skipped",'
1676
+ f' reason: "concurrent-run", concurrentStages: [{concurrent_stages}] }}`.\n'
1677
+ "3. Dispatch every worker with `run_in_background: true` and NO\n"
1678
+ " `team_name` (the Phase 5 fallback). Worker completion is detected by\n"
1679
+ " result-file polling, so analysis output is equivalent — only the\n"
1680
+ " Teams split-pane view is lost."
1681
+ )
1658
1682
  else:
1683
+ team_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1684
+ stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1685
+ if task_type == "implementation" and stage:
1686
+ # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1687
+ # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1688
+ team_name = f"{team_name}-s{stage}"
1659
1689
  team_creation_gate_block = (
1660
1690
  "## Team Creation Gate (BLOCKING)\n"
1661
1691
  "\n"
@@ -1671,14 +1701,26 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1671
1701
  "\n"
1672
1702
  "1. Invoke the `okstra-team-contract` skill and verify the selected worker\n"
1673
1703
  " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
1674
- f'2. Call `TeamCreate(team_name: "okstra-{ctx.get("TASK_KEY", "")}", description: ...)`.\n'
1704
+ f'2. Call `TeamCreate(team_name: "{team_name}", description: ...)`.\n'
1675
1705
  "3. Record the outcome in team-state under\n"
1676
1706
  ' `teamCreate: { attempted: true, status: "ok" | "error", error?: <msg> }`\n'
1677
- " BEFORE any `Agent(...)` worker dispatch.\n"
1707
+ " AND record the exact name as `teamName` BEFORE any `Agent(...)` worker\n"
1708
+ " dispatch.\n"
1678
1709
  "4. Only after `teamCreate` is persisted may you dispatch workers — with\n"
1679
1710
  " `team_name` on success, or with `run_in_background: true` and no\n"
1680
1711
  ' `team_name` ONLY when `teamCreate.status == "error"` was recorded.\n'
1681
1712
  "\n"
1713
+ 'If `TeamCreate` fails with "team already exists" (stale leftover from an\n'
1714
+ "earlier attempt): call `TeamList`; if the team is listed in this session,\n"
1715
+ "`TeamDelete` it and retry step 2 once. If it is NOT listed, do NOT remove\n"
1716
+ "`~/.claude/teams/...` / `~/.claude/tasks/...` on your own initiative —\n"
1717
+ "shell deletion of harness-internal state is destructive and `rm -rf` is\n"
1718
+ "commonly denied by user permission rules. Instead ask the user via\n"
1719
+ "AskUserQuestion (recommended option: quarantine), and on approval move\n"
1720
+ f"`~/.claude/teams/{team_name}` and `~/.claude/tasks/{team_name}` into\n"
1721
+ "`~/.okstra/trash/<UTC-timestamp>/` with `mv` (reversible, no delete),\n"
1722
+ "then retry step 2 once.\n"
1723
+ "\n"
1682
1724
  'If the Agent tool rejects a dispatch with `"team must be created first"` /\n'
1683
1725
  '`"team을 먼저 생성하거나 team_name 없이 호출해야 합니다"`, the correct\n'
1684
1726
  "response is to go back to step 2 — NOT to strip `team_name` and retry."
@@ -81,7 +81,11 @@ from .workers import (
81
81
  )
82
82
  from .workflow import compute_workflow_state
83
83
  from .locks import worktree_provision_mutex
84
- from .worktree import provision_task_worktree
84
+ from .worktree import (
85
+ WorktreeProvision,
86
+ okstra_clean_gate_excludes,
87
+ provision_task_worktree,
88
+ )
85
89
 
86
90
  # Frontmatter approval-flag matcher.
87
91
  #
@@ -462,12 +466,41 @@ def _commit_is_ancestor(project_root, ancestor: str, descendant: str) -> bool:
462
466
  return r.returncode == 0
463
467
 
464
468
 
469
+ def _check_multi_dep_merged(project_root, plan_run_root, latest,
470
+ pred_commits: dict, candidate_base: str,
471
+ stage_n: int) -> None:
472
+ """모든 선행 done commit 이 candidate 에 (내용 기준으로라도) 머지됐는지
473
+ 검증. patch-equivalent 면 보정 row 를 자동 기록(다음 run 은 ancestor 로
474
+ 바로 통과). 아니면 회복 안내를 담아 PrepareError."""
475
+ from .git_reconcile import content_merged, _record_reconciled
476
+ for d, head in pred_commits.items():
477
+ if _commit_is_ancestor(project_root, head, candidate_base):
478
+ continue
479
+ match = content_merged(project_root, head, candidate_base)
480
+ if match.status in ("ancestor", "patch-equivalent"):
481
+ if plan_run_root is not None:
482
+ _record_reconciled(
483
+ plan_run_root,
484
+ impl_task_key=(latest.get(d) or {}).get("impl_task_key", ""),
485
+ stage=d, new_commit=match.matched_commit,
486
+ replaced=head, reason="auto-patch-id")
487
+ continue
488
+ raise PrepareError(
489
+ f"multi-dependency stage {stage_n}: predecessor stage {d} "
490
+ f"({head[:8]}) is not merged into the task worktree "
491
+ f"({candidate_base[:8]}). Merge stage branches "
492
+ f"(e.g. the `-s{d}` branches) into the task worktree "
493
+ "(or into main, then refresh the worktree) and retry."
494
+ )
495
+
496
+
465
497
  def _resolve_stage_base_commit(
466
498
  stage: dict,
467
499
  consumer_done_rows: list,
468
500
  anchor_base_commit: str,
469
501
  candidate_base: str = "",
470
502
  project_root=None,
503
+ plan_run_root=None,
471
504
  ) -> str:
472
505
  """Pick the git base commit a stage's isolated worktree branches from.
473
506
 
@@ -479,18 +512,15 @@ def _resolve_stage_base_commit(
479
512
  candidate 를 반환(사용자가 선행을 머지함). 아니면 PrepareError.
480
513
 
481
514
  Raises PrepareError on missing predecessor / anchor / unmerged predecessor."""
515
+ from .consumers import latest_done_by_stage
516
+ latest = latest_done_by_stage(consumer_done_rows)
482
517
  deps = stage.get("depends_on") or []
483
518
  if len(deps) >= 2:
484
519
  n = stage["stage_number"]
485
520
  # 1) 모든 선행의 done head_commit 수집
486
521
  pred_commits = {}
487
522
  for d in deps:
488
- head = next(
489
- (r.get("head_commit") for r in consumer_done_rows
490
- if r.get("stage") == d and r.get("status") == "done"
491
- and r.get("head_commit")),
492
- None,
493
- )
523
+ head = (latest.get(d) or {}).get("head_commit")
494
524
  if not head:
495
525
  raise PrepareError(
496
526
  f"predecessor stage {d} has no done row with head_commit "
@@ -503,16 +533,10 @@ def _resolve_stage_base_commit(
503
533
  f"candidate base missing for multi-dependency stage {n}; "
504
534
  "task-key worktree HEAD could not be resolved"
505
535
  )
506
- # 3) 모든 선행 done 이 candidate ancestor 인지 (=사용자가 머지함)
507
- for d, head in pred_commits.items():
508
- if not _commit_is_ancestor(project_root, head, candidate_base):
509
- raise PrepareError(
510
- f"multi-dependency stage {n}: predecessor stage {d} "
511
- f"({head[:8]}) is not merged into the task worktree "
512
- f"({candidate_base[:8]}). Merge stage branches "
513
- f"(e.g. the `-s{d}` branches) into the task worktree "
514
- "(or into main, then refresh the worktree) and retry."
515
- )
536
+ # 3) 모든 선행 done 이 candidate (ancestor 또는 patch-equivalent )
537
+ # 머지됐는지 patch-equivalent 보정 row 를 자동 기록한다.
538
+ _check_multi_dep_merged(project_root, plan_run_root, latest,
539
+ pred_commits, candidate_base, n)
516
540
  return candidate_base
517
541
  if not deps:
518
542
  if not anchor_base_commit:
@@ -524,11 +548,9 @@ def _resolve_stage_base_commit(
524
548
  return anchor_base_commit
525
549
  # 단일 의존
526
550
  pred = deps[0]
527
- for row in consumer_done_rows:
528
- if row.get("stage") == pred and row.get("status") == "done":
529
- head = row.get("head_commit") or ""
530
- if head:
531
- return head
551
+ head = (latest.get(pred) or {}).get("head_commit") or ""
552
+ if head:
553
+ return head
532
554
  raise PrepareError(
533
555
  f"predecessor stage {pred} has no done row with head_commit in "
534
556
  "consumers.jsonl; cannot derive base for stage "
@@ -553,7 +575,8 @@ def _resolve_whole_task_target(
553
575
  ) -> "_FVTarget":
554
576
  """전체-task 검증 target. 모든 Stage Map stage 가 done + HEAD 에 머지 +
555
577
  worktree clean 이어야 한다. 위반 시 PrepareError."""
556
- done_by_stage = {r["stage"]: r for r in done_rows}
578
+ from .consumers import latest_done_by_stage
579
+ done_by_stage = latest_done_by_stage(done_rows)
557
580
  for s in stage_map:
558
581
  n = s["stage_number"]
559
582
  if n not in done_by_stage:
@@ -587,8 +610,9 @@ def _resolve_single_stage_target(
587
610
  ) -> "_FVTarget":
588
611
  """단독-stage 검증 target. stage N 만 done + stage worktree 존재 + clean.
589
612
  다른 stage 의 done/머지 여부와 무관. 위반 시 PrepareError."""
613
+ from .consumers import latest_done_by_stage
590
614
  n = int(requested_stage)
591
- done_by_stage = {r["stage"]: r for r in done_rows}
615
+ done_by_stage = latest_done_by_stage(done_rows)
592
616
  if n not in done_by_stage:
593
617
  raise PrepareError(
594
618
  f"final-verification(single-stage): stage {n} not done — "
@@ -1131,6 +1155,46 @@ def _apply_qa_waiver_if_requested(inp: "PrepareInputs", project_root: Path) -> N
1131
1155
  manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n")
1132
1156
 
1133
1157
 
1158
+ def _clear_stale_stage_waiver(inp: "PrepareInputs", project_root: Path, stage: int) -> None:
1159
+ """A fresh `implementation` run of stage N must not inherit a waiver left on
1160
+ its conformance entry by an earlier run (e.g. an all-gate run that pre-waived
1161
+ future stages, or an abandoned attempt). A stale waiver makes the verifier
1162
+ skip Tier 3 conformance and silently mask this stage, so clear it — UNLESS
1163
+ the user re-waived this exact stage for this run via `--qa-waiver` (already
1164
+ applied upstream in `_apply_qa_waiver_if_requested`)."""
1165
+ from .conformance import clear_qa_waiver, parse_qa_waiver_arg
1166
+ from .paths import task_dir
1167
+ manifest_path = (
1168
+ task_dir(project_root, inp.task_group, inp.task_id)
1169
+ / "qa" / "conformance-manifest.json"
1170
+ )
1171
+ if not manifest_path.is_file():
1172
+ return
1173
+ manifest = json.loads(manifest_path.read_text())
1174
+ entries = manifest.get("entries") if isinstance(manifest, dict) else None
1175
+ if not isinstance(entries, list):
1176
+ return
1177
+ # The manifest stageKey is `<task-id>-stage-<N>` authored by planning; match
1178
+ # on the `-stage-<N>` suffix so we do not assume the task-id's exact form.
1179
+ suffix = f"-stage-{stage}"
1180
+ stage_key = next(
1181
+ (e["stageKey"] for e in entries
1182
+ if isinstance(e, dict) and isinstance(e.get("stageKey"), str)
1183
+ and e["stageKey"].endswith(suffix)),
1184
+ None,
1185
+ )
1186
+ if stage_key is None:
1187
+ return
1188
+ if inp.qa_waiver:
1189
+ parsed = parse_qa_waiver_arg(inp.qa_waiver)
1190
+ if parsed is not None and parsed[0] == stage_key:
1191
+ return # user intentionally waived this stage for this run
1192
+ if clear_qa_waiver(manifest, stage_key):
1193
+ manifest_path.write_text(
1194
+ json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
1195
+ )
1196
+
1197
+
1134
1198
  def _register_and_check_project(project_root: Path, inp: PrepareInputs) -> None:
1135
1199
  """project.json self-registration + (implementation 한정) qaCommands gate 검증."""
1136
1200
  from okstra_project import ResolverError
@@ -1290,6 +1354,7 @@ class StageSelection:
1290
1354
  worktree_status: str
1291
1355
  worktree_note: str
1292
1356
  started_head_commit: str
1357
+ concurrent_stages: list = field(default_factory=list)
1293
1358
 
1294
1359
 
1295
1360
  def _select_and_provision_implementation_stage(
@@ -1315,6 +1380,7 @@ def _select_and_provision_implementation_stage(
1315
1380
  # carry sidecars are the SSOT for stage completion; recover any `done` rows
1316
1381
  # the lead failed to append before the dependency gate reads them.
1317
1382
  backfill_done_from_carry(plan_run_root)
1383
+ _auto_reconcile_best_effort(inp, plan_run_root)
1318
1384
  consumed = read_consumers(plan_run_root)
1319
1385
  done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
1320
1386
  started_stages = {r["stage"] for r in consumed if r.get("status") == "started"}
@@ -1327,6 +1393,7 @@ def _select_and_provision_implementation_stage(
1327
1393
  started_stages=started_stages, reserved_stages=reserved_stages,
1328
1394
  )
1329
1395
  selected = batch[0]
1396
+ concurrent_stages = sorted(reserved_stages - {selected})
1330
1397
 
1331
1398
  # spec §2.1 degradation: 주변 흐름이 non-git / nested-worktree 로 skipped 면
1332
1399
  # stage 격리도 동일하게 degrade — worktree 없이 project HEAD 만 기록.
@@ -1340,6 +1407,7 @@ def _select_and_provision_implementation_stage(
1340
1407
  worktree_status=executor_worktree_status,
1341
1408
  worktree_note="",
1342
1409
  started_head_commit=head,
1410
+ concurrent_stages=concurrent_stages,
1343
1411
  )
1344
1412
 
1345
1413
  # anchor base commit 1회 고정 (task-key worktree HEAD 기준)
@@ -1360,6 +1428,7 @@ def _select_and_provision_implementation_stage(
1360
1428
  stage_base = _resolve_stage_base_commit(
1361
1429
  selected_stage, consumer_done_rows, anchor_base_commit=anchor,
1362
1430
  candidate_base=head_sha, project_root=Path(inp.project_root),
1431
+ plan_run_root=plan_run_root,
1363
1432
  )
1364
1433
  try:
1365
1434
  prov = _worktree.provision_stage_worktree(
@@ -1372,7 +1441,12 @@ def _select_and_provision_implementation_stage(
1372
1441
  base_commit=stage_base,
1373
1442
  )
1374
1443
  except RuntimeError as exc:
1375
- raise PrepareError(f"stage worktree provisioning failed: {exc}") from exc
1444
+ from .git_reconcile import guidance
1445
+ hint = guidance(plan_run_root=plan_run_root, project_id=inp.project_id,
1446
+ task_group=inp.task_group, task_id=inp.task_id,
1447
+ work_category=inp.work_category)
1448
+ raise PrepareError(
1449
+ f"stage worktree provisioning failed: {exc}\n{hint}") from exc
1376
1450
 
1377
1451
  return StageSelection(
1378
1452
  stage=selected,
@@ -1382,6 +1456,7 @@ def _select_and_provision_implementation_stage(
1382
1456
  worktree_status=prov.status,
1383
1457
  worktree_note=prov.note,
1384
1458
  started_head_commit=prov.base_ref,
1459
+ concurrent_stages=concurrent_stages,
1385
1460
  )
1386
1461
 
1387
1462
 
@@ -1400,6 +1475,7 @@ def _apply_implementation_stage(
1400
1475
  ctx["effective_stages"] = [sel.stage]
1401
1476
  csv = str(sel.stage)
1402
1477
  ctx["EFFECTIVE_STAGES"] = csv
1478
+ ctx["CONCURRENT_RUN_STAGES"] = ",".join(str(s) for s in sel.concurrent_stages)
1403
1479
  ctx["STAGE_BATCH_DIRECTIVE"] = (
1404
1480
  f"- **Stage for this implementation run:** `{csv}`. "
1405
1481
  "Execute exactly this Stage Map stage — this is the authoritative scope. "
@@ -1436,6 +1512,20 @@ def _git_out(cwd, *args) -> str:
1436
1512
  return r.stdout.strip() if r.returncode == 0 else ""
1437
1513
 
1438
1514
 
1515
+ def _auto_reconcile_best_effort(inp: "PrepareInputs", plan_run_root: Path) -> None:
1516
+ try:
1517
+ from .git_reconcile import auto_reconcile
1518
+ for it in auto_reconcile(
1519
+ project_root=Path(inp.project_root), plan_run_root=plan_run_root,
1520
+ project_id=inp.project_id, task_group=inp.task_group,
1521
+ task_id=inp.task_id, work_category=inp.work_category):
1522
+ print(f"git-reconcile: stage {it.stage} done commit "
1523
+ f"{it.recorded[:8]} -> {it.suggested_commit[:8]} "
1524
+ "(patch-equivalent)", file=sys.stdout)
1525
+ except Exception as exc: # 화해는 부가 기능 — 실패 시 기존 gate 가 판정
1526
+ print(f"git-reconcile skipped: {exc}", file=sys.stderr)
1527
+
1528
+
1439
1529
  def _is_ancestor(cwd, commit, head) -> bool:
1440
1530
  if not commit or not head:
1441
1531
  return False
@@ -1447,10 +1537,22 @@ def _is_ancestor(cwd, commit, head) -> bool:
1447
1537
 
1448
1538
 
1449
1539
  def _is_dirty_excluding_okstra(cwd) -> bool:
1450
- out = _git_out(cwd, "status", "--short", "--", ".", ":(exclude).okstra")
1540
+ excludes = [f":(exclude){p}" for p in okstra_clean_gate_excludes(Path(cwd))]
1541
+ out = _git_out(cwd, "status", "--short", "--", ".", *excludes)
1451
1542
  return bool(out.strip())
1452
1543
 
1453
1544
 
1545
+ def _single_stage_final_verification_worktree(inp: "PrepareInputs") -> WorktreeProvision:
1546
+ """Placeholder until the selected stage registry row is resolved."""
1547
+ return WorktreeProvision(
1548
+ status="deferred-final-verification",
1549
+ note=(
1550
+ "final-verification single-stage uses the selected implementation "
1551
+ "stage worktree from the registry"
1552
+ ),
1553
+ )
1554
+
1555
+
1454
1556
  def _reserve_final_verification_target(
1455
1557
  inp: "PrepareInputs", ctx: dict, ctx_stage_map: list,
1456
1558
  ) -> None:
@@ -1464,6 +1566,7 @@ def _reserve_final_verification_target(
1464
1566
  # carry sidecars are the SSOT for stage completion — recover missing `done`
1465
1567
  # rows before the whole-task gate checks every stage.
1466
1568
  backfill_done_from_carry(plan_run_root)
1569
+ _auto_reconcile_best_effort(inp, plan_run_root)
1467
1570
  done_rows = [r for r in read_consumers(plan_run_root)
1468
1571
  if r.get("status") == "done"]
1469
1572
 
@@ -1472,6 +1575,7 @@ def _reserve_final_verification_target(
1472
1575
  row = _reg.get_stage_row(inp.project_id, inp.task_group, inp.task_id, n)
1473
1576
  wt_path = (row or {}).get("worktree_path", "")
1474
1577
  stage_base = (row or {}).get("base_ref", "")
1578
+ stage_branch = (row or {}).get("branch", "")
1475
1579
  head = _git_out(wt_path, "rev-parse", "HEAD") if wt_path else ""
1476
1580
  target = _resolve_single_stage_target(
1477
1581
  requested_stage=inp.stage, done_rows=done_rows,
@@ -1480,13 +1584,20 @@ def _reserve_final_verification_target(
1480
1584
  stage_dirty=_is_dirty_excluding_okstra(wt_path) if wt_path else False,
1481
1585
  )
1482
1586
  ctx["EXECUTOR_WORKTREE_PATH"] = wt_path
1587
+ ctx["EXECUTOR_WORKTREE_BRANCH"] = stage_branch
1588
+ ctx["EXECUTOR_WORKTREE_BASE_REF"] = stage_base
1589
+ ctx["EXECUTOR_WORKTREE_STATUS"] = "reused-stage"
1590
+ ctx["EXECUTOR_WORKTREE_NOTE"] = (
1591
+ f"final-verification uses implementation stage {n} worktree"
1592
+ )
1483
1593
  else:
1484
1594
  wt_path = ctx["EXECUTOR_WORKTREE_PATH"]
1485
1595
  anchor = _reg.get_implementation_base(
1486
1596
  inp.project_id, inp.task_group, inp.task_id) or ""
1487
1597
  head = _git_out(wt_path, "rev-parse", "HEAD")
1488
- merged = {r["stage"]: _is_ancestor(wt_path, r.get("head_commit", ""), head)
1489
- for r in done_rows}
1598
+ from .consumers import latest_done_by_stage
1599
+ merged = {s: _is_ancestor(wt_path, r.get("head_commit", ""), head)
1600
+ for s, r in latest_done_by_stage(done_rows).items()}
1490
1601
  target = _resolve_whole_task_target(
1491
1602
  stage_map=ctx_stage_map, done_rows=done_rows, anchor_base=anchor,
1492
1603
  task_worktree_path=wt_path, task_head=head,
@@ -1755,21 +1866,24 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1755
1866
  with worktree_provision_mutex(
1756
1867
  okstra_home(), inp.project_id, task_group_segment, task_id_segment,
1757
1868
  ):
1758
- try:
1759
- worktree = provision_task_worktree(
1760
- task_type=inp.task_type,
1761
- project_root=project_root,
1762
- project_id=inp.project_id,
1763
- task_group_segment=task_group_segment,
1764
- task_id_segment=task_id_segment,
1765
- work_category=inp.work_category,
1766
- base_ref=inp.base_ref,
1767
- require_base_ref=True,
1768
- )
1769
- except RuntimeError as exc:
1770
- raise PrepareError(
1771
- f"task worktree provisioning failed: {exc}"
1772
- ) from exc
1869
+ if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
1870
+ worktree = _single_stage_final_verification_worktree(inp)
1871
+ else:
1872
+ try:
1873
+ worktree = provision_task_worktree(
1874
+ task_type=inp.task_type,
1875
+ project_root=project_root,
1876
+ project_id=inp.project_id,
1877
+ task_group_segment=task_group_segment,
1878
+ task_id_segment=task_id_segment,
1879
+ work_category=inp.work_category,
1880
+ base_ref=inp.base_ref,
1881
+ require_base_ref=True,
1882
+ )
1883
+ except RuntimeError as exc:
1884
+ raise PrepareError(
1885
+ f"task worktree provisioning failed: {exc}"
1886
+ ) from exc
1773
1887
 
1774
1888
  # ---- implementation stage selection (path-independent) ----
1775
1889
  # Resolve + provision the stage BEFORE run-path compute so RUN_DIR
@@ -1784,6 +1898,10 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1784
1898
  task_key, worktree.status,
1785
1899
  )
1786
1900
  stage_arg = impl_stage_selection.stage
1901
+ # Drop any stale waiver on this stage so the run actually verifies
1902
+ # conformance (kept inside the per-task-key mutex so concurrent
1903
+ # same-task runs don't race the manifest write).
1904
+ _clear_stale_stage_waiver(inp, project_root, impl_stage_selection.stage)
1787
1905
  else:
1788
1906
  impl_stage_selection = None
1789
1907
  stage_arg = None
@@ -1959,10 +2077,19 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1959
2077
  if not inp.render_only:
1960
2078
  _provision_settings_symlink(inp)
1961
2079
 
2080
+ concurrent_run: dict = {}
2081
+ if impl_stage_selection is not None and impl_stage_selection.concurrent_stages:
2082
+ concurrent_run = {
2083
+ "detected": True,
2084
+ "active_stages": impl_stage_selection.concurrent_stages,
2085
+ }
1962
2086
  return PrepareOutputs(
1963
2087
  ctx=ctx,
1964
2088
  prompt_text=prompt_text,
1965
- extras={"profile_content": profile_content},
2089
+ extras={
2090
+ "profile_content": profile_content,
2091
+ "concurrent_run": concurrent_run,
2092
+ },
1966
2093
  )
1967
2094
 
1968
2095
 
@@ -2132,6 +2259,10 @@ def main(argv: list[str]) -> int:
2132
2259
  print(f"okstra instruction-set: {ctx['INSTRUCTION_SET_PATH']}")
2133
2260
  print(f"okstra reference expectations: {ctx['REFERENCE_EXPECTATIONS_FILE']}")
2134
2261
  print(f"okstra final report template: {ctx['FINAL_REPORT_TEMPLATE_PATH']}")
2262
+ cr = out.extras.get("concurrent_run", {})
2263
+ if cr.get("detected"):
2264
+ stages_csv = ",".join(str(s) for s in cr["active_stages"])
2265
+ print(f"okstra concurrent-run stages: {stages_csv}")
2135
2266
  if inputs.render_only:
2136
2267
  print()
2137
2268
  print(out.prompt_text, end="")