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.
- package/dist/commands/lifecycle/install.mjs +1 -0
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/docs/architecture.md +6 -6
- package/docs/contributor-change-matrix.md +2 -2
- package/docs/project-structure-overview.md +8 -5
- package/package.json +2 -3
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/phase-routing.md +18 -0
- package/runtime/python/okstra_ctl/contract_graph.py +75 -10
- package/runtime/python/okstra_ctl/doctor.py +13 -3
- package/runtime/python/okstra_ctl/implementation_direction.py +9 -8
- package/runtime/python/okstra_ctl/next_phase.py +2 -2
- package/runtime/python/okstra_ctl/paths.py +14 -0
- package/runtime/python/okstra_ctl/phases/__init__.py +4 -0
- package/runtime/python/okstra_ctl/phases/catalog.py +216 -0
- package/runtime/python/okstra_ctl/phases/final_verification/__init__.py +4 -0
- package/runtime/python/okstra_ctl/phases/final_verification/entry.py +166 -0
- package/runtime/{prompts/profiles/final-verification.md → python/okstra_ctl/phases/final_verification/profile.md} +4 -4
- package/runtime/python/okstra_ctl/{report_html/view_models/final_verification.py → phases/final_verification/report.py} +12 -3
- package/{docs/task-process/final-verification.md → runtime/python/okstra_ctl/phases/final_verification/spec.md} +42 -25
- package/runtime/python/okstra_ctl/phases/final_verification/target.py +296 -0
- package/runtime/python/okstra_ctl/phases/final_verification/validation.py +190 -0
- package/runtime/python/okstra_ctl/phases/final_verification/wizard.py +38 -0
- package/runtime/python/okstra_ctl/profile_show.py +7 -1
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +3 -3
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_html/router.py +9 -33
- package/runtime/python/okstra_ctl/report_template_loader.py +35 -0
- package/runtime/python/okstra_ctl/report_views.py +17 -1
- package/runtime/python/okstra_ctl/run.py +46 -154
- package/runtime/python/okstra_ctl/stage_targets.py +9 -286
- package/runtime/python/okstra_ctl/user_response.py +199 -4
- package/runtime/python/okstra_ctl/verification_target.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +6 -2
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +17 -15
- package/runtime/skills/okstra-user-response/SKILL.md +23 -4
- package/runtime/validators/validate-run.py +22 -185
- package/docs/task-process/README.md +0 -82
- package/docs/task-process/common-flow.md +0 -173
- package/docs/task-process/error-analysis.md +0 -103
- package/docs/task-process/implementation-option-selection.md +0 -70
- package/docs/task-process/implementation-planning.md +0 -180
- package/docs/task-process/implementation.md +0 -226
- package/docs/task-process/release-handoff.md +0 -220
- package/docs/task-process/requirements-discovery.md +0 -113
- /package/runtime/{prompts/profiles/final-verification.json → python/okstra_ctl/phases/final_verification/profile.json} +0 -0
- /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.html +0 -0
- /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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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 =
|
|
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 =
|
|
5057
|
-
|
|
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
|
-
|
|
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,
|