okstra 0.205.1 → 0.206.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/dist/commands/lifecycle/install.mjs +1 -0
  2. package/dist/commands/lifecycle/install.mjs.map +1 -1
  3. package/docs/architecture.md +6 -6
  4. package/docs/contributor-change-matrix.md +2 -2
  5. package/docs/project-structure-overview.md +8 -5
  6. package/package.json +2 -3
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  9. package/runtime/prompts/lead/phase-routing.md +18 -0
  10. package/runtime/python/okstra_ctl/contract_graph.py +75 -10
  11. package/runtime/python/okstra_ctl/doctor.py +13 -3
  12. package/runtime/python/okstra_ctl/implementation_direction.py +9 -8
  13. package/runtime/python/okstra_ctl/next_phase.py +2 -2
  14. package/runtime/python/okstra_ctl/paths.py +14 -0
  15. package/runtime/python/okstra_ctl/phases/__init__.py +4 -0
  16. package/runtime/python/okstra_ctl/phases/catalog.py +216 -0
  17. package/runtime/python/okstra_ctl/phases/final_verification/__init__.py +4 -0
  18. package/runtime/python/okstra_ctl/phases/final_verification/entry.py +166 -0
  19. package/runtime/{prompts/profiles/final-verification.md → python/okstra_ctl/phases/final_verification/profile.md} +4 -4
  20. package/runtime/python/okstra_ctl/{report_html/view_models/final_verification.py → phases/final_verification/report.py} +12 -3
  21. package/{docs/task-process/final-verification.md → runtime/python/okstra_ctl/phases/final_verification/spec.md} +42 -25
  22. package/runtime/python/okstra_ctl/phases/final_verification/target.py +296 -0
  23. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +190 -0
  24. package/runtime/python/okstra_ctl/phases/final_verification/wizard.py +38 -0
  25. package/runtime/python/okstra_ctl/profile_show.py +7 -1
  26. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  27. package/runtime/python/okstra_ctl/report_assembly.py +3 -3
  28. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  29. package/runtime/python/okstra_ctl/report_html/router.py +9 -33
  30. package/runtime/python/okstra_ctl/report_template_loader.py +35 -0
  31. package/runtime/python/okstra_ctl/report_views.py +17 -1
  32. package/runtime/python/okstra_ctl/run.py +46 -154
  33. package/runtime/python/okstra_ctl/stage_targets.py +9 -286
  34. package/runtime/python/okstra_ctl/user_response.py +199 -4
  35. package/runtime/python/okstra_ctl/verification_target.py +1 -1
  36. package/runtime/python/okstra_ctl/wizard/state.py +6 -2
  37. package/runtime/python/okstra_ctl/wizard/steps_plan.py +17 -15
  38. package/runtime/skills/okstra-user-response/SKILL.md +23 -4
  39. package/runtime/validators/validate-run.py +22 -185
  40. package/docs/task-process/README.md +0 -82
  41. package/docs/task-process/common-flow.md +0 -173
  42. package/docs/task-process/error-analysis.md +0 -103
  43. package/docs/task-process/implementation-option-selection.md +0 -70
  44. package/docs/task-process/implementation-planning.md +0 -180
  45. package/docs/task-process/implementation.md +0 -226
  46. package/docs/task-process/release-handoff.md +0 -220
  47. package/docs/task-process/requirements-discovery.md +0 -113
  48. /package/runtime/{prompts/profiles/final-verification.json → python/okstra_ctl/phases/final_verification/profile.json} +0 -0
  49. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.html +0 -0
  50. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.md +0 -0
@@ -42,9 +42,10 @@ from typing import Any
42
42
  # registers ``markupsafe`` and ``jinja2`` aliases in ``sys.modules`` so
43
43
  # downstream ``from jinja2 import ...`` resolves to the vendored copy.
44
44
  import okstra_vendor # noqa: F401 — side effect: sys.modules aliases
45
- from jinja2 import ChainableUndefined, Environment, FileSystemLoader
45
+ from jinja2 import ChainableUndefined, Environment
46
46
 
47
47
  from okstra_ctl.clarification_items import USER_INPUT_BLOCKS, progress_blocking_ids
48
+ from okstra_ctl.report_template_loader import report_template_loader
48
49
  from okstra_ctl.final_report_schema import (
49
50
  SchemaError,
50
51
  load_schema_for_data,
@@ -523,7 +524,7 @@ def _build_environment(template_dir: Path) -> Environment:
523
524
  # the renderer can't tell "the writer forgot this required field"
524
525
  # from "this field is intentionally absent on this task-type".
525
526
  env = Environment(
526
- loader=FileSystemLoader(str(template_dir)),
527
+ loader=report_template_loader(template_dir),
527
528
  undefined=ChainableUndefined,
528
529
  trim_blocks=True,
529
530
  lstrip_blocks=True,
@@ -32,6 +32,7 @@ from .implementation_direction import (
32
32
  validate_selected_direction_plan,
33
33
  )
34
34
  from .paths import task_dir
35
+ from .phases.final_verification.report import verification_scope
35
36
  from .report_projections import (
36
37
  ReportProjectionError,
37
38
  project_convergence,
@@ -40,7 +41,7 @@ from .report_projections import (
40
41
  project_token_usage,
41
42
  )
42
43
  from .scope_provenance import parse_source
43
- from .verification_target import read_verification_target
44
+
44
45
  from .write_policy import planned_path_declaration_errors
45
46
 
46
47
 
@@ -355,8 +356,7 @@ def _verification_scope(project_root: Path, manifest: Mapping[str, Any]) -> str:
355
356
  relative = str(manifest.get("verificationTargetPath") or "").strip()
356
357
  if not relative:
357
358
  return ""
358
- target = read_verification_target(project_root, relative)
359
- return str((target or {}).get("scope") or "").strip()
359
+ return verification_scope(project_root, relative)
360
360
 
361
361
 
362
362
  def _clarification_carry_in(
@@ -7,7 +7,7 @@ import sys
7
7
  from pathlib import Path
8
8
 
9
9
  import okstra_vendor # noqa: F401 # registers vendored dependency aliases
10
- from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
10
+ from jinja2 import Environment, StrictUndefined, select_autoescape
11
11
 
12
12
  from ..final_report_paths import final_report_data_path, translation_sidecar_path
13
13
  from ..final_report_schema import load_schema_for_data, validate
@@ -16,6 +16,7 @@ from ..report_translation import overlay
16
16
  from ..report_view_artifacts import user_responses_dir_for_report
17
17
  from ..usage_cells import format_duration_ms
18
18
  from ..json_boundary import load_owned_object
19
+ from ..report_template_loader import report_template_loader
19
20
  from .common import anchor_index, worker_finding_links
20
21
  from .context_links import (
21
22
  approved_plan_links,
@@ -145,7 +146,7 @@ def render_v2_html_view(
145
146
  route = resolve_html_route(run_meta.task_type)
146
147
  view = route.view_builder(data)
147
148
  root = _templates_root(templates_root)
148
- env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
149
+ env = Environment(loader=report_template_loader(root), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
149
150
  env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
150
151
  # Binding the index here is what lets a template cite an id without
151
152
  # threading the index through every macro and call site. The record's
@@ -1,46 +1,22 @@
1
1
  """Fail-closed task-type routing for schema-v2 HTML views."""
2
2
  from __future__ import annotations
3
3
 
4
+ import importlib
5
+
6
+ from ..phases.catalog import UnknownTaskType, view_builder_import
4
7
  from ..report_contract import html_template_for
5
8
  from .models import HtmlRoute
6
- from .view_models.change_impact_analysis import build_change_impact_analysis_view
7
- from .view_models.error_analysis import build_error_analysis_view
8
- from .view_models.technical_verification import build_technical_verification_view
9
- from .view_models.feature_analysis import build_feature_analysis_view
10
- from .view_models.final_verification import build_final_verification_view
11
- from .view_models.improvement_discovery import build_improvement_discovery_view
12
- from .view_models.implementation_option_selection import (
13
- build_implementation_option_selection_view,
14
- )
15
- from .view_models.implementation import build_implementation_view
16
- from .view_models.implementation_planning import build_implementation_planning_view
17
- from .view_models.project_analysis import build_project_analysis_view
18
- from .view_models.release_handoff import build_release_handoff_view
19
- from .view_models.requirements_discovery import build_requirements_discovery_view
20
9
 
21
10
 
22
11
  class HtmlRenderError(ValueError):
23
12
  """Raised when a v2 task has no dedicated human view."""
24
13
 
25
14
 
26
- VIEW_BUILDERS = {
27
- "technical-verification": build_technical_verification_view,
28
- "requirements-discovery": build_requirements_discovery_view,
29
- "improvement-discovery": build_improvement_discovery_view,
30
- "error-analysis": build_error_analysis_view,
31
- "implementation-option-selection": build_implementation_option_selection_view,
32
- "project-analysis": build_project_analysis_view,
33
- "feature-analysis": build_feature_analysis_view,
34
- "change-impact-analysis": build_change_impact_analysis_view,
35
- "implementation-planning": build_implementation_planning_view,
36
- "implementation": build_implementation_view,
37
- "final-verification": build_final_verification_view,
38
- "release-handoff": build_release_handoff_view,
39
- }
40
-
41
-
42
15
  def resolve_html_route(task_type: str) -> HtmlRoute:
43
- builder = VIEW_BUILDERS.get(task_type)
44
- if builder is None:
45
- raise HtmlRenderError(f"no v2 html view builder for {task_type}")
16
+ """선택한 작업 유형의 표시 함수만 적재한다."""
17
+ try:
18
+ module_name, function_name = view_builder_import(task_type)
19
+ except UnknownTaskType as exc:
20
+ raise HtmlRenderError(f"no v2 html view builder for {task_type}") from exc
21
+ builder = getattr(importlib.import_module(module_name), function_name)
46
22
  return HtmlRoute(template_name=html_template_for(task_type), view_builder=builder)
@@ -0,0 +1,35 @@
1
+ """보고서 템플릿 로더.
2
+
3
+ 이전된 단계의 본문은 단계 ``report_assets/`` 에 있고, 공통 레이아웃과
4
+ 매크로는 ``templates/reports`` 에 남는다. 논리 이름은 그대로다.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import okstra_vendor # noqa: F401 # registers vendored dependency aliases
11
+ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound
12
+
13
+ from .phases.catalog import migrated_report_template
14
+
15
+
16
+ class ReportTemplateLoader(BaseLoader):
17
+ """논리 템플릿 이름을 단계 정본 또는 공통 템플릿 트리에서 연다."""
18
+
19
+ def __init__(self, templates_root: Path) -> None:
20
+ self._root = templates_root
21
+ self._fallback = FileSystemLoader(str(templates_root))
22
+
23
+ def get_source(self, environment, template):
24
+ physical = migrated_report_template(self._root.parent.parent, template)
25
+ if physical is None:
26
+ return self._fallback.get_source(environment, template)
27
+ if not physical.is_file():
28
+ raise TemplateNotFound(template)
29
+ source = physical.read_text(encoding="utf-8")
30
+ mtime = physical.stat().st_mtime
31
+ return source, str(physical), lambda: physical.stat().st_mtime == mtime
32
+
33
+
34
+ def report_template_loader(templates_root: Path) -> ReportTemplateLoader:
35
+ return ReportTemplateLoader(templates_root)
@@ -30,9 +30,10 @@ import hashlib
30
30
  import html
31
31
  import json
32
32
  import re
33
+ from collections.abc import Mapping
33
34
  from dataclasses import dataclass
34
35
  from pathlib import Path
35
- from typing import Iterable, Optional
36
+ from typing import Any, Iterable, Optional
36
37
 
37
38
  from .user_response_values import ( # noqa: F401 — 재노출(값 타입은 leaf 소유)
38
39
  UserPlanDecision,
@@ -1164,6 +1165,21 @@ def normalize_direction_selection_identity(
1164
1165
  return normalized_id, normalized_name
1165
1166
 
1166
1167
 
1168
+ def direction_candidate_ineligibility(option: Mapping[str, Any]) -> str:
1169
+ """Why a ranked candidate cannot become the planning direction, or ``""``.
1170
+
1171
+ The planning gate (`implementation_direction._selected_option`) and the
1172
+ in-session picker (`user_response.set_direction_selection`) share this rule,
1173
+ so a candidate the picker accepts is one the gate accepts.
1174
+ """
1175
+ coverage = option.get("coverageSummary")
1176
+ if not isinstance(coverage, Mapping) or coverage.get("coverageVerdict") != "exact":
1177
+ return "selected candidate must have exact coverage"
1178
+ if option.get("safetyBlockers") or option.get("unresolvedFeasibilityFacts"):
1179
+ return "selected candidate has a safety blocker"
1180
+ return ""
1181
+
1182
+
1167
1183
  def _quoted_sidecar_field(label: str, value: str) -> str:
1168
1184
  cleaned = value.strip()
1169
1185
  if not cleaned:
@@ -19,7 +19,6 @@ from .technical_verification import (
19
19
  write_technical_verification_input,
20
20
  )
21
21
 
22
- import hashlib
23
22
  from argparse import (
24
23
  Action,
25
24
  ArgumentError,
@@ -217,6 +216,15 @@ from .workers import (
217
216
  from .workflow import compute_workflow_state, load_phase_forbidden
218
217
  from .locks import worktree_provision_mutex
219
218
  from . import stage_targets as _stage_targets
219
+ from .phases.catalog import PhaseAssetError, profile_markdown, resolve_include
220
+ from .phases.final_verification.target import acquire_final_verification_target
221
+ from .phases.final_verification.entry import (
222
+ apply_verification_target,
223
+ reject_prepare_flags,
224
+ single_stage_worktree,
225
+ verification_target_request,
226
+ write_verification_target_snapshot,
227
+ )
220
228
  from .plan_run_root import plan_run_root_from_approved_plan
221
229
  from . import implementation_stage as _implementation_stage
222
230
  from .work_categories import resolve_work_category
@@ -1215,8 +1223,11 @@ _HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
1215
1223
  def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
1216
1224
  """Resolve `{{INCLUDE:<name>}}` directives in a profile file.
1217
1225
 
1218
- Includes are resolved relative to the profile's directory. A maximum
1219
- recursion depth of 4 prevents accidental cycles; the included file
1226
+ `phases.catalog.resolve_include` picks the search root: an unmigrated
1227
+ profile resolves beside itself, and a phase package's `profile.md`
1228
+ resolves through the logical paths of the asset root that holds it.
1229
+ Recursion stops at depth 4
1230
+ so a cycle fails instead of looping; the included file
1220
1231
  contents replace the directive line in-place. Missing include targets
1221
1232
  raise PrepareError so a bad reference fails fast instead of silently
1222
1233
  leaving a `{{INCLUDE:...}}` token in the rendered profile.
@@ -1235,7 +1246,10 @@ def _expand_profile_includes(profile_path: Path, _depth: int = 0) -> str:
1235
1246
 
1236
1247
  def _sub(match: "re.Match[str]") -> str:
1237
1248
  target_name = match.group(1).strip()
1238
- target = profile_path.parent / target_name
1249
+ try:
1250
+ target = resolve_include(profile_path, target_name)
1251
+ except PhaseAssetError as exc:
1252
+ raise PrepareError(str(exc)) from exc
1239
1253
  if not target.is_file():
1240
1254
  raise PrepareError(
1241
1255
  f"profile include target missing: {target} (referenced from {profile_path})"
@@ -1289,7 +1303,10 @@ class _ResolvedAssets:
1289
1303
 
1290
1304
  def _resolve_runtime_assets(workspace_root: Path, inp: PrepareInputs) -> _ResolvedAssets:
1291
1305
  """task-type 별 profile + 공통 템플릿/스킬/validator 경로를 해소하고 존재를 확인한다."""
1292
- profile_file = workspace_root / "prompts" / "profiles" / f"{inp.task_type}.md"
1306
+ try:
1307
+ profile_file = profile_markdown(workspace_root, inp.task_type)
1308
+ except PhaseAssetError as exc:
1309
+ raise PrepareError(str(exc)) from exc
1293
1310
  if not profile_file.is_file():
1294
1311
  raise PrepareError(
1295
1312
  f"analysis profile file not found for task-type {inp.task_type}: "
@@ -1438,18 +1455,10 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
1438
1455
  "--approved-plan <path-to-final-report.md>"
1439
1456
  )
1440
1457
  if inp.task_type == "final-verification":
1441
- # final-verification 에서 --approve / --implementation-option 은
1442
- # 의미가 없다 (승인은 implementation 진입 시 이미 끝났다).
1443
- if inp.approve_plan_ack:
1444
- raise PrepareError(
1445
- "--approve is only meaningful with --task-type implementation "
1446
- "and --approved-plan <path>"
1447
- )
1448
- if inp.implementation_option:
1449
- raise PrepareError(
1450
- "--implementation-option is only meaningful with --task-type "
1451
- "implementation and --approved-plan <path>"
1452
- )
1458
+ reject_prepare_flags(
1459
+ approve_plan_ack=inp.approve_plan_ack,
1460
+ implementation_option=inp.implementation_option,
1461
+ )
1453
1462
  ctx_stage_map = _parse_stage_map_into_ctx(inp.approved_plan_path)
1454
1463
  else:
1455
1464
  if inp.approve_plan_ack:
@@ -3460,139 +3469,6 @@ def _git_out(cwd, *args) -> str:
3460
3469
  return r.stdout.strip() if r.returncode == 0 else ""
3461
3470
 
3462
3471
 
3463
- def _single_stage_final_verification_worktree(inp: "PrepareInputs") -> WorktreeProvision:
3464
- """Placeholder until the selected stage registry row is resolved."""
3465
- return WorktreeProvision(
3466
- status="deferred-final-verification",
3467
- note=(
3468
- "final-verification single-stage uses the selected implementation "
3469
- "stage worktree from the registry"
3470
- ),
3471
- )
3472
-
3473
-
3474
- def _format_integration(integ) -> str:
3475
- """stage 자동 통합 결과를 사람이 읽을 한국어 마크다운 블록으로 만든다."""
3476
- def _csv(xs):
3477
- return ", ".join(str(x) for x in xs) if xs else "없음"
3478
-
3479
- skipped = "; ".join(f"stage {n}({why})" for n, why in integ.teardown_skipped)
3480
- lines = [
3481
- "- **Stage 자동 통합 결과** (whole-task final-verification):",
3482
- "- **머지된 stage:** " + _csv(integ.merged),
3483
- "- **이미 머지됨:** " + _csv(integ.already_merged),
3484
- "- **정리(teardown)된 stage:** " + _csv(integ.torn_down),
3485
- "- **정리 보류:** " + (skipped or "없음"),
3486
- ]
3487
- for w in integ.warnings:
3488
- lines.append("- **경고:** " + w)
3489
- return "\n".join(lines)
3490
-
3491
-
3492
- def _final_verification_target_request(
3493
- inp: "PrepareInputs",
3494
- ctx_stage_map: list[dict],
3495
- ) -> _stage_targets.FinalVerificationTargetRequest:
3496
- stage = (
3497
- int(inp.stage)
3498
- if inp.stage and inp.stage != "auto"
3499
- else None
3500
- )
3501
- return _stage_targets.FinalVerificationTargetRequest(
3502
- project_root=Path(inp.project_root),
3503
- project_id=inp.project_id,
3504
- task_group=inp.task_group,
3505
- task_id=inp.task_id,
3506
- work_category=inp.work_category,
3507
- approved_plan_path=Path(inp.approved_plan_path),
3508
- stage=stage,
3509
- stage_map=tuple(ctx_stage_map),
3510
- )
3511
-
3512
-
3513
- def _apply_final_verification_target(
3514
- ctx: dict,
3515
- acquisition: _stage_targets.FinalVerificationTargetAcquisition,
3516
- ) -> None:
3517
- """Translate acquired domain facts into render-context fields."""
3518
- target = acquisition.target
3519
- if target.scope == "single-stage":
3520
- stage = target.stages[0]
3521
- ctx["EXECUTOR_WORKTREE_PATH"] = target.worktree_path
3522
- ctx["EXECUTOR_WORKTREE_BRANCH"] = acquisition.worktree_branch
3523
- ctx["EXECUTOR_WORKTREE_BASE_REF"] = target.base
3524
- ctx["EXECUTOR_WORKTREE_STATUS"] = "reused-stage"
3525
- ctx["EXECUTOR_WORKTREE_NOTE"] = (
3526
- f"final-verification uses implementation stage {stage} worktree"
3527
- )
3528
- if acquisition.integration_result is not None:
3529
- ctx["STAGE_INTEGRATION"] = _format_integration(
3530
- acquisition.integration_result
3531
- )
3532
- if acquisition.relocations:
3533
- moved = "\n".join(
3534
- f"- stage {row['stage']}: `{row['from']}` → `{row['to']}`"
3535
- for row in acquisition.relocations
3536
- )
3537
- ctx["STAGE_INTEGRATION"] = (
3538
- f"{ctx.get('STAGE_INTEGRATION', '')}\n"
3539
- "Nested stage worktrees moved out of the verification worktree "
3540
- "(registry updated; contents and branches unchanged):\n"
3541
- f"{moved}"
3542
- ).strip()
3543
- diff_stat = _git_out(target.worktree_path, "diff", "--stat",
3544
- f"{target.base}..{target.head}")
3545
- ctx["VERIFICATION_SCOPE"] = target.scope
3546
- ctx["VERIFICATION_WORKTREE_PATH"] = target.worktree_path
3547
- ctx["VERIFICATION_BASE_REF"] = target.base
3548
- ctx["VERIFICATION_HEAD_REF"] = target.head
3549
- ctx["VERIFICATION_TARGET"] = _format_verification_target(target, diff_stat)
3550
-
3551
-
3552
- def _format_verification_target(
3553
- target: _stage_targets.FinalVerificationTarget,
3554
- diff_stat: str,
3555
- ) -> str:
3556
- reports = "\n".join(
3557
- f" - stage {s}: `{rp or '(report_path unrecorded)'}`"
3558
- for s, rp in zip(target.stages, target.reports)
3559
- )
3560
- return (
3561
- f"- **Verification scope:** `{target.scope}`\n"
3562
- f"- **Worktree:** `{target.worktree_path}`\n"
3563
- f"- **Verification base ref:** `{target.base}`\n"
3564
- f"- **Verification head ref:** `{target.head}`\n"
3565
- f"- **Stages under verification:** {target.stages}\n"
3566
- f"- **Source implementation reports:**\n{reports}\n"
3567
- f"- **Verification diff stat:**\n```\n{diff_stat}\n```"
3568
- )
3569
-
3570
-
3571
- def write_verification_target_snapshot(
3572
- instruction_set: Path,
3573
- target_markdown: str,
3574
- ) -> tuple[Path, str]:
3575
- """Persist one normalized final-verification target and return its digest."""
3576
- normalized = target_markdown.replace("\r\n", "\n").replace("\r", "\n")
3577
- normalized = normalized.rstrip() + "\n"
3578
- digest = "sha256:" + hashlib.sha256(normalized.encode("utf-8")).hexdigest()
3579
- path = instruction_set / "verification-target.md"
3580
- # 계산 규칙을 값과 함께 적는다. 워커는 이 다이제스트를 프롬프트 앵커로도
3581
- # 받는데, 규칙을 모르면 파일 전체 바이트로 재고 반드시 불일치를 본다 —
3582
- # 실측(2026-08-26): 그렇게 잰 검증자가 스냅샷이 변조됐다고 보고했다.
3583
- # 다이제스트 줄은 줄머리 앵커로 잘리므로(`_TARGET_DIGEST_RE`) 그 뒤에
3584
- # 붙는 이 줄은 본문에 들어가지 않는다.
3585
- path.write_text(
3586
- normalized
3587
- + f"- **Verification target digest:** `{digest}`\n"
3588
- + "- **Digest rule:** `sha256` of the text above this line, with CRLF "
3589
- "and CR normalized to LF, trailing whitespace stripped, and exactly "
3590
- "one closing newline. This line and the digest line are not covered.\n",
3591
- encoding="utf-8",
3592
- )
3593
- return path, digest
3594
-
3595
-
3596
3472
  def _write_verification_target_artifact(
3597
3473
  inp: PrepareInputs,
3598
3474
  ctx: dict,
@@ -4595,7 +4471,7 @@ def _provision_task_and_stage(
4595
4471
  okstra_home(), inp.project_id, task_group_segment, task_id_segment,
4596
4472
  ):
4597
4473
  if inp.task_type == "final-verification" and inp.stage and inp.stage != "auto":
4598
- worktree = _single_stage_final_verification_worktree(inp)
4474
+ worktree = single_stage_worktree()
4599
4475
  # Single-stage final-verification namespaces its run path under
4600
4476
  # runs/final-verification/stage-<N> (same isolation as
4601
4477
  # implementation) so concurrent per-stage verifications never
@@ -5053,10 +4929,26 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
5053
4929
  ctx["CLAUDE_RESUME_COMMAND_RELATIVE_PATH"] = ""
5054
4930
  ctx["CLAUDE_RESUME_COMMAND_FILENAME"] = ""
5055
4931
  if inp.task_type == "final-verification":
5056
- acquisition = _stage_targets.acquire_final_verification_target(
5057
- _final_verification_target_request(inp, ctx_stage_map)
4932
+ acquisition = acquire_final_verification_target(
4933
+ verification_target_request(
4934
+ project_root=inp.project_root,
4935
+ project_id=inp.project_id,
4936
+ task_group=inp.task_group,
4937
+ task_id=inp.task_id,
4938
+ work_category=inp.work_category,
4939
+ approved_plan_path=inp.approved_plan_path,
4940
+ stage=inp.stage,
4941
+ stage_map=ctx_stage_map,
4942
+ )
4943
+ )
4944
+ target = acquisition.target
4945
+ diff_stat = _git_out(
4946
+ target.worktree_path,
4947
+ "diff",
4948
+ "--stat",
4949
+ f"{target.base}..{target.head}",
5058
4950
  )
5059
- _apply_final_verification_target(ctx, acquisition)
4951
+ apply_verification_target(ctx, acquisition, diff_stat)
5060
4952
 
5061
4953
  _write_bundle_artifacts(
5062
4954
  inp, ctx, project_root, lead_runtime, claude_session_id,